From 4d4edef8e8ef2f6f02c9fe730e7cff21beaaa6d8 Mon Sep 17 00:00:00 2001 From: div72 Date: Wed, 14 Feb 2024 18:29:51 +0300 Subject: [PATCH 1/6] span: add some UCharCast overloads and use reinterpret_cast --- src/span.h | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/span.h b/src/span.h index f274b0b625..72473239ad 100644 --- a/src/span.h +++ b/src/span.h @@ -272,12 +272,14 @@ Span MakeWritableByteSpan(V&& v) noexcept } // Helper functions to safely cast to unsigned char pointers. -inline unsigned char* UCharCast(char* c) { return (unsigned char*)c; } +inline unsigned char* UCharCast(char* c) { return reinterpret_cast(c); } inline unsigned char* UCharCast(unsigned char* c) { return c; } -inline const unsigned char* UCharCast(const char* c) { return (unsigned char*)c; } +inline unsigned char* UCharCast(signed char* c) { return reinterpret_cast(c); } +inline unsigned char* UCharCast(std::byte* c) { return reinterpret_cast(c); } +inline const unsigned char* UCharCast(const char* c) { return reinterpret_cast(c); } inline const unsigned char* UCharCast(const unsigned char* c) { return c; } +inline const unsigned char* UCharCast(const signed char* c) { return reinterpret_cast(c); } inline const unsigned char* UCharCast(const std::byte* c) { return reinterpret_cast(c); } - // Helper function to safely convert a Span to a Span<[const] unsigned char>. template constexpr auto UCharSpanCast(Span s) -> Span::type> { return {UCharCast(s.data()), s.size()}; } From d02b40112410f4959dc848cf2fc320a2310502da Mon Sep 17 00:00:00 2001 From: div72 Date: Wed, 14 Feb 2024 18:34:25 +0300 Subject: [PATCH 2/6] crypto: port upstream changes for poly1305 --- src/crypto/poly1305.cpp | 326 ++++++++++++++++++++++++-------------- src/crypto/poly1305.h | 65 +++++++- src/test/crypto_tests.cpp | 79 ++++++++- 3 files changed, 335 insertions(+), 135 deletions(-) diff --git a/src/crypto/poly1305.cpp b/src/crypto/poly1305.cpp index fae6e1c9bc..d464eb1360 100644 --- a/src/crypto/poly1305.cpp +++ b/src/crypto/poly1305.cpp @@ -2,140 +2,222 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or https://opensource.org/licenses/mit-license.php. -// Based on the public domain implementation by Andrew Moon -// poly1305-donna-unrolled.c from https://github.com/floodyberry/poly1305-donna - #include #include #include -#define mul32x32_64(a,b) ((uint64_t)(a) * (b)) +namespace poly1305_donna { -void poly1305_auth(unsigned char out[POLY1305_TAGLEN], const unsigned char *m, size_t inlen, const unsigned char key[POLY1305_KEYLEN]) { - uint32_t t0,t1,t2,t3; - uint32_t h0,h1,h2,h3,h4; +// Based on the public domain implementation by Andrew Moon +// poly1305-donna-32.h from https://github.com/floodyberry/poly1305-donna + +void poly1305_init(poly1305_context *st, const unsigned char key[32]) noexcept { + /* r &= 0xffffffc0ffffffc0ffffffc0fffffff */ + st->r[0] = (ReadLE32(&key[ 0]) ) & 0x3ffffff; + st->r[1] = (ReadLE32(&key[ 3]) >> 2) & 0x3ffff03; + st->r[2] = (ReadLE32(&key[ 6]) >> 4) & 0x3ffc0ff; + st->r[3] = (ReadLE32(&key[ 9]) >> 6) & 0x3f03fff; + st->r[4] = (ReadLE32(&key[12]) >> 8) & 0x00fffff; + + /* h = 0 */ + st->h[0] = 0; + st->h[1] = 0; + st->h[2] = 0; + st->h[3] = 0; + st->h[4] = 0; + + /* save pad for later */ + st->pad[0] = ReadLE32(&key[16]); + st->pad[1] = ReadLE32(&key[20]); + st->pad[2] = ReadLE32(&key[24]); + st->pad[3] = ReadLE32(&key[28]); + + st->leftover = 0; + st->final = 0; +} + +static void poly1305_blocks(poly1305_context *st, const unsigned char *m, size_t bytes) noexcept { + const uint32_t hibit = (st->final) ? 0 : (1UL << 24); /* 1 << 128 */ uint32_t r0,r1,r2,r3,r4; uint32_t s1,s2,s3,s4; - uint32_t b, nb; - size_t j; - uint64_t t[5]; - uint64_t f0,f1,f2,f3; - uint64_t g0,g1,g2,g3,g4; - uint64_t c; - unsigned char mp[16]; - - /* clamp key */ - t0 = ReadLE32(key+0); - t1 = ReadLE32(key+4); - t2 = ReadLE32(key+8); - t3 = ReadLE32(key+12); - - /* precompute multipliers */ - r0 = t0 & 0x3ffffff; t0 >>= 26; t0 |= t1 << 6; - r1 = t0 & 0x3ffff03; t1 >>= 20; t1 |= t2 << 12; - r2 = t1 & 0x3ffc0ff; t2 >>= 14; t2 |= t3 << 18; - r3 = t2 & 0x3f03fff; t3 >>= 8; - r4 = t3 & 0x00fffff; + uint32_t h0,h1,h2,h3,h4; + uint64_t d0,d1,d2,d3,d4; + uint32_t c; + + r0 = st->r[0]; + r1 = st->r[1]; + r2 = st->r[2]; + r3 = st->r[3]; + r4 = st->r[4]; s1 = r1 * 5; s2 = r2 * 5; s3 = r3 * 5; s4 = r4 * 5; - /* init state */ - h0 = 0; - h1 = 0; - h2 = 0; - h3 = 0; - h4 = 0; - - /* full blocks */ - if (inlen < 16) goto poly1305_donna_atmost15bytes; -poly1305_donna_16bytes: - m += 16; - inlen -= 16; - - t0 = ReadLE32(m-16); - t1 = ReadLE32(m-12); - t2 = ReadLE32(m-8); - t3 = ReadLE32(m-4); - - h0 += t0 & 0x3ffffff; - h1 += ((((uint64_t)t1 << 32) | t0) >> 26) & 0x3ffffff; - h2 += ((((uint64_t)t2 << 32) | t1) >> 20) & 0x3ffffff; - h3 += ((((uint64_t)t3 << 32) | t2) >> 14) & 0x3ffffff; - h4 += (t3 >> 8) | (1 << 24); - - -poly1305_donna_mul: - t[0] = mul32x32_64(h0,r0) + mul32x32_64(h1,s4) + mul32x32_64(h2,s3) + mul32x32_64(h3,s2) + mul32x32_64(h4,s1); - t[1] = mul32x32_64(h0,r1) + mul32x32_64(h1,r0) + mul32x32_64(h2,s4) + mul32x32_64(h3,s3) + mul32x32_64(h4,s2); - t[2] = mul32x32_64(h0,r2) + mul32x32_64(h1,r1) + mul32x32_64(h2,r0) + mul32x32_64(h3,s4) + mul32x32_64(h4,s3); - t[3] = mul32x32_64(h0,r3) + mul32x32_64(h1,r2) + mul32x32_64(h2,r1) + mul32x32_64(h3,r0) + mul32x32_64(h4,s4); - t[4] = mul32x32_64(h0,r4) + mul32x32_64(h1,r3) + mul32x32_64(h2,r2) + mul32x32_64(h3,r1) + mul32x32_64(h4,r0); - - h0 = (uint32_t)t[0] & 0x3ffffff; c = (t[0] >> 26); - t[1] += c; h1 = (uint32_t)t[1] & 0x3ffffff; b = (uint32_t)(t[1] >> 26); - t[2] += b; h2 = (uint32_t)t[2] & 0x3ffffff; b = (uint32_t)(t[2] >> 26); - t[3] += b; h3 = (uint32_t)t[3] & 0x3ffffff; b = (uint32_t)(t[3] >> 26); - t[4] += b; h4 = (uint32_t)t[4] & 0x3ffffff; b = (uint32_t)(t[4] >> 26); - h0 += b * 5; - - if (inlen >= 16) goto poly1305_donna_16bytes; - - /* final bytes */ -poly1305_donna_atmost15bytes: - if (!inlen) goto poly1305_donna_finish; - - for (j = 0; j < inlen; j++) mp[j] = m[j]; - mp[j++] = 1; - for (; j < 16; j++) mp[j] = 0; - inlen = 0; - - t0 = ReadLE32(mp+0); - t1 = ReadLE32(mp+4); - t2 = ReadLE32(mp+8); - t3 = ReadLE32(mp+12); - - h0 += t0 & 0x3ffffff; - h1 += ((((uint64_t)t1 << 32) | t0) >> 26) & 0x3ffffff; - h2 += ((((uint64_t)t2 << 32) | t1) >> 20) & 0x3ffffff; - h3 += ((((uint64_t)t3 << 32) | t2) >> 14) & 0x3ffffff; - h4 += (t3 >> 8); - - goto poly1305_donna_mul; - -poly1305_donna_finish: - b = h0 >> 26; h0 = h0 & 0x3ffffff; - h1 += b; b = h1 >> 26; h1 = h1 & 0x3ffffff; - h2 += b; b = h2 >> 26; h2 = h2 & 0x3ffffff; - h3 += b; b = h3 >> 26; h3 = h3 & 0x3ffffff; - h4 += b; b = h4 >> 26; h4 = h4 & 0x3ffffff; - h0 += b * 5; b = h0 >> 26; h0 = h0 & 0x3ffffff; - h1 += b; - - g0 = h0 + 5; b = g0 >> 26; g0 &= 0x3ffffff; - g1 = h1 + b; b = g1 >> 26; g1 &= 0x3ffffff; - g2 = h2 + b; b = g2 >> 26; g2 &= 0x3ffffff; - g3 = h3 + b; b = g3 >> 26; g3 &= 0x3ffffff; - g4 = h4 + b - (1 << 26); - - b = (g4 >> 31) - 1; - nb = ~b; - h0 = (h0 & nb) | (g0 & b); - h1 = (h1 & nb) | (g1 & b); - h2 = (h2 & nb) | (g2 & b); - h3 = (h3 & nb) | (g3 & b); - h4 = (h4 & nb) | (g4 & b); - - f0 = ((h0 ) | (h1 << 26)) + (uint64_t)ReadLE32(&key[16]); - f1 = ((h1 >> 6) | (h2 << 20)) + (uint64_t)ReadLE32(&key[20]); - f2 = ((h2 >> 12) | (h3 << 14)) + (uint64_t)ReadLE32(&key[24]); - f3 = ((h3 >> 18) | (h4 << 8)) + (uint64_t)ReadLE32(&key[28]); - - WriteLE32(&out[ 0], f0); f1 += (f0 >> 32); - WriteLE32(&out[ 4], f1); f2 += (f1 >> 32); - WriteLE32(&out[ 8], f2); f3 += (f2 >> 32); - WriteLE32(&out[12], f3); + h0 = st->h[0]; + h1 = st->h[1]; + h2 = st->h[2]; + h3 = st->h[3]; + h4 = st->h[4]; + + while (bytes >= POLY1305_BLOCK_SIZE) { + /* h += m[i] */ + h0 += (ReadLE32(m+ 0) ) & 0x3ffffff; + h1 += (ReadLE32(m+ 3) >> 2) & 0x3ffffff; + h2 += (ReadLE32(m+ 6) >> 4) & 0x3ffffff; + h3 += (ReadLE32(m+ 9) >> 6) & 0x3ffffff; + h4 += (ReadLE32(m+12) >> 8) | hibit; + + /* h *= r */ + d0 = ((uint64_t)h0 * r0) + ((uint64_t)h1 * s4) + ((uint64_t)h2 * s3) + ((uint64_t)h3 * s2) + ((uint64_t)h4 * s1); + d1 = ((uint64_t)h0 * r1) + ((uint64_t)h1 * r0) + ((uint64_t)h2 * s4) + ((uint64_t)h3 * s3) + ((uint64_t)h4 * s2); + d2 = ((uint64_t)h0 * r2) + ((uint64_t)h1 * r1) + ((uint64_t)h2 * r0) + ((uint64_t)h3 * s4) + ((uint64_t)h4 * s3); + d3 = ((uint64_t)h0 * r3) + ((uint64_t)h1 * r2) + ((uint64_t)h2 * r1) + ((uint64_t)h3 * r0) + ((uint64_t)h4 * s4); + d4 = ((uint64_t)h0 * r4) + ((uint64_t)h1 * r3) + ((uint64_t)h2 * r2) + ((uint64_t)h3 * r1) + ((uint64_t)h4 * r0); + + /* (partial) h %= p */ + c = (uint32_t)(d0 >> 26); h0 = (uint32_t)d0 & 0x3ffffff; + d1 += c; c = (uint32_t)(d1 >> 26); h1 = (uint32_t)d1 & 0x3ffffff; + d2 += c; c = (uint32_t)(d2 >> 26); h2 = (uint32_t)d2 & 0x3ffffff; + d3 += c; c = (uint32_t)(d3 >> 26); h3 = (uint32_t)d3 & 0x3ffffff; + d4 += c; c = (uint32_t)(d4 >> 26); h4 = (uint32_t)d4 & 0x3ffffff; + h0 += c * 5; c = (h0 >> 26); h0 = h0 & 0x3ffffff; + h1 += c; + + m += POLY1305_BLOCK_SIZE; + bytes -= POLY1305_BLOCK_SIZE; + } + + st->h[0] = h0; + st->h[1] = h1; + st->h[2] = h2; + st->h[3] = h3; + st->h[4] = h4; +} + +void poly1305_finish(poly1305_context *st, unsigned char mac[16]) noexcept { + uint32_t h0,h1,h2,h3,h4,c; + uint32_t g0,g1,g2,g3,g4; + uint64_t f; + uint32_t mask; + + /* process the remaining block */ + if (st->leftover) { + size_t i = st->leftover; + st->buffer[i++] = 1; + for (; i < POLY1305_BLOCK_SIZE; i++) { + st->buffer[i] = 0; + } + st->final = 1; + poly1305_blocks(st, st->buffer, POLY1305_BLOCK_SIZE); + } + + /* fully carry h */ + h0 = st->h[0]; + h1 = st->h[1]; + h2 = st->h[2]; + h3 = st->h[3]; + h4 = st->h[4]; + + c = h1 >> 26; h1 = h1 & 0x3ffffff; + h2 += c; c = h2 >> 26; h2 = h2 & 0x3ffffff; + h3 += c; c = h3 >> 26; h3 = h3 & 0x3ffffff; + h4 += c; c = h4 >> 26; h4 = h4 & 0x3ffffff; + h0 += c * 5; c = h0 >> 26; h0 = h0 & 0x3ffffff; + h1 += c; + + /* compute h + -p */ + g0 = h0 + 5; c = g0 >> 26; g0 &= 0x3ffffff; + g1 = h1 + c; c = g1 >> 26; g1 &= 0x3ffffff; + g2 = h2 + c; c = g2 >> 26; g2 &= 0x3ffffff; + g3 = h3 + c; c = g3 >> 26; g3 &= 0x3ffffff; + g4 = h4 + c - (1UL << 26); + + /* select h if h < p, or h + -p if h >= p */ + mask = (g4 >> ((sizeof(uint32_t) * 8) - 1)) - 1; + g0 &= mask; + g1 &= mask; + g2 &= mask; + g3 &= mask; + g4 &= mask; + mask = ~mask; + h0 = (h0 & mask) | g0; + h1 = (h1 & mask) | g1; + h2 = (h2 & mask) | g2; + h3 = (h3 & mask) | g3; + h4 = (h4 & mask) | g4; + + /* h = h % (2^128) */ + h0 = ((h0 ) | (h1 << 26)) & 0xffffffff; + h1 = ((h1 >> 6) | (h2 << 20)) & 0xffffffff; + h2 = ((h2 >> 12) | (h3 << 14)) & 0xffffffff; + h3 = ((h3 >> 18) | (h4 << 8)) & 0xffffffff; + + /* mac = (h + pad) % (2^128) */ + f = (uint64_t)h0 + st->pad[0] ; h0 = (uint32_t)f; + f = (uint64_t)h1 + st->pad[1] + (f >> 32); h1 = (uint32_t)f; + f = (uint64_t)h2 + st->pad[2] + (f >> 32); h2 = (uint32_t)f; + f = (uint64_t)h3 + st->pad[3] + (f >> 32); h3 = (uint32_t)f; + + WriteLE32(mac + 0, h0); + WriteLE32(mac + 4, h1); + WriteLE32(mac + 8, h2); + WriteLE32(mac + 12, h3); + + /* zero out the state */ + st->h[0] = 0; + st->h[1] = 0; + st->h[2] = 0; + st->h[3] = 0; + st->h[4] = 0; + st->r[0] = 0; + st->r[1] = 0; + st->r[2] = 0; + st->r[3] = 0; + st->r[4] = 0; + st->pad[0] = 0; + st->pad[1] = 0; + st->pad[2] = 0; + st->pad[3] = 0; } + +void poly1305_update(poly1305_context *st, const unsigned char *m, size_t bytes) noexcept { + size_t i; + + /* handle leftover */ + if (st->leftover) { + size_t want = (POLY1305_BLOCK_SIZE - st->leftover); + if (want > bytes) { + want = bytes; + } + for (i = 0; i < want; i++) { + st->buffer[st->leftover + i] = m[i]; + } + bytes -= want; + m += want; + st->leftover += want; + if (st->leftover < POLY1305_BLOCK_SIZE) return; + poly1305_blocks(st, st->buffer, POLY1305_BLOCK_SIZE); + st->leftover = 0; + } + + /* process full blocks */ + if (bytes >= POLY1305_BLOCK_SIZE) { + size_t want = (bytes & ~(POLY1305_BLOCK_SIZE - 1)); + poly1305_blocks(st, m, want); + m += want; + bytes -= want; + } + + /* store leftover */ + if (bytes) { + for (i = 0; i < bytes; i++) { + st->buffer[st->leftover + i] = m[i]; + } + st->leftover += bytes; + } +} + +} // namespace poly1305_donna diff --git a/src/crypto/poly1305.h b/src/crypto/poly1305.h index c48ee38e7e..88f2b57855 100644 --- a/src/crypto/poly1305.h +++ b/src/crypto/poly1305.h @@ -1,17 +1,70 @@ -// Copyright (c) 2019 The Bitcoin Core developers +// Copyright (c) 2019-2022 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or https://opensource.org/licenses/mit-license.php. #ifndef BITCOIN_CRYPTO_POLY1305_H #define BITCOIN_CRYPTO_POLY1305_H +#include + +#include +#include #include -#include -#define POLY1305_KEYLEN 32 -#define POLY1305_TAGLEN 16 +#define POLY1305_BLOCK_SIZE 16 + +namespace poly1305_donna { + +// Based on the public domain implementation by Andrew Moon +// poly1305-donna-32.h from https://github.com/floodyberry/poly1305-donna + +typedef struct { + uint32_t r[5]; + uint32_t h[5]; + uint32_t pad[4]; + size_t leftover; + unsigned char buffer[POLY1305_BLOCK_SIZE]; + unsigned char final; +} poly1305_context; + +void poly1305_init(poly1305_context *st, const unsigned char key[32]) noexcept; +void poly1305_update(poly1305_context *st, const unsigned char *m, size_t bytes) noexcept; +void poly1305_finish(poly1305_context *st, unsigned char mac[16]) noexcept; + +} // namespace poly1305_donna + +/** C++ wrapper with std::byte Span interface around poly1305_donna code. */ +class Poly1305 +{ + poly1305_donna::poly1305_context m_ctx; + +public: + /** Length of the output produced by Finalize(). */ + static constexpr unsigned TAGLEN{16}; + + /** Length of the keys expected by the constructor. */ + static constexpr unsigned KEYLEN{32}; + + /** Construct a Poly1305 object with a given 32-byte key. */ + Poly1305(Span key) noexcept + { + assert(key.size() == KEYLEN); + poly1305_donna::poly1305_init(&m_ctx, UCharCast(key.data())); + } + + /** Process message bytes. */ + Poly1305& Update(Span msg) noexcept + { + poly1305_donna::poly1305_update(&m_ctx, UCharCast(msg.data()), msg.size()); + return *this; + } -void poly1305_auth(unsigned char out[POLY1305_TAGLEN], const unsigned char *m, size_t inlen, - const unsigned char key[POLY1305_KEYLEN]); + /** Write authentication tag to 16-byte out. */ + void Finalize(Span out) noexcept + { + assert(out.size() == TAGLEN); + poly1305_donna::poly1305_finish(&m_ctx, UCharCast(out.data())); + } +}; #endif // BITCOIN_CRYPTO_POLY1305_H diff --git a/src/test/crypto_tests.cpp b/src/test/crypto_tests.cpp index 3e99c8d9b9..7fa55390a5 100644 --- a/src/test/crypto_tests.cpp +++ b/src/test/crypto_tests.cpp @@ -172,13 +172,27 @@ static void TestChaCha20(const std::string &hex_message, const std::string &hexk static void TestPoly1305(const std::string &hexmessage, const std::string &hexkey, const std::string& hextag) { - std::vector key = ParseHex(hexkey); - std::vector m = ParseHex(hexmessage); - std::vector tag = ParseHex(hextag); - std::vector tagres; - tagres.resize(POLY1305_TAGLEN); - poly1305_auth(tagres.data(), m.data(), m.size(), key.data()); - BOOST_CHECK(tag == tagres); + auto key = ParseHex(hexkey); + auto m = ParseHex(hexmessage); + std::vector tagres(Poly1305::TAGLEN); + Poly1305{MakeByteSpan(key)}.Update(MakeByteSpan(m)).Finalize(tagres); + BOOST_CHECK_EQUAL(HexStr(tagres), hextag); + + // Test incremental interface + for (int splits = 0; splits < 10; ++splits) { + for (int iter = 0; iter < 10; ++iter) { + auto data = MakeByteSpan(m); + Poly1305 poly1305{MakeByteSpan(key)}; + for (int chunk = 0; chunk < splits; ++chunk) { + size_t now = InsecureRandRange(data.size() + 1); + poly1305.Update(data.first(now)); + data = data.subspan(now); + } + tagres.assign(Poly1305::TAGLEN, std::byte{}); + poly1305.Update(data).Finalize(tagres); + BOOST_CHECK_EQUAL(HexStr(tagres), hextag); + } + } } static std::string LongTestString() @@ -572,6 +586,57 @@ BOOST_AUTO_TEST_CASE(poly1305_testvector) TestPoly1305("e33594d7505e43b900000000000000003394d7505e4379cd010000000000000000000000000000000000000000000000", "0100000000000000040000000000000000000000000000000000000000000000", "13000000000000000000000000000000"); + + // Tests from https://github.com/floodyberry/poly1305-donna/blob/master/poly1305-donna.c + TestPoly1305("8e993b9f48681273c29650ba32fc76ce48332ea7164d96a4476fb8c531a1186a" + "c0dfc17c98dce87b4da7f011ec48c97271d2c20f9b928fe2270d6fb863d51738" + "b48eeee314a7cc8ab932164548e526ae90224368517acfeabd6bb3732bc0e9da" + "99832b61ca01b6de56244a9e88d5f9b37973f622a43d14a6599b1f654cb45a74" + "e355a5", + "eea6a7251c1e72916d11c2cb214d3c252539121d8e234e652d651fa4c8cff880", + "f3ffc7703f9400e52a7dfb4b3d3305d9"); + { + // mac of the macs of messages of length 0 to 256, where the key and messages have all + // their values set to the length. + auto total_key = ParseHex("01020304050607fffefdfcfbfaf9ffffffffffffffffffffffffffff00000000"); + Poly1305 total_ctx(MakeByteSpan(total_key)); + for (unsigned i = 0; i < 256; ++i) { + std::vector key(32, std::byte{uint8_t(i)}); + std::vector msg(i, std::byte{uint8_t(i)}); + std::array tag; + Poly1305{key}.Update(msg).Finalize(tag); + total_ctx.Update(tag); + } + std::vector total_tag(Poly1305::TAGLEN); + total_ctx.Finalize(total_tag); + BOOST_CHECK_EQUAL(HexStr(total_tag), "64afe2e8d6ad7bbdd287f97c44623d39"); + } + + // Tests with sparse messages and random keys. + TestPoly1305("000000000000000000000094000000000000b07c4300000000002c002600d500" + "00000000000000000000000000bc58000000000000000000c9000000dd000000" + "00000000000000d34c000000000000000000000000f9009100000000000000c2" + "4b0000e900000000000000000000000000000000000e00000027000074000000" + "0000000003000000000000f1000000000000dce2000000000000003900000000" + "0000000000000000000000000000000000000000000000520000000000000000" + "000000000000000000000000009500000000000000000000000000cf00826700" + "000000a900000000000000000000000000000000000000000079000000000000" + "0000de0000004c000000000033000000000000000000000000002800aa000000" + "00003300860000e000000000", + "6e543496db3cf677592989891ab021f58390feb84fb419fbc7bb516a60bfa302", + "7ea80968354d40d9d790b45310caf7f3"); + TestPoly1305("0000005900000000c40000002f00000000000000000000000000000029690000" + "0000e8000037000000000000000000000000000b000000000000000000000000" + "000000000000000000000000001800006e0000000000a4000000000000000000" + "00000000000000004d00000000000000b0000000000000000000005a00000000" + "0000000000b7c300000000000000540000000000000000000000000a00000000" + "00005b0000000000000000000000000000000000002d00e70000000000000000" + "000000000000003400006800d700000000000000000000360000000000000000" + "00eb000000000000000000000000000000000000000000000000000028000000" + "37000000000000000000000000000000000000000000000000000000008f0000" + "000000000000000000000000", + "f0b659a4f3143d8a1e1dacb9a409fe7e7cd501dfb58b16a2623046c5d337922a", + "0e410fa9d7a40ac582e77546be9a72bb"); } BOOST_AUTO_TEST_CASE(countbits_tests) From 07e749fc7e8b75f14f6c4e55cc873e8decaaf8ea Mon Sep 17 00:00:00 2001 From: div72 Date: Wed, 14 Feb 2024 18:34:46 +0300 Subject: [PATCH 3/6] crypto: port upstream changes for chacha20 --- src/crypto/chacha20.cpp | 324 ++++++++++++++++++++++---------------- src/crypto/chacha20.h | 155 ++++++++++++++++-- src/random.cpp | 15 +- src/random.h | 2 +- src/test/crypto_tests.cpp | 300 ++++++++++++++++++++++++++++++----- 5 files changed, 598 insertions(+), 198 deletions(-) diff --git a/src/crypto/chacha20.cpp b/src/crypto/chacha20.cpp index 1ad6e7f1e7..d72cf5aaef 100644 --- a/src/crypto/chacha20.cpp +++ b/src/crypto/chacha20.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2017-2019 The Bitcoin Core developers +// Copyright (c) 2017-2022 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or https://opensource.org/licenses/mit-license.php. @@ -7,7 +7,11 @@ #include #include +#include +#include +#include +// #include #include constexpr static inline uint32_t rotl32(uint32_t v, int c) { return (v << c) | (v >> (32 - c)); } @@ -20,95 +24,70 @@ constexpr static inline uint32_t rotl32(uint32_t v, int c) { return (v << c) | ( #define REPEAT10(a) do { {a}; {a}; {a}; {a}; {a}; {a}; {a}; {a}; {a}; {a}; } while(0) -static const unsigned char sigma[] = "expand 32-byte k"; -static const unsigned char tau[] = "expand 16-byte k"; - -void ChaCha20::SetKey(const unsigned char* k, size_t keylen) +void ChaCha20Aligned::SetKey(Span key) noexcept { - const unsigned char *constants; - - input[4] = ReadLE32(k + 0); - input[5] = ReadLE32(k + 4); - input[6] = ReadLE32(k + 8); - input[7] = ReadLE32(k + 12); - if (keylen == 32) { /* recommended */ - k += 16; - constants = sigma; - } else { /* keylen == 16 */ - constants = tau; - } - input[8] = ReadLE32(k + 0); - input[9] = ReadLE32(k + 4); - input[10] = ReadLE32(k + 8); - input[11] = ReadLE32(k + 12); - input[0] = ReadLE32(constants + 0); - input[1] = ReadLE32(constants + 4); - input[2] = ReadLE32(constants + 8); - input[3] = ReadLE32(constants + 12); - input[12] = 0; - input[13] = 0; - input[14] = 0; - input[15] = 0; + assert(key.size() == KEYLEN); + input[0] = ReadLE32(UCharCast(key.data() + 0)); + input[1] = ReadLE32(UCharCast(key.data() + 4)); + input[2] = ReadLE32(UCharCast(key.data() + 8)); + input[3] = ReadLE32(UCharCast(key.data() + 12)); + input[4] = ReadLE32(UCharCast(key.data() + 16)); + input[5] = ReadLE32(UCharCast(key.data() + 20)); + input[6] = ReadLE32(UCharCast(key.data() + 24)); + input[7] = ReadLE32(UCharCast(key.data() + 28)); + input[8] = 0; + input[9] = 0; + input[10] = 0; + input[11] = 0; } -ChaCha20::ChaCha20() +ChaCha20Aligned::~ChaCha20Aligned() { - memset(input, 0, sizeof(input)); + memory_cleanse(input, sizeof(input)); } -ChaCha20::ChaCha20(const unsigned char* k, size_t keylen) +ChaCha20Aligned::ChaCha20Aligned(Span key) noexcept { - SetKey(k, keylen); + SetKey(key); } -void ChaCha20::SetIV(uint64_t iv) +void ChaCha20Aligned::Seek(Nonce96 nonce, uint32_t block_counter) noexcept { - input[14] = iv; - input[15] = iv >> 32; + input[8] = block_counter; + input[9] = nonce.first; + input[10] = nonce.second; + input[11] = nonce.second >> 32; } -void ChaCha20::Seek(uint64_t pos) +inline void ChaCha20Aligned::Keystream(Span output) noexcept { - input[12] = pos; - input[13] = pos >> 32; -} + unsigned char* c = UCharCast(output.data()); + size_t blocks = output.size() / BLOCKLEN; + assert(blocks * BLOCKLEN == output.size()); -void ChaCha20::Keystream(unsigned char* c, size_t bytes) -{ uint32_t x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15; - uint32_t j0, j1, j2, j3, j4, j5, j6, j7, j8, j9, j10, j11, j12, j13, j14, j15; - unsigned char *ctarget = nullptr; - unsigned char tmp[64]; - unsigned int i; - - if (!bytes) return; - - j0 = input[0]; - j1 = input[1]; - j2 = input[2]; - j3 = input[3]; - j4 = input[4]; - j5 = input[5]; - j6 = input[6]; - j7 = input[7]; - j8 = input[8]; - j9 = input[9]; - j10 = input[10]; - j11 = input[11]; - j12 = input[12]; - j13 = input[13]; - j14 = input[14]; - j15 = input[15]; + uint32_t j4, j5, j6, j7, j8, j9, j10, j11, j12, j13, j14, j15; + + if (!blocks) return; + + j4 = input[0]; + j5 = input[1]; + j6 = input[2]; + j7 = input[3]; + j8 = input[4]; + j9 = input[5]; + j10 = input[6]; + j11 = input[7]; + j12 = input[8]; + j13 = input[9]; + j14 = input[10]; + j15 = input[11]; for (;;) { - if (bytes < 64) { - ctarget = c; - c = tmp; - } - x0 = j0; - x1 = j1; - x2 = j2; - x3 = j3; + x0 = 0x61707865; + x1 = 0x3320646e; + x2 = 0x79622d32; + x3 = 0x6b206574; x4 = j4; x5 = j5; x6 = j6; @@ -134,10 +113,10 @@ void ChaCha20::Keystream(unsigned char* c, size_t bytes) QUARTERROUND( x3, x4, x9,x14); ); - x0 += j0; - x1 += j1; - x2 += j2; - x3 += j3; + x0 += 0x61707865; + x1 += 0x3320646e; + x2 += 0x79622d32; + x3 += 0x6b206574; x4 += j4; x5 += j5; x6 += j6; @@ -171,59 +150,47 @@ void ChaCha20::Keystream(unsigned char* c, size_t bytes) WriteLE32(c + 56, x14); WriteLE32(c + 60, x15); - if (bytes <= 64) { - if (bytes < 64) { - for (i = 0;i < bytes;++i) ctarget[i] = c[i]; - } - input[12] = j12; - input[13] = j13; + if (blocks == 1) { + input[8] = j12; + input[9] = j13; return; } - bytes -= 64; - c += 64; + blocks -= 1; + c += BLOCKLEN; } } -void ChaCha20::Crypt(const unsigned char* m, unsigned char* c, size_t bytes) +inline void ChaCha20Aligned::Crypt(Span in_bytes, Span out_bytes) noexcept { + assert(in_bytes.size() == out_bytes.size()); + const unsigned char* m = UCharCast(in_bytes.data()); + unsigned char* c = UCharCast(out_bytes.data()); + size_t blocks = out_bytes.size() / BLOCKLEN; + assert(blocks * BLOCKLEN == out_bytes.size()); + uint32_t x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15; - uint32_t j0, j1, j2, j3, j4, j5, j6, j7, j8, j9, j10, j11, j12, j13, j14, j15; - unsigned char *ctarget = nullptr; - unsigned char tmp[64]; - unsigned int i; - - if (!bytes) return; - - j0 = input[0]; - j1 = input[1]; - j2 = input[2]; - j3 = input[3]; - j4 = input[4]; - j5 = input[5]; - j6 = input[6]; - j7 = input[7]; - j8 = input[8]; - j9 = input[9]; - j10 = input[10]; - j11 = input[11]; - j12 = input[12]; - j13 = input[13]; - j14 = input[14]; - j15 = input[15]; + uint32_t j4, j5, j6, j7, j8, j9, j10, j11, j12, j13, j14, j15; + + if (!blocks) return; + + j4 = input[0]; + j5 = input[1]; + j6 = input[2]; + j7 = input[3]; + j8 = input[4]; + j9 = input[5]; + j10 = input[6]; + j11 = input[7]; + j12 = input[8]; + j13 = input[9]; + j14 = input[10]; + j15 = input[11]; for (;;) { - if (bytes < 64) { - // if m has fewer than 64 bytes available, copy m to tmp and - // read from tmp instead - for (i = 0;i < bytes;++i) tmp[i] = m[i]; - m = tmp; - ctarget = c; - c = tmp; - } - x0 = j0; - x1 = j1; - x2 = j2; - x3 = j3; + x0 = 0x61707865; + x1 = 0x3320646e; + x2 = 0x79622d32; + x3 = 0x6b206574; x4 = j4; x5 = j5; x6 = j6; @@ -249,10 +216,10 @@ void ChaCha20::Crypt(const unsigned char* m, unsigned char* c, size_t bytes) QUARTERROUND( x3, x4, x9,x14); ); - x0 += j0; - x1 += j1; - x2 += j2; - x3 += j3; + x0 += 0x61707865; + x1 += 0x3320646e; + x2 += 0x79622d32; + x3 += 0x6b206574; x4 += j4; x5 += j5; x6 += j6; @@ -303,16 +270,105 @@ void ChaCha20::Crypt(const unsigned char* m, unsigned char* c, size_t bytes) WriteLE32(c + 56, x14); WriteLE32(c + 60, x15); - if (bytes <= 64) { - if (bytes < 64) { - for (i = 0;i < bytes;++i) ctarget[i] = c[i]; - } - input[12] = j12; - input[13] = j13; + if (blocks == 1) { + input[8] = j12; + input[9] = j13; return; } - bytes -= 64; - c += 64; - m += 64; + blocks -= 1; + c += BLOCKLEN; + m += BLOCKLEN; + } +} + +void ChaCha20::Keystream(Span out) noexcept +{ + if (out.empty()) return; + if (m_bufleft) { + unsigned reuse = std::min(m_bufleft, out.size()); + std::copy(m_buffer.end() - m_bufleft, m_buffer.end() - m_bufleft + reuse, out.begin()); + m_bufleft -= reuse; + out = out.subspan(reuse); + } + if (out.size() >= m_aligned.BLOCKLEN) { + size_t blocks = out.size() / m_aligned.BLOCKLEN; + m_aligned.Keystream(out.first(blocks * m_aligned.BLOCKLEN)); + out = out.subspan(blocks * m_aligned.BLOCKLEN); + } + if (!out.empty()) { + m_aligned.Keystream(m_buffer); + std::copy(m_buffer.begin(), m_buffer.begin() + out.size(), out.begin()); + m_bufleft = m_aligned.BLOCKLEN - out.size(); + } +} + +void ChaCha20::Crypt(Span input, Span output) noexcept +{ + assert(input.size() == output.size()); + + if (!input.size()) return; + if (m_bufleft) { + unsigned reuse = std::min(m_bufleft, input.size()); + for (unsigned i = 0; i < reuse; i++) { + output[i] = input[i] ^ m_buffer[m_aligned.BLOCKLEN - m_bufleft + i]; + } + m_bufleft -= reuse; + output = output.subspan(reuse); + input = input.subspan(reuse); + } + if (input.size() >= m_aligned.BLOCKLEN) { + size_t blocks = input.size() / m_aligned.BLOCKLEN; + m_aligned.Crypt(input.first(blocks * m_aligned.BLOCKLEN), output.first(blocks * m_aligned.BLOCKLEN)); + output = output.subspan(blocks * m_aligned.BLOCKLEN); + input = input.subspan(blocks * m_aligned.BLOCKLEN); + } + if (!input.empty()) { + m_aligned.Keystream(m_buffer); + for (unsigned i = 0; i < input.size(); i++) { + output[i] = input[i] ^ m_buffer[i]; + } + m_bufleft = m_aligned.BLOCKLEN - input.size(); + } +} + +ChaCha20::~ChaCha20() +{ + memory_cleanse(m_buffer.data(), m_buffer.size()); +} + +void ChaCha20::SetKey(Span key) noexcept +{ + m_aligned.SetKey(key); + m_bufleft = 0; + memory_cleanse(m_buffer.data(), m_buffer.size()); +} + +FSChaCha20::FSChaCha20(Span key, uint32_t rekey_interval) noexcept : + m_chacha20(key), 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(input, output); + + // 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(new_key); + // Update its key. + m_chacha20.SetKey(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.Seek({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 244d786806..ea734737da 100644 --- a/src/crypto/chacha20.h +++ b/src/crypto/chacha20.h @@ -1,34 +1,159 @@ -// Copyright (c) 2017-2019 The Bitcoin Core developers +// Copyright (c) 2017-2022 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or https://opensource.org/licenses/mit-license.php. #ifndef BITCOIN_CRYPTO_CHACHA20_H #define BITCOIN_CRYPTO_CHACHA20_H +#include + +#include +#include +#include #include -#include +#include + +// classes for ChaCha20 256-bit stream cipher developed by Daniel J. Bernstein +// https://cr.yp.to/chacha/chacha-20080128.pdf. +// +// The 128-bit input is here implemented as a 96-bit nonce and a 32-bit block +// counter, as in RFC8439 Section 2.3. When the 32-bit block counter overflows +// the first 32-bit part of the nonce is automatically incremented, making it +// conceptually compatible with variants that use a 64/64 split instead. + +/** ChaCha20 cipher that only operates on multiples of 64 bytes. */ +class ChaCha20Aligned +{ +private: + uint32_t input[12]; + +public: + /** Expected key length in constructor and SetKey. */ + static constexpr unsigned KEYLEN{32}; + + /** Block size (inputs/outputs to Keystream / Crypt should be multiples of this). */ + static constexpr unsigned BLOCKLEN{64}; + + /** For safety, disallow initialization without key. */ + ChaCha20Aligned() noexcept = delete; + + /** Initialize a cipher with specified 32-byte key. */ + ChaCha20Aligned(Span key) noexcept; + + /** Destructor to clean up private memory. */ + ~ChaCha20Aligned(); + + /** Set 32-byte key, and seek to nonce 0 and block position 0. */ + void SetKey(Span key) noexcept; + + /** Type for 96-bit nonces used by the Set function below. + * + * The first field corresponds to the LE32-encoded first 4 bytes of the nonce, also referred + * to as the '32-bit fixed-common part' in Example 2.8.2 of RFC8439. + * + * The second field corresponds to the LE64-encoded last 8 bytes of the nonce. + * + */ + using Nonce96 = std::pair; + + /** Set the 96-bit nonce and 32-bit block counter. + * + * Block_counter selects a position to seek to (to byte BLOCKLEN*block_counter). After 256 GiB, + * the block counter overflows, and nonce.first is incremented. + */ + void Seek(Nonce96 nonce, uint32_t block_counter) noexcept; + + /** outputs the keystream into out, whose length must be a multiple of BLOCKLEN. */ + void Keystream(Span out) noexcept; + + /** en/deciphers the message and write the result into + * + * The size of input and output must be equal, and be a multiple of BLOCKLEN. + */ + void Crypt(Span input, Span output) noexcept; +}; -/** A class for ChaCha20 256-bit stream cipher developed by Daniel J. Bernstein - https://cr.yp.to/chacha/chacha-20080128.pdf */ +/** Unrestricted ChaCha20 cipher. */ class ChaCha20 { private: - uint32_t input[16]; + ChaCha20Aligned m_aligned; + std::array m_buffer; + unsigned m_bufleft{0}; public: - ChaCha20(); - ChaCha20(const unsigned char* key, size_t keylen); - void SetKey(const unsigned char* key, size_t keylen); //!< set key with flexible keylength; 256bit recommended */ - void SetIV(uint64_t iv); // set the 64bit nonce - void Seek(uint64_t pos); // set the 64bit block counter + /** Expected key length in constructor and SetKey. */ + static constexpr unsigned KEYLEN = ChaCha20Aligned::KEYLEN; + + /** For safety, disallow initialization without key. */ + ChaCha20() noexcept = delete; + + /** Initialize a cipher with specified 32-byte key. */ + ChaCha20(Span key) noexcept : m_aligned(key) {} + + /** Destructor to clean up private memory. */ + ~ChaCha20(); + + /** Set 32-byte key, and seek to nonce 0 and block position 0. */ + void SetKey(Span key) noexcept; - /** outputs the keystream of size into */ - void Keystream(unsigned char* c, size_t bytes); + /** 96-bit nonce type. */ + using Nonce96 = ChaCha20Aligned::Nonce96; - /** enciphers the message of length and write the enciphered representation into - * Used for encryption and decryption (XOR) + /** Set the 96-bit nonce and 32-bit block counter. See ChaCha20Aligned::Seek. */ + void Seek(Nonce96 nonce, uint32_t block_counter) noexcept + { + m_aligned.Seek(nonce, block_counter); + m_bufleft = 0; + } + + /** en/deciphers the message and write the result into + * + * The size of in_bytes and out_bytes must be equal. */ - void Crypt(const unsigned char* input, unsigned char* output, size_t bytes); + void Crypt(Span in_bytes, Span out_bytes) noexcept; + + /** outputs the keystream to out. */ + void Keystream(Span out) noexcept; +}; + +/** 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/random.cpp b/src/random.cpp index 368fa1f9bc..31b5ba5dde 100644 --- a/src/random.cpp +++ b/src/random.cpp @@ -601,7 +601,7 @@ uint256 GetRandHash() noexcept void FastRandomContext::RandomSeed() { uint256 seed = GetRandHash(); - rng.SetKey(seed.begin(), 32); + rng.SetKey(MakeByteSpan(seed)); requires_seed = false; } @@ -621,15 +621,12 @@ std::vector FastRandomContext::randbytes(size_t len) if (requires_seed) RandomSeed(); std::vector ret(len); if (len > 0) { - rng.Keystream(ret.data(), len); + rng.Keystream(MakeWritableByteSpan(ret)); } return ret; } -FastRandomContext::FastRandomContext(const uint256& seed) noexcept : requires_seed(false), bytebuf_size(0), bitbuf_size(0) -{ - rng.SetKey(seed.begin(), 32); -} +FastRandomContext::FastRandomContext(const uint256& seed) noexcept : requires_seed(false), rng(MakeByteSpan(seed)), bytebuf_size(0), bitbuf_size(0) {} bool Random_SanityCheck() { @@ -677,13 +674,15 @@ bool Random_SanityCheck() return true; } -FastRandomContext::FastRandomContext(bool fDeterministic) noexcept : requires_seed(!fDeterministic), bytebuf_size(0), bitbuf_size(0) +static constexpr std::array ZERO_KEY{}; + +FastRandomContext::FastRandomContext(bool fDeterministic) noexcept : requires_seed(!fDeterministic), rng(ZERO_KEY), bytebuf_size(0), bitbuf_size(0) { if (!fDeterministic) { return; } uint256 seed; - rng.SetKey(seed.begin(), 32); + rng.SetKey(MakeByteSpan(seed)); } FastRandomContext& FastRandomContext::operator=(FastRandomContext&& from) noexcept diff --git a/src/random.h b/src/random.h index 066ad1e68f..f36dcfcd55 100644 --- a/src/random.h +++ b/src/random.h @@ -158,7 +158,7 @@ class FastRandomContext if (requires_seed) { RandomSeed(); } - rng.Keystream(bytebuf, sizeof(bytebuf)); + rng.Keystream(MakeWritableByteSpan(bytebuf)); bytebuf_size = sizeof(bytebuf); } diff --git a/src/test/crypto_tests.cpp b/src/test/crypto_tests.cpp index 7fa55390a5..2b522231be 100644 --- a/src/test/crypto_tests.cpp +++ b/src/test/crypto_tests.cpp @@ -138,36 +138,97 @@ static void TestAES256CBC(const std::string &hexkey, const std::string &hexiv, b } } -static void TestChaCha20(const std::string &hex_message, const std::string &hexkey, uint64_t nonce, uint64_t seek, const std::string& hexout) +static void TestChaCha20(const std::string &hex_message, const std::string &hexkey, ChaCha20::Nonce96 nonce, uint32_t seek, const std::string& hexout) { - std::vector key = ParseHex(hexkey); - std::vector m = ParseHex(hex_message); - ChaCha20 rng(key.data(), key.size()); - rng.SetIV(nonce); - rng.Seek(seek); - std::vector out = ParseHex(hexout); - std::vector outres; - outres.resize(out.size()); - assert(hex_message.empty() || m.size() == out.size()); + auto key = ParseHex(hexkey); + assert(key.size() == 32); + auto m_uc = ParseHex(hex_message); + auto m = std::vector((std::byte*)m_uc.data(), (std::byte*)m_uc.data() + m_uc.size()); + ChaCha20 rng{MakeByteSpan(key)}; + rng.Seek(nonce, seek); + std::vector outres; + outres.resize(hexout.size() / 2); + assert(hex_message.empty() || m.size() * 2 == hexout.size()); // perform the ChaCha20 round(s), if message is provided it will output the encrypted ciphertext otherwise the keystream if (!hex_message.empty()) { - rng.Crypt(m.data(), outres.data(), outres.size()); + rng.Crypt(m, outres); } else { - rng.Keystream(outres.data(), outres.size()); + rng.Keystream(outres); } - BOOST_CHECK(out == outres); + BOOST_CHECK_EQUAL(hexout, HexStr(outres)); if (!hex_message.empty()) { // Manually XOR with the keystream and compare the output - rng.SetIV(nonce); - rng.Seek(seek); - std::vector only_keystream(outres.size()); - rng.Keystream(only_keystream.data(), only_keystream.size()); + rng.Seek(nonce, seek); + std::vector only_keystream(outres.size()); + rng.Keystream(only_keystream); for (size_t i = 0; i != m.size(); i++) { outres[i] = m[i] ^ only_keystream[i]; } - BOOST_CHECK(out == outres); + BOOST_CHECK_EQUAL(hexout, HexStr(outres)); + } + + // Repeat 10x, but fragmented into 3 chunks, to exercise the ChaCha20 class's caching. + for (int i = 0; i < 10; ++i) { + size_t lens[3]; + lens[0] = InsecureRandRange(hexout.size() / 2U + 1U); + lens[1] = InsecureRandRange(hexout.size() / 2U + 1U - lens[0]); + lens[2] = hexout.size() / 2U - lens[0] - lens[1]; + + rng.Seek(nonce, seek); + outres.assign(hexout.size() / 2U, {}); + size_t pos = 0; + for (int j = 0; j < 3; ++j) { + if (!hex_message.empty()) { + rng.Crypt(Span{m}.subspan(pos, lens[j]), Span{outres}.subspan(pos, lens[j])); + } else { + rng.Keystream(Span{outres}.subspan(pos, lens[j])); + } + pos += lens[j]; + } + BOOST_CHECK_EQUAL(hexout, HexStr(outres)); + } +} + +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_uc = ParseHex(hex_plaintext); + auto plaintext = std::vector((std::byte*)plaintext_uc.data(), (std::byte*)plaintext_uc.data() + plaintext_uc.size()); + + auto fsc20 = FSChaCha20{MakeByteSpan(key), rekey_interval}; + auto c20 = ChaCha20{MakeByteSpan(key)}; + + 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(plaintext, c20_output); + 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(plaintext, c20_output); + BOOST_CHECK(c20_output != fsc20_output); + + std::byte new_key[FSChaCha20::KEYLEN]; + c20_copy.Keystream(new_key); + c20.SetKey(new_key); + c20.Seek({0, 1}, 0); + + // Outputs should match again after simulating key rotation + c20.Crypt(plaintext, c20_output); + 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) @@ -478,44 +539,203 @@ BOOST_AUTO_TEST_CASE(aes_cbc_testvectors) { BOOST_AUTO_TEST_CASE(chacha20_testvector) { - // Test vector from RFC 7539 + /* Example from RFC8439 section 2.3.2. */ + TestChaCha20("", + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + {0x09000000, 0x4a000000}, 1, + "10f1e7e4d13b5915500fdd1fa32071c4c7d1f4c733c068030422aa9ac3d46c4e" + "d2826446079faa0914c2d705d98b02a2b5129cd1de164eb9cbd083e8a2503c4e"); + + /* Example from RFC8439 section 2.4.2. */ + TestChaCha20("4c616469657320616e642047656e746c656d656e206f662074686520636c6173" + "73206f66202739393a204966204920636f756c64206f6666657220796f75206f" + "6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73" + "637265656e20776f756c642062652069742e", + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + {0, 0x4a000000}, 1, + "6e2e359a2568f98041ba0728dd0d6981e97e7aec1d4360c20a27afccfd9fae0b" + "f91b65c5524733ab8f593dabcd62b3571639d624e65152ab8f530c359f0861d8" + "07ca0dbf500d6a6156a38e088a22b65e52bc514d16ccf806818ce91ab7793736" + "5af90bbf74a35be6b40b8eedf2785e42874d"); + + // RFC 7539/8439 A.1 Test Vector #1: + TestChaCha20("", + "0000000000000000000000000000000000000000000000000000000000000000", + {0, 0}, 0, + "76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7" + "da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586"); + + // RFC 7539/8439 A.1 Test Vector #2: + TestChaCha20("", + "0000000000000000000000000000000000000000000000000000000000000000", + {0, 0}, 1, + "9f07e7be5551387a98ba977c732d080dcb0f29a048e3656912c6533e32ee7aed" + "29b721769ce64e43d57133b074d839d531ed1f28510afb45ace10a1f4b794d6f"); + + // RFC 7539/8439 A.1 Test Vector #3: + TestChaCha20("", + "0000000000000000000000000000000000000000000000000000000000000001", + {0, 0}, 1, + "3aeb5224ecf849929b9d828db1ced4dd832025e8018b8160b82284f3c949aa5a" + "8eca00bbb4a73bdad192b5c42f73f2fd4e273644c8b36125a64addeb006c13a0"); + + // RFC 7539/8439 A.1 Test Vector #4: + TestChaCha20("", + "00ff000000000000000000000000000000000000000000000000000000000000", + {0, 0}, 2, + "72d54dfbf12ec44b362692df94137f328fea8da73990265ec1bbbea1ae9af0ca" + "13b25aa26cb4a648cb9b9d1be65b2c0924a66c54d545ec1b7374f4872e99f096"); + + // RFC 7539/8439 A.1 Test Vector #5: + TestChaCha20("", + "0000000000000000000000000000000000000000000000000000000000000000", + {0, 0x200000000000000}, 0, + "c2c64d378cd536374ae204b9ef933fcd1a8b2288b3dfa49672ab765b54ee27c7" + "8a970e0e955c14f3a88e741b97c286f75f8fc299e8148362fa198a39531bed6d"); + + // RFC 7539/8439 A.2 Test Vector #1: + TestChaCha20("0000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + {0, 0}, 0, + "76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7" + "da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586"); + + // RFC 7539/8439 A.2 Test Vector #2: + TestChaCha20("416e79207375626d697373696f6e20746f20746865204945544620696e74656e" + "6465642062792074686520436f6e7472696275746f7220666f72207075626c69" + "636174696f6e20617320616c6c206f722070617274206f6620616e2049455446" + "20496e7465726e65742d4472616674206f722052464320616e6420616e792073" + "746174656d656e74206d6164652077697468696e2074686520636f6e74657874" + "206f6620616e204945544620616374697669747920697320636f6e7369646572" + "656420616e20224945544620436f6e747269627574696f6e222e205375636820" + "73746174656d656e747320696e636c756465206f72616c2073746174656d656e" + "747320696e20494554462073657373696f6e732c2061732077656c6c20617320" + "7772697474656e20616e6420656c656374726f6e696320636f6d6d756e696361" + "74696f6e73206d61646520617420616e792074696d65206f7220706c6163652c" + "207768696368206172652061646472657373656420746f", + "0000000000000000000000000000000000000000000000000000000000000001", + {0, 0x200000000000000}, 1, + "a3fbf07df3fa2fde4f376ca23e82737041605d9f4f4f57bd8cff2c1d4b7955ec" + "2a97948bd3722915c8f3d337f7d370050e9e96d647b7c39f56e031ca5eb6250d" + "4042e02785ececfa4b4bb5e8ead0440e20b6e8db09d881a7c6132f420e527950" + "42bdfa7773d8a9051447b3291ce1411c680465552aa6c405b7764d5e87bea85a" + "d00f8449ed8f72d0d662ab052691ca66424bc86d2df80ea41f43abf937d3259d" + "c4b2d0dfb48a6c9139ddd7f76966e928e635553ba76c5c879d7b35d49eb2e62b" + "0871cdac638939e25e8a1e0ef9d5280fa8ca328b351c3c765989cbcf3daa8b6c" + "cc3aaf9f3979c92b3720fc88dc95ed84a1be059c6499b9fda236e7e818b04b0b" + "c39c1e876b193bfe5569753f88128cc08aaa9b63d1a16f80ef2554d7189c411f" + "5869ca52c5b83fa36ff216b9c1d30062bebcfd2dc5bce0911934fda79a86f6e6" + "98ced759c3ff9b6477338f3da4f9cd8514ea9982ccafb341b2384dd902f3d1ab" + "7ac61dd29c6f21ba5b862f3730e37cfdc4fd806c22f221"); + + // RFC 7539/8439 A.2 Test Vector #3: + TestChaCha20("2754776173206272696c6c69672c20616e642074686520736c6974687920746f" + "7665730a446964206779726520616e642067696d626c6520696e207468652077" + "6162653a0a416c6c206d696d737920776572652074686520626f726f676f7665" + "732c0a416e6420746865206d6f6d65207261746873206f757467726162652e", + "1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0", + {0, 0x200000000000000}, 42, + "62e6347f95ed87a45ffae7426f27a1df5fb69110044c0d73118effa95b01e5cf" + "166d3df2d721caf9b21e5fb14c616871fd84c54f9d65b283196c7fe4f60553eb" + "f39c6402c42234e32a356b3e764312a61a5532055716ead6962568f87d3f3f77" + "04c6a8d1bcd1bf4d50d6154b6da731b187b58dfd728afa36757a797ac188d1"); + + // RFC 7539/8439 A.4 Test Vector #1: + TestChaCha20("", + "0000000000000000000000000000000000000000000000000000000000000000", + {0, 0}, 0, + "76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7"); + + // RFC 7539/8439 A.4 Test Vector #2: + TestChaCha20("", + "0000000000000000000000000000000000000000000000000000000000000001", + {0, 0x200000000000000}, 0, + "ecfa254f845f647473d3cb140da9e87606cb33066c447b87bc2666dde3fbb739"); + + // RFC 7539/8439 A.4 Test Vector #3: + TestChaCha20("", + "1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0", + {0, 0x200000000000000}, 0, + "965e3bc6f9ec7ed9560808f4d229f94b137ff275ca9b3fcbdd59deaad23310ae"); // test encryption TestChaCha20("4c616469657320616e642047656e746c656d656e206f662074686520636c617373206f66202739393a204966204920636f756" "c64206f6666657220796f75206f6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73637265656e" "20776f756c642062652069742e", - "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", 0x4a000000UL, 1, + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", {0, 0x4a000000UL}, 1, "6e2e359a2568f98041ba0728dd0d6981e97e7aec1d4360c20a27afccfd9fae0bf91b65c5524733ab8f593dabcd62b3571639d" "624e65152ab8f530c359f0861d807ca0dbf500d6a6156a38e088a22b65e52bc514d16ccf806818ce91ab77937365af90bbf74" "a35be6b40b8eedf2785e42874d" ); // test keystream output - TestChaCha20("", "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", 0x4a000000UL, 1, + TestChaCha20("", "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", {0, 0x4a000000UL}, 1, "224f51f3401bd9e12fde276fb8631ded8c131f823d2c06e27e4fcaec9ef3cf788a3b0aa372600a92b57974cded2b9334794cb" "a40c63e34cdea212c4cf07d41b769a6749f3f630f4122cafe28ec4dc47e26d4346d70b98c73f3e9c53ac40c5945398b6eda1a" "832c89c167eacd901d7e2bf363"); // Test vectors from https://tools.ietf.org/html/draft-agl-tls-chacha20poly1305-04#section-7 - TestChaCha20("", "0000000000000000000000000000000000000000000000000000000000000000", 0, 0, - "76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7da41597c5157488d7724e03fb8d84a376a43b" - "8f41518a11cc387b669b2ee6586"); - TestChaCha20("", "0000000000000000000000000000000000000000000000000000000000000001", 0, 0, - "4540f05a9f1fb296d7736e7b208e3c96eb4fe1834688d2604f450952ed432d41bbe2a0b6ea7566d2a5d1e7e20d42af2c53d79" - "2b1c43fea817e9ad275ae546963"); - TestChaCha20("", "0000000000000000000000000000000000000000000000000000000000000000", 0x0100000000000000ULL, 0, - "de9cba7bf3d69ef5e786dc63973f653a0b49e015adbff7134fcb7df137821031e85a050278a7084527214f73efc7fa5b52770" - "62eb7a0433e445f41e3"); - TestChaCha20("", "0000000000000000000000000000000000000000000000000000000000000000", 1, 0, - "ef3fdfd6c61578fbf5cf35bd3dd33b8009631634d21e42ac33960bd138e50d32111e4caf237ee53ca8ad6426194a88545ddc4" - "97a0b466e7d6bbdb0041b2f586b"); - TestChaCha20("", "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", 0x0706050403020100ULL, 0, - "f798a189f195e66982105ffb640bb7757f579da31602fc93ec01ac56f85ac3c134a4547b733b46413042c9440049176905d3b" - "e59ea1c53f15916155c2be8241a38008b9a26bc35941e2444177c8ade6689de95264986d95889fb60e84629c9bd9a5acb1cc1" - "18be563eb9b3a4a472f82e09a7e778492b562ef7130e88dfe031c79db9d4f7c7a899151b9a475032b63fc385245fe054e3dd5" - "a97a5f576fe064025d3ce042c566ab2c507b138db853e3d6959660996546cc9c4a6eafdc777c040d70eaf46f76dad3979e5c5" - "360c3317166a1c894c94a371876a94df7628fe4eaaf2ccb27d5aaae0ad7ad0f9d4b6ad3b54098746d4524d38407a6deb3ab78" - "fab78c9"); + // The first one is identical to the above one from the RFC8439 A.1 vectors, but repeated here + // for completeness. + TestChaCha20("", + "0000000000000000000000000000000000000000000000000000000000000000", + {0, 0}, 0, + "76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7" + "da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586"); + TestChaCha20("", + "0000000000000000000000000000000000000000000000000000000000000001", + {0, 0}, 0, + "4540f05a9f1fb296d7736e7b208e3c96eb4fe1834688d2604f450952ed432d41" + "bbe2a0b6ea7566d2a5d1e7e20d42af2c53d792b1c43fea817e9ad275ae546963"); + TestChaCha20("", + "0000000000000000000000000000000000000000000000000000000000000000", + {0, 0x0100000000000000ULL}, 0, + "de9cba7bf3d69ef5e786dc63973f653a0b49e015adbff7134fcb7df137821031" + "e85a050278a7084527214f73efc7fa5b5277062eb7a0433e445f41e3"); + TestChaCha20("", + "0000000000000000000000000000000000000000000000000000000000000000", + {0, 1}, 0, + "ef3fdfd6c61578fbf5cf35bd3dd33b8009631634d21e42ac33960bd138e50d32" + "111e4caf237ee53ca8ad6426194a88545ddc497a0b466e7d6bbdb0041b2f586b"); + TestChaCha20("", + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + {0, 0x0706050403020100ULL}, 0, + "f798a189f195e66982105ffb640bb7757f579da31602fc93ec01ac56f85ac3c1" + "34a4547b733b46413042c9440049176905d3be59ea1c53f15916155c2be8241a" + "38008b9a26bc35941e2444177c8ade6689de95264986d95889fb60e84629c9bd" + "9a5acb1cc118be563eb9b3a4a472f82e09a7e778492b562ef7130e88dfe031c7" + "9db9d4f7c7a899151b9a475032b63fc385245fe054e3dd5a97a5f576fe064025" + "d3ce042c566ab2c507b138db853e3d6959660996546cc9c4a6eafdc777c040d7" + "0eaf46f76dad3979e5c5360c3317166a1c894c94a371876a94df7628fe4eaaf2" + "ccb27d5aaae0ad7ad0f9d4b6ad3b54098746d4524d38407a6deb3ab78fab78c9"); + + // Test overflow of 32-bit block counter, should increment the first 32-bit + // part of the nonce to retain compatibility with >256 GiB output. + // The test data was generated with an implementation that uses a 64-bit + // counter and a 64-bit initialization vector (PyCryptodome's ChaCha20 class + // with 8 bytes nonce length). + TestChaCha20("", + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + {0, 0xdeadbeef12345678}, 0xffffffff, + "2d292c880513397b91221c3a647cfb0765a4815894715f411e3df5e0dd0ba9df" + "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(poly1305_testvector) From 139c0ff47432fe2e8af5ba5182e3bd50026314d2 Mon Sep 17 00:00:00 2001 From: div72 Date: Wed, 14 Feb 2024 19:36:34 +0300 Subject: [PATCH 4/6] crypto: port ChaCha20Poly1305 from upstream --- src/Makefile.am | 2 + src/crypto/CMakeLists.txt | 1 + src/crypto/chacha20poly1305.cpp | 140 ++++++++++++++++++++++++++ src/crypto/chacha20poly1305.h | 148 +++++++++++++++++++++++++++ src/test/crypto_tests.cpp | 173 ++++++++++++++++++++++++++++++++ src/test/test_gridcoin.h | 9 ++ 6 files changed, 473 insertions(+) create mode 100644 src/crypto/chacha20poly1305.cpp create mode 100644 src/crypto/chacha20poly1305.h diff --git a/src/Makefile.am b/src/Makefile.am index c521bd11fe..043c100ec1 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -367,6 +367,8 @@ crypto_libgridcoin_crypto_base_a_SOURCES = \ crypto/aes.h \ crypto/chacha20.h \ crypto/chacha20.cpp \ + crypto/chacha20poly1305.h \ + crypto/chacha20poly1305.cpp \ crypto/common.h \ crypto/hmac_sha256.cpp \ crypto/hmac_sha256.h \ diff --git a/src/crypto/CMakeLists.txt b/src/crypto/CMakeLists.txt index f7972902be..3abbd210ef 100644 --- a/src/crypto/CMakeLists.txt +++ b/src/crypto/CMakeLists.txt @@ -2,6 +2,7 @@ set(LIBGRIDCOIN_CRYPTO gridcoin_crypto_base) add_library(gridcoin_crypto_base STATIC aes.cpp chacha20.cpp + chacha20poly1305.cpp hmac_sha256.cpp hmac_sha512.cpp poly1305.cpp diff --git a/src/crypto/chacha20poly1305.cpp b/src/crypto/chacha20poly1305.cpp new file mode 100644 index 0000000000..1e5c665b97 --- /dev/null +++ b/src/crypto/chacha20poly1305.cpp @@ -0,0 +1,140 @@ +// Copyright (c) 2023 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://opensource.org/licenses/mit-license.php. + +#include + +#include +#include +#include +#include +#include + +#include +#include + +AEADChaCha20Poly1305::AEADChaCha20Poly1305(Span key) noexcept : m_chacha20(key) +{ + assert(key.size() == KEYLEN); +} + +void AEADChaCha20Poly1305::SetKey(Span key) noexcept +{ + assert(key.size() == KEYLEN); + m_chacha20.SetKey(key); +} + +namespace { + +#ifndef HAVE_TIMINGSAFE_BCMP +#define HAVE_TIMINGSAFE_BCMP + +int timingsafe_bcmp(const unsigned char* b1, const unsigned char* b2, size_t n) noexcept +{ + const unsigned char *p1 = b1, *p2 = b2; + int ret = 0; + for (; n > 0; n--) + ret |= *p1++ ^ *p2++; + return (ret != 0); +} + +#endif + +/** Compute poly1305 tag. chacha20 must be set to the right nonce, block 0. Will be at block 1 after. */ +void ComputeTag(ChaCha20& chacha20, Span aad, Span cipher, Span tag) noexcept +{ + static const std::byte PADDING[16] = {{}}; + + // Get block of keystream (use a full 64 byte buffer to avoid the need for chacha20's own buffering). + std::byte first_block[ChaCha20Aligned::BLOCKLEN]; + chacha20.Keystream(first_block); + + // Use the first 32 bytes of the first keystream block as poly1305 key. + Poly1305 poly1305{Span{first_block}.first(Poly1305::KEYLEN)}; + + // Compute tag: + // - Process the padded AAD with Poly1305. + const unsigned aad_padding_length = (16 - (aad.size() % 16)) % 16; + poly1305.Update(aad).Update(Span{PADDING}.first(aad_padding_length)); + // - Process the padded ciphertext with Poly1305. + const unsigned cipher_padding_length = (16 - (cipher.size() % 16)) % 16; + poly1305.Update(cipher).Update(Span{PADDING}.first(cipher_padding_length)); + // - Process the AAD and plaintext length with Poly1305. + std::byte length_desc[Poly1305::TAGLEN]; + WriteLE64(UCharCast(length_desc), aad.size()); + WriteLE64(UCharCast(length_desc + 8), cipher.size()); + poly1305.Update(length_desc); + + // Output tag. + poly1305.Finalize(tag); +} + +} // namespace + +void AEADChaCha20Poly1305::Encrypt(Span plain1, Span plain2, Span aad, Nonce96 nonce, Span cipher) noexcept +{ + assert(cipher.size() == plain1.size() + plain2.size() + EXPANSION); + + // Encrypt using ChaCha20 (starting at block 1). + m_chacha20.Seek(nonce, 1); + m_chacha20.Crypt(plain1, cipher.first(plain1.size())); + m_chacha20.Crypt(plain2, cipher.subspan(plain1.size()).first(plain2.size())); + + // Seek to block 0, and compute tag using key drawn from there. + m_chacha20.Seek(nonce, 0); + ComputeTag(m_chacha20, aad, cipher.first(cipher.size() - EXPANSION), cipher.last(EXPANSION)); +} + +bool AEADChaCha20Poly1305::Decrypt(Span cipher, Span aad, Nonce96 nonce, Span plain1, Span plain2) noexcept +{ + assert(cipher.size() == plain1.size() + plain2.size() + EXPANSION); + + // Verify tag (using key drawn from block 0). + m_chacha20.Seek(nonce, 0); + std::byte expected_tag[EXPANSION]; + ComputeTag(m_chacha20, aad, cipher.first(cipher.size() - EXPANSION), expected_tag); + if (timingsafe_bcmp(UCharCast(expected_tag), UCharCast(cipher.last(EXPANSION).data()), EXPANSION)) return false; + + // Decrypt (starting at block 1). + m_chacha20.Crypt(cipher.first(plain1.size()), plain1); + m_chacha20.Crypt(cipher.subspan(plain1.size()).first(plain2.size()), plain2); + return true; +} + +void AEADChaCha20Poly1305::Keystream(Nonce96 nonce, Span keystream) noexcept +{ + // Skip the first output block, as it's used for generating the poly1305 key. + m_chacha20.Seek(nonce, 1); + m_chacha20.Keystream(keystream); +} + +void FSChaCha20Poly1305::NextPacket() noexcept +{ + if (++m_packet_counter == m_rekey_interval) { + // Generate a full block of keystream, to avoid needing the ChaCha20 buffer, even though + // we only need KEYLEN (32) bytes. + std::byte one_block[ChaCha20Aligned::BLOCKLEN]; + m_aead.Keystream({0xFFFFFFFF, m_rekey_counter}, one_block); + // Switch keys. + m_aead.SetKey(Span{one_block}.first(KEYLEN)); + // Wipe the generated keystream (a copy remains inside m_aead, which will be cleaned up + // once it cycles again, or is destroyed). + memory_cleanse(one_block, sizeof(one_block)); + // Update counters. + m_packet_counter = 0; + ++m_rekey_counter; + } +} + +void FSChaCha20Poly1305::Encrypt(Span plain1, Span plain2, Span aad, Span cipher) noexcept +{ + m_aead.Encrypt(plain1, plain2, aad, {m_packet_counter, m_rekey_counter}, cipher); + NextPacket(); +} + +bool FSChaCha20Poly1305::Decrypt(Span cipher, Span aad, Span plain1, Span plain2) noexcept +{ + bool ret = m_aead.Decrypt(cipher, aad, {m_packet_counter, m_rekey_counter}, plain1, plain2); + NextPacket(); + return ret; +} diff --git a/src/crypto/chacha20poly1305.h b/src/crypto/chacha20poly1305.h new file mode 100644 index 0000000000..8cbac36615 --- /dev/null +++ b/src/crypto/chacha20poly1305.h @@ -0,0 +1,148 @@ +// Copyright (c) 2023 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_CRYPTO_CHACHA20POLY1305_H +#define BITCOIN_CRYPTO_CHACHA20POLY1305_H + +#include +#include + +#include +#include +#include + +/** The AEAD_CHACHA20_POLY1305 authenticated encryption algorithm from RFC8439 section 2.8. */ +class AEADChaCha20Poly1305 +{ + /** Internal stream cipher. */ + ChaCha20 m_chacha20; + +public: + /** Expected size of key argument in constructor. */ + static constexpr unsigned KEYLEN = 32; + + /** Expansion when encrypting. */ + static constexpr unsigned EXPANSION = Poly1305::TAGLEN; + + /** Initialize an AEAD instance with a specified 32-byte key. */ + AEADChaCha20Poly1305(Span key) noexcept; + + /** Switch to another 32-byte key. */ + void SetKey(Span key) noexcept; + + /** 96-bit nonce type. */ + using Nonce96 = ChaCha20::Nonce96; + + /** Encrypt a message with a specified 96-bit nonce and aad. + * + * Requires cipher.size() = plain.size() + EXPANSION. + */ + void Encrypt(Span plain, Span aad, Nonce96 nonce, Span cipher) noexcept + { + Encrypt(plain, {}, aad, nonce, cipher); + } + + /** Encrypt a message (given split into plain1 + plain2) with a specified 96-bit nonce and aad. + * + * Requires cipher.size() = plain1.size() + plain2.size() + EXPANSION. + */ + void Encrypt(Span plain1, Span plain2, Span aad, Nonce96 nonce, Span cipher) noexcept; + + /** Decrypt a message with a specified 96-bit nonce and aad. Returns true if valid. + * + * Requires cipher.size() = plain.size() + EXPANSION. + */ + bool Decrypt(Span cipher, Span aad, Nonce96 nonce, Span plain) noexcept + { + return Decrypt(cipher, aad, nonce, plain, {}); + } + + /** Decrypt a message with a specified 96-bit nonce and aad and split the result. Returns true if valid. + * + * Requires cipher.size() = plain1.size() + plain2.size() + EXPANSION. + */ + bool Decrypt(Span cipher, Span aad, Nonce96 nonce, Span plain1, Span plain2) noexcept; + + /** Get a number of keystream bytes from the underlying stream cipher. + * + * This is equivalent to Encrypt() with plain set to that many zero bytes, and dropping the + * last EXPANSION bytes off the result. + */ + void Keystream(Nonce96 nonce, Span keystream) noexcept; +}; + +/** Forward-secure wrapper around AEADChaCha20Poly1305. + * + * This implements an AEAD which automatically increments the nonce on every encryption or + * decryption, and cycles keys after a predetermined number of encryptions or decryptions. + * + * See BIP324 for details. + */ +class FSChaCha20Poly1305 +{ +private: + /** Internal AEAD. */ + AEADChaCha20Poly1305 m_aead; + + /** Every how many iterations this cipher rekeys. */ + const uint32_t m_rekey_interval; + + /** The number of encryptions/decryptions since the last rekey. */ + uint32_t m_packet_counter{0}; + + /** The number of rekeys performed so far. */ + uint64_t m_rekey_counter{0}; + + /** Update counters (and if necessary, key) to transition to the next message. */ + void NextPacket() noexcept; + +public: + /** Length of keys expected by the constructor. */ + static constexpr auto KEYLEN = AEADChaCha20Poly1305::KEYLEN; + + /** Expansion when encrypting. */ + static constexpr auto EXPANSION = AEADChaCha20Poly1305::EXPANSION; + + // No copy or move to protect the secret. + FSChaCha20Poly1305(const FSChaCha20Poly1305&) = delete; + FSChaCha20Poly1305(FSChaCha20Poly1305&&) = delete; + FSChaCha20Poly1305& operator=(const FSChaCha20Poly1305&) = delete; + FSChaCha20Poly1305& operator=(FSChaCha20Poly1305&&) = delete; + + /** Construct an FSChaCha20Poly1305 cipher that rekeys every rekey_interval operations. */ + FSChaCha20Poly1305(Span key, uint32_t rekey_interval) noexcept : + m_aead(key), m_rekey_interval(rekey_interval) {} + + /** Encrypt a message with a specified aad. + * + * Requires cipher.size() = plain.size() + EXPANSION. + */ + void Encrypt(Span plain, Span aad, Span cipher) noexcept + { + Encrypt(plain, {}, aad, cipher); + } + + /** Encrypt a message (given split into plain1 + plain2) with a specified aad. + * + * Requires cipher.size() = plain.size() + EXPANSION. + */ + void Encrypt(Span plain1, Span plain2, Span aad, Span cipher) noexcept; + + /** Decrypt a message with a specified aad. Returns true if valid. + * + * Requires cipher.size() = plain.size() + EXPANSION. + */ + bool Decrypt(Span cipher, Span aad, Span plain) noexcept + { + return Decrypt(cipher, aad, plain, {}); + } + + /** Decrypt a message with a specified aad and split the result. Returns true if valid. + * + * Requires cipher.size() = plain1.size() + plain2.size() + EXPANSION. + */ + bool Decrypt(Span cipher, Span aad, Span plain1, Span plain2) noexcept; +}; + +#endif // BITCOIN_CRYPTO_CHACHA20POLY1305_H diff --git a/src/test/crypto_tests.cpp b/src/test/crypto_tests.cpp index 2b522231be..bf3afdbe1d 100644 --- a/src/test/crypto_tests.cpp +++ b/src/test/crypto_tests.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -256,6 +257,97 @@ static void TestPoly1305(const std::string &hexmessage, const std::string &hexke } } +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) +{ + auto plain_uc = ParseHex(plain_hex); + auto plain = std::vector((std::byte*)plain_uc.data(), (std::byte*)plain_uc.data() + plain_uc.size()); + auto aad = ParseHex(aad_hex); + auto key = ParseHex(key_hex); + auto expected_cipher_uc = ParseHex(cipher_hex); + auto expected_cipher = std::vector((std::byte*)expected_cipher_uc.data(), (std::byte*)expected_cipher_uc.data() + expected_cipher_uc.size()); + + for (int i = 0; i < 10; ++i) { + // During i=0, use single-plain Encrypt/Decrypt; others use a split at prefix. + size_t prefix = i ? InsecureRandRange(plain.size() + 1) : plain.size(); + // Encrypt. + std::vector cipher(plain.size() + AEADChaCha20Poly1305::EXPANSION); + AEADChaCha20Poly1305 aead{MakeByteSpan(key)}; + if (i == 0) { + aead.Encrypt(plain, MakeByteSpan(aad), nonce, cipher); + } else { + aead.Encrypt(Span{plain}.first(prefix), Span{plain}.subspan(prefix), MakeByteSpan(aad), nonce, cipher); + } + BOOST_CHECK(cipher == expected_cipher); + + // Decrypt. + std::vector decipher(cipher.size() - AEADChaCha20Poly1305::EXPANSION); + bool ret{false}; + if (i == 0) { + ret = aead.Decrypt(cipher, MakeByteSpan(aad), nonce, decipher); + } else { + ret = aead.Decrypt(cipher, MakeByteSpan(aad), nonce, Span{decipher}.first(prefix), Span{decipher}.subspan(prefix)); + } + BOOST_CHECK(ret); + BOOST_CHECK(decipher == plain); + } + + // Test Keystream output. + std::vector keystream(plain.size()); + AEADChaCha20Poly1305 aead{MakeByteSpan(key)}; + aead.Keystream(nonce, keystream); + for (size_t i = 0; i < plain.size(); ++i) { + BOOST_CHECK_EQUAL(plain[i] ^ keystream[i], expected_cipher[i]); + } +} + +static void TestFSChaCha20Poly1305(const std::string& plain_hex, const std::string& aad_hex, const std::string& key_hex, uint64_t msg_idx, const std::string& cipher_hex) +{ + auto plain_uc = ParseHex(plain_hex); + auto plain = std::vector((std::byte*)plain_uc.data(), (std::byte*)plain_uc.data() + plain_uc.size()); + auto aad = ParseHex(aad_hex); + auto key = ParseHex(key_hex); + auto expected_cipher_uc = ParseHex(cipher_hex); + auto expected_cipher = std::vector((std::byte*)expected_cipher_uc.data(), (std::byte*)expected_cipher_uc.data() + expected_cipher_uc.size()); + std::vector cipher(plain.size() + FSChaCha20Poly1305::EXPANSION); + + for (int it = 0; it < 10; ++it) { + // During it==0 we use the single-plain Encrypt/Decrypt; others use a split at prefix. + size_t prefix = it ? InsecureRandRange(plain.size() + 1) : plain.size(); + std::byte dummy_tag[FSChaCha20Poly1305::EXPANSION] = {{}}; + + // Do msg_idx dummy encryptions to seek to the correct packet. + FSChaCha20Poly1305 enc_aead{MakeByteSpan(key), 224}; + for (uint64_t i = 0; i < msg_idx; ++i) { + enc_aead.Encrypt(Span{dummy_tag}.first(0), Span{dummy_tag}.first(0), dummy_tag); + } + + // Invoke single-plain or plain1/plain2 Encrypt. + if (it == 0) { + enc_aead.Encrypt(plain, MakeByteSpan(aad), cipher); + } else { + enc_aead.Encrypt(Span{plain}.first(prefix), Span{plain}.subspan(prefix), MakeByteSpan(aad), cipher); + } + BOOST_CHECK(cipher == expected_cipher); + + // Do msg_idx dummy decryptions to seek to the correct packet. + FSChaCha20Poly1305 dec_aead{MakeByteSpan(key), 224}; + for (uint64_t i = 0; i < msg_idx; ++i) { + dec_aead.Decrypt(dummy_tag, Span{dummy_tag}.first(0), Span{dummy_tag}.first(0)); + } + + // Invoke single-plain or plain1/plain2 Decrypt. + std::vector decipher(cipher.size() - AEADChaCha20Poly1305::EXPANSION); + bool ret{false}; + if (it == 0) { + ret = dec_aead.Decrypt(cipher, MakeByteSpan(aad), decipher); + } else { + ret = dec_aead.Decrypt(cipher, MakeByteSpan(aad), Span{decipher}.first(prefix), Span{decipher}.subspan(prefix)); + } + BOOST_CHECK(ret); + BOOST_CHECK(decipher == plain); + } +} + static std::string LongTestString() { std::string ret; @@ -859,6 +951,87 @@ BOOST_AUTO_TEST_CASE(poly1305_testvector) "0e410fa9d7a40ac582e77546be9a72bb"); } +BOOST_AUTO_TEST_CASE(chacha20poly1305_testvectors) +{ + // Note that in our implementation, the authentication is suffixed to the ciphertext. + // The RFC test vectors specify them separately. + + // RFC 8439 Example from section 2.8.2 + TestChaCha20Poly1305("4c616469657320616e642047656e746c656d656e206f662074686520636c6173" + "73206f66202739393a204966204920636f756c64206f6666657220796f75206f" + "6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73" + "637265656e20776f756c642062652069742e", + "50515253c0c1c2c3c4c5c6c7", + "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", + {7, 0x4746454443424140}, + "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6" + "3dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b36" + "92ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc" + "3ff4def08e4b7a9de576d26586cec64b61161ae10b594f09e26a7e902ecbd060" + "0691"); + + // RFC 8439 Test vector A.5 + TestChaCha20Poly1305("496e7465726e65742d4472616674732061726520647261667420646f63756d65" + "6e74732076616c696420666f722061206d6178696d756d206f6620736978206d" + "6f6e74687320616e64206d617920626520757064617465642c207265706c6163" + "65642c206f72206f62736f6c65746564206279206f7468657220646f63756d65" + "6e747320617420616e792074696d652e20497420697320696e617070726f7072" + "6961746520746f2075736520496e7465726e65742d4472616674732061732072" + "65666572656e6365206d6174657269616c206f7220746f206369746520746865" + "6d206f74686572207468616e206173202fe2809c776f726b20696e2070726f67" + "726573732e2fe2809d", + "f33388860000000000004e91", + "1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0", + {0, 0x0807060504030201}, + "64a0861575861af460f062c79be643bd5e805cfd345cf389f108670ac76c8cb2" + "4c6cfc18755d43eea09ee94e382d26b0bdb7b73c321b0100d4f03b7f355894cf" + "332f830e710b97ce98c8a84abd0b948114ad176e008d33bd60f982b1ff37c855" + "9797a06ef4f0ef61c186324e2b3506383606907b6a7c02b0f9f6157b53c867e4" + "b9166c767b804d46a59b5216cde7a4e99040c5a40433225ee282a1b0a06c523e" + "af4534d7f83fa1155b0047718cbc546a0d072b04b3564eea1b422273f548271a" + "0bb2316053fa76991955ebd63159434ecebb4e466dae5a1073a6727627097a10" + "49e617d91d361094fa68f0ff77987130305beaba2eda04df997b714d6c6f2c29" + "a6ad5cb4022b02709beead9d67890cbb22392336fea1851f38"); + + // Test vectors exercising aad and plaintext which are multiples of 16 bytes. + TestChaCha20Poly1305("8d2d6a8befd9716fab35819eaac83b33269afb9f1a00fddf66095a6c0cd91951" + "a6b7ad3db580be0674c3f0b55f618e34", + "", + "72ddc73f07101282bbbcf853b9012a9f9695fc5d36b303a97fd0845d0314e0c3", + {0x3432b75f, 0xb3585537eb7f4024}, + "f760b8224fb2a317b1b07875092606131232a5b86ae142df5df1c846a7f6341a" + "f2564483dd77f836be45e6230808ffe402a6f0a3e8be074b3d1f4ea8a7b09451"); + TestChaCha20Poly1305("", + "36970d8a704c065de16250c18033de5a400520ac1b5842b24551e5823a3314f3" + "946285171e04a81ebfbe3566e312e74ab80e94c7dd2ff4e10de0098a58d0f503", + "77adda51d6730b9ad6c995658cbd49f581b2547e7c0c08fcc24ceec797461021", + {0x1f90da88, 0x75dafa3ef84471a4}, + "aaae5bb81e8407c94b2ae86ae0c7efbe"); + + // FSChaCha20Poly1305 tests. + TestFSChaCha20Poly1305("d6a4cb04ef0f7c09c1866ed29dc24d820e75b0491032a51b4c3366f9ca35c19e" + "a3047ec6be9d45f9637b63e1cf9eb4c2523a5aab7b851ebeba87199db0e839cf" + "0d5c25e50168306377aedbe9089fd2463ded88b83211cf51b73b150608cc7a60" + "0d0f11b9a742948482e1b109d8faf15b450aa7322e892fa2208c6691e3fecf4c" + "711191b14d75a72147", + "786cb9b6ebf44288974cf0", + "5c9e1c3951a74fba66708bf9d2c217571684556b6a6a3573bff2847d38612654", + 500, + "9dcebbd3281ea3dd8e9a1ef7d55a97abd6743e56ebc0c190cb2c4e14160b385e" + "0bf508dddf754bd02c7c208447c131ce23e47a4a14dfaf5dd8bc601323950f75" + "4e05d46e9232f83fc5120fbbef6f5347a826ec79a93820718d4ec7a2b7cfaaa4" + "4b21e16d726448b62f803811aff4f6d827ed78e738ce8a507b81a8ae13131192" + "8039213de18a5120dc9b7370baca878f50ff254418de3da50c"); + TestFSChaCha20Poly1305("8349b7a2690b63d01204800c288ff1138a1d473c832c90ea8b3fc102d0bb3adc" + "44261b247c7c3d6760bfbe979d061c305f46d94c0582ac3099f0bf249f8cb234", + "", + "3bd2093fcbcb0d034d8c569583c5425c1a53171ea299f8cc3bbf9ae3530adfce", + 60000, + "30a6757ff8439b975363f166a0fa0e36722ab35936abd704297948f45083f4d4" + "99433137ce931f7fca28a0acd3bc30f57b550acbc21cbd45bbef0739d9caf30c" + "14b94829deb27f0b1923a2af704ae5d6"); +} + BOOST_AUTO_TEST_CASE(countbits_tests) { for (unsigned int i = 0; i <= 64; ++i) { diff --git a/src/test/test_gridcoin.h b/src/test/test_gridcoin.h index 25235c39ed..4926dafd50 100644 --- a/src/test/test_gridcoin.h +++ b/src/test/test_gridcoin.h @@ -18,4 +18,13 @@ static inline uint64_t InsecureRandRange(uint64_t range) { return g_insecure_ran static inline bool InsecureRandBool() { return g_insecure_rand_ctx.randbool(); } static inline std::vector InsecureRandBytes(size_t len) { return g_insecure_rand_ctx.randbytes(len); } +// Enable BOOST_CHECK_EQUAL for enum class types +namespace std { +template +std::ostream& operator<<(typename std::enable_if::value, std::ostream>::type& stream, const T& e) +{ + return stream << static_cast::type>(e); +} +} // namespace std + #endif From a7b361f20f71cc6e240c4740deb56675d06b8369 Mon Sep 17 00:00:00 2001 From: div72 Date: Fri, 16 Feb 2024 17:53:12 +0300 Subject: [PATCH 5/6] mnemonics: add utility functions for (en/de)coding --- src/CMakeLists.txt | 1 + src/Makefile.am | 2 + src/arith_uint256.cpp | 4 ++ src/arith_uint256.h | 4 ++ src/gridcoin/mnemonics.cpp | 130 ++++++++++++++++++++++++++++++++++++ src/gridcoin/mnemonics.h | 30 +++++++++ src/test/gridcoin_tests.cpp | 23 +++++++ src/test/test_gridcoin.h | 5 ++ 8 files changed, 199 insertions(+) create mode 100644 src/gridcoin/mnemonics.cpp create mode 100644 src/gridcoin/mnemonics.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 479d210bee..8512e60e51 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -127,6 +127,7 @@ add_library(gridcoin_util STATIC gridcoin/cpid.cpp gridcoin/gridcoin.cpp gridcoin/mrc.cpp + gridcoin/mnemonics.cpp gridcoin/project.cpp gridcoin/protocol.cpp gridcoin/quorum.cpp diff --git a/src/Makefile.am b/src/Makefile.am index 043c100ec1..ead9a13514 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -119,6 +119,7 @@ GRIDCOIN_CORE_H = \ gridcoin/cpid.h \ gridcoin/gridcoin.h \ gridcoin/magnitude.h \ + gridcoin/mnemonics.h \ gridcoin/mrc.h \ gridcoin/project.h \ gridcoin/protocol.h \ @@ -250,6 +251,7 @@ GRIDCOIN_CORE_CPP = addrdb.cpp \ gridcoin/contract/registry.cpp \ gridcoin/cpid.cpp \ gridcoin/gridcoin.cpp \ + gridcoin/mnemonics.cpp \ gridcoin/mrc.cpp \ gridcoin/project.cpp \ gridcoin/protocol.cpp \ diff --git a/src/arith_uint256.cpp b/src/arith_uint256.cpp index 946c47a384..f4ee5c98f4 100644 --- a/src/arith_uint256.cpp +++ b/src/arith_uint256.cpp @@ -276,3 +276,7 @@ arith_uint320::arith_uint320(const uint256& b) { std::memcpy(pn, b.data(), b.size()); } +// Explicit instantiation for base_uint<352> for mnemonic parsing. +template base_uint<352>& base_uint<352>::operator<<=(unsigned int); +template base_uint<352>& base_uint<352>::operator>>=(unsigned int); + diff --git a/src/arith_uint256.h b/src/arith_uint256.h index 26d6a45ca7..119388c261 100644 --- a/src/arith_uint256.h +++ b/src/arith_uint256.h @@ -229,6 +229,10 @@ class base_uint void SetHex(const std::string& str); std::string ToString() const; + unsigned char* data() { + return reinterpret_cast(pn); + } + unsigned int size() const { return sizeof(pn); diff --git a/src/gridcoin/mnemonics.cpp b/src/gridcoin/mnemonics.cpp new file mode 100644 index 0000000000..84db56318e --- /dev/null +++ b/src/gridcoin/mnemonics.cpp @@ -0,0 +1,130 @@ +// Copyright (c) 2024 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or https://opensource.org/licenses/mit-license.php. + +#include + +#include +#include +#include + +#include + +using namespace GRC::Mnemonics; + +// Length of longest word in the wordlist. +static constexpr unsigned LONGEST_WORD_LENGTH = 8; + +static const SecureString WORDLIST[2048] = {"abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident", "account", "accuse", "achieve", "acid", "acoustic", "acquire", "across", "act", "action", "actor", "actress", "actual", "adapt", "add", "addict", "address", "adjust", "admit", "adult", "advance", "advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent", "agree", "ahead", "aim", "air", "airport", "aisle", "alarm", "album", "alcohol", "alert", "alien", "all", "alley", "allow", "almost", "alone", "alpha", "already", "also", "alter", "always", "amateur", "amazing", "among", "amount", "amused", "analyst", "anchor", "ancient", "anger", "angle", "angry", "animal", "ankle", "announce", "annual", "another", "answer", "antenna", "antique", "anxiety", "any", "apart", "apology", "appear", "apple", "approve", "april", "arch", "arctic", "area", "arena", "argue", "arm", "armed", "armor", "army", "around", "arrange", "arrest", "arrive", "arrow", "art", "artefact", "artist", "artwork", "ask", "aspect", "assault", "asset", "assist", "assume", "asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", "auction", "audit", "august", "aunt", "author", "auto", "autumn", "average", "avocado", "avoid", "awake", "aware", "away", "awesome", "awful", "awkward", "axis", "baby", "bachelor", "bacon", "badge", "bag", "balance", "balcony", "ball", "bamboo", "banana", "banner", "bar", "barely", "bargain", "barrel", "base", "basic", "basket", "battle", "beach", "bean", "beauty", "because", "become", "beef", "before", "begin", "behave", "behind", "believe", "below", "belt", "bench", "benefit", "best", "betray", "better", "between", "beyond", "bicycle", "bid", "bike", "bind", "biology", "bird", "birth", "bitter", "black", "blade", "blame", "blanket", "blast", "bleak", "bless", "blind", "blood", "blossom", "blouse", "blue", "blur", "blush", "board", "boat", "body", "boil", "bomb", "bone", "bonus", "book", "boost", "border", "boring", "borrow", "boss", "bottom", "bounce", "box", "boy", "bracket", "brain", "brand", "brass", "brave", "bread", "breeze", "brick", "bridge", "brief", "bright", "bring", "brisk", "broccoli", "broken", "bronze", "broom", "brother", "brown", "brush", "bubble", "buddy", "budget", "buffalo", "build", "bulb", "bulk", "bullet", "bundle", "bunker", "burden", "burger", "burst", "bus", "business", "busy", "butter", "buyer", "buzz", "cabbage", "cabin", "cable", "cactus", "cage", "cake", "call", "calm", "camera", "camp", "can", "canal", "cancel", "candy", "cannon", "canoe", "canvas", "canyon", "capable", "capital", "captain", "car", "carbon", "card", "cargo", "carpet", "carry", "cart", "case", "cash", "casino", "castle", "casual", "cat", "catalog", "catch", "category", "cattle", "caught", "cause", "caution", "cave", "ceiling", "celery", "cement", "census", "century", "cereal", "certain", "chair", "chalk", "champion", "change", "chaos", "chapter", "charge", "chase", "chat", "cheap", "check", "cheese", "chef", "cherry", "chest", "chicken", "chief", "child", "chimney", "choice", "choose", "chronic", "chuckle", "chunk", "churn", "cigar", "cinnamon", "circle", "citizen", "city", "civil", "claim", "clap", "clarify", "claw", "clay", "clean", "clerk", "clever", "click", "client", "cliff", "climb", "clinic", "clip", "clock", "clog", "close", "cloth", "cloud", "clown", "club", "clump", "cluster", "clutch", "coach", "coast", "coconut", "code", "coffee", "coil", "coin", "collect", "color", "column", "combine", "come", "comfort", "comic", "common", "company", "concert", "conduct", "confirm", "congress", "connect", "consider", "control", "convince", "cook", "cool", "copper", "copy", "coral", "core", "corn", "correct", "cost", "cotton", "couch", "country", "couple", "course", "cousin", "cover", "coyote", "crack", "cradle", "craft", "cram", "crane", "crash", "crater", "crawl", "crazy", "cream", "credit", "creek", "crew", "cricket", "crime", "crisp", "critic", "crop", "cross", "crouch", "crowd", "crucial", "cruel", "cruise", "crumble", "crunch", "crush", "cry", "crystal", "cube", "culture", "cup", "cupboard", "curious", "current", "curtain", "curve", "cushion", "custom", "cute", "cycle", "dad", "damage", "damp", "dance", "danger", "daring", "dash", "daughter", "dawn", "day", "deal", "debate", "debris", "decade", "december", "decide", "decline", "decorate", "decrease", "deer", "defense", "define", "defy", "degree", "delay", "deliver", "demand", "demise", "denial", "dentist", "deny", "depart", "depend", "deposit", "depth", "deputy", "derive", "describe", "desert", "design", "desk", "despair", "destroy", "detail", "detect", "develop", "device", "devote", "diagram", "dial", "diamond", "diary", "dice", "diesel", "diet", "differ", "digital", "dignity", "dilemma", "dinner", "dinosaur", "direct", "dirt", "disagree", "discover", "disease", "dish", "dismiss", "disorder", "display", "distance", "divert", "divide", "divorce", "dizzy", "doctor", "document", "dog", "doll", "dolphin", "domain", "donate", "donkey", "donor", "door", "dose", "double", "dove", "draft", "dragon", "drama", "drastic", "draw", "dream", "dress", "drift", "drill", "drink", "drip", "drive", "drop", "drum", "dry", "duck", "dumb", "dune", "during", "dust", "dutch", "duty", "dwarf", "dynamic", "eager", "eagle", "early", "earn", "earth", "easily", "east", "easy", "echo", "ecology", "economy", "edge", "edit", "educate", "effort", "egg", "eight", "either", "elbow", "elder", "electric", "elegant", "element", "elephant", "elevator", "elite", "else", "embark", "embody", "embrace", "emerge", "emotion", "employ", "empower", "empty", "enable", "enact", "end", "endless", "endorse", "enemy", "energy", "enforce", "engage", "engine", "enhance", "enjoy", "enlist", "enough", "enrich", "enroll", "ensure", "enter", "entire", "entry", "envelope", "episode", "equal", "equip", "era", "erase", "erode", "erosion", "error", "erupt", "escape", "essay", "essence", "estate", "eternal", "ethics", "evidence", "evil", "evoke", "evolve", "exact", "example", "excess", "exchange", "excite", "exclude", "excuse", "execute", "exercise", "exhaust", "exhibit", "exile", "exist", "exit", "exotic", "expand", "expect", "expire", "explain", "expose", "express", "extend", "extra", "eye", "eyebrow", "fabric", "face", "faculty", "fade", "faint", "faith", "fall", "false", "fame", "family", "famous", "fan", "fancy", "fantasy", "farm", "fashion", "fat", "fatal", "father", "fatigue", "fault", "favorite", "feature", "february", "federal", "fee", "feed", "feel", "female", "fence", "festival", "fetch", "fever", "few", "fiber", "fiction", "field", "figure", "file", "film", "filter", "final", "find", "fine", "finger", "finish", "fire", "firm", "first", "fiscal", "fish", "fit", "fitness", "fix", "flag", "flame", "flash", "flat", "flavor", "flee", "flight", "flip", "float", "flock", "floor", "flower", "fluid", "flush", "fly", "foam", "focus", "fog", "foil", "fold", "follow", "food", "foot", "force", "forest", "forget", "fork", "fortune", "forum", "forward", "fossil", "foster", "found", "fox", "fragile", "frame", "frequent", "fresh", "friend", "fringe", "frog", "front", "frost", "frown", "frozen", "fruit", "fuel", "fun", "funny", "furnace", "fury", "future", "gadget", "gain", "galaxy", "gallery", "game", "gap", "garage", "garbage", "garden", "garlic", "garment", "gas", "gasp", "gate", "gather", "gauge", "gaze", "general", "genius", "genre", "gentle", "genuine", "gesture", "ghost", "giant", "gift", "giggle", "ginger", "giraffe", "girl", "give", "glad", "glance", "glare", "glass", "glide", "glimpse", "globe", "gloom", "glory", "glove", "glow", "glue", "goat", "goddess", "gold", "good", "goose", "gorilla", "gospel", "gossip", "govern", "gown", "grab", "grace", "grain", "grant", "grape", "grass", "gravity", "great", "green", "grid", "grief", "grit", "grocery", "group", "grow", "grunt", "guard", "guess", "guide", "guilt", "guitar", "gun", "gym", "habit", "hair", "half", "hammer", "hamster", "hand", "happy", "harbor", "hard", "harsh", "harvest", "hat", "have", "hawk", "hazard", "head", "health", "heart", "heavy", "hedgehog", "height", "hello", "helmet", "help", "hen", "hero", "hidden", "high", "hill", "hint", "hip", "hire", "history", "hobby", "hockey", "hold", "hole", "holiday", "hollow", "home", "honey", "hood", "hope", "horn", "horror", "horse", "hospital", "host", "hotel", "hour", "hover", "hub", "huge", "human", "humble", "humor", "hundred", "hungry", "hunt", "hurdle", "hurry", "hurt", "husband", "hybrid", "ice", "icon", "idea", "identify", "idle", "ignore", "ill", "illegal", "illness", "image", "imitate", "immense", "immune", "impact", "impose", "improve", "impulse", "inch", "include", "income", "increase", "index", "indicate", "indoor", "industry", "infant", "inflict", "inform", "inhale", "inherit", "initial", "inject", "injury", "inmate", "inner", "innocent", "input", "inquiry", "insane", "insect", "inside", "inspire", "install", "intact", "interest", "into", "invest", "invite", "involve", "iron", "island", "isolate", "issue", "item", "ivory", "jacket", "jaguar", "jar", "jazz", "jealous", "jeans", "jelly", "jewel", "job", "join", "joke", "journey", "joy", "judge", "juice", "jump", "jungle", "junior", "junk", "just", "kangaroo", "keen", "keep", "ketchup", "key", "kick", "kid", "kidney", "kind", "kingdom", "kiss", "kit", "kitchen", "kite", "kitten", "kiwi", "knee", "knife", "knock", "know", "lab", "label", "labor", "ladder", "lady", "lake", "lamp", "language", "laptop", "large", "later", "latin", "laugh", "laundry", "lava", "law", "lawn", "lawsuit", "layer", "lazy", "leader", "leaf", "learn", "leave", "lecture", "left", "leg", "legal", "legend", "leisure", "lemon", "lend", "length", "lens", "leopard", "lesson", "letter", "level", "liar", "liberty", "library", "license", "life", "lift", "light", "like", "limb", "limit", "link", "lion", "liquid", "list", "little", "live", "lizard", "load", "loan", "lobster", "local", "lock", "logic", "lonely", "long", "loop", "lottery", "loud", "lounge", "love", "loyal", "lucky", "luggage", "lumber", "lunar", "lunch", "luxury", "lyrics", "machine", "mad", "magic", "magnet", "maid", "mail", "main", "major", "make", "mammal", "man", "manage", "mandate", "mango", "mansion", "manual", "maple", "marble", "march", "margin", "marine", "market", "marriage", "mask", "mass", "master", "match", "material", "math", "matrix", "matter", "maximum", "maze", "meadow", "mean", "measure", "meat", "mechanic", "medal", "media", "melody", "melt", "member", "memory", "mention", "menu", "mercy", "merge", "merit", "merry", "mesh", "message", "metal", "method", "middle", "midnight", "milk", "million", "mimic", "mind", "minimum", "minor", "minute", "miracle", "mirror", "misery", "miss", "mistake", "mix", "mixed", "mixture", "mobile", "model", "modify", "mom", "moment", "monitor", "monkey", "monster", "month", "moon", "moral", "more", "morning", "mosquito", "mother", "motion", "motor", "mountain", "mouse", "move", "movie", "much", "muffin", "mule", "multiply", "muscle", "museum", "mushroom", "music", "must", "mutual", "myself", "mystery", "myth", "naive", "name", "napkin", "narrow", "nasty", "nation", "nature", "near", "neck", "need", "negative", "neglect", "neither", "nephew", "nerve", "nest", "net", "network", "neutral", "never", "news", "next", "nice", "night", "noble", "noise", "nominee", "noodle", "normal", "north", "nose", "notable", "note", "nothing", "notice", "novel", "now", "nuclear", "number", "nurse", "nut", "oak", "obey", "object", "oblige", "obscure", "observe", "obtain", "obvious", "occur", "ocean", "october", "odor", "off", "offer", "office", "often", "oil", "okay", "old", "olive", "olympic", "omit", "once", "one", "onion", "online", "only", "open", "opera", "opinion", "oppose", "option", "orange", "orbit", "orchard", "order", "ordinary", "organ", "orient", "original", "orphan", "ostrich", "other", "outdoor", "outer", "output", "outside", "oval", "oven", "over", "own", "owner", "oxygen", "oyster", "ozone", "pact", "paddle", "page", "pair", "palace", "palm", "panda", "panel", "panic", "panther", "paper", "parade", "parent", "park", "parrot", "party", "pass", "patch", "path", "patient", "patrol", "pattern", "pause", "pave", "payment", "peace", "peanut", "pear", "peasant", "pelican", "pen", "penalty", "pencil", "people", "pepper", "perfect", "permit", "person", "pet", "phone", "photo", "phrase", "physical", "piano", "picnic", "picture", "piece", "pig", "pigeon", "pill", "pilot", "pink", "pioneer", "pipe", "pistol", "pitch", "pizza", "place", "planet", "plastic", "plate", "play", "please", "pledge", "pluck", "plug", "plunge", "poem", "poet", "point", "polar", "pole", "police", "pond", "pony", "pool", "popular", "portion", "position", "possible", "post", "potato", "pottery", "poverty", "powder", "power", "practice", "praise", "predict", "prefer", "prepare", "present", "pretty", "prevent", "price", "pride", "primary", "print", "priority", "prison", "private", "prize", "problem", "process", "produce", "profit", "program", "project", "promote", "proof", "property", "prosper", "protect", "proud", "provide", "public", "pudding", "pull", "pulp", "pulse", "pumpkin", "punch", "pupil", "puppy", "purchase", "purity", "purpose", "purse", "push", "put", "puzzle", "pyramid", "quality", "quantum", "quarter", "question", "quick", "quit", "quiz", "quote", "rabbit", "raccoon", "race", "rack", "radar", "radio", "rail", "rain", "raise", "rally", "ramp", "ranch", "random", "range", "rapid", "rare", "rate", "rather", "raven", "raw", "razor", "ready", "real", "reason", "rebel", "rebuild", "recall", "receive", "recipe", "record", "recycle", "reduce", "reflect", "reform", "refuse", "region", "regret", "regular", "reject", "relax", "release", "relief", "rely", "remain", "remember", "remind", "remove", "render", "renew", "rent", "reopen", "repair", "repeat", "replace", "report", "require", "rescue", "resemble", "resist", "resource", "response", "result", "retire", "retreat", "return", "reunion", "reveal", "review", "reward", "rhythm", "rib", "ribbon", "rice", "rich", "ride", "ridge", "rifle", "right", "rigid", "ring", "riot", "ripple", "risk", "ritual", "rival", "river", "road", "roast", "robot", "robust", "rocket", "romance", "roof", "rookie", "room", "rose", "rotate", "rough", "round", "route", "royal", "rubber", "rude", "rug", "rule", "run", "runway", "rural", "sad", "saddle", "sadness", "safe", "sail", "salad", "salmon", "salon", "salt", "salute", "same", "sample", "sand", "satisfy", "satoshi", "sauce", "sausage", "save", "say", "scale", "scan", "scare", "scatter", "scene", "scheme", "school", "science", "scissors", "scorpion", "scout", "scrap", "screen", "script", "scrub", "sea", "search", "season", "seat", "second", "secret", "section", "security", "seed", "seek", "segment", "select", "sell", "seminar", "senior", "sense", "sentence", "series", "service", "session", "settle", "setup", "seven", "shadow", "shaft", "shallow", "share", "shed", "shell", "sheriff", "shield", "shift", "shine", "ship", "shiver", "shock", "shoe", "shoot", "shop", "short", "shoulder", "shove", "shrimp", "shrug", "shuffle", "shy", "sibling", "sick", "side", "siege", "sight", "sign", "silent", "silk", "silly", "silver", "similar", "simple", "since", "sing", "siren", "sister", "situate", "six", "size", "skate", "sketch", "ski", "skill", "skin", "skirt", "skull", "slab", "slam", "sleep", "slender", "slice", "slide", "slight", "slim", "slogan", "slot", "slow", "slush", "small", "smart", "smile", "smoke", "smooth", "snack", "snake", "snap", "sniff", "snow", "soap", "soccer", "social", "sock", "soda", "soft", "solar", "soldier", "solid", "solution", "solve", "someone", "song", "soon", "sorry", "sort", "soul", "sound", "soup", "source", "south", "space", "spare", "spatial", "spawn", "speak", "special", "speed", "spell", "spend", "sphere", "spice", "spider", "spike", "spin", "spirit", "split", "spoil", "sponsor", "spoon", "sport", "spot", "spray", "spread", "spring", "spy", "square", "squeeze", "squirrel", "stable", "stadium", "staff", "stage", "stairs", "stamp", "stand", "start", "state", "stay", "steak", "steel", "stem", "step", "stereo", "stick", "still", "sting", "stock", "stomach", "stone", "stool", "story", "stove", "strategy", "street", "strike", "strong", "struggle", "student", "stuff", "stumble", "style", "subject", "submit", "subway", "success", "such", "sudden", "suffer", "sugar", "suggest", "suit", "summer", "sun", "sunny", "sunset", "super", "supply", "supreme", "sure", "surface", "surge", "surprise", "surround", "survey", "suspect", "sustain", "swallow", "swamp", "swap", "swarm", "swear", "sweet", "swift", "swim", "swing", "switch", "sword", "symbol", "symptom", "syrup", "system", "table", "tackle", "tag", "tail", "talent", "talk", "tank", "tape", "target", "task", "taste", "tattoo", "taxi", "teach", "team", "tell", "ten", "tenant", "tennis", "tent", "term", "test", "text", "thank", "that", "theme", "then", "theory", "there", "they", "thing", "this", "thought", "three", "thrive", "throw", "thumb", "thunder", "ticket", "tide", "tiger", "tilt", "timber", "time", "tiny", "tip", "tired", "tissue", "title", "toast", "tobacco", "today", "toddler", "toe", "together", "toilet", "token", "tomato", "tomorrow", "tone", "tongue", "tonight", "tool", "tooth", "top", "topic", "topple", "torch", "tornado", "tortoise", "toss", "total", "tourist", "toward", "tower", "town", "toy", "track", "trade", "traffic", "tragic", "train", "transfer", "trap", "trash", "travel", "tray", "treat", "tree", "trend", "trial", "tribe", "trick", "trigger", "trim", "trip", "trophy", "trouble", "truck", "true", "truly", "trumpet", "trust", "truth", "try", "tube", "tuition", "tumble", "tuna", "tunnel", "turkey", "turn", "turtle", "twelve", "twenty", "twice", "twin", "twist", "two", "type", "typical", "ugly", "umbrella", "unable", "unaware", "uncle", "uncover", "under", "undo", "unfair", "unfold", "unhappy", "uniform", "unique", "unit", "universe", "unknown", "unlock", "until", "unusual", "unveil", "update", "upgrade", "uphold", "upon", "upper", "upset", "urban", "urge", "usage", "use", "used", "useful", "useless", "usual", "utility", "vacant", "vacuum", "vague", "valid", "valley", "valve", "van", "vanish", "vapor", "various", "vast", "vault", "vehicle", "velvet", "vendor", "venture", "venue", "verb", "verify", "version", "very", "vessel", "veteran", "viable", "vibrant", "vicious", "victory", "video", "view", "village", "vintage", "violin", "virtual", "virus", "visa", "visit", "visual", "vital", "vivid", "vocal", "voice", "void", "volcano", "volume", "vote", "voyage", "wage", "wagon", "wait", "walk", "wall", "walnut", "want", "warfare", "warm", "warrior", "wash", "wasp", "waste", "water", "wave", "way", "wealth", "weapon", "wear", "weasel", "weather", "web", "wedding", "weekend", "weird", "welcome", "west", "wet", "whale", "what", "wheat", "wheel", "when", "where", "whip", "whisper", "wide", "width", "wife", "wild", "will", "win", "window", "wine", "wing", "wink", "winner", "winter", "wire", "wisdom", "wise", "wish", "witness", "wolf", "woman", "wonder", "wood", "wool", "word", "work", "world", "worry", "worth", "wrap", "wreck", "wrestle", "wrist", "write", "wrong", "yard", "year", "yellow", "you", "young", "youth", "zebra", "zero", "zone", "zoo"}; + +static_assert((1 << WORDLIST_BIT_LENGTH) == std::size(WORDLIST)); + +// Timing attack safe word to index mapper. Returns -1 if the word is not found. +static int timingsafe_wordfind(const SecureString& phrase, int word_start, int word_end) { + int ret = -1; + int word_length = word_end - word_start; + int last_char_index = std::min(word_end - 1, (int)phrase.size() - 1); + + for (int i = 0; i < (int)std::size(WORDLIST); ++i) { + int other_last_char_index = WORDLIST[i].size() - 1; + bool found = word_length == other_last_char_index + 1; + + for (int j = 0; j < (int)LONGEST_WORD_LENGTH + 1; ++j) { + found &= (phrase[std::min(word_start + j, last_char_index)] == WORDLIST[i][std::min(j, other_last_char_index)]); + } + + found &= (ret == -1); + ret = found * i + (1 - found) * ret; + } + + return ret; +} + +// Timing attack safe space finder for splitting the words. Points to the end if not found. +static int timingsafe_spacefind(const SecureString& phrase, int start) { + int ret = -1; + int last_char_index = phrase.size() - 1; + + for (int i = start; i < start + (int)LONGEST_WORD_LENGTH + 1; ++i) { + bool is_whitespace = (phrase[std::min(i, last_char_index)] == ' '); + is_whitespace &= (ret == -1); + ret = is_whitespace * i + (1 - is_whitespace) * ret; + } + + bool found = ret != -1; + return found * ret + (1 - found) * phrase.size(); +} + +static void timingsafe_memcpy(Span dest, Span src, int max_size) { + int last_char_index = src.size() - 1; + + assert(max_size >= (int)src.size()); + assert(dest.size() >= src.size()); + + for (int i = 0; i < max_size; ++i) { + int index = std::min(i, last_char_index); + dest[index] = (std::byte)src[index]; + } +} + +bool GRC::Mnemonics::DecodeSeedPhrase(const SecureString& seed_phrase, Span data_out) { + base_uint data; + + assert(data_out.size() == ENCIPHERED_LENGTH); + + auto word_count = 0; + SecureString::size_type word_start = 0; + do { + const auto word_end = timingsafe_spacefind(seed_phrase, word_start); + const auto index = timingsafe_wordfind(seed_phrase, word_start, word_end); + + if (index == -1) { + return false; + } + + data |= (unsigned int)index; + + if (word_end != (int)seed_phrase.size()) { + data <<= WORDLIST_BIT_LENGTH; + } + + word_start = word_end + 1; + ++word_count; + } while (word_start < seed_phrase.size()); + + std::copy((std::byte*)data.data(), (std::byte*)data.data() + data.size(), data_out.begin()); + memory_cleanse(data.data(), data.size()); + + return word_count == WORD_COUNT; +} + +SecureString GRC::Mnemonics::EncodeSeedPhrase(Span data_in) { + base_uint data; + + assert(data_in.size() == ENCIPHERED_LENGTH); + std::copy(data_in.begin(), data_in.end(), (std::byte*)data.data()); + + // Add another WORD_COUNT for the spaces. + SecureString seed_phrase(WORD_COUNT * LONGEST_WORD_LENGTH + WORD_COUNT, '\0'); + Span seed_phrase_data{(std::byte*)seed_phrase.data(), seed_phrase.size()}; + + unsigned int char_count = 0; + for (unsigned int i = 0; i < WORD_COUNT; ++i) { + const int index = data.GetLow64() & ((1 << WORDLIST_BIT_LENGTH) - 1); + char_count += WORDLIST[index].size(); + timingsafe_memcpy(seed_phrase_data.last(char_count), MakeByteSpan(WORDLIST[index]), LONGEST_WORD_LENGTH); + + if (i != WORD_COUNT - 1) { + timingsafe_memcpy(seed_phrase_data.last(++char_count), MakeByteSpan(" ").first(1), 1); + } + + data >>= WORDLIST_BIT_LENGTH; + } + + memory_cleanse(data.data(), data.size()); + timingsafe_memcpy(seed_phrase_data, seed_phrase_data.last(char_count), WORD_COUNT * LONGEST_WORD_LENGTH + WORD_COUNT); + seed_phrase.resize(char_count); + + return seed_phrase; +} + diff --git a/src/gridcoin/mnemonics.h b/src/gridcoin/mnemonics.h new file mode 100644 index 0000000000..c2866a3f2f --- /dev/null +++ b/src/gridcoin/mnemonics.h @@ -0,0 +1,30 @@ +// Copyright (c) 2024 The Gridcoin developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or https://opensource.org/licenses/mit-license.php. + +#ifndef GRIDCOIN_MNEMONICS_H +#define GRIDCOIN_MNEMONICS_H + +#include +#include + +namespace GRC { +namespace Mnemonics { + +// Wordlist has 2048 words = 2 ^ 11 +static constexpr unsigned WORDLIST_BIT_LENGTH = 11; + +// plaintext: 1 byte inner version || 2 byte timestamp || 16 bytes of entropy = 19 bytes +static constexpr unsigned PLAINTEXT_LENGTH = 19; + +// enciphered: 1 byte outer version || 19 bytes ciphertext || 16 bytes tag || 8 bytes salt / nonce = 44 bytes = 32 words +static constexpr unsigned ENCIPHERED_LENGTH = 44; +static constexpr unsigned WORD_COUNT = 32; + +bool DecodeSeedPhrase(const SecureString& seed_phrase, Span data_out); +SecureString EncodeSeedPhrase(Span data_in); + +} // namespace Mnemonics +} // namespace GRC + +#endif // GRIDCOIN_MNEMONICS_H diff --git a/src/test/gridcoin_tests.cpp b/src/test/gridcoin_tests.cpp index ff9ae50222..e01200f7c4 100755 --- a/src/test/gridcoin_tests.cpp +++ b/src/test/gridcoin_tests.cpp @@ -5,9 +5,11 @@ #include "chainparams.h" #include "uint256.h" #include "gridcoin/protocol.h" +#include #include "util.h" #include "main.h" #include "gridcoin/staking/reward.h" +#include #include #include @@ -263,4 +265,25 @@ BOOST_AUTO_TEST_CASE(gridcoin_ObsoleteConfigurableCBRShouldResortToDefault) BOOST_CHECK_EQUAL(GRC::GetConstantBlockReward(&index_check), DEFAULT_CBR); } +BOOST_AUTO_TEST_CASE(gridcoin_test_seed_phrase_coding) +{ + std::vector zeroes(44, std::byte{0}); + std::vector full(44, std::byte{0xff}); + + BOOST_CHECK_EQUAL(GRC::Mnemonics::EncodeSeedPhrase(zeroes), SecureString("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon")); + BOOST_CHECK_EQUAL(GRC::Mnemonics::EncodeSeedPhrase(full), SecureString("zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo")); + + FastRandomContext ctx(true); + for (int i = 0; i < 1024; ++i) { + std::vector bytes(GRC::Mnemonics::ENCIPHERED_LENGTH, std::byte{'\0'}); + std::vector bytes_out(GRC::Mnemonics::ENCIPHERED_LENGTH, std::byte{'\0'}); + + auto rand_bytes = ctx.randbytes(GRC::Mnemonics::ENCIPHERED_LENGTH); + std::copy((std::byte*)rand_bytes.data(), (std::byte*)rand_bytes.data() + rand_bytes.size(), bytes.begin()); + + BOOST_CHECK(GRC::Mnemonics::DecodeSeedPhrase(GRC::Mnemonics::EncodeSeedPhrase(bytes), bytes_out)); + BOOST_CHECK_EQUAL(bytes, bytes_out); + } +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/test_gridcoin.h b/src/test/test_gridcoin.h index 4926dafd50..899dae9a52 100644 --- a/src/test/test_gridcoin.h +++ b/src/test/test_gridcoin.h @@ -25,6 +25,11 @@ std::ostream& operator<<(typename std::enable_if::value, std::os { return stream << static_cast::type>(e); } + +template +std::ostream& operator<<(typename std::enable_if_t, std::ostream>& stream, const std::vector& v) { + return stream << HexStr(v); +} } // namespace std #endif From d6900ec523361a50bbb5726548e1eb0b3b7fbe92 Mon Sep 17 00:00:00 2001 From: div72 Date: Sat, 17 Feb 2024 04:59:50 +0300 Subject: [PATCH 6/6] mnemonics: initial implementation for seed phrase generation/parsing --- src/gridcoin/mnemonics.cpp | 84 +++++++++++++++++++++++++++++++++++++ src/gridcoin/mnemonics.h | 9 +++- src/test/gridcoin_tests.cpp | 12 ++++++ 3 files changed, 104 insertions(+), 1 deletion(-) diff --git a/src/gridcoin/mnemonics.cpp b/src/gridcoin/mnemonics.cpp index 84db56318e..b3e9ebc41a 100644 --- a/src/gridcoin/mnemonics.cpp +++ b/src/gridcoin/mnemonics.cpp @@ -5,7 +5,12 @@ #include #include +#include +#include +#include +#include #include +#include #include #include @@ -128,3 +133,82 @@ SecureString GRC::Mnemonics::EncodeSeedPhrase(Span data_in) { return seed_phrase; } +SecureString GRC::Mnemonics::GenerateSeedPhrase(const SecureString& password, CKey& key_out) { + unsigned char plaintext[PLAINTEXT_LENGTH]; + unsigned char enciphered[ENCIPHERED_LENGTH]; + unsigned char key_data[32]; + + do { + GetStrongRandBytes(Span{&plaintext[3], 16}); + CSHA256().Write(&plaintext[3], 16).Finalize(key_data); + key_out.Set(std::begin(key_data), std::end(key_data), /*fCompressedIn=*/true); + } while (!key_out.IsValid()); + memory_cleanse(key_data, sizeof(key_data)); + + // Outer version is 0 for now. + enciphered[0] = 0; + + // Generate the salt / nonce. + GetStrongRandBytes(Span{&enciphered[1], 8}); + uint64_t nonce = ReadLE64(&enciphered[1]); + + // Inner version is also 0 for now. + plaintext[0] = 0; + uint16_t days_since_birthday = (GetTime() - std::chrono::seconds(BITCOIN_GENESIS)).count() / 86400; + plaintext[1] = days_since_birthday & 0xFF; + plaintext[2] = (days_since_birthday >> 8) & 0xFF; + + uint256 salted_pass = scrypt_salted_multiround_hash(password.data(), password.size(), + &enciphered[1], 8, 32768); + AEADChaCha20Poly1305 cipher({(std::byte*)salted_pass.data(), 32}); + cipher.Encrypt(Span{(std::byte*)plaintext, PLAINTEXT_LENGTH}, Span{(std::byte*)enciphered, 9}, + {(uint32_t)nonce, nonce}, Span{(std::byte*)&enciphered[9], PLAINTEXT_LENGTH + AEADChaCha20Poly1305::EXPANSION}); + + memory_cleanse(plaintext, PLAINTEXT_LENGTH); + SecureString seed_phrase = EncodeSeedPhrase({(std::byte*)enciphered, sizeof(enciphered)}); + memory_cleanse(enciphered, ENCIPHERED_LENGTH); + + return seed_phrase; +} + +bool GRC::Mnemonics::ParseSeedPhrase(const SecureString& seed_phrase, const SecureString& password, CKey& key_out) { + unsigned char plaintext[PLAINTEXT_LENGTH]; + unsigned char enciphered[ENCIPHERED_LENGTH]; + + if (!DecodeSeedPhrase(seed_phrase, Span{(std::byte*)enciphered, sizeof(enciphered)})) { + memory_cleanse(enciphered, sizeof(enciphered)); + return false; + } + + if (enciphered[0] != 0) { + memory_cleanse(enciphered, sizeof(enciphered)); + return false; + } + + uint64_t nonce = ReadLE64(&enciphered[1]); + + uint256 salted_pass = scrypt_salted_multiround_hash(password.data(), password.size(), + &enciphered[1], 8, 32768); + AEADChaCha20Poly1305 cipher({(std::byte*)salted_pass.data(), 32}); + if (!cipher.Decrypt(Span{(std::byte*)&enciphered[9], sizeof(plaintext) + AEADChaCha20Poly1305::EXPANSION}, Span{(std::byte*)enciphered, 9}, + {(uint32_t)nonce, nonce}, Span{(std::byte*)plaintext, sizeof(plaintext)})) { + memory_cleanse(enciphered, sizeof(enciphered)); + memory_cleanse(plaintext, sizeof(plaintext)); + return false; + } + + memory_cleanse(enciphered, sizeof(enciphered)); + + if (plaintext[0] != 0) { + memory_cleanse(plaintext, sizeof(plaintext)); + return false; + } + + unsigned char key_data[32]; + CSHA256().Write(&plaintext[3], 16).Finalize(key_data); + memory_cleanse(plaintext, sizeof(plaintext)); + + key_out.Set(std::begin(key_data), std::end(key_data), /*fCompressedIn=*/true); + memory_cleanse(key_data, sizeof(key_data)); + return key_out.IsValid(); +} diff --git a/src/gridcoin/mnemonics.h b/src/gridcoin/mnemonics.h index c2866a3f2f..a3fcdc5e9c 100644 --- a/src/gridcoin/mnemonics.h +++ b/src/gridcoin/mnemonics.h @@ -5,6 +5,7 @@ #ifndef GRIDCOIN_MNEMONICS_H #define GRIDCOIN_MNEMONICS_H +#include #include #include @@ -17,13 +18,19 @@ static constexpr unsigned WORDLIST_BIT_LENGTH = 11; // plaintext: 1 byte inner version || 2 byte timestamp || 16 bytes of entropy = 19 bytes static constexpr unsigned PLAINTEXT_LENGTH = 19; -// enciphered: 1 byte outer version || 19 bytes ciphertext || 16 bytes tag || 8 bytes salt / nonce = 44 bytes = 32 words +// enciphered: 1 byte outer version || 8 bytes salt / nonce || 19 bytes ciphertext || 16 bytes tag = 44 bytes = 32 words static constexpr unsigned ENCIPHERED_LENGTH = 44; static constexpr unsigned WORD_COUNT = 32; +// Bitcoin genesis as a Unix timestamp. +static constexpr unsigned BITCOIN_GENESIS = 1231006505; + bool DecodeSeedPhrase(const SecureString& seed_phrase, Span data_out); SecureString EncodeSeedPhrase(Span data_in); +SecureString GenerateSeedPhrase(const SecureString& password, CKey& key_out); +bool ParseSeedPhrase(const SecureString& seed_phrase, const SecureString& password, CKey& key_out); + } // namespace Mnemonics } // namespace GRC diff --git a/src/test/gridcoin_tests.cpp b/src/test/gridcoin_tests.cpp index e01200f7c4..c6cedebf4a 100755 --- a/src/test/gridcoin_tests.cpp +++ b/src/test/gridcoin_tests.cpp @@ -286,4 +286,16 @@ BOOST_AUTO_TEST_CASE(gridcoin_test_seed_phrase_coding) } } +BOOST_AUTO_TEST_CASE(gridcoin_test_mnemonics_generation) +{ + CKey key1; + CKey key2; + + SecureString seed_phrase = GRC::Mnemonics::GenerateSeedPhrase(SecureString(""), key1); + BOOST_CHECK(GRC::Mnemonics::ParseSeedPhrase(seed_phrase, SecureString(""), key2)); + BOOST_CHECK(key1 == key2); + + BOOST_CHECK(!GRC::Mnemonics::ParseSeedPhrase(seed_phrase, SecureString("wrong password"), key2)); +} + BOOST_AUTO_TEST_SUITE_END()