OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / test / node_modules / hyperdht / lib / persistent.js

316 lines · javascript ref:
const c = require('compact-encoding')
const sodium = require('sodium-universal')
const RecordCache = require('record-cache')
const Cache = require('xache')
const b4a = require('b4a')
const unslab = require('unslab')

const { encodeUnslab } = require('./encode')
const m = require('./messages')
const { NS, ERROR } = require('./constants')

const EMPTY = b4a.alloc(0)
const TMP = b4a.allocUnsafe(32)
const MAX_BUMP_DRIFT = 60_000

module.exports = class Persistent {
  constructor(dht, opts) {
    this.dht = dht
    this.records = new RecordCache(opts.records)
    this.bumps = new Cache(opts.bumps)
    this.refreshes = new Cache(opts.refreshes)
    this.mutables = new Cache(opts.mutables)
    this.immutables = new Cache(opts.immutables)
  }

  onlookup(req) {
    if (!req.target) return

    const k = b4a.toString(req.target, 'hex')
    const records = this.records.get(k, 20)
    const bump = this.bumps.get(k) || 0
    const fwd = this.dht._router.get(k)

    if (fwd && records.length < 20) records.push(fwd.record)

    req.reply(records.length ? c.encode(m.lookupRawReply, { peers: records, bump }) : null)
  }

  onfindpeer(req) {
    if (!req.target) return
    const fwd = this.dht._router.get(req.target)
    req.reply(fwd ? fwd.record : null)
  }

  unannounce(target, publicKey) {
    const k = b4a.toString(target, 'hex')
    sodium.crypto_generichash(TMP, publicKey)

    if (b4a.equals(TMP, target)) this.dht._router.delete(k)
    this.records.remove(k, publicKey)
  }

  onunannounce(req) {
    if (!req.target || !req.token) return

    const unann = decode(m.announce, req.value)
    if (unann === null) return

    const { peer, signature } = unann
    if (!peer || !signature) return

    const signable = annSignable(req.target, req.token, this.dht.id, unann, NS.UNANNOUNCE)

    if (!sodium.crypto_sign_verify_detached(signature, signable, peer.publicKey)) {
      return
    }

    this.unannounce(req.target, peer.publicKey)
    req.reply(null, { token: false, closerNodes: false })
  }

  _onrefresh(token, req) {
    sodium.crypto_generichash(TMP, token)
    const activeRefresh = b4a.toString(TMP, 'hex')

    const r = this.refreshes.get(activeRefresh)
    if (!r) return

    const { announceSelf, k, record } = r
    const publicKey = record.subarray(0, 32)

    if (announceSelf) {
      this.dht._router.set(k, {
        relay: req.from,
        record,
        onconnect: null,
        onholepunch: null
      })
      this.records.remove(k, publicKey)
    } else {
      this.records.add(k, publicKey, record)
    }

    this.refreshes.delete(activeRefresh)
    this.refreshes.set(b4a.toString(token, 'hex'), r)

    req.reply(null, { token: false, closerNodes: false })
  }

  onannounce(req) {
    if (!req.target || !req.token || !this.dht.id) return

    const ann = decode(m.announce, req.value)
    if (ann === null) return

    const { peer, refresh, signature, bump } = ann

    if (!peer) {
      if (!refresh) return
      this._onrefresh(refresh, req)
      return
    }

    const signable = annSignable(req.target, req.token, this.dht.id, ann, NS.ANNOUNCE)

    if (!signature || !sodium.crypto_sign_verify_detached(signature, signable, peer.publicKey)) {
      return
    }

    // TODO: it would be potentially be more optimal to allow more than 3 addresses here for a findPeer response
    // and only use max 3 for a lookup reply
    if (peer.relayAddresses.length > 3) {
      peer.relayAddresses = peer.relayAddresses.slice(0, 3)
    }

    sodium.crypto_generichash(TMP, peer.publicKey)

    const k = b4a.toString(req.target, 'hex')
    const announceSelf = b4a.equals(TMP, req.target)
    const record = encodeUnslab(m.peer, peer)

    if (announceSelf) {
      this.dht._router.set(k, {
        relay: req.from,
        record,
        onconnect: null,
        onholepunch: null
      })
      this.records.remove(k, peer.publicKey)
    } else {
      const currentBump = this.bumps.get(k) || 0
      if (bump > currentBump && bump <= Date.now() + MAX_BUMP_DRIFT) this.bumps.set(k, bump)
      this.records.add(k, peer.publicKey, record)
    }

    if (refresh) {
      this.refreshes.set(b4a.toString(refresh, 'hex'), { k, record, announceSelf })
    }

    req.reply(null, { token: false, closerNodes: false })
  }

  onmutableget(req) {
    if (!req.target || !req.value) return

    let seq = 0
    try {
      seq = c.decode(c.uint, req.value)
    } catch {
      return
    }

    const k = b4a.toString(req.target, 'hex')
    const value = this.mutables.get(k)

    if (!value) {
      req.reply(null)
      return
    }

    const localSeq = c.decode(c.uint, value)
    req.reply(localSeq < seq ? null : value)
  }

  onmutableput(req) {
    if (!req.target || !req.token || !req.value) return

    const p = decode(m.mutablePutRequest, req.value)
    if (!p) return

    const { publicKey, seq, value, signature } = p

    const hash = b4a.allocUnsafe(32)
    sodium.crypto_generichash(hash, publicKey)
    if (!b4a.equals(hash, req.target)) return

    if (!value || !verifyMutable(signature, seq, value, publicKey)) return

    const k = b4a.toString(hash, 'hex')
    const local = this.mutables.get(k)

    if (local) {
      const existing = c.decode(m.mutableGetResponse, local)
      if (existing.value && existing.seq === seq && b4a.compare(value, existing.value) !== 0) {
        req.error(ERROR.SEQ_REUSED)
        return
      }
      if (seq < existing.seq) {
        req.error(ERROR.SEQ_TOO_LOW)
        return
      }
    }

    this.mutables.set(k, encodeUnslab(m.mutableGetResponse, { seq, value, signature }))
    req.reply(null)
  }

  onimmutableget(req) {
    if (!req.target) return

    const k = b4a.toString(req.target, 'hex')
    const value = this.immutables.get(k)

    req.reply(value || null)
  }

  onimmutableput(req) {
    if (!req.target || !req.token || !req.value) return

    const hash = b4a.alloc(32)
    sodium.crypto_generichash(hash, req.value)
    if (!b4a.equals(hash, req.target)) return

    const k = b4a.toString(hash, 'hex')
    this.immutables.set(k, unslab(req.value))

    req.reply(null)
  }

  onplugin(req) {
    if (!req.value) return

    const plugreq = decode(m.pluginRequest, req.value)
    if (plugreq === null) return

    const p = this.dht.plugins.get(plugreq.plugin)
    if (!p || p.version !== plugreq.version) return

    p.onrequest(plugreq, req)
  }

  destroy() {
    this.records.destroy()
    this.refreshes.destroy()
    this.mutables.destroy()
    this.immutables.destroy()
  }

  static signMutable(seq, value, keyPair) {
    const signable = b4a.allocUnsafe(32 + 32)
    const hash = signable.subarray(32)

    signable.set(NS.MUTABLE_PUT, 0)

    sodium.crypto_generichash(hash, c.encode(m.mutableSignable, { seq, value }))
    return sign(signable, keyPair)
  }

  static verifyMutable(signature, seq, value, publicKey) {
    return verifyMutable(signature, seq, value, publicKey)
  }

  static signAnnounce(target, token, id, ann, keyPair) {
    return sign(annSignable(target, token, id, ann, NS.ANNOUNCE), keyPair)
  }

  static signUnannounce(target, token, id, ann, keyPair) {
    return sign(annSignable(target, token, id, ann, NS.UNANNOUNCE), keyPair)
  }
}

function verifyMutable(signature, seq, value, publicKey) {
  const signable = b4a.allocUnsafe(32 + 32)
  const hash = signable.subarray(32)

  signable.set(NS.MUTABLE_PUT, 0)

  sodium.crypto_generichash(hash, c.encode(m.mutableSignable, { seq, value }))
  return sodium.crypto_sign_verify_detached(signature, signable, publicKey)
}

function annSignable(target, token, id, ann, ns) {
  const signable = b4a.allocUnsafe(32 + 32)
  const hash = signable.subarray(32)

  signable.set(ns, 0)

  sodium.crypto_generichash_batch(hash, [
    target,
    id,
    token,
    c.encode(m.peer, ann.peer), // note that this is the partial encoding of the announce message so we could just use that for perf
    ann.refresh || EMPTY
  ])

  return signable
}

function sign(signable, keyPair) {
  if (keyPair.sign) {
    return keyPair.sign(signable)
  }
  const secretKey = keyPair.secretKey ? keyPair.secretKey : keyPair
  const signature = b4a.allocUnsafe(64)
  sodium.crypto_sign_detached(signature, signable, secretKey)
  return signature
}

function decode(enc, val) {
  try {
    return val && c.decode(enc, val)
  } catch (err) {
    return null
  }
}