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.
- package/README.md +233 -91
- package/dist/cli.js +115 -19
- package/dist/client/claude-setup.js +97 -0
- package/dist/client/codex-setup.js +6 -5
- package/dist/client/discord.js +67 -0
- package/dist/client/github.js +28 -0
- package/dist/client/setup.js +40 -6
- package/dist/client/update-package.js +166 -0
- package/dist/client/update.js +138 -0
- package/dist/client/windows-command.js +54 -0
- package/dist/core/browser.js +0 -5
- package/dist/core/config.js +1 -0
- package/dist/core/doctor.js +5 -1
- package/dist/core/vault.js +110 -5
- package/dist/discord/http.js +84 -0
- package/dist/discord/policy.js +47 -0
- package/dist/discord/service.js +349 -0
- package/dist/discord/store.js +85 -0
- package/dist/github/http.js +93 -0
- package/dist/github/policy.js +24 -0
- package/dist/github/service.js +185 -0
- package/dist/github/store.js +54 -0
- package/dist/index.js +1 -1
- package/dist/ipc/client.js +74 -6
- package/dist/ipc/server.js +38 -3
- package/dist/mcp/server.js +25 -2
- package/dist/platform/windows/assets/prompt-src.js +160 -38
- package/dist/platform/windows/doctor.js +2 -2
- package/dist/platform/windows/install.js +4 -0
- package/dist/service/control.js +47 -5
- package/dist/service/doctor.js +3 -0
- package/dist/service/main.js +34 -17
- package/dist/service/manage.js +20 -16
- package/dist/service/state.js +28 -0
- package/dist/service/update/coordinator.js +171 -0
- package/dist/service/update/files.js +47 -0
- package/dist/service/update/gate.js +21 -0
- package/dist/service/update/journal.js +157 -0
- package/dist/service/update/native.js +50 -0
- package/dist/service/update/network.js +28 -0
- package/dist/service/update/protocol.js +28 -0
- package/dist/service/update/release.js +102 -0
- package/dist/service/update/worker-main.js +116 -0
- package/dist/service/update/worker.js +58 -0
- package/package.json +1 -1
- package/scripts/debug-install.bat +69 -69
- package/scripts/desktop-browser.ps1 +110 -103
- package/scripts/login-bw.bat +21 -21
- package/scripts/migrate.bat +4 -4
- package/scripts/start-browser.ps1 +33 -33
- 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,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-
|
|
76
|
-
if ($shell
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
$
|
|
92
|
-
if (-
|
|
93
|
-
|
|
94
|
-
$
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
$
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
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
|
+
}
|
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
|
+
|