Repository: pyca/cryptography

Issue:
ChaCha20 Support
Two things to answer here. Are we going to add ChaCha20 (separate from ChaCha20Poly1305)? It can be useful as a component in constructing other things so I think the answer is yes.

If yes, then how do we want to handle nonce/iv? The spec calls it a 96-bit nonce/iv with a 32-bit block counter. OpenSSL treats it as a single 128-bit value. Do we want to just treat it as an opaque byte string or separate out the two arguments and have the backends compose them as necessary? I'm leaning towards opaque byte string and documenting the way in which they're concatenated.

refs #2968


Discussion:
To thicken the plot a bit I haven't actually figured out how to make test vectors 2 and 3 pass (https://tools.ietf.org/html/rfc7539#appendix-A.2)

OpenSSH's version of Chacha20Poly1305 is different than what is used in TLS. As a result, I needed to be able to make separate calls to each of those to make things work. I'm currently using libnacl/libsodium for this, with some added shims to allow me to directly call some libsodium functions that libnacl doesn't normally expose. Specifically, here are the primitives I'm depending on:

For chacha20:

```
nacl.crypto_stream_chacha20_keybytes()
nacl.crypto_stream_chacha20_noncebytes()
nacl.crypto_stream_chacha20(buf, buflen, nonce, key)
nacl.crypto_stream_chacha20_xor_ic(outbuf, inbuf, buflen, nonce, ctr, key)
```

For poly1305:

```
nacl.crypto_onetimeauth_poly1305_bytes()
nacl.crypto_onetimeauth_poly1305_keybytes()
nacl.crypto_onetimeauth_poly1305(outtag, msg, msglen, polykey)
nacl.crypto_onetimeauth_poly1305_verify(intag, msg, msglen, polykey)
```

My wrappers look something like:

```
crypt_chacha20(key, data, nonce, ctr) -> encrypted/decrypted data
compute_poly1305_tag(data, nonce) -> tag
verify_poly1305_tag(data, nonce, tag) -> bool
```

The high level API is:

```
crypt_len(data, nonce) -> encrypted/decrypted data containing len
encrypt_and_sign(header, plaintext, nonce) -> ciphertext, tag
verify_and_decrypt(header, ciphertext, nonce, tag) -> plaintext or None (if verify fails)
```

The "header" here is a 32-bit length of the plaintext, and the nonce is a 64-bit counter that is incremented for each message. There are separate encrypt/decrypt calls for the header and message, with different chacha20 keys. Also, the header encrypt sets ctr=0 and the message encrypt sets ctr=1. The poly1305 tag is computed over the concatenation of the header and ciphertext and verified before the ciphertext is decrypted.

Thanks for the info @ronf! Unfortunately for your purposes, OpenSSL doesn't expose poly1305 as an independent MAC (d'oh), but it's good to be aware of this use case for us.
No problem - while it'd be great to get all my crypto functions through a single dependency on "cryptography" eventually, the libnacl/libsodium version is working for me in the meantime.

Resolving patch:
diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py
--- a/src/cryptography/hazmat/backends/openssl/backend.py
+++ b/src/cryptography/hazmat/backends/openssl/backend.py
@@ -58,7 +58,7 @@
     MGF1, OAEP, PKCS1v15, PSS
 )
 from cryptography.hazmat.primitives.ciphers.algorithms import (
-    AES, ARC4, Blowfish, CAST5, Camellia, IDEA, SEED, TripleDES
+    AES, ARC4, Blowfish, CAST5, Camellia, ChaCha20, IDEA, SEED, TripleDES
 )
 from cryptography.hazmat.primitives.ciphers.modes import (
     CBC, CFB, CFB8, CTR, ECB, GCM, OFB
@@ -258,6 +258,11 @@ def _register_default_ciphers(self):
             type(None),
             GetCipherByName("rc4")
         )
+        self.register_cipher_adapter(
+            ChaCha20,
+            type(None),
+            GetCipherByName("chacha20")
+        )
 
     def create_symmetric_encryption_ctx(self, cipher, mode):
         return _CipherContext(self, cipher, mode, _CipherContext._ENCRYPT)
