OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / packages / git-remote-pear / lib / server-identity.js

164 lines · javascript ref:
/**
 * Server Identity Manager for Pear-Git P2P
 *
 * Manages the server's persistent keypair used for:
 * - Deriving P2P topic hashes (topic = hash(serverKey + repoName))
 * - Identifying the server in pear:// URLs
 * - Signing P2P protocol messages (future)
 *
 * The server key is stored persistently so that pear:// URLs remain stable.
 */

import fs from 'bare-fs'
import path from 'bare-path'
import crypto from 'hypercore-crypto'

export class ServerIdentity {
  constructor(options = {}) {
    this.storagePath = options.storagePath
    this.keyFile = path.join(this.storagePath, 'server-identity.json')
    this.publicKey = null
    this.secretKey = null
  }

  /**
   * Initialize server identity
   * Loads existing keypair or generates new one
   */
  async init() {
    // Ensure storage directory exists
    try {
      await fs.promises.mkdir(this.storagePath, { recursive: true })
    } catch (err) {
      if (err.code !== 'EEXIST') throw err
    }

    // Try to load existing identity
    try {
      const data = await fs.promises.readFile(this.keyFile, 'utf-8')
      const identity = JSON.parse(data)

      this.publicKey = Buffer.from(identity.publicKey, 'hex')
      this.secretKey = Buffer.from(identity.secretKey, 'hex')

      console.log(`[Identity] Loaded server key: ${this.getPublicKeyHex().slice(0, 8)}...`)
    } catch (err) {
      if (err.code !== 'ENOENT') throw err

      // Generate new identity
      await this._generateIdentity()
    }
  }

  /**
   * Get public key as hex string (for URLs)
   */
  getPublicKeyHex() {
    return this.publicKey.toString('hex')
  }

  /**
   * Get public key as buffer (for crypto operations)
   */
  getPublicKeyBuffer() {
    return this.publicKey
  }

  /**
   * Derive a topic hash for a repository
   * topic = hash(serverPublicKey + repoName)
   *
   * @param {string} repoName - Repository name
   * @returns {Buffer} - 32-byte topic hash
   */
  deriveTopic(repoName) {
    return crypto.hash(Buffer.concat([
      this.publicKey,
      Buffer.from(repoName, 'utf-8')
    ])).subarray(0, 32)
  }

  /**
   * Derive topic from any key (for clients)
   * @param {string|Buffer} serverKey - Server public key
   * @param {string} repoName - Repository name
   * @returns {Buffer} - 32-byte topic hash
   */
  static deriveTopicFromKey(serverKey, repoName) {
    const keyBuffer = typeof serverKey === 'string'
      ? Buffer.from(serverKey, 'hex')
      : serverKey

    return crypto.hash(Buffer.concat([
      keyBuffer,
      Buffer.from(repoName, 'utf-8')
    ])).subarray(0, 32)
  }

  /**
   * Sign data with server secret key
   * @param {Buffer|string} data - Data to sign
   * @returns {Buffer} - Signature
   */
  sign(data) {
    const dataBuffer = typeof data === 'string' ? Buffer.from(data) : data
    return crypto.sign(dataBuffer, this.secretKey)
  }

  /**
   * Verify a signature from this server
   * @param {Buffer|string} data - Original data
   * @param {Buffer|string} signature - Signature to verify
   * @returns {boolean}
   */
  verify(data, signature) {
    const dataBuffer = typeof data === 'string' ? Buffer.from(data) : data
    const sigBuffer = typeof signature === 'string' ? Buffer.from(signature, 'hex') : signature
    return crypto.verify(dataBuffer, sigBuffer, this.publicKey)
  }

  /**
   * Verify a signature from any server
   * @param {Buffer|string} data - Original data
   * @param {Buffer|string} signature - Signature
   * @param {Buffer|string} publicKey - Server public key
   * @returns {boolean}
   */
  static verifySignature(data, signature, publicKey) {
    const dataBuffer = typeof data === 'string' ? Buffer.from(data) : data
    const sigBuffer = typeof signature === 'string' ? Buffer.from(signature, 'hex') : signature
    const keyBuffer = typeof publicKey === 'string' ? Buffer.from(publicKey, 'hex') : publicKey
    return crypto.verify(dataBuffer, sigBuffer, keyBuffer)
  }

  /**
   * Export identity info (for display, NOT including secret key)
   */
  toJSON() {
    return {
      publicKey: this.getPublicKeyHex(),
      shortKey: this.getPublicKeyHex().slice(0, 8) + '...'
    }
  }

  // === Private Methods ===

  async _generateIdentity() {
    const keypair = crypto.keyPair()
    this.publicKey = keypair.publicKey
    this.secretKey = keypair.secretKey

    // Save to disk
    const identity = {
      publicKey: this.publicKey.toString('hex'),
      secretKey: this.secretKey.toString('hex'),
      createdAt: Date.now()
    }

    await fs.promises.writeFile(this.keyFile, JSON.stringify(identity, null, 2), 'utf-8')
    console.log(`[Identity] Generated new server key: ${this.getPublicKeyHex().slice(0, 8)}...`)
  }
}

export default ServerIdentity