OverviewFilesCommitsIssuesDocsReleasesPeers

mirall-cli / node_modules / bip39-mnemonic / index.js

217 lines · javascript ref:
const sodium = require('sodium-universal')
const b4a = require('b4a')
const assert = require('nanoassert')
const { detectLanguage, loadWordlist } = require('./wordlist')

module.exports = {
  generateEntropy,
  generateMnemonic,
  normalizeMnemonic,
  validateMnemonic,
  mnemonicToEntropy,
  mnemonicToSeed,
  entropyToMnemonic
}

function generateMnemonic ({ entropy = generateEntropy(), language = 'english' } = {}) {
  const wordlist = loadWordlist(language)
  const extended = computeCheckSum(entropy)

  const words = []

  for (const index of uint11Reader(extended)) {
    words.push(wordlist[index])
  }

  const delimiter = language === 'japanese' ? '\u3000' : ' '

  return words.join(delimiter).trim()
}

function entropyToMnemonic (entropy, { language = 'english' } = {}) {
  return generateMnemonic({ entropy, language })
}

function mnemonicToEntropy (mnemonic) {
  const words = mnemonic.replace(/\u3000/g, ' ').trim().split(' ')
  const language = detectLanguage(words)

  if (!language) {
    throw new Error('Language not recognised')
  }

  if (words.length % 3 !== 0) {
    throw new Error('Invalid length')
  }

  const wordlist = loadWordlist(language)

  const indexes = []
  for (const word of words) {
    const index = wordlist.indexOf(word)
    if (index === -1) {
      throw new Error('Bad word')
    }

    indexes.push(index)
  }

  const bits = words.length * 11
  const len = (bits * 32 / 33) >> 3

  const extended = b4a.alloc(Math.ceil(bits / 8))
  const entropy = extended.subarray(0, len)

  uint11Writer(extended, indexes)

  if (!b4a.equals(extended, computeCheckSum(entropy))) {
    throw new Error('Invalid checksum')
  }

  return entropy
}

function normalizeMnemonic (mnemonic) {
  return mnemonic.trim().replace(/\u3000/, ' ').split(/\s+/).map(c => c.toLowerCase()).join(' ')
}

async function mnemonicToSeed (mnemonic, passphrase = '') {
  mnemonic = normalizeMnemonic(mnemonic)

  if (!validateMnemonic(mnemonic)) {
    throw new Error('Invalid mnemonic')
  }

  const input = b4a.from(mnemonic)
  const salt = b4a.from('mnemonic' + passphrase)

  const output = b4a.alloc(64)

  await sodium.extension_pbkdf2_sha512_async(
    output,
    input,
    salt,
    2048,
    64
  )

  return output
}

function validateMnemonic (mnemonic) {
  try {
    mnemonicToEntropy(mnemonic)
  } catch (e) {
    return false
  }
  return true
}

function sha256 (data, output = b4a.alloc(32)) {
  sodium.crypto_hash_sha256(output, data)
  return output
}

function computeCheckSum (seed) {
  assert((seed.byteLength & 4) === 0, 'seed must be a multiple of 4 bytes')

  const len = seed.byteLength
  const cklen = len >> 2 // cksum bits
  const total = len + Math.ceil(cklen / 8)

  const output = b4a.alloc(len + 32)
  output.set(seed)

  const entropy = output.subarray(0, len)
  const cksum = output.subarray(len)

  sha256(entropy, cksum)

  // only append cklen bits
  output[total - 1] &= (0xff ^ (0xff >> cklen))

  return output.subarray(0, total)
}

function generateEntropy (length = 32) {
  const seed = b4a.alloc(length)
  sodium.randombytes_buf(seed)

  return seed
}

function * uint11Reader (state) {
  yield * uintReader(state, 11)
}

function uint11Writer (buf, uints) {
  return uintWriter(buf, uints, 11)
}

function * uintReader (buffer, width) {
  const MASK = (2 << (width - 1)) - 1

  let pos = 0
  let value = 0

  while (true) {
    const offset = pos >> 3 // byte offset

    if (offset >= buffer.byteLength) {
      return value & MASK
    }

    const height = width - (pos % width)
    const leftover = (offset + 1) * 8 - pos

    value += shift(buffer[offset], height - leftover)

    pos += Math.min(height, leftover)
    if (pos % width) continue

    yield value & MASK

    value = 0
  }
}

function uintWriter (buffer, uints, width) {
  let pos = 0

  while (true) {
    const offset = pos >> 3 // byte offset

    const i = Math.floor(pos / width)
    if (i >= uints.length) break

    if (offset >= buffer.length) {
      throw new Error('Failed to encode uints')
    }

    const rem = 8 - pos % 8
    const height = (i + 1) * width - pos

    const value = shift(uints[i], rem - height)

    buffer[offset] += mask(value, rem)

    pos += Math.min(rem, height)
  }

  return buffer
}

// when n is positive, shift left n bits
// when n is negative, shift right -n bits
function shift (val, n) {
  if (n === 0) return val
  if (n > 0) return val << n

  return val >> (-1 * n)
}

function mask (val, bits) {
  if (bits < 32) return val & ((1 << bits) - 1)
  return val % (2 ** bits)
}