Attackers are stealing encrypted databases today and storing them for later. Their hope is that future quantum computers will let them read what is protected now.
TextrunSQL adds post-quantum protection around the key that opens the database. SQLCipher handles the encryption, while SQLite stores and queries the data.
Why post-quantum matters now
Nobody knows when a quantum computer powerful enough to break the public-key protection used today will arrive. Updating products and stored data takes time, so waiting for that day is risky.
Encrypted files and traffic can be copied today and stored for years. If the public-key layer that protected those keys later falls, old archives can open. Someone can steal encrypted databases now and wait.
That attack is called harvest-now, decrypt-later.
Governments and product teams are not waiting for a dramatic “quantum day”:
- NIST published its first post-quantum standards in August 2024 and provides guidance for moving existing systems to them.
- Apple introduced PQ3 for iMessage, combining established and post-quantum cryptography to reduce harvest-now, decrypt-later risk.
- Signal introduced and deployed PQXDH in its apps, combining its existing key agreement with a post-quantum method.
- Cloudflare enabled post-quantum key agreement broadly across its network and documented the rollout.
The standards TextrunSQL uses
TextrunSQL wraps a database key with ML-KEM-768 so a stolen envelope is harder to open later. This is a standard from NIST FIPS 203, implemented in OpenSSL.
The envelope also uses SHA-256, HKDF-SHA-256, and AES-256-GCM. This strong symmetric crypto can stay because it is much less exposed to known quantum attacks than public-key crypto, according to NIST.
Other standards and recommendations are not implemented, because they do not apply to TextrunSQL:
- Hybrid KEMs: Apple PQ3 and Signal PQXDH combine classical key exchange with a post-quantum
KEM, but TextrunSQL is not a messaging handshake. - FIPS 204 and FIPS 205: These cover signing and verification, not database key envelopes.
- CNSA 2.0, TLS hybrids, and certificate PKI migration: These are network and enterprise PKI concerns.
TextrunSQL only defines the envelope format. Audit claims, formal certification, storage, backups, key custody, sharing, rotation, and migration are left to the application.
How TextrunSQL works
TextrunSQL protects the 32-byte key that opens the database. SQLCipher still encrypts and checks the database pages. Your app still decides where the database, envelope, private key, and recovery information live.
Seal protects the database key inside a 1236-byte Envelope. Bytes 0..1187 are authenticated but not encrypted, while the final 48 bytes contain the wrapped key and its tag.
Open checks the envelope, refuses changes or a different recipient or context, and returns the key only when all checks succeed. A separate Handoff then passes the key to SQLCipher.
What the labels mean:
AAD(Additional Authenticated Data): Envelope bytes0..1187, whichAES-GCMauthenticates but does not encrypt.AES-GCM/GCM: The diagram’s short labels forAES-256-GCM, which TextrunSQL uses to wrap theDEKand authenticate the envelope prefix.- Bind: Computes
rid = SHA-256(pk)andchash = SHA-256(context)so Open can reject a different recipient or context. chash(context hash): The 32-byteSHA-256digest of the caller context, stored in the envelope and used as theHKDFsalt.context: A nonempty, app-defined binary value from 1 through 1024 bytes, supplied unchanged to Seal & Open as a stable database-context identifier.- Decaps: Uses the matching
ML-KEMdecapsulation key andKEMciphertext to produce the shared secret. DEK(database encryption key): The random 32-byte database encryption key supplied to SQLCipher in raw-key form.- Derive: Uses
HKDF-SHA-256with the shared secret, context hash, and fixed protocol label to produce theAES-GCMwrapping key. - Encapsulate: Uses the recipient’s
ML-KEMencapsulation key to produce a 1088-byteKEMciphertext and 32-byte shared secret. - Envelope: The fixed 1236-byte record containing the header, recipient and context bindings,
KEMciphertext,nonce, wrappedDEK, and tag. - Format: Converts the recovered
DEKinto SQLCipher’s lowercase raw-key syntax. That syntax uses the prefixx', exactly 64 hex digits, and a closing'. GCMoutput: Envelope bytes1188..1235contain the 32-byte wrappedDEKfollowed by its 16-byte authentication tag.- Handoff: The separate caller-controlled step that formats the recovered
DEKand applies it to an already-open SQLCipher database. hdr(header): The fixed 24-byte envelope header containing the magic, version, suite, flags, reserved bytes, and declared lengths.HKDF(HMAC-based Key Derivation Function): The key-derivation function that combines the shared secret, context hash, and fixed protocol label to produce the wrapping key.- Input: Open receives the
envelope, matching recipientkey, and unchangedcontext. Handoff receives the opendb, selectedschema, and recoveredDEK. KEMciphertext: The 1088-byteML-KEMoutput that the matching decapsulation key uses to produce the same shared secret.ML-KEM-768(Module-Lattice-Based Key-Encapsulation Mechanism): The FIPS 203 parameter set used by TextrunSQL. NIST classifies it in security category 3.768is a parameter-set name, not a key size.nonce: A fresh random 12-byteAES-GCMinput stored in the envelope. It must not repeat with the same wrapping key.- Open: Authenticates an envelope for the matching recipient and context and returns the
DEK. It does not itself apply theDEKto SQLCipher. The tests below check both its success and failure paths. pk(public key): The recipient’s 1184-byteML-KEMencapsulation key, also called the public key in this API.rid(recipient identifier): The 32-byteSHA-256digest ofpk, stored in the envelope and checked during Open.- Seal: Creates the canonical 1236-byte envelope that protects one 32-byte
DEKfor one recipient and context. - Set key: Calls
sqlite3_key_v2()with the formatted raw key for the selected schema. SHA-256: The hash function used to computeridfrompkandchashfrom the context.- SQLCipher: The SQLite-compatible library that encrypts and authenticates database pages after the
DEKis applied. sqlite3_key_v2(): The SQLCipher API that applies the formatted key to the selected schema. Success means the key was accepted, not that existing database content was authenticated.tag: The 16-byteAES-GCMauthentication tag covering the wrappedDEKand the AAD.textrunsql_pq_key_sqlcipher(): Formats the 32-byteDEKas a SQLCipher raw key and callssqlite3_key_v2(). Success sets the key but does not authenticate existing database content. The caller must read protected content next.textrunsql_pq_open_dek(): Validates and authenticates the envelope and returns the 32-byteDEKonly on success. The output remains zero on failure.textrunsql_pq_seal_dek(): Seals a supplied 32-byteDEKinto a 1236-byte envelope for one recipient and context.- Unwrap: Uses
AES-GCMto authenticate the AAD and wrappedDEK, then releases the recoveredDEKonly if authentication succeeds. - Validate: Checks the exact envelope format, version, suite, recipient binding, and context binding before decapsulation.
wDEK(wrappedDEK): The 32-byteAES-GCMciphertext of theDEKstored in the envelope.- Wrap: Uses the derived key and random
noncetoAES-GCM-encrypt theDEKand authenticate the AAD, producingwDEKand the tag.
How to test this
The tests follow the Seal & Open paths in the diagrams above. They check that Seal creates an envelope that Open can read, that the recovered DEK opens the SQLCipher database, and that Open refuses changed data, the wrong recipient, or the wrong context.
Start with the regular checks. After you have configured the repo with OpenSSL 3.5 or newer, run these commands from its root:
make libsqlite3.a testfixture
./testfixture test/sqlcipher.test
make -C textrunsql check
The first two commands build SQLCipher and run its own checks. The last command tests TextrunSQL, including NIST examples, the Seal & Open behavior above, and the handoff of the recovered key to SQLCipher. The full setup and exact coverage are in textrunsql/TESTING.md.
For deeper checks, run two more commands that look for memory errors or parser crashes:
make -C textrunsql asan
make -C textrunsql fuzz-smoke
asan reruns the focused C tests with checks for memory errors and undefined behavior. fuzz-smoke sends 5,000 generated inputs through the envelope parser and looks for crashes, memory errors, or undefined behavior.
Passing these tests shows that the code worked in the setup you used: this commit, compiler, operating system, OpenSSL provider, architecture, and build settings. It is not a security review or certification, and it cannot cover every way an app or its environment might expose the database or its keys.
Why we don't encrypt the entire envelope
Open needs the envelope’s header, recipient and context bindings, ML-KEM ciphertext, and nonce before it can derive the key that unwraps the database key. Encrypting those fields with that same key would create a circle: Open would need the key to read the data required to derive it.
Those fields are not secret. They remain readable, but AES-GCM authenticates them as AAD, so Open rejects the envelope if any of them change. The database key is the part that needs to stay hidden, so TextrunSQL encrypts it and uses the authentication tag to protect both it and the visible fields.
Why we don't use something that already exists
SQLCipher currently does not provide post-quantum protection for the key. There is also no fork that does. TextrunSQL adds that missing layer and relies on existing cryptographic code:
- OpenSSL: TextrunSQL calls OpenSSL for
ML-KEMinstead of implementing the algorithm itself. - A current standard: OpenSSL’s
ML-KEMconforms to FIPS 203, and its 3.5 release has long-term support through April 2030. - Used elsewhere: NGINX and HAProxy also use OpenSSL’s post-quantum support for TLS. Their protocols are different, but the
ML-KEMcode comes from the same maintained library.
How to learn more
These talks explain the ideas behind post-quantum cryptography, why the change matters now, and how ML-KEM is being used:
-
Tomas Gustavsson @ OpenSSL Conference 2025
Tomas from Refactor.com shows hybrid
ML-KEMTLS working out of the box with OpenSSL 3.5, then examines the remaining gaps across certificates, browsers, and hardware security modules. -
Sophie Schmieg & Bas Westerbaan @ RWPQC 2026
Sophie from Google and Bas from Cloudflare describe how hybrid
ML-KEMreached production TLS. They explain why store-now, decrypt-later makes key exchange urgent, while signatures, certificates, and legacy systems make the rest of the migration much harder. -
Jeff Crume @ IBM Technology 2026
Jeff uses a chessboard analogy to explain how high-dimensional lattices and added noise create problems that neither classical nor quantum computers can solve efficiently. He then connects the idea to harvest-now, decrypt-later and why encrypted data with a long useful life needs protection now.
How AI helped build this
Maximilian Götzfried sees AI as a useful tool for developers, but does not believe that coding is “solved.”
I have always enjoyed writing code, and I will never give that up. I use AI as a supporting tool, similar to code generators and automations in the past. To write code and explore new ideas. I also use AI to improve my English because it is not my first language, and to learn new things by having it explain them to me.
I use AI to ship the best product I can and to become a better developer. But I am not a vibe coder. I write a lot of code every day, thoroughly review every AI contribution and try to get to the bottom of every issue – because I care about quality.