diff options
| author | drebs <drebs@leap.se> | 2013-04-23 12:52:06 -0300 | 
|---|---|---|
| committer | drebs <drebs@leap.se> | 2013-04-23 14:26:46 -0300 | 
| commit | 4c74a91ad905e7d59260ccec789930c16b29a62d (patch) | |
| tree | de517b9eb258d42c581f7d878f4b8573986649cf | |
| parent | b48f000e311daf543a8b8f776c5438725485bffd (diff) | |
Add encryption_scheme property to LeapDocument.
| -rw-r--r-- | src/leap/soledad/backends/couch.py | 9 | ||||
| -rw-r--r-- | src/leap/soledad/backends/leap_backend.py | 72 | ||||
| -rw-r--r-- | src/leap/soledad/backends/sqlcipher.py | 41 | ||||
| -rw-r--r-- | src/leap/soledad/crypto.py | 186 | ||||
| -rw-r--r-- | src/leap/soledad/tests/__init__.py | 2 | ||||
| -rw-r--r-- | src/leap/soledad/tests/test_couch.py | 35 | ||||
| -rw-r--r-- | src/leap/soledad/tests/test_leap_backend.py | 2 | ||||
| -rw-r--r-- | src/leap/soledad/tests/test_sqlcipher.py | 30 | 
8 files changed, 350 insertions, 27 deletions
| diff --git a/src/leap/soledad/backends/couch.py b/src/leap/soledad/backends/couch.py index 6440232d..13b6733c 100644 --- a/src/leap/soledad/backends/couch.py +++ b/src/leap/soledad/backends/couch.py @@ -35,6 +35,8 @@ from u1db.remote.server_state import ServerState  from u1db.errors import DatabaseDoesNotExist  from couchdb.client import Server, Document as CouchDocument  from couchdb.http import ResourceNotFound + +  from leap.soledad.backends.objectstore import (      ObjectStoreDatabase,      ObjectStoreSyncTarget, @@ -142,7 +144,8 @@ class CouchDatabase(ObjectStoreDatabase):          doc = self._factory(              doc_id=doc_id,              rev=cdoc['u1db_rev'], -            has_conflicts=has_conflicts) +            has_conflicts=has_conflicts, +            encryption_scheme=cdoc['encryption_scheme'])          contents = self._database.get_attachment(cdoc, 'u1db_json')          if contents:              doc.content = json.loads(contents.read()) @@ -192,12 +195,14 @@ class CouchDatabase(ObjectStoreDatabase):          # prepare couch's Document          cdoc = CouchDocument()          cdoc['_id'] = doc.doc_id -        # we have to guarantee that couch's _rev is cosistent +        # we have to guarantee that couch's _rev is consistent          old_cdoc = self._database.get(doc.doc_id)          if old_cdoc is not None:              cdoc['_rev'] = old_cdoc['_rev']          # store u1db's rev          cdoc['u1db_rev'] = doc.rev +        # store document's encryption scheme +        cdoc['encryption_scheme'] = doc.encryption_scheme          # save doc in db          self._database.save(cdoc)          # store u1db's content as json string diff --git a/src/leap/soledad/backends/leap_backend.py b/src/leap/soledad/backends/leap_backend.py index 687b59ef..dfec9e85 100644 --- a/src/leap/soledad/backends/leap_backend.py +++ b/src/leap/soledad/backends/leap_backend.py @@ -56,6 +56,16 @@ class DocumentNotEncrypted(Exception):      pass +class EncryptionSchemes(object): +    """ +    Representation of encryption schemes used to encrypt documents. +    """ + +    NONE = 'none' +    SYMKEY = 'symkey' +    PUBKEY = 'pubkey' + +  class LeapDocument(Document):      """      Encryptable and syncable document. @@ -69,7 +79,8 @@ class LeapDocument(Document):      """      def __init__(self, doc_id=None, rev=None, json='{}', has_conflicts=False, -                 encrypted_json=None, crypto=None, syncable=True): +                 encrypted_json=None, crypto=None, syncable=True, +                 encryption_scheme=EncryptionSchemes.NONE):          """          Container for handling an encryptable document. @@ -89,10 +100,14 @@ class LeapDocument(Document):          @type crypto: soledad.Soledad          @param syncable: Should this document be synced with remote replicas?          @type syncable: bool +        @param encryption_scheme: The encryption scheme for this objects' +            contents. +        @type encryption_scheme: str          """          Document.__init__(self, doc_id, rev, json, has_conflicts)          self._crypto = crypto          self._syncable = syncable +        self._encryption_scheme = encryption_scheme          if encrypted_json:              self.set_encrypted_json(encrypted_json) @@ -118,6 +133,7 @@ class LeapDocument(Document):              cyphertext,              self._crypto._hash_passphrase(self.doc_id))          self.set_json(plaintext) +        self.encryption_scheme = EncryptionSchemes.NONE      def get_encrypted_json(self):          """ @@ -163,6 +179,31 @@ class LeapDocument(Document):          doc="Determine if document should be synced with server."      ) +    def _get_encryption_scheme(self): +        """ +        Return the encryption scheme used to encrypt this document's contents. + +        @return: The encryption scheme used to encrypt this document's +            contents. +        @rtype: str +        """ +        return self._encryption_scheme + +    def _set_encryption_scheme(self, encryption_scheme=True): +        """ +        Set the encryption scheme used to encrypt this document's contents. + +        @param encryption_scheme: The encryption scheme. +        @type encryption_scheme: str +        """ +        self._encryption_scheme = encryption_scheme + +    encryption_scheme = property( +        _get_encryption_scheme, +        _set_encryption_scheme, +        doc="The encryption scheme used to encrypt this document's contents." +    ) +      def _get_rev(self):          """          Get the document revision. @@ -249,13 +290,31 @@ class LeapSyncTarget(HTTPSyncTarget):                  # decrypt after receiving from server.                  if not self._crypto:                      raise NoSoledadCryptoInstance() +                # all content arriving should be encrypted either with the +                # user's symmetric key or with the user's public key.                  enc_json = json.loads(entry['content'])['_encrypted_json'] -                if not self._crypto.is_encrypted_sym(enc_json): +                plain_json = None +                if entry['encryption_scheme'] == EncryptionScheme.SYMKEY: +                    if not self._crypto.is_encrypted_sym(enc_json): +                        raise DocumentNotEncrypted( +                            'Incoming document\'s contents should be ' +                            'encrypted with a symmetric key.') +                    plain_json = self._crypto.decrypt_symmetric( +                        enc_json, self._crypto._symkey) +                elif entry['encryption_scheme'] == EncryptionScheme.PUBKEY: +                    if not self._crypto.is_encrypted_asym(enc_json): +                        raise DocumentNotEncrypted( +                            'Incoming document\'s contents should be ' +                            'encrypted to the user\'s public key.') +                    plain_json = self._crypto.decrypt(enc_json) +                else:                      raise DocumentNotEncrypted(                          "Incoming document from sync is not encrypted.") +                # if decryption was OK, then create the document.                  doc = LeapDocument(entry['id'], entry['rev'], -                                   encrypted_json=entry['content'], -                                   crypto=self._crypto) +                                   json=plain_json, +                                   crypto=self._crypto, +                                   encryption_scheme=EncryptionScheme.NONE)                  return_doc_cb(doc, entry['gen'], entry['trans_id'])          if parts[-1] != ']':              try: @@ -307,8 +366,9 @@ class LeapSyncTarget(HTTPSyncTarget):                      raise DocumentNotEncrypted(                          "Could not encrypt document before sync.")                  size += prepare(id=doc.doc_id, rev=doc.rev, -                                content=doc.get_encrypted_json(), -                                gen=gen, trans_id=trans_id) +                                content=enc_json, +                                gen=gen, trans_id=trans_id, +                                encryption_scheme=EncryptionSchemes.SYMKEY)          entries.append('\r\n]')          size += len(entries[-1])          self._conn.putheader('content-length', str(size)) diff --git a/src/leap/soledad/backends/sqlcipher.py b/src/leap/soledad/backends/sqlcipher.py index 288680d4..fb5c3e79 100644 --- a/src/leap/soledad/backends/sqlcipher.py +++ b/src/leap/soledad/backends/sqlcipher.py @@ -19,23 +19,22 @@  """A U1DB backend that uses SQLCipher as its persistence layer."""  import os -from pysqlcipher import dbapi2  import time -# TODO: uncomment imports below after solving circular dependency issue -# between leap_client and soledad. -#from leap import util -from u1db.backends import sqlite_backend -#util.logger.debug( -#    "Monkey-patching u1db.backends.sqlite_backend with pysqlcipher.dbapi2..." -#) -sqlite_backend.dbapi2 = dbapi2 +from u1db.backends import sqlite_backend +from pysqlcipher import dbapi2  from u1db import (      errors,  ) +from leap.soledad.backends.leap_backend import ( +    LeapDocument, +    EncryptionSchemes, +) + -from leap.soledad.backends.leap_backend import LeapDocument +# Monkey-patch u1db.backends.sqlite_backend with pysqlcipher.dbapi2 +sqlite_backend.dbapi2 = dbapi2  def open(path, password, create=True, document_factory=None, crypto=None): @@ -101,12 +100,14 @@ class SQLCipherDatabase(sqlite_backend.SQLitePartialExpandDatabase):          self._crypto = crypto          def factory(doc_id=None, rev=None, json='{}', has_conflicts=False, -                    encrypted_json=None, syncable=True): +                    encrypted_json=None, syncable=True, +                    encryption_scheme=EncryptionSchemes.NONE):              return LeapDocument(doc_id=doc_id, rev=rev, json=json,                                  has_conflicts=has_conflicts,                                  encrypted_json=encrypted_json, +                                crypto=self._crypto,                                  syncable=syncable, -                                crypto=self._crypto) +                                encryption_scheme=encryption_scheme)          self.set_document_factory(factory)      def _check_if_db_is_encrypted(self, sqlcipher_file): @@ -247,6 +248,10 @@ class SQLCipherDatabase(sqlite_backend.SQLitePartialExpandDatabase):          c.execute(              'ALTER TABLE document '              'ADD COLUMN syncable BOOL NOT NULL DEFAULT TRUE') +        c.execute( +            'ALTER TABLE document ' +            'ADD COLUMN encryption_scheme TEXT NOT NULL DEFAULT \'%s\'' % +            EncryptionSchemes.NONE)      def _put_and_update_indexes(self, old_doc, doc):          """ @@ -260,8 +265,9 @@ class SQLCipherDatabase(sqlite_backend.SQLitePartialExpandDatabase):          sqlite_backend.SQLitePartialExpandDatabase._put_and_update_indexes(              self, old_doc, doc)          c = self._db_handle.cursor() -        c.execute('UPDATE document SET syncable=? WHERE doc_id=?', -                  (doc.syncable, doc.doc_id)) +        c.execute('UPDATE document SET syncable=?, encryption_scheme=? ' +                  'WHERE doc_id=?', +                  (doc.syncable, doc.encryption_scheme, doc.doc_id))      def _get_doc(self, doc_id, check_for_conflicts=False):          """ @@ -281,9 +287,12 @@ class SQLCipherDatabase(sqlite_backend.SQLitePartialExpandDatabase):              self, doc_id, check_for_conflicts)          if doc:              c = self._db_handle.cursor() -            c.execute('SELECT syncable FROM document WHERE doc_id=?', +            c.execute('SELECT syncable, encryption_scheme FROM document ' +                      'WHERE doc_id=?',                        (doc.doc_id,)) -            doc.syncable = bool(c.fetchone()[0]) +            result = c.fetchone() +            doc.syncable = bool(result[0]) +            doc.encryption_scheme = result[1]          return doc  sqlite_backend.SQLiteDatabase.register_implementation(SQLCipherDatabase) diff --git a/src/leap/soledad/crypto.py b/src/leap/soledad/crypto.py new file mode 100644 index 00000000..471b35e2 --- /dev/null +++ b/src/leap/soledad/crypto.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- +# crypto.py +# Copyright (C) 2013 LEAP +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +""" +Cryptographic utilities for Soledad. +""" + + +from binascii import b2a_base64 +from hashlib import sha256 + + +from leap.common.keymanager import KeyManager +from leap.soledad.util import GPGWrapper + + +class NoSymmetricSecret(Exception): +    """ +    Raised when trying to get a hashed passphrase. +    """ + + +class SoledadCrypto(object): +    """ +    General cryptographic functionality. +    """ + +    def __init__(self, gnupg_home, symkey=None): +        """ +        Initialize the crypto object. + +        @param gnupg_home: Home of the gpg instance. +        @type gnupg_home: str +        @param symkey: A key to use for symmetric encryption. +        @type symkey: str +        """ +        self._gpg = GPGWrapper(gnupghome=gnupg_home) +        self._symkey = symkey + +    def encrypt(self, data, recipients=None, sign=None, passphrase=None, +                symmetric=False): +        """ +        Encrypt data. + +        @param data: the data to be encrypted +        @type data: str +        @param recipients: to whom C{data} should be encrypted  +        @type recipients: list or str +        @param sign: the fingerprint of key to be used for signature +        @type sign: str +        @param passphrase: the passphrase to be used for encryption +        @type passphrase: str +        @param symmetric: whether the encryption scheme should be symmetric +        @type symmetric: bool + +        @return: the encrypted data +        @rtype: str +        """ +        return str(self._gpg.encrypt(data, recipients, sign=sign, +                                     passphrase=passphrase, +                                     symmetric=symmetric)) + +    def encrypt_symmetric(self, data, passphrase, sign=None): +        """ +        Encrypt C{data} using a {password}. + +        @param data: the data to be encrypted +        @type data: str +        @param passphrase: the passphrase to use for encryption +        @type passphrase: str +        @param data: the data to be encrypted +        @param sign: the fingerprint of key to be used for signature +        @type sign: str + +        @return: the encrypted data +        @rtype: str +        """ +        return self.encrypt(data, sign=sign, +                            passphrase=passphrase, +                            symmetric=True) + +    def decrypt(self, data, passphrase=None): +        """ +        Decrypt data. + +        @param data: the data to be decrypted +        @type data: str +        @param passphrase: the passphrase to be used for decryption +        @type passphrase: str + +        @return: the decrypted data +        @rtype: str +        """ +        return str(self._gpg.decrypt(data, passphrase=passphrase)) + +    def decrypt_symmetric(self, data, passphrase): +        """ +        Decrypt data using symmetric secret. + +        @param data: the data to be decrypted +        @type data: str +        @param passphrase: the passphrase to use for decryption +        @type passphrase: str + +        @return: the decrypted data +        @rtype: str +        """ +        return self.decrypt(data, passphrase=passphrase) + +    def is_encrypted(self, data): +        """ +        Test whether some chunk of data is a cyphertext. + +        @param data: the data to be tested +        @type data: str + +        @return: whether the data is a cyphertext +        @rtype: bool +        """ +        return self._gpg.is_encrypted(data) + +    def is_encrypted_sym(self, data): +        """ +        Test whether some chunk of data was encrypted with a symmetric key. + +        @return: whether data is encrypted to a symmetric key +        @rtype: bool +        """ +        return self._gpg.is_encrypted_sym(data) + +    def is_encrypted_asym(self, data): +        """ +        Test whether some chunk of data was encrypted to an OpenPGP private +        key. + +        @return: whether data is encrypted to an OpenPGP private key +        @rtype: bool +        """ +        return self._gpg.is_encrypted_asym(data) + +    def _hash_passphrase(self, suffix): +        """ +        Generate a passphrase for symmetric encryption. + +        The password is derived from C{suffix} and the secret for +        symmetric encryption previously loaded. + +        @param suffix: Will be appended to the symmetric key before hashing. +        @type suffix: str + +        @return: the passphrase +        @rtype: str +        @raise NoSymmetricSecret: if no symmetric secret was supplied. +        """ +        if self._symkey is None: +            raise NoSymmetricSecret() +        return b2a_base64( +            sha256('%s%s' % (self._symkey, suffix)).digest())[:-1] + +   # +   # symkey setters/getters +   # + +    def _get_symkey(self): +        return self._symkey + +    def _set_symkey(self, symkey): +        self._symkey = symkey + +    symkey = property(_get_symkey, _set_symkey, +                      doc='The key used for symmetric encryption') diff --git a/src/leap/soledad/tests/__init__.py b/src/leap/soledad/tests/__init__.py index 396b2775..28114391 100644 --- a/src/leap/soledad/tests/__init__.py +++ b/src/leap/soledad/tests/__init__.py @@ -3,6 +3,8 @@ Tests to make sure Soledad provides U1DB functionality and more.  """  import u1db + +  from leap.soledad import Soledad  from leap.soledad.crypto import SoledadCrypto  from leap.soledad.backends.leap_backend import LeapDocument diff --git a/src/leap/soledad/tests/test_couch.py b/src/leap/soledad/tests/test_couch.py index 008c3ca4..cdf9c9ff 100644 --- a/src/leap/soledad/tests/test_couch.py +++ b/src/leap/soledad/tests/test_couch.py @@ -15,6 +15,10 @@ try:      import simplejson as json  except ImportError:      import json  # noqa +from leap.soledad.backends.leap_backend import ( +    LeapDocument, +    EncryptionSchemes, +)  #----------------------------------------------------------------------------- @@ -164,10 +168,14 @@ def copy_couch_database_for_test(test, db):      return new_db +def make_document_for_test(test, doc_id, rev, content, has_conflicts=False): +    return LeapDocument(doc_id, rev, content, has_conflicts=has_conflicts) + +  COUCH_SCENARIOS = [      ('couch', {'make_database_for_test': make_couch_database_for_test,                 'copy_database_for_test': copy_couch_database_for_test, -               'make_document_for_test': tests.make_document_for_test, }), +               'make_document_for_test': make_document_for_test, }),  ] @@ -404,5 +412,30 @@ class CouchDatabaseStorageTests(CouchDBTestCase):          content = self._fetch_u1db_data(db)          self.assertEqual(db._replica_uid, content['replica_uid']) +    def test_store_encryption_scheme(self): +        db = couch.CouchDatabase('http://localhost:' + str(self.wrapper.port), +                                 'u1db_tests') +        doc = db.create_doc_from_json(tests.simple_doc) +        # assert that docs have no encryption_scheme by default +        self.assertEqual(EncryptionSchemes.NONE, doc.encryption_scheme) +        # assert that we can store a different value +        doc.encryption_scheme = EncryptionSchemes.PUBKEY +        db.put_doc(doc) +        self.assertEqual( +            EncryptionSchemes.PUBKEY, +            db.get_doc(doc.doc_id).encryption_scheme) +        # assert that we can store another different value +        doc.encryption_scheme = EncryptionSchemes.SYMKEY +        db.put_doc(doc) +        self.assertEqual( +            EncryptionSchemes.SYMKEY, +            db.get_doc(doc.doc_id).encryption_scheme) +        # assert that we can store the default value +        doc.encryption_scheme = EncryptionSchemes.NONE +        db.put_doc(doc) +        self.assertEqual( +            EncryptionSchemes.NONE, +            db.get_doc(doc.doc_id).encryption_scheme) +  load_tests = tests.load_with_scenarios diff --git a/src/leap/soledad/tests/test_leap_backend.py b/src/leap/soledad/tests/test_leap_backend.py index fd9ef85d..0bf70f9e 100644 --- a/src/leap/soledad/tests/test_leap_backend.py +++ b/src/leap/soledad/tests/test_leap_backend.py @@ -5,6 +5,8 @@ For these tests to run, a leap server has to be running on (default) port  """  import u1db + +  from leap.soledad.backends import leap_backend  from leap.soledad.tests import u1db_tests as tests  from leap.soledad.tests.u1db_tests.test_remote_sync_target import ( diff --git a/src/leap/soledad/tests/test_sqlcipher.py b/src/leap/soledad/tests/test_sqlcipher.py index b71478ac..337ab4ee 100644 --- a/src/leap/soledad/tests/test_sqlcipher.py +++ b/src/leap/soledad/tests/test_sqlcipher.py @@ -21,7 +21,10 @@ from leap.soledad.backends.sqlcipher import (      DatabaseIsNotEncrypted,  )  from leap.soledad.backends.sqlcipher import open as u1db_open -from leap.soledad.backends.leap_backend import LeapDocument +from leap.soledad.backends.leap_backend import ( +    LeapDocument, +    EncryptionSchemes, +)  # u1db tests stuff.  from leap.soledad.tests import u1db_tests as tests @@ -342,6 +345,29 @@ class TestSQLCipherPartialExpandDatabase(          self.db.put_doc(doc)          self.assertEqual(True, self.db.get_doc(doc.doc_id).syncable) +    def test_store_encryption_scheme(self): +        doc = self.db.create_doc_from_json(tests.simple_doc) +        # assert that docs have no encryption_scheme by default +        self.assertEqual(EncryptionSchemes.NONE, doc.encryption_scheme) +        # assert that we can store a different value +        doc.encryption_scheme = EncryptionSchemes.PUBKEY +        self.db.put_doc(doc) +        self.assertEqual( +            EncryptionSchemes.PUBKEY, +            self.db.get_doc(doc.doc_id).encryption_scheme) +        # assert that we can store another different value +        doc.encryption_scheme = EncryptionSchemes.SYMKEY +        self.db.put_doc(doc) +        self.assertEqual( +            EncryptionSchemes.SYMKEY, +            self.db.get_doc(doc.doc_id).encryption_scheme) +        # assert that we can store the default value +        doc.encryption_scheme = EncryptionSchemes.NONE +        self.db.put_doc(doc) +        self.assertEqual( +            EncryptionSchemes.NONE, +            self.db.get_doc(doc.doc_id).encryption_scheme) +  #-----------------------------------------------------------------------------  # The following tests come from `u1db.tests.test_open`. @@ -413,7 +439,7 @@ def sync_via_synchronizer_and_leap(test, db_source, db_target,  sync_scenarios.append(('pyleap', {      'make_database_for_test': test_sync.make_database_for_http_test,      'copy_database_for_test': test_sync.copy_database_for_http_test, -    'make_document_for_test': tests.make_document_for_test, +    'make_document_for_test': make_document_for_test,      'make_app_with_state': tests.test_remote_sync_target.make_http_app,      'do_sync': sync_via_synchronizer_and_leap,  })) | 
