package backup import ( "context" "fmt" "strings" ) // R-200 (controller v0.195.0) — THE DIAGNOSTIC HALF, and only that half. // // The question this answers, once, decisively: **is the offsite repository password actually // recoverable from the hub's sealed bundle?** Everything else in the recovery chain is downstream of // that, and until 2026-08-04 nobody had ever asked it — the round-trip proof on record (2026-06-10) // predates the field by a month, and the extraction step did not exist at all. // // IT COMPARES; IT DOES NOT INSTALL. The recovered password is NOT written to offboxPwPath. Comparing // proves recoverability; installing changes a live box's state on a path nobody has walked, and // "the existing repository opens under a recovered key" is a separate link with a drill around it. // Keep this function free of any write — if a future change makes it install, it stops being a // diagnostic and needs the drill's supervision. // // IT HANDLES ONLY HASHES OUTSIDE THE AGENT CALL. The agent returns the password and its sha256; this // reads the hash. The value is dropped on the floor here deliberately, so no controller-side code // path can grow a habit of holding it. // OffsiteKeyRecoverer is the agent-side seam (agent >= v0.125.0, // POST /escrow/recover-offsite-password): it fetches this host's sealed bundle from the hub, unseals // it with R, and returns ONLY the offsite repository password plus its sha256. type OffsiteKeyRecoverer interface { RecoverOffsiteRepoPassword(ctx context.Context, recoveryCode string) (password, sha256hex string, err error) } // RecoveryCheckResult is the verdict. It carries HASHES ONLY — there is no field here that could // leak a password into a log, a report or a terminal. type RecoveryCheckResult struct { // LocalSHA256 is the hash of the repo password currently on disk ("" when there is none). LocalSHA256 string // RecoveredSHA256 is the hash of what came out of the sealed bundle. RecoveredSHA256 string // Match is the whole point: byte-identical keys produce identical hashes. Match bool // LocalPresent distinguishes "they differ" from "there was nothing to compare against" — a // rebuilt box with no repo password yet is a legitimate state and must not read as a mismatch. LocalPresent bool } // CheckOffsiteKeyRecoverable recovers the repository password through the agent and compares it, by // hash, against the one on this box's disk. It writes nothing anywhere. // // R is passed straight through to the agent and is not retained here. The CALLER owns clearing its // own copy; this function keeps none. func (m *Manager) CheckOffsiteKeyRecoverable(ctx context.Context, rec OffsiteKeyRecoverer, recoveryCode string) (RecoveryCheckResult, error) { var out RecoveryCheckResult if rec == nil { return out, fmt.Errorf("offbox: no agent recovery seam configured") } if strings.TrimSpace(recoveryCode) == "" { return out, fmt.Errorf("offbox: the recovery code is required") } // Read the local side FIRST, so a missing local password is reported as such rather than // surfacing as a mismatch after a successful recovery. localHash, ok := m.OffboxRepoPasswordHash() out.LocalSHA256, out.LocalPresent = localHash, ok pw, recoveredHash, err := rec.RecoverOffsiteRepoPassword(ctx, recoveryCode) if err != nil { return out, err // the agent's message already names the step and contains no secret } pw = "" // the VALUE is not this function's business — §8.5, compare, do not install _ = pw out.RecoveredSHA256 = recoveredHash out.Match = ok && recoveredHash != "" && recoveredHash == localHash return out, nil }