oficina / sheets Public

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
Paul Campbell b1a19623ab11f87d7f86af389e8521e0fc50965e parent cd4780e
2 files changed, +178 −0
  • internal/sheets/sealtest/sealtest.go +79 −0
  • internal/sheets/sealtest/sealtest_test.go +99 −0
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)
+ }
+}