Textrun app icon

Textrun is a new native app for personal archiving & outlined research.

It will be available soon for Mac. Later for iPad, iPhone, and Android, too.

UseMyResearch is its platform for sharing research pages. Connected to agents via WebMCP. I built v1 in ten days during the WebMCP Challenge, and already started working on v2. Checkout my submission at Devpost and the site at UseMyResearch.com. Also see my posts on Bluesky, LinkedIn, Mastodon, and X.

Textrun will ship with post-quantum encryption using TextrunSQL. To protect personal secrets but also to provide credentials to agentic researchers safely.

github.com/textrun/TextrunSQL

Prepare your encrypted SQLite databases for the post-quantum future

TextrunSQL helps defend against “harvest now, decrypt later” attacks by adding post-quantum protection to SQLCipher database keys.

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. SQLite stores and queries the data.

TextrunSQL adds a post-quantum key layer. It does NOT replace SQLCipher or SQLite.

How TextrunSQL works

TextrunSQL protects the database key with ML-KEM-768, which is a standard from NIST FIPS 203 and implemented in OpenSSL. 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. The final 48 bytes contain the wrapped key and its tag.

Open validates the envelope and returns the key only when all checks succeed. Validation fails if the envelope has changed or the recipient or context does not match. A Handoff passes the key to SQLCipher.

Seal and envelope flow textrunsql_pq_seal_dek() starts with a context of 1 to 1024 bytes, a 1184-byte recipient public key, and a 32-byte database encryption key. SHA-256 hashes the public key and context to bind the envelope to that recipient and context. ML-KEM-768 uses the public key to create a 1088-byte KEM ciphertext and a shared secret. HKDF turns the shared secret into a wrapping key. AES-GCM uses that key to wrap the database key. The 1236-byte envelope contains a 24-byte header, 32-byte recipient ID, 32-byte context hash, 1088-byte KEM ciphertext, 12-byte nonce, 32-byte wrapped database key, and 16-byte authentication tag. Envelope bytes 0 through 1187 are authenticated but not encrypted. Bytes 1188 through 1235 hold the wrapped database key and tag. Seal textrunsql_pq_seal_dek() context 1–1024 B pk 1184 B DEK 32 B Bind SHA-256 Encapsulate ML-KEM-768 Wrap AES-256-GCM Derive HKDF Envelope 1236 B hdr 24 B rid 32 B chash 32 B KEM ciphertext 1088 B nonce 12 B wDEK 32 B tag 16 B AAD 0..1187 GCM output 1188..1235
Open and handoff flow textrunsql_pq_open_dek() starts with the envelope, the matching private key, and the same context. It first validates the envelope format and checks that its recipient and context bindings match. ML-KEM-768 uses the private key and KEM ciphertext to recover the shared secret. HKDF derives the same wrapping key. AES-GCM checks the envelope and unwraps the 32-byte database key. The separate textrunsql_pq_key_sqlcipher() handoff takes an open database, a schema name, and the recovered key. It formats the key in SQLCipher raw-key syntax and calls sqlite3_key_v2() to set it. SQLCipher then uses the key to encrypt database pages. Open textrunsql_pq_open_dek() Input envelope + key + context Validate format + bindings Decapsulate ML-KEM-768 Derive HKDF Unwrap AES-GCM DEK 32 B Handoff textrunsql_pq_key_sqlcipher() Input db + schema + DEK Format raw-key syntax Set key sqlite3_key_v2() SQLCipher Encrypts pages

