From 0fee267792eb8cbdd48ad78f1712420b5d8d905b Mon Sep 17 00:00:00 2001 From: Pieter Wuille Date: Wed, 28 Jun 2023 18:20:30 -0400 Subject: 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> --- src/crypto/chacha20.cpp | 41 +++++++++++++++++++++++++++++ src/crypto/chacha20.h | 49 +++++++++++++++++++++++++++++++++++ src/test/crypto_tests.cpp | 54 +++++++++++++++++++++++++++++++++++++++ src/test/fuzz/crypto_chacha20.cpp | 20 +++++++++++++++ 4 files changed, 164 insertions(+) diff --git a/src/crypto/chacha20.cpp b/src/crypto/chacha20.cpp index fafd783ab1..469b280494 100644 --- a/src/crypto/chacha20.cpp +++ b/src/crypto/chacha20.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -42,6 +43,11 @@ ChaCha20Aligned::ChaCha20Aligned() memset(input, 0, sizeof(input)); } +ChaCha20Aligned::~ChaCha20Aligned() +{ + memory_cleanse(input, sizeof(input)); +} + ChaCha20Aligned::ChaCha20Aligned(const unsigned char* key32) { SetKey32(key32); @@ -318,3 +324,38 @@ void ChaCha20::Crypt(const unsigned char* m, unsigned char* c, size_t bytes) m_bufleft = 64 - bytes; } } + +ChaCha20::~ChaCha20() +{ + memory_cleanse(m_buffer, sizeof(m_buffer)); +} + +FSChaCha20::FSChaCha20(Span key, uint32_t rekey_interval) noexcept : + m_chacha20(UCharCast(key.data())), m_rekey_interval(rekey_interval) +{ + assert(key.size() == KEYLEN); +} + +void FSChaCha20::Crypt(Span input, Span output) noexcept +{ + assert(input.size() == output.size()); + + // Invoke internal stream cipher for actual encryption/decryption. + m_chacha20.Crypt(UCharCast(input.data()), UCharCast(output.data()), input.size()); + + // Rekey after m_rekey_interval encryptions/decryptions. + if (++m_chunk_counter == m_rekey_interval) { + // Get new key from the stream cipher. + std::byte new_key[KEYLEN]; + m_chacha20.Keystream(UCharCast(new_key), sizeof(new_key)); + // Update its key. + m_chacha20.SetKey32(UCharCast(new_key)); + // Wipe the key (a copy remains inside m_chacha20, where it'll be wiped on the next rekey + // or on destruction). + memory_cleanse(new_key, sizeof(new_key)); + // Set the nonce for the new section of output. + m_chacha20.Seek64({0, ++m_rekey_counter}, 0); + // Reset the chunk counter. + m_chunk_counter = 0; + } +} diff --git a/src/crypto/chacha20.h b/src/crypto/chacha20.h index f2ec21d82e..d1b2094e7e 100644 --- a/src/crypto/chacha20.h +++ b/src/crypto/chacha20.h @@ -5,6 +5,10 @@ #ifndef BITCOIN_CRYPTO_CHACHA20_H #define BITCOIN_CRYPTO_CHACHA20_H +#include + +#include +#include #include #include #include @@ -29,6 +33,9 @@ public: /** Initialize a cipher with specified 32-byte key. */ ChaCha20Aligned(const unsigned char* key32); + /** Destructor to clean up private memory. */ + ~ChaCha20Aligned(); + /** set 32-byte key. */ void SetKey32(const unsigned char* key32); @@ -72,6 +79,9 @@ public: /** Initialize a cipher with specified 32-byte key. */ ChaCha20(const unsigned char* key32) : m_aligned(key32) {} + /** Destructor to clean up private memory. */ + ~ChaCha20(); + /** set 32-byte key. */ void SetKey32(const unsigned char* key32) { @@ -98,4 +108,43 @@ public: void Crypt(const unsigned char* input, unsigned char* output, size_t bytes); }; +/** Forward-secure ChaCha20 + * + * This implements a stream cipher that automatically transitions to a new stream with a new key + * and new nonce after a predefined number of encryptions or decryptions. + * + * See BIP324 for details. + */ +class FSChaCha20 +{ +private: + /** Internal stream cipher. */ + ChaCha20 m_chacha20; + + /** The number of encryptions/decryptions before a rekey happens. */ + const uint32_t m_rekey_interval; + + /** The number of encryptions/decryptions since the last rekey. */ + uint32_t m_chunk_counter{0}; + + /** The number of rekey operations that have happened. */ + uint64_t m_rekey_counter{0}; + +public: + /** Length of keys expected by the constructor. */ + static constexpr unsigned KEYLEN = 32; + + // No copy or move to protect the secret. + FSChaCha20(const FSChaCha20&) = delete; + FSChaCha20(FSChaCha20&&) = delete; + FSChaCha20& operator=(const FSChaCha20&) = delete; + FSChaCha20& operator=(FSChaCha20&&) = delete; + + /** Construct an FSChaCha20 cipher that rekeys every rekey_interval Crypt() calls. */ + FSChaCha20(Span key, uint32_t rekey_interval) noexcept; + + /** Encrypt or decrypt a chunk. */ + void Crypt(Span input, Span output) noexcept; +}; + #endif // BITCOIN_CRYPTO_CHACHA20_H diff --git a/src/test/crypto_tests.cpp b/src/test/crypto_tests.cpp index f62072f5bb..d50596c204 100644 --- a/src/test/crypto_tests.cpp +++ b/src/test/crypto_tests.cpp @@ -182,6 +182,46 @@ static void TestChaCha20(const std::string &hex_message, const std::string &hexk } } +static void TestFSChaCha20(const std::string& hex_plaintext, const std::string& hexkey, uint32_t rekey_interval, const std::string& ciphertext_after_rotation) +{ + auto key = ParseHex(hexkey); + BOOST_CHECK_EQUAL(FSChaCha20::KEYLEN, key.size()); + + auto plaintext = ParseHex(hex_plaintext); + + auto fsc20 = FSChaCha20{key, rekey_interval}; + auto c20 = ChaCha20{UCharCast(key.data())}; + + std::vector fsc20_output; + fsc20_output.resize(plaintext.size()); + + std::vector c20_output; + c20_output.resize(plaintext.size()); + + for (size_t i = 0; i < rekey_interval; i++) { + fsc20.Crypt(plaintext, fsc20_output); + c20.Crypt(UCharCast(plaintext.data()), UCharCast(c20_output.data()), plaintext.size()); + BOOST_CHECK(c20_output == fsc20_output); + } + + // At the rotation interval, the outputs will no longer match + fsc20.Crypt(plaintext, fsc20_output); + auto c20_copy = c20; + c20.Crypt(UCharCast(plaintext.data()), UCharCast(c20_output.data()), plaintext.size()); + BOOST_CHECK(c20_output != fsc20_output); + + std::byte new_key[FSChaCha20::KEYLEN]; + c20_copy.Keystream(UCharCast(new_key), sizeof(new_key)); + c20.SetKey32(UCharCast(new_key)); + c20.Seek64({0, 1}, 0); + + // Outputs should match again after simulating key rotation + c20.Crypt(UCharCast(plaintext.data()), UCharCast(c20_output.data()), plaintext.size()); + BOOST_CHECK(c20_output == fsc20_output); + + BOOST_CHECK_EQUAL(HexStr(fsc20_output), ciphertext_after_rotation); +} + static void TestPoly1305(const std::string &hexmessage, const std::string &hexkey, const std::string& hextag) { auto key = ParseHex(hexkey); @@ -696,6 +736,20 @@ BOOST_AUTO_TEST_CASE(chacha20_testvector) "fd565dea5addbdb914208fde7950f23e0385f9a727143f6a6ac51d84b1c0fb3e" "2e3b00b63d6841a1cc6d1538b1d3a74bef1eb2f54c7b7281e36e484dba89b351" "c8f572617e61e342879f211b0e4c515df50ea9d0771518fad96cd0baee62deb6"); + + // Forward secure ChaCha20 + TestFSChaCha20("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "0000000000000000000000000000000000000000000000000000000000000000", + 256, + "a93df4ef03011f3db95f60d996e1785df5de38fc39bfcb663a47bb5561928349"); + TestFSChaCha20("01", + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + 5, + "ea"); + TestFSChaCha20("e93fdb5c762804b9a706816aca31e35b11d2aa3080108ef46a5b1f1508819c0a", + "8ec4c3ccdaea336bdeb245636970be01266509b33f3d2642504eaf412206207a", + 4096, + "8bfaa4eacff308fdb4a94a5ff25bd9d0c1f84b77f81239f67ff39d6e1ac280c9"); } BOOST_AUTO_TEST_CASE(chacha20_midblock) diff --git a/src/test/fuzz/crypto_chacha20.cpp b/src/test/fuzz/crypto_chacha20.cpp index 63c7bf3b45..76370b4e57 100644 --- a/src/test/fuzz/crypto_chacha20.cpp +++ b/src/test/fuzz/crypto_chacha20.cpp @@ -8,6 +8,8 @@ #include #include +#include +#include #include #include @@ -151,3 +153,21 @@ FUZZ_TARGET(chacha20_split_keystream) FuzzedDataProvider provider{buffer.data(), buffer.size()}; ChaCha20SplitFuzz(provider); } + +FUZZ_TARGET(crypto_fschacha20) +{ + FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()}; + + auto key = fuzzed_data_provider.ConsumeBytes(FSChaCha20::KEYLEN); + key.resize(FSChaCha20::KEYLEN); + + auto fsc20 = FSChaCha20{key, fuzzed_data_provider.ConsumeIntegralInRange(1, 1024)}; + + LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10000) + { + auto input = fuzzed_data_provider.ConsumeBytes(fuzzed_data_provider.ConsumeIntegralInRange(0, 4096)); + std::vector output; + output.resize(input.size()); + fsc20.Crypt(input, output); + } +} -- cgit v1.2.3