fix(welcome): BitLocker PIN works first boot (drop -SkipHardwareTest) + show recovery key
All checks were successful
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Successful in 7m5s

- BitLocker: remove -SkipHardwareTest so BitLocker validates the TPM+PIN unseal via
  its hardware test on the next reboot (the wizard's end-of-flow reboot) before
  encrypting — fixes the E_FVE_SECURE_BOOT_CHANGED / PCR-11 drop-to-recovery on the
  first post-enroll boot. The PIN now works first time instead of needing recovery.
- Done step now DISPLAYS the 48-digit BitLocker recovery key (read from the file the
  enrollment saves) with a 'save this' warning — previously it was never surfaced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sysadmin
2026-06-09 21:57:47 +01:00
parent 6d6eb2cdc8
commit a3623b1fbb
3 changed files with 60 additions and 2 deletions

View File

@@ -877,3 +877,27 @@ h1:focus { outline: none; }
padding-left: env(safe-area-inset-left);
}
}
/* ── BitLocker recovery key (Done step) ─────────────────────────────── */
.recovery-panel {
margin: 1.25rem 0;
padding: 1rem 1.25rem;
border: 1px solid var(--clr-accent);
border-radius: var(--radius-sm, 8px);
background: var(--clr-accent-glow, rgba(0,212,255,0.10));
}
.recovery-panel h3 { margin: 0 0 0.5rem; color: var(--clr-accent); font-family: var(--font-mono); }
.recovery-key {
font-family: var(--font-mono);
font-size: 1.05rem;
letter-spacing: 0.04em;
color: var(--clr-text-hi);
background: rgba(0,0,0,0.30);
padding: 0.75rem 1rem;
border-radius: var(--radius-sm, 8px);
white-space: pre-wrap;
word-break: break-all;
user-select: all;
margin: 0.5rem 0;
}
.recovery-note { color: var(--clr-text-lo); }

View File

@@ -31,7 +31,12 @@ public sealed class BitLockerService(IProcessRunner runner) : IBitLockerService
"$p=ConvertTo-SecureString '", p, "' -AsPlainText -Force; ",
"$v=Get-BitLockerVolume -MountPoint $mp; ",
"if ($v.VolumeStatus -eq 'FullyDecrypted') { ",
"Enable-BitLocker -MountPoint $mp -EncryptionMethod XtsAes256 -TpmAndPinProtector -Pin $p -SkipHardwareTest } ",
// NO -SkipHardwareTest: let BitLocker run its hardware test on the next reboot so it
// VALIDATES the TPM+PIN unseal against the real boot measurements before encrypting.
// -SkipHardwareTest seals immediately against possibly-wrong PCRs -> drops to recovery
// on first boot (E_FVE_SECURE_BOOT_CHANGED, PCR 11). The wizard's end-of-flow reboot
// is that validation pass, so the PIN works on first boot instead of bouncing.
"Enable-BitLocker -MountPoint $mp -EncryptionMethod XtsAes256 -TpmAndPinProtector -Pin $p } ",
"elseif (-not ($v.KeyProtector | Where-Object { $_.KeyProtectorType -eq 'TpmPin' })) { ",
"Add-BitLockerKeyProtector -MountPoint $mp -TpmAndPinProtector -Pin $p }; ",
"$kp=(Get-BitLockerVolume -MountPoint $mp).KeyProtector; ",

View File

@@ -2,11 +2,40 @@
<div class="step done-step">
<h1>All Done!</h1>
<p>Your SilverOS device is configured and ready. Click below to restart and start using it.</p>
<p>Your SilverMetal device is configured and ready.</p>
@if (!string.IsNullOrWhiteSpace(_recoveryKey))
{
<div class="recovery-panel">
<h3>⚠ Save your BitLocker recovery key</h3>
<p class="step-subtitle">
This is the <strong>only</strong> way back into your drive if you ever forget your PIN.
Write it down or photograph it now and keep it somewhere safe and separate from this device.
</p>
<pre class="recovery-key">@_recoveryKey</pre>
<p class="recovery-note"><small>Also saved to <code>C:\ProgramData\SilverMetal\bitlocker-recovery.txt</code> on this device.</small></p>
</div>
}
<p>Click below to restart and start using it.</p>
<button class="btn-primary btn-restart" @onclick="RestartNow">Restart Now</button>
</div>
@code {
private string? _recoveryKey;
protected override void OnInitialized()
{
// The BitLocker step saved the 48-digit recovery key to ProgramData; surface it
// here so the user records it before finishing (TPM+PIN alone is unrecoverable).
try
{
const string path = @"C:\ProgramData\SilverMetal\bitlocker-recovery.txt";
if (File.Exists(path)) _recoveryKey = File.ReadAllText(path).Trim();
}
catch { /* best-effort display */ }
}
private async Task RestartNow()
{
await ProcessRunner.RunAsync("cmd.exe", "/c shutdown /r /t 5", CancellationToken.None);