Secure storage and distribution of cryptographic keys in ATProto applications
1/**
2 * Basic Usage Example for @atpkeyserver/client
3 *
4 * This example demonstrates:
5 * 1. Crypto functions only (no keyserver)
6 * 2. KeyserverClient with encryption
7 * 3. KeyserverClient with decryption
8 * 4. Error handling patterns
9 *
10 * Prerequisites:
11 * - Node.js 22+ or Bun
12 * - @atpkeyserver/client installed
13 * - @atproto/api installed (for service auth)
14 *
15 * Run: bun run examples/basic-usage.ts
16 */
17
18import {
19 encryptMessage,
20 decryptMessage,
21 KeyserverClient,
22 ForbiddenError,
23 NotFoundError,
24 DecryptionError,
25 NetworkError,
26} from '../src/index'
27import { AtpAgent } from '@atproto/api'
28
29// ============================================================================
30// Example 1: Crypto Functions Only (No Keyserver)
31// ============================================================================
32
33function example1_cryptoOnly() {
34 console.log('\n=== Example 1: Crypto Functions Only ===\n')
35
36 // Message ID (AT-URI) used as Additional Authenticated Data (AAD)
37 const messageId = 'at://did:plc:abc123/app.bsky.feed.post/xyz789'
38
39 // 32-byte secret key (64 hex characters)
40 const secretKey =
41 '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
42
43 // Plaintext message
44 const plaintext = JSON.stringify({
45 text: 'Secret message for followers',
46 createdAt: new Date().toISOString(),
47 })
48
49 console.log('Original plaintext:', plaintext)
50
51 // Encrypt
52 const ciphertext = encryptMessage(messageId, secretKey, plaintext)
53 console.log('Encrypted (hex):', ciphertext.slice(0, 64) + '...')
54
55 // Decrypt
56 const decrypted = decryptMessage(messageId, secretKey, ciphertext)
57 console.log('Decrypted:', decrypted)
58
59 console.log('✅ Crypto round-trip successful!')
60}
61
62// ============================================================================
63// Example 2: KeyserverClient with Encryption
64// ============================================================================
65
66async function example2_keyserverEncryption() {
67 console.log('\n=== Example 2: KeyserverClient Encryption ===\n')
68
69 // Setup PDS client
70 const agent = new AtpAgent({ service: 'https://bsky.social' })
71
72 // In real usage, authenticate with user credentials:
73 // await agent.login({
74 // identifier: 'user.bsky.social',
75 // password: 'app-password'
76 // })
77
78 // For this example, we'll use a mock setup
79 console.log('⚠️ Skipping PDS login (example only)')
80 const mockDid = 'did:plc:example123'
81 const mockServiceAuthToken = 'mock_jwt_token'
82
83 // Create keyserver client
84 const keyserver = new KeyserverClient({
85 keyserverDid: 'did:web:keyserver.example.com',
86
87 // Service auth token provider
88 getServiceAuthToken: async (aud: string, lxm?: string) => {
89 console.log(`Getting service auth token for aud=${aud}, lxm=${lxm}`)
90
91 // In real usage, call PDS:
92 // const { data } = await agent.com.atproto.server.getServiceAuth({
93 // aud,
94 // lxm,
95 // exp: Math.floor(Date.now() / 1000) + 60
96 // })
97 // return data.token
98
99 return mockServiceAuthToken
100 },
101 })
102
103 // Encrypt a post
104 const groupId = `${mockDid}#followers`
105 const postUri = 'at://did:plc:abc123/app.bsky.feed.post/post1'
106 const plaintext = JSON.stringify({
107 text: 'Secret message for followers only',
108 createdAt: new Date().toISOString(),
109 })
110
111 console.log('Encrypting for group:', groupId)
112 console.log('Post URI:', postUri)
113
114 try {
115 const { ciphertext, version } = await keyserver.encrypt(
116 postUri,
117 groupId,
118 plaintext,
119 )
120 console.log('✅ Encrypted successfully!')
121 console.log('Ciphertext:', ciphertext.slice(0, 64) + '...')
122 console.log('Key version:', version)
123
124 // In real usage, store this in your PDS:
125 // await agent.com.atproto.repo.putRecord({
126 // repo: mockDid,
127 // collection: 'app.bsky.feed.post',
128 // record: {
129 // encrypted_content: ciphertext,
130 // key_version: version,
131 // encrypted_at: new Date().toISOString(),
132 // visibility: 'followers'
133 // }
134 // })
135
136 console.log(
137 '📝 In production, store encrypted_content + key_version in PDS',
138 )
139 } catch (error) {
140 console.error('❌ Encryption failed:', error)
141 }
142
143 // Clear cache when done
144 keyserver.clearCache()
145}
146
147// ============================================================================
148// Example 3: KeyserverClient with Decryption
149// ============================================================================
150
151async function example3_keyserverDecryption() {
152 console.log('\n=== Example 3: KeyserverClient Decryption ===\n')
153
154 // Setup (same as example 2)
155 const mockServiceAuthToken = 'mock_jwt_token'
156
157 const keyserver = new KeyserverClient({
158 keyserverDid: 'did:web:keyserver.example.com',
159 getServiceAuthToken: async () => mockServiceAuthToken,
160 })
161
162 // Simulated encrypted post from feed
163 const encryptedPost = {
164 uri: 'at://did:plc:abc123/app.bsky.feed.post/post1',
165 author: 'did:plc:abc123',
166 encrypted_content: '0123456789abcdef0123456789abcdef0123456789abcdef...',
167 key_version: 2,
168 visibility: 'followers',
169 }
170
171 const groupId = `${encryptedPost.author}#${encryptedPost.visibility}`
172
173 console.log('Decrypting post:', encryptedPost.uri)
174 console.log('Group:', groupId)
175
176 try {
177 const plaintext = await keyserver.decrypt(
178 encryptedPost.uri,
179 groupId,
180 encryptedPost.encrypted_content,
181 encryptedPost.key_version,
182 )
183
184 const decrypted = JSON.parse(plaintext)
185 console.log('✅ Decrypted successfully!')
186 console.log('Content:', decrypted)
187 } catch (error) {
188 console.error('❌ Decryption failed (expected in mock environment)')
189 console.error('Error:', error instanceof Error ? error.message : error)
190 }
191
192 keyserver.clearCache()
193}
194
195// ============================================================================
196// Example 4: Error Handling Patterns
197// ============================================================================
198
199async function example4_errorHandling() {
200 console.log('\n=== Example 4: Error Handling ===\n')
201
202 const mockServiceAuthToken = 'mock_jwt_token'
203
204 const keyserver = new KeyserverClient({
205 keyserverDid: 'did:web:keyserver.example.com',
206 getServiceAuthToken: async () => mockServiceAuthToken,
207 })
208
209 // Simulated encrypted post
210 const post = {
211 uri: 'at://did:plc:abc123/app.bsky.feed.post/post1',
212 groupId: 'did:plc:abc123#followers',
213 ciphertext: 'mock_ciphertext',
214 version: 2,
215 }
216
217 try {
218 const plaintext = await keyserver.decrypt(
219 post.uri,
220 post.groupId,
221 post.ciphertext,
222 post.version,
223 )
224 console.log('Decrypted:', plaintext)
225 } catch (error) {
226 // Handle specific error types
227 if (error instanceof ForbiddenError) {
228 console.log('🚫 ForbiddenError: User lost access to this group')
229 console.log('→ Display: "You no longer have access to this content"')
230 } else if (error instanceof NotFoundError) {
231 console.log('🔍 NotFoundError: Group was deleted')
232 console.log('→ Display: "This group no longer exists"')
233 } else if (error instanceof DecryptionError) {
234 console.log('🔐 DecryptionError: Cannot decrypt (corrupted/wrong key)')
235 console.log('→ Display: "Cannot decrypt this message"')
236 console.log('→ Do NOT retry - this is permanent')
237 } else if (error instanceof NetworkError) {
238 console.log('🌐 NetworkError: Temporary network issue')
239 console.log('→ Can retry with exponential backoff')
240 console.log(
241 `→ Status: ${(error as NetworkError).statusCode || 'unknown'}`,
242 )
243 } else {
244 console.log('❓ Unknown error:', error)
245 console.log('→ Log for investigation and show generic error to user')
246 }
247 }
248
249 keyserver.clearCache()
250}
251
252// ============================================================================
253// Example 5: Cache Management
254// ============================================================================
255
256async function example5_cacheManagement() {
257 console.log('\n=== Example 5: Cache Management ===\n')
258
259 const keyserver = new KeyserverClient({
260 keyserverDid: 'did:web:keyserver.example.com',
261 getServiceAuthToken: async () => 'mock_token',
262 cache: {
263 maxSize: 1000, // Max 1000 cached keys
264 activeKeyTtl: 3600, // Active keys cached for 1 hour
265 historicalKeyTtl: 86400, // Historical keys cached for 24 hours
266 },
267 })
268
269 console.log('Cache configuration:')
270 console.log('- Max size: 1000 entries')
271 console.log('- Active key TTL: 1 hour')
272 console.log('- Historical key TTL: 24 hours')
273
274 // Simulate some operations that would populate cache
275 console.log('\n📦 Keys and tokens are cached automatically')
276 console.log('📦 Subsequent requests use cache (faster, less load)')
277
278 // On logout, clear everything
279 console.log('\n🚪 User logging out...')
280 keyserver.clearCache()
281 console.log('✅ All keys and service auth tokens cleared from memory')
282
283 console.log('\n⚠️ Important: Keys are NEVER persisted to disk')
284 console.log('⚠️ This is a security feature')
285}
286
287// ============================================================================
288// Example 6: Batch Decryption
289// ============================================================================
290
291async function example6_batchDecryption() {
292 console.log('\n=== Example 6: Batch Decryption ===\n')
293
294 const keyserver = new KeyserverClient({
295 keyserverDid: 'did:web:keyserver.example.com',
296 getServiceAuthToken: async () => 'mock_token',
297 })
298
299 // Simulated encrypted posts from feed
300 const encryptedPosts = [
301 {
302 uri: 'at://did:plc:abc/app.bsky.feed.post/1',
303 author: 'did:plc:abc',
304 encrypted_content: 'cipher1...',
305 key_version: 1,
306 visibility: 'followers',
307 },
308 {
309 uri: 'at://did:plc:xyz/app.bsky.feed.post/2',
310 author: 'did:plc:xyz',
311 encrypted_content: 'cipher2...',
312 key_version: 2,
313 visibility: 'premium',
314 },
315 ]
316
317 console.log(`Decrypting ${encryptedPosts.length} posts in parallel...`)
318
319 // Decrypt all posts concurrently
320 const results = await Promise.allSettled(
321 encryptedPosts.map(async (post) => {
322 const groupId = `${post.author}#${post.visibility}`
323 const plaintext = await keyserver.decrypt(
324 post.uri,
325 groupId,
326 post.encrypted_content,
327 post.key_version,
328 )
329 return {
330 uri: post.uri,
331 content: JSON.parse(plaintext),
332 }
333 }),
334 )
335
336 // Process results
337 const successful = results.filter((r) => r.status === 'fulfilled')
338 const failed = results.filter((r) => r.status === 'rejected')
339
340 console.log(`✅ Successfully decrypted: ${successful.length}`)
341 console.log(`❌ Failed to decrypt: ${failed.length}`)
342
343 // In real usage, display successful posts and handle failures gracefully
344 failed.forEach((result, index) => {
345 const reason = (result as PromiseRejectedResult).reason
346 console.log(
347 ` Post ${index + 1}: ${reason instanceof Error ? reason.message : 'Unknown error'}`,
348 )
349 })
350
351 keyserver.clearCache()
352}
353
354// ============================================================================
355// Run All Examples
356// ============================================================================
357
358async function runAllExamples() {
359 console.log('╔═══════════════════════════════════════════════════════════╗')
360 console.log('║ @atpkeyserver/client - Basic Usage Examples ║')
361 console.log('╚═══════════════════════════════════════════════════════════╝')
362
363 try {
364 example1_cryptoOnly()
365 await example2_keyserverEncryption()
366 await example3_keyserverDecryption()
367 await example4_errorHandling()
368 await example5_cacheManagement()
369 await example6_batchDecryption()
370
371 console.log(
372 '\n╔═══════════════════════════════════════════════════════════╗',
373 )
374 console.log('║ All examples completed! ║')
375 console.log('╚═══════════════════════════════════════════════════════════╝')
376 console.log('\n📖 For more information:')
377 console.log(' - README.md: Package documentation')
378 console.log(' - docs/ENCRYPTION_PROTOCOL.md: Protocol specification')
379 console.log(' - docs/SECURITY.md: Security best practices')
380 } catch (error) {
381 console.error('\n❌ Example failed:', error)
382 process.exit(1)
383 }
384}
385
386// Run if executed directly
387if (import.meta.main) {
388 runAllExamples()
389}