Skip to content

Commit afe58e4

Browse files
committed
Adding forward secure FSChaCha20
1 parent efad5d7 commit afe58e4

File tree

4 files changed

+142
-1
lines changed

4 files changed

+142
-1
lines changed

src/crypto/chacha20.cpp

+16-1
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
// Based on the public domain implementation 'merged' by D. J. Bernstein
66
// See https://cr.yp.to/chacha.html.
77

8-
#include <crypto/common.h>
98
#include <crypto/chacha20.h>
109

10+
#include <crypto/common.h>
11+
1112
#include <algorithm>
1213
#include <string.h>
1314

@@ -337,3 +338,17 @@ void ChaCha20::Crypt(const unsigned char* m, unsigned char* c, size_t bytes)
337338
m_bufleft = 64 - bytes;
338339
}
339340
}
341+
342+
void FSChaCha20::Crypt(Span<const std::byte> input, Span<std::byte> output)
343+
{
344+
assert(input.size() == output.size());
345+
c20.Crypt(reinterpret_cast<const unsigned char*>(input.data()),
346+
reinterpret_cast<unsigned char*>(output.data()), input.size());
347+
chunk_counter++;
348+
349+
if (chunk_counter % rekey_interval == 0) {
350+
c20.Keystream(reinterpret_cast<unsigned char*>(key.data()), FSCHACHA20_KEYLEN);
351+
c20.SetKey32(reinterpret_cast<unsigned char*>(key.data()));
352+
set_nonce();
353+
}
354+
}

src/crypto/chacha20.h

+41
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@
55
#ifndef BITCOIN_CRYPTO_CHACHA20_H
66
#define BITCOIN_CRYPTO_CHACHA20_H
77

8+
#include <crypto/common.h>
9+
#include <span.h>
10+
#include <support/cleanse.h>
11+
812
#include <array>
913
#include <cstddef>
1014
#include <cstdlib>
1115
#include <stdint.h>
1216

17+
static constexpr size_t FSCHACHA20_KEYLEN = 32; // bytes
18+
1319
// classes for ChaCha20 256-bit stream cipher developed by Daniel J. Bernstein
1420
// https://cr.yp.to/chacha/chacha-20080128.pdf */
1521

@@ -97,4 +103,39 @@ class ChaCha20
97103
void Crypt(const unsigned char* input, unsigned char* output, size_t bytes);
98104
};
99105

106+
class FSChaCha20
107+
{
108+
private:
109+
ChaCha20 c20;
110+
size_t rekey_interval;
111+
uint32_t chunk_counter{0};
112+
std::array<std::byte, FSCHACHA20_KEYLEN> key;
113+
114+
void set_nonce()
115+
{
116+
std::array<std::byte, 12> nonce;
117+
WriteLE32(reinterpret_cast<unsigned char*>(nonce.data()), uint32_t{0});
118+
WriteLE64(reinterpret_cast<unsigned char*>(nonce.data()) + 4, chunk_counter / rekey_interval);
119+
c20.SetRFC8439Nonce(nonce);
120+
}
121+
122+
public:
123+
FSChaCha20() = delete;
124+
FSChaCha20(const std::array<std::byte, FSCHACHA20_KEYLEN>& key,
125+
size_t rekey_interval)
126+
: c20{reinterpret_cast<const unsigned char*>(key.data())},
127+
rekey_interval{rekey_interval},
128+
key{key}
129+
{
130+
assert(rekey_interval > 0);
131+
set_nonce();
132+
}
133+
134+
~FSChaCha20()
135+
{
136+
memory_cleanse(key.data(), FSCHACHA20_KEYLEN);
137+
}
138+
139+
void Crypt(Span<const std::byte> input, Span<std::byte> output);
140+
};
100141
#endif // BITCOIN_CRYPTO_CHACHA20_H

src/test/crypto_tests.cpp

+60
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,52 @@ static void TestChaCha20(const std::string &hex_message, const std::string &hexk
167167
}
168168
}
169169

