Skip to content

Commit 0fee267

Browse files
sipadhruv
andcommitted
crypto: add FSChaCha20, a rekeying wrapper around ChaCha20
This adds the FSChaCha20 stream cipher as specified in BIP324, a wrapper around the ChaCha20 stream cipher (specified in RFC8439 section 2.4) which automatically rekeys every N messages, and manages the nonces used for encryption. Co-authored-by: dhruv <856960+dhruv@users.noreply.github.com>
1 parent 9ff0768 commit 0fee267

File tree

4 files changed

+164
-0
lines changed

4 files changed

+164
-0
lines changed

src/crypto/chacha20.cpp

+41
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#include <crypto/common.h>
99
#include <crypto/chacha20.h>
10+
#include <support/cleanse.h>
1011

1112
#include <algorithm>
1213
#include <string.h>
@@ -42,6 +43,11 @@ ChaCha20Aligned::ChaCha20Aligned()
4243
memset(input, 0, sizeof(input));
4344
}
4445

46+
ChaCha20Aligned::~ChaCha20Aligned()
47+
{
48+
memory_cleanse(input, sizeof(input));
49+
}
50+
4551
ChaCha20Aligned::ChaCha20Aligned(const unsigned char* key32)
4652
{
4753
SetKey32(key32);
@@ -318,3 +324,38 @@ void ChaCha20::Crypt(const unsigned char* m, unsigned char* c, size_t bytes)
318324
m_bufleft = 64 - bytes;
319325
}
320326
}
327+
328+
ChaCha20::~ChaCha20()
329+
{
330+
memory_cleanse(m_buffer, sizeof(m_buffer));
331+
}
332+
333+
FSChaCha20::FSChaCha20(Span<const std::byte> key, uint32_t rekey_interval) noexcept :
334+
m_chacha20(UCharCast(key.data())), m_rekey_interval(rekey_interval)
335+
{
336+
assert(key.size() == KEYLEN);
337+
}
338+
339+
void FSChaCha20::Crypt(Span<const std::byte> input, Span<std::byte> output) noexcept
340+
{
341+
assert(input.size() == output.size());
342+
343+
// Invoke internal stream cipher for actual encryption/decryption.
344+
m_chacha20.Crypt(UCharCast(input.data()), UCharCast(output.data()), input.size());
345+
346+
// Rekey after m_rekey_interval encryptions/decryptions.
347+
if (++m_chunk_counter == m_rekey_interval) {
348+
// Get new key from the stream cipher.
349+
std::byte new_key[KEYLEN];
350+
m_chacha20.Keystream(UCharCast(new_key), sizeof(new_key));
351+
// Update its key.
352+
m_chacha20.SetKey32(UCharCast(new_key));
353+
// Wipe the key (a copy remains inside m_chacha20, where it'll be wiped on the next rekey
354+
// or on destruction).
355+
memory_cleanse(new_key, sizeof(new_key));
356+
// Set the nonce for the new section of output.
357+
m_chacha20.Seek64({0, ++m_rekey_counter}, 0);
358+
// Reset the chunk counter.
359+
m_chunk_counter = 0;
360+
}
361+
}

src/crypto/chacha20.h

+49
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
#ifndef BITCOIN_CRYPTO_CHACHA20_H
66
#define BITCOIN_CRYPTO_CHACHA20_H
77

8+
#include <span.h>
9+
10+
#include <array>
11+
#include <cstddef>
812
#include <cstdlib>
913
#include <stdint.h>
1014
#include <utility>
@@ -29,6 +33,9 @@ class ChaCha20Aligned
2933
/** Initialize a cipher with specified 32-byte key. */
3034
ChaCha20Aligned(const unsigned char* key32);
3135

36+
/** Destructor to clean up private memory. */
37+
~ChaCha20Aligned();
38+
3239
/** set 32-byte key. */
3340
void SetKey32(const unsigned char* key32);
3441

@@ -72,6 +79,9 @@ class ChaCha20
7279
/** Initialize a cipher with specified 32-byte key. */
7380
ChaCha20(const unsigned char* key32) : m_aligned(key32) {}
7481

82+
/** Destructor to clean up private memory. */
83+
~ChaCha20();
84+
7585
/** set 32-byte key. */
7686
void SetKey32(const unsigned char* key32)
7787
{
@@ -98,4 +108,43 @@ class ChaCha20
98108
void Crypt(const unsigned char* input, unsigned char* output, size_t bytes);
99109
};
100110

111+
/** Forward-secure ChaCha20
112+
*
113+
* This implements a stream cipher that automatically transitions to a new stream with a new key
114+
* and new nonce after a predefined number of encryptions or decryptions.
115+
*
116+
* See BIP324 for details.
117+
*/
118+
class FSChaCha20
119+
{
120+
private:
121+
/** Internal stream cipher. */
122+
ChaCha20 m_chacha20;
123+
124+
/** The number of encryptions/decryptions before a rekey happens. */
125+
const uint32_t m_rekey_interval;
126+
127+
/** The number of encryptions/decryptions since the last rekey. */
128+
uint32_t m_chunk_counter{0};
129+
130+
/** The number of rekey operations that have happened. */
131+
uint64_t m_rekey_counter{0};
132+
133+
public:
134+
/** Length of keys expected by the constructor. */
135+
static constexpr unsigned KEYLEN = 32;
136+
137+
// No copy or move to protect the secret.
138+
FSChaCha20(const FSChaCha20&) = delete;
139+
FSChaCha20(FSChaCha20&&) = delete;
140+
FSChaCha20& operator=(const FSChaCha20&) = delete;
141+
FSChaCha20& operator=(FSChaCha20&&) = delete;
142+
143+
/** Construct an FSChaCha20 cipher that rekeys every rekey_interval Crypt() calls. */
144+
FSChaCha20(Span<const std::byte> key, uint32_t rekey_interval) noexcept;
145+
146+
/** Encrypt or decrypt a chunk. */
147+
void Crypt(Span<const std::byte> input, Span<std::byte> output) noexcept;
148+
};
149+
101150
#endif // BITCOIN_CRYPTO_CHACHA20_H

