OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / lib / auth / providers / keet-identity.js

150 lines · javascript ref:
import { AuthProvider } from '../base-provider.js'
import IdentityKey from 'keet-identity-key'
import crypto from 'hypercore-crypto'
import b4a from 'b4a'

/**
 * KeetIdentityProvider
 *
 * Authenticates users via Keet identity (BIP39 mnemonic-based):
 * - Uses keet-identity-key package for BIP39 derivation
 * - Looks for X-Keet-Signature header with signed challenge
 * - Public key derived from BIP39 mnemonic
 * - Used by hyper-wallet and other Keet-compatible apps
 */
export class KeetIdentityProvider extends AuthProvider {
  constructor(registry) {
    super(registry)
    this.name = 'keet-identity'
  }

  /**
   * Extract Keet signature from request headers
   * @param {Object} req - HTTP request
   * @returns {Promise<Object|null>} { publicKey, signature, challenge } or null
   */
  async extractCredentials(req) {
    const keetSignature = req.headers['x-keet-signature']
    const keetPublicKey = req.headers['x-keet-publickey']
    const keetChallenge = req.headers['x-keet-challenge']

    if (!keetSignature || !keetPublicKey || !keetChallenge) {
      return null
    }

    return {
      publicKey: keetPublicKey,
      signature: keetSignature,
      challenge: keetChallenge
    }
  }

  /**
   * Authenticate using Keet identity signature
   * @param {Object} credentials - { publicKey, signature, challenge, challengeId }
   * @returns {Promise<Object>} { identityId, method, verified }
   */
  async authenticate(credentials) {
    const { publicKey, signature, challenge, challengeId } = credentials

    // Verify signature
    const isValid = await this.verifySignature(publicKey, signature, challenge)

    if (!isValid) {
      throw new Error('Invalid Keet signature')
    }

    // Check if identity exists
    let identity = await this.registry.getIdentityByPublicKey(publicKey)

    if (!identity) {
      // Auto-register new Keet identity with read-only permissions
      const { user, identity: newIdentity } = await this.registry.autoRegister({
        identityId: publicKey,
        method: 'keet-identity',
        credentials: { publicKey }
      })

      console.log(`[KeetIdentity] Auto-registered: ${publicKey.substring(0, 8)}... → user ${user.userId}`)

      return {
        identityId: newIdentity.identityId,
        method: 'keet-identity',
        verified: true,
        credentials: { publicKey }
      }
    }

    // Check if identity is revoked
    const isRevoked = await this.registry.isRevoked(identity.identityId)
    if (isRevoked) {
      throw new Error('Keet identity has been revoked')
    }

    return {
      identityId: identity.identityId,
      method: 'keet-identity',
      verified: true,
      credentials: { publicKey }
    }
  }

  /**
   * Verify Keet signature using ed25519
   * @param {string} publicKey - Base64 public key
   * @param {string} signature - Base64 signature
   * @param {string} challenge - Challenge string
   * @returns {Promise<boolean>} True if valid
   */
  async verifySignature(publicKey, signature, challenge) {
    try {
      // Convert from base64
      const publicKeyBuf = b4a.from(publicKey, 'base64')
      const signatureBuf = b4a.from(signature, 'base64')
      const challengeBuf = b4a.from(challenge, 'utf8')

      // Validate buffer sizes
      if (publicKeyBuf.length !== 32) {
        console.error('[KeetIdentity] Invalid public key size:', publicKeyBuf.length)
        return false
      }

      if (signatureBuf.length !== 64) {
        console.error('[KeetIdentity] Invalid signature size:', signatureBuf.length)
        return false
      }

      // Verify ed25519 signature using hypercore-crypto
      const isValid = crypto.verify(challengeBuf, signatureBuf, publicKeyBuf)

      if (!isValid) {
        console.warn('[KeetIdentity] Signature verification failed')
      }

      return isValid
    } catch (err) {
      console.error('[KeetIdentity] Signature verification error:', err)
      return false
    }
  }

  /**
   * Generate new Keet identity from BIP39 mnemonic
   * @param {Object} options - { mnemonic } (optional)
   * @returns {Promise<Object>} { publicKey, mnemonic }
   */
  async generateIdentity(options = {}) {
    // Generate new mnemonic if not provided
    const mnemonic = options.mnemonic || IdentityKey.generateMnemonic()

    // Derive identity from mnemonic
    const identity = IdentityKey.fromMnemonic(mnemonic)
    const publicKey = identity.publicKey

    return {
      publicKey: b4a.toString(publicKey, 'base64'),  // 44 chars
      mnemonic: mnemonic  // BIP39 12/24 word mnemonic
    }
  }
}