Skip to content

Commit 9ff0768

Browse files
committed
crypto: add the ChaCha20Poly1305 AEAD as specified in RFC8439
This adds an implementation of the ChaCha20Poly1305 AEAD exactly matching the version specified in RFC8439 section 2.8, including tests and official test vectors.
1 parent 9fd085a commit 9ff0768

File tree

4 files changed

+231
-0
lines changed

4 files changed

+231
-0
lines changed

src/Makefile.am

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,8 @@ crypto_libbitcoin_crypto_base_la_SOURCES = \
548548
crypto/aes.h \
549549
crypto/chacha20.h \
550550
crypto/chacha20.cpp \
551+
crypto/chacha20poly1305.h \
552+
crypto/chacha20poly1305.cpp \
551553
crypto/common.h \
552554
crypto/hkdf_sha256_32.cpp \
553555
crypto/hkdf_sha256_32.h \

src/crypto/chacha20poly1305.cpp

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright (c) 2023 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <crypto/chacha20poly1305.h>
6+
7+
#include <crypto/common.h>
8+
#include <crypto/chacha20.h>
9+
#include <crypto/poly1305.h>
10+
#include <span.h>
11+
12+
#include <assert.h>
13+
#include <cstdint>
14+
#include <cstddef>
15+
#include <iterator>
16+
17+
AEADChaCha20Poly1305::AEADChaCha20Poly1305(Span<const std::byte> key) noexcept : m_chacha20(UCharCast(key.data()))
18+
{
19+
assert(key.size() == KEYLEN);
20+
}
21+
22+
void AEADChaCha20Poly1305::SetKey(Span<const std::byte> key) noexcept
23+
{
24+
assert(key.size() == KEYLEN);
25+
m_chacha20.SetKey32(UCharCast(key.data()));
26+
}
27+
28+
namespace {
29+
30+
#ifndef HAVE_TIMINGSAFE_BCMP
31+
#define HAVE_TIMINGSAFE_BCMP
32+
33+
int timingsafe_bcmp(const unsigned char* b1, const unsigned char* b2, size_t n) noexcept
34+
{
35+
const unsigned char *p1 = b1, *p2 = b2;
36+
int ret = 0;
37+
for (; n > 0; n--)
38+
ret |= *p1++ ^ *p2++;
39+
return (ret != 0);
40+
}
41+
42+
#endif
43+
44+
/** Compute poly1305 tag. chacha20 must be set to the right nonce, block 0. Will be at block 1 after. */
45+
void ComputeTag(ChaCha20& chacha20, Span<const std::byte> aad, Span<const std::byte> cipher, Span<std::byte> tag) noexcept
46+
{
47+
static const std::byte PADDING[16] = {{}};
48+
49+
// Get block of keystream (use a full 64 byte buffer to avoid the need for chacha20's own buffering).
50+
std::byte first_block[64];
51+
chacha20.Keystream(UCharCast(first_block), sizeof(first_block));
52+
53+
// Use the first 32 bytes of the first keystream block as poly1305 key.
54+
Poly1305 poly1305{Span{first_block}.first(Poly1305::KEYLEN)};
55+
56+
// Compute tag:
57+
// - Process the padded AAD with Poly1305.
58+
const unsigned aad_padding_length = (16 - (aad.size() % 16)) % 16;
59+
poly1305.Update(aad).Update(Span{PADDING}.first(aad_padding_length));
60+
// - Process the padded ciphertext with Poly1305.
61+
const unsigned cipher_padding_length = (16 - (cipher.size() % 16)) % 16;
62+
poly1305.Update(cipher).Update(Span{PADDING}.first(cipher_padding_length));
63+
// - Process the AAD and plaintext length with Poly1305.
64+
std::byte length_desc[Poly1305::TAGLEN];
65+
WriteLE64(UCharCast(length_desc), aad.size());
66+
WriteLE64(UCharCast(length_desc + 8), cipher.size());
67+
poly1305.Update(length_desc);
68+
69+
// Output tag.
70+
poly1305.Finalize(tag);
71+
}
72+
73+
} // namespace
74+
75+
void AEADChaCha20Poly1305::Encrypt(Span<const std::byte> plain, Span<const std::byte> aad, Nonce96 nonce, Span<std::byte> cipher) noexcept
76+
{
77+
assert(cipher.size() == plain.size() + EXPANSION);
78+
79+
// Encrypt using ChaCha20 (starting at block 1).
80+
m_chacha20.Seek64(nonce, 1);
81+
m_chacha20.Crypt(UCharCast(plain.data()), UCharCast(cipher.data()), plain.size());
82+
83+
// Seek to block 0, and compute tag using key drawn from there.
84+
m_chacha20.Seek64(nonce, 0);
85+
ComputeTag(m_chacha20, aad, cipher.first(plain.size()), cipher.last(EXPANSION));
86+
}
87+
88+
bool AEADChaCha20Poly1305::Decrypt(Span<const std::byte> cipher, Span<const std::byte> aad, Nonce96 nonce, Span<std::byte> plain) noexcept
89+
{
90+
assert(cipher.size() == plain.size() + EXPANSION);
91+
92+
// Verify tag (using key drawn from block 0).
93+
m_chacha20.Seek64(nonce, 0);
94+
std::byte expected_tag[EXPANSION];
95+
ComputeTag(m_chacha20, aad, cipher.first(plain.size()), expected_tag);
96+
if (timingsafe_bcmp(UCharCast(expected_tag), UCharCast(cipher.data() + plain.size()), EXPANSION)) return false;
97+
98+
// Decrypt (starting at block 1).
99+
m_chacha20.Crypt(UCharCast(cipher.data()), UCharCast(plain.data()), plain.size());
100+
return true;
101+
}

