fix(secrets-cli): contain file-source key IO/access errors in doctor + bundle-import flows

This commit is contained in:
Joseph Doherty
2026-07-19 10:38:16 -04:00
parent 6dbb372adf
commit 980b4a37f6
4 changed files with 103 additions and 4 deletions
@@ -85,12 +85,28 @@ public sealed class BundleImportFlow : IInteractiveFlow
return;
}
// The first KekId read is where an env/file source actually resolves the key: it lazily throws
// MasterKeyUnavailableException (an unset var, a missing file) or, for a file source, a raw
// IOException/UnauthorizedAccessException straight out of File.ReadAllBytes (e.g. a
// permission-denied key file), since FileMasterKeyProvider does not wrap those. All must render
// a friendly line and return here — not escape to the shell.
string sourceKekId;
try
{
sourceKekId = sourceKek.KekId;
}
catch (Exception ex) when (ex is MasterKeyUnavailableException or IOException or UnauthorizedAccessException)
{
console.MarkupLineInterpolated($"[red]Source KEK unavailable:[/] {ex.Message}");
return;
}
// A valid-but-wrong key would otherwise silently land every row in SkippedForeignKek. Verify
// the supplied key actually derives the bundle's source KEK id before importing.
if (!string.Equals(sourceKek.KekId, bundle.SourceKekId, StringComparison.Ordinal))
if (!string.Equals(sourceKekId, bundle.SourceKekId, StringComparison.Ordinal))
{
console.MarkupLineInterpolated(
$"[red]That key does not match this bundle's source KEK ('{bundle.SourceKekId}' expected, got '{sourceKek.KekId}').[/]");
$"[red]That key does not match this bundle's source KEK ('{bundle.SourceKekId}' expected, got '{sourceKekId}').[/]");
return;
}
}
@@ -108,7 +108,9 @@ public sealed class KekDoctorFlow : IInteractiveFlow
{
// Acquire the old KEK and resolve its id under their OWN guard: a malformed pasted key throws
// ArgumentException from LiteralMasterKeyProvider, and an env/file source lazily throws
// MasterKeyUnavailableException the moment KekId is read (an unset var, a missing file). Both must
// MasterKeyUnavailableException the moment KekId is read (an unset var, a missing file) — or, for
// a file source, a raw IOException/UnauthorizedAccessException straight out of File.ReadAllBytes
// (e.g. a permission-denied key file), since FileMasterKeyProvider does not wrap those. All must
// render a friendly line and return here — not escape to the shell — so the closing re-diagnosis
// in RunAsync still runs. This is distinct from the rewrap guard below (a wrong key vs a valid one).
IMasterKeyProvider oldKek;
@@ -123,7 +125,7 @@ public sealed class KekDoctorFlow : IInteractiveFlow
console.MarkupLineInterpolated($"[red]Invalid old KEK:[/] {ex.Message}");
return;
}
catch (MasterKeyUnavailableException ex)
catch (Exception ex) when (ex is MasterKeyUnavailableException or IOException or UnauthorizedAccessException)
{
console.MarkupLineInterpolated($"[red]Old KEK unavailable:[/] {ex.Message}");
return;