diff options
| -rw-r--r-- | changes/feature_add-mac-authentication | 1 | ||||
| -rw-r--r-- | src/leap/soledad/__init__.py | 7 | ||||
| -rw-r--r-- | src/leap/soledad/backends/leap_backend.py | 83 | ||||
| -rw-r--r-- | src/leap/soledad/crypto.py | 64 | ||||
| -rw-r--r-- | src/leap/soledad/tests/test_crypto.py | 43 | ||||
| -rw-r--r-- | src/leap/soledad/tests/test_leap_backend.py | 2 | 
6 files changed, 176 insertions, 24 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/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: '<encrypted doc JSON string>',              ENC_SCHEME_KEY: 'symkey', +            MAC_KEY: '<mac>' +            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_blob>',              ENC_SCHEME_KEY: '<enc_scheme>', +            MAC_KEY: '<mac>' +            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( | 
