One toolchain across the estate. The tree carried 1.26.0, .1, .3, .4 and .5
at once, so which compiler built a binary depended on which repo it came
from.
Co-authored-by: Hanzo Dev <[email protected]>
The copy was SQLCipher's sqlcipher_export, a SQL function only the C libsqlcipher
defines. So a build without that library could not convert a plaintext database at
all: the store it was pointed at stayed unopenable, and every test covering the
conversion skipped rather than ran.
Nothing else here needed the C engine. github.com/hanzoai/sqlite encrypts on every
build — the pure-Go SQLCipher codec envelope, byte-compatible with the C one — so
the conversion was the last place the dependency survived, and it did not have to.
It is now a logical copy in Go over two ordinary handles: the plaintext source
opened with no key, a fresh encrypted target opened with one. Tables, then rows,
then indexes and triggers and views, so an index is not rebuilt per insert and a
trigger cannot fire on rows the source never had. Columns are named from
table_xinfo rather than SELECT * — a generated column cannot be inserted into, and
naming them also stops a normalised column order from writing values into the
wrong column. The AUTOINCREMENT counters come across from sqlite_sequence, which
takes a delete and an insert because that table carries no unique index for an
ON CONFLICT target to name.
A byte-level encrypt is not available for this: SQLCipher reserves 80 bytes per
page for its IV and tag, a plaintext database reserves none, and no pragma
retrofits that into an existing file.
A virtual table is REFUSED, naming itself. Its shadow tables hold an index layout
rather than rows, so recreating it regenerates bytes no logical copy can promise
and verify would report a difference the data does not explain. The C function did
that from inside the engine.
The suite now runs on the pure-Go build with nothing skipped — 30 tests, including
fidelity over AUTOINCREMENT, WITHOUT ROWID, a GENERATED column and foreign keys —
and passes identically against a real libsqlcipher. A store converted by either
engine reads back under the other, rows, header fields and counters intact.
Co-authored-by: Hanzo Dev <[email protected]>
sqlite v0.5.0 removes DeriveKey, WrapDEK, UnwrapDEK and PrincipalAAD, which is
right — a driver opens a file under a key it is handed and has no business
deriving one. v0.2.2 borrowed exactly those to read a sidecar, so it cannot be
built against v0.5.0, and taking v0.5.0 meant dropping back to v0.2.1: a cloud
that derives a key for files whose key came from crypto/rand.
But a format is not retired by the code that used to write it. The bytes on disk
still have it, in 211 org directories. Key material is this package's concern, so
this package states it: HKDF-SHA256 over the master bound to (principal, file id)
by a length-prefixed info, AES-256-GCM with the version byte leading the AAD.
Golden vectors produced under the original implementation pin every part of it —
info composition, AAD, version byte, nonce split. A silent drift now fails a
test instead of a volume.
Still read-only: no wrap, no mint, no rewrite. The test seals its own fixtures
because production never needs to.
Builds and passes against sqlite v0.5.0 and v0.4.0 alike.
Co-authored-by: Hanzo Dev <[email protected]>
A key derived from the master and the namespace: not generated, not wrapped,
not stored, not rotated in place. That removes the unwrap step, the rewrap
step, the per-file key material, and the plaintext-to-encrypted migration —
every one of which existed to carry old data forward, and none of which is a
property anyone wanted.
A database is born encrypted or it does not exist. The same namespace and
subsystem always derive the same key, so a file reopens after a restart with
nothing persisted beside it, and two databases never share a key because the
subsystem is part of the binding.
Losing the master loses the data. That is what encryption at rest means, and
why the master lives in KMS.
Co-authored-by: Hanzo Dev <[email protected]>