mecord-connect 1.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/DISCLOSURE +47 -0
- package/LICENSE +64 -0
- package/README.md +44 -0
- package/bin/operator.mjs +10 -0
- package/package.json +58 -0
- package/runtime/app/apps/local-agent/src/approval-store.js +342 -0
- package/runtime/app/apps/local-agent/src/bootstrap-config.js +255 -0
- package/runtime/app/apps/local-agent/src/cli.js +237 -0
- package/runtime/app/apps/local-agent/src/device-reset.js +260 -0
- package/runtime/app/apps/local-agent/src/emergency-stop.js +141 -0
- package/runtime/app/apps/local-agent/src/main.js +311 -0
- package/runtime/app/apps/local-agent/src/privacy-data.js +129 -0
- package/runtime/app/apps/local-agent/src/relay-agent.js +333 -0
- package/runtime/app/apps/local-agent/src/relay-enrollment.js +156 -0
- package/runtime/app/apps/local-agent/src/relay-session-credentials.js +436 -0
- package/runtime/app/apps/local-agent/src/remote.js +130 -0
- package/runtime/app/apps/local-agent/src/runtime-factory.js +66 -0
- package/runtime/app/apps/local-agent/src/server.js +578 -0
- package/runtime/app/package.json +4 -0
- package/runtime/app/src/capabilities/browser-cdp-connection.js +199 -0
- package/runtime/app/src/capabilities/browser-cdp-frames.js +288 -0
- package/runtime/app/src/capabilities/browser-cdp-page.js +430 -0
- package/runtime/app/src/capabilities/browser-cdp.js +384 -0
- package/runtime/app/src/capabilities/browser-managed.js +345 -0
- package/runtime/app/src/capabilities/docker.js +448 -0
- package/runtime/app/src/capabilities/filesystem.js +264 -0
- package/runtime/app/src/capabilities/git-checkpoint.js +415 -0
- package/runtime/app/src/capabilities/git-write.js +321 -0
- package/runtime/app/src/capabilities/git.js +252 -0
- package/runtime/app/src/capabilities/index.js +16 -0
- package/runtime/app/src/capabilities/path-scope.js +86 -0
- package/runtime/app/src/capabilities/postgres.js +566 -0
- package/runtime/app/src/capabilities/process.js +258 -0
- package/runtime/app/src/capabilities/project-command.js +599 -0
- package/runtime/app/src/capabilities/project-transaction.js +215 -0
- package/runtime/app/src/capabilities/project.js +67 -0
- package/runtime/app/src/capabilities/system.js +40 -0
- package/runtime/app/src/capabilities/vscode.js +312 -0
- package/runtime/app/src/capabilities/windows-uia.js +331 -0
- package/runtime/app/src/core/account-device-registry.js +604 -0
- package/runtime/app/src/core/action-identity.js +44 -0
- package/runtime/app/src/core/audit.js +295 -0
- package/runtime/app/src/core/capability-policy.js +58 -0
- package/runtime/app/src/core/child-environment.js +29 -0
- package/runtime/app/src/core/device-enrollment.js +417 -0
- package/runtime/app/src/core/device-identity.js +436 -0
- package/runtime/app/src/core/device-registry.js +477 -0
- package/runtime/app/src/core/device-reset.js +178 -0
- package/runtime/app/src/core/device-routing.js +305 -0
- package/runtime/app/src/core/durable-state.js +306 -0
- package/runtime/app/src/core/errors.js +20 -0
- package/runtime/app/src/core/evidence.js +9 -0
- package/runtime/app/src/core/index.js +17 -0
- package/runtime/app/src/core/machine-state.js +160 -0
- package/runtime/app/src/core/network-authority.js +20 -0
- package/runtime/app/src/core/policy.js +67 -0
- package/runtime/app/src/core/product-identity.js +7 -0
- package/runtime/app/src/core/provenance.js +18 -0
- package/runtime/app/src/core/provider-learning.js +274 -0
- package/runtime/app/src/core/public-plugin-surface.js +25 -0
- package/runtime/app/src/core/public-restricted-data.js +105 -0
- package/runtime/app/src/core/rate-limit.js +118 -0
- package/runtime/app/src/core/relay-client.js +568 -0
- package/runtime/app/src/core/relay-delivery-store.js +512 -0
- package/runtime/app/src/core/relay-result-store.js +321 -0
- package/runtime/app/src/core/release-update.js +253 -0
- package/runtime/app/src/core/router.js +96 -0
- package/runtime/app/src/core/runtime.js +136 -0
- package/runtime/app/src/core/scoped-path-syntax.js +16 -0
- package/runtime/app/src/core/session-token.js +498 -0
- package/runtime/app/src/core/task-orchestrator.js +1148 -0
- package/runtime/app/src/core/task-store.js +517 -0
- package/runtime/app/src/core/task.js +142 -0
- package/runtime/app/src/core/trusted-executable.js +124 -0
- package/runtime/app/src/core/types.js +83 -0
- package/runtime/app/src/core/windows-path-lease.js +138 -0
- package/runtime/native/operator-windows-dpapi.exe +0 -0
- package/runtime/native/operator-windows-path-lease.exe +0 -0
- package/runtime/native/operator-windows-uia.exe +0 -0
- package/runtime/runtime-manifest.json +380 -0
- package/src/cli.mjs +345 -0
- package/src/publish-check.mjs +22 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { OperatorError } from './errors.js';
|
|
7
|
+
import { safeChildEnvironment } from './child-environment.js';
|
|
8
|
+
import { readDurableStateText, writeDurableStateText } from './durable-state.js';
|
|
9
|
+
|
|
10
|
+
const MAX_IDENTITY_BYTES = 256 * 1024;
|
|
11
|
+
const MAX_HELPER_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
12
|
+
const MAX_HELPER_ERROR_BYTES = 4096;
|
|
13
|
+
const DPAPI_SCHEME = 'windows-dpapi-current-user' ;
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
export class DeviceIdentityStore {
|
|
53
|
+
#file ;
|
|
54
|
+
#platform ;
|
|
55
|
+
#protector ;
|
|
56
|
+
#dpapiExecutable ;
|
|
57
|
+
|
|
58
|
+
constructor(stateDir , options
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
= {}) {
|
|
63
|
+
this.#file = path.join(path.resolve(stateDir), 'device-identity.json');
|
|
64
|
+
this.#platform = options.platform ?? process.platform;
|
|
65
|
+
this.#protector = options.secretProtector;
|
|
66
|
+
this.#dpapiExecutable = options.dpapiExecutable ?? process.env.OPERATOR_WINDOWS_DPAPI_PATH;
|
|
67
|
+
if (this.#protector && this.#protector.scheme !== DPAPI_SCHEME) {
|
|
68
|
+
throw new OperatorError('DEVICE_SECRET_PROTECTOR_INVALID', 'Unsupported device secret protection scheme.');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async loadOrCreate(deviceName = os.hostname()) {
|
|
73
|
+
let stored ;
|
|
74
|
+
try {
|
|
75
|
+
stored = await this.#readAndMigrate();
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if ((error ).code !== 'ENOENT') throw error;
|
|
78
|
+
stored = await this.#create(deviceName);
|
|
79
|
+
}
|
|
80
|
+
return publicIdentity(stored);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async loadExisting() {
|
|
84
|
+
try { return publicIdentity(await this.#readAndMigrate()); }
|
|
85
|
+
catch (error) {
|
|
86
|
+
if ((error ).code === 'ENOENT') return null;
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async erase() {
|
|
92
|
+
try {
|
|
93
|
+
const stat = await fs.lstat(this.#file);
|
|
94
|
+
if (stat.isSymbolicLink() || !stat.isFile()) throw new OperatorError('DEVICE_IDENTITY_ERASE_UNSAFE', 'Device identity reset refuses non-file or symbolic-link targets.');
|
|
95
|
+
await fs.rm(this.#file, { force: true });
|
|
96
|
+
return true;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if ((error ).code === 'ENOENT') return false;
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async sign(payload ) {
|
|
104
|
+
let stored ;
|
|
105
|
+
try { stored = await this.#readAndMigrate(); }
|
|
106
|
+
catch (error) {
|
|
107
|
+
if ((error ).code === 'ENOENT') {
|
|
108
|
+
throw new OperatorError('DEVICE_IDENTITY_MISSING', 'Device identity has not been created.');
|
|
109
|
+
}
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (stored.version === 1) {
|
|
114
|
+
return crypto.sign(null, payload, stored.privateKeyPem).toString('base64url');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const protector = this.#requireWindowsProtector();
|
|
118
|
+
const ciphertext = decodeCiphertext(stored.privateKeyProtection.ciphertextBase64);
|
|
119
|
+
let privateDer ;
|
|
120
|
+
try {
|
|
121
|
+
privateDer = await protector.unprotect(ciphertext);
|
|
122
|
+
if (privateDer.length === 0 || privateDer.length > 64 * 1024) {
|
|
123
|
+
throw new OperatorError('DEVICE_PRIVATE_KEY_INVALID', 'Protected device private key had an invalid size.');
|
|
124
|
+
}
|
|
125
|
+
const key = crypto.createPrivateKey({ key: privateDer, format: 'der', type: 'pkcs8' });
|
|
126
|
+
return crypto.sign(null, payload, key).toString('base64url');
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error instanceof OperatorError) throw error;
|
|
129
|
+
throw new OperatorError('DEVICE_PRIVATE_KEY_UNPROTECT_FAILED', 'Windows could not unlock the device private key for the current user.');
|
|
130
|
+
} finally {
|
|
131
|
+
ciphertext.fill(0);
|
|
132
|
+
privateDer?.fill(0);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async verify(payload , signature ) {
|
|
137
|
+
let stored ;
|
|
138
|
+
try { stored = await this.#readAndMigrate(); }
|
|
139
|
+
catch (error) {
|
|
140
|
+
if ((error ).code === 'ENOENT') {
|
|
141
|
+
throw new OperatorError('DEVICE_IDENTITY_MISSING', 'Device identity has not been created.');
|
|
142
|
+
}
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
return crypto.verify(null, payload, stored.publicKeyPem, Buffer.from(signature, 'base64url'));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async #create(deviceName ) {
|
|
149
|
+
await fs.mkdir(path.dirname(this.#file), { recursive: true, mode: 0o700 });
|
|
150
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
|
|
151
|
+
const base = {
|
|
152
|
+
deviceId: crypto.randomUUID(),
|
|
153
|
+
deviceName,
|
|
154
|
+
createdAt: new Date().toISOString(),
|
|
155
|
+
publicKeyPem: publicKey.export({ type: 'spki', format: 'pem' }).toString()
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
if (this.#platform !== 'win32') {
|
|
159
|
+
const stored = {
|
|
160
|
+
version: 1,
|
|
161
|
+
...base,
|
|
162
|
+
privateKeyPem: privateKey.export({ type: 'pkcs8', format: 'pem' }).toString()
|
|
163
|
+
};
|
|
164
|
+
await writeExclusiveJson(this.#file, stored);
|
|
165
|
+
return stored;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const protector = this.#requireWindowsProtector();
|
|
169
|
+
const privateDer = privateKey.export({ type: 'pkcs8', format: 'der' }) ;
|
|
170
|
+
let protectedBytes ;
|
|
171
|
+
try {
|
|
172
|
+
protectedBytes = await protector.protect(privateDer);
|
|
173
|
+
if (protectedBytes.length === 0 || protectedBytes.length > MAX_HELPER_OUTPUT_BYTES) {
|
|
174
|
+
throw new OperatorError('DEVICE_PRIVATE_KEY_PROTECT_FAILED', 'Windows returned an invalid protected device key blob.');
|
|
175
|
+
}
|
|
176
|
+
const stored = {
|
|
177
|
+
version: 2,
|
|
178
|
+
...base,
|
|
179
|
+
privateKeyProtection: {
|
|
180
|
+
scheme: DPAPI_SCHEME,
|
|
181
|
+
keyFormat: 'pkcs8-der',
|
|
182
|
+
ciphertextBase64: protectedBytes.toString('base64')
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
await writeExclusiveJson(this.#file, stored);
|
|
186
|
+
return stored;
|
|
187
|
+
} finally {
|
|
188
|
+
privateDer.fill(0);
|
|
189
|
+
protectedBytes?.fill(0);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async #readAndMigrate() {
|
|
194
|
+
const raw = await readDurableStateText(this.#file, {
|
|
195
|
+
maxBytes: MAX_IDENTITY_BYTES,
|
|
196
|
+
errorCode: 'DEVICE_IDENTITY_INVALID',
|
|
197
|
+
invalidMessage: 'Device identity file is invalid.'
|
|
198
|
+
});
|
|
199
|
+
let stored ;
|
|
200
|
+
try { stored = parseStoredIdentity(JSON.parse(raw)); }
|
|
201
|
+
catch (error) {
|
|
202
|
+
if (error instanceof OperatorError) throw error;
|
|
203
|
+
throw new OperatorError('DEVICE_IDENTITY_INVALID', 'Device identity file could not be parsed.');
|
|
204
|
+
}
|
|
205
|
+
if (this.#platform === 'win32' && stored.version === 1) {
|
|
206
|
+
return await this.#migrateWindowsIdentity(stored);
|
|
207
|
+
}
|
|
208
|
+
return stored;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async #migrateWindowsIdentity(legacy ) {
|
|
212
|
+
const protector = this.#requireWindowsProtector();
|
|
213
|
+
let privateDer ;
|
|
214
|
+
let protectedBytes ;
|
|
215
|
+
try {
|
|
216
|
+
const key = crypto.createPrivateKey(legacy.privateKeyPem);
|
|
217
|
+
privateDer = key.export({ type: 'pkcs8', format: 'der' }) ;
|
|
218
|
+
protectedBytes = await protector.protect(privateDer);
|
|
219
|
+
if (protectedBytes.length === 0 || protectedBytes.length > MAX_HELPER_OUTPUT_BYTES) {
|
|
220
|
+
throw new OperatorError('DEVICE_PRIVATE_KEY_PROTECT_FAILED', 'Windows returned an invalid protected device key blob during migration.');
|
|
221
|
+
}
|
|
222
|
+
const migrated = {
|
|
223
|
+
version: 2,
|
|
224
|
+
deviceId: legacy.deviceId,
|
|
225
|
+
deviceName: legacy.deviceName,
|
|
226
|
+
createdAt: legacy.createdAt,
|
|
227
|
+
publicKeyPem: legacy.publicKeyPem,
|
|
228
|
+
privateKeyProtection: {
|
|
229
|
+
scheme: DPAPI_SCHEME,
|
|
230
|
+
keyFormat: 'pkcs8-der',
|
|
231
|
+
ciphertextBase64: protectedBytes.toString('base64')
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
await replaceJsonAtomic(this.#file, migrated);
|
|
235
|
+
return migrated;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
if (error instanceof OperatorError) throw error;
|
|
238
|
+
throw new OperatorError('DEVICE_IDENTITY_MIGRATION_FAILED', 'Legacy Windows device identity could not be migrated to protected storage.');
|
|
239
|
+
} finally {
|
|
240
|
+
privateDer?.fill(0);
|
|
241
|
+
protectedBytes?.fill(0);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
#requireWindowsProtector() {
|
|
246
|
+
if (this.#platform !== 'win32') {
|
|
247
|
+
throw new OperatorError('DEVICE_SECRET_PROTECTOR_UNAVAILABLE', 'Windows DPAPI protection is only available on Windows.');
|
|
248
|
+
}
|
|
249
|
+
if (this.#protector) return this.#protector;
|
|
250
|
+
const executable = this.#dpapiExecutable;
|
|
251
|
+
if (!executable || !path.isAbsolute(executable)) {
|
|
252
|
+
throw new OperatorError('WINDOWS_DPAPI_HELPER_REQUIRED', 'Windows device identity protection requires an absolute OPERATOR_WINDOWS_DPAPI_PATH.');
|
|
253
|
+
}
|
|
254
|
+
this.#protector = new WindowsDpapiProtector(executable);
|
|
255
|
+
return this.#protector;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export class WindowsDpapiProtector {
|
|
260
|
+
scheme = DPAPI_SCHEME;
|
|
261
|
+
#executable ;
|
|
262
|
+
|
|
263
|
+
constructor(executable ) {
|
|
264
|
+
if (!path.isAbsolute(executable)) {
|
|
265
|
+
throw new OperatorError('WINDOWS_DPAPI_HELPER_REQUIRED', 'Windows DPAPI helper path must be absolute.');
|
|
266
|
+
}
|
|
267
|
+
this.#executable = executable;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async protect(input ) {
|
|
271
|
+
return await runSecretHelper(this.#executable, 'protect', input);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async unprotect(input ) {
|
|
275
|
+
return await runSecretHelper(this.#executable, 'unprotect', input);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function runSecretHelper(executable , operation , input ) {
|
|
280
|
+
if (input.length === 0 || input.length > 1024 * 1024) {
|
|
281
|
+
throw new OperatorError('DEVICE_PRIVATE_KEY_INVALID', 'Device secret helper input had an invalid size.');
|
|
282
|
+
}
|
|
283
|
+
return await new Promise ((resolve, reject) => {
|
|
284
|
+
const child = spawn(executable, [operation], {
|
|
285
|
+
shell: false,
|
|
286
|
+
windowsHide: true,
|
|
287
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
288
|
+
env: safeChildEnvironment('windows-native')
|
|
289
|
+
});
|
|
290
|
+
const stdout = [];
|
|
291
|
+
const stderr = [];
|
|
292
|
+
let stdoutBytes = 0;
|
|
293
|
+
let stderrBytes = 0;
|
|
294
|
+
let settled = false;
|
|
295
|
+
let timedOut = false;
|
|
296
|
+
|
|
297
|
+
const finishReject = (error ) => {
|
|
298
|
+
if (settled) return;
|
|
299
|
+
settled = true;
|
|
300
|
+
reject(error);
|
|
301
|
+
};
|
|
302
|
+
child.once('error', () => finishReject(new OperatorError('WINDOWS_DPAPI_HELPER_FAILED', 'Windows DPAPI helper could not be started.')));
|
|
303
|
+
child.stdout.on('data', (chunk ) => {
|
|
304
|
+
if (stdoutBytes >= MAX_HELPER_OUTPUT_BYTES) return;
|
|
305
|
+
const slice = chunk.subarray(0, MAX_HELPER_OUTPUT_BYTES - stdoutBytes);
|
|
306
|
+
stdout.push(slice);
|
|
307
|
+
stdoutBytes += slice.length;
|
|
308
|
+
});
|
|
309
|
+
child.stderr.on('data', (chunk ) => {
|
|
310
|
+
if (stderrBytes >= MAX_HELPER_ERROR_BYTES) return;
|
|
311
|
+
const slice = chunk.subarray(0, MAX_HELPER_ERROR_BYTES - stderrBytes);
|
|
312
|
+
stderr.push(slice);
|
|
313
|
+
stderrBytes += slice.length;
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const timer = setTimeout(() => {
|
|
317
|
+
timedOut = true;
|
|
318
|
+
child.kill('SIGTERM');
|
|
319
|
+
setTimeout(() => child.kill('SIGKILL'), 1000).unref();
|
|
320
|
+
}, 15_000);
|
|
321
|
+
timer.unref();
|
|
322
|
+
|
|
323
|
+
child.once('close', (code) => {
|
|
324
|
+
clearTimeout(timer);
|
|
325
|
+
if (settled) return;
|
|
326
|
+
settled = true;
|
|
327
|
+
if (timedOut) {
|
|
328
|
+
reject(new OperatorError('WINDOWS_DPAPI_HELPER_TIMEOUT', 'Windows DPAPI helper timed out.'));
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (code !== 0) {
|
|
332
|
+
const detail = Buffer.concat(stderr).toString('utf8').trim().slice(0, 512);
|
|
333
|
+
reject(new OperatorError('WINDOWS_DPAPI_HELPER_FAILED', detail ? `Windows DPAPI helper failed: ${detail}` : 'Windows DPAPI helper failed.'));
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (stdoutBytes >= MAX_HELPER_OUTPUT_BYTES) {
|
|
337
|
+
reject(new OperatorError('WINDOWS_DPAPI_HELPER_FAILED', 'Windows DPAPI helper output exceeded the allowed size.'));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const output = Buffer.concat(stdout);
|
|
341
|
+
if (output.length === 0) {
|
|
342
|
+
output.fill(0);
|
|
343
|
+
reject(new OperatorError('WINDOWS_DPAPI_HELPER_FAILED', 'Windows DPAPI helper returned no data.'));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
resolve(output);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
child.stdin.once('error', () => {
|
|
350
|
+
if (!settled) finishReject(new OperatorError('WINDOWS_DPAPI_HELPER_FAILED', 'Windows DPAPI helper rejected secret input.'));
|
|
351
|
+
});
|
|
352
|
+
child.stdin.end(input);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function parseStoredIdentity(value ) {
|
|
357
|
+
if (!value || typeof value !== 'object') {
|
|
358
|
+
throw new OperatorError('DEVICE_IDENTITY_INVALID', 'Device identity must be an object.');
|
|
359
|
+
}
|
|
360
|
+
const raw = value ;
|
|
361
|
+
const base = {
|
|
362
|
+
deviceId: boundedString(raw.deviceId, 'deviceId', 128),
|
|
363
|
+
deviceName: boundedString(raw.deviceName, 'deviceName', 512),
|
|
364
|
+
createdAt: boundedString(raw.createdAt, 'createdAt', 128),
|
|
365
|
+
publicKeyPem: boundedString(raw.publicKeyPem, 'publicKeyPem', 16 * 1024)
|
|
366
|
+
};
|
|
367
|
+
if (raw.version === 1) {
|
|
368
|
+
return {
|
|
369
|
+
version: 1,
|
|
370
|
+
...base,
|
|
371
|
+
privateKeyPem: boundedString(raw.privateKeyPem, 'privateKeyPem', 64 * 1024)
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
if (raw.version === 2) {
|
|
375
|
+
if (!raw.privateKeyProtection || typeof raw.privateKeyProtection !== 'object') {
|
|
376
|
+
throw new OperatorError('DEVICE_IDENTITY_INVALID', 'Protected device identity is missing privateKeyProtection.');
|
|
377
|
+
}
|
|
378
|
+
const protection = raw.privateKeyProtection ;
|
|
379
|
+
if (protection.scheme !== DPAPI_SCHEME || protection.keyFormat !== 'pkcs8-der') {
|
|
380
|
+
throw new OperatorError('DEVICE_IDENTITY_INVALID', 'Protected device identity uses an unsupported protection format.');
|
|
381
|
+
}
|
|
382
|
+
const ciphertextBase64 = boundedString(protection.ciphertextBase64, 'ciphertextBase64', MAX_HELPER_OUTPUT_BYTES * 2);
|
|
383
|
+
decodeCiphertext(ciphertextBase64).fill(0);
|
|
384
|
+
return {
|
|
385
|
+
version: 2,
|
|
386
|
+
...base,
|
|
387
|
+
privateKeyProtection: {
|
|
388
|
+
scheme: DPAPI_SCHEME,
|
|
389
|
+
keyFormat: 'pkcs8-der',
|
|
390
|
+
ciphertextBase64
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
throw new OperatorError('DEVICE_IDENTITY_INVALID', 'Unsupported device identity version.');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function boundedString(value , field , maxLength ) {
|
|
398
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > maxLength || value.includes('\0')) {
|
|
399
|
+
throw new OperatorError('DEVICE_IDENTITY_INVALID', `Device identity field ${field} is invalid.`);
|
|
400
|
+
}
|
|
401
|
+
return value;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function decodeCiphertext(value ) {
|
|
405
|
+
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value) || value.length % 4 !== 0) {
|
|
406
|
+
throw new OperatorError('DEVICE_IDENTITY_INVALID', 'Protected device key ciphertext is not valid base64.');
|
|
407
|
+
}
|
|
408
|
+
const decoded = Buffer.from(value, 'base64');
|
|
409
|
+
if (decoded.length === 0 || decoded.length > MAX_HELPER_OUTPUT_BYTES || decoded.toString('base64') !== value) {
|
|
410
|
+
decoded.fill(0);
|
|
411
|
+
throw new OperatorError('DEVICE_IDENTITY_INVALID', 'Protected device key ciphertext is invalid.');
|
|
412
|
+
}
|
|
413
|
+
return decoded;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function writeExclusiveJson(file , value ) {
|
|
417
|
+
await fs.writeFile(file, JSON.stringify(value, null, 2), { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function replaceJsonAtomic(file , value ) {
|
|
421
|
+
await writeDurableStateText(file, JSON.stringify(value, null, 2), {
|
|
422
|
+
maxBytes: MAX_IDENTITY_BYTES,
|
|
423
|
+
errorCode: 'DEVICE_IDENTITY_INVALID',
|
|
424
|
+
invalidMessage: 'Device identity file is invalid.'
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function publicIdentity(stored ) {
|
|
429
|
+
return {
|
|
430
|
+
deviceId: stored.deviceId,
|
|
431
|
+
deviceName: stored.deviceName,
|
|
432
|
+
createdAt: stored.createdAt,
|
|
433
|
+
publicKeyPem: stored.publicKeyPem,
|
|
434
|
+
fingerprint: crypto.createHash('sha256').update(stored.publicKeyPem).digest('base64url')
|
|
435
|
+
};
|
|
436
|
+
}
|