bsv-bap 0.2.0 → 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
@@ -3,96 +3,184 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { PrivateKey } from "@bsv/sdk";
6
- import { Command } from "commander";
7
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";
8
15
 
9
16
  // Storage paths
10
17
  const CONFIG_DIR = join(homedir(), ".bap");
11
18
  const CONFIG_FILE = join(CONFIG_DIR, "identity.json");
12
19
  const ACTIVE_FILE = join(CONFIG_DIR, "active");
13
20
 
14
- // Stored config shape
21
+ // Stored config shape — rootPk is plaintext, rootPkEncrypted is Touch ID protected.
22
+ // Only one should be present at a time.
15
23
  interface StoredConfig {
16
- rootPk: string;
17
- ids: string;
18
- labels: Record<string, string>;
19
- createdAt: string;
24
+ rootPk?: string;
25
+ rootPkEncrypted?: string;
26
+ ids: string;
27
+ labels: Record<string, string>;
28
+ createdAt: string;
20
29
  }
21
30
 
22
31
  function ensureConfigDir(): void {
23
- if (!existsSync(CONFIG_DIR)) {
24
- mkdirSync(CONFIG_DIR, { recursive: true });
25
- }
32
+ if (!existsSync(CONFIG_DIR)) {
33
+ mkdirSync(CONFIG_DIR, { recursive: true });
34
+ }
26
35
  }
27
36
 
28
37
  function loadConfig(): StoredConfig | null {
29
- if (!existsSync(CONFIG_FILE)) return null;
30
- return JSON.parse(readFileSync(CONFIG_FILE, "utf-8")) as StoredConfig;
38
+ if (!existsSync(CONFIG_FILE)) return null;
39
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8")) as StoredConfig;
31
40
  }
32
41
 
33
42
  function createBAP(key: string): BAP {
34
- // xprv keys start with "xprv" — use BIP32 mode; otherwise Type42
35
- if (key.startsWith("xprv")) {
36
- return new BAP(key);
37
- }
38
- return new BAP({ rootPk: key });
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 });
48
+ }
49
+
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
+ );
39
73
  }
40
74
 
41
- function loadBAP(): { bap: BAP; config: StoredConfig } {
42
- const config = loadConfig();
43
- if (!config) {
44
- console.error("No identity found. Run 'bap create' first.");
45
- process.exit(1);
46
- }
47
- const bap = createBAP(config.rootPk);
48
- if (config.ids) {
49
- bap.importIds(config.ids);
50
- }
51
- return { bap, config };
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 };
52
91
  }
53
92
 
54
93
  function saveConfig(
55
- bap: BAP,
56
- rootPk: string,
57
- labels: Record<string, string>,
58
- createdAt?: string,
94
+ bap: BAP,
95
+ rootPk: string,
96
+ labels: Record<string, string>,
97
+ createdAt?: string,
98
+ encrypted?: { rootPkEncrypted: string }
59
99
  ): void {
60
- ensureConfigDir();
61
- const config: StoredConfig = {
62
- rootPk,
63
- ids: bap.exportIds(),
64
- labels,
65
- createdAt: createdAt ?? new Date().toISOString(),
66
- };
67
- writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
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));
68
114
  }
69
115
 
70
116
  function getActiveBapId(): string | null {
71
- if (!existsSync(ACTIVE_FILE)) return null;
72
- return readFileSync(ACTIVE_FILE, "utf-8").trim();
117
+ if (!existsSync(ACTIVE_FILE)) return null;
118
+ return readFileSync(ACTIVE_FILE, "utf-8").trim();
73
119
  }
74
120
 
75
121
  function setActiveBapId(bapId: string): void {
76
- ensureConfigDir();
77
- writeFileSync(ACTIVE_FILE, bapId);
122
+ ensureConfigDir();
123
+ writeFileSync(ACTIVE_FILE, bapId);
78
124
  }
79
125
 
