Repository: pyca/cryptography

Issue:
Key versioning/key rotation support (wishlist)
It would be nice if Fernet had built-in support for key versioning.  I don't know exactly what the API would look like, but the idea would be to allow encrypting new data under a new key while still retaining the ability to decrypt old data that was encrypted under older keys.

If Fernet supported this natively, then I could tell developers to "just use Fernet" when they first write their applications, and later when they get a real security engineer, that person could start doing key rotation without having to change application-level code or data structures.



Discussion:
It looks like the Go version of fernet takes a list of keys for the decrypt operation; seems like we should offer something like that as well.

Seems like this should be a pretty trivial composition.

``` python
mf = MultiFernet(list_of_keys)

# Always encrypt with list_of_keys[0]
token = mf.encrypt(b"some stuff")

# Try decrypt with each key in order.
plain = mf.decrypt(a_token)
```

Seems like it might be the kind of thing we should offer?

On Thu, Sep 11, 2014 at 11:00 AM, David Reid notifications@github.com
wrote:

> Seems like this should be a pretty trivial composition.
> 
> mf = MultiFernet(list_of_keys)
> 
> # Always encrypt with list_of_keys[0]token = mf.encrypt(b"some stuff")
> 
> # Try decrypt with each key in order.plain = mf.decrypt(a_token)
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/pyca/cryptography/issues/1327#issuecomment-55303117.

## 

"I disapprove of what you say, but I will defend to the death your right to
say it." -- Evelyn Beatrice Hall (summarizing Voltaire)
"The people's good is the highest law." -- Cicero
GPG Key fingerprint: 125F 5C67 DFE9 4084

Do we need to worry about timing attacks at all in this situation?

On 11 September 2014 19:00:52 David Reid notifications@github.com wrote:

> Seems like this should be a pretty trivial composition.
> 
> ``` python
> mf = MultiFernet(list_of_keys)
> 
> # Always encrypt with list_of_keys[0]
> token = mf.encrypt(b"some stuff")
> 
> # Try decrypt with each key in order.
> plain = mf.decrypt(a_token)
> ```
> 
> ---
> 
> Reply to this email directly or view it on GitHub:
> https://github.com/pyca/cryptography/issues/1327#issuecomment-55303117

I suppose you could leak how many keys there are, and maybe which key a
given thing was encrypted with?

On Thu, Sep 11, 2014 at 11:26 AM, Alex Stapleton notifications@github.com
wrote:

> Do we need to worry about timing attacks at all in this situation?
> 
> On 11 September 2014 19:00:52 David Reid notifications@github.com wrote:
> 
> > Seems like this should be a pretty trivial composition.
> > 
> > ``` python
> > mf = MultiFernet(list_of_keys)
> > 
> > # Always encrypt with list_of_keys[0]
> > token = mf.encrypt(b"some stuff")
> > 
> > # Try decrypt with each key in order.
> > plain = mf.decrypt(a_token)
> > ```
> > 
> > ---
> > 
> > Reply to this email directly or view it on GitHub:
> > https://github.com/pyca/cryptography/issues/1327#issuecomment-55303117
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/pyca/cryptography/issues/1327#issuecomment-55306864.

## 

"I disapprove of what you say, but I will defend to the death your right to
say it." -- Evelyn Beatrice Hall (summarizing Voltaire)
"The people's good is the highest law." -- Cicero
GPG Key fingerprint: 125F 5C67 DFE9 4084

@dreid I'm probably missing something obvious but how would the API be able to tell which key's decryption operation is the correct one?

It would try each of them until one suceeds.

On Thu, Sep 11, 2014 at 10:15 PM, Terry Chia notifications@github.com
wrote:

> @dreid https://github.com/dreid I'm probably missing something obvious
> but how would the API be able to tell which key's decryption operation is
> the correct one?
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/pyca/cryptography/issues/1327#issuecomment-55353997.

## 

"I disapprove of what you say, but I will defend to the death your right to
say it." -- Evelyn Beatrice Hall (summarizing Voltaire)
"The people's good is the highest law." -- Cicero
GPG Key fingerprint: 125F 5C67 DFE9 4084

@alex That adds a performance penalty for doing frequent key rotation.  I was thinking more of something that prefixes the ciphertext with a version number, like this:

Version (0x81?) || Key ID ‖ Timestamp ‖ IV ‖ Ciphertext ‖ HMAC

That would also have the nice benefit of making it possible to determine whether it's "safe" (from an availability standpoint) to disable a particular key in a production system by just looking at the tokens.  I guess it would also leak information to an attacker about which tokens can be decrypted using a compromised key, but the timestamp field probably leaks that info anyway, and having this field would probably help the good guys more than not having it would hinder attackers.

@dlitz If you want to add support for key IDs to the fernet token format you should bring that up at https://github.com/fernet/spec


Resolving patch:
diff --git a/cryptography/fernet.py b/cryptography/fernet.py
--- a/cryptography/fernet.py
+++ b/cryptography/fernet.py
@@ -127,3 +127,24 @@ def decrypt(self, token, ttl=None):
         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


Test patch:
diff --git a/tests/test_fernet.py b/tests/test_fernet.py
--- a/tests/test_fernet.py
+++ b/tests/test_fernet.py
@@ -24,7 +24,7 @@
 
 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 @@ def test_roundtrips(self, message, backend):
     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)