# 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-AsbCryptoParameters { param([string]$Solution) # Read the per-solution `prime`, `generator`, `hashAlgorithm`, and # `keySize` registry values. Each AVEVA install picks its own DH # group at provisioning time, so the Rust port must use the # registry-stored values rather than a hardcoded constant — the # default in `CryptoParameters::defaults` is the .NET reference's # 1024-bit fallback (`AsbRegistry.cs:66-83`), but real installs use # smaller group sizes (768-bit prime is common). Mismatch produces a # working `Connect` (the wire bytes are exchanged) but a broken # `AuthenticateMe` (encrypted ConsumerData decrypts to garbage on # the server side because the shared secret derivation diverges). $path = "$ServicesKeyPath\$Solution" if (-not (Test-Path $path)) { throw "Solution registry key not found at $path." } $key = Get-ItemProperty -Path $path -ErrorAction Stop return [pscustomobject]@{ Prime = if ($key.PSObject.Properties['prime']) { $key.prime } else { $null } Generator = if ($key.PSObject.Properties['generator']) { $key.generator } else { $null } HashAlgorithm = if ($key.PSObject.Properties['hashAlgorithm']) { $key.hashAlgorithm } else { $null } KeySize = if ($key.PSObject.Properties['keySize']) { $key.keySize } else { $null } } } 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" # Lowercase the host segment of the URL — WCF's NetTcpPortSharing # SMSvcHost matches the registered service URL case-sensitively in # the host part; the .NET probe at `src/MxAsbClient.Probe/Program.cs:5` # hardcodes the lowercase form (`desktop-6jl3kko`) which is what # AVEVA actually registered. We keep $AsbHost as-cased for TCP DNS # resolution (`MX_ASB_HOST`) but lowercase it for the Via URL. $viaHost = $AsbHost.ToLowerInvariant() $via = "net.tcp://$viaHost/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 # Per-solution DH crypto parameters from the registry — must override # the Rust port's hardcoded `CryptoParameters::defaults()` (which uses # the .NET reference's 1024-bit default; real installs use whatever # was provisioned at install time, often a smaller 768-bit prime). $crypto = Get-AsbCryptoParameters -Solution $solution if ($crypto.Prime) { # Strip whitespace/newlines that PowerShell display would wrap into # the shown value; the registry-stored decimal must be a single # contiguous integer. $primeClean = $crypto.Prime -replace '\s+', '' Set-LiveEnvVar -Name 'MX_ASB_DH_PRIME' -Value $primeClean } else { Write-Host "[WARN] no `prime` value in registry — leaving Rust default in place" -ForegroundColor Yellow } if ($crypto.Generator) { $genClean = ($crypto.Generator.ToString()) -replace '\s+', '' Set-LiveEnvVar -Name 'MX_ASB_DH_GENERATOR' -Value $genClean } # Always export, even if empty — empty string in the registry means # "use the forceHmac fallback (HMAC-SHA1)" per `AsbSystemAuthenticator # .cs:91-92`. The example must distinguish "no env var" (use library # default, MD5) from "registry says empty" (Unrecognised → SHA1 when # forced). We pick the empty-string sentinel. Set-LiveEnvVar -Name 'MX_ASB_DH_HASH_ALGORITHM' -Value ($crypto.HashAlgorithm ?? '') if ($crypto.KeySize) { Set-LiveEnvVar -Name 'MX_ASB_DH_KEY_SIZE' -Value ($crypto.KeySize.ToString()) } Write-Host '' Write-Host 'Done. Run the example with:' -ForegroundColor Green Write-Host ' cargo run -p mxaccess --example asb-subscribe' -ForegroundColor DarkGray