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