u-chan-secrets 3.0.3 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,14 @@
1
1
  import { lstat } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { privateBrowserPath } from '../platform/runtime-assets.js';
4
- import { bounded } from '../core/doctor.js';
4
+ import { bounded, chromeState, safeCdpOrigin } from '../core/doctor.js';
5
5
  import { checkWindowsPermissions } from '../platform/windows/doctor.js';
6
6
  import { checkDarwinPermissions } from '../platform/darwin/doctor.js';
7
- export async function diagnoseService(home, vault, platform) {
7
+ export async function diagnoseService(home, vault, platform, cdpEndpoint) {
8
8
  const paths = [join(home, 'program', 'dist', 'service', 'main.js'), join(home, 'program', 'node_modules'),
9
9
  platform === 'win32' ? join(home, 'bin', 'node.exe') : process.execPath,
10
10
  platform === 'win32' ? join(home, 'bin', 'bw.exe') : join(home, 'bin', 'bw')];
11
- const [assets, permissions, status] = await Promise.all([
11
+ const [assets, permissions, status, browser] = await Promise.all([
12
12
  bounded(async () => {
13
13
  await privateBrowserPath(home);
14
14
  for (const p of paths) {
@@ -18,8 +18,10 @@ export async function diagnoseService(home, vault, platform) {
18
18
  }
19
19
  return 'pass';
20
20
  }, 'unknown'),
21
- bounded(() => platform === 'win32' ? checkWindowsPermissions(home) : checkDarwinPermissions(home), 'unknown', 20000),
21
+ bounded(() => platform === 'win32' ? checkWindowsPermissions(home) : checkDarwinPermissions(home), 'unknown', 65000),
22
22
  bounded(() => vault.status(), 'unavailable'),
23
+ cdpEndpoint === undefined ? undefined : bounded(() => chromeState(cdpEndpoint), 'unknown'),
23
24
  ]);
24
- return { assets, permissions, vault: status === 'unlocked' ? 'pass' : status === 'unavailable' ? 'unknown' : status };
25
+ return { assets, permissions, vault: status === 'unlocked' ? 'pass' : status === 'unavailable' ? 'unknown' : status,
26
+ ...(cdpEndpoint === undefined ? {} : { browser, browserEndpoint: safeCdpOrigin(cdpEndpoint) }) };
25
27
  }
@@ -24,6 +24,7 @@ import { GrantCoordinator } from './grants.js';
24
24
  import { ensureV3State } from './state.js';
25
25
  import { ManagementService } from './manage.js';
26
26
  import { privateBrowserPath } from '../platform/runtime-assets.js';
27
+ import { safeVersion } from '../core/doctor.js';
27
28
  export function prepareSocketEndpoint(endpoint, platform = process.platform) {
28
29
  if (platform !== 'win32') {
29
30
  const dir = dirname(endpoint);
@@ -64,9 +65,9 @@ export async function runService(platform = process.platform, env = process.env)
64
65
  target: new CdpSessionTarget(new CdpBrowserProvider(loadConfig(env, platform).cdpEndpoint)),
65
66
  });
66
67
  const core = new CoreService({ vault, allowlist, grants, approver, audit, login, mode: () => approver.effectiveMode() });
67
- let currentVersion = '3.0.0';
68
+ let currentVersion = 'unknown';
68
69
  try {
69
- currentVersion = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version;
70
+ currentVersion = safeVersion(JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version) ?? 'unknown';
70
71
  }
71
72
  catch { }
72
73
  const verifier = new NpmPackageVerifier();
@@ -82,8 +83,8 @@ export async function runService(platform = process.platform, env = process.env)
82
83
  stagingDir: cfg.stagingDir,
83
84
  currentVersion,
84
85
  });
85
- const diagnose = () => diagnoseService(cfg.home, vault, platform);
86
- const control = new ControlService({ vault, approver, limiter, settings, allowlist, grants, audit, platform, updater, diagnose, manage: () => manager.open() });
86
+ const diagnose = async () => ({ ...await diagnoseService(cfg.home, vault, platform, loadConfig(env, platform).cdpEndpoint), serviceVersion: safeVersion(currentVersion) });
87
+ const control = new ControlService({ vault, approver, limiter, settings, allowlist, grants, audit, platform, updater, diagnose, serviceVersion: safeVersion(currentVersion), manage: () => manager.open() });
87
88
  const manager = new ManagementService({
88
89
  store: allowlist, grants, vault, audit, diagnose, phrase: async () => (await settings.get()).phrase,
89
90
  show: req => adapters.prompt.manage ? adapters.prompt.manage(req) : Promise.reject(new Error('unsupported')),
@@ -1,6 +1,6 @@
1
1
  import { performance } from 'node:perf_hooks';
2
2
  import { clean, normalizeDomain } from '../core/allowlist.js';
3
- import { bounded } from '../core/doctor.js';
3
+ import { bounded, safeVersion, SERVICE_DIAGNOSTIC_TIMEOUT_MS } from '../core/doctor.js';
4
4
  export class ManagementService {
5
5
  d;
6
6
  opened = false;
@@ -19,12 +19,19 @@ export class ManagementService {
19
19
  const snapshot = await this.d.grants.run(async () => {
20
20
  const [entries, status, events, checks] = await Promise.all([
21
21
  this.d.store.list(), bounded(() => this.d.vault.status(), 'unavailable'),
22
- bounded(() => this.d.audit.readTail(10), []), bounded(this.d.diagnose, null),
22
+ bounded(() => this.d.audit.readTail(10), []), bounded(this.d.diagnose, null, SERVICE_DIAGNOSTIC_TIMEOUT_MS),
23
23
  ]);
24
24
  const all = entries.filter(e => e.domain === normalizeDomain(e.domain));
25
25
  page = Math.min(page, Math.max(0, Math.ceil(all.length / 20) - 1));
26
26
  const grants = all.slice(page * 20, page * 20 + 20);
27
27
  const lines = ['Vault: ' + status, lastAction, 'Grant page ' + (page + 1) + ': ' + grants.length + ' of ' + entries.length + ' shown. Use Previous/Next for more.'];
28
+ lines.push('Service version: ' + (safeVersion(checks?.serviceVersion) ?? 'unknown'), 'Service browser: ' + (['pass', 'fail'].includes(checks?.browser ?? '') ? checks?.browser : 'unknown'));
29
+ if (status === 'locked')
30
+ lines.push('Next step: use Unlock to approve a protected vault unlock.');
31
+ if (checks?.assets !== 'pass' || checks?.permissions !== 'pass')
32
+ lines.push('Next step: run u-chan doctor; repair from a trusted administrator terminal.');
33
+ if (checks?.browser !== 'pass')
34
+ lines.push('Next step: start dedicated Chrome and run u-chan setup.');
28
35
  for (const e of grants) {
29
36
  let label = e.expiresAt ? 'until ' + e.expiresAt : e.remainingUses !== undefined ? e.remainingUses + ' attempts remaining' : 'permanent';
30
37
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-chan-secrets",
3
- "version": "3.0.3",
3
+ "version": "4.0.1",
4
4
  "description": "U-Chan Secrets: local MCP server for Bitwarden-backed logins",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,69 +1,69 @@
1
- @echo off
2
- setlocal
3
- set ROOT=%~dp0..
4
-
5
- fltmc.exe >nul 2>&1
6
- if %errorlevel% neq 0 (
7
- echo [*] Requesting Administrator elevation...
8
- powershell -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs"
9
- exit /b
10
- )
11
-
12
- cd /d "%ROOT%"
13
- echo ========================================================
14
- echo U-Chan Secrets V2 Installation & Setup
15
- echo ========================================================
16
- echo.
17
- echo [*] Step 1: Stopping previous service (if any)...
18
- taskkill /F /IM prompt-helper.exe >nul 2>&1
19
- taskkill /F /IM auth-helper.exe >nul 2>&1
20
- call node dist\cli.js uninstall
21
- echo.
22
- echo [*] Step 2: Installing U-Chan Windows Service...
23
- call node dist\cli.js install
24
- if %errorlevel% neq 0 (
25
- echo.
26
- echo [X] Installation failed with exit code %errorlevel%.
27
- pause
28
- exit /b %errorlevel%
29
- )
30
- icacls C:\ProgramData\u-chan-secrets\bin /grant "Users:(OI)(CI)RX" >nul 2>&1
31
- echo.
32
- echo [*] Step 3: Checking service status...
33
- sc.exe query u-chan-secrets
34
- echo.
35
-
36
- echo [*] Step 4: Bitwarden Vault Login...
37
- set BITWARDENCLI_APPDATA_DIR=C:\ProgramData\u-chan-secrets\bw-data
38
- set BW_BIN=C:\ProgramData\u-chan-secrets\bin\bw.exe
39
- if not exist "%BW_BIN%" (
40
- for /f "delims=" %%i in ('where.exe bw.exe 2^>nul') do set BW_BIN=%%i
41
- )
42
-
43
- if exist "%BW_BIN%" (
44
- "%BW_BIN%" status > "%TEMP%\bw_stat.json" 2>&1
45
- findstr /C:"\"unauthenticated\"" "%TEMP%\bw_stat.json" >nul 2>&1
46
- if %errorlevel% equ 0 (
47
- echo [!] Bitwarden is not logged in for the privileged service.
48
- echo [*] Please log in to your Bitwarden vault below:
49
- "%BW_BIN%" login
50
- ) else (
51
- echo [+] Bitwarden vault is already logged in.
52
- )
53
- del "%TEMP%\bw_stat.json" >nul 2>&1
54
- ) else (
55
- echo [!] Warning: bw.exe not found.
56
- )
57
-
58
- echo.
59
- copy /y C:\ProgramData\u-chan-secrets\service.log "%ROOT%\service-copy.log" >nul 2>&1
60
- copy /y C:\ProgramData\u-chan-secrets\settings.json "%ROOT%\settings-copy.json" >nul 2>&1
61
- echo ========================================================
62
- echo [+] Service and Bitwarden configuration complete!
63
- echo.
64
- echo Next: Close this window, open your normal terminal, and run:
65
- echo u-chan import-v1
66
- echo u-chan unlock
67
- echo ========================================================
68
- echo.
69
- pause
1
+ @echo off
2
+ setlocal
3
+ set ROOT=%~dp0..
4
+
5
+ fltmc.exe >nul 2>&1
6
+ if %errorlevel% neq 0 (
7
+ echo [*] Requesting Administrator elevation...
8
+ powershell -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs"
9
+ exit /b
10
+ )
11
+
12
+ cd /d "%ROOT%"
13
+ echo ========================================================
14
+ echo U-Chan Secrets V2 Installation & Setup
15
+ echo ========================================================
16
+ echo.
17
+ echo [*] Step 1: Stopping previous service (if any)...
18
+ taskkill /F /IM prompt-helper.exe >nul 2>&1
19
+ taskkill /F /IM auth-helper.exe >nul 2>&1
20
+ call node dist\cli.js uninstall
21
+ echo.
22
+ echo [*] Step 2: Installing U-Chan Windows Service...
23
+ call node dist\cli.js install
24
+ if %errorlevel% neq 0 (
25
+ echo.
26
+ echo [X] Installation failed with exit code %errorlevel%.
27
+ pause
28
+ exit /b %errorlevel%
29
+ )
30
+ icacls C:\ProgramData\u-chan-secrets\bin /grant "Users:(OI)(CI)RX" >nul 2>&1
31
+ echo.
32
+ echo [*] Step 3: Checking service status...
33
+ sc.exe query u-chan-secrets
34
+ echo.
35
+
36
+ echo [*] Step 4: Bitwarden Vault Login...
37
+ set BITWARDENCLI_APPDATA_DIR=C:\ProgramData\u-chan-secrets\bw-data
38
+ set BW_BIN=C:\ProgramData\u-chan-secrets\bin\bw.exe
39
+ if not exist "%BW_BIN%" (
40
+ for /f "delims=" %%i in ('where.exe bw.exe 2^>nul') do set BW_BIN=%%i
41
+ )
42
+
43
+ if exist "%BW_BIN%" (
44
+ "%BW_BIN%" status > "%TEMP%\bw_stat.json" 2>&1
45
+ findstr /C:"\"unauthenticated\"" "%TEMP%\bw_stat.json" >nul 2>&1
46
+ if %errorlevel% equ 0 (
47
+ echo [!] Bitwarden is not logged in for the privileged service.
48
+ echo [*] Please log in to your Bitwarden vault below:
49
+ "%BW_BIN%" login
50
+ ) else (
51
+ echo [+] Bitwarden vault is already logged in.
52
+ )
53
+ del "%TEMP%\bw_stat.json" >nul 2>&1
54
+ ) else (
55
+ echo [!] Warning: bw.exe not found.
56
+ )
57
+
58
+ echo.
59
+ copy /y C:\ProgramData\u-chan-secrets\service.log "%ROOT%\service-copy.log" >nul 2>&1
60
+ copy /y C:\ProgramData\u-chan-secrets\settings.json "%ROOT%\settings-copy.json" >nul 2>&1
61
+ echo ========================================================
62
+ echo [+] Service and Bitwarden configuration complete!
63
+ echo.
64
+ echo Next: Close this window, open your normal terminal, and run:
65
+ echo u-chan import-v1
66
+ echo u-chan unlock
67
+ echo ========================================================
68
+ echo.
69
+ pause
@@ -1,21 +1,21 @@
1
- @echo off
2
- setlocal
3
- set ROOT=%~dp0..
4
-
5
- fltmc.exe >nul 2>&1
6
- if %errorlevel% neq 0 (
7
- echo [*] Requesting Administrator elevation...
8
- powershell -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs"
9
- exit /b
10
- )
11
-
12
- set BITWARDENCLI_APPDATA_DIR=C:\ProgramData\u-chan-secrets\bw-data
13
- set BW_BIN=C:\ProgramData\u-chan-secrets\bin\bw.exe
14
- if not exist "%BW_BIN%" (
15
- for /f "delims=" %%i in ('where.exe bw.exe 2^>nul') do set BW_BIN=%%i
16
- )
17
-
18
- echo [*] Logging in to Bitwarden for U-Chan Service Store...
19
- "%BW_BIN%" login
20
- echo.
21
- pause
1
+ @echo off
2
+ setlocal
3
+ set ROOT=%~dp0..
4
+
5
+ fltmc.exe >nul 2>&1
6
+ if %errorlevel% neq 0 (
7
+ echo [*] Requesting Administrator elevation...
8
+ powershell -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs"
9
+ exit /b
10
+ )
11
+
12
+ set BITWARDENCLI_APPDATA_DIR=C:\ProgramData\u-chan-secrets\bw-data
13
+ set BW_BIN=C:\ProgramData\u-chan-secrets\bin\bw.exe
14
+ if not exist "%BW_BIN%" (
15
+ for /f "delims=" %%i in ('where.exe bw.exe 2^>nul') do set BW_BIN=%%i
16
+ )
17
+
18
+ echo [*] Logging in to Bitwarden for U-Chan Service Store...
19
+ "%BW_BIN%" login
20
+ echo.
21
+ pause
@@ -1,4 +1,4 @@
1
- @echo off
2
- setlocal
3
- call "%~dp0debug-install.bat" %*
4
-
1
+ @echo off
2
+ setlocal
3
+ call "%~dp0debug-install.bat" %*
4
+
@@ -1,7 +1,11 @@
1
+ param([switch]$Background)
1
2
  $ErrorActionPreference = 'Stop'
2
3
  $home_ = if ($env:U_CHAN_HOME) { $env:U_CHAN_HOME } else { Join-Path $env:APPDATA 'u-chan-secrets' }
3
4
  $profileDir = Join-Path $home_ 'chrome-profile' # dedicated profile, never the default Chrome profile
4
5
  $cdp = [Uri]$(if ($env:U_CHAN_CDP) { $env:U_CHAN_CDP } else { 'http://127.0.0.1:9222' })
6
+ if ($cdp.Scheme -ne 'http' -or $cdp.Host -notin @('127.0.0.1', 'localhost', '[::1]', '::1') -or $cdp.UserInfo -or $cdp.AbsolutePath -ne '/' -or $cdp.Query -or $cdp.Fragment) {
7
+ throw 'Use an HTTP loopback CDP origin without user information, path, query or fragment.'
8
+ }
5
9
  $port = $cdp.Port
6
10
  $bindHost = $cdp.Host
7
11
 
@@ -24,10 +28,12 @@ $chrome = $candidates | Where-Object { $_ -and (Test-Path $_) } | Select-Object
24
28
  if (-not $chrome) { throw 'chrome.exe not found' }
25
29
 
26
30
  New-Item -ItemType Directory -Force -Path $profileDir | Out-Null
27
- Start-Process -FilePath $chrome -ArgumentList @(
31
+ $chromeArgs = @(
28
32
  "--remote-debugging-port=$port",
29
33
  "--remote-debugging-address=$bindHost",
30
- "--user-data-dir=$profileDir",
34
+ "--user-data-dir=`"$profileDir`"",
31
35
  '--no-first-run'
32
36
  )
37
+ if ($Background) { $chromeArgs += '--no-startup-window' }
38
+ Start-Process -FilePath $chrome -ArgumentList $chromeArgs -WindowStyle Hidden
33
39
  Write-Host "Chrome started (profile: $profileDir, CDP: http://${bindHost}:$port)"
@@ -0,0 +1,142 @@
1
+ // Two-stage disposable check: --prepare runs non-elevated; --run requires UAC approval.
2
+ // No vault is unlocked and no real passwords, keyboard text or screenshots are recorded.
3
+ import fs from 'node:fs/promises'
4
+ import path from 'node:path'
5
+ import os from 'node:os'
6
+ import { randomUUID } from 'node:crypto'
7
+ import { spawn, execFile } from 'node:child_process'
8
+ import { promisify } from 'node:util'
9
+ import { compileCsharp } from '../dist/platform/windows/assets/compiler.js'
10
+ import { PROMPT_HELPER_CSHARP } from '../dist/platform/windows/assets/prompt-src.js'
11
+ import { defaultAdminCheck } from '../dist/platform/windows/install.js'
12
+ import { setAdminOnlyAcl } from '../dist/platform/windows/acl.js'
13
+ import { accepted } from './windows-security/result-policy.mjs'
14
+
15
+ if (process.platform !== 'win32') throw new Error('Windows required')
16
+ const exec = promisify(execFile), command = process.argv[2], runIdArgument = process.argv[3]
17
+ const quote = s => "'" + s.replaceAll("'", "''") + "'"
18
+ const ps = script => exec('powershell.exe', ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(script, 'utf16le').toString('base64')], { windowsHide: true })
19
+ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
20
+ const publicRoot = path.join(process.env.ProgramData ?? 'C:\\ProgramData', 'UChanSecurityChecks')
21
+ async function createChannel(runId) {
22
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(runId ?? '')) throw new Error('Fresh run UUID required')
23
+ const directory = path.join(publicRoot, runId)
24
+ const channelScript = await fs.readFile(new URL('./windows-security/public-channel.ps1', import.meta.url), 'utf8')
25
+ await ps('$probePublicRoot=' + quote(publicRoot) + ';$probeRunDirectory=' + quote(directory) + ';' + channelScript)
26
+ return directory
27
+ }
28
+ async function publishReport(directory, report) {
29
+ const temporaryReport = path.join(directory, 'report.json.tmp')
30
+ await fs.writeFile(temporaryReport, JSON.stringify(report, null, 2), { flag: 'wx' })
31
+ await fs.rename(temporaryReport, path.join(directory, 'report.json'))
32
+ }
33
+
34
+ if (command === '--prepare') {
35
+ if (await defaultAdminCheck()) throw new Error('Prepare must run non-elevated')
36
+ const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'u-chan-security-'))
37
+ const runId = randomUUID(), prefix = path.join(directory, 'probe'), publicDirectory = path.join(publicRoot, runId), publicPrefix = path.join(publicDirectory, 'probe'), attacker = path.join(directory, 'attacker.exe')
38
+ await compileCsharp(await fs.readFile(new URL('./windows-security/attacker.cs', import.meta.url), 'utf8'), attacker, { outputType: 'WindowsApplication', referencedAssemblies: ['System.Windows.Forms.dll', 'System.Drawing.dll'] })
39
+ const child = spawn(attacker, [publicPrefix, prefix, runId], { windowsHide: true, stdio: 'ignore' })
40
+ let spawnError
41
+ child.once('error', e => { spawnError = e })
42
+ const exited = new Promise(resolve => child.once('exit', resolve))
43
+ const config = { runId, prefix, attackerPid: child.pid, directory, publicDirectory }
44
+ await fs.writeFile(path.join(directory, 'config.json'), JSON.stringify(config))
45
+ const deadline = Date.now() + 5000
46
+ while (Date.now() < deadline) {
47
+ if (spawnError) throw spawnError
48
+ if (await fs.stat(prefix + '.armed').catch(() => null)) break
49
+ if (await fs.stat(prefix + '.attacker').catch(() => null)) throw new Error('Attacker positive control failed: ' + await fs.readFile(prefix + '.attacker', 'utf8'))
50
+ await sleep(100)
51
+ }
52
+ if (!await fs.stat(prefix + '.armed').catch(() => null)) throw new Error('Attacker did not arm')
53
+ // Keep the child referenced: run --run <runId> concurrently in a second, elevated terminal.
54
+ console.log(JSON.stringify({ ...config, armed: true, nextCommand: 'node scripts/verify-windows-adversarial.mjs --run ' + runId }))
55
+ await exited
56
+ const nativeReport = path.join(publicDirectory, 'report.json'), reportDeadline = Date.now() + 30000
57
+ while (Date.now() < reportDeadline && !await fs.stat(nativeReport).catch(() => null)) await sleep(100)
58
+ const native = JSON.parse(await fs.readFile(nativeReport, 'utf8'))
59
+ const observed = Object.fromEntries((await fs.readFile(prefix + '.attacker', 'utf8')).trim().split(/\r?\n/).map(line => line.split('=')))
60
+ const report = { ...native, attacker: observed, ok: accepted(native, observed, runId) }
61
+ await fs.writeFile(path.join(directory, 'report.json'), JSON.stringify(report, null, 2))
62
+ console.log(JSON.stringify(report))
63
+ if (!report.ok) process.exitCode = 1
64
+ } else if (command === '--run') {
65
+ if (!await defaultAdminCheck()) throw new Error('Run requires administrator privileges')
66
+ const runId = runIdArgument
67
+ // Atomically create a read-public, admin/SYSTEM-write-only channel. Never read or write user-supplied paths while elevated.
68
+ const directory = await createChannel(runId), prefix = path.join(directory, 'probe')
69
+ const home = path.join(process.env.ProgramData ?? 'C:\\ProgramData', 'u-chan-secrets')
70
+ const root = path.join(home, '#0 backup', 'adversarial-' + randomUUID())
71
+ const task = 'UChanSecurityProbe-' + randomUUID()
72
+ const report = { ok: false, nativeFixture: true, root, runId }
73
+ let registered = false
74
+ try {
75
+ await fs.mkdir(root); await setAdminOnlyAcl(root)
76
+ const trace = path.join(root, 'trace.txt')
77
+ const metadata = prefix + '.metadata'
78
+ const head = 'public static class PromptHelper\n {'
79
+ if (!PROMPT_HELPER_CSHARP.includes(head)) throw new Error('Fixture hooks no longer match')
80
+ let source = PROMPT_HELPER_CSHARP.replace(head, head + '\n private static string ProbeDesktop; private static NamedPipeServerStream ProbeIdlePipe;')
81
+ .replace('desktop = CreateDesktop("UChan-" + Guid.NewGuid().ToString("N"),', 'ProbeDesktop="UChanProbe-"+Guid.NewGuid().ToString("N"); desktop = CreateDesktop(ProbeDesktop,')
82
+ .replace('int remainingSeconds = 120;', 'int remainingSeconds = 4;')
83
+ .replace('SwitchDesktop(original);', `if(SwitchDesktop(original)) File.AppendAllText(@"${trace}", "restored"+Environment.NewLine);`)
84
+ source = source.replace('timer.Start();\n if (txtPassword != null)', `timer.Start();
85
+ string probePipe="";
86
+ for(int k=0;k+1<args.Length;k++)if(args[k]=="--output-pipe")probePipe=args[k+1];
87
+ File.AppendAllText(@"${trace}", "shown-system:"+WindowsIdentity.GetCurrent().IsSystem+";session:"+Process.GetCurrentProcess().SessionId+Environment.NewLine);
88
+ File.WriteAllLines(@"${metadata}",new string[]{ProbeDesktop,probePipe,form.Handle.ToInt64().ToString(),btnOk.Handle.ToInt64().ToString(),"${runId}",probePipe+"-idle"});
89
+ form.BeginInvoke((Action)delegate { form.Activate();btnOk.Focus();File.AppendAllText(@"${trace}","canary-send-count:"+UChanProbeCanary.Send()+Environment.NewLine); });
90
+ if (txtPassword != null)`)
91
+ source = source.replace('DialogResult result = form.ShowDialog();', `Panel probeMarker=new Panel(); probeMarker.BackColor=Color.FromArgb(1,254,71); probeMarker.Size=new Size(64,64); probeMarker.Location=new Point(0,0); form.Controls.Add(probeMarker); probeMarker.BringToFront();
92
+ form.KeyPreview=true; form.KeyDown+=(sender,eventArgs)=>{if(eventArgs.KeyCode==Keys.F24)File.AppendAllText(@"${trace}","protected-canary-received"+Environment.NewLine);};
93
+ DialogResult result = form.ShowDialog();`)
94
+ source = source.replace('List<string> childArgs = new List<string>(args);', `ProbeIdlePipe=new NamedPipeServerStream(name+"-idle",PipeDirection.In,1,PipeTransmissionMode.Byte,PipeOptions.Asynchronous,4096,4096,security);bool onlySystem=true;int systemRules=0;foreach(PipeAccessRule rule in pipe.GetAccessControl().GetAccessRules(true,true,typeof(SecurityIdentifier))){if(rule.AccessControlType==AccessControlType.Allow){systemRules++;if(rule.IdentityReference.Value!="S-1-5-18")onlySystem=false;}}File.AppendAllText(@"${trace}","pipe-system-only:"+(onlySystem&&systemRules>0)+Environment.NewLine);List<string> childArgs = new List<string>(args);`)
95
+ source += '\n' + await fs.readFile(new URL('./windows-security/canary.cs', import.meta.url), 'utf8')
96
+ if (!source.includes('shown-system:') || source.includes('int remainingSeconds = 120;')) throw new Error('Fixture substitution failed')
97
+ const attacked = path.join(root, 'attacked.exe'), failure = path.join(root, 'failure.exe')
98
+ await compileCsharp(source, attacked, { outputType: 'WindowsApplication', referencedAssemblies: ['System.Windows.Forms.dll', 'System.Drawing.dll'] })
99
+ const failing = PROMPT_HELPER_CSHARP
100
+ .replace('return RunDialog(args);', 'throw new InvalidOperationException("Disposable failure fixture");')
101
+ .replace('SwitchDesktop(original);', `if(SwitchDesktop(original)) File.AppendAllText(@"${trace}","failure-restored"+Environment.NewLine);`)
102
+ await compileCsharp(failing, failure, { outputType: 'WindowsApplication', referencedAssemblies: ['System.Windows.Forms.dll', 'System.Drawing.dll'] })
103
+ const output = path.join(root, 'native-result.json'), runner = path.join(root, 'runner.mjs')
104
+ await fs.writeFile(runner, `import{execFile}from'node:child_process';import fs from'node:fs';import{promisify}from'node:util';const exec=promisify(execFile);const results={};for(const[name,file]of ${JSON.stringify([['attacked', attacked], ['failure', failure]])}){const start=Date.now();try{const r=await exec(file,['--mode','confirm','--title','DISPOSABLE SECURITY TEST - DO NOT INTERACT','--phrase','DISPOSABLE TEST - NO PASSWORD','--lines','[]'],{timeout:20000,windowsHide:true});results[name]={exitCode:0,stdoutBytes:Buffer.byteLength(r.stdout),elapsedMs:Date.now()-start};}catch(e){results[name]={exitCode:e.code,stdoutBytes:Buffer.byteLength(e.stdout??''),elapsedMs:Date.now()-start};}if(name==='attacked')fs.writeFileSync(${JSON.stringify(prefix + '.done')},'done');}fs.writeFileSync(${JSON.stringify(output)},JSON.stringify(results));`)
105
+ await setAdminOnlyAcl(root)
106
+ await ps(`$ErrorActionPreference='Stop';$a=New-ScheduledTaskAction -Execute ${quote(path.join(home, 'bin', 'node.exe'))} -Argument ${quote('"' + runner + '"')} -WorkingDirectory ${quote(root)};$p=New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest;$s=New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 2) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries;Register-ScheduledTask -TaskName ${quote(task)} -Action $a -Principal $p -Settings $s | Out-Null`)
107
+ registered = true
108
+ await ps(`Start-ScheduledTask -TaskName ${quote(task)}`)
109
+ const deadline = Date.now() + 60000
110
+ while (Date.now() < deadline && !await fs.stat(output).catch(() => null)) await sleep(200)
111
+ report.execution = JSON.parse(await fs.readFile(output, 'utf8'))
112
+ report.events = (await fs.readFile(trace, 'utf8')).trim().split(/\r?\n/)
113
+ report.ok = report.execution.attacked.exitCode === 1 && report.execution.attacked.elapsedMs >= 3500 && report.execution.attacked.stdoutBytes === 0 && report.execution.failure.exitCode === 1 && report.events.some(e => e.startsWith('shown-system:True;session:')) && report.events.includes('protected-canary-received') && report.events.includes('pipe-system-only:True') && report.events.includes('restored') && report.events.includes('failure-restored')
114
+ } catch (e) { report.errorType = e.name; report.errorMessage = String(e.message).slice(0, 300) }
115
+ finally { if (registered) await ps(`Stop-ScheduledTask -TaskName ${quote(task)} -ErrorAction SilentlyContinue;Unregister-ScheduledTask -TaskName ${quote(task)} -Confirm:$false`) }
116
+ await publishReport(directory, report)
117
+ console.log(JSON.stringify(report))
118
+ if (!report.ok) process.exitCode = 1
119
+ } else if (command === '--browser') {
120
+ if (!await defaultAdminCheck()) throw new Error('Browser fixture requires administrator privileges')
121
+ const publicDirectory = await createChannel(runIdArgument ?? randomUUID())
122
+ const home = path.join(process.env.ProgramData ?? 'C:\\ProgramData', 'u-chan-secrets')
123
+ const root = path.join(home, '#0 backup', 'browser-probe-' + randomUUID()), task = 'UChanBrowserProbe-' + randomUUID()
124
+ let registered = false
125
+ try {
126
+ await fs.mkdir(root); await setAdminOnlyAcl(root)
127
+ const runner = path.join(root, 'runner.mjs')
128
+ await fs.copyFile(new URL('./windows-security/browser-worker.mjs', import.meta.url), runner)
129
+ await fs.copyFile(new URL('./windows-security/module-audit.ps1', import.meta.url), path.join(root, 'module-audit.ps1'))
130
+ await fs.copyFile(new URL('./windows-security/result-policy.mjs', import.meta.url), path.join(root, 'result-policy.mjs'))
131
+ await setAdminOnlyAcl(root)
132
+ const transcripts = path.join(process.env.USERPROFILE, '.codex', 'sessions')
133
+ await ps(`$ErrorActionPreference='Stop';$a=New-ScheduledTaskAction -Execute ${quote(path.join(home, 'bin', 'node.exe'))} -Argument ${quote('"' + runner + '" "' + root + '" "' + transcripts + '"')} -WorkingDirectory ${quote(root)};$p=New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest;$s=New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 3) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries;Register-ScheduledTask -TaskName ${quote(task)} -Action $a -Principal $p -Settings $s | Out-Null`)
134
+ registered = true; await ps(`Start-ScheduledTask -TaskName ${quote(task)}`)
135
+ const output = path.join(root, 'browser-result.json'), deadline = Date.now() + 90000
136
+ while (Date.now() < deadline && !await fs.stat(output).catch(() => null)) await sleep(200)
137
+ const report = JSON.parse(await fs.readFile(output, 'utf8'))
138
+ await publishReport(publicDirectory, { ...report, evidence: root })
139
+ console.log(JSON.stringify({ ...report, evidence: root }))
140
+ if (!report.ok) process.exitCode = 1
141
+ } finally { if (registered) await ps(`Stop-ScheduledTask -TaskName ${quote(task)} -ErrorAction SilentlyContinue;Unregister-ScheduledTask -TaskName ${quote(task)} -Confirm:$false`) }
142
+ } else throw new Error('Use concurrent --prepare and --run <runId> terminals, or --browser from an administrator terminal')
@@ -0,0 +1,56 @@
1
+ // Non-destructive checks. Run as the ordinary desktop user, never as administrator.
2
+ import fs from 'node:fs/promises'
3
+ import net from 'node:net'
4
+ import path from 'node:path'
5
+ import { defaultAdminCheck } from '../dist/platform/windows/install.js'
6
+ import { IpcClient } from '../dist/ipc/client.js'
7
+ import { defaultEndpoint } from '../dist/ipc/endpoint.js'
8
+ import { FrameReader } from '../dist/ipc/protocol.js'
9
+
10
+ if (process.platform !== 'win32' || await defaultAdminCheck()) throw new Error('Run from a non-elevated Windows terminal')
11
+ const home = path.join(process.env.ProgramData ?? 'C:\\ProgramData', 'u-chan-secrets')
12
+ const targets = ['settings.json', 'allowlist.json', 'audit.log', 'bin/node.exe', 'bin/bw.exe', 'bin/prompt-helper.exe', 'bin/service-host.exe', 'bin/service-target.txt', 'program/package.json', 'program/dist/service/main.js', 'bin/browser/chrome.exe']
13
+ const files = []
14
+ for (const target of targets) {
15
+ const item = { target, readDenied: false, writeDenied: false }
16
+ for (const [flag, key] of [['r', 'readDenied'], ['r+', 'writeDenied']]) {
17
+ try { const handle = await fs.open(path.join(home, target), flag); await handle.close() }
18
+ catch (e) { if (e.code === 'EACCES' || e.code === 'EPERM') item[key] = true; else item[key + 'Error'] = e.code }
19
+ }
20
+ files.push(item)
21
+ }
22
+ let protectedDirectoryDenied = false
23
+ try { await fs.opendir(path.join(home, 'bw-data')).then(d => d.close()) }
24
+ catch (e) { protectedDirectoryDenied = e.code === 'EACCES' || e.code === 'EPERM' }
25
+ const endpoint = defaultEndpoint(process.env, process.platform)
26
+ const client = new IpcClient(endpoint, { timeoutMs: 10000 })
27
+ const before = await client.listAllowed()
28
+ if (!before.ok) throw new Error('Cannot establish grant baseline')
29
+ const options = [
30
+ { grant: { kind: 'timed', minutes: 0 } }, { grant: { kind: 'timed', minutes: 1441 } },
31
+ { grant: { kind: 'timed', minutes: 1.5 } }, { grant: { kind: 'once', remainingUses: 100 } },
32
+ { grant: { kind: 'permanent', expiresAt: 'never' } }, { success: { url: 'http://example.invalid/' } },
33
+ { success: { url: 'https://user:password@example.invalid/' } }, { success: { selector: '' } },
34
+ { grant: { kind: 'other' } }, { bypassApproval: true },
35
+ ]
36
+ async function raw(params) {
37
+ return new Promise((resolve, reject) => {
38
+ const socket = net.connect(endpoint), reader = new FrameReader()
39
+ const timer = setTimeout(() => { socket.destroy(); reject(new Error('Invalid request did not fail promptly')) }, 3000)
40
+ const finish = r => { clearTimeout(timer); socket.destroy(); resolve(r) }
41
+ socket.on('connect', () => socket.write(JSON.stringify({ id: 1, method: 'request_allow', params }) + '\n'))
42
+ socket.on('data', chunk => { for (const frame of reader.push(chunk)) finish(frame) })
43
+ socket.on('error', e => { clearTimeout(timer); reject(e) })
44
+ })
45
+ }
46
+ let malformedRequestsDenied = 0
47
+ for (const value of options) {
48
+ const r = await raw({ domain: 'v3-verification.invalid', loginUrl: 'https://v3-verification.invalid/', item: 'DISPOSABLE-INVALID-REQUEST', options: value })
49
+ if (r.ok !== false || r.message !== 'bad request') throw new Error('Malformed policy reached approval or mutation')
50
+ malformedRequestsDenied++
51
+ }
52
+ const after = await client.listAllowed()
53
+ const grantsUnchanged = after.ok && before.message === after.message
54
+ const ok = files.every(f => f.readDenied && f.writeDenied) && protectedDirectoryDenied && grantsUnchanged
55
+ console.log(JSON.stringify({ ok, nonElevated: true, files, protectedDirectoryDenied, malformedRequestsDenied, grantsUnchanged }))
56
+ if (!ok) process.exitCode = 1