80
126
  function getActiveIdentity(bap: BAP, config: StoredConfig) {
81
- const activeBapId = getActiveBapId();
82
- const ids = bap.listIds();
83
-
84
- if (ids.length === 0) {
85
- console.error("No identities found. Run 'bap create' first.");
86
- process.exit(1);
87
- }
88
-
89
- const bapId = activeBapId && ids.includes(activeBapId) ? activeBapId : ids[0];
90
- const identity = bap.getId(bapId);
91
- if (!identity) {
92
- console.error(`Identity ${bapId} not found.`);
93
- process.exit(1);
94
- }
95
- return { identity, bapId, label: config.labels[bapId] };
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] };
142
+ }
143
+
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
+ }
96
184
  }
97
185
 
98
186
  // --- CLI ---
@@ -100,312 +188,494 @@ function getActiveIdentity(bap: BAP, config: StoredConfig) {
100
188
  const program = new Command();
101
189
 
102
190
  program
103
- .name("bap")
104
- .description("BAP - Bitcoin Attestation Protocol CLI")
105
- .version("0.2.0");
191
+ .name("bap")
192
+ .description("BAP - Bitcoin Attestation Protocol CLI")
193
+ .version("0.2.0");
106
194
 
107
195
  // Identity Management
108
196
 
109
197
  program
110
- .command("create")
111
- .description("Create a new identity")
112
- .option("--name <name>", "Human-readable label for the identity")
113
- .option("--wif <wif>", "Use an existing WIF key as the master key")
114
- .action((opts) => {
115
- let config = loadConfig();
116
- let rootPk: string;
117
- let labels: Record<string, string>;
118
- let createdAt: string | undefined;
119
- let bap: BAP;
120
-
121
- if (config) {
122
- // Existing master — add a new identity
123
- rootPk = config.rootPk;
124
- labels = config.labels;
125
- createdAt = config.createdAt;
126
- bap = createBAP(rootPk);
127
- bap.importIds(config.ids);
128
- } else {
129
- // New master
130
- rootPk = opts.wif ?? PrivateKey.fromRandom().toWif();
131
- labels = {};
132
- bap = createBAP(rootPk);
133
- }
134
-
135
- const identity = bap.newId();
136
- const bapId = identity.bapId;
137
-
138
- if (opts.name) {
139
- labels[bapId] = opts.name;
140
- }
141
-
142
- saveConfig(bap, rootPk, labels, createdAt);
143
- setActiveBapId(bapId);
144
-
145
- console.log("Identity created:");
146
- console.log(` BAP ID: ${bapId}`);
147
- if (opts.name) console.log(` Label: ${opts.name}`);
148
- console.log(` Root Address: ${identity.rootAddress}`);
149
- console.log(` Root Path: ${identity.rootPath}`);
150
- console.log(` Stored at: ${CONFIG_FILE}`);
151
- });
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
+ });
152
259
 
153
260
  program
154
- .command("list")
155
- .description("List all identities (* = active)")
156
- .action(() => {
157
- const { bap, config } = loadBAP();
158
- const ids = bap.listIds();
159
- const active = getActiveBapId();
160
-
161
- if (ids.length === 0) {
162
- console.log("No identities. Run 'bap create' to get started.");
163
- return;
164
- }
165
-
166
- for (const bapId of ids) {
167
- const marker = bapId === active ? " *" : " ";
168
- const label = config.labels[bapId];
169
- const suffix = label ? ` (${label})` : "";
170
- console.log(`${marker} ${bapId}${suffix}`);
171
- }
172
- });
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
+ });
173
280
 
174
281
  program
175
- .command("use")
176
- .description("Set active identity")
177
- .argument("<bapId>", "BAP ID to activate")
178
- .action((bapId: string) => {
179
- const { bap } = loadBAP();
180
- const ids = bap.listIds();
181
-
182
- if (!ids.includes(bapId)) {
183
- console.error(`Identity ${bapId} not found.`);
184
- process.exit(1);
185
- }
186
-
187
- setActiveBapId(bapId);
188
- console.log(`Active identity: ${bapId}`);
189
- });
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
+ });
190
297
 
191
298
  program
