ChaCha20Poly1305 support (#3680)

* chacha20poly1305 support

* add chacha20poly1305 backend and some fixes

* refactor

* forgot to remove this

* pep8

* review feedback and a lot of type/value checking

* review feedback

* raise unsupportedalgorithm when creating a ChaCha20Poly1305 object

if it's not supported.

* switch to ciphertext||tag

* typo

* remove a branch we don't need

* review feedback

* decrypts is *also* a word

* use reasons
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 080ebd6..be9bbd8 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -21,6 +21,8 @@
 * Added support for parsing
   :class:`~cryptography.x509.certificate_transparency.SignedCertificateTimestamp`
   objects from X.509 certificate extensions.
+* Added support for
+  :class:`~cryptography.hazmat.primitives.ciphers.aead.ChaCha20Poly1305`.
 
 1.9 - 2017-05-29
 ~~~~~~~~~~~~~~~~
diff --git a/docs/hazmat/primitives/aead.rst b/docs/hazmat/primitives/aead.rst
new file mode 100644
index 0000000..54343b8
--- /dev/null
+++ b/docs/hazmat/primitives/aead.rst
@@ -0,0 +1,79 @@
+.. hazmat::
+
+
+Authenticated encryption
+========================
+
+.. module:: cryptography.hazmat.primitives.ciphers.aead
+
+Authenticated encryption with associated data (AEAD) are encryption schemes
+which provide both confidentiality and integrity for their ciphertext. They
+also support providing integrity for associated data which is not encrypted.
+
+.. class:: ChaCha20Poly1305(key)
+
+    .. versionadded:: 2.0
+
+    The ChaCha20Poly1305 construction is defined in :rfc:`7539` section 2.8.
+    It is a stream cipher combined with a MAC that offers strong integrity
+    guarantees.
+
+    :param bytes key: A 32-byte key. This **must** be kept secret.
+
+    :raises cryptography.exceptions.UnsupportedAlgorithm: If the version of
+        OpenSSL does not support ChaCha20Poly1305.
+
+    .. doctest::
+
+        >>> import os
+        >>> from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
+        >>> data = b"a secret message"
+        >>> aad = b"authenticated but unencrypted data"
+        >>> key = ChaCha20Poly1305.generate_key()
+        >>> chacha = ChaCha20Poly1305(key)
+        >>> nonce = os.urandom(12)
+        >>> ct = chacha.encrypt(nonce, data, aad)
+        >>> chacha.decrypt(nonce, ct, aad)
+        'a secret message'
+
+    .. classmethod:: generate_key()
+
+        Securely generates a random ChaCha20Poly1305 key.
+
+        :returns bytes: A 32 byte key.
+
+    .. method:: encrypt(nonce, data, associated_data)
+
+        .. warning::
+
+            Reuse of a ``nonce`` with a given ``key`` compromises the security
+            of any message with that ``nonce`` and ``key`` pair.
+
+        Encrypts the ``data`` provided and authenticates the
+        ``associated_data``.  The output of this can be passed directly
+        to the ``decrypt`` method.
+
+        :param bytes nonce: A 12 byte value. **NEVER REUSE A NONCE** with a
+            key.
+        :param bytes data: The data to encrypt.
+        :param bytes associated_data: Additional data that should be
+            authenticated with the key, but does not need to be encrypted. Can
+            be ``None``.
+        :returns bytes: The ciphertext bytes with the 16 byte tag appended.
+
+    .. method:: decrypt(nonce, data, associated_data)
+
+        Decrypts the ``data`` and authenticates the ``associated_data``. If you
+        called encrypt with ``associated_data`` you must pass the same
+        ``associated_data`` in decrypt or the integrity check will fail.
+
+        :param bytes nonce: A 12 byte value. **NEVER REUSE A NONCE** with a
+            key.
+        :param bytes data: The data to decrypt (with tag appended).
+        :param bytes associated_data: Additional data to authenticate. Can be
+            ``None`` if none was passed during encryption.
+        :returns bytes: The original plaintext.
+        :raises cryptography.exceptions.InvalidTag: If the authentication tag
+            doesn't validate this exception will be raised. This will occur
+            when the ciphertext has been changed, but will also occur when the
+            key, nonce, or associated data are wrong.
diff --git a/docs/hazmat/primitives/index.rst b/docs/hazmat/primitives/index.rst
index 78beb3d..72e5b26 100644
--- a/docs/hazmat/primitives/index.rst
+++ b/docs/hazmat/primitives/index.rst
@@ -6,6 +6,7 @@
 .. toctree::
     :maxdepth: 1
 
+    aead
     asymmetric/index
     constant-time
     key-derivation-functions
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index b3303a4..d9921a0 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -19,6 +19,7 @@
 cryptographically
 Debian
 decrypt
+Decrypts
 decrypted
 decrypting
 DER
diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py
index 412432d..c003b6d 100644
--- a/src/cryptography/hazmat/backends/openssl/backend.py
+++ b/src/cryptography/hazmat/backends/openssl/backend.py
@@ -20,6 +20,7 @@
     EllipticCurveBackend, HMACBackend, HashBackend, PBKDF2HMACBackend,
     PEMSerializationBackend, RSABackend, ScryptBackend, X509Backend
 )