What the labels mean:

  • AAD (Additional Authenticated Data): The envelope bytes 0..1187 which AES-GCM authenticates.
  • AES-GCM / GCM: Short forms for AES-256-GCM, which TextrunSQL uses to wrap the DEK and authenticate the AAD.
  • Bind: Computes rid = SHA-256(pk) and chash = SHA-256(context) so Open can reject a different recipient or context.
  • chash (context hash): The 32-byte SHA-256 hash of the caller context, stored in the envelope and used as the HKDF salt.
  • context: A nonempty database-context identifier with up to 1024 bytes. Use the same value for Seal & Open.
  • Decapsulate: Computes the shared secret using the ciphertext and the recipient’s private key.
  • DEK (Database Encryption Key): The 32-byte encryption key supplied to SQLCipher in raw-key form.
  • Derive: Uses HKDF-SHA-256 with the shared secret, context hash, and fixed protocol label to produce the AES-GCM wrapping key.
  • Encapsulate: Generates a 1088-byte ciphertext and a 32-byte shared secret using the recipient’s public key.
  • Envelope: The fixed 1236-byte record containing the header, recipient and context bindings, KEM ciphertext, nonce, wrapped DEK, and tag.
  • Format: Converts the recovered DEK into SQLCipher’s lowercase raw-key syntax. That syntax uses the prefix x', exactly 64 hex digits, and a closing '.
  • GCM output: Envelope bytes 1188..1235 contain the 32-byte wrapped DEK followed by its 16-byte authentication tag.
  • Handoff: The separate caller-controlled step that formats the recovered DEK and 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 recipient key, and unchanged context. Handoff receives the open db, selected schema, and recovered DEK.
  • KEM ciphertext: The 1088-byte ML-KEM output 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. 768 is a parameter-set name, not a key size.
  • nonce: A fresh random 12-byte AES-GCM input 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 the DEK to SQLCipher.
  • pk (public key): The recipient’s 1184-byte ML-KEM encapsulation key, also called the public key in this API.
  • rid (recipient identifier): The 32-byte SHA-256 digest of pk, stored in the envelope and checked during Open.
  • Seal: Creates the envelope that protects the database key.
  • Set key: Calls sqlite3_key_v2() with the formatted raw key for the selected schema.
  • SHA-256: The hash function used to compute rid from pk and chash from the context.
  • SQLCipher: The SQLite-compatible library that encrypts and authenticates database pages after the DEK is 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-byte AES-GCM authentication tag covering the wrapped DEK and the AAD.
  • textrunsql_pq_key_sqlcipher(): Formats the 32-byte DEK as a SQLCipher raw key and calls sqlite3_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-byte DEK only on success. The output remains zero on failure.
  • textrunsql_pq_seal_dek(): Seals a supplied 32-byte DEK into a 1236-byte envelope for one recipient and context.
  • Unwrap: Uses AES-GCM to authenticate the AAD and wrapped DEK, then releases the recovered DEK only if authentication succeeds.
  • Validate: Checks the exact envelope format, version, suite, recipient binding, and context binding before decapsulation.
  • wDEK (wrapped DEK): The 32-byte AES-GCM ciphertext of the DEK stored in the envelope.
  • Wrap: Uses the derived key and random nonce to AES-GCM-encrypt the DEK and authenticate the AAD, producing wDEK and the tag.

Why not 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 not 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-KEM instead of implementing the algorithm itself.
  • A current standard: OpenSSL’s ML-KEM conforms 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-KEM code comes from the same maintained library.

Are SHA-256, HKDF-SHA-256, and AES-256-GCM post-quantum-safe?

Post-quantum protection does not mean replacing every cryptographic algorithm. The quantum attacks that threaten many existing public-key systems do not work against SHA-256, HKDF-SHA-256, or AES-256-GCM in the same way. Quantum computers can speed up some attacks against these algorithms, but that does not make them easy to break.

In TextrunSQL, SHA-256 identifies the recipient and context. HKDF-SHA-256 turns the shared secret into a wrapping key. AES-256-GCM protects the database key and authenticates the envelope. These algorithms are still suitable for these tasks when used correctly.

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.

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

    youtube.com/watch?v=VqCi1rBBtgg • 25 min

    Tomas from Refactor.com shows hybrid ML-KEM TLS 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

    youtube.com/watch?v=vndzs8l16Tc • 78 min

    Sophie from Google and Bas from Cloudflare describe how hybrid ML-KEM reached production TLS. They explain why store-now, decrypt-later makes key exchange urgent. Signatures, certificates, and legacy systems make the rest of the migration much harder.

  • Jeff Crume @ IBM Technology 2026

    youtube.com/watch?v=ZRpcYSghGr8 • 14 min

    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.

About the dev who built this

Maximilian Götzfried is a freelance developer from Germany. For more than two decades, he has shipped apps that use encrypted databases.

Apple recognized his work early with a student scholarship for WWDC in 2006.

This was about six months before Apple introduced the iPhone, and Steve Jobs was already dealing with severe health issues. At the beginning of the keynote, he told us that he was feeling better. That remark was later cut from the video recording. At the time, Apple still held full sessions in person, with long Q&As before live audiences at San Francisco’s Moscone Center.

His expertise in high-performance concurrency goes back to 2008, when he shipped iPhoneOS 2.0 apps with manually managed pthreads because NSOperationQueue had too much overhead.

The German Mac Developer magazine published his 15-page Snow Leopard special in 2010. Featured on the issue’s cover, it covered 64-bit development, Clang LLVM, Apple’s then-new Dispatch framework, and other multithreading topics.

Opening spread of the Snow Leopard article “Von 64 Bit bis GCD,” with Maximilian Götzfried’s byline
Mac Developer page with the MacDev Multithreading header, Objective-C code, article text, and a portrait of Maximilian Götzfried
Cover of Mac Developer 1/2010, featuring iPhone app development and a Snow Leopard overview
Bottom of Mac Developer page 67, centered on Grand Central Dispatch listing 31 with parts of listings 30 and 32 and the page footer
Xcode compiler menu with Clang LLVM selected
Maximilian Götzfried in Mac Developer, 2010 — with a lot more hair than today.

He started programming in Swift with version 1.0 in 2014 and released MXStatusMenu as an early project with manual memory management.

In recent years, he has migrated several high-profile projects and parsers to strict concurrency and optimized native app performance with contiguous memory, Metal, SIMD, and other advanced technologies.

His current focus is Textrun, a personal archiving and research app that will use TextrunSQL to ship post-quantum protection to its customers.