From 7b593e1b5ecf9741a1398a739815b8a11599a06a Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sun, 19 Oct 2014 19:09:44 -0700 Subject: Fixes #1327 -- adds multifernet --- cryptography/fernet.py | 18 ++++++++++++++++++ tests/test_fernet.py | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index a8e0330e..9fee3eba 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -127,3 +127,21 @@ class Fernet(object): except ValueError: raise InvalidToken return unpadded + + +class MultiFernet(object): + def __init__(self, fernets): + if not fernets: + raise ValueError("MultiFernet requires at least one fernet") + self._fernets = fernets + + def encrypt(self, msg): + return self._fernets[0].encrypt(msg) + + def decrypt(self, msg, ttl=None): + for f in self._fernets: + try: + return f.decrypt(msg, ttl) + except InvalidToken: + pass + raise InvalidToken diff --git a/tests/test_fernet.py b/tests/test_fernet.py index 0b4e3e87..91af32ad 100644 --- a/tests/test_fernet.py +++ b/tests/test_fernet.py @@ -24,7 +24,7 @@ import pytest import six -from cryptography.fernet import Fernet, InvalidToken +from cryptography.fernet import Fernet, InvalidToken, MultiFernet from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import algorithms, modes @@ -115,3 +115,34 @@ class TestFernet(object): def test_bad_key(self, backend): with pytest.raises(ValueError): Fernet(base64.urlsafe_b64encode(b"abc"), backend=backend) + + +@pytest.mark.supported( + only_if=lambda backend: backend.cipher_supported( + algorithms.AES("\x00" * 32), modes.CBC("\x00" * 16) + ), + skip_message="Does not support AES CBC", +) +class TestMultiFernet(object): + def test_encrypt(self, backend): + single_f = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend) + f = MultiFernet([ + single_f, + Fernet(base64.urlsafe_b64encode(b"\x01" * 32), backend=backend) + ]) + assert single_f.decrypt(f.encrypt(b"abc")) == b"abc" + + def test_decrypt(self, backend): + f1 = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend) + f2 = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend) + f = MultiFernet([f1, f2]) + + assert f.decrypt(f1.encrypt(b"abc")) == b"abc" + assert f.decrypt(f2.encrypt(b"abc")) == b"abc" + + with pytest.raises(InvalidToken): + f.decrypt(b"\x00" * 16) + + def test_no_fernets(self, backend): + with pytest.raises(ValueError): + MultiFernet([]) -- cgit v1.2.3 From e148d01e7587f39d3ce15a1592dbd5e2dc4e9bca Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sun, 19 Oct 2014 19:18:59 -0700 Subject: Added docs for multifernet --- docs/fernet.rst | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/docs/fernet.rst b/docs/fernet.rst index 4b713a54..1ff6cdbf 100644 --- a/docs/fernet.rst +++ b/docs/fernet.rst @@ -5,7 +5,8 @@ Fernet (symmetric encryption) 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. +symmetric (also known as "secret key") authenticated cryptography. Fernet also +has support for implementing key rotation via :class:`MultiFernet`. .. class:: Fernet(key) @@ -40,7 +41,8 @@ symmetric (also known as "secret key") authenticated cryptography. :returns bytes: A secure message that cannot be read or altered without the key. It is URL-safe base64-encoded. This is referred to as a "Fernet token". - :raises TypeError: This exception is raised if ``data`` is not ``bytes``. + :raises TypeError: This exception is raised if ``data`` is not + ``bytes``. .. note:: @@ -67,7 +69,33 @@ symmetric (also known as "secret key") authenticated cryptography. ``ttl``, it is malformed, or it does not have a valid signature. - :raises TypeError: This exception is raised if ``token`` is not ``bytes``. + :raises TypeError: This exception is raised if ``token`` is not + ``bytes``. + + +.. class:: MultiFernet(fernets) + + This class implements key rotation for Fernet. It takes a ``list`` of + :class:`Fernet` instances, and implements the same API: + + .. doctest:: + + >>> from cryptography.fernet import Fernet, MultiFernet + >>> key1 = Fernet(Fernet.generate_key()) + >>> key2 = Fernet(Fernet.generate_key()) + >>> f = MultiFernet([key1, key2]) + >>> token = f.encrypt(b"Secret message!") + >>> token + '...' + >>> f.decrypt(token) + 'Secret message!' + + Fernet performs all encryption options using the *first* key in the + ``list`` provided. Decryption supports using *any* of constituent keys. + + Key rotation makes it easy to replace old keys. You can add your new key at + the front of the list to start encrypting new messages, and remove old keys + as they are no longer needed. .. class:: InvalidToken -- cgit v1.2.3 From d5b592d95d3844f902378ac4d97ea8a0e8843600 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Mon, 20 Oct 2014 10:53:58 -0700 Subject: improved the error message --- cryptography/fernet.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index 9fee3eba..6bc401de 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -132,7 +132,9 @@ class Fernet(object): class MultiFernet(object): def __init__(self, fernets): if not fernets: - raise ValueError("MultiFernet requires at least one fernet") + raise ValueError( + "MultiFernet requires at least one Fernet instance" + ) self._fernets = fernets def encrypt(self, msg): -- cgit v1.2.3 From 4f286cec7a28169ac8939cc01aa6fdead47ac58e Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Mon, 20 Oct 2014 11:30:57 -0700 Subject: Handle non-iterable arguments reasonable --- cryptography/fernet.py | 1 + tests/test_fernet.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/cryptography/fernet.py b/cryptography/fernet.py index 6bc401de..4f98feec 100644 --- a/cryptography/fernet.py +++ b/cryptography/fernet.py @@ -131,6 +131,7 @@ class Fernet(object): class MultiFernet(object): def __init__(self, fernets): + fernets = list(fernets) if not fernets: raise ValueError( "MultiFernet requires at least one Fernet instance" diff --git a/tests/test_fernet.py b/tests/test_fernet.py index 91af32ad..58f89cbf 100644 --- a/tests/test_fernet.py +++ b/tests/test_fernet.py @@ -146,3 +146,7 @@ class TestMultiFernet(object): def test_no_fernets(self, backend): with pytest.raises(ValueError): MultiFernet([]) + + def test_non_iterable_argument(self, backend): + with pytest.raises(TypeError): + MultiFernet(None) -- cgit v1.2.3 From 41b33b70d3f9c937d80c264627d1195692a17863 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Mon, 20 Oct 2014 14:34:35 -0700 Subject: flake8 + cleanup --- tests/test_fernet.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/test_fernet.py b/tests/test_fernet.py index 58f89cbf..5c630b9e 100644 --- a/tests/test_fernet.py +++ b/tests/test_fernet.py @@ -125,16 +125,15 @@ class TestFernet(object): ) class TestMultiFernet(object): def test_encrypt(self, backend): - single_f = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend) - f = MultiFernet([ - single_f, - Fernet(base64.urlsafe_b64encode(b"\x01" * 32), backend=backend) - ]) - assert single_f.decrypt(f.encrypt(b"abc")) == b"abc" + f1 = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend) + f2 = Fernet(base64.urlsafe_b64encode(b"\x01" * 32), backend=backend) + f = MultiFernet([f1, f2]) + + assert f1.decrypt(f.encrypt(b"abc")) == b"abc" def test_decrypt(self, backend): f1 = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend) - f2 = Fernet(base64.urlsafe_b64encode(b"\x00" * 32), backend=backend) + f2 = Fernet(base64.urlsafe_b64encode(b"\x01" * 32), backend=backend) f = MultiFernet([f1, f2]) assert f.decrypt(f1.encrypt(b"abc")) == b"abc" -- cgit v1.2.3 From 4c82513ac8ae9b319eecb1fc18d11c55305c1663 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Mon, 20 Oct 2014 21:27:08 -0700 Subject: added docs stuff on when added --- CHANGELOG.rst | 2 ++ docs/fernet.rst | 2 ++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c8cec58d..1d69d9cb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,8 @@ Changelog .. note:: This version is not yet released and is under active development. +* Added key-rotation support to :doc:`Fernet ` with + :class:`~cryptography.fernet.MultiFernet`. * More bit-lengths are now support for ``p`` and ``q`` when loading DSA keys from numbers. * Added :class:`~cryptography.hazmat.primitives.interfaces.MACContext` as a diff --git a/docs/fernet.rst b/docs/fernet.rst index 1ff6cdbf..f1a4c748 100644 --- a/docs/fernet.rst +++ b/docs/fernet.rst @@ -75,6 +75,8 @@ has support for implementing key rotation via :class:`MultiFernet`. .. class:: MultiFernet(fernets) + .. versionadded:: 0.7 + This class implements key rotation for Fernet. It takes a ``list`` of :class:`Fernet` instances, and implements the same API: -- cgit v1.2.3