OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / public / anproto-client.js

92 lines · javascript ref:
/**
 * ANProto Client-Side Signing
 *
 * Lightweight ed25519 signing for browser using noble/ed25519
 * Vendored to avoid external dependencies
 */

// Import noble/ed25519 from CDN
// In production, vendor this file locally
const ed25519Module = await (async () => {
  try {
    // Try to use @noble/ed25519 if available
    if (typeof window.nobleEd25519 !== 'undefined') {
      return window.nobleEd25519;
    }

    // Otherwise, load from CDN
    const module = await import('https://cdn.jsdelivr.net/npm/@noble/ed25519@2.0.0/+esm');
    return module;
  } catch (err) {
    console.error('[ANProto] Failed to load ed25519:', err);
    throw new Error('ed25519 library not available');
  }
})();

export const ANProtoClient = {
  /**
   * Sign a challenge using ed25519
   * @param {string} challenge - Challenge string
   * @param {string} secretKeyB64 - Base64 secret key (88 chars)
   * @returns {Promise<string>} Base64 signature (88 chars)
   */
  async sign(challenge, secretKeyB64) {
    try {
      // Convert inputs
      const challengeBytes = new TextEncoder().encode(challenge);
      const secretKeyBytes = base64ToBytes(secretKeyB64);

      // Validate secret key size (should be 64 bytes)
      if (secretKeyBytes.length !== 64) {
        throw new Error(\`Invalid secret key length: \${secretKeyBytes.length} (expected 64)\`);
      }

      // Extract signing key (first 32 bytes of 64-byte secret key for ed25519)
      const signingKey = secretKeyBytes.slice(0, 32);

      // Sign using noble/ed25519
      const signature = await ed25519Module.sign(challengeBytes, signingKey);

      // Validate signature size
      if (signature.length !== 64) {
        throw new Error(\`Invalid signature length: \${signature.length}\`);
      }

      // Convert to base64 (should be 88 chars)
      return bytesToBase64(signature);
    } catch (err) {
      console.error('[ANProto] Signing error:', err);
      throw err;
    }
  },

  /**
   * Get public key from keypair
   * @param {string} keypair - 132 char base64 keypair
   * @returns {string} 44 char base64 public key
   */
  getPublicKey(keypair) {
    return keypair.substring(0, 44);
  }
};

/**
 * Convert base64 to Uint8Array
 */
function base64ToBytes(base64) {
  const binary = atob(base64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return bytes;
}

/**
 * Convert Uint8Array to base64
 */
function bytesToBase64(bytes) {
  const binary = String.fromCharCode(...bytes);
  return btoa(binary);
}