TouchVault.psm1
|
#Requires -Version 5.1 # ============================================================================ # TouchVault — YubiKey-backed KeePass credential management for automation # ============================================================================ # 3-tier cache: in-memory → DPAPI disk cache → KeePassXC CLI + YubiKey # ============================================================================ # --------------------------------------------------------------------------- # Module-scoped state # --------------------------------------------------------------------------- $script:VaultCache = @{} $script:MasterPwFile = "$env:USERPROFILE\.keepass_cred" $script:DiskCacheDir = "$env:USERPROFILE\.keepass_session" $script:CacheTTLHours = 8 $script:DefaultDatabase = $null # Set via Set-VaultConfig or auto-detected $script:DefaultCli = $null # Set via Set-VaultConfig or auto-detected # --------------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------------- function Set-VaultConfig { <# .SYNOPSIS Configures TouchVault defaults (database path, CLI path, cache TTL). .PARAMETER DatabasePath Path to the KeePass .kdbx file. .PARAMETER CliPath Path to keepassxc-cli.exe. .PARAMETER CacheTTLHours How many hours cached entries remain valid (default 8). .EXAMPLE Set-VaultConfig -DatabasePath "C:\Secrets\Vault.kdbx" -CacheTTLHours 4 #> [CmdletBinding()] param( [string]$DatabasePath, [string]$CliPath, [int]$CacheTTLHours ) if ($DatabasePath) { $script:DefaultDatabase = $DatabasePath } if ($CliPath) { $script:DefaultCli = $CliPath } if ($CacheTTLHours) { $script:CacheTTLHours = $CacheTTLHours } } function Get-VaultConfig { <# .SYNOPSIS Returns the current TouchVault configuration. #> [PSCustomObject]@{ DatabasePath = Resolve-DatabasePath CliPath = Resolve-CliPath CacheTTLHours = $script:CacheTTLHours MasterPwFile = $script:MasterPwFile DiskCacheDir = $script:DiskCacheDir } } # --------------------------------------------------------------------------- # Path resolution (auto-detect if not explicitly set) # --------------------------------------------------------------------------- function Resolve-DatabasePath { if ($script:DefaultDatabase) { return $script:DefaultDatabase } # Search common locations $candidates = @( "$env:USERPROFILE\.keepass\Vault.kdbx", "$env:USERPROFILE\Passwords.kdbx", "C:\NTSH\Passwords.kdbx" ) foreach ($p in $candidates) { if (Test-Path $p) { $script:DefaultDatabase = $p; return $p } } throw "No KeePass database found. Run Set-VaultConfig -DatabasePath 'path\to\file.kdbx'" } function Resolve-CliPath { if ($script:DefaultCli) { return $script:DefaultCli } $candidates = @( "C:\Program Files\KeePassXC\keepassxc-cli.exe", "${env:ProgramFiles}\KeePassXC\keepassxc-cli.exe", "${env:LOCALAPPDATA}\KeePassXC\keepassxc-cli.exe" ) foreach ($p in $candidates) { if (Test-Path $p) { $script:DefaultCli = $p; return $p } } # Try PATH $cmd = Get-Command keepassxc-cli -ErrorAction SilentlyContinue if ($cmd) { $script:DefaultCli = $cmd.Source; return $cmd.Source } throw "KeePassXC CLI not found. Install KeePassXC or run Set-VaultConfig -CliPath 'path\to\keepassxc-cli.exe'" } # --------------------------------------------------------------------------- # Master Password (DPAPI-encrypted) # --------------------------------------------------------------------------- function Save-VaultMasterPassword { <# .SYNOPSIS Saves the KeePass master password using Windows DPAPI encryption. Only the current Windows user can decrypt it. Run once per machine. .EXAMPLE Save-VaultMasterPassword #> [CmdletBinding()] param() $secure = Read-Host "Enter KeePass master password" -AsSecureString $secure | ConvertFrom-SecureString | Set-Content $script:MasterPwFile -Force Write-Host "Master password saved (DPAPI-encrypted). Only your Windows account can decrypt it." -ForegroundColor Green } function Get-VaultMasterPassword { <# .SYNOPSIS Retrieves the DPAPI-encrypted master password (internal use). #> [CmdletBinding()] param() if (-not (Test-Path $script:MasterPwFile)) { throw "No saved master password. Run Save-VaultMasterPassword first." } $secure = Get-Content $script:MasterPwFile | ConvertTo-SecureString $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure) try { [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) } finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } } # --------------------------------------------------------------------------- # Disk Cache (DPAPI-encrypted, TTL-based) # --------------------------------------------------------------------------- function Get-DiskCachePath { param([string]$EntryName) if (-not (Test-Path $script:DiskCacheDir)) { New-Item -Path $script:DiskCacheDir -ItemType Directory -Force | Out-Null } $safeName = $EntryName -replace '[^\w\-]', '_' Join-Path $script:DiskCacheDir "$safeName.dat" } function Get-DiskCachedEntry { param([string]$EntryName) $path = Get-DiskCachePath $EntryName if (-not (Test-Path $path)) { return $null } $fileAge = (Get-Date) - (Get-Item $path).LastWriteTime if ($fileAge.TotalHours -gt $script:CacheTTLHours) { Remove-Item $path -Force return $null } try { $encrypted = Get-Content $path -Raw $secure = $encrypted | ConvertTo-SecureString $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure) try { $json = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) } finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } $obj = $json | ConvertFrom-Json $entry = @{} $obj.PSObject.Properties | ForEach-Object { $entry[$_.Name] = $_.Value } return $entry } catch { Remove-Item $path -Force -ErrorAction SilentlyContinue return $null } } function Save-DiskCachedEntry { param([string]$EntryName, [hashtable]$Entry) $path = Get-DiskCachePath $EntryName $json = $Entry | ConvertTo-Json -Compress $secure = ConvertTo-SecureString $json -AsPlainText -Force $secure | ConvertFrom-SecureString | Set-Content $path -Force } # --------------------------------------------------------------------------- # YubiKey Authorization Popup (WinForms) # --------------------------------------------------------------------------- function Show-YubiKeyPrompt { <# .SYNOPSIS Displays a topmost popup while waiting for YubiKey touch. Touch = authorize. Deny button = cancel + reason prompt. Returns CLI output or throws on denial. #> param( [string]$EntryName, [string]$CallerInfo, [string]$MasterPassword, [string]$CliPath, [string]$DatabasePath, [int]$YubiKeySlot = 2 ) Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing $form = New-Object System.Windows.Forms.Form $form.Text = "YubiKey Authorization" $form.Size = New-Object System.Drawing.Size(440, 260) $form.StartPosition = "CenterScreen" $form.TopMost = $true $form.FormBorderStyle = "FixedDialog" $form.MaximizeBox = $false $form.MinimizeBox = $false $form.BackColor = [System.Drawing.Color]::FromArgb(30, 30, 30) $touchLabel = New-Object System.Windows.Forms.Label $touchLabel.Text = "Touch YubiKey to Authorize" $touchLabel.Font = New-Object System.Drawing.Font("Segoe UI", 15, [System.Drawing.FontStyle]::Bold) $touchLabel.ForeColor = [System.Drawing.Color]::FromArgb(255, 200, 0) $touchLabel.AutoSize = $false $touchLabel.Size = New-Object System.Drawing.Size(400, 35) $touchLabel.Location = New-Object System.Drawing.Point(15, 10) $touchLabel.TextAlign = "MiddleCenter" $form.Controls.Add($touchLabel) $detailBox = New-Object System.Windows.Forms.GroupBox $detailBox.Text = "Request Details" $detailBox.Font = New-Object System.Drawing.Font("Segoe UI", 9) $detailBox.ForeColor = [System.Drawing.Color]::LightGray $detailBox.Size = New-Object System.Drawing.Size(396, 110) $detailBox.Location = New-Object System.Drawing.Point(15, 50) $form.Controls.Add($detailBox) $entryLabel = New-Object System.Windows.Forms.Label $entryLabel.Text = "Entry:`t$EntryName" $entryLabel.Font = New-Object System.Drawing.Font("Consolas", 10) $entryLabel.ForeColor = [System.Drawing.Color]::White $entryLabel.AutoSize = $false $entryLabel.Size = New-Object System.Drawing.Size(370, 22) $entryLabel.Location = New-Object System.Drawing.Point(10, 25) $detailBox.Controls.Add($entryLabel) $callerLabel = New-Object System.Windows.Forms.Label $callerLabel.Text = "Caller:`t$CallerInfo" $callerLabel.Font = New-Object System.Drawing.Font("Consolas", 10) $callerLabel.ForeColor = [System.Drawing.Color]::White $callerLabel.AutoSize = $false $callerLabel.Size = New-Object System.Drawing.Size(370, 22) $callerLabel.Location = New-Object System.Drawing.Point(10, 50) $detailBox.Controls.Add($callerLabel) $timeLabel = New-Object System.Windows.Forms.Label $timeLabel.Text = "Time:`t$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" $timeLabel.Font = New-Object System.Drawing.Font("Consolas", 10) $timeLabel.ForeColor = [System.Drawing.Color]::White $timeLabel.AutoSize = $false $timeLabel.Size = New-Object System.Drawing.Size(370, 22) $timeLabel.Location = New-Object System.Drawing.Point(10, 75) $detailBox.Controls.Add($timeLabel) $statusLabel = New-Object System.Windows.Forms.Label $statusLabel.Text = "Waiting for YubiKey touch..." $statusLabel.Font = New-Object System.Drawing.Font("Segoe UI", 9) $statusLabel.ForeColor = [System.Drawing.Color]::Gray $statusLabel.AutoSize = $false $statusLabel.Size = New-Object System.Drawing.Size(290, 22) $statusLabel.Location = New-Object System.Drawing.Point(15, 170) $statusLabel.TextAlign = "MiddleLeft" $form.Controls.Add($statusLabel) $denyBtn = New-Object System.Windows.Forms.Button $denyBtn.Text = "Deny" $denyBtn.Font = New-Object System.Drawing.Font("Segoe UI", 10) $denyBtn.Size = New-Object System.Drawing.Size(80, 30) $denyBtn.Location = New-Object System.Drawing.Point(331, 168) $denyBtn.BackColor = [System.Drawing.Color]::FromArgb(140, 30, 30) $denyBtn.ForeColor = [System.Drawing.Color]::White $denyBtn.FlatStyle = "Flat" $form.Controls.Add($denyBtn) $script:_yukDenied = $false $denyBtn.Add_Click({ $script:_yukDenied = $true; $form.Close() }) # Start KeePassXC CLI in background runspace $runspace = [runspacefactory]::CreateRunspace() $runspace.Open() $ps = [powershell]::Create() $ps.Runspace = $runspace [void]$ps.AddScript({ param($pw, $cli, $db, $entry, $slot) $r = $pw | & $cli show $db "$entry" --all --show-protected --yubikey $slot 2>&1 [PSCustomObject]@{ Output = $r; ExitCode = $LASTEXITCODE } }).AddArgument($MasterPassword).AddArgument($CliPath).AddArgument($DatabasePath).AddArgument($EntryName).AddArgument($YubiKeySlot) $asyncResult = $ps.BeginInvoke() # Poll timer: auto-close when CLI completes (touch detected) $timer = New-Object System.Windows.Forms.Timer $timer.Interval = 250 $timer.Add_Tick({ if ($asyncResult.IsCompleted) { $timer.Stop(); $form.Close() } }) $timer.Start() $form.Add_Shown({ $form.Activate() }) [void]$form.ShowDialog() $timer.Dispose() # Handle denial if ($script:_yukDenied) { $ps.Stop(); $ps.Dispose(); $runspace.Close() $reasonForm = New-Object System.Windows.Forms.Form $reasonForm.Text = "Authorization Denied" $reasonForm.Size = New-Object System.Drawing.Size(400, 180) $reasonForm.StartPosition = "CenterScreen" $reasonForm.TopMost = $true $reasonForm.FormBorderStyle = "FixedDialog" $reasonForm.MaximizeBox = $false $reasonForm.MinimizeBox = $false $reasonForm.BackColor = [System.Drawing.Color]::FromArgb(30, 30, 30) $rLabel = New-Object System.Windows.Forms.Label $rLabel.Text = "Why was this request denied?" $rLabel.Font = New-Object System.Drawing.Font("Segoe UI", 10) $rLabel.ForeColor = [System.Drawing.Color]::LightGray $rLabel.AutoSize = $true $rLabel.Location = New-Object System.Drawing.Point(15, 15) $reasonForm.Controls.Add($rLabel) $rBox = New-Object System.Windows.Forms.TextBox $rBox.Font = New-Object System.Drawing.Font("Segoe UI", 10) $rBox.Size = New-Object System.Drawing.Size(355, 25) $rBox.Location = New-Object System.Drawing.Point(15, 45) $rBox.BackColor = [System.Drawing.Color]::FromArgb(50, 50, 50) $rBox.ForeColor = [System.Drawing.Color]::White $reasonForm.Controls.Add($rBox) $rEntryLabel = New-Object System.Windows.Forms.Label $rEntryLabel.Text = "Entry: $EntryName | Caller: $CallerInfo" $rEntryLabel.Font = New-Object System.Drawing.Font("Segoe UI", 8) $rEntryLabel.ForeColor = [System.Drawing.Color]::Gray $rEntryLabel.AutoSize = $true $rEntryLabel.Location = New-Object System.Drawing.Point(15, 78) $reasonForm.Controls.Add($rEntryLabel) $submitBtn = New-Object System.Windows.Forms.Button $submitBtn.Text = "Submit" $submitBtn.Font = New-Object System.Drawing.Font("Segoe UI", 10) $submitBtn.Size = New-Object System.Drawing.Size(80, 30) $submitBtn.Location = New-Object System.Drawing.Point(290, 105) $submitBtn.BackColor = [System.Drawing.Color]::FromArgb(60, 60, 60) $submitBtn.ForeColor = [System.Drawing.Color]::White $submitBtn.FlatStyle = "Flat" $submitBtn.DialogResult = [System.Windows.Forms.DialogResult]::OK $reasonForm.Controls.Add($submitBtn) $reasonForm.AcceptButton = $submitBtn $reasonForm.Add_Shown({ $rBox.Focus() }) [void]$reasonForm.ShowDialog() $denyReason = $rBox.Text $reasonForm.Dispose() $msg = "YubiKey authorization denied for '$EntryName' by $CallerInfo" if ($denyReason) { $msg += " - Reason: $denyReason" } Write-Warning $msg throw $msg } # Collect result (touch = authorized) $cliResult = $ps.EndInvoke($asyncResult) $ps.Dispose() $runspace.Close() return $cliResult } # --------------------------------------------------------------------------- # Core Public Functions # --------------------------------------------------------------------------- function Get-VaultEntry { <# .SYNOPSIS Retrieves all fields from a KeePass entry with 3-tier caching. Tier 1: In-memory (instant, same session) Tier 2: DPAPI disk cache (survives restarts, 8-hour TTL) Tier 3: KeePassXC CLI + YubiKey (physical touch required) .PARAMETER EntryName The title of the KeePass entry to retrieve. .PARAMETER DatabasePath Path to the .kdbx file. Uses auto-detected default if omitted. .PARAMETER ForceRefresh Bypasses all caches and fetches fresh from KeePass (requires YubiKey touch). .PARAMETER YubiKeySlot YubiKey HMAC-SHA1 slot number (default: 2). .EXAMPLE $creds = Get-VaultEntry "Omada Client V2" $creds.UserName $creds.Password .EXAMPLE $creds = Get-VaultEntry "AWS Prod" -ForceRefresh #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string]$EntryName, [string]$DatabasePath, [switch]$ForceRefresh, [int]$YubiKeySlot = 2 ) if (-not $DatabasePath) { $DatabasePath = Resolve-DatabasePath } $cli = Resolve-CliPath # Tier 1: in-memory if (-not $ForceRefresh -and $script:VaultCache.ContainsKey($EntryName)) { return $script:VaultCache[$EntryName] } # Tier 2: DPAPI disk cache if (-not $ForceRefresh) { $diskEntry = Get-DiskCachedEntry $EntryName if ($diskEntry) { $script:VaultCache[$EntryName] = $diskEntry return $diskEntry } } # Tier 3: KeePassXC CLI + YubiKey if (-not (Test-Path $cli)) { throw "KeePassXC CLI not found at $cli" } if (-not (Test-Path $DatabasePath)) { throw "KeePass database not found at $DatabasePath" } $masterPw = Get-VaultMasterPassword # Detect calling script for popup context $callerInfo = "Unknown" $stack = Get-PSCallStack foreach ($frame in $stack) { if ($frame.ScriptName -and $frame.ScriptName -ne $MyInvocation.ScriptName) { $callerInfo = [System.IO.Path]::GetFileName($frame.ScriptName) break } } $cliResult = Show-YubiKeyPrompt ` -EntryName $EntryName ` -CallerInfo $callerInfo ` -MasterPassword $masterPw ` -CliPath $cli ` -DatabasePath $DatabasePath ` -YubiKeySlot $YubiKeySlot $result = $cliResult.Output $exitCode = $cliResult.ExitCode if ($exitCode -ne 0) { throw "KeePassXC CLI error: $result" } $entry = @{} foreach ($line in $result) { if ($line -match "^(\w+):\s*(.*)$") { $entry[$Matches[1]] = $Matches[2].Trim() } } # Populate both caches $script:VaultCache[$EntryName] = $entry Save-DiskCachedEntry $EntryName $entry return $entry } function Get-VaultSecret { <# .SYNOPSIS Retrieves a single field from a KeePass entry. Convenience wrapper around Get-VaultEntry. .PARAMETER EntryName The title of the KeePass entry. .PARAMETER Field Which field to return (Password, UserName, URL, Notes). Default: Password. .EXAMPLE $apiKey = Get-VaultSecret "AWS Prod" -Field Password $user = Get-VaultSecret "AWS Prod" -Field UserName #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string]$EntryName, [ValidateSet("Password", "UserName", "URL", "Notes")] [string]$Field = "Password", [string]$DatabasePath, [int]$YubiKeySlot = 2 ) $params = @{ EntryName = $EntryName; YubiKeySlot = $YubiKeySlot } if ($DatabasePath) { $params.DatabasePath = $DatabasePath } $entry = Get-VaultEntry @params return $entry[$Field] } function Clear-VaultCache { <# .SYNOPSIS Clears all cached credentials (in-memory and DPAPI disk cache). The next call to Get-VaultEntry will require a YubiKey touch. #> [CmdletBinding()] param() $script:VaultCache = @{} if (Test-Path $script:DiskCacheDir) { Remove-Item "$script:DiskCacheDir\*.dat" -Force -ErrorAction SilentlyContinue } Write-Host "TouchVault caches cleared (memory + disk)." -ForegroundColor Yellow } function Update-VaultEntry { <# .SYNOPSIS Forces a fresh fetch from KeePass, bypassing all caches (requires YubiKey touch). .EXAMPLE Update-VaultEntry "Omada Client V2" #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [string]$EntryName, [int]$YubiKeySlot = 2 ) Get-VaultEntry -EntryName $EntryName -ForceRefresh -YubiKeySlot $YubiKeySlot } function Initialize-VaultApp { <# .SYNOPSIS Creates a config + initializer scaffold for a new application. Generates a non-secret config JSON and an Initialize-*.ps1 script. .PARAMETER AppName Short name for the app (e.g., "Omada", "AWS"). .PARAMETER KeePassEntry Title of the KeePass entry containing the credentials. .PARAMETER ConfigDir Directory for the non-secret config file. Default: ~\.<AppName>\ .PARAMETER Properties Hashtable of non-secret config values (baseUrl, tenantId, etc.). .PARAMETER OutputDir Where to write the Initialize-*.ps1 script. Default: module directory. .EXAMPLE Initialize-VaultApp -AppName "Omada" -KeePassEntry "Omada Client V2" ` -Properties @{ baseUrl = "https://api.example.com"; omadacId = "abc123" } #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$AppName, [Parameter(Mandatory)] [string]$KeePassEntry, [string]$ConfigDir, [hashtable]$Properties = @{}, [string]$OutputDir = $PSScriptRoot ) if (-not $ConfigDir) { $ConfigDir = "$env:USERPROFILE\.$($AppName.ToLower())" } # Create config directory and JSON if (-not (Test-Path $ConfigDir)) { New-Item -Path $ConfigDir -ItemType Directory -Force | Out-Null } $configPath = Join-Path $ConfigDir "config.json" if ($Properties.Count -gt 0) { $Properties | ConvertTo-Json -Depth 3 | Set-Content $configPath -Force Write-Host "Config written: $configPath" -ForegroundColor Green } # Generate initializer script $varName = "`$${AppName}Config" $initContent = @" # Initialize-$AppName.ps1 - Dot-source this to get $varName with secrets from KeePass # Usage: . "$OutputDir\Initialize-$AppName.ps1" Import-Module TouchVault -ErrorAction Stop # Non-secret config `$_nonSecret = Get-Content '$configPath' -Raw | ConvertFrom-Json # Secrets from KeePass (YubiKey touch, cached per session) `$_creds = Get-VaultEntry "$KeePassEntry" # Build config object $varName = [PSCustomObject]@{ "@ foreach ($key in $Properties.Keys) { $initContent += "`n $key = `$_nonSecret.$key" } $initContent += @" clientId = `$_creds.UserName clientSecret = `$_creds.Password } "@ $initPath = Join-Path $OutputDir "Initialize-$AppName.ps1" $initContent | Set-Content $initPath -Force Write-Host "Initializer written: $initPath" -ForegroundColor Green Write-Host "Usage: . '$initPath'" -ForegroundColor Cyan } # --------------------------------------------------------------------------- # Utility # --------------------------------------------------------------------------- function Format-Masked { <# .SYNOPSIS Masks a secret string, showing only a prefix and the character count. .EXAMPLE $secret | Format-Masked # "abcd******* [32ch]" Format-Masked $secret -ShowChars 2 # "ab********* [32ch]" #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [AllowEmptyString()] [string]$Value, [int]$ShowChars = 4 ) if ([string]::IsNullOrEmpty($Value)) { return '(empty)' } if ($Value.Length -le $ShowChars) { return '*' * $Value.Length } $Value.Substring(0, $ShowChars) + ('*' * ($Value.Length - $ShowChars)) + " [$($Value.Length)ch]" } function Test-VaultPrerequisites { <# .SYNOPSIS Checks that all prerequisites are met (KeePassXC, YubiKey, master password). .EXAMPLE Test-VaultPrerequisites #> [CmdletBinding()] param() $results = @() # KeePassXC CLI try { $cli = Resolve-CliPath $ver = & $cli --version 2>&1 $results += [PSCustomObject]@{ Check = "KeePassXC CLI"; Status = "OK"; Detail = "$cli (v$ver)" } } catch { $results += [PSCustomObject]@{ Check = "KeePassXC CLI"; Status = "FAIL"; Detail = $_.Exception.Message } } # Database try { $db = Resolve-DatabasePath $results += [PSCustomObject]@{ Check = "KeePass Database"; Status = "OK"; Detail = $db } } catch { $results += [PSCustomObject]@{ Check = "KeePass Database"; Status = "FAIL"; Detail = $_.Exception.Message } } # Master password if (Test-Path $script:MasterPwFile) { $results += [PSCustomObject]@{ Check = "Master Password"; Status = "OK"; Detail = "DPAPI-encrypted at $($script:MasterPwFile)" } } else { $results += [PSCustomObject]@{ Check = "Master Password"; Status = "FAIL"; Detail = "Not saved. Run Save-VaultMasterPassword" } } # YubiKey (ykman) $ykman = Get-Command ykman -ErrorAction SilentlyContinue if (-not $ykman) { $ykman = Get-Command "C:\Program Files\Yubico\YubiKey Manager\ykman.exe" -ErrorAction SilentlyContinue } if ($ykman) { try { $ykInfo = & $ykman.Source list 2>&1 if ($ykInfo -match "YubiKey") { $results += [PSCustomObject]@{ Check = "YubiKey"; Status = "OK"; Detail = ($ykInfo | Select-Object -First 1) } } else { $results += [PSCustomObject]@{ Check = "YubiKey"; Status = "WARN"; Detail = "ykman found but no YubiKey detected (is it plugged in?)" } } } catch { $results += [PSCustomObject]@{ Check = "YubiKey"; Status = "WARN"; Detail = "ykman found but could not list devices" } } } else { $results += [PSCustomObject]@{ Check = "YubiKey"; Status = "WARN"; Detail = "ykman not found (optional: install YubiKey Manager)" } } # Disk cache status if (Test-Path $script:DiskCacheDir) { $cacheFiles = Get-ChildItem "$script:DiskCacheDir\*.dat" -ErrorAction SilentlyContinue $results += [PSCustomObject]@{ Check = "Disk Cache"; Status = "OK"; Detail = "$($cacheFiles.Count) cached entries in $($script:DiskCacheDir)" } } else { $results += [PSCustomObject]@{ Check = "Disk Cache"; Status = "OK"; Detail = "No cache directory yet (will be created on first use)" } } $results | Format-Table -AutoSize } # --------------------------------------------------------------------------- # Backward-compatible aliases (for existing scripts using Get-KeePassEntry etc.) # --------------------------------------------------------------------------- New-Alias -Name Get-KeePassEntry -Value Get-VaultEntry -Force New-Alias -Name Get-KeePassSecret -Value Get-VaultSecret -Force New-Alias -Name Clear-KeePassCache -Value Clear-VaultCache -Force New-Alias -Name Save-KeePassMasterPassword -Value Save-VaultMasterPassword -Force New-Alias -Name Refresh-KeePassEntry -Value Update-VaultEntry -Force # --------------------------------------------------------------------------- # Module exports # --------------------------------------------------------------------------- Export-ModuleMember -Function @( 'Set-VaultConfig' 'Get-VaultConfig' 'Save-VaultMasterPassword' 'Get-VaultMasterPassword' 'Get-VaultEntry' 'Get-VaultSecret' 'Clear-VaultCache' 'Update-VaultEntry' 'Initialize-VaultApp' 'Format-Masked' 'Test-VaultPrerequisites' ) -Alias @( 'Get-KeePassEntry' 'Get-KeePassSecret' 'Clear-KeePassCache' 'Save-KeePassMasterPassword' 'Refresh-KeePassEntry' ) |