Skip to content

Commit 3b7abc2

Browse files
committed
Adding forward secure FSChaCha20
1 parent 5dcb1ef commit 3b7abc2

File tree

4 files changed

+166
-1
lines changed

4 files changed

+166
-1
lines changed

src/crypto/chacha20.cpp

+27-1
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
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+
#include <crypto/sha256.h>
12+
1113
#include <algorithm>
1214
#include <string.h>
1315

@@ -337,3 +339,27 @@ void ChaCha20::Crypt(const unsigned char* m, unsigned char* c, size_t bytes)
337339
m_bufleft = 64 - bytes;
338340
}
339341
}
342+
343+
void FSChaCha20::Crypt(Span<const std::byte> input, Span<std::byte> output)
344+
{
345+
assert(input.size() == output.size());
346+
c20.Crypt(reinterpret_cast<const unsigned char*>(input.data()),
347+
reinterpret_cast<unsigned char*>(output.data()), input.size());
348+
chunk_counter++;
349+
350+
if (chunk_counter % rekey_interval == 0) {
351+
CommitToKey({(std::byte*)nullptr, 0});
352+
}
353+
}
354+
355+
void FSChaCha20::CommitToKey(const Span<const std::byte> data)
356+
{
357+
assert(CSHA256::OUTPUT_SIZE == FSCHACHA20_KEYLEN);
358+
auto hasher = rekey_hasher;
359+
hasher << MakeUCharSpan(data) << MakeUCharSpan(key);
360+
auto new_key = hasher.GetSHA256();
361+
memory_cleanse(key.data(), key.size());
362+
memcpy(key.data(), new_key.data(), FSCHACHA20_KEYLEN);
363+
c20.SetKey32(reinterpret_cast<unsigned char*>(key.data()));
364+
set_nonce();
365+
}

src/crypto/chacha20.h

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

8+
#include <crypto/common.h>
9+
#include <hash.h>
10+
#include <span.h>
11+
#include <support/cleanse.h>
12+
813
#include <array>
914
#include <cstddef>
1015
#include <cstdlib>
1116
#include <stdint.h>
1217

18+
static constexpr size_t FSCHACHA20_KEYLEN = 32; // bytes
19+
static constexpr size_t FSCHACHA20_REKEY_SALT_LEN = 23; // bytes
20+
1321
// classes for ChaCha20 256-bit stream cipher developed by Daniel J. Bernstein
1422
// https://cr.yp.to/chacha/chacha-20080128.pdf */
1523

@@ -97,4 +105,44 @@ class ChaCha20
97105
void Crypt(const unsigned char* input, unsigned char* output, size_t bytes);
98106
};
99107

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

src/test/crypto_tests.cpp

