summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordrebs <drebs@leap.se>2016-12-05 09:10:47 -0200
committerKali Kaneko <kali@leap.se>2017-02-09 17:41:26 +0100
commitff85c2a41fe933d9959fb84a0df2a13a6e199cec (patch)
tree6f52cae13589aa13675f662f40f5040056575ef1
parent8a463796bbaba3979234b0699d140947581421e7 (diff)
[refactor] improve secrets generation and storage code
-rw-r--r--client/src/leap/soledad/client/_secrets/__init__.py132
-rw-r--r--client/src/leap/soledad/client/_secrets/crypto.py123
-rw-r--r--client/src/leap/soledad/client/_secrets/storage.py124
-rw-r--r--client/src/leap/soledad/client/_secrets/util.py46
-rw-r--r--client/src/leap/soledad/client/api.py100
-rw-r--r--client/src/leap/soledad/client/interfaces.py20
-rw-r--r--client/src/leap/soledad/client/secrets.py794
-rw-r--r--client/src/leap/soledad/client/shared_db.py12
-rw-r--r--testing/tests/client/test_aux_methods.py27
-rw-r--r--testing/tests/client/test_crypto.py227
-rw-r--r--testing/tests/client/test_deprecated_crypto.py6
-rw-r--r--testing/tests/client/test_shared_db.py38
-rw-r--r--testing/tests/client/test_signals.py98
-rw-r--r--testing/tests/sync/test_sync_target.py2
14 files changed, 643 insertions, 1106 deletions
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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+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 <http://www.gnu.org/licenses/>.
+
+
+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 <http://www.gnu.org/licenses/>.
-
-
-"""
-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': {
- '<storage_secret id>': {
- 'cipher': 'aes256',
- 'length': <secret length>,
- 'secret': '<encrypted storage_secret>',
- },
- },
- 'active_secret': '<secret_id>',
- 'version': '<recovery document format 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': {
- '<storage_secret id>': {
- 'cipher': 'aes256',
- 'length': <secret length>,
- 'secret': '<encrypted storage_secret>',
- },
- },
- 'active_secret': '<secret_id>',
- 'version': '<recovery document format 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:
-
- '<storage_secret id>': {
- 'cipher': 'aes256',
- 'length': <secret length>,
- 'secret': '<encrypted storage_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:
-
- {
- '<secret_id>': {
- 'kdf': 'scrypt',
- 'kdf_salt': '<b64 repr of salt>'
- 'kdf_length': <key length>
- 'cipher': 'aes256',
- 'length': <secret length>,
- 'secret': '<encrypted b64 repr of storage_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)
diff --git a/testing/tests/client/test_aux_methods.py b/testing/tests/client/test_aux_methods.py
index 9b4a175f..a08f7d36 100644
--- a/testing/tests/client/test_aux_methods.py
+++ b/testing/tests/client/test_aux_methods.py
@@ -19,12 +19,11 @@ Tests for general Soledad functionality.
"""
import os
-from twisted.internet import defer
+from pytest import inlineCallbacks
from leap.soledad.client import Soledad
from leap.soledad.client.adbapi import U1DBConnectionPool
-from leap.soledad.client.secrets import PassphraseTooShort
-from leap.soledad.client.secrets import SecretsException
+from leap.soledad.client._secrets.util import SecretsError
from test_soledad.util import BaseSoledadTest
@@ -34,7 +33,7 @@ class AuxMethodsTestCase(BaseSoledadTest):
def test__init_dirs(self):
sol = self._soledad_instance(prefix='_init_dirs')
local_db_dir = os.path.dirname(sol.local_db_path)
- secrets_path = os.path.dirname(sol.secrets.secrets_path)
+ secrets_path = os.path.dirname(sol.secrets.storage._local_path)
self.assertTrue(os.path.isdir(local_db_dir))
self.assertTrue(os.path.isdir(secrets_path))
@@ -85,14 +84,14 @@ class AuxMethodsTestCase(BaseSoledadTest):
cert_file=None)
self.assertEqual(
os.path.join(self.tempdir, 'value_3'),
- sol.secrets.secrets_path)
+ sol.secrets.storage._local_path)
self.assertEqual(
os.path.join(self.tempdir, 'value_2'),
sol.local_db_path)
self.assertEqual('value_1', sol._server_url)
sol.close()
- @defer.inlineCallbacks
+ @inlineCallbacks
def test_change_passphrase(self):
"""
Test if passphrase can be changed.
@@ -108,7 +107,7 @@ class AuxMethodsTestCase(BaseSoledadTest):
sol.change_passphrase(u'654321')
sol.close()
- with self.assertRaises(SecretsException):
+ with self.assertRaises(SecretsError):
self._soledad_instance(
'leap@leap.se',
passphrase=u'123',
@@ -124,20 +123,6 @@ class AuxMethodsTestCase(BaseSoledadTest):
sol2.close()
- def test_change_passphrase_with_short_passphrase_raises(self):
- """
- Test if attempt to change passphrase passing a short passphrase
- raises.
- """
- sol = self._soledad_instance(
- 'leap@leap.se',
- passphrase=u'123')
- # check that soledad complains about new passphrase length
- self.assertRaises(
- PassphraseTooShort,
- sol.change_passphrase, u'54321')
- sol.close()
-
def test_get_passphrase(self):
"""
Assert passphrase getter works fine.
diff --git a/testing/tests/client/test_crypto.py b/testing/tests/client/test_crypto.py
index 49a61438..379475cd 100644
--- a/testing/tests/client/test_crypto.py
+++ b/testing/tests/client/test_crypto.py
@@ -19,9 +19,9 @@ Tests for cryptographic related stuff.
"""
import binascii
import base64
-import hashlib
import json
import os
+import scrypt
from io import BytesIO
@@ -34,6 +34,7 @@ from cryptography.exceptions import InvalidTag
from leap.soledad.common.document import SoledadDocument
from test_soledad.util import BaseSoledadTest
from leap.soledad.client import _crypto
+from leap.soledad.client._secrets import SecretsCrypto
from twisted.trial import unittest
from twisted.internet import defer
@@ -186,99 +187,145 @@ class BlobTestCase(unittest.TestCase):
yield crypto.decrypt_doc(doc2)
-class RecoveryDocumentTestCase(BaseSoledadTest):
-
- def test_export_recovery_document_raw(self):
- rd = self._soledad.secrets._export_recovery_document()
- secret_id = rd[self._soledad.secrets.STORAGE_SECRETS_KEY].items()[0][0]
- # assert exported secret is the same
- secret = self._soledad.secrets._decrypt_storage_secret_version_1(
- rd[self._soledad.secrets.STORAGE_SECRETS_KEY][secret_id])
- self.assertEqual(secret_id, self._soledad.secrets._secret_id)
- self.assertEqual(secret, self._soledad.secrets._secrets[secret_id])
- # assert recovery document structure
- encrypted_secret = rd[
- self._soledad.secrets.STORAGE_SECRETS_KEY][secret_id]
- self.assertTrue(self._soledad.secrets.CIPHER_KEY in encrypted_secret)
- self.assertEquals(
- _crypto.ENC_METHOD.aes_256_gcm,
- encrypted_secret[self._soledad.secrets.CIPHER_KEY])
- self.assertTrue(self._soledad.secrets.LENGTH_KEY in encrypted_secret)
- self.assertTrue(self._soledad.secrets.SECRET_KEY in encrypted_secret)
-
- def test_import_recovery_document(self, cipher='aes256'):
- rd = self._soledad.secrets._export_recovery_document(cipher)
- s = self._soledad_instance()
- s.secrets._import_recovery_document(rd)
- s.secrets.set_secret_id(self._soledad.secrets._secret_id)
- self.assertEqual(self._soledad.storage_secret,
- s.storage_secret,
- 'Failed settinng secret for symmetric encryption.')
- s.close()
-
- def test_import_GCM_recovery_document(self):
- cipher = self._soledad.secrets.CIPHER_AES256_GCM
- self.test_import_recovery_document(cipher)
-
- def test_import_legacy_CTR_recovery_document(self):
- cipher = self._soledad.secrets.CIPHER_AES256
- self.test_import_recovery_document(cipher)
+class SecretsCryptoTestCase(unittest.TestCase):
+
+ SECRETS = {'remote': 'a' * 512, 'salt': 'b' * 64, 'local': 'c' * 448}
+ ENCRYPTED_V2 = {
+ 'cipher': 'aes_256_gcm',
+ 'length': 1417,
+ 'kdf_salt': '3DCkfecls0GcX2RadA04FAC2cqkI+vpGwwCLwffdRI6vpO5SPxaw/eM0/'
+ 'z3GUADm3If3YCQBldKXNdqHQLsU1Q==\n',
+ 'iv': 'rRwCDw5Rbp5+J3QwjQ46Hw==',
+ 'secrets': 'lxf6yrGDcBr8XFWNDgsCoO2XPGfDJndviL9Y2GmHcSEBWnO2dm2sieuPoq'
+ 'PwSHRSJSrzM4Ezgdaan7X8+ErnuRLUVqbPAqPl8xx8FdCjnid4vFyFYNFI'
+ '/dmo8SQAf8O9vdlVEPZ5Nk2DuWIrh+oPlrSUOmR6XzI0YVdoJDmGWowygU'
+ 'MR0R9Bi9xFGlG135NVcNP8KGdnQDkI0V+U/3qm3tctbo4LRCxxJ60wdi0M'
+ 'DA6iYFI/IMshxI/ZXFHp5/YPk0k2m0i6z71kMVksgjIMMgT5Kmz7WR54na'
+ 'IkWbvNkbYRFR/Hbg9p6Bs7NjJlOLTjnwGJNYPbdyfJXKd1R/S8Mg7ZqsyQ'
+ 'VbBqXHwEN7gYlMZ66D8wu8LOK70mN7LLiSz5J8tXO3rDT1mIIf3IvNhv/j'
+ 'rEZHf1fTFPRp+ZVEt/hJKyPv71ua4p2lgdgNlCs2IsACk9ku/LQwXP6uZr'
+ 'hMJsTvniTQoCVXFYVN/jKo7Pz/+uT5wOXOXtL7smpBE/2r3uoERNM+Zw11'
+ 'SA8UzzMZQMxJQKVNwLmKtwvztN5dxVXhxCUyeLmeQc84VzV7NK0WMUOdfA'
+ '18I0HS6rHLKcdsvrPAdzvGim7tiE8TBdp8ITNQ8yMFNiGNyOVliTSTwQFf'
+ 'sCj6m5nYcjvprNQ8RkeitvicrtI1Ylc8CfFK50xPV77XVmlgvNsfm54msN'
+ 'tV0K5+XwaNgimlh/1m2bVEYj55gO0twVASwRuZj3sSY2z669iuXRk7EPyT'
+ 'jcE2NnfW+lqOQkJ73N7pv73t6OjiEnrKx7VmH94zYlY8ZReVVn4RTZhare'
+ 'D7rqCmGPhsPaCPaAfotfNBBa0w6p6L9ZlNxpIesnMObtyGob1g4Vcu8O6K'
+ '2Q1Ldj95+Q53tJDpx2NLP/5tfAUlbehD3whKwKOz/rGKEfhgE+Nx32RR0y'
+ 'YM4aJ7CYI/U3YH82xqGoa1ufIJbSBt965CVIHSVJt/mYfilhMACV/wBlvL'
+ 'ua08iKpHwc7suMc9DuFS4s/bAzc128L8wtfNvNiP6zhAV+UvfgUmyNKjgl'
+ '0be9Ke2pCNChEQmViNal3zbWNcBrXYQpFpX1lWNkx/OuQalxzSaqmZiOR5'
+ 'eRwqRDZ3R9EpkOFj2ZXS1NlJg1kYXL/ibS8uvjKgJFPrZQzwaKmPNsZyGc'
+ 'CnHupfgC2iRIu97wnvmDxWQ9Cs62NSynr0IYGkTLN5PZU6Z5gd1F7zV6uh'
+ 'oFiHOYidj2EoUj7xnb8GHi5U6PQzaC97nSCR4CFnmcpfv+XcRIWe8nrM8G'
+ 'AVdcUob8pofUlnyGV6GEGlO3mnb7ls5B6lvuZqB/x6UqZiNKwmZvxvS11X'
+ 'AGkhfBGTfFZeqRlLwXvXWnOUOO0KJ8h3gSlc1gFVY+4HCbTOqjUASWw0mV'
+ 'JP+U0anK9wu9B/icLDUZxM/NRdbTQFmcfvABjwdm2GTmwGpQek/H0wN3dO'
+ 'terlTiS7arMUft7A6hkhkmLb0iDfWPWdN50V+XOMpdZtaJSGqwNHokc75p'
+ '3zYll0/ZpxTgmWXariOkKxr6KHHjml89QNQSBE2TJW/YnQ5SrkaHLHKdcy'
+ 'PqQtcXDz/WxKquQfRF+fsvcwqaeqlAWOxUXHU77cBvDGPU5O3uvEIJnHr1'
+ 'kuabqRQbJIV5Uzo4sEW828r2IWQnUd4Om79y+9yp/aT10DusEmvOgS3oSp'
+ '3eYkhvlVULeCQEJoI41t4nGLhHiiK4xBG8yFknuV7nF4k2O+EbyCXsJeeD'
+ 'qlGok91zEhQl1MlQA8ZofRK7bDPcn97USiJMss81s5bwIv4yN8s0QL62Ha'
+ 'vrIYG7C26DV6c0GxULu02H1YOnoPf6JsGC/2+zA+b7a+4O0EP0BXU3FYCb'
+ 'iEDbDpB3dFe63ed+ml2HQjqzOLAtKVXzAQq5UNV4m2zY0/y7gV7qSrM='
+ '\n',
+ 'version': 2,
+ 'kdf': 'scrypt',
+ 'kdf_length': 32
+ }
+
+ ENCRYPTED_V1 = {
+ 'version': 1,
+ 'active_secret': 'secret_id',
+ 'storage_secrets': {
+ 'secret_id': {
+ 'kdf': 'scrypt',
+ 'secret': 'u31ObvxNU8jB0HgMj3TVwQ==:JQwlYq6sAQmHYS3x2CJzObT9h'
+ 'j1iiHthvrMh887qedNCcOfJyCA3jpRkc0vjd2Qk/2HSJ+JxM2F'
+ 'MrPzzx5O34EHlgF2scen34guZRRIf42WpnMy+PrL4cnMlZLgCh'
+ 'H1Jz6wcIMEpU9LQ8OaCShk1/yJ6qcVHOV4DDt3mTF7ttiqI5cp'
+ 'msaVtxxYCcpxFiWSeSCEgr0h4/Ih1qHuM6vk+CQjf/zg1f/7HR'
+ 'imIyNYXit9Fw3YTkxBen1wG3f5L7OAODRTuqnWpkQFOmclx050'
+ 'k0frKRcX6UWhIOWpW2mqJXnvzDtQQVGzqIdSgGTGtUDGQ7Onnc'
+ 'NkUlSnuVC7PkDNNRuwit3pCB9YWBWyPAQgs0kLqoV4YcuSctz6'
+ 'SAf76ozdcK5/SrOzutOfyPag4V3AYKMv6rCKALJ10OnFJ61FL9'
+ 'kd6JZam7WOlEUXyO7Gdgvz+eKiQMTZXbtO2kAKqel513MedPXC'
+ 'dzajUe1U2JaGg86UdiDWoPYOiWxnAPwfNJk+1QuNy5NZ7PaMtF'
+ 'IKT3/Xema2U8mufS0FbvJyK2flP1VUWcCzHKTSqX6+kU7UpoWa'
+ 'hYa7PlO40El+putTQLBmNaEeaWFngO+XB4TReICHSiCdcAb3pw'
+ 'sabjtxt+OpK4vbj3yBSfpiZTpVbEjt9U/tUpVp/T2M66lMi3ZC'
+ 'oHLlhu45Zo0aEq3UmQ/WBXu6EkO2eLYz2br9YQwRbSJ6z5CHmu'
+ 'hjKBQlpvGNfZYObx5lY4o6Ab4f/N8gyukskjmAFAf7Fr8cEog/'
+ 'oxmbagoCtUGRYJp2paooqH8L6xXp0Y8+23g7WJaAIr1i4V4aKS'
+ 'r9x7iUK6prcZTtMJZEHCswkLN/+DU6/FX3YZcOjseC+Qv3P+9v'
+ 'zIDp/92KJzqVqITGwrsc6ZsglMW37qxs6albtw3lMWSHlkcLbj'
+ 'Xf/iHPeKnb2WNLdkFNQ1J5OaTJR+E1CrXN+pm1JtB6XaUbaLGV'
+ 'CGUo13lAPVDtXcPbo64kMrQtQu4m9m8X8t8tfuJmINfwBnrKzk'
+ 'O6pl+LwimFaFEArV6wcaMxmwi0lM7mt4U1u9OIQjghQ/dEmOyV'
+ 'dZBnvyG7T/oRuLdUyZ/QGXZMlPQ3lAZ0ONn1Mk4bmKToW8ToE8'
+ 'ylld3rLlWDjjoQP8mP05Izg3mguLHXUhikUL8MD5NdYyeZJ1XZ'
+ '0OZ5S9uncurYj2ABWJoVaq/tFCdCEo9bbjWsePei26GZjaM3Fx'
+ 'RkAICXe/bt6/uLgaPZtO+sdARDuU3DRKMIdgM9NBaIn0kC7Wk4'
+ 'bnYShZ/rbhVt2/ds5XinnDBZsxSR3s553DixJ9v6w9Db++9Stw'
+ '4DgePd9lLy+6WuVBlKmcNflx9zg7US0AOarX2UNiQ==',
+ 'kdf_length': 32,
+ 'kdf_salt': 'MYH68QH48nRFMWH44piFWqBnKtU8KCz6Ajh24otrvzJlqPgB'
+ 'v6bvFJjRvjRp/0/v1j2nt40RZ6H5hfoKmore0g==\n',
+ 'length': 1024,
+ 'cipher': 'aes256',
+ }
+ }
+ }
+
+ def setUp(self):
+ def _get_pass():
+ return '123'
+ self._crypto = SecretsCrypto(_get_pass)
+
+ def test__get_pass(self):
+ self.assertEqual(self._crypto._get_pass(), '123')
+
+ def test__get_key(self):
+ salt = 'abc'
+ expected = scrypt.hash('123', salt, buflen=32)
+ key = self._crypto._get_key(salt)
+ self.assertEqual(expected, key)
+
+ def test_encrypt(self):
+ info = self._crypto.encrypt(self.SECRETS)
+ self.assertEqual(8, len(info))
+ for key, value in [
+ ('kdf', 'scrypt'),
+ ('kdf_salt', None),
+ ('kdf_length', None),
+ ('cipher', 'aes_256_gcm'),
+ ('length', None),
+ ('iv', None),
+ ('secrets', None),
+ ('version', 2)]:
+ self.assertTrue(key in info)
+ if value:
+ self.assertEqual(info[key], value)
+
+ def test__decrypt_v2(self):
+ encrypted = self.ENCRYPTED_V2
+ decrypted = self._crypto.decrypt(encrypted)
+ self.assertEqual(decrypted, self.SECRETS)
+
+ def test__decrypt_v1(self):
+ encrypted = self.ENCRYPTED_V1
+ decrypted = self._crypto.decrypt(encrypted)
+ self.assertEqual(decrypted, self.SECRETS)
class SoledadSecretsTestCase(BaseSoledadTest):
- def test_new_soledad_instance_generates_one_secret(self):
- self.assertTrue(
- self._soledad.storage_secret is not None,
- "Expected secret to be something different than None")
- number_of_secrets = len(self._soledad.secrets._secrets)
- self.assertTrue(
- number_of_secrets == 1,
- "Expected exactly 1 secret, got %d instead." % number_of_secrets)
-
- def test_generated_secret_is_of_correct_type(self):
- expected_type = str
- self.assertIsInstance(
- self._soledad.storage_secret, expected_type,
- "Expected secret to be of type %s" % expected_type)
-
- def test_generated_secret_has_correct_lengt(self):
- expected_length = self._soledad.secrets.GEN_SECRET_LENGTH
- actual_length = len(self._soledad.storage_secret)
- self.assertTrue(
- expected_length == actual_length,
- "Expected secret with length %d, got %d instead."
- % (expected_length, actual_length))
-
- def test_generated_secret_id_is_sha256_hash_of_secret(self):
- generated = self._soledad.secrets.secret_id
- expected = hashlib.sha256(self._soledad.storage_secret).hexdigest()
- self.assertTrue(
- generated == expected,
- "Expeceted generated secret id to be sha256 hash, got something "
- "else instead.")
-
- def test_generate_new_secret_generates_different_secret_id(self):
- # generate new secret
- secret_id_1 = self._soledad.secrets.secret_id
- secret_id_2 = self._soledad.secrets._gen_secret()
- self.assertTrue(
- len(self._soledad.secrets._secrets) == 2,
- "Expected exactly 2 secrets.")
- self.assertTrue(
- secret_id_1 != secret_id_2,
- "Expected IDs of secrets to be distinct.")
- self.assertTrue(
- secret_id_1 in self._soledad.secrets._secrets,
- "Expected to find ID of first secret in Soledad Secrets.")
- self.assertTrue(
- secret_id_2 in self._soledad.secrets._secrets,
- "Expected to find ID of second secret in Soledad Secrets.")
-
- def test__has_secret(self):
- self.assertTrue(
- self._soledad._secrets._has_secret(),
- "Should have a secret at this point")
+ def test_generated_secrets_have_correct_length(self):
+ expected = self._soledad.secrets.lengths
+ for name, length in expected.iteritems():
+ secret = getattr(self._soledad.secrets, name)
+ self.assertEqual(length, len(secret))
class SoledadCryptoAESTestCase(BaseSoledadTest):
diff --git a/testing/tests/client/test_deprecated_crypto.py b/testing/tests/client/test_deprecated_crypto.py
index 8ee3735c..8c711c22 100644
--- a/testing/tests/client/test_deprecated_crypto.py
+++ b/testing/tests/client/test_deprecated_crypto.py
@@ -1,5 +1,7 @@
import json
-from twisted.internet import defer
+
+from pytest import inlineCallbacks
+
from uuid import uuid4
from urlparse import urljoin
@@ -39,7 +41,7 @@ class DeprecatedCryptoTest(SoledadWithCouchServerMixin, TestCaseWithServer):
def make_app_with_state(state):
return make_token_soledad_app(state)
- @defer.inlineCallbacks
+ @inlineCallbacks
def test_touch_updates_remote_representation(self):
self.startTwistedServer()
user = 'user-' + uuid4().hex
diff --git a/testing/tests/client/test_shared_db.py b/testing/tests/client/test_shared_db.py
index aac766c2..b045e524 100644
--- a/testing/tests/client/test_shared_db.py
+++ b/testing/tests/client/test_shared_db.py
@@ -2,7 +2,6 @@ from leap.soledad.common.document import SoledadDocument
from leap.soledad.client.shared_db import SoledadSharedDatabase
from test_soledad.util import BaseSoledadTest
-from test_soledad.util import ADDRESS
class SoledadSharedDBTestCase(BaseSoledadTest):
@@ -14,37 +13,28 @@ class SoledadSharedDBTestCase(BaseSoledadTest):
def setUp(self):
BaseSoledadTest.setUp(self)
self._shared_db = SoledadSharedDatabase(
- 'https://provider/', ADDRESS, document_factory=SoledadDocument,
+ 'https://provider/', document_factory=SoledadDocument,
creds=None)
def tearDown(self):
BaseSoledadTest.tearDown(self)
- def test__get_secrets_from_shared_db(self):
+ def test__get_remote_doc(self):
"""
Ensure the shared db is queried with the correct doc_id.
"""
- doc_id = self._soledad.secrets._shared_db_doc_id()
- self._soledad.secrets._get_secrets_from_shared_db()
- self.assertTrue(
- self._soledad.shared_db.get_doc.assert_called_with(
- doc_id) is None,
- 'Wrong doc_id when fetching recovery document.')
-
- def test__put_secrets_in_shared_db(self):
+ doc_id = self._soledad.secrets.storage._remote_doc_id()
+ self._soledad.secrets.storage._get_remote_doc()
+ self._soledad.secrets.storage._shared_db.get_doc.assert_called_with(
+ doc_id)
+
+ def test_save_remote(self):
"""
Ensure recovery document is put into shared recover db.
"""
- doc_id = self._soledad.secrets._shared_db_doc_id()
- self._soledad.secrets._put_secrets_in_shared_db()
- self.assertTrue(
- self._soledad.shared_db.get_doc.assert_called_with(
- doc_id) is None,
- 'Wrong doc_id when fetching recovery document.')
- self.assertTrue(
- self._soledad.shared_db.put_doc.assert_called_with(
- self._doc_put) is None,
- 'Wrong document when putting recovery document.')
- self.assertTrue(
- self._doc_put.doc_id == doc_id,
- 'Wrong doc_id when putting recovery document.')
+ doc_id = self._soledad.secrets.storage._remote_doc_id()
+ storage = self._soledad.secrets.storage
+ storage.save_remote({'content': 'blah'})
+ storage._shared_db.get_doc.assert_called_with(doc_id)
+ storage._shared_db.put_doc.assert_called_with(self._doc_put)
+ self.assertTrue(self._doc_put.doc_id == doc_id)
diff --git a/testing/tests/client/test_signals.py b/testing/tests/client/test_signals.py
index 4e9ebfd0..c7609a74 100644
--- a/testing/tests/client/test_signals.py
+++ b/testing/tests/client/test_signals.py
@@ -20,7 +20,7 @@ class SoledadSignalingTestCase(BaseSoledadTest):
def setUp(self):
# mock signaling
soledad.client.signal = Mock()
- soledad.client.secrets.events.emit_async = Mock()
+ soledad.client._secrets.util.events.emit_async = Mock()
# run parent's setUp
BaseSoledadTest.setUp(self)
@@ -42,55 +42,36 @@ class SoledadSignalingTestCase(BaseSoledadTest):
- downloading keys / done downloading keys.
- uploading keys / done uploading keys.
"""
- soledad.client.secrets.events.emit_async.reset_mock()
+ soledad.client._secrets.util.events.emit_async.reset_mock()
# get a fresh instance so it emits all bootstrap signals
sol = self._soledad_instance(
secrets_path='alternative_stage3.json',
local_db_path='alternative_stage3.u1db')
# reverse call order so we can verify in the order the signals were
# expected
- soledad.client.secrets.events.emit_async.mock_calls.reverse()
- soledad.client.secrets.events.emit_async.call_args = \
- soledad.client.secrets.events.emit_async.call_args_list[0]
- soledad.client.secrets.events.emit_async.call_args_list.reverse()
+ soledad.client._secrets.util.events.emit_async.mock_calls.reverse()
+ soledad.client._secrets.util.events.emit_async.call_args = \
+ soledad.client._secrets.util.events.emit_async.call_args_list[0]
+ soledad.client._secrets.util.events.emit_async.call_args_list.reverse()
user_data = {'userid': ADDRESS, 'uuid': ADDRESS}
- # downloading keys signals
- soledad.client.secrets.events.emit_async.assert_called_with(
- catalog.SOLEDAD_DOWNLOADING_KEYS, user_data
- )
- self._pop_mock_call(soledad.client.secrets.events.emit_async)
- soledad.client.secrets.events.emit_async.assert_called_with(
- catalog.SOLEDAD_DONE_DOWNLOADING_KEYS, user_data
- )
- # creating keys signals
- self._pop_mock_call(soledad.client.secrets.events.emit_async)
- soledad.client.secrets.events.emit_async.assert_called_with(
- catalog.SOLEDAD_CREATING_KEYS, user_data
- )
- self._pop_mock_call(soledad.client.secrets.events.emit_async)
- soledad.client.secrets.events.emit_async.assert_called_with(
- catalog.SOLEDAD_DONE_CREATING_KEYS, user_data
- )
- # downloading once more (inside _put_keys_in_shared_db)
- self._pop_mock_call(soledad.client.secrets.events.emit_async)
- soledad.client.secrets.events.emit_async.assert_called_with(
- catalog.SOLEDAD_DOWNLOADING_KEYS, user_data
- )
- self._pop_mock_call(soledad.client.secrets.events.emit_async)
- soledad.client.secrets.events.emit_async.assert_called_with(
- catalog.SOLEDAD_DONE_DOWNLOADING_KEYS, user_data
- )
- # uploading keys signals
- self._pop_mock_call(soledad.client.secrets.events.emit_async)
- soledad.client.secrets.events.emit_async.assert_called_with(
- catalog.SOLEDAD_UPLOADING_KEYS, user_data
- )
- self._pop_mock_call(soledad.client.secrets.events.emit_async)
- soledad.client.secrets.events.emit_async.assert_called_with(
- catalog.SOLEDAD_DONE_UPLOADING_KEYS, user_data
- )
+ def _assert(*args, **kwargs):
+ mocked = soledad.client._secrets.util.events.emit_async
+ mocked.assert_called_with(*args)
+ pop = kwargs.get('pop')
+ if pop or pop is None:
+ self._pop_mock_call(mocked)
+
+ _assert(catalog.SOLEDAD_DOWNLOADING_KEYS, user_data)
+ _assert(catalog.SOLEDAD_DONE_DOWNLOADING_KEYS, user_data)
+ _assert(catalog.SOLEDAD_CREATING_KEYS, user_data)
+ _assert(catalog.SOLEDAD_DONE_CREATING_KEYS, user_data)
+ _assert(catalog.SOLEDAD_UPLOADING_KEYS, user_data)
+ _assert(catalog.SOLEDAD_DOWNLOADING_KEYS, user_data)
+ _assert(catalog.SOLEDAD_DONE_DOWNLOADING_KEYS, user_data)
+ _assert(catalog.SOLEDAD_DONE_UPLOADING_KEYS, user_data, pop=False)
+
sol.close()
def test_stage2_bootstrap_signals(self):
@@ -101,11 +82,11 @@ class SoledadSignalingTestCase(BaseSoledadTest):
# get existing instance so we have access to keys
sol = self._soledad_instance()
# create a document with secrets
- doc = SoledadDocument(doc_id=sol.secrets._shared_db_doc_id())
- doc.content = sol.secrets._export_recovery_document()
+ doc = SoledadDocument(doc_id=sol.secrets.storage._remote_doc_id())
+ doc.content = sol.secrets.crypto.encrypt(sol.secrets._secrets)
sol.close()
# reset mock
- soledad.client.secrets.events.emit_async.reset_mock()
+ soledad.client._secrets.util.events.emit_async.reset_mock()
# get a fresh instance so it emits all bootstrap signals
shared_db = self.get_default_shared_mock(get_doc_return_value=doc)
sol = self._soledad_instance(
@@ -114,20 +95,23 @@ class SoledadSignalingTestCase(BaseSoledadTest):
shared_db_class=shared_db)
# reverse call order so we can verify in the order the signals were
# expected
- soledad.client.secrets.events.emit_async.mock_calls.reverse()
- soledad.client.secrets.events.emit_async.call_args = \
- soledad.client.secrets.events.emit_async.call_args_list[0]
- soledad.client.secrets.events.emit_async.call_args_list.reverse()
+ mocked = soledad.client._secrets.util.events.emit_async
+ mocked.mock_calls.reverse()
+ mocked.call_args = mocked.call_args_list[0]
+ mocked.call_args_list.reverse()
+
+ def _assert(*args, **kwargs):
+ mocked = soledad.client._secrets.util.events.emit_async
+ mocked.assert_called_with(*args)
+ pop = kwargs.get('pop')
+ if pop or pop is None:
+ self._pop_mock_call(mocked)
+
# assert download keys signals
- soledad.client.secrets.events.emit_async.assert_called_with(
- catalog.SOLEDAD_DOWNLOADING_KEYS,
- {'userid': ADDRESS, 'uuid': ADDRESS}
- )
- self._pop_mock_call(soledad.client.secrets.events.emit_async)
- soledad.client.secrets.events.emit_async.assert_called_with(
- catalog.SOLEDAD_DONE_DOWNLOADING_KEYS,
- {'userid': ADDRESS, 'uuid': ADDRESS},
- )
+ user_data = {'userid': ADDRESS, 'uuid': ADDRESS}
+ _assert(catalog.SOLEDAD_DOWNLOADING_KEYS, user_data)
+ _assert(catalog.SOLEDAD_DONE_DOWNLOADING_KEYS, user_data, pop=False)
+
sol.close()
def test_stage1_bootstrap_signals(self):
diff --git a/testing/tests/sync/test_sync_target.py b/testing/tests/sync/test_sync_target.py
index 6ce9a5c5..302a16b8 100644
--- a/testing/tests/sync/test_sync_target.py
+++ b/testing/tests/sync/test_sync_target.py
@@ -838,7 +838,7 @@ class TestSoledadDbSync(
# already created on some setUp method.
import binascii
tohex = binascii.b2a_hex
- key = tohex(self._soledad.secrets.get_local_storage_key())
+ key = tohex(self._soledad.secrets.local)
dbpath = self._soledad._local_db_path
self.opts = SQLCipherOptions(