On 05/28/2016 07:51 AM, Cory Benfield wrote:
Unfortunately, Fernet is implemented by more than just the Python cryptography project, and *those* implementations may not react as well to higher-key-length AES encryption mechanisms.Would the attached be an acceptable change? The Fernet class continues to use AES128. The ExtFernet192 and ExtFernet256 classes follow the same token format but use AES192 and AES256 encryption respectively and change the version byte in the output to distinguish the data from standard Fernet.
It's also available as a pull request on Github.
diff -urw -x .git cryptography.master/AUTHORS.rst cryptography.aes256/AUTHORS.rst --- cryptography.master/AUTHORS.rst 2016-05-26 13:10:09.765420621 -0700 +++ cryptography.aes256/AUTHORS.rst 2016-05-28 18:55:53.797059164 -0700 @@ -29,3 +29,4 @@ * Phoebe Queen <foi...@gmail.com> (10D4 7741 AB65 50F4 B264 3888 DA40 201A 072B C1FA) * Google Inc. * Amaury Forgeot d'Arc <amaur...@google.com> +* Todd Knarr <tkn...@silverglass.org> diff -urw -x .git cryptography.master/src/cryptography/fernet.py cryptography.aes256/src/cryptography/fernet.py --- cryptography.master/src/cryptography/fernet.py 2016-05-26 13:10:09.809422812 -0700 +++ cryptography.aes256/src/cryptography/fernet.py 2016-05-29 19:34:46.703859228 -0700 @@ -26,24 +26,39 @@ _MAX_CLOCK_SKEW = 60 -class Fernet(object): +class FernetBase(object): + """ + Base class for Fernet objects. Do not use directly. + """ + def __init__(self, key, backend=None): if backend is None: backend = default_backend() - key = base64.urlsafe_b64decode(key) - if len(key) != 32: + # key size in bytes = len(key) * 8 bits/byte / 2 keys + key_size = len(key) * 4 + key_bytes = len(key) // 2 + if key_size not in algorithms.AES.key_sizes: raise ValueError( - "Fernet key must be 32 url-safe base64-encoded bytes." + "Fernet key must be 32 or 48 or 64 url-safe" + " base64-encoded bytes." ) - self._signing_key = key[:16] - self._encryption_key = key[16:] + self._signing_key = key[:key_bytes] + self._encryption_key = key[key_bytes:] self._backend = backend + # Base class has an invalid version byte + self._version = b"\x00" @classmethod - def generate_key(cls): - return base64.urlsafe_b64encode(os.urandom(32)) + def _generate_key_of_length(cls, key_bits): + if key_bits not in algorithms.AES.key_sizes: + raise ValueError( + "Fernet key must be 128 or 192 or 256 bits." + ) + # Need random bytes for 2 keys at 8 bits/byte/key + key_bytes = key_bits // 4 + return base64.urlsafe_b64encode(os.urandom(key_bytes)) def encrypt(self, data): current_time = int(time.time()) @@ -62,7 +77,7 @@ ciphertext = encryptor.update(padded_data) + encryptor.finalize() basic_parts = ( - b"\x80" + struct.pack(">Q", current_time) + iv + ciphertext + self._version + struct.pack(">Q", current_time) + iv + ciphertext ) h = HMAC(self._signing_key, hashes.SHA256(), backend=self._backend) @@ -81,7 +96,7 @@ except (TypeError, binascii.Error): raise InvalidToken - if not data or six.indexbytes(data, 0) != 0x80: + if not data or six.indexbytes(data, 0) != six.byte2int(self._version): raise InvalidToken try: @@ -141,3 +156,73 @@ except InvalidToken: pass raise InvalidToken + + +class Fernet(FernetBase): + """ + Standard Fernet using AES128 encryption. + """ + + def __init__(self, key, backend=None): + key = base64.urlsafe_b64decode(key) + if len(key) != 32: + raise ValueError( + "Fernet key must be 32 url-safe base64-encoded bytes." + ) + super(self.__class__, self).__init__(key, backend) + # Overwrite the version byte FernetBase's __init__ set + self._version = b"\x80" + + @classmethod + def generate_key(cls): + return cls._generate_key_of_length(128) + + +class ExtFernet192(FernetBase): + """ + Extended version of Fernet using AES192 encryption. + + The version byte differs from standard Fernet to distinguish this format. + The low 5 bits indicate the version of standard Fernet the extended version + is based on, with 0x1 indicating Fernet version 0x80. The high 3 bits + indicate the encryption key length, with binary 001 (0x1) indicating + 192-bit AES. This yields a version byte of 0x21 (binary 00100001). + """ + + def __init__(self, key, backend=None): + key = base64.urlsafe_b64decode(key) + if len(key) != 48: + raise ValueError( + "Fernet192 key must be 48 url-safe base64-encoded bytes." + ) + super(self.__class__, self).__init__(key, backend) + self._version = b"\x21" + + @classmethod + def generate_key(cls): + return cls._generate_key_of_length(192) + + +class ExtFernet256(FernetBase): + """ + Extended version of Fernet using AES256 encryption. + + The version byte differs from standard Fernet to distinguish this format. + The low 5 bits indicate the version of standard Fernet the extended version + is based on, with 0x1 indicating Fernet version 0x80. The high 3 bits + indicate the encryption key length, with binary 010 (0x2) indicating + 256-bit AES. This yields a version byte of 0x41 (binary 01000001). + """ + + def __init__(self, key, backend=None): + key = base64.urlsafe_b64decode(key) + if len(key) != 64: + raise ValueError( + "Fernet256 key must be 64 url-safe base64-encoded bytes." + ) + super(self.__class__, self).__init__(key, backend) + self._version = b"\x41" + + @classmethod + def generate_key(cls): + return cls._generate_key_of_length(256) diff -urw -x .git cryptography.master/tests/test_fernet.py cryptography.aes256/tests/test_fernet.py --- cryptography.master/tests/test_fernet.py 2016-05-26 13:10:09.849424805 -0700 +++ cryptography.aes256/tests/test_fernet.py 2016-05-30 18:24:50.419424296 -0700 @@ -16,7 +16,8 @@ import six -from cryptography.fernet import Fernet, InvalidToken, MultiFernet +from cryptography.fernet import ExtFernet192, ExtFernet256, Fernet +from cryptography.fernet import InvalidToken, MultiFernet from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends.interfaces import CipherBackend, HMACBackend from cryptography.hazmat.primitives.ciphers import algorithms, modes @@ -126,9 +127,95 @@ @pytest.mark.requires_backend_interface(interface=HMACBackend) @pytest.mark.supported( only_if=lambda backend: backend.cipher_supported( + algorithms.AES(b"\x00" * 16), modes.CBC(b"\x00" * 16) + ), + skip_message="Does not support AES128 CBC", +) +class TestExtFernet192(object): + # TODO test ExtFernet192 class + @json_parametrize( + ("secret", "now", "iv", "src", "token"), "generate_192.json", + ) + def test_generate(self, secret, now, iv, src, token, backend): + f = ExtFernet192(secret.encode("ascii"), backend=backend) + actual_token = f._encrypt_from_parts( + src.encode("ascii"), + calendar.timegm(iso8601.parse_date(now).utctimetuple()), + b"".join(map(six.int2byte, iv)) + ) + assert actual_token == token.encode("ascii") + + @json_parametrize( + ("secret", "now", "src", "ttl_sec", "token"), "verify_192.json", + ) + def test_verify(self, secret, now, src, ttl_sec, token, backend, + monkeypatch): + f = ExtFernet192(secret.encode("ascii"), backend=backend) + current_time = calendar.timegm(iso8601.parse_date(now).utctimetuple()) + monkeypatch.setattr(time, "time", lambda: current_time) + payload = f.decrypt(token.encode("ascii"), ttl=ttl_sec) + assert payload == src.encode("ascii") + + @pytest.mark.parametrize("message", [b"", b"Abc!", b"\x00\xFF\x00\x80"]) + def test_roundtrips(self, message, backend): + f = ExtFernet192(ExtFernet192.generate_key(), backend=backend) + assert f.decrypt(f.encrypt(message)) == message + + def test_bad_key(self, backend): + with pytest.raises(ValueError): + ExtFernet192(base64.urlsafe_b64encode(b"abc"), backend=backend) + + +@pytest.mark.requires_backend_interface(interface=CipherBackend) +@pytest.mark.requires_backend_interface(interface=HMACBackend) +@pytest.mark.supported( + only_if=lambda backend: backend.cipher_supported( + algorithms.AES(b"\x00" * 24), modes.CBC(b"\x00" * 16) + ), + skip_message="Does not support AES192 CBC", +) +class TestExtFernet256(object): + # TODO Test ExtFernet256 class + @json_parametrize( + ("secret", "now", "iv", "src", "token"), "generate_256.json", + ) + def test_generate(self, secret, now, iv, src, token, backend): + f = ExtFernet256(secret.encode("ascii"), backend=backend) + actual_token = f._encrypt_from_parts( + src.encode("ascii"), + calendar.timegm(iso8601.parse_date(now).utctimetuple()), + b"".join(map(six.int2byte, iv)) + ) + assert actual_token == token.encode("ascii") + + @json_parametrize( + ("secret", "now", "src", "ttl_sec", "token"), "verify_256.json", + ) + def test_verify(self, secret, now, src, ttl_sec, token, backend, + monkeypatch): + f = ExtFernet256(secret.encode("ascii"), backend=backend) + current_time = calendar.timegm(iso8601.parse_date(now).utctimetuple()) + monkeypatch.setattr(time, "time", lambda: current_time) + payload = f.decrypt(token.encode("ascii"), ttl=ttl_sec) + assert payload == src.encode("ascii") + + @pytest.mark.parametrize("message", [b"", b"Abc!", b"\x00\xFF\x00\x80"]) + def test_roundtrips(self, message, backend): + f = ExtFernet256(ExtFernet256.generate_key(), backend=backend) + assert f.decrypt(f.encrypt(message)) == message + + def test_bad_key(self, backend): + with pytest.raises(ValueError): + ExtFernet256(base64.urlsafe_b64encode(b"abc"), backend=backend) + + +@pytest.mark.requires_backend_interface(interface=CipherBackend) +@pytest.mark.requires_backend_interface(interface=HMACBackend) +@pytest.mark.supported( + only_if=lambda backend: backend.cipher_supported( algorithms.AES(b"\x00" * 32), modes.CBC(b"\x00" * 16) ), - skip_message="Does not support AES CBC", + skip_message="Does not support AES256 CBC", ) class TestMultiFernet(object): def test_encrypt(self, backend): Only in cryptography.aes256/vectors/cryptography_vectors/fernet: generate_192.json Only in cryptography.aes256/vectors/cryptography_vectors/fernet: generate_256.json Only in cryptography.aes256/vectors/cryptography_vectors/fernet: verify_192.json Only in cryptography.aes256/vectors/cryptography_vectors/fernet: verify_256.json
smime.p7s
Description: S/MIME Cryptographic Signature
_______________________________________________ Cryptography-dev mailing list Cryptography-dev@python.org https://mail.python.org/mailman/listinfo/cryptography-dev