123 lines
9.6 KiB
PowerShell
123 lines
9.6 KiB
PowerShell
# Installs (or replaces) the relay agent on this PC as the per-user logon task IgneumRelayService, through the signed
|
|
# jobs channel with nobody at the keyboard (MF-11, PC 2, 7 October 2026). The client zip (the relay URL, key and token,
|
|
# this machine's secret) never travels through the downloads host: the Mac encrypts it (AES-256-CBC, PKCS7) and posts it
|
|
# as a relay file item; this job fetches that item with the relay values already baked into the client on this PC, checks
|
|
# its sha256, decrypts it with the key this job carries, and unzips it under the user's profile. The jobs file and the
|
|
# relay feed are two different secrets away from each other, so neither alone holds the zip. Then: every IgneumRelayAgent*
|
|
# task and the stale client folders go, install-agent.ps1 registers the task (LeastPrivilege, no UAC), starts it, and this
|
|
# job reads back the relay's machine list and the app's state. It never quits, pauses or resumes the installed app.
|
|
# Published with the four placeholders filled: __URL__ (the ciphertext's unlisted blob URL on the relay store), __SHA__
|
|
# (its sha256), __NONCE__ and __IV__ (hex). The key is sha256(<machine-id>:<nonce>); nothing of it is printed.
|
|
$ErrorActionPreference = 'Continue'
|
|
function Say([string]$m) { Write-Host ((Get-Date -Format 'HH:mm:ss') + ' ' + $m) }
|
|
function Hex-Bytes([string]$h) { $b = New-Object byte[] ($h.Length / 2); for ($i = 0; $i -lt $b.Length; $i++) { $b[$i] = [Convert]::ToByte($h.Substring($i * 2, 2), 16) }; return ,$b }
|
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
$dest = Join-Path $env:LOCALAPPDATA 'igneum-relay\agent'
|
|
$stateDir = Join-Path $env:LOCALAPPDATA 'igneum-relay'
|
|
|
|
# 1. the key: this PC's own per-install id (%LOCALAPPDATA%\igneum\app\machine-id, 16 hex, random at the first run of the
|
|
# app, never in a package and never on the downloads host) hashed with the nonce this job carries. The jobs file alone
|
|
# (one dl token away) holds the nonce, the IV and the unlisted blob URL, not the key.
|
|
$idFile = Join-Path $env:LOCALAPPDATA 'igneum\app\machine-id'
|
|
if (-not (Test-Path $idFile)) { Say 'no machine-id on this PC; nothing installed'; exit 2 }
|
|
$mid = (Get-Content $idFile -Raw).Trim().ToLower()
|
|
if ($mid -notmatch '^[0-9a-f]{16}$') { Say 'machine-id is not 16 hex; nothing installed'; exit 2 }
|
|
$sha256 = [Security.Cryptography.SHA256]::Create()
|
|
$keyBytes = $sha256.ComputeHash([Text.Encoding]::UTF8.GetBytes($mid + ':' + '__NONCE__'))
|
|
$sha256.Dispose()
|
|
|
|
# 2. the encrypted zip from its unlisted blob URL, sha256 checked, decrypted, unzipped under the profile
|
|
$enc = Join-Path $env:TEMP 'igneum-relay-client.zip.enc'
|
|
$zip = Join-Path $env:TEMP 'igneum-relay-client.zip'
|
|
$unz = Join-Path $env:TEMP 'igneum-relay-client-unzip'
|
|
try {
|
|
Invoke-WebRequest -Uri '__URL__' -OutFile $enc -UseBasicParsing -MaximumRedirection 5 -TimeoutSec 120
|
|
} catch { Say ('could not fetch the client zip: ' + $_.Exception.Message); exit 3 }
|
|
$have = (Get-FileHash -Path $enc -Algorithm SHA256).Hash.ToLower()
|
|
if ($have -ne '__SHA__') { Say ('the fetched zip has sha256 ' + $have + ', not the one this job carries; nothing installed'); Remove-Item $enc -Force -ErrorAction SilentlyContinue; exit 4 }
|
|
Say ('encrypted zip fetched, ' + (Get-Item $enc).Length + ' bytes, sha256 verified')
|
|
try {
|
|
$aes = [Security.Cryptography.Aes]::Create()
|
|
$aes.Mode = [Security.Cryptography.CipherMode]::CBC
|
|
$aes.Padding = [Security.Cryptography.PaddingMode]::PKCS7
|
|
$aes.Key = $keyBytes
|
|
$aes.IV = Hex-Bytes '__IV__'
|
|
$bytes = [IO.File]::ReadAllBytes($enc)
|
|
$plain = $aes.CreateDecryptor().TransformFinalBlock($bytes, 0, $bytes.Length)
|
|
[IO.File]::WriteAllBytes($zip, $plain)
|
|
$aes.Dispose()
|
|
} catch { Say ('decrypt failed (the key is this machine-id with the nonce; another PC cannot open it): ' + $_.Exception.Message); Remove-Item $enc -Force -ErrorAction SilentlyContinue; exit 5 }
|
|
Remove-Item $enc -Force -ErrorAction SilentlyContinue
|
|
if (Test-Path $unz) { Remove-Item $unz -Recurse -Force -ErrorAction SilentlyContinue }
|
|
try { Expand-Archive -Path $zip -DestinationPath $unz -Force } catch { Say ('unzip failed: ' + $_.Exception.Message); Remove-Item $zip -Force -ErrorAction SilentlyContinue; exit 6 }
|
|
Remove-Item $zip -Force -ErrorAction SilentlyContinue
|
|
$src = Get-ChildItem -Path $unz -Recurse -Filter 'install-agent.ps1' -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
if (-not $src) { Say 'the zip carries no install-agent.ps1; nothing installed'; exit 7 }
|
|
New-Item -ItemType Directory -Force -Path $dest | Out-Null
|
|
& robocopy.exe $src.DirectoryName $dest /MIR /R:1 /W:1 /NFL /NDL /NJH /NJS /NP | Out-Null
|
|
if ($LASTEXITCODE -ge 8) { Say ('robocopy into ' + $dest + ' failed (' + $LASTEXITCODE + ')'); exit 8 }
|
|
Remove-Item $unz -Recurse -Force -ErrorAction SilentlyContinue
|
|
Say ('client installed under ' + $dest + ': ' + ((Get-ChildItem $dest | ForEach-Object { $_.Name }) -join ', '))
|
|
# the relay values for the read-back below come from the installed client; never printed
|
|
$t = Get-Content (Join-Path $dest 'igneum-agent.ps1') -Raw
|
|
$RelayUrl = [regex]::Match($t, '\$RelayUrl = ''([^'']+)''').Groups[1].Value
|
|
$RelayKey = [regex]::Match($t, '\$RelayKey = ''([^'']+)''').Groups[1].Value
|
|
$RelayToken = [regex]::Match($t, '\$RelayToken = ''([^'']+)''').Groups[1].Value
|
|
$Headers = @{ 'x-relay-token' = $RelayToken; 'x-igneum-key' = $RelayKey }
|
|
# earlier client folders on this PC (another machine's secret, or an old copy): found for the clean-up below
|
|
$clients = @()
|
|
$roots = @((Join-Path $env:USERPROFILE 'Downloads'), (Join-Path $env:USERPROFILE 'Desktop'))
|
|
try {
|
|
$tr = & schtasks.exe /Query /TN IgneumRelayAgent /V /FO LIST 2>$null | ForEach-Object { "$_" } | Where-Object { $_ -match '^Task To Run:' }
|
|
if ($tr) { $m = [regex]::Match("$tr", '([A-Za-z]:\\[^"]*?igneum-agent\.bat)'); if ($m.Success) { $roots = @((Split-Path -Parent $m.Groups[1].Value)) + $roots; Say ('the stale task points at ' + $m.Groups[1].Value) } }
|
|
} catch { }
|
|
foreach ($root in $roots) {
|
|
if ($root -and (Test-Path $root)) { $clients += @(Get-ChildItem -Path $root -Recurse -Depth 4 -Filter 'igneum-agent.ps1' -ErrorAction SilentlyContinue) }
|
|
}
|
|
|
|
# 3. the stale one-shot tasks (install-agent.ps1 removes IgneumRelayAgent* itself) and the stale client folders that
|
|
# carried another machine's secret; the state folder's machine.txt goes so the new registration names this PC
|
|
foreach ($c in $clients) {
|
|
$dir = $c.DirectoryName
|
|
if ($dir -like ($dest + '*')) { continue }
|
|
try { Remove-Item -Path $dir -Recurse -Force -ErrorAction Stop; Say ('removed the stale client folder ' + $dir) } catch { Say ('could not remove ' + $dir + ': ' + $_.Exception.Message) }
|
|
}
|
|
Get-ChildItem -Path (Join-Path $env:USERPROFILE 'Downloads') -Filter 'igneum-relay-clients*.zip' -ErrorAction SilentlyContinue | ForEach-Object { Remove-Item $_.FullName -Force -ErrorAction SilentlyContinue; Say ('removed ' + $_.Name) }
|
|
Remove-Item (Join-Path $stateDir 'machine.txt') -Force -ErrorAction SilentlyContinue
|
|
|
|
# 4. register and start the task (per user, LeastPrivilege, no prompt; a failure is a line here, never a dialog)
|
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $dest 'install-agent.ps1') 2>&1 | ForEach-Object { "$_" } | ForEach-Object { Say ('install-agent: ' + $_) }
|
|
Say ('install-agent exit ' + $LASTEXITCODE)
|
|
& schtasks.exe /Query /FO CSV /NH 2>$null | ForEach-Object { "$_" } | Where-Object { $_ -match 'IgneumRelay' } | ForEach-Object { Say ('task: ' + $_) }
|
|
|
|
# 5. the relay's machine list: this PC must be seen inside a minute
|
|
$seen = ''
|
|
$deadline = (Get-Date).AddSeconds(75)
|
|
while ((Get-Date) -lt $deadline) {
|
|
try {
|
|
$m = Invoke-RestMethod -Uri ($RelayUrl + '/api/relay?fn=machines') -Headers $Headers -TimeoutSec 30
|
|
$mine = @($m.machines | Where-Object { $_.hostname -and ($_.hostname -like ($env:COMPUTERNAME + '*')) })
|
|
$fresh = @($mine | Where-Object { $_.last_seen -and (([DateTime]$_.last_seen).ToUniversalTime() -gt (Get-Date).ToUniversalTime().AddSeconds(-90)) })
|
|
if ($fresh.Count -gt 0) { $seen = ($fresh | ForEach-Object { 'RESULT machine ' + $_.name + ' hostname ' + $_.hostname + ' role ' + $_.role + ' last_seen ' + $_.last_seen }) -join "`n"; break }
|
|
} catch { Say ('machines: ' + $_.Exception.Message) }
|
|
Start-Sleep -Seconds 5
|
|
}
|
|
if ($seen) { Write-Host $seen } else { Say 'RESULT machine: this PC was not seen on the relay inside 75 s (read the agent-service.log lines below)' }
|
|
$slog = Join-Path $stateDir 'logs\agent-service.log'
|
|
if (Test-Path $slog) { Say '--- agent-service.log (last 25)'; Get-Content $slog -Tail 25 }
|
|
|
|
# 6. the app's state (api/state read only) and what its window is
|
|
$urlFile = Join-Path $env:LOCALAPPDATA 'igneum\app\app.url'
|
|
if (Test-Path $urlFile) {
|
|
try {
|
|
$st = Invoke-RestMethod -Uri ((Get-Content $urlFile -Raw).Trim() + 'api/state') -TimeoutSec 5 -UseBasicParsing
|
|
Say ('RESULT app version ' + $st.version + ' phase ' + $st.phase + ' mining ' + $st.mining.state + ' node ' + $st.node.state + ' blocks ' + $st.node.blocks + ' peers ' + $st.node.peers + ' up ' + $st.uptime_s + ' s')
|
|
} catch { Say ('app state: ' + $_.Exception.Message) }
|
|
}
|
|
foreach ($p in @(Get-Process -Name 'Igneum Miner', 'igneum-app', 'igneumd' -ErrorAction SilentlyContinue)) { Say ('RESULT process ' + $p.ProcessName + ' pid ' + $p.Id + ' started ' + $p.StartTime.ToString('o') + ' window "' + $p.MainWindowTitle + '"') }
|
|
try {
|
|
$wv = Get-ItemProperty -Path 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' -ErrorAction SilentlyContinue
|
|
if ($wv) { Say ('RESULT webview2 runtime ' + $wv.pv) } else { Say 'RESULT webview2 runtime: not found in the registry (the dashboard would open in the browser and the window would stay dark)' }
|
|
} catch { }
|
|
Say 'agent install done'
|
|
exit 0
|