Files
orca/internal/secrets/secrets_test.go
T
Jon Chery 2e6436608f feat(P03): secrets subsystem (REQ-080) — AES-256-GCM + HKDF-SHA256 per-ns
internal/secrets/secrets.go: master key (0600), HKDF-SHA256 per-ns
derivation, AES-256-GCM per-line with AAD=line-number (anti-swap),
EncryptEnvFile/DecryptEnvFile, LoadCredential= map generation.
internal/cli/secrets.go: orca secrets set/get/list/rotate/delete.
Tests: round-trip, nonce uniqueness, AAD anti-swap, 0600 enforcement.

---ci---
project: orca
phase: 03
milestone: v0.11
status: execute
---/ci---
2026-08-07 04:47:33 +00:00

232 lines
6.0 KiB
Go

package secrets
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestGenerateMasterKey(t *testing.T) {
k1, err := GenerateMasterKey()
if err != nil {
t.Fatalf("GenerateMasterKey: %v", err)
}
if len(k1) != MasterKeyLen {
t.Fatalf("key len = %d, want %d", len(k1), MasterKeyLen)
}
k2, err := GenerateMasterKey()
if err != nil {
t.Fatalf("GenerateMasterKey 2: %v", err)
}
if string(k1) == string(k2) {
t.Fatalf("GenerateMasterKey returned identical bytes twice — crypto/rand not random?")
}
}
func TestSaveLoadMasterKey_0600(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "master.key")
key, err := GenerateMasterKey()
if err != nil {
t.Fatalf("GenerateMasterKey: %v", err)
}
if err := SaveMasterKey(path, key); err != nil {
t.Fatalf("SaveMasterKey: %v", err)
}
info, err := os.Stat(path)
if err != nil {
t.Fatalf("stat: %v", err)
}
if info.Mode().Perm() != 0o600 {
t.Fatalf("perm = %04o, want 0600", info.Mode().Perm())
}
loaded, err := LoadMasterKey(path)
if err != nil {
t.Fatalf("LoadMasterKey: %v", err)
}
if string(loaded) != string(key) {
t.Fatalf("loaded key differs from saved key")
}
}
func TestLoadMasterKey_RefusesLoose(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "master.key")
key, err := GenerateMasterKey()
if err != nil {
t.Fatalf("GenerateMasterKey: %v", err)
}
if err := SaveMasterKey(path, key); err != nil {
t.Fatalf("SaveMasterKey: %v", err)
}
if err := os.Chmod(path, 0o644); err != nil {
t.Fatalf("chmod 0644: %v", err)
}
_, err = LoadMasterKey(path)
if err == nil {
t.Fatalf("LoadMasterKey succeeded with mode 0644, want refuse")
}
if !strings.Contains(err.Error(), "REFUSE") {
t.Fatalf("error missing REFUSE marker: %v", err)
}
}
func TestLoadMasterKey_WrongLen(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "master.key")
if err := os.WriteFile(path, []byte("tooshort"), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
_, err := LoadMasterKey(path)
if err == nil {
t.Fatalf("LoadMasterKey succeeded with wrong length, want error")
}
}
func TestDeriveNamespaceKey_Deterministic(t *testing.T) {
mk, _ := GenerateMasterKey()
a1, err := DeriveNamespaceKey(mk, "nsA")
if err != nil {
t.Fatalf("DeriveA: %v", err)
}
a2, err := DeriveNamespaceKey(mk, "nsA")
if err != nil {
t.Fatalf("DeriveA 2: %v", err)
}
if string(a1) != string(a2) {
t.Fatalf("same (master,namespace) yielded different sub-keys")
}
if len(a1) != SubKeyLen {
t.Fatalf("sub-key len = %d, want %d", len(a1), SubKeyLen)
}
}
func TestDeriveNamespaceKey_DifferentNamespaces(t *testing.T) {
mk, _ := GenerateMasterKey()
a, err := DeriveNamespaceKey(mk, "nsA")
if err != nil {
t.Fatalf("DeriveA: %v", err)
}
b, err := DeriveNamespaceKey(mk, "nsB")
if err != nil {
t.Fatalf("DeriveB: %v", err)
}
if string(a) == string(b) {
t.Fatalf("different namespaces yielded the same sub-key")
}
}
func TestDeriveNamespaceKey_EmptyNamespace(t *testing.T) {
mk, _ := GenerateMasterKey()
if _, err := DeriveNamespaceKey(mk, ""); err == nil {
t.Fatalf("DeriveNamespaceKey(empty) succeeded, want error")
}
}
func TestEncryptDecryptLine_RoundTrip(t *testing.T) {
mk, _ := GenerateMasterKey()
nsKey, err := DeriveNamespaceKey(mk, "ns")
if err != nil {
t.Fatalf("Derive: %v", err)
}
pt := []byte("API_KEY=supersecret")
enc, err := EncryptLine(nsKey, 1, pt)
if err != nil {
t.Fatalf("EncryptLine: %v", err)
}
dec, err := DecryptLine(nsKey, 1, enc)
if err != nil {
t.Fatalf("DecryptLine: %v", err)
}
if string(dec) != string(pt) {
t.Fatalf("round-trip mismatch: got %q want %q", dec, pt)
}
}
func TestNonceUniqueness(t *testing.T) {
mk, _ := GenerateMasterKey()
nsKey, _ := DeriveNamespaceKey(mk, "ns")
pt := []byte("same plaintext")
a, _ := EncryptLine(nsKey, 1, pt)
b, _ := EncryptLine(nsKey, 1, pt)
if a == b {
t.Fatalf("encrypting same plaintext twice yielded the same ciphertext — nonce not random")
}
}
func TestAADLineNumber_SwapFails(t *testing.T) {
mk, _ := GenerateMasterKey()
nsKey, _ := DeriveNamespaceKey(mk, "ns")
line1 := []byte("A=1")
line2 := []byte("B=2")
enc1, _ := EncryptLine(nsKey, 1, line1)
enc2, _ := EncryptLine(nsKey, 2, line2)
if _, err := DecryptLine(nsKey, 1, enc1); err != nil {
t.Fatalf("decrypt line1 at pos1 failed: %v", err)
}
if _, err := DecryptLine(nsKey, 2, enc2); err != nil {
t.Fatalf("decrypt line2 at pos2 failed: %v", err)
}
if _, err := DecryptLine(nsKey, 1, enc2); err == nil {
t.Fatalf("decrypted line2 ciphertext at pos1 — AAD line-swap NOT detected")
}
if _, err := DecryptLine(nsKey, 2, enc1); err == nil {
t.Fatalf("decrypted line1 ciphertext at pos2 — AAD line-swap NOT detected")
}
}
func TestEncryptEnvFile_RoundTrip(t *testing.T) {
mk, _ := GenerateMasterKey()
nsKey, _ := DeriveNamespaceKey(mk, "ns")
lines := []string{
"API_KEY=abc",
"DB_PASSWORD=hunter2",
"",
"TOKEN=xyz",
}
enc, err := EncryptEnvFile(nsKey, lines)
if err != nil {
t.Fatalf("EncryptEnvFile: %v", err)
}
dec, err := DecryptEnvFile(nsKey, enc)
if err != nil {
t.Fatalf("DecryptEnvFile: %v", err)
}
if len(dec) != len(lines) {
t.Fatalf("len(dec) = %d, want %d", len(dec), len(lines))
}
for i, want := range lines {
if dec[i] != want {
t.Fatalf("line %d: got %q want %q", i, dec[i], want)
}
}
}
func TestGenerateLoadCredentialFile(t *testing.T) {
mk, _ := GenerateMasterKey()
nsKey, _ := DeriveNamespaceKey(mk, "ns")
lines := []string{
"API_KEY=value1",
"DB_PASSWORD=value2",
}
enc, err := EncryptEnvFile(nsKey, lines)
if err != nil {
t.Fatalf("EncryptEnvFile: %v", err)
}
encLines := strings.Split(strings.TrimRight(enc, "\n"), "\n")
m, err := GenerateLoadCredentialFile(nsKey, encLines)
if err != nil {
t.Fatalf("GenerateLoadCredentialFile: %v", err)
}
if m["API_KEY"] != "value1" {
t.Fatalf("API_KEY = %q, want value1", m["API_KEY"])
}
if m["DB_PASSWORD"] != "value2" {
t.Fatalf("DB_PASSWORD = %q, want value2", m["DB_PASSWORD"])
}
if len(m) != 2 {
t.Fatalf("map size = %d, want 2", len(m))
}
}