diff options
| -rw-r--r-- | src/leap/soledad/__init__.py | 245 | ||||
| -rw-r--r-- | src/leap/soledad/backends/leap.py | 53 | ||||
| -rw-r--r-- | src/leap/soledad/backends/objectstore.py | 7 | ||||
| -rw-r--r-- | src/leap/soledad/tests/test_encrypted.py | 15 | ||||
| -rw-r--r-- | src/leap/soledad/tests/test_logs.py | 2 | ||||
| -rw-r--r-- | src/leap/soledad/util.py | 170 | 
6 files changed, 294 insertions, 198 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 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)) + | 
