Files
verdande/internal/secret/secret_test.go
T
Kristian WindandClaude Opus 5 a38a79e9b6 feat(secrets): tokens og kodeord forlader ikke længere huset med en kopi
Gmails refresh-token lå i klartekst i databasen. Det er ikke filen på serveren,
der er problemet, men kopierne af den: sikkerhedskopier kan hentes ned gennem
fladen, så hver eneste kopi, der er taget indtil nu, bar et levende token.

Derfor ligger nøglen uden for databasen — i miljøet, eller i en fil ved siden af
dataene med tilstand 0600. VAPID-parret er præcedens for en hemmelighed,
instansen selv laver og gemmer i basen, men det mønster dur ikke her: en nøgle i
samme fil som det, den låser, er ingen lås. Backup er VACUUM INTO af selve
databasen, så filen følger ikke med.

Kun de navngivne felter forsegles — refresh_token, access_token, password,
client_secret. Resten af klatten er det, man læser, når en postkasse driller:
hvilken mappe, hvilken etiket, hvilken adresse. At kryptere det med ville ikke
købe noget og ville koste muligheden for at kigge.

Ingen migration, og ingen der skal forbinde forfra: en værdi uden mærket læses
som sig selv, og næste skrivning lægger den forseglet på plads. Kolonnen
konverterer sig selv efterhånden som den bruges, i stedet for i én omgang, der
skal have ret om hver eneste række på én gang. Der er en prøve for netop det.

Fladen siger det på siden, hvor man henter kopien — for den dag nøglen betyder
noget, er den dag, man gendanner et andet sted og undrer sig over, at postkassen
er tavs. Runen er på v4; en ny variabel uden et versionsspring er usynlig.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:18:48 +02:00

150 lines
3.8 KiB
Go

package secret
import (
"encoding/base64"
"os"
"path/filepath"
"strings"
"testing"
)
func TestAValueSurvivesTheRoundTrip(t *testing.T) {
b := mustBox(t)
for _, in := range []string{"1//0abc-refresh", "kodeord med æøå", strings.Repeat("x", 5000)} {
sealed, err := b.Seal(in)
if err != nil {
t.Fatal(err)
}
if strings.Contains(sealed, in) {
t.Fatal("the sealed form still contains the value")
}
out, err := b.Unseal(sealed)
if err != nil {
t.Fatal(err)
}
if out != in {
t.Errorf("got %q, want %q", out, in)
}
}
}
// Two seals of the same value must not look alike, or the column tells anyone
// reading it which accounts share a password.
func TestSealingTwiceGivesDifferentText(t *testing.T) {
b := mustBox(t)
first, _ := b.Seal("same")
second, _ := b.Seal("same")
if first == second {
t.Error("the same value sealed to the same text twice")
}
}
// The column holds values written before there was a key. Reading must not fail
// on them, and sealing must not wrap an already-sealed value a second time.
func TestPlainTextIsReadAsItself(t *testing.T) {
b := mustBox(t)
out, err := b.Unseal("1//0abc-written-before-there-was-a-key")
if err != nil {
t.Fatal(err)
}
if out != "1//0abc-written-before-there-was-a-key" {
t.Errorf("an unsealed value came back as %q", out)
}
sealed, _ := b.Seal("x")
again, _ := b.Seal(sealed)
if again != sealed {
t.Error("an already-sealed value was sealed a second time")
}
}
func TestNothingIsStillNothing(t *testing.T) {
b := mustBox(t)
if out, _ := b.Seal(""); out != "" {
t.Errorf("an empty value sealed to %q", out)
}
}
// The message somebody gets after restoring a database without its key file. It
// has to point at the key, because every other symptom points at the mailbox.
func TestTheWrongKeySaysItIsTheWrongKey(t *testing.T) {
sealed, err := mustBox(t).Seal("hemmelig")
if err != nil {
t.Fatal(err)
}
_, err = mustBox(t).Unseal(sealed)
if err == nil {
t.Fatal("another key opened it")
}
if !strings.Contains(err.Error(), "different key") {
t.Errorf("the error does not name the cause: %v", err)
}
}
func TestTheKeyFileIsMadeOnceAndKeptPrivate(t *testing.T) {
dir := t.TempDir()
if _, err := Open("", dir); err != nil {
t.Fatal(err)
}
path := filepath.Join(dir, KeyFile)
info, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
if perm := info.Mode().Perm(); perm != 0o600 {
t.Errorf("the key file is %04o, not 0600", perm)
}
// Opening again must reuse it. A second key here would quietly make every
// stored token unreadable, and it would look like the mailboxes disconnecting
// themselves overnight.
first, _ := os.ReadFile(path)
if _, err := Open("", dir); err != nil {
t.Fatal(err)
}
second, _ := os.ReadFile(path)
if string(first) != string(second) {
t.Error("opening a second time replaced the key")
}
}
func TestAKeyOthersCanReadIsRefused(t *testing.T) {
dir := t.TempDir()
if _, err := Open("", dir); err != nil {
t.Fatal(err)
}
path := filepath.Join(dir, KeyFile)
if err := os.Chmod(path, 0o644); err != nil {
t.Fatal(err)
}
if _, err := Open("", dir); err == nil {
t.Error("a world-readable key was accepted")
}
}
// The environment wins, so a host can keep the key off its disks altogether.
func TestTheEnvironmentBeatsTheFile(t *testing.T) {
dir := t.TempDir()
key := base64.StdEncoding.EncodeToString([]byte(strings.Repeat("k", 32)))
if _, err := Open(key, dir); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(dir, KeyFile)); !os.IsNotExist(err) {
t.Error("a key file was written even though the environment carried one")
}
if _, err := Open("too-short", dir); err == nil {
t.Error("a key that is not 32 bytes was accepted")
}
}
func mustBox(t *testing.T) *Box {
t.Helper()
b, err := Open("", t.TempDir())
if err != nil {
t.Fatal(err)
}
return b
}