u-chan-secrets 3.0.2 → 3.0.3
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 +3 -1
- 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/service/update/verifier.js +13 -10
- package/package.json +1 -1
- package/scripts/debug-install.bat +69 -69
- package/scripts/login-bw.bat +21 -21
- package/scripts/migrate.bat +4 -4
- package/scripts/verify-windows-prompt-desktop.mjs +50 -50
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A local MCP server that signs into approved websites with Bitwarden credentials. A privileged service performs credential entry in an isolated browser and transfers filtered session state into a dedicated user Chrome profile.
|
|
4
4
|
|
|
5
|
-
V3 adds diagnostics, temporary approvals, explicit login outcomes, and a native management window. The source version is `3.0.
|
|
5
|
+
V3 adds diagnostics, temporary approvals, explicit login outcomes, and a native management window. The source version is `3.0.3`; merging this repository does not publish an npm package or update an installed service.
|
|
6
6
|
|
|
7
7
|
**Verification:** Windows/macOS build and test CI is separate from installed-service security verification. Native process identity, input isolation, permissions, and installation rehearsals remain [release gates](docs/attack-checklist.md). See [validation evidence](docs/superpowers/research/2026-10-29-v3-validation.md) and [SECURITY.md](SECURITY.md).
|
|
8
8
|
|
|
@@ -200,6 +200,8 @@ Supported V3 tooling refuses V2 downgrade after migration. Historical V2 adminis
|
|
|
200
200
|
|
|
201
201
|
Package updates verify registry signatures and integrity and retain private dependencies. Changed dependencies or native helpers require a trusted administrator reinstall. Expired/exhausted grants and already transferred website sessions are separate from vault lock and upgrade state.
|
|
202
202
|
|
|
203
|
+
Versions 3.0.0–3.0.2 have an npm signature-verification defect and cannot reliably bootstrap their own update. Install the matching corrected package from a trusted shell, then use the administrator reinstall above. Version 3.0.3 requires SHA-512 integrity and a valid signature from its pinned npm key; unsigned repository-attestation JSON cannot replace that signature. Future signing-key rotation fails closed and requires a trusted verifier upgrade.
|
|
204
|
+
|
|
203
205
|
For V1 data, use `u-chan import-v1 [path]`: each entry requires protected approval. Retire user-side Bitwarden sessions and the old `bw-session` file through a trusted migration process; never copy them into the service as trusted authorization.
|
|
204
206
|
|
|
205
207
|
## Validation and security
|
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
export function buildJxaManagementScript(req) {
|
|
2
|
-
return `ObjC.import('Cocoa');
|
|
3
|
-
ObjC.import('stdlib');
|
|
4
|
-
var alert = $.NSAlert.alloc.init;
|
|
5
|
-
alert.messageText = ${JSON.stringify(req.title)};
|
|
6
|
-
alert.informativeText = ${JSON.stringify('Phrase: ' + req.phrase)};
|
|
7
|
-
['Refresh','Previous','Next','Lock','Unlock','Revoke selected','Close'].forEach(function(t){ alert.addButtonWithTitle(t); });
|
|
8
|
-
var view = $.NSView.alloc.initWithFrame($.NSMakeRect(0,0,640,420));
|
|
9
|
-
var scroll = $.NSScrollView.alloc.initWithFrame($.NSMakeRect(0,50,640,370));
|
|
10
|
-
scroll.hasVerticalScroller = true;
|
|
11
|
-
var text = $.NSTextView.alloc.initWithFrame($.NSMakeRect(0,0,620,370));
|
|
12
|
-
text.editable = false;
|
|
13
|
-
text.string = ${JSON.stringify(req.lines.join('\n'))};
|
|
14
|
-
scroll.documentView = text;
|
|
15
|
-
view.addSubview(scroll);
|
|
16
|
-
var domains = ${JSON.stringify(req.domains ?? [])};
|
|
17
|
-
var select = $.NSPopUpButton.alloc.initWithFramePullsDown($.NSMakeRect(0,5,640,32),false);
|
|
18
|
-
domains.forEach(function(d){ select.addItemWithTitle(d); });
|
|
19
|
-
view.addSubview(select);
|
|
20
|
-
alert.accessoryView = view;
|
|
21
|
-
var response = Number(alert.runModal) - 1000;
|
|
22
|
-
var actions = ['refresh','previous','next','lock','unlock','revoke','close'];
|
|
23
|
-
var result = {action: actions[response] || 'close'};
|
|
24
|
-
if (result.action === 'revoke') {
|
|
25
|
-
var index = Number(select.indexOfSelectedItem);
|
|
26
|
-
if (index < 0 || index >= domains.length) result.action = 'refresh';
|
|
27
|
-
else result.domain = domains[index];
|
|
28
|
-
}
|
|
29
|
-
$.NSFileHandle.fileHandleWithStandardOutput.writeData(
|
|
30
|
-
$.NSString.alloc.initWithString(JSON.stringify(result)).dataUsingEncoding($.NSUTF8StringEncoding));
|
|
31
|
-
$.exit(0);
|
|
2
|
+
return `ObjC.import('Cocoa');
|
|
3
|
+
ObjC.import('stdlib');
|
|
4
|
+
var alert = $.NSAlert.alloc.init;
|
|
5
|
+
alert.messageText = ${JSON.stringify(req.title)};
|
|
6
|
+
alert.informativeText = ${JSON.stringify('Phrase: ' + req.phrase)};
|
|
7
|
+
['Refresh','Previous','Next','Lock','Unlock','Revoke selected','Close'].forEach(function(t){ alert.addButtonWithTitle(t); });
|
|
8
|
+
var view = $.NSView.alloc.initWithFrame($.NSMakeRect(0,0,640,420));
|
|
9
|
+
var scroll = $.NSScrollView.alloc.initWithFrame($.NSMakeRect(0,50,640,370));
|
|
10
|
+
scroll.hasVerticalScroller = true;
|
|
11
|
+
var text = $.NSTextView.alloc.initWithFrame($.NSMakeRect(0,0,620,370));
|
|
12
|
+
text.editable = false;
|
|
13
|
+
text.string = ${JSON.stringify(req.lines.join('\n'))};
|
|
14
|
+
scroll.documentView = text;
|
|
15
|
+
view.addSubview(scroll);
|
|
16
|
+
var domains = ${JSON.stringify(req.domains ?? [])};
|
|
17
|
+
var select = $.NSPopUpButton.alloc.initWithFramePullsDown($.NSMakeRect(0,5,640,32),false);
|
|
18
|
+
domains.forEach(function(d){ select.addItemWithTitle(d); });
|
|
19
|
+
view.addSubview(select);
|
|
20
|
+
alert.accessoryView = view;
|
|
21
|
+
var response = Number(alert.runModal) - 1000;
|
|
22
|
+
var actions = ['refresh','previous','next','lock','unlock','revoke','close'];
|
|
23
|
+
var result = {action: actions[response] || 'close'};
|
|
24
|
+
if (result.action === 'revoke') {
|
|
25
|
+
var index = Number(select.indexOfSelectedItem);
|
|
26
|
+
if (index < 0 || index >= domains.length) result.action = 'refresh';
|
|
27
|
+
else result.domain = domains[index];
|
|
28
|
+
}
|
|
29
|
+
$.NSFileHandle.fileHandleWithStandardOutput.writeData(
|
|
30
|
+
$.NSString.alloc.initWithString(JSON.stringify(result)).dataUsingEncoding($.NSUTF8StringEncoding));
|
|
31
|
+
$.exit(0);
|
|
32
32
|
`;
|
|
33
33
|
}
|
|
@@ -1,191 +1,191 @@
|
|
|
1
|
-
export const AUTH_HELPER_CSHARP = `using System;
|
|
2
|
-
using System.Diagnostics;
|
|
3
|
-
using System.Runtime.InteropServices;
|
|
4
|
-
using System.Security.Principal;
|
|
5
|
-
|
|
6
|
-
namespace UChan.Platform.Windows.Assets
|
|
7
|
-
{
|
|
8
|
-
public static class AuthHelper
|
|
9
|
-
{
|
|
10
|
-
private const uint PROCESS_QUERY_INFORMATION = 0x0400;
|
|
11
|
-
private const uint PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;
|
|
12
|
-
private const uint PROCESS_DUP_HANDLE = 0x0040;
|
|
13
|
-
private const uint TOKEN_QUERY = 0x0008;
|
|
14
|
-
private const uint DUPLICATE_SAME_ACCESS = 0x0002;
|
|
15
|
-
|
|
16
|
-
[DllImport("kernel32.dll", SetLastError = true)]
|
|
17
|
-
private static extern IntPtr GetCurrentProcess();
|
|
18
|
-
|
|
19
|
-
[DllImport("kernel32.dll", SetLastError = true)]
|
|
20
|
-
private static extern IntPtr OpenProcess(uint processAccess, bool bInheritHandle, uint processId);
|
|
21
|
-
|
|
22
|
-
[DllImport("kernel32.dll", SetLastError = true)]
|
|
23
|
-
private static extern bool DuplicateHandle(
|
|
24
|
-
IntPtr hSourceProcessHandle,
|
|
25
|
-
IntPtr hSourceHandle,
|
|
26
|
-
IntPtr hTargetProcessHandle,
|
|
27
|
-
out IntPtr lpTargetHandle,
|
|
28
|
-
uint dwDesiredAccess,
|
|
29
|
-
bool bInheritHandle,
|
|
30
|
-
uint dwOptions);
|
|
31
|
-
|
|
32
|
-
[DllImport("kernel32.dll", SetLastError = true)]
|
|
33
|
-
private static extern bool GetNamedPipeClientProcessId(IntPtr pipeHandle, out uint clientProcessId);
|
|
34
|
-
|
|
35
|
-
[DllImport("advapi32.dll", SetLastError = true)]
|
|
36
|
-
private static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle);
|
|
37
|
-
|
|
38
|
-
[DllImport("kernel32.dll", SetLastError = true)]
|
|
39
|
-
private static extern bool CloseHandle(IntPtr hObject);
|
|
40
|
-
|
|
41
|
-
private static string GetProcessUserSid(uint pid)
|
|
42
|
-
{
|
|
43
|
-
IntPtr hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid);
|
|
44
|
-
if (hProcess == IntPtr.Zero)
|
|
45
|
-
{
|
|
46
|
-
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, false, pid);
|
|
47
|
-
}
|
|
48
|
-
if (hProcess == IntPtr.Zero) return null;
|
|
49
|
-
|
|
50
|
-
try
|
|
51
|
-
{
|
|
52
|
-
IntPtr hToken;
|
|
53
|
-
if (!OpenProcessToken(hProcess, TOKEN_QUERY, out hToken))
|
|
54
|
-
{
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
try
|
|
59
|
-
{
|
|
60
|
-
using (WindowsIdentity identity = new WindowsIdentity(hToken))
|
|
61
|
-
{
|
|
62
|
-
if (identity.User != null)
|
|
63
|
-
{
|
|
64
|
-
return identity.User.Value;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
finally
|
|
69
|
-
{
|
|
70
|
-
CloseHandle(hToken);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
finally
|
|
74
|
-
{
|
|
75
|
-
CloseHandle(hProcess);
|
|
76
|
-
}
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
public static int Main(string[] args)
|
|
81
|
-
{
|
|
82
|
-
if (args.Length < 1)
|
|
83
|
-
{
|
|
84
|
-
Console.Error.WriteLine("Usage: auth-helper <serverPid> [targetSid]");
|
|
85
|
-
return 1;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
uint serverPid;
|
|
89
|
-
if (!uint.TryParse(args[0], out serverPid))
|
|
90
|
-
{
|
|
91
|
-
Console.Error.WriteLine("Invalid server PID");
|
|
92
|
-
return 1;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
string targetSid = args.Length > 1 ? args[1] : null;
|
|
96
|
-
bool hasTargetSid = !string.IsNullOrEmpty(targetSid) && !string.Equals(targetSid, "dummy", StringComparison.OrdinalIgnoreCase);
|
|
97
|
-
|
|
98
|
-
long pipeHandle = 0;
|
|
99
|
-
if (args.Length > 2)
|
|
100
|
-
{
|
|
101
|
-
long.TryParse(args[2], out pipeHandle);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
string matchedSid = null;
|
|
105
|
-
string fallbackNonSystemSid = null;
|
|
106
|
-
string anySid = null;
|
|
107
|
-
|
|
108
|
-
IntPtr hServer = OpenProcess(PROCESS_DUP_HANDLE | PROCESS_QUERY_INFORMATION, false, serverPid);
|
|
109
|
-
if (hServer != IntPtr.Zero)
|
|
110
|
-
{
|
|
111
|
-
try
|
|
112
|
-
{
|
|
113
|
-
// Exact handle of this connection when the server passes it; otherwise scan.
|
|
114
|
-
int start = pipeHandle > 0 ? (int)pipeHandle : 4;
|
|
115
|
-
int end = pipeHandle > 0 ? (int)pipeHandle + 4 : 0x10000;
|
|
116
|
-
for (int h = start; h < end; h += 4)
|
|
117
|
-
{
|
|
118
|
-
IntPtr hLocal;
|
|
119
|
-
if (DuplicateHandle(hServer, (IntPtr)h, GetCurrentProcess(), out hLocal, 0, false, DUPLICATE_SAME_ACCESS))
|
|
120
|
-
{
|
|
121
|
-
try
|
|
122
|
-
{
|
|
123
|
-
uint candidatePid;
|
|
124
|
-
if (GetNamedPipeClientProcessId(hLocal, out candidatePid) && candidatePid != 0 && candidatePid != serverPid)
|
|
125
|
-
{
|
|
126
|
-
string sid = GetProcessUserSid(candidatePid);
|
|
127
|
-
if (!string.IsNullOrEmpty(sid))
|
|
128
|
-
{
|
|
129
|
-
if (hasTargetSid && string.Equals(sid, targetSid, StringComparison.OrdinalIgnoreCase))
|
|
130
|
-
{
|
|
131
|
-
matchedSid = sid;
|
|
132
|
-
break;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (!string.Equals(sid, "S-1-5-18", StringComparison.OrdinalIgnoreCase))
|
|
136
|
-
{
|
|
137
|
-
fallbackNonSystemSid = sid;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (anySid == null)
|
|
141
|
-
{
|
|
142
|
-
anySid = sid;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
finally
|
|
148
|
-
{
|
|
149
|
-
CloseHandle(hLocal);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
finally
|
|
155
|
-
{
|
|
156
|
-
CloseHandle(hServer);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
string clientSid = matchedSid ?? fallbackNonSystemSid ?? anySid;
|
|
161
|
-
|
|
162
|
-
// Fail closed: if clientSid could not be identified on any pipe handle, reject
|
|
163
|
-
if (string.IsNullOrEmpty(clientSid))
|
|
164
|
-
{
|
|
165
|
-
Console.Error.WriteLine("Could not identify client process on pipe");
|
|
166
|
-
return 1;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Verify string sid parses as SecurityIdentifier
|
|
170
|
-
try
|
|
171
|
-
{
|
|
172
|
-
SecurityIdentifier secId = new SecurityIdentifier(clientSid);
|
|
173
|
-
}
|
|
174
|
-
catch
|
|
175
|
-
{
|
|
176
|
-
Console.Error.WriteLine("Resolved SID is invalid");
|
|
177
|
-
return 1;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (hasTargetSid && !string.Equals(clientSid, targetSid, StringComparison.OrdinalIgnoreCase))
|
|
181
|
-
{
|
|
182
|
-
Console.Error.WriteLine("SID mismatch: expected " + targetSid + " but got " + clientSid);
|
|
183
|
-
return 1;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
Console.WriteLine("AUTHORIZED:" + clientSid);
|
|
187
|
-
return 0;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
1
|
+
export const AUTH_HELPER_CSHARP = `using System;
|
|
2
|
+
using System.Diagnostics;
|
|
3
|
+
using System.Runtime.InteropServices;
|
|
4
|
+
using System.Security.Principal;
|
|
5
|
+
|
|
6
|
+
namespace UChan.Platform.Windows.Assets
|
|
7
|
+
{
|
|
8
|
+
public static class AuthHelper
|
|
9
|
+
{
|
|
10
|
+
private const uint PROCESS_QUERY_INFORMATION = 0x0400;
|
|
11
|
+
private const uint PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;
|
|
12
|
+
private const uint PROCESS_DUP_HANDLE = 0x0040;
|
|
13
|
+
private const uint TOKEN_QUERY = 0x0008;
|
|
14
|
+
private const uint DUPLICATE_SAME_ACCESS = 0x0002;
|
|
15
|
+
|
|
16
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
17
|
+
private static extern IntPtr GetCurrentProcess();
|
|
18
|
+
|
|
19
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
20
|
+
private static extern IntPtr OpenProcess(uint processAccess, bool bInheritHandle, uint processId);
|
|
21
|
+
|
|
22
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
23
|
+
private static extern bool DuplicateHandle(
|
|
24
|
+
IntPtr hSourceProcessHandle,
|
|
25
|
+
IntPtr hSourceHandle,
|
|
26
|
+
IntPtr hTargetProcessHandle,
|
|
27
|
+
out IntPtr lpTargetHandle,
|
|
28
|
+
uint dwDesiredAccess,
|
|
29
|
+
bool bInheritHandle,
|
|
30
|
+
uint dwOptions);
|
|
31
|
+
|
|
32
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
33
|
+
private static extern bool GetNamedPipeClientProcessId(IntPtr pipeHandle, out uint clientProcessId);
|
|
34
|
+
|
|
35
|
+
[DllImport("advapi32.dll", SetLastError = true)]
|
|
36
|
+
private static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle);
|
|
37
|
+
|
|
38
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
39
|
+
private static extern bool CloseHandle(IntPtr hObject);
|
|
40
|
+
|
|
41
|
+
private static string GetProcessUserSid(uint pid)
|
|
42
|
+
{
|
|
43
|
+
IntPtr hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid);
|
|
44
|
+
if (hProcess == IntPtr.Zero)
|
|
45
|
+
{
|
|
46
|
+
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, false, pid);
|
|
47
|
+
}
|
|
48
|
+
if (hProcess == IntPtr.Zero) return null;
|
|
49
|
+
|
|
50
|
+
try
|
|
51
|
+
{
|
|
52
|
+
IntPtr hToken;
|
|
53
|
+
if (!OpenProcessToken(hProcess, TOKEN_QUERY, out hToken))
|
|
54
|
+
{
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try
|
|
59
|
+
{
|
|
60
|
+
using (WindowsIdentity identity = new WindowsIdentity(hToken))
|
|
61
|
+
{
|
|
62
|
+
if (identity.User != null)
|
|
63
|
+
{
|
|
64
|
+
return identity.User.Value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
finally
|
|
69
|
+
{
|
|
70
|
+
CloseHandle(hToken);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
finally
|
|
74
|
+
{
|
|
75
|
+
CloseHandle(hProcess);
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public static int Main(string[] args)
|
|
81
|
+
{
|
|
82
|
+
if (args.Length < 1)
|
|
83
|
+
{
|
|
84
|
+
Console.Error.WriteLine("Usage: auth-helper <serverPid> [targetSid]");
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
uint serverPid;
|
|
89
|
+
if (!uint.TryParse(args[0], out serverPid))
|
|
90
|
+
{
|
|
91
|
+
Console.Error.WriteLine("Invalid server PID");
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
string targetSid = args.Length > 1 ? args[1] : null;
|
|
96
|
+
bool hasTargetSid = !string.IsNullOrEmpty(targetSid) && !string.Equals(targetSid, "dummy", StringComparison.OrdinalIgnoreCase);
|
|
97
|
+
|
|
98
|
+
long pipeHandle = 0;
|
|
99
|
+
if (args.Length > 2)
|
|
100
|
+
{
|
|
101
|
+
long.TryParse(args[2], out pipeHandle);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
string matchedSid = null;
|
|
105
|
+
string fallbackNonSystemSid = null;
|
|
106
|
+
string anySid = null;
|
|
107
|
+
|
|
108
|
+
IntPtr hServer = OpenProcess(PROCESS_DUP_HANDLE | PROCESS_QUERY_INFORMATION, false, serverPid);
|
|
109
|
+
if (hServer != IntPtr.Zero)
|
|
110
|
+
{
|
|
111
|
+
try
|
|
112
|
+
{
|
|
113
|
+
// Exact handle of this connection when the server passes it; otherwise scan.
|
|
114
|
+
int start = pipeHandle > 0 ? (int)pipeHandle : 4;
|
|
115
|
+
int end = pipeHandle > 0 ? (int)pipeHandle + 4 : 0x10000;
|
|
116
|
+
for (int h = start; h < end; h += 4)
|
|
117
|
+
{
|
|
118
|
+
IntPtr hLocal;
|
|
119
|
+
if (DuplicateHandle(hServer, (IntPtr)h, GetCurrentProcess(), out hLocal, 0, false, DUPLICATE_SAME_ACCESS))
|
|
120
|
+
{
|
|
121
|
+
try
|
|
122
|
+
{
|
|
123
|
+
uint candidatePid;
|
|
124
|
+
if (GetNamedPipeClientProcessId(hLocal, out candidatePid) && candidatePid != 0 && candidatePid != serverPid)
|
|
125
|
+
{
|
|
126
|
+
string sid = GetProcessUserSid(candidatePid);
|
|
127
|
+
if (!string.IsNullOrEmpty(sid))
|
|
128
|
+
{
|
|
129
|
+
if (hasTargetSid && string.Equals(sid, targetSid, StringComparison.OrdinalIgnoreCase))
|
|
130
|
+
{
|
|
131
|
+
matchedSid = sid;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!string.Equals(sid, "S-1-5-18", StringComparison.OrdinalIgnoreCase))
|
|
136
|
+
{
|
|
137
|
+
fallbackNonSystemSid = sid;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (anySid == null)
|
|
141
|
+
{
|
|
142
|
+
anySid = sid;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
finally
|
|
148
|
+
{
|
|
149
|
+
CloseHandle(hLocal);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
finally
|
|
155
|
+
{
|
|
156
|
+
CloseHandle(hServer);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
string clientSid = matchedSid ?? fallbackNonSystemSid ?? anySid;
|
|
161
|
+
|
|
162
|
+
// Fail closed: if clientSid could not be identified on any pipe handle, reject
|
|
163
|
+
if (string.IsNullOrEmpty(clientSid))
|
|
164
|
+
{
|
|
165
|
+
Console.Error.WriteLine("Could not identify client process on pipe");
|
|
166
|
+
return 1;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Verify string sid parses as SecurityIdentifier
|
|
170
|
+
try
|
|
171
|
+
{
|
|
172
|
+
SecurityIdentifier secId = new SecurityIdentifier(clientSid);
|
|
173
|
+
}
|
|
174
|
+
catch
|
|
175
|
+
{
|
|
176
|
+
Console.Error.WriteLine("Resolved SID is invalid");
|
|
177
|
+
return 1;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (hasTargetSid && !string.Equals(clientSid, targetSid, StringComparison.OrdinalIgnoreCase))
|
|
181
|
+
{
|
|
182
|
+
Console.Error.WriteLine("SID mismatch: expected " + targetSid + " but got " + clientSid);
|
|
183
|
+
return 1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
Console.WriteLine("AUTHORIZED:" + clientSid);
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
191
|
`;
|
|
@@ -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,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
|
@@ -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
|
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
|
+
|
|
@@ -1,50 +1,50 @@
|
|
|
1
|
-
// Run from a trusted administrator terminal after building and installing the service.
|
|
2
|
-
// Exercises SYSTEM relay, desktop entry, restoration and reply transport without a password UI.
|
|
3
|
-
import fs from 'node:fs/promises'
|
|
4
|
-
import path from 'node:path'
|
|
5
|
-
import { randomUUID } from 'node:crypto'
|
|
6
|
-
import { promisify } from 'node:util'
|
|
7
|
-
import { execFile } from 'node:child_process'
|
|
8
|
-
import { compileCsharp } from '../dist/platform/windows/assets/compiler.js'
|
|
9
|
-
import { PROMPT_HELPER_CSHARP } from '../dist/platform/windows/assets/prompt-src.js'
|
|
10
|
-
import { defaultAdminCheck } from '../dist/platform/windows/install.js'
|
|
11
|
-
import { setAdminOnlyAcl } from '../dist/platform/windows/acl.js'
|
|
12
|
-
|
|
13
|
-
if (process.platform !== 'win32' || !await defaultAdminCheck()) throw new Error('Use a trusted Windows administrator terminal')
|
|
14
|
-
const home = path.join(process.env.ProgramData ?? 'C:\\ProgramData', 'u-chan-secrets')
|
|
15
|
-
const root = path.join(home, '#0 backup', 'desktop-probe-' + randomUUID())
|
|
16
|
-
const task = 'UChanDesktopProbe-' + randomUUID()
|
|
17
|
-
const exec = promisify(execFile)
|
|
18
|
-
const powershell = script => exec('powershell.exe', ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(script, 'utf16le').toString('base64')], { windowsHide: true })
|
|
19
|
-
const quote = value => "'" + value.replaceAll("'", "''") + "'"
|
|
20
|
-
let registered = false
|
|
21
|
-
try {
|
|
22
|
-
await fs.mkdir(root)
|
|
23
|
-
await setAdminOnlyAcl(root)
|
|
24
|
-
const trace = path.join(root, 'desktop-trace.txt')
|
|
25
|
-
if (!PROMPT_HELPER_CSHARP.includes('return RunDialog(args);') || !PROMPT_HELPER_CSHARP.includes('SwitchDesktop(original);')) throw new Error('Native probe hooks no longer match; no helper was run')
|
|
26
|
-
const source = PROMPT_HELPER_CSHARP
|
|
27
|
-
.replace('return RunDialog(args);', `File.AppendAllText(@"${trace}", "entered"+Environment.NewLine); return 0;`)
|
|
28
|
-
.replace('SwitchDesktop(original);', `if (SwitchDesktop(original)) File.AppendAllText(@"${trace}", "restored"+Environment.NewLine);`)
|
|
29
|
-
const helper = path.join(root, 'probe.exe')
|
|
30
|
-
await compileCsharp(source, helper, { outputType: 'WindowsApplication', referencedAssemblies: ['System.Windows.Forms.dll', 'System.Drawing.dll'] })
|
|
31
|
-
const output = path.join(root, 'result.json'), runner = path.join(root, 'runner.mjs')
|
|
32
|
-
await fs.writeFile(runner, `import{execFile}from'node:child_process';import fs from'node:fs';execFile(${JSON.stringify(helper)},['--mode','confirm','--phrase','DISPOSABLE-NATIVE-PROBE','--lines','[]'],{timeout:35000,windowsHide:true},(e,out)=>fs.writeFileSync(${JSON.stringify(output)},JSON.stringify({exitCode:e?.code??0,stdoutBytes:Buffer.byteLength(out)})));`)
|
|
33
|
-
await setAdminOnlyAcl(root)
|
|
34
|
-
await powershell(`$ErrorActionPreference='Stop';$a=New-ScheduledTaskAction -Execute ${quote(path.join(home, 'bin', 'node.exe'))} -Argument ${quote('"' + runner + '"')};$p=New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest;$s=New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 2) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries;Register-ScheduledTask -TaskName ${quote(task)} -Action $a -Principal $p -Settings $s | Out-Null`)
|
|
35
|
-
registered = true
|
|
36
|
-
await powershell(`Start-ScheduledTask -TaskName ${quote(task)}`)
|
|
37
|
-
const deadline = Date.now() + 60000
|
|
38
|
-
let execution
|
|
39
|
-
while (Date.now() < deadline) {
|
|
40
|
-
try { execution = JSON.parse(await fs.readFile(output, 'utf8')); break }
|
|
41
|
-
catch (e) { if (e.code !== 'ENOENT') throw e }
|
|
42
|
-
await new Promise(resolve => setTimeout(resolve, 500))
|
|
43
|
-
}
|
|
44
|
-
const events = await fs.readFile(trace, 'utf8').catch(() => '')
|
|
45
|
-
const ok = execution?.exitCode === 0 && execution.stdoutBytes === 0 && events.trim() === 'entered\r\nrestored'
|
|
46
|
-
console.log(JSON.stringify({ ok, nativeDryRun: true, desktopEntered: events.includes('entered'), desktopRestored: events.includes('restored'), execution, evidence: root }))
|
|
47
|
-
if (!ok) process.exitCode = 1
|
|
48
|
-
} finally {
|
|
49
|
-
if (registered) await powershell(`Stop-ScheduledTask -TaskName ${quote(task)} -ErrorAction SilentlyContinue;Unregister-ScheduledTask -TaskName ${quote(task)} -Confirm:$false`)
|
|
50
|
-
}
|
|
1
|
+
// Run from a trusted administrator terminal after building and installing the service.
|
|
2
|
+
// Exercises SYSTEM relay, desktop entry, restoration and reply transport without a password UI.
|
|
3
|
+
import fs from 'node:fs/promises'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { randomUUID } from 'node:crypto'
|
|
6
|
+
import { promisify } from 'node:util'
|
|
7
|
+
import { execFile } from 'node:child_process'
|
|
8
|
+
import { compileCsharp } from '../dist/platform/windows/assets/compiler.js'
|
|
9
|
+
import { PROMPT_HELPER_CSHARP } from '../dist/platform/windows/assets/prompt-src.js'
|
|
10
|
+
import { defaultAdminCheck } from '../dist/platform/windows/install.js'
|
|
11
|
+
import { setAdminOnlyAcl } from '../dist/platform/windows/acl.js'
|
|
12
|
+
|
|
13
|
+
if (process.platform !== 'win32' || !await defaultAdminCheck()) throw new Error('Use a trusted Windows administrator terminal')
|
|
14
|
+
const home = path.join(process.env.ProgramData ?? 'C:\\ProgramData', 'u-chan-secrets')
|
|
15
|
+
const root = path.join(home, '#0 backup', 'desktop-probe-' + randomUUID())
|
|
16
|
+
const task = 'UChanDesktopProbe-' + randomUUID()
|
|
17
|
+
const exec = promisify(execFile)
|
|
18
|
+
const powershell = script => exec('powershell.exe', ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(script, 'utf16le').toString('base64')], { windowsHide: true })
|
|
19
|
+
const quote = value => "'" + value.replaceAll("'", "''") + "'"
|
|
20
|
+
let registered = false
|
|
21
|
+
try {
|
|
22
|
+
await fs.mkdir(root)
|
|
23
|
+
await setAdminOnlyAcl(root)
|
|
24
|
+
const trace = path.join(root, 'desktop-trace.txt')
|
|
25
|
+
if (!PROMPT_HELPER_CSHARP.includes('return RunDialog(args);') || !PROMPT_HELPER_CSHARP.includes('SwitchDesktop(original);')) throw new Error('Native probe hooks no longer match; no helper was run')
|
|
26
|
+
const source = PROMPT_HELPER_CSHARP
|
|
27
|
+
.replace('return RunDialog(args);', `File.AppendAllText(@"${trace}", "entered"+Environment.NewLine); return 0;`)
|
|
28
|
+
.replace('SwitchDesktop(original);', `if (SwitchDesktop(original)) File.AppendAllText(@"${trace}", "restored"+Environment.NewLine);`)
|
|
29
|
+
const helper = path.join(root, 'probe.exe')
|
|
30
|
+
await compileCsharp(source, helper, { outputType: 'WindowsApplication', referencedAssemblies: ['System.Windows.Forms.dll', 'System.Drawing.dll'] })
|
|
31
|
+
const output = path.join(root, 'result.json'), runner = path.join(root, 'runner.mjs')
|
|
32
|
+
await fs.writeFile(runner, `import{execFile}from'node:child_process';import fs from'node:fs';execFile(${JSON.stringify(helper)},['--mode','confirm','--phrase','DISPOSABLE-NATIVE-PROBE','--lines','[]'],{timeout:35000,windowsHide:true},(e,out)=>fs.writeFileSync(${JSON.stringify(output)},JSON.stringify({exitCode:e?.code??0,stdoutBytes:Buffer.byteLength(out)})));`)
|
|
33
|
+
await setAdminOnlyAcl(root)
|
|
34
|
+
await powershell(`$ErrorActionPreference='Stop';$a=New-ScheduledTaskAction -Execute ${quote(path.join(home, 'bin', 'node.exe'))} -Argument ${quote('"' + runner + '"')};$p=New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest;$s=New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 2) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries;Register-ScheduledTask -TaskName ${quote(task)} -Action $a -Principal $p -Settings $s | Out-Null`)
|
|
35
|
+
registered = true
|
|
36
|
+
await powershell(`Start-ScheduledTask -TaskName ${quote(task)}`)
|
|
37
|
+
const deadline = Date.now() + 60000
|
|
38
|
+
let execution
|
|
39
|
+
while (Date.now() < deadline) {
|
|
40
|
+
try { execution = JSON.parse(await fs.readFile(output, 'utf8')); break }
|
|
41
|
+
catch (e) { if (e.code !== 'ENOENT') throw e }
|
|
42
|
+
await new Promise(resolve => setTimeout(resolve, 500))
|
|
43
|
+
}
|
|
44
|
+
const events = await fs.readFile(trace, 'utf8').catch(() => '')
|
|
45
|
+
const ok = execution?.exitCode === 0 && execution.stdoutBytes === 0 && events.trim() === 'entered\r\nrestored'
|
|
46
|
+
console.log(JSON.stringify({ ok, nativeDryRun: true, desktopEntered: events.includes('entered'), desktopRestored: events.includes('restored'), execution, evidence: root }))
|
|
47
|
+
if (!ok) process.exitCode = 1
|
|
48
|
+
} finally {
|
|
49
|
+
if (registered) await powershell(`Stop-ScheduledTask -TaskName ${quote(task)} -ErrorAction SilentlyContinue;Unregister-ScheduledTask -TaskName ${quote(task)} -Confirm:$false`)
|
|
50
|
+
}
|