summaryrefslogtreecommitdiff
path: root/client/src/leap/soledad/client
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/leap/soledad/client')
-rw-r--r--client/src/leap/soledad/client/_crypto.py39
-rw-r--r--client/src/leap/soledad/client/_secrets/__init__.py129
-rw-r--r--client/src/leap/soledad/client/_secrets/crypto.py138
-rw-r--r--client/src/leap/soledad/client/_secrets/storage.py120
-rw-r--r--client/src/leap/soledad/client/_secrets/util.py63
-rw-r--r--client/src/leap/soledad/client/api.py191
-rw-r--r--client/src/leap/soledad/client/http_target/__init__.py9
-rw-r--r--client/src/leap/soledad/client/http_target/fetch_protocol.py8
-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.py31
-rw-r--r--client/src/leap/soledad/client/sqlcipher.py1
12 files changed, 527 insertions, 1016 deletions
diff --git a/client/src/leap/soledad/client/_crypto.py b/client/src/leap/soledad/client/_crypto.py
index 4bbdd044..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
@@ -49,7 +50,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,11 +190,14 @@ 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._aes.authenticate(self._make_preamble())
+ 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
def iv(self):
@@ -214,7 +219,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(
@@ -224,7 +229,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 +277,21 @@ 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:
+ 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:
+ 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
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..b6c81cda
--- /dev/null
+++ b/client/src/leap/soledad/client/_secrets/__init__.py
@@ -0,0 +1,129 @@
+# -*- 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
+import scrypt
+
+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, UserDataMixin
+
+
+logger = getLogger(__name__)
+
+
+class Secrets(UserDataMixin):
+
+ lengths = {
+ '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, soledad):
+ self._soledad = soledad
+ self._secrets = {}
+ self.crypto = SecretsCrypto(soledad)
+ self.storage = SecretsStorage(soledad)
+ self._bootstrap()
+
+ #
+ # bootstrap
+ #
+
+ def _bootstrap(self):
+
+ # attempt to load secrets from local storage
+ encrypted = self.storage.load_local()
+ if encrypted:
+ self._secrets = self.crypto.decrypt(encrypted)
+ # maybe update the format of storage of local secret.
+ if encrypted['version'] < self.crypto.VERSION:
+ self.store_secrets()
+ 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
+ #
+
+ @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 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)
+
+ #
+ # secrets
+ #
+
+ @property
+ def remote_secret(self):
+ return self._secrets.get('remote_secret')
+
+ @property
+ def local_salt(self):
+ return self._secrets.get('local_salt')
+
+ @property
+ def local_secret(self):
+ return self._secrets.get('local_secret')
+
+ @property
+ 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
new file mode 100644
index 00000000..fa7aaca0
--- /dev/null
+++ b/client/src/leap/soledad/client/_secrets/crypto.py
@@ -0,0 +1,138 @@
+# -*- 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, soledad):
+ self._soledad = soledad
+
+ def _get_key(self, salt):
+ passphrase = self._soledad.passphrase.encode('utf8')
+ key = scrypt.hash(passphrase, 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': ENC_METHOD.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):
+ # 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]
+
+ # 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, cipher)
+
+ # create secrets dictionary
+ secrets = {
+ 'remote_secret': plaintext[0:512],
+ 'local_salt': plaintext[512:576],
+ 'local_secret': plaintext[576:1024],
+ }
+ return secrets
+
+ def _decrypt_v2(self, encrypted):
+ 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, cipher)
+ 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..056c4322
--- /dev/null
+++ b/client/src/leap/soledad/client/_secrets/storage.py
@@ -0,0 +1,120 @@
+# -*- 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, UserDataMixin
+
+
+logger = getLogger(__name__)
+
+
+class SecretsStorage(UserDataMixin):
+
+ 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):
+ uuid = self._soledad.uuid
+ token = self._soledad.token
+ return {'token': {'uuid': uuid, 'token': token}}
+
+ #
+ # local storage
+ #
+
+ def load_local(self):
+ path = self._soledad.secrets_path
+ logger.info("trying to load secrets from disk: %s" % path)
+ try:
+ with open(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):
+ path = self._soledad.secrets_path
+ json_data = json.dumps(encrypted)
+ with open(path, 'w') as f:
+ f.write(json_data)
+
+ #
+ # remote storage
+ #
+
+ def _init_shared_db(self):
+ url = urlparse.urljoin(self._soledad.server_url, SHARED_DB_NAME)
+ creds = self._creds
+ db = SoledadSharedDatabase.open_database(url, creds)
+ return db
+
+ def _remote_doc_id(self):
+ passphrase = self._soledad.passphrase.encode('utf8')
+ uuid = self._soledad.uuid
+ text = '%s%s' % (passphrase, 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..6401889b
--- /dev/null
+++ b/client/src/leap/soledad/client/_secrets/util.py
@@ -0,0 +1,63 @@
+# -*- 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
+
+
+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}
+
+
+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..4be38cf1 100644
--- a/client/src/leap/soledad/client/api.py
+++ b/client/src/leap/soledad/client/api.py
@@ -39,13 +39,12 @@ 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
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__)
@@ -126,11 +124,11 @@ 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,
- auth_token=None, syncable=True):
+ auth_token=None, offline=False):
"""
Initialize configuration, cryptographic keys and dbs.
@@ -167,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
@@ -178,41 +177,32 @@ 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._dbsyncer = None
-
+ self.server_url = server_url
self.shared_db = shared_db
+ self.token = auth_token
+ self.offline = offline
+
+ self._dbsyncer = None
# 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
-
- # 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._crypto = SoledadCrypto(self._secrets.remote_storage_secret)
+ self._secrets = Secrets(self)
+ self._crypto = SoledadCrypto(self._secrets.remote_secret)
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
@@ -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,15 +245,6 @@ class Soledad(object):
for path in paths:
create_path_if_not_exists(path)
- def _init_secrets(self):
- """
- Initialize Soledad secrets.
- """
- self._secrets = SoledadSecrets(
- self.uuid, self._passphrase, self._secrets_path,
- self.shared_db, userid=self.userid)
- self._secrets.bootstrap()
-
def _init_u1db_sqlcipher_backend(self):
"""
Initialize the U1DB SQLCipher database for local storage.
@@ -279,7 +260,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_key)
opts = sqlcipher.SQLCipherOptions(
self._local_db_path, key,
@@ -648,10 +629,6 @@ class Soledad(object):
return self._local_db_path
@property
- def uuid(self):
- return self._uuid
-
- @property
def userid(self):
return self.uuid
@@ -659,21 +636,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.
@@ -686,6 +648,11 @@ class Soledad(object):
generation before the synchronization was performed.
:rtype: twisted.internet.defer.Deferred
"""
+ # maybe bypass sync
+ if self.offline or not self.token:
+ generation = self._dbsyncer.get_generation()
+ return defer.succeed(generation)
+
d = self.sync_lock.run(
self._sync)
return d
@@ -698,12 +665,11 @@ 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
- 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
@@ -760,101 +726,17 @@ 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.
-
- Internally, this builds the credentials dictionary with the following
- format:
-
- {
- 'token': {
- 'uuid': '<uuid>'
- 'token': '<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']
-
- token = property(_get_token, _set_token, doc='The authentication Token.')
-
#
# 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
@@ -867,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
@@ -891,7 +774,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
@@ -906,11 +789,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)
diff --git a/client/src/leap/soledad/client/http_target/__init__.py b/client/src/leap/soledad/client/http_target/__init__.py
index 0e250bf1..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,11 @@ after receiving.
import os
-from leap.soledad.common.log import getLogger
-from leap.common.certs import get_compatible_ssl_context_factory
from twisted.web.client import Agent
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
@@ -86,8 +87,8 @@ 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))
+ 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/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):
"""
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..4f70c74b 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.
@@ -55,10 +55,6 @@ class SoledadSharedDatabase(http_database.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,31 +91,20 @@ class SoledadSharedDatabase(http_database.HTTPDatabase, TokenBasedAuth):
#
@staticmethod
- def open_database(url, uuid, creds=None, syncable=True):
+ 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
- :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
+ db = SoledadSharedDatabase(url, creds=creds)
return db
@staticmethod
@@ -134,20 +119,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/client/src/leap/soledad/client/sqlcipher.py b/client/src/leap/soledad/client/sqlcipher.py
index c9a9444e..a3e45228 100644
--- a/client/src/leap/soledad/client/sqlcipher.py
+++ b/client/src/leap/soledad/client/sqlcipher.py
@@ -397,7 +397,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