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
108 lines
2.9 KiB
Go
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)
|
|
}
|
|
}
|