2e6436608f
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---
232 lines
6.0 KiB
Go
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))
|
|
}
|
|
}
|