import * as createHash from 'create-hash'; import { pbkdf2 as pbkdf2Async, pbkdf2Sync as pbkdf2 } from 'pbkdf2'; import * as randomBytes from 'randombytes'; import { _default as _DEFAULT_WORDLIST, wordlists } from './_wordlists'; let DEFAULT_WORDLIST: string[] | undefined = _DEFAULT_WORDLIST; const INVALID_MNEMONIC = 'Invalid mnemonic'; const INVALID_ENTROPY = 'Invalid entropy'; const INVALID_CHECKSUM = 'Invalid mnemonic checksum'; const WORDLIST_REQUIRED = 'A wordlist is required but a default could not be found.\n' + 'Please explicitly pass a 2048 word array explicitly.'; function lpad(str: string, padString: string, length: number): string { while (str.length < length) str = padString + str; return str; } function binaryToByte(bin: string): number { return parseInt(bin, 2); } function bytesToBinary(bytes: number[]): string { return bytes.map(x => lpad(x.toString(2), '0', 8)).join(''); } function deriveChecksumBits(entropyBuffer: Buffer): string { const ENT = entropyBuffer.length * 8; const CS = ENT / 32; const hash = createHash('sha256') .update(entropyBuffer) .digest(); return bytesToBinary([...hash]).slice(0, CS); } function salt(password?: string): string { return 'mnemonic' + (password || ''); } export function mnemonicToSeed(mnemonic: string, password: string): Buffer { const mnemonicBuffer = Buffer.from( (mnemonic || '').normalize('NFKD'), 'utf8', ); const saltBuffer = Buffer.from( salt((password || '').normalize('NFKD')), 'utf8', ); return pbkdf2(mnemonicBuffer, saltBuffer, 2048, 64, 'sha512'); } export function mnemonicToSeedHex(mnemonic: string, password: string): string { return mnemonicToSeed(mnemonic, password).toString('hex'); } export function mnemonicToSeedAsync( mnemonic: string, password: string, ): Promise { return new Promise( (resolve, reject): void => { try { const mnemonicBuffer = Buffer.from( (mnemonic || '').normalize('NFKD'), 'utf8', ); const saltBuffer = Buffer.from( salt((password || '').normalize('NFKD')), 'utf8', ); pbkdf2Async( mnemonicBuffer, saltBuffer, 2048, 64, 'sha512', (err, data) => { if (err) return reject(err); else return resolve(data); }, ); } catch (error) { return reject(error); } }, ); } export async function mnemonicToSeedHexAsync( mnemonic: string, password: string, ): Promise { const buf = await mnemonicToSeedAsync(mnemonic, password); return buf.toString('hex'); } export function mnemonicToEntropy( mnemonic: string, wordlist?: string[], ): string { wordlist = wordlist || DEFAULT_WORDLIST; if (!wordlist) { throw new Error(WORDLIST_REQUIRED); } const words = (mnemonic || '').normalize('NFKD').split(' '); if (words.length % 3 !== 0) throw new Error(INVALID_MNEMONIC); // convert word indices to 11 bit binary strings const bits = words .map(word => { const index = wordlist!.indexOf(word); if (index === -1) throw new Error(INVALID_MNEMONIC); return lpad(index.toString(2), '0', 11); }) .join(''); // split the binary string into ENT/CS const dividerIndex = Math.floor(bits.length / 33) * 32; const entropyBits = bits.slice(0, dividerIndex); const checksumBits = bits.slice(dividerIndex); // calculate the checksum and compare const entropyBytes = entropyBits.match(/(.{1,8})/g)!.map(binaryToByte); if (entropyBytes.length < 16) throw new Error(INVALID_ENTROPY); if (entropyBytes.length > 32) throw new Error(INVALID_ENTROPY); if (entropyBytes.length % 4 !== 0) throw new Error(INVALID_ENTROPY); const entropy = Buffer.from(entropyBytes); const newChecksum = deriveChecksumBits(entropy); if (newChecksum !== checksumBits) throw new Error(INVALID_CHECKSUM); return entropy.toString('hex'); } export function entropyToMnemonic( entropy: Buffer | string, wordlist?: string[], ): string { if (!Buffer.isBuffer(entropy)) entropy = Buffer.from(entropy, 'hex'); wordlist = wordlist || DEFAULT_WORDLIST; if (!wordlist) { throw new Error(WORDLIST_REQUIRED); } // 128 <= ENT <= 256 if (entropy.length < 16) throw new TypeError(INVALID_ENTROPY); if (entropy.length > 32) throw new TypeError(INVALID_ENTROPY); if (entropy.length % 4 !== 0) throw new TypeError(INVALID_ENTROPY); const entropyBits = bytesToBinary([...entropy]); const checksumBits = deriveChecksumBits(entropy); const bits = entropyBits + checksumBits; const chunks = bits.match(/(.{1,11})/g)!; const words = chunks.map(binary => { const index = binaryToByte(binary); return wordlist![index]; }); return wordlist[0] === '\u3042\u3044\u3053\u304f\u3057\u3093' // Japanese wordlist ? words.join('\u3000') : words.join(' '); } export function generateMnemonic( strength?: number, rng?: (size: number) => Buffer, wordlist?: string[], ): string { strength = strength || 128; if (strength % 32 !== 0) throw new TypeError(INVALID_ENTROPY); rng = rng || randomBytes; return entropyToMnemonic(rng(strength / 8), wordlist); } export function validateMnemonic( mnemonic: string, wordlist?: string[], ): boolean { try { mnemonicToEntropy(mnemonic, wordlist); } catch (e) { return false; } return true; } export function setDefaultWordlist(language: string): void { const result = wordlists[language]; if (result) DEFAULT_WORDLIST = result; else throw new Error('Could not find wordlist for language "' + language + '"'); } export function getDefaultWordlist(): string { if (!DEFAULT_WORDLIST) throw new Error('No Default Wordlist set'); return Object.keys(wordlists).filter(lang => { if (lang === 'JA' || lang === 'EN') return false; return wordlists[lang].every( (word, index) => word === DEFAULT_WORDLIST![index], ); })[0]; } export { wordlists } from './_wordlists';