diff --git a/app/core/Encryptor.js b/app/core/Encryptor.js deleted file mode 100644 index 1eda492b402..00000000000 --- a/app/core/Encryptor.js +++ /dev/null @@ -1,77 +0,0 @@ -import { NativeModules } from 'react-native'; -const Aes = NativeModules.Aes; -const AesForked = NativeModules.AesForked; - -/** - * Class that exposes two public methods: Encrypt and Decrypt - * This is used by the KeyringController to encrypt / decrypt the state - * which contains sensitive seed words and addresses - */ -export default class Encryptor { - key = null; - - _generateSalt(byteCount = 32) { - const view = new Uint8Array(byteCount); - global.crypto.getRandomValues(view); - // eslint-disable-next-line no-undef - const b64encoded = btoa(String.fromCharCode.apply(null, view)); - return b64encoded; - } - - _generateKey = (password, salt, lib) => - lib === 'original' - ? Aes.pbkdf2(password, salt, 5000, 256) - : AesForked.pbkdf2(password, salt); - - _keyFromPassword = (password, salt, lib) => - this._generateKey(password, salt, lib); - - _encryptWithKey = async (text, keyBase64) => { - const iv = await Aes.randomKey(16); - return Aes.encrypt(text, keyBase64, iv).then((cipher) => ({ cipher, iv })); - }; - - _decryptWithKey = (encryptedData, key, lib) => - lib === 'original' - ? Aes.decrypt(encryptedData.cipher, key, encryptedData.iv) - : AesForked.decrypt(encryptedData.cipher, key, encryptedData.iv); - - /** - * Encrypts a JS object using a password (and AES encryption with native libraries) - * - * @param {string} password - Password used for encryption - * @param {object} object - Data object to encrypt - * @returns - Promise resolving to stringified data - */ - encrypt = async (password, object) => { - const salt = this._generateSalt(16); - const key = await this._keyFromPassword(password, salt, 'original'); - const result = await this._encryptWithKey(JSON.stringify(object), key); - result.salt = salt; - result.lib = 'original'; - return JSON.stringify(result); - }; - - /** - * Decrypts an encrypted JS object (encryptedString) - * using a password (and AES decryption with native libraries) - * - * @param {string} password - Password used for decryption - * @param {string} encryptedString - String to decrypt - * @returns - Promise resolving to decrypted data object - */ - decrypt = async (password, encryptedString) => { - const encryptedData = JSON.parse(encryptedString); - const key = await this._keyFromPassword( - password, - encryptedData.salt, - encryptedData.lib, - ); - const data = await this._decryptWithKey( - encryptedData, - key, - encryptedData.lib, - ); - return JSON.parse(data); - }; -} diff --git a/app/core/Encryptor/Encryptor.test.ts b/app/core/Encryptor/Encryptor.test.ts new file mode 100644 index 00000000000..c66a2222e4a --- /dev/null +++ b/app/core/Encryptor/Encryptor.test.ts @@ -0,0 +1,243 @@ +import { NativeModules } from 'react-native'; +import { Encryptor } from './Encryptor'; +import { + ENCRYPTION_LIBRARY, + DERIVATION_PARAMS, + KeyDerivationIteration, +} from './constants'; + +const Aes = NativeModules.Aes; +const AesForked = NativeModules.AesForked; + +describe('Encryptor', () => { + let encryptor: Encryptor; + + beforeEach(() => { + encryptor = new Encryptor({ derivationParams: DERIVATION_PARAMS }); + }); + + describe('constructor', () => { + it('throws an error if the provided iterations do not meet the minimum required', () => { + expect( + () => + new Encryptor({ + derivationParams: { + algorithm: 'PBKDF2', + params: { + iterations: 100, + }, + }, + }), + ).toThrowError( + `Invalid key derivation iterations: 100. Recommended number of iterations is ${KeyDerivationIteration.Default}. Minimum required is ${KeyDerivationIteration.Minimum}.`, + ); + }); + }); + + describe('encrypt', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should encrypt an object correctly', async () => { + const password = 'testPassword'; + const objectToEncrypt = { key: 'value' }; + + const encryptedString = await encryptor.encrypt( + password, + objectToEncrypt, + ); + const encryptedObject = JSON.parse(encryptedString); + + expect(encryptedObject).toHaveProperty('cipher'); + expect(encryptedObject).toHaveProperty('iv'); + expect(encryptedObject).toHaveProperty('salt'); + expect(encryptedObject).toHaveProperty('lib', 'original'); + }); + }); + + describe('decrypt', () => { + let decryptAesSpy: jest.SpyInstance, + pbkdf2AesSpy: jest.SpyInstance, + decryptAesForkedSpy: jest.SpyInstance, + pbkdf2AesForkedSpy: jest.SpyInstance; + + beforeEach(() => { + decryptAesSpy = jest + .spyOn(Aes, 'decrypt') + .mockResolvedValue('{"mockData": "mockedPlainText"}'); + pbkdf2AesSpy = jest + .spyOn(Aes, 'pbkdf2') + .mockResolvedValue('mockedAesKey'); + decryptAesForkedSpy = jest + .spyOn(AesForked, 'decrypt') + .mockResolvedValue('{"mockData": "mockedPlainText"}'); + pbkdf2AesForkedSpy = jest + .spyOn(AesForked, 'pbkdf2') + .mockResolvedValue('mockedAesForkedKey'); + }); + + afterEach(() => { + decryptAesSpy.mockRestore(); + pbkdf2AesSpy.mockRestore(); + decryptAesForkedSpy.mockRestore(); + pbkdf2AesForkedSpy.mockRestore(); + }); + + it.each([ + { + lib: ENCRYPTION_LIBRARY.original, + expectedKey: 'mockedAesKey', + expectedPBKDF2Args: ['testPassword', 'mockedSalt', 600000, 256], + description: + 'with original library and default iterations number for key generation', + keyMetadata: DERIVATION_PARAMS, + }, + { + lib: ENCRYPTION_LIBRARY.original, + expectedKey: 'mockedAesKey', + expectedPBKDF2Args: ['testPassword', 'mockedSalt', 5000, 256], + description: + 'with original library and old iterations number for key generation', + }, + { + lib: 'random-lib', // Assuming not using "original" should lead to AesForked + expectedKey: 'mockedAesForkedKey', + expectedPBKDF2Args: ['testPassword', 'mockedSalt'], + description: + 'with library different to "original" and default iterations number for key generation', + keyMetadata: DERIVATION_PARAMS, + }, + { + lib: 'random-lib', // Assuming not using "original" should lead to AesForked + expectedKey: 'mockedAesForkedKey', + expectedPBKDF2Args: ['testPassword', 'mockedSalt'], + description: + 'with library different to "original" and old iterations number for key generation', + }, + ])( + 'decrypts a string correctly $description', + async ({ lib, expectedKey, expectedPBKDF2Args, keyMetadata }) => { + const password = 'testPassword'; + const mockVault = { + cipher: 'mockedCipher', + iv: 'mockedIV', + salt: 'mockedSalt', + lib, + }; + + const decryptedObject = await encryptor.decrypt( + password, + JSON.stringify( + keyMetadata !== undefined + ? { ...mockVault, keyMetadata } + : mockVault, + ), + ); + + expect(decryptedObject).toEqual(expect.any(Object)); + expect( + lib === ENCRYPTION_LIBRARY.original + ? decryptAesSpy + : decryptAesForkedSpy, + ).toHaveBeenCalledWith(mockVault.cipher, expectedKey, mockVault.iv); + expect( + lib === ENCRYPTION_LIBRARY.original + ? pbkdf2AesSpy + : pbkdf2AesForkedSpy, + ).toHaveBeenCalledWith(...expectedPBKDF2Args); + }, + ); + }); + + describe('isVaultUpdated', () => { + it('returns true if a vault has the correct format', () => { + expect( + encryptor.isVaultUpdated( + JSON.stringify({ + cipher: 'mockedCipher', + iv: 'mockedIV', + salt: 'mockedSalt', + lib: 'original', + keyMetadata: DERIVATION_PARAMS, + }), + ), + ).toBe(true); + }); + + it('returns false if a vault has the incorrect format', () => { + expect( + encryptor.isVaultUpdated( + JSON.stringify({ + cipher: 'mockedCipher', + iv: 'mockedIV', + salt: 'mockedSalt', + lib: 'original', + }), + ), + ).toBe(false); + }); + }); + + describe('updateVault', () => { + let encryptSpy: jest.SpyInstance, decryptSpy: jest.SpyInstance; + const expectedKeyMetadata = DERIVATION_PARAMS; + + beforeEach(() => { + encryptSpy = jest + .spyOn(Aes, 'encrypt') + .mockResolvedValue(() => Promise.resolve('mockedCipher')); + decryptSpy = jest + .spyOn(Aes, 'decrypt') + .mockResolvedValue('{"mockData": "mockedPlainText"}'); + }); + + afterEach(() => { + encryptSpy.mockRestore(); + decryptSpy.mockRestore(); + }); + + it('updates a vault correctly if keyMetadata is not present', async () => { + const mockVault = { + cipher: 'mockedCipher', + iv: 'mockedIV', + salt: 'mockedSalt', + lib: 'original', + }; + + const updatedVault = await encryptor.updateVault( + JSON.stringify(mockVault), + 'mockPassword', + ); + + const vault = JSON.parse(updatedVault); + + expect(encryptSpy).toBeCalledTimes(1); + expect(decryptSpy).toBeCalledTimes(1); + expect(vault).toHaveProperty('keyMetadata'); + expect(vault.keyMetadata).toStrictEqual(expectedKeyMetadata); + }); + + it('does not update a vault if algorithm is PBKDF2 and the number of iterations is 900000', async () => { + const mockVault = { + cipher: 'mockedCipher', + iv: 'mockedIV', + salt: 'mockedSalt', + lib: 'original', + keyMetadata: DERIVATION_PARAMS, + }; + + const updatedVault = await encryptor.updateVault( + JSON.stringify(mockVault), + 'mockPassword', + ); + + const vault = JSON.parse(updatedVault); + + expect(encryptSpy).toBeCalledTimes(0); + expect(decryptSpy).toBeCalledTimes(0); + expect(vault).toHaveProperty('keyMetadata'); + expect(vault.keyMetadata).toStrictEqual(expectedKeyMetadata); + }); + }); +}); diff --git a/app/core/Encryptor/Encryptor.ts b/app/core/Encryptor/Encryptor.ts new file mode 100644 index 00000000000..575c424da14 --- /dev/null +++ b/app/core/Encryptor/Encryptor.ts @@ -0,0 +1,268 @@ +import { NativeModules } from 'react-native'; +import { hasProperty, isPlainObject, Json } from '@metamask/utils'; +import { + SALT_BYTES_COUNT, + SHA256_DIGEST_LENGTH, + ENCRYPTION_LIBRARY, + KeyDerivationIteration, +} from './constants'; +import type { + EncryptionResult, + KeyDerivationOptions, + GenericEncryptor, +} from './types'; + +const Aes = NativeModules.Aes; +const AesForked = NativeModules.AesForked; + +/** + * Checks if the provided object is a `KeyDerivationOptions`. + * + * @param derivationOptions - The object to check. + * @returns Whether or not the object is a `KeyDerivationOptions`. + */ +const isKeyDerivationOptions = ( + derivationOptions: unknown, +): derivationOptions is KeyDerivationOptions => + isPlainObject(derivationOptions) && + hasProperty(derivationOptions, 'algorithm') && + hasProperty(derivationOptions, 'params'); + +/** + * The Encryptor class provides methods for encrypting and + * decrypting data objects using AES encryption with native libraries. + * It supports generating a salt, deriving an encryption key from a + * password and salt, and performing the encryption and decryption processes. + */ +class Encryptor implements GenericEncryptor { + /** + * The key derivation parameters used for encryption and decryption operations. + * These parameters include the algorithm and its specific parameters, for example, number of iterations for key derivation. + * They are set during the construction of the Encryptor instance and used for generating encryption keys. + * @property derivationParams - The key derivation options. + */ + private derivationParams: KeyDerivationOptions; + + /** + * Constructs an instance of the Encryptor class. + * @param params - An object containing key derivation parameters. + * @param params.derivationParams - The key derivation options to use for encryption and decryption operations. + * @throws Error if the provided iterations in `derivationParams` do not meet the minimum required. + */ + constructor({ + derivationParams, + }: { + derivationParams: KeyDerivationOptions; + }) { + this.checkMinimalRequiredIterations(derivationParams.params.iterations); + this.derivationParams = derivationParams; + } + + /** + * Throws an error if the provided number of iterations does not meet the minimum required for key derivation. + * This method ensures that the key derivation process is secure by enforcing a minimum number of iterations. + * @param iterations - The number of iterations to check. + * @throws Error if the number of iterations is less than the minimum required. + */ + private checkMinimalRequiredIterations = (iterations: number): void => { + if (!this.isMinimalRequiredIterationsMet(iterations)) { + throw new Error( + `Invalid key derivation iterations: ${iterations}. Recommended number of iterations is ${KeyDerivationIteration.Default}. Minimum required is ${KeyDerivationIteration.Minimum}.`, + ); + } + }; + + /** + * Checks if the provided number of iterations meets the minimum required for key derivation. + * @param iterations - The number of iterations to check. + * @returns A boolean indicating whether the minimum required iterations are met. + */ + private isMinimalRequiredIterationsMet = (iterations: number): boolean => + iterations >= KeyDerivationIteration.Minimum; + + /** + * Generates a random base64-encoded salt string. + * @param byteCount - The number of bytes for the salt. Defaults to `constant.SALT_BYTES_COUNT`. + * @returns The base64-encoded salt string. + */ + private generateSalt = (saltBytesCount = SALT_BYTES_COUNT) => { + const salt = new Uint8Array(saltBytesCount); + // @ts-expect-error - globalThis is not recognized by TypeScript + global.crypto.getRandomValues(salt); + return salt; + }; + + /** + * Encodes a byte array to a base64 string. + * @param byteArray The byte array to encode. + * @returns The base64-encoded string. + */ + private encodeByteArrayToBase64 = (byteArray: Uint8Array): string => + btoa(String.fromCharCode.apply(null, Array.from(byteArray))); + + /** + * Wrapper method for key generation from a password. + * @param params.password - The password used for key derivation. + * @param params.salt - The salt used for key derivation. + * @param params.lib - The library to use ('original' or forked version). + * @returns A promise that resolves to the derived encryption key. + */ + private generateKeyFromPassword = ({ + password, + salt, + iterations, + lib, + }: { + password: string; + salt: string; + iterations: number; + lib: string; + }): Promise => + lib === ENCRYPTION_LIBRARY.original + ? Aes.pbkdf2(password, salt, iterations, SHA256_DIGEST_LENGTH) + : AesForked.pbkdf2(password, salt); + + /** + * Encrypts a text string using the provided key. + * @param params.text - The text to encrypt. + * @param params.keyBase64 - The base64-encoded encryption key. + * @returns A promise that resolves to an object containing the cipher text and initialization vector (IV). + */ + private encryptWithKey = async ({ + text, + keyBase64, + }: { + text: string; + keyBase64: string; + }): Promise => { + const iv = await Aes.randomKey(16); + return Aes.encrypt(text, keyBase64, iv).then((cipher: string) => ({ + cipher, + iv, + })); + }; + + /** + * Decrypts encrypted data using the provided key. + * @param params.encryptedData - The encrypted data object containing the cipher text and IV. + * @param params.key - The decryption key. + * @param params.lib - The library to use ('original' or forked version) for decryption. + * @returns A promise that resolves to the decrypted text. + */ + private decryptWithKey = ({ + encryptedData, + key, + lib, + }: { + encryptedData: { cipher: string; iv: string }; + key: string; + lib: string; + }): Promise => + lib === ENCRYPTION_LIBRARY.original + ? Aes.decrypt(encryptedData.cipher, key, encryptedData.iv) + : AesForked.decrypt(encryptedData.cipher, key, encryptedData.iv); + + /** + * Asynchronously encrypts a given object using AES encryption. + * The encryption process involves generating a salt, deriving a key from the provided password and salt, + * and then using the key to encrypt the object. The result includes the encrypted data, the salt used, + * and the library version ('original' in this case). + * + * @param params.password - The password used for generating the encryption key. + * @param params.object - The data object to encrypt. It can be of any type, as it will be stringified during the encryption process. + * @returns A promise that resolves to a string. The string is a JSON representation of an object containing the encrypted data, the salt used for encryption, and the library version. + */ + encrypt = async (password: string, object: Json): Promise => { + const salt = this.generateSalt(16); + const base64salt = this.encodeByteArrayToBase64(salt); + const key = await this.generateKeyFromPassword({ + password, + salt: base64salt, + iterations: this.derivationParams.params.iterations, + lib: ENCRYPTION_LIBRARY.original, + }); + const result = await this.encryptWithKey({ + text: JSON.stringify(object), + keyBase64: key, + }); + result.salt = base64salt; + result.lib = ENCRYPTION_LIBRARY.original; + result.keyMetadata = this.derivationParams; + return JSON.stringify(result); + }; + + /** + * Decrypts an encrypted JS object (encryptedString) + * using a password (and AES decryption with native libraries) + * + * @param password - Password used for decryption + * @param encryptedString - String to decrypt + * @returns - Promise resolving to decrypted data object + */ + decrypt = async ( + password: string, + encryptedString: string, + ): Promise => { + const payload = JSON.parse(encryptedString); + const key = await this.generateKeyFromPassword({ + password, + salt: payload.salt, + iterations: + payload.keyMetadata?.params.iterations || KeyDerivationIteration.Legacy, + lib: payload.lib, + }); + const data = await this.decryptWithKey({ + encryptedData: payload, + key, + lib: payload.lib, + }); + + return JSON.parse(data); + }; + + /** + * Checks if the provided vault is an updated encryption format. + * + * @param vault - The vault to check. + * @param targetDerivationParams - The options to use for key derivation. + * @returns Whether or not the vault is an updated encryption format. + */ + isVaultUpdated = ( + vault: string, + targetDerivationParams = this.derivationParams, + ): boolean => { + const { keyMetadata } = JSON.parse(vault); + return ( + isKeyDerivationOptions(keyMetadata) && + keyMetadata.algorithm === targetDerivationParams.algorithm && + keyMetadata.params.iterations === targetDerivationParams.params.iterations + ); + }; + + /** + * Updates the provided vault, re-encrypting + * data with a safer algorithm if one is available. + * + * If the provided vault is already using the latest available encryption method, + * it is returned as is. + * + * @param vault - The vault to update. + * @param password - The password to use for encryption. + * @param targetDerivationParams - The options to use for key derivation. + * @returns A promise resolving to the updated vault. + */ + updateVault = async ( + vault: string, + password: string, + targetDerivationParams = this.derivationParams, + ): Promise => { + if (this.isVaultUpdated(vault, targetDerivationParams)) { + return vault; + } + + return this.encrypt(password, await this.decrypt(password, vault)); + }; +} + +// eslint-disable-next-line import/prefer-default-export +export { Encryptor }; diff --git a/app/core/Encryptor/constants.ts b/app/core/Encryptor/constants.ts new file mode 100644 index 00000000000..edf9563e5c7 --- /dev/null +++ b/app/core/Encryptor/constants.ts @@ -0,0 +1,20 @@ +import { KeyDerivationOptions } from './types'; + +export const SALT_BYTES_COUNT = 32; +export const SHA256_DIGEST_LENGTH = 256; +export const ENCRYPTION_LIBRARY = { + original: 'original', +}; + +export enum KeyDerivationIteration { + Legacy = 5_000, + Minimum = 600_000, + Default = 900_000, +} + +export const DERIVATION_PARAMS: KeyDerivationOptions = { + algorithm: 'PBKDF2', + params: { + iterations: KeyDerivationIteration.Minimum, + }, +}; diff --git a/app/core/Encryptor/index.ts b/app/core/Encryptor/index.ts new file mode 100644 index 00000000000..e9f4b61263d --- /dev/null +++ b/app/core/Encryptor/index.ts @@ -0,0 +1,4 @@ +import { Encryptor } from './Encryptor'; +import { DERIVATION_PARAMS } from './constants'; + +export { Encryptor, DERIVATION_PARAMS }; diff --git a/app/core/Encryptor/types.ts b/app/core/Encryptor/types.ts new file mode 100644 index 00000000000..fa441927842 --- /dev/null +++ b/app/core/Encryptor/types.ts @@ -0,0 +1,76 @@ +import type { Json } from '@metamask/utils'; + +/** + * Parameters used for key derivation. + * @interface KeyParams + * @property iterations - The number of iterations to use in the key derivation process. + */ +export interface KeyParams { + iterations: number; +} + +/** + * Options for key derivation, specifying the algorithm and parameters to use. + * @interface KeyDerivationOptions + * @property algorithm - The name of the algorithm to use for key derivation. + * @property params - The parameters to use with the specified algorithm. + */ +export interface KeyDerivationOptions { + algorithm: string; + params: KeyParams; +} + +/** + * The result of an encryption operation. + * @interface EncryptionResult + * @property cipher - The encrypted data. + * @property iv - The initialization vector used in the encryption process. + * @property [salt] - The salt used in the encryption process, if applicable. + * @property [lib] - The library or algorithm used for encryption, if applicable. + * @property [keyMetadata] - Metadata about the key derivation, if key derivation was used. + */ +export interface EncryptionResult { + cipher: string; + iv: string; + salt?: string; + lib?: string; + keyMetadata?: KeyDerivationOptions; +} + +/** + * Defines the structure for a generic encryption utility. + * This utility provides methods for encrypting and decrypting objects + * using a specified password. It may also include an optional method + * for checking if an encrypted vault is up to date with the desired + * encryption algorithm and parameters. + */ +export interface GenericEncryptor { + /** + * Encrypts the given object with the given password. + * + * @param password - The password to encrypt with. + * @param object - The object to encrypt. + * @returns The encrypted string. + */ + encrypt: (password: string, object: Json) => Promise; + /** + * Decrypts the given encrypted string with the given password. + * + * @param password - The password to decrypt with. + * @param encryptedString - The encrypted string to decrypt. + * @returns The decrypted object. + */ + decrypt: (password: string, encryptedString: string) => Promise; + /** + * Optional vault migration helper. Checks if the provided vault is up to date + * with the desired encryption algorithm. + * + * @param vault - The encrypted string to check. + * @param targetDerivationParams - The desired target derivation params. + * @returns The updated encrypted string. + */ + isVaultUpdated?: ( + vault: string, + targetDerivationParams?: KeyDerivationOptions, + ) => boolean; +} diff --git a/app/core/Engine.ts b/app/core/Engine.ts index 632ae72a657..0dd009d4c91 100644 --- a/app/core/Engine.ts +++ b/app/core/Engine.ts @@ -121,7 +121,7 @@ import { LoggingControllerActions, } from '@metamask/logging-controller'; import LedgerKeyring from '@consensys/ledgerhq-metamask-keyring'; -import Encryptor from './Encryptor'; +import { Encryptor, DERIVATION_PARAMS } from './Encryptor'; import { isMainnetByChainId, getDecimalChainId, @@ -190,7 +190,9 @@ import { const NON_EMPTY = 'NON_EMPTY'; -const encryptor = new Encryptor(); +const encryptor = new Encryptor({ + derivationParams: DERIVATION_PARAMS, +}); let currentChainId: any; ///: BEGIN:ONLY_INCLUDE_IF(snaps) diff --git a/app/core/SecureKeychain.js b/app/core/SecureKeychain.js index 09fb8c14382..6607c947386 100644 --- a/app/core/SecureKeychain.js +++ b/app/core/SecureKeychain.js @@ -1,5 +1,5 @@ import * as Keychain from 'react-native-keychain'; // eslint-disable-line import/no-namespace -import Encryptor from './Encryptor'; +import { Encryptor, DERIVATION_PARAMS } from './Encryptor'; import { strings } from '../../locales/i18n'; import AsyncStorage from '../store/async-storage-wrapper'; import { Platform } from 'react-native'; @@ -14,7 +14,9 @@ import { import Device from '../util/device'; const privates = new WeakMap(); -const encryptor = new Encryptor(); +const encryptor = new Encryptor({ + derivationParams: DERIVATION_PARAMS, +}); const defaultOptions = { service: 'com.metamask', authenticationPromptTitle: strings('authentication.auth_prompt_title'), diff --git a/app/util/test/testSetup.js b/app/util/test/testSetup.js index 2ae37d53d64..3d65f9d33b3 100644 --- a/app/util/test/testSetup.js +++ b/app/util/test/testSetup.js @@ -194,6 +194,23 @@ NativeModules.Aes = { const hashBase = '012345678987654'; return Promise.resolve(hashBase + uniqueAddressChar); }), + pbkdf2: jest + .fn() + .mockImplementation((_password, _salt, _iterations, _keyLength) => + Promise.resolve('mockedKey'), + ), + randomKey: jest.fn().mockResolvedValue('mockedIV'), + encrypt: jest.fn().mockResolvedValue('mockedCipher'), + decrypt: jest.fn().mockResolvedValue('{"mockData": "mockedPlainText"}'), +}; + +NativeModules.AesForked = { + pbkdf2: jest + .fn() + .mockImplementation((_password, _salt) => + Promise.resolve('mockedKeyForked'), + ), + decrypt: jest.fn().mockResolvedValue('{"mockData": "mockedPlainTextForked"}'), }; jest.mock( @@ -285,3 +302,13 @@ afterEach(() => { jest.restoreAllMocks(); global.gc && global.gc(true); }); + +global.crypto = { + getRandomValues: (arr) => { + const uint8Max = 255; + for (let i = 0; i < arr.length; i++) { + arr[i] = Math.floor(Math.random() * (uint8Max + 1)); + } + return arr; + }, +}; diff --git a/app/util/validators/index.js b/app/util/validators/index.js index dddc41cc44a..596bfd1699b 100644 --- a/app/util/validators/index.js +++ b/app/util/validators/index.js @@ -1,5 +1,5 @@ import { ethers } from 'ethers'; -import Encryptor from '../../core/Encryptor'; +import { Encryptor, DERIVATION_PARAMS } from '../../core/Encryptor'; import { regex } from '../regex'; export const failedSeedPhraseRequirements = (seed) => { @@ -26,7 +26,9 @@ export const parseVaultValue = async (password, vault) => { seedObject?.iv && seedObject?.lib ) { - const encryptor = new Encryptor(); + const encryptor = new Encryptor({ + derivationParams: DERIVATION_PARAMS, + }); const result = await encryptor.decrypt(password, vault); vaultSeed = result[0]?.data?.mnemonic; } diff --git a/package.json b/package.json index b2591854dee..b403554b537 100644 --- a/package.json +++ b/package.json @@ -150,7 +150,7 @@ "@metamask/gas-fee-controller": "^10.0.0", "@metamask/key-tree": "^9.0.0", "@metamask/keyring-api": "^4.0.0", - "@metamask/keyring-controller": "^8.1.0", + "@metamask/keyring-controller": "^9.0.0", "@metamask/logging-controller": "^1.0.1", "@metamask/network-controller": "^15.0.0", "@metamask/permission-controller": "7.1.0", diff --git a/patches/@metamask+eth-keyring-controller+13.0.1.patch b/patches/@metamask+eth-keyring-controller+15.1.0.patch similarity index 77% rename from patches/@metamask+eth-keyring-controller+13.0.1.patch rename to patches/@metamask+eth-keyring-controller+15.1.0.patch index 1aed5a46e8f..8bc3bb7b32a 100644 --- a/patches/@metamask+eth-keyring-controller+13.0.1.patch +++ b/patches/@metamask+eth-keyring-controller+15.1.0.patch @@ -1,12 +1,12 @@ diff --git a/node_modules/@metamask/eth-keyring-controller/dist/KeyringController.js b/node_modules/@metamask/eth-keyring-controller/dist/KeyringController.js -index 3644209..027666c 100644 +index 3f70b51..f2d99d0 100644 --- a/node_modules/@metamask/eth-keyring-controller/dist/KeyringController.js +++ b/node_modules/@metamask/eth-keyring-controller/dist/KeyringController.js -@@ -637,6 +637,19 @@ class KeyringController extends events_1.EventEmitter { +@@ -651,6 +651,19 @@ class KeyringController extends events_1.EventEmitter { serializedKeyrings.push(...this.unsupportedKeyrings); let vault; let newEncryptionKey; -+ ++ + /** + * ============================== PATCH INFORMATION ============================== + * The HD keyring is the default keyring for all wallets if this keyring is missing @@ -19,6 +19,6 @@ index 3644209..027666c 100644 + throw new Error(`HD keyring missing while saving keyrings - the available types are [${types}]`); + } + - if (this.cacheEncryptionKey) { - if (this.password) { - const { vault: newVault, exportedKeyString } = await this.encryptor.encryptWithDetail(this.password, serializedKeyrings); + if (__classPrivateFieldGet(this, _KeyringController_cacheEncryptionKey, "f")) { + assertIsExportableKeyEncryptor(__classPrivateFieldGet(this, _KeyringController_encryptor, "f")); + if (encryptionKey) { diff --git a/patches/@metamask+keyring-controller+8.1.0.patch b/patches/@metamask+keyring-controller+9.0.0.patch similarity index 95% rename from patches/@metamask+keyring-controller+8.1.0.patch rename to patches/@metamask+keyring-controller+9.0.0.patch index 2373eba1e7e..fb5bc2538ee 100644 --- a/patches/@metamask+keyring-controller+8.1.0.patch +++ b/patches/@metamask+keyring-controller+9.0.0.patch @@ -1,8 +1,8 @@ diff --git a/node_modules/@metamask/keyring-controller/dist/KeyringController.js b/node_modules/@metamask/keyring-controller/dist/KeyringController.js -index 08a8714..a096388 100644 +index da9523a..2cb136d 100644 --- a/node_modules/@metamask/keyring-controller/dist/KeyringController.js +++ b/node_modules/@metamask/keyring-controller/dist/KeyringController.js -@@ -773,15 +773,25 @@ class KeyringController extends base_controller_1.BaseControllerV2 { +@@ -785,15 +785,25 @@ class KeyringController extends base_controller_1.BaseControllerV2 { return (yield __classPrivateFieldGet(this, _KeyringController_keyring, "f").getKeyringForAccount(account)).type; }); } diff --git a/yarn.lock b/yarn.lock index 0d1800b8132..4a9f26e872f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1884,7 +1884,7 @@ "@ethereumjs/util" "^8.1.0" ethereum-cryptography "^2.0.0" -"@ethereumjs/util@^8.0.0", "@ethereumjs/util@^8.0.2", "@ethereumjs/util@^8.0.6", "@ethereumjs/util@^8.1.0": +"@ethereumjs/util@^8.0.0", "@ethereumjs/util@^8.1.0": version "8.1.0" resolved "https://registry.yarnpkg.com/@ethereumjs/util/-/util-8.1.0.tgz#299df97fb6b034e0577ce9f94c7d9d1004409ed4" integrity sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA== @@ -3659,7 +3659,7 @@ "@metamask/utils" "^8.3.0" immer "^9.0.6" -"@metamask/browser-passworder@^4.1.0": +"@metamask/browser-passworder@^4.3.0": version "4.3.0" resolved "https://registry.yarnpkg.com/@metamask/browser-passworder/-/browser-passworder-4.3.0.tgz#62c200750efcea864bd31d685120331859e1ab1e" integrity sha512-RU1TVVV5DkbZRr6zPYg0NkexZ0/T2LCKNvF3A50jvUweyxDFuoNbSTN6z8K3Fy8O6/X2JQ1yyAbVzxZLq0qrGg== @@ -3794,15 +3794,16 @@ resolved "https://registry.yarnpkg.com/@metamask/eslint-config/-/eslint-config-9.0.0.tgz#22d4911b705f7e4e566efbdda0e37912da33e30f" integrity sha512-mWlLGQKjXXFOj9EtDClKSoTLeQuPW2kM1w3EpUMf4goYAQ+kLXCCa8pEff6h8ApWAnjhYmXydA1znQ2J4XvD+A== -"@metamask/eth-hd-keyring@^6.0.0": - version "6.0.0" - resolved "https://registry.yarnpkg.com/@metamask/eth-hd-keyring/-/eth-hd-keyring-6.0.0.tgz#a46788d4bbc7aa5d8263ed6e4348101a80519a69" - integrity sha512-dEj/I6Ag9FyCmjPcRXeXCkRXkVJE/uElhDVRcLBU6mT/GsXKgzVWXC/k0dhE8rEDrQbidhl+8wEElSJ2LI1InA== +"@metamask/eth-hd-keyring@^7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@metamask/eth-hd-keyring/-/eth-hd-keyring-7.0.1.tgz#799006d8fd57c5580dc5843f74a7343eeb2985f3" + integrity sha512-EVdaZxsBgDBIcUAHIDYLI10TepQwUuwzBMJ2GQjNwiCwkARYHeckKnvlCkPcYWhwlnjhkfFg+iJn24cA5poulw== dependencies: - "@ethereumjs/util" "^8.0.2" - "@metamask/eth-sig-util" "^5.0.2" - "@metamask/scure-bip39" "^2.0.3" - ethereum-cryptography "^1.1.2" + "@ethereumjs/util" "^8.1.0" + "@metamask/eth-sig-util" "^7.0.0" + "@metamask/scure-bip39" "^2.1.0" + "@metamask/utils" "^8.1.0" + ethereum-cryptography "^2.1.2" "@metamask/eth-json-rpc-infura@^8.1.1": version "8.1.1" @@ -3874,18 +3875,18 @@ "@metamask/safe-event-emitter" "^3.0.0" "@metamask/utils" "^8.3.0" -"@metamask/eth-keyring-controller@^13.0.1": - version "13.0.1" - resolved "https://registry.yarnpkg.com/@metamask/eth-keyring-controller/-/eth-keyring-controller-13.0.1.tgz#9756a70ed2ea4f4dc6a8c335ac55b6322990e435" - integrity sha512-zxULUAAR4CUiIw5lYeELfkyKqfxoepbRjxpEJ9OaSPMZz66oVQGCxIyzKLgXe5i782WfGEihTHLHj338A0RLZw== +"@metamask/eth-keyring-controller@^15.0.0": + version "15.1.0" + resolved "https://registry.yarnpkg.com/@metamask/eth-keyring-controller/-/eth-keyring-controller-15.1.0.tgz#dc7c5ec3a075a2eb3f90b1f452cd67232714451d" + integrity sha512-FL6bVet2Rp3n6z+tKM9Lp0dBhTNj7wPKFLBvTTqJq7wBEbXir/AdN7JtnSXWC4BzbfsonXtnGuX353z0x3+8lw== dependencies: "@ethereumjs/tx" "^4.2.0" - "@metamask/browser-passworder" "^4.1.0" - "@metamask/eth-hd-keyring" "^6.0.0" - "@metamask/eth-sig-util" "^6.0.0" - "@metamask/eth-simple-keyring" "^5.0.0" + "@metamask/browser-passworder" "^4.3.0" + "@metamask/eth-hd-keyring" "^7.0.1" + "@metamask/eth-sig-util" "^7.0.0" + "@metamask/eth-simple-keyring" "^6.0.1" "@metamask/obs-store" "^8.1.0" - "@metamask/utils" "^8.1.0" + "@metamask/utils" "^8.2.0" "@metamask/eth-query@^3.0.1": version "3.0.1" @@ -3914,18 +3915,6 @@ tweetnacl "^1.0.3" tweetnacl-util "^0.15.1" -"@metamask/eth-sig-util@^5.0.1", "@metamask/eth-sig-util@^5.0.2": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-5.1.0.tgz#a47f62800ee1917fef976ba67544a0ccd7d1bd6b" - integrity sha512-mlgziIHYlA9pi/XZerChqg4NocdOgBPB9NmxgXWQO2U2hH8RGOJQrz6j/AIKkYxgCMIE2PY000+joOwXfzeTDQ== - dependencies: - "@ethereumjs/util" "^8.0.6" - bn.js "^4.12.0" - ethereum-cryptography "^2.0.0" - ethjs-util "^0.1.6" - tweetnacl "^1.0.3" - tweetnacl-util "^0.15.1" - "@metamask/eth-sig-util@^6.0.0": version "6.0.2" resolved "https://registry.yarnpkg.com/@metamask/eth-sig-util/-/eth-sig-util-6.0.2.tgz#d81dc87e0cd5a6580010911501976b48821746ad" @@ -3951,14 +3940,15 @@ tweetnacl "^1.0.3" tweetnacl-util "^0.15.1" -"@metamask/eth-simple-keyring@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@metamask/eth-simple-keyring/-/eth-simple-keyring-5.0.0.tgz#307772d1aa3298e41a2444b428cd8f4522b7bf5c" - integrity sha512-UJfP36Z9g1eeD8mSHWaVqUvkgbgYm3S7YuzlMzQi+WgPnWu81CdbldMMtvreTlu4I1mTyljXLDMjIp65P0bygQ== +"@metamask/eth-simple-keyring@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@metamask/eth-simple-keyring/-/eth-simple-keyring-6.0.1.tgz#2c914c51746aba3588530a256842ce0436481ab5" + integrity sha512-UEMOojkcyPAJF30lRUesZvqESeUZIOHQgWyHQv3FzwE4ekXgJMfvvW+TuGydcGY2aeIdJy9l9wJQF0/+V9wNhg== dependencies: - "@ethereumjs/util" "^8.0.0" - "@metamask/eth-sig-util" "^5.0.1" - ethereum-cryptography "^1.1.2" + "@ethereumjs/util" "^8.1.0" + "@metamask/eth-sig-util" "^7.0.0" + "@metamask/utils" "^8.1.0" + ethereum-cryptography "^2.1.2" randombytes "^2.1.0" "@metamask/eth-snap-keyring@^2.1.1": @@ -4060,17 +4050,17 @@ superstruct "^1.0.3" uuid "^9.0.0" -"@metamask/keyring-controller@^8.1.0": - version "8.1.0" - resolved "https://registry.yarnpkg.com/@metamask/keyring-controller/-/keyring-controller-8.1.0.tgz#e8c9a9a4e4689492b1a4e440ece321a9eea3d279" - integrity sha512-b5T2tULBoYjuc/xeP7JxsVPo2zABJsTR3JkboKUkcptL72qGHgMix1eQgPeU4msMHrDC4hc5Bgc8ZD2PxtjzEw== +"@metamask/keyring-controller@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@metamask/keyring-controller/-/keyring-controller-9.0.0.tgz#d0b3790ed43b8626edd6c4e092c14a9e3c8a3de4" + integrity sha512-KSD59AiYvChFt2NyIuHslU6IW5F5pMxyEWKQ3FqjmNEtbtk36DCvXay//8IK+ZURWh6TdWanu2O/+9bn/UcUVQ== dependencies: "@keystonehq/metamask-airgapped-keyring" "^0.13.1" "@metamask/base-controller" "^3.2.3" - "@metamask/eth-keyring-controller" "^13.0.1" + "@metamask/eth-keyring-controller" "^15.0.0" "@metamask/message-manager" "^7.3.5" "@metamask/preferences-controller" "^4.4.3" - "@metamask/utils" "^8.1.0" + "@metamask/utils" "^8.2.0" async-mutex "^0.2.6" ethereumjs-util "^7.0.10" ethereumjs-wallet "^1.0.1" @@ -4404,7 +4394,7 @@ resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-3.0.0.tgz#8c2b9073fe0722d48693143b0dc8448840daa3bd" integrity sha512-j6Z47VOmVyGMlnKXZmL0fyvWfEYtKWCA9yGZkU3FCsGZUT5lHGmvaV9JA5F2Y+010y7+ROtR3WMXIkvl/nVzqQ== -"@metamask/scure-bip39@^2.0.3", "@metamask/scure-bip39@^2.1.0": +"@metamask/scure-bip39@^2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@metamask/scure-bip39/-/scure-bip39-2.1.0.tgz#13456884736e56ede15e471bd93c0aa0acdedd0b" integrity sha512-Ndwdnld0SI6YaftEUUVq20sdoWcWNXsJXxvQkbiY42FKmrA16U6WoSh9Eq+NpugpKKwK6f5uvaTDusjndiEDGQ== @@ -4706,11 +4696,6 @@ resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.7.3.tgz#57e1677bf6885354b466c38e2b620c62f45a7123" integrity sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ== -"@noble/hashes@1.2.0", "@noble/hashes@~1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12" - integrity sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ== - "@noble/hashes@1.3.3", "@noble/hashes@~1.3.2": version "1.3.3" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699" @@ -4726,7 +4711,7 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.5.tgz#1a0377f3b9020efe2fae03290bd2a12140c95c11" integrity sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ== -"@noble/secp256k1@1.7.1", "@noble/secp256k1@^1.5.5", "@noble/secp256k1@~1.7.0": +"@noble/secp256k1@^1.5.5": version "1.7.1" resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" integrity sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw== @@ -5682,15 +5667,6 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.6.tgz#8ce5d304b436e4c84f896e0550c83e4d88cb917d" integrity sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g== -"@scure/bip32@1.1.5": - version "1.1.5" - resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.1.5.tgz#d2ccae16dcc2e75bc1d75f5ef3c66a338d1ba300" - integrity sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw== - dependencies: - "@noble/hashes" "~1.2.0" - "@noble/secp256k1" "~1.7.0" - "@scure/base" "~1.1.0" - "@scure/bip32@1.3.3": version "1.3.3" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.3.tgz#a9624991dc8767087c57999a5d79488f48eae6c8" @@ -5700,14 +5676,6 @@ "@noble/hashes" "~1.3.2" "@scure/base" "~1.1.4" -"@scure/bip39@1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.1.1.tgz#b54557b2e86214319405db819c4b6a370cf340c5" - integrity sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg== - dependencies: - "@noble/hashes" "~1.2.0" - "@scure/base" "~1.1.0" - "@scure/bip39@1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.2.tgz#f3426813f4ced11a47489cbcf7294aa963966527" @@ -12058,7 +12026,7 @@ bn.js@5.2.1, bn.js@^5.0.0, bn.js@^5.1.2, bn.js@^5.2.0, bn.js@^5.2.1: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.10.0, bn.js@^4.11.0, bn.js@^4.11.8, bn.js@^4.11.9, bn.js@^4.12.0: +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.10.0, bn.js@^4.11.0, bn.js@^4.11.8, bn.js@^4.11.9: version "4.12.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== @@ -15801,16 +15769,6 @@ ethereum-cryptography@^0.1.3: secp256k1 "^4.0.1" setimmediate "^1.0.5" -ethereum-cryptography@^1.1.2: - version "1.2.0" - resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz#5ccfa183e85fdaf9f9b299a79430c044268c9b3a" - integrity sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw== - dependencies: - "@noble/hashes" "1.2.0" - "@noble/secp256k1" "1.7.1" - "@scure/bip32" "1.1.5" - "@scure/bip39" "1.1.1" - ethereum-cryptography@^2.0.0, ethereum-cryptography@^2.1.2: version "2.1.3" resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-2.1.3.tgz#1352270ed3b339fe25af5ceeadcf1b9c8e30768a"