All files Mnemonic.ts

100% Statements 51/51
100% Branches 21/21
100% Functions 6/6
100% Lines 49/49

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 2241x 1x 1x 1x           1x       1x                                           27x 27x 27x                                                                                                                   65x 1x             64x 35x         29x 29x     29x     522x     29x                               27x 1x             26x       26x 26x 447503x 455x 1x   454x       25x         25x 25x 8x     25x       25x 25x 25x 1x             24x                             54x 54x   54x 54x   54x     1169x 1169x     1169x     1122x 1122x           54x   54x    
import { sha256, pbkdf2Sync } from "@node-lightning/crypto";
import { EnglishWordList } from "./MnemonicWordLists";
import { BitcoinError } from "./BitcoinError";
import { BitcoinErrorCode } from "./BitcoinErrorCode";
 
/**
 * Implements mnemonic seed generation methods as specified in BIP39.
 * https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
 */
export class Mnemonic {
    /**
     * Default English word list of 2048 words as specified in BIP39
     */
    public static English = EnglishWordList;
 
    /**
     * Implements generation of a seed from mnemonic phrase and an
     * optional password for additional security.
     *
     * @remarks
     *
     * This method uses PBKDF2 using HMAC-SHA512 and 2048 rounds to
     * generate a 512-bit seed value. The mnemonic phrase acts as the
     * key password input. The optional password is appended to the
     * salt (default of "mnemonic") so that it is "mnemonic"+password.
     *
     * The mnemonic phrase and password are both encoded with utf-8
     * and use NFKD normalization.
     *
     * @param phrase mnemonic phrase
     * @param password optional passphrase adds additional layer of security
     *
     * @returns
     */
    public static phraseToSeed(phrase: string, password?: string): Buffer {
        const key = Buffer.from(phrase.normalize("NFKD"), "utf-8");
        const salt = Buffer.from(("mnemonic" + (password || "")).normalize("NFKD"), "utf-8");
        return pbkdf2Sync(key, salt, 2048, 64, "sha512");
    }
 
    /**
     * Encodes entropy plus a checksum into a mnemonic phrase by
     * concatenating words from a word list acording to BIP39.
     *
     * @remarks
     * Refer to
     * https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
     *
     * This method requires entropy that is >= 128 bits and <= 256 bits.
     * The entropy bit length must also be divisible by 32 bits.
     *
     * The checksum length is calculated from the bit length of entropy
     * divided by 32.
     *
     * ```
     * CS = ENT / 32
     * ```
     *
     * The checksum then takes the first xx bits from the sha256 of the
     * entropy.
     *
     * ```
     * CS = sha256(ENT)
     * ```
     *
     * From this, the combined value is converted into 11-bit chunks.
     * Each chunk can be represented by a word in the 2048 word list.
     *
     * The follow table shows the relationship between entropy size,
     * checksum size, and words:
     *
     * ```
     * CS = ENT / 32
     * MS = (ENT + CS) / 11
     *
     * |  ENT  | CS | ENT+CS |  MS  |
     * +-------+----+--------+------+
     * |  128  |  4 |   132  |  12  |
     * |  160  |  5 |   165  |  15  |
     * |  192  |  6 |   198  |  18  |
     * |  224  |  7 |   231  |  21  |
     * |  256  |  8 |   264  |  24  |
     * ```
     *
     * @param entropy Valid bit lengths of 128, 160, 192, 224, and 256 bits.
     * @param wordlist Requires a 2048 length word list. Defaults to the
     *
     * @throw {@link BitcoinError} throws if the entropy is not between
     * 128 and 256 bits and is not divisible by 32. Throws if the word
     * list is not 2048 words.
     *
     * @returns The phrase with checksum encoded
     */
    public static entropyToPhrase(entropy: Buffer, wordlist: string[] = Mnemonic.English): string {
        // ensure word list has 2048 words in it
        if (wordlist.length !== 2048) {
            throw new BitcoinError(BitcoinErrorCode.InvalidMnemonicWordList, {
                expected: 2048,
                got: wordlist.length,
            });
        }
 
        // ensure entropy is the corrent length
        if (entropy.length % 4 > 0 || entropy.length < 16 || entropy.length > 32) {
            throw new BitcoinError(BitcoinErrorCode.InvalidMnemonicEntropy, { entropy });
        }
 
        // convert the entropy into an array and push the first byte of
        // the checksum into the output
        const input = Array.from(entropy);
        input.push(sha256(entropy)[0]);
 
        // convert bytes into 11-bit words
        const [indices] = convert(input, 8, 11);
 
        // map 11-bit words into the actual word
        const words = indices.map(i => wordlist[i]);
 
        // return the concatenated phrase
        return words.join(" ");
    }
 
