OverviewFilesCommitsIssuesDocsReleasesPeers

hypermosh-shell / lib / identity.mjs

62 lines · javascript ref:
// identity — load the hyperident device keyPair (same vault as hyperident-shell)
// so a hypermosh tunnel is addressed by the device pubkey and firewalled by the
// same allow-list. Reads only via `#node:fs` (bare-fs on bare).
import fs from '#node:fs'
import os from '#node:os'
import path from '#node:path'
import b4a from 'b4a'
import DHT from 'hyperdht'
import process from '#node:process'
import HypercoreId from 'hypercore-id-encoding'

function homedir () { return os.homedir ? os.homedir() : (process.env.HOME || '/root') }
const DEFAULT_VAULT = () => path.join(homedir(), '.hyperident')

// device.key = 64-byte libsodium secret (hex), device.pub = 32-byte public (hex).
// The 64-byte secret is itself a valid hyperdht keyPair — pass straight to
// server.listen / node.connect.
export function loadDeviceKeyPair (vault = DEFAULT_VAULT()) {
  let secretHex, publicHex
  try {
    secretHex = fs.readFileSync(path.join(vault, 'device.key'), 'utf8').trim()
    publicHex = fs.readFileSync(path.join(vault, 'device.pub'), 'utf8').trim()
  } catch (e) {
    if (e.code === 'ENOENT') throw new Error('no hyperident device identity at ' + vault + ' — run `pgh init` first')
    throw e
  }
  const secretKey = b4a.from(secretHex, 'hex')
  const publicKey = b4a.from(publicHex, 'hex')
  if (secretKey.length !== 64 || publicKey.length !== 32) {
    throw new Error('unexpected device key sizes (secret=' + secretKey.length + ' public=' + publicKey.length + ')')
  }
  return { publicKey, secretKey, pub: HypercoreId.encode(publicKey) }
}

// The hypermosh session daemon runs on its OWN key (separate from the device
// shell key — hyperident-shell already holds that, and one keyPair = one
// hyperdht server). Persisted in ~/.hypermosh/session.json, created on first use.
export function loadOrCreateKey (vault) {
  const dir = vault || path.join(homedir(), '.hypermosh')
  const kf = path.join(dir, 'session.json')
  try {
    const j = JSON.parse(fs.readFileSync(kf, 'utf8'))
    return { publicKey: b4a.from(j.publicKey, 'hex'), secretKey: b4a.from(j.secretKey, 'hex') }
  } catch (e) {
    if (e.code !== 'ENOENT') throw e
    const kp = DHT.keyPair()
    fs.mkdirSync(dir, { recursive: true })
    fs.writeFileSync(kf, JSON.stringify({ publicKey: b4a.toString(kp.publicKey, 'hex'), secretKey: b4a.toString(kp.secretKey, 'hex') }))
    return kp
  }
}

// Read an allow-list firewall file (one z-base32/hex key per line, '#' comments).
export function readAllowed (firewallPath) {
  if (!firewallPath) return []
  let txt
  try { txt = fs.readFileSync(firewallPath, 'utf8') } catch (e) { if (e.code === 'ENOENT') return []; throw e }
  return txt.split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#')).map((k) => HypercoreId.decode(k))
}

export { HypercoreId }