class SecretsKeyring(Keyring): """A Keyring interface into the freedesktop "secrets" service. This interface is available on Freedesktop platforms (GNOME, KDE). """ def __init__(self, connection): """Create a new keyring. Requires a python-tdbus dispatcher as an argument.""" self.connection = connection self.logger = logging.getLogger( 'bluepass.platform.freedesktop.keyring') self.crypto = CryptoProvider() def _call_svc(self, path, method, interface, format=None, args=None): """INTERNAL: call into the secrets service.""" try: result = self.connection.call_method(path, method, interface=interface, format=format, args=args, destination=CONN_SERVICE) except tdbus.Error as e: raise KeyringError('D-BUS error for method "%s": %s' % (method, str(e))) return result def isavailable(self): """Return whether or not we can store a key. This requires the secrets service to be available and the login keyring to be unlocked.""" try: reply = self._call_svc(PATH_LOGIN_COLLECTION, 'Get', IFACE_PROPS, 'ss', (IFACE_COLLECTION, 'Locked')) except KeyringError: self.logger.debug('could not access secrets service') return False value = reply.get_args()[0] if value[0] != 'b': raise KeyringError('expecting type "b" for "Locked" property') self.logger.debug('login keyring is locked: %s', value[1]) return not value[1] def _open_session(self): """INTERNAL: open a session.""" algo = 'dh-ietf1024-sha256-aes128-cbc-pkcs7' params = dhparams['ietf1024'] keypair = self.crypto.dh_genkey(params) reply = self._call_svc(PATH_SERVICE, 'OpenSession', IFACE_SERVICE, 'sv', (algo, ('ay', keypair[1]))) if reply.get_signature() != 'vo': raise KeyringError( 'expecting "vo" reply signature for "OpenSession"') output, path = reply.get_args() if output[0] != 'ay': raise KeyringError( 'expecting "ay" type for output argument of "OpenSession"') pubkey = output[1] if not self.crypto.dh_checkkey(params, pubkey): raise KeyringError('insecure public key returned by "OpenSession"') secret = self.crypto.dh_compute(params, keypair[0], pubkey) symkey = self.crypto.hkdf(secret, None, '', 16, 'sha256') return path, symkey def store(self, key, value): """Store a secret in the keyring.""" session, symkey = self._open_session() try: attrib = {'application': 'bluepass', 'bluepass-key-id': key} props = { 'org.freedesktop.Secret.Item.Label': ('s', 'Bluepass Key: %s' % key), 'org.freedesktop.Secret.Item.Attributes': ('a{ss}', attrib) } iv = self.crypto.random(16) encrypted = self.crypto.aes_encrypt(value, symkey, iv, 'cbc-pkcs7') secret = (session, iv, encrypted, 'text/plain') reply = self._call_svc(PATH_LOGIN_COLLECTION, 'CreateItem', IFACE_COLLECTION, 'a{sv}(oayays)b', (props, secret, True)) item, prompt = reply.get_args() if item == '/': raise KeyringError('not expecting a prompt for "CreateItem"') return item finally: self._call_svc(session, 'Close', IFACE_SESSION) def retrieve(self, key): """Retrieve a secret from the keyring.""" session, symkey = self._open_session() try: attrib = {'application': 'bluepass', 'bluepass-key-id': key} reply = self._call_svc(PATH_LOGIN_COLLECTION, 'SearchItems', IFACE_COLLECTION, 'a{ss}', (attrib, )) paths = reply.get_args()[0] if len(paths) > 1: self.logger.error( 'SearchItems returned %d entries for key "%s"' % (len(paths), key)) return elif len(paths) == 0: return item = paths[0] reply = self._call_svc(item, 'GetSecret', IFACE_ITEM, 'o', (session, )) secret = reply.get_args()[0] decrypted = self.crypto.aes_decrypt(secret[2], symkey, secret[1], 'cbc-pkcs7') return decrypted finally: self._call_svc(session, 'Close', IFACE_SESSION)
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 SecretsKeyring(Keyring): """A Keyring interface into the freedesktop "secrets" service. This interface is available on Freedesktop platforms (GNOME, KDE). """ def __init__(self, connection): """Create a new keyring. Requires a python-tdbus dispatcher as an argument.""" self.connection = connection self.logger = logging.getLogger('bluepass.platform.freedesktop.keyring') self.crypto = CryptoProvider() def _call_svc(self, path, method, interface, format=None, args=None): """INTERNAL: call into the secrets service.""" try: result = self.connection.call_method(path, method, interface=interface, format=format, args=args, destination=CONN_SERVICE) except tdbus.Error as e: raise KeyringError('D-BUS error for method "%s": %s' % (method, str(e))) return result def isavailable(self): """Return whether or not we can store a key. This requires the secrets service to be available and the login keyring to be unlocked.""" try: reply = self._call_svc(PATH_LOGIN_COLLECTION, 'Get', IFACE_PROPS, 'ss', (IFACE_COLLECTION, 'Locked')) except KeyringError: self.logger.debug('could not access secrets service') return False value = reply.get_args()[0] if value[0] != 'b': raise KeyringError('expecting type "b" for "Locked" property') self.logger.debug('login keyring is locked: %s', value[1]) return not value[1] def _open_session(self): """INTERNAL: open a session.""" algo = 'dh-ietf1024-sha256-aes128-cbc-pkcs7' params = dhparams['ietf1024'] keypair = self.crypto.dh_genkey(params) reply = self._call_svc(PATH_SERVICE, 'OpenSession', IFACE_SERVICE, 'sv', (algo, ('ay', keypair[1]))) if reply.get_signature() != 'vo': raise KeyringError('expecting "vo" reply signature for "OpenSession"') output, path = reply.get_args() if output[0] != 'ay': raise KeyringError('expecting "ay" type for output argument of "OpenSession"') pubkey = output[1] if not self.crypto.dh_checkkey(params, pubkey): raise KeyringError('insecure public key returned by "OpenSession"') secret = self.crypto.dh_compute(params, keypair[0], pubkey) symkey = self.crypto.hkdf(secret, None, '', 16, 'sha256') return path, symkey def store(self, key, value): """Store a secret in the keyring.""" session, symkey = self._open_session() try: attrib = { 'application': 'bluepass', 'bluepass-key-id': key } props = { 'org.freedesktop.Secret.Item.Label': ('s', 'Bluepass Key: %s' % key), 'org.freedesktop.Secret.Item.Attributes': ('a{ss}', attrib) } iv = self.crypto.random(16) encrypted = self.crypto.aes_encrypt(value, symkey, iv, 'cbc-pkcs7') secret = (session, iv, encrypted, 'text/plain') reply = self._call_svc(PATH_LOGIN_COLLECTION, 'CreateItem', IFACE_COLLECTION, 'a{sv}(oayays)b', (props, secret, True)) item, prompt = reply.get_args() if item == '/': raise KeyringError('not expecting a prompt for "CreateItem"') return item finally: self._call_svc(session, 'Close', IFACE_SESSION) def retrieve(self, key): """Retrieve a secret from the keyring.""" session, symkey = self._open_session() try: attrib = { 'application': 'bluepass', 'bluepass-key-id': key } reply = self._call_svc(PATH_LOGIN_COLLECTION, 'SearchItems', IFACE_COLLECTION, 'a{ss}', (attrib,)) paths = reply.get_args()[0] if len(paths) > 1: self.logger.error('SearchItems returned %d entries for key "%s"' % (len(paths), key)) return elif len(paths) == 0: return item = paths[0] reply = self._call_svc(item, 'GetSecret', IFACE_ITEM, 'o', (session,)) secret = reply.get_args()[0] decrypted = self.crypto.aes_decrypt(secret[2], symkey, secret[1], 'cbc-pkcs7') return decrypted finally: self._call_svc(session, 'Close', IFACE_SESSION)
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)