    /**
     * Converts a phrase into an entropy buffer. This method extracts
     * and validates the checksum that is included in the phrase.
     *
     * @param phrase mnemonic phrase
     * @param wordlist a word list that must  contain 2048 words
     * @throw {@link BitcoinError} throws if word list does not have 2048
     * words. Throws if there is a word that does not below. Throws if
     * the checksum fails.
     * @returns
     */
    public static phraseToEntropy(phrase: string, wordlist: string[] = Mnemonic.English): Buffer {
        // ensure word list has 2048 words in it
        if (wordlist.length !== 2048) {
            throw new BitcoinError(BitcoinErrorCode.InvalidMnemonicWordList, {
                expected: 2048,
                got: wordlist.length,
            });
        }
 
        // split phrase into words
        const words = phrase.split(" ");
 
        // convert words into indices (11-bits each) and throw if we
        // can't find a word
        const indices = [];
        for (const word of words) {
            const index = wordlist.findIndex(p => p === word);
            if (index === -1) {
                throw new BitcoinError(BitcoinErrorCode.InvalidMnemonicWord, { word, phrase });
            }
            indices.push(index);
        }
 
        // convert entropy words into bytes
        const results = convert(indices, 11, 8);
 
        // check if we had a remaininer which should happen in all but
        // 24 words. If we had 24 words, there won't be a remaininder,
        // but our checksum will be the final byte.
        let checksum = results[1];
        if (checksum === null) {
            checksum = results[0].pop();
        }
 
        const entropy = Buffer.from(results[0]);
 
        // calcualte the checksum and validate that the calculated and
        // extracted values match
        const calcedChecksumBits = entropy.length / 4;
        const calcedChecksum = sha256(entropy)[0] >> (8 - calcedChecksumBits);
        if (calcedChecksum !== checksum) {
            throw new BitcoinError(BitcoinErrorCode.InvalidMnemonicChecksum, {
                expected: checksum,
                got: calcedChecksum,
                phrase,
            });
        }
 
        return entropy;
    }
}
 
/**
 * Converts words of an input bit size into output words of the output
 * size. Conceptually, this is the equivalent of constructing a giant
 * bit stream from the input words and then consuming the stream using
 * the size of the output bits.
 * @param inWords input words
 * @param inSize bit size of input words
 * @param outSize bit size of output words
 * @returns
 */
function convert(inWords: Iterable<number>, inSize: number, outSize: number): [number[], number] {
    const outWords: number[] = [];
    const outMask = (1 << outSize) - 1;
 
    let bufBits = 0;
    let buf = 0;
 
    for (const inWord of inWords) {
        // Constuct a new buffer by left shifting the existing value
        // and putting the new inWord in the LSB position.
        buf = (buf << inSize) | inWord;
        bufBits += inSize;
 
        // Extact outWords when buffer is large enough
        while (bufBits >= outSize) {
            // Extract the outWord by right shifting the remaining lower
            // bits and masking any upper bits
            bufBits -= outSize;
            outWords.push((buf >> bufBits) & outMask);
        }
    }
 
    // The remainder will be the remaining lower bits or null if
    // no remaining bits were produced.
    const remainder = bufBits ? buf & ((1 << bufBits) - 1) : null;
 
    return [outWords, remainder];
}