From 86c9dc79366f8364cce59341ab735407cea36d59 Mon Sep 17 00:00:00 2001 From: drebs Date: Mon, 8 Jul 2013 08:06:53 -0300 Subject: Add method for password change. --- .../feature_3080-add-method-for-password-change | 1 + soledad/src/leap/soledad/__init__.py | 65 +++++++++++++++++++++- soledad/src/leap/soledad/tests/test_soledad.py | 34 +++++++++++ 3 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 soledad/changes/feature_3080-add-method-for-password-change (limited to 'soledad') diff --git a/soledad/changes/feature_3080-add-method-for-password-change b/soledad/changes/feature_3080-add-method-for-password-change new file mode 100644 index 00000000..0158ad00 --- /dev/null +++ b/soledad/changes/feature_3080-add-method-for-password-change @@ -0,0 +1 @@ + o Add method for password change. diff --git a/soledad/src/leap/soledad/__init__.py b/soledad/src/leap/soledad/__init__.py index 2e1155f9..956f47a7 100644 --- a/soledad/src/leap/soledad/__init__.py +++ b/soledad/src/leap/soledad/__init__.py @@ -164,6 +164,20 @@ SECRETS_DOC_ID_HASH_PREFIX = 'uuid-' # Soledad: local encrypted storage and remote encrypted sync. # +class NoStorageSecret(Exception): + """ + Raised when trying to use a storage secret but none is available. + """ + pass + + +class PassphraseTooShort(Exception): + """ + Raised when trying to change the passphrase but the provided passphrase is + too short. + """ + + class Soledad(object): """ Soledad provides encrypted data storage and sync. @@ -232,6 +246,12 @@ class Soledad(object): encryption. """ + MINIMUM_PASSPHRASE_LENGTH = 6 + """ + The minimum length for a passphrase. The passphrase length is only checked + when the user changes her passphras, not when she instantiates Soledad. + """ + IV_SEPARATOR = ":" """ A separator used for storing the encryption initial value prepended to the @@ -246,6 +266,8 @@ class Soledad(object): KDF_KEY = 'kdf' KDF_SALT_KEY = 'kdf_salt' KDF_LENGTH_KEY = 'kdf_length' + KDF_SCRYPT = 'scrypt' + CIPHER_AES256 = 'aes256' """ Keys used to access storage secrets in recovery documents. """ @@ -563,10 +585,10 @@ class Soledad(object): self._secrets[secret_id] = { # leap.soledad.crypto submodule uses AES256 for symmetric # encryption. - self.KDF_KEY: 'scrypt', # TODO: remove hard coded kdf + self.KDF_KEY: self.KDF_SCRYPT, self.KDF_SALT_KEY: binascii.b2a_base64(salt), self.KDF_LENGTH_KEY: len(key), - self.CIPHER_KEY: 'aes256', # TODO: remove hard coded cipher + self.CIPHER_KEY: self.CIPHER_AES256, self.LENGTH_KEY: len(secret), self.SECRET_KEY: '%s%s%s' % ( str(iv), self.IV_SEPARATOR, binascii.b2a_base64(ciphertext)), @@ -600,6 +622,45 @@ class Soledad(object): with open(self._secrets_path, 'w') as f: f.write(json.dumps(data)) + def change_passphrase(self, new_passphrase): + """ + Change the passphrase that encrypts the storage secret. + + @param new_passphrase: The new passphrase. + @type new_passphrase: str + + @raise NoStorageSecret: Raised if there's no storage secret available. + """ + # maybe we want to add more checks to guarantee passphrase is + # reasonable? + soledad_assert_type(new_passphrase, str) + 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() + secret = self._get_storage_secret() + # generate random salt + new_salt = os.urandom(self.SALT_LENGTH) + # get a 256-bit key + key = scrypt.hash(new_passphrase, new_salt, buflen=32) + iv, ciphertext = self._crypto.encrypt_sym(secret, key) + self._secrets[self._secret_id] = { + # leap.soledad.crypto submodule uses AES256 for symmetric + # encryption. + self.KDF_KEY: self.KDF_SCRYPT, # TODO: remove hard coded kdf + self.KDF_SALT_KEY: binascii.b2a_base64(new_salt), + self.KDF_LENGTH_KEY: len(key), + self.CIPHER_KEY: self.CIPHER_AES256, + self.LENGTH_KEY: len(secret), + self.SECRET_KEY: '%s%s%s' % ( + str(iv), self.IV_SEPARATOR, binascii.b2a_base64(ciphertext)), + } + self._store_secrets() + self._passphrase = new_passphrase + # # General crypto utility methods. # diff --git a/soledad/src/leap/soledad/tests/test_soledad.py b/soledad/src/leap/soledad/tests/test_soledad.py index 281b7a0f..875ecc56 100644 --- a/soledad/src/leap/soledad/tests/test_soledad.py +++ b/soledad/src/leap/soledad/tests/test_soledad.py @@ -28,6 +28,7 @@ import simplejson as json from mock import Mock +from pysqlcipher.dbapi2 import DatabaseError from leap.common.testing.basetest import BaseLeapTest from leap.common.events import events_pb2 as proto from leap.soledad.tests import ( @@ -106,6 +107,39 @@ class AuxMethodsTestCase(BaseSoledadTest): sol.local_db_path) self.assertEqual('value_1', sol.server_url) + def test_change_passphrase(self): + """ + Test if passphrase can be changed. + """ + sol = self._soledad_instance( + 'leap@leap.se', + passphrase='123') + doc = sol.create_doc({'simple': 'doc'}) + doc_id = doc.doc_id + # change the passphrase + sol.change_passphrase('654321') + # assert we can not use the old passphrase anymore + self.assertRaises( + DatabaseError, + self._soledad_instance, 'leap@leap.se', '123') + # use new passphrase and retrieve doc + sol2 = self._soledad_instance('leap@leap.se', '654321') + doc2 = sol2.get_doc(doc_id) + self.assertEqual(doc, doc2) + + 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='123') + # check that soledad complains about new passphrase length + self.assertRaises( + soledad.PassphraseTooShort, + sol.change_passphrase, '54321') + class SoledadSharedDBTestCase(BaseSoledadTest): """ -- cgit v1.2.3