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