OverviewFilesCommitsIssuesDocsReleasesPeers

favela-identity / lib / observer-grant.js

43 lines · javascript ref:
// observer-grant.js — a bridge tells a peer which observation feed is theirs and who founded it, signed with the
// bridge's own swarm key (hyper-social #82, I1 S3, operator decision O-2 option b).
//
// ⛔ WHY SIGNED, AND WHY BY THAT KEY. A bridge writes what the fediverse does to a user into a feed it founds under
// its own favela identity; the user's node holds that feed and renders it as notifications. The node must only
// accept a feed the bridge it CHOSE vouches for. It chose the bridge by its swarm key (`bridgeKey` — the key it
// claimed its name against), so the grant is signed with exactly that key, and verified against it.
//
// ⛔ ONE DEFINITION, BECAUSE A MISMATCH IS SILENT (the rule bridge-claim.js states). ap-bridge signs these bytes
// and favela-peer verifies them; neither may import the other, so the format lives here, beside the claim formats.
// A fifth prefix: a grant must never verify as any proof a peer signs.
//
// ⚠ BOUND TO username AND generation, so a grant cannot be replayed to another user or to a re-claimed name.

import b4a from 'b4a'
import crypto from 'hypercore-crypto'

export const OBSERVER = 'favela-bridge:v1:observer'

const gen = (g) => { const n = Math.floor(Number(g)); return String(Number.isFinite(n) && n > 0 ? n : 0) }
const isHex = (s, n) => typeof s === 'string' && s.length === n && /^[0-9a-f]+$/i.test(s)

export function observerBytes ({ username, generation, observationFeed, observerRoot }) {
  return b4a.from([OBSERVER, String(username || ''), gen(generation), String(observationFeed || ''), String(observerRoot || '')].join(':'), 'utf8')
}

/** Sign a grant with the bridge's swarm keypair. → hex signature. */
export function signObserver (grant, keyPair) {
  return b4a.toString(crypto.sign(observerBytes(grant), keyPair.secretKey), 'hex')
}

/** Does `sigHex` sign this grant under `bridgeKeyHex`? Never throws. */
export function verifyObserver (grant, sigHex, bridgeKeyHex) {
  try {
    if (!isHex(sigHex, 128) || !isHex(bridgeKeyHex, 64)) return false
    return crypto.verify(observerBytes(grant), b4a.from(sigHex, 'hex'), b4a.from(bridgeKeyHex, 'hex'))
  } catch {
    return false
  }
}

export default { observerBytes, signObserver, verifyObserver, OBSERVER }