chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// 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),
};
}