github.com has no runner for hanzo-build-linux-amd64, so a caller under
.github/workflows is a gate that can never be scheduled. This is the ~7-line
caller on the plane that can (git.hanzo.ai git-runner fleet), pinned @v1.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
EncryptFile requires its source to reserve Reserve (80) bytes per page, but a
plaintext SQLite engine cannot be told to create a database with a non-zero
reserve through SQL (the reserve is SQLITE_FCNTL_RESERVE_BYTES, not a PRAGMA).
So a pure-Go writer that wants a fresh encrypted database had nowhere to start.
EmptyPlaintext returns a minimal empty SQLite database (one leaf page, empty
sqlite_master) with byte 20 = Reserve, per the SQLite file format spec. A driver
opens it, writes its schema (SQLite honours the header reserve, so every page it
writes leaves room for the IV+HMAC trailer), then EncryptFile turns the result
into a SQLCipher database.
- empty.go: EmptyPlaintext(Params) — validates page size (power of two, usable
>= SQLite's 480-byte minimum, so >= 1024 with an 80-byte reserve; 65536 encoded
as the value 1 per the spec).
- empty_test.go (zero-dep): structural layout + EncryptFile/DecryptFile round trip.
- parity: TestParityNewDatabaseFromSeed — mint the seed, modernc writes a table,
this port encrypts, and the C SQLCipher library reads back what Go wrote.
Reads a database the C library wrote, decrypting pages in memory, on
modernc's existing read-only VFS. The pager-level blocker that rules out a
read-write VFS does not apply: reading has no WAL frames and no journal
records whose checksums could disagree.
immutable=1 is load-bearing, not decoration. modernc's read-only VFS reports
os.O_RDONLY (0) out of xOpen where SQLite expects SQLITE_OPEN_READONLY (1),
and SQLite decides read-only from exactly that flag (pager.c:5044 ->
btree.c:2640). So mode=ro silently yields a read-WRITE pager, leaving only
that VFS's blanket refusal to open a journal between a caller and its null
xWrite. PRAGMA journal_mode=MEMORY removes the accident and the next write
SIGSEGVs the process -- reproduced, and now a regression test. immutable=1
routes through pager.c:5081, where readOnly comes from vfsFlags rather than
from the VFS, so the pager really is read-only and writes are refused before
any journal or page write. It cannot be turned off from SQL, unlike PRAGMA
query_only.
Read-only is proven at open, not assumed: Open runs a write and refuses to
return a handle if it succeeds.
Open also refuses a database with a non-empty -wal or -journal beside it.
Either may hold committed data not yet in the main file, or uncommitted data
that must come out of it, and whether a WAL is checkpointed cannot be settled
from the file alone. Answering anyway would mean stale numbers off a ledger.
A wrong key is an error at open, not corruption later.
parity/ is a separate module so the format package keeps zero dependencies.
It runs the full loop with two independent engines as oracles: the C library
writes -> pure Go reads -> modernc writes into the decrypted database -> pure
Go encrypts -> the C library reads back what Go wrote. It skips where
libsqlcipher or a C compiler are absent; the committed fixtures cover the
C-written direction everywhere.
LLM.md records the VFS-vs-codec verdict with its evidence, so it does not get
re-litigated from memory: the codec is a pager hook, not a VFS hook, and
SQLite checksums WAL frames and journal records over the CIPHERTEXT the codec
returns -- verified numerically against a real hot C-written WAL. A VFS shim
sits below the pager and cannot reproduce that.
Golden vectors pin the KDF, the page-authentication key derivation and the
exact on-disk bytes of a page under a fixed IV, so a refactor cannot silently
change the format.
The vectors alone would only pin this port to itself, so both are
cross-verified against libsqlcipher 4.5.6 by decrypting databases the C
library actually wrote:
testdata/c-4.5.6.db raw key (x'HEX'), 7 pages, overflow +
index + random and zero blobs
testdata/c-4.5.6-passphrase.db passphrase, proving PBKDF2-HMAC-SHA512 at
256000 iterations agrees byte for byte
Fail-closed is asserted, not assumed: wrong key, wrong salt, wrong
passphrase, a flipped ciphertext/IV/tag bit, and a page replayed at another
page number all return ErrKey with no data.
Note the reserve trailer carries the IV and is fresh on every write, so a
round-trip is the identity on the usable bytes, not on the trailer.
Port of the SQLCipher codec (sqlcipher/sqlcipher v4.5.6, BSD-3, ZETETIC LLC)
in pure Go, with no third-party dependencies.
The format, and only the format: key derivation, per-page AES-256-CBC,
per-page HMAC-SHA512 over ciphertext||IV||pgno_le32, and page-1 salt
handling. It knows nothing about database/sql or SQLite drivers, so a
driver, a backup path, a migration or a forensic tool can all use it
without dragging in an engine.
Constants taken from the C source and confirmed against libsqlcipher 4.5.6:
kdf_iter 256000, fast_kdf_iter 2, hmac salt mask 0x3a, salt 16 bytes,
key 32, iv 16, hmac 64, reserve 80, LE page numbers.
Decrypt authenticates before it decrypts: a wrong key returns ErrKey, never
garbage.