예제 #1
0
class SyncAPIApplication(WSGIApplication):
    """A WSGI application that implements our SyncAPI."""

    def __init__(self):
        super(SyncAPIApplication, self).__init__()
        self.crypto = CryptoProvider()
        self.allow_pairing = False
        self.key_exchanges = {}

    def _do_auth_hmac_cb(self, uuid):
        """Perform mutual HMAC_CB authentication."""
        wwwauth = create_option_header('HMAC_CB', realm=uuid)
        headers = [('WWW-Authenticate', wwwauth)]
        auth = self.environ.get('HTTP_AUTHORIZATION')
        if auth is None:
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        try:
            method, options = parse_option_header(auth)
        except ValueError:
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        if method != 'HMAC_CB':
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        if 'name' in options:
            # pair step 1 - ask user for permission to pair
            name = options['name']
            if not self.allow_pairing:
                raise HTTPReturn('403 Pairing Disabled')
            bus = instance(MessageBusServer)
            kxid = self.crypto.random(16).encode('hex')
            pin = '%06d' % (self.crypto.randint(bits=31) % 1000000)
            approved = bus.call_method(None, 'get_pairing_approval',
                                       name, uuid, pin, kxid, timeout=60)
            if not approved:
                raise HTTPReturn('403 Approval Denied')
            restrictions = {}
            self.key_exchanges[kxid] = (time.time(), restrictions, pin)
            wwwauth = create_option_header('HMAC_CB', kxid=kxid)
            headers = [('WWW-Authenticate', wwwauth)]
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        elif 'kxid' in options:
            # pair step 2 - check auth and do the actual pairing
            kxid = options['kxid']
            if kxid not in self.key_exchanges:
                raise HTTPReturn(http.FORBIDDEN)
            starttime, restrictions, pin = self.key_exchanges.pop(kxid)
            signature = base64.try_decode(options.get('signature', ''))
            if not signature:
                raise HTTPReturn(http.FORBIDDEN)
            now = time.time()
            if now - starttime > 60:
                raise HTTPReturn('403 Request Timeout')
            cb = self.environ['SSL_CHANNEL_BINDING_TLS_UNIQUE']
            check = self.crypto.hmac(adjust_pin(pin, +1), cb, 'sha1')
            if check != signature:
                raise HTTPReturn('403 Invalid PIN')
            bus = instance(MessageBusServer)
            bus.send_signal(None, 'PairingComplete', kxid)
            # Prove to the other side we also know the PIN
            signature = self.crypto.hmac(adjust_pin(pin, -1), cb, 'sha1')
            signature = base64.encode(signature)
            authinfo = create_option_header('HMAC_CB', kxid=kxid,
                                            signature=signature)
            self.headers.append(('Authentication-Info', authinfo))
        else:
            raise HTTPReturn(http.UNAUTHORIZED, headers)
 
    def _do_auth_rsa_cb(self, uuid):
        """Perform mutual RSA_CB authentication."""
        wwwauth = create_option_header('RSA_CB', realm=uuid)
        headers = [('WWW-Authenticate', wwwauth)]
        auth = self.environ.get('HTTP_AUTHORIZATION')
        if auth  is None:
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        try:
            method, opts = parse_option_header(auth)
        except ValueError:
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        if method != 'RSA_CB':
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        if 'node' not in opts or not check_uuid4(opts['node']):
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        if 'signature' not in opts or not base64.check(opts['signature']):
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        model = instance(Model)
        cert = model.get_certificate(uuid, opts['node'])
        if cert is None:
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        signature = base64.decode(opts['signature'])
        pubkey = base64.decode(cert['payload']['keys']['auth']['key'])
        cb = self.environ['SSL_CHANNEL_BINDING_TLS_UNIQUE']
        if not self.crypto.rsa_verify(cb, signature, pubkey, 'pss-sha1'):
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        # The peer was authenticated. Authenticate ourselves as well.
        privkey = model.get_auth_key(uuid)
        vault = model.get_vault(uuid)
        node = vault['node']
        signature = self.crypto.rsa_sign(cb, privkey, 'pss-sha1')
        signature = base64.encode(signature)
        auth = create_option_header('RSA_CB', node=node, signature=signature)
        self.headers.append(('Authentication-Info', auth))

    @expose('/api/vaults/:vault/pair', method='POST')
    def pair(self, env):
        uuid = env['mapper.vault']
        if not check_uuid4(uuid):
            raise HTTPReturn(http.NOT_FOUND)
        model = instance(Model)
        vault = model.get_vault(uuid)
        if not vault:
            raise HTTPReturn(http.NOT_FOUND)
        self._do_auth_hmac_cb(uuid)
        # Sign the certificate request that was sent to tus
        certinfo = self.entity
        if not certinfo or not isinstance(certinfo, dict):
            raise HTTPReturn(http.BAD_REQUEST)
        model.add_certificate(uuid, certinfo)
        # And send our own certificate request in return
        certinfo = { 'node': vault['node'], 'name': socket.gethostname() }
        certkeys = certinfo['keys'] = {}
        for key in vault['keys']:
            certkeys[key] = { 'key': vault['keys'][key]['public'],
                              'keytype': vault['keys'][key]['keytype'] }
        return certinfo

    @expose('/api/vaults/:vault/items', method='GET')
    def sync_outbound(self, env):
        uuid = env['mapper.vault']
        if not check_uuid4(uuid):
            raise HTTPReturn(http.NOT_FOUND)
        model = instance(Model)
        vault = model.get_vault(uuid)
        if vault is None:
            raise HTTPReturn(http.NOT_FOUND)
        self._do_auth_rsa_cb(uuid)
        args = parse_qs(env.get('QUERY_STRING', ''))
        vector = args.get('vector', [''])[0]
        if vector:
            try:
                vector = parse_vector(vector)
            except ValueError:
                raise HTTPReturn(http.BAD_REQUEST)
        items = model.get_items(uuid, vector)
        myvector = model.get_vector(uuid)
        self.headers.append(('X-Vector', dump_vector(myvector)))
        return items

    @expose('/api/vaults/:vault/items', method='POST')
    def sync_inbound(self, env):
        uuid = env['mapper.vault']
        if not check_uuid4(uuid):
            raise HTTPReturn(http.NOT_FOUND)
        model = instance(Model)
        vault = model.get_vault(uuid)
        if vault is None:
            raise HTTPReturn(http.NOT_FOUND)
        self._do_auth_rsa_cb(uuid)
        items = self.entity
        if items is None or not isinstance(items, list):
            raise HTTPReturn(http.BAD_REQUEST)
        model.import_items(uuid, items)
