import * as bip39 from 'bip39' ; import { Hasher , sha3_256 } from 'js-sha3' ; import secretJS from 'secrets.js-grempe' ; export class BIPSigner { public readonly checkSumLength : number = 10 ; private getOffsetMapping ( share ) { const shareWordCount = share . split ( ' ' ) . length ; switch ( shareWordCount ) { case 48 : return { offset : 99 , seedOffset : 64 } ; case 36 : return { offset : 67 , seedOffset : 42 } ; case 24 : return { offset : 67 , seedOffset : 32 } ; default : throw new Error ( 'Currently only recovery of secrets with 48, 36 or 24 words are supported' ) ; } } private getRandomIntInclusive ( min , max ) { const randomBuffer = new Uint32Array ( 1 ) ; window . crypto . getRandomValues ( randomBuffer ) ; const randomNumber = randomBuffer [ 0 ] / ( 0xffffffff + 1 ) ; const ceilMin = Math . ceil ( min ) ; const floorMax = Math . floor ( max ) ; return Math . floor ( randomNumber * ( floorMax - ceilMin + 1 ) ) + ceilMin ; } public entropyToMnemonic ( entropy ) { return bip39 . entropyToMnemonic ( entropy ) ; } public mnemonicToEntropy ( mnemonic ) { const usedList = BIPSigner . determineWordList ( mnemonic ) ; if ( ! usedList ) { throw Error ( 'non-compatible secret' ) ; } return bip39 . mnemonicToEntropy ( mnemonic , usedList ) ; } public mnemonicToSeedSync ( mnemonic , passphrase ? ) { return bip39 . mnemonicToSeedSync ( mnemonic , passphrase ) ; } public static prepareMnemonic ( mnemonic ) { return mnemonic . trim ( ) . toLowerCase ( ) ; } public static validateMnemonic ( mnemonic ) { const preparedMnemonic = BIPSigner . prepareMnemonic ( mnemonic ) ; const wordList = BIPSigner . determineWordList ( preparedMnemonic ) ; return bip39 . validateMnemonic ( preparedMnemonic , wordList ) ; } public static determineWordList ( mnemonic ) { for ( const list of BIPSigner . wordLists ( ) ) { if ( bip39 . validateMnemonic ( BIPSigner . prepareMnemonic ( mnemonic ) , list ) ) { return list ; } } return undefined ; } public static wordLists ( ) { return [ bip39 . wordlists . english ] ; } public generateSocialRecover ( secret , numberOfShares , threshold ) { if ( threshold > numberOfShares ) { throw new Error ( 'The threshold needs to be smaller or equal to the number or shares' ) ; } else if ( numberOfShares < 2 ) { throw new Error ( 'At least two shares are needed' ) ; } const secretDigester = sha3_256 . create ( ) ; const seed = bip39 . mnemonicToEntropy ( secret ) ; secretDigester . update ( seed ) ; const shares = secretJS . share ( seed + secretDigester . hex ( ) . slice ( 0 , this . checkSumLength ) , numberOfShares , threshold ) ; const calculatedShares = [ ] ; for ( let i = 0 ; i < shares . length ; i ++ ) { const paddedShare = shares [ i ] . concat ( Array ( 29 ) . fill ( 0 ) . map ( ( ) => this . getRandomIntInclusive ( 0 , 9 ) ) . join ( '' ) ) ; calculatedShares [ i ] = `${ bip39 . entropyToMnemonic ( paddedShare . slice ( 0 , 64 ) ) } ${ bip39 . entropyToMnemonic ( paddedShare . slice ( 64 , 128 ) ) }` ; } return calculatedShares ; } public recoverKey ( shares ) { const offset = this . getOffsetMapping ( shares [ 0 ] ) ; const translatedShares = [ ] ; for ( let i = 0 ; i < shares . length ; i ++ ) { const words = shares [ i ] . split ( ' ' ) ; const firstHalf = words . slice ( 0 , 24 ) ; const secondHalf = words . slice ( 24 , words . length ) ; translatedShares [ i ] = `${ bip39 . mnemonicToEntropy ( firstHalf . join ( ' ' ) ) }${ bip39 . mnemonicToEntropy ( secondHalf . join ( ' ' ) ) }` . substr ( 0 , offset . offset ) ; } const secretDigester = sha3_256 . create ( ) ; const combine = secretJS . combine ( translatedShares ) ; const seed = combine . slice ( 0 , - this . checkSumLength ) ; secretDigester . update ( seed ) ; const checksum = secretDigester . hex ( ) . slice ( 0 , this . checkSumLength ) ; const checksum2 = combine . substr ( - this . checkSumLength ) ; if ( checksum !== checksum2 ) { throw new Error ( 'Checksum error, either the passed shares were generated for different secrets or the amount of shares is below the threshold' ) ; } return bip39 . entropyToMnemonic ( seed ) ; } }