package escrow import ( "bytes" "context" "os" "os/exec" "path/filepath" "runtime" "testing" ) func ageAvailable() bool { if runtime.GOOS != "linux" { return false } if _, err := exec.LookPath("age"); err == nil { return true } return false } func ensureAge(t *testing.T) { t.Helper() if !ageAvailable() { t.Skip("skipping: the `age` CLI + linux required (runs on the demo/build host)") } if p, err := exec.LookPath("age"); err == nil { ageBinary = p } } func TestIdentity_InputValidation(t *testing.T) { ctx := context.Background() if _, err := WrapIdentity(ctx, nil, "R"); err == nil { t.Error("empty bundle must error") } if _, err := WrapIdentity(ctx, []byte("x"), ""); err == nil { t.Error("empty R must error") } if _, err := UnwrapIdentity(ctx, nil, "R"); err == nil { t.Error("empty blob must error") } } // Round-trip: a bundle wraps under R and recovers byte-identical (the identity analog of K-escrow). func TestIdentity_RoundTrip(t *testing.T) { ensureAge(t) ctx := context.Background() const R = "throwaway-correct-horse-battery-staple-words" bundle := IdentityBundle{TunnelToken: "eyJhIjoidGVzdCIsInQiOiJ4In0", PBSToken: "felhom@pbs!n100:deadbeefcafe"} blob, err := WrapIdentityBundle(ctx, bundle, R) if err != nil { t.Fatalf("WrapIdentityBundle: %v", err) } // the blob is opaque ciphertext, not the bundle. if bytes.Contains(blob, []byte(bundle.TunnelToken)) || bytes.Contains(blob, []byte(bundle.PBSToken)) { t.Fatal("the blob leaks plaintext token bytes — not encrypted") } got, err := UnwrapIdentityBundle(ctx, blob, R) if err != nil { t.Fatalf("UnwrapIdentityBundle: %v", err) } if got != bundle { t.Errorf("recovered bundle = %+v, want %+v", got, bundle) } } // fork-4: the IdentityBundle carries the offsite restic repo password under R, byte-exact and encrypted; // a wrong R fails closed. (The spike proved a recovered value opens the real repo; this guards the field.) func TestIdentity_RoundTrip_CarriesResticPassword(t *testing.T) { ensureAge(t) ctx := context.Background() const R = "throwaway-correct-horse-battery-staple-fork4" const pw = "deadbeefcafef00d0123456789abcdef0123456789abcdef0123456789abcdef" // 64 hex, synthetic bundle := IdentityBundle{TunnelToken: "tt", PBSToken: "pt", ResticRepoPassword: pw} blob, err := WrapIdentityBundle(ctx, bundle, R) if err != nil { t.Fatalf("WrapIdentityBundle: %v", err) } if bytes.Contains(blob, []byte(pw)) { t.Fatal("the blob leaks the restic password plaintext — not encrypted") } got, err := UnwrapIdentityBundle(ctx, blob, R) if err != nil { t.Fatalf("UnwrapIdentityBundle: %v", err) } if got.ResticRepoPassword != pw { t.Fatalf("recovered restic password not byte-exact: got %q", got.ResticRepoPassword) } if got != bundle { t.Fatalf("recovered bundle = %+v, want %+v", got, bundle) } if _, err := UnwrapIdentityBundle(ctx, blob, R+"-WRONG"); err == nil { t.Fatal("a wrong recovery code must fail closed (no bundle, no restic password)") } } // PINNED CROSS-REPO TEST VECTOR (SLICE 3): the same vector is asserted in felhom-controller — if either // side drifts (trailing newline, encoding, trim behavior), its half of this test fails and auto-confirm // can never silently mismatch. Convention: sha256 hex over the TRIMMED password string. func TestHashResticPassword_PinnedVector(t *testing.T) { const vector = "cafef00ddeadbeef0123456789abcdef0123456789abcdef0123456789abcdef" const want = "dbfc02f987e1ac0c91911d5761267089b1144628745a3343e4d96194e43c08e4" if got := HashResticPassword(vector); got != want { t.Fatalf("pinned vector drift: got %s want %s", got, want) } // trim convention: surrounding whitespace/newlines do not change the hash (both sides trim) if got := HashResticPassword(" " + vector + "\n"); got != want { t.Fatalf("whitespace must not change the hash (trim convention), got %s", got) } } // AttachResticPassword: missing file → clean no-attach; staged file → trimmed value attached; empty → error. func TestAttachResticPassword(t *testing.T) { b := &IdentityBundle{} if ok, err := AttachResticPassword(b, filepath.Join(t.TempDir(), "absent")); ok || err != nil { t.Fatalf("missing staged file must be a clean no-attach, got ok=%v err=%v", ok, err) } f := filepath.Join(t.TempDir(), "pw") if err := os.WriteFile(f, []byte(" abc123def \n"), 0o600); err != nil { t.Fatal(err) } ok, err := AttachResticPassword(b, f) if err != nil || !ok { t.Fatalf("attach from staged file: ok=%v err=%v", ok, err) } if b.ResticRepoPassword != "abc123def" { t.Fatalf("want trimmed value, got %q", b.ResticRepoPassword) } if err := os.WriteFile(f, []byte(" \n"), 0o600); err != nil { t.Fatal(err) } if _, err := AttachResticPassword(&IdentityBundle{}, f); err == nil { t.Fatal("an empty staged file must error (an operator would want to know)") } } // Wrong R fails CLOSED — no bundle emitted. func TestIdentity_WrongRFailsClosed(t *testing.T) { ensureAge(t) ctx := context.Background() blob, err := WrapIdentity(ctx, []byte(`{"tunnel_token":"a","pbs_token":"b"}`), "the-correct-code") if err != nil { t.Fatalf("WrapIdentity: %v", err) } if _, err := UnwrapIdentity(ctx, blob, "DEFINITELY-the-wrong-code"); err == nil { t.Fatal("a wrong recovery code must fail closed (no bundle)") } // the blob is unchanged / retryable: the RIGHT code still works after a wrong attempt. if _, err := UnwrapIdentity(ctx, blob, "the-correct-code"); err != nil { t.Errorf("the blob was not retryable after a wrong-R attempt: %v", err) } }