diff --git a/src/cryptography/hazmat/backends/openssl/ciphers.py b/src/cryptography/hazmat/backends/openssl/ciphers.py
--- a/src/cryptography/hazmat/backends/openssl/ciphers.py
+++ b/src/cryptography/hazmat/backends/openssl/ciphers.py
@@ -59,6 +59,8 @@ def __init__(self, backend, cipher, mode, operation):
             iv_nonce = mode.initialization_vector
         elif isinstance(mode, modes.ModeWithNonce):
             iv_nonce = mode.nonce
+        elif isinstance(cipher, modes.ModeWithNonce):
+            iv_nonce = cipher.nonce
         else:
             iv_nonce = self._backend._ffi.NULL
         # begin init with cipher and operation type
diff --git a/src/cryptography/hazmat/primitives/ciphers/algorithms.py b/src/cryptography/hazmat/primitives/ciphers/algorithms.py
--- a/src/cryptography/hazmat/primitives/ciphers/algorithms.py
+++ b/src/cryptography/hazmat/primitives/ciphers/algorithms.py
@@ -8,6 +8,7 @@
 from cryptography.hazmat.primitives.ciphers import (
     BlockCipherAlgorithm, CipherAlgorithm
 )
+from cryptography.hazmat.primitives.ciphers.modes import ModeWithNonce
 
 
 def _verify_key_size(algorithm, key):
@@ -138,3 +139,26 @@ def __init__(self, key):
     @property
     def key_size(self):
         return len(self.key) * 8
+
+
+@utils.register_interface(CipherAlgorithm)
+@utils.register_interface(ModeWithNonce)
+class ChaCha20(object):
+    name = "ChaCha20"
+    key_sizes = frozenset([256])
+
+    def __init__(self, key, nonce):
+        self.key = _verify_key_size(self, key)
+        if not isinstance(nonce, bytes):
+            raise TypeError("nonce must be bytes")
+
+        if len(nonce) != 16:
+            raise ValueError("nonce must be 128-bits (16 bytes)")
+
+        self._nonce = nonce
+
+    nonce = utils.read_only_property("_nonce")
+
+    @property
+    def key_size(self):
+        return len(self.key) * 8


Test patch:
diff --git a/tests/hazmat/primitives/test_chacha20.py b/tests/hazmat/primitives/test_chacha20.py
new file mode 100644
--- /dev/null
+++ b/tests/hazmat/primitives/test_chacha20.py
@@ -0,0 +1,60 @@
+# This file is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file in the root of this repository
+# for complete details.
+
+from __future__ import absolute_import, division, print_function
+
+import binascii
+import os
+import struct
+
+import pytest
+
+from cryptography.hazmat.backends.interfaces import CipherBackend
+from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
+
+from .utils import _load_all_params
+from ...utils import load_nist_vectors
+
+
+@pytest.mark.supported(
+    only_if=lambda backend: backend.cipher_supported(
+        algorithms.ChaCha20(b"\x00" * 32, b"0" * 16), None
+    ),
+    skip_message="Does not support ChaCha20",
+)
+@pytest.mark.requires_backend_interface(interface=CipherBackend)
+class TestChaCha20(object):
+    @pytest.mark.parametrize(
+        "vector",
+        _load_all_params(
+            os.path.join("ciphers", "ChaCha20"),
+            ["rfc7539.txt"],
+            load_nist_vectors
+        )
+    )
+    def test_vectors(self, vector, backend):
+        key = binascii.unhexlify(vector["key"])
+        nonce = binascii.unhexlify(vector["nonce"])
+        ibc = struct.pack("<i", int(vector["initial_block_counter"]))
+        pt = binascii.unhexlify(vector["plaintext"])
+        encryptor = Cipher(
+            algorithms.ChaCha20(key, ibc + nonce), None, backend
+        ).encryptor()
+        computed_ct = encryptor.update(pt) + encryptor.finalize()
+        assert binascii.hexlify(computed_ct) == vector["ciphertext"]
+
+    def test_key_size(self):
+        chacha = algorithms.ChaCha20(b"0" * 32, b"0" * 16)
+        assert chacha.key_size == 256
+
+    def test_invalid_key_size(self):
+        with pytest.raises(ValueError):
+            algorithms.ChaCha20(b"wrongsize", b"0" * 16)
+
+    def test_invalid_nonce(self):
+        with pytest.raises(ValueError):
+            algorithms.ChaCha20(b"0" * 32, b"0")
+
+        with pytest.raises(TypeError):
+            algorithms.ChaCha20(b"0" * 32, object())