Encrypted, ephemeral, private memos on atproto

feat(consumer): generate key pairs

graham.systems 17100ea8 63d21e05

verified
Changed files
+68 -11
packages
+6
deno.lock
··· 7 7 "jsr:@noble/hashes@2.0": "2.0.1", 8 8 "jsr:@noble/hashes@^2.0.1": "2.0.1", 9 9 "jsr:@noble/post-quantum@~0.5.2": "0.5.2", 10 + "jsr:@puregarlic/randimal@^1.0.1": "1.0.1", 10 11 "jsr:@std/assert@^1.0.14": "1.0.14", 11 12 "jsr:@std/expect@^1.0.17": "1.0.17", 12 13 "jsr:@std/internal@^1.0.10": "1.0.10", ··· 35 36 "jsr:@noble/curves", 36 37 "jsr:@noble/hashes@2.0" 37 38 ] 39 + }, 40 + "@puregarlic/randimal@1.0.1": { 41 + "integrity": "101da9e89561f4b8038426f47f6cb258484a11887e1f4d8a2b07cd5ebf487a5d" 38 42 }, 39 43 "@std/assert@1.0.14": { 40 44 "integrity": "68d0d4a43b365abc927f45a9b85c639ea18a9fab96ad92281e493e4ed84abaa4", ··· 169 173 "members": { 170 174 "packages/consumer": { 171 175 "dependencies": [ 176 + "jsr:@puregarlic/randimal@^1.0.1", 177 + "npm:@atcute/atproto@^3.1.9", 172 178 "npm:@atcute/client@^4.0.5", 173 179 "npm:@atcute/lexicons@^1.2.2" 174 180 ]
+3 -1
packages/consumer/deno.jsonc
··· 4 4 ".": "./mod.ts" 5 5 }, 6 6 "imports": { 7 + "@atcute/atproto": "npm:@atcute/atproto@^3.1.9", 7 8 "@atcute/client": "npm:@atcute/client@^4.0.5", 8 - "@atcute/lexicons": "npm:@atcute/lexicons@^1.2.2" 9 + "@atcute/lexicons": "npm:@atcute/lexicons@^1.2.2", 10 + "@puregarlic/randimal": "jsr:@puregarlic/randimal@^1.0.1" 9 11 } 10 12 }
+50 -6
packages/consumer/mod.ts
··· 1 - import type { Client, CredentialManager } from "@atcute/client"; 2 1 import { produceRequirements } from "@cistern/shared"; 2 + import { generateKeys } from "@cistern/crypto"; 3 + import { generateRandomName } from "@puregarlic/randimal"; 3 4 import type { Did } from "@atcute/lexicons/syntax"; 5 + import type { Client, CredentialManager } from "@atcute/client"; 6 + import type { AppCisternLexiconPubkey } from "@cistern/lexicon"; 4 7 import type { ConsumerOptions, ConsumerParams, LocalKeyPair } from "./types.ts"; 8 + 9 + import type {} from "@atcute/atproto"; 5 10 6 11 export async function createConsumer( 7 12 options: ConsumerOptions, ··· 22 27 23 28 constructor(params: ConsumerParams) { 24 29 this.did = params.miniDoc.did; 25 - this.keypair = params.options.keypair; 30 + this.keypair = params.options.keypair 31 + ? { 32 + privateKey: Uint8Array.fromBase64(params.options.keypair.privateKey), 33 + publicKey: params.options.keypair.publicKey, 34 + } 35 + : undefined; 26 36 this.rpc = params.rpc; 27 37 this.manager = params.manager; 28 38 } 29 39 30 40 /** 31 41 * Generates a key pair, uploading the public key to PDS and returning the pair. 32 - * @todo Generate key pair 33 - * @todo Store private key in local registry 34 - * @todo Upload public key to PDS 35 42 */ 36 - async generateKeyPair() {} 43 + async generateKeyPair(): Promise<LocalKeyPair> { 44 + if (this.keypair) { 45 + throw new Error("client already has a key pair"); 46 + } 47 + 48 + const keys = generateKeys(); 49 + const name = await generateRandomName(); 50 + 51 + const record: AppCisternLexiconPubkey.Main = { 52 + $type: "app.cistern.lexicon.pubkey", 53 + name, 54 + algorithm: "x_wing", 55 + content: keys.publicKey.toBase64(), 56 + createdAt: new Date().toISOString(), 57 + }; 58 + const res = await this.rpc.post("com.atproto.repo.createRecord", { 59 + input: { 60 + collection: "app.cistern.lexicon.pubkey", 61 + repo: this.did, 62 + record, 63 + }, 64 + }); 65 + 66 + if (!res.ok) { 67 + throw new Error( 68 + `failed to save public key: ${res.status} ${res.data.error}`, 69 + ); 70 + } 71 + 72 + const keypair = { 73 + privateKey: keys.secretKey, 74 + publicKey: res.data.uri, 75 + }; 76 + 77 + this.keypair = keypair; 78 + 79 + return keypair; 80 + } 37 81 38 82 /** 39 83 * Returns an async iterator that returns pages of the user's items from their PDS.
+9 -4
packages/consumer/types.ts
··· 1 1 import type { BaseClientOptions, ClientRequirements } from "@cistern/shared"; 2 - import type { AppCisternLexiconPubkey } from "@cistern/lexicon"; 2 + import type { ResourceUri } from "@atcute/lexicons"; 3 + 4 + export interface InputLocalKeyPair { 5 + privateKey: string; 6 + publicKey: ResourceUri; 7 + } 3 8 4 9 export interface LocalKeyPair { 5 - privateKey: string; 6 - publicKey: AppCisternLexiconPubkey.Main; 10 + privateKey: Uint8Array; 11 + publicKey: ResourceUri; 7 12 } 8 13 9 14 export interface ConsumerOptions extends BaseClientOptions { 10 - keypair?: LocalKeyPair; 15 + keypair?: InputLocalKeyPair; 11 16 } 12 17 13 18 export type ConsumerParams = ClientRequirements<ConsumerOptions>;