OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / lib / auth / verify.js

72 lines · javascript ref:
/**
 * Challenge-response verification using canonical ANProto wire format.
 *
 * The browser-side anproto-client.js produces:
 *   hash = SHA-256(challenge)
 *   signature = pubkey(44) + base64(ed25519_sign(timestamp + hash) || timestamp + hash)
 *
 * Server verifies using an.open() from the standalone anproto module:
 *   opened = an.open(signature)  →  "{timestamp}{hash}"
 *   parsed = an.parse(opened)    →  { timestamp, hash }
 *   check: hash === SHA-256(challenge) && timestamp is recent
 */

import { an } from 'anproto'
import b4a from 'b4a'

/**
 * Verify a challenge signature in canonical ANProto format.
 *
 * @param {string} challenge   - the challenge string the server issued
 * @param {string} signature   - canonical wire-format: pubkey(44) + base64(combined envelope)
 * @returns {Promise<{ valid: boolean, publicKey: string|null, error: string|null }>}
 */
export async function verifyChallenge (challenge, signature) {
  if (typeof challenge !== 'string' || !challenge) {
    return { valid: false, publicKey: null, error: 'missing challenge' }
  }
  if (typeof signature !== 'string' || signature.length < 44) {
    return { valid: false, publicKey: null, error: 'missing or short signature' }
  }

  try {
    const publicKey = signature.substring(0, 44)
    const opened = await an.open(signature)
    const parsed = an.parse(opened)
    const expectedHash = await an.hash(challenge)

    if (parsed.hash !== expectedHash) {
      return { valid: false, publicKey, error: 'hash mismatch' }
    }

    // Check timestamp freshness (5 minute window)
    const age = Date.now() - parsed.timestamp
    if (age < -60000 || age > 5 * 60 * 1000) {
      return { valid: false, publicKey, error: 'timestamp out of range' }
    }

    return { valid: true, publicKey, error: null }
  } catch (err) {
    return { valid: false, publicKey: null, error: err.message }
  }
}

/**
 * Convert base64 public key (44 chars) to hex (64 chars).
 * Used for storing identities in trust-map (which keys by hex).
 */
export function publicKeyB64ToHex (publicKeyB64) {
  const pk = b4a.from(publicKeyB64, 'base64')
  if (pk.length !== 32) throw new Error('invalid pubkey length')
  return b4a.toString(pk, 'hex')
}

/**
 * Convert hex public key (64 chars) to base64 (44 chars).
 */
export function publicKeyHexToB64 (publicKeyHex) {
  const pk = b4a.from(publicKeyHex, 'hex')
  if (pk.length !== 32) throw new Error('invalid pubkey length')
  return b4a.toString(pk, 'base64')
}