From 01219a46592e0a4a32e1ae8ef46beb09a2363291 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 2 Mar 2026 11:40:10 -0600 Subject: [PATCH 1/3] Fix ChaCha20Poly1305 to be singleshot --- tests/test_chacha20poly1305.py | 51 ++++++++----- wolfcrypt/ciphers.py | 136 +++++++++------------------------ 2 files changed, 69 insertions(+), 118 deletions(-) diff --git a/tests/test_chacha20poly1305.py b/tests/test_chacha20poly1305.py index 4125a14..d9149bf 100644 --- a/tests/test_chacha20poly1305.py +++ b/tests/test_chacha20poly1305.py @@ -1,5 +1,5 @@ -# test_aesgcmstream.py +# test_chacha20poly1305.py # # Copyright (C) 2022 wolfSSL Inc. # @@ -24,7 +24,6 @@ from wolfcrypt._ffi import lib as _lib if _lib.CHACHA20_POLY1305_ENABLED: - from collections import namedtuple import pytest from wolfcrypt.utils import t2b from wolfcrypt.exceptions import WolfCryptError @@ -32,20 +31,34 @@ from wolfcrypt.ciphers import ChaCha20Poly1305 def test_encrypt_decrypt(): - key = "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f" - key = h2b(key) - iv = "07000000404142434445464748" #Chnage from C test - iv = h2b(iv) - aad = "50515253c0c1c2c3c4c5c6c7" #Change from C test - aad = h2b(aad) - plaintext1 = "4c616469657320616e642047656e746c656d656e206f662074686520636c617373206f66202739393a204966204920636f756c64206f6666657220796f75206f6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73637265656e20776f756c642062652069742e" - plaintext1 = h2b(plaintext1) - cipher1 = "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d63dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b3692ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc3ff4def08e4b7a9de576d26586cec64b6116" - authTag = "1ae10b594f09e26a7e902ecbd0600691" - chacha = ChaCha20Poly1305(key, iv, aad) - generatedChipherText, generatedAuthTag = chacha.encrypt(plaintext1) - assert h2b(cipher1) == generatedChipherText - assert h2b(authTag) == generatedAuthTag - chachadec = ChaCha20Poly1305(key, iv, aad) - generatedPlaintextdec = chachadec.decrypt(generatedAuthTag, generatedChipherText)#takes in the generated authtag made by encrypt and decrypts and produces the plaintext - assert generatedPlaintextdec == t2b("Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it.") + key = h2b("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f") + iv = h2b("07000000404142434445464748") + aad = h2b("50515253c0c1c2c3c4c5c6c7") + plaintext = h2b("4c616469657320616e642047656e746c656d656e206f662074686520636c617373206f66202739393a204966204920636f756c64206f6666657220796f75206f6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73637265656e20776f756c642062652069742e") + expected_ciphertext = h2b("d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d63dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b3692ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc3ff4def08e4b7a9de576d26586cec64b6116") + expected_authTag = h2b("1ae10b594f09e26a7e902ecbd0600691") + + chacha = ChaCha20Poly1305(key) + ciphertext, authTag = chacha.encrypt(aad, iv, plaintext) + assert ciphertext == expected_ciphertext + assert authTag == expected_authTag + + decrypted = chacha.decrypt(aad, iv, authTag, ciphertext) + assert decrypted == t2b("Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it.") + + def test_invalid_key_size(): + with pytest.raises(ValueError): + ChaCha20Poly1305(b"tooshort") + + def test_decrypt_bad_tag(): + key = h2b("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f") + iv = h2b("07000000404142434445464748") + aad = h2b("50515253c0c1c2c3c4c5c6c7") + plaintext = b"hello world" + + chacha = ChaCha20Poly1305(key) + ciphertext, authTag = chacha.encrypt(aad, iv, plaintext) + + bad_tag = b"\x00" * 16 + with pytest.raises(WolfCryptError): + chacha.decrypt(aad, iv, bad_tag, ciphertext) diff --git a/wolfcrypt/ciphers.py b/wolfcrypt/ciphers.py index eb3f27f..eaf4efd 100644 --- a/wolfcrypt/ciphers.py +++ b/wolfcrypt/ciphers.py @@ -538,133 +538,71 @@ def set_iv(self, nonce, counter = 0): if _lib.CHACHA20_POLY1305_ENABLED: class ChaCha20Poly1305(object): """ - ChaCha20 Poly1305 + ChaCha20-Poly1305 AEAD cipher. + + One-shot encrypt/decrypt interface (non-streaming). """ - block_size = 16 - _key_sizes = [16, 24, 32] - _native_type = "ChaChaPoly_Aead *" - _aad = None + _key_sizes = [32] _tag_bytes = 16 - _mode = None - _key = bytes() - _IV = bytes() - def __init__(self, key, IV, aad, tag_bytes=16): - """ - tag_bytes is the number of bytes to use for the authentication tag during encryption - """ + def __init__(self, key): self._key = t2b(key) - self._IV = t2b(IV) - self._aad = t2b(aad) if len(self._key) not in self._key_sizes: raise ValueError("key must be %s in length, not %d" % (self._key_sizes, len(self._key))) - self._native_object = _ffi.new(self._native_type) - self._mode = None - ret = _lib.wc_ChaCha20Poly1305_Init( - self._native_object, - _ffi.from_buffer(self._key), - _ffi.from_buffer(self._IV), - 1 - ) - if ret < 0: - raise WolfCryptError("Init error (%d)" % ret) - def set_aad(self, data): - """ - Set the additional authentication data for the stream + def encrypt(self, aad, iv, plaintext): """ - if self._mode is not None: - raise WolfCryptError("AAD can only be set before encrypt() or decrypt() is called") - self._aad = t2b(data) - - def get_aad(self): - return self._aad + Encrypt plaintext data using the IV/nonce provided. The + associated data (aad) is not encrypted but is included in the + authentication tag. - def encrypt(self, inPlainText): + Returns a tuple of (ciphertext, authTag). """ - Add more data to the encryption stream - """ - inPlainText = t2b(inPlainText) - if self._mode is None: - self._mode = _ENCRYPTION - aad = self._aad - elif self._mode == _DECRYPTION: - raise WolfCryptError("Class instance already in use for decryption") - outGeneratedCipherText = _ffi.new("byte[%d]" % (len(inPlainText))) - outGeneratedAuthTag = _ffi.new("byte[%d]" % self._tag_bytes) + aad = t2b(aad) + iv = t2b(iv) + plaintext = t2b(plaintext) + ciphertext = _ffi.new("byte[%d]" % len(plaintext)) + authTag = _ffi.new("byte[%d]" % self._tag_bytes) ret = _lib.wc_ChaCha20Poly1305_Encrypt( _ffi.from_buffer(self._key), - _ffi.from_buffer(self._IV), + _ffi.from_buffer(iv), _ffi.from_buffer(aad), len(aad), - _ffi.from_buffer(inPlainText), - len(inPlainText), - outGeneratedCipherText, - outGeneratedAuthTag + _ffi.from_buffer(plaintext), + len(plaintext), + ciphertext, + authTag ) - if ret < 0: raise WolfCryptError("Encryption error (%d)" % ret) - return bytes(outGeneratedCipherText), bytes(outGeneratedAuthTag) + return bytes(ciphertext), bytes(authTag) - def decrypt(self, inGeneratedAuthTag, inGeneratedCipher): + def decrypt(self, aad, iv, authTag, ciphertext): """ - Add more data to the decryption stream + Decrypt the ciphertext using the IV/nonce and authentication tag + provided. The integrity of the associated data (aad) is checked. + + Returns the decrypted plaintext. """ - inGeneratedCipher = t2b(inGeneratedCipher) - inGeneratedAuthTag = t2b(inGeneratedAuthTag) - if self._mode is None: - self._mode = _DECRYPTION - aad = self._aad - elif self._mode == _ENCRYPTION: - raise WolfCryptError("Class instance already in use for decryption") - outPlainText = _ffi.new("byte[%d]" % (len(inGeneratedCipher))) + aad = t2b(aad) + iv = t2b(iv) + authTag = t2b(authTag) + ciphertext = t2b(ciphertext) + plaintext = _ffi.new("byte[%d]" % len(ciphertext)) ret = _lib.wc_ChaCha20Poly1305_Decrypt( _ffi.from_buffer(self._key), - _ffi.from_buffer(self._IV), + _ffi.from_buffer(iv), _ffi.from_buffer(aad), len(aad), - _ffi.from_buffer(inGeneratedCipher), - len(inGeneratedCipher), - _ffi.from_buffer(inGeneratedAuthTag), - outPlainText + _ffi.from_buffer(ciphertext), + len(ciphertext), + _ffi.from_buffer(authTag), + plaintext ) if ret < 0: raise WolfCryptError("Decryption error (%d)" % ret) - return bytes(outPlainText) - - def checkTag(self, authTag): - """ - Check the authentication tag for the stream - """ - authTag = t2b(authTag) - ret = _lib.wc_ChaCha20Poly1305_CheckTag(authTag, len(authTag)) - if ret < 0: - raise WolfCryptError("Decryption error (%d)" % ret) - - def final(self, authTag=None): - """ - When encrypting, finalize the stream and return an authentication tag for the stream. - When decrypting, verify the authentication tag for the stream. - The authTag parameter is only used for decrypting. - """ - if self._mode is None: - raise WolfCryptError("Final called with no encryption or decryption") - elif self._mode == _ENCRYPTION: - authTag = _ffi.new("byte[%d]" % self._tag_bytes) - ret = _lib.wc_ChaCha20Poly1305_Final(self._native_type, authTag) - if ret < 0: - raise WolfCryptError("Encryption error (%d)" % ret) - return _ffi.buffer(authTag)[:] - else: - if authTag is None: - raise WolfCryptError("authTag parameter required") - authTag = t2b(authTag) - self._native_object = _ffi.new(self._native_type) - ret = _lib.wc_ChaCha20Poly1305_Final(self._native_type, authTag) - if ret < 0: - raise WolfCryptError("Decryption error (%d)" % ret) + return bytes(plaintext) if _lib.DES3_ENABLED: class Des3(_Cipher): From 6e9f2ba1dc1caff8daf989675cebe1107e839a8a Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 2 Mar 2026 11:54:06 -0600 Subject: [PATCH 2/3] Fixes from review --- tests/test_chacha20poly1305.py | 18 ++++++++++++++++++ wolfcrypt/ciphers.py | 7 +++++++ 2 files changed, 25 insertions(+) diff --git a/tests/test_chacha20poly1305.py b/tests/test_chacha20poly1305.py index d9149bf..adba98d 100644 --- a/tests/test_chacha20poly1305.py +++ b/tests/test_chacha20poly1305.py @@ -50,6 +50,24 @@ def test_invalid_key_size(): with pytest.raises(ValueError): ChaCha20Poly1305(b"tooshort") + def test_encrypt_invalid_iv_length(): + key = h2b("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f") + chacha = ChaCha20Poly1305(key) + with pytest.raises(ValueError): + chacha.encrypt(b"aad", b"short", b"plaintext") + + def test_decrypt_invalid_iv_length(): + key = h2b("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f") + chacha = ChaCha20Poly1305(key) + with pytest.raises(ValueError): + chacha.decrypt(b"aad", b"short", b"\x00" * 16, b"ciphertext") + + def test_decrypt_invalid_tag_length(): + key = h2b("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f") + chacha = ChaCha20Poly1305(key) + with pytest.raises(ValueError): + chacha.decrypt(b"aad", b"\x00" * 12, b"short", b"ciphertext") + def test_decrypt_bad_tag(): key = h2b("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f") iv = h2b("07000000404142434445464748") diff --git a/wolfcrypt/ciphers.py b/wolfcrypt/ciphers.py index eaf4efd..412e9b1 100644 --- a/wolfcrypt/ciphers.py +++ b/wolfcrypt/ciphers.py @@ -561,6 +561,8 @@ def encrypt(self, aad, iv, plaintext): """ aad = t2b(aad) iv = t2b(iv) + if len(iv) != 12: + raise ValueError("iv must be 12 bytes, got %d" % len(iv)) plaintext = t2b(plaintext) ciphertext = _ffi.new("byte[%d]" % len(plaintext)) authTag = _ffi.new("byte[%d]" % self._tag_bytes) @@ -587,7 +589,12 @@ def decrypt(self, aad, iv, authTag, ciphertext): """ aad = t2b(aad) iv = t2b(iv) + if len(iv) != 12: + raise ValueError("iv must be 12 bytes, got %d" % len(iv)) authTag = t2b(authTag) + if len(authTag) != self._tag_bytes: + raise ValueError("authTag must be %d bytes, got %d" % + (self._tag_bytes, len(authTag))) ciphertext = t2b(ciphertext) plaintext = _ffi.new("byte[%d]" % len(ciphertext)) ret = _lib.wc_ChaCha20Poly1305_Decrypt( From 3ab80ac113d5bac3cf8e8ad366a44e8ef8614e6e Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Mon, 2 Mar 2026 14:15:54 -0600 Subject: [PATCH 3/3] Fix test vectors --- tests/test_chacha20poly1305.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_chacha20poly1305.py b/tests/test_chacha20poly1305.py index adba98d..c67840e 100644 --- a/tests/test_chacha20poly1305.py +++ b/tests/test_chacha20poly1305.py @@ -32,7 +32,7 @@ def test_encrypt_decrypt(): key = h2b("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f") - iv = h2b("07000000404142434445464748") + iv = h2b("070000004041424344454647") aad = h2b("50515253c0c1c2c3c4c5c6c7") plaintext = h2b("4c616469657320616e642047656e746c656d656e206f662074686520636c617373206f66202739393a204966204920636f756c64206f6666657220796f75206f6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73637265656e20776f756c642062652069742e") expected_ciphertext = h2b("d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d63dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b3692ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc3ff4def08e4b7a9de576d26586cec64b6116") @@ -70,7 +70,7 @@ def test_decrypt_invalid_tag_length(): def test_decrypt_bad_tag(): key = h2b("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f") - iv = h2b("07000000404142434445464748") + iv = h2b("070000004041424344454647") aad = h2b("50515253c0c1c2c3c4c5c6c7") plaintext = b"hello world"