// Mints a rotation (recovery) key entirely in the browser for a migration. The // private key is generated here and NEVER sent anywhere — only the public // did:key goes into the PLC operation as a rotation key. The user keeps the // private key so they can recover their identity if their PDS is ever lost. // // secp256k1, to match the wider atproto tooling (goat, pds-moover): a key minted // here can later be handed to `goat account plc` to sign a recovery operation. import { secp256k1 } from '@noble/curves/secp256k1'; // Multicodec prefix for a secp256k1 public key, varint-encoded (0xe701). A // compressed point behind this prefix, base58btc-encoded, is the `zQ3sh…` // did:key every atproto rotation key uses. const SECP256K1_MULTICODEC = [0xe7, 0x01]; const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; /** Encode bytes as base58btc (Bitcoin alphabet), preserving leading zeros. */ function base58btc(/** @type {Uint8Array} */ bytes) { let value = 0n; for (const byte of bytes) value = value * 256n + BigInt(byte); let out = ''; while (value > 0n) { out = BASE58_ALPHABET[Number(value % 58n)] + out; value /= 58n; } for (const byte of bytes) { if (byte === 0) out = `1${out}`; else break; } return out; } /** Lowercase hex, the format `goat` expects for a k256 private key. */ function toHex(/** @type {Uint8Array} */ bytes) { return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join(''); } /** * Generate a fresh secp256k1 recovery key. * @returns {{ didKey: string, privateKeyHex: string }} the public did:key to put * in the PLC op, and the private key (hex) for the user to save. */ export function generateRecoveryKey() { const privateKey = secp256k1.utils.randomPrivateKey(); const publicKey = secp256k1.getPublicKey(privateKey, true); // compressed, 33 bytes const prefixed = new Uint8Array([...SECP256K1_MULTICODEC, ...publicKey]); return { didKey: `did:key:z${base58btc(prefixed)}`, privateKeyHex: toHex(privateKey), }; }