192
- .command("info")
193
- .description("Show active identity details")
194
- .action(() => {
195
- const { bap, config } = loadBAP();
196
- const { identity, bapId, label } = getActiveIdentity(bap, config);
197
-
198
- console.log("Active Identity:");
199
- console.log(` BAP ID: ${bapId}`);
200
- if (label) console.log(` Label: ${label}`);
201
- console.log(` Root Address: ${identity.rootAddress}`);
202
- console.log(` Root Path: ${identity.rootPath}`);
203
- console.log(` Current Path: ${identity.currentPath}`);
204
- console.log(` Previous Path: ${identity.previousPath}`);
205
- console.log(` Account Key: ${identity.getAccountKey().toPublicKey().toString()}`);
206
- });
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
+ });
207
316
 
208
317
  program
209
- .command("remove")
210
- .description("Remove an identity")
211
- .argument("<bapId>", "BAP ID to remove")
212
- .action((bapId: string) => {
213
- const { bap, config } = loadBAP();
214
-
215
- if (!bap.getId(bapId)) {
216
- console.error(`Identity ${bapId} not found.`);
217
- process.exit(1);
218
- }
219
-
220
- bap.removeId(bapId);
221
- delete config.labels[bapId];
222
- saveConfig(bap, config.rootPk, config.labels, config.createdAt);
223
-
224
- // Clear active if it was this one
225
- if (getActiveBapId() === bapId) {
226
- const remaining = bap.listIds();
227
- if (remaining.length > 0) {
228
- setActiveBapId(remaining[0]);
229
- } else {
230
- writeFileSync(ACTIVE_FILE, "");
231
- }
232
- }
233
-
234
- console.log(`Removed identity: ${bapId}`);
235
- });
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
+ });
236
350
 
237
351
  // Backup
238
352
 
239
353
  program
240
- .command("export")
241
- .description("Export master backup (JSON to stdout)")
242
- .action(() => {
243
- const { bap } = loadBAP();
244
- const backup = bap.exportForBackup();
245
- console.log(JSON.stringify(backup, null, 2));
246
- });
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
+ });
247
370
 
248
371
  program
249
- .command("export-account")
250
- .description("Export account backup for active or specified identity")
251
- .option("--id <bapId>", "Specific BAP ID to export")
252
- .action((opts) => {
253
- const { bap, config } = loadBAP();
254
-
255
- let identity;
256
- if (opts.id) {
257
- identity = bap.getId(opts.id);
258
- if (!identity) {
259
- console.error(`Identity ${opts.id} not found.`);
260
- process.exit(1);
261
- }
262
- } else {
263
- ({ identity } = getActiveIdentity(bap, config));
264
- }
265
-
266
- const backup = identity.exportAccountBackup();
267
- console.log(JSON.stringify(backup, null, 2));
268
- });
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
+ });
269
392
 
270
393
  program
271
- .command("import")
272
- .description("Import from backup file")
273
- .argument("<file>", "Path to backup JSON file")
274
- .action((file: string) => {
275
- if (!existsSync(file)) {
276
- console.error(`File not found: ${file}`);
277
- process.exit(1);
278
- }
279
-
280
- const backup = JSON.parse(readFileSync(file, "utf-8"));
281
-
282
- if (!backup.rootPk && !backup.xprv) {
283
- console.error("Invalid backup format: missing rootPk or xprv");
284
- process.exit(1);
285
- }
286
-
287
- let bap: BAP;
288
- let rootPk: string;
289
-
290
- if (backup.rootPk) {
291
- bap = new BAP({ rootPk: backup.rootPk });
292
- rootPk = backup.rootPk;
293
- } else {
294
- bap = new BAP(backup.xprv);
295
- rootPk = backup.xprv;
296
- }
297
-
298
- if (backup.ids) {
299
- bap.importIds(backup.ids);
300
- }
301
-
302
- const labels: Record<string, string> = {};
303
- if (backup.label) {
304
- // Apply label to first identity as a default
305
- const ids = bap.listIds();
306
- if (ids.length > 0) {
307
- labels[ids[0]] = backup.label;
308
- }
309
- }
310
-
311
- saveConfig(bap, rootPk, labels);
312
-
313
- const ids = bap.listIds();
314
- if (ids.length > 0) {
315
- setActiveBapId(ids[0]);
316
- }
317
-
318
- console.log("Backup imported:");
319
- console.log(` Identities: ${ids.length}`);
320
- if (backup.label) console.log(` Label: ${backup.label}`);
321
- console.log(` Stored at: ${CONFIG_FILE}`);
322
- });
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
+ });
323
456
 
