summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--changes/feature_add-mac-authentication1
-rw-r--r--src/leap/soledad/__init__.py7
-rw-r--r--src/leap/soledad/backends/leap_backend.py83
-rw-r--r--src/leap/soledad/crypto.py64
-rw-r--r--src/leap/soledad/tests/test_crypto.py43
-rw-r--r--src/leap/soledad/tests/test_leap_backend.py2
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(