Client side atproto account migrator in your web browser, along with services for backups and adversarial migrations.
at next 280 lines 8.7 kB view raw
1import {defs, normalizeOp} from '@atcute/did-plc'; 2import {P256PrivateKey, parsePrivateMultikey, Secp256k1PrivateKey, Secp256k1PrivateKeyExportable} from '@atcute/crypto'; 3import * as CBOR from '@atcute/cbor'; 4import {fromBase16, toBase64Url} from '@atcute/multibase'; 5 6//NOTES 7// Don't forget disputes can check https://github.dev/mary-ext/boat/blob/trunk/src/views/identity/plc-applicator/steps/step1_handle-input.tsx 8//This is if a previous operation should be disputed, it will be a check box on recovery 9 10const PLC_DIRECTORY_URL = 'https://plc.directory'; 11 12 13// Helper to base64url-encode JSON 14const jsonToB64Url = (obj) => { 15 const enc = new TextEncoder(); 16 const json = JSON.stringify(obj); 17 return toBase64Url(enc.encode(json)); 18}; 19 20class PlcOps { 21 constructor() { 22 23 } 24 25 //TODO ui 26 // For unvaldiating a record in 72hr window add an advance option input for that cid 27 //that will be the easiest way at launch and just help ppl 28 29 //NEEDS 30 // Function to get current rotation keys 31 // function to create a new key 32 // function to add a new key and save and submit 33 // Can use that same function for signing a recover 34 35 async testSignAServiceAuthToken(did) { 36 // 37 const testPrivateKey = 'z3vLhP9c6gwUUwA4jkyYE9SXifE7wD1f3rVMMLmqBq8TaT2B'; 38 let signingKeypair = await this.getKeyPair(testPrivateKey); 39 let test = await this.createANewServiceAuthToken(did, 'did:web:dev.pdsmoover.com', signingKeypair.keypair, 'com.pdsmoover.backup.requestBackup'); 40 console.log(test); 41 } 42 43 44 async exampleOfSigningAPLCOPManuallyThatWorks() { 45 //dev keys 46 // New Rotation Key: did:key:zQ3shXuksWLbyTTbWrSJ41qZvR2eyNFGTdbjjG3b2MWRo5cSx 47 // New Private Key: z3vLhP9c6gwUUwA4jkyYE9SXifE7wD1f3rVMMLmqBq8TaT2B 48 const testPrivateKey = 'z3vLhP9c6gwUUwA4jkyYE9SXifE7wD1f3rVMMLmqBq8TaT2B'; 49 let signingKeypair = await this.getKeyPair(testPrivateKey); 50 let {lastOperation, base} = await this.getLastPlcOpFromPlc(did); 51 console.log(lastOperation); 52 let {privateKey, publicKey} = await this.createANewSecp256k1(); 53 console.log('New Rotation Key:', publicKey); 54 console.log('New Private Key:', privateKey); 55 let newRotationKeys = lastOperation.rotationKeys || []; 56 //Adds the new one to the top 57 newRotationKeys.unshift(publicKey); 58 await this.signAndPublishNewOp(did, signingKeypair.keypair, lastOperation.alsoKnownAs, newRotationKeys, lastOperation.services.atproto_pds.endpoint, lastOperation.verificationMethods.atproto, base.cid); 59 60 } 61 62 async getCurrentRotationKeysForUser(did) { 63 const logs = await this.getPlcAuditLogs(did); 64 const {rotationKeys} = this.getLastPlcOp(logs); 65 return rotationKeys; 66 } 67 68 async getLastPlcOpFromPlc(did) { 69 const logs = await this.getPlcAuditLogs(did); 70 return this.getLastPlcOp(logs); 71 } 72 73 getLastPlcOp(logs) { 74 const lastOp = logs.at(-1); 75 return {lastOperation: normalizeOp(lastOp.operation), base: lastOp}; 76 } 77 78 79 async getPlcAuditLogs(did) { 80 const response = await fetch(`${PLC_DIRECTORY_URL}/${did}/log/audit`); 81 if (!response.ok) { 82 throw new Error(`got resposne ${response.status}`); 83 } 84 85 const json = await response.json(); 86 return defs.indexedEntryLog.parse(json); 87 } 88 89 /** 90 * Creates a new secp256k1 key that can be used for either rotation or verification key 91 * @returns {Promise<{privateKey: string, publicKey: `did:key:${string}`}>} 92 */ 93 94 async createANewSecp256k1() { 95 let keypair = await Secp256k1PrivateKeyExportable.createKeypair(); 96 let publicKey = await keypair.exportPublicKey('did'); 97 let privateKey = await keypair.exportPrivateKey('multikey'); 98 return { 99 privateKey, 100 publicKey 101 }; 102 } 103 104 105 /** 106 * Signs a new operation with the provided signing key and submits it 107 * @param did 108 * @param signingRotationKey 109 * @param alsoKnownAs 110 * @param rotationKeys 111 * @param pds 112 * @param verificationKey 113 * @returns {Promise<void>} 114 */ 115 async signAndPublishNewOp(did, signingRotationKey, alsoKnownAs, rotationKeys, pds, verificationKey, prev) { 116 const operation = { 117 type: 'plc_operation', 118 // prev: prev!.cid, 119 prev, 120 alsoKnownAs, 121 rotationKeys, 122 services: { 123 atproto_pds: { 124 type: 'AtprotoPersonalDataServer', 125 endpoint: pds 126 } 127 }, 128 verificationMethods: { 129 atproto: verificationKey 130 } 131 }; 132 const opBytes = CBOR.encode(operation); 133 const sigBytes = await signingRotationKey.sign(opBytes); 134 135 const signature = toBase64Url(sigBytes); 136 137 const signedOperation = { 138 ...operation, 139 sig: signature, 140 }; 141 142 await this.pushPlcOperation(did, signedOperation); 143 } 144 145 /** 146 * Takes a multi or hexbased private key and returns a keypair 147 * @param privateKeyString 148 * @param type 149 * @returns {Promise<{type: string, didPublicKey: `did:key:${string}`, keypair: P256PrivateKey|Secp256k1PrivateKey}>} 150 */ 151 async getKeyPair(privateKeyString, type = 'p256') { 152 const HEX_REGEX = /^[0-9a-f]+$/i; 153 const MULTIKEY_REGEX = /^z[a-km-zA-HJ-NP-Z1-9]+$/; 154 let keypair = undefined; 155 156 if (HEX_REGEX.test(privateKeyString)) { 157 const privateKeyBytes = fromBase16(privateKeyString); 158 159 switch (type) { 160 case 'p256': { 161 keypair = await P256PrivateKey.importRaw(privateKeyBytes); 162 break; 163 } 164 case 'secp256k1': { 165 keypair = await Secp256k1PrivateKey.importRaw(privateKeyBytes); 166 break; 167 } 168 default: { 169 throw new Error(`unsupported "${type}" type`); 170 } 171 } 172 } else if (MULTIKEY_REGEX.test(privateKeyString)) { 173 174 const match = parsePrivateMultikey(privateKeyString); 175 console.log(match); 176 const privateKeyBytes = match.privateKeyBytes; 177 178 switch (match.type) { 179 case 'p256': { 180 keypair = await P256PrivateKey.importRaw(privateKeyBytes); 181 console.log(keypair); 182 break; 183 } 184 case 'secp256k1': { 185 keypair = await Secp256k1PrivateKey.importRaw(privateKeyBytes); 186 break; 187 } 188 default: { 189 throw new Error(`unsupported "${type}" type`); 190 } 191 } 192 } else { 193 throw new Error('unknown input format'); 194 } 195 196 return { 197 type: 'private_key', 198 didPublicKey: await keypair.exportPublicKey('did'), 199 keypair: keypair, 200 }; 201 } 202 203 async pushPlcOperation(did, operation) { 204 const response = await fetch(`${PLC_DIRECTORY_URL}/${did}`, { 205 method: 'post', 206 headers: { 207 'content-type': 'application/json', 208 }, 209 body: JSON.stringify(operation), 210 }); 211 212 const headers = response.headers; 213 if (!response.ok) { 214 const type = headers.get('content-type'); 215 216 if (type?.includes('application/json')) { 217 const json = await response.json(); 218 if (typeof json === 'object' && json !== null && typeof json.message === 'string') { 219 throw new Error(json.message); 220 } 221 } 222 223 throw new Error(`got http ${response.status} from plc`); 224 } 225 }; 226 227 228 /** 229 * 230 * @param iss The user's did 231 * @param aud The did:web, if it's a PDS it's usually from /xrpc/com.atproto.server.describeServer 232 * @param keypair The keypair to sign with only supporting ES256K atm 233 * @param lxm The lxm which is usually com.atproto.server.createAccount for creating a new account 234 * @returns {Promise<string>} 235 */ 236 async createANewServiceAuthToken(iss, aud, keypair, lxm) { 237 238 239 // Compute iat/exp defaults (60s window like reference: MINUTE/1e3) 240 const iat = Math.floor(Date.now() / 1e3); 241 const exp = iat + 60; 242 243 // Generate a 16-byte hex jti 244 const jti = (() => { 245 const bytes = new Uint8Array(16); 246 // crypto in browser or node; fall back safely 247 (globalThis.crypto || window.crypto).getRandomValues(bytes); 248 return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join(''); 249 })(); 250 251 252 // Build header and payload (omit undefined fields) 253 // Just defaulting to ES256K since p256 was not importing on firefox 254 const header = {typ: 'JWT', alg: 'ES256K'}; 255 const payload = {}; 256 payload.iat = iat; 257 payload.iss = iss; 258 payload.aud = aud; 259 payload.exp = exp; 260 payload.lxm = lxm; 261 payload.jti = jti; 262 263 const headerB64 = jsonToB64Url(header); 264 const payloadB64 = jsonToB64Url(payload); 265 const toSignStr = `${headerB64}.${payloadB64}`; 266 267 // Sign 268 const toSignBytes = new TextEncoder().encode(toSignStr); 269 const sigBytes = await keypair.sign(toSignBytes); 270 271 // Return compact JWS 272 const sigB64 = toBase64Url(sigBytes); 273 return `${toSignStr}.${sigB64}`; 274 } 275 276 277} 278 279 280export {PlcOps};