v0.108.0: hub-verified escrow auto-confirm on current-password hash match (SLICE 3)

EscrowAutoConfirmer flips pending->escrowed ONLY when sha256(local repo
password) matches the ACK's restic_pw_sha256 (blob-presence alone never
confirms — red-proofed). Mismatch warns once per hash naming the ceremony;
never un-confirms; wipes the staged secret on flip. Pinned cross-repo hash
vector; manual confirm deprecated to a legacy-blob fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-09 23:19:17 +02:00
parent fec2e8fd84
commit febf6757dc
8 changed files with 327 additions and 4 deletions
+39
View File
@@ -47,6 +47,45 @@ func argsContainTimeout(args []string) bool {
return strings.Contains(strings.Join(args, " "), "-oConnectTimeout=")
}
// PINNED CROSS-REPO TEST VECTOR (SLICE 3): the same vector is asserted in felhom-agent's
// escrow.HashResticPassword test — if either hasher drifts (newline, encoding, trim), its half fails and
// the escrow auto-confirm can never silently mismatch. Convention: sha256 hex over the TRIMMED 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)
}
if got := HashResticPassword(" " + vector + "\n"); got != want {
t.Fatalf("whitespace must not change the hash (trim convention), got %s", got)
}
}
// OffboxRepoPasswordHash: hashes the on-disk password file (the auto-confirm's local side); absent → ok=false.
func TestOffboxRepoPasswordHash(t *testing.T) {
// bare manager (no secrets written yet) → no password file → ok=false
logger := log.New(os.Stderr, "", 0)
dataDir := t.TempDir()
sett, err := settings.Load(filepath.Join(dataDir, "settings.json"), logger)
if err != nil {
t.Fatal(err)
}
cfg := &config.Config{}
cfg.Paths.DataDir = dataDir
m := NewManager(cfg, sett, logger)
if _, ok := m.OffboxRepoPasswordHash(); ok {
t.Fatal("no password file → ok must be false")
}
const pw = "cafef00ddeadbeef0123456789abcdef0123456789abcdef0123456789abcdef"
if err := m.InjectOffboxPassword(pw, false); err != nil {
t.Fatal(err)
}
got, ok := m.OffboxRepoPasswordHash()
if !ok || got != "dbfc02f987e1ac0c91911d5761267089b1144628745a3343e4d96194e43c08e4" {
t.Fatalf("hash of the injected password wrong: ok=%v got=%s", ok, got)
}
}
// TestOffbox_BaseArgsCarryConnectTimeout asserts the mandatory fail-fast + hardening args are present.
func TestOffbox_BaseArgsCarryConnectTimeout(t *testing.T) {
m, sett := newOffboxManager(t)