From 58417b8e4c547bd0fe83deaa646f479b4a3323c7 Mon Sep 17 00:00:00 2001 From: HippocampusSK Date: Mon, 9 Nov 2026 10:37:43 +0000 Subject: [PATCH 1/2] fix(wallets): canonicalise keystore phrases before derivation Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y7T5QKWXE2vQmr4U7aSQak --- .changeset/canonical-mnemonic-phrase.md | 5 +++++ packages/wallets/src/keystore-helpers.ts | 11 ++++++++--- packages/wallets/src/keystore.ts | 4 +++- packages/wallets/test/keystore.test.ts | 25 ++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 .changeset/canonical-mnemonic-phrase.md diff --git a/.changeset/canonical-mnemonic-phrase.md b/.changeset/canonical-mnemonic-phrase.md new file mode 100644 index 00000000..ed8e184e --- /dev/null +++ b/.changeset/canonical-mnemonic-phrase.md @@ -0,0 +1,5 @@ +--- +"@swapkit/wallets": patch +--- + +The keystore wallet now cleans up a seed phrase before deriving any key, using `canonicalizeMnemonic` from `@swapkit/helpers`. A phrase pasted with tabs, repeated spaces, non-breaking spaces or a trailing newline connects to the same addresses as the clean phrase, a phrase with a bad checksum is rejected with `wallet_keystore_invalid_phrase`, and `validatePhrase` follows the same rules. Clean phrases are unaffected. diff --git a/packages/wallets/src/keystore-helpers.ts b/packages/wallets/src/keystore-helpers.ts index 1bde34bf..1d8eea83 100644 --- a/packages/wallets/src/keystore-helpers.ts +++ b/packages/wallets/src/keystore-helpers.ts @@ -1,7 +1,7 @@ import { createCipheriv, createDecipheriv, pbkdf2Sync, randomBytes } from "node:crypto"; -import { generateMnemonic, validateMnemonic } from "@scure/bip39"; +import { generateMnemonic } from "@scure/bip39"; import { wordlist } from "@scure/bip39/wordlists/english.js"; -import { SwapKitError } from "@swapkit/helpers"; +import { canonicalizeMnemonic, SwapKitError } from "@swapkit/helpers"; export type PhraseWordCount = 12 | 15 | 18 | 21 | 24; @@ -77,7 +77,12 @@ export async function generateKeystore({ } export function validatePhrase(phrase: string) { - return validateMnemonic(phrase, wordlist); + try { + canonicalizeMnemonic(phrase); + return true; + } catch { + return false; + } } export async function decryptFromKeystore(keystore: Keystore, password: string) { diff --git a/packages/wallets/src/keystore.ts b/packages/wallets/src/keystore.ts index 39a61574..23539f06 100644 --- a/packages/wallets/src/keystore.ts +++ b/packages/wallets/src/keystore.ts @@ -2,6 +2,7 @@ import { ACCOUNT_INDEXED_CHAINS, type AssetValue, Chain, + canonicalizeMnemonic, CosmosChains, type DerivationPathArray, EVMChains, @@ -209,6 +210,7 @@ export async function createKeystoreWallet({ supportedChains: KEYSTORE_SUPPORTED_CHAINS, walletType: WalletOption.KEYSTORE, }); + const canonicalPhrase = canonicalizeMnemonic(phrase); const walletIndex = typeof derivationPathMapOrIndex === "number" ? derivationPathMapOrIndex : 0; const walletResults = await Promise.allSettled( @@ -241,7 +243,7 @@ export async function createKeystoreWallet({ const toolbox = await getToolbox(chain, { derivationPath, - phrase, + phrase: canonicalPhrase, ...(resolvedScriptType ? { scriptType: resolvedScriptType } : {}), }); const address = (await toolbox.getAddress()) || ""; diff --git a/packages/wallets/test/keystore.test.ts b/packages/wallets/test/keystore.test.ts index 14cafa07..245df895 100644 --- a/packages/wallets/test/keystore.test.ts +++ b/packages/wallets/test/keystore.test.ts @@ -93,3 +93,28 @@ describe("keystore phrase generation", () => { expect(await decryptFromKeystore(keystore, "password")).toBe(phrase); }); }); + +describe("keystore phrase canonicalisation", () => { + test("a phrase with stray whitespace connects to the same addresses as the clean phrase", async () => { + const messy = `\n${TEST_PHRASE.split(" ").join("\t ")}\u00a0\r\n`; + const chains = [Chain.Bitcoin, Chain.Ethereum, Chain.Solana]; + + const clean = await createKeystoreWallet({ chains, phrase: TEST_PHRASE }); + const dirty = await createKeystoreWallet({ chains, phrase: messy }); + + for (const chain of chains) { + expect(clean[chain]?.address).toBeTruthy(); + expect(`${chain}: ${dirty[chain]?.address}`).toBe(`${chain}: ${clean[chain]?.address}`); + } + expect(validatePhrase(messy)).toBe(true); + }); + + test("a phrase with a bad checksum is rejected before any chain connects", async () => { + const badChecksum = TEST_PHRASE.replace("about", "abandon"); + + expect(validatePhrase(badChecksum)).toBe(false); + await expect(createKeystoreWallet({ chains: [Chain.Ethereum], phrase: badChecksum })).rejects.toThrow( + "wallet_keystore_invalid_phrase", + ); + }); +}); From cf691e2db91d433ca01f6cd1ec2b3b7722cd1319 Mon Sep 17 00:00:00 2001 From: HippocampusSK Date: Mon, 9 Nov 2026 10:37:56 +0000 Subject: [PATCH 2/2] style(wallets): sort keystore imports Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y7T5QKWXE2vQmr4U7aSQak --- packages/wallets/src/keystore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wallets/src/keystore.ts b/packages/wallets/src/keystore.ts index 23539f06..287359fb 100644 --- a/packages/wallets/src/keystore.ts +++ b/packages/wallets/src/keystore.ts @@ -2,8 +2,8 @@ import { ACCOUNT_INDEXED_CHAINS, type AssetValue, Chain, - canonicalizeMnemonic, CosmosChains, + canonicalizeMnemonic, type DerivationPathArray, EVMChains, filterSupportedChains,