src/test/crypto_tests.cpp

+54
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,46 @@ static void TestChaCha20(const std::string &hex_message, const std::string &hexk
182182
}
183183
}
184184

185+
static void TestFSChaCha20(const std::string& hex_plaintext, const std::string& hexkey, uint32_t rekey_interval, const std::string& ciphertext_after_rotation)
186+
{
187+
auto key = ParseHex<std::byte>(hexkey);
188+
BOOST_CHECK_EQUAL(FSChaCha20::KEYLEN, key.size());
189+
190+
auto plaintext = ParseHex<std::byte>(hex_plaintext);
191+
192+
auto fsc20 = FSChaCha20{key, rekey_interval};
193+
auto c20 = ChaCha20{UCharCast(key.data())};
194+
195+
std::vector<std::byte> fsc20_output;
196+
fsc20_output.resize(plaintext.size());
197+
198+
std::vector<std::byte> c20_output;
199+
c20_output.resize(plaintext.size());
200+
201+
for (size_t i = 0; i < rekey_interval; i++) {
202+
fsc20.Crypt(plaintext, fsc20_output);
203+
c20.Crypt(UCharCast(plaintext.data()), UCharCast(c20_output.data()), plaintext.size());
204+
BOOST_CHECK(c20_output == fsc20_output);
205+
}
206+
207+
// At the rotation interval, the outputs will no longer match
208+
fsc20.Crypt(plaintext, fsc20_output);
209+
auto c20_copy = c20;
210+
c20.Crypt(UCharCast(plaintext.data()), UCharCast(c20_output.data()), plaintext.size());
211+
BOOST_CHECK(c20_output != fsc20_output);
212+
213+
std::byte new_key[FSChaCha20::KEYLEN];
214+
c20_copy.Keystream(UCharCast(new_key), sizeof(new_key));
215+
c20.SetKey32(UCharCast(new_key));
216+
c20.Seek64({0, 1}, 0);
217+
218+
// Outputs should match again after simulating key rotation
219+
c20.Crypt(UCharCast(plaintext.data()), UCharCast(c20_output.data()), plaintext.size());
220+
BOOST_CHECK(c20_output == fsc20_output);
221+
222+
BOOST_CHECK_EQUAL(HexStr(fsc20_output), ciphertext_after_rotation);
223+
}
224+
185225
static void TestPoly1305(const std::string &hexmessage, const std::string &hexkey, const std::string& hextag)
186226
{
187227
auto key = ParseHex<std::byte>(hexkey);
@@ -696,6 +736,20 @@ BOOST_AUTO_TEST_CASE(chacha20_testvector)
696736
"fd565dea5addbdb914208fde7950f23e0385f9a727143f6a6ac51d84b1c0fb3e"
697737
"2e3b00b63d6841a1cc6d1538b1d3a74bef1eb2f54c7b7281e36e484dba89b351"
698738
"c8f572617e61e342879f211b0e4c515df50ea9d0771518fad96cd0baee62deb6");
739+
740+
// Forward secure ChaCha20
741+
TestFSChaCha20("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
742+
"0000000000000000000000000000000000000000000000000000000000000000",
743+
256,
744+
"a93df4ef03011f3db95f60d996e1785df5de38fc39bfcb663a47bb5561928349");
745+
TestFSChaCha20("01",
746+
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
747+
5,
748+
"ea");
749+
TestFSChaCha20("e93fdb5c762804b9a706816aca31e35b11d2aa3080108ef46a5b1f1508819c0a",
750+
"8ec4c3ccdaea336bdeb245636970be01266509b33f3d2642504eaf412206207a",
751+
4096,
752+
"8bfaa4eacff308fdb4a94a5ff25bd9d0c1f84b77f81239f67ff39d6e1ac280c9");
699753
}
700754

701755
BOOST_AUTO_TEST_CASE(chacha20_midblock)

src/test/fuzz/crypto_chacha20.cpp

+20
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
#include <test/fuzz/util.h>
99
#include <test/util/xoroshiro128plusplus.h>
1010

11+
#include <array>
12+
#include <cstddef>
1113
#include <cstdint>
1214
#include <vector>
1315

@@ -151,3 +153,21 @@ FUZZ_TARGET(chacha20_split_keystream)
151153
FuzzedDataProvider provider{buffer.data(), buffer.size()};
152154
ChaCha20SplitFuzz<false>(provider);
153155
}
156+
157+
FUZZ_TARGET(crypto_fschacha20)
158+
{
159+
FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
160+
161+
auto key = fuzzed_data_provider.ConsumeBytes<std::byte>(FSChaCha20::KEYLEN);
162+
key.resize(FSChaCha20::KEYLEN);
163+
164+
auto fsc20 = FSChaCha20{key, fuzzed_data_provider.ConsumeIntegralInRange<uint32_t>(1, 1024)};
165+
166+
LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10000)
167+
{
168+
auto input = fuzzed_data_provider.ConsumeBytes<std::byte>(fuzzed_data_provider.ConsumeIntegralInRange(0, 4096));
169+
std::vector<std::byte> output;
170+
output.resize(input.size());
171+
fsc20.Crypt(input, output);
172+
}
173+
}

0 commit comments

Comments
 (0)