summaryrefslogtreecommitdiff
path: root/src/leap
diff options
context:
space:
mode:
authordrebs <drebs@leap.se>2013-04-23 12:52:06 -0300
committerdrebs <drebs@leap.se>2013-04-23 14:26:46 -0300
commit4c74a91ad905e7d59260ccec789930c16b29a62d (patch)
treede517b9eb258d42c581f7d878f4b8573986649cf /src/leap
parentb48f000e311daf543a8b8f776c5438725485bffd (diff)
Add encryption_scheme property to LeapDocument.
Diffstat (limited to 'src/leap')
-rw-r--r--src/leap/soledad/backends/couch.py9
-rw-r--r--src/leap/soledad/backends/leap_backend.py72
-rw-r--r--src/leap/soledad/backends/sqlcipher.py41
-rw-r--r--src/leap/soledad/crypto.py186
-rw-r--r--src/leap/soledad/tests/__init__.py2
-rw-r--r--src/leap/soledad/tests/test_couch.py35
-rw-r--r--src/leap/soledad/tests/test_leap_backend.py2
-rw-r--r--src/leap/soledad/tests/test_sqlcipher.py30
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,
}))