@cogcoin/client 1.1.5 → 1.1.7
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 +2 -2
- package/dist/bitcoind/indexer-daemon.d.ts +3 -7
- package/dist/bitcoind/indexer-daemon.js +39 -204
- package/dist/bitcoind/managed-runtime/bitcoind-policy.d.ts +16 -0
- package/dist/bitcoind/managed-runtime/bitcoind-policy.js +177 -0
- package/dist/bitcoind/managed-runtime/bitcoind-runtime.d.ts +20 -0
- package/dist/bitcoind/managed-runtime/bitcoind-runtime.js +74 -0
- package/dist/bitcoind/managed-runtime/bitcoind-status.d.ts +11 -0
- package/dist/bitcoind/managed-runtime/bitcoind-status.js +44 -0
- package/dist/bitcoind/managed-runtime/indexer-policy.d.ts +34 -0
- package/dist/bitcoind/managed-runtime/indexer-policy.js +200 -0
- package/dist/bitcoind/managed-runtime/indexer-runtime.d.ts +15 -0
- package/dist/bitcoind/managed-runtime/indexer-runtime.js +82 -0
- package/dist/bitcoind/managed-runtime/status.d.ts +11 -0
- package/dist/bitcoind/managed-runtime/status.js +59 -0
- package/dist/bitcoind/managed-runtime/types.d.ts +77 -0
- package/dist/bitcoind/node.d.ts +2 -2
- package/dist/bitcoind/node.js +2 -2
- package/dist/bitcoind/rpc.d.ts +2 -1
- package/dist/bitcoind/rpc.js +53 -3
- package/dist/bitcoind/service.d.ts +2 -7
- package/dist/bitcoind/service.js +79 -207
- package/dist/cli/command-registry.d.ts +1 -1
- package/dist/cli/command-registry.js +2 -64
- package/dist/cli/commands/client-admin.js +3 -18
- package/dist/cli/commands/mining-runtime.js +4 -60
- package/dist/cli/commands/wallet-admin.js +6 -6
- package/dist/cli/context.js +1 -3
- package/dist/cli/mining-json.d.ts +1 -22
- package/dist/cli/mining-json.js +0 -23
- package/dist/cli/output.js +16 -2
- package/dist/cli/parse.js +0 -2
- package/dist/cli/preview-json.d.ts +1 -22
- package/dist/cli/preview-json.js +0 -19
- package/dist/cli/types.d.ts +1 -3
- package/dist/cli/wallet-format.js +1 -1
- package/dist/cli/workflow-hints.d.ts +1 -2
- package/dist/cli/workflow-hints.js +5 -8
- package/dist/wallet/lifecycle/access.d.ts +5 -0
- package/dist/wallet/lifecycle/access.js +79 -0
- package/dist/wallet/lifecycle/context.d.ts +26 -0
- package/dist/wallet/lifecycle/context.js +57 -0
- package/dist/wallet/lifecycle/managed-core.d.ts +1 -9
- package/dist/wallet/lifecycle/managed-core.js +3 -63
- package/dist/wallet/lifecycle/repair-bitcoind.d.ts +10 -0
- package/dist/wallet/lifecycle/repair-bitcoind.js +142 -0
- package/dist/wallet/lifecycle/repair-indexer.d.ts +8 -0
- package/dist/wallet/lifecycle/repair-indexer.js +117 -0
- package/dist/wallet/lifecycle/repair-mining.d.ts +1 -5
- package/dist/wallet/lifecycle/repair-mining.js +5 -39
- package/dist/wallet/lifecycle/repair.d.ts +2 -4
- package/dist/wallet/lifecycle/repair.js +74 -318
- package/dist/wallet/lifecycle/setup-prompts.d.ts +7 -0
- package/dist/wallet/lifecycle/setup-prompts.js +88 -0
- package/dist/wallet/lifecycle/setup-state.d.ts +26 -0
- package/dist/wallet/lifecycle/setup-state.js +159 -0
- package/dist/wallet/lifecycle/setup.d.ts +3 -4
- package/dist/wallet/lifecycle/setup.js +47 -351
- package/dist/wallet/lifecycle/types.d.ts +33 -5
- package/dist/wallet/managed-core-wallet.d.ts +2 -0
- package/dist/wallet/managed-core-wallet.js +27 -1
- package/dist/wallet/mining/candidate.d.ts +1 -0
- package/dist/wallet/mining/candidate.js +38 -6
- package/dist/wallet/mining/competitiveness.d.ts +1 -0
- package/dist/wallet/mining/competitiveness.js +6 -0
- package/dist/wallet/mining/cycle.d.ts +2 -0
- package/dist/wallet/mining/cycle.js +14 -4
- package/dist/wallet/mining/engine-types.d.ts +1 -0
- package/dist/wallet/mining/index.d.ts +1 -1
- package/dist/wallet/mining/index.js +1 -1
- package/dist/wallet/mining/publish.d.ts +3 -0
- package/dist/wallet/mining/publish.js +78 -6
- package/dist/wallet/mining/runner.d.ts +0 -32
- package/dist/wallet/mining/runner.js +59 -104
- package/dist/wallet/mining/stop.d.ts +7 -0
- package/dist/wallet/mining/stop.js +23 -0
- package/dist/wallet/mining/supervisor.d.ts +2 -36
- package/dist/wallet/mining/supervisor.js +139 -246
- package/dist/wallet/read/context.d.ts +1 -5
- package/dist/wallet/read/context.js +20 -379
- package/dist/wallet/read/managed-services.d.ts +33 -0
- package/dist/wallet/read/managed-services.js +222 -0
- package/dist/wallet/state/client-password/bootstrap.d.ts +2 -0
- package/dist/wallet/state/client-password/bootstrap.js +3 -0
- package/dist/wallet/state/client-password/context.d.ts +10 -0
- package/dist/wallet/state/client-password/context.js +46 -0
- package/dist/wallet/state/client-password/crypto.d.ts +34 -0
- package/dist/wallet/state/client-password/crypto.js +117 -0
- package/dist/wallet/state/client-password/files.d.ts +10 -0
- package/dist/wallet/state/client-password/files.js +109 -0
- package/dist/wallet/state/client-password/legacy-cleanup.d.ts +11 -0
- package/dist/wallet/state/client-password/legacy-cleanup.js +338 -0
- package/dist/wallet/state/client-password/messages.d.ts +3 -0
- package/dist/wallet/state/client-password/messages.js +9 -0
- package/dist/wallet/state/client-password/migration.d.ts +4 -0
- package/dist/wallet/state/client-password/migration.js +32 -0
- package/dist/wallet/state/client-password/prompts.d.ts +12 -0
- package/dist/wallet/state/client-password/prompts.js +79 -0
- package/dist/wallet/state/client-password/protected-secrets.d.ts +13 -0
- package/dist/wallet/state/client-password/protected-secrets.js +90 -0
- package/dist/wallet/state/client-password/readiness.d.ts +4 -0
- package/dist/wallet/state/client-password/readiness.js +48 -0
- package/dist/wallet/state/client-password/references.d.ts +1 -0
- package/dist/wallet/state/client-password/references.js +56 -0
- package/dist/wallet/state/client-password/rotation.d.ts +6 -0
- package/dist/wallet/state/client-password/rotation.js +98 -0
- package/dist/wallet/state/client-password/session-policy.d.ts +6 -0
- package/dist/wallet/state/client-password/session-policy.js +28 -0
- package/dist/wallet/state/client-password/session.d.ts +19 -0
- package/dist/wallet/state/client-password/session.js +170 -0
- package/dist/wallet/state/client-password/setup.d.ts +8 -0
- package/dist/wallet/state/client-password/setup.js +49 -0
- package/dist/wallet/state/client-password/types.d.ts +82 -0
- package/dist/wallet/state/client-password/types.js +5 -0
- package/dist/wallet/state/client-password.d.ts +7 -38
- package/dist/wallet/state/client-password.js +52 -937
- package/dist/wallet/tx/anchor.js +123 -216
- package/dist/wallet/tx/cog.js +294 -489
- package/dist/wallet/tx/common.d.ts +2 -0
- package/dist/wallet/tx/common.js +2 -0
- package/dist/wallet/tx/domain-admin.js +111 -220
- package/dist/wallet/tx/domain-market.js +401 -681
- package/dist/wallet/tx/executor.d.ts +176 -0
- package/dist/wallet/tx/executor.js +302 -0
- package/dist/wallet/tx/field.js +109 -215
- package/dist/wallet/tx/register.js +158 -269
- package/dist/wallet/tx/reputation.js +120 -227
- package/package.json +1 -1
- package/dist/wallet/mining/worker-main.js +0 -17
- package/dist/wallet/state/client-password-agent.d.ts +0 -1
- package/dist/wallet/state/client-password-agent.js +0 -211
- /package/dist/{wallet/mining/worker-main.d.ts → bitcoind/managed-runtime/types.js} +0 -0
|
@@ -1,959 +1,74 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export const CLIENT_PASSWORD_SETUP_AUTO_UNLOCK_SECONDS = 86_400;
|
|
18
|
-
const CLIENT_PASSWORD_DERIVED_KEY_BYTES = 32;
|
|
19
|
-
const CLIENT_PASSWORD_KDF = {
|
|
20
|
-
memoryKib: 65_536,
|
|
21
|
-
iterations: 3,
|
|
22
|
-
parallelism: 1,
|
|
23
|
-
};
|
|
24
|
-
function sanitizeSecretKeyId(keyId) {
|
|
25
|
-
return keyId.replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
26
|
-
}
|
|
27
|
-
export function resolveLocalSecretFilePath(directoryPath, keyId) {
|
|
28
|
-
return join(directoryPath, `${sanitizeSecretKeyId(keyId)}.secret`);
|
|
29
|
-
}
|
|
30
|
-
function resolveClientPasswordStatePath(directoryPath) {
|
|
31
|
-
return join(directoryPath, "client-password.json");
|
|
32
|
-
}
|
|
33
|
-
function resolveClientPasswordRotationJournalPath(directoryPath) {
|
|
34
|
-
return join(directoryPath, "client-password-rotation.json");
|
|
35
|
-
}
|
|
36
|
-
function resolveAgentEndpoint(stateRoot) {
|
|
37
|
-
const hash = createHash("sha256").update(stateRoot).digest("hex").slice(0, 24);
|
|
38
|
-
// Wallet provider tests simulate foreign platforms, but the local agent transport
|
|
39
|
-
// still has to follow the real host runtime.
|
|
40
|
-
if (process.platform === "win32") {
|
|
41
|
-
return `\\\\.\\pipe\\cogcoin-client-password-${hash}`;
|
|
42
|
-
}
|
|
43
|
-
return join(tmpdir(), `cogcoin-client-password-${hash}.sock`);
|
|
44
|
-
}
|
|
45
|
-
function shouldRemoveAgentEndpointPath(endpoint) {
|
|
46
|
-
return !endpoint.startsWith("\\\\.\\pipe\\");
|
|
47
|
-
}
|
|
48
|
-
function isMissingFileError(error) {
|
|
49
|
-
return error instanceof Error
|
|
50
|
-
&& "code" in error
|
|
51
|
-
&& error.code === "ENOENT";
|
|
52
|
-
}
|
|
53
|
-
function createRuntimeError(code, cause) {
|
|
54
|
-
return cause === undefined ? new Error(code) : new Error(code, { cause });
|
|
55
|
-
}
|
|
56
|
-
function isClientPasswordStateV1(value) {
|
|
57
|
-
return value !== null
|
|
58
|
-
&& typeof value === "object"
|
|
59
|
-
&& value.format === CLIENT_PASSWORD_STATE_FORMAT
|
|
60
|
-
&& value.version === 1
|
|
61
|
-
&& typeof value.passwordHint === "string"
|
|
62
|
-
&& value.kdf?.name === "argon2id"
|
|
63
|
-
&& typeof value.verifier?.nonce === "string"
|
|
64
|
-
&& typeof value.verifier?.tag === "string"
|
|
65
|
-
&& typeof value.verifier?.ciphertext === "string";
|
|
66
|
-
}
|
|
67
|
-
function isWrappedSecretEnvelope(value) {
|
|
68
|
-
return value !== null
|
|
69
|
-
&& typeof value === "object"
|
|
70
|
-
&& value.format === LOCAL_SECRET_ENVELOPE_FORMAT
|
|
71
|
-
&& value.version === 1
|
|
72
|
-
&& value.cipher === "aes-256-gcm"
|
|
73
|
-
&& value.wrappedBy === "client-password"
|
|
74
|
-
&& typeof value.nonce === "string"
|
|
75
|
-
&& typeof value.tag === "string"
|
|
76
|
-
&& typeof value.ciphertext === "string";
|
|
77
|
-
}
|
|
78
|
-
function isClientPasswordRotationJournalV1(value) {
|
|
79
|
-
return value !== null
|
|
80
|
-
&& typeof value === "object"
|
|
81
|
-
&& value.format === CLIENT_PASSWORD_ROTATION_JOURNAL_FORMAT
|
|
82
|
-
&& value.version === 1
|
|
83
|
-
&& isClientPasswordStateV1(value.nextState)
|
|
84
|
-
&& Array.isArray(value.secrets)
|
|
85
|
-
&& (value.secrets).every((entry) => (entry !== null
|
|
86
|
-
&& typeof entry === "object"
|
|
87
|
-
&& typeof entry.keyId === "string"
|
|
88
|
-
&& entry.keyId.trim().length > 0
|
|
89
|
-
&& isWrappedSecretEnvelope(entry.envelope)));
|
|
90
|
-
}
|
|
91
|
-
async function readLocalSecretFile(path) {
|
|
92
|
-
try {
|
|
93
|
-
const raw = await readFile(path, "utf8");
|
|
94
|
-
const trimmed = raw.trim();
|
|
95
|
-
try {
|
|
96
|
-
const parsed = JSON.parse(trimmed);
|
|
97
|
-
if (isWrappedSecretEnvelope(parsed)) {
|
|
98
|
-
return {
|
|
99
|
-
state: "wrapped",
|
|
100
|
-
envelope: parsed,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
// Legacy local secrets were raw base64 bytes.
|
|
106
|
-
}
|
|
107
|
-
return {
|
|
108
|
-
state: "raw",
|
|
109
|
-
secret: new Uint8Array(Buffer.from(trimmed, "base64")),
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
catch (error) {
|
|
113
|
-
if (isMissingFileError(error)) {
|
|
114
|
-
return { state: "missing" };
|
|
115
|
-
}
|
|
116
|
-
throw error;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
async function loadClientPasswordStateOrNull(path) {
|
|
120
|
-
try {
|
|
121
|
-
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
122
|
-
if (!isClientPasswordStateV1(parsed)) {
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
return parsed;
|
|
126
|
-
}
|
|
127
|
-
catch (error) {
|
|
128
|
-
if (isMissingFileError(error)) {
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
async function loadClientPasswordRotationJournalOrNull(path) {
|
|
135
|
-
try {
|
|
136
|
-
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
137
|
-
if (!isClientPasswordRotationJournalV1(parsed)) {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
return parsed;
|
|
141
|
-
}
|
|
142
|
-
catch (error) {
|
|
143
|
-
if (isMissingFileError(error)) {
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
async function derivePasswordKey(passwordBytes, saltBytes) {
|
|
150
|
-
return Buffer.from(await argon2idAsync(passwordBytes, saltBytes, {
|
|
151
|
-
m: CLIENT_PASSWORD_KDF.memoryKib,
|
|
152
|
-
t: CLIENT_PASSWORD_KDF.iterations,
|
|
153
|
-
p: CLIENT_PASSWORD_KDF.parallelism,
|
|
154
|
-
dkLen: CLIENT_PASSWORD_DERIVED_KEY_BYTES,
|
|
155
|
-
}));
|
|
156
|
-
}
|
|
157
|
-
function zeroizeBuffer(buffer) {
|
|
158
|
-
if (buffer != null) {
|
|
159
|
-
buffer.fill(0);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
async function createClientPasswordState(options) {
|
|
163
|
-
const salt = randomBytes(16);
|
|
164
|
-
const derivedKey = await derivePasswordKey(options.passwordBytes, salt);
|
|
165
|
-
const verifier = encryptBytesWithKey(Buffer.from(CLIENT_PASSWORD_VERIFIER_TEXT, "utf8"), derivedKey, {
|
|
166
|
-
format: CLIENT_PASSWORD_VERIFIER_FORMAT,
|
|
167
|
-
wrappedBy: "client-password-verifier",
|
|
168
|
-
});
|
|
169
|
-
return {
|
|
170
|
-
state: {
|
|
171
|
-
format: CLIENT_PASSWORD_STATE_FORMAT,
|
|
172
|
-
version: 1,
|
|
173
|
-
passwordHint: options.passwordHint,
|
|
174
|
-
kdf: {
|
|
175
|
-
name: "argon2id",
|
|
176
|
-
memoryKib: CLIENT_PASSWORD_KDF.memoryKib,
|
|
177
|
-
iterations: CLIENT_PASSWORD_KDF.iterations,
|
|
178
|
-
parallelism: CLIENT_PASSWORD_KDF.parallelism,
|
|
179
|
-
salt: salt.toString("base64"),
|
|
180
|
-
},
|
|
181
|
-
verifier: {
|
|
182
|
-
cipher: "aes-256-gcm",
|
|
183
|
-
nonce: verifier.nonce,
|
|
184
|
-
tag: verifier.tag,
|
|
185
|
-
ciphertext: verifier.ciphertext,
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
derivedKey,
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
function createWrappedSecretEnvelope(secret, derivedKey) {
|
|
192
|
-
const envelope = encryptBytesWithKey(secret, derivedKey, {
|
|
193
|
-
format: LOCAL_SECRET_ENVELOPE_FORMAT,
|
|
194
|
-
wrappedBy: "client-password",
|
|
195
|
-
});
|
|
196
|
-
return {
|
|
197
|
-
format: LOCAL_SECRET_ENVELOPE_FORMAT,
|
|
198
|
-
version: 1,
|
|
199
|
-
cipher: "aes-256-gcm",
|
|
200
|
-
wrappedBy: "client-password",
|
|
201
|
-
nonce: envelope.nonce,
|
|
202
|
-
tag: envelope.tag,
|
|
203
|
-
ciphertext: envelope.ciphertext,
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
async function verifyPassword(options) {
|
|
207
|
-
const derivedKey = await derivePasswordKey(options.passwordBytes, Buffer.from(options.state.kdf.salt, "base64"));
|
|
208
|
-
try {
|
|
209
|
-
const plaintext = decryptBytesWithKey({
|
|
210
|
-
format: CLIENT_PASSWORD_VERIFIER_FORMAT,
|
|
211
|
-
version: 1,
|
|
212
|
-
cipher: "aes-256-gcm",
|
|
213
|
-
wrappedBy: "client-password-verifier",
|
|
214
|
-
nonce: options.state.verifier.nonce,
|
|
215
|
-
tag: options.state.verifier.tag,
|
|
216
|
-
ciphertext: options.state.verifier.ciphertext,
|
|
217
|
-
}, derivedKey);
|
|
218
|
-
if (plaintext.toString("utf8") !== CLIENT_PASSWORD_VERIFIER_TEXT) {
|
|
219
|
-
zeroizeBuffer(derivedKey);
|
|
220
|
-
return null;
|
|
221
|
-
}
|
|
222
|
-
return derivedKey;
|
|
223
|
-
}
|
|
224
|
-
catch {
|
|
225
|
-
zeroizeBuffer(derivedKey);
|
|
226
|
-
return null;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
async function collectWalletStateRoots(stateRoot) {
|
|
230
|
-
const roots = [stateRoot];
|
|
231
|
-
const seedsRoot = join(stateRoot, "seeds");
|
|
232
|
-
try {
|
|
233
|
-
const entries = await readdir(seedsRoot, { withFileTypes: true });
|
|
234
|
-
for (const entry of entries) {
|
|
235
|
-
if (entry.isDirectory()) {
|
|
236
|
-
roots.push(join(seedsRoot, entry.name));
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
catch (error) {
|
|
241
|
-
if (!isMissingFileError(error)) {
|
|
242
|
-
throw error;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
return roots;
|
|
246
|
-
}
|
|
247
|
-
async function readReferencedSecretIdsFromWalletStateRoot(walletStateRoot) {
|
|
248
|
-
const ids = new Set();
|
|
249
|
-
const candidatePaths = [
|
|
250
|
-
join(walletStateRoot, "wallet-state.enc"),
|
|
251
|
-
join(walletStateRoot, "wallet-state.enc.bak"),
|
|
252
|
-
join(walletStateRoot, "wallet-init-pending.enc"),
|
|
253
|
-
join(walletStateRoot, "wallet-init-pending.enc.bak"),
|
|
254
|
-
];
|
|
255
|
-
for (const candidatePath of candidatePaths) {
|
|
256
|
-
try {
|
|
257
|
-
const parsed = JSON.parse(await readFile(candidatePath, "utf8"));
|
|
258
|
-
const keyId = parsed.secretProvider?.keyId?.trim() ?? "";
|
|
259
|
-
if (keyId.length > 0) {
|
|
260
|
-
ids.add(keyId);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
catch (error) {
|
|
264
|
-
if (!isMissingFileError(error)) {
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
return ids;
|
|
270
|
-
}
|
|
271
|
-
async function collectReferencedSecretIds(stateRoot) {
|
|
272
|
-
const ids = new Set();
|
|
273
|
-
const roots = await collectWalletStateRoots(stateRoot);
|
|
274
|
-
for (const root of roots) {
|
|
275
|
-
const rootIds = await readReferencedSecretIdsFromWalletStateRoot(root);
|
|
276
|
-
for (const keyId of rootIds) {
|
|
277
|
-
ids.add(keyId);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
return [...ids].sort((left, right) => left.localeCompare(right));
|
|
281
|
-
}
|
|
282
|
-
async function finalizePendingClientPasswordRotationIfNeeded(options) {
|
|
283
|
-
const journal = await loadClientPasswordRotationJournalOrNull(resolveClientPasswordRotationJournalPath(options.directoryPath));
|
|
284
|
-
if (journal === null) {
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
await mkdir(options.directoryPath, { recursive: true, mode: 0o700 });
|
|
288
|
-
for (const secretEntry of journal.secrets) {
|
|
289
|
-
await writeJsonFileAtomic(resolveLocalSecretFilePath(options.directoryPath, secretEntry.keyId), secretEntry.envelope, { mode: 0o600 });
|
|
290
|
-
}
|
|
291
|
-
await writeJsonFileAtomic(resolveClientPasswordStatePath(options.directoryPath), journal.nextState, { mode: 0o600 });
|
|
292
|
-
await rm(resolveClientPasswordRotationJournalPath(options.directoryPath), { force: true });
|
|
293
|
-
}
|
|
294
|
-
async function legacyMacKeychainHasSecret(options, keyId) {
|
|
295
|
-
if (options.platform !== "darwin" || options.legacyMacKeychainReader == null) {
|
|
296
|
-
return false;
|
|
297
|
-
}
|
|
298
|
-
try {
|
|
299
|
-
await options.legacyMacKeychainReader.loadSecret(keyId);
|
|
300
|
-
return true;
|
|
301
|
-
}
|
|
302
|
-
catch {
|
|
303
|
-
return false;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
async function inspectReadinessForKey(options, keyId) {
|
|
307
|
-
const local = await readLocalSecretFile(resolveLocalSecretFilePath(options.directoryPath, keyId));
|
|
308
|
-
const keychain = await legacyMacKeychainHasSecret(options, keyId);
|
|
309
|
-
return { local, keychain };
|
|
1
|
+
export { CLIENT_PASSWORD_SETUP_AUTO_UNLOCK_SECONDS } from "./client-password/crypto.js";
|
|
2
|
+
export { resolveLocalSecretFilePath, createLegacyKeychainServiceName, } from "./client-password/context.js";
|
|
3
|
+
export { createAgentBootstrapState } from "./client-password/bootstrap.js";
|
|
4
|
+
export { describeClientPasswordLockedMessage, describeClientPasswordMigrationMessage, describeClientPasswordSetupMessage, } from "./client-password/messages.js";
|
|
5
|
+
export { listLocalSecretFilesForTesting } from "./client-password/files.js";
|
|
6
|
+
import { resolveClientPasswordContext } from "./client-password/context.js";
|
|
7
|
+
import { cleanupLegacyClientPasswordArtifactsResolved } from "./client-password/legacy-cleanup.js";
|
|
8
|
+
import { inspectClientPasswordReadinessResolved } from "./client-password/readiness.js";
|
|
9
|
+
import { lockClientPasswordSessionResolved, readClientPasswordSessionStatusResolved, unlockClientPasswordSessionResolved, } from "./client-password/session.js";
|
|
10
|
+
import { ensureClientPasswordConfiguredResolved } from "./client-password/setup.js";
|
|
11
|
+
import { deleteClientProtectedSecretResolved, loadClientProtectedSecretResolved, storeClientProtectedSecretResolved, } from "./client-password/protected-secrets.js";
|
|
12
|
+
import { changeClientPasswordResolved, finalizePendingClientPasswordRotationIfNeeded, } from "./client-password/rotation.js";
|
|
13
|
+
async function resolveCleanClientPasswordContext(options) {
|
|
14
|
+
const context = resolveClientPasswordContext(options);
|
|
15
|
+
await cleanupLegacyClientPasswordArtifactsResolved(context);
|
|
16
|
+
return context;
|
|
310
17
|
}
|
|
311
18
|
export async function inspectClientPasswordReadiness(options) {
|
|
312
|
-
await
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if (keyIds.length === 0) {
|
|
316
|
-
return passwordState === null ? "setup-required" : "ready";
|
|
317
|
-
}
|
|
318
|
-
for (const keyId of keyIds) {
|
|
319
|
-
const sourceState = await inspectReadinessForKey(options, keyId);
|
|
320
|
-
if (passwordState === null) {
|
|
321
|
-
if (sourceState.local.state === "raw" || sourceState.keychain) {
|
|
322
|
-
return "migration-required";
|
|
323
|
-
}
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
if (sourceState.local.state === "raw") {
|
|
327
|
-
return "migration-required";
|
|
328
|
-
}
|
|
329
|
-
if (sourceState.local.state === "missing" && sourceState.keychain) {
|
|
330
|
-
return "migration-required";
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
return passwordState === null ? "setup-required" : "ready";
|
|
334
|
-
}
|
|
335
|
-
function describeReadinessError(readiness) {
|
|
336
|
-
return readiness === "migration-required"
|
|
337
|
-
? "wallet_client_password_migration_required"
|
|
338
|
-
: "wallet_client_password_setup_required";
|
|
339
|
-
}
|
|
340
|
-
async function openAgentConnection(endpoint) {
|
|
341
|
-
return await new Promise((resolve, reject) => {
|
|
342
|
-
const socket = net.createConnection(endpoint);
|
|
343
|
-
const cleanup = () => {
|
|
344
|
-
socket.off("connect", onConnect);
|
|
345
|
-
socket.off("error", onError);
|
|
346
|
-
};
|
|
347
|
-
const onConnect = () => {
|
|
348
|
-
cleanup();
|
|
349
|
-
resolve(socket);
|
|
350
|
-
};
|
|
351
|
-
const onError = (error) => {
|
|
352
|
-
cleanup();
|
|
353
|
-
reject(error);
|
|
354
|
-
};
|
|
355
|
-
socket.on("connect", onConnect);
|
|
356
|
-
socket.on("error", onError);
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
async function requestAgent(options, request) {
|
|
360
|
-
const endpoint = resolveAgentEndpoint(options.stateRoot);
|
|
361
|
-
const socket = await openAgentConnection(endpoint);
|
|
362
|
-
return await new Promise((resolve, reject) => {
|
|
363
|
-
let received = "";
|
|
364
|
-
const cleanup = () => {
|
|
365
|
-
socket.off("data", onData);
|
|
366
|
-
socket.off("error", onError);
|
|
367
|
-
socket.off("end", onEnd);
|
|
368
|
-
socket.off("close", onClose);
|
|
369
|
-
};
|
|
370
|
-
const finish = (response) => {
|
|
371
|
-
cleanup();
|
|
372
|
-
socket.end();
|
|
373
|
-
resolve(response);
|
|
374
|
-
};
|
|
375
|
-
const fail = (error) => {
|
|
376
|
-
cleanup();
|
|
377
|
-
socket.destroy();
|
|
378
|
-
reject(error);
|
|
379
|
-
};
|
|
380
|
-
const onData = (chunk) => {
|
|
381
|
-
received += chunk.toString("utf8");
|
|
382
|
-
const newlineIndex = received.indexOf("\n");
|
|
383
|
-
if (newlineIndex === -1) {
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
try {
|
|
387
|
-
finish(JSON.parse(received.slice(0, newlineIndex)));
|
|
388
|
-
}
|
|
389
|
-
catch (error) {
|
|
390
|
-
fail(error instanceof Error ? error : new Error(String(error)));
|
|
391
|
-
}
|
|
392
|
-
};
|
|
393
|
-
const onError = (error) => {
|
|
394
|
-
fail(error);
|
|
395
|
-
};
|
|
396
|
-
const onEnd = () => {
|
|
397
|
-
if (received.length === 0) {
|
|
398
|
-
fail(new Error("wallet_client_password_locked"));
|
|
399
|
-
}
|
|
400
|
-
};
|
|
401
|
-
const onClose = () => {
|
|
402
|
-
if (received.length === 0) {
|
|
403
|
-
fail(new Error("wallet_client_password_locked"));
|
|
404
|
-
}
|
|
405
|
-
};
|
|
406
|
-
socket.on("data", onData);
|
|
407
|
-
socket.on("error", onError);
|
|
408
|
-
socket.on("end", onEnd);
|
|
409
|
-
socket.on("close", onClose);
|
|
410
|
-
socket.write(`${JSON.stringify(request)}\n`);
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
async function requestAgentOrNull(options, request) {
|
|
414
|
-
try {
|
|
415
|
-
return await requestAgent(options, request);
|
|
416
|
-
}
|
|
417
|
-
catch (error) {
|
|
418
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
419
|
-
if (message === "wallet_client_password_locked") {
|
|
420
|
-
return null;
|
|
421
|
-
}
|
|
422
|
-
const code = error instanceof Error && "code" in error
|
|
423
|
-
? String(error.code ?? "")
|
|
424
|
-
: "";
|
|
425
|
-
if (code === "ENOENT" || code === "ECONNREFUSED" || code === "ECONNRESET" || code === "EPIPE") {
|
|
426
|
-
const endpoint = resolveAgentEndpoint(options.stateRoot);
|
|
427
|
-
if (shouldRemoveAgentEndpointPath(endpoint)) {
|
|
428
|
-
await rm(endpoint, { force: true }).catch(() => undefined);
|
|
429
|
-
}
|
|
430
|
-
return null;
|
|
431
|
-
}
|
|
432
|
-
throw error;
|
|
433
|
-
}
|
|
19
|
+
const context = await resolveCleanClientPasswordContext(options);
|
|
20
|
+
await finalizePendingClientPasswordRotationIfNeeded(context);
|
|
21
|
+
return await inspectClientPasswordReadinessResolved(context);
|
|
434
22
|
}
|
|
435
23
|
export async function readClientPasswordSessionStatus(options) {
|
|
436
|
-
|
|
437
|
-
if (response === null || !response.ok) {
|
|
438
|
-
return {
|
|
439
|
-
unlocked: false,
|
|
440
|
-
unlockUntilUnixMs: null,
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
return {
|
|
444
|
-
unlocked: true,
|
|
445
|
-
unlockUntilUnixMs: response.unlockUntilUnixMs ?? null,
|
|
446
|
-
};
|
|
24
|
+
return await readClientPasswordSessionStatusResolved(await resolveCleanClientPasswordContext(options));
|
|
447
25
|
}
|
|
448
26
|
export async function lockClientPasswordSession(options) {
|
|
449
|
-
await
|
|
450
|
-
const endpoint = resolveAgentEndpoint(options.stateRoot);
|
|
451
|
-
if (shouldRemoveAgentEndpointPath(endpoint)) {
|
|
452
|
-
await rm(endpoint, { force: true }).catch(() => undefined);
|
|
453
|
-
}
|
|
454
|
-
return {
|
|
455
|
-
unlocked: false,
|
|
456
|
-
unlockUntilUnixMs: null,
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
async function waitForAgentReady(child) {
|
|
460
|
-
const stdout = child.stdout;
|
|
461
|
-
if (stdout == null) {
|
|
462
|
-
throw new Error("wallet_client_password_agent_start_failed");
|
|
463
|
-
}
|
|
464
|
-
await new Promise((resolve, reject) => {
|
|
465
|
-
let received = "";
|
|
466
|
-
const cleanup = () => {
|
|
467
|
-
stdout.off("data", onData);
|
|
468
|
-
child.off("exit", onExit);
|
|
469
|
-
child.off("error", onError);
|
|
470
|
-
};
|
|
471
|
-
const onData = (chunk) => {
|
|
472
|
-
received += chunk.toString("utf8");
|
|
473
|
-
const newlineIndex = received.indexOf("\n");
|
|
474
|
-
if (newlineIndex === -1) {
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
cleanup();
|
|
478
|
-
if (received.slice(0, newlineIndex).trim() === "ready") {
|
|
479
|
-
resolve();
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
reject(new Error("wallet_client_password_agent_start_failed"));
|
|
483
|
-
};
|
|
484
|
-
const onExit = () => {
|
|
485
|
-
cleanup();
|
|
486
|
-
reject(new Error("wallet_client_password_agent_start_failed"));
|
|
487
|
-
};
|
|
488
|
-
const onError = (error) => {
|
|
489
|
-
cleanup();
|
|
490
|
-
reject(error);
|
|
491
|
-
};
|
|
492
|
-
stdout.on("data", onData);
|
|
493
|
-
child.on("exit", onExit);
|
|
494
|
-
child.on("error", onError);
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
function releaseAgentBootstrapHandles(child) {
|
|
498
|
-
child.stdin?.destroy();
|
|
499
|
-
child.stdout?.destroy();
|
|
500
|
-
}
|
|
501
|
-
async function startClientPasswordSession(options) {
|
|
502
|
-
return await startClientPasswordSessionWithExpiry({
|
|
503
|
-
...options,
|
|
504
|
-
unlockUntilUnixMs: Date.now() + (options.unlockDurationSeconds * 1_000),
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
async function startClientPasswordSessionWithExpiry(options) {
|
|
508
|
-
const unlockUntilUnixMs = options.unlockUntilUnixMs;
|
|
509
|
-
const endpoint = resolveAgentEndpoint(options.stateRoot);
|
|
510
|
-
await lockClientPasswordSession(options).catch(() => undefined);
|
|
511
|
-
await mkdir(options.runtimeRoot, { recursive: true }).catch(() => undefined);
|
|
512
|
-
const child = spawn(process.execPath, [fileURLToPath(new URL("./client-password-agent.js", import.meta.url)), endpoint, String(unlockUntilUnixMs)], {
|
|
513
|
-
detached: true,
|
|
514
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
515
|
-
});
|
|
516
|
-
try {
|
|
517
|
-
child.stdin?.end(`${JSON.stringify({
|
|
518
|
-
derivedKeyBase64: options.derivedKey.toString("base64"),
|
|
519
|
-
})}\n`);
|
|
520
|
-
await waitForAgentReady(child);
|
|
521
|
-
}
|
|
522
|
-
catch (error) {
|
|
523
|
-
child.kill();
|
|
524
|
-
throw error;
|
|
525
|
-
}
|
|
526
|
-
finally {
|
|
527
|
-
releaseAgentBootstrapHandles(child);
|
|
528
|
-
zeroizeBuffer(options.derivedKey);
|
|
529
|
-
}
|
|
530
|
-
child.unref();
|
|
531
|
-
return {
|
|
532
|
-
unlocked: true,
|
|
533
|
-
unlockUntilUnixMs,
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
async function promptForHiddenValue(prompt, message) {
|
|
537
|
-
const value = prompt.promptHidden != null
|
|
538
|
-
? await prompt.promptHidden(message)
|
|
539
|
-
: await prompt.prompt(message);
|
|
540
|
-
return value.trim();
|
|
541
|
-
}
|
|
542
|
-
async function promptForUnlockDuration(prompt) {
|
|
543
|
-
return await promptForUnlockDurationWithDefault(prompt, CLIENT_PASSWORD_MANUAL_UNLOCK_SECONDS);
|
|
544
|
-
}
|
|
545
|
-
async function promptForUnlockDurationWithDefault(prompt, defaultSeconds) {
|
|
546
|
-
while (true) {
|
|
547
|
-
const answer = (await prompt.prompt(`Unlock duration in seconds [${defaultSeconds}]: `)).trim();
|
|
548
|
-
if (answer === "") {
|
|
549
|
-
return defaultSeconds;
|
|
550
|
-
}
|
|
551
|
-
if (/^[1-9]\d*$/.test(answer)) {
|
|
552
|
-
return Number(answer);
|
|
553
|
-
}
|
|
554
|
-
prompt.writeLine("Enter a whole-number duration in seconds.");
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
function resolveRemainingUnlockSeconds(status) {
|
|
558
|
-
if (status.unlockUntilUnixMs === null) {
|
|
559
|
-
return CLIENT_PASSWORD_MANUAL_UNLOCK_SECONDS;
|
|
560
|
-
}
|
|
561
|
-
return Math.max(1, Math.ceil((status.unlockUntilUnixMs - Date.now()) / 1_000));
|
|
562
|
-
}
|
|
563
|
-
function resolvePostChangeUnlockUntilUnixMs(status) {
|
|
564
|
-
if (status.unlocked && status.unlockUntilUnixMs != null) {
|
|
565
|
-
return status.unlockUntilUnixMs;
|
|
566
|
-
}
|
|
567
|
-
return Date.now() + (CLIENT_PASSWORD_SETUP_AUTO_UNLOCK_SECONDS * 1_000);
|
|
568
|
-
}
|
|
569
|
-
async function refreshClientPasswordSession(options) {
|
|
570
|
-
const response = await requestAgentOrNull(options, {
|
|
571
|
-
command: "refresh",
|
|
572
|
-
unlockUntilUnixMs: options.unlockUntilUnixMs,
|
|
573
|
-
});
|
|
574
|
-
if (response === null || !response.ok) {
|
|
575
|
-
return null;
|
|
576
|
-
}
|
|
577
|
-
return {
|
|
578
|
-
unlocked: true,
|
|
579
|
-
unlockUntilUnixMs: response.unlockUntilUnixMs ?? options.unlockUntilUnixMs,
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
|
-
async function unlockClientPasswordSessionWithPrompt(options) {
|
|
583
|
-
const derivedKey = await promptForVerifiedClientPassword({
|
|
584
|
-
...options,
|
|
585
|
-
promptMessage: "Client password: ",
|
|
586
|
-
ttyErrorCode: "wallet_client_password_unlock_requires_tty",
|
|
587
|
-
});
|
|
588
|
-
const unlockDurationSeconds = await promptForUnlockDuration(options.prompt);
|
|
589
|
-
return await startClientPasswordSession({
|
|
590
|
-
...options,
|
|
591
|
-
derivedKey,
|
|
592
|
-
unlockDurationSeconds,
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
async function promptForVerifiedClientPassword(options) {
|
|
596
|
-
const readiness = await inspectClientPasswordReadiness(options);
|
|
597
|
-
if (readiness !== "ready") {
|
|
598
|
-
throw new Error(describeReadinessError(readiness));
|
|
599
|
-
}
|
|
600
|
-
if (!options.prompt.isInteractive) {
|
|
601
|
-
throw new Error(options.ttyErrorCode);
|
|
602
|
-
}
|
|
603
|
-
const state = await loadClientPasswordStateOrNull(resolveClientPasswordStatePath(options.directoryPath));
|
|
604
|
-
if (state === null) {
|
|
605
|
-
throw new Error("wallet_client_password_setup_required");
|
|
606
|
-
}
|
|
607
|
-
let attempts = 0;
|
|
608
|
-
while (true) {
|
|
609
|
-
if (attempts >= 2 && state.passwordHint.trim().length > 0) {
|
|
610
|
-
options.prompt.writeLine(`Hint: ${state.passwordHint}`);
|
|
611
|
-
}
|
|
612
|
-
const passwordText = await promptForHiddenValue(options.prompt, options.promptMessage);
|
|
613
|
-
const passwordBytes = Buffer.from(passwordText, "utf8");
|
|
614
|
-
let derivedKey = null;
|
|
615
|
-
try {
|
|
616
|
-
derivedKey = await verifyPassword({
|
|
617
|
-
state,
|
|
618
|
-
passwordBytes,
|
|
619
|
-
});
|
|
620
|
-
}
|
|
621
|
-
finally {
|
|
622
|
-
zeroizeBuffer(passwordBytes);
|
|
623
|
-
}
|
|
624
|
-
if (derivedKey !== null) {
|
|
625
|
-
return derivedKey;
|
|
626
|
-
}
|
|
627
|
-
attempts += 1;
|
|
628
|
-
options.prompt.writeLine("Incorrect client password.");
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
async function writeWrappedSecret(options) {
|
|
632
|
-
const envelope = createWrappedSecretEnvelope(options.secret, options.derivedKey);
|
|
633
|
-
await writeJsonFileAtomic(options.path, envelope, { mode: 0o600 });
|
|
634
|
-
}
|
|
635
|
-
async function migrateReferencedSecrets(options) {
|
|
636
|
-
const keyIds = await collectReferencedSecretIds(options.stateRoot);
|
|
637
|
-
let migrated = false;
|
|
638
|
-
for (const keyId of keyIds) {
|
|
639
|
-
const localPath = resolveLocalSecretFilePath(options.directoryPath, keyId);
|
|
640
|
-
const localState = await readLocalSecretFile(localPath);
|
|
641
|
-
if (localState.state === "wrapped") {
|
|
642
|
-
continue;
|
|
643
|
-
}
|
|
644
|
-
if (localState.state === "raw") {
|
|
645
|
-
await writeWrappedSecret({
|
|
646
|
-
path: localPath,
|
|
647
|
-
secret: localState.secret,
|
|
648
|
-
derivedKey: options.derivedKey,
|
|
649
|
-
});
|
|
650
|
-
migrated = true;
|
|
651
|
-
continue;
|
|
652
|
-
}
|
|
653
|
-
if (options.platform === "darwin" && options.legacyMacKeychainReader != null) {
|
|
654
|
-
try {
|
|
655
|
-
const secret = await options.legacyMacKeychainReader.loadSecret(keyId);
|
|
656
|
-
await writeWrappedSecret({
|
|
657
|
-
path: localPath,
|
|
658
|
-
secret,
|
|
659
|
-
derivedKey: options.derivedKey,
|
|
660
|
-
});
|
|
661
|
-
migrated = true;
|
|
662
|
-
}
|
|
663
|
-
catch {
|
|
664
|
-
// Best-effort legacy migration only.
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
return migrated;
|
|
669
|
-
}
|
|
670
|
-
async function promptForNewPassword(prompt) {
|
|
671
|
-
if (!prompt.isInteractive) {
|
|
672
|
-
throw new Error("wallet_client_password_setup_requires_tty");
|
|
673
|
-
}
|
|
674
|
-
while (true) {
|
|
675
|
-
const first = await promptForHiddenValue(prompt, "Create client password: ");
|
|
676
|
-
const firstBytes = Buffer.from(first, "utf8");
|
|
677
|
-
if (firstBytes.length === 0) {
|
|
678
|
-
zeroizeBuffer(firstBytes);
|
|
679
|
-
prompt.writeLine("Client password cannot be blank.");
|
|
680
|
-
continue;
|
|
681
|
-
}
|
|
682
|
-
const second = await promptForHiddenValue(prompt, "Confirm client password: ");
|
|
683
|
-
const secondBytes = Buffer.from(second, "utf8");
|
|
684
|
-
if (!firstBytes.equals(secondBytes)) {
|
|
685
|
-
zeroizeBuffer(firstBytes);
|
|
686
|
-
zeroizeBuffer(secondBytes);
|
|
687
|
-
prompt.writeLine("Client password entries did not match.");
|
|
688
|
-
continue;
|
|
689
|
-
}
|
|
690
|
-
zeroizeBuffer(secondBytes);
|
|
691
|
-
let passwordHint = "";
|
|
692
|
-
while (passwordHint.length === 0) {
|
|
693
|
-
passwordHint = (await prompt.prompt("Password hint: ")).trim();
|
|
694
|
-
if (passwordHint.length === 0) {
|
|
695
|
-
prompt.writeLine("Password hint cannot be blank.");
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
return {
|
|
699
|
-
passwordBytes: firstBytes,
|
|
700
|
-
passwordHint,
|
|
701
|
-
};
|
|
702
|
-
}
|
|
27
|
+
return await lockClientPasswordSessionResolved(await resolveCleanClientPasswordContext(options));
|
|
703
28
|
}
|
|
704
29
|
export async function ensureClientPasswordConfigured(options) {
|
|
705
|
-
await
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
action: "already-configured",
|
|
710
|
-
session: await readClientPasswordSessionStatus(options),
|
|
711
|
-
};
|
|
712
|
-
}
|
|
713
|
-
const setup = await promptForNewPassword(options.prompt);
|
|
714
|
-
let derivedKey = null;
|
|
715
|
-
try {
|
|
716
|
-
const created = await createClientPasswordState({
|
|
717
|
-
passwordBytes: setup.passwordBytes,
|
|
718
|
-
passwordHint: setup.passwordHint,
|
|
719
|
-
});
|
|
720
|
-
derivedKey = created.derivedKey;
|
|
721
|
-
await mkdir(options.directoryPath, { recursive: true, mode: 0o700 });
|
|
722
|
-
await writeJsonFileAtomic(resolveClientPasswordStatePath(options.directoryPath), created.state, { mode: 0o600 });
|
|
723
|
-
const migrated = await migrateReferencedSecrets({
|
|
724
|
-
...options,
|
|
725
|
-
derivedKey,
|
|
726
|
-
});
|
|
727
|
-
const session = await startClientPasswordSession({
|
|
728
|
-
...options,
|
|
729
|
-
derivedKey,
|
|
730
|
-
unlockDurationSeconds: CLIENT_PASSWORD_SETUP_AUTO_UNLOCK_SECONDS,
|
|
731
|
-
});
|
|
732
|
-
derivedKey = null;
|
|
733
|
-
return {
|
|
734
|
-
action: migrated || readiness === "migration-required" ? "migrated" : "created",
|
|
735
|
-
session,
|
|
736
|
-
};
|
|
737
|
-
}
|
|
738
|
-
finally {
|
|
739
|
-
zeroizeBuffer(setup.passwordBytes);
|
|
740
|
-
zeroizeBuffer(derivedKey);
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
async function decryptWrappedSecretWithSession(options) {
|
|
744
|
-
let response = await requestAgentOrNull(options, {
|
|
745
|
-
command: "decrypt",
|
|
746
|
-
envelope: options.envelope,
|
|
30
|
+
const context = await resolveCleanClientPasswordContext(options);
|
|
31
|
+
return await ensureClientPasswordConfiguredResolved({
|
|
32
|
+
context,
|
|
33
|
+
prompt: options.prompt,
|
|
747
34
|
});
|
|
748
|
-
if (response === null && options.prompt != null && options.prompt.isInteractive) {
|
|
749
|
-
await unlockClientPasswordSessionWithPrompt({
|
|
750
|
-
...options,
|
|
751
|
-
prompt: options.prompt,
|
|
752
|
-
});
|
|
753
|
-
response = await requestAgentOrNull(options, {
|
|
754
|
-
command: "decrypt",
|
|
755
|
-
envelope: options.envelope,
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
if (response === null || !response.ok || response.secretBase64 == null) {
|
|
759
|
-
throw new Error("wallet_client_password_locked");
|
|
760
|
-
}
|
|
761
|
-
return new Uint8Array(Buffer.from(response.secretBase64, "base64"));
|
|
762
|
-
}
|
|
763
|
-
async function encryptWrappedSecretWithSession(options) {
|
|
764
|
-
let response = await requestAgentOrNull(options, {
|
|
765
|
-
command: "encrypt",
|
|
766
|
-
secretBase64: Buffer.from(options.secret).toString("base64"),
|
|
767
|
-
});
|
|
768
|
-
if (response === null && options.prompt != null && options.prompt.isInteractive) {
|
|
769
|
-
await unlockClientPasswordSessionWithPrompt({
|
|
770
|
-
...options,
|
|
771
|
-
prompt: options.prompt,
|
|
772
|
-
});
|
|
773
|
-
response = await requestAgentOrNull(options, {
|
|
774
|
-
command: "encrypt",
|
|
775
|
-
secretBase64: Buffer.from(options.secret).toString("base64"),
|
|
776
|
-
});
|
|
777
|
-
}
|
|
778
|
-
if (response === null || !response.ok || response.envelope == null) {
|
|
779
|
-
throw new Error("wallet_client_password_locked");
|
|
780
|
-
}
|
|
781
|
-
return response.envelope;
|
|
782
35
|
}
|
|
783
36
|
export async function loadClientProtectedSecret(options) {
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
throw new Error("wallet_client_password_migration_required");
|
|
791
|
-
}
|
|
792
|
-
throw new Error("wallet_client_password_setup_required");
|
|
793
|
-
}
|
|
794
|
-
if (localState.state === "missing") {
|
|
795
|
-
if (await legacyMacKeychainHasSecret(options, options.keyId)) {
|
|
796
|
-
throw new Error("wallet_client_password_migration_required");
|
|
797
|
-
}
|
|
798
|
-
throw new Error(`wallet_secret_missing_${options.keyId}`);
|
|
799
|
-
}
|
|
800
|
-
if (localState.state === "raw") {
|
|
801
|
-
throw new Error("wallet_client_password_migration_required");
|
|
802
|
-
}
|
|
803
|
-
return await decryptWrappedSecretWithSession({
|
|
804
|
-
...options,
|
|
805
|
-
envelope: localState.envelope,
|
|
806
|
-
});
|
|
807
|
-
}
|
|
808
|
-
catch (error) {
|
|
809
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
810
|
-
if (message.startsWith("wallet_client_password_")
|
|
811
|
-
|| message.startsWith("wallet_secret_missing_")) {
|
|
812
|
-
throw error;
|
|
813
|
-
}
|
|
814
|
-
throw createRuntimeError(options.runtimeErrorCode, error);
|
|
815
|
-
}
|
|
37
|
+
const context = await resolveCleanClientPasswordContext(options);
|
|
38
|
+
return await loadClientProtectedSecretResolved({
|
|
39
|
+
...context,
|
|
40
|
+
keyId: options.keyId,
|
|
41
|
+
prompt: options.prompt,
|
|
42
|
+
});
|
|
816
43
|
}
|
|
817
44
|
export async function storeClientProtectedSecret(options) {
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
const envelope = await encryptWrappedSecretWithSession(options);
|
|
826
|
-
await writeFileAtomic(resolveLocalSecretFilePath(options.directoryPath, options.keyId), `${JSON.stringify({
|
|
827
|
-
format: LOCAL_SECRET_ENVELOPE_FORMAT,
|
|
828
|
-
version: 1,
|
|
829
|
-
cipher: "aes-256-gcm",
|
|
830
|
-
wrappedBy: "client-password",
|
|
831
|
-
nonce: envelope.nonce,
|
|
832
|
-
tag: envelope.tag,
|
|
833
|
-
ciphertext: envelope.ciphertext,
|
|
834
|
-
}, null, 2)}\n`, { mode: 0o600 });
|
|
835
|
-
}
|
|
836
|
-
catch (error) {
|
|
837
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
838
|
-
if (message.startsWith("wallet_client_password_")) {
|
|
839
|
-
throw error;
|
|
840
|
-
}
|
|
841
|
-
throw createRuntimeError(options.runtimeErrorCode, error);
|
|
842
|
-
}
|
|
45
|
+
const context = await resolveCleanClientPasswordContext(options);
|
|
46
|
+
await storeClientProtectedSecretResolved({
|
|
47
|
+
...context,
|
|
48
|
+
keyId: options.keyId,
|
|
49
|
+
secret: options.secret,
|
|
50
|
+
prompt: options.prompt,
|
|
51
|
+
});
|
|
843
52
|
}
|
|
844
53
|
export async function deleteClientProtectedSecret(options) {
|
|
845
|
-
|
|
54
|
+
const context = await resolveCleanClientPasswordContext(options);
|
|
55
|
+
await deleteClientProtectedSecretResolved({
|
|
56
|
+
...context,
|
|
57
|
+
keyId: options.keyId,
|
|
58
|
+
});
|
|
846
59
|
}
|
|
847
60
|
export async function unlockClientPasswordSession(options) {
|
|
848
|
-
await
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
if (currentStatus.unlocked) {
|
|
854
|
-
const unlockDurationSeconds = await promptForUnlockDurationWithDefault(options.prompt, resolveRemainingUnlockSeconds(currentStatus));
|
|
855
|
-
const refreshed = await refreshClientPasswordSession({
|
|
856
|
-
...options,
|
|
857
|
-
unlockUntilUnixMs: Date.now() + (unlockDurationSeconds * 1_000),
|
|
858
|
-
});
|
|
859
|
-
if (refreshed !== null) {
|
|
860
|
-
return refreshed;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
return await unlockClientPasswordSessionWithPrompt(options);
|
|
864
|
-
}
|
|
865
|
-
async function prepareClientPasswordRotation(options) {
|
|
866
|
-
const next = await createClientPasswordState({
|
|
867
|
-
passwordBytes: options.newPasswordBytes,
|
|
868
|
-
passwordHint: options.newPasswordHint,
|
|
61
|
+
const context = await resolveCleanClientPasswordContext(options);
|
|
62
|
+
await finalizePendingClientPasswordRotationIfNeeded(context);
|
|
63
|
+
return await unlockClientPasswordSessionResolved({
|
|
64
|
+
context,
|
|
65
|
+
prompt: options.prompt,
|
|
869
66
|
});
|
|
870
|
-
const keyIds = await collectReferencedSecretIds(options.stateRoot);
|
|
871
|
-
const secrets = [];
|
|
872
|
-
try {
|
|
873
|
-
for (const keyId of keyIds) {
|
|
874
|
-
const localState = await readLocalSecretFile(resolveLocalSecretFilePath(options.directoryPath, keyId));
|
|
875
|
-
if (localState.state === "missing") {
|
|
876
|
-
throw new Error(`wallet_secret_missing_${keyId}`);
|
|
877
|
-
}
|
|
878
|
-
if (localState.state === "raw") {
|
|
879
|
-
throw new Error("wallet_client_password_migration_required");
|
|
880
|
-
}
|
|
881
|
-
const secret = decryptBytesWithKey(localState.envelope, options.currentDerivedKey);
|
|
882
|
-
try {
|
|
883
|
-
secrets.push({
|
|
884
|
-
keyId,
|
|
885
|
-
envelope: createWrappedSecretEnvelope(secret, next.derivedKey),
|
|
886
|
-
});
|
|
887
|
-
}
|
|
888
|
-
finally {
|
|
889
|
-
zeroizeBuffer(secret);
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
return {
|
|
893
|
-
journal: {
|
|
894
|
-
format: CLIENT_PASSWORD_ROTATION_JOURNAL_FORMAT,
|
|
895
|
-
version: 1,
|
|
896
|
-
nextState: next.state,
|
|
897
|
-
secrets,
|
|
898
|
-
},
|
|
899
|
-
newDerivedKey: next.derivedKey,
|
|
900
|
-
};
|
|
901
|
-
}
|
|
902
|
-
catch (error) {
|
|
903
|
-
zeroizeBuffer(next.derivedKey);
|
|
904
|
-
throw error;
|
|
905
|
-
}
|
|
906
67
|
}
|
|
907
68
|
export async function changeClientPassword(options) {
|
|
908
|
-
await
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
promptMessage: "Current client password: ",
|
|
913
|
-
ttyErrorCode: "wallet_client_password_change_requires_tty",
|
|
69
|
+
const context = await resolveCleanClientPasswordContext(options);
|
|
70
|
+
return await changeClientPasswordResolved({
|
|
71
|
+
context,
|
|
72
|
+
prompt: options.prompt,
|
|
914
73
|
});
|
|
915
|
-
const nextPassword = await promptForNewPassword(options.prompt);
|
|
916
|
-
let newDerivedKey = null;
|
|
917
|
-
try {
|
|
918
|
-
const prepared = await prepareClientPasswordRotation({
|
|
919
|
-
...options,
|
|
920
|
-
currentDerivedKey,
|
|
921
|
-
newPasswordBytes: nextPassword.passwordBytes,
|
|
922
|
-
newPasswordHint: nextPassword.passwordHint,
|
|
923
|
-
});
|
|
924
|
-
newDerivedKey = prepared.newDerivedKey;
|
|
925
|
-
await mkdir(options.directoryPath, { recursive: true, mode: 0o700 });
|
|
926
|
-
await writeJsonFileAtomic(resolveClientPasswordRotationJournalPath(options.directoryPath), prepared.journal, { mode: 0o600 });
|
|
927
|
-
await finalizePendingClientPasswordRotationIfNeeded(options);
|
|
928
|
-
const session = await startClientPasswordSessionWithExpiry({
|
|
929
|
-
...options,
|
|
930
|
-
derivedKey: newDerivedKey,
|
|
931
|
-
unlockUntilUnixMs: resolvePostChangeUnlockUntilUnixMs(previousSession),
|
|
932
|
-
});
|
|
933
|
-
newDerivedKey = null;
|
|
934
|
-
return session;
|
|
935
|
-
}
|
|
936
|
-
finally {
|
|
937
|
-
zeroizeBuffer(currentDerivedKey);
|
|
938
|
-
zeroizeBuffer(nextPassword.passwordBytes);
|
|
939
|
-
zeroizeBuffer(newDerivedKey);
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
export function createLegacyKeychainServiceName() {
|
|
943
|
-
return "org.cogcoin.wallet";
|
|
944
|
-
}
|
|
945
|
-
export function createAgentBootstrapState(options) {
|
|
946
|
-
return options;
|
|
947
|
-
}
|
|
948
|
-
export function describeClientPasswordLockedMessage() {
|
|
949
|
-
return "Wallet state exists but the client password is locked.";
|
|
950
|
-
}
|
|
951
|
-
export function describeClientPasswordSetupMessage() {
|
|
952
|
-
return "Wallet-local secret access is not configured yet. Run `cogcoin init` to create the client password.";
|
|
953
|
-
}
|
|
954
|
-
export function describeClientPasswordMigrationMessage() {
|
|
955
|
-
return "Wallet-local secret migration is still required. Run `cogcoin init` to migrate this client to password-protected local secrets.";
|
|
956
|
-
}
|
|
957
|
-
export function listLocalSecretFilesForTesting(options) {
|
|
958
|
-
return readdir(options.directoryPath).catch(() => []);
|
|
959
74
|
}
|