From 02fad008d3e99a49871144b56a692c2237a0d396 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Wed, 30 Oct 2013 14:16:13 -0700 Subject: Started implementating encryption for fernet --- cryptography/fernet.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 cryptography/fernet.py diff --git a/cryptography/fernet.py b/cryptography/fernet.py new file mode 100644 index 00000000..a0996afc --- /dev/null +++ b/cryptography/fernet.py @@ -0,0 +1,36 @@ +import base64 +import os +import struct +import time + +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__() + 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 + ) -- cgit v1.2.3 From bbeba7176d77df0ca47e2bad8a4f66915f07609d Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Wed, 30 Oct 2013 14:29:58 -0700 Subject: Implemented decryption --- cryptography/fernet.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index a0996afc..549abb36 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -3,6 +3,8 @@ 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 @@ -34,3 +36,30 @@ class Fernet(object): return base64.urlsafe_b64encode( b"\x80" + struct.pack(">Q", current_time) + iv + ciphertext + hmac ) + + def decrypt(self, data, ttl=None): + # TODO: whole function is a giant hack job with no error checking + data = base64.urlsafe_b64decode(data) + assert data[0] == b"\x80" + if ttl is not None: + if struct.unpack(">Q", data[1:9])[0] + ttl > int(time.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 + unencryptor = BlockCipher(ciphers.AES(self.encryption_key), modes.CBC(data[9:25])).unencryptor() + plaintext_padded = unencryptor.update(data[25:-32]) + unencryptor.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 -- cgit v1.2.3 From f593848419b0d871509df388dadef8c1c98d9a99 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Wed, 30 Oct 2013 14:34:55 -0700 Subject: Slightly cleaner --- cryptography/fernet.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index 549abb36..59d8ad0c 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -41,16 +41,20 @@ class Fernet(object): # TODO: whole function is a giant hack job with no error checking data = base64.urlsafe_b64decode(data) assert data[0] == b"\x80" + timestamp = data[1:9] + iv = data[9:25] + ciphertext = data[25:-32] + hmac = data[-32:] if ttl is not None: - if struct.unpack(">Q", data[1:9])[0] + ttl > int(time.time()): + if struct.unpack(">Q", timestamp)[0] + ttl > int(time.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 - unencryptor = BlockCipher(ciphers.AES(self.encryption_key), modes.CBC(data[9:25])).unencryptor() - plaintext_padded = unencryptor.update(data[25:-32]) + unencryptor.finalize() + unencryptor = BlockCipher(ciphers.AES(self.encryption_key), modes.CBC(iv)).unencryptor() + plaintext_padded = unencryptor.update(ciphertext) + unencryptor.finalize() unpadder = padding.PKCS7(ciphers.AES.block_size).unpadder() return unpadder.update(plaintext_padded) + unpadder.finalize() -- cgit v1.2.3 From 2b21b12d337e65c59b5d18e42f1927c64565945d Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 31 Oct 2013 09:39:25 -0700 Subject: Added test cases, fixed a bug --- cryptography/fernet.py | 4 ++-- tests/test_fernet.py | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 tests/test_fernet.py diff --git a/cryptography/fernet.py b/cryptography/fernet.py index 59d8ad0c..2c134bbd 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -53,8 +53,8 @@ class Fernet(object): hmac = h.digest() if not constant_time_compare(hmac, data[-32:]): raise ValueError - unencryptor = BlockCipher(ciphers.AES(self.encryption_key), modes.CBC(iv)).unencryptor() - plaintext_padded = unencryptor.update(ciphertext) + unencryptor.finalize() + 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() diff --git a/tests/test_fernet.py b/tests/test_fernet.py new file mode 100644 index 00000000..e9d07f81 --- /dev/null +++ b/tests/test_fernet.py @@ -0,0 +1,23 @@ +import base64 + +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"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F", + ) + assert token == b"gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==" + + def test_verify(self): + f = Fernet(base64.urlsafe_b64decode( + b"cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" + )) + payload = f.decrypt(b"gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==", 60) + assert payload == b"hello" -- cgit v1.2.3 From 5e87dfdd7f9853d4072efa6dd0e0515141ab7eb2 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 31 Oct 2013 09:46:03 -0700 Subject: Fixed test and implementation --- cryptography/fernet.py | 6 ++++-- tests/test_fernet.py | 10 ++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index 2c134bbd..ef747b7c 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -37,8 +37,10 @@ class Fernet(object): b"\x80" + struct.pack(">Q", current_time) + iv + ciphertext + hmac ) - def decrypt(self, data, ttl=None): + 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] @@ -46,7 +48,7 @@ class Fernet(object): ciphertext = data[25:-32] hmac = data[-32:] if ttl is not None: - if struct.unpack(">Q", timestamp)[0] + ttl > int(time.time()): + if struct.unpack(">Q", timestamp)[0] + ttl < current_time: raise ValueError h = HMAC(self.signing_key, digestmod=hashes.SHA256) h.update(data[:-32]) diff --git a/tests/test_fernet.py b/tests/test_fernet.py index e9d07f81..f7c06b95 100644 --- a/tests/test_fernet.py +++ b/tests/test_fernet.py @@ -13,11 +13,17 @@ class TestFernet(object): 499162800, b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F", ) - assert token == b"gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==" + assert token == (b"gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM" + "4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==") def test_verify(self): f = Fernet(base64.urlsafe_b64decode( b"cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" )) - payload = f.decrypt(b"gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==", 60) + payload = f.decrypt( + (b"gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM4BLLF_5CV9dO" + "PmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA=="), + ttl=60, + current_time=499162801 + ) assert payload == b"hello" -- cgit v1.2.3 From cd47c4ae14444ba0802d16fdb2960f7665dc9cbd Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 31 Oct 2013 09:46:27 -0700 Subject: Extra assert --- cryptography/fernet.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index ef747b7c..e4ee059c 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -13,6 +13,7 @@ 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:] -- cgit v1.2.3 From 05b94a6d5ea020ef1fec9d6192935687d2db7ebb Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 31 Oct 2013 09:49:35 -0700 Subject: Unused --- cryptography/fernet.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index e4ee059c..4220e0cb 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -47,7 +47,6 @@ class Fernet(object): timestamp = data[1:9] iv = data[9:25] ciphertext = data[25:-32] - hmac = data[-32:] if ttl is not None: if struct.unpack(">Q", timestamp)[0] + ttl < current_time: raise ValueError -- cgit v1.2.3 From de36e90815f31ec39fe160bf69a81f1bb42b92d2 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 31 Oct 2013 10:10:44 -0700 Subject: Address pep8 concerns --- cryptography/fernet.py | 9 +++++++-- tests/test_fernet.py | 8 +++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index 4220e0cb..2de4a622 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -25,7 +25,9 @@ class Fernet(object): 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() + 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) @@ -55,11 +57,14 @@ class Fernet(object): hmac = h.digest() if not constant_time_compare(hmac, data[-32:]): raise ValueError - decryptor = BlockCipher(ciphers.AES(self.encryption_key), modes.CBC(iv)).decryptor() + 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) diff --git a/tests/test_fernet.py b/tests/test_fernet.py index f7c06b95..27d24182 100644 --- a/tests/test_fernet.py +++ b/tests/test_fernet.py @@ -1,5 +1,7 @@ import base64 +import six + from cryptography.fernet import Fernet @@ -11,10 +13,10 @@ class TestFernet(object): token = f._encrypt_from_parts( b"hello", 499162800, - b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F", + b"".join(map(six.int2byte, range(16))), ) assert token == (b"gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM" - "4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==") + "4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==") def test_verify(self): f = Fernet(base64.urlsafe_b64decode( @@ -22,7 +24,7 @@ class TestFernet(object): )) payload = f.decrypt( (b"gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM4BLLF_5CV9dO" - "PmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA=="), + "PmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA=="), ttl=60, current_time=499162801 ) -- cgit v1.2.3 From 333fb1024e20fa10ec3e85cbd196cbdff059000d Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 31 Oct 2013 10:27:35 -0700 Subject: Docs --- docs/fernet.rst | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 2 files changed, 49 insertions(+) create mode 100644 docs/fernet.rst diff --git a/docs/fernet.rst b/docs/fernet.rst new file mode 100644 index 00000000..938ba0cb --- /dev/null +++ b/docs/fernet.rst @@ -0,0 +1,48 @@ +Fernet +====== + +.. currentmodule:: cryptography.fernet + +.. testsetup:: + + import binascii + key = binascii.unhexlify(b"0" * 32) + + +`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") + >>> 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. + + .. 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. + :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 -- cgit v1.2.3 From de475eb9f56a34868c7debb707427ab5678eda6c Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 31 Oct 2013 10:35:19 -0700 Subject: Improve the docs --- docs/fernet.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/fernet.rst b/docs/fernet.rst index 938ba0cb..ac610eb8 100644 --- a/docs/fernet.rst +++ b/docs/fernet.rst @@ -22,6 +22,9 @@ using it cannot be manipulated or read without the key. >>> 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' @@ -33,7 +36,7 @@ using it cannot be manipulated or read without the key. :param bytes plaintext: The message you would like to encrypt. :returns bytes: A secure message which cannot be read or altered - without the key. + without the key. It is URL safe base64-encoded. .. method:: decrypt(ciphertext, ttl=None) -- cgit v1.2.3 From 13e0d54510d3f939c749d3efc810bad675f4f908 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 31 Oct 2013 10:38:04 -0700 Subject: Be explicit --- docs/fernet.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/fernet.rst b/docs/fernet.rst index ac610eb8..d44e737b 100644 --- a/docs/fernet.rst +++ b/docs/fernet.rst @@ -44,7 +44,9 @@ using it cannot be manipulated or read without the key. :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. + 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. -- cgit v1.2.3 From 36e2df0955aa1c6534049be21868c24e93569b8b Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 31 Oct 2013 10:40:17 -0700 Subject: Fixed keylength in example --- docs/fernet.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fernet.rst b/docs/fernet.rst index d44e737b..33488891 100644 --- a/docs/fernet.rst +++ b/docs/fernet.rst @@ -6,7 +6,7 @@ Fernet .. testsetup:: import binascii - key = binascii.unhexlify(b"0" * 32) + key = binascii.unhexlify(b"0" * 64) `Fernet`_ is an implementation of symmetric (also known as "secret key") -- cgit v1.2.3 From 413bd8b45a51dc9c9afe3262534abba2d8528457 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 31 Oct 2013 11:22:11 -0700 Subject: py3k syntax fix --- tests/test_fernet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_fernet.py b/tests/test_fernet.py index 27d24182..7bdfa3fa 100644 --- a/tests/test_fernet.py +++ b/tests/test_fernet.py @@ -16,7 +16,7 @@ class TestFernet(object): b"".join(map(six.int2byte, range(16))), ) assert token == (b"gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM" - "4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==") + b"4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==") def test_verify(self): f = Fernet(base64.urlsafe_b64decode( @@ -24,7 +24,7 @@ class TestFernet(object): )) payload = f.decrypt( (b"gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM4BLLF_5CV9dO" - "PmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA=="), + b"PmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA=="), ttl=60, current_time=499162801 ) -- cgit v1.2.3 From 5c5342ec17aba6f6f10ab2136192be97180e2225 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 31 Oct 2013 11:25:54 -0700 Subject: fix for py3k --- cryptography/fernet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index 2de4a622..d6a94424 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -45,7 +45,7 @@ class Fernet(object): if current_time is None: current_time = int(time.time()) data = base64.urlsafe_b64decode(data) - assert data[0] == b"\x80" + assert six.indexbytes(data, 0) == 0x80 timestamp = data[1:9] iv = data[9:25] ciphertext = data[25:-32] -- cgit v1.2.3 From 5ac6524f790713090754572fb775405f64a87df2 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 31 Oct 2013 11:28:13 -0700 Subject: fix --- docs/fernet.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/fernet.rst b/docs/fernet.rst index 33488891..02b99705 100644 --- a/docs/fernet.rst +++ b/docs/fernet.rst @@ -22,7 +22,6 @@ using it cannot be manipulated or read without the key. >>> from cryptography.fernet import Fernet >>> f = Fernet(key) >>> ciphertext = f.encrypt(b"my deep dark secret") - # Secret bytes. >>> ciphertext '...' >>> f.decrypt(ciphertext) -- cgit v1.2.3 From 139cf462ca0126d6ed161a4b32e6b6c889c77318 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 31 Oct 2013 11:36:01 -0700 Subject: py3k fix --- cryptography/fernet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index d6a94424..064aceec 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -71,6 +71,6 @@ def constant_time_compare(a, b): if len(a) != len(b): return False result = 0 - for i in xrange(len(a)): + for i in range(len(a)): result |= six.indexbytes(a, i) ^ six.indexbytes(b, i) return result == 0 -- cgit v1.2.3 From fb8adfcb2f0a67519ee81cad0c50d2e359ff3a20 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 31 Oct 2013 14:16:24 -0700 Subject: Use raw vector files --- dev-requirements.txt | 3 +- tests/test_fernet.py | 56 +++++++++++++++++++++++++------------- tests/vectors/fernet/generate.json | 9 ++++++ tests/vectors/fernet/verify.json | 9 ++++++ tox.ini | 3 +- 5 files changed, 59 insertions(+), 21 deletions(-) create mode 100644 tests/vectors/fernet/generate.json create mode 100644 tests/vectors/fernet/verify.json diff --git a/dev-requirements.txt b/dev-requirements.txt index 752517dd..530ada91 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,6 +1,7 @@ +coverage flake8 +iso8601 pretend pytest -coverage sphinx tox diff --git a/tests/test_fernet.py b/tests/test_fernet.py index 7bdfa3fa..382a232c 100644 --- a/tests/test_fernet.py +++ b/tests/test_fernet.py @@ -1,31 +1,49 @@ import base64 +import calendar +import json +import os + +import iso8601 + +import pytest import six from cryptography.fernet import Fernet +def json_parametrize(keys, path): + with open(path) as f: + data = json.load(f) + return pytest.mark.parametrize(keys, [ + tuple([entry[k] for k in keys]) + for entry in data + ]) + + 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))), + @json_parametrize( + ("secret", "now", "iv", "src", "token"), + os.path.join(os.path.dirname(__file__), "vectors", "fernet", "generate.json") + ) + def test_generate(self, secret, now, iv, src, token): + f = Fernet(base64.urlsafe_b64decode(secret.encode("ascii"))) + actual_token = f._encrypt_from_parts( + src.encode("ascii"), + calendar.timegm(iso8601.parse_date(now).utctimetuple()), + b"".join(map(six.int2byte, iv)) ) - assert token == (b"gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM" - b"4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==") + assert actual_token == token - def test_verify(self): - f = Fernet(base64.urlsafe_b64decode( - b"cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" - )) + @json_parametrize( + ("secret", "now", "src", "ttl_sec", "token"), + os.path.join(os.path.dirname(__file__), "vectors", "fernet", "verify.json") + ) + def test_verify(self, secret, now, src, ttl_sec, token): + f = Fernet(base64.urlsafe_b64decode(secret.encode("ascii"))) payload = f.decrypt( - (b"gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM4BLLF_5CV9dO" - b"PmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA=="), - ttl=60, - current_time=499162801 + token.encode("ascii"), + ttl=ttl_sec, + current_time=calendar.timegm(iso8601.parse_date(now).utctimetuple()) ) - assert payload == b"hello" + assert payload == src diff --git a/tests/vectors/fernet/generate.json b/tests/vectors/fernet/generate.json new file mode 100644 index 00000000..d1f3e083 --- /dev/null +++ b/tests/vectors/fernet/generate.json @@ -0,0 +1,9 @@ +[ + { + "token": "gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==", + "now": "1985-10-26T01:20:00-07:00", + "iv": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + "src": "hello", + "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" + } +] diff --git a/tests/vectors/fernet/verify.json b/tests/vectors/fernet/verify.json new file mode 100644 index 00000000..08c480f5 --- /dev/null +++ b/tests/vectors/fernet/verify.json @@ -0,0 +1,9 @@ +[ + { + "token": "gAAAAAAdwJ6wAAECAwQFBgcICQoLDA0ODy021cpGVWKZ_eEwCGM4BLLF_5CV9dOPmrhuVUPgJobwOz7JcbmrR64jVmpU4IwqDA==", + "now": "1985-10-26T01:20:01-07:00", + "ttl_sec": 60, + "src": "hello", + "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" + } +] diff --git a/tox.ini b/tox.ini index 92bcb75b..57b42412 100644 --- a/tox.ini +++ b/tox.ini @@ -3,9 +3,10 @@ envlist = py26,py27,pypy,py32,py33,docs,pep8 [testenv] deps = - pytest coverage + iso8601 pretend + pytest commands = coverage run --source=cryptography/,tests/ -m pytest coverage report -m --fail-under 100 -- cgit v1.2.3 From 38f34557e432f98cc8a023e621b5efe525ef886c Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 31 Oct 2013 14:50:00 -0700 Subject: Started working on the invalid cases --- cryptography/fernet.py | 32 +++++++++++++++++---- tests/test_fernet.py | 28 +++++++++++++++---- tests/vectors/fernet/invalid.json | 58 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 11 deletions(-) create mode 100644 tests/vectors/fernet/invalid.json diff --git a/cryptography/fernet.py b/cryptography/fernet.py index 064aceec..880d96f0 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -10,6 +10,10 @@ from cryptography.hazmat.primitives.hmac import HMAC from cryptography.hazmat.primitives.block import BlockCipher, ciphers, modes +class InvalidToken(Exception): + pass + + class Fernet(object): def __init__(self, key): super(Fernet, self).__init__() @@ -23,6 +27,9 @@ class Fernet(object): return self._encrypt_from_parts(data, current_time, iv) def _encrypt_from_parts(self, data, current_time, iv): + if isinstance(data, six.text_type): + raise TypeError("Unicode-objects must be encoded before encryption") + padder = padding.PKCS7(ciphers.AES.block_size).padder() padded_data = padder.update(data) + padder.finalize() encryptor = BlockCipher( @@ -41,28 +48,43 @@ class Fernet(object): ) def decrypt(self, data, ttl=None, current_time=None): - # TODO: whole function is a giant hack job with no error checking + if isinstance(data, six.text_type): + raise TypeError("Unicode-objects must be encoded before decryption") + if current_time is None: current_time = int(time.time()) - data = base64.urlsafe_b64decode(data) + + try: + data = base64.urlsafe_b64decode(data) + except TypeError: + raise InvalidToken + assert six.indexbytes(data, 0) == 0x80 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 + raise InvalidToken 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 + raise InvalidToken + 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() + + unpadded = unpadder.update(plaintext_padded) + try: + unpadded += unpadder.finalize() + except ValueError: + raise InvalidToken + return unpadded def constant_time_compare(a, b): diff --git a/tests/test_fernet.py b/tests/test_fernet.py index 382a232c..15071718 100644 --- a/tests/test_fernet.py +++ b/tests/test_fernet.py @@ -9,10 +9,11 @@ import pytest import six -from cryptography.fernet import Fernet +from cryptography.fernet import Fernet, InvalidToken -def json_parametrize(keys, path): +def json_parametrize(keys, fname): + path = os.path.join(os.path.dirname(__file__), "vectors", "fernet", fname) with open(path) as f: data = json.load(f) return pytest.mark.parametrize(keys, [ @@ -23,8 +24,7 @@ def json_parametrize(keys, path): class TestFernet(object): @json_parametrize( - ("secret", "now", "iv", "src", "token"), - os.path.join(os.path.dirname(__file__), "vectors", "fernet", "generate.json") + ("secret", "now", "iv", "src", "token"), "generate.json", ) def test_generate(self, secret, now, iv, src, token): f = Fernet(base64.urlsafe_b64decode(secret.encode("ascii"))) @@ -36,8 +36,7 @@ class TestFernet(object): assert actual_token == token @json_parametrize( - ("secret", "now", "src", "ttl_sec", "token"), - os.path.join(os.path.dirname(__file__), "vectors", "fernet", "verify.json") + ("secret", "now", "src", "ttl_sec", "token"), "verify.json", ) def test_verify(self, secret, now, src, ttl_sec, token): f = Fernet(base64.urlsafe_b64decode(secret.encode("ascii"))) @@ -47,3 +46,20 @@ class TestFernet(object): current_time=calendar.timegm(iso8601.parse_date(now).utctimetuple()) ) assert payload == src + + @json_parametrize(("secret", "token", "now", "ttl_sec"), "invalid.json") + def test_invalid(self, secret, token, now, ttl_sec): + f = Fernet(base64.urlsafe_b64decode(secret.encode("ascii"))) + with pytest.raises(InvalidToken): + f.decrypt( + token.encode("ascii"), + ttl=ttl_sec, + current_time=calendar.timegm(iso8601.parse_date(now).utctimetuple()) + ) + + def test_unicode(self): + f = Fernet(b"\x00" * 32) + with pytest.raises(TypeError): + f.encrypt(six.u("")) + with pytest.raises(TypeError): + f.decrypt(six.u("")) diff --git a/tests/vectors/fernet/invalid.json b/tests/vectors/fernet/invalid.json new file mode 100644 index 00000000..d80e7b4a --- /dev/null +++ b/tests/vectors/fernet/invalid.json @@ -0,0 +1,58 @@ +[ + { + "desc": "incorrect mac", + "token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAl1-szkFVzXTuGb4hR8AKtwcaX1YdykQUFBQUFBQUFBQQ==", + "now": "1985-10-26T01:20:01-07:00", + "ttl_sec": 60, + "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" + }, + { + "desc": "too short", + "token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPA==", + "now": "1985-10-26T01:20:01-07:00", + "ttl_sec": 60, + "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" + }, + { + "desc": "invalid base64", + "token": "%%%%%%%%%%%%%AECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAl1-szkFVzXTuGb4hR8AKtwcaX1YdykRtfsH-p1YsUD2Q==", + "now": "1985-10-26T01:20:01-07:00", + "ttl_sec": 60, + "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" + }, + { + "desc": "payload size not multiple of block size", + "token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPOm73QeoCk9uGib28Xe5vz6oxq5nmxbx_v7mrfyudzUm", + "now": "1985-10-26T01:20:01-07:00", + "ttl_sec": 60, + "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" + }, + { + "desc": "payload padding error", + "token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0ODz4LEpdELGQAad7aNEHbf-JkLPIpuiYRLQ3RtXatOYREu2FWke6CnJNYIbkuKNqOhw==", + "now": "1985-10-26T01:20:01-07:00", + "ttl_sec": 60, + "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" + }, + { + "desc": "far-future TS (unacceptable clock skew)", + "token": "gAAAAAAdwStRAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAnja1xKYyhd-Y6mSkTOyTGJmw2Xc2a6kBd-iX9b_qXQcw==", + "now": "1985-10-26T01:20:01-07:00", + "ttl_sec": 60, + "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" + }, + { + "desc": "expired TTL", + "token": "gAAAAAAdwJ6xAAECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAl1-szkFVzXTuGb4hR8AKtwcaX1YdykRtfsH-p1YsUD2Q==", + "now": "1985-10-26T01:21:31-07:00", + "ttl_sec": 60, + "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" + }, + { + "desc": "incorrect IV (causes padding error)", + "token": "gAAAAAAdwJ6xBQECAwQFBgcICQoLDA0OD3HkMATM5lFqGaerZ-fWPAkLhFLHpGtDBRLRTZeUfWgHSv49TF2AUEZ1TIvcZjK1zQ==", + "now": "1985-10-26T01:20:01-07:00", + "ttl_sec": 60, + "secret": "cw_0x689RpI-jtRR7oE8h_eQsKImvJapLeSbXpwF4e4=" + } +] -- cgit v1.2.3 From c1ea0a0d23115bb0586230a139bcb2b60adb6262 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 31 Oct 2013 15:03:53 -0700 Subject: Fixed pep8 issues --- cryptography/fernet.py | 8 ++++++-- tests/test_fernet.py | 10 ++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index 880d96f0..ef64b7e9 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -28,7 +28,9 @@ class Fernet(object): def _encrypt_from_parts(self, data, current_time, iv): if isinstance(data, six.text_type): - raise TypeError("Unicode-objects must be encoded before encryption") + raise TypeError( + "Unicode-objects must be encoded before encryption" + ) padder = padding.PKCS7(ciphers.AES.block_size).padder() padded_data = padder.update(data) + padder.finalize() @@ -49,7 +51,9 @@ class Fernet(object): def decrypt(self, data, ttl=None, current_time=None): if isinstance(data, six.text_type): - raise TypeError("Unicode-objects must be encoded before decryption") + raise TypeError( + "Unicode-objects must be encoded before decryption" + ) if current_time is None: current_time = int(time.time()) diff --git a/tests/test_fernet.py b/tests/test_fernet.py index 15071718..b0f22f0c 100644 --- a/tests/test_fernet.py +++ b/tests/test_fernet.py @@ -40,21 +40,19 @@ class TestFernet(object): ) def test_verify(self, secret, now, src, ttl_sec, token): f = Fernet(base64.urlsafe_b64decode(secret.encode("ascii"))) + current_time = calendar.timegm(iso8601.parse_date(now).utctimetuple()) payload = f.decrypt( - token.encode("ascii"), - ttl=ttl_sec, - current_time=calendar.timegm(iso8601.parse_date(now).utctimetuple()) + token.encode("ascii"), ttl=ttl_sec, current_time=current_time ) assert payload == src @json_parametrize(("secret", "token", "now", "ttl_sec"), "invalid.json") def test_invalid(self, secret, token, now, ttl_sec): f = Fernet(base64.urlsafe_b64decode(secret.encode("ascii"))) + current_time = calendar.timegm(iso8601.parse_date(now).utctimetuple()) with pytest.raises(InvalidToken): f.decrypt( - token.encode("ascii"), - ttl=ttl_sec, - current_time=calendar.timegm(iso8601.parse_date(now).utctimetuple()) + token.encode("ascii"), ttl=ttl_sec, current_time=current_time ) def test_unicode(self): -- cgit v1.2.3 From ce8f9a4e2a5d159356a06147f65e221dbdf43171 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 31 Oct 2013 15:23:15 -0700 Subject: A test for roundtripping --- tests/test_fernet.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_fernet.py b/tests/test_fernet.py index b0f22f0c..a42011a6 100644 --- a/tests/test_fernet.py +++ b/tests/test_fernet.py @@ -61,3 +61,10 @@ class TestFernet(object): f.encrypt(six.u("")) with pytest.raises(TypeError): f.decrypt(six.u("")) + + @pytest.mark.parametrize("message", [b"", b"Abc!", b"\x00\xFF\x00\x80"]) + def test_roundtrips(self, message): + f = Fernet(b"\x00" * 32) + ciphertext = f.encrypt(message) + plaintext = f.decrypt(ciphertext) + assert plaintext == message -- cgit v1.2.3 From 69ab59e59670c3865fb8240aa81ca9195a96e003 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 31 Oct 2013 15:26:43 -0700 Subject: py3k uses a different exception here --- cryptography/fernet.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index ef64b7e9..f923911a 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -1,4 +1,5 @@ import base64 +import binascii import os import struct import time @@ -60,7 +61,7 @@ class Fernet(object): try: data = base64.urlsafe_b64decode(data) - except TypeError: + except (TypeError, binascii.Error): raise InvalidToken assert six.indexbytes(data, 0) == 0x80 -- cgit v1.2.3 From 3d5041cda29f8201418a4c2d340059f68c7bb41d Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 31 Oct 2013 15:55:33 -0700 Subject: Moved constant_time_compare to be C --- cryptography/fernet.py | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index f923911a..214b9374 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -4,6 +4,8 @@ import os import struct import time +import cffi + import six from cryptography.hazmat.primitives import padding, hashes @@ -15,6 +17,25 @@ class InvalidToken(Exception): pass +ffi = cffi.FFI() +ffi.cdef(""" +bool constant_time_compare(uint8_t *, size_t, uint8_t *, size_t); +""") +lib = ffi.verify(""" +#include + +bool constant_time_compare(uint8_t *a, size_t len_a, uint8_t *b, size_t len_b) { + if (len_a != len_b) { + return false; + } + int result = 0; + for (size_t i = 0; i < len_a; i++) { + result |= a[i] ^ b[i]; + } + return result == 0; +} +""") + class Fernet(object): def __init__(self, key): super(Fernet, self).__init__() @@ -75,7 +96,7 @@ class Fernet(object): h.update(data[:-32]) hmac = h.digest() - if not constant_time_compare(hmac, data[-32:]): + if not lib.constant_time_compare(hmac, len(hmac), data[-32:], 32): raise InvalidToken decryptor = BlockCipher( @@ -90,14 +111,3 @@ class Fernet(object): except ValueError: raise InvalidToken return unpadded - - -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 range(len(a)): - result |= six.indexbytes(a, i) ^ six.indexbytes(b, i) - return result == 0 -- cgit v1.2.3 From 6b9770b159b920cccddc8e3c3d1b0fb0287a0c15 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 31 Oct 2013 16:07:35 -0700 Subject: write more readably --- tests/test_fernet.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_fernet.py b/tests/test_fernet.py index a42011a6..baae36d2 100644 --- a/tests/test_fernet.py +++ b/tests/test_fernet.py @@ -65,6 +65,4 @@ class TestFernet(object): @pytest.mark.parametrize("message", [b"", b"Abc!", b"\x00\xFF\x00\x80"]) def test_roundtrips(self, message): f = Fernet(b"\x00" * 32) - ciphertext = f.encrypt(message) - plaintext = f.decrypt(ciphertext) - assert plaintext == message + assert f.decrypt(f.encrypt(message)) == message -- cgit v1.2.3 From 38db2140e270c19d781aa35c5aa098a107179141 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 31 Oct 2013 16:25:25 -0700 Subject: Be C99 --- cryptography/fernet.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index 214b9374..10640d1b 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -25,11 +25,12 @@ lib = ffi.verify(""" #include bool constant_time_compare(uint8_t *a, size_t len_a, uint8_t *b, size_t len_b) { + size_t i = 0; + int result = 0; if (len_a != len_b) { return false; } - int result = 0; - for (size_t i = 0; i < len_a; i++) { + for (i = 0; i < len_a; i++) { result |= a[i] ^ b[i]; } return result == 0; -- cgit v1.2.3 From 7ecd3148acc35668bf679be5a603ed4bd7313148 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 31 Oct 2013 16:29:18 -0700 Subject: py3k fixes --- tests/test_fernet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_fernet.py b/tests/test_fernet.py index baae36d2..ca8e4ccd 100644 --- a/tests/test_fernet.py +++ b/tests/test_fernet.py @@ -33,7 +33,7 @@ class TestFernet(object): calendar.timegm(iso8601.parse_date(now).utctimetuple()), b"".join(map(six.int2byte, iv)) ) - assert actual_token == token + assert actual_token == token.encode("ascii") @json_parametrize( ("secret", "now", "src", "ttl_sec", "token"), "verify.json", @@ -44,7 +44,7 @@ class TestFernet(object): payload = f.decrypt( token.encode("ascii"), ttl=ttl_sec, current_time=current_time ) - assert payload == src + assert payload == src.encode("ascii") @json_parametrize(("secret", "token", "now", "ttl_sec"), "invalid.json") def test_invalid(self, secret, token, now, ttl_sec): -- cgit v1.2.3 From 2c58bbe5fa222fed3d917252a64868171443def9 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 31 Oct 2013 16:31:38 -0700 Subject: flake8 --- cryptography/fernet.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index 10640d1b..f9d5e93f 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -24,7 +24,8 @@ bool constant_time_compare(uint8_t *, size_t, uint8_t *, size_t); lib = ffi.verify(""" #include -bool constant_time_compare(uint8_t *a, size_t len_a, uint8_t *b, size_t len_b) { +bool constant_time_compare(uint8_t *a, size_t len_a, uint8_t *b, + size_t len_b) { size_t i = 0; int result = 0; if (len_a != len_b) { @@ -37,6 +38,7 @@ bool constant_time_compare(uint8_t *a, size_t len_a, uint8_t *b, size_t len_b) { } """) + class Fernet(object): def __init__(self, key): super(Fernet, self).__init__() -- cgit v1.2.3 From fa7081a6275b876c0fd734d0ac3a27b591b661f6 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 1 Nov 2013 16:38:25 -0700 Subject: Update for new API --- cryptography/fernet.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index f9d5e93f..f907a363 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -64,12 +64,12 @@ class Fernet(object): ).encryptor() ciphertext = encryptor.update(padded_data) + encryptor.finalize() - h = HMAC(self.signing_key, digestmod=hashes.SHA256) + h = HMAC(self.signing_key, hashes.SHA256()) h.update(b"\x80") h.update(struct.pack(">Q", current_time)) h.update(iv) h.update(ciphertext) - hmac = h.digest() + hmac = h.finalize() return base64.urlsafe_b64encode( b"\x80" + struct.pack(">Q", current_time) + iv + ciphertext + hmac ) @@ -95,9 +95,9 @@ class Fernet(object): if ttl is not None: if struct.unpack(">Q", timestamp)[0] + ttl < current_time: raise InvalidToken - h = HMAC(self.signing_key, digestmod=hashes.SHA256) + h = HMAC(self.signing_key, hashes.SHA256()) h.update(data[:-32]) - hmac = h.digest() + hmac = h.finalize() if not lib.constant_time_compare(hmac, len(hmac), data[-32:], 32): raise InvalidToken -- cgit v1.2.3 From 8912d3afde05ff6f91a508f0a54b5c24960eb09a Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 2 Nov 2013 14:04:19 -0700 Subject: Include the license --- cryptography/fernet.py | 13 +++++++++++++ tests/test_fernet.py | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index f907a363..d1138cb2 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -1,3 +1,16 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import base64 import binascii import os diff --git a/tests/test_fernet.py b/tests/test_fernet.py index ca8e4ccd..922f7223 100644 --- a/tests/test_fernet.py +++ b/tests/test_fernet.py @@ -1,3 +1,16 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import base64 import calendar import json -- cgit v1.2.3 From 105e8137799a2ef7ec8275e3c01d61a04884413b Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 7 Nov 2013 14:25:42 -0800 Subject: Pass around a backend --- cryptography/fernet.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index d1138cb2..668958be 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -53,11 +53,12 @@ bool constant_time_compare(uint8_t *a, size_t len_a, uint8_t *b, class Fernet(object): - def __init__(self, key): + def __init__(self, key, backend=None): super(Fernet, self).__init__() assert len(key) == 32 self.signing_key = key[:16] self.encryption_key = key[16:] + self.backend = backend def encrypt(self, data): current_time = int(time.time()) @@ -73,11 +74,11 @@ class Fernet(object): 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) + ciphers.AES(self.encryption_key), modes.CBC(iv), self.backend ).encryptor() ciphertext = encryptor.update(padded_data) + encryptor.finalize() - h = HMAC(self.signing_key, hashes.SHA256()) + h = HMAC(self.signing_key, hashes.SHA256(), self.backend) h.update(b"\x80") h.update(struct.pack(">Q", current_time)) h.update(iv) @@ -108,7 +109,7 @@ class Fernet(object): if ttl is not None: if struct.unpack(">Q", timestamp)[0] + ttl < current_time: raise InvalidToken - h = HMAC(self.signing_key, hashes.SHA256()) + h = HMAC(self.signing_key, hashes.SHA256(), self.backend) h.update(data[:-32]) hmac = h.finalize() @@ -116,7 +117,7 @@ class Fernet(object): raise InvalidToken decryptor = BlockCipher( - ciphers.AES(self.encryption_key), modes.CBC(iv) + ciphers.AES(self.encryption_key), modes.CBC(iv), self.backend ).decryptor() plaintext_padded = decryptor.update(ciphertext) + decryptor.finalize() unpadder = padding.PKCS7(ciphers.AES.block_size).unpadder() -- cgit v1.2.3 From dcc3f668c590bd0da898f331b968ed56d46936cf Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 7 Nov 2013 14:28:16 -0800 Subject: Fixes for the module renaming --- cryptography/fernet.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index 668958be..2a9f6a96 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -23,7 +23,7 @@ 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 +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes class InvalidToken(Exception): @@ -71,10 +71,10 @@ class Fernet(object): "Unicode-objects must be encoded before encryption" ) - padder = padding.PKCS7(ciphers.AES.block_size).padder() + padder = padding.PKCS7(algorithms.AES.block_size).padder() padded_data = padder.update(data) + padder.finalize() - encryptor = BlockCipher( - ciphers.AES(self.encryption_key), modes.CBC(iv), self.backend + encryptor = Cipher( + algorithms.AES(self.encryption_key), modes.CBC(iv), self.backend ).encryptor() ciphertext = encryptor.update(padded_data) + encryptor.finalize() @@ -116,11 +116,11 @@ class Fernet(object): if not lib.constant_time_compare(hmac, len(hmac), data[-32:], 32): raise InvalidToken - decryptor = BlockCipher( - ciphers.AES(self.encryption_key), modes.CBC(iv), self.backend + decryptor = Cipher( + algorithms.AES(self.encryption_key), modes.CBC(iv), self.backend ).decryptor() plaintext_padded = decryptor.update(ciphertext) + decryptor.finalize() - unpadder = padding.PKCS7(ciphers.AES.block_size).unpadder() + unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() unpadded = unpadder.update(plaintext_padded) try: -- cgit v1.2.3 From 286433de03ad1f54f1f563922a327d22945b0733 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Mon, 18 Nov 2013 10:15:20 -0800 Subject: Make it more constant time --- cryptography/fernet.py | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index 2a9f6a96..abd5e251 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -30,24 +30,39 @@ class InvalidToken(Exception): pass -ffi = cffi.FFI() -ffi.cdef(""" -bool constant_time_compare(uint8_t *, size_t, uint8_t *, size_t); +_ffi = cffi.FFI() +_ffi.cdef(""" +bool Cryptography_constant_time_compare(uint8_t *, size_t, uint8_t *, size_t); """) -lib = ffi.verify(""" +_lib = _ffi.verify(""" #include -bool constant_time_compare(uint8_t *a, size_t len_a, uint8_t *b, - size_t len_b) { + +/* Returns the value of the input with the most-significant-bit copied to all + of the bits. This relies on implementation details of computers with 2's + complement representations of integers, which is not required by the C + standard. */ +static uint8_t Cryptography_DUPLICATE_MSB_TO_ALL(uint8_t a) { + return (uint8_t)((int8_t)(a) >> (sizeof(int8_t) * 8 - 1)); +} + +bool Cryptography_constant_time_compare(uint8_t *a, size_t len_a, uint8_t *b, + size_t len_b) { size_t i = 0; - int result = 0; + uint8_t mismatch = 0; if (len_a != len_b) { return false; } for (i = 0; i < len_a; i++) { - result |= a[i] ^ b[i]; + mismatch |= a[i] ^ b[i]; } - return result == 0; + + /* Make sure any bits set are copied to the lowest bit */ + mismatch |= mismatch >> 4; + mismatch |= mismatch >> 2; + mismatch |= mismatch >> 1; + /* Now check the low bit to see if it's set */ + return (mismatch & 1) == 0; } """) @@ -112,8 +127,10 @@ class Fernet(object): h = HMAC(self.signing_key, hashes.SHA256(), self.backend) h.update(data[:-32]) hmac = h.finalize() - - if not lib.constant_time_compare(hmac, len(hmac), data[-32:], 32): + valid = _lib.Cryptography_constant_time_compare( + hmac, len(hmac), data[-32:], 32 + ) + if not valid: raise InvalidToken decryptor = Cipher( -- cgit v1.2.3 From 867acfa0300ca75f2c11c15490e04b556d8bfe99 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Mon, 18 Nov 2013 10:19:31 -0800 Subject: This is unused --- cryptography/fernet.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index abd5e251..32bd35d5 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -37,15 +37,6 @@ bool Cryptography_constant_time_compare(uint8_t *, size_t, uint8_t *, size_t); _lib = _ffi.verify(""" #include - -/* Returns the value of the input with the most-significant-bit copied to all - of the bits. This relies on implementation details of computers with 2's - complement representations of integers, which is not required by the C - standard. */ -static uint8_t Cryptography_DUPLICATE_MSB_TO_ALL(uint8_t a) { - return (uint8_t)((int8_t)(a) >> (sizeof(int8_t) * 8 - 1)); -} - bool Cryptography_constant_time_compare(uint8_t *a, size_t len_a, uint8_t *b, size_t len_b) { size_t i = 0; -- cgit v1.2.3 From 898fe0f899eb3ec744acaaa0a8641644fc6cf219 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Wed, 20 Nov 2013 16:38:32 -0800 Subject: Key in the right place --- cryptography/fernet.py | 1 + tests/test_fernet.py | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index 32bd35d5..ae3a8bfa 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -61,6 +61,7 @@ bool Cryptography_constant_time_compare(uint8_t *a, size_t len_a, uint8_t *b, class Fernet(object): def __init__(self, key, backend=None): super(Fernet, self).__init__() + key = base64.urlsafe_b64decode(key) assert len(key) == 32 self.signing_key = key[:16] self.encryption_key = key[16:] diff --git a/tests/test_fernet.py b/tests/test_fernet.py index 922f7223..4080bd2d 100644 --- a/tests/test_fernet.py +++ b/tests/test_fernet.py @@ -40,7 +40,7 @@ class TestFernet(object): ("secret", "now", "iv", "src", "token"), "generate.json", ) def test_generate(self, secret, now, iv, src, token): - f = Fernet(base64.urlsafe_b64decode(secret.encode("ascii"))) + f = Fernet(secret.encode("ascii")) actual_token = f._encrypt_from_parts( src.encode("ascii"), calendar.timegm(iso8601.parse_date(now).utctimetuple()), @@ -52,7 +52,7 @@ class TestFernet(object): ("secret", "now", "src", "ttl_sec", "token"), "verify.json", ) def test_verify(self, secret, now, src, ttl_sec, token): - f = Fernet(base64.urlsafe_b64decode(secret.encode("ascii"))) + f = Fernet(secret.encode("ascii")) current_time = calendar.timegm(iso8601.parse_date(now).utctimetuple()) payload = f.decrypt( token.encode("ascii"), ttl=ttl_sec, current_time=current_time @@ -61,7 +61,7 @@ class TestFernet(object): @json_parametrize(("secret", "token", "now", "ttl_sec"), "invalid.json") def test_invalid(self, secret, token, now, ttl_sec): - f = Fernet(base64.urlsafe_b64decode(secret.encode("ascii"))) + f = Fernet(secret.encode("ascii")) current_time = calendar.timegm(iso8601.parse_date(now).utctimetuple()) with pytest.raises(InvalidToken): f.decrypt( @@ -69,7 +69,7 @@ class TestFernet(object): ) def test_unicode(self): - f = Fernet(b"\x00" * 32) + f = Fernet(base64.b64encode(b"\x00" * 32)) with pytest.raises(TypeError): f.encrypt(six.u("")) with pytest.raises(TypeError): @@ -77,5 +77,5 @@ class TestFernet(object): @pytest.mark.parametrize("message", [b"", b"Abc!", b"\x00\xFF\x00\x80"]) def test_roundtrips(self, message): - f = Fernet(b"\x00" * 32) + f = Fernet(base64.urlsafe_b64encode(b"\x00" * 32)) assert f.decrypt(f.encrypt(message)) == message -- cgit v1.2.3 From 43307c7b57b5d2cbee01f1a89eae212d2325ca40 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 21 Nov 2013 21:50:14 -0800 Subject: Fixed a typo --- docs/fernet.rst | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/fernet.rst b/docs/fernet.rst index 02b99705..e4756c09 100644 --- a/docs/fernet.rst +++ b/docs/fernet.rst @@ -5,12 +5,13 @@ Fernet .. testsetup:: + import base64 import binascii - key = binascii.unhexlify(b"0" * 64) + key = base64.urlsafe_b64encode(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 +authenticated cryptography. Fernet provides guarantees that a message encrypted using it cannot be manipulated or read without the key. .. class:: Fernet(key) @@ -27,8 +28,9 @@ using it cannot be manipulated or read without the key. >>> 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. + :param bytes key: A base64 encoded 32-byte key. This **must** be kept + secret. Anyone with this key is able to create and read + messages. .. method:: encrypt(plaintext) -- cgit v1.2.3 From 1d2901cae193cb965480835aaf96696f8eecfaab Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 22 Nov 2013 10:12:05 -0800 Subject: Hide the dangerous bits --- cryptography/fernet.py | 18 ++++++++---------- tests/test_fernet.py | 15 +++++++-------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index ae3a8bfa..1c6cb5dd 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -85,24 +85,22 @@ class Fernet(object): ).encryptor() ciphertext = encryptor.update(padded_data) + encryptor.finalize() + basic_parts = ( + b"\x80" + struct.pack(">Q", current_time) + iv + ciphertext + ) + h = HMAC(self.signing_key, hashes.SHA256(), self.backend) - h.update(b"\x80") - h.update(struct.pack(">Q", current_time)) - h.update(iv) - h.update(ciphertext) + h.update(basic_parts) hmac = h.finalize() - return base64.urlsafe_b64encode( - b"\x80" + struct.pack(">Q", current_time) + iv + ciphertext + hmac - ) + return base64.urlsafe_b64encode(basic_parts + hmac) - def decrypt(self, data, ttl=None, current_time=None): + def decrypt(self, data, ttl=None): if isinstance(data, six.text_type): raise TypeError( "Unicode-objects must be encoded before decryption" ) - if current_time is None: - current_time = int(time.time()) + current_time = int(time.time()) try: data = base64.urlsafe_b64decode(data) diff --git a/tests/test_fernet.py b/tests/test_fernet.py index 4080bd2d..c1caaa05 100644 --- a/tests/test_fernet.py +++ b/tests/test_fernet.py @@ -15,6 +15,7 @@ import base64 import calendar import json import os +import time import iso8601 @@ -51,22 +52,20 @@ class TestFernet(object): @json_parametrize( ("secret", "now", "src", "ttl_sec", "token"), "verify.json", ) - def test_verify(self, secret, now, src, ttl_sec, token): + def test_verify(self, secret, now, src, ttl_sec, token, monkeypatch): f = Fernet(secret.encode("ascii")) current_time = calendar.timegm(iso8601.parse_date(now).utctimetuple()) - payload = f.decrypt( - token.encode("ascii"), ttl=ttl_sec, current_time=current_time - ) + monkeypatch.setattr(time, "time", lambda: current_time) + payload = f.decrypt(token.encode("ascii"), ttl=ttl_sec) assert payload == src.encode("ascii") @json_parametrize(("secret", "token", "now", "ttl_sec"), "invalid.json") - def test_invalid(self, secret, token, now, ttl_sec): + def test_invalid(self, secret, token, now, ttl_sec, monkeypatch): f = Fernet(secret.encode("ascii")) current_time = calendar.timegm(iso8601.parse_date(now).utctimetuple()) + monkeypatch.setattr(time, "time", lambda: current_time) with pytest.raises(InvalidToken): - f.decrypt( - token.encode("ascii"), ttl=ttl_sec, current_time=current_time - ) + f.decrypt(token.encode("ascii"), ttl=ttl_sec) def test_unicode(self): f = Fernet(base64.b64encode(b"\x00" * 32)) -- cgit v1.2.3 From 56bcade581e68ad0dd82dbabe97c75a9f0701fed Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 22 Nov 2013 10:15:14 -0800 Subject: fix, technically --- tests/test_fernet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_fernet.py b/tests/test_fernet.py index c1caaa05..8759229a 100644 --- a/tests/test_fernet.py +++ b/tests/test_fernet.py @@ -68,7 +68,7 @@ class TestFernet(object): f.decrypt(token.encode("ascii"), ttl=ttl_sec) def test_unicode(self): - f = Fernet(base64.b64encode(b"\x00" * 32)) + f = Fernet(base64.urlsafe_b64encode(b"\x00" * 32)) with pytest.raises(TypeError): f.encrypt(six.u("")) with pytest.raises(TypeError): -- cgit v1.2.3 From 7a121fce784efb6d436816d84ed01e873f251490 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 22 Nov 2013 10:18:30 -0800 Subject: More info in the docs --- docs/fernet.rst | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/fernet.rst b/docs/fernet.rst index e4756c09..c95077bb 100644 --- a/docs/fernet.rst +++ b/docs/fernet.rst @@ -28,16 +28,16 @@ using it cannot be manipulated or read without the key. >>> f.decrypt(ciphertext) 'my deep dark secret' - :param bytes key: A base64 encoded 32-byte key. This **must** be kept - secret. Anyone with this key is able to create and read - messages. + :param bytes key: A URL-safe base64-encoded 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. + without the key. It is URL-safe base64-encoded. .. method:: decrypt(ciphertext, ttl=None) @@ -49,6 +49,16 @@ using it cannot be manipulated or read without the key. provided (or is ``None``), the age of the message is not considered. :returns bytes: The original plaintext. + :raises InvalidToken: If the ``ciphertext`` is in any way invalid, this + exception is raised. A ciphertext may be invalid + for a number of reasons: it is older than the + ``ttl``, it is malformed, or it does not have a + valid signature. + + +.. class:: InvalidToken + + See :meth:`Fernet.decrypt` for more information. .. _`Fernet`: https://github.com/fernet/spec/ -- cgit v1.2.3 From 36597b4379bd62e520b9076072a030c73b85f471 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 22 Nov 2013 10:25:13 -0800 Subject: An API for generating keys --- cryptography/fernet.py | 4 ++++ docs/fernet.rst | 13 ++++++------- tests/test_fernet.py | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index 1c6cb5dd..ba2ff4e3 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -67,6 +67,10 @@ class Fernet(object): self.encryption_key = key[16:] self.backend = backend + @classmethod + def generate_key(cls): + return base64.urlsafe_b64encode(os.urandom(32)) + def encrypt(self, data): current_time = int(time.time()) iv = os.urandom(16) diff --git a/docs/fernet.rst b/docs/fernet.rst index c95077bb..241bf1ea 100644 --- a/docs/fernet.rst +++ b/docs/fernet.rst @@ -3,13 +3,6 @@ Fernet .. currentmodule:: cryptography.fernet -.. testsetup:: - - import base64 - import binascii - key = base64.urlsafe_b64encode(binascii.unhexlify(b"0" * 64)) - - `Fernet`_ is an implementation of symmetric (also known as "secret key") authenticated cryptography. Fernet provides guarantees that a message encrypted using it cannot be manipulated or read without the key. @@ -21,6 +14,7 @@ using it cannot be manipulated or read without the key. .. doctest:: >>> from cryptography.fernet import Fernet + >>> key = Fernet.generate_key() >>> f = Fernet(key) >>> ciphertext = f.encrypt(b"my deep dark secret") >>> ciphertext @@ -32,6 +26,11 @@ using it cannot be manipulated or read without the key. kept secret. Anyone with this key is able to create and read messages. + .. classmethod:: generate_key() + + Generates a fresh fernet key. Keep this some place safe! If you lose it + you'll no longer be able to decrypt messages; if anyone else gains + access to it, they'll be able to decrypt all of your messages. .. method:: encrypt(plaintext) diff --git a/tests/test_fernet.py b/tests/test_fernet.py index 8759229a..af64175e 100644 --- a/tests/test_fernet.py +++ b/tests/test_fernet.py @@ -76,5 +76,5 @@ class TestFernet(object): @pytest.mark.parametrize("message", [b"", b"Abc!", b"\x00\xFF\x00\x80"]) def test_roundtrips(self, message): - f = Fernet(base64.urlsafe_b64encode(b"\x00" * 32)) + f = Fernet(Fernet.generate_key()) assert f.decrypt(f.encrypt(message)) == message -- cgit v1.2.3 From 033af15d5f8d98007834be4aac4f260327e3c0c1 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 22 Nov 2013 10:32:56 -0800 Subject: removed silly line --- cryptography/fernet.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index ba2ff4e3..efa13b8f 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -60,7 +60,6 @@ bool Cryptography_constant_time_compare(uint8_t *a, size_t len_a, uint8_t *b, class Fernet(object): def __init__(self, key, backend=None): - super(Fernet, self).__init__() key = base64.urlsafe_b64decode(key) assert len(key) == 32 self.signing_key = key[:16] -- cgit v1.2.3 From 09bff867916af9694d66c2fea917d192f7dd1a25 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 22 Nov 2013 17:27:08 -0800 Subject: Handle another error case --- cryptography/fernet.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index efa13b8f..aa46b36f 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -129,7 +129,11 @@ class Fernet(object): decryptor = Cipher( algorithms.AES(self.encryption_key), modes.CBC(iv), self.backend ).decryptor() - plaintext_padded = decryptor.update(ciphertext) + decryptor.finalize() + plaintext_padded = decryptor.update(ciphertext) + try: + plaintext_padded += decryptor.finalize() + except ValueError: + raise InvalidToken unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() unpadded = unpadder.update(plaintext_padded) -- cgit v1.2.3 From 3417656d6e35bd5c6e4687bb7d008be6d8c73b43 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 23 Nov 2013 07:47:23 -0800 Subject: Handle the clock skew check --- cryptography/fernet.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index aa46b36f..2ae0ae8b 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -57,6 +57,8 @@ bool Cryptography_constant_time_compare(uint8_t *a, size_t len_a, uint8_t *b, } """) +_MAX_CLOCK_SKEW = 60 + class Fernet(object): def __init__(self, key, backend=None): @@ -117,6 +119,8 @@ class Fernet(object): if ttl is not None: if struct.unpack(">Q", timestamp)[0] + ttl < current_time: raise InvalidToken + if current_time + _MAX_CLOCK_SKEW < struct.unpack(">Q", timestamp)[0]: + raise InvalidToken h = HMAC(self.signing_key, hashes.SHA256(), self.backend) h.update(data[:-32]) hmac = h.finalize() -- cgit v1.2.3 From ebf3428ebddb7587b5f25497bed73489386126b5 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Wed, 27 Nov 2013 08:46:58 -0600 Subject: For now always use the default backend --- cryptography/fernet.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index 2ae0ae8b..6220e9c7 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -21,6 +21,7 @@ import cffi import six +from cryptography.hazmat.bindings import default_backend from cryptography.hazmat.primitives import padding, hashes from cryptography.hazmat.primitives.hmac import HMAC from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes @@ -61,12 +62,12 @@ _MAX_CLOCK_SKEW = 60 class Fernet(object): - def __init__(self, key, backend=None): + def __init__(self, key): key = base64.urlsafe_b64decode(key) assert len(key) == 32 self.signing_key = key[:16] self.encryption_key = key[16:] - self.backend = backend + self.backend = default_backend() @classmethod def generate_key(cls): -- cgit v1.2.3 From 022bc3afd8b8901b940a6bf0aa4f863914557c7e Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 14 Dec 2013 08:35:36 -0800 Subject: Replace our constant time check with the new module --- cryptography/fernet.py | 36 ++---------------------------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index 6220e9c7..3fcd187c 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -17,12 +17,10 @@ import os import struct import time -import cffi - import six from cryptography.hazmat.bindings import default_backend -from cryptography.hazmat.primitives import padding, hashes +from cryptography.hazmat.primitives import padding, hashes, constant_time from cryptography.hazmat.primitives.hmac import HMAC from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes @@ -31,33 +29,6 @@ class InvalidToken(Exception): pass -_ffi = cffi.FFI() -_ffi.cdef(""" -bool Cryptography_constant_time_compare(uint8_t *, size_t, uint8_t *, size_t); -""") -_lib = _ffi.verify(""" -#include - -bool Cryptography_constant_time_compare(uint8_t *a, size_t len_a, uint8_t *b, - size_t len_b) { - size_t i = 0; - uint8_t mismatch = 0; - if (len_a != len_b) { - return false; - } - for (i = 0; i < len_a; i++) { - mismatch |= a[i] ^ b[i]; - } - - /* Make sure any bits set are copied to the lowest bit */ - mismatch |= mismatch >> 4; - mismatch |= mismatch >> 2; - mismatch |= mismatch >> 1; - /* Now check the low bit to see if it's set */ - return (mismatch & 1) == 0; -} -""") - _MAX_CLOCK_SKEW = 60 @@ -125,10 +96,7 @@ class Fernet(object): h = HMAC(self.signing_key, hashes.SHA256(), self.backend) h.update(data[:-32]) hmac = h.finalize() - valid = _lib.Cryptography_constant_time_compare( - hmac, len(hmac), data[-32:], 32 - ) - if not valid: + if not constant_time.bytes_eq(hmac, data[-32:]): raise InvalidToken decryptor = Cipher( -- cgit v1.2.3 From badee1b5faf9c6bcc099aee0c6c1d9d29af8b7c7 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 14 Dec 2013 08:43:51 -0800 Subject: Reduce duplication --- cryptography/fernet.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index 3fcd187c..3cab5479 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -85,13 +85,13 @@ class Fernet(object): raise InvalidToken assert six.indexbytes(data, 0) == 0x80 - timestamp = data[1:9] + timestamp = struct.unpack(">Q", data[1:9])[0] iv = data[9:25] ciphertext = data[25:-32] if ttl is not None: - if struct.unpack(">Q", timestamp)[0] + ttl < current_time: + if timestamp + ttl < current_time: raise InvalidToken - if current_time + _MAX_CLOCK_SKEW < struct.unpack(">Q", timestamp)[0]: + if current_time + _MAX_CLOCK_SKEW < timestamp: raise InvalidToken h = HMAC(self.signing_key, hashes.SHA256(), self.backend) h.update(data[:-32]) -- cgit v1.2.3 From c3f7c37f5718b5deb08edb41125d8c7fb3733cfc Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 14 Dec 2013 08:58:35 -0800 Subject: flake8 --- cryptography/fernet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index 3cab5479..f2dd9341 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -89,7 +89,7 @@ class Fernet(object): iv = data[9:25] ciphertext = data[25:-32] if ttl is not None: - if timestamp + ttl < current_time: + if timestamp + ttl < current_time: raise InvalidToken if current_time + _MAX_CLOCK_SKEW < timestamp: raise InvalidToken -- cgit v1.2.3 From f272c14e7d41d84e095f61410fc8006e99d802e7 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sun, 15 Dec 2013 22:18:49 -0800 Subject: Update import location --- cryptography/fernet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index f2dd9341..8bcaa40a 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -19,7 +19,7 @@ import time import six -from cryptography.hazmat.bindings import default_backend +from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import padding, hashes, constant_time from cryptography.hazmat.primitives.hmac import HMAC from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes -- cgit v1.2.3 From 6cf242bee212b5b6069865a48c6bdc4836f78ff6 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Mon, 16 Dec 2013 11:17:07 -0800 Subject: Document the other consequence of losing your key --- docs/contributing.rst | 3 ++- docs/fernet.rst | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/contributing.rst b/docs/contributing.rst index cb9c7283..036043f5 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -244,7 +244,8 @@ Use `tox`_ to build the documentation. For example: docs: commands succeeded congratulations :) -The HTML documentation index can now be found at ``docs/_build/html/index.html`` +The HTML documentation index can now be found at +``docs/_build/html/index.html``. .. _`GitHub`: https://github.com/pyca/cryptography diff --git a/docs/fernet.rst b/docs/fernet.rst index 241bf1ea..3f0cdded 100644 --- a/docs/fernet.rst +++ b/docs/fernet.rst @@ -30,7 +30,9 @@ using it cannot be manipulated or read without the key. Generates a fresh fernet key. Keep this some place safe! If you lose it you'll no longer be able to decrypt messages; if anyone else gains - access to it, they'll be able to decrypt all of your messages. + access to it, they'll be able to decrypt all of your messages, and + they'll also be able forge arbitrary messages which will be + authenticated and decrypted. .. method:: encrypt(plaintext) -- cgit v1.2.3 From fae20715b85e84297f01b60fc153cde93a7549c7 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Mon, 16 Dec 2013 15:29:30 -0800 Subject: Address dreid's comments --- cryptography/fernet.py | 20 ++++++++++++-------- tests/test_fernet.py | 26 ++++++++++++++++---------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index 8bcaa40a..c0c5631f 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -33,12 +33,16 @@ _MAX_CLOCK_SKEW = 60 class Fernet(object): - def __init__(self, key): + def __init__(self, key, backend=None): + if backend is None: + backend = default_backend() + key = base64.urlsafe_b64decode(key) assert len(key) == 32 - self.signing_key = key[:16] - self.encryption_key = key[16:] - self.backend = default_backend() + + self._signing_key = key[:16] + self._encryption_key = key[16:] + self._backend = backend @classmethod def generate_key(cls): @@ -58,7 +62,7 @@ class Fernet(object): padder = padding.PKCS7(algorithms.AES.block_size).padder() padded_data = padder.update(data) + padder.finalize() encryptor = Cipher( - algorithms.AES(self.encryption_key), modes.CBC(iv), self.backend + algorithms.AES(self._encryption_key), modes.CBC(iv), self._backend ).encryptor() ciphertext = encryptor.update(padded_data) + encryptor.finalize() @@ -66,7 +70,7 @@ class Fernet(object): b"\x80" + struct.pack(">Q", current_time) + iv + ciphertext ) - h = HMAC(self.signing_key, hashes.SHA256(), self.backend) + h = HMAC(self._signing_key, hashes.SHA256(), backend=self._backend) h.update(basic_parts) hmac = h.finalize() return base64.urlsafe_b64encode(basic_parts + hmac) @@ -93,14 +97,14 @@ class Fernet(object): raise InvalidToken if current_time + _MAX_CLOCK_SKEW < timestamp: raise InvalidToken - h = HMAC(self.signing_key, hashes.SHA256(), self.backend) + h = HMAC(self._signing_key, hashes.SHA256(), backend=self._backend) h.update(data[:-32]) hmac = h.finalize() if not constant_time.bytes_eq(hmac, data[-32:]): raise InvalidToken decryptor = Cipher( - algorithms.AES(self.encryption_key), modes.CBC(iv), self.backend + algorithms.AES(self._encryption_key), modes.CBC(iv), self._backend ).decryptor() plaintext_padded = decryptor.update(ciphertext) try: diff --git a/tests/test_fernet.py b/tests/test_fernet.py index af64175e..48df867c 100644 --- a/tests/test_fernet.py +++ b/tests/test_fernet.py @@ -24,6 +24,7 @@ import pytest import six from cryptography.fernet import Fernet, InvalidToken +from cryptography.hazmat.backends import default_backend def json_parametrize(keys, fname): @@ -40,8 +41,8 @@ class TestFernet(object): @json_parametrize( ("secret", "now", "iv", "src", "token"), "generate.json", ) - def test_generate(self, secret, now, iv, src, token): - f = Fernet(secret.encode("ascii")) + def test_generate(self, secret, now, iv, src, token, backend): + f = Fernet(secret.encode("ascii"), backend=backend) actual_token = f._encrypt_from_parts( src.encode("ascii"), calendar.timegm(iso8601.parse_date(now).utctimetuple()), @@ -52,29 +53,34 @@ class TestFernet(object): @json_parametrize( ("secret", "now", "src", "ttl_sec", "token"), "verify.json", ) - def test_verify(self, secret, now, src, ttl_sec, token, monkeypatch): - f = Fernet(secret.encode("ascii")) + def test_verify(self, secret, now, src, ttl_sec, token, backend, + monkeypatch): + f = Fernet(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") @json_parametrize(("secret", "token", "now", "ttl_sec"), "invalid.json") - def test_invalid(self, secret, token, now, ttl_sec, monkeypatch): - f = Fernet(secret.encode("ascii")) + def test_invalid(self, secret, token, now, ttl_sec, backend, monkeypatch): + f = Fernet(secret.encode("ascii"), backend=backend) current_time = calendar.timegm(iso8601.parse_date(now).utctimetuple()) monkeypatch.setattr(time, "time", lambda: current_time) with pytest.raises(InvalidToken): f.decrypt(token.encode("ascii"), ttl=ttl_sec) - def test_unicode(self): - f = Fernet(base64.urlsafe_b64encode(b"\x00" * 32)) + def test_unicode(self, backend): + f = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend) with pytest.raises(TypeError): f.encrypt(six.u("")) with pytest.raises(TypeError): f.decrypt(six.u("")) @pytest.mark.parametrize("message", [b"", b"Abc!", b"\x00\xFF\x00\x80"]) - def test_roundtrips(self, message): - f = Fernet(Fernet.generate_key()) + def test_roundtrips(self, message, backend): + f = Fernet(Fernet.generate_key(), backend=backend) assert f.decrypt(f.encrypt(message)) == message + + def test_default_backend(self): + f = Fernet(Fernet.generate_key()) + assert f._backend is default_backend() -- cgit v1.2.3 From a8f0b63dddc6a22a1a982c6217d4cef2f598b781 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Mon, 16 Dec 2013 15:44:06 -0800 Subject: Replace assertions with real error checks --- cryptography/fernet.py | 9 +++++++-- tests/test_fernet.py | 9 +++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index c0c5631f..c5474af4 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -38,7 +38,10 @@ class Fernet(object): backend = default_backend() key = base64.urlsafe_b64decode(key) - assert len(key) == 32 + if len(key) != 32: + raise ValueError( + "Fernet key must be 32 url-safe base64-encoded bytes" + ) self._signing_key = key[:16] self._encryption_key = key[16:] @@ -88,7 +91,9 @@ class Fernet(object): except (TypeError, binascii.Error): raise InvalidToken - assert six.indexbytes(data, 0) == 0x80 + if six.indexbytes(data, 0) != 0x80: + raise InvalidToken + timestamp = struct.unpack(">Q", data[1:9])[0] iv = data[9:25] ciphertext = data[25:-32] diff --git a/tests/test_fernet.py b/tests/test_fernet.py index 48df867c..77661180 100644 --- a/tests/test_fernet.py +++ b/tests/test_fernet.py @@ -69,6 +69,11 @@ class TestFernet(object): with pytest.raises(InvalidToken): f.decrypt(token.encode("ascii"), ttl=ttl_sec) + def test_invalid_start_byte(self, backend): + f = Fernet(Fernet.generate_key(), backend=backend) + with pytest.raises(InvalidToken): + f.decrypt(base64.urlsafe_b64encode(b"\x81")) + def test_unicode(self, backend): f = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend) with pytest.raises(TypeError): @@ -84,3 +89,7 @@ class TestFernet(object): def test_default_backend(self): f = Fernet(Fernet.generate_key()) assert f._backend is default_backend() + + def test_bad_key(self, backend): + with pytest.raises(ValueError): + Fernet(base64.urlsafe_b64encode(b"abc"), backend=backend) -- cgit v1.2.3 From e9083291b9dac1c1ab7b0a2da38f9455536a807d Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Tue, 17 Dec 2013 16:56:29 -0800 Subject: Include more info in the title --- docs/fernet.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/fernet.rst b/docs/fernet.rst index 3f0cdded..287c991b 100644 --- a/docs/fernet.rst +++ b/docs/fernet.rst @@ -1,5 +1,5 @@ -Fernet -====== +Fernet (Symmetric encryption) +============================= .. currentmodule:: cryptography.fernet -- cgit v1.2.3 From 0d0896319f59fe7b03d8ef6a153275f87816976b Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Tue, 17 Dec 2013 20:23:43 -0800 Subject: Use the term fernet token --- cryptography/fernet.py | 6 +++--- docs/fernet.rst | 24 +++++++++++++----------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index c5474af4..10698f29 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -78,8 +78,8 @@ class Fernet(object): hmac = h.finalize() return base64.urlsafe_b64encode(basic_parts + hmac) - def decrypt(self, data, ttl=None): - if isinstance(data, six.text_type): + def decrypt(self, token, ttl=None): + if isinstance(token, six.text_type): raise TypeError( "Unicode-objects must be encoded before decryption" ) @@ -87,7 +87,7 @@ class Fernet(object): current_time = int(time.time()) try: - data = base64.urlsafe_b64decode(data) + data = base64.urlsafe_b64decode(token) except (TypeError, binascii.Error): raise InvalidToken diff --git a/docs/fernet.rst b/docs/fernet.rst index 287c991b..0122e364 100644 --- a/docs/fernet.rst +++ b/docs/fernet.rst @@ -16,10 +16,10 @@ using it cannot be manipulated or read without the key. >>> from cryptography.fernet import Fernet >>> key = Fernet.generate_key() >>> f = Fernet(key) - >>> ciphertext = f.encrypt(b"my deep dark secret") - >>> ciphertext + >>> token = f.encrypt(b"my deep dark secret") + >>> token '...' - >>> f.decrypt(ciphertext) + >>> f.decrypt(token) 'my deep dark secret' :param bytes key: A URL-safe base64-encoded 32-byte key. This **must** be @@ -38,11 +38,13 @@ using it cannot be manipulated or read without the key. :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. + without the key. It is URL-safe base64-encoded. This is + refered to as a "Fernet token". - .. method:: decrypt(ciphertext, ttl=None) + .. method:: decrypt(token, ttl=None) - :param bytes ciphertext: An encrypted message. + :param bytes token: The Fernet token. This is the result of calling + :meth:`encrypt`. :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 @@ -50,11 +52,11 @@ using it cannot be manipulated or read without the key. provided (or is ``None``), the age of the message is not considered. :returns bytes: The original plaintext. - :raises InvalidToken: If the ``ciphertext`` is in any way invalid, this - exception is raised. A ciphertext may be invalid - for a number of reasons: it is older than the - ``ttl``, it is malformed, or it does not have a - valid signature. + :raises InvalidToken: If the ``token`` is in any way invalid, this + exception is raised. A token may be invalid for a + number of reasons: it is older than the ``ttl``, + it is malformed, or it does not have a valid + signature. .. class:: InvalidToken -- cgit v1.2.3 From 05515723738870170b05b47ee260564b9ebe62f9 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Tue, 17 Dec 2013 20:43:59 -0800 Subject: Mention that the timestamp is plaintext --- docs/fernet.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/fernet.rst b/docs/fernet.rst index 0122e364..a47ae2e3 100644 --- a/docs/fernet.rst +++ b/docs/fernet.rst @@ -39,7 +39,10 @@ using it cannot be manipulated or read without the key. :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. This is - refered to as a "Fernet token". + refered to as a "Fernet token". Note that this *does* + contain the current time when it was generated in + plaintext, the time a message was created will + therefore be visible to a possible attacker. .. method:: decrypt(token, ttl=None) -- cgit v1.2.3 From e78960fa8c2a210484695bf2e20c4847313cf5a0 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 20 Dec 2013 11:02:33 -0800 Subject: Handle invalid timestamp length --- cryptography/fernet.py | 5 ++++- tests/test_fernet.py | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index 10698f29..b59f6a94 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -94,7 +94,10 @@ class Fernet(object): if six.indexbytes(data, 0) != 0x80: raise InvalidToken - timestamp = struct.unpack(">Q", data[1:9])[0] + try: + timestamp, = struct.unpack(">Q", data[1:9]) + except struct.error: + raise InvalidToken iv = data[9:25] ciphertext = data[25:-32] if ttl is not None: diff --git a/tests/test_fernet.py b/tests/test_fernet.py index 77661180..45188c47 100644 --- a/tests/test_fernet.py +++ b/tests/test_fernet.py @@ -74,6 +74,11 @@ class TestFernet(object): with pytest.raises(InvalidToken): f.decrypt(base64.urlsafe_b64encode(b"\x81")) + def test_timestamp_too_short(self, backend): + f = Fernet(Fernet.generate_key(), backend=backend) + with pytest.raises(InvalidToken): + f.decrypt(base64.urlsafe_b64encode(b"\x80abc")) + def test_unicode(self, backend): f = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend) with pytest.raises(TypeError): -- cgit v1.2.3 From d66f3726a5e945e74a32d10895b0f6acf5676f91 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 20 Dec 2013 11:05:13 -0800 Subject: Don't look at other material until the signature is validated --- cryptography/fernet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index b59f6a94..9f4294f0 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -98,8 +98,6 @@ class Fernet(object): timestamp, = struct.unpack(">Q", data[1:9]) except struct.error: raise InvalidToken - iv = data[9:25] - ciphertext = data[25:-32] if ttl is not None: if timestamp + ttl < current_time: raise InvalidToken @@ -111,6 +109,8 @@ class Fernet(object): if not constant_time.bytes_eq(hmac, data[-32:]): raise InvalidToken + iv = data[9:25] + ciphertext = data[25:-32] decryptor = Cipher( algorithms.AES(self._encryption_key), modes.CBC(iv), self._backend ).decryptor() -- cgit v1.2.3 From 3ef458ac7dc021378d8ca14bfcf654c0d51d9a0d Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 20 Dec 2013 13:19:43 -0800 Subject: Reword slightly --- docs/fernet.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/fernet.rst b/docs/fernet.rst index a47ae2e3..4e618f59 100644 --- a/docs/fernet.rst +++ b/docs/fernet.rst @@ -39,10 +39,10 @@ using it cannot be manipulated or read without the key. :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. This is - refered to as a "Fernet token". Note that this *does* - contain the current time when it was generated in - plaintext, the time a message was created will - therefore be visible to a possible attacker. + refered to as a "Fernet token". Note that this contains + the current time when it was generated in *plaintext*, + the time a message was created will therefore be + visible to a possible attacker. .. method:: decrypt(token, ttl=None) -- cgit v1.2.3 From 32dc4e4e9f3036f04598134369af50fd70143dae Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 20 Dec 2013 13:26:12 -0800 Subject: Make into a warning as suggested by @dstufft --- docs/fernet.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/fernet.rst b/docs/fernet.rst index 4e618f59..68184b3a 100644 --- a/docs/fernet.rst +++ b/docs/fernet.rst @@ -39,10 +39,13 @@ using it cannot be manipulated or read without the key. :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. This is - refered to as a "Fernet token". Note that this contains - the current time when it was generated in *plaintext*, - the time a message was created will therefore be - visible to a possible attacker. + refered to as a "Fernet token". + + .. warning:: + + The encrypted message contains the current time when it was + generated in *plaintext*, the time a message was created will + therefore be visible to a possible attacker. .. method:: decrypt(token, ttl=None) -- cgit v1.2.3 From 719eb6a412b5d3eab3ca84a9d4e8af76955bcbcc Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 20 Dec 2013 13:35:57 -0800 Subject: Linkify this --- docs/fernet.rst | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/fernet.rst b/docs/fernet.rst index 68184b3a..2fe2b860 100644 --- a/docs/fernet.rst +++ b/docs/fernet.rst @@ -41,7 +41,7 @@ using it cannot be manipulated or read without the key. without the key. It is URL-safe base64-encoded. This is refered to as a "Fernet token". - .. warning:: + .. note:: The encrypted message contains the current time when it was generated in *plaintext*, the time a message was created will @@ -58,11 +58,14 @@ using it cannot be manipulated or read without the key. provided (or is ``None``), the age of the message is not considered. :returns bytes: The original plaintext. - :raises InvalidToken: If the ``token`` is in any way invalid, this - exception is raised. A token may be invalid for a - number of reasons: it is older than the ``ttl``, - it is malformed, or it does not have a valid - signature. + :raises cryptography.fernet.InvalidToken: If the ``token`` is in any + way invalid, this exception + is raised. A token may be + invalid for a number of + reasons: it is older than the + ``ttl``, it is malformed, or + it does not have a valid + signature. .. class:: InvalidToken -- cgit v1.2.3 From 2724ff6af8ba5f8dfd1f0f511ed95fab5cd8abd8 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 20 Dec 2013 13:51:42 -0800 Subject: Link from symmetric encryption to fernet --- docs/cryptography-docs.py | 17 +++++++++++++++-- docs/hazmat/primitives/symmetric-encryption.rst | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/cryptography-docs.py b/docs/cryptography-docs.py index ea7e8eef..f07c18bb 100644 --- a/docs/cryptography-docs.py +++ b/docs/cryptography-docs.py @@ -6,17 +6,30 @@ from sphinx.util.compat import Directive, make_admonition DANGER_MESSAGE = """ This is a "Hazardous Materials" module. You should **ONLY** use it if you're 100% absolutely sure that you know what you're doing because this module is -full of land mines, dragons, and dinosaurs with laser guns. """ +full of land mines, dragons, and dinosaurs with laser guns. +""" + +DANGER_ALTERNATE = """ + +You may instead be interested in :doc:`{alternate}`. +""" + class HazmatDirective(Directive): + has_content = True + def run(self): + message = DANGER_MESSAGE + if self.content: + message += DANGER_ALTERNATE.format(alternate=self.content[0]) + ad = make_admonition( Hazmat, self.name, [], self.options, - nodes.paragraph("", DANGER_MESSAGE), + nodes.paragraph("", message), self.lineno, self.content_offset, self.block_text, diff --git a/docs/hazmat/primitives/symmetric-encryption.rst b/docs/hazmat/primitives/symmetric-encryption.rst index f4d0457a..7b012975 100644 --- a/docs/hazmat/primitives/symmetric-encryption.rst +++ b/docs/hazmat/primitives/symmetric-encryption.rst @@ -1,4 +1,4 @@ -.. hazmat:: +.. hazmat:: /fernet Symmetric Encryption -- cgit v1.2.3 From 3ac297e4c9b655b3222da1830e9677c9d03a3926 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 20 Dec 2013 14:16:34 -0800 Subject: flake8 fix --- docs/cryptography-docs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/cryptography-docs.py b/docs/cryptography-docs.py index f07c18bb..0252d693 100644 --- a/docs/cryptography-docs.py +++ b/docs/cryptography-docs.py @@ -15,7 +15,6 @@ You may instead be interested in :doc:`{alternate}`. """ - class HazmatDirective(Directive): has_content = True -- cgit v1.2.3 From b9bc6c3e4c9b647de1a1a2dd852ab591e9a69b01 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 27 Dec 2013 08:23:14 -0800 Subject: Switch to the newer, safer, API --- cryptography/fernet.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index 9f4294f0..c19309d5 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -19,8 +19,9 @@ import time import six +from cryptography.exceptions import InvalidSignature from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import padding, hashes, constant_time +from cryptography.hazmat.primitives import padding, hashes from cryptography.hazmat.primitives.hmac import HMAC from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes @@ -105,8 +106,9 @@ class Fernet(object): raise InvalidToken h = HMAC(self._signing_key, hashes.SHA256(), backend=self._backend) h.update(data[:-32]) - hmac = h.finalize() - if not constant_time.bytes_eq(hmac, data[-32:]): + try: + h.verify(data[-32:]) + except InvalidSignature: raise InvalidToken iv = data[9:25] -- cgit v1.2.3 From be60d7065c5a55a06a1e3f301de8f1dbfc7c8eb3 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Mon, 30 Dec 2013 21:08:57 -0800 Subject: alphabetize --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 1ec04223..0f52900b 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -4,6 +4,6 @@ iso8601 pretend pytest sphinx -tox sphinx_rtd_theme +tox -e . -- cgit v1.2.3 From 681fca8f43f9cbed97ce2df0b871447953c7edda Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Tue, 31 Dec 2013 14:13:39 -0800 Subject: Rearange sentence on recommendation of @jacobian --- docs/cryptography-docs.py | 3 +++ docs/fernet.rst | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/cryptography-docs.py b/docs/cryptography-docs.py index 0252d693..f27a8467 100644 --- a/docs/cryptography-docs.py +++ b/docs/cryptography-docs.py @@ -23,6 +23,9 @@ class HazmatDirective(Directive): if self.content: message += DANGER_ALTERNATE.format(alternate=self.content[0]) + import pdb + pdb.set_trace() + ad = make_admonition( Hazmat, self.name, diff --git a/docs/fernet.rst b/docs/fernet.rst index 2fe2b860..4e94e212 100644 --- a/docs/fernet.rst +++ b/docs/fernet.rst @@ -3,9 +3,9 @@ Fernet (Symmetric encryption) .. currentmodule:: cryptography.fernet -`Fernet`_ is an implementation of symmetric (also known as "secret key") -authenticated cryptography. Fernet provides guarantees that a message encrypted -using it cannot be manipulated or read without the key. +Fernet provides guarantees that a message encrypted using it cannot be +manipulated or read without the key. `Fernet`_ is an implementation of +symmetric (also known as "secret key") authenticated cryptography. .. class:: Fernet(key) -- cgit v1.2.3 From 09aa74635f54ace5480a6d502b0da92651f516b6 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Tue, 31 Dec 2013 15:18:34 -0800 Subject: Remove this one weird debugger --- docs/cryptography-docs.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/cryptography-docs.py b/docs/cryptography-docs.py index f27a8467..0252d693 100644 --- a/docs/cryptography-docs.py +++ b/docs/cryptography-docs.py @@ -23,9 +23,6 @@ class HazmatDirective(Directive): if self.content: message += DANGER_ALTERNATE.format(alternate=self.content[0]) - import pdb - pdb.set_trace() - ad = make_admonition( Hazmat, self.name, -- cgit v1.2.3 From 3aa243cddc5cbe4e4205b019946dc6c4f271f589 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Mon, 6 Jan 2014 13:13:18 -0800 Subject: Spell a word correctly --- docs/fernet.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fernet.rst b/docs/fernet.rst index 4e94e212..13295c0c 100644 --- a/docs/fernet.rst +++ b/docs/fernet.rst @@ -39,7 +39,7 @@ symmetric (also known as "secret key") authenticated cryptography. :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. This is - refered to as a "Fernet token". + referred to as a "Fernet token". .. note:: -- cgit v1.2.3 From 2b22fae990513eeb4026cd0883bc2e244af8b56a Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Mon, 6 Jan 2014 13:19:33 -0800 Subject: Compute the version in the same way as setup.py does --- docs/conf.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 5dbcdab8..00660314 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -60,10 +60,13 @@ copyright = '2013-2014, Individual Contributors' # |version| and |release|, also used in various other places throughout the # built documents. # -# The short X.Y version. -version = '0.1dev' -# The full version, including alpha/beta/rc tags. -release = '0.1dev' + +base_dir = os.path.join(os.path.dirname(__file__), os.pardir) +about = {} +with open(os.path.join(base_dir, "cryptography", "__about__.py")) as f: + exec(f.read(), about) + +version = release = about["__version__"] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -- cgit v1.2.3