diff options
-rw-r--r-- | cryptography/fernet.py | 76 | ||||
-rw-r--r-- | docs/fernet.rst | 53 | ||||
-rw-r--r-- | docs/index.rst | 1 | ||||
-rw-r--r-- | tests/test_fernet.py | 31 |
4 files changed, 161 insertions, 0 deletions
diff --git a/cryptography/fernet.py b/cryptography/fernet.py new file mode 100644 index 00000000..2de4a622 --- /dev/null +++ b/cryptography/fernet.py @@ -0,0 +1,76 @@ +import base64 +import os +import struct +import time + +import six + +from cryptography.hazmat.primitives import padding, hashes +from cryptography.hazmat.primitives.hmac import HMAC +from cryptography.hazmat.primitives.block import BlockCipher, ciphers, modes + + +class Fernet(object): + def __init__(self, key): + super(Fernet, self).__init__() + assert len(key) == 32 + self.signing_key = key[:16] + self.encryption_key = key[16:] + + def encrypt(self, data): + current_time = int(time.time()) + iv = os.urandom(16) + return self._encrypt_from_parts(data, current_time, iv) + + def _encrypt_from_parts(self, data, current_time, iv): + padder = padding.PKCS7(ciphers.AES.block_size).padder() + padded_data = padder.update(data) + padder.finalize() + encryptor = BlockCipher( + ciphers.AES(self.encryption_key), modes.CBC(iv) + ).encryptor() + ciphertext = encryptor.update(padded_data) + encryptor.finalize() + + h = HMAC(self.signing_key, digestmod=hashes.SHA256) + h.update(b"\x80") + h.update(struct.pack(">Q", current_time)) + h.update(iv) + h.update(ciphertext) + hmac = h.digest() + return base64.urlsafe_b64encode( + b"\x80" + struct.pack(">Q", current_time) + iv + ciphertext + hmac + ) + + def decrypt(self, data, ttl=None, current_time=None): + # TODO: whole function is a giant hack job with no error checking + if current_time is None: + current_time = int(time.time()) + data = base64.urlsafe_b64decode(data) + assert data[0] == b"\x80" + timestamp = data[1:9] + iv = data[9:25] + ciphertext = data[25:-32] + if ttl is not None: + if struct.unpack(">Q", timestamp)[0] + ttl < current_time: + raise ValueError + h = HMAC(self.signing_key, digestmod=hashes.SHA256) + h.update(data[:-32]) + hmac = h.digest() + if not constant_time_compare(hmac, data[-32:]): + raise ValueError + decryptor = BlockCipher( + ciphers.AES(self.encryption_key), modes.CBC(iv) + ).decryptor() + plaintext_padded = decryptor.update(ciphertext) + decryptor.finalize() + unpadder = padding.PKCS7(ciphers.AES.block_size).unpadder() + return unpadder.update(plaintext_padded) + unpadder.finalize() + + +def constant_time_compare(a, b): + # TOOD: replace with a cffi function + assert isinstance(a, bytes) and isinstance(b, bytes) + if len(a) != len(b): + return False + result = 0 + for i in xrange(len(a)): + result |= six.indexbytes(a, i) ^ six.indexbytes(b, i) + return result == 0 diff --git a/docs/fernet.rst b/docs/fernet.rst new file mode 100644 index 00000000..33488891 --- /dev/null +++ b/docs/fernet.rst @@ -0,0 +1,53 @@ +Fernet +====== + +.. currentmodule:: cryptography.fernet + +.. testsetup:: + + import binascii + key = binascii.unhexlify(b"0" * 64) + + +`Fernet`_ is an implementation of symmetric (also known as "secret key") +authenticated cryptography. Fernet provides guarntees that a message encrypted +using it cannot be manipulated or read without the key. + +.. class:: Fernet(key) + + This class provides both encryption and decryption facilities. + + .. doctest:: + + >>> from cryptography.fernet import Fernet + >>> f = Fernet(key) + >>> ciphertext = f.encrypt(b"my deep dark secret") + # Secret bytes. + >>> ciphertext + '...' + >>> f.decrypt(ciphertext) + 'my deep dark secret' + + :param bytes key: A 32-byte key. This **must** be kept secret. Anyone with + this key is able to create and read messages. + + + .. method:: encrypt(plaintext) + + :param bytes plaintext: The message you would like to encrypt. + :returns bytes: A secure message which cannot be read or altered + without the key. It is URL safe base64-encoded. + + .. method:: decrypt(ciphertext, ttl=None) + + :param bytes ciphertext: An encrypted message. + :param int ttl: Optionally, the number of seconds old a message may be + for it to be valid. If the message is older than + ``ttl`` seconds (from the time it was originally + created) an exception will be raised. If ``ttl`` is not + provided (or is ``None``), the age of the message is + not considered. + :returns bytes: The original plaintext. + + +.. _`Fernet`: https://github.com/fernet/spec/ diff --git a/docs/index.rst b/docs/index.rst index 4fd5d3be..b9c5b5fb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -30,6 +30,7 @@ Contents .. toctree:: :maxdepth: 2 + fernet architecture contributing security diff --git a/tests/test_fernet.py b/tests/test_fernet.py new file mode 100644 index 00000000..7bdfa3fa --- /dev/null +++ b/tests/test_fernet.py @@ -0,0 +1,31 @@ +import base64 + +import six + +from cryptography.fernet import Fernet + + +class TestFernet(object): + def test_generate(self): + f = Fernet(base64.urlsafe_b64decode( + b"cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" + )) + token = f._encrypt_from_parts( + b"hello", + 499162800, + b"".join(map(six.int2byte, range(16))), + ) + assert token == (b"gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM" + b"4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==") + + def test_verify(self): + f = Fernet(base64.urlsafe_b64decode( + b"cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" + )) + payload = f.decrypt( + (b"gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM4BLLF_5CV9dO" + b"PmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA=="), + ttl=60, + current_time=499162801 + ) + assert payload == b"hello" |