OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-note / lib / anproto.js

97 lines · javascript ref:
/**
 * ANProto Integration for Pear-Git
 *
 * Implements the ANProto protocol for web identity authentication:
 * - ed25519 keypairs
 * - Sign challenges with base64 encoding
 * - Verify signatures
 */

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

export class ANProto {
  /**
   * Generate a new ed25519 keypair
   * @returns {Object} { publicKey: Buffer, secretKey: Buffer, keypair: string }
   */
  static gen() {
    // Use 1-argument form which returns the keypair
    const seed = crypto.randomBytes(32)
    const keypair = crypto.keyPair(seed)

    return {
      publicKey: keypair.publicKey,
      secretKey: keypair.secretKey,
      keypair: b4a.toString(keypair.publicKey, 'base64') + b4a.toString(keypair.secretKey, 'base64')
    }
  }

  /**
   * Load keypair from base64 string
   * @param {string} keypairString - Base64 encoded public+secret key (132 chars)
   * @returns {Object} { publicKey: Buffer, secretKey: Buffer }
   */
  static loadKeypair(keypairString) {
    if (keypairString.length !== 132) {
      throw new Error('Invalid keypair length (expected 132 chars: 44 pubkey + 88 secretkey)')
    }

    const publicKey = b4a.from(keypairString.substring(0, 44), 'base64')
    const secretKey = b4a.from(keypairString.substring(44, 132), 'base64')

    return { publicKey, secretKey }
  }

  /**
   * Sign a challenge with keypair
   * @param {string} challenge - Challenge string to sign
   * @param {Object} keypair - { publicKey, secretKey }
   * @returns {string} Base64 encoded signature (88 chars)
   */
  static sign(challenge, keypair) {
    const challengeBuffer = b4a.from(challenge, 'utf8')

    // Use 2-argument form which returns the signature
    const signature = crypto.sign(challengeBuffer, keypair.secretKey)

    return b4a.toString(signature, 'base64')
  }

  /**
   * Verify signature
   * @param {string} challenge - Challenge string
   * @param {string} signature - Base64 signature
   * @param {string} publicKey - Base64 public key
   * @returns {boolean} True if valid
   */
  static verify(challenge, signature, publicKey) {
    try {
      const challengeBuffer = b4a.from(challenge, 'utf8')
      const signatureBuffer = b4a.from(signature, 'base64')
      const publicKeyBuffer = b4a.from(publicKey, 'base64')

      if (publicKeyBuffer.length !== 32) return false
      if (signatureBuffer.length !== 64) return false

      return crypto.verify(challengeBuffer, signatureBuffer, publicKeyBuffer)
    } catch {
      return false
    }
  }

  /**
   * Extract public key from keypair string
   * @param {string} keypairString - Base64 keypair (132 chars)
   * @returns {string} Base64 public key (44 chars)
   */
  static getPublicKey(keypairString) {
    if (keypairString.length !== 132) {
      throw new Error('Invalid keypair length')
    }
    return keypairString.substring(0, 44)
  }
}

export default ANProto