@redseat/api 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/encryption.md CHANGED
@@ -1,533 +1,533 @@
1
- # Encryption Module Documentation
2
-
3
- ## Overview
4
-
5
- The encryption module (`packages/api/src/encryption.ts`) provides cross-platform encryption and decryption functions for text and binary data. It uses the Web Crypto API (AES-CBC 256-bit encryption) and is compatible with Node.js, Web browsers, and React Native/Expo.
6
-
7
- ## Key Concepts
8
-
9
- ### Key Derivation (PBKDF2)
10
-
11
- Keys are derived from passphrases using **PBKDF2** (Password-Based Key Derivation Function 2) with:
12
- - **Hash algorithm**: SHA-1
13
- - **Iterations**: 1000
14
- - **Key length**: 256 bits (AES-256)
15
- - **Salt**: Different salts for text and file encryption
16
- - Text encryption salt: `a1209660b32cca003630cb963f730b54`
17
- - File encryption salt: `e5709660b22ab0803630cb963f703b83`
18
-
19
- **Important**: Text and file encryption use different keys derived from the same passphrase but with different salts. This means:
20
- - Text data (filenames, tags, folder names) uses the "text" key
21
- - Binary data (file contents, thumbnails) uses the "file" key
22
- - Both keys must be derived from the same passphrase to decrypt data encrypted with that passphrase
23
-
24
- ### Encryption Algorithm
25
-
26
- - **Algorithm**: AES-CBC (Advanced Encryption Standard in Cipher Block Chaining mode)
27
- - **Key size**: 256 bits
28
- - **IV (Initialization Vector)**: 16 bytes, randomly generated for each encryption operation
29
-
30
- ### Encoding Formats
31
-
32
- - **Base64**: Standard base64 encoding (uses `+` and `/` characters)
33
- - **Base64URL**: URL-safe base64 encoding (uses `-` and `_` instead of `+` and `/`, padding removed)
34
- - Text encryption uses Base64URL for the final output to ensure URL safety
35
-
36
- ## Core Functions
37
-
38
- ### `deriveKey(passPhrase: string, type: 'text' | 'file'): Promise<CryptoKey>`
39
-
40
- Derives a cryptographic key from a passphrase using PBKDF2.
41
-
42
- **Parameters:**
43
- - `passPhrase`: The password/passphrase to derive the key from
44
- - `type`: Either `'text'` for text encryption or `'file'` for file/binary encryption
45
-
46
- **Returns:** A `CryptoKey` object suitable for AES-CBC encryption/decryption
47
-
48
- **Example:**
49
- ```typescript
50
- const textKey = await deriveKey('my-password', 'text');
51
- const fileKey = await deriveKey('my-password', 'file');
52
- ```
53
-
54
- **Note:** The returned key is not extractable (cannot be exported) for security reasons.
55
-
56
- ---
57
-
58
- ### `getRandomIV(): Uint8Array`
59
-
60
- Generates a random 16-byte Initialization Vector (IV) for encryption operations.
61
-
62
- **Returns:** A `Uint8Array` of 16 random bytes
63
-
64
- **Example:**
65
- ```typescript
66
- const iv = getRandomIV();
67
- ```
68
-
69
- **Important:** A new IV must be generated for each encryption operation. Never reuse IVs with the same key.
70
-
71
- ---
72
-
73
- ## Text Encryption
74
-
75
- ### `encryptText(key: CryptoKey, text: string): Promise<string>`
76
-
77
- Encrypts a text string using AES-CBC encryption.
78
-
79
- **Parameters:**
80
- - `key`: The CryptoKey to use for encryption (should be derived with `type: 'text'`)
81
- - `text`: The plaintext string to encrypt
82
-
83
- **Returns:** A string in the format: `${base64Url(IV)}.${base64Url(encryptedData)}`
84
-
85
- **Format:**
86
- - The IV and encrypted data are both base64url encoded
87
- - They are concatenated with a `.` (dot) separator
88
- - Example: `SGVsbG8gV29ybGQ.5Xq3K8mN2pQ9rT1vW4xY7zA0bC3dE6fG8hI1jK2lM3nO4p`
89
-
90
- **Example:**
91
- ```typescript
92
- const key = await deriveKey('password', 'text');
93
- const encrypted = await encryptText(key, 'Hello World');
94
- // Returns: "SGVsbG8gV29ybGQ.5Xq3K8mN2pQ9rT1vW4xY7zA0bC3dE6fG8hI1jK2lM3nO4p"
95
- ```
96
-
97
- **Use cases:**
98
- - Encrypting filenames
99
- - Encrypting tag names
100
- - Encrypting folder names
101
- - Any text metadata that needs to be encrypted
102
-
103
- ---
104
-
105
- ### `decryptText(key: CryptoKey, encryptedText: string, iv?: ArrayBuffer | Uint8Array): Promise<string>`
106
-
107
- Decrypts a text string that was encrypted with `encryptText`.
108
-
109
- **Parameters:**
110
- - `key`: The CryptoKey to use for decryption (must match the key used for encryption)
111
- - `encryptedText`: The encrypted string in format `${IV}.${encryptedData}` OR just the encrypted data if IV is provided separately
112
- - `iv`: (Optional) If provided, `encryptedText` should contain only the encrypted data (without the IV prefix)
113
-
114
- **Returns:** The decrypted plaintext string
115
-
116
- **Example:**
117
- ```typescript
118
- const key = await deriveKey('password', 'text');
119
- const decrypted = await decryptText(key, 'SGVsbG8gV29ybGQ.5Xq3K8mN2pQ9rT1vW4xY7zA0bC3dE6fG8hI1jK2lM3nO4p');
120
- // Returns: "Hello World"
121
- ```
122
-
123
- **Error handling:**
124
- - Throws an error if the encrypted text format is invalid (not in `IV.encryptedData` format)
125
- - Throws an error if decryption fails (wrong key, corrupted data, etc.)
126
-
127
- ---
128
-
129
- ## Binary Data Encryption
130
-
131
- ### `encryptBuffer(key: CryptoKey, iv: ArrayBuffer | Uint8Array, buffer: ArrayBuffer | Uint8Array): Promise<ArrayBuffer>`
132
-
133
- Encrypts binary data (ArrayBuffer or Uint8Array) using AES-CBC encryption.
134
-
135
- **Parameters:**
136
- - `key`: The CryptoKey to use for encryption (should be derived with `type: 'file'`)
137
- - `iv`: The 16-byte Initialization Vector (use `getRandomIV()`)
138
- - `buffer`: The binary data to encrypt
139
-
140
- **Returns:** An ArrayBuffer containing the encrypted data
141
-
142
- **Example:**
143
- ```typescript
144
- const key = await deriveKey('password', 'file');
145
- const iv = getRandomIV();
146
- const data = new Uint8Array([1, 2, 3, 4, 5]);
147
- const encrypted = await encryptBuffer(key, iv, data);
148
- ```
149
-
150
- **Use cases:**
151
- - Encrypting file contents
152
- - Encrypting thumbnails
153
- - Encrypting any binary data
154
-
155
- ---
156
-
157
- ### `decryptBuffer(key: CryptoKey, iv: ArrayBuffer | Uint8Array, buffer: ArrayBuffer | Uint8Array): Promise<ArrayBuffer>`
158
-
159
- Decrypts binary data that was encrypted with `encryptBuffer`.
160
-
161
- **Parameters:**
162
- - `key`: The CryptoKey to use for decryption (must match the key used for encryption)
163
- - `iv`: The same IV that was used during encryption
164
- - `buffer`: The encrypted binary data
165
-
166
- **Returns:** An ArrayBuffer containing the decrypted data
167
-
168
- **Example:**
169
- ```typescript
170
- const key = await deriveKey('password', 'file');
171
- const iv = /* same IV used for encryption */;
172
- const decrypted = await decryptBuffer(key, iv, encryptedData);
173
- ```
174
-
175
- **Important:** The IV must be exactly the same as the one used during encryption. Using a different IV will result in corrupted decrypted data (though it may not throw an error).
176
-
177
- ---
178
-
179
- ## File Encryption (Structured Format)
180
-
181
- ### File Format Structure
182
-
183
- Files encrypted with `encryptFile` follow a specific binary structure:
184
-
185
- ```
186
- [16 bytes: IV]
187
- [4 bytes: Thumbnail size (big-endian)]
188
- [4 bytes: Info size (big-endian, currently always 0)]
189
- [32 bytes: Thumbnail MIME type (padded)]
190
- [256 bytes: File MIME type (padded)]
191
- [T bytes: Encrypted thumbnail data]
192
- [I bytes: Encrypted info data (currently always 0 bytes)]
193
- [F bytes: Encrypted file data]
194
- ```
195
-
196
- **Byte offsets:**
197
- - `0-15`: IV (16 bytes)
198
- - `16-19`: Thumbnail size (4 bytes, big-endian)
199
- - `20-23`: Info size (4 bytes, big-endian)
200
- - `24-55`: Thumbnail MIME type (32 bytes, null-padded)
201
- - `56-311`: File MIME type (256 bytes, null-padded)
202
- - `312+`: Encrypted thumbnail (length = thumbnail size)
203
- - `312 + thumbnailSize +`: Encrypted file data
204
-
205
- **Endianness:** Sizes are stored in **big-endian** format. The decryption functions try big-endian first, then fall back to little-endian for backward compatibility.
206
-
207
- ---
208
-
209
- ### `encryptFile(key: CryptoKey, options: EncryptFileOptions): Promise<EncryptedFile>`
210
-
211
- Encrypts a file with its thumbnail and metadata into a structured binary format.
212
-
213
- **Parameters:**
214
- - `key`: The CryptoKey to use for encryption (should be derived with `type: 'file'`)
215
- - `options`: An object containing:
216
- - `filename`: The original filename (will be encrypted separately)
217
- - `buffer`: The file data to encrypt (ArrayBuffer or Uint8Array)
218
- - `thumb`: The thumbnail data to encrypt (ArrayBuffer or Uint8Array, can be empty)
219
- - `thumbMime`: MIME type of the thumbnail (e.g., `'image/jpeg'`)
220
- - `mime`: MIME type of the file (e.g., `'image/png'`)
221
-
222
- **Returns:** An `EncryptedFile` object containing:
223
- - `filename`: Base64URL-encoded encrypted filename (encrypted with the same IV as the file)
224
- - `ivb64`: Base64-encoded IV (for storage in metadata)
225
- - `thumbsize`: Size of the encrypted thumbnail in bytes
226
- - `blob`: The complete encrypted file structure as an ArrayBuffer
227
-
228
- **Example:**
229
- ```typescript
230
- const key = await deriveKey('password', 'file');
231
- const fileData = await file.arrayBuffer();
232
- const thumbData = await thumbnail.arrayBuffer();
233
-
234
- const encrypted = await encryptFile(key, {
235
- filename: 'photo.jpg',
236
- buffer: fileData,
237
- thumb: thumbData,
238
- thumbMime: 'image/jpeg',
239
- mime: 'image/jpeg'
240
- });
241
-
242
- // encrypted.filename: base64url encoded encrypted filename
243
- // encrypted.ivb64: base64 encoded IV (store this in metadata)
244
- // encrypted.thumbsize: size of encrypted thumbnail
245
- // encrypted.blob: complete encrypted file structure
246
- ```
247
-
248
- **Important notes:**
249
- - The filename is encrypted separately using `encryptBuffer` with the same IV
250
- - The filename is returned as base64url-encoded (for URL safety)
251
- - The IV is returned as base64-encoded (for storage in database metadata)
252
- - All data (file, thumbnail, filename) uses the same IV
253
-
254
- ---
255
-
256
- ### `decryptFile(key: CryptoKey, buffer: ArrayBuffer): Promise<ArrayBuffer>`
257
-
258
- Decrypts the file data from an encrypted file structure.
259
-
260
- **Parameters:**
261
- - `key`: The CryptoKey to use for decryption (must match the key used for encryption)
262
- - `buffer`: The complete encrypted file structure (as returned by `encryptFile`)
263
-
264
- **Returns:** An ArrayBuffer containing the decrypted file data
265
-
266
- **Example:**
267
- ```typescript
268
- const key = await deriveKey('password', 'file');
269
- const encryptedBlob = /* encrypted file blob */;
270
- const decryptedFile = await decryptFile(key, encryptedBlob);
271
- ```
272
-
273
- **How it works:**
274
- 1. Extracts the IV from the first 16 bytes
275
- 2. Reads the thumbnail size (tries big-endian first, falls back to little-endian)
276
- 3. Reads the info size
277
- 4. Calculates the offset to the encrypted file data
278
- 5. Decrypts and returns the file data
279
-
280
- ---
281
-
282
- ### `decryptFileThumb(key: CryptoKey, buffer: ArrayBuffer): Promise<ArrayBuffer>`
283
-
284
- Decrypts the thumbnail from an encrypted file structure.
285
-
286
- **Parameters:**
287
- - `key`: The CryptoKey to use for decryption (must match the key used for encryption)
288
- - `buffer`: The complete encrypted file structure (as returned by `encryptFile`)
289
-
290
- **Returns:** An ArrayBuffer containing the decrypted thumbnail data, or an empty ArrayBuffer if no thumbnail exists
291
-
292
- **Example:**
293
- ```typescript
294
- const key = await deriveKey('password', 'file');
295
- const encryptedBlob = /* encrypted file blob */;
296
- const decryptedThumb = await decryptFileThumb(key, encryptedBlob);
297
- ```
298
-
299
- **How it works:**
300
- 1. Extracts the IV from the first 16 bytes
301
- 2. Reads the thumbnail size (tries big-endian first, falls back to little-endian)
302
- 3. If thumbnail size is 0, returns empty buffer
303
- 4. Calculates the offset to the encrypted thumbnail (after IV, sizes, and MIME types)
304
- 5. Decrypts and returns the thumbnail data
305
-
306
- **Note:** The server's `getThumb` API endpoint returns only the encrypted thumbnail portion (already sliced), not the full file structure. In that case, use `decryptBuffer` directly with the IV from the media metadata.
307
-
308
- ---
309
-
310
- ### `encryptFilename(key: CryptoKey, filename: string, iv: ArrayBuffer | Uint8Array): Promise<string>`
311
-
312
- Encrypts a filename using the same IV as the file encryption.
313
-
314
- **Parameters:**
315
- - `key`: The CryptoKey to use for encryption (should be derived with `type: 'file'`)
316
- - `filename`: The original filename to encrypt
317
- - `iv`: The same IV used for encrypting the file
318
-
319
- **Returns:** Base64URL-encoded encrypted filename
320
-
321
- **Example:**
322
- ```typescript
323
- const key = await deriveKey('password', 'file');
324
- const iv = getRandomIV();
325
- const encryptedFilename = await encryptFilename(key, 'photo.jpg', iv);
326
- ```
327
-
328
- **Note:** This function is used internally by `encryptFile`. For new uploads, filenames are encrypted using `encryptText` with the text key instead, which produces the format `${IV}.${encryptedData}`.
329
-
330
- ---
331
-
332
- ## Cross-Platform Compatibility
333
-
334
- The encryption module is designed to work across multiple platforms:
335
-
336
- ### Supported Platforms
337
-
338
- 1. **Web Browsers**: Uses `window.crypto.subtle` or `globalThis.crypto.subtle`
339
- 2. **Node.js**: Uses `crypto.webcrypto.subtle` (Node.js 15+)
340
- 3. **React Native/Expo**: Uses `globalThis.crypto.subtle` or `crypto.subtle`
341
-
342
- ### Platform Detection
343
-
344
- The `crypto.ts` module automatically detects the available crypto APIs:
345
-
346
- - **`getCryptoSubtle()`**: Returns the appropriate `SubtleCrypto` implementation
347
- - **`getCryptoRandomValues()`**: Returns the appropriate random number generator
348
-
349
- ### Base64 Encoding
350
-
351
- Base64 encoding/decoding is handled cross-platform:
352
- - **Node.js**: Uses `Buffer` for efficient conversion
353
- - **Browsers/RN**: Uses `atob`/`btoa` or manual conversion
354
-
355
- ---
356
-
357
- ## Usage Patterns
358
-
359
- ### Complete File Upload Flow
360
-
361
- ```typescript
362
- import { deriveKey, encryptFile, encryptText, getRandomIV } from '@redseat/api';
363
-
364
- // 1. Derive keys from passphrase
365
- const fileKey = await deriveKey('user-password', 'file');
366
- const textKey = await deriveKey('user-password', 'text');
367
-
368
- // 2. Encrypt the file
369
- const encrypted = await encryptFile(fileKey, {
370
- filename: 'document.pdf',
371
- buffer: fileArrayBuffer,
372
- thumb: thumbnailArrayBuffer,
373
- thumbMime: 'image/jpeg',
374
- mime: 'application/pdf'
375
- });
376
-
377
- // 3. Encrypt the filename (for new uploads, use text encryption)
378
- const encryptedFilename = await encryptText(textKey, 'document.pdf');
379
-
380
- // 4. Store metadata
381
- const metadata = {
382
- name: encryptedFilename, // Text-encrypted filename
383
- iv: encrypted.ivb64, // Base64-encoded IV
384
- thumbsize: encrypted.thumbsize
385
- };
386
-
387
- // 5. Upload encrypted.blob and metadata to server
388
- ```
389
-
390
- ### Decrypting Media
391
-
392
- ```typescript
393
- import { deriveKey, decryptFile, decryptFileThumb, decryptText } from '@redseat/api';
394
-
395
- // 1. Derive keys
396
- const fileKey = await deriveKey('user-password', 'file');
397
- const textKey = await deriveKey('user-password', 'text');
398
-
399
- // 2. Decrypt filename
400
- const filename = await decryptText(textKey, media.name);
401
-
402
- // 3. Decrypt thumbnail (if server returns full blob)
403
- const thumb = await decryptFileThumb(fileKey, encryptedBlob);
404
-
405
- // OR if server returns only the thumb portion:
406
- import { uint8ArrayFromBase64, decryptBuffer } from '@redseat/api';
407
- const iv = uint8ArrayFromBase64(media.iv);
408
- const thumb = await decryptBuffer(fileKey, iv, thumbBlob);
409
-
410
- // 4. Decrypt file
411
- const fileData = await decryptFile(fileKey, encryptedBlob);
412
- ```
413
-
414
- ---
415
-
416
- ## Important Notes and Gotchas
417
-
418
- ### Key Management
419
-
420
- 1. **Never reuse keys across different passphrases**: Each passphrase generates unique keys
421
- 2. **Store keys securely**: Keys are not extractable, so they must be re-derived from the passphrase each time
422
- 3. **Text vs File keys**: Always use the correct key type:
423
- - Text key for: filenames, tags, folder names, any text metadata
424
- - File key for: file contents, thumbnails, any binary data
425
-
426
- ### IV Handling
427
-
428
- 1. **Never reuse IVs**: Always generate a new IV for each encryption operation
429
- 2. **Store IVs securely**: The IV is not secret but must be stored to decrypt data
430
- 3. **IV format**:
431
- - Stored in metadata as base64-encoded string
432
- - Used in decryption as `Uint8Array` (convert using `uint8ArrayFromBase64`)
433
-
434
- ### File Format Compatibility
435
-
436
- 1. **Endianness**: The file format uses big-endian for size fields, but decryption functions support both for backward compatibility
437
- 2. **Info size**: Currently always 0, but the format reserves space for future use
438
- 3. **MIME types**: Padded to fixed lengths (32 bytes for thumb, 256 bytes for file)
439
-
440
- ### Error Handling
441
-
442
- - **Invalid format**: Functions throw errors if data format is incorrect
443
- - **Wrong key**: Decryption with wrong key may not throw an error but will produce garbage data
444
- - **Missing key**: Always check if key is set before attempting decryption
445
-
446
- ### Performance Considerations
447
-
448
- 1. **Key derivation**: PBKDF2 with 1000 iterations is intentionally slow to prevent brute-force attacks
449
- 2. **Large files**: Encryption/decryption of large files may take time - consider showing progress indicators
450
- 3. **Memory**: Large files are loaded into memory - be mindful of memory constraints on mobile devices
451
-
452
- ---
453
-
454
- ## Migration Notes
455
-
456
- ### Filename Encryption Change
457
-
458
- **Old format (deprecated):**
459
- - Filenames were encrypted using `encryptBuffer` with the file key
460
- - Stored as base64url-encoded encrypted buffer
461
-
462
- **New format (current):**
463
- - Filenames are encrypted using `encryptText` with the text key
464
- - Stored in format: `${base64Url(IV)}.${base64Url(encryptedData)}`
465
- - This matches the format used for other text data (tags, folder names)
466
-
467
- **Backward compatibility:**
468
- - The `tryDecryptFilename` function in `LibraryStore` handles both formats
469
- - Old filenames can still be decrypted using the old method
470
- - New uploads use the new text encryption format
471
-
472
- ---
473
-
474
- ## Testing
475
-
476
- The encryption module includes comprehensive tests in `packages/api/src/encryption.test.ts`:
477
-
478
- - Key derivation tests
479
- - Text encryption/decryption tests
480
- - Buffer encryption/decryption tests
481
- - File encryption/decryption tests
482
- - Thumbnail extraction tests
483
- - Cross-platform compatibility tests
484
-
485
- Run tests with:
486
- ```bash
487
- cd packages/api
488
- npm test
489
- ```
490
-
491
- ---
492
-
493
- ## Related Files
494
-
495
- - **`packages/api/src/crypto.ts`**: Cross-platform crypto utilities
496
- - **`packages/api/src/library.ts`**: LibraryApi class that uses encryption functions
497
- - **`src/contexts/LibraryStore.ts`**: Frontend store that uses encryption for media management
498
- - **`src/lib/encryption.ts`**: Legacy encryption implementation (being phased out)
499
-
500
- ---
501
-
502
- ## Security Considerations
503
-
504
- 1. **Key derivation**: Uses PBKDF2 with SHA-1 (1000 iterations) - consider increasing iterations for higher security
505
- 2. **Key storage**: Keys are never stored, only derived from passphrases
506
- 3. **IV generation**: Uses cryptographically secure random number generation
507
- 4. **Key extraction**: Keys are marked as non-extractable for security
508
- 5. **Passphrase handling**: Never log or store passphrases in plaintext
509
-
510
- ---
511
-
512
- ## Future Improvements
513
-
514
- Potential enhancements to consider:
515
-
516
- 1. **Argon2 support**: Consider migrating from PBKDF2 to Argon2 for better security
517
- 2. **AEAD modes**: Consider using AES-GCM for authenticated encryption
518
- 3. **Key rotation**: Add support for key rotation without re-encrypting all data
519
- 4. **Streaming encryption**: Support for encrypting/decrypting large files in chunks
520
- 5. **Performance**: Optimize for large file encryption on mobile devices
521
-
522
- ---
523
-
524
- ## Questions or Issues?
525
-
526
- If you encounter issues or have questions about the encryption module:
527
-
528
- 1. Check the test files for usage examples
529
- 2. Review the error messages - they often indicate the specific problem
530
- 3. Verify that you're using the correct key type (text vs file)
531
- 4. Ensure IVs match between encryption and decryption
532
- 5. Check that data formats match expected structures
533
-
1
+ # Encryption Module Documentation
2
+
3
+ ## Overview
4
+
5
+ The encryption module (`packages/api/src/encryption.ts`) provides cross-platform encryption and decryption functions for text and binary data. It uses the Web Crypto API (AES-CBC 256-bit encryption) and is compatible with Node.js, Web browsers, and React Native/Expo.
6
+
7
+ ## Key Concepts
8
+
9
+ ### Key Derivation (PBKDF2)
10
+
11
+ Keys are derived from passphrases using **PBKDF2** (Password-Based Key Derivation Function 2) with:
12
+ - **Hash algorithm**: SHA-1
13
+ - **Iterations**: 1000
14
+ - **Key length**: 256 bits (AES-256)
15
+ - **Salt**: Different salts for text and file encryption
16
+ - Text encryption salt: `a1209660b32cca003630cb963f730b54`
17
+ - File encryption salt: `e5709660b22ab0803630cb963f703b83`
18
+
19
+ **Important**: Text and file encryption use different keys derived from the same passphrase but with different salts. This means:
20
+ - Text data (filenames, tags, folder names) uses the "text" key
21
+ - Binary data (file contents, thumbnails) uses the "file" key
22
+ - Both keys must be derived from the same passphrase to decrypt data encrypted with that passphrase
23
+
24
+ ### Encryption Algorithm
25
+
26
+ - **Algorithm**: AES-CBC (Advanced Encryption Standard in Cipher Block Chaining mode)
27
+ - **Key size**: 256 bits
28
+ - **IV (Initialization Vector)**: 16 bytes, randomly generated for each encryption operation
29
+
30
+ ### Encoding Formats
31
+
32
+ - **Base64**: Standard base64 encoding (uses `+` and `/` characters)
33
+ - **Base64URL**: URL-safe base64 encoding (uses `-` and `_` instead of `+` and `/`, padding removed)
34
+ - Text encryption uses Base64URL for the final output to ensure URL safety
35
+
36
+ ## Core Functions
37
+
38
+ ### `deriveKey(passPhrase: string, type: 'text' | 'file'): Promise<CryptoKey>`
39
+
40
+ Derives a cryptographic key from a passphrase using PBKDF2.
41
+
42
+ **Parameters:**
43
+ - `passPhrase`: The password/passphrase to derive the key from
44
+ - `type`: Either `'text'` for text encryption or `'file'` for file/binary encryption
45
+
46
+ **Returns:** A `CryptoKey` object suitable for AES-CBC encryption/decryption
47
+
48
+ **Example:**
49
+ ```typescript
50
+ const textKey = await deriveKey('my-password', 'text');
51
+ const fileKey = await deriveKey('my-password', 'file');
52
+ ```
53
+
54
+ **Note:** The returned key is not extractable (cannot be exported) for security reasons.
55
+
56
+ ---
57
+
58
+ ### `getRandomIV(): Uint8Array`
59
+
60
+ Generates a random 16-byte Initialization Vector (IV) for encryption operations.
61
+
62
+ **Returns:** A `Uint8Array` of 16 random bytes
63
+
64
+ **Example:**
65
+ ```typescript
66
+ const iv = getRandomIV();
67
+ ```
68
+
69
+ **Important:** A new IV must be generated for each encryption operation. Never reuse IVs with the same key.
70
+
71
+ ---
72
+
73
+ ## Text Encryption
74
+
75
+ ### `encryptText(key: CryptoKey, text: string): Promise<string>`
76
+
77
+ Encrypts a text string using AES-CBC encryption.
78
+
79
+ **Parameters:**
80
+ - `key`: The CryptoKey to use for encryption (should be derived with `type: 'text'`)
81
+ - `text`: The plaintext string to encrypt
82
+
83
+ **Returns:** A string in the format: `${base64Url(IV)}.${base64Url(encryptedData)}`
84
+
85
+ **Format:**
86
+ - The IV and encrypted data are both base64url encoded
87
+ - They are concatenated with a `.` (dot) separator
88
+ - Example: `SGVsbG8gV29ybGQ.5Xq3K8mN2pQ9rT1vW4xY7zA0bC3dE6fG8hI1jK2lM3nO4p`
89
+
90
+ **Example:**
91
+ ```typescript
92
+ const key = await deriveKey('password', 'text');
93
+ const encrypted = await encryptText(key, 'Hello World');
94
+ // Returns: "SGVsbG8gV29ybGQ.5Xq3K8mN2pQ9rT1vW4xY7zA0bC3dE6fG8hI1jK2lM3nO4p"
95
+ ```
96
+
97
+ **Use cases:**
98
+ - Encrypting filenames
99
+ - Encrypting tag names
100
+ - Encrypting folder names
101
+ - Any text metadata that needs to be encrypted
102
+
103
+ ---
104
+
105
+ ### `decryptText(key: CryptoKey, encryptedText: string, iv?: ArrayBuffer | Uint8Array): Promise<string>`
106
+
107
+ Decrypts a text string that was encrypted with `encryptText`.
108
+
109
+ **Parameters:**
110
+ - `key`: The CryptoKey to use for decryption (must match the key used for encryption)
111
+ - `encryptedText`: The encrypted string in format `${IV}.${encryptedData}` OR just the encrypted data if IV is provided separately
112
+ - `iv`: (Optional) If provided, `encryptedText` should contain only the encrypted data (without the IV prefix)
113
+
114
+ **Returns:** The decrypted plaintext string
115
+
116
+ **Example:**
117
+ ```typescript
118
+ const key = await deriveKey('password', 'text');
119
+ const decrypted = await decryptText(key, 'SGVsbG8gV29ybGQ.5Xq3K8mN2pQ9rT1vW4xY7zA0bC3dE6fG8hI1jK2lM3nO4p');
120
+ // Returns: "Hello World"
121
+ ```
122
+
123
+ **Error handling:**
124
+ - Throws an error if the encrypted text format is invalid (not in `IV.encryptedData` format)
125
+ - Throws an error if decryption fails (wrong key, corrupted data, etc.)
126
+
127
+ ---
128
+
129
+ ## Binary Data Encryption
130
+
131
+ ### `encryptBuffer(key: CryptoKey, iv: ArrayBuffer | Uint8Array, buffer: ArrayBuffer | Uint8Array): Promise<ArrayBuffer>`
132
+
133
+ Encrypts binary data (ArrayBuffer or Uint8Array) using AES-CBC encryption.
134
+
135
+ **Parameters:**
136
+ - `key`: The CryptoKey to use for encryption (should be derived with `type: 'file'`)
137
+ - `iv`: The 16-byte Initialization Vector (use `getRandomIV()`)
138
+ - `buffer`: The binary data to encrypt
139
+
140
+ **Returns:** An ArrayBuffer containing the encrypted data
141
+
142
+ **Example:**
143
+ ```typescript
144
+ const key = await deriveKey('password', 'file');
145
+ const iv = getRandomIV();
146
+ const data = new Uint8Array([1, 2, 3, 4, 5]);
147
+ const encrypted = await encryptBuffer(key, iv, data);
148
+ ```
149
+
150
+ **Use cases:**
151
+ - Encrypting file contents
152
+ - Encrypting thumbnails
153
+ - Encrypting any binary data
154
+
155
+ ---
156
+
157
+ ### `decryptBuffer(key: CryptoKey, iv: ArrayBuffer | Uint8Array, buffer: ArrayBuffer | Uint8Array): Promise<ArrayBuffer>`
158
+
159
+ Decrypts binary data that was encrypted with `encryptBuffer`.
160
+
161
+ **Parameters:**
162
+ - `key`: The CryptoKey to use for decryption (must match the key used for encryption)
163
+ - `iv`: The same IV that was used during encryption
164
+ - `buffer`: The encrypted binary data
165
+
166
+ **Returns:** An ArrayBuffer containing the decrypted data
167
+
168
+ **Example:**
169
+ ```typescript
170
+ const key = await deriveKey('password', 'file');
171
+ const iv = /* same IV used for encryption */;
172
+ const decrypted = await decryptBuffer(key, iv, encryptedData);
173
+ ```
174
+
175
+ **Important:** The IV must be exactly the same as the one used during encryption. Using a different IV will result in corrupted decrypted data (though it may not throw an error).
176
+
177
+ ---
178
+
179
+ ## File Encryption (Structured Format)
180
+
181
+ ### File Format Structure
182
+
183
+ Files encrypted with `encryptFile` follow a specific binary structure:
184
+
185
+ ```
186
+ [16 bytes: IV]
187
+ [4 bytes: Thumbnail size (big-endian)]
188
+ [4 bytes: Info size (big-endian, currently always 0)]
189
+ [32 bytes: Thumbnail MIME type (padded)]
190
+ [256 bytes: File MIME type (padded)]
191
+ [T bytes: Encrypted thumbnail data]
192
+ [I bytes: Encrypted info data (currently always 0 bytes)]
193
+ [F bytes: Encrypted file data]
194
+ ```
195
+
196
+ **Byte offsets:**
197
+ - `0-15`: IV (16 bytes)
198
+ - `16-19`: Thumbnail size (4 bytes, big-endian)
199
+ - `20-23`: Info size (4 bytes, big-endian)
200
+ - `24-55`: Thumbnail MIME type (32 bytes, null-padded)
201
+ - `56-311`: File MIME type (256 bytes, null-padded)
202
+ - `312+`: Encrypted thumbnail (length = thumbnail size)
203
+ - `312 + thumbnailSize +`: Encrypted file data
204
+
205
+ **Endianness:** Sizes are stored in **big-endian** format. The decryption functions try big-endian first, then fall back to little-endian for backward compatibility.
206
+
207
+ ---
208
+
209
+ ### `encryptFile(key: CryptoKey, options: EncryptFileOptions): Promise<EncryptedFile>`
210
+
211
+ Encrypts a file with its thumbnail and metadata into a structured binary format.
212
+
213
+ **Parameters:**
214
+ - `key`: The CryptoKey to use for encryption (should be derived with `type: 'file'`)
215
+ - `options`: An object containing:
216
+ - `filename`: The original filename (will be encrypted separately)
217
+ - `buffer`: The file data to encrypt (ArrayBuffer or Uint8Array)
218
+ - `thumb`: The thumbnail data to encrypt (ArrayBuffer or Uint8Array, can be empty)
219
+ - `thumbMime`: MIME type of the thumbnail (e.g., `'image/jpeg'`)
220
+ - `mime`: MIME type of the file (e.g., `'image/png'`)
221
+
222
+ **Returns:** An `EncryptedFile` object containing:
223
+ - `filename`: Base64URL-encoded encrypted filename (encrypted with the same IV as the file)
224
+ - `ivb64`: Base64-encoded IV (for storage in metadata)
225
+ - `thumbsize`: Size of the encrypted thumbnail in bytes
226
+ - `blob`: The complete encrypted file structure as an ArrayBuffer
227
+
228
+ **Example:**
229
+ ```typescript
230
+ const key = await deriveKey('password', 'file');
231
+ const fileData = await file.arrayBuffer();
232
+ const thumbData = await thumbnail.arrayBuffer();
233
+
234
+ const encrypted = await encryptFile(key, {
235
+ filename: 'photo.jpg',
236
+ buffer: fileData,
237
+ thumb: thumbData,
238
+ thumbMime: 'image/jpeg',
239
+ mime: 'image/jpeg'
240
+ });
241
+
242
+ // encrypted.filename: base64url encoded encrypted filename
243
+ // encrypted.ivb64: base64 encoded IV (store this in metadata)
244
+ // encrypted.thumbsize: size of encrypted thumbnail
245
+ // encrypted.blob: complete encrypted file structure
246
+ ```
247
+
248
+ **Important notes:**
249
+ - The filename is encrypted separately using `encryptBuffer` with the same IV
250
+ - The filename is returned as base64url-encoded (for URL safety)
251
+ - The IV is returned as base64-encoded (for storage in database metadata)
252
+ - All data (file, thumbnail, filename) uses the same IV
253
+
254
+ ---
255
+
256
+ ### `decryptFile(key: CryptoKey, buffer: ArrayBuffer): Promise<ArrayBuffer>`
257
+
258
+ Decrypts the file data from an encrypted file structure.
259
+
260
+ **Parameters:**
261
+ - `key`: The CryptoKey to use for decryption (must match the key used for encryption)
262
+ - `buffer`: The complete encrypted file structure (as returned by `encryptFile`)
263
+
264
+ **Returns:** An ArrayBuffer containing the decrypted file data
265
+
266
+ **Example:**
267
+ ```typescript
268
+ const key = await deriveKey('password', 'file');
269
+ const encryptedBlob = /* encrypted file blob */;
270
+ const decryptedFile = await decryptFile(key, encryptedBlob);
271
+ ```
272
+
273
+ **How it works:**
274
+ 1. Extracts the IV from the first 16 bytes
275
+ 2. Reads the thumbnail size (tries big-endian first, falls back to little-endian)
276
+ 3. Reads the info size
277
+ 4. Calculates the offset to the encrypted file data
278
+ 5. Decrypts and returns the file data
279
+
280
+ ---
281
+
282
+ ### `decryptFileThumb(key: CryptoKey, buffer: ArrayBuffer): Promise<ArrayBuffer>`
283
+
284
+ Decrypts the thumbnail from an encrypted file structure.
285
+
286
+ **Parameters:**
287
+ - `key`: The CryptoKey to use for decryption (must match the key used for encryption)
288
+ - `buffer`: The complete encrypted file structure (as returned by `encryptFile`)
289
+
290
+ **Returns:** An ArrayBuffer containing the decrypted thumbnail data, or an empty ArrayBuffer if no thumbnail exists
291
+
292
+ **Example:**
293
+ ```typescript
294
+ const key = await deriveKey('password', 'file');
295
+ const encryptedBlob = /* encrypted file blob */;
296
+ const decryptedThumb = await decryptFileThumb(key, encryptedBlob);
297
+ ```
298
+
299
+ **How it works:**
300
+ 1. Extracts the IV from the first 16 bytes
301
+ 2. Reads the thumbnail size (tries big-endian first, falls back to little-endian)
302
+ 3. If thumbnail size is 0, returns empty buffer
303
+ 4. Calculates the offset to the encrypted thumbnail (after IV, sizes, and MIME types)
304
+ 5. Decrypts and returns the thumbnail data
305
+
306
+ **Note:** The server's `getThumb` API endpoint returns only the encrypted thumbnail portion (already sliced), not the full file structure. In that case, use `decryptBuffer` directly with the IV from the media metadata.
307
+
308
+ ---
309
+
310
+ ### `encryptFilename(key: CryptoKey, filename: string, iv: ArrayBuffer | Uint8Array): Promise<string>`
311
+
312
+ Encrypts a filename using the same IV as the file encryption.
313
+
314
+ **Parameters:**
315
+ - `key`: The CryptoKey to use for encryption (should be derived with `type: 'file'`)
316
+ - `filename`: The original filename to encrypt
317
+ - `iv`: The same IV used for encrypting the file
318
+
319
+ **Returns:** Base64URL-encoded encrypted filename
320
+
321
+ **Example:**
322
+ ```typescript
323
+ const key = await deriveKey('password', 'file');
324
+ const iv = getRandomIV();
325
+ const encryptedFilename = await encryptFilename(key, 'photo.jpg', iv);
326
+ ```
327
+
328
+ **Note:** This function is used internally by `encryptFile`. For new uploads, filenames are encrypted using `encryptText` with the text key instead, which produces the format `${IV}.${encryptedData}`.
329
+
330
+ ---
331
+
332
+ ## Cross-Platform Compatibility
333
+
334
+ The encryption module is designed to work across multiple platforms:
335
+
336
+ ### Supported Platforms
337
+
338
+ 1. **Web Browsers**: Uses `window.crypto.subtle` or `globalThis.crypto.subtle`
339
+ 2. **Node.js**: Uses `crypto.webcrypto.subtle` (Node.js 15+)
340
+ 3. **React Native/Expo**: Uses `globalThis.crypto.subtle` or `crypto.subtle`
341
+
342
+ ### Platform Detection
343
+
344
+ The `crypto.ts` module automatically detects the available crypto APIs:
345
+
346
+ - **`getCryptoSubtle()`**: Returns the appropriate `SubtleCrypto` implementation
347
+ - **`getCryptoRandomValues()`**: Returns the appropriate random number generator
348
+
349
+ ### Base64 Encoding
350
+
351
+ Base64 encoding/decoding is handled cross-platform:
352
+ - **Node.js**: Uses `Buffer` for efficient conversion
353
+ - **Browsers/RN**: Uses `atob`/`btoa` or manual conversion
354
+
355
+ ---
356
+
357
+ ## Usage Patterns
358
+
359
+ ### Complete File Upload Flow
360
+
361
+ ```typescript
362
+ import { deriveKey, encryptFile, encryptText, getRandomIV } from '@redseat/api';
363
+
364
+ // 1. Derive keys from passphrase
365
+ const fileKey = await deriveKey('user-password', 'file');
366
+ const textKey = await deriveKey('user-password', 'text');
367
+
368
+ // 2. Encrypt the file
369
+ const encrypted = await encryptFile(fileKey, {
370
+ filename: 'document.pdf',
371
+ buffer: fileArrayBuffer,
372
+ thumb: thumbnailArrayBuffer,
373
+ thumbMime: 'image/jpeg',
374
+ mime: 'application/pdf'
375
+ });
376
+
377
+ // 3. Encrypt the filename (for new uploads, use text encryption)
378
+ const encryptedFilename = await encryptText(textKey, 'document.pdf');
379
+
380
+ // 4. Store metadata
381
+ const metadata = {
382
+ name: encryptedFilename, // Text-encrypted filename
383
+ iv: encrypted.ivb64, // Base64-encoded IV
384
+ thumbsize: encrypted.thumbsize
385
+ };
386
+
387
+ // 5. Upload encrypted.blob and metadata to server
388
+ ```
389
+
390
+ ### Decrypting Media
391
+
392
+ ```typescript
393
+ import { deriveKey, decryptFile, decryptFileThumb, decryptText } from '@redseat/api';
394
+
395
+ // 1. Derive keys
396
+ const fileKey = await deriveKey('user-password', 'file');
397
+ const textKey = await deriveKey('user-password', 'text');
398
+
399
+ // 2. Decrypt filename
400
+ const filename = await decryptText(textKey, media.name);
401
+
402
+ // 3. Decrypt thumbnail (if server returns full blob)
403
+ const thumb = await decryptFileThumb(fileKey, encryptedBlob);
404
+
405
+ // OR if server returns only the thumb portion:
406
+ import { uint8ArrayFromBase64, decryptBuffer } from '@redseat/api';
407
+ const iv = uint8ArrayFromBase64(media.iv);
408
+ const thumb = await decryptBuffer(fileKey, iv, thumbBlob);
409
+
410
+ // 4. Decrypt file
411
+ const fileData = await decryptFile(fileKey, encryptedBlob);
412
+ ```
413
+
414
+ ---
415
+
416
+ ## Important Notes and Gotchas
417
+
418
+ ### Key Management
419
+
420
+ 1. **Never reuse keys across different passphrases**: Each passphrase generates unique keys
421
+ 2. **Store keys securely**: Keys are not extractable, so they must be re-derived from the passphrase each time
422
+ 3. **Text vs File keys**: Always use the correct key type:
423
+ - Text key for: filenames, tags, folder names, any text metadata
424
+ - File key for: file contents, thumbnails, any binary data
425
+
426
+ ### IV Handling
427
+
428
+ 1. **Never reuse IVs**: Always generate a new IV for each encryption operation
429
+ 2. **Store IVs securely**: The IV is not secret but must be stored to decrypt data
430
+ 3. **IV format**:
431
+ - Stored in metadata as base64-encoded string
432
+ - Used in decryption as `Uint8Array` (convert using `uint8ArrayFromBase64`)
433
+
434
+ ### File Format Compatibility
435
+
436
+ 1. **Endianness**: The file format uses big-endian for size fields, but decryption functions support both for backward compatibility
437
+ 2. **Info size**: Currently always 0, but the format reserves space for future use
438
+ 3. **MIME types**: Padded to fixed lengths (32 bytes for thumb, 256 bytes for file)
439
+
440
+ ### Error Handling
441
+
442
+ - **Invalid format**: Functions throw errors if data format is incorrect
443
+ - **Wrong key**: Decryption with wrong key may not throw an error but will produce garbage data
444
+ - **Missing key**: Always check if key is set before attempting decryption
445
+
446
+ ### Performance Considerations
447
+
448
+ 1. **Key derivation**: PBKDF2 with 1000 iterations is intentionally slow to prevent brute-force attacks
449
+ 2. **Large files**: Encryption/decryption of large files may take time - consider showing progress indicators
450
+ 3. **Memory**: Large files are loaded into memory - be mindful of memory constraints on mobile devices
451
+
452
+ ---
453
+
454
+ ## Migration Notes
455
+
456
+ ### Filename Encryption Change
457
+
458
+ **Old format (deprecated):**
459
+ - Filenames were encrypted using `encryptBuffer` with the file key
460
+ - Stored as base64url-encoded encrypted buffer
461
+
462
+ **New format (current):**
463
+ - Filenames are encrypted using `encryptText` with the text key
464
+ - Stored in format: `${base64Url(IV)}.${base64Url(encryptedData)}`
465
+ - This matches the format used for other text data (tags, folder names)
466
+
467
+ **Backward compatibility:**
468
+ - The `tryDecryptFilename` function in `LibraryStore` handles both formats
469
+ - Old filenames can still be decrypted using the old method
470
+ - New uploads use the new text encryption format
471
+
472
+ ---
473
+
474
+ ## Testing
475
+
476
+ The encryption module includes comprehensive tests in `packages/api/src/encryption.test.ts`:
477
+
478
+ - Key derivation tests
479
+ - Text encryption/decryption tests
480
+ - Buffer encryption/decryption tests
481
+ - File encryption/decryption tests
482
+ - Thumbnail extraction tests
483
+ - Cross-platform compatibility tests
484
+
485
+ Run tests with:
486
+ ```bash
487
+ cd packages/api
488
+ npm test
489
+ ```
490
+
491
+ ---
492
+
493
+ ## Related Files
494
+
495
+ - **`packages/api/src/crypto.ts`**: Cross-platform crypto utilities
496
+ - **`packages/api/src/library.ts`**: LibraryApi class that uses encryption functions
497
+ - **`src/contexts/LibraryStore.ts`**: Frontend store that uses encryption for media management
498
+ - **`src/lib/encryption.ts`**: Legacy encryption implementation (being phased out)
499
+
500
+ ---
501
+
502
+ ## Security Considerations
503
+
504
+ 1. **Key derivation**: Uses PBKDF2 with SHA-1 (1000 iterations) - consider increasing iterations for higher security
505
+ 2. **Key storage**: Keys are never stored, only derived from passphrases
506
+ 3. **IV generation**: Uses cryptographically secure random number generation
507
+ 4. **Key extraction**: Keys are marked as non-extractable for security
508
+ 5. **Passphrase handling**: Never log or store passphrases in plaintext
509
+
510
+ ---
511
+
512
+ ## Future Improvements
513
+
514
+ Potential enhancements to consider:
515
+
516
+ 1. **Argon2 support**: Consider migrating from PBKDF2 to Argon2 for better security
517
+ 2. **AEAD modes**: Consider using AES-GCM for authenticated encryption
518
+ 3. **Key rotation**: Add support for key rotation without re-encrypting all data
519
+ 4. **Streaming encryption**: Support for encrypting/decrypting large files in chunks
520
+ 5. **Performance**: Optimize for large file encryption on mobile devices
521
+
522
+ ---
523
+
524
+ ## Questions or Issues?
525
+
526
+ If you encounter issues or have questions about the encryption module:
527
+
528
+ 1. Check the test files for usage examples
529
+ 2. Review the error messages - they often indicate the specific problem
530
+ 3. Verify that you're using the correct key type (text vs file)
531
+ 4. Ensure IVs match between encryption and decryption
532
+ 5. Check that data formats match expected structures
533
+