import {defs, normalizeOp} from '@atcute/did-plc'; import {P256PrivateKey, parsePrivateMultikey, Secp256k1PrivateKey, Secp256k1PrivateKeyExportable} from '@atcute/crypto'; import * as CBOR from '@atcute/cbor'; import {fromBase16, toBase64Url} from '@atcute/multibase'; //NOTES // Don't forget disputes can check https://github.dev/mary-ext/boat/blob/trunk/src/views/identity/plc-applicator/steps/step1_handle-input.tsx //This is if a previous operation should be disputed, it will be a check box on recovery const PLC_DIRECTORY_URL = 'https://plc.directory'; // Helper to base64url-encode JSON const jsonToB64Url = (obj) => { const enc = new TextEncoder(); const json = JSON.stringify(obj); return toBase64Url(enc.encode(json)); }; class PlcOps { constructor() { } //TODO ui // For unvaldiating a record in 72hr window add an advance option input for that cid //that will be the easiest way at launch and just help ppl //NEEDS // Function to get current rotation keys // function to create a new key // function to add a new key and save and submit // Can use that same function for signing a recover async testSignAServiceAuthToken(did) { // const testPrivateKey = 'z3vLhP9c6gwUUwA4jkyYE9SXifE7wD1f3rVMMLmqBq8TaT2B'; let signingKeypair = await this.getKeyPair(testPrivateKey); let test = await this.createANewServiceAuthToken(did, 'did:web:dev.pdsmoover.com', signingKeypair.keypair, 'com.pdsmoover.backup.requestBackup'); console.log(test); } async exampleOfSigningAPLCOPManuallyThatWorks() { //dev keys // New Rotation Key: did:key:zQ3shXuksWLbyTTbWrSJ41qZvR2eyNFGTdbjjG3b2MWRo5cSx // New Private Key: z3vLhP9c6gwUUwA4jkyYE9SXifE7wD1f3rVMMLmqBq8TaT2B const testPrivateKey = 'z3vLhP9c6gwUUwA4jkyYE9SXifE7wD1f3rVMMLmqBq8TaT2B'; let signingKeypair = await this.getKeyPair(testPrivateKey); let {lastOperation, base} = await this.getLastPlcOpFromPlc(did); console.log(lastOperation); let {privateKey, publicKey} = await this.createANewSecp256k1(); console.log('New Rotation Key:', publicKey); console.log('New Private Key:', privateKey); let newRotationKeys = lastOperation.rotationKeys || []; //Adds the new one to the top newRotationKeys.unshift(publicKey); await this.signAndPublishNewOp(did, signingKeypair.keypair, lastOperation.alsoKnownAs, newRotationKeys, lastOperation.services.atproto_pds.endpoint, lastOperation.verificationMethods.atproto, base.cid); } async getCurrentRotationKeysForUser(did) { const logs = await this.getPlcAuditLogs(did); const {rotationKeys} = this.getLastPlcOp(logs); return rotationKeys; } async getLastPlcOpFromPlc(did) { const logs = await this.getPlcAuditLogs(did); return this.getLastPlcOp(logs); } getLastPlcOp(logs) { const lastOp = logs.at(-1); return {lastOperation: normalizeOp(lastOp.operation), base: lastOp}; } async getPlcAuditLogs(did) { const response = await fetch(`${PLC_DIRECTORY_URL}/${did}/log/audit`); if (!response.ok) { throw new Error(`got resposne ${response.status}`); } const json = await response.json(); return defs.indexedEntryLog.parse(json); } /** * Creates a new secp256k1 key that can be used for either rotation or verification key * @returns {Promise<{privateKey: string, publicKey: `did:key:${string}`}>} */ async createANewSecp256k1() { let keypair = await Secp256k1PrivateKeyExportable.createKeypair(); let publicKey = await keypair.exportPublicKey('did'); let privateKey = await keypair.exportPrivateKey('multikey'); return { privateKey, publicKey }; } /** * Signs a new operation with the provided signing key and submits it * @param did * @param signingRotationKey * @param alsoKnownAs * @param rotationKeys * @param pds * @param verificationKey * @returns {Promise} */ async signAndPublishNewOp(did, signingRotationKey, alsoKnownAs, rotationKeys, pds, verificationKey, prev) { const operation = { type: 'plc_operation', // prev: prev!.cid, prev, alsoKnownAs, rotationKeys, services: { atproto_pds: { type: 'AtprotoPersonalDataServer', endpoint: pds } }, verificationMethods: { atproto: verificationKey } }; const opBytes = CBOR.encode(operation); const sigBytes = await signingRotationKey.sign(opBytes); const signature = toBase64Url(sigBytes); const signedOperation = { ...operation, sig: signature, }; await this.pushPlcOperation(did, signedOperation); } /** * Takes a multi or hexbased private key and returns a keypair * @param privateKeyString * @param type * @returns {Promise<{type: string, didPublicKey: `did:key:${string}`, keypair: P256PrivateKey|Secp256k1PrivateKey}>} */ async getKeyPair(privateKeyString, type = 'p256') { const HEX_REGEX = /^[0-9a-f]+$/i; const MULTIKEY_REGEX = /^z[a-km-zA-HJ-NP-Z1-9]+$/; let keypair = undefined; if (HEX_REGEX.test(privateKeyString)) { const privateKeyBytes = fromBase16(privateKeyString); switch (type) { case 'p256': { keypair = await P256PrivateKey.importRaw(privateKeyBytes); break; } case 'secp256k1': { keypair = await Secp256k1PrivateKey.importRaw(privateKeyBytes); break; } default: { throw new Error(`unsupported "${type}" type`); } } } else if (MULTIKEY_REGEX.test(privateKeyString)) { const match = parsePrivateMultikey(privateKeyString); console.log(match); const privateKeyBytes = match.privateKeyBytes; switch (match.type) { case 'p256': { keypair = await P256PrivateKey.importRaw(privateKeyBytes); console.log(keypair); break; } case 'secp256k1': { keypair = await Secp256k1PrivateKey.importRaw(privateKeyBytes); break; } default: { throw new Error(`unsupported "${type}" type`); } } } else { throw new Error('unknown input format'); } return { type: 'private_key', didPublicKey: await keypair.exportPublicKey('did'), keypair: keypair, }; } async pushPlcOperation(did, operation) { const response = await fetch(`${PLC_DIRECTORY_URL}/${did}`, { method: 'post', headers: { 'content-type': 'application/json', }, body: JSON.stringify(operation), }); const headers = response.headers; if (!response.ok) { const type = headers.get('content-type'); if (type?.includes('application/json')) { const json = await response.json(); if (typeof json === 'object' && json !== null && typeof json.message === 'string') { throw new Error(json.message); } } throw new Error(`got http ${response.status} from plc`); } }; /** * * @param iss The user's did * @param aud The did:web, if it's a PDS it's usually from /xrpc/com.atproto.server.describeServer * @param keypair The keypair to sign with only supporting ES256K atm * @param lxm The lxm which is usually com.atproto.server.createAccount for creating a new account * @returns {Promise} */ async createANewServiceAuthToken(iss, aud, keypair, lxm) { // Compute iat/exp defaults (60s window like reference: MINUTE/1e3) const iat = Math.floor(Date.now() / 1e3); const exp = iat + 60; // Generate a 16-byte hex jti const jti = (() => { const bytes = new Uint8Array(16); // crypto in browser or node; fall back safely (globalThis.crypto || window.crypto).getRandomValues(bytes); return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join(''); })(); // Build header and payload (omit undefined fields) // Just defaulting to ES256K since p256 was not importing on firefox const header = {typ: 'JWT', alg: 'ES256K'}; const payload = {}; payload.iat = iat; payload.iss = iss; payload.aud = aud; payload.exp = exp; payload.lxm = lxm; payload.jti = jti; const headerB64 = jsonToB64Url(header); const payloadB64 = jsonToB64Url(payload); const toSignStr = `${headerB64}.${payloadB64}`; // Sign const toSignBytes = new TextEncoder().encode(toSignStr); const sigBytes = await keypair.sign(toSignBytes); // Return compact JWS const sigB64 = toBase64Url(sigBytes); return `${toSignStr}.${sigB64}`; } } export {PlcOps};