@metamask-previews/seedless-onboarding-controller 7.1.0-preview-2aeb1204 → 7.1.0-preview-40468f94

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/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+
12
+ - **BREAKING** The `encryptor` constructor param requires `encryptWithKey` method. ([#7800](https://github.com/MetaMask/core/pull/7800))
13
+ - The method is to encrypt the vault with cached encryption key while the wallet is unlocked.
14
+ - Added new public method, `getAccessToken`. ([#7800](https://github.com/MetaMask/core/pull/7800))
15
+ - Clients can use this method to get `accessToken` from the controller, instead of directly accessing from the state.
16
+ - This method also adds refresh token mechanism when `accessToken` is expired, hence preventing expired token usage in the clients.
17
+
10
18
  ### Changed
11
19
 
12
20
  - Update StateMetadata's `includeInStateLogs` property. ([#7750](https://github.com/MetaMask/core/pull/7750))
@@ -14,6 +22,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
14
22
  - Bump `@metamask/keyring-controller` from `^25.0.0` to `^25.1.0` ([#7713](https://github.com/MetaMask/core/pull/7713))
15
23
  - Upgrade `@metamask/utils` from `^11.8.1` to `^11.9.0` ([#7511](https://github.com/MetaMask/core/pull/7511))
16
24
 
25
+ ### Fixed
26
+
27
+ - Fixed new `accessToken` not being persisted in the vault after the token refresh. ([#7800](https://github.com/MetaMask/core/pull/7800))
28
+
17
29
  ## [7.1.0]
18
30
 
19
31
  ### Added
@@ -10,11 +10,12 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
10
10
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
11
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
12
  };
13
- var _SeedlessOnboardingController_instances, _SeedlessOnboardingController_vaultEncryptor, _SeedlessOnboardingController_controllerOperationMutex, _SeedlessOnboardingController_vaultOperationMutex, _SeedlessOnboardingController_refreshJWTToken, _SeedlessOnboardingController_revokeRefreshToken, _SeedlessOnboardingController_renewRefreshToken, _SeedlessOnboardingController_passwordOutdatedCacheTTL, _SeedlessOnboardingController_isUnlocked, _SeedlessOnboardingController_cachedDecryptedVaultData, _SeedlessOnboardingController_submitGlobalPassword, _SeedlessOnboardingController_setUnlocked, _SeedlessOnboardingController_persistOprfKey, _SeedlessOnboardingController_persistAuthPubKey, _SeedlessOnboardingController_storeKeyringEncryptionKey, _SeedlessOnboardingController_loadKeyringEncryptionKey, _SeedlessOnboardingController_loadSeedlessEncryptionKey, _SeedlessOnboardingController_recoverAuthPubKey, _SeedlessOnboardingController_recoverEncKey, _SeedlessOnboardingController_fetchAllSecretDataFromMetadataStore, _SeedlessOnboardingController_changeEncryptionKey, _SeedlessOnboardingController_encryptAndStoreSecretData, _SeedlessOnboardingController_unlockVaultAndGetVaultData, _SeedlessOnboardingController_decryptAndParseVaultData, _SeedlessOnboardingController_withPersistedSecretMetadataBackupsState, _SeedlessOnboardingController_filterDupesAndUpdateSocialBackupsMetadata, _SeedlessOnboardingController_createNewVaultWithAuthData, _SeedlessOnboardingController_updateVault, _SeedlessOnboardingController_getAccessTokenAndRevokeToken, _SeedlessOnboardingController_withControllerLock, _SeedlessOnboardingController_withVaultLock, _SeedlessOnboardingController_parseVaultData, _SeedlessOnboardingController_assertIsUnlocked, _SeedlessOnboardingController_assertIsAuthenticatedUser, _SeedlessOnboardingController_assertPasswordInSync, _SeedlessOnboardingController_resetPasswordOutdatedCache, _SeedlessOnboardingController_addRefreshTokenToRevokeList, _SeedlessOnboardingController_isAuthTokenError, _SeedlessOnboardingController_isMaxKeyChainLengthError, _SeedlessOnboardingController_executeWithTokenRefresh;
13
+ var _SeedlessOnboardingController_instances, _SeedlessOnboardingController_vaultEncryptor, _SeedlessOnboardingController_controllerOperationMutex, _SeedlessOnboardingController_vaultOperationMutex, _SeedlessOnboardingController_refreshJWTToken, _SeedlessOnboardingController_revokeRefreshToken, _SeedlessOnboardingController_renewRefreshToken, _SeedlessOnboardingController_passwordOutdatedCacheTTL, _SeedlessOnboardingController_isUnlocked, _SeedlessOnboardingController_cachedDecryptedVaultData, _SeedlessOnboardingController_submitGlobalPassword, _SeedlessOnboardingController_setUnlocked, _SeedlessOnboardingController_persistOprfKey, _SeedlessOnboardingController_persistAuthPubKey, _SeedlessOnboardingController_storeKeyringEncryptionKey, _SeedlessOnboardingController_loadKeyringEncryptionKey, _SeedlessOnboardingController_loadSeedlessEncryptionKey, _SeedlessOnboardingController_recoverAuthPubKey, _SeedlessOnboardingController_recoverEncKey, _SeedlessOnboardingController_fetchAllSecretDataFromMetadataStore, _SeedlessOnboardingController_changeEncryptionKey, _SeedlessOnboardingController_encryptAndStoreSecretData, _SeedlessOnboardingController_unlockVaultAndGetVaultData, _SeedlessOnboardingController_decryptAndParseVaultData, _SeedlessOnboardingController_withPersistedSecretMetadataBackupsState, _SeedlessOnboardingController_filterDupesAndUpdateSocialBackupsMetadata, _SeedlessOnboardingController_createNewVaultWithAuthData, _SeedlessOnboardingController_updateVault, _SeedlessOnboardingController_getAccessTokenAndRevokeToken, _SeedlessOnboardingController_withControllerLock, _SeedlessOnboardingController_withVaultLock, _SeedlessOnboardingController_parseVaultData, _SeedlessOnboardingController_assertIsUnlocked, _SeedlessOnboardingController_assertIsAuthenticatedUser, _SeedlessOnboardingController_assertPasswordInSync, _SeedlessOnboardingController_resetPasswordOutdatedCache, _SeedlessOnboardingController_addRefreshTokenToRevokeList, _SeedlessOnboardingController_isAuthTokenError, _SeedlessOnboardingController_isMaxKeyChainLengthError, _SeedlessOnboardingController_executeWithTokenRefresh, _SeedlessOnboardingController_updateVaultAfterAuthTokenRefresh, _SeedlessOnboardingController_checkTokensExpired;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.SeedlessOnboardingController = exports.getInitialSeedlessOnboardingControllerStateWithDefaults = void 0;
16
16
  const auth_network_utils_1 = require("@metamask/auth-network-utils");
17
17
  const base_controller_1 = require("@metamask/base-controller");
18
+ const messenger_1 = require("@metamask/messenger");
18
19
  const toprf_secure_backup_1 = require("@metamask/toprf-secure-backup");
19
20
  const utils_1 = require("@metamask/utils");
20
21
  const aes_1 = require("@noble/ciphers/aes");
@@ -242,6 +243,7 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
242
243
  __classPrivateFieldSet(this, _SeedlessOnboardingController_refreshJWTToken, refreshJWTToken, "f");
243
244
  __classPrivateFieldSet(this, _SeedlessOnboardingController_revokeRefreshToken, revokeRefreshToken, "f");
244
245
  __classPrivateFieldSet(this, _SeedlessOnboardingController_renewRefreshToken, renewRefreshToken, "f");
246
+ this.messenger.registerActionHandler('SeedlessOnboardingController:getAccessToken', this.getAccessToken.bind(this));
245
247
  }
246
248
  async fetchMetadataAccessCreds() {
247
249
  const { metadataAccessToken } = this.state;
@@ -552,7 +554,7 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
552
554
  /**
553
555
  * Submit the password to the controller, verify the password validity and unlock the controller.
554
556
  *
555
- * This method will be used especially when user rehydrate/unlock the wallet.
557
+ * This method will be used especially when user unlock the wallet.
556
558
  * The provided password will be verified against the encrypted vault, encryption key will be derived and saved in the controller state.
557
559
  *
558
560
  * This operation is useful when user performs some actions that requires the user password/encryption key. e.g. add new srp backup
@@ -562,7 +564,37 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
562
564
  */
563
565
  async submitPassword(password) {
564
566
  return await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_withControllerLock).call(this, async () => {
565
- await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_unlockVaultAndGetVaultData).call(this, { password });
567
+ // get the access token from the state before unlocking, it might be the new token set from the `refreshAuthTokens` method.
568
+ const { accessToken: accessTokenBeforeUnlock } = this.state;
569
+ const deserializedVaultData = await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_unlockVaultAndGetVaultData).call(this, {
570
+ password,
571
+ });
572
+ const { accessToken: accessTokenAfterUnlock } = this.state;
573
+ let latestAccessToken = accessTokenAfterUnlock;
574
+ // compare the access token from state before unlocking and after unlocking, take the latest access token if it's different.
575
+ if (accessTokenBeforeUnlock &&
576
+ accessTokenAfterUnlock &&
577
+ accessTokenBeforeUnlock !== accessTokenAfterUnlock) {
578
+ // If there was an access token in the state before unlocking, it might be the token set from the `refreshAuthTokens` method.
579
+ // Compare it with the access token value from decrypted vault after unlocking and take the latest access token.
580
+ latestAccessToken = (0, utils_3.compareAndGetLatestToken)(accessTokenBeforeUnlock, accessTokenAfterUnlock);
581
+ }
582
+ // update the state and vault with the latest access token if it's different from the current access token in the state.
583
+ if (latestAccessToken && latestAccessToken !== accessTokenAfterUnlock) {
584
+ // update the access token in the state with the latest access token if it's different from the decrypted access token after unlocking
585
+ this.update((state) => {
586
+ state.accessToken = latestAccessToken;
587
+ });
588
+ const updatedVaultData = {
589
+ ...deserializedVaultData,
590
+ accessToken: latestAccessToken,
591
+ };
592
+ await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_updateVault).call(this, {
593
+ password,
594
+ vaultData: updatedVaultData,
595
+ pwEncKey: deserializedVaultData.toprfPwEncryptionKey,
596
+ });
597
+ }
566
598
  __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_setUnlocked).call(this);
567
599
  });
568
600
  }
@@ -768,6 +800,8 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
768
800
  refreshToken,
769
801
  skipLock: true,
770
802
  });
803
+ // update the vault with new access token
804
+ await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_updateVaultAfterAuthTokenRefresh).call(this, accessToken);
771
805
  }
