Dual-backend SQLite driver for the Hanzo ecosystem. Registers the
database/sql driver name sqlite under both build configurations and
exposes the same API either way:
| Build | Backend | Encryption | Use |
|---|---|---|---|
CGO_ENABLED=1 + -tags libsqlite3 + libsqlcipher |
mattn/go-sqlite3 → SQLCipher | AES-256 page-level, at rest | production |
CGO_ENABLED=0 |
modernc.org/sqlite (pure Go) | none | CI tests / lint / local dev |
One import, one driver name, two backends:
import _ "github.com/hanzoai/sqlite" // registers "sqlite" under both tags
db, _ := sql.Open("sqlite", dsn) // mattn+SQLCipher (cgo) or modernc (!cgo)The pure-Go backend cannot encrypt. Demanding a key on it
(Open(path, WithKey(...)), OpenDB(path, key)) returns
ErrEncryptionUnavailable and writes nothing — it never silently persists
plaintext.
mainline mattn/go-sqlite3 has no sqlcipher build tag and no
sqlite3_key() binding. SQLCipher works only when you:
- link the system sqlite (the
libsqlite3tag) against libsqlcipher, and - enable the codec + URI keying via CGO flags, and
- supply the key as SQLCipher's native URI
keyparameter so it is applied insidesqlite3_open_v2— before mattn's pragma battery runs.
CGO_ENABLED=1 \
CGO_CFLAGS="-DSQLITE_HAS_CODEC -DSQLITE_USE_URI=1 -I<sqlcipher>/include/sqlcipher" \
CGO_LDFLAGS="-L<sqlcipher>/lib -lsqlcipher" \
go build -tags "libsqlite3 sqlite_fts5" ./...Alpine: apk add gcc musl-dev sqlcipher-dev pkgconfig.
Both are traps that ship plaintext:
-tags sqlcipheris inert in mainline mattn (no such tag) → links plain sqlite →PRAGMA keyis a silent no-op.- mattn runs
PRAGMA busy_timeout/journal_mode/foreign_keys/...viasqlite3_execbefore the ConnectHook fires. On an existing encrypted file that touches the header before the key is set →file is not a databaseon reopen. So a ConnectHook can create but never reopen a SQLCipher DB.
The URI key parameter sidesteps both: SQLCipher's VFS keys the connection at
open time. TestEncryptionProof asserts real ciphertext on disk and a working
keyed reopen, so a mis-linked build fails CI instead of shipping plaintext.
The key rides the DSN (
file:PATH?...&key=x'HEX'). Never log the DSN. IAM keepsshowSql=falseand does not log it.
- Algorithm: AES-256 (SQLCipher 4 defaults: 4096-byte pages, PBKDF2-HMAC-SHA512, 256000 iters, per-page HMAC-SHA512).
- Key: raw 256-bit (no passphrase KDF) via
WithRawKey/ thekey=x'HEX'DSN param, or derived per principal:
// CEK = HKDF-SHA256(masterKey, "{org|user}:{id}")
db, _ := sqlite.Open("data.db", sqlite.WithPrincipalKey(masterKey, sqlite.PrincipalOrg, "acme"))
// Or get a *sql.DB to hand to xorm.NewEngineWithDB:
cek, _ := sqlite.DeriveKey(masterKey, sqlite.PrincipalOrg, "acme")
sqldb, _ := sqlite.OpenDB(dbPath, cek)
eng, _ := xorm.NewEngineWithDB("sqlite", "", core.FromDB(sqldb))Different orgs/users get different CEKs (domain-separated info); destroying the
master key renders every derived CEK irrecoverable.
DeriveKey(masterKey, principalType, principalID) → 32-byte CEK via HKDF-SHA256.
Master key is 32 bytes, sourced from KMS. Used for per-org and per-user database
isolation in IAM, KMS, and other Hanzo services.
ThresholdManager coordinates t-of-n Ed25519 attestations for writes (MPC shard
storage, multi-sig). Pure Go; available under both backends.
MIT OR Apache-2.0, at your option — per HIP-0137.