OverviewFilesCommitsIssuesDocsReleasesPeers

mirall-cli / node_modules / keet-identity-key / lib / keychain.js

138 lines · javascript ref:
const sodium = require('sodium-universal')
const c = require('compact-encoding')
const assert = require('nanoassert')
const b4a = require('b4a')
const { HMAC } = require('sodium-hmac')
const bip39 = require('bip39-mnemonic')

const { sign } = require('./crypto')

const MASTER_CHAIN_CODE = b4a.from('ed25519 seed')
const MASTER_SYMMETRIC_CODE = b4a.from('Symmetric key seed')

const HARDENED_OFFSET = 0x80000000

class KeyChain {
  constructor (seed) {
    this.seed = seed

    this._buffer = b4a.alloc(64)

    this.chainKey = this._buffer.subarray(0, 32)
    this.chainCode = this._buffer.subarray(32)

    this.publicKey = b4a.alloc(sodium.crypto_sign_PUBLICKEYBYTES)
    this.secretKey = b4a.alloc(sodium.crypto_sign_SECRETKEYBYTES)

    this._initialise()
  }

  static generateMnemonic () {
    return bip39.generateMnemonic()
  }

  static deriveSeed (mnemonic) {
    return bip39.mnemonicToSeed(mnemonic)
  }

  static async from ({ mnemonic, seed }, path) {
    assert(mnemonic || seed, 'No mnemonic or seed was passed.')

    if (!seed) seed = await KeyChain.deriveSeed(mnemonic)

    const key = new KeyChain(seed)

    if (!path) return key

    return key.get(path)
  }

  getSymmetricKey (path) {
    const buffer = b4a.alloc(64)

    const chainKey = buffer.subarray(0, 32)
    const key = buffer.subarray(32)

    HMAC.sha512(this.seed, MASTER_SYMMETRIC_CODE, buffer)

    for (const step of path) {
      const label = b4a.alloc(step.length + 1)
      b4a.write(label, step, 1, 'utf8')

      HMAC.sha512(label, chainKey, buffer)
    }

    return key
  }

  get isKeychain () {
    return true
  }

  _initialise () {
    HMAC.sha512(this.seed, MASTER_CHAIN_CODE, this._buffer)
    sodium.crypto_sign_seed_keypair(this.publicKey, this.secretKey, this.chainKey)
  }

  clear () {
    this.secretKey.fill(0)
    this.chainKey.fill(0)
    this.chainCode.fill(0)
  }

  get (path) {
    return this.derive(path)
  }

  sign (signable) {
    return sign(signable, this)
  }

  derive (path) {
    const buffer = b4a.from(this._buffer)

    const chainKey = buffer.subarray(0, 32)
    const chainCode = buffer.subarray(32)

    for (const step of path) {
      const index = ensureHardened(step) // hardened indices are >= 2^31
      HMAC.sha512(encodeDerivationData(chainKey, index), chainCode, buffer)
    }

    return createKeyPair(chainKey)
  }
}

module.exports = KeyChain

function createKeyPair (seed) {
  if (!seed) throw new Error('No seed provided')

  const publicKey = b4a.alloc(sodium.crypto_sign_PUBLICKEYBYTES)
  const secretKey = b4a.alloc(sodium.crypto_sign_SECRETKEYBYTES)

  sodium.crypto_sign_seed_keypair(publicKey, secretKey, seed)

  return {
    publicKey,
    secretKey
  }
}

function encodeDerivationData (key, step) {
  const state = { buffer: b4a.alloc(37), start: 0, end: 37 }

  c.uint8.encode(state, 0)
  c.fixed32.encode(state, key)
  state.buffer.writeUInt32BE(step, state.start)

  state.buffer[33] |= 0x80

  return state.buffer
}

function ensureHardened (n) {
  if (n >= HARDENED_OFFSET) return n
  return n + HARDENED_OFFSET
}