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>
67 lines
5.3 KiB
C#
67 lines
5.3 KiB
C#
namespace SilverOS.Welcome.Core.Apply;
|
|
public sealed class BitLockerService(IProcessRunner runner) : IBitLockerService
|
|
{
|
|
public async Task EnableAsync(string pin, CancellationToken ct = default)
|
|
{
|
|
var p = pin.Replace("'", "''");
|
|
|
|
// 1. Set the FVE "Require additional authentication at startup" policy so the
|
|
// TPM+PIN protector is permitted. Without this, Enable-BitLocker -TpmAndPinProtector
|
|
// silently degrades to TPM-only (boots with no PIN prompt).
|
|
// 2. Add the TPM+PIN protector — Enable-BitLocker if the volume is still decrypted,
|
|
// or Add-BitLockerKeyProtector if Windows automatic device-encryption already
|
|
// encrypted it with a TPM-only protector.
|
|
// 3. Remove any TPM-only protector (only once a TPM+PIN protector is confirmed present)
|
|
// so the device actually requires the PIN at pre-boot.
|
|
var script = string.Concat(
|
|
// Eject optical install media first — BitLocker -TpmAndPinProtector refuses to enroll
|
|
// while bootable CD/DVD media is present ("detected bootable media in the computer").
|
|
"try { $s=New-Object -ComObject Shell.Application; ",
|
|
"$s.Namespace(17).Items() | Where-Object { $_.Type -match 'CD|DVD' } | ForEach-Object { try { $_.InvokeVerb('Eject') } catch {} } } catch {}; ",
|
|
"Start-Sleep -Seconds 3; ",
|
|
"$fve='HKLM:\\SOFTWARE\\Policies\\Microsoft\\FVE'; ",
|
|
"New-Item -Path $fve -Force | Out-Null; ",
|
|
"New-ItemProperty -Path $fve -Name UseAdvancedStartup -Value 1 -PropertyType DWord -Force | Out-Null; ",
|
|
"New-ItemProperty -Path $fve -Name EnableBDEWithNoTPM -Value 0 -PropertyType DWord -Force | Out-Null; ",
|
|
"New-ItemProperty -Path $fve -Name UseTPM -Value 2 -PropertyType DWord -Force | Out-Null; ",
|
|
"New-ItemProperty -Path $fve -Name UseTPMPIN -Value 2 -PropertyType DWord -Force | Out-Null; ",
|
|
"New-ItemProperty -Path $fve -Name UseTPMKey -Value 2 -PropertyType DWord -Force | Out-Null; ",
|
|
"New-ItemProperty -Path $fve -Name UseTPMKeyPIN -Value 2 -PropertyType DWord -Force | Out-Null; ",
|
|
"$mp=$env:SystemDrive; ",
|
|
"$p=ConvertTo-SecureString '", p, "' -AsPlainText -Force; ",
|
|
"$v=Get-BitLockerVolume -MountPoint $mp; ",
|
|
"if ($v.VolumeStatus -eq 'FullyDecrypted') { ",
|
|
// 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; ",
|
|
"if ($kp | Where-Object { $_.KeyProtectorType -eq 'TpmPin' }) { ",
|
|
"$kp | Where-Object { $_.KeyProtectorType -eq 'Tpm' } | ForEach-Object { ",
|
|
"Remove-BitLockerKeyProtector -MountPoint $mp -KeyProtectorId $_.KeyProtectorId | Out-Null } }; ",
|
|
// 4. Add a RECOVERY-PASSWORD protector so a forgotten/mistyped PIN is recoverable —
|
|
// TPM+PIN alone is an unrecoverable brick. Save the 48-digit key to ProgramData
|
|
// AND to the unencrypted EFI System Partition (readable even when the OS volume is
|
|
// locked). PRODUCT TODO: escrow to SilverSync + show it in the wizard's Done step.
|
|
"if (-not ((Get-BitLockerVolume -MountPoint $mp).KeyProtector | Where-Object { $_.KeyProtectorType -eq 'RecoveryPassword' })) { ",
|
|
"Add-BitLockerKeyProtector -MountPoint $mp -RecoveryPasswordProtector | Out-Null }; ",
|
|
"$rp=((Get-BitLockerVolume -MountPoint $mp).KeyProtector | Where-Object { $_.KeyProtectorType -eq 'RecoveryPassword' } | Select-Object -First 1).RecoveryPassword; ",
|
|
"if ($rp) { New-Item -ItemType Directory -Force 'C:\\ProgramData\\SilverMetal' | Out-Null; ",
|
|
"Set-Content -Path 'C:\\ProgramData\\SilverMetal\\bitlocker-recovery.txt' -Value $rp; ",
|
|
"try { mountvol Q: /S; Set-Content -Path 'Q:\\SilverMetal-Recovery.txt' -Value $rp; mountvol Q: /D } catch {} }; ",
|
|
// Outcome check: fail loudly (non-zero exit) if a TPM+PIN protector is not present at
|
|
// the end — this is what actually matters (a benign non-terminating warning alone
|
|
// must not pass, and a real failure must not stay silent).
|
|
"$ok=(Get-BitLockerVolume -MountPoint $mp).KeyProtector | Where-Object { $_.KeyProtectorType -eq 'TpmPin' }; ",
|
|
"if (-not $ok) { Write-Error 'TPM+PIN protector not present after enrollment'; exit 1 }");
|
|
|
|
var r = await runner.RunAsync("powershell.exe",
|
|
$"-NoProfile -ExecutionPolicy Bypass -Command \"{script}\"", ct);
|
|
r.EnsureSuccess("BitLocker enrollment");
|
|
}
|
|
}
|