bsv-bap 0.2.0-alpha.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +276 -52
- package/dist/MasterID.d.ts +2 -1
- package/dist/constants.d.ts +3 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.modern.js +2 -2
- package/dist/index.modern.js.map +6 -6
- package/dist/index.module.js +2 -2
- package/dist/index.module.js.map +6 -6
- package/dist/interface.d.ts +4 -0
- package/dist/poa.d.ts +1 -1
- package/dist/touchid.d.ts +45 -0
- package/dist/utils.d.ts +12 -0
- package/package.json +13 -7
- package/src/cli.ts +653 -378
- package/src/touchid.ts +87 -0
package/src/cli.ts
CHANGED
|
@@ -1,406 +1,681 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
* BAP CLI - Bitcoin Attestation Protocol Command Line Interface
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* bap create [--name <name>] [--wif <wif>] Create new BAP identity
|
|
7
|
-
* bap sign <message> Sign a message
|
|
8
|
-
* bap verify <message> <sig> <address> Verify a signature
|
|
9
|
-
* bap friend-pubkey <friendBapId> Get friend public key
|
|
10
|
-
* bap encrypt <data> <friendBapId> Encrypt for friend
|
|
11
|
-
* bap decrypt <ciphertext> <friendBapId> Decrypt from friend
|
|
12
|
-
* bap export Export identity backup
|
|
13
|
-
* bap import <backup> Import identity from backup
|
|
14
|
-
* bap info Show current identity info
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
3
|
import { homedir } from "node:os";
|
|
19
4
|
import { join } from "node:path";
|
|
20
|
-
import { PrivateKey
|
|
21
|
-
import { BAP } from "bsv-bap";
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
5
|
+
import { PrivateKey } from "@bsv/sdk";
|
|
6
|
+
import { BAP, bapIdFromAddress, bapIdFromPubkey } from "bsv-bap";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import {
|
|
9
|
+
getTouchIDStatus,
|
|
10
|
+
isTouchIDSupported,
|
|
11
|
+
protectRootKey,
|
|
12
|
+
removeProtection,
|
|
13
|
+
unlockRootKey,
|
|
14
|
+
} from "./touchid.js";
|
|
15
|
+
|
|
16
|
+
// Storage paths
|
|
26
17
|
const CONFIG_DIR = join(homedir(), ".bap");
|
|
27
18
|
const CONFIG_FILE = join(CONFIG_DIR, "identity.json");
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
19
|
+
const ACTIVE_FILE = join(CONFIG_DIR, "active");
|
|
20
|
+
|
|
21
|
+
// Stored config shape — rootPk is plaintext, rootPkEncrypted is Touch ID protected.
|
|
22
|
+
// Only one should be present at a time.
|
|
23
|
+
interface StoredConfig {
|
|
24
|
+
rootPk?: string;
|
|
25
|
+
rootPkEncrypted?: string;
|
|
26
|
+
ids: string;
|
|
27
|
+
labels: Record<string, string>;
|
|
28
|
+
createdAt: string;
|
|
34
29
|
}
|
|
35
30
|
|
|
36
31
|
function ensureConfigDir(): void {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function loadIdentity(): BAP | null {
|
|
44
|
-
if (!existsSync(CONFIG_FILE)) {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
const data = JSON.parse(readFileSync(CONFIG_FILE, "utf-8")) as StoredIdentity;
|
|
50
|
-
const bap = new BAP({ rootPk: data.wif });
|
|
51
|
-
if (data.ids) {
|
|
52
|
-
bap.importIds(data.ids);
|
|
53
|
-
}
|
|
54
|
-
return bap;
|
|
55
|
-
} catch (error) {
|
|
56
|
-
console.error("Failed to load identity:", error);
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function saveIdentity(bap: BAP, wif: string, label?: string): void {
|
|
62
|
-
ensureConfigDir();
|
|
63
|
-
const backup: StoredIdentity = {
|
|
64
|
-
wif,
|
|
65
|
-
ids: bap.exportIds(),
|
|
66
|
-
...(label && { label }),
|
|
67
|
-
createdAt: new Date().toISOString(),
|
|
68
|
-
};
|
|
69
|
-
writeFileSync(CONFIG_FILE, JSON.stringify(backup, null, 2));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function printUsage(): void {
|
|
73
|
-
console.log(`
|
|
74
|
-
BAP CLI - Bitcoin Attestation Protocol
|
|
75
|
-
|
|
76
|
-
Commands:
|
|
77
|
-
create [--name <name>] [--wif <wif>] Create new BAP identity
|
|
78
|
-
sign <message> Sign a message with identity
|
|
79
|
-
verify <message> <sig> <address> Verify a BSM signature
|
|
80
|
-
friend-pubkey <friendBapId> Get encryption pubkey for friend
|
|
81
|
-
encrypt <data> <friendBapId> Encrypt data for friend
|
|
82
|
-
decrypt <ciphertext> <friendBapId> Decrypt data from friend
|
|
83
|
-
export Export identity backup (JSON)
|
|
84
|
-
import <file> Import identity from backup file
|
|
85
|
-
info Show current identity info
|
|
86
|
-
help Show this help message
|
|
87
|
-
|
|
88
|
-
Options:
|
|
89
|
-
--name <name> Identity name (for create)
|
|
90
|
-
--wif <wif> Use existing WIF key (for create)
|
|
91
|
-
|
|
92
|
-
Examples:
|
|
93
|
-
bap create --name "My Identity"
|
|
94
|
-
bap sign "Hello World"
|
|
95
|
-
bap verify "Hello World" <signature> <address>
|
|
96
|
-
bap friend-pubkey "abc123..."
|
|
97
|
-
bap encrypt "secret message" "abc123..."
|
|
98
|
-
bap decrypt "<ciphertext>" "abc123..."
|
|
99
|
-
bap export > backup.json
|
|
100
|
-
bap import backup.json
|
|
101
|
-
`);
|
|
32
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
33
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
34
|
+
}
|
|
102
35
|
}
|
|
103
36
|
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// Parse arguments
|
|
109
|
-
for (let i = 0; i < args.length; i++) {
|
|
110
|
-
if (args[i] === "--name" && args[i + 1]) {
|
|
111
|
-
name = args[i + 1];
|
|
112
|
-
i++;
|
|
113
|
-
} else if (args[i] === "--wif" && args[i + 1]) {
|
|
114
|
-
wif = args[i + 1];
|
|
115
|
-
i++;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Generate or use provided WIF
|
|
120
|
-
if (!wif) {
|
|
121
|
-
const privateKey = PrivateKey.fromRandom();
|
|
122
|
-
wif = privateKey.toWif();
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Create BAP instance with Type42
|
|
126
|
-
const bap = new BAP({ rootPk: wif });
|
|
127
|
-
const identity = bap.newId(name);
|
|
128
|
-
|
|
129
|
-
// Save to config
|
|
130
|
-
saveIdentity(bap, wif);
|
|
131
|
-
|
|
132
|
-
console.log("Identity created successfully!");
|
|
133
|
-
console.log(` Name: ${name}`);
|
|
134
|
-
console.log(` Identity Key: ${identity.getIdentityKey()}`);
|
|
135
|
-
console.log(` Root Address: ${identity.rootAddress}`);
|
|
136
|
-
console.log(` Signing Address: ${identity.getCurrentAddress()}`);
|
|
137
|
-
console.log(`\nStored at: ${CONFIG_FILE}`);
|
|
37
|
+
function loadConfig(): StoredConfig | null {
|
|
38
|
+
if (!existsSync(CONFIG_FILE)) return null;
|
|
39
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8")) as StoredConfig;
|
|
138
40
|
}
|
|
139
41
|
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const ids = bap.listIds();
|
|
148
|
-
if (ids.length === 0) {
|
|
149
|
-
console.error("No identities in BAP instance.");
|
|
150
|
-
process.exit(1);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const identity = bap.getId(ids[0]);
|
|
154
|
-
if (!identity) {
|
|
155
|
-
console.error("Failed to get identity.");
|
|
156
|
-
process.exit(1);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const { address, signature } = identity.signMessage(toArray(message, "utf8"));
|
|
160
|
-
|
|
161
|
-
console.log(JSON.stringify({ message, address, signature }, null, 2));
|
|
42
|
+
function createBAP(key: string): BAP {
|
|
43
|
+
// xprv keys start with "xprv" — use BIP32 mode; otherwise Type42
|
|
44
|
+
if (key.startsWith("xprv")) {
|
|
45
|
+
return new BAP(key);
|
|
46
|
+
}
|
|
47
|
+
return new BAP({ rootPk: key });
|
|
162
48
|
}
|
|
163
49
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the root private key from config.
|
|
52
|
+
* If Touch ID protected, triggers biometric auth to decrypt.
|
|
53
|
+
* Returns the plaintext key string (WIF or xprv).
|
|
54
|
+
*/
|
|
55
|
+
async function resolveRootKey(config: StoredConfig): Promise<string> {
|
|
56
|
+
if (config.rootPk) {
|
|
57
|
+
return config.rootPk;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (config.rootPkEncrypted) {
|
|
61
|
+
return unlockRootKey(config.rootPkEncrypted);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Legacy format: older BAP configs used "wif" instead of "rootPk"
|
|
65
|
+
const legacy = config as Record<string, unknown>;
|
|
66
|
+
if (typeof legacy.wif === "string") {
|
|
67
|
+
return legacy.wif;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
throw new Error(
|
|
71
|
+
"Config has neither rootPk nor rootPkEncrypted. File may be corrupt."
|
|
72
|
+
);
|
|
169
73
|
}
|
|
170
74
|
|
|
171
|
-
function
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
process.exit(1);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const publicKey = identity.getEncryptionPublicKeyWithSeed(friendBapId);
|
|
191
|
-
console.log(JSON.stringify({ friendBapId, publicKey }, null, 2));
|
|
75
|
+
async function loadBAP(): Promise<{
|
|
76
|
+
bap: BAP;
|
|
77
|
+
config: StoredConfig;
|
|
78
|
+
rootPk: string;
|
|
79
|
+
}> {
|
|
80
|
+
const config = loadConfig();
|
|
81
|
+
if (!config) {
|
|
82
|
+
console.error("No identity found. Run 'bap create' first.");
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
const rootPk = await resolveRootKey(config);
|
|
86
|
+
const bap = createBAP(rootPk);
|
|
87
|
+
if (config.ids) {
|
|
88
|
+
bap.importIds(config.ids);
|
|
89
|
+
}
|
|
90
|
+
return { bap, config, rootPk };
|
|
192
91
|
}
|
|
193
92
|
|
|
194
|
-
function
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
93
|
+
function saveConfig(
|
|
94
|
+
bap: BAP,
|
|
95
|
+
rootPk: string,
|
|
96
|
+
labels: Record<string, string>,
|
|
97
|
+
createdAt?: string,
|
|
98
|
+
encrypted?: { rootPkEncrypted: string }
|
|
99
|
+
): void {
|
|
100
|
+
ensureConfigDir();
|
|
101
|
+
const config: StoredConfig = {
|
|
102
|
+
ids: bap.exportIds(),
|
|
103
|
+
labels,
|
|
104
|
+
createdAt: createdAt ?? new Date().toISOString(),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (encrypted) {
|
|
108
|
+
config.rootPkEncrypted = encrypted.rootPkEncrypted;
|
|
109
|
+
} else {
|
|
110
|
+
config.rootPk = rootPk;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
215
114
|
}
|
|
216
115
|
|
|
217
|
-
function
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
console.error("No identity found. Run 'bap create' first.");
|
|
221
|
-
process.exit(1);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const ids = bap.listIds();
|
|
225
|
-
if (ids.length === 0) {
|
|
226
|
-
console.error("No identities in BAP instance.");
|
|
227
|
-
process.exit(1);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const identity = bap.getId(ids[0]);
|
|
231
|
-
if (!identity) {
|
|
232
|
-
console.error("Failed to get identity.");
|
|
233
|
-
process.exit(1);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
try {
|
|
237
|
-
const data = identity.decryptWithSeed(ciphertext, friendBapId);
|
|
238
|
-
console.log(JSON.stringify({ data, friendBapId }, null, 2));
|
|
239
|
-
} catch (error) {
|
|
240
|
-
console.error("Decryption failed:", error instanceof Error ? error.message : error);
|
|
241
|
-
process.exit(1);
|
|
242
|
-
}
|
|
116
|
+
function getActiveBapId(): string | null {
|
|
117
|
+
if (!existsSync(ACTIVE_FILE)) return null;
|
|
118
|
+
return readFileSync(ACTIVE_FILE, "utf-8").trim();
|
|
243
119
|
}
|
|
244
120
|
|
|
245
|
-
function
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
console.error("No identity found. Run 'bap create' first.");
|
|
249
|
-
process.exit(1);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Read stored WIF
|
|
253
|
-
const stored = JSON.parse(readFileSync(CONFIG_FILE, "utf-8")) as StoredIdentity;
|
|
254
|
-
const backup = bap.exportForBackup(stored.label);
|
|
255
|
-
|
|
256
|
-
console.log(JSON.stringify(backup, null, 2));
|
|
121
|
+
function setActiveBapId(bapId: string): void {
|
|
122
|
+
ensureConfigDir();
|
|
123
|
+
writeFileSync(ACTIVE_FILE, bapId);
|
|
257
124
|
}
|
|
258
125
|
|
|
259
|
-
function
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (backup.rootPk) {
|
|
277
|
-
// Type42 format
|
|
278
|
-
bap = new BAP({ rootPk: backup.rootPk });
|
|
279
|
-
wif = backup.rootPk;
|
|
280
|
-
} else {
|
|
281
|
-
// BIP32 format (legacy)
|
|
282
|
-
bap = new BAP(backup.xprv);
|
|
283
|
-
wif = backup.xprv;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (backup.ids) {
|
|
287
|
-
bap.importIds(backup.ids);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
saveIdentity(bap, wif, backup.label);
|
|
291
|
-
|
|
292
|
-
const ids = bap.listIds();
|
|
293
|
-
console.log("Identity imported successfully!");
|
|
294
|
-
console.log(` Identities: ${ids.length}`);
|
|
295
|
-
if (backup.label) {
|
|
296
|
-
console.log(` Label: ${backup.label}`);
|
|
297
|
-
}
|
|
298
|
-
console.log(`\nStored at: ${CONFIG_FILE}`);
|
|
299
|
-
} catch (error) {
|
|
300
|
-
console.error("Failed to import identity:", error);
|
|
301
|
-
process.exit(1);
|
|
302
|
-
}
|
|
126
|
+
function getActiveIdentity(bap: BAP, config: StoredConfig) {
|
|
127
|
+
const activeBapId = getActiveBapId();
|
|
128
|
+
const ids = bap.listIds();
|
|
129
|
+
|
|
130
|
+
if (ids.length === 0) {
|
|
131
|
+
console.error("No identities found. Run 'bap create' first.");
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const bapId = activeBapId && ids.includes(activeBapId) ? activeBapId : ids[0];
|
|
136
|
+
const identity = bap.getId(bapId);
|
|
137
|
+
if (!identity) {
|
|
138
|
+
console.error(`Identity ${bapId} not found.`);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
return { identity, bapId, label: config.labels?.[bapId] };
|
|
303
142
|
}
|
|
304
143
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Attempt to protect a root key with Touch ID.
|
|
146
|
+
* Returns { rootPkEncrypted } on success, or null if Touch ID is unavailable.
|
|
147
|
+
* Prints status messages.
|
|
148
|
+
*
|
|
149
|
+
* Set BAP_NO_TOUCHID=1 to disable (used by tests and non-interactive environments).
|
|
150
|
+
*/
|
|
151
|
+
async function tryProtectWithTouchID(
|
|
152
|
+
rootPk: string
|
|
153
|
+
): Promise<{ rootPkEncrypted: string } | null> {
|
|
154
|
+
if (process.env.BAP_NO_TOUCHID === "1") {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!isTouchIDSupported()) {
|
|
159
|
+
console.log(
|
|
160
|
+
" Touch ID: not available (platform unsupported) -- key stored as plaintext"
|
|
161
|
+
);
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const status = await getTouchIDStatus(false);
|
|
167
|
+
if (!status.available) {
|
|
168
|
+
console.log(
|
|
169
|
+
` Touch ID: not available (${status.biometryType}) -- key stored as plaintext`
|
|
170
|
+
);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const rootPkEncrypted = await protectRootKey(rootPk);
|
|
175
|
+
console.log(` Touch ID: protected (${status.biometryType})`);
|
|
176
|
+
return { rootPkEncrypted };
|
|
177
|
+
} catch (err) {
|
|
178
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
179
|
+
console.log(` Touch ID: skipped (${msg})`);
|
|
180
|
+
console.log(" WARNING: Key is stored as plaintext on disk.");
|
|
181
|
+
console.log(" Run 'bap touchid enable' later to protect it with Secure Enclave.");
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
327
184
|
}
|
|
328
185
|
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
186
|
+
// --- CLI ---
|
|
187
|
+
|
|
188
|
+
const program = new Command();
|
|
189
|
+
|
|
190
|
+
program
|
|
191
|
+
.name("bap")
|
|
192
|
+
.description("BAP - Bitcoin Attestation Protocol CLI")
|
|
193
|
+
.version("0.2.0");
|
|
194
|
+
|
|
195
|
+
// Identity Management
|
|
196
|
+
|
|
197
|
+
program
|
|
198
|
+
.command("create")
|
|
199
|
+
.description("Create a new identity")
|
|
200
|
+
.option("--name <name>", "Human-readable label for the identity")
|
|
201
|
+
.option("--wif <wif>", "Use an existing WIF key as the master key")
|
|
202
|
+
.option("--no-touchid", "Skip Touch ID protection")
|
|
203
|
+
.action(async (opts) => {
|
|
204
|
+
const config = loadConfig();
|
|
205
|
+
let rootPk: string;
|
|
206
|
+
let labels: Record<string, string>;
|
|
207
|
+
let createdAt: string | undefined;
|
|
208
|
+
let bap: BAP;
|
|
209
|
+
let encrypted: { rootPkEncrypted: string } | undefined;
|
|
210
|
+
|
|
211
|
+
if (config) {
|
|
212
|
+
// Existing master -- add a new identity
|
|
213
|
+
rootPk = await resolveRootKey(config);
|
|
214
|
+
labels = config.labels;
|
|
215
|
+
createdAt = config.createdAt;
|
|
216
|
+
bap = createBAP(rootPk);
|
|
217
|
+
bap.importIds(config.ids);
|
|
218
|
+
|
|
219
|
+
// Preserve existing protection state
|
|
220
|
+
if (config.rootPkEncrypted) {
|
|
221
|
+
encrypted = { rootPkEncrypted: config.rootPkEncrypted };
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
// New master
|
|
225
|
+
rootPk = opts.wif ?? PrivateKey.fromRandom().toWif();
|
|
226
|
+
labels = {};
|
|
227
|
+
bap = createBAP(rootPk);
|
|
228
|
+
|
|
229
|
+
// Protect new key with Touch ID if available
|
|
230
|
+
if (opts.touchid !== false) {
|
|
231
|
+
const protection = await tryProtectWithTouchID(rootPk);
|
|
232
|
+
if (protection) {
|
|
233
|
+
encrypted = protection;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const identity = bap.newId();
|
|
239
|
+
const bapId = identity.bapId;
|
|
240
|
+
|
|
241
|
+
if (opts.name) {
|
|
242
|
+
labels[bapId] = opts.name;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
saveConfig(bap, rootPk, labels, createdAt, encrypted);
|
|
246
|
+
setActiveBapId(bapId);
|
|
247
|
+
|
|
248
|
+
console.log("Identity created:");
|
|
249
|
+
console.log(` BAP ID: ${bapId}`);
|
|
250
|
+
if (opts.name) console.log(` Label: ${opts.name}`);
|
|
251
|
+
console.log(` Root Address: ${identity.rootAddress}`);
|
|
252
|
+
console.log(` Root Path: ${identity.rootPath}`);
|
|
253
|
+
console.log(` Stored at: ${CONFIG_FILE}`);
|
|
254
|
+
console.log("");
|
|
255
|
+
console.log(" IMPORTANT: Back up your identity now with 'bap export > backup.json'.");
|
|
256
|
+
console.log(" If Touch ID is enabled, your key is hardware-bound and cannot be");
|
|
257
|
+
console.log(" recovered from another machine without this backup.");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
program
|
|
261
|
+
.command("list")
|
|
262
|
+
.description("List all identities (* = active)")
|
|
263
|
+
.action(async () => {
|
|
264
|
+
const { bap, config } = await loadBAP();
|
|
265
|
+
const ids = bap.listIds();
|
|
266
|
+
const active = getActiveBapId();
|
|
267
|
+
|
|
268
|
+
if (ids.length === 0) {
|
|
269
|
+
console.log("No identities. Run 'bap create' to get started.");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
for (const bapId of ids) {
|
|
274
|
+
const marker = bapId === active ? " *" : " ";
|
|
275
|
+
const label = config.labels?.[bapId];
|
|
276
|
+
const suffix = label ? ` (${label})` : "";
|
|
277
|
+
console.log(`${marker} ${bapId}${suffix}`);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
program
|
|
282
|
+
.command("use")
|
|
283
|
+
.description("Set active identity")
|
|
284
|
+
.argument("<bapId>", "BAP ID to activate")
|
|
285
|
+
.action(async (bapId: string) => {
|
|
286
|
+
const { bap } = await loadBAP();
|
|
287
|
+
const ids = bap.listIds();
|
|
288
|
+
|
|
289
|
+
if (!ids.includes(bapId)) {
|
|
290
|
+
console.error(`Identity ${bapId} not found.`);
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
setActiveBapId(bapId);
|
|
295
|
+
console.log(`Active identity: ${bapId}`);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
program
|
|
299
|
+
.command("info")
|
|
300
|
+
.description("Show active identity details")
|
|
301
|
+
.action(async () => {
|
|
302
|
+
const { bap, config } = await loadBAP();
|
|
303
|
+
const { identity, bapId, label } = getActiveIdentity(bap, config);
|
|
304
|
+
|
|
305
|
+
console.log("Active Identity:");
|
|
306
|
+
console.log(` BAP ID: ${bapId}`);
|
|
307
|
+
if (label) console.log(` Label: ${label}`);
|
|
308
|
+
console.log(` Root Address: ${identity.rootAddress}`);
|
|
309
|
+
console.log(` Root Path: ${identity.rootPath}`);
|
|
310
|
+
console.log(` Current Path: ${identity.currentPath}`);
|
|
311
|
+
console.log(` Previous Path: ${identity.previousPath}`);
|
|
312
|
+
console.log(
|
|
313
|
+
` Account Key: ${identity.getAccountKey().toPublicKey().toString()}`
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
program
|
|
318
|
+
.command("remove")
|
|
319
|
+
.description("Remove an identity")
|
|
320
|
+
.argument("<bapId>", "BAP ID to remove")
|
|
321
|
+
.action(async (bapId: string) => {
|
|
322
|
+
const { bap, config, rootPk } = await loadBAP();
|
|
323
|
+
|
|
324
|
+
if (!bap.getId(bapId)) {
|
|
325
|
+
console.error(`Identity ${bapId} not found.`);
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
bap.removeId(bapId);
|
|
330
|
+
if (config.labels) delete config.labels[bapId];
|
|
331
|
+
|
|
332
|
+
// Preserve Touch ID protection state
|
|
333
|
+
const encrypted = config.rootPkEncrypted
|
|
334
|
+
? { rootPkEncrypted: config.rootPkEncrypted }
|
|
335
|
+
: undefined;
|
|
336
|
+
saveConfig(bap, rootPk, config.labels, config.createdAt, encrypted);
|
|
337
|
+
|
|
338
|
+
// Clear active if it was this one
|
|
339
|
+
if (getActiveBapId() === bapId) {
|
|
340
|
+
const remaining = bap.listIds();
|
|
341
|
+
if (remaining.length > 0) {
|
|
342
|
+
setActiveBapId(remaining[0]);
|
|
343
|
+
} else {
|
|
344
|
+
writeFileSync(ACTIVE_FILE, "");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
console.log(`Removed identity: ${bapId}`);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Backup
|
|
352
|
+
|
|
353
|
+
program
|
|
354
|
+
.command("export")
|
|
355
|
+
.description("Export master backup (JSON to stdout)")
|
|
356
|
+
.action(async () => {
|
|
357
|
+
const { bap, config } = await loadBAP();
|
|
358
|
+
if (config.rootPkEncrypted) {
|
|
359
|
+
console.error(
|
|
360
|
+
"NOTE: Your key is protected by Secure Enclave (hardware-bound)."
|
|
361
|
+
);
|
|
362
|
+
console.error(
|
|
363
|
+
"This backup is the ONLY recovery path if this machine is lost or wiped."
|
|
364
|
+
);
|
|
365
|
+
console.error("Store it securely.\n");
|
|
366
|
+
}
|
|
367
|
+
const backup = bap.exportForBackup();
|
|
368
|
+
console.log(JSON.stringify(backup, null, 2));
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
program
|
|
372
|
+
.command("export-account")
|
|
373
|
+
.description("Export account backup for active or specified identity")
|
|
374
|
+
.option("--id <bapId>", "Specific BAP ID to export")
|
|
375
|
+
.action(async (opts) => {
|
|
376
|
+
const { bap, config } = await loadBAP();
|
|
377
|
+
|
|
378
|
+
const identity = opts.id
|
|
379
|
+
? (() => {
|
|
380
|
+
const id = bap.getId(opts.id);
|
|
381
|
+
if (!id) {
|
|
382
|
+
console.error(`Identity ${opts.id} not found.`);
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
return id;
|
|
386
|
+
})()
|
|
387
|
+
: getActiveIdentity(bap, config).identity;
|
|
388
|
+
|
|
389
|
+
const backup = identity.exportAccountBackup();
|
|
390
|
+
console.log(JSON.stringify(backup, null, 2));
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
program
|
|
394
|
+
.command("import")
|
|
395
|
+
.description("Import from backup file")
|
|
396
|
+
.argument("<file>", "Path to backup JSON file")
|
|
397
|
+
.option("--no-touchid", "Skip Touch ID protection")
|
|
398
|
+
.action(async (file: string, opts) => {
|
|
399
|
+
if (!existsSync(file)) {
|
|
400
|
+
console.error(`File not found: ${file}`);
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const backup = JSON.parse(readFileSync(file, "utf-8"));
|
|
405
|
+
|
|
406
|
+
if (!backup.rootPk && !backup.xprv) {
|
|
407
|
+
console.error("Invalid backup format: missing rootPk or xprv");
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let bap: BAP;
|
|
412
|
+
let rootPk: string;
|
|
413
|
+
|
|
414
|
+
if (backup.rootPk) {
|
|
415
|
+
bap = new BAP({ rootPk: backup.rootPk });
|
|
416
|
+
rootPk = backup.rootPk;
|
|
417
|
+
} else {
|
|
418
|
+
bap = new BAP(backup.xprv);
|
|
419
|
+
rootPk = backup.xprv;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (backup.ids) {
|
|
423
|
+
bap.importIds(backup.ids);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const labels: Record<string, string> = {};
|
|
427
|
+
if (backup.label) {
|
|
428
|
+
// Apply label to first identity as a default
|
|
429
|
+
const ids = bap.listIds();
|
|
430
|
+
if (ids.length > 0) {
|
|
431
|
+
labels[ids[0]] = backup.label;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Protect with Touch ID if available
|
|
436
|
+
let encrypted: { rootPkEncrypted: string } | undefined;
|
|
437
|
+
if (opts.touchid !== false) {
|
|
438
|
+
const protection = await tryProtectWithTouchID(rootPk);
|
|
439
|
+
if (protection) {
|
|
440
|
+
encrypted = protection;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
saveConfig(bap, rootPk, labels, undefined, encrypted);
|
|
445
|
+
|
|
446
|
+
const ids = bap.listIds();
|
|
447
|
+
if (ids.length > 0) {
|
|
448
|
+
setActiveBapId(ids[0]);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
console.log("Backup imported:");
|
|
452
|
+
console.log(` Identities: ${ids.length}`);
|
|
453
|
+
if (backup.label) console.log(` Label: ${backup.label}`);
|
|
454
|
+
console.log(` Stored at: ${CONFIG_FILE}`);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// Crypto
|
|
458
|
+
|
|
459
|
+
program
|
|
460
|
+
.command("encrypt")
|
|
461
|
+
.description("Encrypt data with master key (ECIES)")
|
|
462
|
+
.argument("<data>", "Data to encrypt")
|
|
463
|
+
.action(async (data: string) => {
|
|
464
|
+
const { bap } = await loadBAP();
|
|
465
|
+
console.log(bap.encrypt(data));
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
program
|
|
469
|
+
.command("decrypt")
|
|
470
|
+
.description("Decrypt ciphertext with master key")
|
|
471
|
+
.argument("<ciphertext>", "Base64 ciphertext to decrypt")
|
|
472
|
+
.action(async (ciphertext: string) => {
|
|
473
|
+
const { bap } = await loadBAP();
|
|
474
|
+
console.log(bap.decrypt(ciphertext));
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
program
|
|
478
|
+
.command("verify")
|
|
479
|
+
.description("Verify a BSM signature")
|
|
480
|
+
.argument("<message>", "Original message")
|
|
481
|
+
.argument("<signature>", "Base64 signature")
|
|
482
|
+
.argument("<address>", "Signing address")
|
|
483
|
+
.action((message: string, signature: string, address: string) => {
|
|
484
|
+
const bap = new BAP({ rootPk: PrivateKey.fromRandom().toWif() });
|
|
485
|
+
let valid = false;
|
|
486
|
+
try {
|
|
487
|
+
valid = bap.verifySignature(message, address, signature);
|
|
488
|
+
} catch {
|
|
489
|
+
// Invalid signature format -- treat as not valid
|
|
490
|
+
}
|
|
491
|
+
console.log(
|
|
492
|
+
JSON.stringify({ valid, message, address, signature }, null, 2)
|
|
493
|
+
);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// API Lookups
|
|
497
|
+
|
|
498
|
+
program
|
|
499
|
+
.command("lookup")
|
|
500
|
+
.description("Lookup identity on the BAP overlay")
|
|
501
|
+
.argument("<bapId>", "BAP ID to lookup")
|
|
502
|
+
.action(async (bapId: string) => {
|
|
503
|
+
const bap = new BAP({ rootPk: PrivateKey.fromRandom().toWif() });
|
|
504
|
+
const result = await bap.getIdentity(bapId);
|
|
505
|
+
console.log(JSON.stringify(result, null, 2));
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
program
|
|
509
|
+
.command("lookup-address")
|
|
510
|
+
.description("Lookup identity by Bitcoin address")
|
|
511
|
+
.argument("<address>", "Bitcoin address to lookup")
|
|
512
|
+
.action(async (address: string) => {
|
|
513
|
+
const bap = new BAP({ rootPk: PrivateKey.fromRandom().toWif() });
|
|
514
|
+
const result = await bap.getIdentityFromAddress(address);
|
|
515
|
+
console.log(JSON.stringify(result, null, 2));
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
program
|
|
519
|
+
.command("attestations")
|
|
520
|
+
.description("Get attestations for an attribute hash")
|
|
521
|
+
.argument("<hash>", "Attribute hash to lookup")
|
|
522
|
+
.action(async (hash: string) => {
|
|
523
|
+
const bap = new BAP({ rootPk: PrivateKey.fromRandom().toWif() });
|
|
524
|
+
const result = await bap.getAttestationsForHash(hash);
|
|
525
|
+
console.log(JSON.stringify(result, null, 2));
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// Utilities
|
|
529
|
+
|
|
530
|
+
program
|
|
531
|
+
.command("id-from-address")
|
|
532
|
+
.description("Derive BAP ID from a Bitcoin address")
|
|
533
|
+
.argument("<address>", "Bitcoin address (must be the root/member address)")
|
|
534
|
+
.action((address: string) => {
|
|
535
|
+
console.log(bapIdFromAddress(address));
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
program
|
|
539
|
+
.command("id-from-pubkey")
|
|
540
|
+
.description("Derive BAP ID from a compressed public key")
|
|
541
|
+
.argument("<pubkey>", "Compressed public key hex (must be the member key)")
|
|
542
|
+
.action((pubkey: string) => {
|
|
543
|
+
console.log(bapIdFromPubkey(pubkey));
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// Touch ID Management
|
|
547
|
+
|
|
548
|
+
const touchid = program
|
|
549
|
+
.command("touchid")
|
|
550
|
+
.description("Manage Touch ID key protection");
|
|
551
|
+
|
|
552
|
+
touchid
|
|
553
|
+
.command("status")
|
|
554
|
+
.description("Check Touch ID availability and protection status")
|
|
555
|
+
.action(async () => {
|
|
556
|
+
const config = loadConfig();
|
|
557
|
+
const hasEncryptedKey = !!config?.rootPkEncrypted;
|
|
558
|
+
const status = await getTouchIDStatus(hasEncryptedKey);
|
|
559
|
+
|
|
560
|
+
console.log("Touch ID Status:");
|
|
561
|
+
console.log(` Available: ${status.available}`);
|
|
562
|
+
console.log(` Biometry: ${status.biometryType}`);
|
|
563
|
+
console.log(` Key Protected: ${status.protected}`);
|
|
564
|
+
|
|
565
|
+
if (config && !hasEncryptedKey && status.available) {
|
|
566
|
+
console.log("\n Your identity key is stored as plaintext.");
|
|
567
|
+
console.log(" Run 'bap touchid enable' to protect it with Touch ID.");
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
touchid
|
|
572
|
+
.command("enable")
|
|
573
|
+
.description("Protect identity key with Touch ID")
|
|
574
|
+
.action(async () => {
|
|
575
|
+
const config = loadConfig();
|
|
576
|
+
if (!config) {
|
|
577
|
+
console.error("No identity found. Run 'bap create' first.");
|
|
578
|
+
process.exit(1);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (config.rootPkEncrypted) {
|
|
582
|
+
if (config.rootPkEncrypted.startsWith("se:")) {
|
|
583
|
+
console.log("Identity key is already protected with Secure Enclave.");
|
|
584
|
+
} else {
|
|
585
|
+
console.error(
|
|
586
|
+
'Identity key uses the legacy encryption format which is no longer supported. Re-import your backup with "bap import <file>" to migrate.'
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Support legacy "wif" field from older BAP configs
|
|
593
|
+
const plainKey = config.rootPk ?? (config as Record<string, unknown>).wif as string | undefined;
|
|
594
|
+
if (!plainKey) {
|
|
595
|
+
console.error(
|
|
596
|
+
"Config has no plaintext key to protect. File may be corrupt."
|
|
597
|
+
);
|
|
598
|
+
process.exit(1);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (!isTouchIDSupported()) {
|
|
602
|
+
console.error(
|
|
603
|
+
"Secure Enclave is not available on this platform (requires macOS arm64)."
|
|
604
|
+
);
|
|
605
|
+
process.exit(1);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const status = await getTouchIDStatus(false);
|
|
609
|
+
if (!status.available) {
|
|
610
|
+
console.error(
|
|
611
|
+
`Touch ID is not available on this machine (biometry type: ${status.biometryType}).`
|
|
612
|
+
);
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
console.log("WARNING: Secure Enclave keys are hardware-bound to THIS machine.");
|
|
617
|
+
console.log("Export a backup first with 'bap export > backup.json' if you haven't already.\n");
|
|
618
|
+
console.log("Encrypting identity key with Secure Enclave...");
|
|
619
|
+
const rootPkEncrypted = await protectRootKey(plainKey);
|
|
620
|
+
|
|
621
|
+
// Rewrite config: replace rootPk with rootPkEncrypted (sentinel "se:bap-master")
|
|
622
|
+
const newConfig: StoredConfig = {
|
|
623
|
+
rootPkEncrypted,
|
|
624
|
+
ids: config.ids,
|
|
625
|
+
labels: config.labels,
|
|
626
|
+
createdAt: config.createdAt,
|
|
627
|
+
};
|
|
628
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(newConfig, null, 2));
|
|
629
|
+
|
|
630
|
+
console.log("Identity key is now protected with Secure Enclave + Touch ID.");
|
|
631
|
+
console.log("The plaintext key has been removed from disk.");
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
touchid
|
|
635
|
+
.command("disable")
|
|
636
|
+
.description("Remove Touch ID protection (stores key as plaintext)")
|
|
637
|
+
.action(async () => {
|
|
638
|
+
const config = loadConfig();
|
|
639
|
+
if (!config) {
|
|
640
|
+
console.error("No identity found. Run 'bap create' first.");
|
|
641
|
+
process.exit(1);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (!config.rootPkEncrypted) {
|
|
645
|
+
console.log(
|
|
646
|
+
"Identity key is not Touch ID protected. Nothing to disable."
|
|
647
|
+
);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (!config.rootPkEncrypted.startsWith("se:")) {
|
|
652
|
+
console.error(
|
|
653
|
+
"Identity key uses the legacy encryption format which is no longer supported."
|
|
654
|
+
);
|
|
655
|
+
console.error(
|
|
656
|
+
"You will need to re-import your backup. Run 'bap export' if you can still decrypt, or use your backup file."
|
|
657
|
+
);
|
|
658
|
+
process.exit(1);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
console.log("Decrypting identity key (Touch ID required)...");
|
|
662
|
+
const rootPk = await unlockRootKey(config.rootPkEncrypted);
|
|
663
|
+
|
|
664
|
+
// Rewrite config: replace rootPkEncrypted with rootPk
|
|
665
|
+
const newConfig: StoredConfig = {
|
|
666
|
+
rootPk,
|
|
667
|
+
ids: config.ids,
|
|
668
|
+
labels: config.labels,
|
|
669
|
+
createdAt: config.createdAt,
|
|
670
|
+
};
|
|
671
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(newConfig, null, 2));
|
|
672
|
+
|
|
673
|
+
// Remove the Secure Enclave key and vault file
|
|
674
|
+
await removeProtection();
|
|
675
|
+
|
|
676
|
+
console.log("Secure Enclave protection removed.");
|
|
677
|
+
console.log("WARNING: Your identity key is now stored as plaintext on disk.");
|
|
678
|
+
console.log("Anyone with access to this machine can read it.");
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
program.parse();
|