1 | import { stringToU8a, u8aToU8a } from '@polkadot/util';
|
2 | import { pbkdf2Encode } from '../pbkdf2/index.js';
|
3 | import { randomAsU8a } from '../random/index.js';
|
4 | import { sha256AsU8a } from '../sha/index.js';
|
5 | import DEFAULT_WORDLIST from './wordlists/en.js';
|
6 | const INVALID_MNEMONIC = 'Invalid mnemonic';
|
7 | const INVALID_ENTROPY = 'Invalid entropy';
|
8 | const INVALID_CHECKSUM = 'Invalid mnemonic checksum';
|
9 |
|
10 | function normalize(str) {
|
11 | return (str || '').normalize('NFKD');
|
12 | }
|
13 |
|
14 | function binaryToByte(bin) {
|
15 | return parseInt(bin, 2);
|
16 | }
|
17 |
|
18 | function bytesToBinary(bytes) {
|
19 | return bytes.map((x) => x.toString(2).padStart(8, '0')).join('');
|
20 | }
|
21 |
|
22 | function deriveChecksumBits(entropyBuffer) {
|
23 | return bytesToBinary(Array.from(sha256AsU8a(entropyBuffer))).slice(0, (entropyBuffer.length * 8) / 32);
|
24 | }
|
25 | export function mnemonicToSeedSync(mnemonic, password) {
|
26 | return pbkdf2Encode(stringToU8a(normalize(mnemonic)), stringToU8a(`mnemonic${normalize(password)}`)).password;
|
27 | }
|
28 | export function mnemonicToEntropy(mnemonic, wordlist = DEFAULT_WORDLIST) {
|
29 | const words = normalize(mnemonic).split(' ');
|
30 | if (words.length % 3 !== 0) {
|
31 | throw new Error(INVALID_MNEMONIC);
|
32 | }
|
33 |
|
34 | const bits = words
|
35 | .map((word) => {
|
36 | const index = wordlist.indexOf(word);
|
37 | if (index === -1) {
|
38 | throw new Error(INVALID_MNEMONIC);
|
39 | }
|
40 | return index.toString(2).padStart(11, '0');
|
41 | })
|
42 | .join('');
|
43 |
|
44 | const dividerIndex = Math.floor(bits.length / 33) * 32;
|
45 | const entropyBits = bits.slice(0, dividerIndex);
|
46 | const checksumBits = bits.slice(dividerIndex);
|
47 |
|
48 | const matched = entropyBits.match(/(.{1,8})/g);
|
49 | const entropyBytes = matched?.map(binaryToByte);
|
50 | if (!entropyBytes || (entropyBytes.length % 4 !== 0) || (entropyBytes.length < 16) || (entropyBytes.length > 32)) {
|
51 | throw new Error(INVALID_ENTROPY);
|
52 | }
|
53 | const entropy = u8aToU8a(entropyBytes);
|
54 | if (deriveChecksumBits(entropy) !== checksumBits) {
|
55 | throw new Error(INVALID_CHECKSUM);
|
56 | }
|
57 | return entropy;
|
58 | }
|
59 | export function entropyToMnemonic(entropy, wordlist = DEFAULT_WORDLIST) {
|
60 |
|
61 | if ((entropy.length % 4 !== 0) || (entropy.length < 16) || (entropy.length > 32)) {
|
62 | throw new Error(INVALID_ENTROPY);
|
63 | }
|
64 | const matched = `${bytesToBinary(Array.from(entropy))}${deriveChecksumBits(entropy)}`.match(/(.{1,11})/g);
|
65 | const mapped = matched?.map((b) => wordlist[binaryToByte(b)]);
|
66 | if (!mapped || (mapped.length < 12)) {
|
67 | throw new Error('Unable to map entropy to mnemonic');
|
68 | }
|
69 | return mapped.join(' ');
|
70 | }
|
71 | export function generateMnemonic(numWords, wordlist) {
|
72 | return entropyToMnemonic(randomAsU8a((numWords / 3) * 4), wordlist);
|
73 | }
|
74 | export function validateMnemonic(mnemonic, wordlist) {
|
75 | try {
|
76 | mnemonicToEntropy(mnemonic, wordlist);
|
77 | }
|
78 | catch {
|
79 | return false;
|
80 | }
|
81 | return true;
|
82 | }
|