@redseat/api 0.0.11 → 0.0.12
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/README.md +132 -132
- package/agents.md +275 -275
- package/client.md +318 -288
- package/dist/client.d.ts +1 -0
- package/dist/client.js +3 -0
- package/dist/library.d.ts +3 -0
- package/dist/library.js +1 -1
- package/encryption.md +533 -533
- package/firebase.md +602 -602
- package/libraries.md +1652 -1652
- package/package.json +49 -49
- package/server.md +196 -196
- package/test.md +291 -291
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
|
+
|