Secure storage and distribution of cryptographic keys in ATProto applications
at main 389 lines 13 kB view raw
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}