+from cryptography.hazmat.backends.openssl import chacha20poly1305
 from cryptography.hazmat.backends.openssl.ciphers import _CipherContext
 from cryptography.hazmat.backends.openssl.cmac import _CMACContext
 from cryptography.hazmat.backends.openssl.dh import (
@@ -1780,6 +1781,22 @@
         self.openssl_assert(res == 1)
         return self._ffi.buffer(buf)[:]
 
+    def chacha20poly1305_encrypt(self, key, nonce, data, associated_data):
+        return chacha20poly1305.encrypt(
+            self, key, nonce, data, associated_data
+        )
+
+    def chacha20poly1305_decrypt(self, key, nonce, data, associated_data):
+        return chacha20poly1305.decrypt(
+            self, key, nonce, data, associated_data
+        )
+
+    def chacha20poly1305_supported(self):
+        return (
+            self._lib.EVP_get_cipherbyname(b"chacha20-poly1305") !=
+            self._ffi.NULL
+        )
+
 
 class GetCipherByName(object):
     def __init__(self, fmt):
diff --git a/src/cryptography/hazmat/backends/openssl/chacha20poly1305.py b/src/cryptography/hazmat/backends/openssl/chacha20poly1305.py
new file mode 100644
index 0000000..0834f19
--- /dev/null
+++ b/src/cryptography/hazmat/backends/openssl/chacha20poly1305.py
@@ -0,0 +1,101 @@
+# 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
+
+from cryptography.exceptions import InvalidTag
+
+
+_ENCRYPT = 1
+_DECRYPT = 0
+
+
+def _chacha20poly1305_setup(backend, key, nonce, tag, operation):
+    evp_cipher = backend._lib.EVP_get_cipherbyname(b"chacha20-poly1305")
+    ctx = backend._lib.EVP_CIPHER_CTX_new()
+    ctx = backend._ffi.gc(ctx, backend._lib.EVP_CIPHER_CTX_free)
+    res = backend._lib.EVP_CipherInit_ex(
+        ctx, evp_cipher,
+        backend._ffi.NULL,
+        backend._ffi.NULL,
+        backend._ffi.NULL,
+        int(operation == _ENCRYPT)
+    )
+    backend.openssl_assert(res != 0)
+    res = backend._lib.EVP_CIPHER_CTX_set_key_length(ctx, len(key))
+    backend.openssl_assert(res != 0)
+    res = backend._lib.EVP_CIPHER_CTX_ctrl(
+        ctx, backend._lib.EVP_CTRL_AEAD_SET_IVLEN, len(nonce),
+        backend._ffi.NULL
+    )
+    backend.openssl_assert(res != 0)
+    if operation == _DECRYPT:
+        res = backend._lib.EVP_CIPHER_CTX_ctrl(
+            ctx, backend._lib.EVP_CTRL_AEAD_SET_TAG, len(tag), tag
+        )
+        backend.openssl_assert(res != 0)
+
+    res = backend._lib.EVP_CipherInit_ex(
+        ctx,
+        backend._ffi.NULL,
+        backend._ffi.NULL,
+        key,
+        nonce,
+        int(operation == _ENCRYPT)
+    )
+    backend.openssl_assert(res != 0)
+    return ctx
+
+
+def _process_aad(backend, ctx, associated_data):
+    outlen = backend._ffi.new("int *")
+    res = backend._lib.EVP_CipherUpdate(
+        ctx, backend._ffi.NULL, outlen, associated_data, len(associated_data)
+    )
+    backend.openssl_assert(res != 0)
+
+
+def _process_data(backend, ctx, data):
+    outlen = backend._ffi.new("int *")
+    buf = backend._ffi.new("unsigned char[]", len(data))
+    res = backend._lib.EVP_CipherUpdate(ctx, buf, outlen, data, len(data))
+    backend.openssl_assert(res != 0)
+    return backend._ffi.buffer(buf, outlen[0])[:]
+
+
+def encrypt(backend, key, nonce, data, associated_data):
+    ctx = _chacha20poly1305_setup(backend, key, nonce, None, _ENCRYPT)
+
+    _process_aad(backend, ctx, associated_data)
+    processed_data = _process_data(backend, ctx, data)
+    outlen = backend._ffi.new("int *")
+    res = backend._lib.EVP_CipherFinal_ex(ctx, backend._ffi.NULL, outlen)
+    backend.openssl_assert(res != 0)
+    backend.openssl_assert(outlen[0] == 0)
+    # get the tag
+    tag_buf = backend._ffi.new("unsigned char[]", 16)
+    res = backend._lib.EVP_CIPHER_CTX_ctrl(
+        ctx, backend._lib.EVP_CTRL_AEAD_GET_TAG, 16, tag_buf
+    )
+    backend.openssl_assert(res != 0)
+    tag = backend._ffi.buffer(tag_buf)[:]
+
+    return processed_data + tag
+
+
+def decrypt(backend, key, nonce, data, associated_data):
+    if len(data) < 16:
+        raise InvalidTag
+    tag = data[-16:]
+    data = data[:-16]
+    ctx = _chacha20poly1305_setup(backend, key, nonce, tag, _DECRYPT)
+    _process_aad(backend, ctx, associated_data)
+    processed_data = _process_data(backend, ctx, data)
+    outlen = backend._ffi.new("int *")
+    res = backend._lib.EVP_CipherFinal_ex(ctx, backend._ffi.NULL, outlen)
+    if res == 0:
+        backend._consume_errors()
+        raise InvalidTag
+
+    return processed_data
diff --git a/src/cryptography/hazmat/primitives/ciphers/aead.py b/src/cryptography/hazmat/primitives/ciphers/aead.py
new file mode 100644
index 0000000..e89c697
--- /dev/null
+++ b/src/cryptography/hazmat/primitives/ciphers/aead.py
@@ -0,0 +1,54 @@
+# 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 os
+
+from cryptography import exceptions, utils
+from cryptography.hazmat.backends.openssl.backend import backend
+
+
+class ChaCha20Poly1305(object):
+    def __init__(self, key):
+        if not backend.chacha20poly1305_supported():
+            raise exceptions.UnsupportedAlgorithm(
+                "ChaCha20Poly1305 is not supported by this version of OpenSSL",
+                exceptions._Reasons.UNSUPPORTED_CIPHER
+            )
+        utils._check_bytes("key", key)
+
+        if len(key) != 32:
+            raise ValueError("ChaCha20Poly1305 key must be 32 bytes.")
+
+        self._key = key
+
+    @classmethod
+    def generate_key(cls):
+        return os.urandom(32)
+
+    def encrypt(self, nonce, data, associated_data):
+        if associated_data is None:
+            associated_data = b""
+
+        self._check_params(nonce, data, associated_data)
+        return backend.chacha20poly1305_encrypt(
+            self._key, nonce, data, associated_data
+        )
+
+    def decrypt(self, nonce, data, associated_data):
+        if associated_data is None:
+            associated_data = b""
+
+        self._check_params(nonce, data, associated_data)
+        return backend.chacha20poly1305_decrypt(
+            self._key, nonce, data, associated_data
+        )
+
+    def _check_params(self, nonce, data, associated_data):
+        utils._check_bytes("nonce", nonce)
+        utils._check_bytes("data", data)
+        utils._check_bytes("associated_data", associated_data)
+        if len(nonce) != 12:
+            raise ValueError("Nonce must be 12 bytes")
diff --git a/src/cryptography/utils.py b/src/cryptography/utils.py
index cb7137c..d28dc71 100644
--- a/src/cryptography/utils.py
+++ b/src/cryptography/utils.py
@@ -18,6 +18,11 @@
 DeprecatedIn19 = DeprecationWarning
 
 
+def _check_bytes(name, value):
+    if not isinstance(value, bytes):
+        raise TypeError("{0} must be bytes".format(name))
+
+
 def read_only_property(name):
     return property(lambda self: getattr(self, name))
 
diff --git a/tests/hazmat/primitives/test_aead.py b/tests/hazmat/primitives/test_aead.py
new file mode 100644
index 0000000..aea2380
--- /dev/null
+++ b/tests/hazmat/primitives/test_aead.py
@@ -0,0 +1,142 @@
+# 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 pytest
+
+from cryptography.exceptions import InvalidTag, _Reasons
+from cryptography.hazmat.backends.interfaces import CipherBackend
+from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
+
+from ...utils import (
+    load_nist_vectors, load_vectors_from_file, raises_unsupported_algorithm
+)
+
+
+@pytest.mark.supported(
+    only_if=lambda backend: (
+        not backend.chacha20poly1305_supported()
+    ),
+    skip_message="Requires OpenSSL without ChaCha20Poly1305 support"
+)
+@pytest.mark.requires_backend_interface(interface=CipherBackend)
+def test_chacha20poly1305_unsupported_on_older_openssl(backend):
+    with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_CIPHER):
+        ChaCha20Poly1305(ChaCha20Poly1305.generate_key())
+
+
+@pytest.mark.supported(
+    only_if=lambda backend: backend.chacha20poly1305_supported(),
+    skip_message="Does not support ChaCha20Poly1305"
+)
+@pytest.mark.requires_backend_interface(interface=CipherBackend)
+class TestChaCha20Poly1305(object):
+    def test_generate_key(self):
+        key = ChaCha20Poly1305.generate_key()
+        assert len(key) == 32
+
+    def test_bad_key(self, backend):
+        with pytest.raises(TypeError):
+            ChaCha20Poly1305(object())
+
+        with pytest.raises(ValueError):
+            ChaCha20Poly1305(b"0" * 31)
+
+    @pytest.mark.parametrize(
+        ("nonce", "data", "associated_data"),
+        [
+            [object(), b"data", b""],
+            [b"0" * 12, object(), b""],
+            [b"0" * 12, b"data", object()]
+        ]
+    )
+    def test_params_not_bytes_encrypt(self, nonce, data, associated_data,
+                                      backend):
+        key = ChaCha20Poly1305.generate_key()
+        chacha = ChaCha20Poly1305(key)
+        with pytest.raises(TypeError):
+            chacha.encrypt(nonce, data, associated_data)
+
+        with pytest.raises(TypeError):
+            chacha.decrypt(nonce, data, associated_data)
+
+    def test_nonce_not_12_bytes(self, backend):
+        key = ChaCha20Poly1305.generate_key()
+        chacha = ChaCha20Poly1305(key)
+        with pytest.raises(ValueError):
+            chacha.encrypt(b"00", b"hello", b"")
+
+        with pytest.raises(ValueError):
+            chacha.decrypt(b"00", b"hello", b"")
+
+    def test_decrypt_data_too_short(self, backend):
+        key = ChaCha20Poly1305.generate_key()
+        chacha = ChaCha20Poly1305(key)
+        with pytest.raises(InvalidTag):
+            chacha.decrypt(b"0" * 12, b"0", None)
+
+    def test_associated_data_none_equal_to_empty_bytestring(self, backend):
+        key = ChaCha20Poly1305.generate_key()
+        chacha = ChaCha20Poly1305(key)
+        nonce = os.urandom(12)
+        ct1 = chacha.encrypt(nonce, b"some_data", None)
+        ct2 = chacha.encrypt(nonce, b"some_data", b"")
+        assert ct1 == ct2
+        pt1 = chacha.decrypt(nonce, ct1, None)
+        pt2 = chacha.decrypt(nonce, ct2, b"")
+        assert pt1 == pt2
+
+    @pytest.mark.parametrize(
+        "vector",
+        load_vectors_from_file(
+            os.path.join("ciphers", "ChaCha20Poly1305", "openssl.txt"),
+            load_nist_vectors
+        )
+    )
+    def test_openssl_vectors(self, vector, backend):
+        key = binascii.unhexlify(vector["key"])
+        nonce = binascii.unhexlify(vector["iv"])
+        aad = binascii.unhexlify(vector["aad"])
+        tag = binascii.unhexlify(vector["tag"])
+        pt = binascii.unhexlify(vector["plaintext"])
+        ct = binascii.unhexlify(vector["ciphertext"])
+        chacha = ChaCha20Poly1305(key)
+        if vector.get("result") == b"CIPHERFINAL_ERROR":
+            with pytest.raises(InvalidTag):
+                chacha.decrypt(nonce, ct + tag, aad)
+        else:
+            computed_pt = chacha.decrypt(nonce, ct + tag, aad)
+            assert computed_pt == pt
+            computed_ct = chacha.encrypt(nonce, pt, aad)
+            assert computed_ct == ct + tag
+
+    @pytest.mark.parametrize(
+        "vector",
+        load_vectors_from_file(
+            os.path.join("ciphers", "ChaCha20Poly1305", "boringssl.txt"),
+            load_nist_vectors
+        )
+    )
+    def test_boringssl_vectors(self, vector, backend):
+        key = binascii.unhexlify(vector["key"])
+        nonce = binascii.unhexlify(vector["nonce"])
+        if vector["ad"].startswith(b'"'):
+            aad = vector["ad"][1:-1]
+        else:
+            aad = binascii.unhexlify(vector["ad"])
+        tag = binascii.unhexlify(vector["tag"])
+        if vector["in"].startswith(b'"'):
+            pt = vector["in"][1:-1]
+        else:
+            pt = binascii.unhexlify(vector["in"])
+        ct = binascii.unhexlify(vector["ct"].strip(b'"'))
+        chacha = ChaCha20Poly1305(key)
+        computed_pt = chacha.decrypt(nonce, ct + tag, aad)
+        assert computed_pt == pt
+        computed_ct = chacha.encrypt(nonce, pt, aad)
+        assert computed_ct == ct + tag