diff --git a/app/core/Encryptor/index.ts b/app/core/Encryptor/index.ts index 1bef04a6e1f..89973267818 100644 --- a/app/core/Encryptor/index.ts +++ b/app/core/Encryptor/index.ts @@ -5,11 +5,12 @@ import { DERIVATION_OPTIONS_MINIMUM_OWASP2023, DERIVATION_OPTIONS_DEFAULT_OWASP2023, } from './constants'; - +import { pbkdf2 } from './pbkdf2'; export { Encryptor, ENCRYPTION_LIBRARY, LEGACY_DERIVATION_OPTIONS, DERIVATION_OPTIONS_MINIMUM_OWASP2023, DERIVATION_OPTIONS_DEFAULT_OWASP2023, + pbkdf2, }; diff --git a/app/core/Encryptor/pbkdf2.test.ts b/app/core/Encryptor/pbkdf2.test.ts new file mode 100644 index 00000000000..e679b63b7f0 --- /dev/null +++ b/app/core/Encryptor/pbkdf2.test.ts @@ -0,0 +1,76 @@ +import { stringToBytes } from '@metamask/utils'; +import { pbkdf2 } from './pbkdf2'; +import { NativeModules } from 'react-native'; + +const mockPassword = 'mockPassword'; +const mockSalt = '00112233445566778899001122334455'; + +describe('pbkdf2', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('uses the native implementation of pbkdf2 with main aes', async () => { + NativeModules.Aes.pbkdf2 = jest + .fn() + .mockImplementation(() => + Promise.resolve( + 'd5217329ae279885bbfe1f25ac3aacc9adabc3c9c0b9bdbaa1c095c8b03dcad0d703f96a4fa453c960a9a3e540c585fd7e6406edae20b995dcef6a0883919457', + ), + ); + + const mockPasswordBytes = stringToBytes(mockPassword); + const mockSaltBytes = stringToBytes(mockSalt); + const mockIterations = 2048; + const mockKeyLength = 64; // 512 bits + + await expect( + pbkdf2(mockPasswordBytes, mockSaltBytes, mockIterations, mockKeyLength), + ).resolves.toBeDefined(); + }); + + it('throws on native module errors', async () => { + NativeModules.Aes.pbkdf2 = jest + .fn() + .mockRejectedValue(new Error('Native module error')); + + const mockPasswordBytes = stringToBytes('password'); + const mockSaltBytes = stringToBytes('salt'); + + await expect( + pbkdf2(mockPasswordBytes, mockSaltBytes, 2048, 64), + ).rejects.toThrow('Native module error'); + }); + + it('does not fail when empty password', async () => { + NativeModules.Aes.pbkdf2 = jest + .fn() + .mockImplementation(() => + Promise.resolve( + '0000000000000000000000000000000000000000000000000000000000000000', + ), + ); + + const mockPasswordBytes = stringToBytes(''); + const mockSaltBytes = stringToBytes(mockSalt); + + const result = await pbkdf2(mockPasswordBytes, mockSaltBytes, 2048, 64); + expect(result).toBeDefined(); + }); + + it('does not fail when empty salt', async () => { + NativeModules.Aes.pbkdf2 = jest + .fn() + .mockImplementation(() => + Promise.resolve( + 'f347723a89a783c4de6a65d6a066d0e9a9c13319f8389f97d0566c79d87b6f80', + ), + ); + + const mockPasswordBytes = stringToBytes(mockPassword); + const mockSaltBytes = stringToBytes(''); + + const result = await pbkdf2(mockPasswordBytes, mockSaltBytes, 2048, 64); + expect(result).toBeDefined(); + }); +}); diff --git a/app/core/Encryptor/pbkdf2.ts b/app/core/Encryptor/pbkdf2.ts new file mode 100644 index 00000000000..e89b16b9fed --- /dev/null +++ b/app/core/Encryptor/pbkdf2.ts @@ -0,0 +1,33 @@ +import { bytesToString, hexToBytes } from '@metamask/utils'; +import { NativeModules } from 'react-native'; +import { ShaAlgorithm } from './constants'; +import { bytesLengthToBitsLength } from '../../util/bytes'; + +/** + * Derives a key using PBKDF2. + * + * @param password - The password used to generate the key. + * @param salt - The salt used during key generation. + * @param iterations - The number of iterations used during key generation. + * @param keyLength - The length (in bytes) of the key to generate. + * @returns The generated key. + */ +const pbkdf2 = async ( + password: Uint8Array, + salt: Uint8Array, + iterations: number, + keyLength: number, +): Promise => { + const Aes = NativeModules.Aes; + const derivedKey = await Aes.pbkdf2( + bytesToString(password), + bytesToString(salt), + iterations, + bytesLengthToBitsLength(keyLength), + ShaAlgorithm.Sha512, + ); + + return hexToBytes(derivedKey); +}; + +export { pbkdf2 }; diff --git a/app/core/Engine/Engine.ts b/app/core/Engine/Engine.ts index 9c6415f7fe4..a5776bd0f4d 100644 --- a/app/core/Engine/Engine.ts +++ b/app/core/Engine/Engine.ts @@ -46,6 +46,7 @@ import { AcceptOptions, ApprovalController, } from '@metamask/approval-controller'; +import HDKeyring from '@metamask/eth-hd-keyring'; import { SelectedNetworkController } from '@metamask/selected-network-controller'; import { PermissionController, @@ -80,7 +81,7 @@ import { LedgerMobileBridge, LedgerTransportMiddleware, } from '@metamask/eth-ledger-bridge-keyring'; -import { Encryptor, LEGACY_DERIVATION_OPTIONS } from '../Encryptor'; +import { Encryptor, LEGACY_DERIVATION_OPTIONS, pbkdf2 } from '../Encryptor'; import { isMainnetByChainId, fetchEstimatedMultiLayerL1Fee, @@ -522,6 +523,13 @@ export class Engine { additionalKeyrings.push(ledgerKeyringBuilder); + const hdKeyringBuilder = () => + new HDKeyring({ + cryptographicFunctions: { pbkdf2Sha512: pbkdf2 }, + }); + hdKeyringBuilder.type = HDKeyring.type; + additionalKeyrings.push(hdKeyringBuilder); + ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) const snapKeyringBuildMessenger = this.controllerMessenger.getRestricted({ name: 'SnapKeyringBuilder', diff --git a/app/util/bytes.test.ts b/app/util/bytes.test.ts new file mode 100644 index 00000000000..ba324f781e3 --- /dev/null +++ b/app/util/bytes.test.ts @@ -0,0 +1,10 @@ +import { bytesLengthToBitsLength } from './bytes'; + +describe('bytesLengthToBitsLength', () => { + it('converts bytes length to bits length', () => { + expect(bytesLengthToBitsLength(1)).toBe(8); + expect(bytesLengthToBitsLength(2)).toBe(16); + expect(bytesLengthToBitsLength(32)).toBe(256); + expect(bytesLengthToBitsLength(0)).toBe(0); + }); +}); diff --git a/app/util/bytes.ts b/app/util/bytes.ts index 3d046a30889..8101a52fc6a 100644 --- a/app/util/bytes.ts +++ b/app/util/bytes.ts @@ -13,3 +13,13 @@ export default function byteArrayToHex(value: Uint8Array): string { } return '0x' + result.join(''); } + +/** + * Converts bytes length to bits length + * + * @param bytesLength - Bytes length to convert + * @returns Bits length + */ +export function bytesLengthToBitsLength(bytesLength: number): number { + return bytesLength * 8; +} diff --git a/package.json b/package.json index a9a7609b4f7..16e6b345cb5 100644 --- a/package.json +++ b/package.json @@ -150,6 +150,7 @@ "@metamask/composable-controller": "^3.0.0", "@metamask/controller-utils": "^11.3.0", "@metamask/design-tokens": "^4.0.0", + "@metamask/eth-hd-keyring": "^9.0.0", "@metamask/eth-json-rpc-filters": "^8.0.0", "@metamask/eth-json-rpc-middleware": "^11.0.2", "@metamask/eth-ledger-bridge-keyring": "^6.0.0", diff --git a/patches/@metamask+keyring-controller+18.0.0.patch b/patches/@metamask+keyring-controller+18.0.0.patch new file mode 100644 index 00000000000..c395747a377 --- /dev/null +++ b/patches/@metamask+keyring-controller+18.0.0.patch @@ -0,0 +1,14 @@ +diff --git a/node_modules/@metamask/keyring-controller/dist/KeyringController.cjs b/node_modules/@metamask/keyring-controller/dist/KeyringController.cjs +index 8529ba1..77871be 100644 +--- a/node_modules/@metamask/keyring-controller/dist/KeyringController.cjs ++++ b/node_modules/@metamask/keyring-controller/dist/KeyringController.cjs +@@ -1379,7 +1379,8 @@ async function _KeyringController_newKeyring(type, data) { + if (!keyring.generateRandomMnemonic) { + throw new Error(constants_1.KeyringControllerError.UnsupportedGenerateRandomMnemonic); + } +- keyring.generateRandomMnemonic(); ++ // This patch can be removed once all the keyrings types are updated across the monorepo, the work is in progress atm ++ await keyring.generateRandomMnemonic(); + await keyring.addAccounts(1); + } + await __classPrivateFieldGet(this, _KeyringController_instances, "m", _KeyringController_checkForDuplicate).call(this, type, await keyring.getAccounts()); diff --git a/yarn.lock b/yarn.lock index d862de7e7b6..39421afcb56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4255,6 +4255,18 @@ "@metamask/utils" "^9.2.1" ethereum-cryptography "^2.1.2" +"@metamask/eth-hd-keyring@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@metamask/eth-hd-keyring/-/eth-hd-keyring-9.0.0.tgz#cba58308af5cfb9b7b1b958eeea380ec9c798e64" + integrity sha512-dhvirCCWmFGd0HyiEmho0Zwdl2g86kLA+K78f3uHnV1PV0ELKsMgRqlcA8OuXlrz0jnGQPDRNFXGG6/yFyyLlg== + dependencies: + "@ethereumjs/util" "^8.1.0" + "@metamask/eth-sig-util" "^8.0.0" + "@metamask/key-tree" "^10.0.0" + "@metamask/scure-bip39" "^2.1.1" + "@metamask/utils" "^9.2.1" + ethereum-cryptography "^2.1.2" + "@metamask/eth-json-rpc-filters@^8.0.0": version "8.0.0" resolved "https://registry.yarnpkg.com/@metamask/eth-json-rpc-filters/-/eth-json-rpc-filters-8.0.0.tgz#fd0ca224dc198e270e142c1f2007e05cacb5f16a" @@ -4548,6 +4560,17 @@ "@metamask/utils" "^10.0.0" readable-stream "^3.6.2" +"@metamask/key-tree@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@metamask/key-tree/-/key-tree-10.0.0.tgz#58eb9b7ba2b92a5ffa170ce4efdd236e5ac7e891" + integrity sha512-U95FwOOg4d61uJp1x2g0MH66eOtjLwsthZiBGMgP3PYMgdOb4exHynBCFqZ6wxxQbYGdDyBjC6USVRT7idkGKw== + dependencies: + "@metamask/scure-bip39" "^2.1.1" + "@metamask/utils" "^10.0.1" + "@noble/curves" "^1.2.0" + "@noble/hashes" "^1.3.2" + "@scure/base" "^1.0.0" + "@metamask/key-tree@^9.0.0", "@metamask/key-tree@^9.1.2": version "9.1.2" resolved "https://registry.yarnpkg.com/@metamask/key-tree/-/key-tree-9.1.2.tgz#3f89fc7990c395be3aa9c3e6e045d3d28768149b"