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)
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)
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)
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)