diff options
Diffstat (limited to 'src/leap/soledad/backends/leap_backend.py')
-rw-r--r-- | src/leap/soledad/backends/leap_backend.py | 199 |
1 files changed, 144 insertions, 55 deletions
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]) |