772
806
  catch (error) {
773
807
  log('Error refreshing node auth tokens', error);
@@ -858,6 +892,21 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
858
892
  }
859
893
  });
860
894
  }
895
+ /**
896
+ * Get the access token from the state.
897
+ *
898
+ * If the tokens are expired, the method will refresh them and return the new access token.
899
+ *
900
+ * @returns The access token.
901
+ */
902
+ async getAccessToken() {
903
+ return __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_withControllerLock).call(this, async () => {
904
+ __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertIsAuthenticatedUser).call(this, this.state);
905
+ return __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_executeWithTokenRefresh).call(this, async () => {
906
+ return this.state.accessToken;
907
+ }, 'getAccessToken');
908
+ });
909
+ }
861
910
  /**
862
911
  * Check if the current node auth token is expired.
863
912
  *
@@ -926,22 +975,39 @@ _SeedlessOnboardingController_vaultEncryptor = new WeakMap(), _SeedlessOnboardin
926
975
  * corresponding to the current authPubKey in state.
927
976
  */
928
977
  async function _SeedlessOnboardingController_submitGlobalPassword({ targetAuthPubKey, globalPassword, maxKeyChainLength, }) {
929
- const { pwEncKey: curPwEncKey, authKeyPair: curAuthKeyPair } = await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_recoverEncKey).call(this, globalPassword);
978
+ const { pwEncKey: globalPwEncKey, authKeyPair: globalAuthKeyPair } = await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_recoverEncKey).call(this, globalPassword);
930
979
  try {
931
980
  // Recover vault encryption key.
932
981
  const res = await this.toprfClient.recoverPwEncKey({
933
982
  targetAuthPubKey,
934
- curPwEncKey,
935
- curAuthKeyPair,
983
+ curPwEncKey: globalPwEncKey,
984
+ curAuthKeyPair: globalAuthKeyPair,
936
985
  maxPwChainLength: maxKeyChainLength,
937
986
  });
938
987
  const { pwEncKey } = res;
939
988
  const vaultKey = await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_loadSeedlessEncryptionKey).call(this, pwEncKey);
