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 From 6eed9d0f3d1eb36ff5a834ff52e06c80cbde0681 Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Tue, 21 Oct 2014 11:28:19 -0700 Subject: add load_dsa_parameter_numbers on multibackend --- cryptography/hazmat/backends/multibackend.py | 6 ++++++ tests/hazmat/backends/test_multibackend.py | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/cryptography/hazmat/backends/multibackend.py b/cryptography/hazmat/backends/multibackend.py index db189787..e873f504 100644 --- a/cryptography/hazmat/backends/multibackend.py +++ b/cryptography/hazmat/backends/multibackend.py @@ -210,6 +210,12 @@ class MultiBackend(object): raise UnsupportedAlgorithm("DSA is not supported by the backend.", _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM) + def load_dsa_parameter_numbers(self, numbers): + for b in self._filtered_backends(DSABackend): + return b.load_dsa_parameter_numbers(numbers) + raise UnsupportedAlgorithm("DSA is not supported by the backend.", + _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM) + def cmac_algorithm_supported(self, algorithm): return any( b.cmac_algorithm_supported(algorithm) diff --git a/tests/hazmat/backends/test_multibackend.py b/tests/hazmat/backends/test_multibackend.py index 93934ad6..c50b6cf6 100644 --- a/tests/hazmat/backends/test_multibackend.py +++ b/tests/hazmat/backends/test_multibackend.py @@ -131,6 +131,9 @@ class DummyDSABackend(object): def load_dsa_public_numbers(self, numbers): pass + def load_dsa_parameter_numbers(self, numbers): + pass + @utils.register_interface(CMACBackend) class DummyCMACBackend(object): @@ -330,6 +333,7 @@ class TestMultiBackend(object): backend.dsa_parameters_supported(1, 2, 3) backend.load_dsa_private_numbers("numbers") backend.load_dsa_public_numbers("numbers") + backend.load_dsa_parameter_numbers("numbers") backend = MultiBackend([]) with raises_unsupported_algorithm( @@ -367,6 +371,11 @@ class TestMultiBackend(object): ): backend.load_dsa_public_numbers("numbers") + with raises_unsupported_algorithm( + _Reasons.UNSUPPORTED_PUBLIC_KEY_ALGORITHM + ): + backend.load_dsa_parameter_numbers("numbers") + def test_cmac(self): backend = MultiBackend([ DummyCMACBackend([algorithms.AES]) -- cgit v1.2.3 From 2d8b99674dcce97f0dd5fe9b2e0ee3412275b159 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Tue, 21 Oct 2014 14:43:42 -0700 Subject: Added a utility for implementing a read only property of another field --- .../hazmat/backends/commoncrypto/ciphers.py | 4 +- cryptography/hazmat/backends/openssl/ciphers.py | 4 +- cryptography/hazmat/backends/openssl/dsa.py | 8 +--- cryptography/hazmat/backends/openssl/ec.py | 8 +--- cryptography/hazmat/backends/openssl/rsa.py | 8 +--- cryptography/hazmat/primitives/asymmetric/dsa.py | 32 ++++------------ cryptography/hazmat/primitives/asymmetric/ec.py | 27 +++---------- cryptography/hazmat/primitives/asymmetric/rsa.py | 44 +++++----------------- cryptography/utils.py | 4 ++ 9 files changed, 35 insertions(+), 104 deletions(-) diff --git a/cryptography/hazmat/backends/commoncrypto/ciphers.py b/cryptography/hazmat/backends/commoncrypto/ciphers.py index 6d3ba863..d94746c6 100644 --- a/cryptography/hazmat/backends/commoncrypto/ciphers.py +++ b/cryptography/hazmat/backends/commoncrypto/ciphers.py @@ -198,6 +198,4 @@ class _GCMCipherContext(object): ) self._backend._check_cipher_response(res) - @property - def tag(self): - return self._tag + tag = utils.read_only_property("_tag") diff --git a/cryptography/hazmat/backends/openssl/ciphers.py b/cryptography/hazmat/backends/openssl/ciphers.py index d37bb014..4ec2ac89 100644 --- a/cryptography/hazmat/backends/openssl/ciphers.py +++ b/cryptography/hazmat/backends/openssl/ciphers.py @@ -187,9 +187,7 @@ class _CipherContext(object): ) assert res != 0 - @property - def tag(self): - return self._tag + tag = utils.read_only_property("_tag") @utils.register_interface(interfaces.CipherContext) diff --git a/cryptography/hazmat/backends/openssl/dsa.py b/cryptography/hazmat/backends/openssl/dsa.py index 3fb67a5d..2298e7d9 100644 --- a/cryptography/hazmat/backends/openssl/dsa.py +++ b/cryptography/hazmat/backends/openssl/dsa.py @@ -129,9 +129,7 @@ class _DSAPrivateKey(object): self._dsa_cdata = dsa_cdata self._key_size = self._backend._lib.BN_num_bits(self._dsa_cdata.p) - @property - def key_size(self): - return self._key_size + key_size = utils.read_only_property("_key_size") def signer(self, algorithm): return _DSASignatureContext(self._backend, self, algorithm) @@ -180,9 +178,7 @@ class _DSAPublicKey(object): self._dsa_cdata = dsa_cdata self._key_size = self._backend._lib.BN_num_bits(self._dsa_cdata.p) - @property - def key_size(self): - return self._key_size + key_size = utils.read_only_property("_key_size") def verifier(self, signature, algorithm): return _DSAVerificationContext( diff --git a/cryptography/hazmat/backends/openssl/ec.py b/cryptography/hazmat/backends/openssl/ec.py index 7798c3dc..13b0ddbb 100644 --- a/cryptography/hazmat/backends/openssl/ec.py +++ b/cryptography/hazmat/backends/openssl/ec.py @@ -146,9 +146,7 @@ class _EllipticCurvePrivateKey(object): sn = _ec_key_curve_sn(backend, ec_key_cdata) self._curve = _sn_to_elliptic_curve(backend, sn) - @property - def curve(self): - return self._curve + curve = utils.read_only_property("_curve") def signer(self, signature_algorithm): if isinstance(signature_algorithm, ec.ECDSA): @@ -200,9 +198,7 @@ class _EllipticCurvePublicKey(object): sn = _ec_key_curve_sn(backend, ec_key_cdata) self._curve = _sn_to_elliptic_curve(backend, sn) - @property - def curve(self): - return self._curve + curve = utils.read_only_property("_curve") def verifier(self, signature, signature_algorithm): if isinstance(signature_algorithm, ec.ECDSA): diff --git a/cryptography/hazmat/backends/openssl/rsa.py b/cryptography/hazmat/backends/openssl/rsa.py index 7312fcb2..0a2a7f96 100644 --- a/cryptography/hazmat/backends/openssl/rsa.py +++ b/cryptography/hazmat/backends/openssl/rsa.py @@ -532,9 +532,7 @@ class _RSAPrivateKey(object): self._key_size = self._backend._lib.BN_num_bits(self._rsa_cdata.n) - @property - def key_size(self): - return self._key_size + key_size = utils.read_only_property("_key_size") def signer(self, padding, algorithm): return _RSASignatureContext(self._backend, self, padding, algorithm) @@ -588,9 +586,7 @@ class _RSAPublicKey(object): self._key_size = self._backend._lib.BN_num_bits(self._rsa_cdata.n) - @property - def key_size(self): - return self._key_size + key_size = utils.read_only_property("_key_size") def verifier(self, signature, padding, algorithm): return _RSAVerificationContext( diff --git a/cryptography/hazmat/primitives/asymmetric/dsa.py b/cryptography/hazmat/primitives/asymmetric/dsa.py index 97265868..83e01377 100644 --- a/cryptography/hazmat/primitives/asymmetric/dsa.py +++ b/cryptography/hazmat/primitives/asymmetric/dsa.py @@ -61,17 +61,9 @@ class DSAParameterNumbers(object): self._q = q self._g = g - @property - def p(self): - return self._p - - @property - def q(self): - return self._q - - @property - def g(self): - return self._g + p = utils.read_only_property("_p") + q = utils.read_only_property("_q") + g = utils.read_only_property("_g") def parameters(self, backend): return backend.load_dsa_parameter_numbers(self) @@ -90,13 +82,8 @@ class DSAPublicNumbers(object): self._y = y self._parameter_numbers = parameter_numbers - @property - def y(self): - return self._y - - @property - def parameter_numbers(self): - return self._parameter_numbers + y = utils.read_only_property("_y") + parameter_numbers = utils.read_only_property("_parameter_numbers") def public_key(self, backend): return backend.load_dsa_public_numbers(self) @@ -114,13 +101,8 @@ class DSAPrivateNumbers(object): self._public_numbers = public_numbers self._x = x - @property - def x(self): - return self._x - - @property - def public_numbers(self): - return self._public_numbers + x = utils.read_only_property("_x") + public_numbers = utils.read_only_property("_public_numbers") def private_key(self, backend): return backend.load_dsa_private_numbers(self) diff --git a/cryptography/hazmat/primitives/asymmetric/ec.py b/cryptography/hazmat/primitives/asymmetric/ec.py index 6dcf39cf..b27d0458 100644 --- a/cryptography/hazmat/primitives/asymmetric/ec.py +++ b/cryptography/hazmat/primitives/asymmetric/ec.py @@ -213,9 +213,7 @@ class ECDSA(object): def __init__(self, algorithm): self._algorithm = algorithm - @property - def algorithm(self): - return self._algorithm + algorithm = utils.read_only_property("_algorithm") def generate_private_key(curve, backend): @@ -243,17 +241,9 @@ class EllipticCurvePublicNumbers(object): except AttributeError: return backend.elliptic_curve_public_key_from_numbers(self) - @property - def curve(self): - return self._curve - - @property - def x(self): - return self._x - - @property - def y(self): - return self._y + curve = utils.read_only_property("_curve") + x = utils.read_only_property("_x") + y = utils.read_only_property("_y") class EllipticCurvePrivateNumbers(object): @@ -276,10 +266,5 @@ class EllipticCurvePrivateNumbers(object): except AttributeError: return backend.elliptic_curve_private_key_from_numbers(self) - @property - def private_value(self): - return self._private_value - - @property - def public_numbers(self): - return self._public_numbers + private_value = utils.read_only_property("_private_value") + public_numbers = utils.read_only_property("_public_numbers") diff --git a/cryptography/hazmat/primitives/asymmetric/rsa.py b/cryptography/hazmat/primitives/asymmetric/rsa.py index c192811d..cf2be4c4 100644 --- a/cryptography/hazmat/primitives/asymmetric/rsa.py +++ b/cryptography/hazmat/primitives/asymmetric/rsa.py @@ -15,6 +15,7 @@ from __future__ import absolute_import, division, print_function import six +from cryptography import utils from cryptography.exceptions import UnsupportedAlgorithm, _Reasons from cryptography.hazmat.backends.interfaces import RSABackend @@ -157,33 +158,13 @@ class RSAPrivateNumbers(object): self._iqmp = iqmp self._public_numbers = public_numbers - @property - def p(self): - return self._p - - @property - def q(self): - return self._q - - @property - def d(self): - return self._d - - @property - def dmp1(self): - return self._dmp1 - - @property - def dmq1(self): - return self._dmq1 - - @property - def iqmp(self): - return self._iqmp - - @property - def public_numbers(self): - return self._public_numbers + p = utils.read_only_property("_p") + q = utils.read_only_property("_q") + d = utils.read_only_property("_d") + dmp1 = utils.read_only_property("_dmp1") + dmq1 = utils.read_only_property("_dmq1") + iqmp = utils.read_only_property("iqmp") + public_numbers = utils.read_only_property("public_numbers") def private_key(self, backend): return backend.load_rsa_private_numbers(self) @@ -200,13 +181,8 @@ class RSAPublicNumbers(object): self._e = e self._n = n - @property - def e(self): - return self._e - - @property - def n(self): - return self._n + e = utils.read_only_property("_e") + n = utils.read_only_property("_n") def public_key(self, backend): return backend.load_rsa_public_numbers(self) diff --git a/cryptography/utils.py b/cryptography/utils.py index 55187c3b..1deb3d1d 100644 --- a/cryptography/utils.py +++ b/cryptography/utils.py @@ -26,6 +26,10 @@ def register_interface(iface): return register_decorator +def read_only_property(name): + return property(lambda self: getattr(self, name)) + + def bit_length(x): if sys.version_info >= (2, 7): return x.bit_length() -- cgit v1.2.3 From f28f8eecfdbe21cd466bedb851c12fd19cf0e584 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Tue, 21 Oct 2014 14:53:07 -0700 Subject: oops, fix recursion --- cryptography/hazmat/primitives/asymmetric/rsa.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cryptography/hazmat/primitives/asymmetric/rsa.py b/cryptography/hazmat/primitives/asymmetric/rsa.py index cf2be4c4..db38ed55 100644 --- a/cryptography/hazmat/primitives/asymmetric/rsa.py +++ b/cryptography/hazmat/primitives/asymmetric/rsa.py @@ -163,8 +163,8 @@ class RSAPrivateNumbers(object): d = utils.read_only_property("_d") dmp1 = utils.read_only_property("_dmp1") dmq1 = utils.read_only_property("_dmq1") - iqmp = utils.read_only_property("iqmp") - public_numbers = utils.read_only_property("public_numbers") + iqmp = utils.read_only_property("_iqmp") + public_numbers = utils.read_only_property("_public_numbers") def private_key(self, backend): return backend.load_rsa_private_numbers(self) -- cgit v1.2.3