/**
* Challenge-response verification using canonical ANProto wire format.
*
* The browser-side anproto-client.js produces:
* hash = SHA-256(challenge)
* signature = pubkey(44) + base64(ed25519_sign(timestamp + hash) || timestamp + hash)
*
* Server verifies using an.open() from the standalone anproto module:
* opened = an.open(signature) → "{timestamp}{hash}"
* parsed = an.parse(opened) → { timestamp, hash }
* check: hash === SHA-256(challenge) && timestamp is recent
*/
import { an } from 'anproto'
import b4a from 'b4a'
/**
* Verify a challenge signature in canonical ANProto format.
*
* @param {string} challenge - the challenge string the server issued
* @param {string} signature - canonical wire-format: pubkey(44) + base64(combined envelope)
* @returns {Promise<{ valid: boolean, publicKey: string|null, error: string|null }>}
*/
export async function verifyChallenge (challenge, signature) {
if (typeof challenge !== 'string' || !challenge) {
return { valid: false, publicKey: null, error: 'missing challenge' }
}
if (typeof signature !== 'string' || signature.length < 44) {
return { valid: false, publicKey: null, error: 'missing or short signature' }
}
try {
const publicKey = signature.substring(0, 44)
const opened = await an.open(signature)
const parsed = an.parse(opened)
const expectedHash = await an.hash(challenge)
if (parsed.hash !== expectedHash) {
return { valid: false, publicKey, error: 'hash mismatch' }
}
// Check timestamp freshness (5 minute window)
const age = Date.now() - parsed.timestamp
if (age < -60000 || age > 5 * 60 * 1000) {
return { valid: false, publicKey, error: 'timestamp out of range' }
}
return { valid: true, publicKey, error: null }
} catch (err) {
return { valid: false, publicKey: null, error: err.message }
}
}
/**
* Convert base64 public key (44 chars) to hex (64 chars).
* Used for storing identities in trust-map (which keys by hex).
*/
export function publicKeyB64ToHex (publicKeyB64) {
const pk = b4a.from(publicKeyB64, 'base64')
if (pk.length !== 32) throw new Error('invalid pubkey length')
return b4a.toString(pk, 'hex')
}
/**
* Convert hex public key (64 chars) to base64 (44 chars).
*/
export function publicKeyHexToB64 (publicKeyHex) {
const pk = b4a.from(publicKeyHex, 'hex')
if (pk.length !== 32) throw new Error('invalid pubkey length')
return b4a.toString(pk, 'base64')
}