OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-proto-peer / lib / swarm-keypair.js

43 lines · javascript ref:
// Persistent Hyperswarm keypair for the peer.
//
// A hyper-proto-peer is a SERVER: the bridge (and any replica) finds it by DID topic and then
// reconnects to it by its Noise/DHT public key. If the peer booted with a RANDOM keypair each
// time (`new Hyperswarm()`), every restart is a brand-new DHT identity — Hyperswarm can't retry
// the peer it knew, so a restarted peer is only rediscovered via the slow topic re-query and its
// fronts go stale in the meantime. A STABLE keypair (persisted across restarts) makes the peer's
// serverKey durable, so reconnection is prompt. (Same pattern + rationale as pear-git's
// TopicConnector; the mesh analogue of the hyper-tps ephemeral-DHT-key bug.)
//
// The swarm identity is deliberately SEPARATE from the atproto signing key (in peer-identity.json)
// — it is only a transport/DHT identity, never signs anything.

import { readFileSync, writeFileSync, mkdirSync } from 'bare-fs'
import { join } from 'bare-path'
import b4a from 'b4a'
import crypto from 'hypercore-crypto'

const KEYPAIR_FILE = 'swarm-keypair.json'

// Load the peer's persistent swarm keypair from <storage>/swarm-keypair.json, or create + persist
// one on first boot. Returns a hypercore-crypto keypair ({ publicKey, secretKey } as Buffers).
// `name` selects WHICH stable identity: the daemon's (default) or a separate one for a client that must
// be allowlistable without impersonating the daemon — running two Hyperswarms on one keypair would put two
// DHT identities behind the same key, so `diag --remote` gets its own file rather than borrowing this one.
export function loadOrCreateSwarmKeyPair (storage, { name = KEYPAIR_FILE } = {}) {
  if (typeof storage !== 'string') throw new Error('loadOrCreateSwarmKeyPair: storage must be a directory path')
  const file = join(storage, name)
  try {
    const saved = JSON.parse(readFileSync(file, 'utf8'))
    return { publicKey: b4a.from(saved.publicKey, 'hex'), secretKey: b4a.from(saved.secretKey, 'hex') }
  } catch {
    const keyPair = crypto.keyPair()
    try { mkdirSync(storage, { recursive: true }) } catch {}
    writeFileSync(file, JSON.stringify({
      publicKey: b4a.toString(keyPair.publicKey, 'hex'),
      secretKey: b4a.toString(keyPair.secretKey, 'hex'),
      createdAt: Date.now()
    }, null, 2), { mode: 0o600 })
    return keyPair
  }
}