@did-btcr2/cli 0.10.3 → 0.11.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/dist/.tsbuildinfo +1 -1
- package/dist/cjs/index.js +889 -43
- package/dist/esm/src/cli.js +30 -12
- package/dist/esm/src/cli.js.map +1 -1
- package/dist/esm/src/commands/completion.js +36 -0
- package/dist/esm/src/commands/completion.js.map +1 -0
- package/dist/esm/src/commands/config.js +69 -0
- package/dist/esm/src/commands/config.js.map +1 -0
- package/dist/esm/src/commands/deactivate.js +21 -8
- package/dist/esm/src/commands/deactivate.js.map +1 -1
- package/dist/esm/src/commands/index.js +4 -0
- package/dist/esm/src/commands/index.js.map +1 -1
- package/dist/esm/src/commands/key.js +175 -0
- package/dist/esm/src/commands/key.js.map +1 -0
- package/dist/esm/src/commands/profile.js +63 -0
- package/dist/esm/src/commands/profile.js.map +1 -0
- package/dist/esm/src/commands/update.js +19 -9
- package/dist/esm/src/commands/update.js.map +1 -1
- package/dist/esm/src/config.js +99 -12
- package/dist/esm/src/config.js.map +1 -1
- package/dist/esm/src/keystore/atomic.js +64 -0
- package/dist/esm/src/keystore/atomic.js.map +1 -0
- package/dist/esm/src/keystore/envelope.js +123 -0
- package/dist/esm/src/keystore/envelope.js.map +1 -0
- package/dist/esm/src/keystore/error.js +16 -0
- package/dist/esm/src/keystore/error.js.map +1 -0
- package/dist/esm/src/keystore/file-backed-key-manager.js +78 -0
- package/dist/esm/src/keystore/file-backed-key-manager.js.map +1 -0
- package/dist/esm/src/keystore/file-key-store.js +184 -0
- package/dist/esm/src/keystore/file-key-store.js.map +1 -0
- package/dist/esm/src/keystore/passphrase.js +87 -0
- package/dist/esm/src/keystore/passphrase.js.map +1 -0
- package/dist/esm/src/keystore/paths.js +20 -0
- package/dist/esm/src/keystore/paths.js.map +1 -0
- package/dist/esm/src/keystore/resolve-key-ref.js +47 -0
- package/dist/esm/src/keystore/resolve-key-ref.js.map +1 -0
- package/dist/types/src/cli.d.ts +6 -2
- package/dist/types/src/cli.d.ts.map +1 -1
- package/dist/types/src/commands/completion.d.ts +5 -0
- package/dist/types/src/commands/completion.d.ts.map +1 -0
- package/dist/types/src/commands/config.d.ts +5 -0
- package/dist/types/src/commands/config.d.ts.map +1 -0
- package/dist/types/src/commands/deactivate.d.ts.map +1 -1
- package/dist/types/src/commands/index.d.ts +4 -0
- package/dist/types/src/commands/index.d.ts.map +1 -1
- package/dist/types/src/commands/key.d.ts +10 -0
- package/dist/types/src/commands/key.d.ts.map +1 -0
- package/dist/types/src/commands/profile.d.ts +5 -0
- package/dist/types/src/commands/profile.d.ts.map +1 -0
- package/dist/types/src/commands/update.d.ts.map +1 -1
- package/dist/types/src/config.d.ts +48 -5
- package/dist/types/src/config.d.ts.map +1 -1
- package/dist/types/src/keystore/atomic.d.ts +19 -0
- package/dist/types/src/keystore/atomic.d.ts.map +1 -0
- package/dist/types/src/keystore/envelope.d.ts +64 -0
- package/dist/types/src/keystore/envelope.d.ts.map +1 -0
- package/dist/types/src/keystore/error.d.ts +14 -0
- package/dist/types/src/keystore/error.d.ts.map +1 -0
- package/dist/types/src/keystore/file-backed-key-manager.d.ts +41 -0
- package/dist/types/src/keystore/file-backed-key-manager.d.ts.map +1 -0
- package/dist/types/src/keystore/file-key-store.d.ts +52 -0
- package/dist/types/src/keystore/file-key-store.d.ts.map +1 -0
- package/dist/types/src/keystore/passphrase.d.ts +20 -0
- package/dist/types/src/keystore/passphrase.d.ts.map +1 -0
- package/dist/types/src/keystore/paths.d.ts +13 -0
- package/dist/types/src/keystore/paths.d.ts.map +1 -0
- package/dist/types/src/keystore/resolve-key-ref.d.ts +19 -0
- package/dist/types/src/keystore/resolve-key-ref.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +91 -0
- package/dist/types/src/types.d.ts.map +1 -1
- package/package.json +9 -4
- package/src/cli.ts +36 -11
- package/src/commands/completion.ts +40 -0
- package/src/commands/config.ts +84 -0
- package/src/commands/deactivate.ts +25 -12
- package/src/commands/index.ts +4 -0
- package/src/commands/key.ts +193 -0
- package/src/commands/profile.ts +65 -0
- package/src/commands/update.ts +23 -13
- package/src/config.ts +142 -20
- package/src/keystore/atomic.ts +73 -0
- package/src/keystore/envelope.ts +172 -0
- package/src/keystore/error.ts +16 -0
- package/src/keystore/file-backed-key-manager.ts +99 -0
- package/src/keystore/file-key-store.ts +242 -0
- package/src/keystore/passphrase.ts +99 -0
- package/src/keystore/paths.ts +20 -0
- package/src/keystore/resolve-key-ref.ts +62 -0
- package/src/types.ts +30 -11
package/dist/esm/src/config.js
CHANGED
|
@@ -1,9 +1,59 @@
|
|
|
1
1
|
import { createApi, Identifier } from '@did-btcr2/api';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
5
|
import { CLIError } from './error.js';
|
|
6
|
+
import { ensureDir, writeFileAtomic } from './keystore/atomic.js';
|
|
7
|
+
import { FileBackedKeyManager } from './keystore/file-backed-key-manager.js';
|
|
8
|
+
import { defaultKeystorePath } from './keystore/paths.js';
|
|
9
|
+
import { acquirePassphrase } from './keystore/passphrase.js';
|
|
6
10
|
import { SUPPORTED_NETWORKS } from './types.js';
|
|
11
|
+
/** Current config-file schema version, stamped on every write. */
|
|
12
|
+
export const CONFIG_SCHEMA_VERSION = 1;
|
|
13
|
+
/**
|
|
14
|
+
* Read-modify-write a config file, preserving unknown keys. Reads the raw JSON
|
|
15
|
+
* (so keys outside {@link ConfigFile} survive a rewrite), applies `mutate`,
|
|
16
|
+
* stamps the schema version, and writes atomically (file 0600, dir 0700).
|
|
17
|
+
*/
|
|
18
|
+
export function writeConfigFile(path, mutate) {
|
|
19
|
+
const raw = readConfigFile(path) ?? {};
|
|
20
|
+
mutate(raw);
|
|
21
|
+
raw.schemaVersion = CONFIG_SCHEMA_VERSION;
|
|
22
|
+
ensureDir(dirname(path), 0o700);
|
|
23
|
+
writeFileAtomic(path, `${JSON.stringify(raw, null, 2)}\n`, 0o600);
|
|
24
|
+
}
|
|
25
|
+
/** Reads the value at a dotted path (e.g. `profiles.regtest.btc.rest`). */
|
|
26
|
+
export function getConfigPath(config, path) {
|
|
27
|
+
return path.split('.').reduce((node, key) => node?.[key], config);
|
|
28
|
+
}
|
|
29
|
+
/** Sets the value at a dotted path, creating intermediate objects. */
|
|
30
|
+
export function setConfigPath(config, path, value) {
|
|
31
|
+
const keys = path.split('.');
|
|
32
|
+
const last = keys.pop();
|
|
33
|
+
if (!last)
|
|
34
|
+
throw new CLIError('Config path must be non-empty.', 'INVALID_ARGUMENT_ERROR');
|
|
35
|
+
let node = config;
|
|
36
|
+
for (const key of keys) {
|
|
37
|
+
if (typeof node[key] !== 'object' || node[key] === null)
|
|
38
|
+
node[key] = {};
|
|
39
|
+
node = node[key];
|
|
40
|
+
}
|
|
41
|
+
node[last] = value;
|
|
42
|
+
}
|
|
43
|
+
/** Deletes the value at a dotted path. No-op if the path does not exist. */
|
|
44
|
+
export function unsetConfigPath(config, path) {
|
|
45
|
+
const keys = path.split('.');
|
|
46
|
+
const last = keys.pop();
|
|
47
|
+
if (!last)
|
|
48
|
+
return;
|
|
49
|
+
let node = config;
|
|
50
|
+
for (const key of keys) {
|
|
51
|
+
node = node?.[key];
|
|
52
|
+
if (!node)
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
delete node[last];
|
|
56
|
+
}
|
|
7
57
|
/**
|
|
8
58
|
* Environment variable names consulted by {@link defaultApiFactory}.
|
|
9
59
|
*
|
|
@@ -80,23 +130,23 @@ export function profileToOverrides(config, profileName) {
|
|
|
80
130
|
};
|
|
81
131
|
}
|
|
82
132
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
133
|
+
* Resolves the Bitcoin and CAS connection config for a network by merging,
|
|
134
|
+
* in precedence order, CLI flags, environment variables, and the config-file
|
|
135
|
+
* profile on top of the per-network defaults (handled by `BitcoinConnection`).
|
|
86
136
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
137
|
+
* Returns an empty config when no network is given, since offline operations
|
|
138
|
+
* (create, key management) need no connection.
|
|
89
139
|
*
|
|
90
|
-
* When no `--profile` is given, the network name is used as the profile
|
|
91
|
-
*
|
|
140
|
+
* When no `--profile` is given, the network name is used as the profile key
|
|
141
|
+
* (e.g. a regtest DID auto-selects the `"regtest"` profile).
|
|
92
142
|
*/
|
|
93
|
-
|
|
143
|
+
function resolveConnectionConfig(network, overrides) {
|
|
94
144
|
if (!network)
|
|
95
|
-
return
|
|
145
|
+
return {};
|
|
96
146
|
// Layer 1: Config file profile (lowest precedence of the three override layers)
|
|
97
147
|
const configPath = overrides?.config ?? defaultConfigPath();
|
|
98
|
-
const profileName = overrides?.profile ?? network;
|
|
99
148
|
const file = readConfigFile(configPath);
|
|
149
|
+
const profileName = overrides?.profile ?? file?.defaults?.profile ?? network;
|
|
100
150
|
const fileOverrides = file ? profileToOverrides(file, profileName) : {};
|
|
101
151
|
// Layer 2: Environment variables
|
|
102
152
|
const env = readEnvOverrides();
|
|
@@ -120,7 +170,44 @@ export function defaultApiFactory(network, overrides) {
|
|
|
120
170
|
};
|
|
121
171
|
}
|
|
122
172
|
const cas = merged.casGateway ? { gateway: merged.casGateway } : undefined;
|
|
123
|
-
return
|
|
173
|
+
return { btc, ...(cas && { cas }) };
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Default {@link ApiFactory} backed by network defaults from
|
|
177
|
+
* `@did-btcr2/bitcoin` (mempool.space for public networks, localhost for
|
|
178
|
+
* regtest). Keystore-free: suitable for offline `create` and read-only
|
|
179
|
+
* `resolve`, which never need a signing identity.
|
|
180
|
+
*
|
|
181
|
+
* Override precedence (highest wins):
|
|
182
|
+
* CLI flags -> env vars -> config file profile -> network defaults.
|
|
183
|
+
*/
|
|
184
|
+
export function defaultApiFactory(network, overrides) {
|
|
185
|
+
return createApi(resolveConnectionConfig(network, overrides));
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Builds a keystore-backed {@link KeyManager} reading secret keys from the
|
|
189
|
+
* encrypted on-disk keystore. The passphrase is acquired lazily, so building
|
|
190
|
+
* this never prompts; a prompt happens only when a secret is actually sealed
|
|
191
|
+
* or opened. The persisted active-key pointer is re-applied (a non-decrypting
|
|
192
|
+
* existence check) so "the active key" survives across invocations.
|
|
193
|
+
*/
|
|
194
|
+
function buildKeystoreKms(overrides) {
|
|
195
|
+
return new FileBackedKeyManager({
|
|
196
|
+
path: overrides?.keystore ?? defaultKeystorePath(),
|
|
197
|
+
getPassphrase: () => acquirePassphrase({ passphraseFile: overrides?.passphraseFile }),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Keystore-aware {@link ApiFactory} for commands that need a signing identity
|
|
202
|
+
* (key management, update, deactivate). Identical to {@link defaultApiFactory}
|
|
203
|
+
* for Bitcoin and CAS, plus an injected keystore-backed KeyManager. Offline key
|
|
204
|
+
* commands (no network) still get the keystore.
|
|
205
|
+
*/
|
|
206
|
+
export function keystoreApiFactory(network, overrides) {
|
|
207
|
+
return createApi({
|
|
208
|
+
...resolveConnectionConfig(network, overrides),
|
|
209
|
+
kms: buildKeystoreKms(overrides),
|
|
210
|
+
});
|
|
124
211
|
}
|
|
125
212
|
/**
|
|
126
213
|
* Extracts and validates the Bitcoin network from a DID string.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,UAAU,EAA2C,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join } from 'node:path';
|
|
3
|
+
import { KeyStoreError } from './error.js';
|
|
4
|
+
const isWindows = process.platform === 'win32';
|
|
5
|
+
let permsWarned = false;
|
|
6
|
+
let tmpCounter = 0;
|
|
7
|
+
/**
|
|
8
|
+
* Creates a directory (recursively) and, on POSIX systems, tightens it to the
|
|
9
|
+
* requested mode. `mkdir`'s mode is subject to the umask, so it is reapplied
|
|
10
|
+
* with an explicit `chmod`.
|
|
11
|
+
*/
|
|
12
|
+
export function ensureDir(dir, mode) {
|
|
13
|
+
mkdirSync(dir, { recursive: true, mode });
|
|
14
|
+
if (!isWindows) {
|
|
15
|
+
try {
|
|
16
|
+
chmodSync(dir, mode);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// A pre-existing directory we do not own cannot be re-moded; best effort.
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Writes a file atomically: serialize to a sibling temporary file, tighten its
|
|
25
|
+
* permissions, then rename over the target so a crash mid-write cannot leave a
|
|
26
|
+
* truncated or partially-written file. The temporary file is removed on failure.
|
|
27
|
+
*/
|
|
28
|
+
export function writeFileAtomic(path, data, mode) {
|
|
29
|
+
const tmp = join(dirname(path), `.${basename(path)}.${process.pid}.${tmpCounter++}.tmp`);
|
|
30
|
+
try {
|
|
31
|
+
writeFileSync(tmp, data, { mode });
|
|
32
|
+
if (!isWindows)
|
|
33
|
+
chmodSync(tmp, mode);
|
|
34
|
+
renameSync(tmp, path);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
try {
|
|
38
|
+
rmSync(tmp, { force: true });
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Ignore cleanup failure; surface the original write error.
|
|
42
|
+
}
|
|
43
|
+
throw new KeyStoreError(`Failed to write keystore at ${path}.`, 'ATOMIC_WRITE_ERROR', { path, cause: error instanceof Error ? error.message : String(error) });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Fails closed if a keystore file is readable or writable by group or other.
|
|
48
|
+
* On Windows, where POSIX mode bits are not enforced, this is a no-op that
|
|
49
|
+
* warns once on standard error.
|
|
50
|
+
*/
|
|
51
|
+
export function assertSecurePerms(path) {
|
|
52
|
+
if (isWindows) {
|
|
53
|
+
if (!permsWarned) {
|
|
54
|
+
process.stderr.write('warning: file permissions are not enforced on Windows; protect the keystore directory manually.\n');
|
|
55
|
+
permsWarned = true;
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const mode = statSync(path).mode & 0o777;
|
|
60
|
+
if ((mode & 0o077) !== 0) {
|
|
61
|
+
throw new KeyStoreError(`Keystore at ${path} has insecure permissions 0${mode.toString(8)}; expected 0600.`, 'KEYSTORE_PERMISSION_ERROR', { path, mode: `0${mode.toString(8)}` });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=atomic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"atomic.js","sourceRoot":"","sources":["../../../../src/keystore/atomic.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC5F,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC;AAC/C,IAAI,WAAW,GAAG,KAAK,CAAC;AACxB,IAAI,UAAU,GAAG,CAAC,CAAC;AAEnB;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,GAAW,EAAE,IAAY;IACjD,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1C,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,IAAI,CAAC;YACH,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACvB,CAAC;QAAC,MAAM,CAAC;YACP,0EAA0E;QAC5E,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,IAAY,EAAE,IAAY;IACtE,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,GAAG,IAAI,UAAU,EAAE,MAAM,CAAC,CAAC;IACzF,IAAI,CAAC;QACH,aAAa,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;QACnC,IAAI,CAAC,SAAS;YAAE,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACrC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACxB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/B,CAAC;QAAC,MAAM,CAAC;YACP,4DAA4D;QAC9D,CAAC;QACD,MAAM,IAAI,aAAa,CACrB,+BAA+B,IAAI,GAAG,EACtC,oBAAoB,EACpB,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CACxE,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,IAAI,SAAS,EAAE,CAAC;QACd,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,mGAAmG,CACpG,CAAC;YACF,WAAW,GAAG,IAAI,CAAC;QACrB,CAAC;QACD,OAAO;IACT,CAAC;IACD,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC;IACzC,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,aAAa,CACrB,eAAe,IAAI,8BAA8B,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,kBAAkB,EACnF,2BAA2B,EAC3B,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,EAAE,CACvC,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { xchacha20poly1305 } from '@noble/ciphers/chacha.js';
|
|
2
|
+
import { argon2id } from '@noble/hashes/argon2.js';
|
|
3
|
+
import { randomBytes, utf8ToBytes } from '@noble/hashes/utils.js';
|
|
4
|
+
import { base64urlnopad } from '@scure/base';
|
|
5
|
+
import { KeyStoreError } from './error.js';
|
|
6
|
+
/** Current keystore secret-envelope format version. */
|
|
7
|
+
export const ENVELOPE_VERSION = 1;
|
|
8
|
+
/** Random salt length in bytes for argon2id. */
|
|
9
|
+
const SALT_BYTES = 16;
|
|
10
|
+
/** XChaCha20-Poly1305 extended nonce length in bytes (safe with random nonces). */
|
|
11
|
+
const NONCE_BYTES = 24;
|
|
12
|
+
/** Derived symmetric key length in bytes (the XChaCha20-Poly1305 key size). */
|
|
13
|
+
const KEY_BYTES = 32;
|
|
14
|
+
/**
|
|
15
|
+
* Production argon2id parameters: 3 passes over 64 MiB across 4 lanes, deriving
|
|
16
|
+
* a 32-byte key. Recorded in every envelope so the cost can be raised later
|
|
17
|
+
* without making previously sealed envelopes undecryptable.
|
|
18
|
+
*/
|
|
19
|
+
export const DEFAULT_ARGON_PARAMS = { t: 3, m: 65536, p: 4, dkLen: KEY_BYTES };
|
|
20
|
+
/**
|
|
21
|
+
* Builds the header with a fixed key order so the additional-data bytes are
|
|
22
|
+
* byte-identical on the encrypt and decrypt paths.
|
|
23
|
+
*/
|
|
24
|
+
function buildHeader(saltB64, params) {
|
|
25
|
+
return {
|
|
26
|
+
v: ENVELOPE_VERSION,
|
|
27
|
+
kdf: {
|
|
28
|
+
alg: 'argon2id',
|
|
29
|
+
salt: saltB64,
|
|
30
|
+
t: params.t,
|
|
31
|
+
m: params.m,
|
|
32
|
+
p: params.p,
|
|
33
|
+
dkLen: params.dkLen,
|
|
34
|
+
},
|
|
35
|
+
cipher: 'xchacha20poly1305',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Serializes the header into the AEAD additional-data byte string. */
|
|
39
|
+
function headerAad(header) {
|
|
40
|
+
return utf8ToBytes(JSON.stringify(header));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Stretches a passphrase into the symmetric key. The transient UTF-8 copy of
|
|
44
|
+
* the passphrase is zeroized here; the caller is responsible for zeroizing the
|
|
45
|
+
* returned key after use.
|
|
46
|
+
*/
|
|
47
|
+
function deriveKey(passphrase, salt, params) {
|
|
48
|
+
const password = utf8ToBytes(passphrase);
|
|
49
|
+
try {
|
|
50
|
+
return argon2id(password, salt, { t: params.t, m: params.m, p: params.p, dkLen: params.dkLen });
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
password.fill(0);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Seals a secret under a passphrase into a {@link SecretEnvelope}. A fresh
|
|
58
|
+
* random salt and nonce are generated per call, so encrypting the same secret
|
|
59
|
+
* twice yields different envelopes.
|
|
60
|
+
*
|
|
61
|
+
* @param secret - The secret bytes to encrypt. Must be non-empty.
|
|
62
|
+
* @param passphrase - The passphrase the encryption key is derived from.
|
|
63
|
+
* @param params - argon2id cost parameters. Defaults to {@link DEFAULT_ARGON_PARAMS}.
|
|
64
|
+
* @returns The versioned, authenticated envelope.
|
|
65
|
+
* @throws {KeyStoreError} `ENVELOPE_ENCRYPT_ERROR` when `secret` is empty.
|
|
66
|
+
*/
|
|
67
|
+
export function encryptSecret(secret, passphrase, params = DEFAULT_ARGON_PARAMS) {
|
|
68
|
+
if (secret.length === 0) {
|
|
69
|
+
throw new KeyStoreError('Cannot encrypt an empty secret.', 'ENVELOPE_ENCRYPT_ERROR');
|
|
70
|
+
}
|
|
71
|
+
const salt = randomBytes(SALT_BYTES);
|
|
72
|
+
const nonce = randomBytes(NONCE_BYTES);
|
|
73
|
+
const header = buildHeader(base64urlnopad.encode(salt), params);
|
|
74
|
+
const key = deriveKey(passphrase, salt, params);
|
|
75
|
+
try {
|
|
76
|
+
const ciphertext = xchacha20poly1305(key, nonce, headerAad(header)).encrypt(secret);
|
|
77
|
+
return {
|
|
78
|
+
...header,
|
|
79
|
+
nonce: base64urlnopad.encode(nonce),
|
|
80
|
+
ciphertext: base64urlnopad.encode(ciphertext),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
key.fill(0);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Opens a {@link SecretEnvelope} sealed by {@link encryptSecret} and returns the
|
|
89
|
+
* plaintext secret. A wrong passphrase, corrupted ciphertext, or a tampered
|
|
90
|
+
* header all fail authentication and raise `DECRYPT_ERROR`.
|
|
91
|
+
*
|
|
92
|
+
* @param env - The envelope to open.
|
|
93
|
+
* @param passphrase - The passphrase the envelope was sealed with.
|
|
94
|
+
* @returns The decrypted secret bytes.
|
|
95
|
+
* @throws {KeyStoreError} `ENVELOPE_VERSION_ERROR` for an unknown version or
|
|
96
|
+
* algorithm; `DECRYPT_ERROR` for failed authentication.
|
|
97
|
+
*/
|
|
98
|
+
export function decryptSecret(env, passphrase) {
|
|
99
|
+
if (env.v !== ENVELOPE_VERSION) {
|
|
100
|
+
throw new KeyStoreError(`Unsupported keystore envelope version: ${String(env.v)}.`, 'ENVELOPE_VERSION_ERROR', { version: env.v });
|
|
101
|
+
}
|
|
102
|
+
if (env.kdf?.alg !== 'argon2id' || env.cipher !== 'xchacha20poly1305') {
|
|
103
|
+
throw new KeyStoreError('Unsupported keystore envelope algorithm.', 'ENVELOPE_VERSION_ERROR');
|
|
104
|
+
}
|
|
105
|
+
const params = { t: env.kdf.t, m: env.kdf.m, p: env.kdf.p, dkLen: env.kdf.dkLen };
|
|
106
|
+
const salt = base64urlnopad.decode(env.kdf.salt);
|
|
107
|
+
const nonce = base64urlnopad.decode(env.nonce);
|
|
108
|
+
const ciphertext = base64urlnopad.decode(env.ciphertext);
|
|
109
|
+
const header = buildHeader(env.kdf.salt, params);
|
|
110
|
+
const key = deriveKey(passphrase, salt, params);
|
|
111
|
+
try {
|
|
112
|
+
return xchacha20poly1305(key, nonce, headerAad(header)).decrypt(ciphertext);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (error instanceof KeyStoreError)
|
|
116
|
+
throw error;
|
|
117
|
+
throw new KeyStoreError('Keystore decryption failed: wrong passphrase or corrupted keystore.', 'DECRYPT_ERROR');
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
key.fill(0);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=envelope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envelope.js","sourceRoot":"","sources":["../../../../src/keystore/envelope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,uDAAuD;AACvD,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAU,CAAC;AAE3C,gDAAgD;AAChD,MAAM,UAAU,GAAG,EAAE,CAAC;AACtB,mFAAmF;AACnF,MAAM,WAAW,GAAG,EAAE,CAAC;AACvB,+EAA+E;AAC/E,MAAM,SAAS,GAAG,EAAE,CAAC;AAcrB;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAgB,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;AA0B5F;;;GAGG;AACH,SAAS,WAAW,CAAC,OAAe,EAAE,MAAmB;IACvD,OAAO;QACL,CAAC,EAAK,gBAAgB;QACtB,GAAG,EAAG;YACJ,GAAG,EAAK,UAAU;YAClB,IAAI,EAAI,OAAO;YACf,CAAC,EAAO,MAAM,CAAC,CAAC;YAChB,CAAC,EAAO,MAAM,CAAC,CAAC;YAChB,CAAC,EAAO,MAAM,CAAC,CAAC;YAChB,KAAK,EAAG,MAAM,CAAC,KAAK;SACrB;QACD,MAAM,EAAG,mBAAmB;KAC7B,CAAC;AACJ,CAAC;AAED,uEAAuE;AACvE,SAAS,SAAS,CAAC,MAAsB;IACvC,OAAO,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;AAC7C,CAAC;AAED;;;;GAIG;AACH,SAAS,SAAS,CAAC,UAAkB,EAAE,IAAgB,EAAE,MAAmB;IAC1E,MAAM,QAAQ,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;IACzC,IAAI,CAAC;QACH,OAAO,QAAQ,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;IAClG,CAAC;YAAS,CAAC;QACT,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa,CAC3B,MAAuB,EACvB,UAAmB,EACnB,SAA2B,oBAAoB;IAE/C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,aAAa,CAAC,iCAAiC,EAAE,wBAAwB,CAAC,CAAC;IACvF,CAAC;IACD,MAAM,IAAI,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;IACrC,MAAM,KAAK,GAAG,WAAW,CAAC,WAAW,CAAC,CAAC;IACvC,MAAM,MAAM,GAAG,WAAW,CAAC,cAAc,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;IAChE,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IAChD,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,iBAAiB,CAAC,GAAG,EAAE,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACpF,OAAO;YACL,GAAG,MAAM;YACT,KAAK,EAAQ,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC;YACzC,UAAU,EAAG,cAAc,CAAC,MAAM,CAAC,UAAU,CAAC;SAC/C,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa,CAAC,GAAmB,EAAE,UAAkB;IACnE,IAAI,GAAG,CAAC,CAAC,KAAK,gBAAgB,EAAE,CAAC;QAC/B,MAAM,IAAI,aAAa,CACrB,0CAA0C,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAC1D,wBAAwB,EACxB,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC,EAAE,CACnB,CAAC;IACJ,CAAC;IACD,IAAI,GAAG,CAAC,GAAG,EAAE,GAAG,KAAK,UAAU,IAAI,GAAG,CAAC,MAAM,KAAK,mBAAmB,EAAE,CAAC;QACtE,MAAM,IAAI,aAAa,CAAC,0CAA0C,EAAE,wBAAwB,CAAC,CAAC;IAChG,CAAC;IACD,MAAM,MAAM,GAAgB,EAAE,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;IAC/F,MAAM,IAAI,GAAG,cAAc,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACjD,MAAM,KAAK,GAAG,cAAc,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC/C,MAAM,UAAU,GAAG,cAAc,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACzD,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACjD,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IAChD,IAAI,CAAC;QACH,OAAO,iBAAiB,CAAC,GAAG,EAAE,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC9E,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,aAAa;YAAE,MAAM,KAAK,CAAC;QAChD,MAAM,IAAI,aAAa,CACrB,qEAAqE,EACrE,eAAe,CAChB,CAAC;IACJ,CAAC;YAAS,CAAC;QACT,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { DidMethodError } from '@did-btcr2/common';
|
|
2
|
+
/**
|
|
3
|
+
* Error raised by the CLI keystore layer: secret-envelope encryption and
|
|
4
|
+
* decryption, on-disk file permission enforcement, and passphrase acquisition.
|
|
5
|
+
*
|
|
6
|
+
* Unlike {@link CLIError} (whose `name` is fixed to `'CLIError'`), this follows
|
|
7
|
+
* the {@link DidMethodError} sibling convention where `name` mirrors the `type`
|
|
8
|
+
* code, so a thrown error's `name` reflects the specific failure category
|
|
9
|
+
* (for example `DECRYPT_ERROR` or `KEYSTORE_PERMISSION_ERROR`).
|
|
10
|
+
*/
|
|
11
|
+
export class KeyStoreError extends DidMethodError {
|
|
12
|
+
constructor(message, type = 'KeyStoreError', data) {
|
|
13
|
+
super(message, { type, name: type, data });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error.js","sourceRoot":"","sources":["../../../../src/keystore/error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD;;;;;;;;GAQG;AACH,MAAM,OAAO,aAAc,SAAQ,cAAc;IAC/C,YAAY,OAAe,EAAE,OAAe,eAAe,EAAE,IAA0B;QACrF,KAAK,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC;CACF"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { LocalKeyManager, } from '@did-btcr2/key-manager';
|
|
2
|
+
import { FileKeyStore } from './file-key-store.js';
|
|
3
|
+
/**
|
|
4
|
+
* A {@link KeyManager} backed by the encrypted on-disk {@link FileKeyStore}.
|
|
5
|
+
*
|
|
6
|
+
* It composes a {@link LocalKeyManager} over a {@link FileKeyStore} and adds the
|
|
7
|
+
* one thing the store interface cannot express: persisting the active-key
|
|
8
|
+
* pointer. `LocalKeyManager` tracks the active key only in process memory, so
|
|
9
|
+
* this wrapper mirrors every active-key change to the keystore file and
|
|
10
|
+
* re-applies the persisted pointer at construction. Read and signing
|
|
11
|
+
* operations delegate straight through.
|
|
12
|
+
*
|
|
13
|
+
* Injected as the api's KeyManager so every command reaches it uniformly via
|
|
14
|
+
* `api.kms`, and "the active key" survives across CLI invocations.
|
|
15
|
+
*/
|
|
16
|
+
export class FileBackedKeyManager {
|
|
17
|
+
/** Capability probe: the local store supports exporting secret material. */
|
|
18
|
+
canExport = true;
|
|
19
|
+
#store;
|
|
20
|
+
#inner;
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.#store = new FileKeyStore(options);
|
|
23
|
+
this.#inner = new LocalKeyManager(this.#store);
|
|
24
|
+
// Apply the persisted active pointer only if the key still exists. A
|
|
25
|
+
// dangling pointer (from out-of-band file editing or a partial write) is
|
|
26
|
+
// ignored rather than thrown, so recovery commands stay usable; the next
|
|
27
|
+
// setActiveKey overwrites it. has() is a non-decrypting cache lookup.
|
|
28
|
+
const active = this.#store.getActive();
|
|
29
|
+
if (active && this.#store.has(active))
|
|
30
|
+
this.#inner.setActiveKey(active);
|
|
31
|
+
}
|
|
32
|
+
get activeKeyId() {
|
|
33
|
+
return this.#inner.activeKeyId;
|
|
34
|
+
}
|
|
35
|
+
setActiveKey(id) {
|
|
36
|
+
this.#inner.setActiveKey(id);
|
|
37
|
+
this.#store.setActive(id);
|
|
38
|
+
}
|
|
39
|
+
importKey(keyPair, options) {
|
|
40
|
+
const id = this.#inner.importKey(keyPair, options);
|
|
41
|
+
if (options?.setActive)
|
|
42
|
+
this.#store.setActive(id);
|
|
43
|
+
return id;
|
|
44
|
+
}
|
|
45
|
+
generateKey(options) {
|
|
46
|
+
const id = this.#inner.generateKey(options);
|
|
47
|
+
if (options?.setActive)
|
|
48
|
+
this.#store.setActive(id);
|
|
49
|
+
return id;
|
|
50
|
+
}
|
|
51
|
+
removeKey(id, options) {
|
|
52
|
+
// LocalKeyManager.removeKey calls FileKeyStore.delete, which already clears
|
|
53
|
+
// the persisted active pointer when the removed key was the active one.
|
|
54
|
+
this.#inner.removeKey(id, options);
|
|
55
|
+
}
|
|
56
|
+
listKeys() {
|
|
57
|
+
return this.#inner.listKeys();
|
|
58
|
+
}
|
|
59
|
+
getPublicKey(id) {
|
|
60
|
+
return this.#inner.getPublicKey(id);
|
|
61
|
+
}
|
|
62
|
+
getEntry(id) {
|
|
63
|
+
return this.#inner.getEntry(id);
|
|
64
|
+
}
|
|
65
|
+
sign(data, id, options) {
|
|
66
|
+
return this.#inner.sign(data, id, options);
|
|
67
|
+
}
|
|
68
|
+
verify(signature, data, id, options) {
|
|
69
|
+
return this.#inner.verify(signature, data, id, options);
|
|
70
|
+
}
|
|
71
|
+
digest(data) {
|
|
72
|
+
return this.#inner.digest(data);
|
|
73
|
+
}
|
|
74
|
+
exportKey(id) {
|
|
75
|
+
return this.#inner.exportKey(id);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=file-backed-key-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-backed-key-manager.js","sourceRoot":"","sources":["../../../../src/keystore/file-backed-key-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { base64urlnopad } from '@scure/base';
|
|
4
|
+
import { assertSecurePerms, ensureDir, writeFileAtomic } from './atomic.js';
|
|
5
|
+
import { DEFAULT_ARGON_PARAMS, decryptSecret, encryptSecret } from './envelope.js';
|
|
6
|
+
import { KeyStoreError } from './error.js';
|
|
7
|
+
import { defaultKeystorePath } from './paths.js';
|
|
8
|
+
/** Current on-disk keystore file format version. */
|
|
9
|
+
export const KEYSTORE_VERSION = 1;
|
|
10
|
+
/**
|
|
11
|
+
* A Node-only, file-backed {@link KeyValueStore} that encrypts secret keys at
|
|
12
|
+
* rest. It satisfies the synchronous store contract by caching the parsed file
|
|
13
|
+
* in memory at construction and flushing the whole file atomically on every
|
|
14
|
+
* mutation.
|
|
15
|
+
*
|
|
16
|
+
* Secrets are materialized only through {@link FileKeyStore.get}. The
|
|
17
|
+
* {@link FileKeyStore.list} and {@link FileKeyStore.entries} projections omit
|
|
18
|
+
* secret keys and never decrypt, so enumerating the store never triggers a
|
|
19
|
+
* passphrase prompt.
|
|
20
|
+
*/
|
|
21
|
+
export class FileKeyStore {
|
|
22
|
+
#path;
|
|
23
|
+
#getPassphrase;
|
|
24
|
+
#argonParams;
|
|
25
|
+
#cache = new Map();
|
|
26
|
+
#active;
|
|
27
|
+
constructor(options) {
|
|
28
|
+
this.#path = options.path ?? defaultKeystorePath();
|
|
29
|
+
this.#getPassphrase = options.getPassphrase;
|
|
30
|
+
this.#argonParams = options.argonParams ?? DEFAULT_ARGON_PARAMS;
|
|
31
|
+
ensureDir(dirname(this.#path), 0o700);
|
|
32
|
+
this.#load();
|
|
33
|
+
}
|
|
34
|
+
#load() {
|
|
35
|
+
if (!existsSync(this.#path))
|
|
36
|
+
return;
|
|
37
|
+
assertSecurePerms(this.#path);
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = JSON.parse(readFileSync(this.#path, 'utf-8'));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new KeyStoreError(`Keystore at ${this.#path} is corrupt or unreadable.`, 'KEYSTORE_CORRUPT_ERROR', { path: this.#path });
|
|
44
|
+
}
|
|
45
|
+
if (parsed.v !== KEYSTORE_VERSION) {
|
|
46
|
+
throw new KeyStoreError(`Unsupported keystore version: ${String(parsed.v)}.`, 'KEYSTORE_VERSION_ERROR', { version: parsed.v });
|
|
47
|
+
}
|
|
48
|
+
this.#active = parsed.active;
|
|
49
|
+
for (const [id, stored] of Object.entries(parsed.keys ?? {})) {
|
|
50
|
+
let publicKey;
|
|
51
|
+
try {
|
|
52
|
+
if (typeof stored.publicKey !== 'string')
|
|
53
|
+
throw new Error('missing publicKey');
|
|
54
|
+
publicKey = base64urlnopad.decode(stored.publicKey);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new KeyStoreError(`Keystore entry ${id} has a malformed public key.`, 'KEYSTORE_CORRUPT_ERROR', { path: this.#path, keyId: id });
|
|
58
|
+
}
|
|
59
|
+
if (publicKey.length !== 33) {
|
|
60
|
+
throw new KeyStoreError(`Keystore entry ${id} has a ${publicKey.length}-byte public key; expected 33.`, 'KEYSTORE_CORRUPT_ERROR', { path: this.#path, keyId: id });
|
|
61
|
+
}
|
|
62
|
+
this.#cache.set(id, {
|
|
63
|
+
publicKey,
|
|
64
|
+
...(stored.tags && { tags: stored.tags }),
|
|
65
|
+
...(stored.secret && { secret: stored.secret }),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
#flush() {
|
|
70
|
+
const keys = {};
|
|
71
|
+
for (const [id, entry] of this.#cache) {
|
|
72
|
+
keys[id] = {
|
|
73
|
+
publicKey: base64urlnopad.encode(entry.publicKey),
|
|
74
|
+
...(entry.tags && { tags: entry.tags }),
|
|
75
|
+
...(entry.secret && { secret: entry.secret }),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const file = {
|
|
79
|
+
v: KEYSTORE_VERSION,
|
|
80
|
+
...(this.#active && { active: this.#active }),
|
|
81
|
+
keys,
|
|
82
|
+
};
|
|
83
|
+
writeFileAtomic(this.#path, `${JSON.stringify(file, null, 2)}\n`, 0o600);
|
|
84
|
+
}
|
|
85
|
+
get(id) {
|
|
86
|
+
const entry = this.#cache.get(id);
|
|
87
|
+
if (!entry)
|
|
88
|
+
return undefined;
|
|
89
|
+
const result = {
|
|
90
|
+
publicKey: entry.publicKey,
|
|
91
|
+
...(entry.tags && { tags: entry.tags }),
|
|
92
|
+
};
|
|
93
|
+
if (entry.secret) {
|
|
94
|
+
// Materialize the secret lazily, only when it is actually accessed, so
|
|
95
|
+
// reads that need just public material (an active-key existence check,
|
|
96
|
+
// getPublicKey, getEntry) never trigger a passphrase prompt. The property
|
|
97
|
+
// is non-enumerable so spreading or serializing the entry cannot silently
|
|
98
|
+
// decrypt the secret.
|
|
99
|
+
const sealed = entry.secret;
|
|
100
|
+
Object.defineProperty(result, 'secretKey', {
|
|
101
|
+
configurable: true,
|
|
102
|
+
enumerable: false,
|
|
103
|
+
get: () => {
|
|
104
|
+
entry.decrypted ??= decryptSecret(sealed, this.#getPassphrase());
|
|
105
|
+
return entry.decrypted;
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
has(id) {
|
|
112
|
+
return this.#cache.has(id);
|
|
113
|
+
}
|
|
114
|
+
set(id, value) {
|
|
115
|
+
const secret = value.secretKey
|
|
116
|
+
? encryptSecret(value.secretKey, this.#getPassphrase(), this.#argonParams)
|
|
117
|
+
: undefined;
|
|
118
|
+
this.#cache.set(id, {
|
|
119
|
+
publicKey: value.publicKey,
|
|
120
|
+
...(value.tags && { tags: value.tags }),
|
|
121
|
+
...(secret && { secret }),
|
|
122
|
+
...(value.secretKey && { decrypted: value.secretKey }),
|
|
123
|
+
});
|
|
124
|
+
this.#flush();
|
|
125
|
+
}
|
|
126
|
+
delete(id) {
|
|
127
|
+
const existed = this.#cache.delete(id);
|
|
128
|
+
if (existed) {
|
|
129
|
+
if (this.#active === id)
|
|
130
|
+
this.#active = undefined;
|
|
131
|
+
this.#flush();
|
|
132
|
+
}
|
|
133
|
+
return existed;
|
|
134
|
+
}
|
|
135
|
+
clear() {
|
|
136
|
+
this.#cache.clear();
|
|
137
|
+
this.#active = undefined;
|
|
138
|
+
this.#flush();
|
|
139
|
+
}
|
|
140
|
+
/** All stored values with secret keys omitted. Never decrypts, never prompts. */
|
|
141
|
+
list() {
|
|
142
|
+
return this.entries().map(([, value]) => value);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* All entries as id-value tuples with secret keys omitted. Never decrypts,
|
|
146
|
+
* never prompts: {@link FileKeyStore.get} is the only secret-materializing
|
|
147
|
+
* path, so callers that only need identifiers (such as `listKeys`) do not
|
|
148
|
+
* force a passphrase prompt. This deviates intentionally from the in-memory
|
|
149
|
+
* store, which returns stored values verbatim.
|
|
150
|
+
*/
|
|
151
|
+
entries() {
|
|
152
|
+
const out = [];
|
|
153
|
+
for (const [id, entry] of this.#cache) {
|
|
154
|
+
out.push([id, {
|
|
155
|
+
publicKey: entry.publicKey,
|
|
156
|
+
...(entry.tags && { tags: entry.tags }),
|
|
157
|
+
}]);
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
close() {
|
|
162
|
+
for (const entry of this.#cache.values()) {
|
|
163
|
+
entry.decrypted?.fill(0);
|
|
164
|
+
entry.decrypted = undefined;
|
|
165
|
+
}
|
|
166
|
+
this.#cache.clear();
|
|
167
|
+
}
|
|
168
|
+
/** The persisted active-key identifier, or undefined if none is set. */
|
|
169
|
+
getActive() {
|
|
170
|
+
return this.#active;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Persists the active-key pointer in the keystore file. Passing undefined
|
|
174
|
+
* clears it. Throws if the identifier is not a known key.
|
|
175
|
+
*/
|
|
176
|
+
setActive(id) {
|
|
177
|
+
if (id !== undefined && !this.#cache.has(id)) {
|
|
178
|
+
throw new KeyStoreError(`Cannot set unknown key as active: ${id}.`, 'KEY_NOT_FOUND_ERROR', { keyId: id });
|
|
179
|
+
}
|
|
180
|
+
this.#active = id;
|
|
181
|
+
this.#flush();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=file-key-store.js.map
|