diff options
author | Paul Kehrer <paul.l.kehrer@gmail.com> | 2017-06-07 18:08:57 -1000 |
---|---|---|
committer | Alex Gaynor <alex.gaynor@gmail.com> | 2017-06-08 00:08:57 -0400 |
commit | 7e53d911577881d87ce30291cef68e24f3c1b763 (patch) | |
tree | 3a8a0b43fdaae7d3d44549f7282048f5f3f1db58 | |
parent | f12955cd242664cffbaa041ef815579a8d6b2d3a (diff) | |
download | cryptography-7e53d911577881d87ce30291cef68e24f3c1b763.tar.gz cryptography-7e53d911577881d87ce30291cef68e24f3c1b763.tar.bz2 cryptography-7e53d911577881d87ce30291cef68e24f3c1b763.zip |
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
-rw-r--r-- | CHANGELOG.rst | 2 | ||||
-rw-r--r-- | docs/hazmat/primitives/aead.rst | 79 | ||||
-rw-r--r-- | docs/hazmat/primitives/index.rst | 1 | ||||
-rw-r--r-- | docs/spelling_wordlist.txt | 1 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/openssl/backend.py | 17 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/openssl/chacha20poly1305.py | 101 | ||||
-rw-r--r-- | src/cryptography/hazmat/primitives/ciphers/aead.py | 54 | ||||
-rw-r--r-- | src/cryptography/utils.py | 5 | ||||
-rw-r--r-- | tests/hazmat/primitives/test_aead.py | 142 |
9 files changed, 402 insertions, 0 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 080ebd66..be9bbd89 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,6 +21,8 @@ Changelog * 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 00000000..54343b86 --- /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 78beb3d9..72e5b26c 100644 --- a/docs/hazmat/primitives/index.rst +++ b/docs/hazmat/primitives/index.rst @@ -6,6 +6,7 @@ Primitives .. toctree:: :maxdepth: 1 + aead asymmetric/index constant-time key-derivation-functions diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index b3303a43..d9921a0e 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -19,6 +19,7 @@ cryptographic 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 412432df..c003b6d3 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -20,6 +20,7 @@ from cryptography.hazmat.backends.interfaces import ( 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 @@ class Backend(object): 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 00000000..0834f19c --- /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 00000000..e89c6979 --- /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 cb7137cc..d28dc71d 100644 --- a/src/cryptography/utils.py +++ b/src/cryptography/utils.py @@ -18,6 +18,11 @@ PersistentlyDeprecated = DeprecationWarning 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 00000000..aea2380b --- /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 |