170+
static void TestFSChaCha20(const std::string& hex_plaintext, const std::string& hexkey, size_t rekey_interval, const std::string& ciphertext_after_rotation)
171+
{
172+
auto key_vec = ParseHex(hexkey);
173+
BOOST_CHECK_EQUAL(FSCHACHA20_KEYLEN, key_vec.size());
174+
std::array<std::byte, FSCHACHA20_KEYLEN> key;
175+
memcpy(key.data(), key_vec.data(), FSCHACHA20_KEYLEN);
176+
177+
auto plaintext = ParseHex(hex_plaintext);
178+
179+
auto fsc20 = FSChaCha20{key, rekey_interval};
180+
auto c20 = ChaCha20{reinterpret_cast<const unsigned char*>(key.data())};
181+
182+
std::vector<std::byte> fsc20_output;
183+
fsc20_output.resize(plaintext.size());
184+
185+
std::vector<unsigned char> c20_output;
186+
c20_output.resize(plaintext.size());
187+
188+
for (size_t i = 0; i < rekey_interval; i++) {
189+
fsc20.Crypt(MakeByteSpan(plaintext), fsc20_output);
190+
c20.Crypt(plaintext.data(), c20_output.data(), plaintext.size());
191+
BOOST_CHECK_EQUAL(0, memcmp(c20_output.data(), fsc20_output.data(), plaintext.size()));
192+
}
193+
194+
// At the rotation interval, the outputs will no longer match
195+
fsc20.Crypt(MakeByteSpan(plaintext), fsc20_output);
196+
auto c20_copy = c20;
197+
c20.Crypt(plaintext.data(), c20_output.data(), plaintext.size());
198+
BOOST_CHECK(memcmp(c20_output.data(), fsc20_output.data(), plaintext.size()) != 0);
199+
200+
unsigned char new_key[FSCHACHA20_KEYLEN];
201+
c20_copy.Keystream(new_key, FSCHACHA20_KEYLEN);
202+
c20.SetKey32(new_key);
203+
204+
std::array<std::byte, 12> new_nonce;
205+
WriteLE32(reinterpret_cast<unsigned char*>(new_nonce.data()), 0);
206+
WriteLE64(reinterpret_cast<unsigned char*>(new_nonce.data()) + 4, 1);
207+
c20.SetRFC8439Nonce(new_nonce);
208+
209+
// Outputs should match again after simulating key rotation
210+
c20.Crypt(plaintext.data(), c20_output.data(), plaintext.size());
211+
BOOST_CHECK_EQUAL(0, memcmp(c20_output.data(), fsc20_output.data(), plaintext.size()));
212+
213+
BOOST_CHECK_EQUAL(HexStr(fsc20_output), ciphertext_after_rotation);
214+
}
215+
170216
static void TestChaCha20RFC8439(const std::string& hex_key, const std::array<std::byte, 12>& nonce, uint32_t seek, const std::string& hex_expected_keystream, const std::string& hex_input, const std::string& hex_expected_output)
171217
{
172218
auto key = ParseHex(hex_key);
@@ -658,6 +704,20 @@ BOOST_AUTO_TEST_CASE(chacha20_testvector)
658704
rfc8439_nonce2, 0, "ecfa254f845f647473d3cb140da9e87606cb33066c447b87bc2666dde3fbb739", "", "");
659705
TestChaCha20RFC8439("1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0",
660706
rfc8439_nonce2, 0, "965e3bc6f9ec7ed9560808f4d229f94b137ff275ca9b3fcbdd59deaad23310ae", "", "");
707+
708+
// Forward secure ChaCha20
709+
TestFSChaCha20("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
710+
"0000000000000000000000000000000000000000000000000000000000000000",
711+
256,
712+
"a93df4ef03011f3db95f60d996e1785df5de38fc39bfcb663a47bb5561928349");
713+
TestFSChaCha20("01",
714+
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
715+
5,
716+
"ea");
717+
TestFSChaCha20("e93fdb5c762804b9a706816aca31e35b11d2aa3080108ef46a5b1f1508819c0a",
718+
"8ec4c3ccdaea336bdeb245636970be01266509b33f3d2642504eaf412206207a",
719+
4096,
720+
"8bfaa4eacff308fdb4a94a5ff25bd9d0c1f84b77f81239f67ff39d6e1ac280c9");
661721
}
662722

663723
BOOST_AUTO_TEST_CASE(chacha20_midblock)

src/test/fuzz/crypto_chacha20.cpp

+25
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

@@ -150,3 +152,26 @@ FUZZ_TARGET(chacha20_split_keystream)
150152
FuzzedDataProvider provider{buffer.data(), buffer.size()};
151153
ChaCha20SplitFuzz<false>(provider);
152154
}
155+
156+
FUZZ_TARGET(crypto_fschacha20)
157+
{
158+
FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
159+
160+
auto key_vec = ConsumeFixedLengthByteVector(fuzzed_data_provider, FSCHACHA20_KEYLEN);
161+
key_vec.resize(FSCHACHA20_KEYLEN);
162+
auto salt_vec = ConsumeFixedLengthByteVector(fuzzed_data_provider, FSCHACHA20_KEYLEN);
163+
salt_vec.resize(FSCHACHA20_KEYLEN);
164+
165+
std::array<std::byte, FSCHACHA20_KEYLEN> key;
166+
memcpy(key.data(), key_vec.data(), FSCHACHA20_KEYLEN);
167+
168+
auto fsc20 = FSChaCha20{key, fuzzed_data_provider.ConsumeIntegralInRange<size_t>(1, 1024)};
169+
170+
LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10000)
171+
{
172+
auto input = fuzzed_data_provider.ConsumeBytes<std::byte>(fuzzed_data_provider.ConsumeIntegralInRange(0, 4096));
173+
std::vector<std::byte> output;
174+
output.resize(input.size());
175+
fsc20.Crypt(input, output);
176+
}
177+
}

0 commit comments

Comments
 (0)