예제 #2
0
class SyncAPIApplication(WSGIApplication):
    """A WSGI application that implements our SyncAPI."""
    def __init__(self):
        super(SyncAPIApplication, self).__init__()
        self.crypto = CryptoProvider()
        self.allow_pairing = False
        self.key_exchanges = {}

    def _do_auth_hmac_cb(self, uuid):
        """Perform mutual HMAC_CB authentication."""
        wwwauth = create_option_header('HMAC_CB', realm=uuid)
        headers = [('WWW-Authenticate', wwwauth)]
        auth = self.environ.get('HTTP_AUTHORIZATION')
        if auth is None:
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        try:
            method, options = parse_option_header(auth)
        except ValueError:
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        if method != 'HMAC_CB':
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        if 'name' in options:
            # pair step 1 - ask user for permission to pair
            name = options['name']
            if not self.allow_pairing:
                raise HTTPReturn('403 Pairing Disabled')
            bus = instance(MessageBusServer)
            kxid = self.crypto.random(16).encode('hex')
            pin = '%06d' % (self.crypto.randint(bits=31) % 1000000)
            approved = bus.call_method(None,
                                       'get_pairing_approval',
                                       name,
                                       uuid,
                                       pin,
                                       kxid,
                                       timeout=60)
            if not approved:
                raise HTTPReturn('403 Approval Denied')
            restrictions = {}
            self.key_exchanges[kxid] = (time.time(), restrictions, pin)
            wwwauth = create_option_header('HMAC_CB', kxid=kxid)
            headers = [('WWW-Authenticate', wwwauth)]
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        elif 'kxid' in options:
            # pair step 2 - check auth and do the actual pairing
            kxid = options['kxid']
            if kxid not in self.key_exchanges:
                raise HTTPReturn(http.FORBIDDEN)
            starttime, restrictions, pin = self.key_exchanges.pop(kxid)
            signature = base64.try_decode(options.get('signature', ''))
            if not signature:
                raise HTTPReturn(http.FORBIDDEN)
            now = time.time()
            if now - starttime > 60:
                raise HTTPReturn('403 Request Timeout')
            cb = self.environ['SSL_CHANNEL_BINDING_TLS_UNIQUE']
            check = self.crypto.hmac(adjust_pin(pin, +1), cb, 'sha1')
            if check != signature:
                raise HTTPReturn('403 Invalid PIN')
            bus = instance(MessageBusServer)
            bus.send_signal(None, 'PairingComplete', kxid)
            # Prove to the other side we also know the PIN
            signature = self.crypto.hmac(adjust_pin(pin, -1), cb, 'sha1')
            signature = base64.encode(signature)
            authinfo = create_option_header('HMAC_CB',
                                            kxid=kxid,
                                            signature=signature)
            self.headers.append(('Authentication-Info', authinfo))
        else:
            raise HTTPReturn(http.UNAUTHORIZED, headers)

    def _do_auth_rsa_cb(self, uuid):
        """Perform mutual RSA_CB authentication."""
        wwwauth = create_option_header('RSA_CB', realm=uuid)
        headers = [('WWW-Authenticate', wwwauth)]
        auth = self.environ.get('HTTP_AUTHORIZATION')
        if auth is None:
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        try:
            method, opts = parse_option_header(auth)
        except ValueError:
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        if method != 'RSA_CB':
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        if 'node' not in opts or not check_uuid4(opts['node']):
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        if 'signature' not in opts or not base64.check(opts['signature']):
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        model = instance(Model)
        cert = model.get_certificate(uuid, opts['node'])
        if cert is None:
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        signature = base64.decode(opts['signature'])
        pubkey = base64.decode(cert['payload']['keys']['auth']['key'])
        cb = self.environ['SSL_CHANNEL_BINDING_TLS_UNIQUE']
        if not self.crypto.rsa_verify(cb, signature, pubkey, 'pss-sha1'):
            raise HTTPReturn(http.UNAUTHORIZED, headers)
        # The peer was authenticated. Authenticate ourselves as well.
        privkey = model.get_auth_key(uuid)
        vault = model.get_vault(uuid)
        node = vault['node']
        signature = self.crypto.rsa_sign(cb, privkey, 'pss-sha1')
        signature = base64.encode(signature)
        auth = create_option_header('RSA_CB', node=node, signature=signature)
        self.headers.append(('Authentication-Info', auth))

    @expose('/api/vaults/:vault/pair', method='POST')
    def pair(self, env):
        uuid = env['mapper.vault']
        if not check_uuid4(uuid):
            raise HTTPReturn(http.NOT_FOUND)
        model = instance(Model)
        vault = model.get_vault(uuid)
        if not vault:
            raise HTTPReturn(http.NOT_FOUND)
        self._do_auth_hmac_cb(uuid)
        # Sign the certificate request that was sent to tus
        certinfo = self.entity
        if not certinfo or not isinstance(certinfo, dict):
            raise HTTPReturn(http.BAD_REQUEST)
        model.add_certificate(uuid, certinfo)
        # And send our own certificate request in return
        certinfo = {'node': vault['node'], 'name': socket.gethostname()}
        certkeys = certinfo['keys'] = {}
        for key in vault['keys']:
            certkeys[key] = {
                'key': vault['keys'][key]['public'],
                'keytype': vault['keys'][key]['keytype']
            }
        return certinfo

    @expose('/api/vaults/:vault/items', method='GET')
    def sync_outbound(self, env):
        uuid = env['mapper.vault']
        if not check_uuid4(uuid):
            raise HTTPReturn(http.NOT_FOUND)
        model = instance(Model)
        vault = model.get_vault(uuid)
        if vault is None:
            raise HTTPReturn(http.NOT_FOUND)
        self._do_auth_rsa_cb(uuid)
        args = parse_qs(env.get('QUERY_STRING', ''))
        vector = args.get('vector', [''])[0]
        if vector:
            try:
                vector = parse_vector(vector)
            except ValueError:
                raise HTTPReturn(http.BAD_REQUEST)
        items = model.get_items(uuid, vector)
        myvector = model.get_vector(uuid)
        self.headers.append(('X-Vector', dump_vector(myvector)))
        return items

    @expose('/api/vaults/:vault/items', method='POST')
    def sync_inbound(self, env):
        uuid = env['mapper.vault']
        if not check_uuid4(uuid):
            raise HTTPReturn(http.NOT_FOUND)
        model = instance(Model)
        vault = model.get_vault(uuid)
        if vault is None:
            raise HTTPReturn(http.NOT_FOUND)
        self._do_auth_rsa_cb(uuid)
        items = self.entity
        if items is None or not isinstance(items, list):
            raise HTTPReturn(http.BAD_REQUEST)
        model.import_items(uuid, items)
