diff --git a/web/cashtab/src/hooks/useWallet.js b/web/cashtab/src/hooks/useWallet.js --- a/web/cashtab/src/hooks/useWallet.js +++ b/web/cashtab/src/hooks/useWallet.js @@ -839,34 +839,8 @@ }; const validateMnemonic = (mnemonic, wordlist = bip39.wordlists.english) => { - let mnemonicTestOutput; - - // temporary validation of wordlist exclusion - // to be removed in next diff in stack - console.log('english: ' + bip39.wordlists.english); - if ( - !bip39.wordlists.japanese && - !bip39.wordlists.spanish && - !bip39.wordlists.italian && - !bip39.wordlists.french && - !bip39.wordlists.korean && - !bip39.wordlists.czech && - !bip39.wordlists.portuguese && - !bip39.wordlists.chinese_traditional - ) { - console.log( - 'bip39 wordlist is excluding japanese, spanish, italian, french, korean, czech, portuguese and chinese', - ); - } - try { - mnemonicTestOutput = validateMnemonicWordList(mnemonic, wordlist); - - if (mnemonicTestOutput === 'Valid mnemonic') { - return true; - } else { - return false; - } + return validateMnemonicWordList(mnemonic, wordlist); } catch (err) { console.log(err); return false; diff --git a/web/cashtab/src/utils/__tests__/validation.test.js b/web/cashtab/src/utils/__tests__/validation.test.js --- a/web/cashtab/src/utils/__tests__/validation.test.js +++ b/web/cashtab/src/utils/__tests__/validation.test.js @@ -52,23 +52,23 @@ import * as bip39 from 'bip39'; describe('Validation utils', () => { - it(`validateMnemonicWordList() returns a success message for a valid mnemonic`, () => { + it(`validateMnemonicWordList() returns true for a valid mnemonic`, () => { const validMnemonic = 'labor tail bulb distance estate collect lecture into smile differ yard legal'; expect( validateMnemonicWordList(validMnemonic, bip39.wordlists.english), - ).toBe('Valid mnemonic'); + ).toBe(true); }); - it(`validateMnemonicWordList() returns an error message for an invalid mnemonic`, () => { + it(`validateMnemonicWordList() returns false for an invalid mnemonic`, () => { const validMnemonic = 'labor tail bulb not valid collect lecture into smile differ yard legal'; expect( validateMnemonicWordList(validMnemonic, bip39.wordlists.english), - ).toBe('Invalid mnemonic'); + ).toBe(false); }); - it(`validateMnemonicWordList() returns an error message for an empty mnemonic`, () => { + it(`validateMnemonicWordList() returns false for an empty mnemonic`, () => { expect(validateMnemonicWordList('', bip39.wordlists.english)).toBe( - 'Invalid mnemonic', + false, ); }); it(`Returns 'false' if ${currency.ticker} send amount is a valid send amount`, () => { diff --git a/web/cashtab/src/utils/validation.js b/web/cashtab/src/utils/validation.js --- a/web/cashtab/src/utils/validation.js +++ b/web/cashtab/src/utils/validation.js @@ -2,22 +2,89 @@ import { currency } from 'components/Common/Ticker.js'; import { fromSatoshisToXec } from 'utils/cashMethods'; import cashaddr from 'ecashaddrjs'; -import * as bip39 from 'bip39'; +const createHash = require('create-hash'); -// reference: https://github.com/Permissionless-Software-Foundation/bch-js/blob/62e56c832b35731880fe448269818b853c76dd80/src/mnemonic.js#L160-L180 +// bip39 reference: https://github.com/bitcoinjs/bip39/blob/master/src/index.js#L66 export const validateMnemonicWordList = (mnemonic, wordlist) => { - // Preprocess the words - const words = mnemonic.split(' '); - // Detect blank phrase - if (words.length === 0) return 'Blank mnemonic'; + if (!mnemonic || !wordlist) { + return false; + } + try { + // NFKD = Compatibility Decomposition + const words = mnemonic.normalize('NFKD').split(' '); + + // Detect blank phrase + if (words.length === 0 || words.length % 3 !== 0) { + return false; + } - // Check the words are valid - const isValid = bip39.validateMnemonic(mnemonic, wordlist); - if (!isValid) return 'Invalid mnemonic'; + // convert word indices to 11 bit binary strings + const bits = words + .map(word => { + const index = wordlist.indexOf(word); + if (index === -1) { + return false; + } + 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 || + entropyBytes.length > 32 || + entropyBytes.length % 4 !== 0 + ) { + return false; + } + const entropy = Buffer.from(entropyBytes); + const newChecksum = deriveChecksumBits(entropy); + if (newChecksum !== checksumBits) { + return false; + } - return 'Valid mnemonic'; + return true; + } catch (err) { + console.log('validateMnemonicWordList(): Error validating Mnemonic'); + throw err; + } }; +/* + * helper functions used by validateMnemonicWordList() + * binaryToByte() + * lpad() + * bytesToBinary() + * deriveChecksumBits() + */ +// https://github.com/bitcoinjs/bip39/blob/d527196f6f5837b6ac3e2bf4829758ad4f1b1d5c/src/index.js#L35 +function binaryToByte(bin) { + return parseInt(bin, 2); +} +// https://github.com/bitcoinjs/bip39/blob/d527196f6f5837b6ac3e2bf4829758ad4f1b1d5c/src/index.js#L29 +function lpad(str, padString, length) { + while (str.length < length) { + str = padString + str; + } + return str; +} +// https://github.com/bitcoinjs/bip39/blob/d527196f6f5837b6ac3e2bf4829758ad4f1b1d5c/src/index.js#L38 +function bytesToBinary(bytes) { + return bytes.map(x => lpad(x.toString(2), '0', 8)).join(''); +} +// https://github.com/bitcoinjs/bip39/blob/d527196f6f5837b6ac3e2bf4829758ad4f1b1d5c/src/index.js#L41 +function deriveChecksumBits(entropyBuffer) { + const ENT = entropyBuffer.length * 8; + const CS = ENT / 32; + const hash = createHash('sha256').update(entropyBuffer).digest(); + return bytesToBinary(Array.from(hash)).slice(0, CS); +} + // Validate cash amount export const shouldRejectAmountInput = ( cashAmount,