u-chan-secrets 5.0.0 → 5.1.0

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.
Files changed (51) hide show
  1. package/README.md +233 -91
  2. package/dist/cli.js +115 -19
  3. package/dist/client/claude-setup.js +97 -0
  4. package/dist/client/codex-setup.js +6 -5
  5. package/dist/client/discord.js +67 -0
  6. package/dist/client/github.js +28 -0
  7. package/dist/client/setup.js +40 -6
  8. package/dist/client/update-package.js +166 -0
  9. package/dist/client/update.js +138 -0
  10. package/dist/client/windows-command.js +54 -0
  11. package/dist/core/browser.js +0 -5
  12. package/dist/core/config.js +1 -0
  13. package/dist/core/doctor.js +5 -1
  14. package/dist/core/vault.js +110 -5
  15. package/dist/discord/http.js +84 -0
  16. package/dist/discord/policy.js +47 -0
  17. package/dist/discord/service.js +349 -0
  18. package/dist/discord/store.js +85 -0
  19. package/dist/github/http.js +93 -0
  20. package/dist/github/policy.js +24 -0
  21. package/dist/github/service.js +185 -0
  22. package/dist/github/store.js +54 -0
  23. package/dist/index.js +1 -1
  24. package/dist/ipc/client.js +74 -6
  25. package/dist/ipc/server.js +38 -3
  26. package/dist/mcp/server.js +25 -2
  27. package/dist/platform/windows/assets/prompt-src.js +160 -38
  28. package/dist/platform/windows/doctor.js +2 -2
  29. package/dist/platform/windows/install.js +4 -0
  30. package/dist/service/control.js +47 -5
  31. package/dist/service/doctor.js +3 -0
  32. package/dist/service/main.js +34 -17
  33. package/dist/service/manage.js +20 -16
  34. package/dist/service/state.js +28 -0
  35. package/dist/service/update/coordinator.js +171 -0
  36. package/dist/service/update/files.js +47 -0
  37. package/dist/service/update/gate.js +21 -0
  38. package/dist/service/update/journal.js +157 -0
  39. package/dist/service/update/native.js +50 -0
  40. package/dist/service/update/network.js +28 -0
  41. package/dist/service/update/protocol.js +28 -0
  42. package/dist/service/update/release.js +102 -0
  43. package/dist/service/update/worker-main.js +116 -0
  44. package/dist/service/update/worker.js +58 -0
  45. package/package.json +1 -1
  46. package/scripts/debug-install.bat +69 -69
  47. package/scripts/desktop-browser.ps1 +110 -103
  48. package/scripts/login-bw.bat +21 -21
  49. package/scripts/migrate.bat +4 -4
  50. package/scripts/start-browser.ps1 +33 -33
  51. package/scripts/windows-security/desktop-matrix.mjs +54 -54