예제 #3
0
class SyncAPIClient(object):
    """
    SyncAPI client.

    This classs implements a client to the Bluepass HTTP based synchronization
    API. The two main functions are pairing (pair_step1() and pair_step2())
    and synchronization (sync()).
    """

    def __init__(self, address, **ssl_args):
        """Create a new client for the syncapi API at `address`."""
        self.address = address
        ssl_args.setdefault('dhparams', dhparams['skip2048'])
        ssl_args.setdefault('ciphers', 'ADH+AES')
        self.ssl_args = ssl_args
        self.connection = None
        logger = logging.getLogger(__name__)
        self.logger = ContextLogger(logger)
        self.crypto = CryptoProvider()

    def _make_request(self, method, url, headers=None, body=None):
        """Make an HTTP request to the API.
        
        This returns the HTTPResponse object on success, or None on failure.
        """
        logger = self.logger
        if headers is None:
            headers = []
        headers.append(('User-Agent', 'Bluepass/%s' % _version.version))
        headers.append(('Accept', 'text/json'))
        if body is None:
            body = ''
        else:
            body = json.dumps(body)
            headers.append(('Content-Type', 'text/json'))
        connection = self.connection
        assert connection is not None
        try:
            logger.debug('client request: %s %s', method, url)
            connection.request(method, url, body, dict(headers))
            response = connection.getresponse()
            headers = response.getheaders()
            body = response.read()
        except (socket.error, HTTPException) as e:
            logger.error('error when making HTTP request: %s', str(e))
            return
        ctype = response.getheader('Content-Type')
        if ctype == 'text/json':
            parsed = json.try_loads(body)
            if parsed is None:
                logger.error('response body contains invalid JSON')
                return
            response.entity = parsed
            logger.debug('parsed "%s" request body (%d bytes)', ctype, len(body))
        else:
            response.entity = None
        return response

    def connect(self):
        """Connect to the remote syncapi."""
        ssl_args = { 'dhparams': dhparams['skip2048'], 'ciphers': 'ADH+AES' }
        # Support both dict style addresses for arbitrary address families,
        # as well as (host, port) tuples for IPv4.
        if isinstance(self.address, dict):
            host = self.address['host']; port = None
            sockinfo = self.address
        else:
            host, port = self.address
            sockinfo = None
        connection = HTTPSConnection(host, port, sockinfo=sockinfo, **ssl_args)
        try:
            connection.connect()
        except socket.error as e:
            self.logger.error('could not connect to %s:%d' % self.address)
            raise SyncAPIError('RemoteError', 'Could not connect')
        self.connection = connection

    def close(self):
        """Close the connection."""
        if self.connection is not None:
            try:
                conection.close()
            except Exception:
                pass
        self.connection = None

    def _get_hmac_cb_auth(self, kxid, pin):
        """Return the headers for a client to server HMAC_CB auth."""
        cb = self.connection.sock.get_channel_binding('tls-unique')
        signature = self.crypto.hmac(adjust_pin(pin, +1), cb, 'sha1')
        signature = base64.encode(signature)
        auth = create_option_header('HMAC_CB', kxid=kxid, signature=signature)
        headers = [('Authorization', auth)]
        return headers

    def _check_hmac_cb_auth(self, response, pin):
        """Check a server to client HMAC_CB auth."""
        logger = self.logger
        authinfo = response.getheader('Authentication-Info', '')
        try:
            method, options = parse_option_header(authinfo)
        except ValueError:
            logger.error('illegal Authentication-Info header: %s', authinfo)
            return False
        if 'signature' not in options or not base64.check(options['signature']):
            logger.error('illegal Authentication-Info header: %s', authinfo)
            return False
        signature = base64.decode(options['signature'])
        cb = self.connection.sock.get_channel_binding('tls-unique')
        check = self.crypto.hmac(adjust_pin(pin, -1), cb, 'sha1')
        if check != signature:
            logger.error('HMAC_CB signature did not match')
            return False
        return True

    def pair_step1(self, uuid, name):
        """Perform step 1 in a pairing exchange.
        
        If succesful, this returns a key exchange ID. On error, a SyncAPIError
        exception is raised.
        """
        if self.connection is None:
            raise SyncAPIError('ProgrammingError', 'Not connected')
        logger = self.logger
        logger.setContext('pair step #1')
        url = '/api/vaults/%s/pair' % uuid
        headers = [('Authorization', 'HMAC_CB name=%s' % name)]
        response = self._make_request('POST', url, headers)
        if response is None:
            raise SyncAPIError('RemoteError', 'Could not make HTTP request')
        status = response.status
        if status != 401:
            logger.error('expecting HTTP status 401 (got: %s)', status)
            raise SyncAPIError('RemoteError', response.reason)
        wwwauth = response.getheader('WWW-Authenticate', '')
        try:
            method, options = parse_option_header(wwwauth)
        except ValueError:
            raise SyncAPIError('RemoteError', 'Illegal response')
        if method != 'HMAC_CB' or 'kxid' not in options:
            logger.error('illegal WWW-Authenticate header: %s', wwwauth)
            raise SyncAPIError('RemoteError', 'Illegal response')
        return options['kxid']

    def pair_step2(self, uuid, kxid, pin, certinfo):
        """Perform step 2 in pairing exchange.
        
        If successfull, this returns the peer certificate. On error, a
        SyncAPIError is raised.
        """
        if self.connection is None:
            raise SyncAPIError('ProgrammingError', 'Not connected')
        logger = self.logger
        logger.setContext('pair step #2')
        url = '/api/vaults/%s/pair' % uuid
        headers = self._get_hmac_cb_auth(kxid, pin)
        response = self._make_request('POST', url, headers, certinfo)
        if response is None:
            raise SyncAPIError('RemoteError', 'Could not make syncapi request')
        status = response.status
        if status != 200:
            logger.error('expecting HTTP status 200 (got: %s)', status)
            raise SyncAPIError('RemoteError', response.reason)
        if not self._check_hmac_cb_auth(response, pin):
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        peercert = response.entity
        if peercert is None or not isinstance(peercert, dict):
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        return peercert

    def _get_rsa_cb_auth(self, uuid, model):
        """Return the headers for RSA_CB authentication."""
        cb = self.connection.sock.get_channel_binding('tls-unique')
        privkey = model.get_auth_key(uuid)
        assert privkey is not None
        signature = self.crypto.rsa_sign(cb, privkey, 'pss-sha1')
        signature = base64.encode(signature)
        vault = model.get_vault(uuid)
        auth = create_option_header('RSA_CB', node=vault['node'], signature=signature)
        headers = [('Authorization', auth)]
        return headers

    def _check_rsa_cb_auth(self, uuid, response, model):
        """Verify RSA_CB authentication."""
        logger = self.logger
        authinfo = response.getheader('Authentication-Info', '')
        try:
            method, options = parse_option_header(authinfo)
        except ValueError:
            logger.error('illegal Authentication-Info header')
            return False
        if 'signature' not in options or 'node' not in options \
                or not base64.check(options['signature']) \
                or not check_uuid4(options['node']):
            logger.error('illegal Authentication-Info header')
            return False
        cb = self.connection.sock.get_channel_binding('tls-unique')
        signature = base64.decode(options['signature'])
        cert = model.get_certificate(uuid, options['node'])
        if cert is None:
            logger.error('unknown node in RSA_CB authentication', node)
            return False
        pubkey = base64.decode(cert['payload']['keys']['auth']['key'])
        try:
            status = self.crypto.rsa_verify(cb, signature, pubkey, 'pss-sha1')
        except CryptoError:
            logger.error('corrupt RSA_CB signature')
            return False
        if not status:
            logger.error('RSA_CB signature did not match')
        return status

    def sync(self, uuid, model, notify=True):
        """Synchronize vault `uuid` with the remote peer."""
        if self.connection is None:
            raise SyncAPIError('ProgrammingError', 'Not connected')
        logger = self.logger
        logger.setContext('sync')
        vault = model.get_vault(uuid)
        if vault is None:
            raise SyncAPIError('NotFound', 'Vault not found')
        vector = model.get_vector(uuid)
        vector = dump_vector(vector)
        url = '/api/vaults/%s/items?vector=%s' % (vault['id'], vector)
        headers = self._get_rsa_cb_auth(uuid, model)
        response = self._make_request('GET', url, headers)
        if not response:
            raise SyncAPIError('RemoteError', 'Could not make HTTP request')
        status = response.status
        if status != 200:
            logger.error('expecting HTTP status 200 (got: %s)', status)
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        if not self._check_rsa_cb_auth(uuid, response, model):
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        initems = response.entity
        if initems is None or not isinstance(initems, list):
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        nitems = model.import_items(uuid, initems, notify=notify)
        logger.debug('imported %d items into model', nitems)
        vector = response.getheader('X-Vector', '')
        try:
            vector = parse_vector(vector)
        except ValueError as e:
            logger.error('illegal X-Vector header: %s (%s)', vector, str(e))
            raise SyncAPIError('RemoteError', 'Invalid response')
        outitems = model.get_items(uuid, vector)
        url = '/api/vaults/%s/items' % uuid
        response = self._make_request('POST', url, headers, outitems)
        if not response:
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        if status != 200:
            logger.error('expecting HTTP status 200 (got: %s)', status)
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        if not self._check_rsa_cb_auth(uuid, response, model):
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        logger.debug('succesfully retrieved %d items from peer', len(initems))
        logger.debug('succesfully pushed %d items to peer', len(outitems))
        return len(initems) + len(outitems)
