gitmote

gitmote / internal / secrets / keyring_test.go  ·  history  ·  branches

4237 bytes · raw

package secrets

import (
	"encoding/base64"
	"strings"
	"testing"
)

// b64key returns a distinct base64 32-byte key seeded by n.
func b64key(n byte) string {
	var k [masterKeyLen]byte
	for i := range k {
		k[i] = n + byte(i)
	}
	return base64.StdEncoding.EncodeToString(k[:])
}

// keyringEnv sets GITMOTE_CI_SECRET_KEY_V<v> for each (v→key) and builds a
// keyring from the env.
func keyringEnv(t *testing.T, keys map[int]string) *Keyring {
	t.Helper()
	// Clear any inherited key vars so the test is hermetic.
	for v := 1; v <= 9; v++ {
		t.Setenv(keyEnvPrefix+string(rune('0'+v)), "")
	}
	for v, k := range keys {
		t.Setenv(keyEnvPrefix+string(rune('0'+v)), k)
	}
	kr, err := NewKeyringFromEnv()
	if err != nil {
		t.Fatalf("NewKeyringFromEnv: %v", err)
	}
	return kr
}

func TestEncryptDecryptRoundTrip(t *testing.T) {
	kr := keyringEnv(t, map[int]string{1: b64key(1)})
	env, err := kr.Encrypt(7, "TOKEN", "s3cr3t-value")
	if err != nil {
		t.Fatalf("Encrypt: %v", err)
	}
	if env.Version != 1 || len(env.IV) != ivLen || len(env.CT) == 0 {
		t.Fatalf("envelope = %+v, want v1 with a 12-byte IV and ciphertext", env)
	}
	got, err := kr.Decrypt(7, "TOKEN", env)
	if err != nil {
		t.Fatalf("Decrypt: %v", err)
	}
	if got != "s3cr3t-value" {
		t.Errorf("decrypted = %q, want the original", got)
	}
}

func TestWrongAADFails(t *testing.T) {
	kr := keyringEnv(t, map[int]string{1: b64key(1)})
	env, _ := kr.Encrypt(7, "TOKEN", "v")

	if _, err := kr.Decrypt(8, "TOKEN", env); err == nil {
		t.Error("decrypt under a different repoID succeeded, want auth-tag failure")
	}
	if _, err := kr.Decrypt(7, "OTHER", env); err == nil {
		t.Error("decrypt under a different name succeeded, want auth-tag failure")
	}
	tampered := Envelope{Version: 1, IV: env.IV, CT: append([]byte{env.CT[0] ^ 0xff}, env.CT[1:]...)}
	if _, err := kr.Decrypt(7, "TOKEN", tampered); err == nil {
		t.Error("decrypt of tampered ciphertext succeeded, want auth-tag failure")
	}
}

func TestFreshIVPerEncryption(t *testing.T) {
	kr := keyringEnv(t, map[int]string{1: b64key(1)})
	a, _ := kr.Encrypt(7, "TOKEN", "same")
	b, _ := kr.Encrypt(7, "TOKEN", "same")
	if string(a.IV) == string(b.IV) {
		t.Error("two encryptions reused the IV")
	}
	if string(a.CT) == string(b.CT) {
		t.Error("two encryptions of the same plaintext produced identical ciphertext")
	}
}

func TestMissingVersionFailsClosed(t *testing.T) {
	// Encrypt under V1, then present a keyring that only knows V2.
	env, _ := keyringEnv(t, map[int]string{1: b64key(1)}).Encrypt(7, "TOKEN", "v")
	only2 := keyringEnv(t, map[int]string{2: b64key(2)})
	if _, err := only2.Decrypt(7, "TOKEN", env); err == nil {
		t.Error("decrypt with the sealing key version absent succeeded, want a fail-closed error")
	} else if !strings.Contains(err.Error(), "version 1") {
		t.Errorf("error = %v, want it to name the missing version", err)
	}
}

func TestRotation(t *testing.T) {
	// A secret written under V1 must still decrypt after V2 is added and becomes
	// current; new writes then use V2.
	old := keyringEnv(t, map[int]string{1: b64key(1)})
	e1, _ := old.Encrypt(7, "TOKEN", "old-value")

	rotated := keyringEnv(t, map[int]string{1: b64key(1), 2: b64key(2)})
	got, err := rotated.Decrypt(7, "TOKEN", e1)
	if err != nil || got != "old-value" {
		t.Fatalf("V1 secret after rotation = %q, %v; want the original", got, err)
	}
	e2, err := rotated.Encrypt(7, "TOKEN", "new-value")
	if err != nil {
		t.Fatalf("Encrypt after rotation: %v", err)
	}
	if e2.Version != 2 {
		t.Errorf("new write sealed under v%d, want v2 (current)", e2.Version)
	}
}

func TestDisabledKeyring(t *testing.T) {
	kr := keyringEnv(t, nil)
	if kr.Enabled() {
		t.Error("keyring with no keys reports enabled")
	}
	if _, err := kr.Encrypt(7, "TOKEN", "v"); err == nil {
		t.Error("Encrypt with no key configured succeeded, want an error")
	}
}

func TestBadKeyEnvIsHardError(t *testing.T) {
	t.Setenv(keyEnvPrefix+"1", "not-base64!!")
	if _, err := NewKeyringFromEnv(); err == nil {
		t.Error("malformed base64 key accepted, want a hard error")
	}
	t.Setenv(keyEnvPrefix+"1", base64.StdEncoding.EncodeToString([]byte("too-short")))
	if _, err := NewKeyringFromEnv(); err == nil {
		t.Error("wrong-length key accepted, want a hard error")
	}
}