u-chan-secrets 4.0.0 → 5.0.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.
- package/README.md +30 -6
- package/dist/cli.js +30 -23
- package/dist/client/browser-choice.js +49 -0
- package/dist/client/codex-setup.js +11 -4
- package/dist/client/setup.js +17 -5
- package/dist/core/browser.js +90 -22
- package/dist/core/config.js +13 -0
- package/dist/core/doctor.js +26 -5
- package/dist/core/login-filler.js +25 -25
- package/dist/core/login-success.js +24 -0
- package/dist/core/service.js +6 -4
- package/dist/core/types.js +1 -1
- package/dist/index.js +2 -2
- package/dist/ipc/client.js +20 -1
- package/dist/ipc/server.js +1 -1
- package/dist/mcp/server.js +4 -4
- package/dist/platform/darwin/manage-script.js +30 -30
- package/dist/platform/windows/desktop-browser.js +14 -0
- package/dist/platform/windows/doctor.js +1 -1
- package/dist/service/control.js +1 -1
- package/dist/service/doctor.js +5 -4
- package/dist/service/isolated-login.js +33 -10
- package/dist/service/main.js +9 -3
- package/dist/service/manage.js +3 -2
- package/dist/service/private-network.js +97 -0
- package/package.json +1 -1
- package/scripts/debug-install.bat +69 -69
- package/scripts/desktop-browser.ps1 +103 -0
- package/scripts/login-bw.bat +21 -21
- package/scripts/migrate.bat +4 -4
- package/scripts/start-browser.ps1 +33 -0
- package/scripts/start-chrome.ps1 +4 -38
- package/scripts/verify-windows-adversarial.mjs +11 -1
- package/scripts/windows-security/browser-worker.mjs +50 -10
- package/scripts/windows-security/desktop-matrix.mjs +54 -0
package/dist/service/manage.js
CHANGED
|
@@ -26,12 +26,13 @@ export class ManagementService {
|
|
|
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
28
|
lines.push('Service version: ' + (safeVersion(checks?.serviceVersion) ?? 'unknown'), 'Service browser: ' + (['pass', 'fail'].includes(checks?.browser ?? '') ? checks?.browser : 'unknown'));
|
|
29
|
+
lines.push('Observed desktop browser: ' + (checks?.browserKind === 'edge' ? 'Edge' : checks?.browserKind === 'chrome' ? 'Chrome' : 'unknown'));
|
|
29
30
|
if (status === 'locked')
|
|
30
31
|
lines.push('Next step: use Unlock to approve a protected vault unlock.');
|
|
31
32
|
if (checks?.assets !== 'pass' || checks?.permissions !== 'pass')
|
|
32
33
|
lines.push('Next step: run u-chan doctor; repair from a trusted administrator terminal.');
|
|
33
34
|
if (checks?.browser !== 'pass')
|
|
34
|
-
lines.push('Next step: start dedicated
|
|
35
|
+
lines.push('Next step: start the selected dedicated browser and run u-chan setup.');
|
|
35
36
|
for (const e of grants) {
|
|
36
37
|
let label = e.expiresAt ? 'until ' + e.expiresAt : e.remainingUses !== undefined ? e.remainingUses + ' attempts remaining' : 'permanent';
|
|
37
38
|
try {
|
|
@@ -42,7 +43,7 @@ export class ManagementService {
|
|
|
42
43
|
}
|
|
43
44
|
lines.push(clean(e.domain) + ' — ' + label);
|
|
44
45
|
}
|
|
45
|
-
lines.push('Revocation stops future logins. It does not log out existing
|
|
46
|
+
lines.push('Revocation stops future logins. It does not log out existing browser sessions.', 'Revocation waits for any earlier login to finish.', 'Diagnostics: assets ' + (checks?.assets ?? 'unknown') + '; permissions ' + (checks?.permissions ?? 'unknown'), 'Recent activity:');
|
|
46
47
|
for (const e of events.slice(-10)) {
|
|
47
48
|
const ts = Number.isFinite(Date.parse(e.ts)) ? new Date(e.ts).toISOString() : 'unknown time';
|
|
48
49
|
const action = ['login', 'request_allow', 'revoke', 'unlock', 'lock', 'mode', 'import', 'update'].includes(e.action) ? e.action : 'event';
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createServer, request as httpRequest } from 'node:http';
|
|
2
|
+
import { connect } from 'node:net';
|
|
3
|
+
/** Per-context HTTP/CONNECT proxy: every redirect and child target must obtain a separately checked destination. */
|
|
4
|
+
export async function createPrivateNetwork(hostCheck, signal, loopbackHttpOriginForTesting) {
|
|
5
|
+
let blocked = false, closing;
|
|
6
|
+
const testOrigin = loopbackHttpOriginForTesting ? new URL(loopbackHttpOriginForTesting) : undefined;
|
|
7
|
+
if (testOrigin && (testOrigin.protocol !== 'http:' || testOrigin.hostname !== '127.0.0.1' || testOrigin.username || testOrigin.password))
|
|
8
|
+
throw new Error('Invalid loopback fixture origin');
|
|
9
|
+
const sockets = new Set();
|
|
10
|
+
const allowed = (url) => {
|
|
11
|
+
try {
|
|
12
|
+
return !signal.aborted && !url.username && !url.password && hostCheck(url.href);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const track = (socket) => { sockets.add(socket); socket.once('close', () => sockets.delete(socket)); socket.on('error', () => { }); socket.setTimeout(60000, () => socket.destroy()); };
|
|
19
|
+
const server = createServer((incoming, response) => {
|
|
20
|
+
let url;
|
|
21
|
+
try {
|
|
22
|
+
url = new URL(incoming.url ?? '');
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
blocked = true;
|
|
26
|
+
response.writeHead(403);
|
|
27
|
+
response.end();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (!testOrigin || url.protocol !== 'http:' || url.origin !== testOrigin.origin || !allowed(url)) {
|
|
31
|
+
blocked = true;
|
|
32
|
+
response.writeHead(403);
|
|
33
|
+
response.end();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const headers = { ...incoming.headers, host: testOrigin.host };
|
|
37
|
+
delete headers['proxy-authorization'];
|
|
38
|
+
delete headers['proxy-connection'];
|
|
39
|
+
// Plain HTTP exists only for explicitly injected loopback fixtures; the production path uses approved HTTPS CONNECT.
|
|
40
|
+
const outgoing = httpRequest({ hostname: '127.0.0.1', port: Number(testOrigin.port || 80), path: url.pathname + url.search, method: incoming.method, headers, agent: false }, upstream => {
|
|
41
|
+
response.writeHead(upstream.statusCode ?? 502, upstream.headers);
|
|
42
|
+
upstream.pipe(response);
|
|
43
|
+
});
|
|
44
|
+
outgoing.on('socket', track);
|
|
45
|
+
outgoing.on('error', () => { if (!response.headersSent)
|
|
46
|
+
response.writeHead(502); response.end(); });
|
|
47
|
+
incoming.on('error', () => outgoing.destroy());
|
|
48
|
+
incoming.pipe(outgoing); // No redirect following or credential buffering in the proxy.
|
|
49
|
+
});
|
|
50
|
+
server.on('connection', socket => { track(socket); if (sockets.size > 128)
|
|
51
|
+
socket.destroy(); });
|
|
52
|
+
server.on('clientError', (_error, socket) => socket.destroy());
|
|
53
|
+
server.on('upgrade', (_request, socket) => { blocked = true; socket.destroy(); }); // Production permits WSS via checked CONNECT only.
|
|
54
|
+
server.on('connect', (incoming, connection, head) => {
|
|
55
|
+
const client = connection;
|
|
56
|
+
let url;
|
|
57
|
+
try {
|
|
58
|
+
url = new URL('https://' + incoming.url);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
blocked = true;
|
|
62
|
+
client.destroy();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (url.pathname !== '/' || url.search || url.hash || !allowed(url)) {
|
|
66
|
+
blocked = true;
|
|
67
|
+
client.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const upstream = connect({ host: url.hostname.replace(/^\[|\]$/g, ''), port: Number(url.port || 443) });
|
|
71
|
+
track(upstream);
|
|
72
|
+
upstream.once('connect', () => {
|
|
73
|
+
if (signal.aborted || client.destroyed) {
|
|
74
|
+
upstream.destroy();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
client.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
78
|
+
if (head.length)
|
|
79
|
+
upstream.write(head);
|
|
80
|
+
upstream.pipe(client);
|
|
81
|
+
client.pipe(upstream);
|
|
82
|
+
});
|
|
83
|
+
upstream.once('error', () => client.destroy());
|
|
84
|
+
client.once('close', () => upstream.destroy());
|
|
85
|
+
});
|
|
86
|
+
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', () => { server.off('error', reject); resolve(); }); });
|
|
87
|
+
const close = () => closing ??= new Promise(resolve => { signal.removeEventListener('abort', abort); for (const socket of sockets)
|
|
88
|
+
socket.destroy(); server.close(() => resolve()); });
|
|
89
|
+
const abort = () => { void close(); };
|
|
90
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
91
|
+
if (signal.aborted)
|
|
92
|
+
await close();
|
|
93
|
+
const address = server.address();
|
|
94
|
+
if (!address || typeof address === 'string')
|
|
95
|
+
throw new Error('Private network unavailable');
|
|
96
|
+
return { proxy: { server: `http://127.0.0.1:${address.port}`, bypass: '<-loopback>' }, wasBlocked: () => blocked, close };
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -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
|
|
@@ -0,0 +1,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-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
|
+
}
|
package/scripts/login-bw.bat
CHANGED
|
@@ -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
|
package/scripts/migrate.bat
CHANGED
|
@@ -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
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
param([ValidateSet('chrome','edge')][string]$Browser, [switch]$Background, [switch]$CheckOnly, [string]$DesktopOwnerSid)
|
|
2
|
+
$ErrorActionPreference='Stop'
|
|
3
|
+
. (Join-Path $PSScriptRoot 'desktop-browser.ps1')
|
|
4
|
+
if ($DesktopOwnerSid) {
|
|
5
|
+
if (-not $CheckOnly) { throw 'An owner-specific inspection cannot launch a desktop browser.' }
|
|
6
|
+
$observed = 'unknown'
|
|
7
|
+
$kinds = if ($Browser) { @($Browser) } else { @('chrome','edge') }
|
|
8
|
+
foreach ($kind in $kinds) {
|
|
9
|
+
$configuration = Get-UChanOwnerBrowserConfiguration $DesktopOwnerSid $kind
|
|
10
|
+
$candidate = Get-UChanBrowserState $configuration
|
|
11
|
+
if ($candidate -eq 'pass') { 'pass'; return }
|
|
12
|
+
if ($candidate -eq 'fail' -or $candidate -eq 'missing') { $observed = 'fail' }
|
|
13
|
+
}
|
|
14
|
+
$observed; return
|
|
15
|
+
}
|
|
16
|
+
$configuration=Get-UChanBrowserConfiguration $Browser
|
|
17
|
+
$state=Get-UChanBrowserState $configuration
|
|
18
|
+
if ($CheckOnly) { if ($state -eq 'missing') { 'fail' } else { $state }; return }
|
|
19
|
+
if ($state -eq 'pass') { Write-Host ($configuration.Browser + ' dedicated browser is ready.'); return }
|
|
20
|
+
if ($state -ne 'missing') { throw 'The browser endpoint is occupied, mismatched, or unverifiable. Close only the previous dedicated U-Chan browser before switching; keep personal windows open.' }
|
|
21
|
+
if ($configuration.Executables.Count -eq 0) { throw ($configuration.Browser + ' is not installed. Install it or explicitly select the other supported browser.') }
|
|
22
|
+
New-Item -ItemType Directory -Force -Path $configuration.Profile | Out-Null
|
|
23
|
+
$arguments=@(('--remote-debugging-port='+$configuration.Endpoint.Port), '--remote-debugging-address=127.0.0.1', ('--user-data-dir="'+$configuration.Profile+'"'), '--no-first-run')
|
|
24
|
+
if ($Background) { $arguments += '--no-startup-window' }
|
|
25
|
+
Start-Process -FilePath $configuration.Executables[0] -ArgumentList $arguments -WindowStyle Hidden
|
|
26
|
+
$deadline=[DateTime]::UtcNow.AddSeconds(15)
|
|
27
|
+
do {
|
|
28
|
+
Start-Sleep -Milliseconds 300
|
|
29
|
+
$state=Get-UChanBrowserState $configuration
|
|
30
|
+
if ($state -eq 'pass') { Write-Host ($configuration.Browser + ' dedicated browser started.'); return }
|
|
31
|
+
if ($state -eq 'fail') { break }
|
|
32
|
+
} while ([DateTime]::UtcNow -lt $deadline)
|
|
33
|
+
throw 'Selected dedicated browser did not become verifiably ready. Check browser policy and run setup again.'
|
package/scripts/start-chrome.ps1
CHANGED
|
@@ -1,39 +1,5 @@
|
|
|
1
|
-
param([switch]$Background)
|
|
1
|
+
param([switch]$Background, [switch]$CheckOnly)
|
|
2
2
|
$ErrorActionPreference = 'Stop'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
}
|
|
9
|
-
$port = $cdp.Port
|
|
10
|
-
$bindHost = $cdp.Host
|
|
11
|
-
|
|
12
|
-
# Already listening? Then do nothing (short TCP probe; Test-NetConnection is slow).
|
|
13
|
-
$client = New-Object System.Net.Sockets.TcpClient
|
|
14
|
-
try {
|
|
15
|
-
$iar = $client.BeginConnect($bindHost, $port, $null, $null)
|
|
16
|
-
if ($iar.AsyncWaitHandle.WaitOne(500) -and $client.Connected) {
|
|
17
|
-
Write-Host "Port $port already open; Chrome is already running with remote debugging."
|
|
18
|
-
return
|
|
19
|
-
}
|
|
20
|
-
} catch { } finally { $client.Close() }
|
|
21
|
-
|
|
22
|
-
$candidates = @(
|
|
23
|
-
(Join-Path $env:ProgramFiles 'Google\Chrome\Application\chrome.exe'),
|
|
24
|
-
(Join-Path ${env:ProgramFiles(x86)} 'Google\Chrome\Application\chrome.exe'),
|
|
25
|
-
(Join-Path $env:LOCALAPPDATA 'Google\Chrome\Application\chrome.exe')
|
|
26
|
-
)
|
|
27
|
-
$chrome = $candidates | Where-Object { $_ -and (Test-Path $_) } | Select-Object -First 1
|
|
28
|
-
if (-not $chrome) { throw 'chrome.exe not found' }
|
|
29
|
-
|
|
30
|
-
New-Item -ItemType Directory -Force -Path $profileDir | Out-Null
|
|
31
|
-
$chromeArgs = @(
|
|
32
|
-
"--remote-debugging-port=$port",
|
|
33
|
-
"--remote-debugging-address=$bindHost",
|
|
34
|
-
"--user-data-dir=`"$profileDir`"",
|
|
35
|
-
'--no-first-run'
|
|
36
|
-
)
|
|
37
|
-
if ($Background) { $chromeArgs += '--no-startup-window' }
|
|
38
|
-
Start-Process -FilePath $chrome -ArgumentList $chromeArgs -WindowStyle Hidden
|
|
39
|
-
Write-Host "Chrome started (profile: $profileDir, CDP: http://${bindHost}:$port)"
|
|
3
|
+
# Compatibility entry point: dedicated profile, never the default Chrome profile.
|
|
4
|
+
# Already listening? Validate browser identity, owner, profile and CDP before reusing it.
|
|
5
|
+
& (Join-Path $PSScriptRoot 'start-browser.ps1') -Browser chrome -Background:$Background -CheckOnly:$CheckOnly
|
|
@@ -121,16 +121,26 @@ if (command === '--prepare') {
|
|
|
121
121
|
const publicDirectory = await createChannel(runIdArgument ?? randomUUID())
|
|
122
122
|
const home = path.join(process.env.ProgramData ?? 'C:\\ProgramData', 'u-chan-secrets')
|
|
123
123
|
const root = path.join(home, '#0 backup', 'browser-probe-' + randomUUID()), task = 'UChanBrowserProbe-' + randomUUID()
|
|
124
|
+
const program = process.argv[4] ? path.resolve(process.argv[4]) : path.join(home, 'program'), endpoint = process.argv[5] ?? 'http://127.0.0.1:9222', matrix = process.argv[6] === 'matrix'
|
|
125
|
+
if (program !== path.join(home, 'program') && !program.startsWith(path.resolve(home, '#0 backup') + path.sep)) throw new Error('Candidate must be in protected backup storage')
|
|
126
|
+
const { safeCdpOrigin } = await import('../dist/core/doctor.js')
|
|
127
|
+
if (!safeCdpOrigin(endpoint)) throw new Error('Unsafe fixture endpoint')
|
|
128
|
+
const desktopHome = matrix ? path.resolve(process.argv[7] ?? '') : undefined
|
|
129
|
+
if (matrix && (!desktopHome.startsWith(path.resolve(os.tmpdir(), 'u-chan-v5-matrix-' + runIdArgument) + path.sep) || !['chrome','edge'].includes(path.basename(desktopHome)))) throw new Error('Unsafe disposable desktop home')
|
|
124
130
|
let registered = false
|
|
125
131
|
try {
|
|
126
132
|
await fs.mkdir(root); await setAdminOnlyAcl(root)
|
|
133
|
+
if (matrix) {
|
|
134
|
+
const settings = JSON.parse(await fs.readFile(path.join(home, 'settings.json'), 'utf8'))
|
|
135
|
+
await fs.writeFile(path.join(root, 'matrix-context.json'), JSON.stringify({ desktopHome, ownerSid: settings.authorizedSid }))
|
|
136
|
+
}
|
|
127
137
|
const runner = path.join(root, 'runner.mjs')
|
|
128
138
|
await fs.copyFile(new URL('./windows-security/browser-worker.mjs', import.meta.url), runner)
|
|
129
139
|
await fs.copyFile(new URL('./windows-security/module-audit.ps1', import.meta.url), path.join(root, 'module-audit.ps1'))
|
|
130
140
|
await fs.copyFile(new URL('./windows-security/result-policy.mjs', import.meta.url), path.join(root, 'result-policy.mjs'))
|
|
131
141
|
await setAdminOnlyAcl(root)
|
|
132
142
|
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`)
|
|
143
|
+
await ps(`$ErrorActionPreference='Stop';$a=New-ScheduledTaskAction -Execute ${quote(path.join(home, 'bin', 'node.exe'))} -Argument ${quote('"' + runner + '" "' + root + '" "' + transcripts + '" "' + program + '" "' + endpoint + '"' + (matrix ? ' matrix' : ''))} -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
144
|
registered = true; await ps(`Start-ScheduledTask -TaskName ${quote(task)}`)
|
|
135
145
|
const output = path.join(root, 'browser-result.json'), deadline = Date.now() + 90000
|
|
136
146
|
while (Date.now() < deadline && !await fs.stat(output).catch(() => null)) await sleep(200)
|