989
+ // accessToken before unlocking vault and flooding the state with values from the decrypted vault
990
+ // it might be the new token set from the `refreshAuthTokens` method.
991
+ const { accessToken: accessTokenBeforeUnlock } = this.state;
940
992
  // Unlock the controller
941
- await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_unlockVaultAndGetVaultData).call(this, {
993
+ const decryptedVaultData = await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_unlockVaultAndGetVaultData).call(this, {
942
994
  encryptionKey: vaultKey,
943
995
  });
944
996
  __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_setUnlocked).call(this);
997
+ // accessToken from decrypted vault
998
+ const accessTokenFromDecryptedVault = decryptedVaultData.accessToken;
999
+ // compare the two access tokens, take the latest access token if it's different.
1000
+ if (accessTokenBeforeUnlock &&
1001
+ accessTokenFromDecryptedVault &&
1002
+ accessTokenBeforeUnlock !== accessTokenFromDecryptedVault) {
1003
+ const latestAccessToken = (0, utils_3.compareAndGetLatestToken)(accessTokenBeforeUnlock, accessTokenFromDecryptedVault);
1004
+ // update the access token in the state with the latest access token if it's different from the decrypted access token after unlocking
1005
+ // later when we call `syncLatestGlobalPassword`, the encrypted vault will be updated with the latest access token.
1006
+ this.update((state) => {
1007
+ state.accessToken = latestAccessToken;
1008
+ });
1009
+ }
1010
+ __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_resetPasswordOutdatedCache).call(this);
945
1011
  }
