From 3e22ea2445f805dfe0df9bbf15a03cbc53a88167 Mon Sep 17 00:00:00 2001 From: drebs Date: Tue, 14 May 2013 18:56:12 -0300 Subject: Add MAC authentication to encrypted docs. * Fix review comments: * Use of literal string instead of self.STORAGE_SECRETS_KEY * Add mac_method param to mac_doc() * Verify mac_method in mac_doc() and raise in there if unknown method * Use different parts of storage_secret for generating doc passphrase and mac key. * Add changes file. --- src/leap/soledad/__init__.py | 7 ++- src/leap/soledad/backends/leap_backend.py | 83 +++++++++++++++++++++++++++-- src/leap/soledad/crypto.py | 64 ++++++++++++++++------ src/leap/soledad/tests/test_crypto.py | 43 +++++++++++++++ src/leap/soledad/tests/test_leap_backend.py | 2 +- 5 files changed, 175 insertions(+), 24 deletions(-) (limited to 'src') diff --git a/src/leap/soledad/__init__.py b/src/leap/soledad/__init__.py index 70ff146d..e3313ffe 100644 --- a/src/leap/soledad/__init__.py +++ b/src/leap/soledad/__init__.py @@ -373,7 +373,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): """ @@ -942,7 +941,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 @@ -974,6 +973,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 diff --git a/src/leap/soledad/backends/leap_backend.py b/src/leap/soledad/backends/leap_backend.py index 6f1f9546..87f63432 100644 --- a/src/leap/soledad/backends/leap_backend.py +++ b/src/leap/soledad/backends/leap_backend.py @@ -25,6 +25,8 @@ try: import simplejson as json except ImportError: import json # noqa +import hashlib +import hmac from u1db import Document @@ -37,6 +39,7 @@ from leap.common.keymanager import KeyManager from leap.common.check import leap_assert from leap.soledad.auth import TokenBasedAuth + # # Exceptions # @@ -52,6 +55,21 @@ 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,12 +86,55 @@ 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' +MAC_KEY = '_mac' +MAC_METHOD_KEY = '_mac_method' + + +def mac_doc(crypto, doc_id, doc_rev, ciphertext, mac_method): + """ + Calculate a MAC for C{doc} using C{ciphertext}. + + 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).hexdigest() + # 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): @@ -85,6 +146,8 @@ def encrypt_doc(crypto, doc): { ENC_JSON_KEY: '', ENC_SCHEME_KEY: 'symkey', + MAC_KEY: '' + MAC_METHOD_KEY: 'hmac' } @param crypto: A SoledadCryto instance used to perform the encryption. @@ -100,16 +163,17 @@ def encrypt_doc(crypto, doc): # encrypt content ciphertext = crypto.encrypt_sym( doc.get_json(), - crypto.passphrase_hash(doc.doc_id)) + crypto.doc_passphrase(doc.doc_id)) # verify it is indeed encrypted if not crypto.is_encrypted_sym(ciphertext): raise DocumentNotEncrypted('Failed encrypting document.') - # calculate hmac - #mac = hmac.new(doc_id,, doc_json, # update doc's content with encrypted version return json.dumps({ ENC_JSON_KEY: ciphertext, ENC_SCHEME_KEY: EncryptionSchemes.SYMKEY, + MAC_KEY: mac_doc( + crypto, doc.doc_id, doc.rev, ciphertext, MacMethods.HMAC), + MAC_METHOD_KEY: MacMethods.HMAC, }) @@ -124,6 +188,8 @@ def decrypt_doc(crypto, doc): { ENC_JSON_KEY: '', ENC_SCHEME_KEY: '', + MAC_KEY: '' + MAC_METHOD_KEY: 'hmac' } C{enc_blob} is the encryption of the JSON serialization of the document's @@ -141,6 +207,15 @@ def decrypt_doc(crypto, doc): 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(MAC_KEY in doc.content) + leap_assert(MAC_METHOD_KEY in doc.content) + # verify MAC + mac = mac_doc( + crypto, doc.doc_id, doc.rev, + doc.content[ENC_JSON_KEY], + doc.content[MAC_METHOD_KEY]) + if doc.content[MAC_KEY] != mac: + raise WrongMac('Could not authenticate document\'s contents.') # decrypt doc's content ciphertext = doc.content[ENC_JSON_KEY] enc_scheme = doc.content[ENC_SCHEME_KEY] @@ -153,7 +228,7 @@ def decrypt_doc(crypto, doc): 'symmetric key.') plainjson = crypto.decrypt_sym( ciphertext, - crypto.passphrase_hash(doc.doc_id)) + crypto.doc_passphrase(doc.doc_id)) else: raise UnknownEncryptionScheme(enc_scheme) return plainjson diff --git a/src/leap/soledad/crypto.py b/src/leap/soledad/crypto.py index 605380ec..6140ef31 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).hexdigest() + + 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).hexdigest() # # 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/tests/test_crypto.py b/src/leap/soledad/tests/test_crypto.py index 720e95fa..6804723a 100644 --- a/src/leap/soledad/tests/test_crypto.py +++ b/src/leap/soledad/tests/test_crypto.py @@ -37,6 +37,10 @@ from leap.soledad.backends.leap_backend import ( 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 @@ -243,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 c0510373..9bd7b604 100644 --- a/src/leap/soledad/tests/test_leap_backend.py +++ b/src/leap/soledad/tests/test_leap_backend.py @@ -284,7 +284,7 @@ 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(self._soledad._crypto, doc) tgt = leap_backend.LeapSyncTarget( -- cgit v1.2.3