mirror of
https://github.com/hanzoai/llm-toolchain.git
synced 2026-10-30 18:29:08 +00:00
116 lines
3.5 KiB
Go
116 lines
3.5 KiB
Go
package server_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hanzoai/base/core"
|
|
luxlog "github.com/luxfi/log"
|
|
zaplib "github.com/luxfi/zap"
|
|
zcap "github.com/zap-proto/go/cap"
|
|
|
|
gen "github.com/hanzoai/llm-toolchain/gen"
|
|
"github.com/hanzoai/llm-toolchain/server"
|
|
)
|
|
|
|
// TestSecretEncryptedAtRest is the durable security proof: after a key is
|
|
// created, the PLAINTEXT secret must not appear anywhere in the on-disk SQLite
|
|
// files — only AES-256-GCM ciphertext. We boot a Base app against a real data
|
|
// dir, create a key over the live ZAP path, then scan every *.db byte for the
|
|
// plaintext. This is the CLAUDE.md "never store secrets in plaintext" invariant,
|
|
// verified on disk rather than asserted in prose.
|
|
func TestSecretEncryptedAtRest(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
|
|
app := core.NewBaseApp(core.BaseAppConfig{
|
|
DataDir: dataDir,
|
|
IsDev: true,
|
|
})
|
|
if err := app.Bootstrap(); err != nil {
|
|
t.Fatalf("bootstrap: %v", err)
|
|
}
|
|
defer app.ResetBootstrapState()
|
|
if err := app.RunAllMigrations(); err != nil {
|
|
t.Fatalf("migrations: %v", err)
|
|
}
|
|
|
|
if err := server.EnsureCollections(app); err != nil {
|
|
t.Fatalf("ensure collections: %v", err)
|
|
}
|
|
secrets, err := server.NewSecretBox(testMasterKey, testOrg)
|
|
if err != nil {
|
|
t.Fatalf("secret box: %v", err)
|
|
}
|
|
|
|
const port = 19740
|
|
node := zaplib.NewNode(zaplib.NodeConfig{NodeID: "llmtc-atrest-srv", Port: port, NoDiscovery: true})
|
|
srv := server.NewServer(app, luxlog.New("component", "llmtc-atrest"), testOrg, secrets, zcap.Verifier{})
|
|
srv.Register(node)
|
|
if err := node.Start(); err != nil {
|
|
t.Fatalf("node start: %v", err)
|
|
}
|
|
defer node.Stop()
|
|
|
|
cli, stopCli := newClient(t, "127.0.0.1:"+itoa(port), "llmtc-atrest-srv", server.PermKeyWrite|server.PermKeyRead, 19741)
|
|
defer stopCli()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
const plaintext = "sk-PLAINTEXT-MUST-NOT-PERSIST-9f8e7d6c"
|
|
const headerVal = "HEADER-SECRET-SHOULD-ALSO-BE-ENCRYPTED"
|
|
if _, err := cli.KeyCreate(ctx, gen.KeyCreateParamsInput{
|
|
ProjectId: "p1",
|
|
Provider: "openai",
|
|
Adapter: "openai",
|
|
SecretKey: plaintext,
|
|
ExtraHeaders: `{"X-Secret":"` + headerVal + `"}`,
|
|
}); err != nil {
|
|
t.Fatalf("KeyCreate: %v", err)
|
|
}
|
|
|
|
// Force a checkpoint so the WAL is flushed into the main db file, then scan
|
|
// every db artifact (db, -wal, -shm) for the plaintext.
|
|
if _, err := app.DB().NewQuery("PRAGMA wal_checkpoint(TRUNCATE)").Execute(); err != nil {
|
|
t.Logf("wal_checkpoint: %v (continuing — scanning all db artifacts anyway)", err)
|
|
}
|
|
node.Stop() // release file handles / flush
|
|
|
|
needles := [][]byte{[]byte(plaintext), []byte(headerVal)}
|
|
scanned := 0
|
|
err = filepath.Walk(dataDir, func(path string, info os.FileInfo, werr error) error {
|
|
if werr != nil || info.IsDir() {
|
|
return nil
|
|
}
|
|
data, rerr := os.ReadFile(path)
|
|
if rerr != nil {
|
|
return nil
|
|
}
|
|
scanned++
|
|
for _, n := range needles {
|
|
if bytes.Contains(data, n) {
|
|
t.Fatalf("SECURITY: plaintext %q found at rest in %s", n, path)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("walk data dir: %v", err)
|
|
}
|
|
if scanned == 0 {
|
|
t.Fatal("scanned no files — data dir empty, test would be vacuous")
|
|
}
|
|
t.Logf("at-rest OK: scanned %d files under %s, no plaintext secret present", scanned, dataDir)
|
|
|
|
// Sanity: the SecretBox round-trips (so we know encryption, not omission,
|
|
// is what kept the plaintext off disk).
|
|
sealed, _ := secrets.Seal(plaintext)
|
|
if got, _ := secrets.Open(sealed); got != plaintext {
|
|
t.Fatalf("secretbox round-trip failed: got %q", got)
|
|
}
|
|
}
|