946
1012
  catch (error) {
947
1013
  if (__classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_isAuthTokenError).call(this, error)) {
@@ -1371,14 +1437,14 @@ async function _SeedlessOnboardingController_updateVault({ password, vaultData,
1371
1437
  // from the password using an intentionally slow key derivation function.
1372
1438
  // We should make sure that we only call it very intentionally.
1373
1439
  const { vault, exportedKeyString } = await __classPrivateFieldGet(this, _SeedlessOnboardingController_vaultEncryptor, "f").encryptWithDetail(password, serializedVaultData);
1374
- // Encrypt vault key.
1375
1440
  const aes = (0, webcrypto_1.managedNonce)(aes_1.gcm)(pwEncKey);
1376
1441
  const encryptedKey = aes.encrypt((0, utils_2.utf8ToBytes)(exportedKeyString));
1442
+ const encryptedSeedlessEncryptionKey = (0, utils_1.bytesToBase64)(encryptedKey);
1377
1443
  this.update((state) => {
1378
1444
  state.vault = vault;
1379
1445
  state.vaultEncryptionKey = exportedKeyString;
1380
1446
  state.vaultEncryptionSalt = JSON.parse(vault).salt;
1381
- state.encryptedSeedlessEncryptionKey = (0, utils_1.bytesToBase64)(encryptedKey);
1447
+ state.encryptedSeedlessEncryptionKey = encryptedSeedlessEncryptionKey;
1382
1448
  });
1383
1449
  });
1384
1450
  }, _SeedlessOnboardingController_getAccessTokenAndRevokeToken =
