summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--setup.py1
-rw-r--r--src/leap/soledad/__init__.py88
-rw-r--r--src/leap/soledad/backends/couch.py17
-rw-r--r--src/leap/soledad/backends/leap_backend.py45
-rw-r--r--src/leap/soledad/shared_db.py97
-rw-r--r--src/leap/soledad/tests/test_soledad.py57
6 files changed, 171 insertions, 134 deletions
diff --git a/setup.py b/setup.py
index f9124a32..9e3f6193 100644
--- a/setup.py
+++ b/setup.py
@@ -53,6 +53,7 @@ dependency_links = [
tests_requirements = [
+ 'mock',
'nose2',
'testscenarios',
]
diff --git a/src/leap/soledad/__init__.py b/src/leap/soledad/__init__.py
index 12de0bba..76b5656e 100644
--- a/src/leap/soledad/__init__.py
+++ b/src/leap/soledad/__init__.py
@@ -77,6 +77,12 @@ class NotADirectory(Exception):
"""
+class NoSharedDbUrl(Exception):
+ """
+ Tried to get access to shared recovery database but there's no URL for it.
+ """
+
+
#
# Soledad: local encrypted storage and remote encrypted sync.
#
@@ -155,7 +161,7 @@ class Soledad(object):
# TODO: allow for fingerprint enforcing.
self._address = address
self._passphrase = passphrase
- self._auth_token = auth_token
+ self._set_token(auth_token)
self._init_config(
config_path=config_path,
secret_path=secret_path,
@@ -194,22 +200,22 @@ class Soledad(object):
# Stage 0 - Local environment setup
self._init_dirs()
self._crypto = SoledadCrypto(self)
- if self._config.get_shared_db_url() and self._auth_token:
- # TODO: eliminate need to create db here.
- self._shared_db = SoledadSharedDatabase.open_database(
- self._config.get_shared_db_url(),
- True,
- token=self._auth_token)
- else:
- self._shared_db = None
# Stage 1 - Keys generation/loading
if self._has_keys():
self._load_keys()
else:
+ logger.info(
+ 'Trying to fetch cryptographic secrets from shared recovery '
+ 'database...')
doc = self._fetch_keys_from_shared_db()
if not doc:
+ logger.info(
+ 'No cryptographic secrets found, creating new secrets...')
self._init_keys()
else:
+ logger.info(
+ 'Found cryptographic secrets in shared recovery '
+ 'database.')
self._set_symkey(
self._crypto.decrypt_sym(
doc.content[self.KEY_SYMKEY],
@@ -219,6 +225,18 @@ 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):
"""
@@ -382,6 +400,7 @@ class Soledad(object):
"""
Load the key for symmetric encryption from persistent storage.
"""
+ logger.info('Loading cryptographic secrets from local storage...')
self._load_symkey()
def _gen_keys(self):
@@ -410,10 +429,7 @@ class Soledad(object):
"""
events.signal(
events.events_pb2.SOLEDAD_DOWNLOADING_KEYS, self._address)
- # TODO: change below to raise appropriate exceptions
- if not self._shared_db:
- return None
- doc = self._shared_db.get_doc_unauth(self._address_hash())
+ doc = self._shared_db().get_doc_unauth(self._address_hash())
events.signal(
events.events_pb2.SOLEDAD_DONE_DOWNLOADING_KEYS, self._address)
return doc
@@ -431,11 +447,9 @@ class Soledad(object):
self._has_keys(),
'Tried to send keys to server but they don\'t exist in local '
'storage.')
- if not self._shared_db:
- return
doc = self._fetch_keys_from_shared_db()
if doc:
- remote_symkey = self.decrypt_sym(
+ remote_symkey = self._crypto.decrypt_sym(
doc.content[self.SYMKEY_KEY],
passphrase=self._address_hash())
leap_assert(
@@ -445,12 +459,12 @@ class Soledad(object):
events.signal(
events.events_pb2.SOLEDAD_UPLOADING_KEYS, self._address)
content = {
- self.SYMKEY_KEY: self.encrypt_sym(
+ self.SYMKEY_KEY: self._crypto.encrypt_sym(
self._symkey, self._passphrase),
}
doc = LeapDocument(doc_id=self._address_hash())
doc.content = content
- self._shared_db.put_doc(doc)
+ self._shared_db().put_doc(doc)
events.signal(
events.events_pb2.SOLEDAD_DONE_UPLOADING_KEYS, self._address)
@@ -694,7 +708,7 @@ class Soledad(object):
"""
return self._db.resolve_doc(doc, conflicted_doc_revs)
- def sync(self, url, creds=None):
+ def sync(self, url):
"""
Synchronize the local encrypted replica with a remote replica.
@@ -705,8 +719,7 @@ class Soledad(object):
performed.
@rtype: str
"""
- # TODO: create authentication scheme for sync with server.
- local_gen = self._db.sync(url, creds=creds, autocreate=True)
+ local_gen = self._db.sync(url, creds=self._creds, autocreate=True)
events.signal(events.events_pb2.SOLEDAD_DONE_DATA_SYNC, self._address)
return local_gen
@@ -720,8 +733,7 @@ class Soledad(object):
@return: Whether remote replica and local replica differ.
@rtype: bool
"""
- # TODO: create auth scheme for sync with server
- target = LeapSyncTarget(url, creds=None, crypto=self._crypto)
+ target = LeapSyncTarget(url, creds=self._creds, crypto=self._crypto)
info = target.get_sync_info(self._db._get_replica_uid())
# compare source generation with target's last known source generation
if self._db._get_generation() != info[4]:
@@ -730,6 +742,36 @@ class Soledad(object):
return True
return False
+ def _set_token(self, token):
+ """
+ Set the authentication token for remote database access.
+
+ Build the credentials dictionary with the following format:
+
+ self._{
+ 'token': {
+ 'address': 'user@provider',
+ 'token': '<token>'
+ }
+
+ @param token: The authentication token.
+ @type token: str
+ """
+ self._creds = {
+ 'token': {
+ 'address': self._address,
+ '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.')
+
#-------------------------------------------------------------------------
# Recovery document export and import
#-------------------------------------------------------------------------
diff --git a/src/leap/soledad/backends/couch.py b/src/leap/soledad/backends/couch.py
index 95090510..4dcea3f8 100644
--- a/src/leap/soledad/backends/couch.py
+++ b/src/leap/soledad/backends/couch.py
@@ -387,7 +387,8 @@ class CouchDatabase(ObjectStoreDatabase):
indexes = {}
for name, idx in self._indexes.iteritems():
indexes[name] = {}
- for attr in [INDEX_NAME_KEY, INDEX_DEFINITION_KEY, INDEX_VALUES_KEY]:
+ for attr in [self.INDEX_NAME_KEY, self.INDEX_DEFINITION_KEY,
+ self.INDEX_VALUES_KEY]:
indexes[name][attr] = getattr(idx, '_' + attr)
return json.dumps(indexes)
@@ -404,8 +405,8 @@ class CouchDatabase(ObjectStoreDatabase):
"""
dict = {}
for name, idx_dict in json.loads(indexes).iteritems():
- idx = InMemoryIndex(name, idx_dict[INDEX_DEFINITION_KEY])
- idx._values = idx_dict[INDEX_VALUES_KEY]
+ idx = InMemoryIndex(name, idx_dict[self.INDEX_DEFINITION_KEY])
+ idx._values = idx_dict[self.INDEX_VALUES_KEY]
dict[name] = idx
return dict
@@ -435,8 +436,9 @@ class CouchServerState(ServerState):
@rtype: CouchDatabase
"""
# TODO: open couch
- return CouchDatabase.open_database(self.couch_url + '/' + dbname,
- create=False)
+ return CouchDatabase.open_database(
+ self.couch_url + '/' + dbname,
+ create=False)
def ensure_database(self, dbname):
"""
@@ -448,8 +450,9 @@ class CouchServerState(ServerState):
@return: The CouchDatabase object and the replica uid.
@rtype: (CouchDatabase, str)
"""
- db = CouchDatabase.open_database(self.couch_url + '/' + dbname,
- create=True)
+ db = CouchDatabase.open_database(
+ self.couch_url + '/' + dbname,
+ create=True)
return db, db._replica_uid
def delete_database(self, dbname):
diff --git a/src/leap/soledad/backends/leap_backend.py b/src/leap/soledad/backends/leap_backend.py
index 9750ffad..81f6c211 100644
--- a/src/leap/soledad/backends/leap_backend.py
+++ b/src/leap/soledad/backends/leap_backend.py
@@ -30,33 +30,22 @@ except ImportError:
from u1db import Document
from u1db.remote import utils
-from u1db.remote.http_target import HTTPSyncTarget
-from u1db.remote.http_database import HTTPDatabase
from u1db.errors import BrokenSyncStream
+from u1db.remote.http_target import HTTPSyncTarget
from leap.common.keymanager import KeyManager
from leap.common.check import leap_assert
+from leap.soledad.auth import (
+ set_token_credentials,
+ _sign_request,
+)
#
# Exceptions
#
-class NoDefaultKey(Exception):
- """
- Exception to signal that there's no default OpenPGP key configured.
- """
- pass
-
-
-class NoSoledadCryptoInstance(Exception):
- """
- Exception to signal that no Soledad instance was found.
- """
- pass
-
-
class DocumentNotEncrypted(Exception):
"""
Raised for failures in document encryption.
@@ -267,6 +256,18 @@ class LeapSyncTarget(HTTPSyncTarget):
receiving.
"""
+ #
+ # Token auth methods.
+ #
+
+ set_token_credentials = set_token_credentials
+
+ _sign_request = _sign_request
+
+ #
+ # Modified HTTPSyncTarget methods.
+ #
+
@staticmethod
def connect(url, crypto=None):
return LeapSyncTarget(url, crypto=crypto)
@@ -403,15 +404,3 @@ class LeapSyncTarget(HTTPSyncTarget):
res = self._parse_sync_stream(data, return_doc_cb, ensure_callback)
data = None
return res['new_generation'], res['new_transaction_id']
-
- def set_token_credentials(self, address, token):
- self._creds = {'token': (address, token)}
-
- def _sign_request(self, method, url_query, params):
- if 'token' in self._creds:
- address, token = self._creds['token']
- auth = '%s:%s' % (address, token)
- return [('Authorization', 'Token %s' % auth.encode('base64'))]
- else:
- return HTTPSyncTarget._sign_request(
- self, method, url_query, params)
diff --git a/src/leap/soledad/shared_db.py b/src/leap/soledad/shared_db.py
index 01296885..dbb78d97 100644
--- a/src/leap/soledad/shared_db.py
+++ b/src/leap/soledad/shared_db.py
@@ -30,6 +30,12 @@ from u1db import errors
from u1db.remote import http_database
+from leap.soledad.auth import (
+ set_token_credentials,
+ _sign_request,
+)
+
+
#-----------------------------------------------------------------------------
# Soledad shared database
#-----------------------------------------------------------------------------
@@ -58,8 +64,20 @@ class SoledadSharedDatabase(http_database.HTTPDatabase):
# TODO: prevent client from messing with the shared DB.
# TODO: define and document API.
+ #
+ # Token auth methods.
+ #
+
+ set_token_credentials = set_token_credentials
+
+ _sign_request = _sign_request
+
+ #
+ # Modified HTTPDatabase methods.
+ #
+
@staticmethod
- def open_database(url, create, token=None):
+ def open_database(url, create, creds=None):
# TODO: users should not be able to create the shared database, so we
# have to remove this from here in the future.
"""
@@ -76,7 +94,7 @@ class SoledadSharedDatabase(http_database.HTTPDatabase):
@return: The shared database in the given url.
@rtype: SoledadSharedDatabase
"""
- db = SoledadSharedDatabase(url, token=token)
+ db = SoledadSharedDatabase(url, creds=creds)
db.open(create)
return db
@@ -92,7 +110,7 @@ class SoledadSharedDatabase(http_database.HTTPDatabase):
"""
raise Unauthorized("Can't delete shared database.")
- def __init__(self, url, document_factory=None, creds=None, token=None):
+ def __init__(self, url, document_factory=None, creds=None):
"""
Initialize database with auth token and encryption powers.
@@ -103,83 +121,10 @@ class SoledadSharedDatabase(http_database.HTTPDatabase):
@param creds: A tuple containing the authentication method and
credentials.
@type creds: tuple
- @param token: An authentication token for accessing the shared db.
- @type token: str
"""
- self._token = token
http_database.HTTPDatabase.__init__(self, url, document_factory,
creds)
- def _request(self, method, url_parts, params=None, body=None,
- content_type=None, auth=True):
- """
- Perform token-based http request.
-
- @param method: The HTTP method for the request.
- @type method: str
- @param url_parts: A list with extra parts for the URL.
- @type url_parts: list
- @param params: Parameters to be added as query string.
- @type params: dict
- @param body: The body of the request (must be JSON serializable).
- @type body: object
- @param content_type: The content-type of the request.
- @type content_type: str
- @param auth: Should the request be authenticated?
- @type auth: bool
-
- @raise u1db.errors.Unavailable: If response status is 503.
- @raise u1db.errors.HTTPError: If response status is neither 200, 201
- or 503
-
- @return: The headers and body of the HTTP response.
- @rtype: tuple
- """
- # add `auth-token` as a request parameter
- if auth:
- if not self._token:
- raise NoTokenForAuth()
- if not params:
- params = {}
- params['auth_token'] = self._token
- return http_database.HTTPDatabase._request(
- self,
- method, url_parts,
- params,
- body,
- content_type)
-
- def _request_json(self, method, url_parts, params=None, body=None,
- content_type=None, auth=True):
- """
- Perform token-based http request and deserialize the JSON results.
-
- @param method: The HTTP method for the request.
- @type method: str
- @param url_parts: A list with extra parts for the URL.
- @type url_parts: list
- @param params: Parameters to be added as query string.
- @type params: dict
- @param body: The body of the request (must be JSON serializable).
- @type body: object
- @param content_type: The content-type of the request.
- @type content_type: str
- @param auth: Should the request be authenticated?
- @type auth: bool
-
- @raise u1db.errors.Unavailable: If response status is 503.
- @raise u1db.errors.HTTPError: If response status is neither 200, 201
- or 503
-
- @return: The headers and body of the HTTP response.
- @rtype: tuple
- """
- # allow for token-authenticated requests.
- res, headers = self._request(method, url_parts,
- params=params, body=body,
- content_type=content_type, auth=auth)
- return json.loads(res), headers
-
def get_doc_unauth(self, doc_id):
"""
Modified method to allow for unauth request.
diff --git a/src/leap/soledad/tests/test_soledad.py b/src/leap/soledad/tests/test_soledad.py
index caf9be44..1ddfb6a0 100644
--- a/src/leap/soledad/tests/test_soledad.py
+++ b/src/leap/soledad/tests/test_soledad.py
@@ -30,9 +30,13 @@ except ImportError:
import json # noqa
+from mock import Mock
+from leap.common.testing.basetest import BaseLeapTest
from leap.soledad.tests import BaseSoledadTest
from leap.soledad import Soledad
from leap.soledad.crypto import SoledadCrypto
+from leap.soledad.shared_db import SoledadSharedDatabase
+from leap.soledad.backends.leap_backend import LeapDocument
class AuxMethodsTestCase(BaseSoledadTest):
@@ -132,3 +136,56 @@ class AuxMethodsTestCase(BaseSoledadTest):
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())
+
+
+class SoledadSharedDBTestCase(BaseSoledadTest):
+ """
+ These tests ensure the functionalities of the shared recovery database.
+ """
+
+ def setUp(self):
+ BaseSoledadTest.setUp(self)
+ self._shared_db = SoledadSharedDatabase(
+ 'https://provider/', LeapDocument, None)
+
+ def test__fetch_keys_from_shared_db(self):
+ """
+ Ensure the shared db is queried with the correct doc_id.
+ """
+ self._soledad._shared_db = Mock()
+ doc_id = self._soledad._address_hash()
+ self._soledad._fetch_keys_from_shared_db()
+ self.assertTrue(
+ self._soledad._shared_db.get_doc_unauth.assert_called_once(doc_id),
+ 'Wrong doc_id when fetching recovery document.')
+
+ def test__assert_keys_in_shared_db(self):
+ """
+ Ensure recovery document is put into shared recover db.
+ """
+
+ def _put_doc_side_effect(doc):
+ self._doc_put = doc
+
+ class MockSharedDB(object):
+
+ get_doc_unauth = Mock(return_value=None)
+ put_doc = Mock(side_effect=_put_doc_side_effect)
+
+ def __call__(self):
+ return self
+
+ self._soledad._shared_db = MockSharedDB()
+ doc_id = self._soledad._address_hash()
+ self._soledad._assert_keys_in_shared_db()
+ self.assertTrue(
+ self._soledad._shared_db().get_doc_unauth.assert_called_once_with(
+ doc_id) is None,
+ 'Wrong doc_id when fetching recovery document.')
+ self.assertTrue(
+ self._soledad._shared_db.put_doc.assert_called_once_with(
+ self._doc_put) is None,
+ 'Wrong document when putting recovery document.')
+ self.assertTrue(
+ self._doc_put.doc_id == doc_id,
+ 'Wrong doc_id when putting recovery document.')