diff options
-rw-r--r-- | changes/feature_add-mac-authentication | 1 | ||||
-rw-r--r-- | changes/feature_change-symmetric-encryption-method-to-aes-256-ctr | 1 | ||||
-rw-r--r-- | setup.py | 23 | ||||
-rw-r--r-- | src/leap/soledad/__init__.py | 12 | ||||
-rw-r--r-- | src/leap/soledad/auth.py | 4 | ||||
-rw-r--r-- | src/leap/soledad/backends/leap_backend.py | 199 | ||||
-rw-r--r-- | src/leap/soledad/crypto.py | 64 | ||||
-rw-r--r-- | src/leap/soledad/server.py | 1 | ||||
-rw-r--r-- | src/leap/soledad/tests/__init__.py | 18 | ||||
-rw-r--r-- | src/leap/soledad/tests/test_crypto.py | 75 | ||||
-rw-r--r-- | src/leap/soledad/tests/test_leap_backend.py | 64 | ||||
-rw-r--r-- | src/leap/soledad/tests/test_soledad.py | 3 | ||||
-rw-r--r-- | src/leap/soledad/tests/test_sqlcipher.py | 10 | ||||
-rw-r--r-- | src/leap/soledad/tests/u1db_tests/test_https.py | 3 |
14 files changed, 332 insertions, 146 deletions
diff --git a/changes/feature_add-mac-authentication b/changes/feature_add-mac-authentication new file mode 100644 index 00000000..ce5a4789 --- /dev/null +++ b/changes/feature_add-mac-authentication @@ -0,0 +1 @@ + o Add MAC authentication to encrypted representation of documents. diff --git a/changes/feature_change-symmetric-encryption-method-to-aes-256-ctr b/changes/feature_change-symmetric-encryption-method-to-aes-256-ctr new file mode 100644 index 00000000..8c44436a --- /dev/null +++ b/changes/feature_change-symmetric-encryption-method-to-aes-256-ctr @@ -0,0 +1 @@ + o Change symmetric encryption method to AES-256 CTR mode. @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. + import os from setuptools import ( setup, @@ -26,7 +27,6 @@ install_requirements = [ 'configparser', 'couchdb', 'leap.common', - 'oauth', 'pysqlcipher', 'python-gnupg', 'simplejson', @@ -53,7 +53,8 @@ install_requirements = [ dependency_links = [ #'git+git://git.futeisha.org/pysqlcipher.git@develop#egg=pysqlcipher', #'git+ssh://code.leap.se/leap_pycommon.git@develop#egg=leap.common', - 'http://twistedmatrix.com/Releases/Twisted/13.0/Twisted-13.0.0.tar.bz2#egg=twisted-13.0.0' + 'http://twistedmatrix.com/Releases/Twisted/13.0/Twisted-13.0.0.tar.bz2' + '#egg=twisted-13.0.0' # break the string to adhere to pep8 style. ] @@ -63,17 +64,19 @@ tests_requirements = [ 'testscenarios', ] + if os.environ.get('VIRTUAL_ENV', None): data_files = None else: # XXX this should go only for linux/mac data_files = [("/etc/init.d/", ["pkg/soledad"])] + setup( name='leap.soledad', # TODO: change version according to decisions regarding soledad versus # leap client versions. - version='0.0.2-dev', + version='0.1.0-dev', url='https://leap.se/', license='GPLv3+', description='Synchronization of locally encrypted data among devices.', @@ -85,11 +88,21 @@ setup( "LEAP client, an API for data storage and sync." ), namespace_packages=["leap"], - packages=find_packages('src', exclude=['leap.soledad.tests']), + # For now, we do not exclude tests because of the circular dependency + # between leap.common and leap.soledad. + #packages=find_packages('src', exclude=['leap.soledad.tests']), + packages=find_packages('src'), package_dir={'': 'src'}, test_suite='leap.soledad.tests', install_requires=install_requirements, tests_require=tests_requirements, dependency_links=dependency_links, - data_files = data_files + data_files=data_files, + # the following files are only used for testing, and might be removed if + # we manage or decide to not install tests in the future. + package_data={ + 'leap.soledad.tests.u1db_tests.testing-certs': [ + '*.pem', '*.cert', '*.key' + ] + } ) diff --git a/src/leap/soledad/__init__.py b/src/leap/soledad/__init__.py index c70fee91..b051a80c 100644 --- a/src/leap/soledad/__init__.py +++ b/src/leap/soledad/__init__.py @@ -374,7 +374,6 @@ class Soledad(object): This method will also replace the secret in the crypto object. """ self._secret_id = secret_id - self._crypto.secret = self._get_storage_secret() def _load_secrets(self): """ @@ -407,7 +406,7 @@ class Soledad(object): content = json.loads(f.read()) self._secrets = content[self.STORAGE_SECRETS_KEY] # choose first secret if no secret_id was given - if self._secret_id == None: + if self._secret_id is None: self._set_secret_id(self._secrets.items()[0][0]) # check secret is isncrypted if not self._crypto.is_encrypted_sym( @@ -952,7 +951,7 @@ class Soledad(object): # set uuid self._uuid = data[self.UUID_KEY] # choose first secret to use - self._set_secret_id(self._secrets.items()[0][0]) + self._set_secret_id(data[self.STORAGE_SECRETS_KEY].items()[0][0]) # # Setters/getters @@ -984,6 +983,10 @@ class Soledad(object): _get_server_url, doc='The URL of the Soledad server.') + storage_secret = property( + _get_storage_secret, + doc='The secret used for symmetric encryption.') + #----------------------------------------------------------------------------- # Monkey patching u1db to be able to provide a custom SSL cert @@ -1004,8 +1007,7 @@ class VerifiedHTTPSConnection(httplib.HTTPSConnection): self.sock = ssl.wrap_socket(sock, ca_certs=SOLEDAD_CERT, cert_reqs=ssl.CERT_REQUIRED) - # TODO: enable this when the certificate is fixed - #match_hostname(self.sock.getpeercert(), self.host) + match_hostname(self.sock.getpeercert(), self.host) old__VerifiedHTTPSConnection = http_client._VerifiedHTTPSConnection diff --git a/src/leap/soledad/auth.py b/src/leap/soledad/auth.py index 1d8f1a42..562a8263 100644 --- a/src/leap/soledad/auth.py +++ b/src/leap/soledad/auth.py @@ -44,7 +44,6 @@ class TokenBasedAuth(object): """ self._creds = {'token': (uuid, token)} - def _sign_request(self, method, url_query, params): """ Return an authorization header to be included in the HTTP request, in @@ -67,4 +66,5 @@ class TokenBasedAuth(object): auth = '%s:%s' % (uuid, token) return [('Authorization', 'Token %s' % auth.encode('base64')[:-1])] else: - return HTTPClientBase._sign_request(self, method, url_query, params) + return HTTPClientBase._sign_request( + self, method, url_query, params) diff --git a/src/leap/soledad/backends/leap_backend.py b/src/leap/soledad/backends/leap_backend.py index 2585379a..8fa662e9 100644 --- a/src/leap/soledad/backends/leap_backend.py +++ b/src/leap/soledad/backends/leap_backend.py @@ -25,6 +25,9 @@ try: import simplejson as json except ImportError: import json # noqa +import hashlib +import hmac +import binascii from u1db import Document @@ -33,10 +36,16 @@ from u1db.errors import BrokenSyncStream from u1db.remote.http_target import HTTPSyncTarget +from leap.common.crypto import ( + EncryptionMethods, + encrypt_sym, + decrypt_sym, +) from leap.common.keymanager import KeyManager from leap.common.check import leap_assert from leap.soledad.auth import TokenBasedAuth + # # Exceptions # @@ -48,10 +57,25 @@ class DocumentNotEncrypted(Exception): pass -class UnknownEncryptionSchemes(Exception): +class UnknownEncryptionScheme(Exception): """ Raised when trying to decrypt from unknown encryption schemes. """ + pass + + +class UnknownMacMethod(Exception): + """ + Raised when trying to authenticate document's content with unknown MAC + mehtod. + """ + pass + + +class WrongMac(Exception): + """ + Raised when failing to authenticate document's contents based on MAC. + """ # @@ -68,96 +92,164 @@ class EncryptionSchemes(object): PUBKEY = 'pubkey' +class MacMethods(object): + """ + Representation of MAC methods used to authenticate document's contents. + """ + + HMAC = 'hmac' + + # # Crypto utilities for a LeapDocument. # ENC_JSON_KEY = '_enc_json' ENC_SCHEME_KEY = '_enc_scheme' +ENC_METHOD_KEY = '_enc_method' +ENC_IV_KEY = '_enc_iv' MAC_KEY = '_mac' +MAC_METHOD_KEY = '_mac_method' -def encrypt_doc_json(crypto, doc_id, doc_json): +def mac_doc(crypto, doc_id, doc_rev, ciphertext, mac_method): """ - Return a valid JSON string containing the C{doc} content encrypted to - a symmetric key and the encryption scheme. + Calculate a MAC for C{doc} using C{ciphertext}. - The returned JSON string is the serialization of the following dictionary: + Current MAC method used is HMAC, with the following parameters: + + * key: sha256(storage_secret, doc_id) + * msg: doc_id + doc_rev + ciphertext + * digestmod: sha256 + + @param crypto: A SoledadCryto instance used to perform the encryption. + @type crypto: leap.soledad.crypto.SoledadCrypto + @param doc_id: The id of the document. + @type doc_id: str + @param doc_rev: The revision of the document. + @type doc_rev: str + @param ciphertext: The content of the document. + @type ciphertext: str + @param mac_method: The MAC method to use. + @type mac_method: str + + @return: The calculated MAC. + @rtype: str + """ + if mac_method == MacMethods.HMAC: + return hmac.new( + crypto.doc_mac_key(doc_id), + str(doc_id) + str(doc_rev) + ciphertext, + hashlib.sha256).digest() + # raise if we do not know how to handle this MAC method + raise UnknownMacMethod('Unknown MAC method: %s.' % mac_method) + + +def encrypt_doc(crypto, doc): + """ + Encrypt C{doc}'s content. + + Encrypt doc's contents using AES-256 CTR mode and return a valid JSON + string representing the following: { - '_enc_json': encrypt_sym(doc_content), - '_enc_scheme': 'symkey', - '_mac': <mac> [Not implemented yet] + ENC_JSON_KEY: '<encrypted doc JSON string>', + ENC_SCHEME_KEY: 'symkey', + ENC_METHOD_KEY: EncryptionMethods.AES_256_CTR, + ENC_IV_KEY: '<the initial value used to encrypt>', + MAC_KEY: '<mac>' + MAC_METHOD_KEY: 'hmac' } - @param crypto: A SoledadCryto instance to perform the encryption. + @param crypto: A SoledadCryto instance used to perform the encryption. @type crypto: leap.soledad.crypto.SoledadCrypto - @param doc_id: The unique id of the document. - @type doc_id: str - @param doc_json: The JSON serialization of the document's contents. - @type doc_json: str + @param doc: The document with contents to be encrypted. + @type doc: LeapDocument - @return: The JSON serialization representing the encrypted content. + @return: The JSON serialization of the dict representing the encrypted + content. @rtype: str """ - ciphertext = crypto.encrypt_sym( - doc_json, - crypto.passphrase_hash(doc_id)) - if not crypto.is_encrypted_sym(ciphertext): - raise DocumentNotEncrypted('Failed encrypting document.') + leap_assert(doc.is_tombstone() is False) + # encrypt content using AES-256 CTR mode + iv, ciphertext = encrypt_sym( + doc.get_json(), + crypto.doc_passphrase(doc.doc_id), + method=EncryptionMethods.AES_256_CTR) + # Return a representation for the encrypted content. In the following, we + # convert binary data to hexadecimal representation so the JSON + # serialization does not complain about what it tries to serialize. + hex_ciphertext = binascii.b2a_hex(ciphertext) return json.dumps({ - ENC_JSON_KEY: ciphertext, + ENC_JSON_KEY: hex_ciphertext, ENC_SCHEME_KEY: EncryptionSchemes.SYMKEY, + ENC_METHOD_KEY: EncryptionMethods.AES_256_CTR, + ENC_IV_KEY: iv, + MAC_KEY: binascii.b2a_hex(mac_doc( # store the mac as hex. + crypto, doc.doc_id, doc.rev, + ciphertext, + MacMethods.HMAC)), + MAC_METHOD_KEY: MacMethods.HMAC, }) -def decrypt_doc_json(crypto, doc_id, doc_json): +def decrypt_doc(crypto, doc): """ - Return a JSON serialization of the decrypted content contained in - C{encrypted_json}. + Decrypt C{doc}'s content. + + Return the JSON string representation of the document's decrypted content. - The C{encrypted_json} parameter is the JSON serialization of the - following dictionary: + The content of the document should have the following structure: { - ENC_JSON_KEY: enc_blob, - ENC_SCHEME_KEY: enc_scheme, + ENC_JSON_KEY: '<enc_blob>', + ENC_SCHEME_KEY: '<enc_scheme>', + ENC_METHOD_KEY: '<enc_method>', + ENC_IV_KEY: '<initial value used to encrypt>', # (optional) + MAC_KEY: '<mac>' + MAC_METHOD_KEY: 'hmac' } C{enc_blob} is the encryption of the JSON serialization of the document's content. For now Soledad just deals with documents whose C{enc_scheme} is - EncryptionSchemes.SYMKEY. + EncryptionSchemes.SYMKEY and C{enc_method} is + EncryptionMethods.AES_256_CTR. @param crypto: A SoledadCryto instance to perform the encryption. @type crypto: leap.soledad.crypto.SoledadCrypto - @param doc_id: The unique id of the document. - @type doc_id: str - @param doc_json: The JSON serialization representation of the encrypted - document's contents. - @type doc_json: str + @param doc: The document to be decrypted. + @type doc: LeapDocument @return: The JSON serialization of the decrypted content. @rtype: str """ - leap_assert(isinstance(doc_id, str), 'Document id is not a string.') - leap_assert(doc_id != '', 'Received empty document id.') - leap_assert(isinstance(doc_json, str), 'Document JSON is not a string.') - leap_assert(doc_json != '', 'Received empty document JSON.') - content = json.loads(doc_json) - ciphertext = content[ENC_JSON_KEY] - enc_scheme = content[ENC_SCHEME_KEY] + leap_assert(doc.is_tombstone() is False) + leap_assert(ENC_JSON_KEY in doc.content) + leap_assert(ENC_SCHEME_KEY in doc.content) + leap_assert(ENC_METHOD_KEY in doc.content) + leap_assert(MAC_KEY in doc.content) + leap_assert(MAC_METHOD_KEY in doc.content) + # verify MAC + ciphertext = binascii.a2b_hex( # content is stored as hex. + doc.content[ENC_JSON_KEY]) + mac = mac_doc( + crypto, doc.doc_id, doc.rev, + ciphertext, + doc.content[MAC_METHOD_KEY]) + if binascii.a2b_hex(doc.content[MAC_KEY]) != mac: # mac is stored as hex. + raise WrongMac('Could not authenticate document\'s contents.') + # decrypt doc's content + enc_scheme = doc.content[ENC_SCHEME_KEY] plainjson = None if enc_scheme == EncryptionSchemes.SYMKEY: - if not crypto.is_encrypted_sym(ciphertext): - raise DocumentNotEncrypted( - 'Unable to identify document encryption for incoming ' - 'document, although it is marked as being encrypted with a ' - 'symmetric key.') - plainjson = crypto.decrypt_sym( + leap_assert(ENC_IV_KEY in doc.content) + plainjson = decrypt_sym( ciphertext, - crypto.passphrase_hash(doc_id)) + crypto.doc_passphrase(doc.doc_id), + method=doc.content[ENC_METHOD_KEY], + iv=doc.content[ENC_IV_KEY]) else: - raise UnknownEncryptionSchemes(enc_scheme) + raise UnknownEncryptionScheme(enc_scheme) return plainjson @@ -354,9 +446,7 @@ class LeapSyncTarget(HTTPSyncTarget, TokenBasedAuth): if doc.content and ENC_SCHEME_KEY in doc.content: if doc.content[ENC_SCHEME_KEY] == \ EncryptionSchemes.SYMKEY: - doc.set_json( - decrypt_doc_json( - self._crypto, doc.doc_id, entry['content'])) + doc.set_json(decrypt_doc(self._crypto, doc)) #------------------------------------------------------------- # end of symmetric decryption #------------------------------------------------------------- @@ -433,15 +523,14 @@ class LeapSyncTarget(HTTPSyncTarget, TokenBasedAuth): #------------------------------------------------------------- # symmetric encryption of document's contents #------------------------------------------------------------- - enc_json = doc.get_json() + doc_json = doc.get_json() if not doc.is_tombstone(): - enc_json = encrypt_doc_json( - self._crypto, doc.doc_id, doc.get_json()) + doc_json = encrypt_doc(self._crypto, doc) #------------------------------------------------------------- # end of symmetric encryption #------------------------------------------------------------- size += prepare(id=doc.doc_id, rev=doc.rev, - content=enc_json, + content=doc_json, gen=gen, trans_id=trans_id) entries.append('\r\n]') size += len(entries[-1]) diff --git a/src/leap/soledad/crypto.py b/src/leap/soledad/crypto.py index 605380ec..d0e2c720 100644 --- a/src/leap/soledad/crypto.py +++ b/src/leap/soledad/crypto.py @@ -21,7 +21,8 @@ Cryptographic utilities for Soledad. """ -from hashlib import sha256 +import hmac +import hashlib from leap.common.keymanager import openpgp @@ -38,6 +39,8 @@ class SoledadCrypto(object): General cryptographic functionality. """ + MAC_KEY_LENGTH = 64 + def __init__(self, soledad): """ Initialize the crypto object. @@ -47,7 +50,6 @@ class SoledadCrypto(object): """ self._soledad = soledad self._pgp = openpgp.OpenPGPScheme(self._soledad) - self._secret = None def encrypt_sym(self, data, passphrase): """ @@ -98,33 +100,61 @@ class SoledadCrypto(object): """ return openpgp.is_encrypted_sym(data) - def passphrase_hash(self, suffix): + def doc_passphrase(self, doc_id): """ - Generate a passphrase for symmetric encryption. + Generate a passphrase for symmetric encryption of document's contents. - The password is derived from the secret for symmetric encryption and - a C{suffix} that is appended to the secret prior to hashing. + The password is derived using HMAC having sha256 as underlying hash + function. The key used for HMAC is Soledad's storage secret stripped + from the first MAC_KEY_LENGTH characters. The HMAC message is + C{doc_id}. - @param suffix: Will be appended to the symmetric key before hashing. - @type suffix: str + @param doc_id: The id of the document that will be encrypted using + this passphrase. + @type doc_id: str - @return: the passphrase + @return: The passphrase. @rtype: str + @raise NoSymmetricSecret: if no symmetric secret was supplied. """ - if self._secret is None: + if self.secret is None: raise NoSymmetricSecret() - return sha256('%s%s' % (self._secret, suffix)).hexdigest() + return hmac.new( + self.secret[self.MAC_KEY_LENGTH:], + doc_id, + hashlib.sha256).digest() + + def doc_mac_key(self, doc_id): + """ + Generate a key for calculating a MAC for a document whose id is + C{doc_id}. + + The key is derived using HMAC having sha256 as underlying hash + function. The key used for HMAC is the first MAC_KEY_LENGTH characters + of Soledad's storage secret. The HMAC message is C{doc_id}. + + @param doc_id: The id of the document. + @type doc_id: str + + @return: The key. + @rtype: str + + @raise NoSymmetricSecret: if no symmetric secret was supplied. + """ + if self.secret is None: + raise NoSymmetricSecret() + return hmac.new( + self.secret[:self.MAC_KEY_LENGTH], + doc_id, + hashlib.sha256).digest() # # secret setters/getters # def _get_secret(self): - return self._secret - - def _set_secret(self, secret): - self._secret = secret + return self._soledad.storage_secret - secret = property(_get_secret, _set_secret, - doc='The key used for symmetric encryption') + secret = property( + _get_secret, doc='The secret used for symmetric encryption') diff --git a/src/leap/soledad/server.py b/src/leap/soledad/server.py index e7b55a3e..7aa253a3 100644 --- a/src/leap/soledad/server.py +++ b/src/leap/soledad/server.py @@ -187,7 +187,6 @@ class SoledadAuthMiddleware(object): return not environ.get(self.PATH_INFO_KEY).startswith('/shared/') - #----------------------------------------------------------------------------- # Soledad WSGI application #----------------------------------------------------------------------------- diff --git a/src/leap/soledad/tests/__init__.py b/src/leap/soledad/tests/__init__.py index 6787aa9d..79ee69c4 100644 --- a/src/leap/soledad/tests/__init__.py +++ b/src/leap/soledad/tests/__init__.py @@ -10,7 +10,7 @@ from leap.soledad import Soledad from leap.soledad.crypto import SoledadCrypto from leap.soledad.backends.leap_backend import ( LeapDocument, - decrypt_doc_json, + decrypt_doc, ENC_SCHEME_KEY, ) from leap.common.testing.basetest import BaseLeapTest @@ -23,7 +23,7 @@ from leap.common.testing.basetest import BaseLeapTest class BaseSoledadTest(BaseLeapTest): """ - Instantiates GPG and Soledad for usage in tests. + Instantiates Soledad for usage in tests. """ def setUp(self): @@ -44,7 +44,8 @@ class BaseSoledadTest(BaseLeapTest): self._db2.close() self._soledad.close() - def _soledad_instance(self, user='leap@leap.se', passphrase='123', prefix='', + def _soledad_instance(self, user='leap@leap.se', passphrase='123', + prefix='', secrets_path=Soledad.STORAGE_SECRETS_FILE_NAME, local_db_path='/soledad.u1db', server_url='', cert_file=None): @@ -69,15 +70,16 @@ class BaseSoledadTest(BaseLeapTest): server_url=server_url, # Soledad will fail if not given an url. cert_file=cert_file) - def assertGetEncryptedDoc(self, db, doc_id, doc_rev, content, has_conflicts): - """Assert that the document in the database looks correct.""" + def assertGetEncryptedDoc( + self, db, doc_id, doc_rev, content, has_conflicts): + """ + Assert that the document in the database looks correct. + """ exp_doc = self.make_document(doc_id, doc_rev, content, has_conflicts=has_conflicts) doc = db.get_doc(doc_id) if ENC_SCHEME_KEY in doc.content: - doc.set_json( - decrypt_doc_json( - self._soledad._crypto, doc.doc_id, doc.get_json())) + doc.set_json(decrypt_doc(self._soledad._crypto, doc)) self.assertEqual(exp_doc.doc_id, doc.doc_id) self.assertEqual(exp_doc.rev, doc.rev) self.assertEqual(exp_doc.has_conflicts, doc.has_conflicts) diff --git a/src/leap/soledad/tests/test_crypto.py b/src/leap/soledad/tests/test_crypto.py index 61c5f5b0..9a219bd0 100644 --- a/src/leap/soledad/tests/test_crypto.py +++ b/src/leap/soledad/tests/test_crypto.py @@ -31,13 +31,16 @@ except ImportError: from leap.soledad.backends.leap_backend import ( LeapDocument, - encrypt_doc_json, - decrypt_doc_json, + encrypt_doc, + decrypt_doc, EncryptionSchemes, LeapSyncTarget, ENC_JSON_KEY, ENC_SCHEME_KEY, + MAC_METHOD_KEY, MAC_KEY, + UnknownMacMethod, + WrongMac, ) from leap.soledad.backends.couch import CouchDatabase from leap.soledad import KeyAlreadyExists, Soledad @@ -66,16 +69,21 @@ class EncryptedSyncTestCase(BaseSoledadTest): """ Test encrypting and decrypting documents. """ + simpledoc = {'key': 'val'} doc1 = LeapDocument(doc_id='id') - doc1.content = {'key': 'val'} - enc_json = encrypt_doc_json( - self._soledad._crypto, doc1.doc_id, doc1.get_json()) - plain_json = decrypt_doc_json( - self._soledad._crypto, doc1.doc_id, enc_json) - doc2 = LeapDocument(doc_id=doc1.doc_id, json=plain_json) - res1 = doc1.get_json() - res2 = doc2.get_json() - self.assertEqual(res1, res2, 'incorrect document encryption') + doc1.content = simpledoc + # encrypt doc + doc1.set_json(encrypt_doc(self._soledad._crypto, doc1)) + # assert content is different and includes keys + self.assertNotEqual( + simpledoc, doc1.content, + 'incorrect document encryption') + self.assertTrue(ENC_JSON_KEY in doc1.content) + self.assertTrue(ENC_SCHEME_KEY in doc1.content) + # decrypt doc + doc1.set_json(decrypt_doc(self._soledad._crypto, doc1)) + self.assertEqual( + simpledoc, doc1.content, 'incorrect document encryption') def test_encrypt_sym(self): """ @@ -84,9 +92,7 @@ class EncryptedSyncTestCase(BaseSoledadTest): doc1 = LeapDocument() doc1.content = {'key': 'val'} enc_json = json.loads( - encrypt_doc_json( - self._soledad._crypto, - doc1.doc_id, doc1.get_json()))[ENC_JSON_KEY] + encrypt_doc(self._soledad._crypto, doc1))[ENC_JSON_KEY] self.assertEqual( True, self._soledad._crypto.is_encrypted_sym(enc_json), @@ -161,7 +167,7 @@ class EncryptedSyncTestCase(BaseSoledadTest): # # create and encrypt a doc to insert directly in couchdb # doc = LeapDocument('doc-id') # doc.set_json( -# encrypt_doc_json( +# encrypt_doc( # self._soledad._crypto, 'doc-id', json.dumps(simple_doc))) # db.put_doc(doc) # # setup credentials for access to soledad server @@ -241,3 +247,42 @@ class CryptoMethodsTestCase(BaseSoledadTest): sol = self._soledad_instance(user='user@leap.se', prefix='/3') self.assertTrue(sol._has_secret(), "Should have a secret at " "this point") + + +class MacAuthTestCase(BaseSoledadTest): + + def test_decrypt_with_wrong_mac_raises(self): + """ + Trying to decrypt a document with wrong MAC should raise. + """ + simpledoc = {'key': 'val'} + doc = LeapDocument(doc_id='id') + doc.content = simpledoc + # encrypt doc + doc.set_json(encrypt_doc(self._soledad._crypto, doc)) + self.assertTrue(MAC_KEY in doc.content) + self.assertTrue(MAC_METHOD_KEY in doc.content) + # mess with MAC + doc.content[MAC_KEY] = 'wrongmac' + # try to decrypt doc + self.assertRaises( + WrongMac, + decrypt_doc, self._soledad._crypto, doc) + + def test_decrypt_with_unknown_mac_method_raises(self): + """ + Trying to decrypt a document with unknown MAC method should raise. + """ + simpledoc = {'key': 'val'} + doc = LeapDocument(doc_id='id') + doc.content = simpledoc + # encrypt doc + doc.set_json(encrypt_doc(self._soledad._crypto, doc)) + self.assertTrue(MAC_KEY in doc.content) + self.assertTrue(MAC_METHOD_KEY in doc.content) + # mess with MAC method + doc.content[MAC_METHOD_KEY] = 'mymac' + # try to decrypt doc + self.assertRaises( + UnknownMacMethod, + decrypt_doc, self._soledad._crypto, doc) diff --git a/src/leap/soledad/tests/test_leap_backend.py b/src/leap/soledad/tests/test_leap_backend.py index dbebadb5..8afae6f6 100644 --- a/src/leap/soledad/tests/test_leap_backend.py +++ b/src/leap/soledad/tests/test_leap_backend.py @@ -106,7 +106,8 @@ def make_token_http_database_for_test(test, replica_uid): auth.TokenBasedAuth.set_token_credentials(self, uuid, token) def _sign_request(self, method, url_query, params): - return auth.TokenBasedAuth._sign_request(self, method, url_query, params) + return auth.TokenBasedAuth._sign_request( + self, method, url_query, params) http_db = _HTTPDatabaseWithToken(test.getURL('test')) http_db.set_token_credentials('user-uuid', 'auth-token') @@ -162,7 +163,8 @@ class TestLeapClientBase(test_http_client.TestHTTPClientBase): auth.TokenBasedAuth.set_token_credentials(self, uuid, token) def _sign_request(self, method, url_query, params): - return auth.TokenBasedAuth._sign_request(self, method, url_query, params) + return auth.TokenBasedAuth._sign_request( + self, method, url_query, params) return _HTTPClientWithToken(self.getURL('dbase'), **kwds) @@ -185,7 +187,8 @@ class TestLeapClientBase(test_http_client.TestHTTPClientBase): pass def app(self, environ, start_response): - res = test_http_client.TestHTTPClientBase.app(self, environ, start_response) + res = test_http_client.TestHTTPClientBase.app( + self, environ, start_response) if res is not None: return res # mime solead application here. @@ -195,13 +198,13 @@ class TestLeapClientBase(test_http_client.TestHTTPClientBase): start_response("401 Unauthorized", [('Content-Type', 'application/json')]) return [json.dumps({"error": "unauthorized", - "message": e.message})] + "message": e.message})] scheme, encoded = auth.split(None, 1) if scheme.lower() != 'token': start_response("401 Unauthorized", [('Content-Type', 'application/json')]) return [json.dumps({"error": "unauthorized", - "message": e.message})] + "message": e.message})] uuid, token = encoded.decode('base64').split(':', 1) if uuid != 'user-uuid' and token != 'auth-token': return unauth_err("Incorrect address or token.") @@ -228,7 +231,6 @@ class TestLeapClientBase(test_http_client.TestHTTPClientBase): ['/dbase/doc/token', 'user-uuid', 'auth-token'], json.loads(res)) - #----------------------------------------------------------------------------- # The following tests come from `u1db.tests.test_document`. #----------------------------------------------------------------------------- @@ -284,10 +286,9 @@ class TestLeapParsingSyncStream( """ Test adapted to use encrypted content. """ - doc = leap_backend.LeapDocument('i') + doc = leap_backend.LeapDocument('i', rev='r') doc.content = {} - enc_json = leap_backend.encrypt_doc_json( - self._soledad._crypto, doc.doc_id, doc.get_json()) + enc_json = leap_backend.encrypt_doc(self._soledad._crypto, doc) tgt = leap_backend.LeapSyncTarget( "http://foo/foo", crypto=self._soledad._crypto) @@ -367,6 +368,7 @@ def oauth_leap_sync_target(test, path): tests.token1.key, tests.token1.secret) return st + def token_leap_sync_target(test, path): st = leap_sync_target(test, path) st.set_token_credentials('user-uuid', 'auth-token') @@ -374,7 +376,7 @@ def token_leap_sync_target(test, path): class TestLeapSyncTarget( - test_remote_sync_target.TestRemoteSyncTargets, BaseSoledadTest): + test_remote_sync_target.TestRemoteSyncTargets, BaseSoledadTest): scenarios = [ ('http', {'make_app_with_state': make_soledad_app, @@ -383,9 +385,10 @@ class TestLeapSyncTarget( ('oauth_http', {'make_app_with_state': make_oauth_http_app, 'make_document_for_test': make_leap_document_for_test, 'sync_target': oauth_leap_sync_target}), - ('token_soledad', {'make_app_with_state': make_token_soledad_app, - 'make_document_for_test': make_leap_document_for_test, - 'sync_target': token_leap_sync_target}), + ('token_soledad', + {'make_app_with_state': make_token_soledad_app, + 'make_document_for_test': make_leap_document_for_test, + 'sync_target': token_leap_sync_target}), ] def test_sync_exchange_send(self): @@ -523,10 +526,11 @@ class TestLeapSyncTargetHttpsSupport(test_https.TestHttpSyncTargetHttpsSupport, BaseSoledadTest): scenarios = [ - ('token_soledad_https', {'server_def': test_https.https_server_def, - 'make_app_with_state': make_token_soledad_app, - 'make_document_for_test': make_leap_document_for_test, - 'sync_target': token_leap_https_sync_target}), + ('token_soledad_https', + {'server_def': test_https.https_server_def, + 'make_app_with_state': make_token_soledad_app, + 'make_document_for_test': make_leap_document_for_test, + 'sync_target': token_leap_https_sync_target}), ] def setUp(self): @@ -568,6 +572,7 @@ class TestLeapSyncTargetHttpsSupport(test_https.TestHttpSyncTargetHttpsSupport, http_client.CertificateError, remote_target.record_sync_info, 'other-id', 2, 'T-id') + #----------------------------------------------------------------------------- # The following tests come from `u1db.tests.test_http_database`. #----------------------------------------------------------------------------- @@ -585,7 +590,8 @@ class _HTTPDatabase(http_database.HTTPDatabase, auth.TokenBasedAuth): self, method, url_query, params) -class TestHTTPDatabaseWithCreds(test_http_database.TestHTTPDatabaseCtrWithCreds): +class TestHTTPDatabaseWithCreds( + test_http_database.TestHTTPDatabaseCtrWithCreds): def test_get_sync_target_inherits_token_credentials(self): # this test was from TestDatabaseSimpleOperations but we put it here @@ -595,7 +601,6 @@ class TestHTTPDatabaseWithCreds(test_http_database.TestHTTPDatabaseCtrWithCreds) st = self.db.get_sync_target() self.assertEqual(self.db._creds, st._creds) - def test_ctr_with_creds(self): db1 = _HTTPDatabase('http://dbs/db', creds={'token': { 'uuid': 'user-uuid', @@ -658,7 +663,6 @@ class LeapDatabaseSyncTargetTests( (self.other_changes, new_gen, last_trans_id)) self.assertEqual(10, self.st.get_sync_info('replica')[3]) - def test_sync_exchange_push_many(self): """ Test sync exchange. @@ -666,9 +670,10 @@ class LeapDatabaseSyncTargetTests( This test was adapted to decrypt remote content before assert. """ docs_by_gen = [ - (self.make_document('doc-id', 'replica:1', tests.simple_doc), 10, 'T-1'), - (self.make_document('doc-id2', 'replica:1', tests.nested_doc), 11, - 'T-2')] + (self.make_document( + 'doc-id', 'replica:1', tests.simple_doc), 10, 'T-1'), + (self.make_document( + 'doc-id2', 'replica:1', tests.nested_doc), 11, 'T-2')] new_gen, trans_id = self.st.sync_exchange( docs_by_gen, 'replica', last_known_generation=0, last_known_trans_id=None, return_doc_cb=self.receive_doc) @@ -682,7 +687,6 @@ class LeapDatabaseSyncTargetTests( (self.other_changes, new_gen, trans_id)) self.assertEqual(11, self.st.get_sync_info('replica')[3]) - def test_sync_exchange_returns_many_new_docs(self): """ Test sync exchange. @@ -766,10 +770,10 @@ class TestLeapDbSync(test_sync.TestDbSync, BaseSoledadTest): self.assertEqual(1, len(changes)) self.assertEqual(doc2.doc_id, changes[0][0]) self.assertEqual(1, gen - local_gen_before_sync) - self.assertGetEncryptedDoc(self.db2, doc1.doc_id, doc1.rev, tests.simple_doc, - False) - self.assertGetEncryptedDoc(self.db, doc2.doc_id, doc2.rev, tests.nested_doc, - False) + self.assertGetEncryptedDoc( + self.db2, doc1.doc_id, doc1.rev, tests.simple_doc, False) + self.assertGetEncryptedDoc( + self.db, doc2.doc_id, doc2.rev, tests.nested_doc, False) def test_db_sync_autocreate(self): """ @@ -785,8 +789,8 @@ class TestLeapDbSync(test_sync.TestDbSync, BaseSoledadTest): gen, _, changes = db3.whats_changed() self.assertEqual(1, len(changes)) self.assertEqual(doc1.doc_id, changes[0][0]) - self.assertGetEncryptedDoc(db3, doc1.doc_id, doc1.rev, tests.simple_doc, - False) + self.assertGetEncryptedDoc( + db3, doc1.doc_id, doc1.rev, tests.simple_doc, False) t_gen, _ = self.db._get_replica_gen_and_trans_id('test3.db') s_gen, _ = db3._get_replica_gen_and_trans_id('test1') self.assertEqual(1, t_gen) diff --git a/src/leap/soledad/tests/test_soledad.py b/src/leap/soledad/tests/test_soledad.py index 49358ab6..6a4261c0 100644 --- a/src/leap/soledad/tests/test_soledad.py +++ b/src/leap/soledad/tests/test_soledad.py @@ -69,7 +69,8 @@ class AuxMethodsTestCase(BaseSoledadTest): secrets_path=None, local_db_path=None, server_url='', cert_file=None) # otherwise Soledad will fail. self.assertEquals( - os.path.join(sol.DEFAULT_PREFIX, Soledad.STORAGE_SECRETS_FILE_NAME), + os.path.join( + sol.DEFAULT_PREFIX, Soledad.STORAGE_SECRETS_FILE_NAME), sol.secrets_path) self.assertEquals( os.path.join(sol.DEFAULT_PREFIX, 'soledad.u1db'), diff --git a/src/leap/soledad/tests/test_sqlcipher.py b/src/leap/soledad/tests/test_sqlcipher.py index c4282c0f..60261111 100644 --- a/src/leap/soledad/tests/test_sqlcipher.py +++ b/src/leap/soledad/tests/test_sqlcipher.py @@ -52,10 +52,9 @@ from leap.soledad.backends.sqlcipher import open as u1db_open from leap.soledad.backends.leap_backend import ( LeapDocument, EncryptionSchemes, - decrypt_doc_json, + decrypt_doc, ENC_JSON_KEY, ENC_SCHEME_KEY, - MAC_KEY, ) @@ -634,9 +633,7 @@ class SQLCipherDatabaseSyncTests( self.sync(self.db2, db3) doc3 = db3.get_doc('the-doc') if ENC_SCHEME_KEY in doc3.content: - doc3.set_json( - decrypt_doc_json( - self._soledad._crypto, doc3.doc_id, doc3.get_json())) + doc3.set_json(decrypt_doc(self._soledad._crypto, doc3)) self.assertEqual(doc4.get_json(), doc3.get_json()) self.assertFalse(doc3.has_conflicts) @@ -715,7 +712,8 @@ class SQLCipherSyncTargetTests( sever-side. """ docs_by_gen = [ - (self.make_document('doc-id', 'replica:1', tests.simple_doc), 10, 'T-1'), + (self.make_document( + 'doc-id', 'replica:1', tests.simple_doc), 10, 'T-1'), (self.make_document('doc-id2', 'replica:1', tests.nested_doc), 11, 'T-2')] new_gen, trans_id = self.st.sync_exchange( diff --git a/src/leap/soledad/tests/u1db_tests/test_https.py b/src/leap/soledad/tests/u1db_tests/test_https.py index b4b14722..62180f8c 100644 --- a/src/leap/soledad/tests/u1db_tests/test_https.py +++ b/src/leap/soledad/tests/u1db_tests/test_https.py @@ -74,7 +74,8 @@ class TestHttpSyncTargetHttpsSupport(tests.TestCaseWithServer): # class with one that will do HTTPS independent of the platform. In # order to maintain the compatibility with u1db default tests, we undo # that replacement here. - http_client._VerifiedHTTPSConnection = soledad.old__VerifiedHTTPSConnection + http_client._VerifiedHTTPSConnection = \ + soledad.old__VerifiedHTTPSConnection super(TestHttpSyncTargetHttpsSupport, self).setUp() def getSyncTarget(self, host, path=None): |