From d6bc7e7a4e071f3da6ed9db688e83b1e48f05027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Tro=C3=9Fbach?= Date: Tue, 3 Nov 2026 15:54:48 +0100 Subject: [PATCH 1/2] Fix the ChaCha20 overflow check for start counter 0xffffffff chacha20_counter_wraps() assumed that the call starts at counter + 1. That holds for the page and VLE encryption, but chacha20_xor() passes the start counter of the call itself. For the start counter 0xffffffff (page counter 0xfffffffe) the check saw no overflow, so the accelerated code was used and carried the counter into the nonce. A page with this counter written by version 2.5 or earlier was still read incorrectly on x86 and ARM64. The function now takes the start counter of the call, and the two encrypting callers pass counter + 1. The counters chosen when encrypting do not change. --- src/chacha20poly1305.c | 5 ++--- src/cipher_chacha20.c | 2 +- src/sqlite3mc_vle.c | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/chacha20poly1305.c b/src/chacha20poly1305.c index 4bdc43b..ce309cd 100644 --- a/src/chacha20poly1305.c +++ b/src/chacha20poly1305.c @@ -328,10 +328,9 @@ int poly1305_tagcmp_scalar(const uint8_t tag1[16], const uint8_t tag2[16]) static int chacha20_counter_wraps(uint32_t counter, size_t n) { - /* The data call starts at counter+1 and uses ceil(n/64) blocks */ - uint64_t start = (uint32_t)(counter + 1); + /* A call starting at block counter 'counter' uses ceil(n/64) blocks */ uint64_t blocks = ((uint64_t)n + 63) / 64; - return (start + blocks) > ((uint64_t)1 << 32); + return ((uint64_t)counter + blocks) > ((uint64_t)1 << 32); } /* diff --git a/src/cipher_chacha20.c b/src/cipher_chacha20.c index a1d9248..82073fd 100644 --- a/src/cipher_chacha20.c +++ b/src/cipher_chacha20.c @@ -220,7 +220,7 @@ EncryptPageChaCha20Cipher(void* cipher, int page, unsigned char* data, int len, chacha20_rng(data + n, PAGE_NONCE_LEN_CHACHA20); counter = LOAD32_LE(data + n + PAGE_NONCE_LEN_CHACHA20 - 4) ^ (uint32_t)page; } - while (chacha20_counter_wraps(counter, n - offset)); + while (chacha20_counter_wraps(counter + 1, n - offset)); chacha20_xor(otk, OTK_LEN_CHACHA20, chacha20Cipher->m_key, data + n, counter); chacha20_xor(data + offset, n - offset, otk + 32, data + n, counter + 1); diff --git a/src/sqlite3mc_vle.c b/src/sqlite3mc_vle.c index a26fb95..7a59b1a 100644 --- a/src/sqlite3mc_vle.c +++ b/src/sqlite3mc_vle.c @@ -402,7 +402,7 @@ vle_aeadEncrypt(const VleContext* ctx, chacha20_rng(nonce, nonceLen); counter = vle_readU32BE(nonce + nonceLen - 4) ^ (uint32_t) VLE_MAGIC_NUM; } - while (chacha20_counter_wraps(counter, ctextLen)); + while (chacha20_counter_wraps(counter + 1, ctextLen)); /* otk is all zero, so this yields pure keystream under the subkey */ chacha20_xor(otk, VLE_OTK_LEN, subkey, nonce, counter); From eb5933c45b9ef93f2fbe646d04495083fe647239 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Tro=C3=9Fbach?= Date: Tue, 3 Nov 2026 15:54:48 +0100 Subject: [PATCH 2/2] Test ChaCha20 counter overflow within a call in cryptotest For lengths that use the accelerated code, chacha20_xor has to give the same result as sqleet for every start counter from just below the overflow up to 0xffffffff. Without the previous commit the test fails for the start counter 0xffffffff. --- test/cryptotest.c | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/test/cryptotest.c b/test/cryptotest.c index ecd6837..800a372 100644 --- a/test/cryptotest.c +++ b/test/cryptotest.c @@ -391,7 +391,10 @@ static void testAesCbc(void) /* ** ChaCha20 and Poly1305: every implementation has to reproduce the RFC 8439 ** test vectors and give the same result as the first implementation in its -** list (the reference) for pseudo-random input. +** list (the reference) for pseudo-random input. In addition, chacha20_xor has +** to wrap the 32-bit block counter like sqleet when it overflows within a call, +** because a page may be read with a different implementation than the one +** that wrote it. */ typedef void (*ChaCha20Function)(unsigned char* output, const unsigned char* input, size_t length, @@ -482,6 +485,10 @@ static const Poly1305Implementation poly1305List[] = static void testChaCha20(void) { static const uint32_t counters[] = { 0, 1, 0x12345, 0xfffff000u }; +#ifdef HAVE_SELECTABLE_IMPLEMENTATIONS + static const size_t overflowLengths[] = { 1024, 2048, 4064, 8000 }; + const int defaultIndex = ARRAY_COUNT(chacha20List) - 1; +#endif const ChaCha20Implementation* reference = &chacha20List[0]; int failures[ARRAY_COUNT(chacha20List)] = { 0 }; int checks[ARRAY_COUNT(chacha20List)] = { 0 }; @@ -531,6 +538,30 @@ static void testChaCha20(void) } } +#ifdef HAVE_SELECTABLE_IMPLEMENTATIONS + /* Counter overflow within the call: every start counter from just below the overflow up to 2^32-1 */ + for (i = 0; i < ARRAY_COUNT(overflowLengths); i++) + { + size_t length = overflowLengths[i]; + uint64_t blocks = (length + 63) / 64; + uint64_t start; + pseudoRandomBytes(testKey, sizeof(testKey)); + pseudoRandomBytes(testNonce, sizeof(testNonce)); + pseudoRandomBytes(inputBuffer, length); + for (start = 0x100000000ull - blocks - 1; start <= 0xffffffffull; start++) + { + chacha20Sqleet(expectedBuffer, inputBuffer, length, testKey, testNonce, (uint32_t) start); + snprintf(description, sizeof(description), "counter overflow, length %d, counter 0x%08x", + (int) length, (unsigned int) start); + memset(outputBuffer, 0, length); + chacha20Default(outputBuffer, inputBuffer, length, testKey, testNonce, (uint32_t) start); + failures[defaultIndex] += checkResult(memcmp(outputBuffer, expectedBuffer, length) == 0, + chacha20List[defaultIndex].name, description); + checks[defaultIndex]++; + } + } +#endif + for (j = 0; j < ARRAY_COUNT(chacha20List); j++) { if (cpuSupports(chacha20List[j].requiredCpuFeatures))