diff --git a/packages/wallet-cli/CHANGELOG.md b/packages/wallet-cli/CHANGELOG.md index d8295d12e18..f6e4ff99e97 100644 --- a/packages/wallet-cli/CHANGELOG.md +++ b/packages/wallet-cli/CHANGELOG.md @@ -21,5 +21,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The daemon RPC server now validates `params` against each handler's superstruct before dispatch, returning a `-32602 invalidParams` error on mismatch instead of passing raw params to the handler ([#8846](https://github.com/MetaMask/core/pull/8846)) - Report daemon socket connection errors consistently across `mm daemon call` and `mm daemon list` ([#9339](https://github.com/MetaMask/core/pull/9339)) - Bump `@metamask/wallet` from `^3.0.0` to `^7.0.1` ([#9218](https://github.com/MetaMask/core/pull/9218), [#9263](https://github.com/MetaMask/core/pull/9263), [#9349](https://github.com/MetaMask/core/pull/9349), [#9396](https://github.com/MetaMask/core/pull/9396), [#9470](https://github.com/MetaMask/core/pull/9470)) +- Wrap daemon password and SRP in opaque `Password` and `Srp` types that redact on logging; validated and unwrapped only at trust boundaries ([#8863](https://github.com/MetaMask/core/pull/8863)) [Unreleased]: https://github.com/MetaMask/core/ diff --git a/packages/wallet-cli/package.json b/packages/wallet-cli/package.json index ab76006349b..17173d8432d 100644 --- a/packages/wallet-cli/package.json +++ b/packages/wallet-cli/package.json @@ -48,6 +48,7 @@ "@metamask/base-controller": "^9.1.0", "@metamask/remote-feature-flag-controller": "^4.2.2", "@metamask/rpc-errors": "^7.0.2", + "@metamask/scure-bip39": "^2.1.1", "@metamask/storage-service": "^1.0.2", "@metamask/superstruct": "^3.1.0", "@metamask/utils": "^11.11.0", diff --git a/packages/wallet-cli/src/commands/daemon/start.test.ts b/packages/wallet-cli/src/commands/daemon/start.test.ts index 68e044010e4..f38a95813cd 100644 --- a/packages/wallet-cli/src/commands/daemon/start.test.ts +++ b/packages/wallet-cli/src/commands/daemon/start.test.ts @@ -6,14 +6,10 @@ jest.mock('../../daemon/daemon-spawn'); const mockEnsureDaemon = jest.mocked(ensureDaemon); -const FLAGS = [ - '--infura-project-id', - 'key', - '--password', - 'pw', - '--srp', - 'phrase', -]; +const SRP = + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; + +const FLAGS = ['--infura-project-id', 'key', '--password', 'pw', '--srp', SRP]; describe('daemon start', () => { it('reports the socket path on a fresh start', async () => { diff --git a/packages/wallet-cli/src/commands/daemon/start.ts b/packages/wallet-cli/src/commands/daemon/start.ts index fb14e29f4d6..193ef0ef793 100644 --- a/packages/wallet-cli/src/commands/daemon/start.ts +++ b/packages/wallet-cli/src/commands/daemon/start.ts @@ -1,6 +1,7 @@ import { Command, Flags } from '@oclif/core'; import { ensureDaemon } from '../../daemon/daemon-spawn'; +import { Password, Srp } from '../../daemon/secrets'; export default class DaemonStart extends Command { static override description = 'Start the wallet daemon'; @@ -33,7 +34,8 @@ export default class DaemonStart extends Command { public async run(): Promise { const { flags } = await this.parse(DaemonStart); const infuraProjectId = flags['infura-project-id']; - const { password, srp } = flags; + const password = Password.from(flags.password); + const srp = Srp.from(flags.srp); const { state, socketPath } = await ensureDaemon({ dataDir: this.config.dataDir, diff --git a/packages/wallet-cli/src/daemon/daemon-entry.test.ts b/packages/wallet-cli/src/daemon/daemon-entry.test.ts index 72528b26040..212744bccd7 100644 --- a/packages/wallet-cli/src/daemon/daemon-entry.test.ts +++ b/packages/wallet-cli/src/daemon/daemon-entry.test.ts @@ -91,7 +91,7 @@ describe('daemon-entry', () => { process.env.INFURA_PROJECT_ID = 'key'; process.env.MM_WALLET_PASSWORD = 'pass'; process.env.MM_WALLET_SRP = - 'test test test test test test test test test test test ball'; + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; process.exitCode = undefined; stderrSpy = jest .spyOn(process.stderr, 'write') @@ -184,13 +184,21 @@ describe('daemon-entry', () => { await importDaemonEntry(); expect(mockEnsureOwnerOnlyDirectory).toHaveBeenCalledWith('/tmp/data'); - expect(mockCreateWallet).toHaveBeenCalledWith({ - databasePath: '/tmp/wallet.db', - password: 'pass', - srp: 'test test test test test test test test test test test ball', - infuraProjectId: 'key', - log: expect.any(Function), - }); + expect(mockCreateWallet).toHaveBeenCalledWith( + expect.objectContaining({ + databasePath: '/tmp/wallet.db', + infuraProjectId: 'key', + log: expect.any(Function), + }), + ); + // The Password/Srp instances are constructed in an isolated module scope + // (jest.isolateModulesAsync), so their class identity differs from any + // import in this test file. Verify structurally via `.unwrap()`. + const passedConfig = mockCreateWallet.mock.calls[0][0]; + expect(passedConfig.password.unwrap()).toBe('pass'); + expect(passedConfig.srp.unwrap()).toBe( + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', + ); expect(mockWriteFile).toHaveBeenCalledWith( '/tmp/daemon.pid', expect.stringMatching(new RegExp(`^${process.pid}\\n\\d+\\n$`, 'u')), @@ -210,18 +218,28 @@ describe('daemon-entry', () => { await importDaemonEntry(); - // The captured values still reach createWallet... - expect(mockCreateWallet).toHaveBeenCalledWith( - expect.objectContaining({ - password: 'pass', - srp: 'test test test test test test test test test test test ball', - }), + // The captured values still reach createWallet (as opaque Password/Srp + // instances, verified structurally via `.unwrap()`)... + const passedConfig = mockCreateWallet.mock.calls[0][0]; + expect(passedConfig.password.unwrap()).toBe('pass'); + expect(passedConfig.srp.unwrap()).toBe( + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', ); // ...but no longer linger in the long-lived daemon's environment. expect(process.env.MM_WALLET_PASSWORD).toBeUndefined(); expect(process.env.MM_WALLET_SRP).toBeUndefined(); }); + it('scrubs wallet secrets even when Srp.from throws on an invalid mnemonic', async () => { + process.env.MM_WALLET_SRP = 'not a valid mnemonic at all'; + + await importDaemonEntry(); + + expect(process.exitCode).toBe(1); + expect(process.env.MM_WALLET_PASSWORD).toBeUndefined(); + expect(process.env.MM_WALLET_SRP).toBeUndefined(); + }); + it('uses MM_DAEMON_SOCKET_PATH override when set', async () => { process.env.MM_DAEMON_SOCKET_PATH = '/custom/sock'; diff --git a/packages/wallet-cli/src/daemon/daemon-entry.ts b/packages/wallet-cli/src/daemon/daemon-entry.ts index 34c89f87dcd..8e2c8bff881 100644 --- a/packages/wallet-cli/src/daemon/daemon-entry.ts +++ b/packages/wallet-cli/src/daemon/daemon-entry.ts @@ -8,6 +8,7 @@ import { ensureOwnerOnlyDirectory } from './data-dir'; import { getDaemonPaths } from './paths'; import { startRpcSocketServer } from './rpc-socket-server'; import type { RpcSocketServerHandle } from './rpc-socket-server'; +import { Password, Srp } from './secrets'; import { defineHandler } from './types'; import type { DaemonStatusInfo, @@ -57,23 +58,24 @@ async function main(): Promise { throw new Error('INFURA_PROJECT_ID environment variable is required'); } - const password = process.env.MM_WALLET_PASSWORD; - if (!password) { + const passwordRaw = process.env.MM_WALLET_PASSWORD; + if (!passwordRaw) { throw new Error('MM_WALLET_PASSWORD environment variable is required'); } - const srp = process.env.MM_WALLET_SRP; - if (!srp) { + const srpRaw = process.env.MM_WALLET_SRP; + if (!srpRaw) { throw new Error('MM_WALLET_SRP environment variable is required'); } - // Scrub the wallet secrets from the environment now they are captured. The - // daemon is long-lived and dispatches arbitrary messenger actions over its - // socket, so leaving the SRP/password in `process.env` for its whole lifetime - // needlessly widens their exposure to any in-process code. + // Scrub before validation so a throw from Password.from / Srp.from (bad + // value) does not leave the raw secrets in the long-lived daemon's env. delete process.env.MM_WALLET_PASSWORD; delete process.env.MM_WALLET_SRP; + const password = Password.from(passwordRaw); + const srp = Srp.from(srpRaw); + await ensureOwnerOnlyDirectory(dataDir); const { diff --git a/packages/wallet-cli/src/daemon/daemon-spawn.test.ts b/packages/wallet-cli/src/daemon/daemon-spawn.test.ts index 7d884b7f2ee..20f603503a8 100644 --- a/packages/wallet-cli/src/daemon/daemon-spawn.test.ts +++ b/packages/wallet-cli/src/daemon/daemon-spawn.test.ts @@ -6,6 +6,7 @@ import { pingDaemon } from './daemon-client'; import { ensureDaemon } from './daemon-spawn'; import { ensureOwnerOnlyDirectory } from './data-dir'; import { getDaemonPaths } from './paths'; +import { Password, Srp } from './secrets'; import type { DaemonSpawnConfig } from './types'; jest.mock('node:child_process'); @@ -26,11 +27,14 @@ const mockGetDaemonPaths = jest.mocked(getDaemonPaths); // assert it is wired into the child's stdio and later closed in the parent. const LOG_FILE_DESCRIPTOR = 7; +const SRP = + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; + const CONFIG: DaemonSpawnConfig = { dataDir: '/tmp/data', infuraProjectId: 'test-key', - password: 'test-pass', - srp: 'test test test test test test test test test test test ball', + password: Password.from('test-pass'), + srp: Srp.from(SRP), packageRoot: '/pkg', }; @@ -146,8 +150,7 @@ describe('ensureDaemon', () => { MM_DAEMON_SOCKET_PATH: '/tmp/test.sock', INFURA_PROJECT_ID: 'test-key', MM_WALLET_PASSWORD: 'test-pass', - MM_WALLET_SRP: - 'test test test test test test test test test test test ball', + MM_WALLET_SRP: SRP, }), }), ); diff --git a/packages/wallet-cli/src/daemon/daemon-spawn.ts b/packages/wallet-cli/src/daemon/daemon-spawn.ts index 5f740943f17..f0592d37b95 100644 --- a/packages/wallet-cli/src/daemon/daemon-spawn.ts +++ b/packages/wallet-cli/src/daemon/daemon-spawn.ts @@ -90,8 +90,8 @@ export async function ensureDaemon( MM_DAEMON_DATA_DIR: config.dataDir, MM_DAEMON_SOCKET_PATH: socketPath, INFURA_PROJECT_ID: config.infuraProjectId, - MM_WALLET_PASSWORD: config.password, - MM_WALLET_SRP: config.srp, + MM_WALLET_PASSWORD: config.password.unwrap(), + MM_WALLET_SRP: config.srp.unwrap(), }, }); // The child dup'd the file descriptor into its stderr, so drop the parent's diff --git a/packages/wallet-cli/src/daemon/secrets.test.ts b/packages/wallet-cli/src/daemon/secrets.test.ts new file mode 100644 index 00000000000..b54ea515aa2 --- /dev/null +++ b/packages/wallet-cli/src/daemon/secrets.test.ts @@ -0,0 +1,171 @@ +import { inspect } from 'node:util'; + +import { Password, Srp } from './secrets'; + +const VALID_SRP_12 = + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; +const VALID_SRP_24 = + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon ' + + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art'; + +const VALID_SRPS: Record = { + 12: VALID_SRP_12, + 15: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon address', + 18: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent', + 21: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon admit', + 24: VALID_SRP_24, +}; + +describe('Password', () => { + describe('from', () => { + it('wraps a non-empty string', () => { + const password = Password.from('hunter2'); + expect(password).toBeInstanceOf(Password); + }); + + it('throws on an empty string', () => { + expect(() => Password.from('')).toThrow( + 'Password must be a non-empty string', + ); + }); + }); + + describe('unwrap', () => { + it('returns the original value', () => { + expect(Password.from('hunter2').unwrap()).toBe('hunter2'); + }); + }); + + describe('redaction', () => { + const SECRET = 'do-not-log-me'; + let password: Password; + + beforeEach(() => { + password = Password.from(SECRET); + }); + + it('redacts under util.inspect', () => { + const inspected = inspect(password); + expect(inspected).toBe('[redacted]'); + expect(inspected).not.toContain(SECRET); + }); + + it('redacts inside an inspected object', () => { + const inspected = inspect({ password }); + expect(inspected).toContain('[redacted]'); + expect(inspected).not.toContain(SECRET); + }); + + it('redacts under JSON.stringify', () => { + const serialized = JSON.stringify({ password }); + expect(serialized).toBe('{"password":"[redacted]"}'); + expect(serialized).not.toContain(SECRET); + }); + + it('redacts under String() conversion', () => { + expect(String(password)).toBe('[redacted]'); + }); + + it('redacts under template-literal interpolation', () => { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions -- We are intentionally exercising the redacting toString(). + const message = `password is ${password}`; + expect(message).toBe('password is [redacted]'); + expect(message).not.toContain(SECRET); + }); + }); +}); + +describe('Srp', () => { + describe('from', () => { + it.each([12, 15, 18, 21, 24])( + 'accepts a %i-word mnemonic of valid words', + (count) => { + expect(Srp.from(VALID_SRPS[count])).toBeInstanceOf(Srp); + }, + ); + + it('throws when the word count is invalid', () => { + expect(() => Srp.from('abandon abandon abandon')).toThrow( + /must be 12, 15, 18, 21, or 24 words \(got 3\)/u, + ); + }); + + it('throws when a word is not in the BIP-39 wordlist', () => { + const phrase = + 'notabip39word abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; + expect(() => Srp.from(phrase)).toThrow( + 'Secret recovery phrase contains a word not in the BIP-39 English wordlist', + ); + }); + + it('throws when the phrase has an invalid checksum', () => { + // All valid BIP-39 words, correct count, but wrong last word → bad checksum + expect(() => + Srp.from( + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon above', + ), + ).toThrow('Secret recovery phrase has an invalid checksum'); + }); + + it('accepts a phrase with a trailing space', () => { + expect(Srp.from(`${VALID_SRP_12} `)).toBeInstanceOf(Srp); + }); + + it('accepts a phrase with leading whitespace', () => { + expect(Srp.from(` ${VALID_SRP_12}`)).toBeInstanceOf(Srp); + }); + + it('accepts a phrase with multiple spaces between words', () => { + const phrase = VALID_SRP_12.replace(' test ', ' test '); + expect(Srp.from(phrase)).toBeInstanceOf(Srp); + }); + + it('normalizes whitespace in the stored value', () => { + expect(Srp.from(` ${VALID_SRP_12} `).unwrap()).toBe(VALID_SRP_12); + }); + }); + + describe('unwrap', () => { + it('returns the normalized phrase', () => { + expect(Srp.from(VALID_SRP_12).unwrap()).toBe(VALID_SRP_12); + expect(Srp.from(VALID_SRP_24).unwrap()).toBe(VALID_SRP_24); + }); + }); + + describe('redaction', () => { + let srp: Srp; + + beforeEach(() => { + srp = Srp.from(VALID_SRP_12); + }); + + it('redacts under util.inspect', () => { + const inspected = inspect(srp); + expect(inspected).toBe('[redacted]'); + expect(inspected).not.toContain('ball'); + }); + + it('redacts inside an inspected object', () => { + const inspected = inspect({ srp }); + expect(inspected).toContain('[redacted]'); + expect(inspected).not.toContain('ball'); + }); + + it('redacts under JSON.stringify', () => { + const serialized = JSON.stringify({ srp }); + expect(serialized).toBe('{"srp":"[redacted]"}'); + expect(serialized).not.toContain('ball'); + }); + + it('redacts under String() conversion', () => { + expect(String(srp)).toBe('[redacted]'); + }); + + it('redacts under template-literal interpolation', () => { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions -- We are intentionally exercising the redacting toString(). + const message = `srp is ${srp}`; + expect(message).toBe('srp is [redacted]'); + expect(message).not.toContain('ball'); + }); + }); +}); diff --git a/packages/wallet-cli/src/daemon/secrets.ts b/packages/wallet-cli/src/daemon/secrets.ts new file mode 100644 index 00000000000..af64973c49a --- /dev/null +++ b/packages/wallet-cli/src/daemon/secrets.ts @@ -0,0 +1,142 @@ +import { validateMnemonic } from '@metamask/scure-bip39'; +import { wordlist } from '@metamask/scure-bip39/dist/wordlists/english'; + +const REDACTED = '[redacted]'; + +const VALID_SRP_WORD_COUNTS: readonly number[] = [12, 15, 18, 21, 24]; + +const INSPECT_CUSTOM = Symbol.for('nodejs.util.inspect.custom'); + +const WORDLIST_SET: ReadonlySet = new Set(wordlist); + +/** + * Opaque wrapper around a wallet password. + * + * Constructed via {@link Password.from}, which validates the input. The + * underlying string is only reachable through {@link Password.unwrap}; every + * other path (`toString`, `JSON.stringify`, `util.inspect`, template-literal + * interpolation) yields `[redacted]`. This makes accidental logging produce a + * harmless placeholder instead of leaking the secret. + */ +export class Password { + readonly #value: string; + + // See .from() for why this is private. + // eslint-disable-next-line no-restricted-syntax + private constructor(value: string) { + this.#value = value; + } + + /** + * Wrap a non-empty string as a {@link Password}. + * + * Matches the `@metamask/keyring-controller` convention: any non-empty + * string is acceptable; minimum-length policy is left to the keyring. + * + * @param value - The raw password string. + * @returns A redacting {@link Password} wrapper. + * @throws If `value` is empty. + */ + static from(value: string): Password { + if (value.length === 0) { + throw new Error('Password must be a non-empty string'); + } + return new Password(value); + } + + /** + * Reveal the underlying password string. Call this only at trust boundaries + * (e.g. handing the value to the keyring or to a child-process env var). + * + * @returns The original password string. + */ + unwrap(): string { + return this.#value; + } + + toString(): string { + return REDACTED; + } + + toJSON(): string { + return REDACTED; + } + + [INSPECT_CUSTOM](): string { + return REDACTED; + } +} + +/** + * Opaque wrapper around a BIP-39 secret recovery phrase. + * + * Constructed via {@link Srp.from}, which validates the word count + * (12/15/18/21/24) and that every word is present in the BIP-39 English + * wordlist. The underlying string is only reachable through {@link Srp.unwrap}; + * every other path yields `[redacted]`. + */ +export class Srp { + readonly #value: string; + + // See .from() for why this is private. + // eslint-disable-next-line no-restricted-syntax + private constructor(value: string) { + this.#value = value; + } + + /** + * Validate and wrap a BIP-39 mnemonic phrase. + * + * Whitespace is normalized (trimmed and collapsed) before validation so that + * copy-pasted phrases with accidental leading/trailing/extra spaces are + * accepted. Catching malformed input here (rather than letting it reach + * `KeyringController:createNewVaultAndRestore`) produces a clearer error. + * + * @param value - The raw mnemonic string. + * @returns A redacting {@link Srp} wrapper containing the normalized phrase. + * @throws If the word count is not one of 12/15/18/21/24, if any word is + * not present in the BIP-39 English wordlist, or if the checksum is invalid. + */ + static from(value: string): Srp { + const words = value.trim().split(/\s+/u); + if (!VALID_SRP_WORD_COUNTS.includes(words.length)) { + throw new Error( + `Secret recovery phrase must be 12, 15, 18, 21, or 24 words (got ${words.length})`, + ); + } + for (const word of words) { + if (!WORDLIST_SET.has(word)) { + throw new Error( + 'Secret recovery phrase contains a word not in the BIP-39 English wordlist', + ); + } + } + + const normalized = words.join(' '); + if (!validateMnemonic(normalized, wordlist)) { + throw new Error('Secret recovery phrase has an invalid checksum'); + } + return new Srp(normalized); + } + + /** + * Reveal the underlying mnemonic string. Call this only at trust boundaries. + * + * @returns The original mnemonic string. + */ + unwrap(): string { + return this.#value; + } + + toString(): string { + return REDACTED; + } + + toJSON(): string { + return REDACTED; + } + + [INSPECT_CUSTOM](): string { + return REDACTED; + } +} diff --git a/packages/wallet-cli/src/daemon/types.ts b/packages/wallet-cli/src/daemon/types.ts index 0f46714535f..3d5b8f1b927 100644 --- a/packages/wallet-cli/src/daemon/types.ts +++ b/packages/wallet-cli/src/daemon/types.ts @@ -1,6 +1,8 @@ import type { Struct } from '@metamask/superstruct'; import type { Json } from '@metamask/utils'; +import type { Password, Srp } from './secrets'; + /** * Sink for daemon diagnostic messages. A backgrounded daemon's stdio may be * discarded, so hosts supply a logger that writes somewhere durable. @@ -101,7 +103,7 @@ export type DaemonStatusInfo = { export type DaemonSpawnConfig = { dataDir: string; infuraProjectId: string; - password: string; - srp: string; + password: Password; + srp: Srp; packageRoot: string; }; diff --git a/packages/wallet-cli/src/daemon/wallet-factory-integration.test.ts b/packages/wallet-cli/src/daemon/wallet-factory-integration.test.ts index a8a53498f35..10ae9a90f6a 100644 --- a/packages/wallet-cli/src/daemon/wallet-factory-integration.test.ts +++ b/packages/wallet-cli/src/daemon/wallet-factory-integration.test.ts @@ -1,3 +1,4 @@ +import { Password, Srp } from './secrets'; import { createWallet } from './wallet-factory'; // Unlike the unit test alongside it, this does NOT mock `@metamask/wallet`, so @@ -7,15 +8,16 @@ import { createWallet } from './wallet-factory'; // fetches in `updateRemoteFeatureFlags`, and NetworkController's `init()` is // synchronous and does not call `lookupNetwork`. -const TEST_SRP = 'test test test test test test test test test test test ball'; +const TEST_SRP = + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; const TEST_PASSWORD = 'testpass'; describe('createWallet (real Wallet, in-memory)', () => { it('constructs an unlocked wallet on first run and dispatches messenger actions', async () => { const { wallet, dispose } = await createWallet({ databasePath: ':memory:', - password: TEST_PASSWORD, - srp: TEST_SRP, + password: Password.from(TEST_PASSWORD), + srp: Srp.from(TEST_SRP), infuraProjectId: 'test-infura-id', log: () => undefined, }); diff --git a/packages/wallet-cli/src/daemon/wallet-factory.test.ts b/packages/wallet-cli/src/daemon/wallet-factory.test.ts index db1a24f9e0f..7d5444f0ad9 100644 --- a/packages/wallet-cli/src/daemon/wallet-factory.test.ts +++ b/packages/wallet-cli/src/daemon/wallet-factory.test.ts @@ -12,6 +12,7 @@ import { join } from 'node:path'; import { KeyValueStore } from '../persistence/KeyValueStore'; import * as persistenceModule from '../persistence/persistence'; +import { Password, Srp } from './secrets'; import { createWallet } from './wallet-factory'; jest.mock('@metamask/wallet'); @@ -24,12 +25,13 @@ const mockRm = jest.mocked(rm); const createdTempDbPaths: string[] = []; -const SRP = 'test test test test test test test test test test test ball'; +const SRP = + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; const CONFIG = { databasePath: ':memory:', - password: 'test-pass', - srp: SRP, + password: Password.from('test-pass'), + srp: Srp.from(SRP), infuraProjectId: 'test-infura-id', }; diff --git a/packages/wallet-cli/src/daemon/wallet-factory.ts b/packages/wallet-cli/src/daemon/wallet-factory.ts index e68eace3ddb..74ccc18efa4 100644 --- a/packages/wallet-cli/src/daemon/wallet-factory.ts +++ b/packages/wallet-cli/src/daemon/wallet-factory.ts @@ -16,14 +16,15 @@ import { rm } from 'node:fs/promises'; import { KeyValueStore } from '../persistence/KeyValueStore'; import { loadState, subscribeToChanges } from '../persistence/persistence'; +import type { Password, Srp } from './secrets'; import type { Logger } from './types'; const IN_MEMORY_DATABASE_PATH = ':memory:'; export type CreateWalletConfig = { databasePath: string; - password: string; - srp: string; + password: Password; + srp: Srp; infuraProjectId: string; log?: Logger; }; @@ -184,7 +185,7 @@ export async function createWallet({ } if (wasFirstRun) { - await importSecretRecoveryPhrase(wallet, password, srp); + await importSecretRecoveryPhrase(wallet, password.unwrap(), srp.unwrap()); } let disposePromise: Promise | undefined; diff --git a/packages/wallet-cli/tests/lifecycle.e2e.test.ts b/packages/wallet-cli/tests/lifecycle.e2e.test.ts index 9db668dfc7c..bbd25c44120 100644 --- a/packages/wallet-cli/tests/lifecycle.e2e.test.ts +++ b/packages/wallet-cli/tests/lifecycle.e2e.test.ts @@ -21,7 +21,8 @@ import { isProcessAlive, readPidFile } from '../src/daemon/utils'; // only action called here, `KeyringController:getState`, is local. // A valid 12-word BIP-39 mnemonic — the same fixtures the in-process e2e uses. -const TEST_SRP = 'test test test test test test test test test test test ball'; +const TEST_SRP = + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; const TEST_PASSWORD = 'testpass'; // NetworkController requires a project ID but is never reached over the network // here, so any well-formed-looking value works. diff --git a/yarn.lock b/yarn.lock index 1b7b3f7c166..9695068d5d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9045,6 +9045,7 @@ __metadata: "@metamask/base-controller": "npm:^9.1.0" "@metamask/remote-feature-flag-controller": "npm:^4.2.2" "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/scure-bip39": "npm:^2.1.1" "@metamask/storage-service": "npm:^1.0.2" "@metamask/superstruct": "npm:^3.1.0" "@metamask/utils": "npm:^11.11.0"