OverviewFilesCommitsIssuesDocsReleasesPeers

mirall-cli / node_modules / keet-identity-key / index.js

267 lines · javascript ref:
const c = require('compact-encoding')
const b4a = require('b4a')

const KeyChain = require('./lib/keychain')
const { sign, verify, hash } = require('./lib/crypto')

const {
  ProofEncoding,
  AttestedData,
  AttestedDevice,
  ReceiptEncoding
} = require('./lib/encoding')

const PROOF_VERSION = 1
const KEET_TYPE = 5338

const NS_PROFILE_DISC_ENC = hash(b4a.from('profile discovery'))

module.exports = class IdentityKey {
  constructor (keyChain) {
    this.keyChain = keyChain

    this.identityKeyPair = this.keyChain.get(identityKeyPath(0))
    this.profileDiscoveryKeyPair = this.keyChain.get(discoveryCorePath(0))
  }

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

  static deriveSeed (mnemonic) {
    return KeyChain.deriveSeed(mnemonic)
  }

  get identityPublicKey () {
    return this.identityKeyPair.publicKey
  }

  get profileDiscoveryPublicKey () {
    return this.profileDiscoveryKeyPair.publicKey
  }

  getProfileDiscoveryEncryptionKey () {
    return this.keyChain.getSymmetricKey(encryptionKeyPath(NS_PROFILE_DISC_ENC))
  }

  getEncryptionKey (profileKey) {
    return this.keyChain.getSymmetricKey(encryptionKeyPath(profileKey))
  }

  bootstrap (device) {
    return IdentityKey.bootstrap({ identity: this.identityKeyPair }, device)
  }

  clear () {
    this.keyChain.clear()
    this.identityKeyPair.secretKey.fill(0)
    this.profileDiscoveryKeyPair.secretKey.fill(0)
  }

  static async bootstrap ({ identity, seed, mnemonic }, device) {
    if (!identity) {
      const identityPath = identityKeyPath(0) // accountIndex unused for now
      identity = await KeyChain.from({ seed, mnemonic }, identityPath)
    }

    const proof = {
      version: PROOF_VERSION,
      epoch: Date.now(),
      identity: identity.publicKey,
      chain: []
    }

    return IdentityKey.attestDevice(device, identity, proof)
  }

  static attestDevice (publicKey, parent, proof) {
    if (!proof) return IdentityKey.bootstrap({ identity: parent }, publicKey)

    if (b4a.isBuffer(proof)) {
      proof = c.decode(ProofEncoding, proof)
    }

    if (proof.version === 0) {
      throw new Error('Version 0 proofs are not supported')
    }

    const signable = c.encode(AttestedDevice, {
      epoch: proof.epoch,
      identity: proof.identity,
      device: publicKey
    })

    const signature = sign(signable, parent)

    proof.chain.push({ publicKey, signature })

    return c.encode(ProofEncoding, proof)
  }

  static attestData (attestedData, keyPair, proof) {
    if (attestedData === null) throw new Error('Data is not attestable')

    if (b4a.isBuffer(proof)) {
      proof = c.decode(ProofEncoding, proof)
    }

    if (proof && proof.version === 0) {
      throw new Error('Version 0 proofs are not supported')
    }

    if (!proof) {
      proof = {
        version: PROOF_VERSION,
        epoch: Date.now(),
        identity: keyPair.publicKey,
        chain: []
      }
    }

    const signable = c.encode(AttestedData, {
      epoch: proof.epoch,
      identity: proof.identity,
      data: hash(attestedData)
    })

    proof.data = { signature: sign(signable, keyPair) }

    return c.encode(ProofEncoding, proof)
  }

  static async from ({ seed, mnemonic }) {
    const keyChain = await KeyChain.from({ seed, mnemonic })

    return new IdentityKey(keyChain)
  }

  static verify (proof, attestedData, opts = {}) {
    if (b4a.isBuffer(proof)) {
      proof = c.decode(ProofEncoding, proof)
    }

    if (!validateProof(proof, attestedData, opts)) {
      return null
    }

    const { epoch, identity, chain } = proof

    const candidate = getLastKey(chain) || identity

    if (opts.expectedDevice) {
      if (!b4a.equals(candidate, opts.expectedDevice)) return null
    }

    let parent = identity

    // verify chain
    for (let i = 0; i < chain.length; i++) {
      const { publicKey, signature } = chain[i]

      const signable = c.encode(AttestedDevice, {
        epoch,
        identity,
        device: publicKey
      })

      if (!verify(signable, signature, parent)) {
        return null
      }

      parent = publicKey
    }

    if (proof.data) {
      const signable = c.encode(AttestedData, {
        epoch,
        identity,
        data: hash(attestedData)
      })

      if (!verify(signable, proof.data.signature, parent)) {
        return null
      }
    }

    const receipt = c.encode(ReceiptEncoding, { epoch })

    return {
      receipt,
      identityPublicKey: identity,
      devicePublicKey: candidate
    }
  }
}

function validateProof (proof, attestedData, opts = {}) {
  // version 0 is ignored
  if (proof.version === 0) return false

  // validate version
  if (proof.version > PROOF_VERSION) return false

  // verify epoch
  if (opts.receipt) {
    const { epoch } = c.decode(ReceiptEncoding, opts.receipt)
    if (proof.epoch < epoch) return false
  }

  // verify identity
  if (opts.expectedIdentity) {
    if (!b4a.equals(proof.identity, opts.expectedIdentity)) return false
  }

  return validateAttestedData(proof.data, attestedData)
}

function validateAttestedData (data, attestedData) {
  if (!data && attestedData) return false
  return true
}

function getLastKey (chain) {
  if (!chain.length) return null

  const last = chain[chain.length - 1]
  return last.publicKey
}

// slip48 derivations:
// https://github.com/satoshilabs/slips/blob/master/slip-0048.md

function keyPath (accountIndex, index) {
  // https://github.com/bitcoin/bips/blob/master/bip-0043.mediawiki
  const purpose = 48 // SLIP-48 wallet
  const role = 0 // owner

  return [purpose, KEET_TYPE, role, accountIndex, index]
}

// identityKey -> m/48'/keet'/0'/0'

function identityKeyPath (accountIndex) {
  return keyPath(accountIndex, 0)
}

// identityKey -> m/48'/keet'/0'/1'

function discoveryCorePath (accountIndex) {
  return keyPath(accountIndex, 1)
}

// slip21 derivations
// https://github.com/satoshilabs/slips/blob/master/slip-0021.md

function symmetricPath (...path) {
  const purpose = 'SLIP-0021' // SLIP-21 wallet
  const namespace = 'keet-identity-key' // keet

  return [purpose, namespace, ...path]
}

// encryptionKey  -> m/SLIP-10/keet-identity-key/account/"encryption key"

function encryptionKeyPath (profileKey) {
  return symmetricPath(b4a.toString(profileKey, 'hex'), 'encryption key')
}