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/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, Utils } from "@bsv/sdk";
21
- import { BAP } from "bsv-bap";
22
-
23
- const { toHex, toArray } = Utils;
24
-
25
- // Default config path
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
- interface StoredIdentity {
30
- wif: string;
31
- ids: string;
32
- label?: string;
33
- createdAt: string;
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
- const { mkdirSync } = require("node:fs");
38
- if (!existsSync(CONFIG_DIR)) {
39
- mkdirSync(CONFIG_DIR, { recursive: true });
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 createIdentity(args: string[]): void {
105
- let name = "Default Identity";
106
- let wif: string | undefined;
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 signMessage(message: string): void {
141
- const bap = loadIdentity();
142
- if (!bap) {
143
- console.error("No identity found. Run 'bap create' first.");
144
- process.exit(1);
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
- function verifySignature(message: string, signature: string, address: string): void {
165
- const bap = new BAP({ rootPk: PrivateKey.fromRandom().toWif() }); // Temporary instance
166
- const isValid = bap.verifySignature(message, address, signature);
167
-
168
- console.log(JSON.stringify({ valid: isValid, message, address, signature }, null, 2));
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 getFriendPubkey(friendBapId: string): void {
172
- const bap = loadIdentity();
173
- if (!bap) {
174
- console.error("No identity found. Run 'bap create' first.");
175
- process.exit(1);
176
- }
177
-
178
- const ids = bap.listIds();
179
- if (ids.length === 0) {
180
- console.error("No identities in BAP instance.");
181
- process.exit(1);
182
- }
183
-
184
- const identity = bap.getId(ids[0]);
185
- if (!identity) {
186
- console.error("Failed to get identity.");
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 encryptForFriend(data: string, friendBapId: string): void {
195
- const bap = loadIdentity();
196
- if (!bap) {
197
- console.error("No identity found. Run 'bap create' first.");
198
- process.exit(1);
199
- }
200
-
201
- const ids = bap.listIds();
202
- if (ids.length === 0) {
203
- console.error("No identities in BAP instance.");
204
- process.exit(1);
205
- }
206
-
207
- const identity = bap.getId(ids[0]);
208
- if (!identity) {
209
- console.error("Failed to get identity.");
210
- process.exit(1);
211
- }
212
-
213
- const ciphertext = identity.encryptWithSeed(data, friendBapId);
214
- console.log(JSON.stringify({ ciphertext, friendBapId }, null, 2));
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 decryptFromFriend(ciphertext: string, friendBapId: string): void {
218
- const bap = loadIdentity();
219
- if (!bap) {
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 exportIdentity(): void {
246
- const bap = loadIdentity();
247
- if (!bap) {
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 importIdentity(file: string): void {
260
- if (!existsSync(file)) {
261
- console.error(`File not found: ${file}`);
262
- process.exit(1);
263
- }
264
-
265
- try {
266
- const backup = JSON.parse(readFileSync(file, "utf-8"));
267
-
268
- if (!backup.rootPk && !backup.xprv) {
269
- console.error("Invalid backup format: missing rootPk or xprv");
270
- process.exit(1);
271
- }
272
-
273
- let bap: BAP;
274
- let wif: string;
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
- function showInfo(): void {
306
- const bap = loadIdentity();
307
- if (!bap) {
308
- console.error("No identity found. Run 'bap create' first.");
309
- process.exit(1);
310
- }
311
-
312
- const ids = bap.listIds();
313
- console.log("BAP Identity Info");
314
- console.log(` Config: ${CONFIG_FILE}`);
315
- console.log(` Identities: ${ids.length}`);
316
-
317
- for (const idKey of ids) {
318
- const identity = bap.getId(idKey);
319
- if (identity) {
320
- console.log(`\n Identity: ${identity.idName}`);
321
- console.log(` Key: ${idKey}`);
322
- console.log(` Root Address: ${identity.rootAddress}`);
323
- console.log(` Current Address: ${identity.getCurrentAddress()}`);
324
- console.log(` Encryption Pubkey: ${identity.getEncryptionPublicKey()}`);
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
- // Main CLI entry point
330
- const args = process.argv.slice(2);
331
- const command = args[0];
332
-
333
- switch (command) {
334
- case "create":
335
- createIdentity(args.slice(1));
336
- break;
337
-
338
- case "sign":
339
- if (!args[1]) {
340
- console.error("Usage: bap sign <message>");
341
- process.exit(1);
342
- }
343
- signMessage(args[1]);
344
- break;
345
-
346
- case "verify":
347
- if (!args[1] || !args[2] || !args[3]) {
348
- console.error("Usage: bap verify <message> <signature> <address>");
349
- process.exit(1);
350
- }
351
- verifySignature(args[1], args[2], args[3]);
352
- break;
353
-
354
- case "friend-pubkey":
355
- if (!args[1]) {
356
- console.error("Usage: bap friend-pubkey <friendBapId>");
357
- process.exit(1);
358
- }
359
- getFriendPubkey(args[1]);
360
- break;
361
-
362
- case "encrypt":
363
- if (!args[1] || !args[2]) {
364
- console.error("Usage: bap encrypt <data> <friendBapId>");
365
- process.exit(1);
366
- }
367
- encryptForFriend(args[1], args[2]);
368
- break;
369
-
370
- case "decrypt":
371
- if (!args[1] || !args[2]) {
372
- console.error("Usage: bap decrypt <ciphertext> <friendBapId>");
373
- process.exit(1);
374
- }
375
- decryptFromFriend(args[1], args[2]);
376
- break;
377
-
378
- case "export":
379
- exportIdentity();
380
- break;
381
-
382
- case "import":
383
- if (!args[1]) {
384
- console.error("Usage: bap import <backup-file>");
385
- process.exit(1);
386
- }
387
- importIdentity(args[1]);
388
- break;
389
-
390
- case "info":
391
- showInfo();
392
- break;
393
-
394
- case "help":
395
- case "--help":
396
- case "-h":
397
- printUsage();
398
- break;
399
-
400
- default:
401
- if (command) {
402
- console.error(`Unknown command: ${command}`);
403
- }
404
- printUsage();
405
- process.exit(command ? 1 : 0);
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();