From 7a67c36efd95d86dea04ab0741c68f5307a95c09 Mon Sep 17 00:00:00 2001 From: drebs Date: Tue, 18 Dec 2012 18:51:01 -0200 Subject: Refactor and symmetric encryption --- src/leap/soledad/__init__.py | 245 ++++++++++--------------------- src/leap/soledad/backends/leap.py | 53 ++++--- src/leap/soledad/backends/objectstore.py | 7 +- src/leap/soledad/tests/test_encrypted.py | 15 +- src/leap/soledad/tests/test_logs.py | 2 +- src/leap/soledad/util.py | 170 +++++++++++++++++++++ 6 files changed, 294 insertions(+), 198 deletions(-) create mode 100644 src/leap/soledad/util.py (limited to 'src/leap') 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 ce00c8f3..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({'cyphertext' : 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. """ - cyphertext = json.loads(encrypted_json)['cyphertext'] - plaintext = str(self._gpg.decrypt(cyphertext)) + if not self._soledad: + raise NoSoledadInstance() + cyphertext = json.loads(encrypted_json)['_encrypted_json'] + 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 @@ -97,8 +111,10 @@ class LeapSyncTarget(HTTPSyncTarget): raise BrokenSyncStream line, comma = utils.check_and_strip_comma(entry) 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: @@ -142,6 +158,7 @@ class LeapSyncTarget(HTTPSyncTarget): ensure=ensure_callback is not None) comma = ',' for doc, gen, trans_id in docs_by_generations: + # encrypt before sending to server. size += prepare(id=doc.doc_id, rev=doc.rev, content=doc.get_encrypted_json(), gen=gen, trans_id=trans_id) 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)) + -- cgit v1.2.3