From ff85c2a41fe933d9959fb84a0df2a13a6e199cec Mon Sep 17 00:00:00 2001 From: drebs Date: Mon, 5 Dec 2016 09:10:47 -0200 Subject: [refactor] improve secrets generation and storage code --- .../src/leap/soledad/client/_secrets/__init__.py | 132 ++++ client/src/leap/soledad/client/_secrets/crypto.py | 123 ++++ client/src/leap/soledad/client/_secrets/storage.py | 124 ++++ client/src/leap/soledad/client/_secrets/util.py | 46 ++ client/src/leap/soledad/client/api.py | 100 +-- client/src/leap/soledad/client/interfaces.py | 20 - client/src/leap/soledad/client/secrets.py | 794 --------------------- client/src/leap/soledad/client/shared_db.py | 12 +- 8 files changed, 440 insertions(+), 911 deletions(-) create mode 100644 client/src/leap/soledad/client/_secrets/__init__.py create mode 100644 client/src/leap/soledad/client/_secrets/crypto.py create mode 100644 client/src/leap/soledad/client/_secrets/storage.py create mode 100644 client/src/leap/soledad/client/_secrets/util.py delete mode 100644 client/src/leap/soledad/client/secrets.py (limited to 'client/src') diff --git a/client/src/leap/soledad/client/_secrets/__init__.py b/client/src/leap/soledad/client/_secrets/__init__.py new file mode 100644 index 00000000..f9da8423 --- /dev/null +++ b/client/src/leap/soledad/client/_secrets/__init__.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# _secrets/__init__.py +# Copyright (C) 2016 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os + +from collections import namedtuple + +from leap.soledad.common.log import getLogger + +from leap.soledad.client._secrets.storage import SecretsStorage +from leap.soledad.client._secrets.crypto import SecretsCrypto +from leap.soledad.client._secrets.util import emit + + +logger = getLogger(__name__) + + +SecretLength = namedtuple('SecretLength', 'name length') + + +class Secrets(object): + + lengths = { + 'remote': 512, + 'salt': 64, + 'local': 448, + } + + def __init__(self, uuid, passphrase, url, local_path, creds, userid, + shared_db=None): + self._passphrase = passphrase + self._secrets = {} + self._user_data = {'uuid': uuid, 'userid': userid} + self.crypto = SecretsCrypto(self.get_passphrase) + self.storage = SecretsStorage( + uuid, self.get_passphrase, url, local_path, creds, userid, + shared_db=shared_db) + self._bootstrap() + + # + # bootstrap + # + + def _bootstrap(self): + force_storage = False + + # attempt to load secrets from local storage + encrypted = self.storage.load_local() + + # if not found, attempt to load secrets from remote storage + if not encrypted: + encrypted = self.storage.load_remote() + + if not encrypted: + # if not found, generate new secrets + secrets = self._generate() + encrypted = self.crypto.encrypt(secrets) + force_storage = True + else: + # decrypt secrets found either in local or remote storage + secrets = self.crypto.decrypt(encrypted) + + self._secrets = secrets + + if encrypted['version'] < self.crypto.VERSION or force_storage: + self.storage.save_local(encrypted) + self.storage.save_remote(encrypted) + + # + # generation + # + + @emit('creating') + def _generate(self): + logger.info("generating new set of secrets...") + secrets = {} + for name, length in self.lengths.iteritems(): + secret = os.urandom(length) + secrets[name] = secret + logger.info("new set of secrets successfully generated") + return secrets + + # + # crypto + # + + def _encrypt(self): + # encrypt secrets + secrets = self._secrets + encrypted = self.crypto.encrypt(secrets) + # create the recovery document + data = {'secret': encrypted, 'version': 2} + return data + + def get_passphrase(self): + return self._passphrase.encode('utf-8') + + @property + def passphrase(self): + return self.get_passphrase() + + def change_passphrase(self, new_passphrase): + self._passphrase = new_passphrase + encrypted = self.crypto.encrypt(self._secrets) + self.storage.save_local(encrypted) + self.storage.save_remote(encrypted) + + @property + def remote(self): + return self._secrets.get('remote') + + @property + def salt(self): + return self._secrets.get('salt') + + @property + def local(self): + return self._secrets.get('local') diff --git a/client/src/leap/soledad/client/_secrets/crypto.py b/client/src/leap/soledad/client/_secrets/crypto.py new file mode 100644 index 00000000..76e80222 --- /dev/null +++ b/client/src/leap/soledad/client/_secrets/crypto.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +# _secrets/crypto.py +# Copyright (C) 2016 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import binascii +import json +import os +import scrypt + +from leap.soledad.common import soledad_assert +from leap.soledad.common.log import getLogger + +from leap.soledad.client._crypto import encrypt_sym, decrypt_sym, ENC_METHOD +from leap.soledad.client._secrets.util import SecretsError + + +logger = getLogger(__name__) + + +class SecretsCrypto(object): + + VERSION = 2 + + def __init__(self, get_pass): + self._get_pass = get_pass + + def _get_key(self, salt): + key = scrypt.hash(self._get_pass(), salt, buflen=32) + return key + + # + # encryption + # + + def encrypt(self, secrets): + encoded = {} + for name, value in secrets.iteritems(): + encoded[name] = binascii.b2a_base64(value) + plaintext = json.dumps(encoded) + salt = os.urandom(64) # TODO: get salt length from somewhere else + key = self._get_key(salt) + iv, ciphertext = encrypt_sym(plaintext, key, + method=ENC_METHOD.aes_256_gcm) + encrypted = { + 'version': self.VERSION, + 'kdf': 'scrypt', + 'kdf_salt': binascii.b2a_base64(salt), + 'kdf_length': len(key), + 'cipher': 'aes_256_gcm', + 'length': len(plaintext), + 'iv': str(iv), + 'secrets': binascii.b2a_base64(ciphertext), + } + return encrypted + + # + # decryption + # + + def decrypt(self, data): + version = data.get('version') + method = getattr(self, '_decrypt_v%d' % version) + try: + return method(data) + except Exception as e: + logger.error('error decrypting secrets: %r' % e) + raise SecretsError(e) + + def _decrypt_v1(self, data): + secret_id = data['active_secret'] + encrypted = data['storage_secrets'][secret_id] + soledad_assert(encrypted['cipher'] == 'aes256') + + salt = binascii.a2b_base64(encrypted['kdf_salt']) + key = self._get_key(salt) + separator = ':' + iv, ciphertext = encrypted['secret'].split(separator, 1) + ciphertext = binascii.a2b_base64(ciphertext) + plaintext = self._decrypt( + key, iv, ciphertext, encrypted, ENC_METHOD.aes_256_ctr) + secrets = { + 'remote': plaintext[0:512], + 'salt': plaintext[512:576], + 'local': plaintext[576:1024], + } + return secrets + + def _decrypt_v2(self, encrypted): + soledad_assert(encrypted['cipher'] == 'aes_256_gcm') + + salt = binascii.a2b_base64(encrypted['kdf_salt']) + key = self._get_key(salt) + iv = encrypted['iv'] + ciphertext = binascii.a2b_base64(encrypted['secrets']) + plaintext = self._decrypt( + key, iv, ciphertext, encrypted, ENC_METHOD.aes_256_gcm) + encoded = json.loads(plaintext) + secrets = {} + for name, value in encoded.iteritems(): + secrets[name] = binascii.a2b_base64(value) + return secrets + + def _decrypt(self, key, iv, ciphertext, encrypted, method): + # assert some properties of the stored secret + soledad_assert(encrypted['kdf'] == 'scrypt') + soledad_assert(encrypted['kdf_length'] == len(key)) + # decrypt + plaintext = decrypt_sym(ciphertext, key, iv, method) + soledad_assert(encrypted['length'] == len(plaintext)) + return plaintext diff --git a/client/src/leap/soledad/client/_secrets/storage.py b/client/src/leap/soledad/client/_secrets/storage.py new file mode 100644 index 00000000..da3aa9d7 --- /dev/null +++ b/client/src/leap/soledad/client/_secrets/storage.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +# _secrets/storage.py +# Copyright (C) 2016 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import json +import urlparse + +from hashlib import sha256 + +from leap.soledad.common import SHARED_DB_NAME +from leap.soledad.common.log import getLogger + +from leap.soledad.common.document import SoledadDocument +from leap.soledad.client.shared_db import SoledadSharedDatabase +from leap.soledad.client._secrets.util import emit + + +logger = getLogger(__name__) + + +class SecretsStorage(object): + + def __init__(self, uuid, get_pass, url, local_path, creds, userid, + shared_db=None): + self._uuid = uuid + self._get_pass = get_pass + self._local_path = local_path + self._userid = userid + + self._shared_db = shared_db or self._init_shared_db(url, creds) + self.__remote_doc = None + + # + # properties + # + + @property + def _user_data(self): + return {'uuid': self._uuid, 'userid': self._userid} + + # + # local storage + # + + def load_local(self): + logger.info("trying to load secrets from disk: %s" % self._local_path) + try: + with open(self._local_path, 'r') as f: + encrypted = json.loads(f.read()) + logger.info("secrets loaded successfully from disk") + return encrypted + except IOError: + logger.warn("secrets not found in disk") + return None + + def save_local(self, encrypted): + json_data = json.dumps(encrypted) + with open(self._local_path, 'w') as f: + f.write(json_data) + + # + # remote storage + # + + def _init_shared_db(self, url, creds): + url = urlparse.urljoin(url, SHARED_DB_NAME) + db = SoledadSharedDatabase.open_database( + url, self._uuid, creds=creds) + self._shared_db = db + + def _remote_doc_id(self): + passphrase = self._get_pass() + text = '%s%s' % (passphrase, self._uuid) + digest = sha256(text).hexdigest() + return digest + + @property + def _remote_doc(self): + if not self.__remote_doc and self._shared_db: + doc = self._get_remote_doc() + self.__remote_doc = doc + return self.__remote_doc + + @emit('downloading') + def _get_remote_doc(self): + logger.info('trying to load secrets from server...') + doc = self._shared_db.get_doc(self._remote_doc_id()) + if doc: + logger.info('secrets loaded successfully from server') + else: + logger.warn('secrets not found in server') + return doc + + def load_remote(self): + doc = self._remote_doc + if not doc: + return None + encrypted = doc.content + return encrypted + + @emit('uploading') + def save_remote(self, encrypted): + doc = self._remote_doc + if not doc: + doc = SoledadDocument(doc_id=self._remote_doc_id()) + doc.content = encrypted + db = self._shared_db + if not db: + logger.warn('no shared db found') + return + db.put_doc(doc) diff --git a/client/src/leap/soledad/client/_secrets/util.py b/client/src/leap/soledad/client/_secrets/util.py new file mode 100644 index 00000000..f75b2bb6 --- /dev/null +++ b/client/src/leap/soledad/client/_secrets/util.py @@ -0,0 +1,46 @@ +# -*- coding:utf-8 -*- +# _secrets/util.py +# Copyright (C) 2016 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +from leap.soledad.client import events + + +class SecretsError(Exception): + pass + + +def emit(verb): + def _decorator(method): + def _decorated(self, *args, **kwargs): + + # emit starting event + user_data = self._user_data + name = 'SOLEDAD_' + verb.upper() + '_KEYS' + event = getattr(events, name) + events.emit_async(event, user_data) + + # run the method + result = method(self, *args, **kwargs) + + # emit a finished event + name = 'SOLEDAD_DONE_' + verb.upper() + '_KEYS' + event = getattr(events, name) + events.emit_async(event, user_data) + + return result + return _decorated + return _decorator diff --git a/client/src/leap/soledad/client/api.py b/client/src/leap/soledad/client/api.py index da6eec66..2e1d1cd3 100644 --- a/client/src/leap/soledad/client/api.py +++ b/client/src/leap/soledad/client/api.py @@ -45,7 +45,6 @@ from zope.interface import implements from leap.common.config import get_path_prefix from leap.common.plugins import collect_plugins -from leap.soledad.common import SHARED_DB_NAME from leap.soledad.common import soledad_assert from leap.soledad.common import soledad_assert_type from leap.soledad.common.log import getLogger @@ -57,8 +56,7 @@ from leap.soledad.client import adbapi from leap.soledad.client import events as soledad_events from leap.soledad.client import interfaces as soledad_interfaces from leap.soledad.client import sqlcipher -from leap.soledad.client.secrets import SoledadSecrets -from leap.soledad.client.shared_db import SoledadSharedDatabase +from leap.soledad.client._secrets import Secrets from leap.soledad.client._crypto import SoledadCrypto logger = getLogger(__name__) @@ -130,7 +128,7 @@ class Soledad(object): def __init__(self, uuid, passphrase, secrets_path, local_db_path, server_url, cert_file, shared_db=None, - auth_token=None, syncable=True): + auth_token=None): """ Initialize configuration, cryptographic keys and dbs. @@ -185,8 +183,6 @@ class Soledad(object): self._secrets_path = None self._dbsyncer = None - self.shared_db = shared_db - # configure SSL certificate global SOLEDAD_CERT SOLEDAD_CERT = cert_file @@ -198,21 +194,15 @@ class Soledad(object): self._secrets_path = secrets_path - # Initialize shared recovery database - self.init_shared_db(server_url, uuid, self._creds, syncable=syncable) - - # The following can raise BootstrapSequenceError, that will be - # propagated upwards. - self._init_secrets() + self._init_secrets(shared_db=shared_db) - self._crypto = SoledadCrypto(self._secrets.remote_storage_secret) + self._crypto = SoledadCrypto(self._secrets.remote) try: # initialize database access, trap any problems so we can shutdown # smoothly. self._init_u1db_sqlcipher_backend() - if syncable: - self._init_u1db_syncer() + self._init_u1db_syncer() except DatabaseAccessError: # oops! something went wrong with backend initialization. We # have to close any thread-related stuff we have already opened @@ -255,14 +245,13 @@ class Soledad(object): for path in paths: create_path_if_not_exists(path) - def _init_secrets(self): + def _init_secrets(self, shared_db=None): """ Initialize Soledad secrets. """ - self._secrets = SoledadSecrets( - self.uuid, self._passphrase, self._secrets_path, - self.shared_db, userid=self.userid) - self._secrets.bootstrap() + self._secrets = Secrets( + self._uuid, self._passphrase, self._server_url, self._secrets_path, + self._creds, self.userid, shared_db=shared_db) def _init_u1db_sqlcipher_backend(self): """ @@ -279,7 +268,7 @@ class Soledad(object): """ tohex = binascii.b2a_hex # sqlcipher only accepts the hex version - key = tohex(self._secrets.get_local_storage_key()) + key = tohex(self._secrets.local) opts = sqlcipher.SQLCipherOptions( self._local_db_path, key, @@ -659,21 +648,6 @@ class Soledad(object): # ISyncableStorage # - def set_syncable(self, syncable): - """ - Toggle the syncable state for this database. - - This can be used to start a database with offline state and switch it - online afterwards. Or the opposite: stop syncs when connection is lost. - - :param syncable: new status for syncable. - :type syncable: bool - """ - # TODO should check that we've got a token! - self.shared_db.syncable = syncable - if syncable and not self._dbsyncer: - self._init_u1db_syncer() - def sync(self): """ Synchronize documents with the server replica. @@ -760,13 +734,6 @@ class Soledad(object): """ return self.sync_lock.locked - @property - def syncable(self): - if self.shared_db: - return self.shared_db.syncable - else: - return False - def _set_token(self, token): """ Set the authentication token for remote database access. @@ -803,58 +770,13 @@ class Soledad(object): # ISecretsStorage # - def init_shared_db(self, server_url, uuid, creds, syncable=True): - """ - Initialize the shared database. - - :param server_url: URL of the remote database. - :type server_url: str - :param uuid: The user's unique id. - :type uuid: str - :param creds: A tuple containing the authentication method and - credentials. - :type creds: tuple - :param syncable: - If syncable is False, the database will not attempt to sync against - a remote replica. - :type syncable: bool - """ - # only case this is False is for testing purposes - if self.shared_db is None: - shared_db_url = urlparse.urljoin(server_url, SHARED_DB_NAME) - self.shared_db = SoledadSharedDatabase.open_database( - shared_db_url, - uuid, - creds=creds, - syncable=syncable) - - @property - def storage_secret(self): - """ - Return the secret used for local storage encryption. - - :return: The secret used for local storage encryption. - :rtype: str - """ - return self._secrets.storage_secret - - @property - def remote_storage_secret(self): - """ - Return the secret used for encryption of remotely stored data. - - :return: The secret used for remote storage encryption. - :rtype: str - """ - return self._secrets.remote_storage_secret - @property def secrets(self): """ Return the secrets object. :return: The secrets object. - :rtype: SoledadSecrets + :rtype: Secrets """ return self._secrets diff --git a/client/src/leap/soledad/client/interfaces.py b/client/src/leap/soledad/client/interfaces.py index 82927ff4..1be47df7 100644 --- a/client/src/leap/soledad/client/interfaces.py +++ b/client/src/leap/soledad/client/interfaces.py @@ -351,28 +351,12 @@ class ISecretsStorage(Interface): secrets_file_name = Attribute( "The name of the file where the storage secrets will be stored") - storage_secret = Attribute("") - remote_storage_secret = Attribute("") - shared_db = Attribute("The shared db object") - # XXX this used internally from secrets, so it might be good to preserve # as a public boundary with other components. # We should also probably document its interface. secrets = Attribute("A SoledadSecrets object containing access to secrets") - def init_shared_db(self, server_url, uuid, creds): - """ - Initialize the shared recovery database. - - :param server_url: - :type server_url: - :param uuid: - :type uuid: - :param creds: - :type creds: - """ - def change_passphrase(self, new_passphrase): """ Change the passphrase that encrypts the storage secret. @@ -382,7 +366,3 @@ class ISecretsStorage(Interface): :raise NoStorageSecret: Raised if there's no storage secret available. """ - - # XXX not in use. Uncomment if we ever decide to allow - # multiple secrets. - # secret_id = Attribute("The id of the storage secret to be used") diff --git a/client/src/leap/soledad/client/secrets.py b/client/src/leap/soledad/client/secrets.py deleted file mode 100644 index 3fe98c64..00000000 --- a/client/src/leap/soledad/client/secrets.py +++ /dev/null @@ -1,794 +0,0 @@ -# -*- coding: utf-8 -*- -# secrets.py -# Copyright (C) 2014 LEAP -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - - -""" -Soledad secrets handling. -""" - - -import os -import scrypt -import binascii -import errno -import json - -from hashlib import sha256 - -from leap.soledad.common import soledad_assert -from leap.soledad.common import soledad_assert_type -from leap.soledad.common import document -from leap.soledad.common.log import getLogger -from leap.soledad.client import events -from leap.soledad.client import _crypto - - -logger = getLogger(__name__) - - -# -# Exceptions -# - - -class SecretsException(Exception): - - """ - Generic exception type raised by this module. - """ - - -class NoStorageSecret(SecretsException): - - """ - Raised when trying to use a storage secret but none is available. - """ - pass - - -class PassphraseTooShort(SecretsException): - - """ - Raised when trying to change the passphrase but the provided passphrase is - too short. - """ - - -class BootstrapSequenceError(SecretsException): - - """ - Raised when an attempt to generate a secret and store it in a recovery - document on server failed. - """ - - -# -# Secrets handler -# - - -class SoledadSecrets(object): - - """ - Soledad secrets handler. - - The first C{self.REMOTE_STORAGE_SECRET_LENGTH} bytes of the storage - secret are used for remote storage encryption. We use the next - C{self.LOCAL_STORAGE_SECRET} bytes to derive a key for local storage. - From these bytes, the first C{self.SALT_LENGTH} bytes are used as the - salt and the rest as the password for the scrypt hashing. - """ - - LOCAL_STORAGE_SECRET_LENGTH = 512 - """ - The length, in bytes, of the secret used to derive a passphrase for the - SQLCipher database. - """ - - REMOTE_STORAGE_SECRET_LENGTH = 512 - """ - The length, in bytes, of the secret used to derive an encryption key for - remote storage. - """ - - SALT_LENGTH = 64 - """ - The length, in bytes, of the salt used to derive the key for the storage - secret encryption. - """ - - GEN_SECRET_LENGTH = LOCAL_STORAGE_SECRET_LENGTH \ - + REMOTE_STORAGE_SECRET_LENGTH \ - + SALT_LENGTH # for sync db - """ - The length, in bytes, of the secret to be generated. This includes local - and remote secrets, and the salt for deriving the sync db secret. - """ - - MINIMUM_PASSPHRASE_LENGTH = 6 - """ - The minimum length, in bytes, for a passphrase. The passphrase length is - only checked when the user changes her passphrase, not when she - instantiates Soledad. - """ - - SEPARATOR = ":" - """ - A separator used for storing the encryption initial value prepended to the - ciphertext. - """ - - UUID_KEY = 'uuid' - STORAGE_SECRETS_KEY = 'storage_secrets' - ACTIVE_SECRET_KEY = 'active_secret' - SECRET_KEY = 'secret' - CIPHER_KEY = 'cipher' - LENGTH_KEY = 'length' - KDF_KEY = 'kdf' - KDF_SALT_KEY = 'kdf_salt' - KDF_LENGTH_KEY = 'kdf_length' - KDF_SCRYPT = 'scrypt' - CIPHER_AES256 = 'aes256' # deprecated, AES-CTR - CIPHER_AES256_GCM = _crypto.ENC_METHOD.aes_256_gcm - RECOVERY_DOC_VERSION_KEY = 'version' - RECOVERY_DOC_VERSION = 1 - """ - Keys used to access storage secrets in recovery documents. - """ - - def __init__(self, uuid, passphrase, secrets_path, shared_db, userid=None): - """ - Initialize the secrets manager. - - :param uuid: User's unique id. - :type uuid: str - :param passphrase: The passphrase for locking and unlocking encryption - secrets for local and remote storage. - :type passphrase: unicode - :param secrets_path: Path for storing encrypted key used for - symmetric encryption. - :type secrets_path: str - :param shared_db: The shared database that stores user secrets. - :type shared_db: leap.soledad.client.shared_db.SoledadSharedDatabase - """ - self._uuid = uuid - self._userid = userid - self._passphrase = passphrase - self._secrets_path = secrets_path - self._shared_db = shared_db - self._secrets = {} - self._secret_id = None - - def bootstrap(self): - """ - Bootstrap secrets. - - Soledad secrets bootstrap is the following sequence of stages: - - * stage 1 - local secret loading: - - if secrets exist locally, load them. - * stage 2 - remote secret loading: - - else, if secrets exist in server, download them. - * stage 3 - secret generation: - - else, generate a new secret and store in server. - - This method decides which bootstrap stages have already been performed - and performs the missing ones in order. - - :raise BootstrapSequenceError: Raised when the secret generation and - storage on server sequence has failed for some reason. - """ - # STAGE 1 - verify if secrets exist locally - try: - logger.info("trying to load secrets from local storage...") - version = self._load_secrets_from_local_file() - # eventually migrate local and remote stored documents from old - # format version - if version < self.RECOVERY_DOC_VERSION: - self._store_secrets() - self._upload_crypto_secrets() - logger.info("found secrets in local storage") - return - - except NoStorageSecret: - logger.info("could not find secrets in local storage") - - # STAGE 2 - there are no secrets in local storage and this is the - # first time we are running soledad with the specified - # secrets_path. Try to fetch encrypted secrets from - # server. - try: - logger.info('trying to fetch secrets from remote storage...') - version = self._download_crypto_secrets() - self._store_secrets() - # eventually migrate remote stored document from old format - # version - if version < self.RECOVERY_DOC_VERSION: - self._upload_crypto_secrets() - logger.info('found secrets in remote storage.') - return - except NoStorageSecret: - logger.info("could not find secrets in remote storage.") - - # STAGE 3 - there are no secrets in server also, so we want to - # generate the secrets and store them in the remote - # db. - logger.info("generating secrets...") - self._gen_crypto_secrets() - logger.info("uploading secrets...") - self._upload_crypto_secrets() - - def _has_secret(self): - """ - Return whether there is a storage secret available for use or not. - - :return: Whether there's a storage secret for symmetric encryption. - :rtype: bool - """ - return self.storage_secret is not None - - def _maybe_set_active_secret(self, active_secret): - """ - If no secret_id is already set, choose the passed active secret, or - just choose first secret available if none. - """ - if not self._secret_id: - if not active_secret: - active_secret = self._secrets.items()[0][0] - self.set_secret_id(active_secret) - - def _load_secrets_from_local_file(self): - """ - Load storage secrets from local file. - - :return version: The version of the locally stored recovery document. - - :raise NoStorageSecret: Raised if there are no secrets available in - local storage. - """ - # check if secrets file exists and we can read it - if not os.path.isfile(self._secrets_path): - raise NoStorageSecret - - # read storage secrets from file - content = None - with open(self._secrets_path, 'r') as f: - content = json.loads(f.read()) - _, active_secret, version = self._import_recovery_document(content) - - self._maybe_set_active_secret(active_secret) - - return version - - def _download_crypto_secrets(self): - """ - Download crypto secrets. - - :return version: The version of the remotelly stored recovery document. - - :raise NoStorageSecret: Raised if there are no secrets available in - remote storage. - """ - doc = None - if self._shared_db.syncable: - doc = self._get_secrets_from_shared_db() - - if doc is None: - raise NoStorageSecret - - _, active_secret, version = self._import_recovery_document(doc.content) - self._maybe_set_active_secret(active_secret) - - return version - - def _gen_crypto_secrets(self): - """ - Generate the crypto secrets. - """ - logger.info('no cryptographic secrets found, creating new secrets...') - secret_id = self._gen_secret() - self.set_secret_id(secret_id) - - def _upload_crypto_secrets(self): - """ - Send crypto secrets to shared db. - - :raises BootstrapSequenceError: Raised when unable to store secrets in - shared database. - """ - if self._shared_db.syncable: - try: - self._put_secrets_in_shared_db() - except Exception as ex: - # storing generated secret in shared db failed for - # some reason, so we erase the generated secret and - # raise. - try: - os.unlink(self._secrets_path) - except OSError as e: - if e.errno != errno.ENOENT: - # no such file or directory - logger.exception(e) - logger.exception(ex) - raise BootstrapSequenceError( - 'Could not store generated secret in the shared ' - 'database, bailing out...') - - # - # Shared DB related methods - # - - def _shared_db_doc_id(self): - """ - Calculate the doc_id of the document in the shared db that stores key - material. - - :return: the hash - :rtype: str - """ - return sha256( - '%s%s' % - (self._passphrase_as_string(), self._uuid)).hexdigest() - - def _export_recovery_document(self, cipher=None): - """ - Export the storage secrets. - - Current format of recovery document has the following structure: - - { - 'storage_secrets': { - '': { - 'cipher': 'aes256', - 'length': , - 'secret': '', - }, - }, - 'active_secret': '', - 'version': '', - } - - Note that multiple storage secrets might be stored in one recovery - document. - - :param cipher: (Optional) The ciper to use. Defaults to AES256 - :type cipher: str - - :return: The recovery document. - :rtype: dict - """ - # encrypt secrets - encrypted_secrets = {} - for secret_id in self._secrets: - encrypted_secrets[secret_id] = self._encrypt_storage_secret( - self._secrets[secret_id], doc_cipher=cipher) - # create the recovery document - data = { - self.STORAGE_SECRETS_KEY: encrypted_secrets, - self.ACTIVE_SECRET_KEY: self._secret_id, - self.RECOVERY_DOC_VERSION_KEY: self.RECOVERY_DOC_VERSION, - } - return data - - def _import_recovery_document(self, data): - """ - Import storage secrets for symmetric encryption from a recovery - document. - - Note that this method does not store the imported data on disk. For - that, use C{self._store_secrets()}. - - :param data: The recovery document. - :type data: dict - - :return: A tuple containing the number of imported secrets, the - secret_id of the last active secret, and the recovery - document format version. - :rtype: (int, str, int) - """ - soledad_assert(self.STORAGE_SECRETS_KEY in data) - version = data.get(self.RECOVERY_DOC_VERSION_KEY, 1) - meth = getattr(self, '_import_recovery_document_version_%d' % version) - secret_count, active_secret = meth(data) - return secret_count, active_secret, version - - def _import_recovery_document_version_1(self, data): - """ - Import storage secrets for symmetric encryption from a recovery - document with format version 1. - - Version 1 of recovery document has the following structure: - - { - 'storage_secrets': { - '': { - 'cipher': 'aes256', - 'length': , - 'secret': '', - }, - }, - 'active_secret': '', - 'version': '', - } - - :param data: The recovery document. - :type data: dict - - :return: A tuple containing the number of imported secrets, the - secret_id of the last active secret, and the recovery - document format version. - :rtype: (int, str, int) - """ - # include secrets in the secret pool. - secret_count = 0 - secrets = data[self.STORAGE_SECRETS_KEY].items() - active_secret = None - # XXX remove check for existence of key (included for backwards - # compatibility) - if self.ACTIVE_SECRET_KEY in data: - active_secret = data[self.ACTIVE_SECRET_KEY] - for secret_id, encrypted_secret in secrets: - if secret_id not in self._secrets: - try: - self._secrets[secret_id] = \ - self._decrypt_storage_secret_version_1( - encrypted_secret) - secret_count += 1 - except SecretsException as e: - logger.error("failed to decrypt storage secret: %s" - % str(e)) - raise e - return secret_count, active_secret - - def _get_secrets_from_shared_db(self): - """ - Retrieve the document with encrypted key material from the shared - database. - - :return: a document with encrypted key material in its contents - :rtype: document.SoledadDocument - """ - user_data = self._get_user_data() - events.emit_async(events.SOLEDAD_DOWNLOADING_KEYS, user_data) - db = self._shared_db - if not db: - logger.warn('no shared db found') - return - doc = db.get_doc(self._shared_db_doc_id()) - user_data = {'userid': self._userid, 'uuid': self._uuid} - events.emit_async(events.SOLEDAD_DONE_DOWNLOADING_KEYS, user_data) - return doc - - def _put_secrets_in_shared_db(self): - """ - Assert local keys are the same as shared db's ones. - - Try to fetch keys from shared recovery database. If they already exist - in the remote db, assert that that data is the same as local data. - Otherwise, upload keys to shared recovery database. - """ - soledad_assert( - self._has_secret(), - 'Tried to send keys to server but they don\'t exist in local ' - 'storage.') - # try to get secrets doc from server, otherwise create it - doc = self._get_secrets_from_shared_db() - if doc is None: - doc = document.SoledadDocument( - doc_id=self._shared_db_doc_id()) - # fill doc with encrypted secrets - doc.content = self._export_recovery_document() - # upload secrets to server - user_data = self._get_user_data() - events.emit_async(events.SOLEDAD_UPLOADING_KEYS, user_data) - db = self._shared_db - if not db: - logger.warn('no shared db found') - return - db.put_doc(doc) - events.emit_async(events.SOLEDAD_DONE_UPLOADING_KEYS, user_data) - - # - # Management of secret for symmetric encryption. - # - - def _decrypt_storage_secret_version_1(self, encrypted_secret_dict): - """ - Decrypt the storage secret. - - Storage secret is encrypted before being stored. This method decrypts - and returns the decrypted storage secret. - - Version 1 of storage secret format has the following structure: - - '': { - 'cipher': 'aes256', - 'length': , - 'secret': '', - }, - - :param encrypted_secret_dict: The encrypted storage secret. - :type encrypted_secret_dict: dict - - :return: The decrypted storage secret. - :rtype: str - - :raise SecretsException: Raised in case the decryption of the storage - secret fails for some reason. - """ - # calculate the encryption key - if encrypted_secret_dict[self.KDF_KEY] != self.KDF_SCRYPT: - raise SecretsException("Unknown KDF in stored secret.") - key = scrypt.hash( - self._passphrase_as_string(), - # the salt is stored base64 encoded - binascii.a2b_base64( - encrypted_secret_dict[self.KDF_SALT_KEY]), - buflen=32, # we need a key with 256 bits (32 bytes). - ) - if encrypted_secret_dict[self.KDF_LENGTH_KEY] != len(key): - raise SecretsException("Wrong length of decryption key.") - supported_ciphers = [self.CIPHER_AES256, self.CIPHER_AES256_GCM] - doc_cipher = encrypted_secret_dict[self.CIPHER_KEY] - if doc_cipher not in supported_ciphers: - raise SecretsException("Unknown cipher in stored secret.") - # recover the initial value and ciphertext - iv, ciphertext = encrypted_secret_dict[self.SECRET_KEY].split( - self.SEPARATOR, 1) - ciphertext = binascii.a2b_base64(ciphertext) - try: - decrypted_secret = _crypto.decrypt_sym( - ciphertext, key, iv, doc_cipher) - except Exception as e: - logger.error(e) - raise SecretsException("Unable to decrypt secret.") - if encrypted_secret_dict[self.LENGTH_KEY] != len(decrypted_secret): - raise SecretsException("Wrong length of decrypted secret.") - return decrypted_secret - - def _encrypt_storage_secret(self, decrypted_secret, doc_cipher=None): - """ - Encrypt the storage secret. - - An encrypted secret has the following structure: - - { - '': { - 'kdf': 'scrypt', - 'kdf_salt': '' - 'kdf_length': - 'cipher': 'aes256', - 'length': , - 'secret': '', - } - } - - :param decrypted_secret: The decrypted storage secret. - :type decrypted_secret: str - :param cipher: (Optional) The ciper to use. Defaults to AES256 - :type cipher: str - - :return: The encrypted storage secret. - :rtype: dict - """ - # generate random salt - salt = os.urandom(self.SALT_LENGTH) - # get a 256-bit key - key = scrypt.hash(self._passphrase_as_string(), salt, buflen=32) - doc_cipher = doc_cipher or self.CIPHER_AES256_GCM - iv, ciphertext = _crypto.encrypt_sym(decrypted_secret, key, doc_cipher) - ciphertext = binascii.b2a_base64(ciphertext) - encrypted_secret_dict = { - # leap.soledad.crypto submodule uses AES256 for symmetric - # encryption. - self.KDF_KEY: self.KDF_SCRYPT, - self.KDF_SALT_KEY: binascii.b2a_base64(salt), - self.KDF_LENGTH_KEY: len(key), - self.CIPHER_KEY: doc_cipher, - self.LENGTH_KEY: len(decrypted_secret), - self.SECRET_KEY: self.SEPARATOR.join([str(iv), ciphertext]) - } - return encrypted_secret_dict - - @property - def storage_secret(self): - """ - Return the storage secret. - - :return: The decrypted storage secret. - :rtype: str - """ - return self._secrets.get(self._secret_id) - - def set_secret_id(self, secret_id): - """ - Define the id of the storage secret to be used. - - This method will also replace the secret in the crypto object. - - :param secret_id: The id of the storage secret to be used. - :type secret_id: str - """ - self._secret_id = secret_id - - def _gen_secret(self): - """ - Generate a secret for symmetric encryption and store in a local - encrypted file. - - This method emits the following events.signals: - - * SOLEDAD_CREATING_KEYS - * SOLEDAD_DONE_CREATING_KEYS - - :return: The id of the generated secret. - :rtype: str - """ - user_data = self._get_user_data() - events.emit_async(events.SOLEDAD_CREATING_KEYS, user_data) - # generate random secret - secret = os.urandom(self.GEN_SECRET_LENGTH) - secret_id = sha256(secret).hexdigest() - self._secrets[secret_id] = secret - self._store_secrets() - events.emit_async(events.SOLEDAD_DONE_CREATING_KEYS, user_data) - return secret_id - - def _store_secrets(self): - """ - Store secrets in C{Soledad.STORAGE_SECRETS_FILE_PATH}. - """ - with open(self._secrets_path, 'w') as f: - f.write( - json.dumps( - self._export_recovery_document())) - - def change_passphrase(self, new_passphrase): - """ - Change the passphrase that encrypts the storage secret. - - :param new_passphrase: The new passphrase. - :type new_passphrase: unicode - - :raise NoStorageSecret: Raised if there's no storage secret available. - """ - # TODO: maybe we want to add more checks to guarantee passphrase is - # reasonable? - soledad_assert_type(new_passphrase, unicode) - if len(new_passphrase) < self.MINIMUM_PASSPHRASE_LENGTH: - raise PassphraseTooShort( - 'Passphrase must be at least %d characters long!' % - self.MINIMUM_PASSPHRASE_LENGTH) - # ensure there's a secret for which the passphrase will be changed. - if not self._has_secret(): - raise NoStorageSecret() - self._passphrase = new_passphrase - self._store_secrets() - self._put_secrets_in_shared_db() - - # - # Setters and getters - # - - @property - def secret_id(self): - return self._secret_id - - def _get_secrets_path(self): - return self._secrets_path - - def _set_secrets_path(self, secrets_path): - self._secrets_path = secrets_path - - secrets_path = property( - _get_secrets_path, - _set_secrets_path, - doc='The path for the file containing the encrypted symmetric secret.') - - @property - def passphrase(self): - """ - Return the passphrase for locking and unlocking encryption secrets for - local and remote storage. - """ - return self._passphrase - - def _passphrase_as_string(self): - return self._passphrase.encode('utf-8') - - # - # remote storage secret - # - - @property - def remote_storage_secret(self): - """ - Return the secret for remote storage. - """ - key_start = 0 - key_end = self.REMOTE_STORAGE_SECRET_LENGTH - return self.storage_secret[key_start:key_end] - - # - # local storage key - # - - def _get_local_storage_secret(self): - """ - Return the local storage secret. - - :return: The local storage secret. - :rtype: str - """ - secret_len = self.REMOTE_STORAGE_SECRET_LENGTH - lsecret_len = self.LOCAL_STORAGE_SECRET_LENGTH - pwd_start = secret_len + self.SALT_LENGTH - pwd_end = secret_len + lsecret_len - return self.storage_secret[pwd_start:pwd_end] - - def _get_local_storage_salt(self): - """ - Return the local storage salt. - - :return: The local storage salt. - :rtype: str - """ - salt_start = self.REMOTE_STORAGE_SECRET_LENGTH - salt_end = salt_start + self.SALT_LENGTH - return self.storage_secret[salt_start:salt_end] - - def get_local_storage_key(self): - """ - Return the local storage key derived from the local storage secret. - - :return: The key for protecting the local database. - :rtype: str - """ - return scrypt.hash( - password=self._get_local_storage_secret(), - salt=self._get_local_storage_salt(), - buflen=32, # we need a key with 256 bits (32 bytes) - ) - - # - # sync db key - # - - def _get_sync_db_salt(self): - """ - Return the salt for sync db. - """ - salt_start = self.LOCAL_STORAGE_SECRET_LENGTH \ - + self.REMOTE_STORAGE_SECRET_LENGTH - salt_end = salt_start + self.SALT_LENGTH - return self.storage_secret[salt_start:salt_end] - - def get_sync_db_key(self): - """ - Return the key for protecting the sync database. - - :return: The key for protecting the sync database. - :rtype: str - """ - return scrypt.hash( - password=self._get_local_storage_secret(), - salt=self._get_sync_db_salt(), - buflen=32, # we need a key with 256 bits (32 bytes) - ) - - def _get_user_data(self): - return {'uuid': self._uuid, 'userid': self._userid} diff --git a/client/src/leap/soledad/client/shared_db.py b/client/src/leap/soledad/client/shared_db.py index d43db045..52b226b9 100644 --- a/client/src/leap/soledad/client/shared_db.py +++ b/client/src/leap/soledad/client/shared_db.py @@ -17,7 +17,7 @@ """ A shared database for storing/retrieving encrypted key material. """ -from leap.soledad.common.l2db.remote import http_database +from leap.soledad.common.l2db.remote.http_database import HTTPDatabase from leap.soledad.client.auth import TokenBasedAuth @@ -47,7 +47,7 @@ class ImproperlyConfiguredError(Exception): """ -class SoledadSharedDatabase(http_database.HTTPDatabase, TokenBasedAuth): +class SoledadSharedDatabase(HTTPDatabase, TokenBasedAuth): """ This is a shared recovery database that enables users to store their encryption secrets in the server and retrieve them afterwards. @@ -134,20 +134,16 @@ class SoledadSharedDatabase(http_database.HTTPDatabase, TokenBasedAuth): """ raise Unauthorized("Can't delete shared database.") - def __init__(self, url, uuid, document_factory=None, creds=None): + def __init__(self, url, document_factory=None, creds=None): """ Initialize database with auth token and encryption powers. :param url: URL of the remote database. :type url: str - :param uuid: The user's unique id. - :type uuid: str :param document_factory: A factory for U1BD documents. :type document_factory: u1db.Document :param creds: A tuple containing the authentication method and credentials. :type creds: tuple """ - http_database.HTTPDatabase.__init__(self, url, document_factory, - creds) - self._uuid = uuid + HTTPDatabase.__init__(self, url, document_factory, creds) -- cgit v1.2.3 From 8d9782c689daa14aca495d7b6b2598b2743c4e7c Mon Sep 17 00:00:00 2001 From: drebs Date: Sat, 24 Dec 2016 14:05:15 -0200 Subject: [bug] use derived key for local storage --- .../src/leap/soledad/client/_secrets/__init__.py | 36 ++++++++++++++++------ client/src/leap/soledad/client/_secrets/crypto.py | 6 ++-- client/src/leap/soledad/client/api.py | 4 +-- 3 files changed, 32 insertions(+), 14 deletions(-) (limited to 'client/src') diff --git a/client/src/leap/soledad/client/_secrets/__init__.py b/client/src/leap/soledad/client/_secrets/__init__.py index f9da8423..42fe5a2d 100644 --- a/client/src/leap/soledad/client/_secrets/__init__.py +++ b/client/src/leap/soledad/client/_secrets/__init__.py @@ -16,6 +16,7 @@ # along with this program. If not, see . import os +import scrypt from collections import namedtuple @@ -34,10 +35,12 @@ SecretLength = namedtuple('SecretLength', 'name length') class Secrets(object): + # remote secret is used + lengths = { - 'remote': 512, - 'salt': 64, - 'local': 448, + 'remote_secret': 512, # remote_secret is used to encrypt remote data. + 'local_salt': 64, # local_salt is used in conjunction with + 'local_secret': 448, # local_secret to derive a local_key for storage } def __init__(self, uuid, passphrase, url, local_path, creds, userid, @@ -119,14 +122,29 @@ class Secrets(object): self.storage.save_local(encrypted) self.storage.save_remote(encrypted) + # + # secrets + # + + @property + def remote_secret(self): + return self._secrets.get('remote_secret') + @property - def remote(self): - return self._secrets.get('remote') + def local_salt(self): + return self._secrets.get('local_salt') @property - def salt(self): - return self._secrets.get('salt') + def local_secret(self): + return self._secrets.get('local_secret') @property - def local(self): - return self._secrets.get('local') + def local_key(self): + # local storage key is scrypt-derived from `local_secret` and + # `local_salt` above + secret = scrypt.hash( + password=self.local_secret, + salt=self.local_salt, + buflen=32, # we need a key with 256 bits (32 bytes) + ) + return secret diff --git a/client/src/leap/soledad/client/_secrets/crypto.py b/client/src/leap/soledad/client/_secrets/crypto.py index 76e80222..88f32507 100644 --- a/client/src/leap/soledad/client/_secrets/crypto.py +++ b/client/src/leap/soledad/client/_secrets/crypto.py @@ -92,9 +92,9 @@ class SecretsCrypto(object): plaintext = self._decrypt( key, iv, ciphertext, encrypted, ENC_METHOD.aes_256_ctr) secrets = { - 'remote': plaintext[0:512], - 'salt': plaintext[512:576], - 'local': plaintext[576:1024], + 'remote_secret': plaintext[0:512], + 'local_salt': plaintext[512:576], + 'local_secret': plaintext[576:1024], } return secrets diff --git a/client/src/leap/soledad/client/api.py b/client/src/leap/soledad/client/api.py index 2e1d1cd3..54cbcd9d 100644 --- a/client/src/leap/soledad/client/api.py +++ b/client/src/leap/soledad/client/api.py @@ -196,7 +196,7 @@ class Soledad(object): self._init_secrets(shared_db=shared_db) - self._crypto = SoledadCrypto(self._secrets.remote) + self._crypto = SoledadCrypto(self._secrets.remote_secret) try: # initialize database access, trap any problems so we can shutdown @@ -268,7 +268,7 @@ class Soledad(object): """ tohex = binascii.b2a_hex # sqlcipher only accepts the hex version - key = tohex(self._secrets.local) + key = tohex(self._secrets.local_key) opts = sqlcipher.SQLCipherOptions( self._local_db_path, key, -- cgit v1.2.3 From 1aa4fa6861626bd9ece719513698ef494cc23610 Mon Sep 17 00:00:00 2001 From: drebs Date: Fri, 20 Jan 2017 19:43:25 -0200 Subject: [bug] several fixes for secrets refactor - store ENC_METHOD value instead of string in secrets file - allow for migration of not-activated secrets - allow migration of 'aes256' and ENC_METHOD secrets cipher --- client/src/leap/soledad/client/_secrets/crypto.py | 24 ++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) (limited to 'client/src') diff --git a/client/src/leap/soledad/client/_secrets/crypto.py b/client/src/leap/soledad/client/_secrets/crypto.py index 88f32507..dc80cf0b 100644 --- a/client/src/leap/soledad/client/_secrets/crypto.py +++ b/client/src/leap/soledad/client/_secrets/crypto.py @@ -59,7 +59,7 @@ class SecretsCrypto(object): 'kdf': 'scrypt', 'kdf_salt': binascii.b2a_base64(salt), 'kdf_length': len(key), - 'cipher': 'aes_256_gcm', + 'cipher': ENC_METHOD.aes_256_gcm, 'length': len(plaintext), 'iv': str(iv), 'secrets': binascii.b2a_base64(ciphertext), @@ -80,17 +80,26 @@ class SecretsCrypto(object): raise SecretsError(e) def _decrypt_v1(self, data): - secret_id = data['active_secret'] + # get encrypted secret from dictionary + secret_id = data['storage_secrets'].keys().pop() encrypted = data['storage_secrets'][secret_id] - soledad_assert(encrypted['cipher'] == 'aes256') + # assert that we know how to decrypt the secret + soledad_assert('cipher' in encrypted) + cipher = encrypted['cipher'] + if cipher == 'aes256': + cipher = ENC_METHOD.aes_256_ctr + soledad_assert(cipher in ENC_METHOD) + + # decrypt salt = binascii.a2b_base64(encrypted['kdf_salt']) key = self._get_key(salt) separator = ':' iv, ciphertext = encrypted['secret'].split(separator, 1) ciphertext = binascii.a2b_base64(ciphertext) - plaintext = self._decrypt( - key, iv, ciphertext, encrypted, ENC_METHOD.aes_256_ctr) + plaintext = self._decrypt(key, iv, ciphertext, encrypted, cipher) + + # create secrets dictionary secrets = { 'remote_secret': plaintext[0:512], 'local_salt': plaintext[512:576], @@ -99,14 +108,15 @@ class SecretsCrypto(object): return secrets def _decrypt_v2(self, encrypted): - soledad_assert(encrypted['cipher'] == 'aes_256_gcm') + cipher = encrypted['cipher'] + soledad_assert(cipher in ENC_METHOD) salt = binascii.a2b_base64(encrypted['kdf_salt']) key = self._get_key(salt) iv = encrypted['iv'] ciphertext = binascii.a2b_base64(encrypted['secrets']) plaintext = self._decrypt( - key, iv, ciphertext, encrypted, ENC_METHOD.aes_256_gcm) + key, iv, ciphertext, encrypted, cipher) encoded = json.loads(plaintext) secrets = {} for name, value in encoded.iteritems(): -- cgit v1.2.3 From 3787a645993ea36bbecebc850296de4b0fdd3620 Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 26 Jan 2017 10:52:20 -0200 Subject: [doc] improve comment for client secrets file migration function --- client/src/leap/soledad/client/_secrets/crypto.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'client/src') diff --git a/client/src/leap/soledad/client/_secrets/crypto.py b/client/src/leap/soledad/client/_secrets/crypto.py index dc80cf0b..02d7dc02 100644 --- a/client/src/leap/soledad/client/_secrets/crypto.py +++ b/client/src/leap/soledad/client/_secrets/crypto.py @@ -80,7 +80,11 @@ class SecretsCrypto(object): raise SecretsError(e) def _decrypt_v1(self, data): - # get encrypted secret from dictionary + # get encrypted secret from dictionary: the old format allowed for + # storage of more than one secret, but this feature was never used and + # soledad has been using only one secret so far. As there is a corner + # case where the old 'active_secret' key might not be set, we just + # ignore it and pop the only secret found in the 'storage_secrets' key. secret_id = data['storage_secrets'].keys().pop() encrypted = data['storage_secrets'][secret_id] -- cgit v1.2.3 From 994eaa79b274c3c37af42cb343c41b5dec6e8d19 Mon Sep 17 00:00:00 2001 From: drebs Date: Sun, 18 Dec 2016 21:27:02 -0200 Subject: [feat] use cookies in the client syncer --- .../leap/soledad/client/http_target/__init__.py | 26 +++++++++++++++++----- client/src/leap/soledad/client/sqlcipher.py | 9 ++++++-- 2 files changed, 28 insertions(+), 7 deletions(-) (limited to 'client/src') diff --git a/client/src/leap/soledad/client/http_target/__init__.py b/client/src/leap/soledad/client/http_target/__init__.py index 0e250bf1..590ae8f6 100644 --- a/client/src/leap/soledad/client/http_target/__init__.py +++ b/client/src/leap/soledad/client/http_target/__init__.py @@ -24,10 +24,14 @@ after receiving. import os -from leap.soledad.common.log import getLogger -from leap.common.certs import get_compatible_ssl_context_factory +from cookielib import CookieJar + from twisted.web.client import Agent +from twisted.web.client import CookieAgent from twisted.internet import reactor + +from leap.common.certs import get_compatible_ssl_context_factory +from leap.soledad.common.log import getLogger from leap.soledad.client.http_target.send import HTTPDocSender from leap.soledad.client.http_target.api import SyncTargetAPI from leap.soledad.client.http_target.fetch import HTTPDocFetcher @@ -43,6 +47,14 @@ if os.environ.get('SOLEDAD_STATS'): DO_STATS = True +def newCookieAgent(cert_file): + _factory = get_compatible_ssl_context_factory(cert_file) + _agent = Agent(reactor, _factory) + _cookieJar = CookieJar() + agent = CookieAgent(_agent, _cookieJar) + return agent + + class SoledadHTTPSyncTarget(SyncTargetAPI, HTTPDocSender, HTTPDocFetcher): """ @@ -54,7 +66,8 @@ class SoledadHTTPSyncTarget(SyncTargetAPI, HTTPDocSender, HTTPDocFetcher): the parsed documents that the remote send us, before being decrypted and written to the main database. """ - def __init__(self, url, source_replica_uid, creds, crypto, cert_file): + def __init__(self, url, source_replica_uid, creds, crypto, cert_file, + agent=None): """ Initialize the sync target. @@ -72,6 +85,8 @@ class SoledadHTTPSyncTarget(SyncTargetAPI, HTTPDocSender, HTTPDocFetcher): the SSL certificate used by the remote soledad server. :type cert_file: str + :param agent: an http agent + :type agent: twisted.web.client.Agent """ if url.endswith("/"): url = url[:-1] @@ -86,8 +101,9 @@ class SoledadHTTPSyncTarget(SyncTargetAPI, HTTPDocSender, HTTPDocFetcher): self._insert_doc_cb = None # Twisted default Agent with our own ssl context factory - self._http = Agent(reactor, - get_compatible_ssl_context_factory(cert_file)) + if not agent: + agent = newCookieAgent(cert_file) + self._http = agent if DO_STATS: self.sync_exchange_phase = [0] diff --git a/client/src/leap/soledad/client/sqlcipher.py b/client/src/leap/soledad/client/sqlcipher.py index c9a9444e..9b352bbf 100644 --- a/client/src/leap/soledad/client/sqlcipher.py +++ b/client/src/leap/soledad/client/sqlcipher.py @@ -59,6 +59,7 @@ from leap.soledad.common.l2db.backends import sqlite_backend from leap.soledad.common.errors import DatabaseAccessError from leap.soledad.client.http_target import SoledadHTTPSyncTarget +from leap.soledad.client.http_target import newCookieAgent from leap.soledad.client.sync import SoledadSynchronizer from leap.soledad.client import pragmas @@ -397,7 +398,6 @@ class SQLCipherU1DBSync(SQLCipherDatabase): ENCRYPT_LOOP_PERIOD = 1 def __init__(self, opts, soledad_crypto, replica_uid, cert_file): - self._opts = opts self._path = opts.path self._crypto = soledad_crypto @@ -407,6 +407,10 @@ class SQLCipherU1DBSync(SQLCipherDatabase): # storage for the documents received during a sync self.received_docs = [] + # setup an http agent capable of storing cookies, so we can use + # server's session persistence feature + self._agent = newCookieAgent(cert_file) + self.running = False self._db_handle = None @@ -491,7 +495,8 @@ class SQLCipherU1DBSync(SQLCipherDatabase): self._replica_uid, creds=creds, crypto=self._crypto, - cert_file=self._cert_file)) + cert_file=self._cert_file, + agent=self._agent)) # # Symmetric encryption of syncing docs -- cgit v1.2.3 From 4fce575de20effc9c4d934028f8ccdfbd97932e1 Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 29 Dec 2016 09:28:10 -0200 Subject: [refactor] remove twisted session persistence The need for token caching in server is a matter of debate, as is the ideal way to do it. Twisted sessions store the session id in a cookie and use that session id to persist. It is not clear if that implementation is needed, works with future features (as multiple soledad servers) or represents a security problem in some way. Because of these, this commit removes it for now. The feature is left in git history so we can bring it back later if needed. --- .../src/leap/soledad/client/http_target/__init__.py | 21 +++------------------ client/src/leap/soledad/client/sqlcipher.py | 8 +------- 2 files changed, 4 insertions(+), 25 deletions(-) (limited to 'client/src') diff --git a/client/src/leap/soledad/client/http_target/__init__.py b/client/src/leap/soledad/client/http_target/__init__.py index 590ae8f6..b67d03f6 100644 --- a/client/src/leap/soledad/client/http_target/__init__.py +++ b/client/src/leap/soledad/client/http_target/__init__.py @@ -24,10 +24,7 @@ after receiving. import os -from cookielib import CookieJar - from twisted.web.client import Agent -from twisted.web.client import CookieAgent from twisted.internet import reactor from leap.common.certs import get_compatible_ssl_context_factory @@ -47,14 +44,6 @@ if os.environ.get('SOLEDAD_STATS'): DO_STATS = True -def newCookieAgent(cert_file): - _factory = get_compatible_ssl_context_factory(cert_file) - _agent = Agent(reactor, _factory) - _cookieJar = CookieJar() - agent = CookieAgent(_agent, _cookieJar) - return agent - - class SoledadHTTPSyncTarget(SyncTargetAPI, HTTPDocSender, HTTPDocFetcher): """ @@ -66,8 +55,7 @@ class SoledadHTTPSyncTarget(SyncTargetAPI, HTTPDocSender, HTTPDocFetcher): the parsed documents that the remote send us, before being decrypted and written to the main database. """ - def __init__(self, url, source_replica_uid, creds, crypto, cert_file, - agent=None): + def __init__(self, url, source_replica_uid, creds, crypto, cert_file): """ Initialize the sync target. @@ -85,8 +73,6 @@ class SoledadHTTPSyncTarget(SyncTargetAPI, HTTPDocSender, HTTPDocFetcher): the SSL certificate used by the remote soledad server. :type cert_file: str - :param agent: an http agent - :type agent: twisted.web.client.Agent """ if url.endswith("/"): url = url[:-1] @@ -101,9 +87,8 @@ class SoledadHTTPSyncTarget(SyncTargetAPI, HTTPDocSender, HTTPDocFetcher): self._insert_doc_cb = None # Twisted default Agent with our own ssl context factory - if not agent: - agent = newCookieAgent(cert_file) - self._http = agent + factory = get_compatible_ssl_context_factory(cert_file) + self._http = Agent(reactor, factory) if DO_STATS: self.sync_exchange_phase = [0] diff --git a/client/src/leap/soledad/client/sqlcipher.py b/client/src/leap/soledad/client/sqlcipher.py index 9b352bbf..a3e45228 100644 --- a/client/src/leap/soledad/client/sqlcipher.py +++ b/client/src/leap/soledad/client/sqlcipher.py @@ -59,7 +59,6 @@ from leap.soledad.common.l2db.backends import sqlite_backend from leap.soledad.common.errors import DatabaseAccessError from leap.soledad.client.http_target import SoledadHTTPSyncTarget -from leap.soledad.client.http_target import newCookieAgent from leap.soledad.client.sync import SoledadSynchronizer from leap.soledad.client import pragmas @@ -407,10 +406,6 @@ class SQLCipherU1DBSync(SQLCipherDatabase): # storage for the documents received during a sync self.received_docs = [] - # setup an http agent capable of storing cookies, so we can use - # server's session persistence feature - self._agent = newCookieAgent(cert_file) - self.running = False self._db_handle = None @@ -495,8 +490,7 @@ class SQLCipherU1DBSync(SQLCipherDatabase): self._replica_uid, creds=creds, crypto=self._crypto, - cert_file=self._cert_file, - agent=self._agent)) + cert_file=self._cert_file)) # # Symmetric encryption of syncing docs -- cgit v1.2.3 From e6ed77ce83a37dd4fffb8ac560ae34fbee8acc22 Mon Sep 17 00:00:00 2001 From: Victor Shyba Date: Wed, 15 Feb 2017 17:43:17 -0300 Subject: [tests] add tests for preamble encoding --- client/src/leap/soledad/client/_crypto.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'client/src') diff --git a/client/src/leap/soledad/client/_crypto.py b/client/src/leap/soledad/client/_crypto.py index 4bbdd044..9f403cc9 100644 --- a/client/src/leap/soledad/client/_crypto.py +++ b/client/src/leap/soledad/client/_crypto.py @@ -192,7 +192,7 @@ class BlobEncryptor(object): sym_key = _get_sym_key_for_doc(doc_info.doc_id, secret) self._aes = AESWriter(sym_key) - self._aes.authenticate(self._make_preamble()) + self._aes.authenticate(self._encode_preamble()) @property def iv(self): @@ -214,7 +214,7 @@ class BlobEncryptor(object): d.addCallback(lambda _: self._end_crypto_stream()) return d - def _make_preamble(self): + def _encode_preamble(self): current_time = int(time.time()) return PACMAN.pack( -- cgit v1.2.3 From d2ef605af73a592ea21c5bae005f53f483e310a6 Mon Sep 17 00:00:00 2001 From: Victor Shyba Date: Thu, 16 Feb 2017 04:48:58 -0300 Subject: [feature] add doc size to preamble That's necessary for blobs-io. Current code includes backwards compatibility branching and tests, which shall be removed on next releases. --- client/src/leap/soledad/client/_crypto.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) (limited to 'client/src') diff --git a/client/src/leap/soledad/client/_crypto.py b/client/src/leap/soledad/client/_crypto.py index 9f403cc9..c4c6d336 100644 --- a/client/src/leap/soledad/client/_crypto.py +++ b/client/src/leap/soledad/client/_crypto.py @@ -49,7 +49,8 @@ SECRET_LENGTH = 64 CRYPTO_BACKEND = MultiBackend([OpenSSLBackend()]) -PACMAN = struct.Struct('2sbbQ16s255p255p') +PACMAN = struct.Struct('2sbbQ16s255p255pQ') +LEGACY_PACMAN = struct.Struct('2sbbQ16s255p255p') BLOB_SIGNATURE_MAGIC = '\x13\x37' @@ -188,10 +189,13 @@ class BlobEncryptor(object): self.doc_id = doc_info.doc_id self.rev = doc_info.rev self._content_fd = content_fd + content_fd.seek(0, os.SEEK_END) + self._content_size = content_fd.tell() + content_fd.seek(0) self._producer = FileBodyProducer(content_fd, readSize=2**16) - sym_key = _get_sym_key_for_doc(doc_info.doc_id, secret) - self._aes = AESWriter(sym_key) + self.sym_key = _get_sym_key_for_doc(doc_info.doc_id, secret) + self._aes = AESWriter(self.sym_key) self._aes.authenticate(self._encode_preamble()) @property @@ -224,7 +228,8 @@ class BlobEncryptor(object): current_time, self.iv, str(self.doc_id), - str(self.rev)) + str(self.rev), + self._content_size) def _end_crypto_stream(self): preamble, encrypted = self._aes.end() @@ -271,14 +276,17 @@ class BlobDecryptor(object): raise InvalidBlob ciphertext_fd.close() - if len(preamble) != PACMAN.size: - raise InvalidBlob - try: - unpacked_data = PACMAN.unpack(preamble) - magic, sch, meth, ts, iv, doc_id, rev = unpacked_data - except struct.error: - raise InvalidBlob + if len(preamble) == LEGACY_PACMAN.size: + unpacked_data = LEGACY_PACMAN.unpack(preamble) + magic, sch, meth, ts, iv, doc_id, rev = unpacked_data + elif len(preamble) == PACMAN.size: + unpacked_data = PACMAN.unpack(preamble) + magic, sch, meth, ts, iv, doc_id, rev, doc_size = unpacked_data + else: + raise InvalidBlob("Unexpected preamble size %d", len(preamble)) + except struct.error, e: + raise InvalidBlob(e) if magic != BLOB_SIGNATURE_MAGIC: raise InvalidBlob -- cgit v1.2.3 From ac6d87e83f91ed61b160e7cdd968f4a6f3d68f34 Mon Sep 17 00:00:00 2001 From: Victor Shyba Date: Thu, 16 Feb 2017 17:18:06 -0300 Subject: [style] add deprecation warning on legacy decoder --- client/src/leap/soledad/client/_crypto.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'client/src') diff --git a/client/src/leap/soledad/client/_crypto.py b/client/src/leap/soledad/client/_crypto.py index c4c6d336..f91084a4 100644 --- a/client/src/leap/soledad/client/_crypto.py +++ b/client/src/leap/soledad/client/_crypto.py @@ -22,6 +22,7 @@ Cryptographic operations for the soledad client import binascii import base64 import hashlib +import warnings import hmac import os import re @@ -278,6 +279,10 @@ class BlobDecryptor(object): try: if len(preamble) == LEGACY_PACMAN.size: + warnings.warn("Decrypting a legacy document without size. " + + "This will be deprecated in 0.12. Doc was: " + + "doc_id: %s rev: %s" % (self.doc_id, self.rev), + Warning) unpacked_data = LEGACY_PACMAN.unpack(preamble) magic, sch, meth, ts, iv, doc_id, rev = unpacked_data elif len(preamble) == PACMAN.size: -- cgit v1.2.3 From 1ab75f97d77f9883c4d83d43513d9d47dfb397d9 Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 23 Feb 2017 18:06:07 -0300 Subject: [refactor] remove creds from client api --- client/src/leap/soledad/client/api.py | 44 ++++++++--------------------------- 1 file changed, 10 insertions(+), 34 deletions(-) (limited to 'client/src') diff --git a/client/src/leap/soledad/client/api.py b/client/src/leap/soledad/client/api.py index 54cbcd9d..6a508d05 100644 --- a/client/src/leap/soledad/client/api.py +++ b/client/src/leap/soledad/client/api.py @@ -187,7 +187,7 @@ class Soledad(object): global SOLEDAD_CERT SOLEDAD_CERT = cert_file - self._set_token(auth_token) + self.set_token(auth_token) self._init_config_with_defaults() self._init_working_dirs() @@ -249,9 +249,10 @@ class Soledad(object): """ Initialize Soledad secrets. """ + creds = {'token': {'uuid': self.uuid, 'token': self.token}} self._secrets = Secrets( self._uuid, self._passphrase, self._server_url, self._secrets_path, - self._creds, self.userid, shared_db=shared_db) + creds, self.userid, shared_db=shared_db) def _init_u1db_sqlcipher_backend(self): """ @@ -675,9 +676,8 @@ class Soledad(object): sync_url = urlparse.urljoin(self._server_url, 'user-%s' % self.uuid) if not self._dbsyncer: return - d = self._dbsyncer.sync( - sync_url, - creds=self._creds) + creds = {'token': {'uuid': self.uuid, 'token': self.token}} + d = self._dbsyncer.sync(sync_url, creds=creds) def _sync_callback(local_gen): self._last_received_docs = docs = self._dbsyncer.received_docs @@ -734,37 +734,13 @@ class Soledad(object): """ return self.sync_lock.locked - def _set_token(self, token): - """ - Set the authentication token for remote database access. - - Internally, this builds the credentials dictionary with the following - format: + def set_token(self, token): + self._token = token - { - 'token': { - 'uuid': '' - 'token': '' - } - } - - :param token: The authentication token. - :type token: str - """ - self._creds = { - 'token': { - 'uuid': self.uuid, - 'token': token, - } - } - - def _get_token(self): - """ - Return current token from credentials dictionary. - """ - return self._creds['token']['token'] + def get_token(self): + return self._token - token = property(_get_token, _set_token, doc='The authentication Token.') + token = property(get_token, set_token, doc='The authentication Token.') # # ISecretsStorage -- cgit v1.2.3 From 459c5efd59e2ebaa21e5c461c275d866a43534e8 Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 23 Feb 2017 18:11:49 -0300 Subject: [refactor] add EmitMixin for a cleaner emitting experience --- client/src/leap/soledad/client/_secrets/__init__.py | 7 ++++--- client/src/leap/soledad/client/_secrets/storage.py | 12 ++---------- client/src/leap/soledad/client/_secrets/util.py | 7 +++++++ 3 files changed, 13 insertions(+), 13 deletions(-) (limited to 'client/src') diff --git a/client/src/leap/soledad/client/_secrets/__init__.py b/client/src/leap/soledad/client/_secrets/__init__.py index 42fe5a2d..26294364 100644 --- a/client/src/leap/soledad/client/_secrets/__init__.py +++ b/client/src/leap/soledad/client/_secrets/__init__.py @@ -24,7 +24,7 @@ from leap.soledad.common.log import getLogger from leap.soledad.client._secrets.storage import SecretsStorage from leap.soledad.client._secrets.crypto import SecretsCrypto -from leap.soledad.client._secrets.util import emit +from leap.soledad.client._secrets.util import emit, EmitMixin logger = getLogger(__name__) @@ -33,7 +33,7 @@ logger = getLogger(__name__) SecretLength = namedtuple('SecretLength', 'name length') -class Secrets(object): +class Secrets(EmitMixin): # remote secret is used @@ -45,9 +45,10 @@ class Secrets(object): def __init__(self, uuid, passphrase, url, local_path, creds, userid, shared_db=None): + self._uuid = uuid self._passphrase = passphrase + self._userid = userid self._secrets = {} - self._user_data = {'uuid': uuid, 'userid': userid} self.crypto = SecretsCrypto(self.get_passphrase) self.storage = SecretsStorage( uuid, self.get_passphrase, url, local_path, creds, userid, diff --git a/client/src/leap/soledad/client/_secrets/storage.py b/client/src/leap/soledad/client/_secrets/storage.py index da3aa9d7..730926ee 100644 --- a/client/src/leap/soledad/client/_secrets/storage.py +++ b/client/src/leap/soledad/client/_secrets/storage.py @@ -25,13 +25,13 @@ from leap.soledad.common.log import getLogger from leap.soledad.common.document import SoledadDocument from leap.soledad.client.shared_db import SoledadSharedDatabase -from leap.soledad.client._secrets.util import emit +from leap.soledad.client._secrets.util import emit, EmitMixin logger = getLogger(__name__) -class SecretsStorage(object): +class SecretsStorage(EmitMixin): def __init__(self, uuid, get_pass, url, local_path, creds, userid, shared_db=None): @@ -43,14 +43,6 @@ class SecretsStorage(object): self._shared_db = shared_db or self._init_shared_db(url, creds) self.__remote_doc = None - # - # properties - # - - @property - def _user_data(self): - return {'uuid': self._uuid, 'userid': self._userid} - # # local storage # diff --git a/client/src/leap/soledad/client/_secrets/util.py b/client/src/leap/soledad/client/_secrets/util.py index f75b2bb6..0dcdd3af 100644 --- a/client/src/leap/soledad/client/_secrets/util.py +++ b/client/src/leap/soledad/client/_secrets/util.py @@ -23,6 +23,13 @@ class SecretsError(Exception): pass +class EmitMixin(object): + + @property + def _user_data(self): + return {'uuid': self._uuid, 'userid': self._userid} + + def emit(verb): def _decorator(method): def _decorated(self, *args, **kwargs): -- cgit v1.2.3 From 2422561268aa0214d8521ef5d2f456318391711d Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 23 Feb 2017 18:18:39 -0300 Subject: [refactor] use get_token in client secrets api --- client/src/leap/soledad/client/_secrets/__init__.py | 4 ++-- client/src/leap/soledad/client/_secrets/storage.py | 9 +++++++-- client/src/leap/soledad/client/api.py | 3 +-- 3 files changed, 10 insertions(+), 6 deletions(-) (limited to 'client/src') diff --git a/client/src/leap/soledad/client/_secrets/__init__.py b/client/src/leap/soledad/client/_secrets/__init__.py index 26294364..69c9141f 100644 --- a/client/src/leap/soledad/client/_secrets/__init__.py +++ b/client/src/leap/soledad/client/_secrets/__init__.py @@ -43,7 +43,7 @@ class Secrets(EmitMixin): 'local_secret': 448, # local_secret to derive a local_key for storage } - def __init__(self, uuid, passphrase, url, local_path, creds, userid, + def __init__(self, uuid, passphrase, url, local_path, get_token, userid, shared_db=None): self._uuid = uuid self._passphrase = passphrase @@ -51,7 +51,7 @@ class Secrets(EmitMixin): self._secrets = {} self.crypto = SecretsCrypto(self.get_passphrase) self.storage = SecretsStorage( - uuid, self.get_passphrase, url, local_path, creds, userid, + uuid, self.get_passphrase, url, local_path, get_token, userid, shared_db=shared_db) self._bootstrap() diff --git a/client/src/leap/soledad/client/_secrets/storage.py b/client/src/leap/soledad/client/_secrets/storage.py index 730926ee..5fde8988 100644 --- a/client/src/leap/soledad/client/_secrets/storage.py +++ b/client/src/leap/soledad/client/_secrets/storage.py @@ -33,16 +33,21 @@ logger = getLogger(__name__) class SecretsStorage(EmitMixin): - def __init__(self, uuid, get_pass, url, local_path, creds, userid, + def __init__(self, uuid, get_pass, url, local_path, get_token, userid, shared_db=None): self._uuid = uuid self._get_pass = get_pass self._local_path = local_path + self._get_token = get_token self._userid = userid - self._shared_db = shared_db or self._init_shared_db(url, creds) + self._shared_db = shared_db or self._init_shared_db(url, self._creds) self.__remote_doc = None + @property + def _creds(self): + return {'token': {'uuid': self._uuid, 'token': self._get_token()}} + # # local storage # diff --git a/client/src/leap/soledad/client/api.py b/client/src/leap/soledad/client/api.py index 6a508d05..e84ba848 100644 --- a/client/src/leap/soledad/client/api.py +++ b/client/src/leap/soledad/client/api.py @@ -249,10 +249,9 @@ class Soledad(object): """ Initialize Soledad secrets. """ - creds = {'token': {'uuid': self.uuid, 'token': self.token}} self._secrets = Secrets( self._uuid, self._passphrase, self._server_url, self._secrets_path, - creds, self.userid, shared_db=shared_db) + self.get_token, self.userid, shared_db=shared_db) def _init_u1db_sqlcipher_backend(self): """ -- cgit v1.2.3 From 75e4d4e7dafe4472ee55fa0159eeb8270df2dd49 Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 23 Feb 2017 18:40:02 -0300 Subject: [feat] avoid client sync if no token is set --- client/src/leap/soledad/client/_secrets/__init__.py | 2 ++ client/src/leap/soledad/client/api.py | 15 ++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) (limited to 'client/src') diff --git a/client/src/leap/soledad/client/_secrets/__init__.py b/client/src/leap/soledad/client/_secrets/__init__.py index 69c9141f..78cfae5e 100644 --- a/client/src/leap/soledad/client/_secrets/__init__.py +++ b/client/src/leap/soledad/client/_secrets/__init__.py @@ -81,6 +81,8 @@ class Secrets(EmitMixin): self._secrets = secrets if encrypted['version'] < self.crypto.VERSION or force_storage: + # TODO: what should we do if it's the first run and remote save + # fails? self.storage.save_local(encrypted) self.storage.save_remote(encrypted) diff --git a/client/src/leap/soledad/client/api.py b/client/src/leap/soledad/client/api.py index e84ba848..07eb8e9e 100644 --- a/client/src/leap/soledad/client/api.py +++ b/client/src/leap/soledad/client/api.py @@ -39,7 +39,7 @@ from itertools import chain from StringIO import StringIO from collections import defaultdict -from twisted.internet.defer import DeferredLock, returnValue, inlineCallbacks +from twisted.internet import defer from zope.interface import implements from leap.common.config import get_path_prefix @@ -124,7 +124,7 @@ class Soledad(object): same database replica. The dictionary indexes are the paths to each local db, so we guarantee that only one sync happens for a local db at a time. """ - _sync_lock = defaultdict(DeferredLock) + _sync_lock = defaultdict(defer.DeferredLock) def __init__(self, uuid, passphrase, secrets_path, local_db_path, server_url, cert_file, shared_db=None, @@ -660,6 +660,11 @@ class Soledad(object): generation before the synchronization was performed. :rtype: twisted.internet.defer.Deferred """ + # bypass sync if there's no token set + if not self.token: + generation = self._dbsyncer.get_generation() + return defer.succeed(generation) + d = self.sync_lock.run( self._sync) return d @@ -788,7 +793,7 @@ class Soledad(object): # Service authentication # - @inlineCallbacks + @defer.inlineCallbacks def get_or_create_service_token(self, service): """ Return the stored token for a given service, or generates and stores a @@ -803,11 +808,11 @@ class Soledad(object): docs = yield self._get_token_for_service(service) if docs: doc = docs[0] - returnValue(doc.content['token']) + defer.returnValue(doc.content['token']) else: token = str(uuid.uuid4()).replace('-', '')[-24:] yield self._set_token_for_service(service, token) - returnValue(token) + defer.returnValue(token) def _get_token_for_service(self, service): return self.get_from_index('by-servicetoken', 'servicetoken', service) -- cgit v1.2.3 From 67fc01d1b055b4a0ff3742ba4c3d32bbef3e1b87 Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 23 Feb 2017 19:01:17 -0300 Subject: [feature] add offline status to soledad client api --- client/src/leap/soledad/client/api.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) (limited to 'client/src') diff --git a/client/src/leap/soledad/client/api.py b/client/src/leap/soledad/client/api.py index 07eb8e9e..16569ec2 100644 --- a/client/src/leap/soledad/client/api.py +++ b/client/src/leap/soledad/client/api.py @@ -128,7 +128,7 @@ class Soledad(object): def __init__(self, uuid, passphrase, secrets_path, local_db_path, server_url, cert_file, shared_db=None, - auth_token=None): + auth_token=None, offline=False): """ Initialize configuration, cryptographic keys and dbs. @@ -165,10 +165,11 @@ class Soledad(object): Authorization token for accessing remote databases. :type auth_token: str - :param syncable: - If set to ``False``, this database will not attempt to synchronize - with remote replicas (default is ``True``) - :type syncable: bool + :param offline: + If set to ``True``, this database will not attempt to save/load + secrets to/from server or synchronize with remote replicas (default + is ``False``) + :type offline: bool :raise BootstrapSequenceError: Raised when the secret initialization sequence (i.e. retrieval @@ -182,6 +183,7 @@ class Soledad(object): self._server_url = server_url self._secrets_path = None self._dbsyncer = None + self._offline = offline # configure SSL certificate global SOLEDAD_CERT @@ -212,6 +214,14 @@ class Soledad(object): self._dbpool.close() raise + def _get_offline(self): + return self._offline + + def _set_offline(self, offline): + self._offline = offline + + offline = property(_get_offline, _set_offline) + # # initialization/destruction methods # @@ -660,8 +670,8 @@ class Soledad(object): generation before the synchronization was performed. :rtype: twisted.internet.defer.Deferred """ - # bypass sync if there's no token set - if not self.token: + # maybe bypass sync + if self.offline or not self.token: generation = self._dbsyncer.get_generation() return defer.succeed(generation) -- cgit v1.2.3 From a2e29d8bebbe7809dc10195982a205ed3709459c Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 23 Feb 2017 19:08:02 -0300 Subject: [refactor] remove syncable property from shared db --- client/src/leap/soledad/client/shared_db.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) (limited to 'client/src') diff --git a/client/src/leap/soledad/client/shared_db.py b/client/src/leap/soledad/client/shared_db.py index 52b226b9..b429d2cb 100644 --- a/client/src/leap/soledad/client/shared_db.py +++ b/client/src/leap/soledad/client/shared_db.py @@ -55,10 +55,6 @@ class SoledadSharedDatabase(HTTPDatabase, TokenBasedAuth): # TODO: prevent client from messing with the shared DB. # TODO: define and document API. - # If syncable is False, the database will not attempt to sync against - # a remote replica. Default is True. - syncable = True - # # Token auth methods. # @@ -95,7 +91,7 @@ class SoledadSharedDatabase(HTTPDatabase, TokenBasedAuth): # @staticmethod - def open_database(url, uuid, creds=None, syncable=True): + def open_database(url, uuid, creds=None): """ Open a Soledad shared database. @@ -106,20 +102,11 @@ class SoledadSharedDatabase(HTTPDatabase, TokenBasedAuth): :param creds: A tuple containing the authentication method and credentials. :type creds: tuple - :param syncable: - If syncable is False, the database will not attempt to sync against - a remote replica. - :type syncable: bool :return: The shared database in the given url. :rtype: SoledadSharedDatabase """ - # XXX fix below, doesn't work with tests. - # if syncable and not url.startswith('https://'): - # raise ImproperlyConfiguredError( - # "Remote soledad server must be an https URI") db = SoledadSharedDatabase(url, uuid, creds=creds) - db.syncable = syncable return db @staticmethod -- cgit v1.2.3 From b433c1ed736f5d4c19da4cdb21108a02459ca7fd Mon Sep 17 00:00:00 2001 From: drebs Date: Sat, 25 Feb 2017 08:53:38 -0300 Subject: [refactor] pass soledad object to client secrets api In order to be able to change passphrase, token and offline status of soledad from the bitmask client api, the secrets api also has to be able to use up-to-date values when encrypting/decrypting secrets and uploading/downloading them to the server. This commit makes public some soledad attributes that were previously "private" (i.e. used to start with "_" and were not meant to be accessed from outside), and passes the whole soledad object to the client secrets api. This makes the code cleaner and also allows for always getting newest values of soledad attributes. --- .../src/leap/soledad/client/_secrets/__init__.py | 25 +++------- client/src/leap/soledad/client/_secrets/crypto.py | 7 +-- client/src/leap/soledad/client/_secrets/storage.py | 40 +++++++-------- client/src/leap/soledad/client/_secrets/util.py | 4 +- client/src/leap/soledad/client/api.py | 57 ++++++---------------- 5 files changed, 48 insertions(+), 85 deletions(-) (limited to 'client/src') diff --git a/client/src/leap/soledad/client/_secrets/__init__.py b/client/src/leap/soledad/client/_secrets/__init__.py index 78cfae5e..43541e16 100644 --- a/client/src/leap/soledad/client/_secrets/__init__.py +++ b/client/src/leap/soledad/client/_secrets/__init__.py @@ -43,16 +43,11 @@ class Secrets(EmitMixin): 'local_secret': 448, # local_secret to derive a local_key for storage } - def __init__(self, uuid, passphrase, url, local_path, get_token, userid, - shared_db=None): - self._uuid = uuid - self._passphrase = passphrase - self._userid = userid + def __init__(self, soledad): + self._soledad = soledad self._secrets = {} - self.crypto = SecretsCrypto(self.get_passphrase) - self.storage = SecretsStorage( - uuid, self.get_passphrase, url, local_path, get_token, userid, - shared_db=shared_db) + self.crypto = SecretsCrypto(soledad) + self.storage = SecretsStorage(soledad) self._bootstrap() # @@ -83,6 +78,8 @@ class Secrets(EmitMixin): if encrypted['version'] < self.crypto.VERSION or force_storage: # TODO: what should we do if it's the first run and remote save # fails? + # TODO: we have to actually update the encrypted version before + # saving, we are currently not doing it. self.storage.save_local(encrypted) self.storage.save_remote(encrypted) @@ -112,15 +109,7 @@ class Secrets(EmitMixin): data = {'secret': encrypted, 'version': 2} return data - def get_passphrase(self): - return self._passphrase.encode('utf-8') - - @property - def passphrase(self): - return self.get_passphrase() - - def change_passphrase(self, new_passphrase): - self._passphrase = new_passphrase + def store_secrets(self): encrypted = self.crypto.encrypt(self._secrets) self.storage.save_local(encrypted) self.storage.save_remote(encrypted) diff --git a/client/src/leap/soledad/client/_secrets/crypto.py b/client/src/leap/soledad/client/_secrets/crypto.py index 02d7dc02..fa7aaca0 100644 --- a/client/src/leap/soledad/client/_secrets/crypto.py +++ b/client/src/leap/soledad/client/_secrets/crypto.py @@ -34,11 +34,12 @@ class SecretsCrypto(object): VERSION = 2 - def __init__(self, get_pass): - self._get_pass = get_pass + def __init__(self, soledad): + self._soledad = soledad def _get_key(self, salt): - key = scrypt.hash(self._get_pass(), salt, buflen=32) + passphrase = self._soledad.passphrase.encode('utf8') + key = scrypt.hash(passphrase, salt, buflen=32) return key # diff --git a/client/src/leap/soledad/client/_secrets/storage.py b/client/src/leap/soledad/client/_secrets/storage.py index 5fde8988..bb74dba3 100644 --- a/client/src/leap/soledad/client/_secrets/storage.py +++ b/client/src/leap/soledad/client/_secrets/storage.py @@ -33,29 +33,26 @@ logger = getLogger(__name__) class SecretsStorage(EmitMixin): - def __init__(self, uuid, get_pass, url, local_path, get_token, userid, - shared_db=None): - self._uuid = uuid - self._get_pass = get_pass - self._local_path = local_path - self._get_token = get_token - self._userid = userid - - self._shared_db = shared_db or self._init_shared_db(url, self._creds) + def __init__(self, soledad): + self._soledad = soledad + self._shared_db = self._soledad.shared_db or self._init_shared_db() self.__remote_doc = None @property def _creds(self): - return {'token': {'uuid': self._uuid, 'token': self._get_token()}} + uuid = self._soledad.uuid + token = self._soledad.token + return {'token': {'uuid': uuid, 'token': token}} # # local storage # def load_local(self): - logger.info("trying to load secrets from disk: %s" % self._local_path) + path = self._soledad.secrets_path + logger.info("trying to load secrets from disk: %s" % path) try: - with open(self._local_path, 'r') as f: + with open(path, 'r') as f: encrypted = json.loads(f.read()) logger.info("secrets loaded successfully from disk") return encrypted @@ -64,23 +61,26 @@ class SecretsStorage(EmitMixin): return None def save_local(self, encrypted): + path = self._soledad.secrets_path json_data = json.dumps(encrypted) - with open(self._local_path, 'w') as f: + with open(path, 'w') as f: f.write(json_data) # # remote storage # - def _init_shared_db(self, url, creds): - url = urlparse.urljoin(url, SHARED_DB_NAME) - db = SoledadSharedDatabase.open_database( - url, self._uuid, creds=creds) - self._shared_db = db + def _init_shared_db(self): + url = urlparse.urljoin(self._soledad.server_url, SHARED_DB_NAME) + uuid = self._soledad.uuid + creds = self._creds + db = SoledadSharedDatabase.open_database(url, uuid, creds) + return db def _remote_doc_id(self): - passphrase = self._get_pass() - text = '%s%s' % (passphrase, self._uuid) + passphrase = self._soledad.passphrase.encode('utf8') + uuid = self._soledad.uuid + text = '%s%s' % (passphrase, uuid) digest = sha256(text).hexdigest() return digest diff --git a/client/src/leap/soledad/client/_secrets/util.py b/client/src/leap/soledad/client/_secrets/util.py index 0dcdd3af..75418518 100644 --- a/client/src/leap/soledad/client/_secrets/util.py +++ b/client/src/leap/soledad/client/_secrets/util.py @@ -27,7 +27,9 @@ class EmitMixin(object): @property def _user_data(self): - return {'uuid': self._uuid, 'userid': self._userid} + uuid = self._soledad.uuid + userid = self._soledad.userid + return {'uuid': uuid, 'userid': userid} def emit(verb): diff --git a/client/src/leap/soledad/client/api.py b/client/src/leap/soledad/client/api.py index 16569ec2..4be38cf1 100644 --- a/client/src/leap/soledad/client/api.py +++ b/client/src/leap/soledad/client/api.py @@ -177,27 +177,25 @@ class Soledad(object): some reason. """ # store config params - self._uuid = uuid - self._passphrase = passphrase + self.uuid = uuid + self.passphrase = passphrase + self.secrets_path = secrets_path self._local_db_path = local_db_path - self._server_url = server_url - self._secrets_path = None + self.server_url = server_url + self.shared_db = shared_db + self.token = auth_token + self.offline = offline + self._dbsyncer = None - self._offline = offline # configure SSL certificate global SOLEDAD_CERT SOLEDAD_CERT = cert_file - self.set_token(auth_token) - self._init_config_with_defaults() self._init_working_dirs() - self._secrets_path = secrets_path - - self._init_secrets(shared_db=shared_db) - + self._secrets = Secrets(self) self._crypto = SoledadCrypto(self._secrets.remote_secret) try: @@ -214,14 +212,6 @@ class Soledad(object): self._dbpool.close() raise - def _get_offline(self): - return self._offline - - def _set_offline(self, offline): - self._offline = offline - - offline = property(_get_offline, _set_offline) - # # initialization/destruction methods # @@ -230,7 +220,7 @@ class Soledad(object): """ Initialize configuration using default values for missing params. """ - soledad_assert_type(self._passphrase, unicode) + soledad_assert_type(self.passphrase, unicode) def initialize(attr, val): return ((getattr(self, attr, None) is None) and @@ -241,7 +231,7 @@ class Soledad(object): initialize("_local_db_path", os.path.join( self.default_prefix, self.local_db_file_name)) # initialize server_url - soledad_assert(self._server_url is not None, + soledad_assert(self.server_url is not None, 'Missing URL for Soledad server.') def _init_working_dirs(self): @@ -255,14 +245,6 @@ class Soledad(object): for path in paths: create_path_if_not_exists(path) - def _init_secrets(self, shared_db=None): - """ - Initialize Soledad secrets. - """ - self._secrets = Secrets( - self._uuid, self._passphrase, self._server_url, self._secrets_path, - self.get_token, self.userid, shared_db=shared_db) - def _init_u1db_sqlcipher_backend(self): """ Initialize the U1DB SQLCipher database for local storage. @@ -646,10 +628,6 @@ class Soledad(object): def local_db_path(self): return self._local_db_path - @property - def uuid(self): - return self._uuid - @property def userid(self): return self.uuid @@ -687,7 +665,7 @@ class Soledad(object): generation before the synchronization was performed. :rtype: twisted.internet.defer.Deferred """ - sync_url = urlparse.urljoin(self._server_url, 'user-%s' % self.uuid) + sync_url = urlparse.urljoin(self.server_url, 'user-%s' % self.uuid) if not self._dbsyncer: return creds = {'token': {'uuid': self.uuid, 'token': self.token}} @@ -748,14 +726,6 @@ class Soledad(object): """ return self.sync_lock.locked - def set_token(self, token): - self._token = token - - def get_token(self): - return self._token - - token = property(get_token, set_token, doc='The authentication Token.') - # # ISecretsStorage # @@ -779,7 +749,8 @@ class Soledad(object): :raise NoStorageSecret: Raised if there's no storage secret available. """ - self._secrets.change_passphrase(new_passphrase) + self.passphrase = new_passphrase + self._secrets.store_secrets() # # Raw SQLCIPHER Queries -- cgit v1.2.3 From 425bfe42ca3758cfa4cda4589ebb42530313850b Mon Sep 17 00:00:00 2001 From: drebs Date: Sat, 25 Feb 2017 09:11:44 -0300 Subject: [doc] improve doc and rename EmitMixin to UserDataMixin --- client/src/leap/soledad/client/_secrets/__init__.py | 4 ++-- client/src/leap/soledad/client/_secrets/storage.py | 4 ++-- client/src/leap/soledad/client/_secrets/util.py | 10 +++++++++- 3 files changed, 13 insertions(+), 5 deletions(-) (limited to 'client/src') diff --git a/client/src/leap/soledad/client/_secrets/__init__.py b/client/src/leap/soledad/client/_secrets/__init__.py index 43541e16..79b6844a 100644 --- a/client/src/leap/soledad/client/_secrets/__init__.py +++ b/client/src/leap/soledad/client/_secrets/__init__.py @@ -24,7 +24,7 @@ from leap.soledad.common.log import getLogger from leap.soledad.client._secrets.storage import SecretsStorage from leap.soledad.client._secrets.crypto import SecretsCrypto -from leap.soledad.client._secrets.util import emit, EmitMixin +from leap.soledad.client._secrets.util import emit, UserDataMixin logger = getLogger(__name__) @@ -33,7 +33,7 @@ logger = getLogger(__name__) SecretLength = namedtuple('SecretLength', 'name length') -class Secrets(EmitMixin): +class Secrets(UserDataMixin): # remote secret is used diff --git a/client/src/leap/soledad/client/_secrets/storage.py b/client/src/leap/soledad/client/_secrets/storage.py index bb74dba3..89b44266 100644 --- a/client/src/leap/soledad/client/_secrets/storage.py +++ b/client/src/leap/soledad/client/_secrets/storage.py @@ -25,13 +25,13 @@ from leap.soledad.common.log import getLogger from leap.soledad.common.document import SoledadDocument from leap.soledad.client.shared_db import SoledadSharedDatabase -from leap.soledad.client._secrets.util import emit, EmitMixin +from leap.soledad.client._secrets.util import emit, UserDataMixin logger = getLogger(__name__) -class SecretsStorage(EmitMixin): +class SecretsStorage(UserDataMixin): def __init__(self, soledad): self._soledad = soledad diff --git a/client/src/leap/soledad/client/_secrets/util.py b/client/src/leap/soledad/client/_secrets/util.py index 75418518..6401889b 100644 --- a/client/src/leap/soledad/client/_secrets/util.py +++ b/client/src/leap/soledad/client/_secrets/util.py @@ -23,12 +23,20 @@ class SecretsError(Exception): pass -class EmitMixin(object): +class UserDataMixin(object): + """ + When emitting an event, we have to pass a dictionary containing user data. + This class only defines a property so we don't have to define it in + multiple places. + """ @property def _user_data(self): uuid = self._soledad.uuid userid = self._soledad.userid + # TODO: seems that uuid and userid hold the same value! We should check + # whether we should pass something different or if the events api + # really needs two different values. return {'uuid': uuid, 'userid': userid} -- cgit v1.2.3 From a0029b3c7beb8682c8aa3691a5d67003168c3e07 Mon Sep 17 00:00:00 2001 From: drebs Date: Sat, 25 Feb 2017 09:42:38 -0300 Subject: [refactor] improve secret bootstrap code and doc --- .../src/leap/soledad/client/_secrets/__init__.py | 50 ++++++++++------------ 1 file changed, 22 insertions(+), 28 deletions(-) (limited to 'client/src') diff --git a/client/src/leap/soledad/client/_secrets/__init__.py b/client/src/leap/soledad/client/_secrets/__init__.py index 79b6844a..f8da792d 100644 --- a/client/src/leap/soledad/client/_secrets/__init__.py +++ b/client/src/leap/soledad/client/_secrets/__init__.py @@ -55,33 +55,29 @@ class Secrets(UserDataMixin): # def _bootstrap(self): - force_storage = False - # attempt to load secrets from local storage encrypted = self.storage.load_local() - # if not found, attempt to load secrets from remote storage if not encrypted: + # we have not found a secret stored locally, so this is a first run + # of soledad for this user in this device. It is mandatory that we + # check if there's a secret stored in server. encrypted = self.storage.load_remote() - if not encrypted: - # if not found, generate new secrets - secrets = self._generate() - encrypted = self.crypto.encrypt(secrets) - force_storage = True + if encrypted: + # we found a secret either in local or in remote storage, so we + # have to decrypt it. + self._secrets = self.crypto.decrypt(encrypted) + if encrypted['version'] < self.crypto.VERSION: + # there is a format version for secret storage that is newer + # than the one we found (either in local or remote storage), so + # we re-encrypt and store with the newest version. + self.store_secrets() else: - # decrypt secrets found either in local or remote storage - secrets = self.crypto.decrypt(encrypted) - - self._secrets = secrets - - if encrypted['version'] < self.crypto.VERSION or force_storage: - # TODO: what should we do if it's the first run and remote save - # fails? - # TODO: we have to actually update the encrypted version before - # saving, we are currently not doing it. - self.storage.save_local(encrypted) - self.storage.save_remote(encrypted) + # we have *not* found a secret neither in local nor in remote + # storage, so we have to generate a new one, and store it. + self._secrets = self._generate() + self.store_secrets() # # generation @@ -101,15 +97,13 @@ class Secrets(UserDataMixin): # crypto # - def _encrypt(self): - # encrypt secrets - secrets = self._secrets - encrypted = self.crypto.encrypt(secrets) - # create the recovery document - data = {'secret': encrypted, 'version': 2} - return data - def store_secrets(self): + # TODO: we have to improve the logic here, as we want to make sure that + # whatever is stored locally should only be used after remote storage + # is successful. Otherwise, this soledad could start encrypting with a + # secret while another soledad in another device could start encrypting + # with another secret, which would lead to decryption failures during + # sync. encrypted = self.crypto.encrypt(self._secrets) self.storage.save_local(encrypted) self.storage.save_remote(encrypted) -- cgit v1.2.3 From 87b65c731bb32bb9f0953d23b750ac8e8fda9eab Mon Sep 17 00:00:00 2001 From: drebs Date: Sat, 25 Feb 2017 18:06:03 -0300 Subject: [bug] remove unused named tuple from client secrets --- client/src/leap/soledad/client/_secrets/__init__.py | 7 ------- 1 file changed, 7 deletions(-) (limited to 'client/src') diff --git a/client/src/leap/soledad/client/_secrets/__init__.py b/client/src/leap/soledad/client/_secrets/__init__.py index f8da792d..bb8e9086 100644 --- a/client/src/leap/soledad/client/_secrets/__init__.py +++ b/client/src/leap/soledad/client/_secrets/__init__.py @@ -18,8 +18,6 @@ import os import scrypt -from collections import namedtuple - from leap.soledad.common.log import getLogger from leap.soledad.client._secrets.storage import SecretsStorage @@ -30,13 +28,8 @@ from leap.soledad.client._secrets.util import emit, UserDataMixin logger = getLogger(__name__) -SecretLength = namedtuple('SecretLength', 'name length') - - class Secrets(UserDataMixin): - # remote secret is used - lengths = { 'remote_secret': 512, # remote_secret is used to encrypt remote data. 'local_salt': 64, # local_salt is used in conjunction with -- cgit v1.2.3 From a96801e7f3f4e6aeeb08355f7bac4f47b2454dac Mon Sep 17 00:00:00 2001 From: drebs Date: Sat, 25 Feb 2017 18:17:18 -0300 Subject: [bug] save client secret downloaded from remote storage After refactor, the client secret bootstrap logic was flawed, and remote secret was not being saved properly. This commit fixed that and tries to improve the bootstrap code to make it more clear. --- .../src/leap/soledad/client/_secrets/__init__.py | 32 +++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) (limited to 'client/src') diff --git a/client/src/leap/soledad/client/_secrets/__init__.py b/client/src/leap/soledad/client/_secrets/__init__.py index bb8e9086..b6c81cda 100644 --- a/client/src/leap/soledad/client/_secrets/__init__.py +++ b/client/src/leap/soledad/client/_secrets/__init__.py @@ -48,29 +48,29 @@ class Secrets(UserDataMixin): # def _bootstrap(self): + # attempt to load secrets from local storage encrypted = self.storage.load_local() - - if not encrypted: - # we have not found a secret stored locally, so this is a first run - # of soledad for this user in this device. It is mandatory that we - # check if there's a secret stored in server. - encrypted = self.storage.load_remote() - if encrypted: - # we found a secret either in local or in remote storage, so we - # have to decrypt it. self._secrets = self.crypto.decrypt(encrypted) + # maybe update the format of storage of local secret. if encrypted['version'] < self.crypto.VERSION: - # there is a format version for secret storage that is newer - # than the one we found (either in local or remote storage), so - # we re-encrypt and store with the newest version. self.store_secrets() - else: - # we have *not* found a secret neither in local nor in remote - # storage, so we have to generate a new one, and store it. - self._secrets = self._generate() + return + + # no secret was found in local storage, so this is a first run of + # soledad for this user in this device. It is mandatory that we check + # if there's a secret stored in server. + encrypted = self.storage.load_remote() + if encrypted: + self._secrets = self.crypto.decrypt(encrypted) self.store_secrets() + return + + # we have *not* found a secret neither in local nor in remote storage, + # so we have to generate a new one, and then store it. + self._secrets = self._generate() + self.store_secrets() # # generation -- cgit v1.2.3 From d92f1d1840d28ee6c2058556b047db75ed737f77 Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 2 Mar 2017 13:58:49 -0300 Subject: [bug] fix shared database initialization --- client/src/leap/soledad/client/_secrets/storage.py | 3 +-- client/src/leap/soledad/client/shared_db.py | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) (limited to 'client/src') diff --git a/client/src/leap/soledad/client/_secrets/storage.py b/client/src/leap/soledad/client/_secrets/storage.py index 89b44266..056c4322 100644 --- a/client/src/leap/soledad/client/_secrets/storage.py +++ b/client/src/leap/soledad/client/_secrets/storage.py @@ -72,9 +72,8 @@ class SecretsStorage(UserDataMixin): def _init_shared_db(self): url = urlparse.urljoin(self._soledad.server_url, SHARED_DB_NAME) - uuid = self._soledad.uuid creds = self._creds - db = SoledadSharedDatabase.open_database(url, uuid, creds) + db = SoledadSharedDatabase.open_database(url, creds) return db def _remote_doc_id(self): diff --git a/client/src/leap/soledad/client/shared_db.py b/client/src/leap/soledad/client/shared_db.py index b429d2cb..4f70c74b 100644 --- a/client/src/leap/soledad/client/shared_db.py +++ b/client/src/leap/soledad/client/shared_db.py @@ -91,14 +91,12 @@ class SoledadSharedDatabase(HTTPDatabase, TokenBasedAuth): # @staticmethod - def open_database(url, uuid, creds=None): + def open_database(url, creds=None): """ Open a Soledad shared database. :param url: URL of the remote database. :type url: str - :param uuid: The user's unique id. - :type uuid: str :param creds: A tuple containing the authentication method and credentials. :type creds: tuple @@ -106,7 +104,7 @@ class SoledadSharedDatabase(HTTPDatabase, TokenBasedAuth): :return: The shared database in the given url. :rtype: SoledadSharedDatabase """ - db = SoledadSharedDatabase(url, uuid, creds=creds) + db = SoledadSharedDatabase(url, creds=creds) return db @staticmethod -- cgit v1.2.3 From 0b60027dacf331c103f764b7b1b5dee8b6123938 Mon Sep 17 00:00:00 2001 From: Victor Shyba Date: Wed, 11 Jan 2017 18:31:34 -0300 Subject: [bug] handle error once Handle it only if self.deferred wasnt called yet, otherwise that's just an out-of-sync call from a scheduled deferred. Since it was already logged, it's ok to ignore. --- client/src/leap/soledad/client/http_target/fetch_protocol.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'client/src') diff --git a/client/src/leap/soledad/client/http_target/fetch_protocol.py b/client/src/leap/soledad/client/http_target/fetch_protocol.py index fa6b1969..c7eabe2b 100644 --- a/client/src/leap/soledad/client/http_target/fetch_protocol.py +++ b/client/src/leap/soledad/client/http_target/fetch_protocol.py @@ -63,6 +63,8 @@ class DocStreamReceiver(ReadBodyProtocol): Deliver the accumulated response bytes to the waiting L{Deferred}, if the response body has been completely received without error. """ + if self.deferred.called: + return try: if reason.check(ResponseDone): self.dataBuffer = self.metadata @@ -125,11 +127,7 @@ class DocStreamReceiver(ReadBodyProtocol): else: d = self._doc_reader( self.current_doc, line.strip() or None, self.total) - d.addErrback(self._error) - - def _error(self, reason): - logger.error(reason) - self.transport.loseConnection() + d.addErrback(self.deferred.errback) def finish(self): """ -- cgit v1.2.3