mirror of
https://github.com/hanzoai/sqlite.git
synced 2026-10-30 18:22:26 +00:00
Converges the driver to ONE way, always encrypted, on every build. Encryption no
longer flips with a build tag: EncryptionAvailable() is always true and a keyed
store is never written as plaintext, because the pure-Go hanzoai/sqlcipher codec
is compiled into every build.
envelope.go (tag-neutral): a keyed open decrypts the SQLCipher file to a plaintext
copy on a RAM-backed filesystem (tmpfs), the database/sql engine (modernc or
csqlite) reads/writes that copy honouring the 80-byte reserve, and Close/Checkpoint
re-encrypts it to the real path under the file's salt via a temp+atomic-rename. The
real path is always SQLCipher ciphertext, byte-compatible with C libsqlcipher.
Routing:
- pure-Go (driver_nocgo): a key always routes to the envelope. The old
ErrEncryptionUnavailable dead-end is REMOVED — native Go now encrypts, it does
not reject keys.
- cgo (driver_cgo): the CodecLinked runtime probe now SELECTS the route — live
libsqlcipher when it is proven to encrypt (per-commit durability), otherwise the
pure-Go envelope. A broken/absent libsqlcipher degrades to the pure-Go codec,
never to plaintext, never to a dead end.
- EncryptionAvailable() is one tag-neutral value: true (detect.go).
Fail-closed confidentiality: the decrypted copy only ever lives on tmpfs/ramfs; if
no RAM-backed scratch dir exists the open fails closed rather than decrypt to
persistent storage (ramfsBase, isRAMBacked via statfs). Durability is at
Close/Checkpoint (documented); single writer per file.
Checkpoint(db) re-encrypts an open envelope mid-session to bound the crash-loss
window (no-op on the live-codec / unencrypted paths).
Tests: TestKeyedRoundTripAndCheckpoint, TestKeyedWrongKeyRejected,
TestEnvelopeFailsClosedWithoutRAMFS (tag-neutral); TestBrokenLibsqlcipherFalls\
BackToEnvelope and TestEnvelopeByteCompatWithLibsqlcipher (cgo: the envelope's
output and the live codec read each other's files, both directions, in one binary).
TestEncryptionProof now asserts ciphertext on every backend (no refuse branch).
Requires hanzoai/sqlcipher v0.1.1 (EmptyPlaintext seed); pinned to the sibling
checkout until that tag is published (see the replace in go.mod).
44 lines
2.2 KiB
Go
44 lines
2.2 KiB
Go
package sqlite
|
|
|
|
import "bytes"
|
|
|
|
// EncryptionAvailable reports whether this build can encrypt a keyed store at rest.
|
|
// It is ALWAYS true: the pure-Go hanzoai/sqlcipher codec is compiled into every
|
|
// build, so a keyed open always encrypts — through the live libsqlcipher codec when
|
|
// it is linked and proven (CodecLinked), otherwise through the pure-Go codec
|
|
// envelope (envelope.go). There is no build, and no build-tag or environment
|
|
// switch, under which a keyed store falls back to plaintext.
|
|
//
|
|
// It is a value, not a capability probe: "can a keyed store be encrypted here?" is
|
|
// unconditionally yes. Which mechanism does it — live C codec vs pure-Go envelope —
|
|
// is CodecLinked's concern, and a broken libsqlcipher is caught by that runtime
|
|
// probe (never silent plaintext), which is orthogonal to whether encryption is
|
|
// available at all.
|
|
func EncryptionAvailable() bool { return true }
|
|
|
|
// sqliteMagic is the 16-byte header of an UNENCRYPTED SQLite database.
|
|
//
|
|
// A SQLCipher 4 database has NO plaintext header: page 1 on disk is
|
|
// [ salt(16) | ciphertext | IV(16) | HMAC-SHA512(64) ], so its first bytes are
|
|
// the random per-database KDF salt, never this magic (see the SQLCipher 4 page
|
|
// format in github.com/hanzoai/sqlcipher). The presence of this magic in a file
|
|
// that was opened under a key is therefore proof the codec did NOT encrypt — the
|
|
// key was silently ignored and the pages were written in the clear.
|
|
var sqliteMagic = []byte("SQLite format 3\x00")
|
|
|
|
// encryptsAtRest reports whether raw — the on-disk bytes of a database that was
|
|
// written under an encryption key — is genuinely ciphertext. It is the pure
|
|
// decision predicate behind the codec probe (EncryptionAvailable): the bytes must
|
|
// NOT begin with the plaintext SQLite magic AND must NOT contain the sentinel
|
|
// marker in the clear. Either condition means the linked engine no-op'd the key
|
|
// and wrote plaintext, so the build cannot be trusted to encrypt at rest.
|
|
func encryptsAtRest(raw, marker []byte) bool {
|
|
if bytes.HasPrefix(raw, sqliteMagic) {
|
|
return false // plaintext SQLite header ⇒ codec not encrypting
|
|
}
|
|
if bytes.Contains(raw, marker) {
|
|
return false // sentinel visible in the clear ⇒ not encrypted
|
|
}
|
|
return true
|
|
}
|