OverviewFilesCommitsIssuesDocsReleasesPeers

keet-deobfuscated / 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
}