fix(kiosk): keyboard filter covers admins + taskbar auto-hide + instant sm-bootstrap disable #16
@@ -1,80 +1,160 @@
|
||||
#Requires -Version 5.1
|
||||
#Requires -Version 5.1
|
||||
|
||||
<#
|
||||
|
||||
.SYNOPSIS Apply SilverMetal Windows branding (4 layers), offline (WIM) or online.
|
||||
|
||||
.DESCRIPTION
|
||||
|
||||
Offline: reg-load the mounted image's SOFTWARE + default NTUSER hives, write
|
||||
|
||||
values, stage assets, reg-unload. Online: write live HKLM + default-user hive.
|
||||
|
||||
Design: ../docs/superpowers/specs/2026-06-09-first-boot-branding-design.md
|
||||
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
|
||||
param(
|
||||
|
||||
[Parameter(Mandatory)][ValidateSet('Offline','Online')][string]$Mode,
|
||||
|
||||
[string]$MountPath, # required for Offline
|
||||
|
||||
[string]$Manifest = "$PSScriptRoot\branding.manifest.json",
|
||||
|
||||
[string]$AssetsDir = "$PSScriptRoot\assets",
|
||||
|
||||
[switch]$PassThru
|
||||
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
. "$PSScriptRoot\lib\BrandingLayers.ps1"
|
||||
|
||||
function Write-Stage { param($m) Write-Host "==> $m" -ForegroundColor Cyan }
|
||||
|
||||
|
||||
|
||||
if ($Mode -eq 'Offline' -and -not $MountPath) { throw 'Offline mode requires -MountPath.' }
|
||||
|
||||
$m = Get-Content $Manifest -Raw | ConvertFrom-Json
|
||||
|
||||
|
||||
|
||||
# Destination paths (image-relative for offline, live for online).
|
||||
|
||||
$winRoot = if ($Mode -eq 'Offline') { $MountPath } else { 'C:' }
|
||||
|
||||
$logoDest = Join-Path $winRoot 'Windows\System32\oemlogo.bmp'
|
||||
|
||||
$lockDir = Join-Path $winRoot 'Windows\Web\Screen\SilverMetal'
|
||||
|
||||
$wallDir = Join-Path $winRoot 'Windows\Web\Wallpaper\SilverMetal'
|
||||
|
||||
$themeDest = Join-Path $winRoot 'Windows\Resources\Themes'
|
||||
|
||||
$lockLive = 'C:\Windows\Web\Screen\SilverMetal\' + $m.lockScreen.image
|
||||
|
||||
$wallLive = 'C:\Windows\Web\Wallpaper\SilverMetal\' + $m.desktop.wallpaper
|
||||
|
||||
$logoLive = 'C:\Windows\System32\oemlogo.bmp'
|
||||
|
||||
|
||||
|
||||
# --- stage assets ---
|
||||
|
||||
Write-Stage "Stage branding assets ($Mode)"
|
||||
|
||||
New-Item -ItemType Directory -Force $lockDir,$wallDir,$themeDest,(Split-Path $logoDest) | Out-Null
|
||||
|
||||
Copy-Item (Join-Path $AssetsDir $m.oem.logo) $logoDest -Force
|
||||
|
||||
Copy-Item (Join-Path $AssetsDir $m.lockScreen.image) (Join-Path $lockDir $m.lockScreen.image) -Force
|
||||
|
||||
Copy-Item (Join-Path $AssetsDir $m.desktop.wallpaper)(Join-Path $wallDir $m.desktop.wallpaper) -Force
|
||||
|
||||
Copy-Item (Join-Path $AssetsDir $m.desktop.theme) (Join-Path $themeDest $m.desktop.theme) -Force
|
||||
|
||||
|
||||
|
||||
$result = [ordered]@{ OemApplied=$false; LockScreenApplied=$false; DesktopApplied=$false; BitLockerApplied=$false }
|
||||
|
||||
|
||||
|
||||
function Invoke-WithHive {
|
||||
|
||||
param([string]$HivePath,[string]$Name,[scriptblock]$Body)
|
||||
|
||||
& reg load "HKLM\$Name" $HivePath | Out-Null
|
||||
|
||||
if ($LASTEXITCODE -ne 0) { throw "reg load $Name ($HivePath) failed" }
|
||||
|
||||
try { & $Body "Registry::HKEY_LOCAL_MACHINE\$Name" }
|
||||
|
||||
finally {
|
||||
|
||||
[gc]::Collect(); Start-Sleep -Milliseconds 500
|
||||
|
||||
& reg unload "HKLM\$Name" | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { Write-Warning "reg unload $Name failed ($LASTEXITCODE) — hive may be leaked" }
|
||||
|
||||
if ($LASTEXITCODE -ne 0) { Write-Warning "reg unload $Name failed ($LASTEXITCODE) -- hive may be leaked" }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
if ($Mode -eq 'Offline') {
|
||||
|
||||
$swHive = Join-Path $MountPath 'Windows\System32\config\SOFTWARE'
|
||||
|
||||
$duHive = Join-Path $MountPath 'Users\Default\NTUSER.DAT'
|
||||
|
||||
Invoke-WithHive $swHive 'SM_BRAND_SW' {
|
||||
|
||||
param($sw)
|
||||
|
||||
Write-Stage 'OEM About'; Set-OemInformation -SoftwareRoot $sw -Manifest $m -LogoPath $logoLive; $result.OemApplied=$true
|
||||
|
||||
Write-Stage 'Lock screen'; Set-LockScreen -SoftwareRoot $sw -ImagePath $lockLive -Lock:$m.lockScreen.lock; $result.LockScreenApplied=$true
|
||||
|
||||
Write-Stage 'BitLocker preboot';Set-BitLockerPreboot -SoftwareRoot $sw -Manifest $m; $result.BitLockerApplied=$true
|
||||
|
||||
}
|
||||
|
||||
Invoke-WithHive $duHive 'SM_BRAND_DU' {
|
||||
|
||||
param($du)
|
||||
|
||||
Write-Stage 'Desktop'; Set-DesktopBranding -DefaultUserRoot $du -Manifest $m -WallpaperPath $wallLive; $result.DesktopApplied=$true
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
Set-OemInformation -SoftwareRoot 'HKLM:\SOFTWARE' -Manifest $m -LogoPath $logoLive; $result.OemApplied=$true
|
||||
|
||||
Set-LockScreen -SoftwareRoot 'HKLM:\SOFTWARE' -ImagePath $lockLive -Lock:$m.lockScreen.lock; $result.LockScreenApplied=$true
|
||||
|
||||
Set-BitLockerPreboot -SoftwareRoot 'HKLM:\SOFTWARE' -Manifest $m; $result.BitLockerApplied=$true
|
||||
|
||||
Invoke-WithHive 'C:\Users\Default\NTUSER.DAT' 'SM_BRAND_DU' {
|
||||
|
||||
param($du) Set-DesktopBranding -DefaultUserRoot $du -Manifest $m -WallpaperPath $wallLive; $result.DesktopApplied=$true
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
Write-Host 'Branding applied.' -ForegroundColor Green
|
||||
|
||||
if ($PassThru) { [pscustomobject]$result }
|
||||
|
||||
|
||||
@@ -1,65 +1,130 @@
|
||||
Set-StrictMode -Version Latest
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
. "$PSScriptRoot\RegistryHelpers.ps1"
|
||||
|
||||
|
||||
|
||||
function Set-OemInformation {
|
||||
|
||||
param([Parameter(Mandatory)][string]$SoftwareRoot,[Parameter(Mandatory)]$Manifest,[Parameter(Mandatory)][string]$LogoPath)
|
||||
|
||||
$sub = 'Microsoft\Windows\CurrentVersion\OEMInformation'
|
||||
|
||||
Set-SmRegValue -Root $SoftwareRoot -SubKey $sub -Name 'Manufacturer' -Type String -Value $Manifest.oem.manufacturer
|
||||
|
||||
Set-SmRegValue -Root $SoftwareRoot -SubKey $sub -Name 'Model' -Type String -Value $Manifest.oem.model
|
||||
|
||||
Set-SmRegValue -Root $SoftwareRoot -SubKey $sub -Name 'SupportURL' -Type String -Value $Manifest.oem.supportUrl
|
||||
|
||||
Set-SmRegValue -Root $SoftwareRoot -SubKey $sub -Name 'SupportHours' -Type String -Value $Manifest.oem.supportHours
|
||||
|
||||
Set-SmRegValue -Root $SoftwareRoot -SubKey $sub -Name 'Logo' -Type String -Value $LogoPath
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function Set-LockScreen {
|
||||
|
||||
param([Parameter(Mandatory)][string]$SoftwareRoot,[Parameter(Mandatory)][string]$ImagePath,[bool]$Lock=$true)
|
||||
|
||||
# Per-device modern lock-screen image (reliable on Enterprise/IoT).
|
||||
|
||||
$csp = 'Microsoft\Windows\CurrentVersion\PersonalizationCSP'
|
||||
|
||||
Set-SmRegValue -Root $SoftwareRoot -SubKey $csp -Name 'LockScreenImagePath' -Type String -Value $ImagePath
|
||||
|
||||
Set-SmRegValue -Root $SoftwareRoot -SubKey $csp -Name 'LockScreenImageUrl' -Type String -Value $ImagePath
|
||||
|
||||
Set-SmRegValue -Root $SoftwareRoot -SubKey $csp -Name 'LockScreenImageStatus' -Type DWord -Value 1
|
||||
|
||||
if ($Lock) {
|
||||
|
||||
$pol = 'Policies\Microsoft\Windows\Personalization'
|
||||
|
||||
Set-SmRegValue -Root $SoftwareRoot -SubKey $pol -Name 'LockScreenImage' -Type String -Value $ImagePath
|
||||
|
||||
Set-SmRegValue -Root $SoftwareRoot -SubKey $pol -Name 'NoChangingLockScreen' -Type DWord -Value 1
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function Set-DesktopBranding {
|
||||
|
||||
param([Parameter(Mandatory)][string]$DefaultUserRoot,[Parameter(Mandatory)]$Manifest,[Parameter(Mandatory)][string]$WallpaperPath)
|
||||
|
||||
Set-SmRegValue -Root $DefaultUserRoot -SubKey 'Control Panel\Desktop' -Name 'WallPaper' -Type String -Value $WallpaperPath
|
||||
|
||||
Set-SmRegValue -Root $DefaultUserRoot -SubKey 'Control Panel\Desktop' -Name 'WallpaperStyle' -Type String -Value '10' # fill
|
||||
|
||||
if ($Manifest.desktop.darkMode) {
|
||||
|
||||
$p = 'Software\Microsoft\Windows\CurrentVersion\Themes\Personalize'
|
||||
|
||||
Set-SmRegValue -Root $DefaultUserRoot -SubKey $p -Name 'AppsUseLightTheme' -Type DWord -Value 0
|
||||
|
||||
Set-SmRegValue -Root $DefaultUserRoot -SubKey $p -Name 'SystemUsesLightTheme' -Type DWord -Value 0
|
||||
|
||||
}
|
||||
|
||||
# Accent (cyan). DWM uses fully-opaque DWORDs with DIFFERENT byte orders:
|
||||
|
||||
# ColorizationColor = 0xAARRGGBB (ARGB); AccentColor = 0xAABBGGRR (ABGR).
|
||||
|
||||
# Manifest holds the plain RGB hex (source of truth); derive both, alpha=FF.
|
||||
|
||||
# NOTE: exact accent rendering is VM-verified (plan §9 soft spot).
|
||||
|
||||
$rgb = $Manifest.desktop.accentColor.TrimStart('#')
|
||||
|
||||
$r = [Convert]::ToInt32($rgb.Substring(0,2),16)
|
||||
|
||||
$g = [Convert]::ToInt32($rgb.Substring(2,2),16)
|
||||
|
||||
$b = [Convert]::ToInt32($rgb.Substring(4,2),16)
|
||||
|
||||
$argb = [int](0xFF000000 -bor ($r -shl 16) -bor ($g -shl 8) -bor $b) # ColorizationColor
|
||||
|
||||
$abgr = [int](0xFF000000 -bor ($b -shl 16) -bor ($g -shl 8) -bor $r) # AccentColor
|
||||
|
||||
Set-SmRegValue -Root $DefaultUserRoot -SubKey 'Software\Microsoft\Windows\DWM' -Name 'AccentColor' -Type DWord -Value $abgr
|
||||
|
||||
Set-SmRegValue -Root $DefaultUserRoot -SubKey 'Software\Microsoft\Windows\DWM' -Name 'ColorizationColor' -Type DWord -Value $argb
|
||||
|
||||
if (-not $Manifest.desktop.lockWallpaper) { return }
|
||||
|
||||
Set-SmRegValue -Root $DefaultUserRoot -SubKey 'Software\Microsoft\Windows\CurrentVersion\Policies\ActiveDesktop' -Name 'NoChangingWallPaper' -Type DWord -Value 1
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function Set-BitLockerPreboot {
|
||||
|
||||
param([Parameter(Mandatory)][string]$SoftwareRoot,[Parameter(Mandatory)]$Manifest)
|
||||
|
||||
# GPO "Configure pre-boot recovery message and URL" (ADMX VolumeEncryption).
|
||||
|
||||
# NOTE: only the BitLocker RECOVERY screen is customisable; the normal PIN-entry
|
||||
|
||||
# screen text is fixed Windows UI. Exact value names are asserted by the read-back
|
||||
|
||||
# test; if a name is wrong the offline-apply verify (Task A4) catches it.
|
||||
|
||||
$fve = 'Policies\Microsoft\FVE'
|
||||
|
||||
Set-SmRegValue -Root $SoftwareRoot -SubKey $fve -Name 'UseCustomRecoveryMessage' -Type DWord -Value 1
|
||||
|
||||
Set-SmRegValue -Root $SoftwareRoot -SubKey $fve -Name 'RecoveryMessage' -Type String -Value $Manifest.bitlocker.recoveryMessage
|
||||
|
||||
Set-SmRegValue -Root $SoftwareRoot -SubKey $fve -Name 'UseCustomRecoveryUrl' -Type DWord -Value 1
|
||||
|
||||
Set-SmRegValue -Root $SoftwareRoot -SubKey $fve -Name 'RecoveryUrl' -Type String -Value $Manifest.bitlocker.recoveryUrl
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,34 @@
|
||||
Set-StrictMode -Version Latest
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
|
||||
|
||||
# Write a registry value under an arbitrary hive root (a live HKLM:/HKCU: path
|
||||
|
||||
# OR a loaded offline hive exposed as a PSDrive). Creates intermediate keys.
|
||||
|
||||
function Set-SmRegValue {
|
||||
|
||||
param(
|
||||
|
||||
[Parameter(Mandatory)][string]$Root,
|
||||
|
||||
[Parameter(Mandatory)][string]$SubKey,
|
||||
|
||||
[Parameter(Mandatory)][string]$Name,
|
||||
|
||||
[Parameter(Mandatory)][ValidateSet('String','ExpandString','DWord','Binary')][string]$Type,
|
||||
|
||||
[Parameter(Mandatory)]$Value
|
||||
|
||||
)
|
||||
|
||||
$key = Join-Path $Root $SubKey
|
||||
|
||||
if (-not (Test-Path $key)) { New-Item -Path $key -Force | Out-Null }
|
||||
|
||||
New-ItemProperty -Path $key -Name $Name -PropertyType $Type -Value $Value -Force | Out-Null
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -24,11 +24,29 @@ Log 'configuring onboarding lockdown (Explorer shell + policy)'
|
||||
# --- Keyboard Filter: block shell/escape hotkeys for the locked-down session ---
|
||||
Enable-WindowsOptionalFeature -Online -FeatureName Client-KeyboardFilter -NoRestart -ErrorAction SilentlyContinue | Out-Null
|
||||
$kf='root\standardcimv2\embedded'
|
||||
# CRITICAL: by default the Keyboard Filter EXEMPTS administrators, and sm-bootstrap is an
|
||||
# admin -> Win/Start/etc. were NOT blocked. Turn that exemption off so the filter applies.
|
||||
$adm=Get-CimInstance -Namespace $kf -ClassName WEKF_Settings -Filter "Name='DisableKeyboardFilterForAdministrators'" -ErrorAction SilentlyContinue
|
||||
if($adm){ $adm.Value='false'; Set-CimInstance -InputObject $adm -ErrorAction SilentlyContinue }
|
||||
foreach($combo in 'Win','Win+L','Ctrl+Esc','Ctrl+Win+F','Win+R','Alt+Tab','Ctrl+Shift+Esc','Alt+F4'){
|
||||
$p=Get-CimInstance -Namespace $kf -ClassName WEKF_PredefinedKey -Filter "Id='$combo'" -ErrorAction SilentlyContinue
|
||||
if($p){ $p.Enabled=$true; Set-CimInstance -InputObject $p -ErrorAction SilentlyContinue }
|
||||
}
|
||||
Log 'keyboard filter rules enabled'
|
||||
Log 'keyboard filter rules enabled (admins included)'
|
||||
|
||||
# --- Hide the taskbar for the locked-down session (auto-hide in the default-user hive,
|
||||
# which the sm-bootstrap profile inherits). The fullscreen wizard covers it, but
|
||||
# auto-hide stops it peeking. StuckRects3 byte 8: 0x03 = auto-hide on. ---
|
||||
try {
|
||||
& reg load 'HKLM\SM_DU_TB' 'C:\Users\Default\NTUSER.DAT' 2>$null | Out-Null
|
||||
$sr='HKLM:\SM_DU_TB\Software\Microsoft\Windows\CurrentVersion\Explorer\StuckRects3'
|
||||
New-Item $sr -Force | Out-Null
|
||||
$bytes=[byte[]](0x30,0x00,0x00,0x00,0x28,0x00,0x00,0x00,0x03,0x00,0x00,0x00,0x30,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00)
|
||||
Set-ItemProperty $sr -Name 'Settings' -Value $bytes -Type Binary
|
||||
} catch {} finally { [gc]::Collect(); Start-Sleep -Milliseconds 300; & reg unload 'HKLM\SM_DU_TB' 2>$null | Out-Null }
|
||||
Log 'taskbar auto-hide set for default user'
|
||||
|
||||
# --- escape policies (machine-wide; reverted at teardown) ---
|
||||
$sys='HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
|
||||
|
||||
@@ -879,25 +879,36 @@ h1:focus { outline: none; }
|
||||
}
|
||||
|
||||
/* ── BitLocker recovery key (Done step) ─────────────────────────────── */
|
||||
.done-step { display: flex; flex-direction: column; align-items: flex-start; }
|
||||
.recovery-panel {
|
||||
margin: 1.25rem 0;
|
||||
padding: 1rem 1.25rem;
|
||||
margin: 0.75rem 0;
|
||||
padding: 0.85rem 1rem;
|
||||
border: 1px solid var(--clr-accent);
|
||||
border-radius: var(--radius-sm, 8px);
|
||||
background: var(--clr-accent-glow, rgba(0,212,255,0.10));
|
||||
width: 100%;
|
||||
}
|
||||
.recovery-panel h3 { margin: 0 0 0.35rem; color: var(--clr-accent); font-family: var(--font-mono); font-size: 1rem; }
|
||||
.recovery-lead { margin: 0 0 0.5rem; color: var(--clr-text-lo); font-size: 0.85rem; }
|
||||
.recovery-row { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; }
|
||||
.recovery-qr {
|
||||
width: 132px; height: 132px;
|
||||
background: #fff; padding: 6px; border-radius: var(--radius-sm, 8px);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.recovery-panel h3 { margin: 0 0 0.5rem; color: var(--clr-accent); font-family: var(--font-mono); }
|
||||
.recovery-key {
|
||||
flex: 1 1 14rem;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 1.05rem;
|
||||
font-size: 1.0rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--clr-text-hi);
|
||||
background: rgba(0,0,0,0.30);
|
||||
padding: 0.75rem 1rem;
|
||||
padding: 0.6rem 0.85rem;
|
||||
border-radius: var(--radius-sm, 8px);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
user-select: all;
|
||||
margin: 0.5rem 0;
|
||||
margin: 0;
|
||||
}
|
||||
.recovery-note { color: var(--clr-text-lo); }
|
||||
.recovery-note { color: var(--clr-text-lo); margin: 0.5rem 0 0; }
|
||||
.done-step .btn-restart { margin-top: 1rem; align-self: flex-start; }
|
||||
|
||||
@@ -39,16 +39,23 @@ public sealed class BootstrapService(IProcessRunner runner) : IBootstrapService
|
||||
// Best-effort in-session removal (usually no-ops — you can't delete the account
|
||||
// you're logged in as), THEN defer the real removal to a SYSTEM startup task that
|
||||
// runs on next boot, when sm-bootstrap is no longer logged on. It removes the
|
||||
// account + profile, then deletes itself. Encoded command avoids schtasks quoting.
|
||||
await Ps($"Remove-LocalUser -Name '{u}' -EA SilentlyContinue", ct);
|
||||
// account + profile, then unregisters itself.
|
||||
// Disable immediately (in-session, takes effect at once so the account is unusable
|
||||
// and shows as disabled), then best-effort delete; the deferred task does the real
|
||||
// delete on next boot when it isn't logged on.
|
||||
await Ps($"Disable-LocalUser -Name '{u}' -EA SilentlyContinue; Remove-LocalUser -Name '{u}' -EA SilentlyContinue", ct);
|
||||
var cleanup =
|
||||
$"Remove-LocalUser -Name '{u}' -ErrorAction SilentlyContinue; " +
|
||||
$"Get-CimInstance Win32_UserProfile | Where-Object {{ $_.LocalPath -like '*\\{u}' }} | Remove-CimInstance -ErrorAction SilentlyContinue; " +
|
||||
"schtasks /delete /tn 'SilverMetalBootstrapCleanup' /f";
|
||||
"Unregister-ScheduledTask -TaskName 'SilverMetalBootstrapCleanup' -Confirm:$false -ErrorAction SilentlyContinue";
|
||||
var b64 = Convert.ToBase64String(System.Text.Encoding.Unicode.GetBytes(cleanup));
|
||||
await Ps("schtasks /create /tn 'SilverMetalBootstrapCleanup' " +
|
||||
$"/tr 'powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand {b64}' " +
|
||||
"/sc onstart /ru SYSTEM /rl HIGHEST /f", ct);
|
||||
// Register-ScheduledTask (not schtasks.exe) — schtasks /tr caps at 261 chars and
|
||||
// silently failed with the encoded payload, so the task was never created.
|
||||
await Ps("$a=New-ScheduledTaskAction -Execute 'powershell.exe' -Argument " +
|
||||
$"'-NoProfile -ExecutionPolicy Bypass -EncodedCommand {b64}'; " +
|
||||
"$t=New-ScheduledTaskTrigger -AtStartup; " +
|
||||
"$p=New-ScheduledTaskPrincipal -UserId 'SYSTEM' -RunLevel Highest; " +
|
||||
"Register-ScheduledTask -TaskName 'SilverMetalBootstrapCleanup' -Action $a -Trigger $t -Principal $p -Force | Out-Null", ct);
|
||||
}
|
||||
private static string Esc(string s) => s.Replace("'", "''");
|
||||
private Task Ps(string s, CancellationToken ct) =>
|
||||
|
||||
@@ -1,39 +1,60 @@
|
||||
@using QRCoder
|
||||
@inject SilverOS.Welcome.Core.Apply.IProcessRunner ProcessRunner
|
||||
|
||||
<div class="step done-step">
|
||||
<h1>All Done!</h1>
|
||||
<p>Your SilverMetal device is configured and ready.</p>
|
||||
<p class="step-subtitle">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.
|
||||
<h3>⚠ Back up your BitLocker recovery key</h3>
|
||||
<p class="recovery-lead">
|
||||
This is the <strong>only</strong> way back into your drive if you forget your PIN.
|
||||
Scan the code with your phone, or copy the key — 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 class="recovery-row">
|
||||
@if (_qrDataUri is not null)
|
||||
{
|
||||
<img class="recovery-qr" src="@_qrDataUri" alt="BitLocker recovery key QR code" />
|
||||
}
|
||||
<pre class="recovery-key">@_recoveryKey</pre>
|
||||
</div>
|
||||
<p class="recovery-note"><small>
|
||||
A copy is saved on this device at <code>C:\ProgramData\SilverMetal\bitlocker-recovery.txt</code>
|
||||
— you can delete it once you've backed the key up elsewhere.
|
||||
</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;
|
||||
private string? _qrDataUri;
|
||||
|
||||
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 */ }
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_recoveryKey))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var gen = new QRCodeGenerator();
|
||||
using var data = gen.CreateQrCode(_recoveryKey, QRCodeGenerator.ECCLevel.M);
|
||||
var png = new PngByteQRCode(data).GetGraphic(6);
|
||||
_qrDataUri = "data:image/png;base64," + Convert.ToBase64String(png);
|
||||
}
|
||||
catch { /* QR is best-effort; the key text still shows */ }
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RestartNow()
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<!-- Pure-managed QR generator (no System.Drawing/native deps) for the recovery key. -->
|
||||
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user