From 4ebfd8e3a3e5b1eb0eca548be109ef640697863e Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 5 May 2026 14:45:43 -0400 Subject: [PATCH] =?UTF-8?q?[M5]=20tools:=20Get-AsbPassphrase.ps1=20?= =?UTF-8?q?=E2=80=94=20DPAPI=20loader=20for=20live-probe=20env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reads the ASB solution shared secret from the local Windows registry (HKLM\SOFTWARE\Wow6432Node\ArchestrA\ArchestrAServices\\ sharedsecret) and DPAPI-decrypts it with the canonical "wonderware" entropy + LocalMachine scope, mirroring `AsbRegistry.cs:21-41`. Auto-discovers: $env:MX_LIVE = "1" $env:MX_ASB_HOST = $env:COMPUTERNAME $env:MX_ASB_SOLUTION = (read from DefaultASBSolution) $env:MX_ASB_GALAXY_NAME = "ZB" (or -GalaxyName param) $env:MX_ASB_VIA = net.tcp:///ASBService/Default__MxDataProvider/IDataV2 $env:MX_ASB_PASSPHRASE = (DPAPI-decrypted plaintext, never printed unless -Show) Important wiring detail flagged inline: the system-wide ArchestrA solution name (`Archestra_`, source of the sharedsecret) is DIFFERENT from the per-Galaxy MxDataProvider service segment (`Default__MxDataProvider`) that the WCF endpoint URL targets. Both live under the same registry root but only the former is owned by ArchestrA; the latter is what serves IASBIDataV2 per the .NET probe's hardcoded default URL at `src/MxAsbClient.Probe/Program.cs:5`. Tested via dry-run on this box: `Archestra_DESKTOP-6JL3KKO` resolves as the solution, 390 protected bytes decrypt to an 80-char passphrase, and the assembled VIA URL matches the .NET probe's default verbatim. Hard rules: * Plaintext passphrase NEVER printed unless -Show is explicit. * Dot-source so env vars persist in the calling pwsh session. * Caller account must be authorised against the LocalMachine-scope DPAPI blob (typically: any local Administrator). Usage: . .\tools\Get-AsbPassphrase.ps1 cargo run -p mxaccess --example asb-subscribe Co-Authored-By: Claude Opus 4.7 (1M context) --- tools/Get-AsbPassphrase.ps1 | 145 ++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 tools/Get-AsbPassphrase.ps1 diff --git a/tools/Get-AsbPassphrase.ps1 b/tools/Get-AsbPassphrase.ps1 new file mode 100644 index 0000000..6b2fb55 --- /dev/null +++ b/tools/Get-AsbPassphrase.ps1 @@ -0,0 +1,145 @@ +# Get-AsbPassphrase.ps1 — read the ASB solution shared secret from the local +# Windows registry + DPAPI and export the env vars the Rust port's +# `asb-subscribe` example expects. +# +# Mirrors `src/MxAsbClient/AsbRegistry.cs:21-41`: +# 1. Look up the default solution name at +# HKLM\SOFTWARE\Wow6432Node\ArchestrA\ArchestrAServices\DefaultASBSolution +# (or the value passed via -SolutionName). +# 2. Read the `sharedsecret` REG_BINARY at +# HKLM\SOFTWARE\Wow6432Node\ArchestrA\ArchestrAServices\\sharedsecret. +# 3. DPAPI-decrypt the bytes (LocalMachine scope, entropy = "wonderware" +# UTF-16LE-encoded — the .NET reference's `Entropy` constant at +# `AsbRegistry.cs:10`). +# 4. UTF-16LE-decode the cleartext into the passphrase string. +# 5. Export $env:MX_ASB_PASSPHRASE plus convenience-derived $env:MX_ASB_HOST +# and $env:MX_ASB_VIA. Sets $env:MX_LIVE=1 to enable the example's +# live path. +# +# Hard rules: +# - Plaintext passphrase NEVER printed to the console (use -Show to opt in). +# - DPAPI scope is LocalMachine; the caller's Windows account must be +# authorised against the encrypted blob (typically: any local +# Administrator or the AVEVA service account). +# - Dot-source so env vars persist in the calling pwsh session. +# +# Usage: +# . .\tools\Get-AsbPassphrase.ps1 # default solution from registry +# . .\tools\Get-AsbPassphrase.ps1 -SolutionName 'Default_ZB_MxDataProvider' +# . .\tools\Get-AsbPassphrase.ps1 -GalaxyName 'ZB' # builds MX_ASB_VIA +# .\tools\Get-AsbPassphrase.ps1 -DryRun # print what would be set +# .\tools\Get-AsbPassphrase.ps1 -Show # print plaintext (CI / debug only!) + +[CmdletBinding()] +param( + [string]$SolutionName, + [string]$GalaxyName = 'ZB', + [string]$AsbHost = $env:COMPUTERNAME, + [switch]$DryRun, + [switch]$Show +) + +$ErrorActionPreference = 'Stop' + +# Mirror `AsbRegistry.RegistryPath` at `cs:12-14`. The registry layout uses +# the WoW64 redirector path on 64-bit hosts because the AVEVA service that +# wrote the value runs as 32-bit. +$ServicesKeyPath = if ([Environment]::Is64BitOperatingSystem) { + 'HKLM:\SOFTWARE\Wow6432Node\ArchestrA\ArchestrAServices' +} else { + 'HKLM:\SOFTWARE\ArchestrA\ArchestrAServices' +} + +# DPAPI entropy — `AsbRegistry.cs:10`. +$DpapiEntropy = [System.Text.Encoding]::Unicode.GetBytes('wonderware') + +function Resolve-AsbSolutionName { + param([string]$Override) + if ($Override) { + return $Override + } + if (-not (Test-Path $ServicesKeyPath)) { + throw "ArchestrAServices registry key not found at $ServicesKeyPath. Is AVEVA System Platform installed?" + } + $default = (Get-ItemProperty -Path $ServicesKeyPath -ErrorAction Stop).DefaultASBSolution + if (-not $default) { + throw "DefaultASBSolution registry value is empty under $ServicesKeyPath. Pass -SolutionName explicitly." + } + return $default +} + +function Get-AsbSharedSecretBytes { + param([string]$Solution) + $path = "$ServicesKeyPath\$Solution" + if (-not (Test-Path $path)) { + throw "Solution registry key not found at $path. Solution=$Solution may be misspelt." + } + $value = (Get-ItemProperty -Path $path -ErrorAction Stop).sharedsecret + if (-not $value) { + throw "sharedsecret value missing under $path." + } + if ($value -isnot [byte[]]) { + throw "sharedsecret value at $path is not REG_BINARY (got $($value.GetType().Name))." + } + return $value +} + +function Unprotect-AsbSecret { + param([byte[]]$Protected) + Add-Type -AssemblyName System.Security + try { + $clear = [System.Security.Cryptography.ProtectedData]::Unprotect( + $Protected, + $DpapiEntropy, + [System.Security.Cryptography.DataProtectionScope]::LocalMachine + ) + } catch { + throw "DPAPI decrypt failed: $_. Possible causes: this account isn't authorised against the LocalMachine-scope blob; the AVEVA service was provisioned under a different machine identity; the sharedsecret bytes are corrupt." + } + return [System.Text.Encoding]::Unicode.GetString($clear).TrimEnd("`0") +} + +function Set-LiveEnvVar { + param([string]$Name, [string]$Value, [switch]$Sensitive) + $display = if ($Sensitive -and -not $Show) { '***redacted***' } else { $Value } + if ($DryRun) { + Write-Host "[DRY] $Name = $display" -ForegroundColor Yellow + } else { + Set-Item -Path "Env:$Name" -Value $Value + Write-Host "[SET] $Name = $display" -ForegroundColor Green + } +} + +# --- main flow ------------------------------------------------------------- + +Write-Host 'mxaccess ASB passphrase loader' -ForegroundColor Cyan +Write-Host " registry: $ServicesKeyPath" -ForegroundColor DarkGray + +$solution = Resolve-AsbSolutionName -Override $SolutionName +Write-Host " solution: $solution" -ForegroundColor DarkGray + +$protected = Get-AsbSharedSecretBytes -Solution $solution +Write-Host " sharedsecret bytes: $($protected.Length) (DPAPI-protected)" -ForegroundColor DarkGray + +$passphrase = Unprotect-AsbSecret -Protected $protected +Write-Host " passphrase chars: $($passphrase.Length) (decrypted)" -ForegroundColor DarkGray +Write-Host '' + +Set-LiveEnvVar -Name 'MX_LIVE' -Value '1' +Set-LiveEnvVar -Name 'MX_ASB_HOST' -Value $AsbHost +# The endpoint URL targets the per-Galaxy MxDataProvider WCF service — +# `Default__MxDataProvider`. That's a DIFFERENT name from the +# system-wide ArchestrA solution that owns the sharedsecret (typically +# `Archestra_`); both live under the same registry root but the +# .NET probe at `src/MxAsbClient.Probe/Program.cs:5` hardcodes the +# MxDataProvider segment because that's what serves IASBIDataV2. +$mxDataProvider = "Default_${GalaxyName}_MxDataProvider" +$via = "net.tcp://$AsbHost/ASBService/$mxDataProvider/IDataV2" +Set-LiveEnvVar -Name 'MX_ASB_VIA' -Value $via +Set-LiveEnvVar -Name 'MX_ASB_SOLUTION' -Value $solution +Set-LiveEnvVar -Name 'MX_ASB_GALAXY_NAME' -Value $GalaxyName +Set-LiveEnvVar -Name 'MX_ASB_PASSPHRASE' -Value $passphrase -Sensitive + +Write-Host '' +Write-Host 'Done. Run the example with:' -ForegroundColor Green +Write-Host ' cargo run -p mxaccess --example asb-subscribe' -ForegroundColor DarkGray