From d0fb5ed7a22d1f2f508a4b53516daa1ab472fe14 Mon Sep 17 00:00:00 2001 From: Jaco Date: Fri, 21 Apr 2023 11:05:20 +0300 Subject: [PATCH] Allow wordlist? param to mnemonic* functions --- .../util-crypto/src/mnemonic/generate.spec.ts | 23 ++++++++++++++++--- packages/util-crypto/src/mnemonic/generate.ts | 6 ++--- .../src/mnemonic/toEntropy.spec.ts | 14 ++++++++++- .../util-crypto/src/mnemonic/toEntropy.ts | 6 ++--- .../src/mnemonic/toMiniSecret.spec.ts | 22 ++++++++++++++---- .../util-crypto/src/mnemonic/toMiniSecret.ts | 10 ++++---- .../src/mnemonic/toMiniSecretCmp.spec.ts | 4 ++-- .../util-crypto/src/mnemonic/validate.spec.ts | 16 +++++++++++-- packages/util-crypto/src/mnemonic/validate.ts | 6 ++--- 9 files changed, 79 insertions(+), 28 deletions(-) diff --git a/packages/util-crypto/src/mnemonic/generate.spec.ts b/packages/util-crypto/src/mnemonic/generate.spec.ts index fcd3ea20325..2eab8e3e80f 100644 --- a/packages/util-crypto/src/mnemonic/generate.spec.ts +++ b/packages/util-crypto/src/mnemonic/generate.spec.ts @@ -4,6 +4,7 @@ /// import { cryptoWaitReady } from '../index.js'; +import { french as frenchWords } from './wordlists/index.js'; import { mnemonicGenerate } from './generate.js'; import { mnemonicValidate } from './validate.js'; @@ -16,11 +17,27 @@ describe('mnemonicGenerate', (): void => { ).toEqual(true); }); + it('generates a french mnemonic', (): void => { + const mnemonic = mnemonicGenerate(24, frenchWords); + const words = mnemonic.split(' '); + + expect(words).toHaveLength(24); + expect( + mnemonicValidate(mnemonic, frenchWords) + ).toEqual(true); + expect( + mnemonicValidate(mnemonic) + ).toEqual(false); + expect( + words.every((w) => frenchWords.includes(w)) + ).toEqual(true); + }); + for (const onlyJs of [false, true]) { describe(`onlyJs=${(onlyJs && 'true') || 'false'}`, (): void => { for (const num of [12, 15, 18, 21, 24] as const) { it(`generates a valid mnemonic (${num} words)`, (): void => { - const mnemonic = mnemonicGenerate(num, onlyJs); + const mnemonic = mnemonicGenerate(num, undefined, onlyJs); const isValid = mnemonicValidate(mnemonic); expect(mnemonic.split(' ')).toHaveLength(num); @@ -29,8 +46,8 @@ describe('mnemonicGenerate', (): void => { } it('generates non-deterministic', (): void => { - const m1 = mnemonicGenerate(24, onlyJs); - const m2 = mnemonicGenerate(24, onlyJs); + const m1 = mnemonicGenerate(24, undefined, onlyJs); + const m2 = mnemonicGenerate(24, undefined, onlyJs); expect(m1 === m2).toEqual(false); expect(mnemonicValidate(m1)).toEqual(true); diff --git a/packages/util-crypto/src/mnemonic/generate.ts b/packages/util-crypto/src/mnemonic/generate.ts index 991a9e8ce0e..14e0f48294e 100644 --- a/packages/util-crypto/src/mnemonic/generate.ts +++ b/packages/util-crypto/src/mnemonic/generate.ts @@ -18,8 +18,8 @@ import { generateMnemonic } from './bip39.js'; * const mnemonic = mnemonicGenerate(); // => string * ``` */ -export function mnemonicGenerate (numWords: 12 | 15 | 18 | 21 | 24 = 12, onlyJs?: boolean): string { - return !hasBigInt || (!onlyJs && isReady()) +export function mnemonicGenerate (numWords: 12 | 15 | 18 | 21 | 24 = 12, wordlist?: string[], onlyJs?: boolean): string { + return !hasBigInt || (!wordlist && !onlyJs && isReady()) ? bip39Generate(numWords) - : generateMnemonic(numWords); + : generateMnemonic(numWords, wordlist); } diff --git a/packages/util-crypto/src/mnemonic/toEntropy.spec.ts b/packages/util-crypto/src/mnemonic/toEntropy.spec.ts index 7a1bd786852..70adddb1f4d 100644 --- a/packages/util-crypto/src/mnemonic/toEntropy.spec.ts +++ b/packages/util-crypto/src/mnemonic/toEntropy.spec.ts @@ -7,6 +7,7 @@ import { u8aToHex } from '@polkadot/util'; import { cryptoWaitReady } from '../index.js'; import tests from '../sr25519/pair/testing.spec.js'; +import { french as frenchWords } from './wordlists/index.js'; import { mnemonicToEntropy } from './toEntropy.js'; await cryptoWaitReady(); @@ -16,9 +17,20 @@ describe('mnemonicToEntropy', (): void => { describe(`onlyJs=${(onlyJs && 'true') || 'false'}`, (): void => { tests.forEach(([mnemonic, entropy], index): void => { it(`Created correct entropy for ${index}`, (): void => { - expect(u8aToHex(mnemonicToEntropy(mnemonic, onlyJs))).toEqual(entropy); + expect(u8aToHex(mnemonicToEntropy(mnemonic, undefined, onlyJs))).toEqual(entropy); }); }); }); } + + it('has the correct entropy for non-Englist mnemonics', (): void => { + const mnemonic = 'pompier circuler pulpe injure aspect abyssal nuque boueux équerre balisage pieuvre médecin petit suffixe soleil cumuler monstre arlequin liasse pixel garrigue noble buisson scandale'; + + expect( + () => mnemonicToEntropy(mnemonic) + ).toThrow(); + expect( + mnemonicToEntropy(mnemonic, frenchWords) + ).toEqual(new Uint8Array([189, 230, 55, 17, 65, 33, 40, 4, 106, 9, 11, 88, 227, 26, 229, 76, 59, 123, 200, 55, 177, 232, 158, 66, 34, 54, 93, 54, 255, 74, 137, 70])); + }); }); diff --git a/packages/util-crypto/src/mnemonic/toEntropy.ts b/packages/util-crypto/src/mnemonic/toEntropy.ts index d07a05f3cb8..9e6b3140158 100644 --- a/packages/util-crypto/src/mnemonic/toEntropy.ts +++ b/packages/util-crypto/src/mnemonic/toEntropy.ts @@ -6,8 +6,8 @@ import { bip39ToEntropy, isReady } from '@polkadot/wasm-crypto'; import { mnemonicToEntropy as jsToEntropy } from './bip39.js'; -export function mnemonicToEntropy (mnemonic: string, onlyJs?: boolean): Uint8Array { - return !hasBigInt || (!onlyJs && isReady()) +export function mnemonicToEntropy (mnemonic: string, wordlist?: string[], onlyJs?: boolean): Uint8Array { + return !hasBigInt || (!wordlist && !onlyJs && isReady()) ? bip39ToEntropy(mnemonic) - : jsToEntropy(mnemonic); + : jsToEntropy(mnemonic, wordlist); } diff --git a/packages/util-crypto/src/mnemonic/toMiniSecret.spec.ts b/packages/util-crypto/src/mnemonic/toMiniSecret.spec.ts index 0c2e99694da..e297dc7399c 100644 --- a/packages/util-crypto/src/mnemonic/toMiniSecret.spec.ts +++ b/packages/util-crypto/src/mnemonic/toMiniSecret.spec.ts @@ -7,6 +7,7 @@ import { u8aEq, u8aToHex } from '@polkadot/util'; import { cryptoWaitReady } from '../index.js'; import tests from '../sr25519/pair/testing.spec.js'; +import { korean as koreanWords } from './wordlists/index.js'; import { mnemonicToMiniSecret } from './toMiniSecret.js'; const MNEMONIC = 'seed sock milk update focus rotate barely fade car face mechanic mercy'; @@ -19,31 +20,42 @@ describe('mnemonicToMiniSecret', (): void => { it(`generates Wasm & Js equivalents for password=${password || 'undefined'}`, (): void => { expect( u8aEq( - mnemonicToMiniSecret(MNEMONIC, password, true), - mnemonicToMiniSecret(MNEMONIC, password, false) + mnemonicToMiniSecret(MNEMONIC, password, undefined, true), + mnemonicToMiniSecret(MNEMONIC, password, undefined, false) ) ).toEqual(true); }); } + it('creates a known minisecret from a non-english mnemonic', (): void => { + const mnemonic = '엉덩이 능동적 숫자 팩시밀리 비난 서적 파출소 도움 독창적 인생 상류 먼지 답변 음반 수박 사업 노란색 공사 우체국 특급 도대체 금지 굉장히 고무신'; + + expect( + () => mnemonicToMiniSecret(mnemonic, 'testing') + ).toThrow(); + expect( + u8aToHex(mnemonicToMiniSecret(mnemonic, 'testing', koreanWords)) + ).toEqual('0xefa278a62535581767a2f49cb542ed91b65fb911e1b05e7a09c702b257f10c13'); + }); + for (const onlyJs of [false, true]) { describe(`onlyJs=${(onlyJs && 'true') || 'false'}`, (): void => { it('generates a valid seed', (): void => { expect( - u8aToHex(mnemonicToMiniSecret(MNEMONIC, undefined, onlyJs)) + u8aToHex(mnemonicToMiniSecret(MNEMONIC, undefined, undefined, onlyJs)) ).toEqual(SEED); }); it('fails with non-mnemonics', (): void => { expect( - () => mnemonicToMiniSecret('foo bar baz', undefined, onlyJs) + () => mnemonicToMiniSecret('foo bar baz', undefined, undefined, onlyJs) ).toThrow(/mnemonic specified/); }); tests.forEach(([mnemonic, , seed], index): void => { it(`Created correct seed for ${index}`, (): void => { expect( - u8aToHex(mnemonicToMiniSecret(mnemonic, 'Substrate', onlyJs)) + u8aToHex(mnemonicToMiniSecret(mnemonic, 'Substrate', undefined, onlyJs)) ).toEqual( // mini returned here, only check first 32-bytes (64 hex + 2 prefix) seed.substring(0, 66) diff --git a/packages/util-crypto/src/mnemonic/toMiniSecret.ts b/packages/util-crypto/src/mnemonic/toMiniSecret.ts index a17f6526110..0373680569f 100644 --- a/packages/util-crypto/src/mnemonic/toMiniSecret.ts +++ b/packages/util-crypto/src/mnemonic/toMiniSecret.ts @@ -8,16 +8,14 @@ import { pbkdf2Encode } from '../pbkdf2/index.js'; import { mnemonicToEntropy } from './toEntropy.js'; import { mnemonicValidate } from './validate.js'; -export function mnemonicToMiniSecret (mnemonic: string, password = '', onlyJs?: boolean): Uint8Array { - if (!mnemonicValidate(mnemonic)) { +export function mnemonicToMiniSecret (mnemonic: string, password = '', wordlist?: string[], onlyJs?: boolean): Uint8Array { + if (!mnemonicValidate(mnemonic, wordlist, onlyJs)) { throw new Error('Invalid bip39 mnemonic specified'); - } - - if (!onlyJs && isReady()) { + } else if (!wordlist && !onlyJs && isReady()) { return bip39ToMiniSecret(mnemonic, password); } - const entropy = mnemonicToEntropy(mnemonic); + const entropy = mnemonicToEntropy(mnemonic, wordlist); const salt = stringToU8a(`mnemonic${password}`); // return the first 32 bytes as the seed diff --git a/packages/util-crypto/src/mnemonic/toMiniSecretCmp.spec.ts b/packages/util-crypto/src/mnemonic/toMiniSecretCmp.spec.ts index 11b430bfd65..0a10a20be60 100644 --- a/packages/util-crypto/src/mnemonic/toMiniSecretCmp.spec.ts +++ b/packages/util-crypto/src/mnemonic/toMiniSecretCmp.spec.ts @@ -32,10 +32,10 @@ for (const onlyJsMnemonic of [false, true]) { // do iterations to check and re-check that all matches for (const count of arrayRange(NUM_CHECKS)) { it(`check=${count + 1}`, (): void => { - const minisecret = mnemonicToMiniSecret(mnemonic, count ? `${count}` : '', onlyJsMnemonic); + const minisecret = mnemonicToMiniSecret(mnemonic, count ? `${count}` : '', undefined, onlyJsMnemonic); const edpub = ed25519PairFromSeed(minisecret).publicKey; const srpub = sr25519PairFromSeed(minisecret).publicKey; - const testmini = mnemonicToMiniSecret(mnemonic, count ? `${count}` : '', onlyJsMini); + const testmini = mnemonicToMiniSecret(mnemonic, count ? `${count}` : '', undefined, onlyJsMini); // explicit minisecret compare expect( diff --git a/packages/util-crypto/src/mnemonic/validate.spec.ts b/packages/util-crypto/src/mnemonic/validate.spec.ts index 37b58d266e4..9afb4539ec2 100644 --- a/packages/util-crypto/src/mnemonic/validate.spec.ts +++ b/packages/util-crypto/src/mnemonic/validate.spec.ts @@ -4,6 +4,7 @@ /// import { cryptoWaitReady } from '../index.js'; +import { french as frenchWords } from './wordlists/index.js'; import { mnemonicValidate } from './validate.js'; await cryptoWaitReady(); @@ -13,15 +14,26 @@ describe('mnemonicValidate', (): void => { describe(`onlyJs=${(onlyJs && 'true') || 'false'}`, (): void => { it('returns true on valid', (): void => { expect( - mnemonicValidate('seed sock milk update focus rotate barely fade car face mechanic mercy', onlyJs) + mnemonicValidate('seed sock milk update focus rotate barely fade car face mechanic mercy', undefined, onlyJs) ).toEqual(true); }); it('returns false on invalid', (): void => { expect( - mnemonicValidate('wine photo extra cushion basket dwarf humor cloud truck job boat submit', onlyJs) + mnemonicValidate('wine photo extra cushion basket dwarf humor cloud truck job boat submit', undefined, onlyJs) ).toEqual(false); }); }); } + + it('allows usage of a different wordlist', (): void => { + const mnemonic = 'pompier circuler pulpe injure aspect abyssal nuque boueux équerre balisage pieuvre médecin petit suffixe soleil cumuler monstre arlequin liasse pixel garrigue noble buisson scandale'; + + expect( + mnemonicValidate(mnemonic, frenchWords) + ).toEqual(true); + expect( + mnemonicValidate(mnemonic) + ).toEqual(false); + }); }); diff --git a/packages/util-crypto/src/mnemonic/validate.ts b/packages/util-crypto/src/mnemonic/validate.ts index 57a17e75442..d7f87610638 100644 --- a/packages/util-crypto/src/mnemonic/validate.ts +++ b/packages/util-crypto/src/mnemonic/validate.ts @@ -19,8 +19,8 @@ import { validateMnemonic } from './bip39.js'; * const isValidMnemonic = mnemonicValidate(mnemonic); // => boolean * ``` */ -export function mnemonicValidate (mnemonic: string, onlyJs?: boolean): boolean { - return !hasBigInt || (!onlyJs && isReady()) +export function mnemonicValidate (mnemonic: string, wordlist?: string[], onlyJs?: boolean): boolean { + return !hasBigInt || (!wordlist && !onlyJs && isReady()) ? bip39Validate(mnemonic) - : validateMnemonic(mnemonic); + : validateMnemonic(mnemonic, wordlist); }