diff options
| -rw-r--r-- | soledad/changes/feature_3080-add-method-for-password-change | 1 | ||||
| -rw-r--r-- | soledad/src/leap/soledad/__init__.py | 65 | ||||
| -rw-r--r-- | soledad/src/leap/soledad/tests/test_soledad.py | 34 | 
3 files changed, 98 insertions, 2 deletions
| 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):      """ | 
