Files
Behavision/server/internal/secret/secret_test.go
Suriyakumarvijayanayagam dad04e8cda Behavision: face recognition for retail, edge to head office
Five components that ship as one product:

- behavision/  the recognition engine. RTSP ingest, YuNet detection, IoU
               tracking, ArcFace embeddings, a FAISS/SQLite gallery, and a
               FastAPI dashboard. Identity is decided once per TRACK from an
               average of at least three embeddings, never per frame.
- agent/       the Go edge agent: supervises the engine, holds a durable
               spool, and drains it to MQTT. Nothing is acked before the
               broker confirms.
- desktop/     the shop PC application (Wails + React + tray).
- server/      the cloud API, MQTT consumer, reports and assistant.
- web/         platform.loyaly.ai, the head-office app, embedded in the
               server binary.

The gallery stores 512-float embeddings and timestamps - no images unless
`app.store_faces` is switched on. Those embeddings are biometric personal
data under GDPR and India's DPDP: template inversion reconstructs a
recognisable face from an ArcFace vector, so data/behavision.db is treated
as a biometric database and DELETE /api/visitors/{id} is a real erasure.

CLAUDE.md carries the reasoning behind every non-obvious decision here,
including the ones that were measured and the ones that were wrong first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HViLj9gYNRtSr7YVZmW5sn
2026-09-04 11:14:18 +05:30

108 lines
2.9 KiB
Go

package secret
import (
"encoding/base64"
"strings"
"testing"
)
func newBox(t *testing.T) *Box {
t.Helper()
k, err := NewKey()
if err != nil {
t.Fatal(err)
}
raw, _ := base64.StdEncoding.DecodeString(k)
b, err := New(raw)
if err != nil {
t.Fatal(err)
}
return b
}
func TestRoundTrip(t *testing.T) {
b := newBox(t)
sealed, err := b.SealString("broker-password", "agent-1")
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(sealed), "broker-password") {
t.Fatal("the plaintext is visible in the ciphertext")
}
got, err := b.OpenString(sealed, "agent-1")
if err != nil || got != "broker-password" {
t.Fatalf("got %q, %v", got, err)
}
}
// The aad binds the ciphertext to the row it lives in. Without it a value
// copied from one agent's row to another decrypts happily, so a database write
// becomes a way to hand one site another site's broker credentials.
func TestCiphertextIsBoundToItsOwner(t *testing.T) {
b := newBox(t)
sealed, _ := b.SealString("broker-password", "agent-1")
if _, err := b.OpenString(sealed, "agent-2"); err == nil {
t.Fatal("a secret decrypted under the wrong agent id")
}
}
func TestAnotherKeyCannotOpenIt(t *testing.T) {
sealed, _ := newBox(t).SealString("broker-password", "agent-1")
if _, err := newBox(t).OpenString(sealed, "agent-1"); err == nil {
t.Fatal("a different key opened the ciphertext")
}
}
func TestTamperingIsDetected(t *testing.T) {
b := newBox(t)
sealed, _ := b.SealString("broker-password", "agent-1")
sealed[len(sealed)-1] ^= 0x01
if _, err := b.OpenString(sealed, "agent-1"); err == nil {
t.Fatal("a modified ciphertext was accepted")
}
}
func TestNonceIsFreshEachTime(t *testing.T) {
b := newBox(t)
// Identical plaintexts must not produce identical ciphertexts, or the
// database shows at a glance which sites share a password.
a, _ := b.SealString("same", "agent-1")
c, _ := b.SealString("same", "agent-1")
if string(a) == string(c) {
t.Fatal("the nonce is being reused")
}
}
func TestShortKeysAreRefusedRatherThanStretched(t *testing.T) {
// A key derived from whatever somebody typed into an env var has unknown
// entropy, and "it worked" would hide that permanently.
if _, err := New([]byte("too short")); err == nil {
t.Fatal("a 9-byte key was accepted")
}
if _, err := New(nil); err == nil {
t.Fatal("an empty key was accepted")
}
}
func TestTruncatedCiphertextDoesNotPanic(t *testing.T) {
b := newBox(t)
if _, err := b.Open([]byte{1, 2, 3}, "agent-1"); err == nil {
t.Fatal("a three-byte ciphertext was accepted")
}
}
func TestFromEnvReportsAMissingKeyByName(t *testing.T) {
if _, err := FromEnv("BEHAVISION_SECRET_KEY_NOT_SET_IN_TESTS"); err != ErrNoKey {
t.Fatalf("got %v", err)
}
t.Setenv("TEST_KEY", "not base64 !!!")
if _, err := FromEnv("TEST_KEY"); err == nil {
t.Fatal("garbage was accepted as a key")
}
k, _ := NewKey()
t.Setenv("TEST_KEY", k)
if _, err := FromEnv("TEST_KEY"); err != nil {
t.Fatalf("a generated key was refused: %v", err)
}
}