Welcome, guest | Sign In | My Account | Store | Cart

PyCrypto-based authenticated encryption using AES-CBC and HMAC-SHA256. This class only supports shared secret encryption. Look elsewhere for public key encryption.

Python, 80 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# PyCrypto-based authenticated symetric encryption
import cPickle as pickle
import hashlib
import hmac
import os
from Crypto.Cipher import AES

class AuthenticationError(Exception): pass

class Crypticle(object):
    """Authenticated encryption class
    
    Encryption algorithm: AES-CBC
    Signing algorithm: HMAC-SHA256
    """

    PICKLE_PAD = "pickle::"
    AES_BLOCK_SIZE = 16
    SIG_SIZE = hashlib.sha256().digest_size

    def __init__(self, key_string, key_size=192):
        self.keys = self.extract_keys(key_string, key_size)
        self.key_size = key_size

    @classmethod
    def generate_key_string(cls, key_size=192):
        key = os.urandom(key_size / 8 + cls.SIG_SIZE)
        return key.encode("base64").replace("\n", "")

    @classmethod
    def extract_keys(cls, key_string, key_size):
        key = key_string.decode("base64")
        assert len(key) == key_size / 8 + cls.SIG_SIZE, "invalid key"
        return key[:-cls.SIG_SIZE], key[-cls.SIG_SIZE:]

    def encrypt(self, data):
        """encrypt data with AES-CBC and sign it with HMAC-SHA256"""
        aes_key, hmac_key = self.keys
        pad = self.AES_BLOCK_SIZE - len(data) % self.AES_BLOCK_SIZE
        data = data + pad * chr(pad)
        iv_bytes = os.urandom(self.AES_BLOCK_SIZE)
        cypher = AES.new(aes_key, AES.MODE_CBC, iv_bytes)
        data = iv_bytes + cypher.encrypt(data)
        sig = hmac.new(hmac_key, data, hashlib.sha256).digest()
        return data + sig

    def decrypt(self, data):
        """verify HMAC-SHA256 signature and decrypt data with AES-CBC"""
        aes_key, hmac_key = self.keys
        sig = data[-self.SIG_SIZE:]
        data = data[:-self.SIG_SIZE]
        if hmac.new(hmac_key, data, hashlib.sha256).digest() != sig:
            raise AuthenticationError("message authentication failed")
        iv_bytes = data[:self.AES_BLOCK_SIZE]
        data = data[self.AES_BLOCK_SIZE:]
        cypher = AES.new(aes_key, AES.MODE_CBC, iv_bytes)
        data = cypher.decrypt(data)
        return data[:-ord(data[-1])]

    def dumps(self, obj, pickler=pickle):
        """pickle and encrypt a python object"""
        return self.encrypt(self.PICKLE_PAD + pickler.dumps(obj))

    def loads(self, data, pickler=pickle):
        """decrypt and unpickle a python object"""
        data = self.decrypt(data)
        # simple integrity check to verify that we got meaningful data
        assert data.startswith(self.PICKLE_PAD), "unexpected header"
        return pickler.loads(data[len(self.PICKLE_PAD):])


if __name__ == "__main__":
    # usage example
    key = Crypticle.generate_key_string()
    data = {"dict": "full", "of": "secrets"}
    crypt = Crypticle(key)
    safe = crypt.dumps(data)
    assert data == crypt.loads(safe)
    print "encrypted data:"
    print safe.encode("base64")

This code was influenced by Kyle's comment (apparently adapted from Google's keyczar) here: http://www.codekoala.com/blog/2009/aes-encryption-python-using-pycrypto/

I also learned how to do HMAC signing with Python here: http://snipplr.com/view/15262/python-sign-data-via-hmac/

A possible enhancement would be to implement a streaming protocol to allow encryption of data-sets larger than available RAM.

[update] Default to AES-192 instead of AES-256. Switched from PyCrypto's RandomPool (apparently broken) to os.urandom. Moved module-level constants to class level to make them easier to override. Credit for these updates goes to the individuals who commented on this comp.lang.python thread: http://groups.google.com/group/comp.lang.python/browse_thread/thread/26ef2de83c5a0337

1 comment

Attila-Mihaly Balazs 10 years, 8 months ago  # | flag

I would really recommend against os.urandom, since on it sacrifices randomness for speed by using /dev/urandom on *nixes. I am not aware of any problems with PyCryptos Random and I would expect it to use the appropriate primitives on each OS (like /dev/random or CryptGenRandom)

Also, I would advise to use a constant time string comparison when checking the HMAC rather than == to thwart timing attacks. See some details here: http://rdist.root.org/2010/01/07/timing-independent-array-comparison/

Also, Python 3 contains this in the stdlib (hmac.compare_digest), but it isn't present in Python 2.