Secure storage and distribution of cryptographic keys in ATProto applications
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix and clarify previous doc files

+837 -1099
+12 -5
README.md
··· 43 43 44 44 ## Documentation 45 45 46 - - [Architecture](./docs/ARCHITECTURE.md) - Complete server architecture 47 - - [Client Implementation](./docs/CLIENT_IMPLEMENTATION.md) - Client library guide 48 - - API Reference - Coming soon 49 - - Encryption Protocol - Coming soon 50 - - Security Best Practices - Coming soon 46 + ### Getting Started 47 + - [Client README](./packages/client/README.md) - Quick start for using the client library 48 + - [Server README](./packages/server/README.md) - Server deployment guide 49 + 50 + ### Technical Documentation 51 + - [Encryption Protocol](./docs/ENCRYPTION_PROTOCOL.md) - End-to-end encryption protocol specification 52 + - [API Reference](./docs/API_REFERENCE.md) - Complete API endpoint reference 53 + - [Architecture](./docs/ARCHITECTURE.md) - Server-side architecture and design 54 + - [Security Best Practices](./docs/SECURITY.md) - Security guidelines and threat model 55 + 56 + ### Implementation 57 + - [Client Implementation Strategy](./docs/CLIENT_IMPLEMENTATION.md) - Client library design decisions 51 58 52 59 ## Requirements 53 60
+123 -378
docs/ARCHITECTURE.md
··· 1 1 # ATP Keyserver Architecture 2 2 3 + ## Documentation Guide 4 + 5 + This document focuses on **server-side architecture**. For other topics: 6 + 7 + - **End-to-end encryption protocol**: See [ENCRYPTION_PROTOCOL.md](./ENCRYPTION_PROTOCOL.md) 8 + - **Security best practices**: See [SECURITY.md](./SECURITY.md) 9 + - **Complete API reference**: See [API_REFERENCE.md](./API_REFERENCE.md) 10 + - **Client library usage**: See [Client README](../packages/client/README.md) 11 + - **Implementation strategy**: See [CLIENT_IMPLEMENTATION.md](./CLIENT_IMPLEMENTATION.md) 12 + 13 + 3 14 ## Overview 4 15 5 16 A specialized AT Protocol (ATProto) service for secure storage and distribution of cryptographic keys. This keyserver manages both asymmetric keypairs (Ed25519) for public key cryptography and symmetric keys (XChaCha20-Poly1305) for group-based encrypted communication. 6 17 18 + **Key principle:** The server manages keys but never performs encryption/decryption. All cryptographic operations happen client-side for true end-to-end encryption. 19 + 20 + 7 21 ## Technology Stack 8 22 9 23 ### Runtime & Core ··· 20 34 - `@noble/ed25519` - Ed25519 asymmetric key operations 21 35 - `@noble/ciphers` - XChaCha20-Poly1305 symmetric encryption 22 36 37 + 23 38 ## Architecture Components 24 39 25 40 ### 1. Entry Point (`main.ts`) ··· 33 48 34 49 ### 2. Database Layer (`lib/db.ts`) 35 50 36 - SQLite database with three normalized tables: 51 + SQLite database with four tables supporting key versioning and access logging: 37 52 38 53 ```sql 54 + -- Asymmetric keypairs (Ed25519) 39 55 keys 40 - ├── did (PRIMARY KEY) 41 - ├── public_key (TEXT) 42 - └── private_key (TEXT) 56 + ├── did (TEXT, NOT NULL) 57 + ├── version (INTEGER, NOT NULL, DEFAULT 1) 58 + ├── public_key (TEXT, NOT NULL) 59 + ├── private_key (TEXT, NOT NULL) 60 + ├── created_at (TEXT, NOT NULL) 61 + ├── revoked_at (TEXT, nullable) 62 + ├── status (TEXT, NOT NULL, DEFAULT 'active') 63 + ├── status_comment (TEXT, nullable) 64 + └── PRIMARY KEY (did, version) 65 + INDEX idx_keys_did_status ON (did, status) 43 66 67 + -- Symmetric group keys (XChaCha20-Poly1305) 44 68 groups 45 - ├── id (PRIMARY KEY) 46 - ├── owner_did (TEXT) 47 - └── secret_key (TEXT) 69 + ├── id (TEXT, NOT NULL) 70 + ├── version (INTEGER, NOT NULL, DEFAULT 1) 71 + ├── owner_did (TEXT, NOT NULL) 72 + ├── secret_key (TEXT, NOT NULL) 73 + ├── created_at (TEXT, NOT NULL) 74 + ├── revoked_at (TEXT, nullable) 75 + ├── status (TEXT, NOT NULL, DEFAULT 'active') 76 + ├── status_comment (TEXT, nullable) 77 + └── PRIMARY KEY (id, version) 78 + INDEX idx_groups_id_status ON (id, status) 48 79 80 + -- Group membership 49 81 group_members 50 - ├── group_id (FOREIGN KEY → groups.id) 51 - └── member_did (TEXT) 52 - ``` 82 + ├── group_id (TEXT, NOT NULL) 83 + ├── member_did (TEXT, NOT NULL) 84 + └── PRIMARY KEY (group_id, member_did) 85 + FOREIGN KEY (group_id) REFERENCES groups(id) 53 86 54 - **Features:** 55 - - WAL (Write-Ahead Logging) mode enabled for better concurrency 56 - - Type-safe schema definitions using TypeScript 57 - - Single database file (`keyserver.db`) 58 - - Exported DBSchema type for type-safe queries throughout the application 87 + -- Key access audit log 88 + key_access_log 89 + ├── id (INTEGER, PRIMARY KEY AUTOINCREMENT) 90 + ├── did (TEXT, NOT NULL) 91 + ├── version (INTEGER, NOT NULL) 92 + ├── accessed_at (TEXT, NOT NULL) 93 + ├── ip (TEXT, nullable) 94 + └── user_agent (TEXT, nullable) 95 + ``` 59 96 60 97 ### 3. Authentication Middleware (`lib/authMiddleware.ts`) 61 98 ··· 127 164 - `checkMembership(group_id, member_did)` - Verify member status 128 165 - `createGroup(group_id, owner_did)` - Create new group with secret key 129 166 167 + 130 168 ## API Endpoints 131 169 170 + The keyserver exposes the following endpoint categories: 171 + 132 172 ### Public Endpoints (No Authentication) 173 + - `GET /` - Service metadata 174 + - `GET /.well-known/did.json` - Service DID document 175 + - `GET /xrpc/dev.atpkeyserver.alpha.key.public` - Get any user's public key 133 176 134 - #### `GET /` 135 - Returns service metadata from package.json: 136 - ```json 137 - { 138 - "name": "atp-keyserver", 139 - "version": "1.0.0" 140 - } 141 - ``` 177 + ### Protected Endpoints (Require Service Auth Token) 142 178 143 - #### `GET /.well-known/did.json` 144 - Returns service DID document per AT Protocol specification: 145 - ```json 146 - { 147 - "@context": ["https://www.w3.org/ns/did/v1"], 148 - "id": "{service-did}", 149 - "service": [ 150 - { 151 - "id": "#atp_keyserver", 152 - "type": "AtpKeyserver", 153 - "serviceEndpoint": "{service-url}" 154 - } 155 - ] 156 - } 157 - ``` 179 + **Personal Key Management:** 180 + - `GET /xrpc/dev.atpkeyserver.alpha.key` - Get personal keypair 181 + - `POST /xrpc/dev.atpkeyserver.alpha.key.rotate` - Rotate personal keypair 182 + - `GET /xrpc/dev.atpkeyserver.alpha.key.versions` - List key versions 183 + - `GET /xrpc/dev.atpkeyserver.alpha.key.accessLog` - Get access logs 158 184 159 - #### `GET /xrpc/dev.atpkeyserver.alpha.key.public?did={did}` 160 - Retrieve any user's public key without authentication. 185 + **Group Key Management:** 186 + - `GET /xrpc/dev.atpkeyserver.alpha.group.key` - Get group key 187 + - `POST /xrpc/dev.atpkeyserver.alpha.group.key.rotate` - Rotate group key (owner only) 188 + - `GET /xrpc/dev.atpkeyserver.alpha.group.key.versions` - List group key versions 161 189 162 - **Query Parameters:** 163 - - `did` (required) - DID of user (must be `did:web:*` or `did:plc:*`) 190 + **Group Membership Management:** 191 + - `POST /xrpc/dev.atpkeyserver.alpha.group.member.add` - Add member (owner only) 192 + - `POST /xrpc/dev.atpkeyserver.alpha.group.member.remove` - Remove member (owner only) 164 193 165 - **Response:** 166 - ```json 167 - { 168 - "publicKey": "hex-encoded-public-key" 169 - } 170 - ``` 171 - 172 - **Validation:** 173 - - DID format validation using `isDid()` 174 - - Only supports `did:web` and `did:plc` methods 175 - - URL-decodes the DID parameter 176 - 177 - ### Protected Endpoints (Require JWT Authentication) 178 - 179 - #### `GET /xrpc/dev.atpkeyserver.alpha.key` 180 - Retrieve the authenticated user's complete keypair. 181 - 182 - **Headers:** 183 - - `Authorization: Bearer {jwt}` 184 - 185 - **Response:** 186 - ```json 187 - { 188 - "publicKey": "hex-encoded-public-key", 189 - "privateKey": "hex-encoded-private-key" 190 - } 191 - ``` 192 - 193 - **Implementation:** Calls `getKeypair(did)` from `lib/keys/asymmetric.ts` which returns or creates the user's Ed25519 keypair. 194 - 195 - ### Key Rotation & Revocation Endpoints 196 - 197 - #### `POST /xrpc/dev.atpkeyserver.alpha.key.rotate` (Authenticated) 198 - Rotate the user's asymmetric keypair, marking the current version as revoked and generating a new version. 199 - 200 - **Headers:** 201 - - `Authorization: Bearer {jwt}` 202 - 203 - **Body:** 204 - ```json 205 - { 206 - "reason": "suspected_compromise" | "routine_rotation" | "user_requested" 207 - } 208 - ``` 209 - 210 - **Response:** 211 - ```json 212 - { 213 - "oldVersion": 1, 214 - "newVersion": 2, 215 - "rotatedAt": "2025-01-23T10:30:00.000Z" 216 - } 217 - ``` 218 - 219 - #### `GET /xrpc/dev.atpkeyserver.alpha.key.versions` (Authenticated) 220 - List all versions of the user's keypair with their status. 221 - 222 - **Headers:** 223 - - `Authorization: Bearer {jwt}` 224 - 225 - **Response:** 226 - ```json 227 - { 228 - "versions": [ 229 - { 230 - "version": 2, 231 - "status": "active", 232 - "created_at": "2025-01-23T10:30:00.000Z", 233 - "revoked_at": null 234 - }, 235 - { 236 - "version": 1, 237 - "status": "revoked", 238 - "created_at": "2025-01-20T08:00:00.000Z", 239 - "revoked_at": "2025-01-23T10:30:00.000Z" 240 - } 241 - ] 242 - } 243 - ``` 244 - 245 - #### `GET /xrpc/dev.atpkeyserver.alpha.key.accessLog` (Authenticated) 246 - Retrieve access logs for the authenticated user's keys. 247 - 248 - **Headers:** 249 - - `Authorization: Bearer {jwt}` 250 - 251 - **Query Parameters:** 252 - - `limit` (optional) - Maximum number of log entries to return (default: 50) 253 - 254 - **Response:** 255 - ```json 256 - { 257 - "logs": [ 258 - { 259 - "version": 2, 260 - "accessed_at": "2025-01-23T10:30:00.000Z", 261 - "ip": "192.0.2.1", 262 - "user_agent": "Mozilla/5.0..." 263 - }, 264 - { 265 - "version": 1, 266 - "accessed_at": "2025-01-22T08:15:00.000Z", 267 - "ip": "192.0.2.1", 268 - "user_agent": "Mozilla/5.0..." 269 - } 270 - ] 271 - } 272 - ``` 273 - 274 - **Note:** IP and user_agent fields may be null if not available from the request headers. 275 - 276 - #### `GET /xrpc/dev.atpkeyserver.alpha.group.key` (Authenticated) 277 - Get a group's symmetric key (supports version parameter for historical keys). 278 - 279 - **Headers:** 280 - - `Authorization: Bearer {jwt}` 281 - 282 - **Query Parameters:** 283 - - `group_id` (required) - Group identifier (format: `{owner_did}#{group_name}`) 284 - - `version` (optional) - Specific version number 285 - 286 - **Response:** 287 - ```json 288 - { 289 - "groupId": "did:plc:abc123#followers", 290 - "secretKey": "hex-encoded-secret-key", 291 - "version": 1 292 - } 293 - ``` 294 - 295 - #### `POST /xrpc/dev.atpkeyserver.alpha.group.key.rotate` (Owner Only, Authenticated) 296 - Rotate a group's symmetric key (only the group owner can perform this action). 297 - 298 - **Headers:** 299 - - `Authorization: Bearer {jwt}` 300 - 301 - **Body:** 302 - ```json 303 - { 304 - "group_id": "did:plc:abc123#followers", 305 - "reason": "suspected_compromise" 306 - } 307 - ``` 308 - 309 - **Response:** 310 - ```json 311 - { 312 - "groupId": "did:plc:abc123#followers", 313 - "oldVersion": 1, 314 - "newVersion": 2, 315 - "rotatedAt": "2025-01-23T10:30:00.000Z" 316 - } 317 - ``` 318 - 319 - #### `GET /xrpc/dev.atpkeyserver.alpha.group.key.versions` (Authenticated) 320 - List all versions of a group key (owner and members can view). 321 - 322 - **Headers:** 323 - - `Authorization: Bearer {jwt}` 324 - 325 - **Query Parameters:** 326 - - `group_id` (required) 327 - 328 - **Response:** 329 - ```json 330 - { 331 - "groupId": "did:plc:abc123#followers", 332 - "versions": [ 333 - { 334 - "version": 2, 335 - "status": "active", 336 - "created_at": "2025-01-23T10:30:00.000Z", 337 - "revoked_at": null 338 - }, 339 - { 340 - "version": 1, 341 - "status": "revoked", 342 - "created_at": "2025-01-20T08:00:00.000Z", 343 - "revoked_at": "2025-01-23T10:30:00.000Z" 344 - } 345 - ] 346 - } 347 - ``` 348 - 349 - #### `POST /xrpc/dev.atpkeyserver.alpha.group.member.add` (Owner Only, Authenticated) 350 - Add a member to a group (only the group owner can perform this action). 351 - 352 - **Headers:** 353 - - `Authorization: Bearer {jwt}` 354 - 355 - **Body:** 356 - ```json 357 - { 358 - "group_id": "did:plc:abc123#followers", 359 - "member_did": "did:plc:xyz789" 360 - } 361 - ``` 362 - 363 - **Response:** 364 - ```json 365 - { 366 - "groupId": "did:plc:abc123#followers", 367 - "memberDid": "did:plc:xyz789", 368 - "status": "added" 369 - } 370 - ``` 371 - 372 - **Error Responses:** 373 - - `404` - Group not found 374 - - `403` - Only owner can add members 375 - - `409` - Member already in group 376 - 377 - #### `POST /xrpc/dev.atpkeyserver.alpha.group.member.remove` (Owner Only, Authenticated) 378 - Remove a member from a group (only the group owner can perform this action). 379 - 380 - **Headers:** 381 - - `Authorization: Bearer {jwt}` 382 - 383 - **Body:** 384 - ```json 385 - { 386 - "group_id": "did:plc:abc123#followers", 387 - "member_did": "did:plc:xyz789" 388 - } 389 - ``` 390 - 391 - **Response:** 392 - ```json 393 - { 394 - "groupId": "did:plc:abc123#followers", 395 - "memberDid": "did:plc:xyz789", 396 - "status": "removed" 397 - } 398 - ``` 399 - 400 - **Error Responses:** 401 - - `404` - Group not found or member not found in group 402 - - `403` - Only owner can remove members 194 + **For complete endpoint documentation including request/response formats, parameters, error codes, and examples, see [API_REFERENCE.md](./API_REFERENCE.md).** 403 195 404 196 ## Key Versioning & Revocation 405 197 ··· 417 209 418 210 **Key rotation limits damage radius:** When a key leaks, rotation prevents new posts from being decrypted, while old posts remain readable to authorized parties. 419 211 420 - ### Database Schema 421 - 422 - Both `keys` and `groups` tables use versioning: 423 - 424 - ```sql 425 - -- Asymmetric keys 426 - CREATE TABLE keys ( 427 - did TEXT NOT NULL, 428 - version INTEGER NOT NULL DEFAULT 1, 429 - public_key TEXT NOT NULL, 430 - private_key TEXT NOT NULL, 431 - created_at TEXT NOT NULL, 432 - revoked_at TEXT, 433 - status TEXT NOT NULL DEFAULT 'active', 434 - PRIMARY KEY (did, version) 435 - ); 436 - CREATE INDEX idx_keys_did_status ON keys(did, status); 437 - 438 - -- Symmetric group keys 439 - CREATE TABLE groups ( 440 - id TEXT NOT NULL, 441 - version INTEGER NOT NULL DEFAULT 1, 442 - owner_did TEXT NOT NULL, 443 - secret_key TEXT NOT NULL, 444 - created_at TEXT NOT NULL, 445 - revoked_at TEXT, 446 - status TEXT NOT NULL DEFAULT 'active', 447 - PRIMARY KEY (id, version) 448 - ); 449 - CREATE INDEX idx_groups_id_status ON groups(id, status); 450 - 451 - -- Access logging 452 - CREATE TABLE key_access_log ( 453 - id INTEGER PRIMARY KEY AUTOINCREMENT, 454 - did TEXT NOT NULL, 455 - version INTEGER NOT NULL, 456 - accessed_at TEXT NOT NULL, 457 - ip TEXT, 458 - user_agent TEXT 459 - ); 460 - ``` 461 - 462 212 ### Key Lifecycle 463 213 464 214 **Status Values:** 465 215 - `active` - Current version, used for encryption 466 - - `revoked` - Compromised or rotated out, still available for decryption 467 - - `rotated` - Replaced during routine rotation (reserved for future use) 216 + - `revoked` - No longer active (compromised or rotated out), still available for decryption 468 217 469 218 **Default Behavior:** 470 219 - New keys start as version 1 with status `active` ··· 537 286 - Audit compliance requirements 538 287 - Incident response investigation 539 288 540 - **API Access:** 289 + **API Access to logs:** 541 290 Users can retrieve access logs for their keys via the authenticated `/xrpc/dev.atpkeyserver.alpha.key.accessLog` endpoint. This allows users to: 542 291 - Monitor their own key usage 543 292 - Detect unauthorized access attempts ··· 564 313 - **Member Verification**: Key access validated against membership table 565 314 - **Namespace Isolation**: Group IDs scoped by owner DID 566 315 316 + For more insight on the security model, see [SECURITY.md](SECURITY.md) 317 + 318 + 567 319 ## Configuration 568 320 569 - ### Environment Variables 321 + There is not much to configure yet in this first version. The only thing would be the DID of the service and the port this service is running on, which are configured with environment variables as described here: 322 + 570 323 - `DID` (required) - Service's own DID identifier 571 324 - `PORT` (optional) - HTTP server port (default: 4000) 572 325 573 - ### Database 574 - - File: `keyserver.db` 575 - - Mode: WAL (Write-Ahead Logging) 576 - - Auto-created on first run 577 326 578 327 ## Development Workflow 579 328 580 - ### Scripts 581 - - `bun run dev` - Development mode with auto-reload (uses `DID=test`) 582 - - `bun start` - Production mode 583 - - `bun run format` - Format code with Prettier 329 + To create a new endpoint for the server, the development workflow would be something like this: 584 330 585 - ### Dependencies 586 - - Uses Bun's native package management 587 - - Lock file: `bun.lock` 588 - - Separate dev dependencies for tooling 331 + 1. Define the endpoint route in `main.ts` and decide whether it requires authentication or not. 332 + 2. Implement the new endpoint inner logic in a file in the `lib` folder. 333 + 3. Create a lexicon definition file for the new endpoint in the `lexicons` folder. 334 + 4. Update the related documentation about API endpoints. 589 335 590 336 ## Design Patterns 591 337 ··· 607 353 - URL-safe without additional encoding 608 354 - Human-readable for debugging 609 355 610 - ### Type-Safe Database Queries 611 - Database schema exported as TypeScript types: 612 - - Compile-time type checking for queries 613 - - IntelliSense support in IDEs 614 - - Reduces runtime errors 615 356 616 357 ## Project Structure 617 358 618 359 ``` 619 - atp-keyserver/ 620 - ├── main.ts # Entry point and routing 621 - ├── package.json # Dependencies and scripts 622 - ├── bun.lock # Dependency lock file 623 - ├── README.md # Project documentation 624 - ├── CLAUDE.md # Architecture documentation (this file) 625 - ├── keyserver.db # SQLite database (generated at runtime) 626 - ├── lib/ 627 - │ ├── db.ts # Database schema and connection 628 - │ ├── authMiddleware.ts # JWT authentication with DID resolution 629 - │ └── keys/ 630 - │ ├── asymmetric.ts # Ed25519 keypair management with versioning 631 - │ ├── symmetric.ts # XChaCha20 encryption & group management with versioning 632 - │ └── access-log.ts # Key access logging for security auditing 633 - └── lexicons/ 634 - └── dev/ 635 - └── atpkeyserver/ 636 - └── defs.json # XRPC lexicon definitions (placeholder) 360 + atp-keyserver/ # Monorepo root 361 + ├── packages/ 362 + │ ├── server/ # Keyserver service 363 + │ │ ├── main.ts # Entry point and routing 364 + │ │ ├── package.json # Server dependencies 365 + │ │ ├── tsconfig.json # Server TypeScript config 366 + │ │ ├── README.md # Server starting documentation 367 + │ │ ├── lib/ 368 + │ │ │ ├── db.ts # Database schema and connection 369 + │ │ │ ├── authMiddleware.ts # JWT authentication with DID resolution 370 + │ │ │ └── keys/ 371 + │ │ │ ├── asymmetric.ts # Ed25519 keypair management with versioning 372 + │ │ │ ├── symmetric.ts # XChaCha20 encryption & group management 373 + │ │ │ └── access-log.ts # Key access logging 374 + │ │ ├── lexicons/ # XRPC lexicon definitions 375 + │ │ │ └── ... 376 + │ │ └── keyserver.db # SQLite database (generated at runtime) 377 + │ └── client/ # Client library 378 + │ ├── src/ 379 + │ │ ├── index.ts # Package entry point 380 + │ │ ├── crypto.ts # Encryption/decryption functions 381 + │ │ └── types.ts # TypeScript type definitions 382 + │ ├── dist/ # Compiled output (generated) 383 + │ ├── package.json # Client dependencies 384 + │ ├── tsconfig.json # ESM build config 385 + │ ├── tsconfig.cjs.json # CommonJS build config 386 + │ └── README.md # Client starting documentation 387 + ├── docs/ # Centralized documentation 388 + │ ├── ARCHITECTURE.md # Server architecture (this file) 389 + │ ├── ENCRYPTION_PROTOCOL.md # E2E encryption protocol specification 390 + │ ├── SECURITY.md # Security best practices for new clients 391 + │ ├── API_REFERENCE.md # Complete API endpoint reference 392 + │ └── CLIENT_IMPLEMENTATION.md # Client library design decisions 393 + ├── package.json # Workspace root configuration 394 + ├── tsconfig.json # Base TypeScript configuration 395 + ├── bun.lock # Dependency lock file 396 + ├── README.md # Project overview 397 + └── LICENSE.md # License information 637 398 ``` 638 399 639 400 ## Code Quality ··· 646 407 - Good use of AT Protocol standards 647 408 - Proper DID validation and method checking 648 409 - WAL mode enabled for SQLite (better concurrency) 649 - - Direct imports eliminate unnecessary module layers 650 410 651 411 ### Considerations for Future Development 652 412 - Database error handling could be more comprehensive ··· 680 440 - **Group Key Rotation**: `/xrpc/dev.atpkeyserver.alpha.group.key.rotate` (owner only) 681 441 - **Version Listing**: View all key versions with status and timestamps 682 442 - **Specific Version Access**: Retrieve historical keys for decrypting old content 683 - - **Status Tracking**: Active, revoked, or rotated status per version 443 + - **Status Tracking**: Active or revoked status per version (with reason in metadata) 684 444 685 445 ### Design Trade-offs 686 446 - **Backward Compatibility Over Forward Secrecy**: All versions retained to prevent data loss ··· 693 453 - Encrypted backup strategy for database 694 454 - Scheduled/automatic key rotation policies 695 455 - Enhanced anomaly detection based on access logs 696 - 697 - ## AT Protocol Integration 698 - 699 - This keyserver implements a custom AT Protocol service type: 700 - 701 - **Service Type**: `AtpKeyserver` 702 - **Lexicon Scope**: `dev.atpkeyserver.alpha` 703 - 704 - The service follows AT Protocol conventions: 705 - - DID-based identity 706 - - JWT authentication with signing key verification 707 - - XRPC method naming (`/xrpc/{lexicon}.{method}`) 708 - - Service discovery via DID documents 709 - 710 - This allows AT Protocol applications to discover and interact with the keyserver using standard DID resolution.
+293 -663
docs/CLIENT_IMPLEMENTATION.md
··· 1 - # Client-Side Encryption Implementation Plan 1 + # Client Implementation Strategy 2 2 3 - ## Overview 3 + ## Purpose 4 4 5 - This document outlines the implementation plan for client-side encryption/decryption of "followers-only" posts in the ATP keyserver ecosystem. The approach prioritizes end-to-end encryption, performance, and alignment with ATProto's decentralized philosophy. 5 + This document outlines the design philosophy and implementation strategy for the ATP Keyserver client library. It serves as a roadmap for contributors and explains key architectural decisions. 6 6 7 - ## Recommended Solution: Client-Side with Key Caching 7 + For protocol details, see [ENCRYPTION_PROTOCOL.md](./ENCRYPTION_PROTOCOL.md). 8 + For security guidelines, see [SECURITY.md](./SECURITY.md). 9 + For usage instructions, see [Client README](../packages/client/README.md). 8 10 9 - The keyserver's role is **key management and authorization**, not content processing. By keeping cryptographic operations client-side, we preserve the security model while maintaining better performance and scalability. 11 + ## Design Philosophy 10 12 11 - **Core Principle:** The server should only answer "Is this user authorized to decrypt this group's messages?" - it should never answer "What does this message say?" 12 - 13 - ## Key Benefits vs Server-Side Encryption 13 + ### Client-Side Encryption 14 14 15 - 1. **Security**: True end-to-end encryption - server never sees plaintext 16 - 2. **Performance**: Operations complete in <1ms vs 100-500ms for server round-trips 17 - 3. **Scalability**: 50% less server load, 97% less bandwidth usage 18 - 4. **Philosophy**: Aligns with ATProto's decentralized design principles 19 - 5. **Cost**: Significantly lower server infrastructure costs 20 - 6. **Privacy**: Users don't need to trust server operator with message content 15 + **Decision:** Cryptographic operations (encryption/decryption) happen on the client, not the server. 21 16 22 - ## Optimal Protocol 17 + **Rationale:** 18 + - **True end-to-end encryption**: Server never sees plaintext 19 + - **Better performance**: <1ms local crypto vs 100-500ms server round-trip 20 + - **Lower server load**: 50% less CPU, 97% less bandwidth 21 + - **Aligned with ATProto**: Decentralized architecture, user sovereignty 22 + - **Cost efficiency**: Scales with user devices, not server capacity 23 23 24 - ### Creating an Encrypted "Followers-Only" Post 24 + **Trade-offs:** 25 + - Clients must handle cryptography correctly 26 + - Requires distributing crypto library (~52KB) 27 + - Key caching responsibility on client 25 28 26 - ```typescript 27 - import { encryptMessage } from '@atpkeyserver/client/crypto' 29 + **Alternative considered:** Server-side encryption (rejected due to trust model and performance) 28 30 29 - // 1. Define the group for followers-only visibility 30 - const groupId = `${authorDid}#followers` 31 + ### Single Package Architecture 31 32 32 - // 2. Fetch active group key from keyserver (with caching) 33 - let key = keyCache.get(groupId) 34 - if (!key) { 35 - const response = await fetch( 36 - `/xrpc/dev.atpkeyserver.alpha.group.key?group_id=${encodeURIComponent(groupId)}`, 37 - { headers: { Authorization: `Bearer ${jwt}` } } 38 - ) 39 - const data = await response.json() 40 - key = { secretKey: data.secretKey, version: data.version } 41 - keyCache.set(groupId, key, { ttl: 3600 }) // Cache for 1 hour 42 - } 33 + **Decision:** One npm package `@atpkeyserver/client` with optional imports via subpath exports. 43 34 44 - // 3. Encrypt the post content locally 45 - const post = { text: "Secret message for followers", createdAt: new Date() } 46 - const postUri = `at://${authorDid}/app.bsky.feed.post/${rkey}` 47 - const ciphertext = encryptMessage(postUri, key.secretKey, JSON.stringify(post)) 35 + **Rationale:** 36 + - Crypto library (@noble/ciphers) is ~52KB and represents 90% of bundle 37 + - High-level client adds only ~8KB on top of crypto functions 38 + - Maintenance overhead of separate packages not justified by small size difference 39 + - Subpath exports allow tree-shaking for minimal bundle impact 48 40 49 - // 4. Store encrypted content in PDS 50 - await pds.createRecord({ 51 - collection: 'app.bsky.feed.post', 52 - record: { 53 - encrypted_content: ciphertext, 54 - key_version: key.version, 55 - encrypted_at: new Date().toISOString(), 56 - visibility: 'followers' 57 - } 58 - }) 41 + **Package structure:** 59 42 ``` 60 - 61 - ### Decrypting a "Followers-Only" Post 43 + @atpkeyserver/client 44 + ├── /crypto → Just crypto functions (52KB) 45 + ├── /client → Just KeyserverClient (60KB, includes crypto) 46 + └── / (main) → Both exports (60KB, tree-shaken) 47 + ``` 62 48 63 - ```typescript 64 - import { decryptMessage } from '@atpkeyserver/client/crypto' 49 + **Alternative considered:** Separate `@atpkeyserver/client-crypto` and `@atpkeyserver/client` packages (rejected due to minimal size benefit) 65 50 66 - // 1. Read encrypted post from feed 67 - const encryptedPost = await pds.getRecord(postUri) 68 - const { encrypted_content, key_version } = encryptedPost.value 69 - 70 - // 2. Determine group ID from author 71 - const authorDid = postUri.split('/')[2] 72 - const groupId = `${authorDid}#followers` 51 + ### Service Auth Integration 73 52 74 - // 3. Fetch the specific key version (with caching) 75 - const cacheKey = `${groupId}:${key_version}` 76 - let key = keyCache.get(cacheKey) 53 + **Decision:** Client library abstracts service auth token management via callback pattern. 77 54 78 - if (!key) { 79 - const response = await fetch( 80 - `/xrpc/dev.atpkeyserver.alpha.group.key?` + 81 - `group_id=${encodeURIComponent(groupId)}&version=${key_version}`, 82 - { headers: { Authorization: `Bearer ${followerJwt}` } } 83 - ) 84 - const data = await response.json() 85 - key = data.secretKey 55 + **Rationale:** 56 + - PDS clients vary by platform (@atproto/api, custom implementations) 57 + - Callback pattern allows any PDS client integration 58 + - Client library handles token caching automatically 59 + - Separates concerns: client lib = crypto + keyserver API, user provides = PDS auth 86 60 87 - // Cache historical keys for 24 hours (they never change) 88 - keyCache.set(cacheKey, key, { ttl: 86400 }) 89 - } 90 - 91 - // 4. Decrypt locally 92 - const plaintext = decryptMessage(postUri, key, encrypted_content) 93 - const post = JSON.parse(plaintext) 61 + **Implementation:** 62 + ```typescript 63 + new KeyserverClient({ 64 + keyserverDid: 'did:web:keyserver.example.com', 65 + getServiceAuthToken: async (aud, lxm) => { 66 + // User provides: obtain token from their PDS client 67 + return await pdsClient.getServiceAuth({ aud, lxm }) 68 + } 69 + }) 94 70 ``` 95 71 96 - ## Authentication Flow 72 + See [ENCRYPTION_PROTOCOL.md](./ENCRYPTION_PROTOCOL.md#authentication-flow) for service auth details. 97 73 98 - ### Overview 74 + ## Component Architecture 99 75 100 - The keyserver uses **ATProto service auth** - a secure, decentralized authentication mechanism where clients obtain short-lived JWT tokens from their PDS (Personal Data Server) to authenticate with the keyserver. This design ensures the keyserver never needs to store passwords or trust specific PDS instances. 76 + ### Core Components 101 77 102 - **Key Properties:** 103 - - Tokens signed with user's ATProto signing key (from DID document) 104 - - Short-lived (default: 60 seconds, configurable up to server limits) 105 - - Audience-bound (only valid for specific keyserver DID) 106 - - Method-bound (optional: restrict to specific XRPC endpoint) 107 - - Direct client → keyserver communication (PDS only issues tokens) 78 + **1. Crypto Module (`crypto.ts`)** 79 + - XChaCha20-Poly1305 encryption/decryption 80 + - Uses @noble/ciphers library 81 + - Stateless, pure functions 82 + - No network dependencies 108 83 109 - ### Complete Authentication Flow 84 + **2. KeyserverClient (`client.ts`)** 85 + - HTTP client for keyserver API 86 + - Service auth token management 87 + - Key caching with TTL 88 + - Error handling and retries 110 89 111 - ``` 112 - ┌─────────┐ ┌─────────┐ ┌───────────┐ 113 - │ Client │ │ PDS │ │ Keyserver │ 114 - └────┬────┘ └────┬────┘ └─────┬─────┘ 115 - │ │ │ 116 - │ 1. Authenticate │ │ 117 - │ ────────────────────────────>│ │ 118 - │ (OAuth or createSession) │ │ 119 - │ │ │ 120 - │ 2. Return access token │ │ 121 - │ <────────────────────────────│ │ 122 - │ │ │ 123 - │ 3. Request service auth │ │ 124 - │ ────────────────────────────>│ │ 125 - │ (getServiceAuth) │ │ 126 - │ │ │ 127 - │ 4. Sign & return JWT │ │ 128 - │ <────────────────────────────│ │ 129 - │ (signed with user's key) │ │ 130 - │ │ │ 131 - │ 5. Call keyserver with JWT │ │ 132 - │ ────────────────────────────────────────────────────────────> │ 133 - │ │ │ 134 - │ │ 6. Verify JWT │ 135 - │ │ - Check aud │ 136 - │ │ - Resolve DID │ 137 - │ │ - Verify permissions │ 138 - │ │ │ 139 - │ 7. Return encrypted key │ │ 140 - │ <──────────────────────────────────────────────────────────── │ 141 - │ │ │ 142 - ``` 90 + **3. Cache Module (`cache.ts`)** 91 + - In-memory LRU cache 92 + - Separate TTLs for active/historical keys 93 + - Service auth token caching 94 + - Automatic expiry 143 95 144 - ### Step 1: Authenticate to PDS 96 + **4. Service Auth Module (`service-auth.ts`)** 97 + - Token cache management 98 + - Expiry checking with safety margin 99 + - Automatic refresh logic 145 100 146 - Clients first authenticate with their PDS using OAuth (recommended) or legacy JWT authentication. 101 + **5. Types Module (`types.ts`)** 102 + - TypeScript interfaces 103 + - Configuration types 104 + - Response types 147 105 148 - **OAuth (Modern):** 149 - ```typescript 150 - import { OAuthClient } from '@atproto/oauth-client' 106 + **6. Errors Module (`errors.ts`)** 107 + - Custom error classes 108 + - Structured error handling 109 + - HTTP status code mapping 151 110 152 - const oauthClient = new OAuthClient({ 153 - clientId: 'your-app-client-id', 154 - // ... OAuth configuration 155 - }) 111 + ### Dependency Graph 156 112 157 - // Redirect user to PDS for authentication 158 - await oauthClient.authorize({ 159 - scope: 'atproto transition:generic' 160 - }) 161 - 162 - // After OAuth callback: 163 - const { access_token, sub: userDid } = oauthClient.getTokens() 164 113 ``` 165 - 166 - **Legacy JWT (com.atproto.server.createSession):** 167 - ```typescript 168 - const { accessJwt, refreshJwt, did } = await fetch( 169 - 'https://bsky.social/xrpc/com.atproto.server.createSession', 170 - { 171 - method: 'POST', 172 - headers: { 'Content-Type': 'application/json' }, 173 - body: JSON.stringify({ 174 - identifier: 'user.bsky.social', 175 - password: 'app-password-here' 176 - }) 177 - } 178 - ).then(r => r.json()) 114 + index.ts 115 + ├── crypto.ts 116 + │ └── @noble/ciphers 117 + ├── client.ts 118 + │ ├── crypto.ts 119 + │ ├── cache.ts 120 + │ ├── service-auth.ts 121 + │ ├── types.ts 122 + │ └── errors.ts 123 + ├── types.ts 124 + └── errors.ts 179 125 ``` 180 126 181 - ### Step 2: Request Service Auth Token 182 - 183 - Call `com.atproto.server.getServiceAuth` on the user's PDS to obtain a service auth token for the keyserver. 184 - 185 - ```typescript 186 - // Get service auth token for keyserver 187 - const { token: serviceAuthToken } = await fetch( 188 - `https://bsky.social/xrpc/com.atproto.server.getServiceAuth?` + 189 - new URLSearchParams({ 190 - aud: 'did:web:keyserver.example.com', // Keyserver DID (required) 191 - lxm: 'dev.atpkeyserver.alpha.key', // Optional: method binding 192 - exp: String(Math.floor(Date.now() / 1000) + 300) // Optional: 5 min expiry 193 - }), 194 - { 195 - headers: { 196 - // OAuth: Authorization: DPoP {access_token} + DPoP header 197 - // Legacy: Authorization: Bearer {accessJwt} 198 - Authorization: `Bearer ${accessJwt}` 199 - } 200 - } 201 - ).then(r => r.json()) 202 - ``` 127 + ## Key Features 203 128 204 - **Parameters:** 205 - - `aud` (required): The DID of your keyserver (must match keyserver's serviceDid) 206 - - `lxm` (optional): XRPC method to bind token to (e.g., `dev.atpkeyserver.alpha.key`) 207 - - `exp` (optional): Expiration time in Unix epoch seconds (default: 60 seconds from now) 129 + ### Automatic Caching 208 130 209 - **What the PDS does:** 210 - 1. Verifies client's authentication (OAuth or JWT) 211 - 2. Creates JWT with claims: 212 - - `iss`: User's DID 213 - - `aud`: Keyserver's DID 214 - - `exp`: Expiration timestamp 215 - - `iat`: Issued-at timestamp 216 - - `lxm`: Method binding (if specified) 217 - 3. Signs JWT with user's ATProto signing key (from DID document) 218 - 4. Returns `{ token: "eyJhbG..." }` 131 + **Key Caching:** 132 + - Active keys: 1 hour TTL (may change with rotation) 133 + - Historical keys: 24 hour TTL (immutable) 134 + - LRU eviction when max size reached 219 135 220 - ### Step 3: Call Keyserver with Service Auth Token 136 + **Service Auth Token Caching:** 137 + - 60 second TTL (or server-specified `exp`) 138 + - Refresh when <10 seconds remain 139 + - Memory-only storage 221 140 222 - ```typescript 223 - // Use service auth token to fetch group key 224 - const { secretKey, version } = await fetch( 225 - `https://keyserver.example.com/xrpc/dev.atpkeyserver.alpha.group.key?` + 226 - `group_id=${encodeURIComponent(`${userDid}#followers`)}`, 227 - { 228 - headers: { 229 - Authorization: `Bearer ${serviceAuthToken}` 230 - } 231 - } 232 - ).then(r => r.json()) 233 - ``` 141 + **Benefits:** 142 + - Reduces keyserver load 143 + - Improves client performance 144 + - Handles token refresh automatically 234 145 235 - **Keyserver verification (automatic in authMiddleware.ts):** 236 - 1. Extracts JWT from Authorization header 237 - 2. Parses JWT to get `iss` (user DID) and `aud` (keyserver DID) 238 - 3. Checks `aud` matches keyserver's DID (prevents token misuse) 239 - 4. Resolves user's DID to get signing key 240 - 5. Verifies JWT signature with user's signing key 241 - 6. Checks expiration timestamp 242 - 7. Returns authenticated user's DID to request handler 146 + See [SECURITY.md](./SECURITY.md#token-caching) for security considerations. 243 147 244 - ### Service Auth Token Caching 148 + ### Request Deduplication 245 149 246 - Service auth tokens are short-lived (60 seconds default). Clients should cache tokens to avoid excessive PDS requests: 150 + Multiple simultaneous requests for the same key are collapsed into a single network request: 247 151 248 152 ```typescript 249 - class ServiceAuthCache { 250 - private tokens = new Map<string, { 251 - token: string 252 - expiresAt: number 253 - }>() 153 + // Both calls trigger only one network request 154 + const [key1, key2] = await Promise.all([ 155 + client.getGroupKey('did:plc:abc#followers'), 156 + client.getGroupKey('did:plc:abc#followers') 157 + ]) 158 + ``` 254 159 255 - async getToken( 256 - pdsClient: PdsClient, 257 - aud: string, 258 - lxm?: string 259 - ): Promise<string> { 260 - const cacheKey = `${aud}:${lxm || ''}` 261 - const cached = this.tokens.get(cacheKey) 160 + ### Automatic Retry 262 161 263 - // Use cached token if valid for at least 10 more seconds 264 - const now = Math.floor(Date.now() / 1000) 265 - if (cached && cached.expiresAt > now + 10) { 266 - return cached.token 267 - } 162 + Network failures retry with exponential backoff: 163 + - Retry on: 5xx errors, network timeouts 164 + - Don't retry on: 4xx errors (client errors are permanent) 165 + - Max retries: 3 attempts 166 + - Backoff: 100ms, 200ms, 400ms 268 167 269 - // Request new token (60 second expiry) 270 - const exp = now + 60 271 - const { token } = await pdsClient.getServiceAuth({ aud, lxm, exp }) 168 + ### Type Safety 272 169 273 - this.tokens.set(cacheKey, { token, expiresAt: exp }) 274 - return token 275 - } 170 + Full TypeScript support: 171 + - Strict typing for all APIs 172 + - IntelliSense support 173 + - Compile-time error detection 174 + - Generated type definitions 276 175 277 - clear(): void { 278 - this.tokens.clear() 279 - } 280 - } 281 - ``` 176 + ## Bundle Size Analysis 282 177 283 - **Cache strategy:** 284 - - Keep tokens for up to 60 seconds (or specified `exp`) 285 - - Refresh when <10 seconds remain (safety margin) 286 - - Separate cache keys for different services (`aud`) and methods (`lxm`) 287 - - Clear cache on logout 178 + Tree-shaking eliminates unused code. Actual bundle sizes depend on what you import: 288 179 289 - ### Complete End-to-End Example 180 + ### Scenario 1: Crypto Functions Only 290 181 291 182 ```typescript 292 - import { KeyserverClient } from '@atpkeyserver/client' 293 - import { encryptMessage } from '@atpkeyserver/client/crypto' 294 - 295 - // 1. Authenticate to PDS (one-time or when session expires) 296 - const pdsClient = new AtpAgent({ service: 'https://bsky.social' }) 297 - await pdsClient.login({ 298 - identifier: 'user.bsky.social', 299 - password: 'app-password' 300 - }) 301 - 302 - // 2. Create keyserver client with service auth integration 303 - const keyserverClient = new KeyserverClient({ 304 - keyserverDid: 'did:web:keyserver.example.com', 305 - getServiceAuthToken: async (aud, lxm) => { 306 - // Get service auth token from PDS 307 - const { token } = await pdsClient.com.atproto.server.getServiceAuth({ 308 - aud, 309 - lxm, 310 - exp: Math.floor(Date.now() / 1000) + 60 311 - }) 312 - return token 313 - } 314 - }) 315 - 316 - // 3. Encrypt and publish post (automatic token handling) 317 - const groupId = `${pdsClient.session.did}#followers` 318 - const { ciphertext, version } = await keyserverClient.encrypt( 319 - groupId, 320 - postUri, 321 - JSON.stringify({ text: "Secret message for followers" }) 322 - ) 323 - 324 - await pdsClient.com.atproto.repo.createRecord({ 325 - repo: pdsClient.session.did, 326 - collection: 'app.bsky.feed.post', 327 - record: { 328 - encrypted_content: ciphertext, 329 - key_version: version, 330 - encrypted_at: new Date().toISOString(), 331 - createdAt: new Date().toISOString() 332 - } 333 - }) 183 + import { encryptMessage, decryptMessage } from '@atpkeyserver/client/crypto' 334 184 ``` 335 185 336 - ### Security Properties 186 + **Bundled:** ~52KB (minified + gzipped ~15KB) 187 + - @noble/ciphers: 50KB 188 + - crypto.ts: 2KB 337 189 338 - **Audience Binding:** 339 - - Tokens only valid for specific keyserver DID 340 - - Prevents token reuse across services 341 - - Keyserver rejects tokens with wrong `aud` 190 + **Use case:** You handle keyserver API calls manually, just need crypto. 342 191 343 - **Method Binding (Optional):** 344 - - `lxm` parameter restricts token to specific endpoint 345 - - Extra security for sensitive operations 346 - - Example: token only valid for `dev.atpkeyserver.alpha.key.rotate` 347 - 348 - **Short-Lived Tokens:** 349 - - Default 60 second expiry limits attack window 350 - - Compromised token quickly becomes useless 351 - - No long-term credential storage on client 352 - 353 - **Decentralized Verification:** 354 - - Keyserver verifies signature using public key from DID document 355 - - No shared secrets between PDS and keyserver 356 - - Works with any ATProto-compliant PDS 357 - 358 - **User Key Signing:** 359 - - Token signed with user's signing key, not PDS key 360 - - User maintains cryptographic control over identity 361 - - PDS cannot forge tokens for users without their key 192 + ### Scenario 2: Full Client 362 193 363 - ## Implementation Plan 364 - 365 - ### 1. Create npm Package: `@atpkeyserver/client` 366 - 367 - **Purpose:** Unified client library for keyserver integration, providing both low-level cryptographic functions and high-level API wrapper with built-in caching. 368 - 369 - **Package Structure:** 370 - ``` 371 - @atpkeyserver/client/ 372 - ├── src/ 373 - │ ├── index.ts # Main exports (KeyserverClient + crypto) 374 - │ ├── client.ts # KeyserverClient class 375 - │ ├── crypto.ts # Encryption/decryption functions 376 - │ ├── cache.ts # Key caching with TTL 377 - │ ├── service-auth.ts # Service auth token caching 378 - │ ├── types.ts # TypeScript types 379 - │ └── errors.ts # Custom error classes 380 - ├── package.json # With subpath exports configured 381 - ├── tsconfig.json 382 - ├── README.md 383 - └── examples/ 384 - ├── basic-usage.ts 385 - ├── react-hook.tsx 386 - └── cli-example.ts 387 - ``` 388 - 389 - **Subpath Exports Configuration:** 390 - ```json 391 - // package.json 392 - { 393 - "name": "@atpkeyserver/client", 394 - "version": "1.0.0", 395 - "exports": { 396 - ".": { 397 - "import": "./dist/index.js", 398 - "require": "./dist/index.cjs" 399 - }, 400 - "./crypto": { 401 - "import": "./dist/crypto.js", 402 - "require": "./dist/crypto.cjs" 403 - }, 404 - "./client": { 405 - "import": "./dist/client.js", 406 - "require": "./dist/client.cjs" 407 - } 408 - } 409 - } 194 + ```typescript 195 + import { KeyserverClient } from '@atpkeyserver/client' 410 196 ``` 411 197 412 - **Import Patterns:** 413 - ```typescript 414 - // Option 1: Import just crypto functions (tree-shaken, minimal bundle) 415 - import { encryptMessage, decryptMessage } from '@atpkeyserver/client/crypto' 198 + **Bundled:** ~60KB (minified + gzipped ~18KB) 199 + - @noble/ciphers: 50KB 200 + - Client code: 10KB (includes crypto, caching, HTTP, errors) 416 201 417 - // Option 2: Import just the client 418 - import { KeyserverClient } from '@atpkeyserver/client/client' 202 + **Use case:** Full-featured client with automatic caching and token management. 419 203 420 - // Option 3: Import everything from main entry 421 - import { KeyserverClient, encryptMessage, decryptMessage } from '@atpkeyserver/client' 422 - ``` 204 + ### Scenario 3: Both 423 205 424 - **Crypto Functions API:** 425 206 ```typescript 426 - // Export from lib/keys/client.ts 427 - export function encryptMessage( 428 - id: string, 429 - key: string, 430 - plaintext: string 431 - ): string 432 - 433 - export function decryptMessage( 434 - id: string, 435 - key: string, 436 - ciphertext: string 437 - ): string 438 - 207 + import { KeyserverClient, encryptMessage } from '@atpkeyserver/client' 439 208 ``` 440 209 441 - **KeyserverClient API:** 442 - ```typescript 443 - class KeyserverClient { 444 - constructor(config: { 445 - keyserverDid: string 446 - getServiceAuthToken: (aud: string, lxm?: string) => Promise<string> 447 - cache?: CacheOptions 448 - }) 210 + **Bundled:** ~60KB (same as scenario 2) 211 + - Client includes crypto, no duplication 449 212 450 - // Get active group key (auto-cached) 451 - async getGroupKey(groupId: string): Promise<{ 452 - secretKey: string 453 - version: number 454 - }> 213 + **Use case:** Mix high-level and low-level APIs as needed. 455 214 456 - // Get specific key version (auto-cached with longer TTL) 457 - async getGroupKeyVersion( 458 - groupId: string, 459 - version: number 460 - ): Promise<{ secretKey: string }> 215 + ### Bundle Optimization 461 216 462 - // Encrypt message with auto key fetching 463 - async encrypt( 464 - groupId: string, 465 - messageId: string, 466 - plaintext: string 467 - ): Promise<{ 468 - ciphertext: string 469 - version: number 470 - }> 217 + **Automatic:** 218 + - Tree-shaking removes unused exports 219 + - Minification reduces code size 220 + - Gzip compresses for network transfer 471 221 472 - // Decrypt message with auto key fetching 473 - async decrypt( 474 - groupId: string, 475 - messageId: string, 476 - ciphertext: string, 477 - version: number 478 - ): Promise<string> 222 + **Manual optimization:** 223 + - Use subpath imports (`/crypto`, `/client`) for smaller bundles 224 + - Import only what you need 225 + - Modern bundlers (webpack, vite, rollup) handle this automatically 479 226 480 - // Clear cache (useful for testing or logout) 481 - clearCache(): void 482 - } 483 - ``` 227 + **Conclusion:** Crypto library dominates bundle size. Single package vs separate packages saves only ~8KB, not worth maintenance complexity. 484 228 485 - **Caching Strategy:** 486 - ```typescript 487 - interface CacheOptions { 488 - activeKeyTtl?: number // Default: 3600 (1 hour) 489 - historicalKeyTtl?: number // Default: 86400 (24 hours) 490 - maxSize?: number // Default: 1000 keys 491 - } 492 - ``` 229 + ## Implementation Phases 493 230 494 - **Error Handling:** 495 - ```typescript 496 - class KeyserverError extends Error { 497 - constructor( 498 - message: string, 499 - public statusCode: number, 500 - public code: string 501 - ) 502 - } 231 + ### Phase 1: Foundation (Complete) 503 232 504 - // Specific error types 505 - class UnauthorizedError extends KeyserverError 506 - class GroupNotFoundError extends KeyserverError 507 - class NetworkError extends KeyserverError 508 - class DecryptionError extends KeyserverError 509 - ``` 233 + - [x] Crypto functions (encryptMessage, decryptMessage) 234 + - [x] Type definitions 235 + - [x] Package structure with subpath exports 236 + - [x] Build configuration (ESM + CJS) 510 237 511 - **Key Features:** 512 - - TypeScript-first with full type safety 513 - - Tree-shakeable ESM exports (import only what you need) 514 - - CommonJS compatibility for Node.js 515 - - **Service auth token management with automatic caching** 516 - - **Integration with PDS via getServiceAuthToken callback** 517 - - Automatic retry logic with exponential backoff 518 - - Request deduplication (multiple simultaneous requests for same key) 519 - - Built-in cache invalidation strategies (keys + auth tokens) 520 - - Supports custom cache implementations 521 - - Comprehensive error handling 522 - - Request/response logging hooks 523 - - Zero runtime dependencies beyond @noble/ciphers 524 - - Comprehensive JSDoc documentation 238 + ### Phase 2: Client Library (In Progress) 525 239 526 - **README Contents:** 527 - - Installation instructions 528 - - Quick start guide showing both high-level and low-level usage 529 - - **Service auth token integration with PDS** 530 - - Complete API documentation for KeyserverClient and crypto functions 531 - - Caching behavior explanation (keys + service auth tokens) 532 - - Error handling guide 533 - - Security considerations and best practices 534 - - Tree-shaking and bundle optimization tips 535 - - Usage examples for common scenarios: 536 - - Creating encrypted posts 537 - - Reading encrypted feeds 538 - - Key rotation handling 539 - - Group management 540 - - **PDS authentication and service auth token flow** 541 - - Integration guides for: 542 - - React/React Native apps 543 - - Node.js services 544 - - CLI tools 545 - - Browser and Node.js compatibility notes 240 + - [ ] KeyserverClient class 241 + - [ ] Service auth token management 242 + - [ ] Key caching with TTL 243 + - [ ] HTTP client with retry logic 244 + - [ ] Error handling 245 + - [ ] Request deduplication 546 246 547 - --- 247 + ### Phase 3: Documentation (In Progress) 548 248 549 - ### 2. Documentation 249 + - [x] Protocol specification (ENCRYPTION_PROTOCOL.md) 250 + - [x] Security guidelines (SECURITY.md) 251 + - [x] API reference (API_REFERENCE.md) 252 + - [ ] Client README with usage examples 253 + - [ ] Example implementations (React, Node.js, CLI) 550 254 551 - #### Protocol Specification Document 255 + ### Phase 4: Testing 552 256 553 - Create `docs/ENCRYPTION_PROTOCOL.md` with: 257 + - [ ] Unit tests for crypto functions 258 + - [ ] Integration tests for client 259 + - [ ] Mock keyserver for testing 260 + - [ ] E2E encryption flow test 261 + - [ ] Cache behavior tests 262 + - [ ] Service auth token refresh tests 554 263 555 - **Overview:** 556 - - End-to-end encryption architecture 557 - - **Service auth token authentication flow** 558 - - Key versioning and rotation strategy 559 - - Group-based access control model 264 + ### Phase 5: Polish 560 265 561 - **Message Format:** 562 - ```typescript 563 - // Encrypted post record structure 564 - { 565 - encrypted_content: string, // hex(nonce) + hex(ciphertext) 566 - key_version: number, // Required for decryption 567 - encrypted_at: string, // ISO timestamp 568 - visibility: 'followers' | 'mentioned' | string 569 - } 570 - ``` 266 + - [ ] Performance benchmarks 267 + - [ ] Bundle size analysis 268 + - [ ] Documentation review 269 + - [ ] Example applications 270 + - [ ] npm package publication 571 271 572 - **Encryption Process:** 573 - 1. Authenticate to PDS (OAuth or legacy JWT) 574 - 2. Request service auth token from PDS for keyserver 575 - 3. Determine group ID from visibility setting 576 - 4. Fetch active group key from keyserver using service auth token 577 - 5. Generate AT-URI as message ID 578 - 6. Encrypt content with XChaCha20-Poly1305 579 - 7. Store ciphertext with key version metadata 272 + ## Success Metrics 580 273 581 - **Decryption Process:** 582 - 1. Authenticate to PDS (OAuth or legacy JWT) 583 - 2. Request service auth token from PDS for keyserver 584 - 3. Read encrypted_content and key_version from record 585 - 4. Determine group ID from author DID and visibility 586 - 5. Fetch specific key version from keyserver using service auth token 587 - 6. Decrypt using AT-URI as AAD (Additional Authenticated Data) 588 - 7. Parse and display plaintext content 274 + ### Performance Targets 589 275 590 - **Security Considerations:** 591 - - **Service auth tokens are short-lived (60 seconds default)** 592 - - **Cache service auth tokens to avoid PDS rate limits** 593 - - **Clear auth token cache on logout** 594 - - Never reuse nonces (automatically handled by random generation) 595 - - Always include AT-URI as AAD for binding 596 - - Cache keys appropriately (short TTL for active, long for historical) 597 - - Validate key_version exists before attempting decryption 598 - - Handle decryption failures gracefully (deleted posts, revoked access) 276 + | Operation | Target | Measurement | 277 + |-----------|--------|-------------| 278 + | Encryption | <1ms | Local crypto operation | 279 + | Decryption (cached key) | <5ms | Cache lookup + crypto | 280 + | Decryption (cache miss) | <200ms | Network fetch + crypto | 281 + | Service auth (cached) | <1ms | Cache lookup | 282 + | Service auth (cache miss) | <100ms | PDS round-trip | 599 283 600 - **Key Rotation Handling:** 601 - - Old posts encrypted with old key versions 602 - - New posts use current active key 603 - - Clients must support multi-version decryption 604 - - No re-encryption required for historical posts 284 + ### Developer Experience 605 285 606 - **Error Scenarios:** 607 - - Missing key_version: Cannot decrypt (malformed post) 608 - - 403 Unauthorized: User lost group access 609 - - 404 Not Found: Group was deleted 610 - - Decryption failure: Corrupted data or wrong key 286 + - [ ] Complete TypeScript types 287 + - [ ] Comprehensive JSDoc comments 288 + - [ ] Working examples for 3+ platforms 289 + - [ ] <5 minutes from install to first encrypted message 290 + - [ ] Clear error messages with actionable guidance 611 291 612 - #### Security Best Practices Document 292 + ### Security 613 293 614 - Create `docs/SECURITY.md` with: 294 + - [ ] Zero plaintext exposure to server 295 + - [ ] Keys cached in memory only 296 + - [ ] Service auth tokens cached for max 60 seconds 297 + - [ ] Proper cleanup on logout 298 + - [ ] No key or token leakage in logs 299 + - [ ] Audience binding prevents token misuse 300 + - [ ] Short-lived tokens limit attack window 615 301 616 - **Client Implementation Guidelines:** 617 - - Key material handling in memory 618 - - Service auth token and key cache security considerations 619 - - Error message sanitization (don't leak keys or tokens) 620 - - Secure key and token deletion on logout 621 - - Rate limiting recommendations 302 + ### Code Quality 622 303 623 - **Service Auth Token Caching:** 624 - - **Short-lived tokens: 60 second TTL (default)** 625 - - **Refresh when <10 seconds remain (safety margin)** 626 - - **Memory-only cache (NEVER persist to disk)** 627 - - **Clear token cache on logout or session end** 628 - - **Separate cache keys per service (aud) and method (lxm)** 304 + - [ ] >80% test coverage 305 + - [ ] Zero runtime dependencies beyond @noble/ciphers 306 + - [ ] Tree-shakeable exports 307 + - [ ] CommonJS + ESM support 308 + - [ ] Works in Node.js, browser, React Native 629 309 630 - **Encryption Key Caching:** 631 - - Active keys: 1 hour TTL (balance freshness vs performance) 632 - - Historical keys: 24 hour TTL (never change, safe to cache longer) 633 - - Memory-only cache (never persist keys to disk without encryption) 634 - - Clear key cache on user logout 635 - - Cache size limits to prevent memory exhaustion 310 + ## Testing Strategy 636 311 637 - **Common Pitfalls:** 638 - - Don't log decrypted content, keys, or auth tokens 639 - - Don't persist keys or tokens in localStorage without encryption 640 - - Don't skip version validation 641 - - Don't assume key fetch always succeeds 642 - - Don't retry decryption failures infinitely 643 - - **Don't reuse expired service auth tokens** 644 - - **Don't share service auth tokens between services (audience binding)** 312 + ### Unit Tests 645 313 646 - #### Example Implementations 314 + **Crypto module:** 315 + - Encrypt/decrypt round-trip 316 + - Nonce uniqueness 317 + - AAD binding 318 + - Error handling 647 319 648 - **React Hook Example:** 649 - ```typescript 650 - // useEncryptedPost.ts 651 - import { useCallback } from 'react' 652 - import { KeyserverClient } from '@atpkeyserver/client' 320 + **Cache module:** 321 + - TTL expiry 322 + - LRU eviction 323 + - Get/set operations 324 + - Clear operation 653 325 654 - export function useEncryptedPost(keyserverClient: KeyserverClient) { 655 - const createEncryptedPost = useCallback(async ( 656 - visibility: 'followers', 657 - content: string 658 - ) => { 659 - const groupId = `${userDid}#${visibility}` 660 - const postUri = generatePostUri() 326 + **Service auth module:** 327 + - Token expiry checking 328 + - Refresh logic 329 + - Cache key generation 661 330 662 - const { ciphertext, version } = await keyserverClient.encrypt( 663 - groupId, 664 - postUri, 665 - JSON.stringify({ text: content, createdAt: new Date() }) 666 - ) 331 + ### Integration Tests 667 332 668 - return { 669 - encrypted_content: ciphertext, 670 - key_version: version, 671 - encrypted_at: new Date().toISOString() 672 - } 673 - }, [keyserverClient, userDid]) 333 + **Client:** 334 + - Key fetch with caching 335 + - Service auth integration 336 + - Retry logic 337 + - Error handling 674 338 675 - const decryptPost = useCallback(async ( 676 - encryptedPost: EncryptedPost 677 - ) => { 678 - const { encrypted_content, key_version } = encryptedPost 679 - const authorDid = extractDidFromUri(encryptedPost.uri) 680 - const groupId = `${authorDid}#followers` 339 + **E2E:** 340 + - Full encryption flow (PDS → keyserver → encrypt → store) 341 + - Full decryption flow (fetch → keyserver → decrypt → display) 342 + - Key rotation handling 681 343 682 - const plaintext = await keyserverClient.decrypt( 683 - groupId, 684 - encryptedPost.uri, 685 - encrypted_content, 686 - key_version 687 - ) 344 + ### Mock Server 345 + 346 + Build lightweight mock keyserver for testing: 347 + - Simulates auth verification 348 + - Returns mock keys 349 + - Configurable latency 350 + - Error injection 688 351 689 - return JSON.parse(plaintext) 690 - }, [keyserverClient]) 352 + ## Open Questions 691 353 692 - return { createEncryptedPost, decryptPost } 693 - } 694 - ``` 354 + ### Rate Limiting 695 355 696 - **CLI Example:** 697 - ```typescript 698 - // encrypt-cli.ts 699 - import { KeyserverClient } from '@atpkeyserver/client' 356 + **Question:** Should client enforce rate limits? 700 357 701 - const client = new KeyserverClient({ 702 - baseUrl: process.env.KEYSERVER_URL, 703 - getAuthToken: async () => process.env.ATP_JWT 704 - }) 358 + **Options:** 359 + 1. No client-side limits (rely on server) 360 + 2. Simple debouncing (prevent rapid duplicate requests) 361 + 3. Full rate limit tracking (mirror server limits) 705 362 706 - // Encrypt a message 707 - const { ciphertext, version } = await client.encrypt( 708 - 'did:plc:abc123#followers', 709 - 'at://did:plc:abc123/app.bsky.feed.post/xyz789', 710 - 'Secret message for followers only' 711 - ) 363 + **Current decision:** Option 2 (debouncing via request deduplication) 712 364 713 - console.log(`Encrypted (v${version}):`, ciphertext) 365 + ### Cache Persistence 714 366 715 - // Decrypt a message 716 - const plaintext = await client.decrypt( 717 - 'did:plc:abc123#followers', 718 - 'at://did:plc:abc123/app.bsky.feed.post/xyz789', 719 - ciphertext, 720 - version 721 - ) 367 + **Question:** Should keys be persisted to disk? 722 368 723 - console.log('Decrypted:', plaintext) 724 - ``` 369 + **Options:** 370 + 1. Memory-only (current implementation) 371 + 2. Encrypted disk cache (requires key derivation) 372 + 3. Platform-specific secure storage (iOS Keychain, Android KeyStore) 725 373 726 - --- 374 + **Current decision:** Memory-only for security and simplicity 727 375 728 - ## Bundle Size & Tree-Shaking 376 + **Rationale:** 377 + - Simpler implementation 378 + - Better security (no disk persistence) 379 + - Works across all platforms 380 + - User can implement custom cache if needed 729 381 730 - Modern bundlers (webpack, vite, rollup) automatically eliminate unused code through tree-shaking. This means developers can choose their level of abstraction without worrying about bundle size: 382 + ### Error Recovery 731 383 732 - **Scenario 1: Just Crypto Functions** 733 - ```typescript 734 - import { encryptMessage, decryptMessage } from '@atpkeyserver/client/crypto' 735 - // Bundled: ~52KB (@noble/ciphers + crypto.ts) 736 - // KeyserverClient, cache, and HTTP client code eliminated 737 - ``` 384 + **Question:** How should clients handle persistent decryption failures? 738 385 739 - **Scenario 2: Full Client with Caching** 740 - ```typescript 741 - import { KeyserverClient } from '@atpkeyserver/client' 742 - // Bundled: ~60KB (includes everything) 743 - // Crypto functions included since client depends on them 744 - ``` 386 + **Options:** 387 + 1. Fail silently (hide post) 388 + 2. Show error message 389 + 3. Retry with exponential backoff 390 + 4. Prompt user action (e.g., "Request access") 745 391 746 - **Scenario 3: Both (Advanced)** 747 - ```typescript 748 - import { KeyserverClient, encryptMessage } from '@atpkeyserver/client' 749 - // Bundled: ~60KB (same as scenario 2) 750 - // Manual crypto calls available alongside client methods 751 - ``` 392 + **Current decision:** Show error message, no retry (decryption failures are permanent) 752 393 753 - **Key Insight:** The crypto library (@noble/ciphers) represents 90%+ of the total bundle size. Whether you import crypto functions alone or the full client, the bundle size difference is only ~8-10KB. This validates the single-package approach - the maintenance complexity of separate packages isn't justified by such a small size difference. 394 + ## Related Documentation 754 395 755 - --- 396 + - [Encryption Protocol](./ENCRYPTION_PROTOCOL.md) - Protocol specification 397 + - [Security Best Practices](./SECURITY.md) - Security guidelines 398 + - [API Reference](./API_REFERENCE.md) - Complete endpoint reference 399 + - [Architecture](./ARCHITECTURE.md) - Server architecture 400 + - [Client README](../packages/client/README.md) - Usage instructions 756 401 757 - ## Success Metrics 402 + ## Contributing 758 403 759 - **Performance Targets:** 760 - - Encryption operation: <1ms 761 - - Decryption operation: <5ms (including cache lookup) 762 - - Key fetch with cache hit: <1ms 763 - - Key fetch with cache miss: <200ms 764 - - **Service auth token fetch: <100ms (PDS round-trip)** 765 - - **Service auth token cache hit: <1ms** 404 + Contributions welcome! Please: 766 405 767 - **Developer Experience:** 768 - - Complete TypeScript types 769 - - Comprehensive documentation 770 - - Working examples for 3+ platforms 771 - - <5 minutes from npm install to first encrypted message 772 - - **Clear service auth integration patterns** 773 - - **Automatic token refresh handling** 406 + 1. Read existing documentation 407 + 2. Follow code style (Prettier) 408 + 3. Add tests for new features 409 + 4. Update documentation as needed 410 + 5. Submit PR with clear description 774 411 775 - **Security:** 776 - - Zero plaintext exposure to server 777 - - All keys cached in memory only 778 - - **Service auth tokens cached for max 60 seconds** 779 - - Proper cleanup on logout (keys + tokens) 780 - - No key or token leakage in logs or errors 781 - - **Audience binding prevents token misuse** 782 - - **Short-lived tokens limit attack window** 412 + For questions, open an issue in the [Codeberg repository](https://codeberg.org/juandjara/atp-keyserver).
+409 -17
packages/client/README.md
··· 1 1 # @atpkeyserver/client 2 2 3 - Client library for ATP keyserver with end-to-end encryption support. 3 + TypeScript client library for ATP Keyserver with end-to-end encryption. 4 4 5 5 ## Status 6 6 7 - 🚧 **Under Development** - This package is currently being built. 7 + 🚧 **Under Development** - Core crypto functions implemented, KeyserverClient in progress. 8 8 9 - ## Overview 9 + ## Features 10 10 11 - This client library provides: 12 - - End-to-end encryption functions (XChaCha20-Poly1305) 13 - - Type-safe keyserver API client 14 - - Automatic service auth token management 15 - - Built-in key caching 11 + - **End-to-end encryption** with XChaCha20-Poly1305 12 + - **Service auth integration** with ATProto PDS 13 + - **Automatic key caching** for performance 14 + - **TypeScript-first** with full type safety 15 + - **Tree-shakeable** ESM and CommonJS support 16 + - **Zero runtime dependencies** (except @noble/ciphers) 16 17 17 18 ## Installation 18 19 19 20 ```bash 20 21 npm install @atpkeyserver/client 22 + # or 23 + bun add @atpkeyserver/client 21 24 ``` 22 25 23 26 ## Quick Start 24 27 28 + ### Using Crypto Functions Only 29 + 30 + If you want to handle key management manually: 31 + 25 32 ```typescript 26 33 import { encryptMessage, decryptMessage } from '@atpkeyserver/client/crypto' 27 34 28 35 // Encrypt a message 36 + const messageId = 'at://did:plc:abc123/app.bsky.feed.post/xyz789' 37 + const secretKey = '0123456789abcdef...' // 64 hex chars (32 bytes) 38 + const plaintext = JSON.stringify({ text: 'Secret message' }) 39 + 40 + const ciphertext = encryptMessage(messageId, secretKey, plaintext) 41 + // Returns: hex(nonce) + hex(ciphertext) 42 + 43 + // Decrypt a message 44 + const decrypted = decryptMessage(messageId, secretKey, ciphertext) 45 + const post = JSON.parse(decrypted) 46 + // Returns: { text: 'Secret message' } 47 + ``` 48 + 49 + ### Using KeyserverClient (Coming Soon) 50 + 51 + Full-featured client with automatic caching and service auth: 52 + 53 + ```typescript 54 + import { KeyserverClient } from '@atpkeyserver/client' 55 + import { AtpAgent } from '@atproto/api' 56 + 57 + // 1. Set up PDS client 58 + const agent = new AtpAgent({ service: 'https://bsky.social' }) 59 + await agent.login({ 60 + identifier: 'user.bsky.social', 61 + password: 'app-password' 62 + }) 63 + 64 + // 2. Create keyserver client 65 + const keyserver = new KeyserverClient({ 66 + keyserverDid: 'did:web:keyserver.example.com', 67 + getServiceAuthToken: async (aud, lxm) => { 68 + const { token } = await agent.com.atproto.server.getServiceAuth({ 69 + aud, 70 + lxm, 71 + exp: Math.floor(Date.now() / 1000) + 60 72 + }) 73 + return token 74 + } 75 + }) 76 + 77 + // 3. Encrypt a post 78 + const groupId = `${agent.session.did}#followers` 79 + const postUri = 'at://did:plc:abc123/app.bsky.feed.post/xyz789' 80 + const { ciphertext, version } = await keyserver.encrypt( 81 + groupId, 82 + postUri, 83 + JSON.stringify({ text: 'Secret message for followers' }) 84 + ) 85 + 86 + // 4. Store encrypted post in PDS 87 + await agent.com.atproto.repo.createRecord({ 88 + repo: agent.session.did, 89 + collection: 'app.bsky.feed.post', 90 + record: { 91 + encrypted_content: ciphertext, 92 + key_version: version, 93 + encrypted_at: new Date().toISOString(), 94 + createdAt: new Date().toISOString() 95 + } 96 + }) 97 + 98 + // 5. Decrypt a post from feed 99 + const encryptedPost = await agent.getPost(postUri) 100 + const plaintext = await keyserver.decrypt( 101 + groupId, 102 + postUri, 103 + encryptedPost.encrypted_content, 104 + encryptedPost.key_version 105 + ) 106 + const post = JSON.parse(plaintext) 107 + ``` 108 + 109 + ## API Reference 110 + 111 + ### Crypto Functions 112 + 113 + #### `encryptMessage(id, key, plaintext)` 114 + 115 + Encrypts plaintext using XChaCha20-Poly1305 with additional authenticated data. 116 + 117 + **Parameters:** 118 + - `id` (string): Message ID (AT-URI) used as AAD 119 + - `key` (string): Hex-encoded 32-byte secret key (64 hex characters) 120 + - `plaintext` (string): UTF-8 plaintext to encrypt 121 + 122 + **Returns:** `string` - Hex-encoded nonce (48 chars) + ciphertext 123 + 124 + **Example:** 125 + ```typescript 29 126 const ciphertext = encryptMessage( 30 - 'at://did:plc:abc123/app.bsky.feed.post/xyz', 31 - secretKey, 127 + 'at://did:plc:abc123/app.bsky.feed.post/xyz789', 128 + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', 32 129 'Secret message' 33 130 ) 131 + ``` 34 132 35 - // Decrypt a message 133 + #### `decryptMessage(id, key, ciphertext)` 134 + 135 + Decrypts ciphertext using XChaCha20-Poly1305. 136 + 137 + **Parameters:** 138 + - `id` (string): Message ID (AT-URI) used as AAD (must match encryption) 139 + - `key` (string): Hex-encoded 32-byte secret key (64 hex characters) 140 + - `ciphertext` (string): Hex-encoded nonce + ciphertext from `encryptMessage` 141 + 142 + **Returns:** `string` - UTF-8 plaintext 143 + 144 + **Throws:** Error if: 145 + - AAD doesn't match (wrong message ID) 146 + - Key is incorrect 147 + - Ciphertext is corrupted 148 + - Authentication tag verification fails 149 + 150 + **Example:** 151 + ```typescript 36 152 const plaintext = decryptMessage( 37 - 'at://did:plc:abc123/app.bsky.feed.post/xyz', 38 - secretKey, 39 - ciphertext 153 + 'at://did:plc:abc123/app.bsky.feed.post/xyz789', 154 + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', 155 + '48a3f2c1...9b8d7e6f' 40 156 ) 41 157 ``` 42 158 43 - ## Documentation 159 + ### KeyserverClient (Coming Soon) 44 160 45 - See the [Client Implementation Guide](../../docs/CLIENT_IMPLEMENTATION.md) for complete documentation. 161 + #### Constructor 162 + 163 + ```typescript 164 + new KeyserverClient(config: KeyserverClientConfig) 165 + ``` 166 + 167 + **Config Options:** 168 + ```typescript 169 + interface KeyserverClientConfig { 170 + keyserverDid: string // DID of keyserver 171 + getServiceAuthToken: (aud: string, lxm?: string) => Promise<string> // Token provider 172 + cache?: CacheOptions // Optional cache config 173 + } 174 + 175 + interface CacheOptions { 176 + activeKeyTtl?: number // Active key TTL in seconds (default: 3600) 177 + historicalKeyTtl?: number // Historical key TTL in seconds (default: 86400) 178 + maxSize?: number // Max cache entries (default: 1000) 179 + } 180 + ``` 181 + 182 + #### `getGroupKey(groupId: string)` 183 + 184 + Fetch the active group key with automatic caching. 185 + 186 + **Parameters:** 187 + - `groupId` (string): Group ID in format `{owner_did}#{group_name}` 188 + 189 + **Returns:** `Promise<{ secretKey: string, version: number }>` 190 + 191 + **Throws:** 192 + - `UnauthorizedError` - Invalid or expired service auth token 193 + - `ForbiddenError` - User not a member of group 194 + - `NotFoundError` - Group doesn't exist 195 + - `NetworkError` - Network or server error 196 + 197 + #### `getGroupKeyVersion(groupId: string, version: number)` 198 + 199 + Fetch a specific historical key version with automatic caching. 200 + 201 + **Parameters:** 202 + - `groupId` (string): Group ID 203 + - `version` (number): Key version number 204 + 205 + **Returns:** `Promise<{ secretKey: string }>` 206 + 207 + #### `encrypt(groupId: string, messageId: string, plaintext: string)` 208 + 209 + Encrypt message with automatic key fetching. 210 + 211 + **Parameters:** 212 + - `groupId` (string): Group ID 213 + - `messageId` (string): Message ID (AT-URI) 214 + - `plaintext` (string): UTF-8 plaintext 215 + 216 + **Returns:** `Promise<{ ciphertext: string, version: number }>` 217 + 218 + #### `decrypt(groupId: string, messageId: string, ciphertext: string, version: number)` 219 + 220 + Decrypt message with automatic key fetching. 221 + 222 + **Parameters:** 223 + - `groupId` (string): Group ID 224 + - `messageId` (string): Message ID (AT-URI) 225 + - `ciphertext` (string): Hex-encoded nonce + ciphertext 226 + - `version` (number): Key version from encrypted record 227 + 228 + **Returns:** `Promise<string>` - Decrypted plaintext 229 + 230 + #### `clearCache()` 231 + 232 + Clear all cached keys and service auth tokens. Call on logout. 233 + 234 + ```typescript 235 + keyserver.clearCache() 236 + ``` 237 + 238 + ## Service Auth Integration 239 + 240 + The keyserver uses ATProto service auth for authentication. You need to provide a `getServiceAuthToken` callback that obtains tokens from the user's PDS. 241 + 242 + ### With @atproto/api 243 + 244 + ```typescript 245 + import { AtpAgent } from '@atproto/api' 246 + import { KeyserverClient } from '@atpkeyserver/client' 247 + 248 + const agent = new AtpAgent({ service: 'https://bsky.social' }) 249 + await agent.login({ identifier: 'user.bsky.social', password: 'app-password' }) 250 + 251 + const keyserver = new KeyserverClient({ 252 + keyserverDid: 'did:web:keyserver.example.com', 253 + getServiceAuthToken: async (aud, lxm) => { 254 + const { token } = await agent.com.atproto.server.getServiceAuth({ 255 + aud, 256 + lxm, 257 + exp: Math.floor(Date.now() / 1000) + 60 258 + }) 259 + return token 260 + } 261 + }) 262 + ``` 263 + 264 + ### With OAuth 265 + 266 + ```typescript 267 + import { OAuthClient } from '@atproto/oauth-client' 268 + import { KeyserverClient } from '@atpkeyserver/client' 269 + 270 + const oauthClient = new OAuthClient({ /* config */ }) 271 + await oauthClient.authorize({ scope: 'atproto transition:generic' }) 272 + 273 + const keyserver = new KeyserverClient({ 274 + keyserverDid: 'did:web:keyserver.example.com', 275 + getServiceAuthToken: async (aud, lxm) => { 276 + // Implement service auth token fetch with OAuth 277 + // See ENCRYPTION_PROTOCOL.md for details 278 + } 279 + }) 280 + ``` 281 + 282 + See [Encryption Protocol](../../docs/ENCRYPTION_PROTOCOL.md#authentication-flow) for complete service auth flow. 283 + 284 + ## Caching Behavior 285 + 286 + ### Key Caching 287 + 288 + The client automatically caches keys in memory for performance: 289 + 290 + - **Active keys:** 1 hour TTL (may change with rotation) 291 + - **Historical keys:** 24 hour TTL (immutable) 292 + - **LRU eviction:** When max size reached (default 1000 entries) 293 + 294 + Cache keys are formatted as `{groupId}:{version}` for precise version tracking. 295 + 296 + ### Service Auth Token Caching 297 + 298 + Service auth tokens are cached with short TTL: 299 + 300 + - **TTL:** 60 seconds (or server-specified `exp`) 301 + - **Refresh:** Automatic when <10 seconds remain 302 + - **Memory-only:** Never persisted to disk 303 + 304 + ### Cache Security 305 + 306 + - All caches are **memory-only** (never persisted) 307 + - Cleared automatically on logout (call `clearCache()`) 308 + - No sensitive data in cache keys 309 + 310 + See [Security Best Practices](../../docs/SECURITY.md#token-caching) for details. 311 + 312 + ## Error Handling 313 + 314 + ### Error Types 315 + 316 + ```typescript 317 + import { 318 + UnauthorizedError, // 401 - Invalid/expired auth token 319 + ForbiddenError, // 403 - Not authorized for resource 320 + NotFoundError, // 404 - Resource doesn't exist 321 + NetworkError, // Network or server error 322 + DecryptionError // Decryption failed 323 + } from '@atpkeyserver/client' 324 + ``` 325 + 326 + ### Handling Errors 327 + 328 + ```typescript 329 + try { 330 + const plaintext = await keyserver.decrypt(groupId, messageId, ciphertext, version) 331 + return JSON.parse(plaintext) 332 + } catch (error) { 333 + if (error instanceof ForbiddenError) { 334 + // User lost access to group 335 + return { error: 'You no longer have access to this content' } 336 + } else if (error instanceof NotFoundError) { 337 + // Group was deleted 338 + return { error: 'This group no longer exists' } 339 + } else if (error instanceof DecryptionError) { 340 + // Corrupted data or wrong key 341 + return { error: 'Cannot decrypt this message' } 342 + } else if (error instanceof NetworkError) { 343 + // Temporary network issue - could retry 344 + throw error 345 + } else { 346 + // Unknown error 347 + throw error 348 + } 349 + } 350 + ``` 351 + 352 + ### Automatic Retry 353 + 354 + Network errors (5xx, timeouts) are automatically retried with exponential backoff: 355 + - Max retries: 3 attempts 356 + - Backoff: 100ms, 200ms, 400ms 357 + 358 + Client errors (4xx) are NOT retried as they indicate permanent issues. 359 + 360 + ## Platform Compatibility 361 + 362 + ### Node.js 363 + 364 + Requires Node.js 22+ with native crypto support. 365 + 366 + ```typescript 367 + import { encryptMessage } from '@atpkeyserver/client/crypto' 368 + ``` 369 + 370 + ### Browser 371 + 372 + Works in all modern browsers with Web Crypto API: 373 + 374 + ```typescript 375 + import { KeyserverClient } from '@atpkeyserver/client' 376 + ``` 377 + 378 + ### React Native 379 + 380 + Requires crypto polyfills. See examples/ directory for setup. 381 + 382 + ## Bundle Size 383 + 384 + Tree-shaking automatically eliminates unused code: 385 + 386 + | Import | Bundle Size (minified + gzipped) | 387 + |--------|----------------------------------| 388 + | `@atpkeyserver/client/crypto` | ~15KB | 389 + | `@atpkeyserver/client` | ~18KB | 390 + 391 + The crypto library (@noble/ciphers) represents 90% of bundle size. 392 + 393 + ## Examples 394 + 395 + Check the `examples/` directory for complete implementations: 396 + 397 + - **basic-usage.ts** - Minimal crypto-only example 398 + - **react-hook.tsx** - React hooks for encrypted posts 399 + - **cli-example.ts** - Command-line encryption tool 400 + 401 + ## Security 402 + 403 + ### Best Practices 404 + 405 + - Never log keys or tokens to console/files 406 + - Never persist keys to localStorage without encryption 407 + - Always use HTTPS for keyserver communication 408 + - Clear cache on logout with `clearCache()` 409 + - Use AT-URI as message ID for AAD binding 410 + 411 + See [Security Best Practices](../../docs/SECURITY.md) for comprehensive guidelines. 412 + 413 + ### Threat Model 414 + 415 + - **Client-side encryption**: Server never sees plaintext 416 + - **Short-lived tokens**: 60 second expiry limits attack window 417 + - **Audience binding**: Tokens valid only for specific keyserver 418 + - **No forward secrecy**: Old keys retained for compatibility (by design) 419 + 420 + See [Encryption Protocol](../../docs/ENCRYPTION_PROTOCOL.md#security-considerations) for details. 421 + 422 + ## Contributing 423 + 424 + Contributions welcome! Please: 425 + 426 + 1. Read [Client Implementation Strategy](../../docs/CLIENT_IMPLEMENTATION.md) 427 + 2. Follow TypeScript and Prettier conventions 428 + 3. Add tests for new features 429 + 4. Update documentation as needed 46 430 47 431 ## License 48 432 49 - MIT 433 + See [LICENSE.md](../../LICENSE.md) 434 + 435 + ## Resources 436 + 437 + - [Encryption Protocol](../../docs/ENCRYPTION_PROTOCOL.md) - Protocol specification 438 + - [Security Best Practices](../../docs/SECURITY.md) - Security guidelines 439 + - [API Reference](../../docs/API_REFERENCE.md) - Complete endpoint reference 440 + - [Client Implementation](../../docs/CLIENT_IMPLEMENTATION.md) - Design decisions 441 + - [Server Architecture](../../docs/ARCHITECTURE.md) - Server architecture
-36
packages/server/README.md
··· 54 54 4. **SSL/TLS**: Terminate SSL at reverse proxy. 55 55 5. **Monitoring**: Monitor database size and access logs regularly. 56 56 57 - ## Database 58 - 59 - SQLite database (`keyserver.db`) is created automatically on first run with: 60 - - Automatic schema migrations 61 - - WAL mode enabled for better concurrency 62 - 63 - **Note:** Ensure the database file is included in your backup strategy. 64 - 65 - ## API Endpoints 66 - 67 - See [Architecture Documentation](../../docs/ARCHITECTURE.md#api-endpoints) for complete API reference. 68 - 69 - ### Public Endpoints 70 - - `GET /` - Service metadata 71 - - `GET /.well-known/did.json` - Service DID document 72 - - `GET /xrpc/dev.atpkeyserver.alpha.key.public` - Get public key (no auth) 73 - 74 - ### Protected Endpoints (require service auth token) 75 - - `GET /xrpc/dev.atpkeyserver.alpha.key` - Get keypair 76 - - `POST /xrpc/dev.atpkeyserver.alpha.key.rotate` - Rotate keypair 77 - - `GET /xrpc/dev.atpkeyserver.alpha.key.versions` - List key versions 78 - - `GET /xrpc/dev.atpkeyserver.alpha.key.accessLog` - Get key access logs 79 - - `GET /xrpc/dev.atpkeyserver.alpha.group.key` - Get group key 80 - - `POST /xrpc/dev.atpkeyserver.alpha.group.key.rotate` - Rotate group key 81 - - `GET /xrpc/dev.atpkeyserver.alpha.group.key.versions` - List group key versions 82 - - `POST /xrpc/dev.atpkeyserver.alpha.group.member.add` - Add group member 83 - - `POST /xrpc/dev.atpkeyserver.alpha.group.member.remove` - Remove group member 84 - 85 - ## Security Considerations 86 - 87 - - Server never performs encryption or decryption 88 - - All keys are stored in SQLite with proper access controls 89 - - JWT tokens are verified via ATProto signing keys 90 - - Basic access logging allows for security monitoring 91 - - Key versioning prevents data loss 92 - 93 57 ## License 94 58 95 59 See [LICENSE.md](../../LICENSE.md)