diff options
Diffstat (limited to 'client/src/leap/soledad/client/_secrets')
-rw-r--r-- | client/src/leap/soledad/client/_secrets/__init__.py | 132 | ||||
-rw-r--r-- | client/src/leap/soledad/client/_secrets/crypto.py | 123 | ||||
-rw-r--r-- | client/src/leap/soledad/client/_secrets/storage.py | 124 | ||||
-rw-r--r-- | client/src/leap/soledad/client/_secrets/util.py | 46 |
4 files changed, 425 insertions, 0 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 |