3f8ecde571
The zero-knowledge verifier sent to /login used to be the raw PBKDF2 output in hex — i.e. the exact bytes of the AES key that encrypts every entry. Intercepting a /login body (loopback, but still) handed over the vault key. This introduces a decoupled scheme where the transmitted verifier is a one-way function of the key. New auth-hash scheme - users.hash_algo 'pbkdf2-sha256-v2': the client sends verifier = SHA256(keyHex + "pmserver/auth-verifier/v2") instead of keyHex. Stored form is still SHA256(verifier) (identical server wrap to 'pbkdf2-sha256'), so only the algo LABEL differs — it tells the client which verifier formula to use. Verification needs no new server branch (VerifierToStoredHash already SHA256-wraps any non-legacy verifier). - The AES key (cryptoKey) stays hex(PBKDF2) for EVERY algo, so entries remain decryptable and switching schemes never re-encrypts data. Adoption: new-registration + master-pw-change only - Register and change-master-password write v2. Existing accounts keep their algo until they rotate — the login/reauth migration signal now fires only for LEGACY 'pbkdf2' (was: anything != CURRENT), so sha256/v2 accounts are never force-migrated (which would have downgraded v2 → sha256 via migrate-kdf). Client (js/app.js): algo-aware everywhere - verifierFromKeyHex(keyHex, algo) central helper; deriveKeyAndVerifier / computeVerifier take an algo arg. state.hashAlgo caches the account scheme, set from /login/challenge, register, change-master, the quick-unlock / PIN cold-start blobs, and the /recovery-key/redeem response. All ~12 verifier sites updated (login, register, reauth ×4, change-master current+new, migrate-kdf, quick-unlock + PIN cold-start, recovery-mode current verifier). Safety invariant: unknown/empty hashAlgo → key hex → byte-identical to the old behaviour, so every pre-decoupling account (and every existing quick-unlock / PIN blob without the new field) keeps working unchanged. Verified: existing account + pre-change quick-unlock still unlocks; a master-pw change now writes 'pbkdf2-sha256-v2' in vault.db. Server: recovery redeem returns hashAlgo; register + change-master store the decoupled algo; login + reauth migration signal narrowed to legacy. Also: BuildAssets.ps1 pipes $null into node --check so the JS syntax gate can't block on stdin in the Delphi pre-build environment. Addresses CODE_AUDIT.md section 1.1. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
166 lines
6.3 KiB
PowerShell
166 lines
6.3 KiB
PowerShell
# BuildAssets.ps1
|
|
# Generates assets.rc + assets.res containing the static web assets
|
|
# (index.html, js/*, css/*) so they can be linked into PMServer.exe as
|
|
# Win32 RCDATA resources.
|
|
#
|
|
# Workflow:
|
|
# 1. Edit Z:\password-manager\index.html / js\app.js / css\style.css
|
|
# 2. Run this script (or set it as pre-build event in Delphi)
|
|
# 3. Build PMServer.dpr in Delphi
|
|
# 4. Run - exe is autonomous, no external files needed at runtime
|
|
#
|
|
# Resource naming: URL path '/js/app.js' -> resource 'JS_APP_JS'
|
|
# - strip leading '/'
|
|
# - replace '/' '\' '.' '-' with '_'
|
|
# - uppercase
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[string] $WebRoot = '',
|
|
[string] $OutRC = '',
|
|
[string] $OutRES = '',
|
|
[string] $OutInc = ''
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
# Resolve $PSScriptRoot — may be empty depending on invocation context.
|
|
# Fallback to the script's own file path.
|
|
$ScriptDir = $PSScriptRoot
|
|
if (-not $ScriptDir) { $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition }
|
|
if (-not $ScriptDir) { $ScriptDir = (Get-Location).Path }
|
|
|
|
# Apply param defaults now that we have a valid script dir
|
|
if (-not $WebRoot) { $WebRoot = Join-Path $ScriptDir '..\..' }
|
|
if (-not $OutRC) { $OutRC = Join-Path $ScriptDir 'assets.rc' }
|
|
if (-not $OutRES) { $OutRES = Join-Path $ScriptDir 'assets.res' }
|
|
if (-not $OutInc) { $OutInc = Join-Path $ScriptDir 'assets.inc' }
|
|
|
|
# All output goes to stdout/stderr. When invoked via BuildAssets.cmd the
|
|
# wrapper captures both streams into build.log; when run interactively
|
|
# everything shows in the terminal.
|
|
|
|
function Log { param([string] $msg) Write-Host $msg }
|
|
|
|
try {
|
|
|
|
$WebRoot = (Resolve-Path $WebRoot).Path
|
|
Log "Web root: $WebRoot"
|
|
|
|
# Whitelist patterns (relative to WebRoot). Add more here when needed.
|
|
# Use exact paths (not wildcards) to avoid embedding *-legacy.* backups.
|
|
$patterns = @(
|
|
'index.html',
|
|
'js\app.js',
|
|
'css\style.css'
|
|
)
|
|
|
|
# Discover files
|
|
$files = @()
|
|
foreach ($p in $patterns) {
|
|
$found = Get-ChildItem -Path (Join-Path $WebRoot $p) -File -ErrorAction SilentlyContinue
|
|
foreach ($f in $found) {
|
|
$rel = $f.FullName.Substring($WebRoot.Length).TrimStart('\','/')
|
|
$files += [pscustomobject]@{
|
|
FullPath = $f.FullName
|
|
Relative = $rel
|
|
UrlPath = '/' + ($rel -replace '\\','/')
|
|
ResName = ($rel -replace '[\\/\.\-]','_').ToUpperInvariant()
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($files.Count -eq 0) {
|
|
throw "No assets found under $WebRoot. Check the patterns."
|
|
}
|
|
|
|
Log "Embedding $($files.Count) file(s):"
|
|
$files | ForEach-Object { Log (" " + $_.UrlPath + " -> " + $_.ResName) }
|
|
|
|
# --- JS syntax gate -----------------------------------------------------------
|
|
# A syntax error in app.js parses fine here but kills the whole frontend at
|
|
# runtime (no event handlers → dead UI), and it's only caught after a full
|
|
# Delphi rebuild. Run `node --check` on every embedded .js so a broken bundle
|
|
# never makes it into assets.res. Node is optional: if it isn't installed we
|
|
# warn and continue rather than blocking the build on a machine without it.
|
|
$node = Get-Command node.exe -ErrorAction SilentlyContinue
|
|
if (-not $node) { $node = Get-Command node -ErrorAction SilentlyContinue }
|
|
$jsFiles = $files | Where-Object { $_.Relative -match '\.js$' }
|
|
if ($jsFiles) {
|
|
if ($node) {
|
|
foreach ($jf in $jsFiles) {
|
|
Log "Syntax check: $($jf.Relative)"
|
|
# --check prints errors to stderr and returns non-zero on failure.
|
|
# Pipe $null into node so it can NEVER block waiting on stdin
|
|
# (some Windows node shims read stdin when launched from a
|
|
# non-interactive pre-build event, which would hang the build).
|
|
$out = $null | & $node.Source --check $jf.FullPath 2>&1
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Log "JS SYNTAX ERROR in $($jf.Relative):"
|
|
Log ($out | Out-String)
|
|
throw "JS syntax check failed for $($jf.Relative) - aborting asset build."
|
|
}
|
|
}
|
|
Log "JS syntax OK."
|
|
} else {
|
|
Log "WARNING: node not found - skipping JS syntax check. Install Node to enable it."
|
|
}
|
|
}
|
|
|
|
# --- Generate assets.rc -------------------------------------------------------
|
|
$rc = New-Object System.Text.StringBuilder
|
|
[void]$rc.AppendLine('// Auto-generated by BuildAssets.ps1 - do not edit by hand.')
|
|
[void]$rc.AppendLine('#pragma code_page(65001)')
|
|
[void]$rc.AppendLine('')
|
|
foreach ($f in $files) {
|
|
# brcc32 accepts forward slashes; backslashes need to be doubled in C strings
|
|
$rcPath = $f.FullPath -replace '\\','\\'
|
|
[void]$rc.AppendLine("$($f.ResName) RCDATA `"$rcPath`"")
|
|
}
|
|
[System.IO.File]::WriteAllText($OutRC, $rc.ToString(), [System.Text.Encoding]::ASCII)
|
|
Log "Wrote $OutRC"
|
|
|
|
# --- Generate assets.inc (Pascal include with manifest) -----------------------
|
|
# Compile-time mapping URL -> resource name, consumed by PM.EmbeddedAssets.pas.
|
|
$nl = [Environment]::NewLine
|
|
$inc = New-Object System.Text.StringBuilder
|
|
[void]$inc.Append('// Auto-generated by BuildAssets.ps1 - do not edit by hand.' + $nl)
|
|
[void]$inc.Append('const' + $nl)
|
|
[void]$inc.Append(' EMBEDDED_ASSET_COUNT = ' + $files.Count + ';' + $nl)
|
|
[void]$inc.Append(' EMBEDDED_ASSETS: array[0..EMBEDDED_ASSET_COUNT-1] of TEmbeddedAsset = (' + $nl)
|
|
for ($i = 0; $i -lt $files.Count; $i++) {
|
|
$f = $files[$i]
|
|
if ($i -eq $files.Count - 1) { $sep = '' } else { $sep = ',' }
|
|
$line = " (UrlPath: '" + $f.UrlPath + "'; ResName: '" + $f.ResName + "')" + $sep + $nl
|
|
[void]$inc.Append($line)
|
|
}
|
|
[void]$inc.Append(' );' + $nl)
|
|
[System.IO.File]::WriteAllText($OutInc, $inc.ToString(), [System.Text.Encoding]::UTF8)
|
|
Log "Wrote $OutInc"
|
|
|
|
# --- Compile to .res via brcc32 ----------------------------------------------
|
|
$brcc = $null
|
|
if ($env:BDS) {
|
|
$candidate = Join-Path $env:BDS 'bin\brcc32.exe'
|
|
if (Test-Path $candidate) { $brcc = $candidate }
|
|
}
|
|
if (-not $brcc) {
|
|
$cmd = Get-Command brcc32.exe -ErrorAction SilentlyContinue
|
|
if ($cmd) { $brcc = $cmd.Source }
|
|
}
|
|
if (-not $brcc) {
|
|
throw "brcc32.exe not found. Set the BDS environment variable to your Delphi install root, or add brcc32.exe to PATH."
|
|
}
|
|
Log "Using $brcc"
|
|
|
|
& $brcc -32 -fo "$OutRES" "$OutRC"
|
|
if ($LASTEXITCODE -ne 0) { throw "brcc32 failed with exit code $LASTEXITCODE" }
|
|
Log "Wrote $OutRES"
|
|
Log "OK"
|
|
|
|
} catch {
|
|
Log "FATAL: $($_.Exception.Message)"
|
|
Log "Stack: $($_.ScriptStackTrace)"
|
|
exit 1
|
|
}
|