u-chan-secrets 3.0.2 → 4.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 +38 -3
- package/dist/cli.js +79 -2
- package/dist/client/allow.js +64 -0
- package/dist/client/codex-setup.js +140 -0
- package/dist/client/setup.js +67 -0
- package/dist/core/doctor.js +45 -7
- package/dist/index.js +2 -1
- package/dist/mcp/server.js +2 -2
- package/dist/platform/darwin/manage-script.js +30 -30
- package/dist/platform/windows/assets/auth-src.js +190 -190
- package/dist/platform/windows/assets/service-src.js +147 -147
- package/dist/platform/windows/doctor.js +2 -0
- package/dist/service/control.js +1 -1
- package/dist/service/doctor.js +6 -4
- package/dist/service/main.js +5 -4
- package/dist/service/manage.js +9 -2
- package/dist/service/update/verifier.js +13 -10
- package/package.json +1 -1
- package/scripts/start-chrome.ps1 +8 -2
- package/scripts/verify-windows-adversarial.mjs +142 -0
- package/scripts/verify-windows-prompt-desktop.mjs +50 -50
- package/scripts/verify-windows-user-boundary.mjs +56 -0
- package/scripts/windows-security/attacker.cs +137 -0
- package/scripts/windows-security/browser-worker.mjs +125 -0
- package/scripts/windows-security/canary.cs +16 -0
- package/scripts/windows-security/module-audit.ps1 +36 -0
- package/scripts/windows-security/public-channel.ps1 +31 -0
- package/scripts/windows-security/result-policy.d.mts +2 -0
- package/scripts/windows-security/result-policy.mjs +9 -0
|
@@ -1,148 +1,148 @@
|
|
|
1
|
-
export const SERVICE_HOST_CSHARP = `using System;
|
|
2
|
-
using System.Diagnostics;
|
|
3
|
-
using System.ServiceProcess;
|
|
4
|
-
using System.Threading;
|
|
5
|
-
|
|
6
|
-
namespace UChan.Platform.Windows.Assets
|
|
7
|
-
{
|
|
8
|
-
public class UChanService : ServiceBase
|
|
9
|
-
{
|
|
10
|
-
private Process _process;
|
|
11
|
-
private readonly string[] _args;
|
|
12
|
-
private volatile bool _stopping;
|
|
13
|
-
private string _targetExe;
|
|
14
|
-
private string _targetArgs;
|
|
15
|
-
|
|
16
|
-
public UChanService(string[] args)
|
|
17
|
-
{
|
|
18
|
-
ServiceName = "UChanSecrets";
|
|
19
|
-
_args = args;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
protected override void OnStart(string[] args)
|
|
23
|
-
{
|
|
24
|
-
_stopping = false;
|
|
25
|
-
string[] effectiveArgs = (args != null && args.Length > 0) ? args : _args;
|
|
26
|
-
string targetExe = null;
|
|
27
|
-
string targetArgs = "";
|
|
28
|
-
|
|
29
|
-
if (effectiveArgs != null && effectiveArgs.Length > 0)
|
|
30
|
-
{
|
|
31
|
-
targetExe = effectiveArgs[0];
|
|
32
|
-
if (effectiveArgs.Length > 1)
|
|
33
|
-
{
|
|
34
|
-
string[] rest = new string[effectiveArgs.Length - 1];
|
|
35
|
-
Array.Copy(effectiveArgs, 1, rest, 0, rest.Length);
|
|
36
|
-
targetArgs = string.Join(" ", rest);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
else
|
|
40
|
-
{
|
|
41
|
-
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
|
42
|
-
string targetFile = System.IO.Path.Combine(baseDir, "service-target.txt");
|
|
43
|
-
if (System.IO.File.Exists(targetFile))
|
|
44
|
-
{
|
|
45
|
-
string[] lines = System.IO.File.ReadAllLines(targetFile);
|
|
46
|
-
if (lines.Length > 0)
|
|
47
|
-
{
|
|
48
|
-
targetExe = lines[0].Trim();
|
|
49
|
-
if (lines.Length > 1)
|
|
50
|
-
{
|
|
51
|
-
targetArgs = lines[1].Trim();
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (string.IsNullOrEmpty(targetExe))
|
|
58
|
-
{
|
|
59
|
-
throw new InvalidOperationException("No target executable specified for UChanSecrets service.");
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
_targetExe = targetExe;
|
|
63
|
-
_targetArgs = targetArgs;
|
|
64
|
-
StartChild();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
private void StartChild()
|
|
68
|
-
{
|
|
69
|
-
if (_stopping) return;
|
|
70
|
-
ProcessStartInfo psi = new ProcessStartInfo();
|
|
71
|
-
psi.FileName = _targetExe;
|
|
72
|
-
psi.Arguments = _targetArgs;
|
|
73
|
-
psi.UseShellExecute = false;
|
|
74
|
-
psi.CreateNoWindow = true;
|
|
75
|
-
|
|
76
|
-
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
|
77
|
-
|
|
78
|
-
string logFile = System.IO.Path.GetFullPath(System.IO.Path.Combine(baseDir, "..", "service.log"));
|
|
79
|
-
psi.RedirectStandardOutput = true;
|
|
80
|
-
psi.RedirectStandardError = true;
|
|
81
|
-
|
|
82
|
-
Process proc = new Process();
|
|
83
|
-
proc.StartInfo = psi;
|
|
84
|
-
proc.EnableRaisingEvents = true;
|
|
85
|
-
proc.OutputDataReceived += (s, e) => {
|
|
86
|
-
if (!string.IsNullOrEmpty(e.Data)) {
|
|
87
|
-
try { System.IO.File.AppendAllText(logFile, string.Format("[{0:s}] {1}", DateTime.Now, e.Data) + Environment.NewLine); } catch {}
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
proc.ErrorDataReceived += (s, e) => {
|
|
91
|
-
if (!string.IsNullOrEmpty(e.Data)) {
|
|
92
|
-
try { System.IO.File.AppendAllText(logFile, string.Format("[{0:s}] [ERR] {1}", DateTime.Now, e.Data) + Environment.NewLine); } catch {}
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
proc.Exited += OnChildExited;
|
|
96
|
-
proc.Start();
|
|
97
|
-
proc.BeginOutputReadLine();
|
|
98
|
-
proc.BeginErrorReadLine();
|
|
99
|
-
_process = proc;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
private void OnChildExited(object sender, EventArgs e)
|
|
103
|
-
{
|
|
104
|
-
if (_stopping) return;
|
|
105
|
-
Thread.Sleep(500);
|
|
106
|
-
if (_stopping) return;
|
|
107
|
-
try
|
|
108
|
-
{
|
|
109
|
-
StartChild();
|
|
110
|
-
}
|
|
111
|
-
catch
|
|
112
|
-
{
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
protected override void OnStop()
|
|
117
|
-
{
|
|
118
|
-
_stopping = true;
|
|
119
|
-
if (_process != null && !_process.HasExited)
|
|
120
|
-
{
|
|
121
|
-
try
|
|
122
|
-
{
|
|
123
|
-
Process killer = Process.Start("taskkill.exe", string.Format("/F /T /PID {0}", _process.Id));
|
|
124
|
-
if (killer != null)
|
|
125
|
-
{
|
|
126
|
-
killer.WaitForExit(5000);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
catch
|
|
130
|
-
{
|
|
131
|
-
try
|
|
132
|
-
{
|
|
133
|
-
_process.Kill();
|
|
134
|
-
}
|
|
135
|
-
catch
|
|
136
|
-
{
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
public static void Main(string[] args)
|
|
143
|
-
{
|
|
144
|
-
ServiceBase.Run(new UChanService(args));
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
1
|
+
export const SERVICE_HOST_CSHARP = `using System;
|
|
2
|
+
using System.Diagnostics;
|
|
3
|
+
using System.ServiceProcess;
|
|
4
|
+
using System.Threading;
|
|
5
|
+
|
|
6
|
+
namespace UChan.Platform.Windows.Assets
|
|
7
|
+
{
|
|
8
|
+
public class UChanService : ServiceBase
|
|
9
|
+
{
|
|
10
|
+
private Process _process;
|
|
11
|
+
private readonly string[] _args;
|
|
12
|
+
private volatile bool _stopping;
|
|
13
|
+
private string _targetExe;
|
|
14
|
+
private string _targetArgs;
|
|
15
|
+
|
|
16
|
+
public UChanService(string[] args)
|
|
17
|
+
{
|
|
18
|
+
ServiceName = "UChanSecrets";
|
|
19
|
+
_args = args;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
protected override void OnStart(string[] args)
|
|
23
|
+
{
|
|
24
|
+
_stopping = false;
|
|
25
|
+
string[] effectiveArgs = (args != null && args.Length > 0) ? args : _args;
|
|
26
|
+
string targetExe = null;
|
|
27
|
+
string targetArgs = "";
|
|
28
|
+
|
|
29
|
+
if (effectiveArgs != null && effectiveArgs.Length > 0)
|
|
30
|
+
{
|
|
31
|
+
targetExe = effectiveArgs[0];
|
|
32
|
+
if (effectiveArgs.Length > 1)
|
|
33
|
+
{
|
|
34
|
+
string[] rest = new string[effectiveArgs.Length - 1];
|
|
35
|
+
Array.Copy(effectiveArgs, 1, rest, 0, rest.Length);
|
|
36
|
+
targetArgs = string.Join(" ", rest);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else
|
|
40
|
+
{
|
|
41
|
+
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
|
42
|
+
string targetFile = System.IO.Path.Combine(baseDir, "service-target.txt");
|
|
43
|
+
if (System.IO.File.Exists(targetFile))
|
|
44
|
+
{
|
|
45
|
+
string[] lines = System.IO.File.ReadAllLines(targetFile);
|
|
46
|
+
if (lines.Length > 0)
|
|
47
|
+
{
|
|
48
|
+
targetExe = lines[0].Trim();
|
|
49
|
+
if (lines.Length > 1)
|
|
50
|
+
{
|
|
51
|
+
targetArgs = lines[1].Trim();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (string.IsNullOrEmpty(targetExe))
|
|
58
|
+
{
|
|
59
|
+
throw new InvalidOperationException("No target executable specified for UChanSecrets service.");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_targetExe = targetExe;
|
|
63
|
+
_targetArgs = targetArgs;
|
|
64
|
+
StartChild();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private void StartChild()
|
|
68
|
+
{
|
|
69
|
+
if (_stopping) return;
|
|
70
|
+
ProcessStartInfo psi = new ProcessStartInfo();
|
|
71
|
+
psi.FileName = _targetExe;
|
|
72
|
+
psi.Arguments = _targetArgs;
|
|
73
|
+
psi.UseShellExecute = false;
|
|
74
|
+
psi.CreateNoWindow = true;
|
|
75
|
+
|
|
76
|
+
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
|
77
|
+
|
|
78
|
+
string logFile = System.IO.Path.GetFullPath(System.IO.Path.Combine(baseDir, "..", "service.log"));
|
|
79
|
+
psi.RedirectStandardOutput = true;
|
|
80
|
+
psi.RedirectStandardError = true;
|
|
81
|
+
|
|
82
|
+
Process proc = new Process();
|
|
83
|
+
proc.StartInfo = psi;
|
|
84
|
+
proc.EnableRaisingEvents = true;
|
|
85
|
+
proc.OutputDataReceived += (s, e) => {
|
|
86
|
+
if (!string.IsNullOrEmpty(e.Data)) {
|
|
87
|
+
try { System.IO.File.AppendAllText(logFile, string.Format("[{0:s}] {1}", DateTime.Now, e.Data) + Environment.NewLine); } catch {}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
proc.ErrorDataReceived += (s, e) => {
|
|
91
|
+
if (!string.IsNullOrEmpty(e.Data)) {
|
|
92
|
+
try { System.IO.File.AppendAllText(logFile, string.Format("[{0:s}] [ERR] {1}", DateTime.Now, e.Data) + Environment.NewLine); } catch {}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
proc.Exited += OnChildExited;
|
|
96
|
+
proc.Start();
|
|
97
|
+
proc.BeginOutputReadLine();
|
|
98
|
+
proc.BeginErrorReadLine();
|
|
99
|
+
_process = proc;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private void OnChildExited(object sender, EventArgs e)
|
|
103
|
+
{
|
|
104
|
+
if (_stopping) return;
|
|
105
|
+
Thread.Sleep(500);
|
|
106
|
+
if (_stopping) return;
|
|
107
|
+
try
|
|
108
|
+
{
|
|
109
|
+
StartChild();
|
|
110
|
+
}
|
|
111
|
+
catch
|
|
112
|
+
{
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
protected override void OnStop()
|
|
117
|
+
{
|
|
118
|
+
_stopping = true;
|
|
119
|
+
if (_process != null && !_process.HasExited)
|
|
120
|
+
{
|
|
121
|
+
try
|
|
122
|
+
{
|
|
123
|
+
Process killer = Process.Start("taskkill.exe", string.Format("/F /T /PID {0}", _process.Id));
|
|
124
|
+
if (killer != null)
|
|
125
|
+
{
|
|
126
|
+
killer.WaitForExit(5000);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch
|
|
130
|
+
{
|
|
131
|
+
try
|
|
132
|
+
{
|
|
133
|
+
_process.Kill();
|
|
134
|
+
}
|
|
135
|
+
catch
|
|
136
|
+
{
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public static void Main(string[] args)
|
|
143
|
+
{
|
|
144
|
+
ServiceBase.Run(new UChanService(args));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
148
|
`;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { commandState } from '../../core/doctor.js';
|
|
2
2
|
export const checkWindowsRegistration = () => commandState('sc.exe', ['query', 'u-chan-secrets'], s => /SERVICE_NAME:\s*u-chan-secrets/i.test(s));
|
|
3
|
+
export const checkWindowsUac = () => commandState('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command',
|
|
4
|
+
"$p=Get-ItemProperty -LiteralPath 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System'; if($p.EnableLUA -eq 1 -and $p.ConsentPromptBehaviorAdmin -eq 2 -and $p.PromptOnSecureDesktop -eq 1){'SAFE'}else{'UNSAFE'}"], s => s.trim() === 'SAFE');
|
|
3
5
|
export function checkWindowsPermissions(home) {
|
|
4
6
|
const encoded = Buffer.from(home, 'utf8').toString('base64');
|
|
5
7
|
const script = `$ErrorActionPreference='Stop'; $p=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encoded}')); $root=Get-Item -LiteralPath $p -Force; if($root.Attributes -band [IO.FileAttributes]::ReparsePoint){'UNSAFE';exit}; $items=@($root)+@(Get-ChildItem -LiteralPath $p -Force -Recurse); foreach($i in $items){if($i.Attributes -band [IO.FileAttributes]::ReparsePoint){'UNSAFE';exit}; $acl=$i.GetAccessControl(); $owner=$acl.GetOwner([Security.Principal.SecurityIdentifier]).Value; if($owner -notin @('S-1-5-18','S-1-5-32-544')){'UNSAFE';exit}; foreach($ace in $acl.GetAccessRules($true,$true,[Security.Principal.SecurityIdentifier])){if($ace.AccessControlType -eq 'Allow' -and $ace.IdentityReference.Value -notin @('S-1-5-18','S-1-5-32-544')){'UNSAFE';exit}} }; 'SAFE'`;
|
package/dist/service/control.js
CHANGED
|
@@ -14,7 +14,7 @@ export class ControlService {
|
|
|
14
14
|
this.grants = d.grants ?? new GrantCoordinator(d.allowlist);
|
|
15
15
|
}
|
|
16
16
|
async capabilities() {
|
|
17
|
-
return { ok: true, message: JSON.stringify({ protocol: 3, features: ['doctor', 'success_conditions', 'grants'] }) };
|
|
17
|
+
return { ok: true, message: JSON.stringify({ protocol: 3, features: ['doctor', 'success_conditions', 'grants'], serviceVersion: this.d.serviceVersion }) };
|
|
18
18
|
}
|
|
19
19
|
async diagnose() {
|
|
20
20
|
try {
|
package/dist/service/doctor.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { lstat } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { privateBrowserPath } from '../platform/runtime-assets.js';
|
|
4
|
-
import { bounded } from '../core/doctor.js';
|
|
4
|
+
import { bounded, chromeState, safeCdpOrigin } from '../core/doctor.js';
|
|
5
5
|
import { checkWindowsPermissions } from '../platform/windows/doctor.js';
|
|
6
6
|
import { checkDarwinPermissions } from '../platform/darwin/doctor.js';
|
|
7
|
-
export async function diagnoseService(home, vault, platform) {
|
|
7
|
+
export async function diagnoseService(home, vault, platform, cdpEndpoint) {
|
|
8
8
|
const paths = [join(home, 'program', 'dist', 'service', 'main.js'), join(home, 'program', 'node_modules'),
|
|
9
9
|
platform === 'win32' ? join(home, 'bin', 'node.exe') : process.execPath,
|
|
10
10
|
platform === 'win32' ? join(home, 'bin', 'bw.exe') : join(home, 'bin', 'bw')];
|
|
11
|
-
const [assets, permissions, status] = await Promise.all([
|
|
11
|
+
const [assets, permissions, status, browser] = await Promise.all([
|
|
12
12
|
bounded(async () => {
|
|
13
13
|
await privateBrowserPath(home);
|
|
14
14
|
for (const p of paths) {
|
|
@@ -20,6 +20,8 @@ export async function diagnoseService(home, vault, platform) {
|
|
|
20
20
|
}, 'unknown'),
|
|
21
21
|
bounded(() => platform === 'win32' ? checkWindowsPermissions(home) : checkDarwinPermissions(home), 'unknown', 20000),
|
|
22
22
|
bounded(() => vault.status(), 'unavailable'),
|
|
23
|
+
cdpEndpoint === undefined ? undefined : bounded(() => chromeState(cdpEndpoint), 'unknown'),
|
|
23
24
|
]);
|
|
24
|
-
return { assets, permissions, vault: status === 'unlocked' ? 'pass' : status === 'unavailable' ? 'unknown' : status
|
|
25
|
+
return { assets, permissions, vault: status === 'unlocked' ? 'pass' : status === 'unavailable' ? 'unknown' : status,
|
|
26
|
+
...(cdpEndpoint === undefined ? {} : { browser, browserEndpoint: safeCdpOrigin(cdpEndpoint) }) };
|
|
25
27
|
}
|
package/dist/service/main.js
CHANGED
|
@@ -24,6 +24,7 @@ import { GrantCoordinator } from './grants.js';
|
|
|
24
24
|
import { ensureV3State } from './state.js';
|
|
25
25
|
import { ManagementService } from './manage.js';
|
|
26
26
|
import { privateBrowserPath } from '../platform/runtime-assets.js';
|
|
27
|
+
import { safeVersion } from '../core/doctor.js';
|
|
27
28
|
export function prepareSocketEndpoint(endpoint, platform = process.platform) {
|
|
28
29
|
if (platform !== 'win32') {
|
|
29
30
|
const dir = dirname(endpoint);
|
|
@@ -64,9 +65,9 @@ export async function runService(platform = process.platform, env = process.env)
|
|
|
64
65
|
target: new CdpSessionTarget(new CdpBrowserProvider(loadConfig(env, platform).cdpEndpoint)),
|
|
65
66
|
});
|
|
66
67
|
const core = new CoreService({ vault, allowlist, grants, approver, audit, login, mode: () => approver.effectiveMode() });
|
|
67
|
-
let currentVersion = '
|
|
68
|
+
let currentVersion = 'unknown';
|
|
68
69
|
try {
|
|
69
|
-
currentVersion = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version;
|
|
70
|
+
currentVersion = safeVersion(JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version) ?? 'unknown';
|
|
70
71
|
}
|
|
71
72
|
catch { }
|
|
72
73
|
const verifier = new NpmPackageVerifier();
|
|
@@ -82,8 +83,8 @@ export async function runService(platform = process.platform, env = process.env)
|
|
|
82
83
|
stagingDir: cfg.stagingDir,
|
|
83
84
|
currentVersion,
|
|
84
85
|
});
|
|
85
|
-
const diagnose = () => diagnoseService(cfg.home, vault, platform);
|
|
86
|
-
const control = new ControlService({ vault, approver, limiter, settings, allowlist, grants, audit, platform, updater, diagnose, manage: () => manager.open() });
|
|
86
|
+
const diagnose = async () => ({ ...await diagnoseService(cfg.home, vault, platform, loadConfig(env, platform).cdpEndpoint), serviceVersion: safeVersion(currentVersion) });
|
|
87
|
+
const control = new ControlService({ vault, approver, limiter, settings, allowlist, grants, audit, platform, updater, diagnose, serviceVersion: safeVersion(currentVersion), manage: () => manager.open() });
|
|
87
88
|
const manager = new ManagementService({
|
|
88
89
|
store: allowlist, grants, vault, audit, diagnose, phrase: async () => (await settings.get()).phrase,
|
|
89
90
|
show: req => adapters.prompt.manage ? adapters.prompt.manage(req) : Promise.reject(new Error('unsupported')),
|
package/dist/service/manage.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { performance } from 'node:perf_hooks';
|
|
2
2
|
import { clean, normalizeDomain } from '../core/allowlist.js';
|
|
3
|
-
import { bounded } from '../core/doctor.js';
|
|
3
|
+
import { bounded, safeVersion, SERVICE_DIAGNOSTIC_TIMEOUT_MS } from '../core/doctor.js';
|
|
4
4
|
export class ManagementService {
|
|
5
5
|
d;
|
|
6
6
|
opened = false;
|
|
@@ -19,12 +19,19 @@ export class ManagementService {
|
|
|
19
19
|
const snapshot = await this.d.grants.run(async () => {
|
|
20
20
|
const [entries, status, events, checks] = await Promise.all([
|
|
21
21
|
this.d.store.list(), bounded(() => this.d.vault.status(), 'unavailable'),
|
|
22
|
-
bounded(() => this.d.audit.readTail(10), []), bounded(this.d.diagnose, null),
|
|
22
|
+
bounded(() => this.d.audit.readTail(10), []), bounded(this.d.diagnose, null, SERVICE_DIAGNOSTIC_TIMEOUT_MS),
|
|
23
23
|
]);
|
|
24
24
|
const all = entries.filter(e => e.domain === normalizeDomain(e.domain));
|
|
25
25
|
page = Math.min(page, Math.max(0, Math.ceil(all.length / 20) - 1));
|
|
26
26
|
const grants = all.slice(page * 20, page * 20 + 20);
|
|
27
27
|
const lines = ['Vault: ' + status, lastAction, 'Grant page ' + (page + 1) + ': ' + grants.length + ' of ' + entries.length + ' shown. Use Previous/Next for more.'];
|
|
28
|
+
lines.push('Service version: ' + (safeVersion(checks?.serviceVersion) ?? 'unknown'), 'Service browser: ' + (['pass', 'fail'].includes(checks?.browser ?? '') ? checks?.browser : 'unknown'));
|
|
29
|
+
if (status === 'locked')
|
|
30
|
+
lines.push('Next step: use Unlock to approve a protected vault unlock.');
|
|
31
|
+
if (checks?.assets !== 'pass' || checks?.permissions !== 'pass')
|
|
32
|
+
lines.push('Next step: run u-chan doctor; repair from a trusted administrator terminal.');
|
|
33
|
+
if (checks?.browser !== 'pass')
|
|
34
|
+
lines.push('Next step: start dedicated Chrome and run u-chan setup.');
|
|
28
35
|
for (const e of grants) {
|
|
29
36
|
let label = e.expiresAt ? 'until ' + e.expiresAt : e.remainingUses !== undefined ? e.remainingUses + ' attempts remaining' : 'permanent';
|
|
30
37
|
try {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
-
export const DEFAULT_NPM_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
export const DEFAULT_NPM_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
|
3
|
+
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEY6Ya7W++7aUPzvMTrezH6Ycx3c+H
|
|
4
|
+
OKYCcNGybJZSCJq/fd7Qa8uuAKtdIkUQtQiEKERhAmE5lMMJhP8OkDOa2g==
|
|
5
5
|
-----END PUBLIC KEY-----`;
|
|
6
|
+
export const DEFAULT_NPM_KEY_ID = 'SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U';
|
|
6
7
|
export function parseSemver(v) {
|
|
7
8
|
const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(v.trim());
|
|
8
9
|
if (!m)
|
|
@@ -91,14 +92,14 @@ export class NpmPackageVerifier {
|
|
|
91
92
|
packageName;
|
|
92
93
|
registryUrl;
|
|
93
94
|
npmPublicKey;
|
|
94
|
-
|
|
95
|
+
npmKeyId;
|
|
95
96
|
expectedRepo;
|
|
96
97
|
fetcher;
|
|
97
98
|
constructor(opts = {}) {
|
|
98
99
|
this.packageName = opts.packageName ?? 'u-chan-secrets';
|
|
99
100
|
this.registryUrl = (opts.registryUrl ?? 'https://registry.npmjs.org').replace(/\/$/, '');
|
|
100
101
|
this.npmPublicKey = opts.npmPublicKey ?? DEFAULT_NPM_PUBLIC_KEY;
|
|
101
|
-
this.
|
|
102
|
+
this.npmKeyId = opts.npmPublicKey === undefined ? DEFAULT_NPM_KEY_ID : undefined;
|
|
102
103
|
this.expectedRepo = opts.expectedRepo ?? 'https://github.com/anchuwithdeadline/u-chan-secrets';
|
|
103
104
|
this.fetcher = opts.fetcher ?? globalThis.fetch;
|
|
104
105
|
}
|
|
@@ -145,17 +146,18 @@ export class NpmPackageVerifier {
|
|
|
145
146
|
}
|
|
146
147
|
// 2. ECDSA signature check
|
|
147
148
|
const hasSignatures = Array.isArray(dist.signatures) && dist.signatures.length > 0;
|
|
148
|
-
|
|
149
|
-
if (!hasSignatures && (this.requireSignature || !hasAttestations)) {
|
|
149
|
+
if (!hasSignatures) {
|
|
150
150
|
return { ok: false, error: 'registry signature verification failed' };
|
|
151
151
|
}
|
|
152
|
-
if (hasSignatures
|
|
153
|
-
const signedStr = `${this.packageName}@${targetVersion}:${dist.
|
|
152
|
+
if (hasSignatures) {
|
|
153
|
+
const signedStr = `${this.packageName}@${targetVersion}:${dist.integrity}`;
|
|
154
154
|
const signedData = Buffer.from(signedStr, 'utf8');
|
|
155
155
|
let valid = false;
|
|
156
156
|
for (const s of dist.signatures) {
|
|
157
|
+
if (this.npmKeyId && s.keyid !== this.npmKeyId)
|
|
158
|
+
continue;
|
|
157
159
|
try {
|
|
158
|
-
if (crypto.verify(
|
|
160
|
+
if (crypto.verify('sha256', signedData, this.npmPublicKey, Buffer.from(s.sig, 'base64'))) {
|
|
159
161
|
valid = true;
|
|
160
162
|
break;
|
|
161
163
|
}
|
|
@@ -167,6 +169,7 @@ export class NpmPackageVerifier {
|
|
|
167
169
|
}
|
|
168
170
|
}
|
|
169
171
|
// 3. Provenance check
|
|
172
|
+
// Repository metadata is an additional policy check, never a substitute for a valid signature.
|
|
170
173
|
if (dist.attestations?.url) {
|
|
171
174
|
const attRes = await this.fetcher(dist.attestations.url);
|
|
172
175
|
if (!attRes.ok) {
|
package/package.json
CHANGED
package/scripts/start-chrome.ps1
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
param([switch]$Background)
|
|
1
2
|
$ErrorActionPreference = 'Stop'
|
|
2
3
|
$home_ = if ($env:U_CHAN_HOME) { $env:U_CHAN_HOME } else { Join-Path $env:APPDATA 'u-chan-secrets' }
|
|
3
4
|
$profileDir = Join-Path $home_ 'chrome-profile' # dedicated profile, never the default Chrome profile
|
|
4
5
|
$cdp = [Uri]$(if ($env:U_CHAN_CDP) { $env:U_CHAN_CDP } else { 'http://127.0.0.1:9222' })
|
|
6
|
+
if ($cdp.Scheme -ne 'http' -or $cdp.Host -notin @('127.0.0.1', 'localhost', '[::1]', '::1') -or $cdp.UserInfo -or $cdp.AbsolutePath -ne '/' -or $cdp.Query -or $cdp.Fragment) {
|
|
7
|
+
throw 'Use an HTTP loopback CDP origin without user information, path, query or fragment.'
|
|
8
|
+
}
|
|
5
9
|
$port = $cdp.Port
|
|
6
10
|
$bindHost = $cdp.Host
|
|
7
11
|
|
|
@@ -24,10 +28,12 @@ $chrome = $candidates | Where-Object { $_ -and (Test-Path $_) } | Select-Object
|
|
|
24
28
|
if (-not $chrome) { throw 'chrome.exe not found' }
|
|
25
29
|
|
|
26
30
|
New-Item -ItemType Directory -Force -Path $profileDir | Out-Null
|
|
27
|
-
|
|
31
|
+
$chromeArgs = @(
|
|
28
32
|
"--remote-debugging-port=$port",
|
|
29
33
|
"--remote-debugging-address=$bindHost",
|
|
30
|
-
"--user-data-dir
|
|
34
|
+
"--user-data-dir=`"$profileDir`"",
|
|
31
35
|
'--no-first-run'
|
|
32
36
|
)
|
|
37
|
+
if ($Background) { $chromeArgs += '--no-startup-window' }
|
|
38
|
+
Start-Process -FilePath $chrome -ArgumentList $chromeArgs -WindowStyle Hidden
|
|
33
39
|
Write-Host "Chrome started (profile: $profileDir, CDP: http://${bindHost}:$port)"
|