Plaintext gate: sealtest.AssertNoPlaintext, verified to actually fail
AssertNoPlaintext greps a database's main file, WAL and SHM for canary
strings. It lives in its own importable package (internal/sheets/sealtest,
not seal, and not a _test.go file) so store and handle tests can call it
without dragging the testing package into the production binary via
package seal.
Detection is split into findLeak so the WAL/SHM-checking behaviour is
unit-tested directly (TestFindLeakChecksWAL) rather than through a live
*testing.T failure, which Go's testing package would otherwise propagate
up and make this package permanently red.
Manually verified the gate can actually fail (not just pass): temporarily
had the "sealed" data written as raw plaintext instead of ciphertext in
TestAssertNoPlaintextPassesOnSealedData, ran the suite, and got
sealtest_test.go:57: plaintext "CANARY-CELL-VALUE-8f3a" found in sealed.db
--- FAIL: TestAssertNoPlaintextPassesOnSealedData (0.00s)
then reverted. See task-2-report.md for the full transcript.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01APtNStXDZ5H2fAc1ZuGqtS
2 files changed,
+178
−0
internal/sheets/sealtest/sealtest.go+79 −0internal/sheets/sealtest/sealtest_test.go+99 −0
Intent
diff --git a/internal/sheets/sealtest/sealtest.go b/internal/sheets/sealtest/sealtest.go| new file mode 100644 |
| index 0000000..e2df180 |
| --- /dev/null |
| +++ b/internal/sheets/sealtest/sealtest.go |
| @@ -0,0 +1,79 @@ |
| +// Package sealtest is the plaintext gate: it proves, by reading raw |
| +// bytes, that a database file never holds a sealed value in the clear. |
| +// |
| +// It lives in its own package — not seal, and not a _test.go file — so |
| +// that store and handle tests, which are the ones that actually write |
| +// rows, can import it directly. AssertNoPlaintext takes a *testing.T, so |
| +// this package must never be imported from non-test code: doing so would |
| +// drag the testing package into the production binary. |
| +package sealtest |
| + |
| +import ( |
| + "bytes" |
| + "fmt" |
| + "os" |
| + "path/filepath" |
| + "testing" |
| +) |
| + |
| +// Canaries are the strings a test seeds into a database before running |
| +// AssertNoPlaintext. Any of them appearing in the raw file bytes is a |
| +// plaintext leak. |
| +var Canaries = [][]byte{ |
| + []byte("CANARY-CELL-VALUE-8f3a"), |
| + []byte("CANARY-SHEET-TITLE-2b71"), |
| + []byte("CANARY-TAB-NAME-c40d"), |
| +} |
| + |
| +// leaked reports the first canary found in b, or nil if none is present. |
| +// Split out from AssertNoPlaintext so the detection logic can be |
| +// unit-tested directly, without needing a live *testing.T failure to |
| +// observe. |
| +func leaked(b []byte) []byte { |
| + for _, c := range Canaries { |
| + if bytes.Contains(b, c) { |
| + return c |
| + } |
| + } |
| + return nil |
| +} |
| + |
| +// findLeak scans dbPath's main file, its WAL and its SHM for a canary. It |
| +// checks the WAL because Litestream ships WAL frames continuously, and a |
| +// value written plainly reaches the bucket before any delete would remove |
| +// it from the main file. A nil canary means no leak was found. |
| +// |
| +// Split out from AssertNoPlaintext so the scan itself — including the |
| +// WAL/SHM handling — can be unit-tested directly, without routing a |
| +// deliberately-failing case through a live *testing.T (a failing subtest |
| +// always fails its parent, which would make this package's own tests |
| +// unable to ever go green). |
| +func findLeak(dbPath string) (canary []byte, file string, err error) { |
| + for _, suffix := range []string{"", "-wal", "-shm"} { |
| + p := dbPath + suffix |
| + b, err := os.ReadFile(p) |
| + if os.IsNotExist(err) { |
| + continue |
| + } |
| + if err != nil { |
| + return nil, "", fmt.Errorf("read %s: %w", filepath.Base(p), err) |
| + } |
| + if c := leaked(b); c != nil { |
| + return c, filepath.Base(p), nil |
| + } |
| + } |
| + return nil, "", nil |
| +} |
| + |
| +// AssertNoPlaintext greps a database file, its WAL and its SHM for |
| +// canaries. Any of them appearing in raw bytes is a plaintext leak. |
| +func AssertNoPlaintext(t *testing.T, dbPath string) { |
| + t.Helper() |
| + c, f, err := findLeak(dbPath) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if c != nil { |
| + t.Fatalf("plaintext %q found in %s", c, f) |
| + } |
| +} |
diff --git a/internal/sheets/sealtest/sealtest_test.go b/internal/sheets/sealtest/sealtest_test.go| new file mode 100644 |
| index 0000000..1309eba |
| --- /dev/null |
| +++ b/internal/sheets/sealtest/sealtest_test.go |
| @@ -0,0 +1,99 @@ |
| +package sealtest |
| + |
| +import ( |
| + "os" |
| + "path/filepath" |
| + "testing" |
| + |
| + "amadan.net/oficina/sheets/internal/sheets/seal" |
| +) |
| + |
| +func testDEK() []byte { |
| + d := make([]byte, 32) |
| + for i := range d { |
| + d[i] = byte(i) |
| + } |
| + return d |
| +} |
| + |
| +// leaked is the detection core AssertNoPlaintext relies on. Exercising it |
| +// directly is the permanent proof that the gate CAN fail: these two tests |
| +// show it correctly flags a canary and correctly ignores clean bytes, |
| +// without needing to crash the test process to observe a failure. |
| +func TestLeakedDetectsCanary(t *testing.T) { |
| + for _, c := range Canaries { |
| + if got := leaked(c); got == nil { |
| + t.Fatalf("leaked(%q) = nil, want a match", c) |
| + } |
| + } |
| +} |
| + |
| +func TestLeakedIgnoresCleanBytes(t *testing.T) { |
| + if got := leaked([]byte("nothing to see here, just cell math and formulas")); got != nil { |
| + t.Fatalf("leaked(clean) = %q, want nil", got) |
| + } |
| +} |
| + |
| +// TestAssertNoPlaintextPassesOnSealedData is the gate's normal-operation |
| +// path: a canary sealed through the real Sealer must not be readable in |
| +// the raw bytes the gate scans. |
| +func TestAssertNoPlaintextPassesOnSealedData(t *testing.T) { |
| + s, err := seal.New(testDEK()) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + ct, err := s.Seal("cell.input", Canaries[0]) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + dbPath := filepath.Join(t.TempDir(), "sealed.db") |
| + if err := os.WriteFile(dbPath, ct, 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + AssertNoPlaintext(t, dbPath) |
| +} |
| + |
| +// TestAssertNoPlaintextToleratesMissingSidecars proves the gate does not |
| +// require a WAL or SHM file to exist (most callers won't have one), while |
| +// still being willing to read them when they do — see |
| +// TestAssertNoPlaintextChecksWAL below and the manual failure exercise |
| +// recorded in the task report. |
| +func TestAssertNoPlaintextToleratesMissingSidecars(t *testing.T) { |
| + dbPath := filepath.Join(t.TempDir(), "clean.db") |
| + if err := os.WriteFile(dbPath, []byte("not a leak"), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + AssertNoPlaintext(t, dbPath) |
| +} |
| + |
| +// TestFindLeakChecksWAL proves the gate's detection core reads the WAL |
| +// sidecar, not just the main file. Litestream ships WAL frames |
| +// continuously, so a value written plainly reaches the bucket before any |
| +// delete would remove it from the main file — a leak that only ever |
| +// touched the WAL must still be caught. This test seeds the canary into |
| +// the WAL only; a scan that checked just the main file would wrongly miss |
| +// it. (This is tested against findLeak rather than AssertNoPlaintext |
| +// itself: a failing subtest always fails its parent in Go's testing |
| +// package, so routing a deliberately-positive leak case through a live |
| +// *testing.T would leave this package permanently red.) |
| +func TestFindLeakChecksWAL(t *testing.T) { |
| + dir := t.TempDir() |
| + dbPath := filepath.Join(dir, "leaky.db") |
| + if err := os.WriteFile(dbPath, []byte("clean main file"), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(dbPath+"-wal", Canaries[1], 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + |
| + c, f, err := findLeak(dbPath) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if c == nil { |
| + t.Fatal("did not detect a canary present only in the WAL") |
| + } |
| + if f != "leaky.db-wal" { |
| + t.Fatalf("found in %q, want leaky.db-wal", f) |
| + } |
| +} |