@@ -1537,18 +1603,7 @@ async function _SeedlessOnboardingController_assertPasswordInSync(options) {
1537
1603
  */
1538
1604
  async function _SeedlessOnboardingController_executeWithTokenRefresh(operation, operationName) {
1539
1605
  try {
1540
- // proactively check for expired tokens and refresh them if needed
1541
- const isNodeAuthTokenExpired = this.checkNodeAuthTokenExpired();
1542
- const isMetadataAccessTokenExpired = this.checkMetadataAccessTokenExpired();
1543
- // access token is only accessible when the vault is unlocked
1544
- // so skip the check if the vault is locked
1545
- let isAccessTokenExpired = false;
1546
- if (__classPrivateFieldGet(this, _SeedlessOnboardingController_isUnlocked, "f")) {
1547
- isAccessTokenExpired = this.checkAccessTokenExpired();
1548
- }
1549
- if (isNodeAuthTokenExpired ||
1550
- isMetadataAccessTokenExpired ||
1551
- isAccessTokenExpired) {
1606
+ if (__classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_checkTokensExpired).call(this)) {
1552
1607
  log(`JWT token expired during ${operationName}, attempting to refresh tokens`, 'node auth token exp check');
1553
1608
  await this.refreshAuthTokens();
1554
1609
  }
@@ -1574,6 +1629,58 @@ async function _SeedlessOnboardingController_executeWithTokenRefresh(operation,
1574
1629
  throw error;
1575
1630
  }
1576
1631
  }
1632
+ }, _SeedlessOnboardingController_updateVaultAfterAuthTokenRefresh = async function _SeedlessOnboardingController_updateVaultAfterAuthTokenRefresh(accessToken) {
1633
+ await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_withVaultLock).call(this, async () => {
1634
+ if (!__classPrivateFieldGet(this, _SeedlessOnboardingController_isUnlocked, "f")) {
1635
+ // we just temporarily store the access token in the state
1636
+ // when user attempts to unlock the vault, we will use this access token to update the vault
1637
+ this.update((state) => {
1638
+ state.accessToken = accessToken;
1639
+ });
1640
+ return;
1641
+ }
1642
+ const { vaultEncryptionKey, vaultEncryptionSalt } = this.state;
1643
+ if (!vaultEncryptionKey ||
1644
+ !vaultEncryptionSalt ||
1645
+ !__classPrivateFieldGet(this, _SeedlessOnboardingController_cachedDecryptedVaultData, "f")) {
1646
+ throw new Error(constants_1.SeedlessOnboardingControllerErrorMessage.MissingCredentials);
1647
+ }
1648
+ const serializedVaultData = (0, utils_3.serializeVaultData)({
1649
+ ...__classPrivateFieldGet(this, _SeedlessOnboardingController_cachedDecryptedVaultData, "f"),
1650
+ accessToken,
1651
+ });
1652
+ const encryptionKey = await __classPrivateFieldGet(this, _SeedlessOnboardingController_vaultEncryptor, "f").importKey(vaultEncryptionKey);
1653
+ const updatedEncVault = await __classPrivateFieldGet(this, _SeedlessOnboardingController_vaultEncryptor, "f").encryptWithKey(encryptionKey, serializedVaultData);
1654
+ // NOTE: Referenced from keyring-controller!
1655
+ // We need to include the salt used to derive
1656
+ // the encryption key, to be able to derive it
1657
+ // from password again.
1658
+ updatedEncVault.salt = vaultEncryptionSalt;
1659
+ this.update((state) => {
1660
+ state.vault = JSON.stringify(updatedEncVault);
1661
+ state.vaultEncryptionSalt = vaultEncryptionSalt;
1662
+ state.accessToken = accessToken;
1663
+ state.vaultEncryptionKey = vaultEncryptionKey;
1664
+ });
1665
+ // update the cached decrypted vault data with the new access token
1666
+ __classPrivateFieldSet(this, _SeedlessOnboardingController_cachedDecryptedVaultData, {
1667
+ ...__classPrivateFieldGet(this, _SeedlessOnboardingController_cachedDecryptedVaultData, "f"),
1668
+ accessToken,
1669
+ }, "f");
1670
+ });
1671
+ }, _SeedlessOnboardingController_checkTokensExpired = function _SeedlessOnboardingController_checkTokensExpired() {
1672
+ // proactively check for expired tokens and refresh them if needed
1673
+ const isNodeAuthTokenExpired = this.checkNodeAuthTokenExpired();
1674
+ const isMetadataAccessTokenExpired = this.checkMetadataAccessTokenExpired();
1675
+ // access token is only accessible when the vault is unlocked
1676
+ // so skip the check if the vault is locked
1677
+ let isAccessTokenExpired = false;
1678
+ if (__classPrivateFieldGet(this, _SeedlessOnboardingController_isUnlocked, "f")) {
1679
+ isAccessTokenExpired = this.checkAccessTokenExpired();
1680
+ }
1681
+ return (isNodeAuthTokenExpired ||
1682
+ isMetadataAccessTokenExpired ||
1683
+ isAccessTokenExpired);
1577
1684
  };
1578
1685
  /**
1579
1686
  * Assert that the provided password is a valid non-empty string.