aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.rst2
-rw-r--r--cryptography/fernet.py21
-rw-r--r--cryptography/hazmat/backends/commoncrypto/ciphers.py4
-rw-r--r--cryptography/hazmat/backends/multibackend.py6
-rw-r--r--cryptography/hazmat/backends/openssl/ciphers.py4
-rw-r--r--cryptography/hazmat/backends/openssl/dsa.py8
-rw-r--r--cryptography/hazmat/backends/openssl/ec.py8
-rw-r--r--cryptography/hazmat/backends/openssl/rsa.py8
-rw-r--r--cryptography/hazmat/primitives/asymmetric/dsa.py32
-rw-r--r--cryptography/hazmat/primitives/asymmetric/ec.py27
-rw-r--r--cryptography/hazmat/primitives/asymmetric/rsa.py44
-rw-r--r--cryptography/utils.py4
-rw-r--r--docs/fernet.rst36
-rw-r--r--tests/hazmat/backends/test_multibackend.py9
-rw-r--r--tests/test_fernet.py36
15 files changed, 141 insertions, 108 deletions
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 </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/cryptography/fernet.py b/cryptography/fernet.py
index a8e0330e..4f98feec 100644
--- a/cryptography/fernet.py
+++ b/cryptography/fernet.py
@@ -127,3 +127,24 @@ class Fernet(object):
except ValueError:
raise InvalidToken
return unpadded
+
+
+class MultiFernet(object):
+ def __init__(self, fernets):
+ fernets = list(fernets)
+ if not fernets:
+ raise ValueError(
+ "MultiFernet requires at least one Fernet instance"
+ )
+ 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/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/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/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..db38ed55 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 e4ec6ccf..8fbcabc5 100644
--- a/cryptography/utils.py
+++ b/cryptography/utils.py
@@ -28,6 +28,10 @@ def register_interface(iface):
return register_decorator
+def read_only_property(name):
+ return property(lambda self: getattr(self, name))
+
+
class InterfaceNotImplemented(Exception):
pass
diff --git a/docs/fernet.rst b/docs/fernet.rst
index 4b713a54..f1a4c748 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,35 @@ 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)
+
+ .. versionadded:: 0.7
+
+ 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
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])
diff --git a/tests/test_fernet.py b/tests/test_fernet.py
index 0b4e3e87..5c630b9e 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,37 @@ 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):
+ 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"\x01" * 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([])
+
+ def test_non_iterable_argument(self, backend):
+ with pytest.raises(TypeError):
+ MultiFernet(None)