Files
sqlite/detect_test.go
zeekay 5e4e629eef fix: EncryptionAvailable is a runtime codec probe; keyed opens fail closed
The cgo EncryptionAvailable() was a compile-time `return true`, so a cgo
build linking plain sqlite (libsqlcipher absent) reported "available" while
Open/OpenDB handed the key to an engine that no-ops PRAGMA key and writes
PLAINTEXT — a file with a "SQLite format 3\0" header and rows in the clear,
believed to be encrypted by cek's per-org envelope.

- EncryptionAvailable() (cgo) now runs a cached one-time runtime probe
  (codecEncrypts): open a throwaway keyed db, prove the on-disk bytes are
  ciphertext (encryptsAtRest), and reopen under the key. A build that cannot
  truly encrypt reports false.
- The keyed-open gate moves to openDB, the single funnel Open AND OpenDB pass
  through, so both fail closed (ErrEncryptionUnavailable, no file). This
  closes the OpenDB bypass that cek/openKeyed used.
- CodecLinked() shares the same cached probe on cgo; always false on pure-Go
  (that backend never links the C codec). Both stay exported under both tags.
- encryption_proof_test drops the CodecLinked/SQLITE_REQUIRE_CODEC skip dance:
  the probe makes "available" mean "actually encrypts", so a mis-linked cgo
  build takes the same refuse-and-write-no-file branch as pure-Go.
- detect.go: the pure plaintext-detection predicate, unit-tested portably.
- probe_cgo_test.go: forces the probe to report a mis-linked codec and asserts
  Open and OpenDB both fail closed with no file (the silent-plaintext gate),
  plus the true-positive path.
2026-07-23 09:46:54 -07:00

29 lines
1.1 KiB
Go

package sqlite
import "testing"
// TestEncryptsAtRest covers the pure decision predicate behind the codec probe,
// portably (no C codec needed): plaintext is anything a SQLCipher file is not.
func TestEncryptsAtRest(t *testing.T) {
marker := []byte("SENTINEL-9f3c")
// A plaintext SQLite database (magic header) is NOT encrypted, even if the
// marker happens not to appear verbatim.
plainHeader := append([]byte("SQLite format 3\x00"), []byte("\x10\x00\x01...")...)
if encryptsAtRest(plainHeader, marker) {
t.Fatal("plaintext SQLite header accepted as encrypted")
}
// No magic header, but the sentinel is visible in the clear ⇒ not encrypted.
if encryptsAtRest([]byte("\x9anot-a-header... SENTINEL-9f3c ...tail"), marker) {
t.Fatal("visible sentinel accepted as encrypted")
}
// SQLCipher-shaped bytes: random salt where the magic would be, no sentinel in
// the clear ⇒ genuinely encrypted.
ct := []byte{0x9a, 0x3f, 0x00, 0x01, 0xde, 0xad, 0xbe, 0xef, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88}
if !encryptsAtRest(ct, marker) {
t.Fatal("ciphertext (no header, no sentinel) rejected as plaintext")
}
}