@bsv/sdk
Version:
BSV Blockchain Software Development Kit
309 lines (282 loc) • 10.6 kB
text/typescript
import { wordList } from './bip-39-wordlist-en.js'
import { encode, toArray, Reader, Writer } from '../primitives/utils.js'
import * as Hash from '../primitives/Hash.js'
import Random from '../primitives/Random.js'
/**
* @class Mnemonic
*
* @description
* Class representing Mnemonic functionality.
* This class provides methods for generating, converting, and validating mnemonic phrases
* according to the BIP39 standard. It supports creating mnemonics from random entropy,
* converting mnemonics to seeds, and validating mnemonic phrases.
*/
export default class Mnemonic {
public mnemonic: string
public seed: number[]
public Wordlist: { value: string[], space: string }
/**
* Constructs a Mnemonic object.
* @param {string} [mnemonic] - An optional mnemonic phrase.
* @param {number[]} [seed] - An optional seed derived from the mnemonic.
* @param {object} [wordlist=wordList] - An object containing a list of words and space character used in the mnemonic.
*/
constructor (mnemonic?: string, seed?: number[], wordlist = wordList) {
this.mnemonic = mnemonic ?? '' // Default to empty string if undefined
this.seed = seed ?? [] // Default to empty array if undefined
this.Wordlist = wordlist
}
/**
* Converts the mnemonic and seed into a binary representation.
* @returns {number[]} The binary representation of the mnemonic and seed.
*/
public toBinary (): number[] {
const bw = new Writer()
if (this.mnemonic !== '') {
const buf = toArray(this.mnemonic, 'utf8')
bw.writeVarIntNum(buf.length)
bw.write(buf)
} else {
bw.writeVarIntNum(0)
}
if (this.seed.length > 0) {
bw.writeVarIntNum(this.seed.length)
bw.write(this.seed)
} else {
bw.writeVarIntNum(0)
}
return bw.toArray()
}
/**
* Loads a mnemonic and seed from a binary representation.
* @param {number[]} bin - The binary representation of a mnemonic and seed.
* @returns {this} The Mnemonic instance with loaded mnemonic and seed.
*/
public fromBinary (bin: number[]): this {
const br = new Reader(bin)
const mnemoniclen = br.readVarIntNum()
if (mnemoniclen > 0) {
this.mnemonic = encode(br.read(mnemoniclen), 'utf8') as string
}
const seedlen = br.readVarIntNum()
if (seedlen > 0) {
this.seed = br.read(seedlen)
}
return this
}
/**
* Generates a random mnemonic from a given bit length.
* @param {number} [bits=128] - The bit length for the random mnemonic (must be a multiple of 32 and at least 128).
* @returns {this} The Mnemonic instance with the new random mnemonic.
* @throws {Error} If the bit length is not a multiple of 32 or is less than 128.
*/
public fromRandom (bits?: number): this {
if (bits === undefined || bits === null || isNaN(bits) || bits === 0) {
bits = 128
}
if (bits % 32 !== 0) {
throw new Error('bits must be multiple of 32')
}
if (bits < 128) {
throw new Error('bits must be at least 128')
}
const buf = Random(bits / 8)
this.entropy2Mnemonic(buf)
this.mnemonic2Seed()
return this
}
/**
* Static method to generate a Mnemonic instance with a random mnemonic.
* @param {number} [bits=128] - The bit length for the random mnemonic.
* @returns {Mnemonic} A new Mnemonic instance.
*/
public static fromRandom (bits?: number): Mnemonic {
return new this().fromRandom(bits)
}
/**
* Converts given entropy into a mnemonic phrase.
* This method is used to generate a mnemonic from a specific entropy source.
* @param {number[]} buf - The entropy buffer, must be at least 128 bits.
* @returns {this} The Mnemonic instance with the mnemonic set from the given entropy.
* @throws {Error} If the entropy is less than 128 bits.
*/
public fromEntropy (buf: number[]): this {
this.entropy2Mnemonic(buf)
return this
}
/**
* Static method to create a Mnemonic instance from a given entropy.
* @param {number[]} buf - The entropy buffer.
* @returns {Mnemonic} A new Mnemonic instance.
*/
public static fromEntropy (buf: number[]): Mnemonic {
return new this().fromEntropy(buf)
}
/**
* Sets the mnemonic for the instance from a string.
* @param {string} mnemonic - The mnemonic phrase as a string.
* @returns {this} The Mnemonic instance with the set mnemonic.
*/
public fromString (mnemonic: string): this {
this.mnemonic = mnemonic
return this
}
/**
* Static method to create a Mnemonic instance from a mnemonic string.
* @param {string} str - The mnemonic phrase.
* @returns {Mnemonic} A new Mnemonic instance.
*/
public static fromString (str: string): Mnemonic {
return new this().fromString(str)
}
/**
* Converts the instance's mnemonic to a string representation.
* @returns {string} The mnemonic phrase as a string.
*/
public toString (): string {
return this.mnemonic
}
/**
* Converts the mnemonic to a seed.
* The mnemonic must pass the validity check before conversion.
* @param {string} [passphrase=''] - An optional passphrase for additional security.
* @returns {number[]} The generated seed.
* @throws {Error} If the mnemonic is invalid.
*/
public toSeed (passphrase?: string): number[] {
this.mnemonic2Seed(passphrase)
return this.seed
}
/**
* Converts entropy to a mnemonic phrase.
* This method takes a buffer of entropy and converts it into a corresponding
* mnemonic phrase based on the Mnemonic wordlist. The entropy should be at least 128 bits.
* The method applies a checksum and maps the entropy to words in the wordlist.
* @param {number[]} buf - The entropy buffer to convert. Must be at least 128 bits.
* @returns {this} The Mnemonic instance with the mnemonic set from the entropy.
* @throws {Error} If the entropy is less than 128 bits or if it's not an even multiple of 11 bits.
*/
public entropy2Mnemonic (buf: number[]): this {
if (buf.length < 128 / 8) {
throw new Error(
'Entropy is less than 128 bits. It must be 128 bits or more.'
)
}
const hash = Hash.sha256(buf)
let bin = ''
const bits = buf.length * 8
for (let i = 0; i < buf.length; i++) {
bin = bin + ('00000000' + buf[i].toString(2)).slice(-8)
}
let hashbits = hash[0].toString(2)
hashbits = ('00000000' + hashbits).slice(-8).slice(0, bits / 32)
bin = bin + hashbits
if (bin.length % 11 !== 0) {
throw new Error(
'internal error - entropy not an even multiple of 11 bits - ' +
bin.length.toString()
)
}
let mnemonic = ''
for (let i = 0; i < bin.length / 11; i++) {
if (mnemonic !== '') {
mnemonic = mnemonic + this.Wordlist.space
}
const wi = parseInt(bin.slice(i * 11, (i + 1) * 11), 2)
mnemonic = mnemonic + this.Wordlist.value[wi]
}
this.mnemonic = mnemonic
return this
}
/**
* Validates the mnemonic phrase.
* Checks for correct length, absence of invalid words, and proper checksum.
* @returns {boolean} True if the mnemonic is valid, false otherwise.
* @throws {Error} If the mnemonic is not an even multiple of 11 bits.
*/
public check (): boolean {
const mnemonic = this.mnemonic
// confirm no invalid words
const words = mnemonic.split(this.Wordlist.space)
let bin = ''
for (let i = 0; i < words.length; i++) {
const ind = this.Wordlist.value.indexOf(words[i])
if (ind < 0) {
return false
}
bin = bin + ('00000000000' + ind.toString(2)).slice(-11)
}
if (bin.length % 11 !== 0) {
throw new Error(
'internal error - entropy not an even multiple of 11 bits - ' +
bin.length.toString()
)
}
// confirm checksum
const cs = bin.length / 33
const hashBits = bin.slice(-cs)
const nonhashBits = bin.slice(0, bin.length - cs)
const buf: number[] = []
for (let i = 0; i < nonhashBits.length / 8; i++) {
buf.push(parseInt(bin.slice(i * 8, (i + 1) * 8), 2))
}
const hash = Hash.sha256(buf.slice(0, nonhashBits.length / 8))
let expectedHashBits = hash[0].toString(2)
expectedHashBits = ('00000000' + expectedHashBits).slice(-8).slice(0, cs)
return expectedHashBits === hashBits
}
/**
* Converts a mnemonic to a seed.
* This method takes the instance's mnemonic phrase, combines it with a passphrase (if provided),
* and uses PBKDF2 to generate a seed. It also validates the mnemonic before conversion.
* This seed can then be used for generating deterministic keys.
* @param {string} [passphrase=''] - An optional passphrase for added security.
* @returns {this} The Mnemonic instance with the seed generated from the mnemonic.
* @throws {Error} If the mnemonic does not pass validation or if the passphrase is not a string.
*/
public mnemonic2Seed (passphrase = ''): this {
let mnemonic = this.mnemonic
if (!this.check()) {
throw new Error(
'Mnemonic does not pass the check - was the mnemonic typed incorrectly? Are there extra spaces?'
)
}
if (typeof passphrase !== 'string') {
throw new Error('passphrase must be a string or undefined')
}
mnemonic = mnemonic.normalize('NFKD')
passphrase = passphrase.normalize('NFKD')
const mbuf = toArray(mnemonic, 'utf8')
const pbuf = [
...toArray('mnemonic', 'utf8'),
...toArray(passphrase, 'utf8')
]
this.seed = Hash.pbkdf2(mbuf, pbuf, 2048, 64, 'sha512')
return this
}
/**
* Determines the validity of a given passphrase with the mnemonic.
* This method is useful for checking if a passphrase matches with the mnemonic.
* @param {string} [passphrase=''] - The passphrase to validate.
* @returns {boolean} True if the mnemonic and passphrase combination is valid, false otherwise.
*/
public isValid (passphrase = ''): boolean {
let isValid
try {
this.mnemonic2Seed(passphrase)
isValid = true
} catch {
isValid = false
}
return isValid
}
/**
* Static method to check the validity of a given mnemonic and passphrase combination.
* @param {string} mnemonic - The mnemonic phrase.
* @param {string} [passphrase=''] - The passphrase to validate.
* @returns {boolean} True if the combination is valid, false otherwise.
*/
public static isValid (mnemonic: string, passphrase = ''): boolean {
return new Mnemonic(mnemonic).isValid(passphrase)
}
}