# lib/vault.ps1 - the DPAPI key vault, and a preset's auth. # # Part of claude-mode.ps1, which dot-sources it into its own script scope after # the settings at its top. Not meant to run on its own. ASCII only: Windows # PowerShell 5.1 reads a .ps1 without a BOM as ANSI. $PSScriptRoot here would be # lib\, so paths beside the main script go through $script:Here. # --------------------------------------------------------------------------- # Vault (DPAPI: CurrentUser scope) # --------------------------------------------------------------------------- function Get-VaultPath { param([string] $Ref) return (Join-Path $script:VaultDir "$Ref.cred") } function ConvertFrom-SecureStringPlain { param([System.Security.SecureString] $Secure) $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Secure) try { return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } } function Set-VaultKey { param([string] $Ref, [string] $Key = '') Initialize-Root if ($Key) { Write-Warn2 'the key was given on the command line, so it is in this shell history - the hidden prompt leaves no trace' $plain = $Key # The vault is written from a SecureString (DPAPI), so the inline form # has to produce one too. $secure = ConvertTo-SecureString -String $Key -AsPlainText -Force } else { Write-Host "Paste the API key for ref '$Ref' (input hidden):" $secure = Read-Host -AsSecureString $plain = ConvertFrom-SecureStringPlain $secure } if ([string]::IsNullOrWhiteSpace($plain)) { throw 'empty key, aborted' } # A hidden prompt will happily swallow a mis-paste. Guard the two shapes that # are never a real key, because the failure is otherwise invisible until the # provider answers 401 and the UI just spins. if ($plain -match '\s') { throw "that value contains whitespace, so it is not an API key (a pasted command line?). Nothing was stored." } if ($plain -like 'claude-mode*') { throw "that value is a claude-mode command, not an API key. Nothing was stored." } if ($plain.Length -lt 16) { $unit = if ($plain.Length -eq 1) { 'character' } else { 'characters' } Write-Warn2 "that key is only $($plain.Length) $unit - unusually short. Storing anyway." } if ($Ref -eq 'openrouter' -and $plain -notlike 'sk-or-*') { Write-Warn2 "key does not start with 'sk-or-' - storing anyway" } $path = Get-VaultPath $Ref # ConvertFrom-SecureString with no -Key uses DPAPI, CurrentUser scope. ConvertFrom-SecureString -SecureString $secure | Set-Content -LiteralPath $path -Encoding ASCII -NoNewline Protect-FileAcl $path Write-Ok "stored DPAPI-encrypted key at $path" } function Get-VaultKey { param([string] $Ref) $path = Get-VaultPath $Ref if (-not (Test-Path -LiteralPath $path)) { return $null } $blob = (Get-Content -LiteralPath $path -Raw).Trim() if ([string]::IsNullOrWhiteSpace($blob)) { return $null } try { return ConvertFrom-SecureStringPlain (ConvertTo-SecureString $blob) } catch { return $null } } function Format-KeyMask { param([string] $Key) if ([string]::IsNullOrEmpty($Key)) { return '(none)' } if ($Key.Length -le 12) { return '****' } return ($Key.Substring(0, 8) + '...' + $Key.Substring($Key.Length - 4)) } function Get-PresetAuth { param($Preset) $auth = $Preset['auth'] if ($null -eq $auth) { $auth = [ordered]@{ mode = 'vault'; keyRef = 'openrouter' } } if (-not $auth.Contains('mode')) { $auth['mode'] = 'vault' } return $auth }