diff options
-rw-r--r-- | src/leap/soledad/__init__.py | 167 | ||||
-rw-r--r-- | src/leap/soledad/config.py | 97 | ||||
-rw-r--r-- | src/leap/soledad/tests/__init__.py | 1 | ||||
-rw-r--r-- | src/leap/soledad/tests/test_soledad.py | 76 |
4 files changed, 104 insertions, 237 deletions
diff --git a/src/leap/soledad/__init__.py b/src/leap/soledad/__init__.py index 76b5656e..94ebec1a 100644 --- a/src/leap/soledad/__init__.py +++ b/src/leap/soledad/__init__.py @@ -36,12 +36,12 @@ except ImportError: import json # noqa +from xdg import BaseDirectory from hashlib import sha256 from leap.common import events from leap.common.check import leap_assert -from leap.soledad.config import SoledadConfig from leap.soledad.backends import sqlcipher from leap.soledad.backends.leap_backend import ( LeapDocument, @@ -77,7 +77,7 @@ class NotADirectory(Exception): """ -class NoSharedDbUrl(Exception): +class NoServerUrl(Exception): """ Tried to get access to shared recovery database but there's no URL for it. """ @@ -131,9 +131,16 @@ class Soledad(object): Key used to access symmetric keys in recovery documents. """ - def __init__(self, address, passphrase, config_path=None, - secret_path=None, local_db_path=None, - shared_db_url=None, auth_token=None, bootstrap=True): + DEFAULT_PREFIX = os.path.join( + BaseDirectory.xdg_config_home, + 'leap', 'soledad') + """ + Prefix for default values for path. + """ + + def __init__(self, address, passphrase, secret_path=None, + local_db_path=None, server_url=None, auth_token=None, + bootstrap=True): """ Initialize configuration, cryptographic keys and dbs. @@ -142,16 +149,15 @@ class Soledad(object): @param passphrase: The passphrase for locking and unlocking encryption secrets for disk storage. @type passphrase: str - @param config_path: Path for configuration file. - @type config_path: str @param secret_path: Path for storing encrypted key used for symmetric encryption. @type secret_path: str @param local_db_path: Path for local encrypted storage db. @type local_db_path: str - @param shared_db_url: URL for shared Soledad DB for key storage and - unauth retrieval. - @type shared_db_url: str + @param server_url: URL for Soledad server. This is used either to sync + with the user's remote db and to interact with the shared recovery + database. + @type server_url: str @param auth_token: Authorization token for accessing remote databases. @type auth_token: str @param bootstrap: True/False, should bootstrap this instance? Mostly @@ -162,15 +168,33 @@ class Soledad(object): self._address = address self._passphrase = passphrase self._set_token(auth_token) - self._init_config( - config_path=config_path, - secret_path=secret_path, - local_db_path=local_db_path, - shared_db_url=shared_db_url, - ) + self._init_config(secret_path, local_db_path, server_url) if bootstrap: self._bootstrap() + def _init_config(self, secret_path, local_db_path, server_url): + """ + Initialize configuration using default values for missing params. + """ + # initialize secret_path + self._secret_path = secret_path + if self._secret_path is None: + self._secret_path = os.path.join( + self.DEFAULT_PREFIX, 'secret.gpg') + # initialize local_db_path + self._local_db_path = local_db_path + if self._local_db_path is None: + self._local_db_path = os.path.join( + self.DEFAULT_PREFIX, 'soledad.u1db') + # initialize server_url + self._server_url = server_url + if self._server_url is None: + raise NoServerUrl() + + # + # initialization methods + # + def _bootstrap(self): """ Bootstrap local Soledad instance. @@ -225,53 +249,13 @@ class Soledad(object): # Stage 3 - Local database initialization self._init_db() - def _shared_db(self): - """ - Return an instance of the shared recovery database object. - """ - if self._config.get_shared_db_url(): - return SoledadSharedDatabase.open_database( - self._config.get_shared_db_url(), - False, # TODO: eliminate need to create db here. - creds=self._creds) - else: - raise NoSharedDbUrl() - - def _init_config(self, config_path, secret_path, local_db_path, - shared_db_url): - """ - Initialize configuration using SoledadConfig. - - Soledad configuration makes use of BaseLeapConfig to load values from - a file or from default configuration. Parameters passed as arguments - for this method will supersede file and default values. - - @param kwargs: a dictionary with configuration parameter values passed - when instantiating this Soledad instance. - @type kwargs: dict - """ - self._config = SoledadConfig() - if config_path is not None: - self._config.load(path=config_path) - else: - self._config.load(data='') - # overwrite config with passed parameters - if secret_path is not None: - self._config._config_checker.config['secret_path'] = secret_path - if local_db_path is not None: - self._config._config_checker.config['local_db_path'] = \ - local_db_path - if shared_db_url is not None: - self._config._config_checker.config['shared_db_url'] = \ - shared_db_url - def _init_dirs(self): """ Create work directories. """ paths = map( lambda x: os.path.dirname(x), - [self._config.get_local_db_path(), self._config.get_secret_path()]) + [self.local_db_path, self.secret_path]) for path in paths: if not os.path.isfile(path): if not os.path.isdir(path): @@ -302,7 +286,7 @@ class Soledad(object): # TODO: verify if secret for sqlcipher should be the same as the # one for symmetric encryption. self._db = sqlcipher.open( - self._config.get_local_db_path(), + self.local_db_path, self._symkey, create=True, document_factory=LeapDocument, @@ -314,9 +298,9 @@ class Soledad(object): """ self._db.close() - #------------------------------------------------------------------------- - # Management of secret for symmetric encryption - #------------------------------------------------------------------------- + # + # Management of secret for symmetric encryption. + # def _has_symkey(self): """ @@ -328,14 +312,14 @@ class Soledad(object): @rtype: bool """ # does the file exist in disk? - if not os.path.isfile(self._config.get_secret_path()): + if not os.path.isfile(self.secret_path): return False # is it symmetrically encrypted? - with open(self._config.get_secret_path(), 'r') as f: + with open(self.secret_path, 'r') as f: content = f.read() if not self._crypto.is_encrypted_sym(content): raise DocumentNotEncrypted( - "File %s is not encrypted!" % self._config.get_secret_path()) + "File %s is not encrypted!" % self.secret_path) # can we decrypt it? plaintext = self._crypto.decrypt_sym( content, passphrase=self._passphrase) @@ -348,7 +332,7 @@ class Soledad(object): if not self._has_symkey(): raise KeyDoesNotExist("Tried to load key for symmetric " "encryption but it does not exist on disk.") - with open(self._config.get_secret_path()) as f: + with open(self.secret_path) as f: self._symkey = \ self._crypto.decrypt_sym( f.read(), passphrase=self._passphrase) @@ -380,12 +364,12 @@ class Soledad(object): def _store_symkey(self): ciphertext = self._crypto.encrypt_sym( self._symkey, self._passphrase) - with open(self._config.get_secret_path(), 'w') as f: + with open(self.secret_path, 'w') as f: f.write(ciphertext) - #------------------------------------------------------------------------- + # # General crypto utility methods. - #------------------------------------------------------------------------- + # def _has_keys(self): """ @@ -419,6 +403,15 @@ class Soledad(object): """ return sha256('address-%s' % self._address).hexdigest() + def _shared_db(self): + """ + Return an instance of the shared recovery database object. + """ + return SoledadSharedDatabase.open_database( + self.server_url, + False, # TODO: eliminate need to create db here. + creds=self._creds) + def _fetch_keys_from_shared_db(self): """ Retrieve the document with encrypted key material from the shared @@ -451,7 +444,7 @@ class Soledad(object): if doc: remote_symkey = self._crypto.decrypt_sym( doc.content[self.SYMKEY_KEY], - passphrase=self._address_hash()) + passphrase=self._passphrase) leap_assert( remote_symkey == self._symkey, 'Local and remote symmetric secrets differ!') @@ -468,9 +461,9 @@ class Soledad(object): events.signal( events.events_pb2.SOLEDAD_DONE_UPLOADING_KEYS, self._address) - #------------------------------------------------------------------------- - # Document storage, retrieval and sync - #------------------------------------------------------------------------- + # + # Document storage, retrieval and sync. + # # TODO: refactor the following methods to somewhere out of here # (SoledadLocalDatabase, maybe?) @@ -708,7 +701,7 @@ class Soledad(object): """ return self._db.resolve_doc(doc, conflicted_doc_revs) - def sync(self, url): + def sync(self): """ Synchronize the local encrypted replica with a remote replica. @@ -719,7 +712,7 @@ class Soledad(object): performed. @rtype: str """ - local_gen = self._db.sync(url, creds=self._creds, autocreate=True) + local_gen = self._db.sync(self.server_url, creds=self._creds, autocreate=True) events.signal(events.events_pb2.SOLEDAD_DONE_DATA_SYNC, self._address) return local_gen @@ -772,9 +765,9 @@ class Soledad(object): token = property(_get_token, _set_token, doc='The authentication Token.') - #------------------------------------------------------------------------- - # Recovery document export and import - #------------------------------------------------------------------------- + # + # Recovery document export and import methods. + # def export_recovery_document(self, passphrase=None): """ @@ -845,5 +838,23 @@ class Soledad(object): address = property(_get_address, doc='The user address.') + def _get_secret_path(self): + return self._secret_path + + secret_path = property( + _get_secret_path, + doc='The path for the file containing the encrypted symmetric secret.') + + def _get_local_db_path(self): + return self._local_db_path + + local_db_path = property( + _get_local_db_path, + doc='The path for the local database replica.') + + def _get_server_url(self): + return self._server_url -__all__ = ['backends', 'util', 'server', 'shared_db'] + server_url = property( + _get_server_url, + doc='The URL of the Soledad server.') diff --git a/src/leap/soledad/config.py b/src/leap/soledad/config.py deleted file mode 100644 index 733ad9e7..00000000 --- a/src/leap/soledad/config.py +++ /dev/null @@ -1,97 +0,0 @@ -# -*- coding: utf-8 -*- -# config.py -# Copyright (C) 2013 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/>. - - -""" -Management of configuration sources for Soledad. -""" - -import os -import logging - - -from xdg import BaseDirectory -from leap.common.config.baseconfig import BaseConfig - - -logger = logging.getLogger(name=__name__) - - -PREFIX = os.path.join( - BaseDirectory.xdg_config_home, - 'leap', 'soledad') - - -soledad_config_spec = { - 'description': 'sample soledad config', - 'type': 'object', - 'properties': { - 'secret_path': { - 'type': unicode, - 'default': PREFIX + '/secret.gpg', - 'required': True, - }, - 'local_db_path': { - #'type': unicode, - 'default': PREFIX + '/soledad.u1db', - 'required': True, - }, - 'shared_db_url': { - 'type': unicode, - 'default': 'http://provider/soledad/shared', - 'required': True, # should this be True? - }, - } -} - - -class SoledadConfig(BaseConfig): - - def _get_spec(self): - """ - Returns the spec object for the specific configuration - """ - return soledad_config_spec - - def get_secret_path(self): - return self._safe_get_value("secret_path") - - def get_local_db_path(self): - return self._safe_get_value("local_db_path") - - def get_shared_db_url(self): - return self._safe_get_value("shared_db_url") - - -if __name__ == "__main__": - logger = logging.getLogger(name='leap') - logger.setLevel(logging.DEBUG) - console = logging.StreamHandler() - console.setLevel(logging.DEBUG) - formatter = logging.Formatter( - '%(asctime)s ' - '- %(name)s - %(levelname)s - %(message)s') - console.setFormatter(formatter) - logger.addHandler(console) - - soledadconfig = SoledadConfig() - - try: - soledadconfig.get_local_db_path() - except Exception as e: - assert isinstance(e, AssertionError), "Expected an assert" - print "Safe value getting is working" diff --git a/src/leap/soledad/tests/__init__.py b/src/leap/soledad/tests/__init__.py index a30193d3..149228e5 100644 --- a/src/leap/soledad/tests/__init__.py +++ b/src/leap/soledad/tests/__init__.py @@ -54,6 +54,7 @@ class BaseSoledadTest(BaseLeapTest): '123', secret_path=self.tempdir+prefix+secret_path, local_db_path=self.tempdir+prefix+local_db_path, + server_url='', # Soledad will fail if not given an url. bootstrap=bootstrap) diff --git a/src/leap/soledad/tests/test_soledad.py b/src/leap/soledad/tests/test_soledad.py index 1ddfb6a0..1ce9adb7 100644 --- a/src/leap/soledad/tests/test_soledad.py +++ b/src/leap/soledad/tests/test_soledad.py @@ -44,8 +44,8 @@ class AuxMethodsTestCase(BaseSoledadTest): def test__init_dirs(self): sol = self._soledad_instance(prefix='/_init_dirs') sol._init_dirs() - local_db_dir = os.path.dirname(sol._config.get_local_db_path()) - secret_path = os.path.dirname(sol._config.get_secret_path()) + local_db_dir = os.path.dirname(sol.local_db_path) + secret_path = os.path.dirname(sol.secret_path) self.assertTrue(os.path.isdir(local_db_dir)) self.assertTrue(os.path.isdir(secret_path)) @@ -61,81 +61,33 @@ class AuxMethodsTestCase(BaseSoledadTest): from leap.soledad.backends.sqlcipher import SQLCipherDatabase self.assertIsInstance(sol._db, SQLCipherDatabase) - def test__init_config_default(self): - """ - Test if configuration defaults point to the correct place. - """ - sol = Soledad('leap@leap.se', passphrase='123', bootstrap=False) - self.assertTrue(bool(re.match( - '.*/\.config/leap/soledad/secret.gpg', - sol._config.get_secret_path()))) - self.assertTrue(bool(re.match( - '.*/\.config/leap/soledad/soledad.u1db', - sol._config.get_local_db_path()))) - self.assertEqual( - 'http://provider/soledad/shared', - sol._config.get_shared_db_url()) - def test__init_config_defaults(self): """ Test if configuration defaults point to the correct place. """ - # we use regexp match here because HOME environment variable is - # changed by the BaseLeapTest class but BaseConfig does not capture - # that change. - sol = Soledad('leap@leap.se', passphrase='123', bootstrap=False) - self.assertTrue(bool(re.match( - '.*/\.config/leap/soledad/secret.gpg', - sol._config.get_secret_path()))) - self.assertTrue(bool(re.match( - '.*/\.config/leap/soledad/soledad.u1db', - sol._config.get_local_db_path()))) - self.assertEqual( - 'http://provider/soledad/shared', - sol._config.get_shared_db_url()) - - def test__init_config_from_file(self): - """ - Test if configuration is correctly read from file. - """ - # we use regexp match here because HOME environment variable is - # changed by the BaseLeapTest class but BaseConfig does not capture - # that change. - config_values = { - "secret_path": "value_1", - "local_db_path": "value_2", - "shared_db_url": "value_3" - } - tmpfile = tempfile.mktemp(dir=self.tempdir) - f = open(tmpfile, 'w') - f.write(json.dumps(config_values)) - f.close() - sol = Soledad( - 'leap@leap.se', - passphrase='123', - bootstrap=False, - config_path=tmpfile) - self.assertEqual('value_1', sol._config.get_secret_path()) - self.assertEqual('value_2', sol._config.get_local_db_path()) - self.assertEqual('value_3', sol._config.get_shared_db_url()) + sol = Soledad('leap@leap.se', passphrase='123', bootstrap=False, + server_url='') # otherwise Soledad will fail. + self.assertEquals( + os.path.join(sol.DEFAULT_PREFIX, 'secret.gpg'), + sol.secret_path) + self.assertEquals( + os.path.join(sol.DEFAULT_PREFIX, 'soledad.u1db'), + sol.local_db_path) def test__init_config_from_params(self): """ Test if configuration is correctly read from file. """ - # we use regexp match here because HOME environment variable is - # changed by the BaseLeapTest class but BaseConfig does not capture - # that change. sol = Soledad( 'leap@leap.se', passphrase='123', bootstrap=False, secret_path='value_3', local_db_path='value_2', - shared_db_url='value_1') - self.assertEqual('value_3', sol._config.get_secret_path()) - self.assertEqual('value_2', sol._config.get_local_db_path()) - self.assertEqual('value_1', sol._config.get_shared_db_url()) + server_url='value_1') + self.assertEqual('value_3', sol.secret_path) + self.assertEqual('value_2', sol.local_db_path) + self.assertEqual('value_1', sol.server_url) class SoledadSharedDBTestCase(BaseSoledadTest): |