src/crypto/chacha20poly1305.h

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) 2023 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#ifndef BITCOIN_CRYPTO_CHACHA20POLY1305_H
6+
#define BITCOIN_CRYPTO_CHACHA20POLY1305_H
7+
8+
#include <cstddef>
9+
#include <cstdlib>
10+
#include <stdint.h>
11+
12+
#include <crypto/chacha20.h>
13+
#include <crypto/poly1305.h>
14+
#include <span.h>
15+
16+
/** The AEAD_CHACHA20_POLY1305 authenticated encryption algorithm from RFC8439 section 2.8. */
17+
class AEADChaCha20Poly1305
18+
{
19+
/** Internal stream cipher. */
20+
ChaCha20 m_chacha20;
21+
22+
public:
23+
/** Expected size of key argument in constructor. */
24+
static constexpr unsigned KEYLEN = 32;
25+
26+
/** Expansion when encrypting. */
27+
static constexpr unsigned EXPANSION = Poly1305::TAGLEN;
28+
29+
/** Initialize an AEAD instance with a specified 32-byte key. */
30+
AEADChaCha20Poly1305(Span<const std::byte> key) noexcept;
31+
32+
/** Switch to another 32-byte key. */
33+
void SetKey(Span<const std::byte> key) noexcept;
34+
35+
/** 96-bit nonce type. */
36+
using Nonce96 = ChaCha20::Nonce96;
37+
38+
/** Encrypt a message with a specified 96-bit nonce and aad.
39+
*
40+
* Requires cipher.size() = plain.size() + EXPANSION.
41+
*/
42+
void Encrypt(Span<const std::byte> plain, Span<const std::byte> aad, Nonce96 nonce, Span<std::byte> cipher) noexcept;
43+
44+
/** Decrypt a message with a specified 96-bit nonce and aad. Returns true if valid.
45+
*
46+
* Requires cipher.size() = plain.size() + EXPANSION.
47+
*/
48+
bool Decrypt(Span<const std::byte> cipher, Span<const std::byte> aad, Nonce96 nonce, Span<std::byte> plain) noexcept;
49+
};
50+
51+
#endif // BITCOIN_CRYPTO_CHACHA20POLY1305_H

src/test/crypto_tests.cpp

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include <crypto/aes.h>
66
#include <crypto/chacha20.h>
7+
#include <crypto/chacha20poly1305.h>
78
#include <crypto/hkdf_sha256_32.h>
89
#include <crypto/hmac_sha256.h>
910
#include <crypto/hmac_sha512.h>
@@ -207,6 +208,24 @@ static void TestPoly1305(const std::string &hexmessage, const std::string &hexke
207208
}
208209
}
209210

211+
static void TestChaCha20Poly1305(const std::string& plain_hex, const std::string& aad_hex, const std::string& key_hex, ChaCha20::Nonce96 nonce, const std::string& cipher_hex)
212+
{
213+
auto plain = ParseHex<std::byte>(plain_hex);
214+
auto aad = ParseHex<std::byte>(aad_hex);
215+
auto key = ParseHex<std::byte>(key_hex);
216+
auto expected_cipher = ParseHex<std::byte>(cipher_hex);
217+
218+
std::vector<std::byte> cipher(plain.size() + AEADChaCha20Poly1305::EXPANSION);
219+
AEADChaCha20Poly1305 aead{key};
220+
aead.Encrypt(plain, aad, nonce, cipher);
221+
BOOST_CHECK(cipher == expected_cipher);
222+
223+
std::vector<std::byte> decipher(cipher.size() - AEADChaCha20Poly1305::EXPANSION);
224+
bool ret = aead.Decrypt(cipher, aad, nonce, decipher);
225+
BOOST_CHECK(ret);
226+
BOOST_CHECK(decipher == plain);
227+
}
228+
210229
static void TestHKDF_SHA256_32(const std::string &ikm_hex, const std::string &salt_hex, const std::string &info_hex, const std::string &okm_check_hex) {
211230
std::vector<unsigned char> initial_key_material = ParseHex(ikm_hex);
212231
std::vector<unsigned char> salt = ParseHex(salt_hex);
@@ -818,6 +837,64 @@ BOOST_AUTO_TEST_CASE(poly1305_testvector)
818837
"0e410fa9d7a40ac582e77546be9a72bb");
819838
}
820839

