OverviewFilesCommitsIssuesDocsReleasesPeers

keet-deobfuscated / keet-identity-key / index.js

262 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")
}