summaryrefslogtreecommitdiff
path: root/src/leap/soledad/__init__.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/soledad/__init__.py')
-rw-r--r--src/leap/soledad/__init__.py168
1 files changed, 139 insertions, 29 deletions
diff --git a/src/leap/soledad/__init__.py b/src/leap/soledad/__init__.py
index c7f8cff3..ea3f676b 100644
--- a/src/leap/soledad/__init__.py
+++ b/src/leap/soledad/__init__.py
@@ -36,6 +36,7 @@ import scrypt
import httplib
import socket
import ssl
+import errno
from xdg import BaseDirectory
@@ -47,9 +48,92 @@ from u1db.remote.ssl_match_hostname import ( # noqa
)
-from leap.common import events
-from leap.common.check import leap_assert
-from leap.common.files import mkdir_p
+#
+# Assert functions
+#
+
+def soledad_assert(condition, message):
+ """
+ Asserts the condition and displays the message if that's not
+ met.
+
+ @param condition: condition to check
+ @type condition: bool
+ @param message: message to display if the condition isn't met
+ @type message: str
+ """
+ assert condition, message
+
+
+# we want to use leap.common.check.leap_assert in case it is available,
+# because it also logs in a way other parts of leap can access log messages.
+try:
+ from leap.common.check import leap_assert
+ soledad_assert = leap_assert
+except ImportError:
+ pass
+
+
+def soledad_assert_type(var, expectedType):
+ """
+ Helper assert check for a variable's expected type
+
+ @param var: variable to check
+ @type var: any
+ @param expectedType: type to check agains
+ @type expectedType: type
+ """
+ soledad_assert(isinstance(var, expectedType),
+ "Expected type %r instead of %r" %
+ (expectedType, type(var)))
+
+try:
+ from leap.common.check import leap_assert_type
+ soledad_assert_type = leap_assert_type
+except ImportError:
+ pass
+
+
+#
+# Signaling function
+#
+
+# we define a fake signaling function and fake signal constants that will
+# allow for logging signaling attempts in case leap.common.events is not
+# available.
+
+def signal(signal, content=""):
+ logger.info("Would signal: %s - %s." % (str(signal), content))
+
+SOLEDAD_CREATING_KEYS = 'Creating keys...'
+SOLEDAD_DONE_CREATING_KEYS = 'Done creating keys.'
+SOLEDAD_DOWNLOADING_KEYS = 'Downloading keys...'
+SOLEDAD_DONE_DOWNLOADING_KEYS = 'Done downloading keys.'
+SOLEDAD_UPLOADING_KEYS = 'Uploading keys...'
+SOLEDAD_DONE_UPLOADING_KEYS = 'Done uploading keys.'
+SOLEDAD_NEW_DATA_TO_SYNC = 'New data available.'
+SOLEDAD_DONE_DATA_SYNC = 'Done data sync.'
+
+# we want to use leap.common.events to emits signals, if it is available.
+try:
+ from leap.common import events
+ # replace fake signaling function with real one
+ signal = events.signal
+ # replace fake string signals with real signals
+ SOLEDAD_CREATING_KEYS = events.events_pb2.SOLEDAD_CREATING_KEYS
+ SOLEDAD_DONE_CREATING_KEYS = events.events_pb2.SOLEDAD_DONE_CREATING_KEYS
+ SOLEDAD_DOWNLOADING_KEYS = events.events_pb2.SOLEDAD_DOWNLOADING_KEYS
+ SOLEDAD_DONE_DOWNLOADING_KEYS = \
+ events.events_pb2.SOLEDAD_DONE_DOWNLOADING_KEYS
+ SOLEDAD_UPLOADING_KEYS = events.events_pb2.SOLEDAD_UPLOADING_KEYS
+ SOLEDAD_DONE_UPLOADING_KEYS = \
+ events.events_pb2.SOLEDAD_DONE_UPLOADING_KEYS
+ SOLEDAD_NEW_DATA_TO_SYNC = events.events_pb2.SOLEDAD_NEW_DATA_TO_SYNC
+ SOLEDAD_DONE_DATA_SYNC = events.events_pb2.SOLEDAD_DONE_DATA_SYNC
+except ImportError:
+ pass
+
+
from leap.soledad.backends import sqlcipher
from leap.soledad.backends.leap_backend import (
LeapDocument,
@@ -64,6 +148,10 @@ from leap.soledad.crypto import SoledadCrypto
logger = logging.getLogger(name=__name__)
+#
+# Constants
+#
+
SOLEDAD_CERT = None
"""
Path to the certificate file used to certify the SSL connection between
@@ -226,7 +314,7 @@ class Soledad(object):
self.DEFAULT_PREFIX, self.LOCAL_DATABASE_FILE_NAME)
# initialize server_url
self._server_url = server_url
- leap_assert(
+ soledad_assert(
self._server_url is not None,
'Missing URL for Soledad server.')
@@ -295,23 +383,48 @@ class Soledad(object):
[self._local_db_path, self._secrets_path])
for path in paths:
logger.info('Creating directory: %s.' % path)
- mkdir_p(path)
+ try:
+ os.makedirs(path)
+ except OSError as exc:
+ if exc.errno == errno.EEXIST and os.path.isdir(path):
+ pass
+ else:
+ raise
def _init_db(self):
"""
Initialize the U1DB SQLCipher database for local storage.
- The local storage passphrase is hexlified version of the last
- C{LOCAL_STORAGE_SECRET_LENGTH} bytes of the storage secret.
- """
+ Currently, Soledad uses the default SQLCipher cipher, i.e.
+ 'aes-256-cbc'. We use scrypt to derive a 256-bit encryption key and
+ uses the 'raw PRAGMA key' format to handle the key to SQLCipher.
+
+ The first C{self.REMOTE_STORAGE_SECRET_LENGTH} bytes of the storage
+ secret are used for remote storage encryption. We use the next
+ C{self.LOCAL_STORAGE_SECRET} bytes to derive a key for local storage.
+ From these bytes, the first C{self.SALT_LENGTH} are used as the salt
+ and the rest as the password for the scrypt hashing.
+ """
+ # salt indexes
+ salt_start = self.REMOTE_STORAGE_SECRET_LENGTH
+ salt_end = salt_start + self.SALT_LENGTH
+ # password indexes
+ pwd_start = salt_end
+ pwd_end = salt_start + self.LOCAL_STORAGE_SECRET_LENGTH
+ # calculate the key for local encryption
+ secret = self._get_storage_secret()
+ key = scrypt.hash(
+ secret[pwd_start:pwd_end], # the password
+ secret[salt_start:salt_end], # the salt
+ buflen=32, # we need a key with 256 bits (32 bytes)
+ )
self._db = sqlcipher.open(
self._local_db_path,
- # storage secret is binary but sqlcipher passphrase must be string
- binascii.b2a_hex(
- self._get_storage_secret()[self.LOCAL_STORAGE_SECRET_LENGTH:]),
+ binascii.b2a_hex(key), # sqlcipher only accepts the hex version
create=True,
document_factory=LeapDocument,
- crypto=self._crypto)
+ crypto=self._crypto,
+ raw_key=True)
def close(self):
"""
@@ -420,8 +533,8 @@ class Soledad(object):
This method emits the following signals:
- * leap.common.events.events_pb2.SOLEDAD_CREATING_KEYS
- * leap.common.events.events_pb2.SOLEDAD_DONE_CREATING_KEYS
+ * SOLEDAD_CREATING_KEYS
+ * SOLEDAD_DONE_CREATING_KEYS
A secret has the following structure:
@@ -439,7 +552,7 @@ class Soledad(object):
@return: The id of the generated secret.
@rtype: str
"""
- events.signal(events.events_pb2.SOLEDAD_CREATING_KEYS, self._uuid)
+ signal(SOLEDAD_CREATING_KEYS, self._uuid)
# generate random secret
secret = os.urandom(self.GENERATED_SECRET_LENGTH)
secret_id = sha256(secret).hexdigest()
@@ -460,8 +573,7 @@ class Soledad(object):
str(iv), self.IV_SEPARATOR, binascii.b2a_base64(ciphertext)),
}
self._store_secrets()
- events.signal(
- events.events_pb2.SOLEDAD_DONE_CREATING_KEYS, self._uuid)
+ signal(SOLEDAD_DONE_CREATING_KEYS, self._uuid)
return secret_id
def _store_secrets(self):
@@ -524,15 +636,13 @@ class Soledad(object):
@return: a document with encrypted key material in its contents
@rtype: LeapDocument
"""
- events.signal(
- events.events_pb2.SOLEDAD_DOWNLOADING_KEYS, self._uuid)
+ signal(SOLEDAD_DOWNLOADING_KEYS, self._uuid)
db = self._shared_db()
if not db:
logger.warning('No shared db found')
return
doc = db.get_doc(self._uuid_hash())
- events.signal(
- events.events_pb2.SOLEDAD_DONE_DOWNLOADING_KEYS, self._uuid)
+ signal(SOLEDAD_DONE_DOWNLOADING_KEYS, self._uuid)
return doc
def _put_secrets_in_shared_db(self):
@@ -544,7 +654,7 @@ class Soledad(object):
Otherwise, upload keys to shared recovery database.
"""
- leap_assert(
+ soledad_assert(
self._has_secret(),
'Tried to send keys to server but they don\'t exist in local '
'storage.')
@@ -555,15 +665,13 @@ class Soledad(object):
# fill doc with encrypted secrets
doc.content = self.export_recovery_document(include_uuid=False)
# upload secrets to server
- events.signal(
- events.events_pb2.SOLEDAD_UPLOADING_KEYS, self._uuid)
+ signal(SOLEDAD_UPLOADING_KEYS, self._uuid)
db = self._shared_db()
if not db:
logger.warning('No shared db found')
return
db.put_doc(doc)
- events.signal(
- events.events_pb2.SOLEDAD_DONE_UPLOADING_KEYS, self._uuid)
+ signal(SOLEDAD_DONE_UPLOADING_KEYS, self._uuid)
#
# Document storage, retrieval and sync.
@@ -816,7 +924,7 @@ class Soledad(object):
local_gen = self._db.sync(
urlparse.urljoin(self.server_url, 'user-%s' % self._uuid),
creds=self._creds, autocreate=True)
- events.signal(events.events_pb2.SOLEDAD_DONE_DATA_SYNC, self._uuid)
+ signal(SOLEDAD_DONE_DATA_SYNC, self._uuid)
return local_gen
def need_sync(self, url):
@@ -833,8 +941,7 @@ class Soledad(object):
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]:
- events.signal(
- events.events_pb2.SOLEDAD_NEW_DATA_TO_SYNC, self._uuid)
+ signal(SOLEDAD_NEW_DATA_TO_SYNC, self._uuid)
return True
return False
@@ -986,3 +1093,6 @@ class VerifiedHTTPSConnection(httplib.HTTPSConnection):
old__VerifiedHTTPSConnection = http_client._VerifiedHTTPSConnection
http_client._VerifiedHTTPSConnection = VerifiedHTTPSConnection
+
+
+__all__ = ['soledad_assert', 'Soledad']