840+
BOOST_AUTO_TEST_CASE(chacha20poly1305_testvectors)
841+
{
842+
// Note that in our implementation, the authentication is suffixed to the ciphertext.
843+
// The RFC test vectors specify them separately.
844+
845+
// RFC 8439 Example from section 2.8.2
846+
TestChaCha20Poly1305("4c616469657320616e642047656e746c656d656e206f662074686520636c6173"
847+
"73206f66202739393a204966204920636f756c64206f6666657220796f75206f"
848+
"6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73"
849+
"637265656e20776f756c642062652069742e",
850+
"50515253c0c1c2c3c4c5c6c7",
851+
"808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f",
852+
{7, 0x4746454443424140},
853+
"d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6"
854+
"3dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b36"
855+
"92ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc"
856+
"3ff4def08e4b7a9de576d26586cec64b61161ae10b594f09e26a7e902ecbd060"
857+
"0691");
858+
859+
// RFC 8439 Test vector A.5
860+
TestChaCha20Poly1305("496e7465726e65742d4472616674732061726520647261667420646f63756d65"
861+
"6e74732076616c696420666f722061206d6178696d756d206f6620736978206d"
862+
"6f6e74687320616e64206d617920626520757064617465642c207265706c6163"
863+
"65642c206f72206f62736f6c65746564206279206f7468657220646f63756d65"
864+
"6e747320617420616e792074696d652e20497420697320696e617070726f7072"
865+
"6961746520746f2075736520496e7465726e65742d4472616674732061732072"
866+
"65666572656e6365206d6174657269616c206f7220746f206369746520746865"
867+
"6d206f74686572207468616e206173202fe2809c776f726b20696e2070726f67"
868+
"726573732e2fe2809d",
869+
"f33388860000000000004e91",
870+
"1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0",
871+
{0, 0x0807060504030201},
872+
"64a0861575861af460f062c79be643bd5e805cfd345cf389f108670ac76c8cb2"
873+
"4c6cfc18755d43eea09ee94e382d26b0bdb7b73c321b0100d4f03b7f355894cf"
874+
"332f830e710b97ce98c8a84abd0b948114ad176e008d33bd60f982b1ff37c855"
875+
"9797a06ef4f0ef61c186324e2b3506383606907b6a7c02b0f9f6157b53c867e4"
876+
"b9166c767b804d46a59b5216cde7a4e99040c5a40433225ee282a1b0a06c523e"
877+
"af4534d7f83fa1155b0047718cbc546a0d072b04b3564eea1b422273f548271a"
878+
"0bb2316053fa76991955ebd63159434ecebb4e466dae5a1073a6727627097a10"
879+
"49e617d91d361094fa68f0ff77987130305beaba2eda04df997b714d6c6f2c29"
880+
"a6ad5cb4022b02709beead9d67890cbb22392336fea1851f38");
881+
882+
// Test vectors exercising aad and plaintext which are multiples of 16 bytes.
883+
TestChaCha20Poly1305("8d2d6a8befd9716fab35819eaac83b33269afb9f1a00fddf66095a6c0cd91951"
884+
"a6b7ad3db580be0674c3f0b55f618e34",
885+
"",
886+
"72ddc73f07101282bbbcf853b9012a9f9695fc5d36b303a97fd0845d0314e0c3",
887+
{0x3432b75f, 0xb3585537eb7f4024},
888+
"f760b8224fb2a317b1b07875092606131232a5b86ae142df5df1c846a7f6341a"
889+
"f2564483dd77f836be45e6230808ffe402a6f0a3e8be074b3d1f4ea8a7b09451");
890+
TestChaCha20Poly1305("",
891+
"36970d8a704c065de16250c18033de5a400520ac1b5842b24551e5823a3314f3"
892+
"946285171e04a81ebfbe3566e312e74ab80e94c7dd2ff4e10de0098a58d0f503",
893+
"77adda51d6730b9ad6c995658cbd49f581b2547e7c0c08fcc24ceec797461021",
894+
{0x1f90da88, 0x75dafa3ef84471a4},
895+
"aaae5bb81e8407c94b2ae86ae0c7efbe");
896+
}
897+
821898
BOOST_AUTO_TEST_CASE(hkdf_hmac_sha256_l32_tests)
822899
{
823900
// Use rfc5869 test vectors but truncated to 32 bytes (our implementation only support length 32)

0 commit comments

Comments
 (0)