Merge pull request 'fix(welcome): BitLocker PIN first-boot + recovery-key display + FlavourStep Next' (#14) from feat/wizard-recipes into main
All checks were successful
Build SilverMetal Enhanced - Windows ISO / build (push) Successful in 6m3s

Reviewed-on: #14
This commit was merged in pull request #14.
This commit is contained in:
2026-06-09 21:05:40 +00:00
5 changed files with 69 additions and 5 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

@@ -37,7 +37,7 @@
<WelcomeStep />
break;
case 1:
<FlavourStep Flavours="_flavours" />
<FlavourStep Flavours="_flavours" OnSelected="StateHasChanged" />
break;
case 2:
<AccountStep OnValidityChanged="@(v => { _accountValid = v; StateHasChanged(); })" />

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);

View File

@@ -19,13 +19,19 @@
@code {
[Parameter] public IReadOnlyList<FlavourManifest> Flavours { get; set; } = Array.Empty<FlavourManifest>();
protected override void OnInitialized()
/// <summary>Notifies the wizard host when the selection changes so it re-evaluates
/// the Next button (otherwise Next stays disabled until a back/forward re-render).</summary>
[Parameter] public EventCallback OnSelected { get; set; }
protected override async Task OnInitializedAsync()
{
State.Flavour ??= Flavours.FirstOrDefault(f => f.IsDefault);
await OnSelected.InvokeAsync();
}
void Select(FlavourManifest f)
async Task Select(FlavourManifest f)
{
State.Flavour = f;
await OnSelected.InvokeAsync();
}
}