/**
* 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 {
console.log('[ANProto] Verifying:', {
challenge: challenge.substring(0, 20) + '...',
signature: signature.substring(0, 20) + '...',
publicKey: publicKey.substring(0, 8) + '...'
})
const challengeBuffer = b4a.from(challenge, 'utf8')
const signatureBuffer = b4a.from(signature, 'base64')
const publicKeyBuffer = b4a.from(publicKey, 'base64')
console.log('[ANProto] Buffer sizes:', {
challenge: challengeBuffer.length,
signature: signatureBuffer.length,
publicKey: publicKeyBuffer.length
})
// Validate buffer sizes
if (publicKeyBuffer.length !== 32) {
console.error('[ANProto] Invalid public key size:', publicKeyBuffer.length)
return false
}
if (signatureBuffer.length !== 64) {
console.error('[ANProto] Invalid signature size:', signatureBuffer.length)
return false
}
const result = crypto.verify(challengeBuffer, signatureBuffer, publicKeyBuffer)
console.log('[ANProto] Verification result:', result)
return result
} catch (err) {
console.error('[ANProto] Verification error:', err)
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