324
457
  // Crypto
325
458
 
326
459
  program
327
- .command("encrypt")
328
- .description("Encrypt data with master key (ECIES)")
329
- .argument("<data>", "Data to encrypt")
330
- .action((data: string) => {
331
- const { bap } = loadBAP();
332
- console.log(bap.encrypt(data));
333
- });
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
+ });
334
467
 
335
468
  program
336
- .command("decrypt")
337
- .description("Decrypt ciphertext with master key")
338
- .argument("<ciphertext>", "Base64 ciphertext to decrypt")
339
- .action((ciphertext: string) => {
340
- const { bap } = loadBAP();
341
- console.log(bap.decrypt(ciphertext));
342
- });
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
+ });
343
476
 
344
477
  program
345
- .command("verify")
346
- .description("Verify a BSM signature")
347
- .argument("<message>", "Original message")
348
- .argument("<signature>", "Base64 signature")
349
- .argument("<address>", "Signing address")
350
- .action((message: string, signature: string, address: string) => {
351
- const bap = new BAP({ rootPk: PrivateKey.fromRandom().toWif() });
352
- let valid = false;
353
- try {
354
- valid = bap.verifySignature(message, address, signature);
355
- } catch {
356
- // Invalid signature format treat as not valid
357
- }
358
- console.log(JSON.stringify({ valid, message, address, signature }, null, 2));
359
- });
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
+ });
360
495
 
361
496
  // API Lookups
362
497
 
363
498
  program
364
- .command("lookup")
365
- .description("Lookup identity on the BAP overlay")
366
- .argument("<bapId>", "BAP ID to lookup")
367
- .action(async (bapId: string) => {
368
- const bap = new BAP({ rootPk: PrivateKey.fromRandom().toWif() });
369
- const result = await bap.getIdentity(bapId);
370
- console.log(JSON.stringify(result, null, 2));
371
- });
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
+ });
372
507
 
373
508
  program
374
- .command("lookup-address")
375
- .description("Lookup identity by Bitcoin address")
376
- .argument("<address>", "Bitcoin address to lookup")
377
- .action(async (address: string) => {
378
- const bap = new BAP({ rootPk: PrivateKey.fromRandom().toWif() });
379
- const result = await bap.getIdentityFromAddress(address);
380
- console.log(JSON.stringify(result, null, 2));
381
- });
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
+ });
382
517
 
383
518
  program
384
- .command("attestations")
385
- .description("Get attestations for an attribute hash")
386
- .argument("<hash>", "Attribute hash to lookup")
387
- .action(async (hash: string) => {
388
- const bap = new BAP({ rootPk: PrivateKey.fromRandom().toWif() });
389
- const result = await bap.getAttestationsForHash(hash);
390
- console.log(JSON.stringify(result, null, 2));
391
- });
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
+ });
392
527
 
393
528
  // Utilities
394
529
 
395
530
  program
396
- .command("id-from-address")
397
- .description("Derive BAP ID from a Bitcoin address")
398
- .argument("<address>", "Bitcoin address (must be the root/member address)")
399
- .action((address: string) => {
400
- console.log(bapIdFromAddress(address));
401
- });
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
+ });
402
537
 
403
538
  program
404
- .command("id-from-pubkey")
405
- .description("Derive BAP ID from a compressed public key")
406
- .argument("<pubkey>", "Compressed public key hex (must be the member key)")
407
- .action((pubkey: string) => {
408
- console.log(bapIdFromPubkey(pubkey));
409
- });
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
+ });
410
680
 
411
681
  program.parse();