+63
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,57 @@ 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, const std::string& hex_rekey_salt, 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 salt_vec = ParseHex(hex_rekey_salt);
178+
BOOST_CHECK_EQUAL(FSCHACHA20_REKEY_SALT_LEN, salt_vec.size());
179+
std::array<std::byte, FSCHACHA20_REKEY_SALT_LEN> rekey_salt;
180+
memcpy(rekey_salt.data(), salt_vec.data(), FSCHACHA20_REKEY_SALT_LEN);
181+
182+
auto plaintext = ParseHex(hex_plaintext);
183+
184+
auto fsc20 = FSChaCha20{key, rekey_salt, rekey_interval};
185+
auto c20 = ChaCha20{reinterpret_cast<const unsigned char*>(key.data())};
186+
187+
std::vector<std::byte> fsc20_output;
188+
fsc20_output.resize(plaintext.size());
189+
190+
std::vector<unsigned char> c20_output;
191+
c20_output.resize(plaintext.size());
192+
193+
for (size_t i = 0; i < rekey_interval; i++) {
194+
fsc20.Crypt(MakeByteSpan(plaintext), fsc20_output);
195+
c20.Crypt(plaintext.data(), c20_output.data(), plaintext.size());
196+
BOOST_CHECK_EQUAL(0, memcmp(c20_output.data(), fsc20_output.data(), plaintext.size()));
197+
}
198+
199+
// At the rotation interval, the outputs will no longer match
200+
fsc20.Crypt(MakeByteSpan(plaintext), fsc20_output);
201+
c20.Crypt(plaintext.data(), c20_output.data(), plaintext.size());
202+
BOOST_CHECK(memcmp(c20_output.data(), fsc20_output.data(), plaintext.size()) != 0);
203+
204+
unsigned char new_key[FSCHACHA20_KEYLEN];
205+
auto hasher = CSHA256().Write(UCharCast(rekey_salt.data()), rekey_salt.size());
206+
hasher.Write(UCharCast(key.data()), key.size()).Finalize(new_key);
207+
c20.SetKey32(reinterpret_cast<unsigned char*>(new_key));
208+
209+
std::array<std::byte, 12> new_nonce;
210+
WriteLE32(reinterpret_cast<unsigned char*>(new_nonce.data()), 0);
211+
WriteLE64(reinterpret_cast<unsigned char*>(new_nonce.data()) + 4, 1);
212+
c20.SetRFC8439Nonce(new_nonce);
213+
214+
// Outputs should match again after simulating key rotation
215+
c20.Crypt(plaintext.data(), c20_output.data(), plaintext.size());
216+
BOOST_CHECK_EQUAL(0, memcmp(c20_output.data(), fsc20_output.data(), plaintext.size()));
217+
218+
BOOST_CHECK_EQUAL(HexStr(fsc20_output), ciphertext_after_rotation);
219+
}
220+
170221
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)
171222
{
172223
auto key = ParseHex(hex_key);
@@ -658,6 +709,18 @@ BOOST_AUTO_TEST_CASE(chacha20_testvector)
658709
rfc8439_nonce2, 0, "ecfa254f845f647473d3cb140da9e87606cb33066c447b87bc2666dde3fbb739", "", "");
659710
TestChaCha20RFC8439("1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0",
660711
rfc8439_nonce2, 0, "965e3bc6f9ec7ed9560808f4d229f94b137ff275ca9b3fcbdd59deaad23310ae", "", "");
712+
713+
// Forward secure ChaCha20
714+
TestFSChaCha20("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
715+
"0000000000000000000000000000000000000000000000000000000000000000",
716+
"0000000000000000000000000000000000000000000000", 256,
717+
"4cf63894c8507adffa163a742db5fdcc9a861187b1c94a5a4c002d1bb7f2223c");
718+
TestFSChaCha20("01", "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
719+
"0000000000000000000000000000000000000000000000", 5, "e0");
720+
TestFSChaCha20("e93fdb5c762804b9a706816aca31e35b11d2aa3080108ef46a5b1f1508819c0a",
721+
"8ec4c3ccdaea336bdeb245636970be01266509b33f3d2642504eaf412206207a",
722+
"8bb571662db12d38ee4e2630d4434f6f626cb0e6007e3c", 4096,
723+
"30a36b4833331bf83bc16fcff408c771044e239b80472edd2e89ba9eb1845f34");
661724
}
662725

663726
BOOST_AUTO_TEST_CASE(chacha20_midblock)

src/test/fuzz/crypto_chacha20.cpp

+28
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,29 @@ 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+
std::array<std::byte, FSCHACHA20_REKEY_SALT_LEN> salt;
169+
memcpy(salt.data(), salt_vec.data(), FSCHACHA20_REKEY_SALT_LEN);
170+
171+
auto fsc20 = FSChaCha20{key, salt, fuzzed_data_provider.ConsumeIntegralInRange<size_t>(1, 1024)};
172+
173+
LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10000)
174+
{
175+
auto input = fuzzed_data_provider.ConsumeBytes<std::byte>(fuzzed_data_provider.ConsumeIntegralInRange(0, 4096));
176+
std::vector<std::byte> output;
177+
output.resize(input.size());
178+
fsc20.Crypt(input, output);
179+
}
180+
}

0 commit comments

Comments
 (0)