summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/leap/soledad/__init__.py245
-rw-r--r--src/leap/soledad/backends/leap.py49
-rw-r--r--src/leap/soledad/backends/objectstore.py7
-rw-r--r--src/leap/soledad/tests/test_encrypted.py15
-rw-r--r--src/leap/soledad/tests/test_logs.py2
-rw-r--r--src/leap/soledad/util.py170
6 files changed, 291 insertions, 197 deletions
diff --git a/src/leap/soledad/__init__.py b/src/leap/soledad/__init__.py
index 45034561..835111a5 100644
--- a/src/leap/soledad/__init__.py
+++ b/src/leap/soledad/__init__.py
@@ -3,170 +3,81 @@
"""A U1DB implementation for using Object Stores as its persistence layer."""
import os
-import gnupg
-
-class GPGWrapper():
- """
- This is a temporary class for handling GPG requests, and should be
- replaced by a more general class used throughout the project.
- """
-
- GNUPG_HOME = os.environ['HOME'] + "/.config/leap/gnupg"
- GNUPG_BINARY = "/usr/bin/gpg" # this has to be changed based on OS
-
- def __init__(self, gpghome=GNUPG_HOME, gpgbinary=GNUPG_BINARY):
- self.gpg = gnupg.GPG(gnupghome=gpghome, gpgbinary=gpgbinary)
-
- def find_key(self, email):
- """
- Find user's key based on their email.
- """
- for key in self.gpg.list_keys():
- for uid in key['uids']:
- if re.search(email, uid):
- return key
- raise LookupError("GnuPG public key for %s not found!" % email)
-
- def encrypt(self, data, recipient, sign=None, always_trust=False,
- passphrase=None, symmetric=False):
- return self.gpg.encrypt(data, recipient, sign=sign,
- always_trust=always_trust,
- passphrase=passphrase, symmetric=symmetric)
-
- def decrypt(self, data, always_trust=False, passphrase=None):
- return self.gpg.decrypt(data, always_trust=always_trust,
- passphrase=passphrase)
-
- def import_keys(self, data):
- return self.gpg.import_keys(data)
-
-
-#----------------------------------------------------------------------------
-# u1db Transaction and Sync logs.
-#----------------------------------------------------------------------------
-
-class SimpleLog(object):
- def __init__(self):
- self._log = []
-
- def _set_log(self, log):
- self._log = log
-
- def _get_log(self):
- return self._log
-
- log = property(
- _get_log, _set_log, doc="Log contents.")
-
- def append(self, msg):
- self._log.append(msg)
-
- def reduce(self, func, initializer=None):
- return reduce(func, self.log, initializer)
-
- def map(self, func):
- return map(func, self.log)
-
- def filter(self, func):
- return filter(func, self.log)
-
-
-class TransactionLog(SimpleLog):
- """
- An ordered list of (generation, doc_id, transaction_id) tuples.
- """
-
- def _set_log(self, log):
- self._log = log
-
- def _get_log(self):
- return sorted(self._log, reverse=True)
-
- log = property(
- _get_log, _set_log, doc="Log contents.")
-
- def get_generation(self):
- """
- Return the current generation.
- """
- gens = self.map(lambda x: x[0])
- if not gens:
- return 0
- return max(gens)
-
- def get_generation_info(self):
- """
- Return the current generation and transaction id.
- """
- if not self._log:
- return(0, '')
- info = self.map(lambda x: (x[0], x[2]))
- return reduce(lambda x, y: x if (x[0] > y[0]) else y, info)
-
- def get_trans_id_for_gen(self, gen):
- """
- Get the transaction id corresponding to a particular generation.
- """
- log = self.reduce(lambda x, y: y if y[0] == gen else x)
- if log is None:
- return None
- return log[2]
-
- def whats_changed(self, old_generation):
- """
- Return a list of documents that have changed since old_generation.
- """
- results = self.filter(lambda x: x[0] > old_generation)
- seen = set()
- changes = []
- newest_trans_id = ''
- for generation, doc_id, trans_id in results:
- if doc_id not in seen:
- changes.append((doc_id, generation, trans_id))
- seen.add(doc_id)
- if changes:
- cur_gen = changes[0][1] # max generation
- newest_trans_id = changes[0][2]
- changes.reverse()
- else:
- results = self.log
- if not results:
- cur_gen = 0
- newest_trans_id = ''
- else:
- cur_gen, _, newest_trans_id = results[0]
-
- return cur_gen, newest_trans_id, changes
-
-
-
-class SyncLog(SimpleLog):
- """
- A list of (replica_id, generation, transaction_id) tuples.
- """
-
- def find_by_replica_uid(self, replica_uid):
- if not self.log:
- return ()
- return self.reduce(lambda x, y: y if y[0] == replica_uid else x)
-
- def get_replica_gen_and_trans_id(self, other_replica_uid):
- """
- Return the last known generation and transaction id for the other db
- replica.
- """
- info = self.find_by_replica_uid(other_replica_uid)
- if not info:
- return (0, '')
- return (info[1], info[2])
-
- def set_replica_gen_and_trans_id(self, other_replica_uid,
- other_generation, other_transaction_id):
- """
- Set the last-known generation and transaction id for the other
- database replica.
- """
- self.log = self.filter(lambda x: x[0] != other_replica_uid)
- self.append((other_replica_uid, other_generation,
- other_transaction_id))
-
+import string
+import random
+import cStringIO
+from soledad.util import GPGWrapper
+
+class Soledad(object):
+
+ PREFIX = os.environ['HOME'] + '/.config/leap/soledad'
+ SECRET_PATH = PREFIX + '/secret.gpg'
+ GNUPG_HOME = PREFIX + '/gnupg'
+ SECRET_LENGTH = 50
+
+ def __init__(self, user_email, gpghome=None):
+ self._user_email = user_email
+ if not os.path.isdir(self.PREFIX):
+ os.makedirs(self.PREFIX)
+ if not gpghome:
+ gpghome = self.GNUPG_HOME
+ self._gpg = GPGWrapper(gpghome=gpghome)
+ # load OpenPGP keypair
+ if not self._has_openpgp_keypair():
+ self._gen_openpgp_keypair()
+ self._load_openpgp_keypair()
+ # load secret
+ if not self._has_secret():
+ self._gen_secret()
+ self._load_secret()
+
+ def _has_secret(self):
+ if os.path.isfile(self.SECRET_PATH):
+ return True
+ return False
+
+ def _load_secret(self):
+ try:
+ with open(self.SECRET_PATH) as f:
+ self._secret = self._gpg.decrypt(f.read())
+ except IOError as e:
+ raise IOError('Failed to open secret file %s.' % self.SECRET_PATH)
+
+ def _gen_secret(self):
+ self._secret = ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(self.SECRET_LENGTH))
+ cyphertext = self._gpg.encrypt(self._secret, self._fingerprint, self._fingerprint)
+ f = open(self.SECRET_PATH, 'w')
+ f.write(str(cyphertext))
+ f.close()
+
+
+ def _has_openpgp_keypair(self):
+ if self._gpg.find_key(self._user_email):
+ return True
+ return False
+
+ def _gen_openpgp_keypair(self):
+ params = self._gpg.gen_key_input(
+ key_type='RSA',
+ key_length=4096,
+ name_real=self._user_email,
+ name_email=self._user_email,
+ name_comment='Generated by LEAP Soledad.')
+ self._gpg.gen_key(params)
+
+ def _load_openpgp_keypair(self):
+ self._fingerprint = self._gpg.find_key(self._user_email)['fingerprint']
+
+ def encrypt(self, data, sign=None, passphrase=None, symmetric=False):
+ return str(self._gpg.encrypt(data, self._fingerprint, sign=sign,
+ passphrase=passphrase, symmetric=symmetric))
+
+ def encrypt_symmetric(self, data, sign=None):
+ return self.encrypt(data, sign=sign, passphrase=self._secret,
+ symmetric=True)
+
+ def decrypt(self, data, passphrase=None, symmetric=False):
+ return str(self._gpg.decrypt(data, passphrase=passphrase))
+
+ def decrypt_symmetric(self, data):
+ return self.decrypt(data, passphrase=self._secret)
diff --git a/src/leap/soledad/backends/leap.py b/src/leap/soledad/backends/leap.py
index c113f5c2..4a496d3e 100644
--- a/src/leap/soledad/backends/leap.py
+++ b/src/leap/soledad/backends/leap.py
@@ -7,12 +7,15 @@ from u1db import Document
from u1db.remote.http_target import HTTPSyncTarget
from u1db.remote.http_database import HTTPDatabase
import base64
-from soledad import GPGWrapper
+from soledad.util import GPGWrapper
class NoDefaultKey(Exception):
pass
+class NoSoledadInstance(Exception):
+ pass
+
class LeapDocument(Document):
"""
@@ -22,41 +25,40 @@ class LeapDocument(Document):
"""
def __init__(self, doc_id=None, rev=None, json='{}', has_conflicts=False,
- encrypted_json=None, default_key=None, gpg_wrapper=None):
+ encrypted_json=None, soledad=None):
super(LeapDocument, self).__init__(doc_id, rev, json, has_conflicts)
- # we might want to get already initialized wrappers for testing.
- if gpg_wrapper is None:
- self._gpg = GPGWrapper()
- else:
- self._gpg = gpg_wrapper
+ self._soledad = soledad
if encrypted_json:
self.set_encrypted_json(encrypted_json)
- self._default_key = default_key
def get_encrypted_json(self):
"""
Returns document's json serialization encrypted with user's public key.
"""
- if self._default_key is None:
- raise NoDefaultKey()
- cyphertext = self._gpg.encrypt(self.get_json(),
- self._default_key,
- always_trust = True)
- # TODO: always trust?
- return json.dumps({'_encrypted_json' : str(cyphertext)})
+ if not self._soledad:
+ raise NoSoledadInstance()
+ cyphertext = self._soledad.encrypt_symmetric(self.get_json())
+ return json.dumps({'_encrypted_json' : cyphertext})
def set_encrypted_json(self, encrypted_json):
"""
Set document's content based on encrypted version of json string.
"""
+ if not self._soledad:
+ raise NoSoledadInstance()
cyphertext = json.loads(encrypted_json)['_encrypted_json']
- plaintext = str(self._gpg.decrypt(cyphertext))
+ plaintext = self._soledad.decrypt_symmetric(cyphertext)
return self.set_json(plaintext)
class LeapDatabase(HTTPDatabase):
"""Implement the HTTP remote database API to a Leap server."""
+ def __init__(self, url, document_factory=None, creds=None, soledad=None):
+ super(LeapDatabase, self).__init__(url, creds=creds)
+ self._soledad = soledad
+ self._factory = LeapDocument
+
@staticmethod
def open_database(url, create):
db = LeapDatabase(url)
@@ -74,9 +76,21 @@ class LeapDatabase(HTTPDatabase):
st._creds = self._creds
return st
+ def create_doc_from_json(self, content, doc_id=None):
+ if doc_id is None:
+ doc_id = self._allocate_doc_id()
+ res, headers = self._request_json('PUT', ['doc', doc_id], {},
+ content, 'application/json')
+ new_doc = self._factory(doc_id, res['rev'], content, soledad=self._soledad)
+ return new_doc
+
class LeapSyncTarget(HTTPSyncTarget):
+ def __init__(self, url, creds=None, soledad=None):
+ super(LeapSyncTarget, self).__init__(url, creds)
+ self._soledad = soledad
+
def _parse_sync_stream(self, data, return_doc_cb, ensure_callback=None):
"""
Does the same as parent's method but ensures incoming content will be
@@ -99,7 +113,8 @@ class LeapSyncTarget(HTTPSyncTarget):
entry = json.loads(line)
# decrypt after receiving from server.
doc = LeapDocument(entry['id'], entry['rev'],
- encrypted_json=entry['content'])
+ encrypted_json=entry['content'],
+ soledad=self._soledad)
return_doc_cb(doc, entry['gen'], entry['trans_id'])
if parts[-1] != ']':
try:
diff --git a/src/leap/soledad/backends/objectstore.py b/src/leap/soledad/backends/objectstore.py
index 298bdda3..a8e139f7 100644
--- a/src/leap/soledad/backends/objectstore.py
+++ b/src/leap/soledad/backends/objectstore.py
@@ -1,8 +1,7 @@
import uuid
from u1db.backends import CommonBackend
-from u1db import errors
-from soledad import SyncLog, TransactionLog
-from soledad.backends.leap import LeapDocument
+from u1db import errors, Document
+from soledad.util import SyncLog, TransactionLog
class ObjectStore(CommonBackend):
@@ -11,7 +10,7 @@ class ObjectStore(CommonBackend):
# This initialization method should be called after the connection
# with the database is established, so it can ensure that u1db data is
# configured and up-to-date.
- self.set_document_factory(LeapDocument)
+ self.set_document_factory(Document)
self._sync_log = SyncLog()
self._transaction_log = TransactionLog()
self._ensure_u1db_data()
diff --git a/src/leap/soledad/tests/test_encrypted.py b/src/leap/soledad/tests/test_encrypted.py
index 2333fc41..eafd258e 100644
--- a/src/leap/soledad/tests/test_encrypted.py
+++ b/src/leap/soledad/tests/test_encrypted.py
@@ -7,7 +7,7 @@ import unittest2 as unittest
import os
import u1db
-from soledad import GPGWrapper
+from soledad import Soledad
from soledad.backends.leap import LeapDocument
@@ -17,28 +17,27 @@ class EncryptedSyncTestCase(unittest.TestCase):
GNUPG_HOME = "%s/gnupg" % PREFIX
DB1_FILE = "%s/db1.u1db" % PREFIX
DB2_FILE = "%s/db2.u1db" % PREFIX
+ EMAIL = 'leap@leap.se'
def setUp(self):
self.db1 = u1db.open(self.DB1_FILE, create=True,
document_factory=LeapDocument)
self.db2 = u1db.open(self.DB2_FILE, create=True,
document_factory=LeapDocument)
- self.gpg = GPGWrapper(gpghome=self.GNUPG_HOME)
- self.gpg.import_keys(PUBLIC_KEY)
- self.gpg.import_keys(PRIVATE_KEY)
+ self.soledad = Soledad(self.EMAIL, gpghome=self.GNUPG_HOME)
+ self.soledad._gpg.import_keys(PUBLIC_KEY)
+ self.soledad._gpg.import_keys(PRIVATE_KEY)
def tearDown(self):
os.unlink(self.DB1_FILE)
os.unlink(self.DB2_FILE)
def test_get_set_encrypted(self):
- doc1 = LeapDocument(gpg_wrapper = self.gpg,
- default_key = KEY_FINGERPRINT)
+ doc1 = LeapDocument(soledad=self.soledad)
doc1.content = { 'key' : 'val' }
doc2 = LeapDocument(doc_id=doc1.doc_id,
encrypted_json=doc1.get_encrypted_json(),
- gpg_wrapper=self.gpg,
- default_key = KEY_FINGERPRINT)
+ soledad=self.soledad)
res1 = doc1.get_json()
res2 = doc2.get_json()
self.assertEqual(res1, res2, 'incorrect document encryption')
diff --git a/src/leap/soledad/tests/test_logs.py b/src/leap/soledad/tests/test_logs.py
index a68e0262..d61700f2 100644
--- a/src/leap/soledad/tests/test_logs.py
+++ b/src/leap/soledad/tests/test_logs.py
@@ -1,5 +1,5 @@
import unittest2 as unittest
-from soledad import TransactionLog, SyncLog
+from soledad.util import TransactionLog, SyncLog
class LogTestCase(unittest.TestCase):
diff --git a/src/leap/soledad/util.py b/src/leap/soledad/util.py
new file mode 100644
index 00000000..1485fce1
--- /dev/null
+++ b/src/leap/soledad/util.py
@@ -0,0 +1,170 @@
+import os
+import gnupg
+import re
+
+class GPGWrapper():
+ """
+ This is a temporary class for handling GPG requests, and should be
+ replaced by a more general class used throughout the project.
+ """
+
+ GNUPG_HOME = os.environ['HOME'] + "/.config/leap/gnupg"
+ GNUPG_BINARY = "/usr/bin/gpg" # this has to be changed based on OS
+
+ def __init__(self, gpghome=GNUPG_HOME, gpgbinary=GNUPG_BINARY):
+ self.gpg = gnupg.GPG(gnupghome=gpghome, gpgbinary=gpgbinary)
+
+ def find_key(self, email):
+ """
+ Find user's key based on their email.
+ """
+ for key in self.gpg.list_keys():
+ for uid in key['uids']:
+ if re.search(email, uid):
+ return key
+ raise LookupError("GnuPG public key for %s not found!" % email)
+
+ def encrypt(self, data, recipient, sign=None, always_trust=True,
+ passphrase=None, symmetric=False):
+ return self.gpg.encrypt(data, recipient, sign=sign,
+ always_trust=always_trust,
+ passphrase=passphrase, symmetric=symmetric)
+
+ def decrypt(self, data, always_trust=True, passphrase=None):
+ result = self.gpg.decrypt(data, always_trust=always_trust,
+ passphrase=passphrase)
+ return result
+
+ def import_keys(self, data):
+ return self.gpg.import_keys(data)
+
+
+#----------------------------------------------------------------------------
+# u1db Transaction and Sync logs.
+#----------------------------------------------------------------------------
+
+class SimpleLog(object):
+ def __init__(self):
+ self._log = []
+
+ def _set_log(self, log):
+ self._log = log
+
+ def _get_log(self):
+ return self._log
+
+ log = property(
+ _get_log, _set_log, doc="Log contents.")
+
+ def append(self, msg):
+ self._log.append(msg)
+
+ def reduce(self, func, initializer=None):
+ return reduce(func, self.log, initializer)
+
+ def map(self, func):
+ return map(func, self.log)
+
+ def filter(self, func):
+ return filter(func, self.log)
+
+
+class TransactionLog(SimpleLog):
+ """
+ An ordered list of (generation, doc_id, transaction_id) tuples.
+ """
+
+ def _set_log(self, log):
+ self._log = log
+
+ def _get_log(self):
+ return sorted(self._log, reverse=True)
+
+ log = property(
+ _get_log, _set_log, doc="Log contents.")
+
+ def get_generation(self):
+ """
+ Return the current generation.
+ """
+ gens = self.map(lambda x: x[0])
+ if not gens:
+ return 0
+ return max(gens)
+
+ def get_generation_info(self):
+ """
+ Return the current generation and transaction id.
+ """
+ if not self._log:
+ return(0, '')
+ info = self.map(lambda x: (x[0], x[2]))
+ return reduce(lambda x, y: x if (x[0] > y[0]) else y, info)
+
+ def get_trans_id_for_gen(self, gen):
+ """
+ Get the transaction id corresponding to a particular generation.
+ """
+ log = self.reduce(lambda x, y: y if y[0] == gen else x)
+ if log is None:
+ return None
+ return log[2]
+
+ def whats_changed(self, old_generation):
+ """
+ Return a list of documents that have changed since old_generation.
+ """
+ results = self.filter(lambda x: x[0] > old_generation)
+ seen = set()
+ changes = []
+ newest_trans_id = ''
+ for generation, doc_id, trans_id in results:
+ if doc_id not in seen:
+ changes.append((doc_id, generation, trans_id))
+ seen.add(doc_id)
+ if changes:
+ cur_gen = changes[0][1] # max generation
+ newest_trans_id = changes[0][2]
+ changes.reverse()
+ else:
+ results = self.log
+ if not results:
+ cur_gen = 0
+ newest_trans_id = ''
+ else:
+ cur_gen, _, newest_trans_id = results[0]
+
+ return cur_gen, newest_trans_id, changes
+
+
+
+class SyncLog(SimpleLog):
+ """
+ A list of (replica_id, generation, transaction_id) tuples.
+ """
+
+ def find_by_replica_uid(self, replica_uid):
+ if not self.log:
+ return ()
+ return self.reduce(lambda x, y: y if y[0] == replica_uid else x)
+
+ def get_replica_gen_and_trans_id(self, other_replica_uid):
+ """
+ Return the last known generation and transaction id for the other db
+ replica.
+ """
+ info = self.find_by_replica_uid(other_replica_uid)
+ if not info:
+ return (0, '')
+ return (info[1], info[2])
+
+ def set_replica_gen_and_trans_id(self, other_replica_uid,
+ other_generation, other_transaction_id):
+ """
+ Set the last-known generation and transaction id for the other
+ database replica.
+ """
+ self.log = self.filter(lambda x: x[0] != other_replica_uid)
+ self.append((other_replica_uid, other_generation,
+ other_transaction_id))
+