@bsv/wallet-toolbox 1.2.36 → 1.2.37

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.
@@ -1,12 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CWIStyleWalletManager = exports.OverlayUMPTokenInteractor = exports.PBKDF2_NUM_ROUNDS = void 0;
3
+ exports.CWIStyleWalletManager = exports.OverlayUMPTokenInteractor = exports.DEFAULT_PROFILE_ID = exports.PBKDF2_NUM_ROUNDS = void 0;
4
4
  const sdk_1 = require("@bsv/sdk");
5
5
  const PrivilegedKeyManager_1 = require("./sdk/PrivilegedKeyManager");
6
6
  /**
7
7
  * Number of rounds used in PBKDF2 for deriving password keys.
8
8
  */
9
9
  exports.PBKDF2_NUM_ROUNDS = 7777;
10
+ /**
11
+ * Unique Identifier for the default profile (16 zero bytes).
12
+ */
13
+ exports.DEFAULT_PROFILE_ID = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
10
14
  /**
11
15
  * @class OverlayUMPTokenInteractor
12
16
  *
@@ -67,28 +71,16 @@ class OverlayUMPTokenInteractor {
67
71
  * then broadcast and published under the `tm_users` topic using a SHIP broadcast, ensuring
68
72
  * overlay participants see the updated token.
69
73
  *
70
- * @param wallet The wallet used to build and sign the transaction.
74
+ * @param wallet The wallet used to build and sign the transaction (MUST be operating under the DEFAULT profile).
71
75
  * @param adminOriginator The domain/FQDN of the administrative originator (wallet operator).
72
76
  * @param token The new UMPToken to create on-chain.
73
77
  * @param oldTokenToConsume Optionally, an existing token to consume/spend in the same transaction.
74
78
  * @returns The outpoint of the newly created UMP token (e.g. "abcd1234...ef.0").
75
79
  */