예제 #4
0
class SyncAPIClient(object):
    """
    SyncAPI client.

    This classs implements a client to the Bluepass HTTP based synchronization
    API. The two main functions are pairing (pair_step1() and pair_step2())
    and synchronization (sync()).
    """
    def __init__(self, address, **ssl_args):
        """Create a new client for the syncapi API at `address`."""
        self.address = address
        ssl_args.setdefault('dhparams', dhparams['skip2048'])
        ssl_args.setdefault('ciphers', 'ADH+AES')
        self.ssl_args = ssl_args
        self.connection = None
        logger = logging.getLogger(__name__)
        self.logger = ContextLogger(logger)
        self.crypto = CryptoProvider()

    def _make_request(self, method, url, headers=None, body=None):
        """Make an HTTP request to the API.
        
        This returns the HTTPResponse object on success, or None on failure.
        """
        logger = self.logger
        if headers is None:
            headers = []
        headers.append(('User-Agent', 'Bluepass/%s' % _version.version))
        headers.append(('Accept', 'text/json'))
        if body is None:
            body = ''
        else:
            body = json.dumps(body)
            headers.append(('Content-Type', 'text/json'))
        connection = self.connection
        assert connection is not None
        try:
            logger.debug('client request: %s %s', method, url)
            connection.request(method, url, body, dict(headers))
            response = connection.getresponse()
            headers = response.getheaders()
            body = response.read()
        except (socket.error, HTTPException) as e:
            logger.error('error when making HTTP request: %s', str(e))
            return
        ctype = response.getheader('Content-Type')
        if ctype == 'text/json':
            parsed = json.try_loads(body)
            if parsed is None:
                logger.error('response body contains invalid JSON')
                return
            response.entity = parsed
            logger.debug('parsed "%s" request body (%d bytes)', ctype,
                         len(body))
        else:
            response.entity = None
        return response

    def connect(self):
        """Connect to the remote syncapi."""
        ssl_args = {'dhparams': dhparams['skip2048'], 'ciphers': 'ADH+AES'}
        # Support both dict style addresses for arbitrary address families,
        # as well as (host, port) tuples for IPv4.
        if isinstance(self.address, dict):
            host = self.address['host']
            port = None
            sockinfo = self.address
        else:
            host, port = self.address
            sockinfo = None
        connection = HTTPSConnection(host, port, sockinfo=sockinfo, **ssl_args)
        try:
            connection.connect()
        except socket.error as e:
            self.logger.error('could not connect to %s:%d' % self.address)
            raise SyncAPIError('RemoteError', 'Could not connect')
        self.connection = connection

    def close(self):
        """Close the connection."""
        if self.connection is not None:
            try:
                conection.close()
            except Exception:
                pass
        self.connection = None

    def _get_hmac_cb_auth(self, kxid, pin):
        """Return the headers for a client to server HMAC_CB auth."""
        cb = self.connection.sock.get_channel_binding('tls-unique')
        signature = self.crypto.hmac(adjust_pin(pin, +1), cb, 'sha1')
        signature = base64.encode(signature)
        auth = create_option_header('HMAC_CB', kxid=kxid, signature=signature)
        headers = [('Authorization', auth)]
        return headers

    def _check_hmac_cb_auth(self, response, pin):
        """Check a server to client HMAC_CB auth."""
        logger = self.logger
        authinfo = response.getheader('Authentication-Info', '')
        try:
            method, options = parse_option_header(authinfo)
        except ValueError:
            logger.error('illegal Authentication-Info header: %s', authinfo)
            return False
        if 'signature' not in options or not base64.check(
                options['signature']):
            logger.error('illegal Authentication-Info header: %s', authinfo)
            return False
        signature = base64.decode(options['signature'])
        cb = self.connection.sock.get_channel_binding('tls-unique')
        check = self.crypto.hmac(adjust_pin(pin, -1), cb, 'sha1')
        if check != signature:
            logger.error('HMAC_CB signature did not match')
            return False
        return True

    def pair_step1(self, uuid, name):
        """Perform step 1 in a pairing exchange.
        
        If succesful, this returns a key exchange ID. On error, a SyncAPIError
        exception is raised.
        """
        if self.connection is None:
            raise SyncAPIError('ProgrammingError', 'Not connected')
        logger = self.logger
        logger.setContext('pair step #1')
        url = '/api/vaults/%s/pair' % uuid
        headers = [('Authorization', 'HMAC_CB name=%s' % name)]
        response = self._make_request('POST', url, headers)
        if response is None:
            raise SyncAPIError('RemoteError', 'Could not make HTTP request')
        status = response.status
        if status != 401:
            logger.error('expecting HTTP status 401 (got: %s)', status)
            raise SyncAPIError('RemoteError', response.reason)
        wwwauth = response.getheader('WWW-Authenticate', '')
        try:
            method, options = parse_option_header(wwwauth)
        except ValueError:
            raise SyncAPIError('RemoteError', 'Illegal response')
        if method != 'HMAC_CB' or 'kxid' not in options:
            logger.error('illegal WWW-Authenticate header: %s', wwwauth)
            raise SyncAPIError('RemoteError', 'Illegal response')
        return options['kxid']

    def pair_step2(self, uuid, kxid, pin, certinfo):
        """Perform step 2 in pairing exchange.
        
        If successfull, this returns the peer certificate. On error, a
        SyncAPIError is raised.
        """
        if self.connection is None:
            raise SyncAPIError('ProgrammingError', 'Not connected')
        logger = self.logger
        logger.setContext('pair step #2')
        url = '/api/vaults/%s/pair' % uuid
        headers = self._get_hmac_cb_auth(kxid, pin)
        response = self._make_request('POST', url, headers, certinfo)
        if response is None:
            raise SyncAPIError('RemoteError', 'Could not make syncapi request')
        status = response.status
        if status != 200:
            logger.error('expecting HTTP status 200 (got: %s)', status)
            raise SyncAPIError('RemoteError', response.reason)
        if not self._check_hmac_cb_auth(response, pin):
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        peercert = response.entity
        if peercert is None or not isinstance(peercert, dict):
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        return peercert

    def _get_rsa_cb_auth(self, uuid, model):
        """Return the headers for RSA_CB authentication."""
        cb = self.connection.sock.get_channel_binding('tls-unique')
        privkey = model.get_auth_key(uuid)
        assert privkey is not None
        signature = self.crypto.rsa_sign(cb, privkey, 'pss-sha1')
        signature = base64.encode(signature)
        vault = model.get_vault(uuid)
        auth = create_option_header('RSA_CB',
                                    node=vault['node'],
                                    signature=signature)
        headers = [('Authorization', auth)]
        return headers

    def _check_rsa_cb_auth(self, uuid, response, model):
        """Verify RSA_CB authentication."""
        logger = self.logger
        authinfo = response.getheader('Authentication-Info', '')
        try:
            method, options = parse_option_header(authinfo)
        except ValueError:
            logger.error('illegal Authentication-Info header')
            return False
        if 'signature' not in options or 'node' not in options \
                or not base64.check(options['signature']) \
                or not check_uuid4(options['node']):
            logger.error('illegal Authentication-Info header')
            return False
        cb = self.connection.sock.get_channel_binding('tls-unique')
        signature = base64.decode(options['signature'])
        cert = model.get_certificate(uuid, options['node'])
        if cert is None:
            logger.error('unknown node in RSA_CB authentication', node)
            return False
        pubkey = base64.decode(cert['payload']['keys']['auth']['key'])
        try:
            status = self.crypto.rsa_verify(cb, signature, pubkey, 'pss-sha1')
        except CryptoError:
            logger.error('corrupt RSA_CB signature')
            return False
        if not status:
            logger.error('RSA_CB signature did not match')
        return status

    def sync(self, uuid, model, notify=True):
        """Synchronize vault `uuid` with the remote peer."""
        if self.connection is None:
            raise SyncAPIError('ProgrammingError', 'Not connected')
        logger = self.logger
        logger.setContext('sync')
        vault = model.get_vault(uuid)
        if vault is None:
            raise SyncAPIError('NotFound', 'Vault not found')
        vector = model.get_vector(uuid)
        vector = dump_vector(vector)
        url = '/api/vaults/%s/items?vector=%s' % (vault['id'], vector)
        headers = self._get_rsa_cb_auth(uuid, model)
        response = self._make_request('GET', url, headers)
        if not response:
            raise SyncAPIError('RemoteError', 'Could not make HTTP request')
        status = response.status
        if status != 200:
            logger.error('expecting HTTP status 200 (got: %s)', status)
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        if not self._check_rsa_cb_auth(uuid, response, model):
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        initems = response.entity
        if initems is None or not isinstance(initems, list):
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        nitems = model.import_items(uuid, initems, notify=notify)
        logger.debug('imported %d items into model', nitems)
        vector = response.getheader('X-Vector', '')
        try:
            vector = parse_vector(vector)
        except ValueError as e:
            logger.error('illegal X-Vector header: %s (%s)', vector, str(e))
            raise SyncAPIError('RemoteError', 'Invalid response')
        outitems = model.get_items(uuid, vector)
        url = '/api/vaults/%s/items' % uuid
        response = self._make_request('POST', url, headers, outitems)
        if not response:
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        if status != 200:
            logger.error('expecting HTTP status 200 (got: %s)', status)
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        if not self._check_rsa_cb_auth(uuid, response, model):
            raise SyncAPIError('RemoteError', 'Illegal syncapi response')
        logger.debug('succesfully retrieved %d items from peer', len(initems))
        logger.debug('succesfully pushed %d items to peer', len(outitems))
        return len(initems) + len(outitems)