/**
* 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