76
- async buildAndSend(wallet, adminOriginator, token, oldTokenToConsume) {
77
- // 1) Construct the data fields for the new UMP token in the same
78
- // 11-field order used by the UMP protocol's PushDrop definition.
79
- const fields = new Array(11);
80
- // See: UMP field ordering
81
- // 0 => passwordSalt
82
- // 1 => passwordPresentationPrimary
83
- // 2 => passwordRecoveryPrimary
84
- // 3 => presentationRecoveryPrimary
85
- // 4 => passwordPrimaryPrivileged
86
- // 5 => presentationRecoveryPrivileged
87
- // 6 => presentationHash
88
- // 7 => recoveryHash
89
- // 8 => presentationKeyEncrypted
90
- // 9 => passwordKeyEncrypted
91
- // 10 => recoveryKeyEncrypted
80
+ async buildAndSend(wallet, // This wallet MUST be the one built for the default profile
81
+ adminOriginator, token, oldTokenToConsume) {
82
+ // 1) Construct the data fields for the new UMP token.
83
+ const fields = [];
92
84
  fields[0] = token.passwordSalt;
93
85
  fields[1] = token.passwordPresentationPrimary;
94
86
  fields[2] = token.passwordRecoveryPrimary;
@@ -100,13 +92,17 @@ class OverlayUMPTokenInteractor {
100
92
  fields[8] = token.presentationKeyEncrypted;
101
93
  fields[9] = token.passwordKeyEncrypted;
102
94
  fields[10] = token.recoveryKeyEncrypted;
103
- // 2) Create a PushDrop script referencing these fields, locked with the admin key (for easy revocation).
95
+ // Optional field (11) for encrypted profiles
96
+ if (token.profilesEncrypted) {
97
+ fields[11] = token.profilesEncrypted;
98
+ }
99
+ // 2) Create a PushDrop script referencing these fields, locked with the admin key.
104
100
  const script = await new sdk_1.PushDrop(wallet, adminOriginator).lock(fields, [2, 'admin user management token'], // protocolID
105
101
  '1', // keyID
106
102
  'self', // counterparty
107
103
  /*forSelf=*/ true,
108
104
  /*includeSignature=*/ true);
109
- // 3) Prepare the createAction call. If oldTokenToConsume is provided, we gather the outpoint.
105
+ // 3) Prepare the createAction call. If oldTokenToConsume is provided, gather the outpoint.
110
106
  const inputs = [];
111
107
  let inputToken;
112
108
  if (oldTokenToConsume === null || oldTokenToConsume === void 0 ? void 0 : oldTokenToConsume.currentOutpoint) {
@@ -131,8 +127,7 @@ class OverlayUMPTokenInteractor {
131
127
  outputs,
132
128
  inputBEEF: inputToken === null || inputToken === void 0 ? void 0 : inputToken.beef
133
129
  }, adminOriginator);
134
- // If the transaction is fully processed by the wallet (some wallets might do signAndProcess automatically),
135
- // we retrieve the final TXID from the result.
130
+ // If the transaction is fully processed by the wallet
136
131
  if (!createResult.signableTransaction) {
137
132
  const finalTxid = createResult.txid || (createResult.tx ? sdk_1.Transaction.fromAtomicBEEF(createResult.tx).id('hex') : undefined);
138
133
  if (!finalTxid) {
@@ -168,19 +163,25 @@ class OverlayUMPTokenInteractor {
168
163
  }
169
164
  // 6) Broadcast to `tm_users`
170
165
  const finalAtomicTx = signResult.tx;
166
+ if (!finalAtomicTx) {
167
+ throw new Error('Final transaction data missing after signing renewed UMP token.');
168
+ }
171
169
  const broadcastTx = sdk_1.Transaction.fromAtomicBEEF(finalAtomicTx);
172
170
  const result = await this.broadcaster.broadcast(broadcastTx);
173
171
  console.log('BROADCAST RESULT', result);
174
172
  return `${finalTxid}.0`;
175
173
  }
176
174
  else {
177
- // Fallbaack
175
+ // Fallback for creating a new token (no input spending)
178
176
  const signResult = await wallet.signAction({ reference, spends: {} }, adminOriginator);
179
177
  finalTxid = signResult.txid || (signResult.tx ? sdk_1.Transaction.fromAtomicBEEF(signResult.tx).id('hex') : '');
180
178
  if (!finalTxid) {
181
179
  throw new Error('Failed to finalize new UMP token transaction.');
182
180
  }
183
181
  const finalAtomicTx = signResult.tx;
182
+ if (!finalAtomicTx) {
183
+ throw new Error('Final transaction data missing after signing new UMP token.');
184
+ }
184
185
  const broadcastTx = sdk_1.Transaction.fromAtomicBEEF(finalAtomicTx);
185
186
  const result = await this.broadcaster.broadcast(broadcastTx);
186
187
  console.log('BROADCAST RESULT', result);
@@ -196,24 +197,26 @@ class OverlayUMPTokenInteractor {
196
197
  * @returns The parsed UMPToken or `undefined` if none found/decodable.
197
198
  */
198
199
  parseLookupAnswer(answer) {
200
+ var _a;
199
201
  if (answer.type !== 'output-list') {
200
202
  return undefined;
201
203
  }
202
204
  if (!answer.outputs || answer.outputs.length === 0) {
203
205
  return undefined;
204
206
  }
205
- // We expect only one relevant UMP token in most queries, so let's parse the first.
206
- // If multiple are returned, we can parse the first.
207
207
  const { beef, outputIndex } = answer.outputs[0];
208
208
  try {
209
209
  const tx = sdk_1.Transaction.fromBEEF(beef);
210
210
  const outpoint = `${tx.id('hex')}.${outputIndex}`;
211
211
  const decoded = sdk_1.PushDrop.decode(tx.outputs[outputIndex].lockingScript);
212
- // Expecting 11 fields for UMP
213
- if (!decoded.fields || decoded.fields.length < 11)
212
+ // Expecting 11 or more fields for UMP
213
+ if (!decoded.fields || decoded.fields.length < 11) {
214
+ console.warn(`Unexpected number of fields in UMP token: ${(_a = decoded.fields) === null || _a === void 0 ? void 0 : _a.length}`);
214
215
  return undefined;
216
+ }
215
217
  // Build the UMP token from these fields, preserving outpoint
216
218
  const t = {
219
+ // Order matches buildAndSend and serialize/deserialize
217
220
  passwordSalt: decoded.fields[0],
218
221
  passwordPresentationPrimary: decoded.fields[1],
219
222
  passwordRecoveryPrimary: decoded.fields[2],
@@ -225,12 +228,13 @@ class OverlayUMPTokenInteractor {
225
228
  presentationKeyEncrypted: decoded.fields[8],
226
229
  passwordKeyEncrypted: decoded.fields[9],
227
230
  recoveryKeyEncrypted: decoded.fields[10],
231
+ profilesEncrypted: decoded.fields[12] ? decoded.fields[11] : undefined, // If there's a signature in field 12, use field 11
228
232
  currentOutpoint: outpoint
229
233
  };
230
234
  return t;
231
235
  }
232
236
  catch (e) {
233
- // If we fail to parse or decode, return undefined
237
+ console.error('Failed to parse or decode UMP token:', e);
234
238
  return undefined;
235
239
  }
236
240
  }
@@ -249,7 +253,7 @@ class OverlayUMPTokenInteractor {
249
253
  if (results.type !== 'output-list') {
250
254
  return undefined;
251
255
  }
252
- if (!results.outputs.length) {
256
+ if (!results.outputs || !results.outputs.length) {
253
257
  return undefined;
254
258
  }
255
259
  return results.outputs[0];
@@ -258,35 +262,38 @@ class OverlayUMPTokenInteractor {
258
262
  exports.OverlayUMPTokenInteractor = OverlayUMPTokenInteractor;
259
263
  /**
260
264
  * Manages a "CWI-style" wallet that uses a UMP token and a
261
- * multi-key authentication scheme (password, presentation key, and recovery key).
265
+ * multi-key authentication scheme (password, presentation key, and recovery key),
266
+ * supporting multiple user profiles under a single account.
262
267
  */
263
268
  class CWIStyleWalletManager {
264
269
  /**
265
270
  * Constructs a new CWIStyleWalletManager.
266
271
  *
267
272
  * @param adminOriginator The domain name of the administrative originator.
268
- * @param walletBuilder A function that can build an underlying wallet instance
269
- * from a primary key and a privileged key manager
270
- * @param interactor An instance of UMPTokenInteractor capable of managing UMP tokens.
271
- * @param recoveryKeySaver A function that can persist or display a newly generated recovery key.
272
- * @param passwordRetriever A function to request the user's password, given a reason and a test function.
273
- * @param newWalletFunder An optional function called with the presentation key and a new Wallet post-construction to fund it before use.
274
- * @param stateSnapshot If provided, a previously saved snapshot of the wallet's state.
273
+ * @param walletBuilder A function that can build an underlying wallet instance for a profile.
274
+ * @param interactor An instance of UMPTokenInteractor.
275
+ * @param recoveryKeySaver A function to persist a new recovery key.
276
+ * @param passwordRetriever A function to request the user's password.
277
+ * @param newWalletFunder Optional function to fund a new wallet.
278
+ * @param stateSnapshot Optional previously saved state snapshot.
275
279
  */
276
280
  constructor(adminOriginator, walletBuilder, interactor = new OverlayUMPTokenInteractor(), recoveryKeySaver, passwordRetriever, newWalletFunder, stateSnapshot) {
277
281
  /**
278
- * The current mode of authentication:
279
- * - 'presentation-key-and-password'
280
- * - 'presentation-key-and-recovery-key'
281
- * - 'recovery-key-and-password'
282
+ * Current mode of authentication.
282
283
  */
283
284
  this.authenticationMode = 'presentation-key-and-password';
284
285
  /**
285
- * Indicates whether this is a new user or an existing user flow:
286
- * - 'new-user'
287
- * - 'existing-user'
286
+ * Indicates new user or existing user flow.
288
287
  */
289
288
  this.authenticationFlow = 'new-user';
289
+ /**
290
+ * The currently active profile ID (null or DEFAULT_PROFILE_ID means default profile).
291
+ */
292
+ this.activeProfileId = exports.DEFAULT_PROFILE_ID;
293
+ /**
294
+ * List of loaded non-default profiles.
295
+ */
296
+ this.profiles = [];
290
297
  this.adminOriginator = adminOriginator;
291
298
  this.walletBuilder = walletBuilder;
292
299
  this.UMPTokenInteractor = interactor;
@@ -295,17 +302,20 @@ class CWIStyleWalletManager {
295
302
  this.authenticated = false;
296
303
  this.newWalletFunder = newWalletFunder;
297
304
  // If a saved snapshot is provided, attempt to load it.
305
+ // Note: loadSnapshot now returns a promise. We don't await it here,
306
+ // as the constructor must be synchronous. The caller should check
307
+ // `this.authenticated` after construction if a snapshot was provided.
298
308
  if (stateSnapshot) {
299
- this.loadSnapshot(stateSnapshot);
309
+ this.loadSnapshot(stateSnapshot).catch(err => {
310
+ console.error('Failed to load snapshot during construction:', err);
311
+ // Clear potentially partially loaded state
312
+ this.destroy();
313
+ });
300
314
  }
301
315
  }
316
+ // --- Authentication Methods ---
302
317
  /**
303
- * Provides the presentation key in an authentication mode that requires it.
304
- * If a UMP token is found based on the key's hash, this is an existing-user flow.
305
- * Otherwise, it is treated as a new-user flow.
306
- *
307
- * @param key The user's presentation key (32 bytes).
308
- * @throws {Error} if user is already authenticated, or if the current mode does not require a presentation key.
318
+ * Provides the presentation key.
309
319
  */
310
320
  async providePresentationKey(key) {
311
321
  if (this.authenticated) {
@@ -329,17 +339,7 @@ class CWIStyleWalletManager {
329
339
  }
330
340
  }
331
341
  /**
332
- * Provides the password in an authentication mode that requires it.
333
- *
334
- * - **Existing user**:
335
- * Decrypts the primary key using the provided password (and either the presentation key or recovery key, depending on the mode).
336
- * Then builds the underlying wallet, marking the user as authenticated.
337
- *
338
- * - **New user**:
339
- * Generates a new UMP token with fresh keys (primary, privileged, recovery). Publishes it on-chain and builds the wallet.
340
- *
341
- * @param password The user's password as a string.
342
- * @throws {Error} If the user is already authenticated, if the mode does not use a password, or if required keys are missing.
342
+ * Provides the password.
343
343
  */
344
344
  async providePassword(password) {
345
345
  if (this.authenticated) {
@@ -348,113 +348,112 @@ class CWIStyleWalletManager {
348
348
  if (this.authenticationMode === 'presentation-key-and-recovery-key') {
349
349
  throw new Error('Password is not needed in this mode');
350
350
  }
351
- // If we detect an existing user flow:
352
351
  if (this.authenticationFlow === 'existing-user') {
352
+ // Existing user flow
353
353
  if (!this.currentUMPToken) {
354
- throw new Error('Provide either a presentation key or a recovery key first, depending on the authentication mode.');
354
+ throw new Error('Provide presentation or recovery key first.');
355
355
  }
356
356
  const derivedPasswordKey = sdk_1.Hash.pbkdf2(sdk_1.Utils.toArray(password, 'utf8'), this.currentUMPToken.passwordSalt, exports.PBKDF2_NUM_ROUNDS, 32, 'sha512');
357
+ let rootPrimaryKey;
358
+ let rootPrivilegedKey; // Only needed for recovery mode
357
359
  if (this.authenticationMode === 'presentation-key-and-password') {
358
- if (!this.presentationKey) {
360
+ if (!this.presentationKey)
359
361
  throw new Error('No presentation key found!');
360
- }
361
- // Decrypt the primary key with XOR(presentationKey, derivedPasswordKey).
362
362
  const xorKey = this.XOR(this.presentationKey, derivedPasswordKey);
363
- const decryptedPrimary = new sdk_1.SymmetricKey(xorKey).decrypt(this.currentUMPToken.passwordPresentationPrimary);
364
- await this.buildUnderlying(decryptedPrimary);
363
+ rootPrimaryKey = new sdk_1.SymmetricKey(xorKey).decrypt(this.currentUMPToken.passwordPresentationPrimary);
365
364
  }
366
365
  else {
367
- // 'recovery-key-and-password' mode
368
- if (!this.recoveryKey) {
366
+ // 'recovery-key-and-password'
367
+ if (!this.recoveryKey)
369
368
  throw new Error('No recovery key found!');
370
- }
371
- // Decrypt the primary key with XOR(recoveryKey, derivedPasswordKey).
372
369
  const primaryDecryptionKey = this.XOR(this.recoveryKey, derivedPasswordKey);
373
- const decryptedPrimary = new sdk_1.SymmetricKey(primaryDecryptionKey).decrypt(this.currentUMPToken.passwordRecoveryPrimary);
374
- // Decrypt the privileged key for immediate use.
375
- const privilegedDecryptionKey = this.XOR(decryptedPrimary, derivedPasswordKey);
376
- const decryptedPrivileged = new sdk_1.SymmetricKey(privilegedDecryptionKey).decrypt(this.currentUMPToken.passwordPrimaryPrivileged);
377
- await this.buildUnderlying(decryptedPrimary, decryptedPrivileged);
370
+ rootPrimaryKey = new sdk_1.SymmetricKey(primaryDecryptionKey).decrypt(this.currentUMPToken.passwordRecoveryPrimary);
371
+ const privilegedDecryptionKey = this.XOR(rootPrimaryKey, derivedPasswordKey);
372
+ rootPrivilegedKey = new sdk_1.SymmetricKey(privilegedDecryptionKey).decrypt(this.currentUMPToken.passwordPrimaryPrivileged);
378
373
  }
379
- return;
380
- }
381
- // Otherwise, handle new user flow (only valid in 'presentation-key-and-password').
382
- if (this.authenticationMode !== 'presentation-key-and-password') {
383
- throw new Error('New-user flow requires presentation key and password, not recovery key mode.');
384
- }
385
- if (!this.presentationKey) {
386
- throw new Error('No presentation key provided for new-user flow.');
374
+ // Build root infrastructure, load profiles, and switch to default profile initially
375
+ await this.setupRootInfrastructure(rootPrimaryKey, rootPrivilegedKey);
376
+ await this.switchProfile(this.activeProfileId);
387
377
  }
388
- // Generate new random keys/salt and create a new UMP token.
389
- const recoveryKey = (0, sdk_1.Random)(32);
390
- await this.recoveryKeySaver(recoveryKey);
391
- const passwordSalt = (0, sdk_1.Random)(32);
392
- const passwordKey = sdk_1.Hash.pbkdf2(sdk_1.Utils.toArray(password, 'utf8'), passwordSalt, exports.PBKDF2_NUM_ROUNDS, 32, 'sha512');
393
- const primaryKey = (0, sdk_1.Random)(32);
394
- const privilegedKey = (0, sdk_1.Random)(32);
395
- // Build XOR-based symmetrical keys:
396
- const presentationPassword = new sdk_1.SymmetricKey(this.XOR(this.presentationKey, passwordKey));
397
- const presentationRecovery = new sdk_1.SymmetricKey(this.XOR(this.presentationKey, recoveryKey));
398
- const recoveryPassword = new sdk_1.SymmetricKey(this.XOR(recoveryKey, passwordKey));
399
- const primaryPassword = new sdk_1.SymmetricKey(this.XOR(primaryKey, passwordKey));
400
- // Temporarily create a privileged key manager for encrypting the keys in the token.
401
- const tempPrivilegedKeyManager = new PrivilegedKeyManager_1.PrivilegedKeyManager(async () => new sdk_1.PrivateKey(privilegedKey));
402
- // Build the new UMP token:
403
- const newToken = {
404
- passwordSalt,
405
- passwordPresentationPrimary: presentationPassword.encrypt(primaryKey),
406
- passwordRecoveryPrimary: recoveryPassword.encrypt(primaryKey),
407
- presentationRecoveryPrimary: presentationRecovery.encrypt(primaryKey),
408
- passwordPrimaryPrivileged: primaryPassword.encrypt(privilegedKey),
409
- presentationRecoveryPrivileged: presentationRecovery.encrypt(privilegedKey),
410
- presentationHash: sdk_1.Hash.sha256(this.presentationKey),
411
- recoveryHash: sdk_1.Hash.sha256(recoveryKey),
412
- presentationKeyEncrypted: (await tempPrivilegedKeyManager.encrypt({
413
- plaintext: this.presentationKey,
414
- protocolID: [2, 'admin key wrapping'],
415
- keyID: '1'
416
- })).ciphertext,
417
- passwordKeyEncrypted: (await tempPrivilegedKeyManager.encrypt({
418
- plaintext: passwordKey,
419
- protocolID: [2, 'admin key wrapping'],
420
- keyID: '1'
421
- })).ciphertext,
422
- recoveryKeyEncrypted: (await tempPrivilegedKeyManager.encrypt({
423
- plaintext: recoveryKey,
424
- protocolID: [2, 'admin key wrapping'],
425
- keyID: '1'
426
- })).ciphertext
427
- };
428
- // Now, we can create our new wallet!
429
- this.currentUMPToken = newToken;
430
- await this.buildUnderlying(primaryKey);
431
- // Before we do anything, the new wallet is most likely empty right now.
432
- // We want to provide a chance for someone to fund it, if they want.
433
- if (this.newWalletFunder) {
434
- try {
435
- await this.newWalletFunder(this.presentationKey, this.underlying, this.adminOriginator);
378
+ else {
379
+ // New user flow (only 'presentation-key-and-password')
380
+ if (this.authenticationMode !== 'presentation-key-and-password') {
381
+ throw new Error('New-user flow requires presentation key and password mode.');
382
+ }
383
+ if (!this.presentationKey) {
384
+ throw new Error('No presentation key provided for new-user flow.');
436
385
  }
437
- catch (e) {
438
- // swallow error
439
- // TODO: Implement better error handling
440
- console.error(e);
386
+ // Generate new keys/salt
387
+ const recoveryKey = (0, sdk_1.Random)(32);
388
+ await this.recoveryKeySaver(recoveryKey);
389
+ const passwordSalt = (0, sdk_1.Random)(32);
390
+ const passwordKey = sdk_1.Hash.pbkdf2(sdk_1.Utils.toArray(password, 'utf8'), passwordSalt, exports.PBKDF2_NUM_ROUNDS, 32, 'sha512');
391
+ const rootPrimaryKey = (0, sdk_1.Random)(32);
392
+ const rootPrivilegedKey = (0, sdk_1.Random)(32);
393
+ // Build XOR keys
394
+ const presentationPassword = new sdk_1.SymmetricKey(this.XOR(this.presentationKey, passwordKey));
395
+ const presentationRecovery = new sdk_1.SymmetricKey(this.XOR(this.presentationKey, recoveryKey));
396
+ const recoveryPassword = new sdk_1.SymmetricKey(this.XOR(recoveryKey, passwordKey));
397
+ const primaryPassword = new sdk_1.SymmetricKey(this.XOR(rootPrimaryKey, passwordKey));
398
+ // Temp manager for encryption
399
+ const tempPrivilegedKeyManager = new PrivilegedKeyManager_1.PrivilegedKeyManager(async () => new sdk_1.PrivateKey(rootPrivilegedKey));
400
+ // Build new UMP token (no profiles initially)
401
+ const newToken = {
402
+ passwordSalt,
403
+ passwordPresentationPrimary: presentationPassword.encrypt(rootPrimaryKey),
404
+ passwordRecoveryPrimary: recoveryPassword.encrypt(rootPrimaryKey),
405
+ presentationRecoveryPrimary: presentationRecovery.encrypt(rootPrimaryKey),
406
+ passwordPrimaryPrivileged: primaryPassword.encrypt(rootPrivilegedKey),
407
+ presentationRecoveryPrivileged: presentationRecovery.encrypt(rootPrivilegedKey),
408
+ presentationHash: sdk_1.Hash.sha256(this.presentationKey),
409
+ recoveryHash: sdk_1.Hash.sha256(recoveryKey),
410
+ presentationKeyEncrypted: (await tempPrivilegedKeyManager.encrypt({
411
+ plaintext: this.presentationKey,
412
+ protocolID: [2, 'admin key wrapping'],
413
+ keyID: '1'
414
+ })).ciphertext,
415
+ passwordKeyEncrypted: (await tempPrivilegedKeyManager.encrypt({
416
+ plaintext: passwordKey,
417
+ protocolID: [2, 'admin key wrapping'],
418
+ keyID: '1'
419
+ })).ciphertext,
420
+ recoveryKeyEncrypted: (await tempPrivilegedKeyManager.encrypt({
421
+ plaintext: recoveryKey,
422
+ protocolID: [2, 'admin key wrapping'],
423
+ keyID: '1'
424
+ })).ciphertext,
425
+ profilesEncrypted: undefined // No profiles yet
426
+ };
427
+ this.currentUMPToken = newToken;
428
+ // Setup root infrastructure and switch to default profile
429
+ await this.setupRootInfrastructure(rootPrimaryKey);
430
+ await this.switchProfile(exports.DEFAULT_PROFILE_ID);
431
+ // Fund the *default* wallet if funder provided
432
+ if (this.newWalletFunder && this.underlying) {
433
+ try {
434
+ await this.newWalletFunder(this.presentationKey, this.underlying, this.adminOriginator);
435
+ }
436
+ catch (e) {
437
+ console.error('Error funding new wallet:', e);
438
+ // Decide if this should halt the process or just log
439
+ }
441
440
  }
441
+ // Publish the new UMP token *after* potentially funding
442
+ // We need the default profile wallet to sign the UMP creation TX
443
+ if (!this.underlying) {
444
+ throw new Error('Default profile wallet not built before attempting to publish UMP token.');
445
+ }
446
+ this.currentUMPToken.currentOutpoint = await this.UMPTokenInteractor.buildAndSend(this.underlying, // Use the default profile wallet
447
+ this.adminOriginator, newToken);
442
448
  }
443
- // Publish the new UMP token on-chain and store the resulting outpoint.
444
- this.currentUMPToken.currentOutpoint = await this.UMPTokenInteractor.buildAndSend(this.underlying, this.adminOriginator, newToken);
445
449
  }
446
450
  /**
447
- * Provides the recovery key in an authentication flow that requires it.
448
- *
449
- * @param recoveryKey The user's recovery key (32 bytes).
450
- * @throws {Error} if user is already authenticated, if the mode does not use a recovery key,
451
- * or if a required presentation key is missing in "presentation-key-and-recovery-key" mode.
451
+ * Provides the recovery key.
452
452
  */
453
453
  async provideRecoveryKey(recoveryKey) {
454
454
  if (this.authenticated) {
455
455
  throw new Error('Already authenticated');
456
456
  }
457
- // Cannot use recovery key in a new-user flow
458
457
  if (this.authenticationFlow === 'new-user') {
459
458
  throw new Error('Do not submit recovery key in new-user flow');
460
459
  }
@@ -462,623 +461,743 @@ class CWIStyleWalletManager {
462
461
  throw new Error('No recovery key required in this mode');
463
462
  }
464
463
  else if (this.authenticationMode === 'recovery-key-and-password') {
465
- // We will need to wait until the user provides the password as well.
464
+ // Wait for password
466
465
  const hash = sdk_1.Hash.sha256(recoveryKey);
467
466
  const token = await this.UMPTokenInteractor.findByRecoveryKeyHash(hash);
468
- if (!token) {
469
- throw new Error('No user found with this key');
470
- }
467
+ if (!token)
468
+ throw new Error('No user found with this recovery key');
471
469
  this.recoveryKey = recoveryKey;
472
470
  this.currentUMPToken = token;
473
471
  }
474
472
  else {
475
473
  // 'presentation-key-and-recovery-key'
476
- if (!this.presentationKey) {
474
+ if (!this.presentationKey)
477
475
  throw new Error('Provide the presentation key first');
478
- }
479
- if (!this.currentUMPToken) {
476
+ if (!this.currentUMPToken)
480
477
  throw new Error('Current UMP token not found');
481
- }
482
- // Decrypt the primary key:
483
478
  const xorKey = this.XOR(this.presentationKey, recoveryKey);
484
- const primaryKey = new sdk_1.SymmetricKey(xorKey).decrypt(this.currentUMPToken.presentationRecoveryPrimary);
485
- // Decrypt the privileged key (for account recovery).
486
- const privilegedKey = new sdk_1.SymmetricKey(xorKey).decrypt(this.currentUMPToken.presentationRecoveryPrivileged);
487
- await this.buildUnderlying(primaryKey, privilegedKey);
479
+ const rootPrimaryKey = new sdk_1.SymmetricKey(xorKey).decrypt(this.currentUMPToken.presentationRecoveryPrimary);
480
+ const rootPrivilegedKey = new sdk_1.SymmetricKey(xorKey).decrypt(this.currentUMPToken.presentationRecoveryPrivileged);
481
+ // Build root infrastructure, load profiles, switch to default
482
+ await this.setupRootInfrastructure(rootPrimaryKey, rootPrivilegedKey);
483
+ await this.switchProfile(this.activeProfileId);
488
484
  }
489
485
  }
486
+ // --- State Management Methods ---
490
487
  /**
491
- * Saves the current wallet state (including the current UMP token and primary key)
492
- * into an encrypted snapshot. This snapshot can be stored locally and later passed
493
- * to `loadSnapshot` to restore the wallet state without re-authenticating manually.
494
- *
495
- * @remarks
496
- * Storing the snapshot provides a fully authenticated state.
497
- * This **must** be securely stored (e.g. system keychain or encrypted file).
498
- * If attackers gain access to this snapshot, they can fully control the wallet.
488
+ * Saves the current wallet state (root key, UMP token, active profile) into an encrypted snapshot.
489
+ * Version 2 format: [1 byte version=2] + [32 byte snapshot key] + [16 byte activeProfileId] + [encrypted payload]
490
+ * Encrypted Payload: [32 byte rootPrimaryKey] + [varint token length + serialized UMP token]
499
491
  *
500
- * @returns An array of bytes representing the encrypted snapshot.
501
- * @throws {Error} if no primary key or token is currently set.
492
+ * @returns Encrypted snapshot bytes.
502
493
  */
503
494
  saveSnapshot() {
504
- if (!this.primaryKey || !this.currentUMPToken) {
505
- throw new Error('No primary key or current UMP token set');
495
+ if (!this.rootPrimaryKey || !this.currentUMPToken) {
496
+ throw new Error('No root primary key or current UMP token set');
506
497
  }
507
- // Generate a random snapshot encryption key:
508
498
  const snapshotKey = (0, sdk_1.Random)(32);
509
- // Serialize the relevant data to a preimage buffer:
510
499
  const snapshotPreimageWriter = new sdk_1.Utils.Writer();
511
- // Write the primary key (32 bytes):
512
- snapshotPreimageWriter.write(this.primaryKey);
513
- // Write the serialized UMP token:
500
+ // Write root primary key
501
+ snapshotPreimageWriter.write(this.rootPrimaryKey);
502
+ // Write serialized UMP token (must have outpoint)
503
+ if (!this.currentUMPToken.currentOutpoint) {
504
+ throw new Error('UMP token cannot be saved without a current outpoint.');
505
+ }
514
506
  const serializedToken = this.serializeUMPToken(this.currentUMPToken);
507
+ snapshotPreimageWriter.writeVarIntNum(serializedToken.length);
515
508
  snapshotPreimageWriter.write(serializedToken);
516
- // Encrypt the combined data with the snapshotKey:
509
+ // Encrypt the payload
517
510
  const snapshotPreimage = snapshotPreimageWriter.toArray();
518
511
  const snapshotPayload = new sdk_1.SymmetricKey(snapshotKey).encrypt(snapshotPreimage);
519
- // Build the final snapshot structure: [snapshotKey (32 bytes) + encryptedPayload]
512
+ // Build final snapshot (Version 2)
520
513
  const snapshotWriter = new sdk_1.Utils.Writer();
514
+ snapshotWriter.writeUInt8(2); // Version
521
515
  snapshotWriter.write(snapshotKey);
522
- snapshotWriter.write(snapshotPayload);
516
+ snapshotWriter.write(this.activeProfileId); // Active profile ID
517
+ snapshotWriter.write(snapshotPayload); // Encrypted data
523
518
  return snapshotWriter.toArray();
524
519
  }
525
520
  /**
526
- * Loads a previously saved state snapshot (e.g. from `saveSnapshot`).
527
- * Upon success, the wallet becomes authenticated without needing to re-enter keys.
521
+ * Loads a previously saved state snapshot. Restores root key, UMP token, profiles, and active profile.
522
+ * Handles Version 1 (legacy) and Version 2 formats.
528
523
  *
529
- * @param snapshot An array of bytes that was previously produced by `saveSnapshot`.
530
- * @throws {Error} If the snapshot format is invalid or decryption fails.
524
+ * @param snapshot Encrypted snapshot bytes.
531
525
  */
532
526
  async loadSnapshot(snapshot) {
533
527
  try {
534
528
  const reader = new sdk_1.Utils.Reader(snapshot);
535
- // First 32 bytes is the snapshotKey:
536
- const snapshotKey = reader.read(32);
537
- // The rest is the encrypted payload:
538
- const encryptedPayload = reader.read();
539
- // Decrypt the payload:
529
+ const version = reader.readUInt8();
530
+ let snapshotKey;
531
+ let encryptedPayload;
532
+ let activeProfileId = exports.DEFAULT_PROFILE_ID; // Default for V1
533
+ if (version === 1) {
534
+ snapshotKey = reader.read(32);
535
+ encryptedPayload = reader.read();
536
+ }
537
+ else if (version === 2) {
538
+ snapshotKey = reader.read(32);
539
+ activeProfileId = reader.read(16); // Read active profile ID
540
+ encryptedPayload = reader.read();
541
+ }
542
+ else {
543
+ throw new Error(`Unsupported snapshot version: ${version}`);
544
+ }
545
+ // Decrypt payload
540
546
  const decryptedPayload = new sdk_1.SymmetricKey(snapshotKey).decrypt(encryptedPayload);
541
547
  const payloadReader = new sdk_1.Utils.Reader(decryptedPayload);
542
- // Read the primary key (32 bytes):
543
- const primaryKey = payloadReader.read(32);
544
- // Read the remainder as the serialized UMP token:
545
- const tokenBytes = payloadReader.read();
548
+ // Read root primary key
549
+ const rootPrimaryKey = payloadReader.read(32);
550
+ // Read serialized UMP token
551
+ const tokenLen = payloadReader.readVarIntNum();
552
+ const tokenBytes = payloadReader.read(tokenLen);
546
553
  const token = this.deserializeUMPToken(tokenBytes);
547
- // Assign and build:
554
+ // Assign loaded data
548
555
  this.currentUMPToken = token;
549
- await this.buildUnderlying(primaryKey);
556
+ // Setup root infrastructure, load profiles, and switch to the loaded active profile
557
+ await this.setupRootInfrastructure(rootPrimaryKey); // Will automatically load profiles
558
+ await this.switchProfile(activeProfileId); // Switch to the profile saved in the snapshot
559
+ this.authenticationFlow = 'existing-user'; // Loading implies existing user
550
560
  }
551
561
  catch (error) {
562
+ this.destroy(); // Clear state on error
552
563
  throw new Error(`Failed to load snapshot: ${error.message}`);
553
564
  }
554
565
  }
555
566
  /**
556
- * Destroys the underlying wallet, returning to a default state
567
+ * Destroys the wallet state, clearing keys, tokens, and profiles.
557
568
  */
558
569
  destroy() {
559
570
  this.underlying = undefined;
560
- this.underlyingPrivilegedKeyManager = undefined;
571
+ this.rootPrivilegedKeyManager = undefined;
561
572
  this.authenticated = false;
562
- this.primaryKey = undefined;
573
+ this.rootPrimaryKey = undefined;
563
574
  this.currentUMPToken = undefined;
564
575
  this.presentationKey = undefined;
565
576
  this.recoveryKey = undefined;
577
+ this.profiles = [];
578
+ this.activeProfileId = exports.DEFAULT_PROFILE_ID;
566
579
  this.authenticationMode = 'presentation-key-and-password';
567
580
  this.authenticationFlow = 'new-user';
568
581
  }
582
+ // --- Profile Management Methods ---
569
583
  /**
570
- * Changes the user's password, re-wrapping the primary and privileged keys with the new password factor.
571
- *
572
- * @param newPassword The user's new password as a string.
573
- * @throws {Error} If the user is not authenticated, or if underlying token references are missing.
584
+ * Lists all available profiles, including the default profile.
585
+ * @returns Array of profile info objects, including an 'active' flag.
574
586
  */
575
- async changePassword(newPassword) {
587
+ listProfiles() {
576
588
  if (!this.authenticated) {
577
589
  throw new Error('Not authenticated.');
578
590
  }
579
- if (!this.currentUMPToken) {
580
- throw new Error('No UMP token to update.');
591
+ const profileList = [
592
+ // Default profile
593
+ {
594
+ id: exports.DEFAULT_PROFILE_ID,
595
+ name: 'default',
596
+ createdAt: null, // Default profile doesn't have a creation timestamp in the same way
597
+ active: this.activeProfileId.every(x => x === 0)
598
+ },
599
+ // Other profiles
600
+ ...this.profiles.map(p => ({
601
+ id: p.id,
602
+ name: p.name,
603
+ createdAt: p.createdAt,
604
+ active: this.activeProfileId.every((x, i) => x === p.id[i])
605
+ }))
606
+ ];
607
+ return profileList;
608
+ }
609
+ /**
610
+ * Adds a new profile with the given name.
611
+ * Generates necessary pads and updates the UMP token.
612
+ * Does not switch to the new profile automatically.
613
+ *
614
+ * @param name The desired name for the new profile.
615
+ * @returns The ID of the newly created profile.
616
+ */
617
+ async addProfile(name) {
618
+ if (!this.authenticated || !this.rootPrimaryKey || !this.currentUMPToken || !this.rootPrivilegedKeyManager) {
619
+ throw new Error('Wallet not fully initialized or authenticated.');
620
+ }
621
+ // Ensure name is unique (including 'default')
622
+ if (name === 'default' || this.profiles.some(p => p.name.toLowerCase() === name.toLowerCase())) {
623
+ throw new Error(`Profile name "${name}" is already in use.`);
624
+ }
625
+ const newProfile = {
626
+ name,
627
+ id: (0, sdk_1.Random)(16),
628
+ primaryPad: (0, sdk_1.Random)(32),
629
+ privilegedPad: (0, sdk_1.Random)(32),
630
+ createdAt: Math.floor(Date.now() / 1000)
631
+ };
632
+ this.profiles.push(newProfile);
633
+ // Update the UMP token with the new profile list
634
+ await this.updateAuthFactors(this.currentUMPToken.passwordSalt,
635
+ // Need to re-derive/decrypt factors needed for re-encryption
636
+ await this.getFactor('passwordKey'), await this.getFactor('presentationKey'), await this.getFactor('recoveryKey'), this.rootPrimaryKey, await this.getFactor('privilegedKey', true), // Get ROOT privileged key
637
+ this.profiles // Pass the updated profile list
638
+ );
639
+ return newProfile.id;
640
+ }
641
+ /**
642
+ * Deletes a profile by its ID.
643
+ * Cannot delete the default profile. If the active profile is deleted,
644
+ * it switches back to the default profile.
645
+ *
646
+ * @param profileId The 16-byte ID of the profile to delete.
647
+ */
648
+ async deleteProfile(profileId) {
649
+ if (!this.authenticated || !this.rootPrimaryKey || !this.currentUMPToken || !this.rootPrivilegedKeyManager) {
650
+ throw new Error('Wallet not fully initialized or authenticated.');
651
+ }
652
+ if (profileId.every(x => x === 0)) {
653
+ throw new Error('Cannot delete the default profile.');
654
+ }
655
+ const profileIndex = this.profiles.findIndex(p => p.id.every((x, i) => x === profileId[i]));
656
+ if (profileIndex === -1) {
657
+ throw new Error('Profile not found.');
658
+ }
659
+ // Remove the profile
660
+ this.profiles.splice(profileIndex, 1);
661
+ // If the deleted profile was active, switch to default
662
+ if (this.activeProfileId.every((x, i) => x === profileId[i])) {
663
+ await this.switchProfile(exports.DEFAULT_PROFILE_ID); // This rebuilds the wallet
664
+ }
665
+ // Update the UMP token
666
+ await this.updateAuthFactors(this.currentUMPToken.passwordSalt, await this.getFactor('passwordKey'), await this.getFactor('presentationKey'), await this.getFactor('recoveryKey'), this.rootPrimaryKey, await this.getFactor('privilegedKey', true), // Get ROOT privileged key
667
+ this.profiles // Pass updated list
668
+ );
669
+ }
670
+ /**
671
+ * Switches the active profile. This re-derives keys and rebuilds the underlying wallet.
672
+ *
673
+ * @param profileId The 16-byte ID of the profile to switch to (use DEFAULT_PROFILE_ID for default).
674
+ */
675
+ async switchProfile(profileId) {
676
+ if (!this.authenticated || !this.rootPrimaryKey || !this.rootPrivilegedKeyManager) {
677
+ throw new Error('Cannot switch profile: Wallet not authenticated or root keys missing.');
678
+ }
679
+ let profilePrimaryKey;
680
+ let profilePrivilegedPad; // Pad for the target profile
681
+ if (profileId.every(x => x === 0)) {
682
+ // Switching to default profile
683
+ profilePrimaryKey = this.rootPrimaryKey;
684
+ profilePrivilegedPad = undefined; // No pad for default
685
+ this.activeProfileId = exports.DEFAULT_PROFILE_ID;
686
+ }
687
+ else {
688
+ // Switching to a non-default profile
689
+ const profile = this.profiles.find(p => p.id.every((x, i) => x === profileId[i]));
690
+ if (!profile) {
691
+ throw new Error('Profile not found.');
692
+ }
693
+ profilePrimaryKey = this.XOR(this.rootPrimaryKey, profile.primaryPad);
694
+ profilePrivilegedPad = profile.privilegedPad;
695
+ this.activeProfileId = profileId;
696
+ }
697
+ // Create a *profile-specific* PrivilegedKeyManager.
698
+ // It uses the ROOT manager internally but applies the profile's pad.
699
+ const profilePrivilegedKeyManager = new PrivilegedKeyManager_1.PrivilegedKeyManager(async (reason) => {
700
+ // Request the ROOT privileged key using the root manager
701
+ const rootPrivileged = await this.rootPrivilegedKeyManager.getPrivilegedKey(reason);
702
+ const rootPrivilegedBytes = rootPrivileged.toArray();
703
+ // Apply the profile's pad if applicable
704
+ const profilePrivilegedBytes = profilePrivilegedPad
705
+ ? this.XOR(rootPrivilegedBytes, profilePrivilegedPad)
706
+ : rootPrivilegedBytes;
707
+ return new sdk_1.PrivateKey(profilePrivilegedBytes);
708
+ });
709
+ // Build the underlying wallet for the specific profile
710
+ this.underlying = await this.walletBuilder(profilePrimaryKey, profilePrivilegedKeyManager, // Pass the profile-specific manager
711
+ this.activeProfileId // Pass the ID of the profile being activated
712
+ );
713
+ }
714
+ // --- Key Management Methods ---
715
+ /**
716
+ * Changes the user's password. Re-wraps keys and updates the UMP token.
717
+ */
718
+ async changePassword(newPassword) {
719
+ if (!this.authenticated || !this.currentUMPToken || !this.rootPrimaryKey || !this.rootPrivilegedKeyManager) {
720
+ throw new Error('Not authenticated or missing required data.');
581
721
  }
582
722
  const passwordSalt = (0, sdk_1.Random)(32);
583
- const passwordKey = sdk_1.Hash.pbkdf2(sdk_1.Utils.toArray(newPassword, 'utf8'), passwordSalt, exports.PBKDF2_NUM_ROUNDS, 32, 'sha512');
584
- // Decrypt existing factors via the privileged key manager:
585
- const recoveryKey = (await this.underlyingPrivilegedKeyManager.decrypt({
586
- ciphertext: this.currentUMPToken.recoveryKeyEncrypted,
587
- protocolID: [2, 'admin key wrapping'],
588
- keyID: '1'
589
- })).plaintext;
590
- const presentationKey = (await this.underlyingPrivilegedKeyManager.decrypt({
591
- ciphertext: this.currentUMPToken.presentationKeyEncrypted,
592
- protocolID: [2, 'admin key wrapping'],
593
- keyID: '1'
594
- })).plaintext;
595
- const privilegedKey = new sdk_1.SymmetricKey(this.XOR(presentationKey, recoveryKey)).decrypt(this.currentUMPToken.presentationRecoveryPrivileged);
596
- await this.updateAuthFactors(passwordSalt, passwordKey, presentationKey, recoveryKey, this.primaryKey, privilegedKey);
723
+ const newPasswordKey = sdk_1.Hash.pbkdf2(sdk_1.Utils.toArray(newPassword, 'utf8'), passwordSalt, exports.PBKDF2_NUM_ROUNDS, 32, 'sha512');
724
+ // Decrypt existing factors needed for re-encryption, using the *root* privileged key manager
725
+ const recoveryKey = await this.getFactor('recoveryKey');
726
+ const presentationKey = await this.getFactor('presentationKey');
727
+ const rootPrivilegedKey = await this.getFactor('privilegedKey', true); // Get ROOT privileged key
728
+ await this.updateAuthFactors(passwordSalt, newPasswordKey, presentationKey, recoveryKey, this.rootPrimaryKey, rootPrivilegedKey, // Pass the explicitly fetched root key
729
+ this.profiles // Preserve existing profiles
730
+ );
597
731
  }
598
732
  /**
599
- * Retrieves the current recovery key.
600
- *
601
- * @throws {Error} If the user is not authenticated, or if underlying token references are missing.
733
+ * Retrieves the current recovery key. Requires privileged access.
602
734
  */
603
735
  async getRecoveryKey() {
604
- if (!this.authenticated) {
605
- throw new Error('Not authenticated.');
736
+ if (!this.authenticated || !this.currentUMPToken || !this.rootPrivilegedKeyManager) {
737
+ throw new Error('Not authenticated or missing required data.');
606
738
  }
607
- if (!this.currentUMPToken) {
608
- throw new Error('No UMP token!');
609
- }
610
- return (await this.underlyingPrivilegedKeyManager.decrypt({
611
- ciphertext: this.currentUMPToken.recoveryKeyEncrypted,
612
- protocolID: [2, 'admin key wrapping'],
613
- keyID: '1'
614
- })).plaintext;
739
+ return this.getFactor('recoveryKey');
615
740
  }
616
741
  /**
617
- * Changes the user's recovery key, prompting the user to save the new key.
618
- *
619
- * @throws {Error} If the user is not authenticated, or if underlying token references are missing.
742
+ * Changes the user's recovery key. Prompts user to save the new key.
620
743
  */
621
744
  async changeRecoveryKey() {
622
- if (!this.authenticated) {
623
- throw new Error('Not authenticated.');
624
- }
625
- if (!this.currentUMPToken) {
626
- throw new Error('No UMP token to update.');
627
- }
628
- // Decrypt existing password/presentation keys via the privileged key manager:
629
- const passwordKey = (await this.underlyingPrivilegedKeyManager.decrypt({
630
- ciphertext: this.currentUMPToken.passwordKeyEncrypted,
631
- protocolID: [2, 'admin key wrapping'],
632
- keyID: '1'
633
- })).plaintext;
634
- const presentationKey = (await this.underlyingPrivilegedKeyManager.decrypt({
635
- ciphertext: this.currentUMPToken.presentationKeyEncrypted,
636
- protocolID: [2, 'admin key wrapping'],
637
- keyID: '1'
638
- })).plaintext;
639
- const privilegedKey = new sdk_1.SymmetricKey(this.XOR(passwordKey, this.primaryKey)).decrypt(this.currentUMPToken.passwordPrimaryPrivileged);
640
- const recoveryKey = (0, sdk_1.Random)(32);
641
- await this.recoveryKeySaver(recoveryKey);
642
- await this.updateAuthFactors(this.currentUMPToken.passwordSalt, passwordKey, presentationKey, recoveryKey, this.primaryKey, privilegedKey);
745
+ if (!this.authenticated || !this.currentUMPToken || !this.rootPrimaryKey || !this.rootPrivilegedKeyManager) {
746
+ throw new Error('Not authenticated or missing required data.');
747
+ }
748
+ // Decrypt existing factors needed
749
+ const passwordKey = await this.getFactor('passwordKey');
750
+ const presentationKey = await this.getFactor('presentationKey');
751
+ const rootPrivilegedKey = await this.getFactor('privilegedKey', true); // Get ROOT privileged key
752
+ // Generate and save new recovery key
753
+ const newRecoveryKey = (0, sdk_1.Random)(32);
754
+ await this.recoveryKeySaver(newRecoveryKey);
755
+ await this.updateAuthFactors(this.currentUMPToken.passwordSalt, passwordKey, presentationKey, newRecoveryKey, // Use the new key
756
+ this.rootPrimaryKey, rootPrivilegedKey, this.profiles // Preserve profiles
757
+ );
643
758
  }
644
759
  /**
645
760
  * Changes the user's presentation key.
646
- *
647
- * @param presentationKey The new presentation key (32 bytes).
648
- * @throws {Error} If the user is not authenticated, or if underlying token references are missing.
649
761
  */
650
- async changePresentationKey(presentationKey) {
651
- if (!this.authenticated) {
652
- throw new Error('Not authenticated.');
762
+ async changePresentationKey(newPresentationKey) {
763
+ if (!this.authenticated || !this.currentUMPToken || !this.rootPrimaryKey || !this.rootPrivilegedKeyManager) {
764
+ throw new Error('Not authenticated or missing required data.');
765
+ }
766
+ if (newPresentationKey.length !== 32) {
767
+ throw new Error('Presentation key must be 32 bytes.');
768
+ }
769
+ // Decrypt existing factors
770
+ const recoveryKey = await this.getFactor('recoveryKey');
771
+ const passwordKey = await this.getFactor('passwordKey');
772
+ const rootPrivilegedKey = await this.getFactor('privilegedKey', true); // Get ROOT privileged key
773
+ await this.updateAuthFactors(this.currentUMPToken.passwordSalt, passwordKey, newPresentationKey, // Use the new key
774
+ recoveryKey, this.rootPrimaryKey, rootPrivilegedKey, this.profiles // Preserve profiles
775
+ );
776
+ // Update the temporarily stored key if it was set
777
+ if (this.presentationKey) {
778
+ this.presentationKey = newPresentationKey;
653
779
  }
654
- if (!this.currentUMPToken) {
655
- throw new Error('No UMP token to update.');
656
- }
657
- // Decrypt existing password/recovery keys via the privileged key manager:
658
- const recoveryKey = (await this.underlyingPrivilegedKeyManager.decrypt({
659
- ciphertext: this.currentUMPToken.recoveryKeyEncrypted,
660
- protocolID: [2, 'admin key wrapping'],
661
- keyID: '1'
662
- })).plaintext;
663
- const passwordKey = (await this.underlyingPrivilegedKeyManager.decrypt({
664
- ciphertext: this.currentUMPToken.passwordKeyEncrypted,
665
- protocolID: [2, 'admin key wrapping'],
666
- keyID: '1'
667
- })).plaintext;
668
- const privilegedKey = new sdk_1.SymmetricKey(this.XOR(passwordKey, this.primaryKey)).decrypt(this.currentUMPToken.passwordPrimaryPrivileged);
669
- await this.updateAuthFactors(this.currentUMPToken.passwordSalt, passwordKey, presentationKey, recoveryKey, this.primaryKey, privilegedKey);
670
780
  }
781
+ // --- Internal Helper Methods ---
671
782
  /**
672
- * Internal helper to recompute a UMP token with updated authentication factors and consume the old token on-chain.
673
- *
674
- * @param passwordSalt The PBKDF2 salt for the new password factor.
675
- * @param passwordKey The PBKDF2-derived password key (32 bytes).
676
- * @param presentationKey The new or existing presentation key (32 bytes).
677
- * @param recoveryKey The new or existing recovery key (32 bytes).
678
- * @param primaryKey The user's primary key for re-wrapping.
679
- * @param privilegedKey The user's privileged key for re-wrapping.
680
- * @throws {Error} If the user is not authenticated or if keys are unavailable.
783
+ * Performs XOR operation on two byte arrays.
681
784
  */
682
- async updateAuthFactors(passwordSalt, passwordKey, presentationKey, recoveryKey, primaryKey, privilegedKey) {
683
- if (!this.authenticated || !this.primaryKey || !this.currentUMPToken) {
684
- throw new Error('Wallet is not properly authenticated or missing data.');
785
+ XOR(n1, n2) {
786
+ if (n1.length !== n2.length) {
787
+ // Provide more context in error
788
+ throw new Error(`XOR length mismatch: ${n1.length} vs ${n2.length}`);
789
+ }
790
+ const r = new Array(n1.length);
791
+ for (let i = 0; i < n1.length; i++) {
792
+ r[i] = n1[i] ^ n2[i];
793
+ }
794
+ return r;
795
+ }
796
+ /**
797
+ * Helper to decrypt a specific factor (key) stored encrypted in the UMP token.
798
+ * Requires the root privileged key manager.
799
+ * @param factorName Name of the factor to decrypt ('passwordKey', 'presentationKey', 'recoveryKey', 'privilegedKey').
800
+ * @param getRoot If true and factorName is 'privilegedKey', returns the root privileged key bytes directly.
801
+ * @returns The decrypted key bytes.
802
+ */
803
+ async getFactor(factorName, getRoot = false) {
804
+ if (!this.authenticated || !this.currentUMPToken || !this.rootPrivilegedKeyManager) {
805
+ throw new Error(`Cannot get factor "${factorName}": Wallet not ready.`);
806
+ }
807
+ const protocolID = [2, 'admin key wrapping']; // Protocol used for encrypting factors
808
+ const keyID = '1'; // Key ID used
809
+ try {
810
+ switch (factorName) {
811
+ case 'passwordKey':
812
+ return (await this.rootPrivilegedKeyManager.decrypt({
813
+ ciphertext: this.currentUMPToken.passwordKeyEncrypted,
814
+ protocolID,
815
+ keyID
816
+ })).plaintext;
817
+ case 'presentationKey':
818
+ return (await this.rootPrivilegedKeyManager.decrypt({
819
+ ciphertext: this.currentUMPToken.presentationKeyEncrypted,
820
+ protocolID,
821
+ keyID
822
+ })).plaintext;
823
+ case 'recoveryKey':
824
+ return (await this.rootPrivilegedKeyManager.decrypt({
825
+ ciphertext: this.currentUMPToken.recoveryKeyEncrypted,
826
+ protocolID,
827
+ keyID
828
+ })).plaintext;
829
+ case 'privilegedKey': {
830
+ // This needs careful handling based on whether the ROOT or PROFILE key is needed.
831
+ // This helper is mostly used for UMP updates, which need the ROOT key.
832
+ // We retrieve the PrivateKey object first.
833
+ const pk = await this.rootPrivilegedKeyManager.getPrivilegedKey('UMP token update', true); // Force retrieval of root key
834
+ return pk.toArray(); // Return bytes
835
+ }
836
+ default:
837
+ throw new Error(`Unknown factor name: ${factorName}`);
838
+ }
839
+ }
840
+ catch (error) {
841
+ console.error(`Error decrypting factor ${factorName}:`, error);
842
+ throw new Error(`Failed to decrypt factor "${factorName}": ${error.message}`);
685
843
  }
686
- // Derive symmetrical encryption keys via XOR:
844
+ }
845
+ /**
846
+ * Recomputes UMP token fields with updated factors and profiles, then publishes the update.
847
+ * This operation requires the *root* privileged key and the *default* profile wallet.
848
+ */
849
+ async updateAuthFactors(passwordSalt, passwordKey, presentationKey, recoveryKey, rootPrimaryKey, rootPrivilegedKey, // Explicitly pass the root key bytes
850
+ profiles // Pass current/new profiles list
851
+ ) {
852
+ if (!this.authenticated || !this.rootPrimaryKey || !this.currentUMPToken) {
853
+ throw new Error('Wallet is not properly authenticated or missing data for update.');
854
+ }
855
+ // Ensure we have the OLD token to consume
856
+ const oldTokenToConsume = { ...this.currentUMPToken };
857
+ if (!oldTokenToConsume.currentOutpoint) {
858
+ throw new Error('Cannot update UMP token: Old token has no outpoint.');
859
+ }
860
+ // Derive symmetrical encryption keys using XOR for the *root* keys
687
861
  const presentationPassword = new sdk_1.SymmetricKey(this.XOR(presentationKey, passwordKey));
688
862
  const presentationRecovery = new sdk_1.SymmetricKey(this.XOR(presentationKey, recoveryKey));
689
863
  const recoveryPassword = new sdk_1.SymmetricKey(this.XOR(recoveryKey, passwordKey));
690
- const primaryPassword = new sdk_1.SymmetricKey(this.XOR(this.primaryKey, passwordKey));
691
- // Build a temporary privileged key manager just to encrypt the new fields:
692
- const tempPrivilegedKeyManager = new PrivilegedKeyManager_1.PrivilegedKeyManager(async () => new sdk_1.PrivateKey(privilegedKey));
693
- // Construct the new UMP token:
694
- const newToken = {
864
+ const primaryPassword = new sdk_1.SymmetricKey(this.XOR(rootPrimaryKey, passwordKey)); // Use rootPrimaryKey
865
+ // Build a temporary privileged key manager using the explicit ROOT privileged key
866
+ const tempRootPrivilegedKeyManager = new PrivilegedKeyManager_1.PrivilegedKeyManager(async () => new sdk_1.PrivateKey(rootPrivilegedKey));
867
+ // Encrypt profiles if provided
868
+ let profilesEncrypted;
869
+ if (profiles && profiles.length > 0) {
870
+ const profilesJson = JSON.stringify(profiles);
871
+ const profilesBytes = sdk_1.Utils.toArray(profilesJson, 'utf8');
872
+ profilesEncrypted = (await tempRootPrivilegedKeyManager.encrypt({
873
+ plaintext: profilesBytes,
874
+ protocolID: [2, 'admin profile wrapping'], // Separate protocol for profiles
875
+ keyID: '1'
876
+ })).ciphertext;
877
+ }
878
+ // Construct the new UMP token data
879
+ const newTokenData = {
695
880
  passwordSalt,
696
- passwordPresentationPrimary: presentationPassword.encrypt(this.primaryKey),
697
- passwordRecoveryPrimary: recoveryPassword.encrypt(this.primaryKey),
698
- presentationRecoveryPrimary: presentationRecovery.encrypt(this.primaryKey),
699
- passwordPrimaryPrivileged: primaryPassword.encrypt(privilegedKey),
700
- presentationRecoveryPrivileged: presentationRecovery.encrypt(privilegedKey),
881
+ passwordPresentationPrimary: presentationPassword.encrypt(rootPrimaryKey),
882
+ passwordRecoveryPrimary: recoveryPassword.encrypt(rootPrimaryKey),
883
+ presentationRecoveryPrimary: presentationRecovery.encrypt(rootPrimaryKey),
884
+ passwordPrimaryPrivileged: primaryPassword.encrypt(rootPrivilegedKey),
885
+ presentationRecoveryPrivileged: presentationRecovery.encrypt(rootPrivilegedKey),
701
886
  presentationHash: sdk_1.Hash.sha256(presentationKey),
702
887
  recoveryHash: sdk_1.Hash.sha256(recoveryKey),
703
- presentationKeyEncrypted: (await tempPrivilegedKeyManager.encrypt({
888
+ presentationKeyEncrypted: (await tempRootPrivilegedKeyManager.encrypt({
704
889
  plaintext: presentationKey,
705
890
  protocolID: [2, 'admin key wrapping'],
706
891
  keyID: '1'
707
892
  })).ciphertext,
708
- passwordKeyEncrypted: (await tempPrivilegedKeyManager.encrypt({
893
+ passwordKeyEncrypted: (await tempRootPrivilegedKeyManager.encrypt({
709
894
  plaintext: passwordKey,
710
895
  protocolID: [2, 'admin key wrapping'],
711
896
  keyID: '1'
712
897
  })).ciphertext,
713
- recoveryKeyEncrypted: (await tempPrivilegedKeyManager.encrypt({
898
+ recoveryKeyEncrypted: (await tempRootPrivilegedKeyManager.encrypt({
714
899
  plaintext: recoveryKey,
715
900
  protocolID: [2, 'admin key wrapping'],
716
901
  keyID: '1'
717
- })).ciphertext
902
+ })).ciphertext,
903
+ profilesEncrypted // Add encrypted profiles
904
+ // currentOutpoint will be set after publishing
718
905
  };
719
- // Publish the new token on-chain and consume the old one:
720
- newToken.currentOutpoint = await this.UMPTokenInteractor.buildAndSend(this.underlying, this.adminOriginator, newToken, this.currentUMPToken);
721
- this.currentUMPToken = newToken;
722
- }
723
- /**
724
- * A helper function to XOR two equal-length byte arrays.
725
- *
726
- * @param n1 The first byte array.
727
- * @param n2 The second byte array.
728
- * @returns A new byte array which is the element-wise XOR of the two inputs.
729
- * @throws {Error} if the two arrays are not the same length.
730
- */
731
- XOR(n1, n2) {
732
- if (n1.length !== n2.length) {
733
- throw new Error('lengths mismatch');
734
- }
735
- const r = new Array(n1.length);
736
- for (let i = 0; i < n1.length; i++) {
737
- r[i] = n1[i] ^ n2[i];
906
+ // We need the wallet built for the DEFAULT profile to publish the UMP token.
907
+ // If the current active profile is not default, temporarily switch, publish, then switch back.
908
+ const currentActiveId = this.activeProfileId;
909
+ let walletToUse = this.underlying;
910
+ if (currentActiveId.every(x => x === 0)) {
911
+ console.log('Temporarily switching to default profile to update UMP token...');
912
+ await this.switchProfile(exports.DEFAULT_PROFILE_ID); // This rebuilds this.underlying
913
+ walletToUse = this.underlying;
914
+ }
915
+ if (!walletToUse) {
916
+ throw new Error('Default profile wallet could not be activated for UMP token update.');
917
+ }
918
+ // Publish the new token on-chain, consuming the old one
919
+ try {
920
+ newTokenData.currentOutpoint = await this.UMPTokenInteractor.buildAndSend(walletToUse, // Use the (potentially temporarily activated) default profile wallet
921
+ this.adminOriginator, newTokenData, oldTokenToConsume // Consume the previous token
922
+ );
923
+ // Update the manager's state
924
+ this.currentUMPToken = newTokenData;
925
+ // Profiles are already updated in this.profiles if they were passed in
926
+ }
927
+ finally {
928
+ // Switch back if we temporarily switched
929
+ if (!currentActiveId.every(x => x === 0)) {
930
+ console.log('Switching back to original profile...');
931
+ await this.switchProfile(currentActiveId);
932
+ }
738
933
  }
739
- return r;
740
934
  }
741
935
  /**
742
- * A helper function to serialize a UMP token to a binary format (version=1).
743
- * The serialization layout is:
744
- * - [1 byte version (value=1)]
745
- * - For each array field in the UMP token, [varint length + bytes]
746
- * - Then [varint length + outpoint string in UTF-8]
747
- *
748
- * @param token The UMP token to serialize.
749
- * @returns A byte array representing the serialized token.
750
- * @throws {Error} if the token has no currentOutpoint (required for serialization).
936
+ * Serializes a UMP token to binary format (Version 2 with optional profiles).
937
+ * Layout: [1 byte version=2] + [11 * (varint len + bytes) for standard fields] + [1 byte profile_flag] + [IF flag=1 THEN varint len + profile bytes] + [varint len + outpoint bytes]
751
938
  */
752
939
  serializeUMPToken(token) {
753
940
  if (!token.currentOutpoint) {
754
941
  throw new Error('Token must have outpoint for serialization');
755
942
  }
756
943
  const writer = new sdk_1.Utils.Writer();
757
- // Write version byte
758
- writer.writeUInt8(1);
759
- // Helper to write array with length prefix
944
+ writer.writeUInt8(2); // Version 2
760
945
  const writeArray = (arr) => {
761
946
  writer.writeVarIntNum(arr.length);
762
947
  writer.write(arr);
763
948
  };
764
- // Write each array-based field in the order they appear on UMPToken
765
- writeArray(token.passwordPresentationPrimary);
766
- writeArray(token.passwordRecoveryPrimary);
767
- writeArray(token.presentationRecoveryPrimary);
768
- writeArray(token.passwordPrimaryPrivileged);
769
- writeArray(token.presentationRecoveryPrivileged);
770
- writeArray(token.presentationHash);
771
- writeArray(token.passwordSalt);
772
- writeArray(token.recoveryHash);
773
- writeArray(token.presentationKeyEncrypted);
774
- writeArray(token.recoveryKeyEncrypted);
775
- writeArray(token.passwordKeyEncrypted);
776
- // Finally, write the outpoint string:
949
+ // Write standard fields in specific order
950
+ writeArray(token.passwordSalt); // 0
951
+ writeArray(token.passwordPresentationPrimary); // 1
952
+ writeArray(token.passwordRecoveryPrimary); // 2
953
+ writeArray(token.presentationRecoveryPrimary); // 3
954
+ writeArray(token.passwordPrimaryPrivileged); // 4
955
+ writeArray(token.presentationRecoveryPrivileged); // 5
956
+ writeArray(token.presentationHash); // 6
957
+ writeArray(token.recoveryHash); // 7
958
+ writeArray(token.presentationKeyEncrypted); // 8
959
+ writeArray(token.passwordKeyEncrypted); // 9 - Swapped order vs original doc comment
960
+ writeArray(token.recoveryKeyEncrypted); // 10
961
+ // Write optional profiles field
962
+ if (token.profilesEncrypted && token.profilesEncrypted.length > 0) {
963
+ writer.writeUInt8(1); // Flag indicating profiles present
964
+ writeArray(token.profilesEncrypted);
965
+ }
966
+ else {
967
+ writer.writeUInt8(0); // Flag indicating no profiles
968
+ }
969
+ // Write outpoint string
777
970
  const outpointBytes = sdk_1.Utils.toArray(token.currentOutpoint, 'utf8');
778
971
  writer.writeVarIntNum(outpointBytes.length);
779
972
  writer.write(outpointBytes);
780
973
  return writer.toArray();
781
974
  }
782
975
  /**
783
- * A helper function to deserialize a UMP token from the format described in `serializeUMPToken`.
784
- *
785
- * @param bin The serialized byte array.
786
- * @returns The reconstructed UMP token.
787
- * @throws {Error} if the version byte is unexpected or if parsing fails.
976
+ * Deserializes a UMP token from binary format (Handles Version 1 and 2).
788
977
  */
789
978
  deserializeUMPToken(bin) {
790
979
  const reader = new sdk_1.Utils.Reader(bin);
791
- // Check version:
792
980
  const version = reader.readUInt8();
793
- if (version !== 1) {
794
- throw new Error(`Unsupported UMP token version: ${version}`);
981
+ if (version !== 1 && version !== 2) {
982
+ throw new Error(`Unsupported UMP token serialization version: ${version}`);
795
983
  }
796
- // Helper to read an array with length prefix
797
984
  const readArray = () => {
798
985
  const length = reader.readVarIntNum();
799
986
  return reader.read(length);
800
987
  };
801
- // Read in the correct order:
802
- const passwordPresentationPrimary = readArray();
803
- const passwordRecoveryPrimary = readArray();
804
- const presentationRecoveryPrimary = readArray();
805
- const passwordPrimaryPrivileged = readArray();
806
- const presentationRecoveryPrivileged = readArray();
807
- const presentationHash = readArray();
808
- const passwordSalt = readArray();
809
- const recoveryHash = readArray();
810
- const presentationKeyEncrypted = readArray();
811
- const recoveryKeyEncrypted = readArray();
812
- const passwordKeyEncrypted = readArray();
813
- // Read outpoint string:
988
+ // Read standard fields (order matches serialization V2)
989
+ const passwordSalt = readArray(); // 0
990
+ const passwordPresentationPrimary = readArray(); // 1
991
+ const passwordRecoveryPrimary = readArray(); // 2
992
+ const presentationRecoveryPrimary = readArray(); // 3
993
+ const passwordPrimaryPrivileged = readArray(); // 4
994
+ const presentationRecoveryPrivileged = readArray(); // 5
995
+ const presentationHash = readArray(); // 6
996
+ const recoveryHash = readArray(); // 7
997
+ const presentationKeyEncrypted = readArray(); // 8
998
+ const passwordKeyEncrypted = readArray(); // 9
999
+ const recoveryKeyEncrypted = readArray(); // 10
1000
+ // Read optional profiles (only in V2)
1001
+ let profilesEncrypted;
1002
+ if (version === 2) {
1003
+ const profilesFlag = reader.readUInt8();
1004
+ if (profilesFlag === 1) {
1005
+ profilesEncrypted = readArray();
1006
+ }
1007
+ }
1008
+ // Read outpoint string
814
1009
  const outpointLen = reader.readVarIntNum();
815
1010
  const outpointBytes = reader.read(outpointLen);
816
1011
  const currentOutpoint = sdk_1.Utils.toUTF8(outpointBytes);
817
1012
  const token = {
1013
+ passwordSalt,
818
1014
  passwordPresentationPrimary,
819
1015
  passwordRecoveryPrimary,
820
1016
  presentationRecoveryPrimary,
821
1017
  passwordPrimaryPrivileged,
822
1018
  presentationRecoveryPrivileged,
823
1019
  presentationHash,
824
- passwordSalt,
825
1020
  recoveryHash,
826
1021
  presentationKeyEncrypted,
1022
+ passwordKeyEncrypted, // Corrected order
827
1023
  recoveryKeyEncrypted,
828
- passwordKeyEncrypted,
1024
+ profilesEncrypted, // May be undefined
829
1025
  currentOutpoint
830
1026
  };
831
1027
  return token;
832
1028
  }
833
1029
  /**
834
- * Builds the underlying wallet once the user is authenticated.
1030
+ * Sets up the root key infrastructure after authentication or loading from snapshot.
1031
+ * Initializes the root primary key, root privileged key manager, loads profiles,
1032
+ * and sets the authenticated flag. Does NOT switch profile initially.
835
1033
  *
836
- * @param primaryKey The user's primary key (32 bytes).
837
- * @param privilegedKey Optionally, a privileged key (for short-term usage in account recovery).
1034
+ * @param rootPrimaryKey The user's root primary key (32 bytes).
1035
+ * @param ephemeralRootPrivilegedKey Optional root privileged key (e.g., during recovery flows).
838
1036
  */
839
- async buildUnderlying(primaryKey, privilegedKey) {
1037
+ async setupRootInfrastructure(rootPrimaryKey, ephemeralRootPrivilegedKey) {
840
1038
  if (!this.currentUMPToken) {
841
- throw new Error('A UMP token must exist before building underlying wallet!');
842
- }
843
- this.primaryKey = primaryKey;
844
- // Create a privileged manager that either uses the ephemeral privilegedKey if provided,
845
- // or derives it later from the user's password on demand.
846
- const privilegedManager = new PrivilegedKeyManager_1.PrivilegedKeyManager(async (reason) => {
847
- if (privilegedKey) {
848
- // For account recovery: a one-off opportunity to recover.
849
- const tempKey = new sdk_1.PrivateKey(privilegedKey);
850
- privilegedKey = undefined;
1039
+ throw new Error('A UMP token must exist before setting up root infrastructure!');
1040
+ }
1041
+ this.rootPrimaryKey = rootPrimaryKey;
1042
+ // Store ephemeral key if provided, for one-time use by the manager
1043
+ let oneTimePrivilegedKey = ephemeralRootPrivilegedKey
1044
+ ? new sdk_1.PrivateKey(ephemeralRootPrivilegedKey)
1045
+ : undefined;
1046
+ // Create the ROOT PrivilegedKeyManager
1047
+ this.rootPrivilegedKeyManager = new PrivilegedKeyManager_1.PrivilegedKeyManager(async (reason) => {
1048
+ // 1. Use one-time key if available (for recovery)
1049
+ if (oneTimePrivilegedKey) {
1050
+ const tempKey = oneTimePrivilegedKey;
1051
+ oneTimePrivilegedKey = undefined; // Consume it
851
1052
  return tempKey;
852
1053
  }
853
- // Otherwise, ask user for their password to decrypt the privileged key.
1054
+ // 2. Otherwise, derive from password
854
1055
  const password = await this.passwordRetriever(reason, (passwordCandidate) => {
855
1056
  try {
856
1057
  const derivedPasswordKey = sdk_1.Hash.pbkdf2(sdk_1.Utils.toArray(passwordCandidate, 'utf8'), this.currentUMPToken.passwordSalt, exports.PBKDF2_NUM_ROUNDS, 32, 'sha512');
857
- // Decrypt the privileged key with XOR(primaryKey, derivedPasswordKey).
858
- const privilegedDecryptor = this.XOR(this.primaryKey, derivedPasswordKey);
1058
+ const privilegedDecryptor = this.XOR(this.rootPrimaryKey, derivedPasswordKey);
859
1059
  const decryptedPrivileged = new sdk_1.SymmetricKey(privilegedDecryptor).decrypt(this.currentUMPToken.passwordPrimaryPrivileged);
860
- if (decryptedPrivileged) {
861
- return true;
862
- }
863
- return false;
1060
+ return !!decryptedPrivileged; // Test passes if decryption works
864
1061
  }
865
1062
  catch (e) {
866
1063
  return false;
867
1064
  }
868
1065
  });
1066
+ // Decrypt the root privileged key using the confirmed password
869
1067
  const derivedPasswordKey = sdk_1.Hash.pbkdf2(sdk_1.Utils.toArray(password, 'utf8'), this.currentUMPToken.passwordSalt, exports.PBKDF2_NUM_ROUNDS, 32, 'sha512');
870
- // Decrypt the privileged key with XOR(primaryKey, derivedPasswordKey).
871
- const privilegedDecryptor = this.XOR(this.primaryKey, derivedPasswordKey);
872
- const decryptedPrivileged = new sdk_1.SymmetricKey(privilegedDecryptor).decrypt(this.currentUMPToken.passwordPrimaryPrivileged);
873
- return new sdk_1.PrivateKey(decryptedPrivileged);
1068
+ const privilegedDecryptor = this.XOR(this.rootPrimaryKey, derivedPasswordKey);
1069
+ const rootPrivilegedBytes = new sdk_1.SymmetricKey(privilegedDecryptor).decrypt(this.currentUMPToken.passwordPrimaryPrivileged);
1070
+ return new sdk_1.PrivateKey(rootPrivilegedBytes); // Return the ROOT key object
874
1071
  });
875
- this.underlyingPrivilegedKeyManager = privilegedManager;
876
- // Build the underlying wallet with the primary key and privileged manager.
877
- this.underlying = await this.walletBuilder(primaryKey, privilegedManager);
1072
+ // Decrypt and load profiles if present in the token
1073
+ this.profiles = []; // Clear existing profiles before loading
1074
+ if (this.currentUMPToken.profilesEncrypted && this.currentUMPToken.profilesEncrypted.length > 0) {
1075
+ try {
1076
+ const decryptedProfileBytes = (await this.rootPrivilegedKeyManager.decrypt({
1077
+ ciphertext: this.currentUMPToken.profilesEncrypted,
1078
+ protocolID: [2, 'admin profile wrapping'], // Use profile protocol ID
1079
+ keyID: '1'
1080
+ })).plaintext;
1081
+ const profilesJson = sdk_1.Utils.toUTF8(decryptedProfileBytes);
1082
+ this.profiles = JSON.parse(profilesJson);
1083
+ }
1084
+ catch (error) {
1085
+ console.error('Failed to decrypt or parse profiles:', error);
1086
+ // Decide if this should be fatal or just log and continue without profiles
1087
+ this.profiles = []; // Ensure profiles are empty on error
1088
+ // Optionally re-throw or handle more gracefully
1089
+ throw new Error(`Failed to load profiles: ${error.message}`);
1090
+ }
1091
+ }
878
1092
  this.authenticated = true;
1093
+ // Note: We don't call switchProfile here anymore.
1094
+ // It's called by the auth methods (providePassword/provideRecoveryKey) or loadSnapshot after this.
879
1095
  }
880
1096
  /*
881
1097
  * ---------------------------------------------------------------------------------------
882
- * Below are the standard WalletInterface methods that simply proxy through to this.underlying,
883
- * ensuring that the user is authenticated and that the admin originator is not misused.
1098
+ * Standard WalletInterface methods proxying to the *active* underlying wallet.
1099
+ * Includes authentication checks and admin originator protection.
884
1100
  * ---------------------------------------------------------------------------------------
885
1101
  */
886
- async getPublicKey(args, originator) {
1102
+ checkAuthAndUnderlying(originator) {
887
1103
  if (!this.authenticated) {
888
1104
  throw new Error('User is not authenticated.');
889
1105
  }
1106
+ if (!this.underlying) {
1107
+ // This might happen if authentication succeeded but profile switching failed
1108
+ throw new Error('Underlying wallet for the active profile is not initialized.');
1109
+ }
890
1110
  if (originator === this.adminOriginator) {
891
1111
  throw new Error('External applications are not allowed to use the admin originator.');
892
1112
  }
1113
+ }
1114
+ // Example proxy method (repeat pattern for all others)
1115
+ async getPublicKey(args, originator) {
1116
+ this.checkAuthAndUnderlying(originator);
893
1117
  return this.underlying.getPublicKey(args, originator);
894
1118
  }
895
1119
  async revealCounterpartyKeyLinkage(args, originator) {
896
- if (!this.authenticated) {
897
- throw new Error('User is not authenticated.');
898
- }
899
- if (originator === this.adminOriginator) {
900
- throw new Error('External applications are not allowed to use the admin originator.');
901
- }
1120
+ this.checkAuthAndUnderlying(originator);
902
1121
  return this.underlying.revealCounterpartyKeyLinkage(args, originator);
903
1122
  }
904
1123
  async revealSpecificKeyLinkage(args, originator) {
905
- if (!this.authenticated) {
906
- throw new Error('User is not authenticated.');
907
- }
908
- if (originator === this.adminOriginator) {
909
- throw new Error('External applications are not allowed to use the admin originator.');
910
- }
1124
+ this.checkAuthAndUnderlying(originator);
911
1125
  return this.underlying.revealSpecificKeyLinkage(args, originator);
912
1126
  }
913
1127
  async encrypt(args, originator) {
914
- if (!this.authenticated) {
915
- throw new Error('User is not authenticated.');
916
- }
917
- if (originator === this.adminOriginator) {
918
- throw new Error('External applications are not allowed to use the admin originator.');
919
- }
1128
+ this.checkAuthAndUnderlying(originator);
920
1129
  return this.underlying.encrypt(args, originator);
921
1130
  }
922
1131
  async decrypt(args, originator) {
923
- if (!this.authenticated) {
924
- throw new Error('User is not authenticated.');
925
- }
926
- if (originator === this.adminOriginator) {
927
- throw new Error('External applications are not allowed to use the admin originator.');
928
- }
1132
+ this.checkAuthAndUnderlying(originator);
929
1133
  return this.underlying.decrypt(args, originator);
930
1134
  }
931
1135
  async createHmac(args, originator) {
932
- if (!this.authenticated) {
933
- throw new Error('User is not authenticated.');
934
- }
935
- if (originator === this.adminOriginator) {
936
- throw new Error('External applications are not allowed to use the admin originator.');
937
- }
1136
+ this.checkAuthAndUnderlying(originator);
938
1137
  return this.underlying.createHmac(args, originator);
939
1138
  }
940
1139
  async verifyHmac(args, originator) {
941
- if (!this.authenticated) {
942
- throw new Error('User is not authenticated.');
943
- }
944
- if (originator === this.adminOriginator) {
945
- throw new Error('External applications are not allowed to use the admin originator.');
946
- }
1140
+ this.checkAuthAndUnderlying(originator);
947
1141
  return this.underlying.verifyHmac(args, originator);
948
1142
  }
949
1143
  async createSignature(args, originator) {
950
- if (!this.authenticated) {
951
- throw new Error('User is not authenticated.');
952
- }
953
- if (originator === this.adminOriginator) {
954
- throw new Error('External applications are not allowed to use the admin originator.');
955
- }
1144
+ this.checkAuthAndUnderlying(originator);
956
1145
  return this.underlying.createSignature(args, originator);
957
1146
  }
958
1147
  async verifySignature(args, originator) {
959
- if (!this.authenticated) {
960
- throw new Error('User is not authenticated.');
961
- }
962
- if (originator === this.adminOriginator) {
963
- throw new Error('External applications are not allowed to use the admin originator.');
964
- }
1148
+ this.checkAuthAndUnderlying(originator);
965
1149
  return this.underlying.verifySignature(args, originator);
966
1150
  }
967
1151
  async createAction(args, originator) {
968
- if (!this.authenticated) {
969
- throw new Error('User is not authenticated.');
970
- }
971
- if (originator === this.adminOriginator) {
972
- throw new Error('External applications are not allowed to use the admin originator.');
973
- }
1152
+ this.checkAuthAndUnderlying(originator);
974
1153
  return this.underlying.createAction(args, originator);
975
1154
  }
976
1155
  async signAction(args, originator) {
977
- if (!this.authenticated) {
978
- throw new Error('User is not authenticated.');
979
- }
980
- if (originator === this.adminOriginator) {
981
- throw new Error('External applications are not allowed to use the admin originator.');
982
- }
1156
+ this.checkAuthAndUnderlying(originator);
983
1157
  return this.underlying.signAction(args, originator);
984
1158
  }
985
1159
  async abortAction(args, originator) {
986
- if (!this.authenticated) {
987
- throw new Error('User is not authenticated.');
988
- }
989
- if (originator === this.adminOriginator) {
990
- throw new Error('External applications are not allowed to use the admin originator.');
991
- }
1160
+ this.checkAuthAndUnderlying(originator);
992
1161
  return this.underlying.abortAction(args, originator);
993
1162
  }
994
1163
  async listActions(args, originator) {
995
- if (!this.authenticated) {
996
- throw new Error('User is not authenticated.');
997
- }
998
- if (originator === this.adminOriginator) {
999
- throw new Error('External applications are not allowed to use the admin originator.');
1000
- }
1164
+ this.checkAuthAndUnderlying(originator);
1001
1165
  return this.underlying.listActions(args, originator);
1002
1166
  }
1003
1167
  async internalizeAction(args, originator) {
1004
- if (!this.authenticated) {
1005
- throw new Error('User is not authenticated.');
1006
- }
1007
- if (originator === this.adminOriginator) {
1008
- throw new Error('External applications are not allowed to use the admin originator.');
1009
- }
1168
+ this.checkAuthAndUnderlying(originator);
1010
1169
  return this.underlying.internalizeAction(args, originator);
1011
1170
  }
1012
1171
  async listOutputs(args, originator) {
1013
- if (!this.authenticated) {
1014
- throw new Error('User is not authenticated.');
1015
- }
1016
- if (originator === this.adminOriginator) {
1017
- throw new Error('External applications are not allowed to use the admin originator.');
1018
- }
1172
+ this.checkAuthAndUnderlying(originator);
1019
1173
  return this.underlying.listOutputs(args, originator);
1020
1174
  }
1021
1175
  async relinquishOutput(args, originator) {
1022
- if (!this.authenticated) {
1023
- throw new Error('User is not authenticated.');
1024
- }
1025
- if (originator === this.adminOriginator) {
1026
- throw new Error('External applications are not allowed to use the admin originator.');
1027
- }
1176
+ this.checkAuthAndUnderlying(originator);
1028
1177
  return this.underlying.relinquishOutput(args, originator);
1029
1178
  }
1030
1179
  async acquireCertificate(args, originator) {
1031
- if (!this.authenticated) {
1032
- throw new Error('User is not authenticated.');
1033
- }
1034
- if (originator === this.adminOriginator) {
1035
- throw new Error('External applications are not allowed to use the admin originator.');
1036
- }
1180
+ this.checkAuthAndUnderlying(originator);
1037
1181
  return this.underlying.acquireCertificate(args, originator);
1038
1182
  }
1039
1183
  async listCertificates(args, originator) {
1040
- if (!this.authenticated) {
1041
- throw new Error('User is not authenticated.');
1042
- }
1043
- if (originator === this.adminOriginator) {
1044
- throw new Error('External applications are not allowed to use the admin originator.');
1045
- }
1184
+ this.checkAuthAndUnderlying(originator);
1046
1185
  return this.underlying.listCertificates(args, originator);
1047
1186
  }
1048
1187
  async proveCertificate(args, originator) {
1049
- if (!this.authenticated) {
1050
- throw new Error('User is not authenticated.');
1051
- }
1052
- if (originator === this.adminOriginator) {
1053
- throw new Error('External applications are not allowed to use the admin originator.');
1054
- }
1188
+ this.checkAuthAndUnderlying(originator);
1055
1189
  return this.underlying.proveCertificate(args, originator);
1056
1190
  }
1057
1191
  async relinquishCertificate(args, originator) {
1058
- if (!this.authenticated) {
1059
- throw new Error('User is not authenticated.');
1060
- }
1061
- if (originator === this.adminOriginator) {
1062
- throw new Error('External applications are not allowed to use the admin originator.');
1063
- }
1192
+ this.checkAuthAndUnderlying(originator);
1064
1193
  return this.underlying.relinquishCertificate(args, originator);
1065
1194
  }
1066
1195
  async discoverByIdentityKey(args, originator) {
1067
- if (!this.authenticated) {
1068
- throw new Error('User is not authenticated.');
1069
- }
1070
- if (originator === this.adminOriginator) {
1071
- throw new Error('External applications are not allowed to use the admin originator.');
1072
- }
1196
+ this.checkAuthAndUnderlying(originator);
1073
1197
  return this.underlying.discoverByIdentityKey(args, originator);
1074
1198
  }
1075
1199
  async discoverByAttributes(args, originator) {
1076
- if (!this.authenticated) {
1077
- throw new Error('User is not authenticated.');
1078
- }
1079
- if (originator === this.adminOriginator) {
1080
- throw new Error('External applications are not allowed to use the admin originator.');
1081
- }
1200
+ this.checkAuthAndUnderlying(originator);
1082
1201
  return this.underlying.discoverByAttributes(args, originator);
1083
1202
  }
1084
1203
  async isAuthenticated(_, originator) {
@@ -1094,45 +1213,25 @@ class CWIStyleWalletManager {
1094
1213
  if (originator === this.adminOriginator) {
1095
1214
  throw new Error('External applications are not allowed to use the admin originator.');
1096
1215
  }
1097
- while (!this.authenticated) {
1216
+ while (!this.authenticated || !this.underlying) {
1098
1217
  await new Promise(resolve => setTimeout(resolve, 100));
1099
1218
  }
1100
1219
  return { authenticated: true };
1101
1220
  }
1102
1221
  async getHeight(_, originator) {
1103
- if (!this.authenticated) {
1104
- throw new Error('User is not authenticated.');
1105
- }
1106
- if (originator === this.adminOriginator) {
1107
- throw new Error('External applications are not allowed to use the admin originator.');
1108
- }
1222
+ this.checkAuthAndUnderlying(originator);
1109
1223
  return this.underlying.getHeight({}, originator);
1110
1224
  }
1111
1225
  async getHeaderForHeight(args, originator) {
1112
- if (!this.authenticated) {
1113
- throw new Error('User is not authenticated.');
1114
- }
1115
- if (originator === this.adminOriginator) {
1116
- throw new Error('External applications are not allowed to use the admin originator.');
1117
- }
1226
+ this.checkAuthAndUnderlying(originator);
1118
1227
  return this.underlying.getHeaderForHeight(args, originator);
1119
1228
  }
1120
1229
  async getNetwork(_, originator) {
1121
- if (!this.authenticated) {
1122
- throw new Error('User is not authenticated.');
1123
- }
1124
- if (originator === this.adminOriginator) {
1125
- throw new Error('External applications are not allowed to use the admin originator.');
1126
- }
1230
+ this.checkAuthAndUnderlying(originator);
1127
1231
  return this.underlying.getNetwork({}, originator);
1128
1232
  }
1129
1233
  async getVersion(_, originator) {
1130
- if (!this.authenticated) {
1131
- throw new Error('User is not authenticated.');
1132
- }
1133
- if (originator === this.adminOriginator) {
1134
- throw new Error('External applications are not allowed to use the admin originator.');
1135
- }
1234
+ this.checkAuthAndUnderlying(originator);
1136
1235
  return this.underlying.getVersion({}, originator);
1137
1236
  }
1138
1237
  }