Пример #1
0
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)
Пример #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 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)
Пример #4
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)