@@ -0,0 +1,116 @@
1
+ import { readFile, lstat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { performance } from 'node:perf_hooks';
4
+ import { defaultProgramData } from '../../platform/windows/install.js';
5
+ import { ServiceManager } from '../../platform/windows/service-manager.js';
6
+ import { productionJournal, verifyUpdateActive, secureUpdateDirectory } from './native.js';
7
+ import { idSchema, sameIdentity } from './protocol.js';
8
+ import { artifactIntegrity, inspectRelease } from './release.js';
9
+ import { noLinks } from './journal.js';
10
+ import { runUpdateTransaction } from './worker.js';
11
+ import { assertCompatibleState } from '../state.js';
12
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
13
+ const alive = (pid) => { try {
14
+ process.kill(pid, 0);
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ } };
20
+ async function main() {
21
+ if (process.platform !== 'win32' || !process.send)
22
+ throw new Error('unsupported_worker');
23
+ const home = process.env.U_CHAN_HOME ?? defaultProgramData(), id = idSchema.parse(process.argv[2]), journal = productionJournal(home), job = await journal.read(id);
24
+ if (job.parentPid !== process.ppid || job.phase !== 'switching')
25
+ throw new Error('invalid_worker_parent');
26
+ delete process.env.BW_SESSION;
27
+ process.send({ type: 'ready', id, nonce: job.nonce });
28
+ await new Promise((resolve, reject) => {
29
+ const timer = setTimeout(() => reject(new Error('handoff_timeout')), 30000);
30
+ process.on('message', message => { const m = message; if (m?.type === 'start' && m.id === id && m.nonce === job.nonce) {
31
+ clearTimeout(timer);
32
+ resolve();
33
+ } });
34
+ });
35
+ const sm = new ServiceManager();
36
+ const waitStatus = async (status) => { const end = performance.now() + 30000; while (await sm.status('u-chan-secrets') !== status) {
37
+ if (performance.now() > end)
38
+ throw new Error('service_timeout');
39
+ await sleep(200);
40
+ } };
41
+ await runUpdateTransaction(journal, id, {
42
+ verify: async () => {
43
+ await verifyUpdateActive(home);
44
+ await assertCompatibleState(home, Number(job.target.version.split('.')[0]));
45
+ const dir = journal.directory(id), archive = join(dir, 'release.tgz');
46
+ await noLinks(archive);
47
+ if ((await lstat(archive)).size > 32 * 1024 * 1024)
48
+ throw new Error('invalid_release');
49
+ const bytes = await readFile(archive);
50
+ if (artifactIntegrity(bytes) !== job.target.integrity)
51
+ throw new Error('invalid_release');
52
+ const release = inspectRelease(bytes);
53
+ for (const [path, expected] of release.files) {
54
+ const p = join(dir, 'next', path);
55
+ await noLinks(p);
56
+ if (!(await readFile(p)).equals(expected))
57
+ throw new Error('staged_release_changed');
58
+ }
59
+ await verifyUpdateActive(home, join(dir, 'next'));
60
+ const parent = join(home, '#0 backup');
61
+ await noLinks(parent);
62
+ try {
63
+ await lstat(parent);
64
+ }
65
+ catch (e) {
66
+ if (e.code !== 'ENOENT')
67
+ throw e;
68
+ const fs = await import('node:fs/promises');
69
+ await fs.mkdir(parent, { mode: 0o700 });
70
+ await secureUpdateDirectory(parent);
71
+ }
72
+ if (await import('../../platform/windows/doctor.js').then(m => m.checkWindowsPermissions(parent, false)) !== 'pass')
73
+ throw new Error('unsafe_backup_parent');
74
+ },
75
+ handoff: async () => {
76
+ process.send?.({ type: 'activated', id, nonce: job.nonce });
77
+ const end = performance.now() + 30000;
78
+ while (alive(job.parentPid)) {
79
+ if (performance.now() > end)
80
+ throw new Error('parent_exit_timeout');
81
+ await sleep(200);
82
+ }
83
+ if (process.connected)
84
+ process.disconnect?.();
85
+ },
86
+ stopForRollback: async () => { if (!alive(job.parentPid)) {
87
+ await sm.stop('u-chan-secrets');
88
+ await waitStatus('stopped');
89
+ } },
90
+ startSource: async () => { if (!alive(job.parentPid)) {
91
+ await sm.start('u-chan-secrets');
92
+ await waitStatus('running');
93
+ } },
94
+ waitReady: async (kind) => {
95
+ const end = performance.now() + 90000, expected = kind === 'target' ? job.target : job.source;
96
+ let stable = 0, lastPid = 0;
97
+ while (performance.now() < end) {
98
+ const m = await journal.marker(id);
99
+ if (m && m.id === id && m.nonce === job.nonce && m.kind === kind && sameIdentity(m.boot, expected) && alive(m.pid)) {
100
+ stable = m.pid === lastPid ? stable + 1 : 1;
101
+ lastPid = m.pid;
102
+ if (stable >= 2)
103
+ return true;
104
+ }
105
+ else {
106
+ stable = 0;
107
+ lastPid = 0;
108
+ }
109
+ await sleep(500);
110
+ }
111
+ return false;
112
+ },
113
+ });
114
+ }
115
+ main().catch(() => { process.exitCode = 1; }).finally(() => { if (process.connected)
116
+ process.disconnect?.(); });
@@ -0,0 +1,58 @@
1
+ import { mkdir, readFile, readdir, rename, lstat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { noLinks } from './journal.js';
4
+ export async function runUpdateTransaction(journal, id, deps) {
5
+ const job = await journal.read(id);
6
+ if (job.phase !== 'switching')
7
+ throw new Error('invalid_update_transition');
8
+ const program = join(journal.home, 'program'), next = join(journal.directory(id), 'next');
9
+ const parent = join(journal.home, '#0 backup'), backup = join(parent, 'update-' + id), failed = join(journal.directory(id), 'failed-program');
10
+ let backedUp = false, activated = false;
11
+ try {
12
+ await deps.verify();
13
+ for (const path of [program, next, parent, backup, failed])
14
+ await noLinks(path);
15
+ await mkdir(parent, { recursive: true, mode: 0o700 });
16
+ for (const path of [backup, failed]) {
17
+ try {
18
+ await lstat(path);
19
+ throw new Error('recovery_path_exists');
20
+ }
21
+ catch (e) {
22
+ if (e.code !== 'ENOENT')
23
+ throw e;
24
+ }
25
+ }
26
+ const names = await readdir(program), pkg = await readFile(join(program, 'package.json'));
27
+ await rename(program, backup);
28
+ backedUp = true;
29
+ const moved = await readdir(backup);
30
+ if (moved.length !== names.length || names.some(n => !moved.includes(n)) || !(await readFile(join(backup, 'package.json'))).equals(pkg))
31
+ throw new Error('backup_mismatch');
32
+ await rename(next, program);
33
+ activated = true;
34
+ await journal.transition(id, 'waiting_for_service');
35
+ await deps.handoff();
36
+ if (!await deps.waitReady('target'))
37
+ throw new Error('startup_failed');
38
+ return await journal.transition(id, 'service_ready');
39
+ }
40
+ catch {
41
+ if (!backedUp)
42
+ return journal.transition(id, 'failed', 'worker_failed');
43
+ try {
44
+ await journal.transition(id, 'rolling_back', 'startup_failed');
45
+ await deps.stopForRollback();
46
+ if (activated)
47
+ await rename(program, failed);
48
+ await rename(backup, program);
49
+ await deps.startSource();
50
+ if (!await deps.waitReady('source'))
51
+ throw new Error('rollback_start_failed');
52
+ return await journal.transition(id, 'rolled_back', 'startup_failed');
53
+ }
54
+ catch {
55
+ return journal.transition(id, 'recovery_required', 'rollback_failed');
56
+ }
57
+ }
58
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-chan-secrets",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
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,103 +1,110 @@
1
- # Shared user-side Chrome/Edge launcher and read-only process policy. No elevation or vault access.
2
- function Split-UChanWindowsCommandLine([string]$CommandLine) {
3
- if (-not ('UChanWindowsCommandLine' -as [type])) {
4
- Add-Type -TypeDefinition @'
5
- using System;
6
- using System.Runtime.InteropServices;
7
- public static class UChanWindowsCommandLine {
8
- [DllImport("shell32.dll", SetLastError=true, CharSet=CharSet.Unicode)] static extern IntPtr CommandLineToArgvW(string value, out int count);
9
- [DllImport("kernel32.dll")] static extern IntPtr LocalFree(IntPtr value);
10
- public static string[] Split(string value) {
11
- int count; IntPtr memory=CommandLineToArgvW(value,out count);
12
- if(memory==IntPtr.Zero) throw new InvalidOperationException("Command line is unavailable");
13
- try { string[] result=new string[count]; for(int i=0;i<count;i++)result[i]=Marshal.PtrToStringUni(Marshal.ReadIntPtr(memory,i*IntPtr.Size)); return result; }
14
- finally { LocalFree(memory); }
15
- }
16
- }
17
- '@
18
- }
19
- return [UChanWindowsCommandLine]::Split($CommandLine)
20
- }
21
- function Test-UChanBrowserProcess($Observation, [string]$Executable, [string]$Profile, [string]$OwnerSid, [int]$Port) {
22
- if (-not $Observation.ExecutablePath -or -not $Observation.OwnerSid -or -not $Observation.CommandLine -or -not $Observation.Addresses) { return 'unknown' }
23
- if ($Observation.ExecutablePath -ine $Executable -or $Observation.OwnerSid -ne $OwnerSid) { return 'fail' }
24
- foreach ($address in $Observation.Addresses) { if ($address -notin @('127.0.0.1','::1')) { return 'fail' } }
25
- try { $tokens = @(Split-UChanWindowsCommandLine $Observation.CommandLine) } catch { return 'unknown' }
26
- if ($tokens.Count -lt 1 -or $tokens[0] -ine $Executable) { return 'fail' }
27
- $profiles = @($tokens | Where-Object { $_ -imatch '^--user-data-dir(?:=|$)' })
28
- if ($profiles.Count -ne 1 -or $profiles[0] -inotmatch '^--user-data-dir=.+$') { return 'fail' }
29
- $actual = $profiles[0].Substring('--user-data-dir='.Length)
30
- try { if ([IO.Path]::GetFullPath($actual).TrimEnd('\') -ine [IO.Path]::GetFullPath($Profile).TrimEnd('\')) { return 'fail' } } catch { return 'fail' }
31
- $ports = @($tokens | Where-Object { $_ -imatch '^--remote-debugging-port(?:=|$)' })
32
- if ($ports.Count -ne 1 -or $ports[0] -inotmatch '^--remote-debugging-port=[0-9]+$' -or $ports[0].Substring('--remote-debugging-port='.Length) -ne [string]$Port) { return 'fail' }
33
- return 'pass'
34
- }
35
-
36
- function Get-UChanBrowserConfiguration([string]$Browser, [string]$HomeOverride, [string]$OwnerOverride, [string]$LocalOverride) {
37
- $userHome = if ($HomeOverride) { $HomeOverride } elseif ($env:U_CHAN_HOME) { $env:U_CHAN_HOME } else { Join-Path $env:APPDATA 'u-chan-secrets' }
38
- if (-not $Browser) { $Browser = $env:U_CHAN_BROWSER }
39
- if (-not $Browser) {
40
- $choice = Join-Path $userHome 'browser.json'
41
- if (Test-Path -LiteralPath $choice) {
42
- $item = Get-Item -LiteralPath $choice -Force
43
- if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'Browser preference path is redirected.' }
44
- $saved = Get-Content -LiteralPath $choice -Raw | ConvertFrom-Json
45
- if (@($saved.PSObject.Properties).Count -ne 1) { throw 'Invalid browser preference.' }
46
- $Browser = $saved.browser
47
- if (-not $Browser) { throw 'Invalid browser preference.' }
48
- } else { $Browser = 'chrome' }
49
- }
50
- if ($Browser -cnotin @('chrome','edge')) { throw 'Choose chrome or edge.' }
51
- $profile = Join-Path $userHome ($Browser + '-profile')
52
- $check = [IO.Path]::GetFullPath($profile)
53
- while ($check) {
54
- if ((Test-Path -LiteralPath $check) -and ((Get-Item -LiteralPath $check -Force).Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'Dedicated browser profile is redirected.' }
55
- $check = [IO.Path]::GetDirectoryName($check)
56
- }
57
- $cdp = [Uri]$(if ($env:U_CHAN_CDP) { $env:U_CHAN_CDP } else { 'http://127.0.0.1:9222' })
58
- 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) { throw 'Use an HTTP loopback CDP origin without credentials, path, query or fragment.' }
59
- $suffix = if ($Browser -eq 'edge') { 'Microsoft\Edge\Application\msedge.exe' } else { 'Google\Chrome\Application\chrome.exe' }
60
- $local = if ($LocalOverride) { $LocalOverride } else { $env:LOCALAPPDATA }
61
- $candidates = @(foreach ($base in @($env:ProgramFiles, ${env:ProgramFiles(x86)}, $local)) { if ($base) { Join-Path $base $suffix } })
62
- $executables = @($candidates | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf })
63
- $owner = if ($OwnerOverride) { $OwnerOverride } else { [Security.Principal.WindowsIdentity]::GetCurrent().User.Value }
64
- return @{ Browser=$Browser; Profile=$profile; Endpoint=$cdp; Executables=$executables; OwnerSid=$owner }
65
- }
66
-
67
- # Privileged checks derive the desktop home from the installed owner's SID, never SYSTEM's APPDATA or user preference files.
68
- function Get-UChanOwnerBrowserConfiguration([string]$OwnerSid, [string]$Browser) {
69
- if ($OwnerSid -notmatch '^S-1-5-21-(?:[0-9]+-){3}[0-9]+$') { throw 'Installed desktop owner is invalid.' }
70
- $profile = (Get-ItemProperty -LiteralPath ('HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\'+$OwnerSid) -ErrorAction Stop).ProfileImagePath
71
- if (-not $profile) { throw 'Installed desktop owner profile is unavailable.' }
72
- $profile = [Environment]::ExpandEnvironmentVariables($profile)
73
- $roaming = Join-Path $profile 'AppData\Roaming'
74
- $local = Join-Path $profile 'AppData\Local'
75
- $shell = Get-ItemProperty -LiteralPath ('Registry::HKEY_USERS\'+$OwnerSid+'\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders') -ErrorAction SilentlyContinue
76
- if ($shell.AppData) { $roaming = [Environment]::ExpandEnvironmentVariables(($shell.AppData -ireplace '%USERPROFILE%', $profile)) }
77
- if ($shell.'Local AppData') { $local = [Environment]::ExpandEnvironmentVariables(($shell.'Local AppData' -ireplace '%USERPROFILE%', $profile)) }
78
- $desktopHome = if ($env:U_CHAN_DESKTOP_HOME) { $env:U_CHAN_DESKTOP_HOME } else { Join-Path $roaming 'u-chan-secrets' }
79
- return Get-UChanBrowserConfiguration $Browser $desktopHome $OwnerSid $local
80
- }
81
-
82
- function Get-UChanBrowserState($Configuration) {
83
- try {
84
- $listeners = @(Get-NetTCPConnection -State Listen -ErrorAction Stop | Where-Object LocalPort -eq $Configuration.Endpoint.Port)
85
- if ($listeners.Count -eq 0) { return 'missing' }
86
- $owners = @($listeners.OwningProcess | Select-Object -Unique)
87
- if ($owners.Count -ne 1) { return 'fail' }
88
- $process = Get-CimInstance Win32_Process -Filter ("ProcessId=" + $owners[0]) -ErrorAction Stop
89
- if (-not $process) { return 'unknown' }
90
- $owner = Invoke-CimMethod -InputObject $process -MethodName GetOwnerSid -ErrorAction Stop
91
- $observation = @{ ExecutablePath=$process.ExecutablePath; OwnerSid=$owner.Sid; CommandLine=$process.CommandLine; Addresses=@($listeners.LocalAddress) }
92
- if (-not $process.ExecutablePath) { return 'unknown' }
93
- if ($process.ExecutablePath -notin $Configuration.Executables) { return 'fail' }
94
- $policy = Test-UChanBrowserProcess $observation $process.ExecutablePath $Configuration.Profile $Configuration.OwnerSid $Configuration.Endpoint.Port
95
- if ($policy -ne 'pass') { return $policy }
96
- $version = Invoke-RestMethod -Uri ([Uri]::new($Configuration.Endpoint,'/json/version')) -TimeoutSec 2 -ErrorAction Stop
97
- $socket = [Uri]$version.webSocketDebuggerUrl
98
- if ($socket.Scheme -ne 'ws' -or $socket.Host -notin @('127.0.0.1','localhost','[::1]','::1') -or $socket.Port -ne $Configuration.Endpoint.Port -or $socket.UserInfo) { return 'fail' }
99
- $edge = $version.'User-Agent' -match '(?:Edg|Edge)/'
100
- if (($Configuration.Browser -eq 'edge') -ne $edge) { return 'fail' }
101
- return 'pass'
102
- } catch { return 'unknown' }
103
- }
1
+ # Shared user-side Chrome/Edge launcher and read-only process policy. No elevation or vault access.
2
+ function Split-UChanWindowsCommandLine([string]$CommandLine) {
3
+ if (-not ('UChanWindowsCommandLine' -as [type])) {
4
+ Add-Type -TypeDefinition @'
5
+ using System;
6
+ using System.Runtime.InteropServices;
7
+ public static class UChanWindowsCommandLine {
8
+ [DllImport("shell32.dll", SetLastError=true, CharSet=CharSet.Unicode)] static extern IntPtr CommandLineToArgvW(string value, out int count);
9
+ [DllImport("kernel32.dll")] static extern IntPtr LocalFree(IntPtr value);
10
+ public static string[] Split(string value) {
11
+ int count; IntPtr memory=CommandLineToArgvW(value,out count);
12
+ if(memory==IntPtr.Zero) throw new InvalidOperationException("Command line is unavailable");
13
+ try { string[] result=new string[count]; for(int i=0;i<count;i++)result[i]=Marshal.PtrToStringUni(Marshal.ReadIntPtr(memory,i*IntPtr.Size)); return result; }
14
+ finally { LocalFree(memory); }
15
+ }
16
+ }
17
+ '@
18
+ }
19
+ return [UChanWindowsCommandLine]::Split($CommandLine)
20
+ }
21
+ function Test-UChanBrowserProcess($Observation, [string]$Executable, [string]$Profile, [string]$OwnerSid, [int]$Port) {
22
+ if (-not $Observation.ExecutablePath -or -not $Observation.OwnerSid -or -not $Observation.CommandLine -or -not $Observation.Addresses) { return 'unknown' }
23
+ if ($Observation.ExecutablePath -ine $Executable -or $Observation.OwnerSid -ne $OwnerSid) { return 'fail' }
24
+ foreach ($address in $Observation.Addresses) { if ($address -notin @('127.0.0.1','::1')) { return 'fail' } }
25
+ try { $tokens = @(Split-UChanWindowsCommandLine $Observation.CommandLine) } catch { return 'unknown' }
26
+ if ($tokens.Count -lt 1 -or $tokens[0] -ine $Executable) { return 'fail' }
27
+ $profiles = @($tokens | Where-Object { $_ -imatch '^--user-data-dir(?:=|$)' })
28
+ if ($profiles.Count -ne 1 -or $profiles[0] -inotmatch '^--user-data-dir=.+$') { return 'fail' }
29
+ $actual = $profiles[0].Substring('--user-data-dir='.Length)
30
+ try { if ([IO.Path]::GetFullPath($actual).TrimEnd('\') -ine [IO.Path]::GetFullPath($Profile).TrimEnd('\')) { return 'fail' } } catch { return 'fail' }
31
+ $ports = @($tokens | Where-Object { $_ -imatch '^--remote-debugging-port(?:=|$)' })
32
+ if ($ports.Count -ne 1 -or $ports[0] -inotmatch '^--remote-debugging-port=[0-9]+$' -or $ports[0].Substring('--remote-debugging-port='.Length) -ne [string]$Port) { return 'fail' }
33
+ return 'pass'
34
+ }
35
+
36
+ function Get-UChanBrowserConfiguration([string]$Browser, [string]$HomeOverride, [string]$OwnerOverride, [string]$LocalOverride) {
37
+ $userHome = if ($HomeOverride) { $HomeOverride } elseif ($env:U_CHAN_HOME) { $env:U_CHAN_HOME } else { Join-Path $env:APPDATA 'u-chan-secrets' }
38
+ if (-not $Browser) { $Browser = $env:U_CHAN_BROWSER }
39
+ if (-not $Browser) {
40
+ $choice = Join-Path $userHome 'browser.json'
41
+ if (Test-Path -LiteralPath $choice) {
42
+ $item = Get-Item -LiteralPath $choice -Force
43
+ if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'Browser preference path is redirected.' }
44
+ $saved = Get-Content -LiteralPath $choice -Raw | ConvertFrom-Json
45
+ if (@($saved.PSObject.Properties).Count -ne 1) { throw 'Invalid browser preference.' }
46
+ $Browser = $saved.browser
47
+ if (-not $Browser) { throw 'Invalid browser preference.' }
48
+ } else { $Browser = 'chrome' }
49
+ }
50
+ if ($Browser -cnotin @('chrome','edge')) { throw 'Choose chrome or edge.' }
51
+ $profile = Join-Path $userHome ($Browser + '-profile')
52
+ $check = [IO.Path]::GetFullPath($profile)
53
+ while ($check) {
54
+ if ((Test-Path -LiteralPath $check) -and ((Get-Item -LiteralPath $check -Force).Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'Dedicated browser profile is redirected.' }
55
+ $check = [IO.Path]::GetDirectoryName($check)
56
+ }
57
+ $cdp = [Uri]$(if ($env:U_CHAN_CDP) { $env:U_CHAN_CDP } else { 'http://127.0.0.1:9222' })
58
+ 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) { throw 'Use an HTTP loopback CDP origin without credentials, path, query or fragment.' }
59
+ $suffix = if ($Browser -eq 'edge') { 'Microsoft\Edge\Application\msedge.exe' } else { 'Google\Chrome\Application\chrome.exe' }
60
+ $local = if ($LocalOverride) { $LocalOverride } else { $env:LOCALAPPDATA }
61
+ $candidates = @(foreach ($base in @($env:ProgramFiles, ${env:ProgramFiles(x86)}, $local)) { if ($base) { Join-Path $base $suffix } })
62
+ $executables = @($candidates | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf })
63
+ $owner = if ($OwnerOverride) { $OwnerOverride } else { [Security.Principal.WindowsIdentity]::GetCurrent().User.Value }
64
+ return @{ Browser=$Browser; Profile=$profile; Endpoint=$cdp; Executables=$executables; OwnerSid=$owner }
65
+ }
66
+
67
+ # Privileged checks derive the desktop home from the installed owner's SID, never SYSTEM's APPDATA or user preference files.
68
+ function Get-UChanOwnerBrowserConfiguration([string]$OwnerSid, [string]$Browser) {
69
+ if ($OwnerSid -notmatch '^S-1-5-21-(?:[0-9]+-){3}[0-9]+$') { throw 'Installed desktop owner is invalid.' }
70
+ $profile = (Get-ItemProperty -LiteralPath ('HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\'+$OwnerSid) -ErrorAction Stop).ProfileImagePath
71
+ if (-not $profile) { throw 'Installed desktop owner profile is unavailable.' }
72
+ $profile = [Environment]::ExpandEnvironmentVariables($profile)
73
+ $roaming = Join-Path $profile 'AppData\Roaming'
74
+ $local = Join-Path $profile 'AppData\Local'
75
+ $shell = Get-Item -LiteralPath ('Registry::HKEY_USERS\'+$OwnerSid+'\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders') -ErrorAction SilentlyContinue
76
+ if ($shell) {
77
+ # Read REG_EXPAND_SZ literally; Registry providers otherwise expand USERPROFILE as SYSTEM before substitution.
78
+ try {
79
+ $rawRoaming = $shell.GetValue('AppData', $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
80
+ $rawLocal = $shell.GetValue('Local AppData', $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
81
+ } finally { $shell.Close() }
82
+ if ($rawRoaming) { $roaming = [Environment]::ExpandEnvironmentVariables(($rawRoaming -ireplace '%USERPROFILE%', $profile)) }
83
+ if ($rawLocal) { $local = [Environment]::ExpandEnvironmentVariables(($rawLocal -ireplace '%USERPROFILE%', $profile)) }
84
+ }
85
+ $desktopHome = if ($env:U_CHAN_DESKTOP_HOME) { $env:U_CHAN_DESKTOP_HOME } else { Join-Path $roaming 'u-chan-secrets' }
86
+ return Get-UChanBrowserConfiguration $Browser $desktopHome $OwnerSid $local
87
+ }
88
+
89
+ function Get-UChanBrowserState($Configuration) {
90
+ try {
91
+ $listeners = @(Get-NetTCPConnection -State Listen -ErrorAction Stop | Where-Object LocalPort -eq $Configuration.Endpoint.Port)
92
+ if ($listeners.Count -eq 0) { return 'missing' }
93
+ $owners = @($listeners.OwningProcess | Select-Object -Unique)
94
+ if ($owners.Count -ne 1) { return 'fail' }
95
+ $process = Get-CimInstance Win32_Process -Filter ("ProcessId=" + $owners[0]) -ErrorAction Stop
96
+ if (-not $process) { return 'unknown' }
97
+ $owner = Invoke-CimMethod -InputObject $process -MethodName GetOwnerSid -ErrorAction Stop
98
+ $observation = @{ ExecutablePath=$process.ExecutablePath; OwnerSid=$owner.Sid; CommandLine=$process.CommandLine; Addresses=@($listeners.LocalAddress) }
99
+ if (-not $process.ExecutablePath) { return 'unknown' }
100
+ if ($process.ExecutablePath -notin $Configuration.Executables) { return 'fail' }
101
+ $policy = Test-UChanBrowserProcess $observation $process.ExecutablePath $Configuration.Profile $Configuration.OwnerSid $Configuration.Endpoint.Port
102
+ if ($policy -ne 'pass') { return $policy }
103
+ $version = Invoke-RestMethod -Uri ([Uri]::new($Configuration.Endpoint,'/json/version')) -TimeoutSec 2 -ErrorAction Stop
104
+ $socket = [Uri]$version.webSocketDebuggerUrl
105
+ if ($socket.Scheme -ne 'ws' -or $socket.Host -notin @('127.0.0.1','localhost','[::1]','::1') -or $socket.Port -ne $Configuration.Endpoint.Port -or $socket.UserInfo) { return 'fail' }
106
+ $edge = $version.'User-Agent' -match '(?:Edg|Edge)/'
107
+ if (($Configuration.Browser -eq 'edge') -ne $edge) { return 'fail' }
108
+ return 'pass'
109
+ } catch { return 'unknown' }
110
+ }
@@ -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
+