diff --git a/packages/keyring/src/index.spec.ts b/packages/keyring/src/index.spec.ts index ccace016a..8f6df0dc2 100644 --- a/packages/keyring/src/index.spec.ts +++ b/packages/keyring/src/index.spec.ts @@ -6,7 +6,8 @@ import type { KeyringPair$Json } from './types.js'; import { hexToU8a, stringToU8a } from '@polkadot/util'; -import { base64Decode, cryptoWaitReady, encodeAddress, randomAsU8a, setSS58Format } from '@polkadot/util-crypto'; +import { base64Decode, cryptoWaitReady, encodeAddress, mnemonicGenerate, randomAsU8a, setSS58Format } from '@polkadot/util-crypto'; +import * as languages from '@polkadot/util-crypto/mnemonic/wordlists/index'; import { decodePair } from './pair/decode.js'; import Keyring from './index.js'; @@ -574,4 +575,35 @@ describe('keypair', (): void => { expect(pair.address).toBe('FLiSDPCcJ6auZUGXALLj6jpahcP6adVFDBUQznPXUQ7yoqH'); }); }); + + describe('wordlist', (): void => { + it('creates keypair from different wordlists mnemonics', (): void => { + Object.keys(languages).forEach((language) => { + const mnemonic = mnemonicGenerate(12, languages[language as keyof typeof languages]); + const keyring = new Keyring({ + type: 'ed25519' + }); + + expect(keyring.addFromMnemonic( + mnemonic, + {}, + 'ed25519', + languages[language as keyof typeof languages] + )).toBeDefined(); + }); + }); + it('cannot create from invalid wordlist', (): void => { + const mnemonic = mnemonicGenerate(12, languages.japanese); + const keyring = new Keyring({ + type: 'ed25519' + }); + + expect(() => keyring.addFromMnemonic( + mnemonic, + {}, + 'ed25519', + languages.english + )).toThrow('Invalid bip39 mnemonic specified'); + }); + }); }); diff --git a/packages/keyring/src/keyring.ts b/packages/keyring/src/keyring.ts index 1fa9d436f..452347a5b 100644 --- a/packages/keyring/src/keyring.ts +++ b/packages/keyring/src/keyring.ts @@ -121,8 +121,8 @@ export class Keyring implements KeyringInstance { * of an account backup), and then generates a keyring pair from it that it passes to * `addPair` to stores in a keyring pair dictionary the public key of the generated pair as a key and the pair as the associated value. */ - public addFromMnemonic (mnemonic: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type): KeyringPair { - return this.addFromUri(mnemonic, meta, type); + public addFromMnemonic (mnemonic: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type, wordlist?: string[]): KeyringPair { + return this.addFromUri(mnemonic, meta, type, wordlist); } /** @@ -153,9 +153,9 @@ export class Keyring implements KeyringInstance { * @summary Creates an account via an suri * @description Extracts the phrase, path and password from a SURI format for specifying secret keys `//////` (the `///password` may be omitted, and `/` and `//` maybe repeated and mixed). The secret can be a hex string, mnemonic phrase or a string (to be padded) */ - public addFromUri (suri: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type): KeyringPair { + public addFromUri (suri: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type, wordlist?: string[]): KeyringPair { return this.addPair( - this.createFromUri(suri, meta, type) + this.createFromUri(suri, meta, type, wordlist) ); } @@ -203,7 +203,7 @@ export class Keyring implements KeyringInstance { * @summary Creates a Keypair from an suri * @description This creates a pair from the suri, but does not add it to the keyring */ - public createFromUri (_suri: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type): KeyringPair { + public createFromUri (_suri: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type, wordlist?: string[]): KeyringPair { // here we only aut-add the dev phrase if we have a hard-derived path const suri = _suri.startsWith('//') ? `${DEV_PHRASE}${_suri}` @@ -220,7 +220,7 @@ export class Keyring implements KeyringInstance { if ([12, 15, 18, 21, 24].includes(parts.length)) { seed = type === 'ethereum' ? mnemonicToLegacySeed(phrase, '', false, 64) - : mnemonicToMiniSecret(phrase, password); + : mnemonicToMiniSecret(phrase, password, wordlist); } else { if (phrase.length > 32) { throw new Error('specified phrase is not a valid mnemonic and is invalid as a raw seed at > 32 bytes'); diff --git a/packages/keyring/src/types.ts b/packages/keyring/src/types.ts index e027098f5..0638d3270 100644 --- a/packages/keyring/src/types.ts +++ b/packages/keyring/src/types.ts @@ -116,13 +116,13 @@ export interface KeyringInstance { addPair (pair: KeyringPair): KeyringPair; addFromAddress (address: string | Uint8Array, meta?: KeyringPair$Meta, encoded?: Uint8Array | null, type?: KeypairType, ignoreChecksum?: boolean): KeyringPair; addFromJson (pair: KeyringPair$Json, ignoreChecksum?: boolean): KeyringPair; - addFromMnemonic (mnemonic: string, meta?: KeyringPair$Meta, type?: KeypairType): KeyringPair; + addFromMnemonic (mnemonic: string, meta?: KeyringPair$Meta, type?: KeypairType, wordlist?: string[]): KeyringPair; addFromPair (pair: Keypair, meta?: KeyringPair$Meta, type?: KeypairType): KeyringPair addFromSeed (seed: Uint8Array, meta?: KeyringPair$Meta, type?: KeypairType): KeyringPair; - addFromUri (suri: string, meta?: KeyringPair$Meta, type?: KeypairType): KeyringPair; + addFromUri (suri: string, meta?: KeyringPair$Meta, type?: KeypairType, wordlist?: string[]): KeyringPair; createFromJson (json: KeyringPair$Json, ignoreChecksum?: boolean): KeyringPair; createFromPair (pair: Keypair, meta: KeyringPair$Meta, type: KeypairType): KeyringPair - createFromUri (suri: string, meta?: KeyringPair$Meta, type?: KeypairType): KeyringPair; + createFromUri (suri: string, meta?: KeyringPair$Meta, type?: KeypairType, wordlist?: string[]): KeyringPair; getPair (address: string | Uint8Array): KeyringPair; getPairs (): KeyringPair[]; getPublicKeys (): Uint8Array[]; diff --git a/packages/util-crypto/src/key/extractSuri.spec.ts b/packages/util-crypto/src/key/extractSuri.spec.ts index c3466452b..a5d7ed469 100644 --- a/packages/util-crypto/src/key/extractSuri.spec.ts +++ b/packages/util-crypto/src/key/extractSuri.spec.ts @@ -126,4 +126,22 @@ describe('keyExtractSuri', (): void => { expect(test.path[0].isHard).toEqual(true); expect(test.path[0].chainCode).toEqual(Uint8Array.from([20, 65, 108, 105, 99, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])); }); + + it('derives on uncommon characters', (): void => { + const languageMnemonics = { + chineseSimplified: '熙 礼 淀 谋 耗 搜 雨 瑞 雷 合 析 感', + chineseTraditional: '召 胸 捕 乏 講 祥 隙 幫 動 框 場 給', + french: 'ruiner minute maison ouragan palourde piscine nerveux descente romance édifier ancien médaille', + japanese: 'ほったん はちみつ おやゆび ほかん いりぐち さんいん てぶくろ だいじょうぶ ふとん でぬかえ ちしき あわてる', + korean: '김밥 방향 논리 저절로 증상 지진 회장 오히려 시리즈 최근 학용품 곡식' + }; + + Object.keys(languageMnemonics).forEach((mnemonic) => { + const test = keyExtractSuri(mnemonic); + + expect(test.password).not.toBeDefined(); + expect(test.phrase).toEqual(mnemonic); + expect(test.path.length).toEqual(0); + }); + }); }); diff --git a/packages/util-crypto/src/key/extractSuri.ts b/packages/util-crypto/src/key/extractSuri.ts index e44c4608c..803d89c27 100644 --- a/packages/util-crypto/src/key/extractSuri.ts +++ b/packages/util-crypto/src/key/extractSuri.ts @@ -12,14 +12,17 @@ export interface ExtractResult { phrase: string; } -const RE_CAPTURE = /^(\w+( \w+)*)((\/\/?[^/]+)*)(\/\/\/(.*))?$/; +const RE_CAPTURE = /^((0x[a-fA-F0-9]+|[\p{L}\d]+(?: [\p{L}\d]+)*))((\/\/?[^/]+)*)(\/\/\/(.*))?$/u; /** * @description Extracts the phrase, path and password from a SURI format for specifying secret keys `//////` (the `///password` may be omitted, and `/` and `//` maybe repeated and mixed). */ export function keyExtractSuri (suri: string): ExtractResult { + // Normalize Unicode to NFC to avoid accent-related mismatches + const normalizedSuri = suri.normalize('NFC'); + // eslint-disable-next-line @typescript-eslint/prefer-regexp-exec - const matches = suri.match(RE_CAPTURE); + const matches = normalizedSuri.match(RE_CAPTURE); if (matches === null) { throw new Error('Unable to match provided value to a secret URI');