From 8d51bfba91bea3b4bc3060b270f15f38d20947fd Mon Sep 17 00:00:00 2001 From: Mathias Berchtold Date: Thu, 3 Sep 2026 21:36:58 +0200 Subject: [PATCH] Introduce two internal page-level functions for the ChaCha20-Poly1305 cipher: chacha20_poly1305_page_encrypt(...) chacha20_poly1305_page_decrypt(...) Initially, these functions would use the existing scalar chacha20_xor() and poly1305() implementations. This is primarily an internal refactoring with no database-format or external API change. The abstraction would allow architecture-specific implementations-particularly a fused WebAssembly SIMD implementation without duplicating or restructuring cipher_chacha20.c. --- src/chacha20poly1305.c | 62 ++++++++++++++++++++++++++++++++++ src/cipher_chacha20.c | 77 ++++++++++++++++++++++++++++++++++-------- src/sqlite3mc.c | 2 ++ 3 files changed, 126 insertions(+), 15 deletions(-) diff --git a/src/chacha20poly1305.c b/src/chacha20poly1305.c index 369e7a7f..e7ba146f 100644 --- a/src/chacha20poly1305.c +++ b/src/chacha20poly1305.c @@ -254,6 +254,68 @@ int poly1305_tagcmp(const uint8_t tag1[16], const uint8_t tag2[16]) return (int) d; } +/* + * Authenticated SQLite3MC pages without a plaintext prefix. + * + * pageSize includes the complete in-place buffer: + * [ encrypted payload | 16-byte nonce | 16-byte authentication tag ] + * The caller initializes the nonce and derives the 64-byte one-time key: + * otk[0..31] is the Poly1305 key; otk[32..63] is the ChaCha20 key. + * counter is the first payload block counter, not the key-derivation counter. + * The MAC covers the ciphertext and the complete 16-byte nonce. This is the + * SQLite3MC page construction, not the RFC 8439 AEAD message format. + * + * Sizes must include the 32 reserved bytes and be multiples of 16. This also + * permits future fused implementations without changing the calling contract. + * Page 1 (header/salt handling) and unauthenticated pages are handled by the + * caller. Key material must not overlap the page buffer. + * + * Invalid arguments leave the buffer unchanged. On authentication failure, + * callers must discard the page: this scalar implementation leaves ciphertext + * intact, but a future fused implementation may have overwritten it in place. + */ +enum +{ + CHACHA20_POLY1305_OK = 0, + CHACHA20_POLY1305_INVALID_ARGUMENT = -1, + CHACHA20_POLY1305_AUTH_FAILED = 1 +}; + +SQLITE_PRIVATE int +chacha20_poly1305_page_encrypt(void* buffer, size_t pageSize, + const uint8_t otk[64], uint32_t counter) +{ + uint8_t* page = (uint8_t*) buffer; + size_t payloadSize; + if (page == NULL || otk == NULL || pageSize < 32 || (pageSize & 15) != 0) + return CHACHA20_POLY1305_INVALID_ARGUMENT; + + payloadSize = pageSize - 32; + chacha20_xor(page, payloadSize, otk + 32, page + payloadSize, counter); + poly1305(page, payloadSize + 16, otk, page + payloadSize + 16); + return CHACHA20_POLY1305_OK; +} + +SQLITE_PRIVATE int +chacha20_poly1305_page_decrypt(void* buffer, size_t pageSize, + const uint8_t otk[64], uint32_t counter) +{ + uint8_t* page = (uint8_t*) buffer; + size_t payloadSize; + uint8_t tag[16]; + if (page == NULL || otk == NULL || pageSize < 32 || (pageSize & 15) != 0) + return CHACHA20_POLY1305_INVALID_ARGUMENT; + + payloadSize = pageSize - 32; + poly1305(page, payloadSize + 16, otk, tag); + if (poly1305_tagcmp(page + payloadSize + 16, tag) != 0) + return CHACHA20_POLY1305_AUTH_FAILED; + + /* The scalar path verifies the MAC before exposing any plaintext. */ + chacha20_xor(page, payloadSize, otk + 32, page + payloadSize, counter); + return CHACHA20_POLY1305_OK; +} + /* * Platform-specific entropy functions for seeding RNG */ diff --git a/src/cipher_chacha20.c b/src/cipher_chacha20.c index 47d66623..e291cb44 100644 --- a/src/cipher_chacha20.c +++ b/src/cipher_chacha20.c @@ -228,12 +228,26 @@ EncryptPageChaCha20Cipher(void* cipher, int page, unsigned char* data, int len, counter = LOAD32_LE(data + n + PAGE_NONCE_LEN_CHACHA20 - 4) ^ page; chacha20_xor(otk, OTK_LEN_CHACHA20, chacha20Cipher->m_key, data + n, counter); - chacha20_xor(data + offset, n - offset, otk + 32, data + n, counter + 1); - if (page == 1 && usePlaintextHeader == 0) + if (page != 1) { - memcpy(data, chacha20Cipher->m_salt, SALTLENGTH_CHACHA20); + int result = chacha20_poly1305_page_encrypt(data, (size_t) len, otk, counter + 1); + if (result != CHACHA20_POLY1305_OK) + { + assert(result == CHACHA20_POLY1305_INVALID_ARGUMENT); + sqlite3mcSecureZeroMemory(otk, OTK_LEN_CHACHA20); + return SQLITE_MISUSE; + } + } + else + { + /* Page 1 authenticates the salt/header as well as the ciphertext. */ + chacha20_xor(data + offset, n - offset, otk + 32, data + n, counter + 1); + if (usePlaintextHeader == 0) + { + memcpy(data, chacha20Cipher->m_salt, SALTLENGTH_CHACHA20); + } + poly1305(data, n + PAGE_NONCE_LEN_CHACHA20, otk, data + n + PAGE_NONCE_LEN_CHACHA20); } - poly1305(data, n + PAGE_NONCE_LEN_CHACHA20, otk, data + n + PAGE_NONCE_LEN_CHACHA20); } else { @@ -316,31 +330,64 @@ DecryptPageChaCha20Cipher(void* cipher, int page, unsigned char* data, int len, counter = LOAD32_LE(data + n + PAGE_NONCE_LEN_CHACHA20 - 4) ^ page; chacha20_xor(otk, OTK_LEN_CHACHA20, chacha20Cipher->m_key, data + n, counter); - /* Only calculate and verify MAC if required */ - if (hmacCheck != 0) + if (page != 1 && hmacCheck != 0) { - poly1305(data, n + PAGE_NONCE_LEN_CHACHA20, otk, tag); - - /* Verify the MAC */ - if (poly1305_tagcmp(data + n + PAGE_NONCE_LEN_CHACHA20, tag)) + int result = chacha20_poly1305_page_decrypt(data, (size_t) len, otk, counter + 1); + if (result != CHACHA20_POLY1305_OK) { + if (result != CHACHA20_POLY1305_AUTH_FAILED) + { + assert(result == CHACHA20_POLY1305_INVALID_ARGUMENT); + sqlite3mcSecureZeroMemory(otk, OTK_LEN_CHACHA20); + return SQLITE_MISUSE; + } + SQLITE3MC_DEBUG_LOG("decrypt: codec=%p page=%d\n", chacha20Cipher, page); SQLITE3MC_DEBUG_HEX("decrypt key:", chacha20Cipher->m_key, 32); SQLITE3MC_DEBUG_HEX("decrypt otk:", otk, OTK_LEN_CHACHA20); SQLITE3MC_DEBUG_HEX("decrypt data+00:", data, 16); SQLITE3MC_DEBUG_HEX("decrypt data+24:", data + 24, 16); SQLITE3MC_DEBUG_HEX("decrypt data+n:", data + n, 16); +#ifdef SQLITE3MC_DEBUG_DATA + poly1305(data, n + PAGE_NONCE_LEN_CHACHA20, otk, tag); SQLITE3MC_DEBUG_HEX("decrypt tag r:", data + n + PAGE_NONCE_LEN_CHACHA20, PAGE_TAG_LEN_CHACHA20); SQLITE3MC_DEBUG_HEX("decrypt tag c:", tag, PAGE_TAG_LEN_CHACHA20); - - /* Bad MAC: Clean up and bail out BEFORE decrypting */ +#endif + + /* A future fused implementation may already have overwritten data. */ sqlite3mcSecureZeroMemory(otk, OTK_LEN_CHACHA20); - return (page == 1) ? SQLITE_NOTADB : SQLITE_CORRUPT; + sqlite3mcSecureZeroMemory(data, len); + return SQLITE_CORRUPT; } } + else + { + /* Preserve page-1 handling and the explicit MAC-check bypass. */ + if (hmacCheck != 0) + { + poly1305(data, n + PAGE_NONCE_LEN_CHACHA20, otk, tag); + + /* Verify the MAC */ + if (poly1305_tagcmp(data + n + PAGE_NONCE_LEN_CHACHA20, tag)) + { + SQLITE3MC_DEBUG_LOG("decrypt: codec=%p page=%d\n", chacha20Cipher, page); + SQLITE3MC_DEBUG_HEX("decrypt key:", chacha20Cipher->m_key, 32); + SQLITE3MC_DEBUG_HEX("decrypt otk:", otk, OTK_LEN_CHACHA20); + SQLITE3MC_DEBUG_HEX("decrypt data+00:", data, 16); + SQLITE3MC_DEBUG_HEX("decrypt data+24:", data + 24, 16); + SQLITE3MC_DEBUG_HEX("decrypt data+n:", data + n, 16); + SQLITE3MC_DEBUG_HEX("decrypt tag r:", data + n + PAGE_NONCE_LEN_CHACHA20, PAGE_TAG_LEN_CHACHA20); + SQLITE3MC_DEBUG_HEX("decrypt tag c:", tag, PAGE_TAG_LEN_CHACHA20); + + /* Bad MAC: Clean up and bail out BEFORE decrypting */ + sqlite3mcSecureZeroMemory(otk, OTK_LEN_CHACHA20); + return (page == 1) ? SQLITE_NOTADB : SQLITE_CORRUPT; + } + } - /* Decrypt only after MAC is verified (or if check is bypassed) */ - chacha20_xor(data + offset, n - offset, otk + 32, data + n, counter + 1); + /* Decrypt only after MAC is verified (or if check is bypassed). */ + chacha20_xor(data + offset, n - offset, otk + 32, data + n, counter + 1); + } if (page == 1 && usePlaintextHeader == 0) { diff --git a/src/sqlite3mc.c b/src/sqlite3mc.c index 272be424..cdb7d17c 100644 --- a/src/sqlite3mc.c +++ b/src/sqlite3mc.c @@ -193,6 +193,8 @@ SQLITE_PRIVATE int sqlite3mcGetMemorySecurity(); SQLITE_PRIVATE void chacha20_xor(void* data, size_t n, const uint8_t key[32], const uint8_t nonce[12], uint32_t counter); SQLITE_PRIVATE void poly1305(const uint8_t* msg, size_t n, const uint8_t key[32], uint8_t tag[16]); SQLITE_PRIVATE int poly1305_tagcmp(const uint8_t tag1[16], const uint8_t tag2[16]); +SQLITE_PRIVATE int chacha20_poly1305_page_encrypt(void* buffer, size_t pageSize, const uint8_t otk[64], uint32_t counter); +SQLITE_PRIVATE int chacha20_poly1305_page_decrypt(void* buffer, size_t pageSize, const uint8_t otk[64], uint32_t counter); SQLITE_PRIVATE void chacha20_rng(void* out, size_t n); #include "chacha20poly1305.c"