@friggframework/core 2.0.0-next.57 → 2.0.0-next.58
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/application/commands/README.md +90 -60
- package/application/commands/user-commands.js +36 -5
- package/database/encryption/documentdb-encryption-service.md +1537 -1232
- package/package.json +5 -5
- package/user/repositories/user-repository-documentdb.js +0 -11
- package/user/repositories/user-repository-factory.js +2 -1
- package/user/repositories/user-repository-interface.js +0 -11
- package/user/repositories/user-repository-mongo.js +0 -11
- package/user/repositories/user-repository-postgres.js +0 -13
|
@@ -29,24 +29,24 @@
|
|
|
29
29
|
|
|
30
30
|
DocumentDB repositories use `$runCommandRaw()` for MongoDB protocol compatibility, which **bypasses Prisma Client Extensions**, including the encryption extension. This results in a **critical security vulnerability** where:
|
|
31
31
|
|
|
32
|
-
-
|
|
33
|
-
-
|
|
32
|
+
- ✅ **MongoDB/PostgreSQL**: Automatic encryption via Prisma Extension
|
|
33
|
+
- ❌ **DocumentDB**: OAuth credentials stored in **plain text**
|
|
34
34
|
|
|
35
35
|
### The Solution
|
|
36
36
|
|
|
37
37
|
Create `DocumentDBEncryptionService` - a centralized encryption service specifically designed for DocumentDB repositories that:
|
|
38
38
|
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
39
|
+
- Provides document-level encryption/decryption
|
|
40
|
+
- Handles nested field paths (e.g., `data.access_token`)
|
|
41
|
+
- Uses the same Cryptor and schema registry as Prisma Extension
|
|
42
|
+
- Maintains consistency with existing encryption architecture
|
|
43
43
|
|
|
44
44
|
### Impact
|
|
45
45
|
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
46
|
+
- **Security**: OAuth credentials encrypted at rest in DocumentDB
|
|
47
|
+
- **Architecture**: DRY principle - single source of encryption logic
|
|
48
|
+
- **Consistency**: All DocumentDB repos use same encryption pattern
|
|
49
|
+
- **Compliance**: Meets production encryption requirements
|
|
50
50
|
|
|
51
51
|
---
|
|
52
52
|
|
|
@@ -69,12 +69,13 @@ Database (encrypted storage)
|
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
**How it works**:
|
|
72
|
+
|
|
72
73
|
```javascript
|
|
73
74
|
// MongoDB Repository - Automatic encryption
|
|
74
75
|
await prisma.credential.create({
|
|
75
76
|
data: {
|
|
76
|
-
access_token:
|
|
77
|
-
}
|
|
77
|
+
access_token: 'plain_secret', // ← Plain text in
|
|
78
|
+
},
|
|
78
79
|
});
|
|
79
80
|
// → Prisma Extension intercepts
|
|
80
81
|
// → FieldEncryptionService.encryptField() called
|
|
@@ -100,16 +101,17 @@ Database (PLAIN TEXT STORAGE) ⚠️ SECURITY VULNERABILITY
|
|
|
100
101
|
```
|
|
101
102
|
|
|
102
103
|
**Why it's broken**:
|
|
104
|
+
|
|
103
105
|
```javascript
|
|
104
106
|
// DocumentDB Repository - NO encryption
|
|
105
107
|
const oauthData = {
|
|
106
|
-
access_token:
|
|
107
|
-
refresh_token:
|
|
108
|
+
access_token: 'ya29.actual_google_token', // Plain text!
|
|
109
|
+
refresh_token: '1//0secret_refresh_token', // Plain text!
|
|
108
110
|
};
|
|
109
111
|
|
|
110
112
|
await prisma.$runCommandRaw({
|
|
111
113
|
insert: 'Credential',
|
|
112
|
-
documents: [{ data: oauthData }]
|
|
114
|
+
documents: [{ data: oauthData }],
|
|
113
115
|
});
|
|
114
116
|
// ❌ Prisma Extension NEVER sees this command
|
|
115
117
|
// ❌ FieldEncryptionService NEVER invoked
|
|
@@ -123,18 +125,19 @@ From Prisma documentation:
|
|
|
123
125
|
> "$runCommandRaw is a low-level database access method. Prisma Client extensions do not apply to raw database access."
|
|
124
126
|
|
|
125
127
|
**Why DocumentDB needs raw commands**:
|
|
126
|
-
|
|
127
|
-
-
|
|
128
|
-
-
|
|
128
|
+
|
|
129
|
+
- DocumentDB has MongoDB compatibility limitations
|
|
130
|
+
- Certain Prisma features don't work (transactions, some aggregations)
|
|
131
|
+
- Raw commands provide direct MongoDB protocol access
|
|
129
132
|
|
|
130
133
|
### Current Repository Status
|
|
131
134
|
|
|
132
|
-
| Repository
|
|
133
|
-
|
|
134
|
-
| **UserRepositoryDocumentDB**
|
|
135
|
-
| **ModuleRepositoryDocumentDB**
|
|
136
|
-
| **CredentialRepositoryDocumentDB**
|
|
137
|
-
| **IntegrationRepositoryDocumentDB** | ✅ No encrypted fields, OK
|
|
135
|
+
| Repository | Encryption Status | Security Risk |
|
|
136
|
+
| ----------------------------------- | ------------------------------------------------------ | -------------------------------------------- |
|
|
137
|
+
| **UserRepositoryDocumentDB** | ✅ Has manual encryption for `hashword` | Low - passwords protected |
|
|
138
|
+
| **ModuleRepositoryDocumentDB** | ⚠️ Has manual decryption for reads only | Medium - assumes credentials encrypted |
|
|
139
|
+
| **CredentialRepositoryDocumentDB** | ❌ **NO encryption on writes, NO decryption on reads** | 🔴 **CRITICAL - OAuth tokens in plain text** |
|
|
140
|
+
| **IntegrationRepositoryDocumentDB** | ✅ No encrypted fields, OK | None |
|
|
138
141
|
|
|
139
142
|
---
|
|
140
143
|
|
|
@@ -142,18 +145,18 @@ From Prisma documentation:
|
|
|
142
145
|
|
|
143
146
|
### Comparison: FieldEncryptionService vs DocumentDBEncryptionService
|
|
144
147
|
|
|
145
|
-
| Aspect
|
|
146
|
-
|
|
147
|
-
| **Purpose**
|
|
148
|
-
| **Invocation**
|
|
149
|
-
| **Scope**
|
|
150
|
-
| **Nested Fields**
|
|
151
|
-
| **Integration**
|
|
152
|
-
| **Query Types**
|
|
153
|
-
| **Database Support** | MongoDB, PostgreSQL (via Prisma)
|
|
154
|
-
| **Schema Registry**
|
|
155
|
-
| **Error Handling**
|
|
156
|
-
| **Testing**
|
|
148
|
+
| Aspect | FieldEncryptionService | DocumentDBEncryptionService |
|
|
149
|
+
| -------------------- | ---------------------------------------------- | ---------------------------------------- |
|
|
150
|
+
| **Purpose** | Encrypt individual fields for Prisma Extension | Encrypt entire documents for raw queries |
|
|
151
|
+
| **Invocation** | Automatic (Prisma intercepts queries) | Manual (repository calls explicitly) |
|
|
152
|
+
| **Scope** | Single field at a time | Entire document with multiple fields |
|
|
153
|
+
| **Nested Fields** | Handled by Prisma Extension traversal | Must manually traverse field paths |
|
|
154
|
+
| **Integration** | Via Prisma Client Extension | Direct import in repositories |
|
|
155
|
+
| **Query Types** | `create()`, `update()`, `findFirst()`, etc. | `$runCommandRaw()`, via documentdb-utils |
|
|
156
|
+
| **Database Support** | MongoDB, PostgreSQL (via Prisma) | DocumentDB (raw MongoDB protocol) |
|
|
157
|
+
| **Schema Registry** | Used by Prisma Extension | Directly queries registry |
|
|
158
|
+
| **Error Handling** | Prisma transaction rollback | Must handle in repository |
|
|
159
|
+
| **Testing** | Integration tests with Prisma | Unit tests + repository tests |
|
|
157
160
|
|
|
158
161
|
### Proposed Architecture (DocumentDB - FIXED)
|
|
159
162
|
|
|
@@ -258,8 +261,9 @@ class DocumentDBEncryptionService {
|
|
|
258
261
|
**Purpose**: Initialize the service and configure Cryptor
|
|
259
262
|
|
|
260
263
|
**Behavior**:
|
|
261
|
-
|
|
262
|
-
-
|
|
264
|
+
|
|
265
|
+
- Calls `_initializeCryptor()` immediately
|
|
266
|
+
- Sets up `this.cryptor` and `this.enabled` properties
|
|
263
267
|
|
|
264
268
|
**No parameters**
|
|
265
269
|
|
|
@@ -270,6 +274,7 @@ class DocumentDBEncryptionService {
|
|
|
270
274
|
**Purpose**: Initialize Cryptor with environment-based configuration
|
|
271
275
|
|
|
272
276
|
**Logic**:
|
|
277
|
+
|
|
273
278
|
```javascript
|
|
274
279
|
1. Get STAGE from environment (default: 'development')
|
|
275
280
|
2. If STAGE in ['dev', 'test', 'local']:
|
|
@@ -288,10 +293,11 @@ class DocumentDBEncryptionService {
|
|
|
288
293
|
```
|
|
289
294
|
|
|
290
295
|
**Environment Variables Used**:
|
|
291
|
-
|
|
292
|
-
- `
|
|
293
|
-
-
|
|
294
|
-
-
|
|
296
|
+
|
|
297
|
+
- `STAGE` or `NODE_ENV`: Determines bypass behavior
|
|
298
|
+
- `KMS_KEY_ARN`: AWS KMS key ARN (enables KMS encryption)
|
|
299
|
+
- `AES_KEY_ID`: AES key identifier (enables AES encryption)
|
|
300
|
+
- `AES_KEY`: AES encryption key (required if AES_KEY_ID present)
|
|
295
301
|
|
|
296
302
|
**Matches**: Logic from `packages/core/database/prisma.js` lines 76-96
|
|
297
303
|
|
|
@@ -302,12 +308,14 @@ class DocumentDBEncryptionService {
|
|
|
302
308
|
**Purpose**: Encrypt fields in a document before storing to DocumentDB
|
|
303
309
|
|
|
304
310
|
**Parameters**:
|
|
305
|
-
|
|
306
|
-
-
|
|
311
|
+
|
|
312
|
+
- `modelName` (string): Model name from schema registry (e.g., 'User', 'Credential')
|
|
313
|
+
- `document` (Object): Document to encrypt
|
|
307
314
|
|
|
308
315
|
**Returns**: `Promise<Object>` - Document with encrypted fields
|
|
309
316
|
|
|
310
317
|
**Algorithm**:
|
|
318
|
+
|
|
311
319
|
```javascript
|
|
312
320
|
1. If !this.enabled or !this.cryptor:
|
|
313
321
|
- Return document unchanged (bypass)
|
|
@@ -324,17 +332,19 @@ class DocumentDBEncryptionService {
|
|
|
324
332
|
```
|
|
325
333
|
|
|
326
334
|
**Error Handling**:
|
|
327
|
-
|
|
328
|
-
-
|
|
335
|
+
|
|
336
|
+
- Invalid inputs: Return unchanged
|
|
337
|
+
- Encryption errors: Propagate to caller (repository must handle)
|
|
329
338
|
|
|
330
339
|
**Example**:
|
|
340
|
+
|
|
331
341
|
```javascript
|
|
332
342
|
const plainDoc = {
|
|
333
|
-
userId:
|
|
343
|
+
userId: '123',
|
|
334
344
|
data: {
|
|
335
|
-
access_token:
|
|
336
|
-
refresh_token:
|
|
337
|
-
}
|
|
345
|
+
access_token: 'plain_secret',
|
|
346
|
+
refresh_token: 'plain_refresh',
|
|
347
|
+
},
|
|
338
348
|
};
|
|
339
349
|
|
|
340
350
|
const encrypted = await service.encryptFields('Credential', plainDoc);
|
|
@@ -349,12 +359,14 @@ const encrypted = await service.encryptFields('Credential', plainDoc);
|
|
|
349
359
|
**Purpose**: Decrypt fields in a document after reading from DocumentDB
|
|
350
360
|
|
|
351
361
|
**Parameters**:
|
|
352
|
-
|
|
353
|
-
-
|
|
362
|
+
|
|
363
|
+
- `modelName` (string): Model name from schema registry
|
|
364
|
+
- `document` (Object): Document to decrypt
|
|
354
365
|
|
|
355
366
|
**Returns**: `Promise<Object>` - Document with decrypted fields
|
|
356
367
|
|
|
357
368
|
**Algorithm**:
|
|
369
|
+
|
|
358
370
|
```javascript
|
|
359
371
|
1. If !this.enabled or !this.cryptor:
|
|
360
372
|
- Return document unchanged (bypass)
|
|
@@ -371,17 +383,19 @@ const encrypted = await service.encryptFields('Credential', plainDoc);
|
|
|
371
383
|
```
|
|
372
384
|
|
|
373
385
|
**Error Handling**:
|
|
374
|
-
|
|
375
|
-
-
|
|
386
|
+
|
|
387
|
+
- Decryption failures: Set field to null (don't expose encrypted data)
|
|
388
|
+
- Log error with context
|
|
376
389
|
|
|
377
390
|
**Example**:
|
|
391
|
+
|
|
378
392
|
```javascript
|
|
379
393
|
const encryptedDoc = {
|
|
380
|
-
userId:
|
|
394
|
+
userId: '123',
|
|
381
395
|
data: {
|
|
382
|
-
access_token:
|
|
383
|
-
refresh_token:
|
|
384
|
-
}
|
|
396
|
+
access_token: 'aes-key-1:iv:cipher:enckey',
|
|
397
|
+
refresh_token: 'aes-key-1:iv:cipher:enckey',
|
|
398
|
+
},
|
|
385
399
|
};
|
|
386
400
|
|
|
387
401
|
const decrypted = await service.decryptFields('Credential', encryptedDoc);
|
|
@@ -396,11 +410,13 @@ const decrypted = await service.decryptFields('Credential', encryptedDoc);
|
|
|
396
410
|
**Purpose**: Encrypt a specific field path in a document (handles nested fields)
|
|
397
411
|
|
|
398
412
|
**Parameters**:
|
|
399
|
-
|
|
400
|
-
-
|
|
401
|
-
-
|
|
413
|
+
|
|
414
|
+
- `document` (Object): Document to modify (mutated in place)
|
|
415
|
+
- `fieldPath` (string): Field path from schema registry (e.g., 'data.access_token')
|
|
416
|
+
- `modelName` (string): For error logging context
|
|
402
417
|
|
|
403
418
|
**Algorithm**:
|
|
419
|
+
|
|
404
420
|
```javascript
|
|
405
421
|
1. Split fieldPath by '.': parts = fieldPath.split('.')
|
|
406
422
|
2. Navigate to parent object:
|
|
@@ -422,9 +438,10 @@ const decrypted = await service.decryptFields('Credential', encryptedDoc);
|
|
|
422
438
|
```
|
|
423
439
|
|
|
424
440
|
**Example Field Paths**:
|
|
425
|
-
|
|
426
|
-
-
|
|
427
|
-
-
|
|
441
|
+
|
|
442
|
+
- `hashword` → Encrypts `document.hashword`
|
|
443
|
+
- `data.access_token` → Encrypts `document.data.access_token`
|
|
444
|
+
- `data.refresh_token` → Encrypts `document.data.refresh_token`
|
|
428
445
|
|
|
429
446
|
---
|
|
430
447
|
|
|
@@ -433,11 +450,13 @@ const decrypted = await service.decryptFields('Credential', encryptedDoc);
|
|
|
433
450
|
**Purpose**: Decrypt a specific field path in a document
|
|
434
451
|
|
|
435
452
|
**Parameters**:
|
|
436
|
-
|
|
437
|
-
-
|
|
438
|
-
-
|
|
453
|
+
|
|
454
|
+
- `document` (Object): Document to modify (mutated in place)
|
|
455
|
+
- `fieldPath` (string): Field path from schema registry
|
|
456
|
+
- `modelName` (string): For error logging context
|
|
439
457
|
|
|
440
458
|
**Algorithm**:
|
|
459
|
+
|
|
441
460
|
```javascript
|
|
442
461
|
1. Split fieldPath by '.': parts = fieldPath.split('.')
|
|
443
462
|
2. Navigate to parent object:
|
|
@@ -460,9 +479,10 @@ const decrypted = await service.decryptFields('Credential', encryptedDoc);
|
|
|
460
479
|
```
|
|
461
480
|
|
|
462
481
|
**Error Tolerance**:
|
|
463
|
-
|
|
464
|
-
-
|
|
465
|
-
-
|
|
482
|
+
|
|
483
|
+
- If decryption fails, set field to `null` instead of throwing
|
|
484
|
+
- Prevents exposing encrypted strings to application
|
|
485
|
+
- Logs error for debugging
|
|
466
486
|
|
|
467
487
|
---
|
|
468
488
|
|
|
@@ -471,11 +491,13 @@ const decrypted = await service.decryptFields('Credential', encryptedDoc);
|
|
|
471
491
|
**Purpose**: Check if a value is in encrypted format
|
|
472
492
|
|
|
473
493
|
**Parameters**:
|
|
474
|
-
|
|
494
|
+
|
|
495
|
+
- `value` (any): Value to check
|
|
475
496
|
|
|
476
497
|
**Returns**: `boolean` - True if value is encrypted
|
|
477
498
|
|
|
478
499
|
**Logic**:
|
|
500
|
+
|
|
479
501
|
```javascript
|
|
480
502
|
1. If typeof value !== 'string': return false
|
|
481
503
|
2. Split by ':': parts = value.split(':')
|
|
@@ -485,11 +507,12 @@ const decrypted = await service.decryptFields('Credential', encryptedDoc);
|
|
|
485
507
|
**Encrypted Format**: `"keyId:iv:cipher:encKey"` (envelope encryption)
|
|
486
508
|
|
|
487
509
|
**Examples**:
|
|
510
|
+
|
|
488
511
|
```javascript
|
|
489
|
-
_isEncryptedValue(
|
|
490
|
-
_isEncryptedValue(
|
|
491
|
-
_isEncryptedValue(null)
|
|
492
|
-
_isEncryptedValue({})
|
|
512
|
+
_isEncryptedValue('plain_text'); // false
|
|
513
|
+
_isEncryptedValue('aes-key-1:iv123:cipher456:enckey789'); // true
|
|
514
|
+
_isEncryptedValue(null); // false
|
|
515
|
+
_isEncryptedValue({}); // false
|
|
493
516
|
```
|
|
494
517
|
|
|
495
518
|
---
|
|
@@ -498,7 +521,9 @@ _isEncryptedValue({}) // false
|
|
|
498
521
|
|
|
499
522
|
```javascript
|
|
500
523
|
const { Cryptor } = require('../encrypt/Cryptor');
|
|
501
|
-
const {
|
|
524
|
+
const {
|
|
525
|
+
getEncryptedFields,
|
|
526
|
+
} = require('./encryption/encryption-schema-registry');
|
|
502
527
|
```
|
|
503
528
|
|
|
504
529
|
**Cryptor**: Handles actual encryption/decryption (KMS or AES)
|
|
@@ -517,10 +542,10 @@ const ENCRYPTED_FIELDS = {
|
|
|
517
542
|
'data.access_token',
|
|
518
543
|
'data.refresh_token',
|
|
519
544
|
'data.id_token',
|
|
520
|
-
'data.domain'
|
|
545
|
+
'data.domain',
|
|
521
546
|
],
|
|
522
547
|
IntegrationMapping: ['mapping'],
|
|
523
|
-
Token: ['token']
|
|
548
|
+
Token: ['token'],
|
|
524
549
|
};
|
|
525
550
|
```
|
|
526
551
|
|
|
@@ -533,6 +558,7 @@ const ENCRYPTED_FIELDS = {
|
|
|
533
558
|
### Phase 1: Create DocumentDBEncryptionService (New File)
|
|
534
559
|
|
|
535
560
|
**Files to Create**:
|
|
561
|
+
|
|
536
562
|
1. `packages/core/database/documentdb-encryption-service.js`
|
|
537
563
|
2. `packages/core/database/__tests__/documentdb-encryption-service.test.js`
|
|
538
564
|
|
|
@@ -540,96 +566,96 @@ const ENCRYPTED_FIELDS = {
|
|
|
540
566
|
|
|
541
567
|
#### 1.1 Service Class (`documentdb-encryption-service.js`)
|
|
542
568
|
|
|
543
|
-
-
|
|
544
|
-
-
|
|
545
|
-
-
|
|
546
|
-
-
|
|
547
|
-
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
-
|
|
583
|
-
-
|
|
569
|
+
- [ ] Create file with standard file header comment
|
|
570
|
+
- [ ] Import dependencies: `Cryptor`, `getEncryptedFields`
|
|
571
|
+
- [ ] Create `DocumentDBEncryptionService` class
|
|
572
|
+
- [ ] Implement `constructor()` - calls `_initializeCryptor()`
|
|
573
|
+
- [ ] Implement `_initializeCryptor()` - matches `prisma.js` logic
|
|
574
|
+
- [ ] Check STAGE environment variable
|
|
575
|
+
- [ ] Implement bypass for dev/test/local
|
|
576
|
+
- [ ] Check for KMS_KEY_ARN
|
|
577
|
+
- [ ] Check for AES_KEY_ID
|
|
578
|
+
- [ ] Create Cryptor with shouldUseAws flag
|
|
579
|
+
- [ ] Set this.enabled flag
|
|
580
|
+
- [ ] Implement `encryptFields(modelName, document)`
|
|
581
|
+
- [ ] Early returns for disabled/invalid input
|
|
582
|
+
- [ ] Get encrypted fields from registry
|
|
583
|
+
- [ ] Loop through field paths
|
|
584
|
+
- [ ] Call `_encryptFieldPath()` for each
|
|
585
|
+
- [ ] Implement `decryptFields(modelName, document)`
|
|
586
|
+
- [ ] Early returns for disabled/invalid input
|
|
587
|
+
- [ ] Get encrypted fields from registry
|
|
588
|
+
- [ ] Loop through field paths
|
|
589
|
+
- [ ] Call `_decryptFieldPath()` for each
|
|
590
|
+
- [ ] Implement `_encryptFieldPath(document, fieldPath, modelName)`
|
|
591
|
+
- [ ] Parse field path (split by '.')
|
|
592
|
+
- [ ] Navigate to parent object
|
|
593
|
+
- [ ] Check if already encrypted
|
|
594
|
+
- [ ] Convert to string if needed
|
|
595
|
+
- [ ] Call `this.cryptor.encrypt()`
|
|
596
|
+
- [ ] Error handling with context
|
|
597
|
+
- [ ] Implement `_decryptFieldPath(document, fieldPath, modelName)`
|
|
598
|
+
- [ ] Parse field path
|
|
599
|
+
- [ ] Navigate to parent object
|
|
600
|
+
- [ ] Check if encrypted format
|
|
601
|
+
- [ ] Call `this.cryptor.decrypt()`
|
|
602
|
+
- [ ] Try to parse as JSON
|
|
603
|
+
- [ ] Error handling (set to null on failure)
|
|
604
|
+
- [ ] Implement `_isEncryptedValue(value)`
|
|
605
|
+
- [ ] Type check (must be string)
|
|
606
|
+
- [ ] Split by ':'
|
|
607
|
+
- [ ] Check for 4+ parts
|
|
608
|
+
- [ ] Add JSDoc comments for all public methods
|
|
609
|
+
- [ ] Export: `module.exports = { DocumentDBEncryptionService };`
|
|
584
610
|
|
|
585
611
|
#### 1.2 Service Tests (`__tests__/documentdb-encryption-service.test.js`)
|
|
586
612
|
|
|
587
|
-
-
|
|
588
|
-
-
|
|
589
|
-
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
-
|
|
613
|
+
- [ ] Create test file with describe block
|
|
614
|
+
- [ ] Mock dependencies: `Cryptor`, `getEncryptedFields`
|
|
615
|
+
- [ ] **Test Group: Initialization**
|
|
616
|
+
- [ ] Test bypass in dev stage
|
|
617
|
+
- [ ] Test bypass in test stage
|
|
618
|
+
- [ ] Test bypass in local stage
|
|
619
|
+
- [ ] Test enabled with KMS_KEY_ARN in production
|
|
620
|
+
- [ ] Test enabled with AES_KEY_ID in production
|
|
621
|
+
- [ ] Test disabled with no keys in production
|
|
622
|
+
- [ ] Test KMS takes precedence over AES
|
|
623
|
+
- [ ] **Test Group: encryptFields()**
|
|
624
|
+
- [ ] Test returns unchanged when disabled (dev stage)
|
|
625
|
+
- [ ] Test returns unchanged for null document
|
|
626
|
+
- [ ] Test returns unchanged for non-object document
|
|
627
|
+
- [ ] Test returns unchanged when no encrypted fields in registry
|
|
628
|
+
- [ ] Test encrypts User.hashword
|
|
629
|
+
- [ ] Test encrypts Credential.data.access_token
|
|
630
|
+
- [ ] Test encrypts Credential.data.refresh_token
|
|
631
|
+
- [ ] Test encrypts multiple nested fields
|
|
632
|
+
- [ ] Test skips already encrypted values
|
|
633
|
+
- [ ] Test skips null values
|
|
634
|
+
- [ ] Test skips non-existent paths
|
|
635
|
+
- [ ] Test encrypts objects (JSON.stringify)
|
|
636
|
+
- [ ] Test error handling (propagates error)
|
|
637
|
+
- [ ] **Test Group: decryptFields()**
|
|
638
|
+
- [ ] Test returns unchanged when disabled
|
|
639
|
+
- [ ] Test returns unchanged for null document
|
|
640
|
+
- [ ] Test returns unchanged for non-object document
|
|
641
|
+
- [ ] Test returns unchanged when no encrypted fields in registry
|
|
642
|
+
- [ ] Test decrypts User.hashword
|
|
643
|
+
- [ ] Test decrypts Credential.data.access_token
|
|
644
|
+
- [ ] Test decrypts multiple nested fields
|
|
645
|
+
- [ ] Test skips plain text values
|
|
646
|
+
- [ ] Test skips null values
|
|
647
|
+
- [ ] Test skips non-existent paths
|
|
648
|
+
- [ ] Test parses JSON objects after decryption
|
|
649
|
+
- [ ] Test handles non-JSON strings
|
|
650
|
+
- [ ] Test error handling (sets field to null)
|
|
651
|
+
- [ ] **Test Group: \_isEncryptedValue()**
|
|
652
|
+
- [ ] Test returns false for plain text
|
|
653
|
+
- [ ] Test returns false for null
|
|
654
|
+
- [ ] Test returns false for numbers
|
|
655
|
+
- [ ] Test returns false for objects
|
|
656
|
+
- [ ] Test returns false for short strings (< 4 parts)
|
|
657
|
+
- [ ] Test returns true for encrypted format (4+ parts with colons)
|
|
658
|
+
- [ ] **Test Coverage Target**: >90% line coverage
|
|
633
659
|
|
|
634
660
|
**Estimated Time**: 2-3 hours
|
|
635
661
|
|
|
@@ -648,22 +674,25 @@ const ENCRYPTED_FIELDS = {
|
|
|
648
674
|
#### Critical Issue #1: JSON.parse Corrupts Date Objects
|
|
649
675
|
|
|
650
676
|
**Problem**:
|
|
677
|
+
|
|
651
678
|
```javascript
|
|
652
679
|
// Current implementation (lines 101, 147)
|
|
653
680
|
const result = JSON.parse(JSON.stringify(document));
|
|
654
681
|
```
|
|
655
682
|
|
|
656
683
|
**Why it's critical**:
|
|
657
|
-
|
|
658
|
-
-
|
|
659
|
-
-
|
|
660
|
-
-
|
|
684
|
+
|
|
685
|
+
- `JSON.stringify()` converts Date objects to ISO strings
|
|
686
|
+
- `JSON.parse()` does NOT convert them back to Date objects
|
|
687
|
+
- OAuth tokens often have `expires_at` as Date objects
|
|
688
|
+
- This causes **silent data corruption** in production
|
|
661
689
|
|
|
662
690
|
**Example of corruption**:
|
|
691
|
+
|
|
663
692
|
```javascript
|
|
664
693
|
const credential = {
|
|
665
694
|
data: { access_token: 'secret' },
|
|
666
|
-
expires_at: new Date('2025-12-31')
|
|
695
|
+
expires_at: new Date('2025-12-31'), // Date object
|
|
667
696
|
};
|
|
668
697
|
|
|
669
698
|
const encrypted = await service.encryptFields('Credential', credential);
|
|
@@ -672,26 +701,30 @@ const encrypted = await service.encryptFields('Credential', credential);
|
|
|
672
701
|
```
|
|
673
702
|
|
|
674
703
|
**Fix**:
|
|
704
|
+
|
|
675
705
|
```javascript
|
|
676
706
|
// Use structuredClone (Node.js 17+)
|
|
677
707
|
const result = structuredClone(document);
|
|
678
708
|
```
|
|
679
709
|
|
|
680
710
|
**Benefits of structuredClone**:
|
|
681
|
-
|
|
682
|
-
-
|
|
683
|
-
-
|
|
684
|
-
-
|
|
685
|
-
-
|
|
711
|
+
|
|
712
|
+
- ✅ Preserves Date objects
|
|
713
|
+
- ✅ Preserves RegExp objects
|
|
714
|
+
- ✅ Preserves Buffer objects
|
|
715
|
+
- ✅ Handles circular references
|
|
716
|
+
- ✅ Native Node.js function (no dependencies)
|
|
686
717
|
|
|
687
718
|
**Files to Update**:
|
|
688
|
-
|
|
719
|
+
|
|
720
|
+
- `documentdb-encryption-service.js` lines 101, 147
|
|
689
721
|
|
|
690
722
|
**Checklist**:
|
|
691
|
-
|
|
692
|
-
-
|
|
693
|
-
-
|
|
694
|
-
-
|
|
723
|
+
|
|
724
|
+
- [ ] Replace `JSON.parse(JSON.stringify(document))` in `encryptFields()` (line 101)
|
|
725
|
+
- [ ] Replace `JSON.parse(JSON.stringify(document))` in `decryptFields()` (line 147)
|
|
726
|
+
- [ ] Add test case: `it('preserves Date objects in documents')`
|
|
727
|
+
- [ ] Verify Node.js version supports structuredClone (>=17)
|
|
695
728
|
|
|
696
729
|
**Estimated Time**: 5 minutes
|
|
697
730
|
|
|
@@ -700,6 +733,7 @@ const result = structuredClone(document);
|
|
|
700
733
|
#### Critical Issue #2: Decryption Failures Set to Null
|
|
701
734
|
|
|
702
735
|
**Problem**:
|
|
736
|
+
|
|
703
737
|
```javascript
|
|
704
738
|
// Current implementation (_decryptFieldPath, line 258)
|
|
705
739
|
catch (error) {
|
|
@@ -709,12 +743,14 @@ catch (error) {
|
|
|
709
743
|
```
|
|
710
744
|
|
|
711
745
|
**Why it's critical**:
|
|
712
|
-
|
|
713
|
-
-
|
|
714
|
-
-
|
|
715
|
-
-
|
|
746
|
+
|
|
747
|
+
- **Silent credential loss** - Application continues with null tokens
|
|
748
|
+
- **Hard to debug** - Error logged but not propagated
|
|
749
|
+
- **Security risk** - Could mask key rotation issues or corrupted data
|
|
750
|
+
- **Cascade failures** - Null propagates until crash elsewhere
|
|
716
751
|
|
|
717
752
|
**Real-world scenario**:
|
|
753
|
+
|
|
718
754
|
```javascript
|
|
719
755
|
// Encrypted credential in database (key rotated or corrupted)
|
|
720
756
|
const credential = await findCredential(userId);
|
|
@@ -726,11 +762,13 @@ const api = new AsanaAPI({ token: credential.access_token });
|
|
|
726
762
|
```
|
|
727
763
|
|
|
728
764
|
**Why this is wrong**:
|
|
729
|
-
|
|
730
|
-
-
|
|
731
|
-
-
|
|
765
|
+
|
|
766
|
+
- Violates fail-fast principle (errors should be discovered immediately)
|
|
767
|
+
- Inconsistent with `encryptFields()` which throws errors
|
|
768
|
+
- Repository can't distinguish null data from decryption failure
|
|
732
769
|
|
|
733
770
|
**Fix**:
|
|
771
|
+
|
|
734
772
|
```javascript
|
|
735
773
|
// Throw error immediately (fail fast)
|
|
736
774
|
catch (error) {
|
|
@@ -740,15 +778,17 @@ catch (error) {
|
|
|
740
778
|
```
|
|
741
779
|
|
|
742
780
|
**Files to Update**:
|
|
743
|
-
|
|
744
|
-
-
|
|
781
|
+
|
|
782
|
+
- `documentdb-encryption-service.js` line 258
|
|
783
|
+
- `documentdb-encryption-service.test.js` update test "sets field to null on decryption error"
|
|
745
784
|
|
|
746
785
|
**Checklist**:
|
|
747
|
-
|
|
748
|
-
-
|
|
749
|
-
-
|
|
750
|
-
-
|
|
751
|
-
-
|
|
786
|
+
|
|
787
|
+
- [ ] Remove `current[fieldName] = null;` from `_decryptFieldPath()` (line 258)
|
|
788
|
+
- [ ] Add `throw new Error(...)` with context
|
|
789
|
+
- [ ] Update test: change from `expect(result.hashword).toBeNull()` to `expect(...).rejects.toThrow()`
|
|
790
|
+
- [ ] Update test name: "throws error on decryption failure" (not "sets field to null")
|
|
791
|
+
- [ ] Verify all 56+ tests still pass
|
|
752
792
|
|
|
753
793
|
**Estimated Time**: 10 minutes
|
|
754
794
|
|
|
@@ -757,6 +797,7 @@ catch (error) {
|
|
|
757
797
|
#### Critical Issue #3: No Cryptor Dependency Injection
|
|
758
798
|
|
|
759
799
|
**Problem**:
|
|
800
|
+
|
|
760
801
|
```javascript
|
|
761
802
|
// Current implementation (constructor, lines 24-26)
|
|
762
803
|
constructor() {
|
|
@@ -769,12 +810,14 @@ _initializeCryptor() {
|
|
|
769
810
|
```
|
|
770
811
|
|
|
771
812
|
**Why it's critical**:
|
|
772
|
-
|
|
773
|
-
-
|
|
774
|
-
-
|
|
775
|
-
- **
|
|
813
|
+
|
|
814
|
+
- **Repository tests break** - Can't mock encryption in Phase 2-4
|
|
815
|
+
- **Requires real keys** - Tests need AWS credentials or AES keys
|
|
816
|
+
- **Slower tests** - Real encryption is slower than mocks
|
|
817
|
+
- **Can't test error scenarios** - Can't simulate Cryptor failures
|
|
776
818
|
|
|
777
819
|
**Impact on Phase 2 (UserRepositoryDocumentDB tests)**:
|
|
820
|
+
|
|
778
821
|
```javascript
|
|
779
822
|
describe('UserRepositoryDocumentDB', () => {
|
|
780
823
|
it('encrypts hashword before saving', async () => {
|
|
@@ -782,7 +825,9 @@ describe('UserRepositoryDocumentDB', () => {
|
|
|
782
825
|
const service = new DocumentDBEncryptionService();
|
|
783
826
|
// Tries to create real Cryptor - tests fail without keys
|
|
784
827
|
|
|
785
|
-
const repo = new UserRepositoryDocumentDB({
|
|
828
|
+
const repo = new UserRepositoryDocumentDB({
|
|
829
|
+
encryptionService: service,
|
|
830
|
+
});
|
|
786
831
|
await repo.createUser({ hashword: 'password' });
|
|
787
832
|
// ❌ Real KMS/AES encryption happens in tests
|
|
788
833
|
});
|
|
@@ -790,6 +835,7 @@ describe('UserRepositoryDocumentDB', () => {
|
|
|
790
835
|
```
|
|
791
836
|
|
|
792
837
|
**Fix**:
|
|
838
|
+
|
|
793
839
|
```javascript
|
|
794
840
|
class DocumentDBEncryptionService {
|
|
795
841
|
constructor({ cryptor = null } = {}) {
|
|
@@ -806,11 +852,12 @@ class DocumentDBEncryptionService {
|
|
|
806
852
|
```
|
|
807
853
|
|
|
808
854
|
**Usage**:
|
|
855
|
+
|
|
809
856
|
```javascript
|
|
810
857
|
// In tests (with mock)
|
|
811
858
|
const mockCryptor = {
|
|
812
859
|
encrypt: jest.fn().mockResolvedValue('encrypted'),
|
|
813
|
-
decrypt: jest.fn().mockResolvedValue('decrypted')
|
|
860
|
+
decrypt: jest.fn().mockResolvedValue('decrypted'),
|
|
814
861
|
};
|
|
815
862
|
const service = new DocumentDBEncryptionService({ cryptor: mockCryptor });
|
|
816
863
|
|
|
@@ -819,16 +866,18 @@ const service = new DocumentDBEncryptionService();
|
|
|
819
866
|
```
|
|
820
867
|
|
|
821
868
|
**Files to Update**:
|
|
822
|
-
|
|
823
|
-
-
|
|
869
|
+
|
|
870
|
+
- `documentdb-encryption-service.js` constructor
|
|
871
|
+
- `documentdb-encryption-service.test.js` add dependency injection test
|
|
824
872
|
|
|
825
873
|
**Checklist**:
|
|
826
|
-
|
|
827
|
-
-
|
|
828
|
-
-
|
|
829
|
-
-
|
|
830
|
-
-
|
|
831
|
-
-
|
|
874
|
+
|
|
875
|
+
- [ ] Change constructor signature: `constructor({ cryptor = null } = {})`
|
|
876
|
+
- [ ] Add conditional logic: if cryptor provided, use it; else call `_initializeCryptor()`
|
|
877
|
+
- [ ] Set `this.enabled = true` when cryptor injected
|
|
878
|
+
- [ ] Add test: `it('accepts injected Cryptor for testing')`
|
|
879
|
+
- [ ] Verify injection test passes
|
|
880
|
+
- [ ] Verify all existing tests still pass
|
|
832
881
|
|
|
833
882
|
**Estimated Time**: 15 minutes
|
|
834
883
|
|
|
@@ -837,37 +886,41 @@ const service = new DocumentDBEncryptionService();
|
|
|
837
886
|
#### Phase 1.5 Summary
|
|
838
887
|
|
|
839
888
|
**Total Changes**:
|
|
840
|
-
|
|
841
|
-
-
|
|
842
|
-
-
|
|
843
|
-
-
|
|
889
|
+
|
|
890
|
+
- 3 files modified
|
|
891
|
+
- 5 lines of code changed (service implementation)
|
|
892
|
+
- 3 new/updated test cases
|
|
893
|
+
- 0 breaking changes (backward compatible)
|
|
844
894
|
|
|
845
895
|
**Total Time**: ~30 minutes
|
|
846
896
|
|
|
847
897
|
**Success Criteria**:
|
|
848
|
-
|
|
849
|
-
-
|
|
850
|
-
-
|
|
851
|
-
-
|
|
852
|
-
-
|
|
853
|
-
-
|
|
898
|
+
|
|
899
|
+
- ✅ All 56+ tests pass
|
|
900
|
+
- ✅ Date objects preserved in documents
|
|
901
|
+
- ✅ Decryption failures throw errors
|
|
902
|
+
- ✅ Cryptor can be injected for testing
|
|
903
|
+
- ✅ 100% code coverage maintained
|
|
904
|
+
- ✅ Code review assessment improves from 6/10 to 8/10
|
|
854
905
|
|
|
855
906
|
**Validation**:
|
|
907
|
+
|
|
856
908
|
```javascript
|
|
857
909
|
// Test 1: Date preservation
|
|
858
910
|
const doc = { data: { token: 'secret' }, createdAt: new Date() };
|
|
859
911
|
const encrypted = await service.encryptFields('Model', doc);
|
|
860
|
-
expect(encrypted.createdAt).toBeInstanceOf(Date);
|
|
912
|
+
expect(encrypted.createdAt).toBeInstanceOf(Date); // ✅ Must pass
|
|
861
913
|
|
|
862
914
|
// Test 2: Decryption error throws
|
|
863
915
|
const corrupted = { data: { token: 'corrupted_encrypted_value' } };
|
|
864
|
-
await expect(service.decryptFields('Model', corrupted))
|
|
865
|
-
|
|
916
|
+
await expect(service.decryptFields('Model', corrupted)).rejects.toThrow(
|
|
917
|
+
'Decryption failed'
|
|
918
|
+
); // ✅ Must pass
|
|
866
919
|
|
|
867
920
|
// Test 3: Dependency injection
|
|
868
921
|
const mockCryptor = { encrypt: jest.fn(), decrypt: jest.fn() };
|
|
869
922
|
const service = new DocumentDBEncryptionService({ cryptor: mockCryptor });
|
|
870
|
-
expect(service.cryptor).toBe(mockCryptor);
|
|
923
|
+
expect(service.cryptor).toBe(mockCryptor); // ✅ Must pass
|
|
871
924
|
```
|
|
872
925
|
|
|
873
926
|
**Next Step**: After Phase 1.5 completion, proceed to Phase 2 (Refactor UserRepositoryDocumentDB)
|
|
@@ -880,61 +933,84 @@ expect(service.cryptor).toBe(mockCryptor); // ✅ Must pass
|
|
|
880
933
|
|
|
881
934
|
**Changes Checklist**:
|
|
882
935
|
|
|
883
|
-
-
|
|
884
|
-
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
-
|
|
916
|
-
|
|
917
|
-
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
-
|
|
934
|
-
|
|
935
|
-
-
|
|
936
|
-
|
|
937
|
-
|
|
936
|
+
- [ ] Import DocumentDBEncryptionService at top of file
|
|
937
|
+
- [ ] **Remove existing encryption methods** (lines 24-148):
|
|
938
|
+
- [ ] Remove `_initializeCryptor()` method
|
|
939
|
+
- [ ] Remove `_encryptField()` method
|
|
940
|
+
- [ ] Remove `_decryptField()` method
|
|
941
|
+
- [ ] Remove `_isEncryptedValue()` method
|
|
942
|
+
- [ ] Remove `_encryptHashword()` method
|
|
943
|
+
- [ ] Remove `_decryptHashword()` method
|
|
944
|
+
- [ ] **Update constructor**:
|
|
945
|
+
- [ ] Add: `this.encryptionService = new DocumentDBEncryptionService();`
|
|
946
|
+
- [ ] Remove: `this._initializeCryptor();`
|
|
947
|
+
- [ ] **Update `createIndividualUser()` method** (around line 183):
|
|
948
|
+
- [ ] After building document, before insertOne():
|
|
949
|
+
```javascript
|
|
950
|
+
const encryptedDocument = await this.encryptionService.encryptFields(
|
|
951
|
+
'User',
|
|
952
|
+
document
|
|
953
|
+
);
|
|
954
|
+
const insertedId = await insertOne(
|
|
955
|
+
this.prisma,
|
|
956
|
+
'User',
|
|
957
|
+
encryptedDocument
|
|
958
|
+
);
|
|
959
|
+
```
|
|
960
|
+
- [ ] After findOne(), before \_mapUser():
|
|
961
|
+
```javascript
|
|
962
|
+
const decryptedUser = await this.encryptionService.decryptFields(
|
|
963
|
+
'User',
|
|
964
|
+
created
|
|
965
|
+
);
|
|
966
|
+
return this._mapUser(decryptedUser);
|
|
967
|
+
```
|
|
968
|
+
- [ ] **Update `createOrganizationUser()` method**:
|
|
969
|
+
- [ ] No changes needed (no encrypted fields for organization users)
|
|
970
|
+
- [ ] **Update `findIndividualUserById()` method** (around line 165):
|
|
971
|
+
- [ ] After findOne():
|
|
972
|
+
```javascript
|
|
973
|
+
const decryptedUser = await this.encryptionService.decryptFields(
|
|
974
|
+
'User',
|
|
975
|
+
doc
|
|
976
|
+
);
|
|
977
|
+
return this._mapUser(decryptedUser);
|
|
978
|
+
```
|
|
979
|
+
- [ ] **Update `findIndividualUserByUsername()` method**:
|
|
980
|
+
- [ ] Same pattern: decrypt after findOne()
|
|
981
|
+
- [ ] **Update `findIndividualUserByEmail()` method**:
|
|
982
|
+
- [ ] Same pattern: decrypt after findOne()
|
|
983
|
+
- [ ] **Update `findIndividualUserByAppUserId()` method**:
|
|
984
|
+
- [ ] Same pattern: decrypt after findOne()
|
|
985
|
+
- [ ] **Update `findIndividualUserById()` method**:
|
|
986
|
+
- [ ] Same pattern: decrypt after findOne()
|
|
987
|
+
- [ ] **Update `updateIndividualUser()` method** (around line 303):
|
|
988
|
+
- [ ] After preparing update payload, encrypt before updateOne():
|
|
989
|
+
```javascript
|
|
990
|
+
const encryptedPayload = await this.encryptionService.encryptFields(
|
|
991
|
+
'User',
|
|
992
|
+
payload
|
|
993
|
+
);
|
|
994
|
+
await updateOne(
|
|
995
|
+
this.prisma,
|
|
996
|
+
'User',
|
|
997
|
+
{ _id: objectId, type: 'INDIVIDUAL' },
|
|
998
|
+
{ $set: encryptedPayload }
|
|
999
|
+
);
|
|
1000
|
+
```
|
|
1001
|
+
- [ ] After findOne(), decrypt before \_mapUser():
|
|
1002
|
+
```javascript
|
|
1003
|
+
const decryptedUser = await this.encryptionService.decryptFields(
|
|
1004
|
+
'User',
|
|
1005
|
+
updated
|
|
1006
|
+
);
|
|
1007
|
+
return this._mapUser(decryptedUser);
|
|
1008
|
+
```
|
|
1009
|
+
- [ ] **Update `updateOrganizationUser()` method**:
|
|
1010
|
+
- [ ] No changes needed (no encrypted fields)
|
|
1011
|
+
- [ ] Verify no references to old encryption methods remain
|
|
1012
|
+
- [ ] Run linter to check for issues
|
|
1013
|
+
- [ ] Test locally
|
|
938
1014
|
|
|
939
1015
|
**Estimated Time**: 1 hour
|
|
940
1016
|
|
|
@@ -946,43 +1022,49 @@ expect(service.cryptor).toBe(mockCryptor); // ✅ Must pass
|
|
|
946
1022
|
|
|
947
1023
|
**Changes Checklist**:
|
|
948
1024
|
|
|
949
|
-
-
|
|
950
|
-
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1025
|
+
- [ ] Import DocumentDBEncryptionService at top of file
|
|
1026
|
+
- [ ] **Remove existing encryption methods** (lines 22-117):
|
|
1027
|
+
- [ ] Remove `_initializeCryptor()` method
|
|
1028
|
+
- [ ] Remove `_encryptField()` method
|
|
1029
|
+
- [ ] Remove `_decryptField()` method
|
|
1030
|
+
- [ ] Remove `_isEncryptedValue()` method
|
|
1031
|
+
- [ ] Remove `_decryptCredentialData()` method
|
|
1032
|
+
- [ ] **Update constructor**:
|
|
1033
|
+
- [ ] Add: `this.encryptionService = new DocumentDBEncryptionService();`
|
|
1034
|
+
- [ ] Remove: `this._initializeCryptor();`
|
|
1035
|
+
- [ ] **Update `_fetchCredential()` method** (around line 241):
|
|
1036
|
+
- [ ] After findOne(), before returning:
|
|
1037
|
+
```javascript
|
|
1038
|
+
const decryptedCredential = await this.encryptionService.decryptFields(
|
|
1039
|
+
'Credential',
|
|
1040
|
+
rawCredential
|
|
1041
|
+
);
|
|
1042
|
+
return {
|
|
1043
|
+
id: fromObjectId(decryptedCredential._id),
|
|
1044
|
+
userId: fromObjectId(decryptedCredential.userId),
|
|
1045
|
+
externalId: decryptedCredential.externalId ?? null,
|
|
1046
|
+
authIsValid: decryptedCredential.authIsValid ?? null,
|
|
1047
|
+
createdAt: decryptedCredential.createdAt,
|
|
1048
|
+
updatedAt: decryptedCredential.updatedAt,
|
|
1049
|
+
data: decryptedCredential.data,
|
|
1050
|
+
};
|
|
1051
|
+
```
|
|
1052
|
+
- [ ] **Update `_fetchCredentialsBulk()` method** (around line 280):
|
|
1053
|
+
- [ ] Inside the map function for each credential:
|
|
1054
|
+
```javascript
|
|
1055
|
+
const decryptedCredential = await this.encryptionService.decryptFields(
|
|
1056
|
+
'Credential',
|
|
1057
|
+
rawCredential
|
|
1058
|
+
);
|
|
1059
|
+
return this._convertCredentialIds({
|
|
1060
|
+
id: fromObjectId(decryptedCredential._id),
|
|
1061
|
+
// ... rest of mapping
|
|
1062
|
+
data: decryptedCredential.data,
|
|
1063
|
+
});
|
|
1064
|
+
```
|
|
1065
|
+
- [ ] Verify no references to old encryption methods remain
|
|
1066
|
+
- [ ] Run linter to check for issues
|
|
1067
|
+
- [ ] Test locally
|
|
986
1068
|
|
|
987
1069
|
**Note**: ModuleRepository doesn't create/update credentials, only reads them. It relies on CredentialRepository for writes.
|
|
988
1070
|
|
|
@@ -998,151 +1080,180 @@ expect(service.cryptor).toBe(mockCryptor); // ✅ Must pass
|
|
|
998
1080
|
|
|
999
1081
|
**Changes Checklist**:
|
|
1000
1082
|
|
|
1001
|
-
-
|
|
1002
|
-
-
|
|
1003
|
-
|
|
1004
|
-
-
|
|
1005
|
-
- [ ] **Current problematic code**:
|
|
1006
|
-
```javascript
|
|
1007
|
-
const { user, userId, authIsValid, externalId, ...oauthData } = details || {};
|
|
1008
|
-
// oauthData contains PLAIN TEXT: access_token, refresh_token, etc.
|
|
1083
|
+
- [ ] Import DocumentDBEncryptionService at top of file
|
|
1084
|
+
- [ ] **Update constructor**:
|
|
1085
|
+
- [ ] Add: `this.encryptionService = new DocumentDBEncryptionService();`
|
|
1086
|
+
- [ ] **Fix `upsertCredential()` method** (around line 50):
|
|
1009
1087
|
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
```javascript
|
|
1017
|
-
const { user, userId, authIsValid, externalId, ...oauthData } = details || {};
|
|
1018
|
-
|
|
1019
|
-
// Build plain text document
|
|
1020
|
-
const plainDocument = {
|
|
1021
|
-
userId: toObjectId(userId || user),
|
|
1022
|
-
externalId: externalId ?? null,
|
|
1023
|
-
authIsValid: authIsValid ?? true,
|
|
1024
|
-
data: oauthData, // Still plain text at this point
|
|
1025
|
-
createdAt: now,
|
|
1026
|
-
updatedAt: now
|
|
1027
|
-
};
|
|
1088
|
+
- [ ] **Current problematic code**:
|
|
1089
|
+
|
|
1090
|
+
```javascript
|
|
1091
|
+
const { user, userId, authIsValid, externalId, ...oauthData } =
|
|
1092
|
+
details || {};
|
|
1093
|
+
// oauthData contains PLAIN TEXT: access_token, refresh_token, etc.
|
|
1028
1094
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1095
|
+
const document = {
|
|
1096
|
+
data: oauthData, // ❌ STORED AS PLAIN TEXT
|
|
1097
|
+
};
|
|
1098
|
+
await insertOne(this.prisma, 'Credential', document);
|
|
1099
|
+
```
|
|
1034
1100
|
|
|
1035
|
-
|
|
1101
|
+
- [ ] **Replace with ENCRYPTED version**:
|
|
1036
1102
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
'Credential',
|
|
1041
|
-
created
|
|
1042
|
-
);
|
|
1103
|
+
```javascript
|
|
1104
|
+
const { user, userId, authIsValid, externalId, ...oauthData } =
|
|
1105
|
+
details || {};
|
|
1043
1106
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1107
|
+
// Build plain text document
|
|
1108
|
+
const plainDocument = {
|
|
1109
|
+
userId: toObjectId(userId || user),
|
|
1110
|
+
externalId: externalId ?? null,
|
|
1111
|
+
authIsValid: authIsValid ?? true,
|
|
1112
|
+
data: oauthData, // Still plain text at this point
|
|
1113
|
+
createdAt: now,
|
|
1114
|
+
updatedAt: now,
|
|
1115
|
+
};
|
|
1116
|
+
|
|
1117
|
+
// ✅ ENCRYPT before storing
|
|
1118
|
+
const encryptedDocument = await this.encryptionService.encryptFields(
|
|
1119
|
+
'Credential',
|
|
1120
|
+
plainDocument
|
|
1121
|
+
);
|
|
1122
|
+
|
|
1123
|
+
const insertedId = await insertOne(
|
|
1124
|
+
this.prisma,
|
|
1125
|
+
'Credential',
|
|
1126
|
+
encryptedDocument
|
|
1127
|
+
);
|
|
1128
|
+
|
|
1129
|
+
// Read back and decrypt
|
|
1130
|
+
const created = await findOne(this.prisma, 'Credential', {
|
|
1131
|
+
_id: insertedId,
|
|
1132
|
+
});
|
|
1133
|
+
const decryptedCredential = await this.encryptionService.decryptFields(
|
|
1134
|
+
'Credential',
|
|
1135
|
+
created
|
|
1136
|
+
);
|
|
1137
|
+
|
|
1138
|
+
return this._mapCredential(decryptedCredential);
|
|
1139
|
+
```
|
|
1140
|
+
|
|
1141
|
+
- [ ] **For UPDATE case** (when credential exists):
|
|
1142
|
+
|
|
1143
|
+
```javascript
|
|
1144
|
+
// Merge existing data with new data
|
|
1145
|
+
const existingData = existing.data || {};
|
|
1146
|
+
const mergedData = { ...existingData, ...oauthData };
|
|
1147
|
+
|
|
1148
|
+
// Build update document
|
|
1149
|
+
const updateDocument = {
|
|
1150
|
+
data: mergedData,
|
|
1151
|
+
authIsValid: authIsValid ?? existing.authIsValid,
|
|
1152
|
+
updatedAt: now,
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1155
|
+
// ✅ ENCRYPT before storing
|
|
1156
|
+
const encryptedUpdate = await this.encryptionService.encryptFields(
|
|
1157
|
+
'Credential',
|
|
1158
|
+
{ data: updateDocument.data } // Only encrypt the data field
|
|
1159
|
+
);
|
|
1058
1160
|
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
{
|
|
1070
|
-
$set: {
|
|
1071
|
-
data: encryptedUpdate.data,
|
|
1072
|
-
authIsValid: updateDocument.authIsValid,
|
|
1073
|
-
updatedAt: updateDocument.updatedAt
|
|
1161
|
+
await updateOne(
|
|
1162
|
+
this.prisma,
|
|
1163
|
+
'Credential',
|
|
1164
|
+
{ _id: existing._id },
|
|
1165
|
+
{
|
|
1166
|
+
$set: {
|
|
1167
|
+
data: encryptedUpdate.data,
|
|
1168
|
+
authIsValid: updateDocument.authIsValid,
|
|
1169
|
+
updatedAt: updateDocument.updatedAt,
|
|
1170
|
+
},
|
|
1074
1171
|
}
|
|
1172
|
+
);
|
|
1173
|
+
|
|
1174
|
+
// Read back and decrypt
|
|
1175
|
+
const updated = await findOne(this.prisma, 'Credential', {
|
|
1176
|
+
_id: existing._id,
|
|
1177
|
+
});
|
|
1178
|
+
const decryptedCredential = await this.encryptionService.decryptFields(
|
|
1179
|
+
'Credential',
|
|
1180
|
+
updated
|
|
1181
|
+
);
|
|
1182
|
+
|
|
1183
|
+
return this._mapCredential(decryptedCredential);
|
|
1184
|
+
```
|
|
1185
|
+
|
|
1186
|
+
- [ ] **Fix `_mapCredential()` method** (around line 192):
|
|
1187
|
+
- [ ] **Current problematic code**:
|
|
1188
|
+
```javascript
|
|
1189
|
+
_mapCredential(doc) {
|
|
1190
|
+
const data = doc?.data || {};
|
|
1191
|
+
return {
|
|
1192
|
+
id: fromObjectId(doc._id),
|
|
1193
|
+
userId: fromObjectId(doc.userId),
|
|
1194
|
+
externalId: doc.externalId ?? null,
|
|
1195
|
+
authIsValid: doc.authIsValid ?? null,
|
|
1196
|
+
...data // ❌ Could be encrypted strings
|
|
1197
|
+
};
|
|
1075
1198
|
}
|
|
1076
|
-
|
|
1199
|
+
```
|
|
1200
|
+
- [ ] **Note**: If we decrypt in `upsertCredential()` before calling `_mapCredential()`, this method doesn't need changes. But for safety:
|
|
1201
|
+
```javascript
|
|
1202
|
+
_mapCredential(doc) {
|
|
1203
|
+
// Assume doc is already decrypted by caller
|
|
1204
|
+
// (upsertCredential, findCredential should decrypt before calling this)
|
|
1205
|
+
const data = doc?.data || {};
|
|
1206
|
+
return {
|
|
1207
|
+
id: fromObjectId(doc._id),
|
|
1208
|
+
userId: fromObjectId(doc.userId),
|
|
1209
|
+
externalId: doc.externalId ?? null,
|
|
1210
|
+
authIsValid: doc.authIsValid ?? null,
|
|
1211
|
+
...data // Already decrypted
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
```
|
|
1215
|
+
- [ ] **Fix `findCredential()` method** (if exists):
|
|
1077
1216
|
|
|
1078
|
-
|
|
1079
|
-
const updated = await findOne(this.prisma, 'Credential', { _id: existing._id });
|
|
1080
|
-
const decryptedCredential = await this.encryptionService.decryptFields(
|
|
1081
|
-
'Credential',
|
|
1082
|
-
updated
|
|
1083
|
-
);
|
|
1217
|
+
- [ ] After findOne(), decrypt:
|
|
1084
1218
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
- [ ] **Current problematic code**:
|
|
1089
|
-
```javascript
|
|
1090
|
-
_mapCredential(doc) {
|
|
1091
|
-
const data = doc?.data || {};
|
|
1092
|
-
return {
|
|
1093
|
-
id: fromObjectId(doc._id),
|
|
1094
|
-
userId: fromObjectId(doc.userId),
|
|
1095
|
-
externalId: doc.externalId ?? null,
|
|
1096
|
-
authIsValid: doc.authIsValid ?? null,
|
|
1097
|
-
...data // ❌ Could be encrypted strings
|
|
1098
|
-
};
|
|
1099
|
-
}
|
|
1100
|
-
```
|
|
1101
|
-
- [ ] **Note**: If we decrypt in `upsertCredential()` before calling `_mapCredential()`, this method doesn't need changes. But for safety:
|
|
1102
|
-
```javascript
|
|
1103
|
-
_mapCredential(doc) {
|
|
1104
|
-
// Assume doc is already decrypted by caller
|
|
1105
|
-
// (upsertCredential, findCredential should decrypt before calling this)
|
|
1106
|
-
const data = doc?.data || {};
|
|
1107
|
-
return {
|
|
1108
|
-
id: fromObjectId(doc._id),
|
|
1109
|
-
userId: fromObjectId(doc.userId),
|
|
1110
|
-
externalId: doc.externalId ?? null,
|
|
1111
|
-
authIsValid: doc.authIsValid ?? null,
|
|
1112
|
-
...data // Already decrypted
|
|
1113
|
-
};
|
|
1114
|
-
}
|
|
1115
|
-
```
|
|
1116
|
-
- [ ] **Fix `findCredential()` method** (if exists):
|
|
1117
|
-
- [ ] After findOne(), decrypt:
|
|
1118
|
-
```javascript
|
|
1119
|
-
const doc = await findOne(this.prisma, 'Credential', filter);
|
|
1120
|
-
if (!doc) return null;
|
|
1219
|
+
```javascript
|
|
1220
|
+
const doc = await findOne(this.prisma, 'Credential', filter);
|
|
1221
|
+
if (!doc) return null;
|
|
1121
1222
|
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
const docs = await findMany(this.prisma, 'Credential', filter);
|
|
1223
|
+
const decryptedDoc = await this.encryptionService.decryptFields(
|
|
1224
|
+
'Credential',
|
|
1225
|
+
doc
|
|
1226
|
+
);
|
|
1227
|
+
return this._mapCredential(decryptedDoc);
|
|
1228
|
+
```
|
|
1129
1229
|
|
|
1130
|
-
|
|
1131
|
-
docs.map(doc => this.encryptionService.decryptFields('Credential', doc))
|
|
1132
|
-
);
|
|
1230
|
+
- [ ] **Fix `findManyCredentials()` method** (if exists):
|
|
1133
1231
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1232
|
+
- [ ] After findMany(), decrypt each:
|
|
1233
|
+
|
|
1234
|
+
```javascript
|
|
1235
|
+
const docs = await findMany(this.prisma, 'Credential', filter);
|
|
1236
|
+
|
|
1237
|
+
const decryptedDocs = await Promise.all(
|
|
1238
|
+
docs.map((doc) =>
|
|
1239
|
+
this.encryptionService.decryptFields('Credential', doc)
|
|
1240
|
+
)
|
|
1241
|
+
);
|
|
1242
|
+
|
|
1243
|
+
return decryptedDocs.map((doc) => this._mapCredential(doc));
|
|
1244
|
+
```
|
|
1245
|
+
|
|
1246
|
+
- [ ] Add JSDoc comments explaining encryption
|
|
1247
|
+
- [ ] Verify all credential read/write operations are covered
|
|
1248
|
+
- [ ] Run linter
|
|
1249
|
+
- [ ] Test locally with real OAuth flow
|
|
1140
1250
|
|
|
1141
1251
|
**Security Verification**:
|
|
1142
|
-
|
|
1143
|
-
-
|
|
1144
|
-
-
|
|
1145
|
-
-
|
|
1252
|
+
|
|
1253
|
+
- [ ] Create test credential with `access_token: "test_secret"`
|
|
1254
|
+
- [ ] Query database directly (bypass repository)
|
|
1255
|
+
- [ ] Verify stored value is encrypted format: `"keyId:iv:cipher:encKey"`
|
|
1256
|
+
- [ ] Verify repository returns decrypted value: `"test_secret"`
|
|
1146
1257
|
|
|
1147
1258
|
**Estimated Time**: 1.5 hours
|
|
1148
1259
|
|
|
@@ -1156,33 +1267,33 @@ expect(service.cryptor).toBe(mockCryptor); // ✅ Must pass
|
|
|
1156
1267
|
|
|
1157
1268
|
**Test Coverage Checklist**:
|
|
1158
1269
|
|
|
1159
|
-
-
|
|
1160
|
-
-
|
|
1161
|
-
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
-
|
|
1270
|
+
- [ ] Create test file with describe block
|
|
1271
|
+
- [ ] Mock DocumentDBEncryptionService
|
|
1272
|
+
- [ ] **Test Group: Encryption on Write**
|
|
1273
|
+
- [ ] Test `createIndividualUser()` encrypts hashword before insert
|
|
1274
|
+
- [ ] Test `updateIndividualUser()` encrypts hashword before update
|
|
1275
|
+
- [ ] Verify encrypted format in database (use direct query)
|
|
1276
|
+
- [ ] Verify plain text never stored
|
|
1277
|
+
- [ ] **Test Group: Decryption on Read**
|
|
1278
|
+
- [ ] Test `findIndividualUserById()` returns decrypted hashword
|
|
1279
|
+
- [ ] Test `findIndividualUserByUsername()` returns decrypted hashword
|
|
1280
|
+
- [ ] Test `findIndividualUserByEmail()` returns decrypted hashword
|
|
1281
|
+
- [ ] Verify application receives plain text
|
|
1282
|
+
- [ ] **Test Group: Stage-Based Bypass**
|
|
1283
|
+
- [ ] Test encryption bypassed in dev stage
|
|
1284
|
+
- [ ] Test encryption bypassed in test stage
|
|
1285
|
+
- [ ] Test encryption bypassed in local stage
|
|
1286
|
+
- [ ] Test encryption enabled in production stage
|
|
1287
|
+
- [ ] **Test Group: Edge Cases**
|
|
1288
|
+
- [ ] Test null hashword handling
|
|
1289
|
+
- [ ] Test undefined hashword handling
|
|
1290
|
+
- [ ] Test empty string hashword
|
|
1291
|
+
- [ ] Test already encrypted hashword (idempotent)
|
|
1292
|
+
- [ ] **Test Group: Error Handling**
|
|
1293
|
+
- [ ] Test encryption service throws error
|
|
1294
|
+
- [ ] Test decryption service throws error
|
|
1295
|
+
- [ ] Verify error propagation to use case
|
|
1296
|
+
- [ ] Run tests: `npm test user-repository-documentdb-encryption.test.js`
|
|
1186
1297
|
|
|
1187
1298
|
**Estimated Time**: 1.5 hours
|
|
1188
1299
|
|
|
@@ -1194,26 +1305,26 @@ expect(service.cryptor).toBe(mockCryptor); // ✅ Must pass
|
|
|
1194
1305
|
|
|
1195
1306
|
**Test Coverage Checklist**:
|
|
1196
1307
|
|
|
1197
|
-
-
|
|
1198
|
-
-
|
|
1199
|
-
-
|
|
1200
|
-
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
-
|
|
1308
|
+
- [ ] Create test file with describe block
|
|
1309
|
+
- [ ] Mock DocumentDBEncryptionService
|
|
1310
|
+
- [ ] Mock credential data in database (pre-encrypted)
|
|
1311
|
+
- [ ] **Test Group: Credential Decryption**
|
|
1312
|
+
- [ ] Test `_fetchCredential()` decrypts credential data
|
|
1313
|
+
- [ ] Test `_fetchCredentialsBulk()` decrypts multiple credentials
|
|
1314
|
+
- [ ] Verify nested field decryption (data.access_token)
|
|
1315
|
+
- [ ] Verify multiple field decryption (access_token, refresh_token, id_token)
|
|
1316
|
+
- [ ] **Test Group: Integration with Entities**
|
|
1317
|
+
- [ ] Test `findEntityById()` returns entity with decrypted credential
|
|
1318
|
+
- [ ] Test `findEntitiesByUserId()` returns entities with decrypted credentials
|
|
1319
|
+
- [ ] Test `findEntitiesByUserIdAndModuleName()` decrypts credentials
|
|
1320
|
+
- [ ] **Test Group: Error Handling**
|
|
1321
|
+
- [ ] Test corrupted encrypted data (decryption fails)
|
|
1322
|
+
- [ ] Test missing credential (null credential)
|
|
1323
|
+
- [ ] Verify graceful degradation
|
|
1324
|
+
- [ ] **Test Group: Performance**
|
|
1325
|
+
- [ ] Test bulk decryption of 10 credentials
|
|
1326
|
+
- [ ] Verify parallel decryption (not sequential)
|
|
1327
|
+
- [ ] Run tests: `npm test module-repository-documentdb-encryption.test.js`
|
|
1217
1328
|
|
|
1218
1329
|
**Estimated Time**: 1.5 hours
|
|
1219
1330
|
|
|
@@ -1225,69 +1336,77 @@ expect(service.cryptor).toBe(mockCryptor); // ✅ Must pass
|
|
|
1225
1336
|
|
|
1226
1337
|
**Test Coverage Checklist**:
|
|
1227
1338
|
|
|
1228
|
-
-
|
|
1229
|
-
-
|
|
1230
|
-
-
|
|
1231
|
-
-
|
|
1232
|
-
- [ ] Test encrypts access_token before insert
|
|
1233
|
-
- [ ] Test encrypts refresh_token before insert
|
|
1234
|
-
- [ ] Test encrypts id_token before insert
|
|
1235
|
-
- [ ] Test encrypts domain before insert
|
|
1236
|
-
- [ ] **Verify encrypted format in database**:
|
|
1237
|
-
```javascript
|
|
1238
|
-
// Direct database query (bypass repository)
|
|
1239
|
-
const rawDoc = await prisma.$runCommandRaw({
|
|
1240
|
-
find: 'Credential',
|
|
1241
|
-
filter: { userId: toObjectId(userId) }
|
|
1242
|
-
});
|
|
1243
|
-
const storedToken = rawDoc.cursor.firstBatch[0].data.access_token;
|
|
1339
|
+
- [ ] Create test file with describe block
|
|
1340
|
+
- [ ] Mock DocumentDBEncryptionService
|
|
1341
|
+
- [ ] Setup DocumentDB test database
|
|
1342
|
+
- [ ] **Test Group: Encryption on Upsert (INSERT)**
|
|
1244
1343
|
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
-
|
|
1250
|
-
- [ ] Test existing credential update encrypts new tokens
|
|
1251
|
-
- [ ] Test merges existing encrypted data with new encrypted data
|
|
1252
|
-
- [ ] Test updates preserve other credential fields
|
|
1253
|
-
- [ ] **Test Group: Decryption on Read**
|
|
1254
|
-
- [ ] Test `upsertCredential()` returns decrypted credential
|
|
1255
|
-
- [ ] Test `findCredential()` returns decrypted credential (if exists)
|
|
1256
|
-
- [ ] Test `_mapCredential()` receives decrypted data
|
|
1257
|
-
- [ ] **Verify plain text returned to application**:
|
|
1258
|
-
```javascript
|
|
1259
|
-
const credential = await repository.upsertCredential({
|
|
1260
|
-
userId, externalId,
|
|
1261
|
-
access_token: 'plain_secret',
|
|
1262
|
-
refresh_token: 'plain_refresh'
|
|
1263
|
-
});
|
|
1344
|
+
- [ ] Test encrypts access_token before insert
|
|
1345
|
+
- [ ] Test encrypts refresh_token before insert
|
|
1346
|
+
- [ ] Test encrypts id_token before insert
|
|
1347
|
+
- [ ] Test encrypts domain before insert
|
|
1348
|
+
- [ ] **Verify encrypted format in database**:
|
|
1264
1349
|
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1350
|
+
```javascript
|
|
1351
|
+
// Direct database query (bypass repository)
|
|
1352
|
+
const rawDoc = await prisma.$runCommandRaw({
|
|
1353
|
+
find: 'Credential',
|
|
1354
|
+
filter: { userId: toObjectId(userId) },
|
|
1355
|
+
});
|
|
1356
|
+
const storedToken = rawDoc.cursor.firstBatch[0].data.access_token;
|
|
1357
|
+
|
|
1358
|
+
// Must match encrypted format
|
|
1359
|
+
expect(storedToken).toMatch(/^[^:]+:[^:]+:[^:]+:[^:]+$/);
|
|
1360
|
+
expect(storedToken).not.toBe('plain_secret');
|
|
1361
|
+
```
|
|
1362
|
+
|
|
1363
|
+
- [ ] **Test Group: Encryption on Upsert (UPDATE)**
|
|
1364
|
+
- [ ] Test existing credential update encrypts new tokens
|
|
1365
|
+
- [ ] Test merges existing encrypted data with new encrypted data
|
|
1366
|
+
- [ ] Test updates preserve other credential fields
|
|
1367
|
+
- [ ] **Test Group: Decryption on Read**
|
|
1368
|
+
|
|
1369
|
+
- [ ] Test `upsertCredential()` returns decrypted credential
|
|
1370
|
+
- [ ] Test `findCredential()` returns decrypted credential (if exists)
|
|
1371
|
+
- [ ] Test `_mapCredential()` receives decrypted data
|
|
1372
|
+
- [ ] **Verify plain text returned to application**:
|
|
1373
|
+
|
|
1374
|
+
```javascript
|
|
1375
|
+
const credential = await repository.upsertCredential({
|
|
1376
|
+
userId,
|
|
1377
|
+
externalId,
|
|
1378
|
+
access_token: 'plain_secret',
|
|
1379
|
+
refresh_token: 'plain_refresh',
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
expect(credential.access_token).toBe('plain_secret');
|
|
1383
|
+
expect(credential.refresh_token).toBe('plain_refresh');
|
|
1384
|
+
```
|
|
1385
|
+
|
|
1386
|
+
- [ ] **Test Group: Integration Flow**
|
|
1387
|
+
- [ ] Test full flow: insert → read → verify
|
|
1388
|
+
- [ ] Test full flow: insert → update → read → verify
|
|
1389
|
+
- [ ] Test multiple credentials per user
|
|
1390
|
+
- [ ] Test credential retrieval by externalId
|
|
1391
|
+
- [ ] **Test Group: Security Validation**
|
|
1392
|
+
- [ ] Test KMS encryption in production stage
|
|
1393
|
+
- [ ] Test AES encryption when KMS unavailable
|
|
1394
|
+
- [ ] Test bypass in dev/test/local stages
|
|
1395
|
+
- [ ] Test plain text never exposed in logs
|
|
1396
|
+
- [ ] **Test Group: Error Handling**
|
|
1397
|
+
- [ ] Test encryption service throws error on insert
|
|
1398
|
+
- [ ] Test decryption service throws error on read
|
|
1399
|
+
- [ ] Test partial credential data (missing fields)
|
|
1400
|
+
- [ ] Test null values for optional fields
|
|
1401
|
+
- [ ] **Test Group: Edge Cases**
|
|
1402
|
+
- [ ] Test empty oauth data
|
|
1403
|
+
- [ ] Test very large token values (>1KB)
|
|
1404
|
+
- [ ] Test special characters in tokens
|
|
1405
|
+
- [ ] Test unicode in tokens
|
|
1406
|
+
- [ ] Run tests: `npm test credential-repository-documentdb-encryption.test.js`
|
|
1289
1407
|
|
|
1290
1408
|
**Security Test Example**:
|
|
1409
|
+
|
|
1291
1410
|
```javascript
|
|
1292
1411
|
describe('Security - Encryption Verification', () => {
|
|
1293
1412
|
it('stores access_token in encrypted format in database', async () => {
|
|
@@ -1299,25 +1418,28 @@ describe('Security - Encryption Verification', () => {
|
|
|
1299
1418
|
await credentialRepo.upsertCredential({
|
|
1300
1419
|
userId: fromObjectId(userId),
|
|
1301
1420
|
externalId,
|
|
1302
|
-
access_token: plainToken
|
|
1421
|
+
access_token: plainToken,
|
|
1303
1422
|
});
|
|
1304
1423
|
|
|
1305
1424
|
// Query database directly (bypass repository and encryption)
|
|
1306
1425
|
const rawResult = await prisma.$runCommandRaw({
|
|
1307
1426
|
find: 'Credential',
|
|
1308
|
-
filter: { userId, externalId }
|
|
1427
|
+
filter: { userId, externalId },
|
|
1309
1428
|
});
|
|
1310
1429
|
|
|
1311
1430
|
const storedCredential = rawResult.cursor.firstBatch[0];
|
|
1312
1431
|
const storedToken = storedCredential.data.access_token;
|
|
1313
1432
|
|
|
1314
1433
|
// CRITICAL: Verify encrypted format
|
|
1315
|
-
expect(storedToken).not.toBe(plainToken);
|
|
1316
|
-
expect(storedToken).toMatch(/^[^:]+:[^:]+:[^:]+:[^:]+$/);
|
|
1434
|
+
expect(storedToken).not.toBe(plainToken); // Must not be plain text
|
|
1435
|
+
expect(storedToken).toMatch(/^[^:]+:[^:]+:[^:]+:[^:]+$/); // Must be "keyId:iv:cipher:encKey"
|
|
1317
1436
|
|
|
1318
1437
|
// Verify repository returns decrypted value
|
|
1319
|
-
const retrieved = await credentialRepo.findCredential({
|
|
1320
|
-
|
|
1438
|
+
const retrieved = await credentialRepo.findCredential({
|
|
1439
|
+
userId,
|
|
1440
|
+
externalId,
|
|
1441
|
+
});
|
|
1442
|
+
expect(retrieved.access_token).toBe(plainToken); // Must be decrypted
|
|
1321
1443
|
});
|
|
1322
1444
|
});
|
|
1323
1445
|
```
|
|
@@ -1329,29 +1451,32 @@ describe('Security - Encryption Verification', () => {
|
|
|
1329
1451
|
### Phase 6: Apply to Both Locations
|
|
1330
1452
|
|
|
1331
1453
|
**Dual Location Rule**: All changes must be applied to BOTH:
|
|
1454
|
+
|
|
1332
1455
|
1. **Development**: `/Users/danielklotz/projects/lefthook/frontify--frigg/tmp/frigg/packages/core/`
|
|
1333
1456
|
2. **Runtime**: `/Users/danielklotz/projects/lefthook/frontify--frigg/backend/node_modules/@friggframework/core/`
|
|
1334
1457
|
|
|
1335
1458
|
**Files to Update in Both Locations**:
|
|
1336
1459
|
|
|
1337
|
-
-
|
|
1338
|
-
-
|
|
1339
|
-
-
|
|
1340
|
-
-
|
|
1341
|
-
-
|
|
1342
|
-
-
|
|
1343
|
-
-
|
|
1344
|
-
-
|
|
1460
|
+
- [ ] `database/documentdb-encryption-service.js` (NEW)
|
|
1461
|
+
- [ ] `database/__tests__/documentdb-encryption-service.test.js` (NEW)
|
|
1462
|
+
- [ ] `user/repositories/user-repository-documentdb.js`
|
|
1463
|
+
- [ ] `user/repositories/__tests__/user-repository-documentdb-encryption.test.js` (NEW)
|
|
1464
|
+
- [ ] `modules/repositories/module-repository-documentdb.js`
|
|
1465
|
+
- [ ] `modules/repositories/__tests__/module-repository-documentdb-encryption.test.js` (NEW)
|
|
1466
|
+
- [ ] `credential/repositories/credential-repository-documentdb.js`
|
|
1467
|
+
- [ ] `credential/repositories/__tests__/credential-repository-documentdb-encryption.test.js` (NEW)
|
|
1345
1468
|
|
|
1346
1469
|
**Verification Steps**:
|
|
1347
1470
|
|
|
1348
1471
|
For each file:
|
|
1349
|
-
|
|
1350
|
-
-
|
|
1351
|
-
-
|
|
1352
|
-
-
|
|
1472
|
+
|
|
1473
|
+
- [ ] Copy from `/tmp/frigg/` to `/backend/node_modules/@friggframework/`
|
|
1474
|
+
- [ ] Verify file checksums match
|
|
1475
|
+
- [ ] Run `diff` to confirm identical content
|
|
1476
|
+
- [ ] Check file permissions
|
|
1353
1477
|
|
|
1354
1478
|
**Script to Automate** (optional):
|
|
1479
|
+
|
|
1355
1480
|
```bash
|
|
1356
1481
|
#!/bin/bash
|
|
1357
1482
|
# sync-documentdb-encryption.sh
|
|
@@ -1388,45 +1513,55 @@ echo "🎉 All files synced successfully"
|
|
|
1388
1513
|
|
|
1389
1514
|
**Test Execution Checklist**:
|
|
1390
1515
|
|
|
1391
|
-
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1516
|
+
- [ ] **Run DocumentDB encryption service tests**:
|
|
1517
|
+
|
|
1518
|
+
```bash
|
|
1519
|
+
cd /Users/danielklotz/projects/lefthook/frontify--frigg/tmp/frigg
|
|
1520
|
+
npm test packages/core/database/__tests__/documentdb-encryption-service.test.js
|
|
1521
|
+
```
|
|
1522
|
+
|
|
1523
|
+
- [ ] Verify all tests pass
|
|
1524
|
+
- [ ] Check coverage >90%
|
|
1525
|
+
|
|
1526
|
+
- [ ] **Run User repository encryption tests**:
|
|
1527
|
+
|
|
1528
|
+
```bash
|
|
1529
|
+
npm test packages/core/user/repositories/__tests__/user-repository-documentdb-encryption.test.js
|
|
1530
|
+
```
|
|
1531
|
+
|
|
1532
|
+
- [ ] Verify all tests pass
|
|
1533
|
+
|
|
1534
|
+
- [ ] **Run Module repository encryption tests**:
|
|
1535
|
+
|
|
1536
|
+
```bash
|
|
1537
|
+
npm test packages/core/modules/repositories/__tests__/module-repository-documentdb-encryption.test.js
|
|
1538
|
+
```
|
|
1539
|
+
|
|
1540
|
+
- [ ] Verify all tests pass
|
|
1541
|
+
|
|
1542
|
+
- [ ] **Run Credential repository encryption tests** (CRITICAL):
|
|
1543
|
+
|
|
1544
|
+
```bash
|
|
1545
|
+
npm test packages/core/credential/repositories/__tests__/credential-repository-documentdb-encryption.test.js
|
|
1546
|
+
```
|
|
1547
|
+
|
|
1548
|
+
- [ ] Verify all tests pass
|
|
1549
|
+
- [ ] Verify security test passes (encrypted format verification)
|
|
1550
|
+
|
|
1551
|
+
- [ ] **Run all repository tests**:
|
|
1552
|
+
|
|
1553
|
+
```bash
|
|
1554
|
+
npm test -- --testPathPattern=documentdb
|
|
1555
|
+
```
|
|
1556
|
+
|
|
1557
|
+
- [ ] Verify no regressions
|
|
1558
|
+
|
|
1559
|
+
- [ ] **Run full test suite**:
|
|
1560
|
+
```bash
|
|
1561
|
+
npm test
|
|
1562
|
+
```
|
|
1563
|
+
- [ ] Verify all tests pass
|
|
1564
|
+
- [ ] Check for no unexpected failures
|
|
1430
1565
|
|
|
1431
1566
|
---
|
|
1432
1567
|
|
|
@@ -1434,115 +1569,124 @@ echo "🎉 All files synced successfully"
|
|
|
1434
1569
|
|
|
1435
1570
|
**Local Environment Setup**:
|
|
1436
1571
|
|
|
1437
|
-
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1572
|
+
- [ ] Start MongoDB (DocumentDB simulation):
|
|
1573
|
+
|
|
1574
|
+
```bash
|
|
1575
|
+
cd /Users/danielklotz/projects/lefthook/frontify--frigg/backend
|
|
1576
|
+
npm run docker:start
|
|
1577
|
+
```
|
|
1578
|
+
|
|
1579
|
+
- [ ] Verify MongoDB is running:
|
|
1580
|
+
|
|
1581
|
+
```bash
|
|
1582
|
+
docker ps | grep mongo
|
|
1583
|
+
```
|
|
1584
|
+
|
|
1585
|
+
- [ ] Set environment variables for encryption:
|
|
1586
|
+
|
|
1587
|
+
```bash
|
|
1588
|
+
export STAGE=production
|
|
1589
|
+
export AES_KEY_ID=local-test-key
|
|
1590
|
+
export AES_KEY=01234567890123456789012345678901 # 32 chars
|
|
1591
|
+
```
|
|
1592
|
+
|
|
1593
|
+
- [ ] Start backend:
|
|
1594
|
+
```bash
|
|
1595
|
+
cd /Users/danielklotz/projects/lefthook/frontify--frigg/backend
|
|
1596
|
+
npm run frigg:start
|
|
1597
|
+
```
|
|
1460
1598
|
|
|
1461
1599
|
**Manual Test: Credential Creation**
|
|
1462
1600
|
|
|
1463
|
-
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1601
|
+
- [ ] Create user and get token:
|
|
1602
|
+
|
|
1603
|
+
```bash
|
|
1604
|
+
curl -X POST http://localhost:3000/user/create \
|
|
1605
|
+
-H "Content-Type: application/json" \
|
|
1606
|
+
-d '{"username":"test@test.com","password":"test"}' \
|
|
1607
|
+
-o /tmp/token.json
|
|
1469
1608
|
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1609
|
+
TOKEN=$(jq -r '.token' /tmp/token.json)
|
|
1610
|
+
echo "Token: $TOKEN"
|
|
1611
|
+
```
|
|
1473
1612
|
|
|
1474
|
-
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1613
|
+
- [ ] Create OAuth credential (if endpoint exists, else use Asana OAuth flow):
|
|
1614
|
+
```bash
|
|
1615
|
+
# Trigger OAuth flow through application
|
|
1616
|
+
# Then verify credential was created encrypted
|
|
1617
|
+
```
|
|
1479
1618
|
|
|
1480
1619
|
**Manual Test: Database Verification**
|
|
1481
1620
|
|
|
1482
|
-
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1621
|
+
- [ ] Connect to MongoDB:
|
|
1622
|
+
|
|
1623
|
+
```bash
|
|
1624
|
+
docker exec -it $(docker ps -q -f name=mongo) mongosh
|
|
1625
|
+
```
|
|
1626
|
+
|
|
1627
|
+
- [ ] Query credential:
|
|
1628
|
+
|
|
1629
|
+
```javascript
|
|
1630
|
+
use frigg
|
|
1631
|
+
db.Credential.findOne()
|
|
1632
|
+
```
|
|
1486
1633
|
|
|
1487
|
-
-
|
|
1488
|
-
```javascript
|
|
1489
|
-
use frigg
|
|
1490
|
-
db.Credential.findOne()
|
|
1491
|
-
```
|
|
1634
|
+
- [ ] **CRITICAL VERIFICATION**:
|
|
1492
1635
|
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
print("access_token:", cred.data.access_token);
|
|
1636
|
+
```javascript
|
|
1637
|
+
// Check data.access_token format
|
|
1638
|
+
const cred = db.Credential.findOne({ externalId: 'google-user-123' });
|
|
1639
|
+
print('access_token:', cred.data.access_token);
|
|
1498
1640
|
|
|
1499
|
-
|
|
1500
|
-
|
|
1641
|
+
// Expected format: "keyId:iv:cipher:encKey"
|
|
1642
|
+
// Example: "aes-key-1:1234567890abcdef:a1b2c3d4e5f6...:9876543210fedcba"
|
|
1501
1643
|
|
|
1502
|
-
|
|
1503
|
-
|
|
1644
|
+
// MUST NOT be plain text like "ya29.a0AfH6SMCX..."
|
|
1645
|
+
```
|
|
1504
1646
|
|
|
1505
|
-
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1647
|
+
- [ ] Verify encrypted format:
|
|
1648
|
+
```javascript
|
|
1649
|
+
// Should have 4+ colon-separated parts
|
|
1650
|
+
const parts = cred.data.access_token.split(':');
|
|
1651
|
+
print('Parts count:', parts.length); // Should be >= 4
|
|
1652
|
+
```
|
|
1511
1653
|
|
|
1512
1654
|
**Manual Test: API Usage**
|
|
1513
1655
|
|
|
1514
|
-
-
|
|
1515
|
-
```bash
|
|
1516
|
-
# Make API request that uses the credential
|
|
1517
|
-
# Example: Fetch Asana user info
|
|
1518
|
-
curl -X GET http://localhost:3000/api/asana/me \
|
|
1519
|
-
-H "Authorization: Bearer $TOKEN"
|
|
1520
|
-
```
|
|
1656
|
+
- [ ] Use credential through API:
|
|
1521
1657
|
|
|
1522
|
-
|
|
1658
|
+
```bash
|
|
1659
|
+
# Make API request that uses the credential
|
|
1660
|
+
# Example: Fetch Asana user info
|
|
1661
|
+
curl -X GET http://localhost:3000/api/asana/me \
|
|
1662
|
+
-H "Authorization: Bearer $TOKEN"
|
|
1663
|
+
```
|
|
1664
|
+
|
|
1665
|
+
- [ ] Verify API call succeeds (credential was decrypted correctly)
|
|
1523
1666
|
|
|
1524
1667
|
**Manual Test: Stage Bypass**
|
|
1525
1668
|
|
|
1526
|
-
-
|
|
1669
|
+
- [ ] Stop backend
|
|
1527
1670
|
|
|
1528
|
-
-
|
|
1529
|
-
```bash
|
|
1530
|
-
export STAGE=dev
|
|
1531
|
-
unset AES_KEY_ID
|
|
1532
|
-
unset AES_KEY
|
|
1533
|
-
```
|
|
1671
|
+
- [ ] Change to dev stage:
|
|
1534
1672
|
|
|
1535
|
-
|
|
1673
|
+
```bash
|
|
1674
|
+
export STAGE=dev
|
|
1675
|
+
unset AES_KEY_ID
|
|
1676
|
+
unset AES_KEY
|
|
1677
|
+
```
|
|
1536
1678
|
|
|
1537
|
-
-
|
|
1679
|
+
- [ ] Start backend
|
|
1538
1680
|
|
|
1539
|
-
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1681
|
+
- [ ] Create credential
|
|
1682
|
+
|
|
1683
|
+
- [ ] Verify credential stored as plain text (bypass worked):
|
|
1684
|
+
```javascript
|
|
1685
|
+
// In mongosh:
|
|
1686
|
+
const devCred = db.Credential.findOne({ userId: ObjectId('...') });
|
|
1687
|
+
print('access_token:', devCred.data.access_token);
|
|
1688
|
+
// Should be plain text (not encrypted) in dev stage
|
|
1689
|
+
```
|
|
1546
1690
|
|
|
1547
1691
|
---
|
|
1548
1692
|
|
|
@@ -1550,33 +1694,34 @@ echo "🎉 All files synced successfully"
|
|
|
1550
1694
|
|
|
1551
1695
|
**OAuth Flow Testing**:
|
|
1552
1696
|
|
|
1553
|
-
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1697
|
+
- [ ] **Asana OAuth Flow**:
|
|
1698
|
+
|
|
1699
|
+
- [ ] Start OAuth flow via Asana integration
|
|
1700
|
+
- [ ] Complete OAuth authorization
|
|
1701
|
+
- [ ] Verify credential created in database
|
|
1702
|
+
- [ ] Check credential is encrypted in database
|
|
1703
|
+
- [ ] Verify Asana API calls work (credential decrypted)
|
|
1559
1704
|
|
|
1560
|
-
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1705
|
+
- [ ] **Frontify OAuth Flow**:
|
|
1706
|
+
- [ ] Start OAuth flow via Frontify integration
|
|
1707
|
+
- [ ] Complete OAuth authorization
|
|
1708
|
+
- [ ] Verify credential created in database
|
|
1709
|
+
- [ ] Check credential is encrypted in database
|
|
1710
|
+
- [ ] Verify Frontify API calls work
|
|
1566
1711
|
|
|
1567
1712
|
**Credential Refresh Testing**:
|
|
1568
1713
|
|
|
1569
|
-
-
|
|
1570
|
-
-
|
|
1571
|
-
-
|
|
1572
|
-
-
|
|
1714
|
+
- [ ] Trigger token refresh (if implemented)
|
|
1715
|
+
- [ ] Verify new tokens are encrypted
|
|
1716
|
+
- [ ] Verify old tokens are overwritten (not duplicated)
|
|
1717
|
+
- [ ] Verify refresh token itself is encrypted
|
|
1573
1718
|
|
|
1574
1719
|
**Multi-User Testing**:
|
|
1575
1720
|
|
|
1576
|
-
-
|
|
1577
|
-
-
|
|
1578
|
-
-
|
|
1579
|
-
-
|
|
1721
|
+
- [ ] Create credentials for 3 different users
|
|
1722
|
+
- [ ] Verify each credential is independently encrypted
|
|
1723
|
+
- [ ] Verify users can only access their own credentials
|
|
1724
|
+
- [ ] Check for no credential leakage between users
|
|
1580
1725
|
|
|
1581
1726
|
---
|
|
1582
1727
|
|
|
@@ -1584,29 +1729,31 @@ echo "🎉 All files synced successfully"
|
|
|
1584
1729
|
|
|
1585
1730
|
**Encryption Performance**:
|
|
1586
1731
|
|
|
1587
|
-
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1732
|
+
- [ ] Measure encryption time for single credential:
|
|
1733
|
+
|
|
1734
|
+
```javascript
|
|
1735
|
+
const start = Date.now();
|
|
1736
|
+
const encrypted = await service.encryptFields('Credential', credential);
|
|
1737
|
+
const encryptTime = Date.now() - start;
|
|
1738
|
+
console.log(`Encryption time: ${encryptTime}ms`);
|
|
1739
|
+
// Should be < 50ms for KMS, < 10ms for AES
|
|
1740
|
+
```
|
|
1595
1741
|
|
|
1596
|
-
-
|
|
1742
|
+
- [ ] Measure decryption time for single credential
|
|
1597
1743
|
|
|
1598
1744
|
**Bulk Operations**:
|
|
1599
1745
|
|
|
1600
|
-
-
|
|
1601
|
-
```javascript
|
|
1602
|
-
const start = Date.now();
|
|
1603
|
-
const entities = await moduleRepo.findEntitiesByUserId(userId);
|
|
1604
|
-
const bulkTime = Date.now() - start;
|
|
1605
|
-
console.log(`Bulk retrieval time: ${bulkTime}ms`);
|
|
1606
|
-
// Should be reasonable (< 500ms for 10 credentials)
|
|
1607
|
-
```
|
|
1746
|
+
- [ ] Test bulk credential retrieval (10 credentials):
|
|
1608
1747
|
|
|
1609
|
-
|
|
1748
|
+
```javascript
|
|
1749
|
+
const start = Date.now();
|
|
1750
|
+
const entities = await moduleRepo.findEntitiesByUserId(userId);
|
|
1751
|
+
const bulkTime = Date.now() - start;
|
|
1752
|
+
console.log(`Bulk retrieval time: ${bulkTime}ms`);
|
|
1753
|
+
// Should be reasonable (< 500ms for 10 credentials)
|
|
1754
|
+
```
|
|
1755
|
+
|
|
1756
|
+
- [ ] Verify parallel decryption is used (not sequential)
|
|
1610
1757
|
|
|
1611
1758
|
---
|
|
1612
1759
|
|
|
@@ -1614,32 +1761,32 @@ echo "🎉 All files synced successfully"
|
|
|
1614
1761
|
|
|
1615
1762
|
**Encryption Format Verification**:
|
|
1616
1763
|
|
|
1617
|
-
-
|
|
1618
|
-
-
|
|
1619
|
-
-
|
|
1620
|
-
-
|
|
1621
|
-
-
|
|
1764
|
+
- [ ] Create credential with known value
|
|
1765
|
+
- [ ] Query database directly
|
|
1766
|
+
- [ ] Verify format matches: `keyId:iv:cipher:encKey`
|
|
1767
|
+
- [ ] Verify at least 4 colon-separated parts
|
|
1768
|
+
- [ ] Verify base64-like characters in each part
|
|
1622
1769
|
|
|
1623
1770
|
**Decryption Verification**:
|
|
1624
1771
|
|
|
1625
|
-
-
|
|
1626
|
-
-
|
|
1627
|
-
-
|
|
1628
|
-
-
|
|
1772
|
+
- [ ] Create credential with known value
|
|
1773
|
+
- [ ] Retrieve via repository
|
|
1774
|
+
- [ ] Verify decrypted value matches original
|
|
1775
|
+
- [ ] Verify no corruption or truncation
|
|
1629
1776
|
|
|
1630
1777
|
**Negative Tests**:
|
|
1631
1778
|
|
|
1632
|
-
-
|
|
1633
|
-
-
|
|
1634
|
-
-
|
|
1635
|
-
-
|
|
1779
|
+
- [ ] Manually corrupt encrypted value in database
|
|
1780
|
+
- [ ] Attempt to retrieve credential
|
|
1781
|
+
- [ ] Verify graceful handling (field set to null, logged error)
|
|
1782
|
+
- [ ] Verify application doesn't crash
|
|
1636
1783
|
|
|
1637
1784
|
**Key Rotation Simulation** (if time permits):
|
|
1638
1785
|
|
|
1639
|
-
-
|
|
1640
|
-
-
|
|
1641
|
-
-
|
|
1642
|
-
-
|
|
1786
|
+
- [ ] Create credential with key1
|
|
1787
|
+
- [ ] Rotate to key2 (change AES_KEY_ID)
|
|
1788
|
+
- [ ] Verify old credentials still decrypt (backward compatible)
|
|
1789
|
+
- [ ] Verify new credentials use key2
|
|
1643
1790
|
|
|
1644
1791
|
**Estimated Time**: 1.5 hours
|
|
1645
1792
|
|
|
@@ -1653,66 +1800,70 @@ echo "🎉 All files synced successfully"
|
|
|
1653
1800
|
|
|
1654
1801
|
**Sections to Add**:
|
|
1655
1802
|
|
|
1656
|
-
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1803
|
+
- [ ] **Add "DocumentDB Encryption" section** (after "How It Works"):
|
|
1804
|
+
|
|
1805
|
+
```markdown
|
|
1806
|
+
## DocumentDB Encryption
|
|
1807
|
+
|
|
1808
|
+
### Why DocumentDB Needs Manual Encryption
|
|
1809
|
+
|
|
1810
|
+
DocumentDB repositories use `$runCommandRaw()` for MongoDB protocol compatibility,
|
|
1811
|
+
which bypasses Prisma Client Extensions. This means the automatic encryption
|
|
1812
|
+
extension does not apply.
|
|
1659
1813
|
|
|
1660
|
-
|
|
1814
|
+
### DocumentDBEncryptionService
|
|
1661
1815
|
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
extension does not apply.
|
|
1816
|
+
For DocumentDB repositories, use `DocumentDBEncryptionService` to manually
|
|
1817
|
+
encrypt/decrypt documents before/after database operations.
|
|
1665
1818
|
|
|
1666
|
-
|
|
1819
|
+
#### Usage Example
|
|
1667
1820
|
|
|
1668
|
-
|
|
1669
|
-
|
|
1821
|
+
\`\`\`javascript
|
|
1822
|
+
const { DocumentDBEncryptionService } = require('../documentdb-encryption-service');
|
|
1823
|
+
const { insertOne, findOne } = require('../documentdb-utils');
|
|
1670
1824
|
|
|
1671
|
-
|
|
1825
|
+
class MyRepositoryDocumentDB {
|
|
1826
|
+
constructor() {
|
|
1827
|
+
this.encryptionService = new DocumentDBEncryptionService();
|
|
1828
|
+
}
|
|
1672
1829
|
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1830
|
+
async create(data) {
|
|
1831
|
+
// Encrypt before write
|
|
1832
|
+
const encrypted = await this.encryptionService.encryptFields('ModelName', data);
|
|
1833
|
+
const id = await insertOne(this.prisma, 'CollectionName', encrypted);
|
|
1676
1834
|
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
}
|
|
1835
|
+
// Decrypt after read
|
|
1836
|
+
const doc = await findOne(this.prisma, 'CollectionName', { _id: id });
|
|
1837
|
+
const decrypted = await this.encryptionService.decryptFields('ModelName', doc);
|
|
1681
1838
|
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
const encrypted = await this.encryptionService.encryptFields('ModelName', data);
|
|
1685
|
-
const id = await insertOne(this.prisma, 'CollectionName', encrypted);
|
|
1839
|
+
return decrypted;
|
|
1840
|
+
}
|
|
1686
1841
|
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
const decrypted = await this.encryptionService.decryptFields('ModelName', doc);
|
|
1842
|
+
}
|
|
1843
|
+
\`\`\`
|
|
1690
1844
|
|
|
1691
|
-
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
\`\`\`
|
|
1845
|
+
#### Configuration
|
|
1695
1846
|
|
|
1696
|
-
|
|
1847
|
+
Uses the same environment variables and Cryptor as the Prisma Extension:
|
|
1697
1848
|
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
- `AES_KEY_ID` + `AES_KEY`: AES encryption (fallback)
|
|
1849
|
+
- `STAGE`: Bypasses encryption for dev/test/local
|
|
1850
|
+
- `KMS_KEY_ARN`: AWS KMS encryption (production)
|
|
1851
|
+
- `AES_KEY_ID` + `AES_KEY`: AES encryption (fallback)
|
|
1702
1852
|
|
|
1703
|
-
|
|
1853
|
+
#### Implementation Details
|
|
1704
1854
|
|
|
1705
|
-
|
|
1706
|
-
|
|
1855
|
+
See: [documentdb-encryption-service.md](./documentdb-encryption-service.md)
|
|
1856
|
+
```
|
|
1707
1857
|
|
|
1708
|
-
-
|
|
1709
|
-
```markdown
|
|
1710
|
-
After adding fields to `encryption-schema-registry.js`:
|
|
1858
|
+
- [ ] **Update "Adding Encrypted Fields" section**:
|
|
1711
1859
|
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1860
|
+
```markdown
|
|
1861
|
+
After adding fields to `encryption-schema-registry.js`:
|
|
1862
|
+
|
|
1863
|
+
1. **For MongoDB/PostgreSQL**: No code changes needed (automatic)
|
|
1864
|
+
2. **For DocumentDB**: Encryption is automatic via DocumentDBEncryptionService
|
|
1865
|
+
(service reads from same registry)
|
|
1866
|
+
```
|
|
1716
1867
|
|
|
1717
1868
|
---
|
|
1718
1869
|
|
|
@@ -1720,60 +1871,60 @@ echo "🎉 All files synced successfully"
|
|
|
1720
1871
|
|
|
1721
1872
|
**UserRepositoryDocumentDB**:
|
|
1722
1873
|
|
|
1723
|
-
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1874
|
+
- [ ] Add class-level JSDoc:
|
|
1875
|
+
```javascript
|
|
1876
|
+
/**
|
|
1877
|
+
* User repository for DocumentDB.
|
|
1878
|
+
* Uses DocumentDBEncryptionService for field-level encryption.
|
|
1879
|
+
*
|
|
1880
|
+
* Encrypted fields: User.hashword
|
|
1881
|
+
*
|
|
1882
|
+
* @see DocumentDBEncryptionService
|
|
1883
|
+
* @see encryption-schema-registry.js
|
|
1884
|
+
*/
|
|
1885
|
+
class UserRepositoryDocumentDB extends UserRepositoryInterface {
|
|
1886
|
+
```
|
|
1736
1887
|
|
|
1737
1888
|
**ModuleRepositoryDocumentDB**:
|
|
1738
1889
|
|
|
1739
|
-
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1890
|
+
- [ ] Add class-level JSDoc:
|
|
1891
|
+
```javascript
|
|
1892
|
+
/**
|
|
1893
|
+
* Module/Entity repository for DocumentDB.
|
|
1894
|
+
* Uses DocumentDBEncryptionService for credential decryption.
|
|
1895
|
+
*
|
|
1896
|
+
* Encrypted fields: Credential.data.*
|
|
1897
|
+
*
|
|
1898
|
+
* Note: This repository only reads credentials. CredentialRepository
|
|
1899
|
+
* handles credential creation/updates with encryption.
|
|
1900
|
+
*
|
|
1901
|
+
* @see DocumentDBEncryptionService
|
|
1902
|
+
* @see CredentialRepositoryDocumentDB
|
|
1903
|
+
*/
|
|
1904
|
+
class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
|
|
1905
|
+
```
|
|
1755
1906
|
|
|
1756
1907
|
**CredentialRepositoryDocumentDB**:
|
|
1757
1908
|
|
|
1758
|
-
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1909
|
+
- [ ] Add class-level JSDoc:
|
|
1910
|
+
```javascript
|
|
1911
|
+
/**
|
|
1912
|
+
* Credential repository for DocumentDB.
|
|
1913
|
+
* Uses DocumentDBEncryptionService for field-level encryption.
|
|
1914
|
+
*
|
|
1915
|
+
* Encrypted fields:
|
|
1916
|
+
* - Credential.data.access_token
|
|
1917
|
+
* - Credential.data.refresh_token
|
|
1918
|
+
* - Credential.data.id_token
|
|
1919
|
+
* - Credential.data.domain
|
|
1920
|
+
*
|
|
1921
|
+
* SECURITY CRITICAL: All OAuth credentials must be encrypted at rest.
|
|
1922
|
+
*
|
|
1923
|
+
* @see DocumentDBEncryptionService
|
|
1924
|
+
* @see encryption-schema-registry.js
|
|
1925
|
+
*/
|
|
1926
|
+
class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
|
|
1927
|
+
```
|
|
1777
1928
|
|
|
1778
1929
|
**Estimated Time**: 30 minutes
|
|
1779
1930
|
|
|
@@ -1781,17 +1932,17 @@ echo "🎉 All files synced successfully"
|
|
|
1781
1932
|
|
|
1782
1933
|
## Total Implementation Time Estimate
|
|
1783
1934
|
|
|
1784
|
-
| Phase
|
|
1785
|
-
|
|
1786
|
-
| Phase 1
|
|
1787
|
-
| Phase 2
|
|
1788
|
-
| Phase 3
|
|
1789
|
-
| Phase 4
|
|
1790
|
-
| Phase 5
|
|
1791
|
-
| Phase 6
|
|
1792
|
-
| Phase 7
|
|
1793
|
-
| Phase 8
|
|
1794
|
-
| **Total** |
|
|
1935
|
+
| Phase | Description | Time |
|
|
1936
|
+
| --------- | ------------------------------------------ | ------------- |
|
|
1937
|
+
| Phase 1 | Create DocumentDBEncryptionService + tests | 2-3 hours |
|
|
1938
|
+
| Phase 2 | Refactor UserRepositoryDocumentDB | 1 hour |
|
|
1939
|
+
| Phase 3 | Refactor ModuleRepositoryDocumentDB | 1 hour |
|
|
1940
|
+
| Phase 4 | Fix CredentialRepositoryDocumentDB | 1.5 hours |
|
|
1941
|
+
| Phase 5 | Add comprehensive tests (3 repos) | 5 hours |
|
|
1942
|
+
| Phase 6 | Apply to both locations | 30 minutes |
|
|
1943
|
+
| Phase 7 | Validation and integration testing | 1.5 hours |
|
|
1944
|
+
| Phase 8 | Documentation updates | 30 minutes |
|
|
1945
|
+
| **Total** | | **~13 hours** |
|
|
1795
1946
|
|
|
1796
1947
|
---
|
|
1797
1948
|
|
|
@@ -1810,21 +1961,24 @@ class CredentialRepositoryDocumentDB {
|
|
|
1810
1961
|
|
|
1811
1962
|
async upsertCredential(credentialDetails) {
|
|
1812
1963
|
const { identifiers, details } = credentialDetails;
|
|
1813
|
-
const { user, userId, authIsValid, externalId, ...oauthData } =
|
|
1964
|
+
const { user, userId, authIsValid, externalId, ...oauthData } =
|
|
1965
|
+
details || {};
|
|
1814
1966
|
|
|
1815
1967
|
// ❌ oauthData contains PLAIN TEXT tokens
|
|
1816
1968
|
const document = {
|
|
1817
1969
|
userId: toObjectId(userId || user),
|
|
1818
1970
|
externalId,
|
|
1819
|
-
data: oauthData,
|
|
1971
|
+
data: oauthData, // ❌ { access_token: "plain_secret", ... }
|
|
1820
1972
|
createdAt: new Date(),
|
|
1821
|
-
updatedAt: new Date()
|
|
1973
|
+
updatedAt: new Date(),
|
|
1822
1974
|
};
|
|
1823
1975
|
|
|
1824
1976
|
// ❌ STORED AS PLAIN TEXT
|
|
1825
1977
|
const insertedId = await insertOne(this.prisma, 'Credential', document);
|
|
1826
1978
|
|
|
1827
|
-
const created = await findOne(this.prisma, 'Credential', {
|
|
1979
|
+
const created = await findOne(this.prisma, 'Credential', {
|
|
1980
|
+
_id: insertedId,
|
|
1981
|
+
});
|
|
1828
1982
|
// ❌ Returns encrypted string (if previously encrypted) or plain text
|
|
1829
1983
|
return this._mapCredential(created);
|
|
1830
1984
|
}
|
|
@@ -1834,7 +1988,9 @@ class CredentialRepositoryDocumentDB {
|
|
|
1834
1988
|
**AFTER (Secure - Encrypted Storage)**:
|
|
1835
1989
|
|
|
1836
1990
|
```javascript
|
|
1837
|
-
const {
|
|
1991
|
+
const {
|
|
1992
|
+
DocumentDBEncryptionService,
|
|
1993
|
+
} = require('../database/documentdb-encryption-service');
|
|
1838
1994
|
|
|
1839
1995
|
class CredentialRepositoryDocumentDB {
|
|
1840
1996
|
constructor() {
|
|
@@ -1845,15 +2001,16 @@ class CredentialRepositoryDocumentDB {
|
|
|
1845
2001
|
|
|
1846
2002
|
async upsertCredential(credentialDetails) {
|
|
1847
2003
|
const { identifiers, details } = credentialDetails;
|
|
1848
|
-
const { user, userId, authIsValid, externalId, ...oauthData } =
|
|
2004
|
+
const { user, userId, authIsValid, externalId, ...oauthData } =
|
|
2005
|
+
details || {};
|
|
1849
2006
|
|
|
1850
2007
|
// Build plain text document
|
|
1851
2008
|
const plainDocument = {
|
|
1852
2009
|
userId: toObjectId(userId || user),
|
|
1853
2010
|
externalId,
|
|
1854
|
-
data: oauthData,
|
|
2011
|
+
data: oauthData, // Still plain text: { access_token: "plain_secret", ... }
|
|
1855
2012
|
createdAt: new Date(),
|
|
1856
|
-
updatedAt: new Date()
|
|
2013
|
+
updatedAt: new Date(),
|
|
1857
2014
|
};
|
|
1858
2015
|
|
|
1859
2016
|
// ✅ ENCRYPT before storing
|
|
@@ -1864,9 +2021,15 @@ class CredentialRepositoryDocumentDB {
|
|
|
1864
2021
|
// encryptedDocument.data = { access_token: "keyId:iv:cipher:encKey", ... }
|
|
1865
2022
|
|
|
1866
2023
|
// ✅ STORED AS ENCRYPTED
|
|
1867
|
-
const insertedId = await insertOne(
|
|
2024
|
+
const insertedId = await insertOne(
|
|
2025
|
+
this.prisma,
|
|
2026
|
+
'Credential',
|
|
2027
|
+
encryptedDocument
|
|
2028
|
+
);
|
|
1868
2029
|
|
|
1869
|
-
const created = await findOne(this.prisma, 'Credential', {
|
|
2030
|
+
const created = await findOne(this.prisma, 'Credential', {
|
|
2031
|
+
_id: insertedId,
|
|
2032
|
+
});
|
|
1870
2033
|
|
|
1871
2034
|
// ✅ DECRYPT before returning
|
|
1872
2035
|
const decryptedCredential = await this.encryptionService.decryptFields(
|
|
@@ -1892,19 +2055,25 @@ class UserRepositoryDocumentDB {
|
|
|
1892
2055
|
const document = {
|
|
1893
2056
|
type: 'INDIVIDUAL',
|
|
1894
2057
|
username: params.username,
|
|
1895
|
-
hashword: await bcrypt.hash(params.hashword, 10),
|
|
1896
|
-
createdAt: new Date()
|
|
2058
|
+
hashword: await bcrypt.hash(params.hashword, 10), // Bcrypt hash
|
|
2059
|
+
createdAt: new Date(),
|
|
1897
2060
|
};
|
|
1898
2061
|
|
|
1899
2062
|
// Encrypt bcrypt hash before storage
|
|
1900
|
-
const encrypted = await this.encryptionService.encryptFields(
|
|
2063
|
+
const encrypted = await this.encryptionService.encryptFields(
|
|
2064
|
+
'User',
|
|
2065
|
+
document
|
|
2066
|
+
);
|
|
1901
2067
|
// encrypted.hashword = "keyId:iv:cipher:encKey"
|
|
1902
2068
|
|
|
1903
2069
|
const id = await insertOne(this.prisma, 'User', encrypted);
|
|
1904
2070
|
const created = await findOne(this.prisma, 'User', { _id: id });
|
|
1905
2071
|
|
|
1906
2072
|
// Decrypt before returning
|
|
1907
|
-
const decrypted = await this.encryptionService.decryptFields(
|
|
2073
|
+
const decrypted = await this.encryptionService.decryptFields(
|
|
2074
|
+
'User',
|
|
2075
|
+
created
|
|
2076
|
+
);
|
|
1908
2077
|
// decrypted.hashword = "$2b$10$..." (bcrypt hash)
|
|
1909
2078
|
|
|
1910
2079
|
return this._mapUser(decrypted);
|
|
@@ -1912,23 +2081,26 @@ class UserRepositoryDocumentDB {
|
|
|
1912
2081
|
}
|
|
1913
2082
|
```
|
|
1914
2083
|
|
|
1915
|
-
**Pattern 2: Nested Fields Encryption (Credential.data
|
|
2084
|
+
**Pattern 2: Nested Fields Encryption (Credential.data.\*)**:
|
|
1916
2085
|
|
|
1917
2086
|
```javascript
|
|
1918
2087
|
class CredentialRepositoryDocumentDB {
|
|
1919
2088
|
async upsertCredential(details) {
|
|
1920
2089
|
const document = {
|
|
1921
2090
|
data: {
|
|
1922
|
-
access_token:
|
|
1923
|
-
refresh_token:
|
|
1924
|
-
id_token:
|
|
1925
|
-
expires_at: 1234567890,
|
|
1926
|
-
scope:
|
|
1927
|
-
}
|
|
2091
|
+
access_token: 'ya29.actual_token',
|
|
2092
|
+
refresh_token: '1//0refresh',
|
|
2093
|
+
id_token: 'eyJhbGci...',
|
|
2094
|
+
expires_at: 1234567890, // Not encrypted (not in registry)
|
|
2095
|
+
scope: 'openid profile', // Not encrypted
|
|
2096
|
+
},
|
|
1928
2097
|
};
|
|
1929
2098
|
|
|
1930
2099
|
// Encrypts only fields defined in encryption-schema-registry.js
|
|
1931
|
-
const encrypted = await this.encryptionService.encryptFields(
|
|
2100
|
+
const encrypted = await this.encryptionService.encryptFields(
|
|
2101
|
+
'Credential',
|
|
2102
|
+
document
|
|
2103
|
+
);
|
|
1932
2104
|
// encrypted.data = {
|
|
1933
2105
|
// access_token: "keyId:iv:cipher:encKey", ← ENCRYPTED
|
|
1934
2106
|
// refresh_token: "keyId:iv:cipher:encKey", ← ENCRYPTED
|
|
@@ -1945,11 +2117,13 @@ class CredentialRepositoryDocumentDB {
|
|
|
1945
2117
|
```javascript
|
|
1946
2118
|
class ModuleRepositoryDocumentDB {
|
|
1947
2119
|
async _fetchCredentialsBulk(credentialIds) {
|
|
1948
|
-
const objectIds = credentialIds
|
|
2120
|
+
const objectIds = credentialIds
|
|
2121
|
+
.map((id) => toObjectId(id))
|
|
2122
|
+
.filter(Boolean);
|
|
1949
2123
|
|
|
1950
2124
|
// Fetch all credentials (encrypted)
|
|
1951
2125
|
const rawCredentials = await findMany(this.prisma, 'Credential', {
|
|
1952
|
-
_id: { $in: objectIds }
|
|
2126
|
+
_id: { $in: objectIds },
|
|
1953
2127
|
});
|
|
1954
2128
|
|
|
1955
2129
|
// Decrypt in parallel
|
|
@@ -1973,34 +2147,37 @@ class ModuleRepositoryDocumentDB {
|
|
|
1973
2147
|
```javascript
|
|
1974
2148
|
// 1. User completes OAuth flow, application receives tokens
|
|
1975
2149
|
const oauthTokens = {
|
|
1976
|
-
access_token:
|
|
1977
|
-
refresh_token:
|
|
1978
|
-
id_token:
|
|
2150
|
+
access_token: 'ya29.a0AfH6SMCXyz...',
|
|
2151
|
+
refresh_token: '1//0gFz6TRvwUm...',
|
|
2152
|
+
id_token: 'eyJhbGciOiJSUzI1...',
|
|
1979
2153
|
expires_in: 3600,
|
|
1980
|
-
token_type:
|
|
2154
|
+
token_type: 'Bearer',
|
|
1981
2155
|
};
|
|
1982
2156
|
|
|
1983
2157
|
// 2. Use case calls repository
|
|
1984
2158
|
const credential = await credentialRepository.upsertCredential({
|
|
1985
|
-
identifiers: { userId:
|
|
1986
|
-
details: oauthTokens
|
|
2159
|
+
identifiers: { userId: 'user123', externalId: 'google-user-456' },
|
|
2160
|
+
details: oauthTokens,
|
|
1987
2161
|
});
|
|
1988
2162
|
|
|
1989
2163
|
// 3. Inside repository: Build plain document
|
|
1990
2164
|
const plainDocument = {
|
|
1991
|
-
userId: toObjectId(
|
|
1992
|
-
externalId:
|
|
2165
|
+
userId: toObjectId('user123'),
|
|
2166
|
+
externalId: 'google-user-456',
|
|
1993
2167
|
data: {
|
|
1994
|
-
access_token:
|
|
1995
|
-
refresh_token:
|
|
1996
|
-
id_token:
|
|
2168
|
+
access_token: 'ya29.a0AfH6SMCXyz...',
|
|
2169
|
+
refresh_token: '1//0gFz6TRvwUm...',
|
|
2170
|
+
id_token: 'eyJhbGciOiJSUzI1...',
|
|
1997
2171
|
expires_in: 3600,
|
|
1998
|
-
token_type:
|
|
1999
|
-
}
|
|
2172
|
+
token_type: 'Bearer',
|
|
2173
|
+
},
|
|
2000
2174
|
};
|
|
2001
2175
|
|
|
2002
2176
|
// 4. DocumentDBEncryptionService encrypts sensitive fields
|
|
2003
|
-
const encryptedDocument = await this.encryptionService.encryptFields(
|
|
2177
|
+
const encryptedDocument = await this.encryptionService.encryptFields(
|
|
2178
|
+
'Credential',
|
|
2179
|
+
plainDocument
|
|
2180
|
+
);
|
|
2004
2181
|
// Result:
|
|
2005
2182
|
// {
|
|
2006
2183
|
// userId: ObjectId("..."),
|
|
@@ -2018,11 +2195,16 @@ const encryptedDocument = await this.encryptionService.encryptFields('Credential
|
|
|
2018
2195
|
await insertOne(this.prisma, 'Credential', encryptedDocument);
|
|
2019
2196
|
|
|
2020
2197
|
// 6. Read back from DocumentDB
|
|
2021
|
-
const rawDocument = await findOne(this.prisma, 'Credential', {
|
|
2198
|
+
const rawDocument = await findOne(this.prisma, 'Credential', {
|
|
2199
|
+
userId: objectId,
|
|
2200
|
+
});
|
|
2022
2201
|
// Returns encrypted data as stored
|
|
2023
2202
|
|
|
2024
2203
|
// 7. DocumentDBEncryptionService decrypts sensitive fields
|
|
2025
|
-
const decryptedDocument = await this.encryptionService.decryptFields(
|
|
2204
|
+
const decryptedDocument = await this.encryptionService.decryptFields(
|
|
2205
|
+
'Credential',
|
|
2206
|
+
rawDocument
|
|
2207
|
+
);
|
|
2026
2208
|
// Result:
|
|
2027
2209
|
// {
|
|
2028
2210
|
// data: {
|
|
@@ -2035,11 +2217,11 @@ const decryptedDocument = await this.encryptionService.decryptFields('Credential
|
|
|
2035
2217
|
// }
|
|
2036
2218
|
|
|
2037
2219
|
// 8. Use case receives plain text credential
|
|
2038
|
-
return credential;
|
|
2220
|
+
return credential; // { access_token: "ya29...", refresh_token: "1//0...", ... }
|
|
2039
2221
|
|
|
2040
2222
|
// 9. Application makes API call
|
|
2041
2223
|
await fetch('https://www.googleapis.com/oauth2/v1/userinfo', {
|
|
2042
|
-
headers: { Authorization: `Bearer ${credential.access_token}` }
|
|
2224
|
+
headers: { Authorization: `Bearer ${credential.access_token}` },
|
|
2043
2225
|
});
|
|
2044
2226
|
// ✅ Works! Token is usable
|
|
2045
2227
|
```
|
|
@@ -2051,9 +2233,10 @@ await fetch('https://www.googleapis.com/oauth2/v1/userinfo', {
|
|
|
2051
2233
|
### Unit Tests: DocumentDBEncryptionService
|
|
2052
2234
|
|
|
2053
2235
|
**Coverage Goals**:
|
|
2054
|
-
|
|
2055
|
-
-
|
|
2056
|
-
-
|
|
2236
|
+
|
|
2237
|
+
- 100% line coverage
|
|
2238
|
+
- All branches covered
|
|
2239
|
+
- All error paths tested
|
|
2057
2240
|
|
|
2058
2241
|
**Key Test Cases**:
|
|
2059
2242
|
|
|
@@ -2069,7 +2252,8 @@ describe('DocumentDBEncryptionService', () => {
|
|
|
2069
2252
|
|
|
2070
2253
|
it('enables KMS encryption in production with KMS_KEY_ARN', () => {
|
|
2071
2254
|
process.env.STAGE = 'production';
|
|
2072
|
-
process.env.KMS_KEY_ARN =
|
|
2255
|
+
process.env.KMS_KEY_ARN =
|
|
2256
|
+
'arn:aws:kms:us-east-1:123456789012:key/abc123';
|
|
2073
2257
|
const service = new DocumentDBEncryptionService();
|
|
2074
2258
|
expect(service.enabled).toBe(true);
|
|
2075
2259
|
expect(service.cryptor.shouldUseAws).toBe(true);
|
|
@@ -2089,14 +2273,14 @@ describe('DocumentDBEncryptionService', () => {
|
|
|
2089
2273
|
it('encrypts User.hashword', async () => {
|
|
2090
2274
|
const document = {
|
|
2091
2275
|
username: 'test@example.com',
|
|
2092
|
-
hashword: '$2b$10$plain_bcrypt_hash'
|
|
2276
|
+
hashword: '$2b$10$plain_bcrypt_hash',
|
|
2093
2277
|
};
|
|
2094
2278
|
|
|
2095
2279
|
const encrypted = await service.encryptFields('User', document);
|
|
2096
2280
|
|
|
2097
|
-
expect(encrypted.username).toBe('test@example.com');
|
|
2098
|
-
expect(encrypted.hashword).not.toBe('$2b$10$plain_bcrypt_hash');
|
|
2099
|
-
expect(encrypted.hashword).toMatch(/^[^:]+:[^:]+:[^:]+:[^:]+$/);
|
|
2281
|
+
expect(encrypted.username).toBe('test@example.com'); // Not encrypted
|
|
2282
|
+
expect(encrypted.hashword).not.toBe('$2b$10$plain_bcrypt_hash'); // Encrypted
|
|
2283
|
+
expect(encrypted.hashword).toMatch(/^[^:]+:[^:]+:[^:]+:[^:]+$/); // Format check
|
|
2100
2284
|
});
|
|
2101
2285
|
|
|
2102
2286
|
it('encrypts Credential.data.access_token', async () => {
|
|
@@ -2104,15 +2288,20 @@ describe('DocumentDBEncryptionService', () => {
|
|
|
2104
2288
|
userId: '123',
|
|
2105
2289
|
data: {
|
|
2106
2290
|
access_token: 'ya29.token_here',
|
|
2107
|
-
scope: 'openid profile'
|
|
2108
|
-
}
|
|
2291
|
+
scope: 'openid profile', // Not in registry
|
|
2292
|
+
},
|
|
2109
2293
|
};
|
|
2110
2294
|
|
|
2111
|
-
const encrypted = await service.encryptFields(
|
|
2295
|
+
const encrypted = await service.encryptFields(
|
|
2296
|
+
'Credential',
|
|
2297
|
+
document
|
|
2298
|
+
);
|
|
2112
2299
|
|
|
2113
2300
|
expect(encrypted.data.access_token).not.toBe('ya29.token_here');
|
|
2114
|
-
expect(encrypted.data.access_token).toMatch(
|
|
2115
|
-
|
|
2301
|
+
expect(encrypted.data.access_token).toMatch(
|
|
2302
|
+
/^[^:]+:[^:]+:[^:]+:[^:]+$/
|
|
2303
|
+
);
|
|
2304
|
+
expect(encrypted.data.scope).toBe('openid profile'); // Not encrypted
|
|
2116
2305
|
});
|
|
2117
2306
|
|
|
2118
2307
|
it('skips already encrypted values', async () => {
|
|
@@ -2121,12 +2310,15 @@ describe('DocumentDBEncryptionService', () => {
|
|
|
2121
2310
|
|
|
2122
2311
|
const result = await service.encryptFields('User', document);
|
|
2123
2312
|
|
|
2124
|
-
expect(result.hashword).toBe(alreadyEncrypted);
|
|
2313
|
+
expect(result.hashword).toBe(alreadyEncrypted); // Unchanged
|
|
2125
2314
|
});
|
|
2126
2315
|
|
|
2127
2316
|
it('returns unchanged for unknown model', async () => {
|
|
2128
2317
|
const document = { field: 'value' };
|
|
2129
|
-
const result = await service.encryptFields(
|
|
2318
|
+
const result = await service.encryptFields(
|
|
2319
|
+
'UnknownModel',
|
|
2320
|
+
document
|
|
2321
|
+
);
|
|
2130
2322
|
expect(result).toEqual(document);
|
|
2131
2323
|
});
|
|
2132
2324
|
});
|
|
@@ -2135,7 +2327,7 @@ describe('DocumentDBEncryptionService', () => {
|
|
|
2135
2327
|
it('decrypts User.hashword', async () => {
|
|
2136
2328
|
const encryptedDoc = {
|
|
2137
2329
|
username: 'test@example.com',
|
|
2138
|
-
hashword: 'keyId:iv:cipher:enckey'
|
|
2330
|
+
hashword: 'keyId:iv:cipher:enckey', // Mock encrypted
|
|
2139
2331
|
};
|
|
2140
2332
|
|
|
2141
2333
|
// Mock Cryptor to return known value
|
|
@@ -2144,16 +2336,20 @@ describe('DocumentDBEncryptionService', () => {
|
|
|
2144
2336
|
const decrypted = await service.decryptFields('User', encryptedDoc);
|
|
2145
2337
|
|
|
2146
2338
|
expect(decrypted.hashword).toBe('$2b$10$plain_bcrypt_hash');
|
|
2147
|
-
expect(mockCryptor.decrypt).toHaveBeenCalledWith(
|
|
2339
|
+
expect(mockCryptor.decrypt).toHaveBeenCalledWith(
|
|
2340
|
+
'keyId:iv:cipher:enckey'
|
|
2341
|
+
);
|
|
2148
2342
|
});
|
|
2149
2343
|
|
|
2150
2344
|
it('handles decryption failures gracefully', async () => {
|
|
2151
2345
|
const encryptedDoc = { hashword: 'corrupted:data:here:error' };
|
|
2152
|
-
mockCryptor.decrypt.mockRejectedValue(
|
|
2346
|
+
mockCryptor.decrypt.mockRejectedValue(
|
|
2347
|
+
new Error('Decryption failed')
|
|
2348
|
+
);
|
|
2153
2349
|
|
|
2154
2350
|
const result = await service.decryptFields('User', encryptedDoc);
|
|
2155
2351
|
|
|
2156
|
-
expect(result.hashword).toBeNull();
|
|
2352
|
+
expect(result.hashword).toBeNull(); // Set to null on error
|
|
2157
2353
|
});
|
|
2158
2354
|
|
|
2159
2355
|
it('parses JSON objects after decryption', async () => {
|
|
@@ -2161,9 +2357,12 @@ describe('DocumentDBEncryptionService', () => {
|
|
|
2161
2357
|
const jsonObject = { nested: 'value', array: [1, 2, 3] };
|
|
2162
2358
|
mockCryptor.decrypt.mockResolvedValue(JSON.stringify(jsonObject));
|
|
2163
2359
|
|
|
2164
|
-
const result = await service.decryptFields(
|
|
2360
|
+
const result = await service.decryptFields(
|
|
2361
|
+
'CustomModel',
|
|
2362
|
+
encryptedDoc
|
|
2363
|
+
);
|
|
2165
2364
|
|
|
2166
|
-
expect(result.data.config).toEqual(jsonObject);
|
|
2365
|
+
expect(result.data.config).toEqual(jsonObject); // Parsed as object
|
|
2167
2366
|
});
|
|
2168
2367
|
});
|
|
2169
2368
|
});
|
|
@@ -2203,29 +2402,29 @@ describe('CredentialRepositoryDocumentDB - Security', () => {
|
|
|
2203
2402
|
// Create credential via repository
|
|
2204
2403
|
await repository.upsertCredential({
|
|
2205
2404
|
identifiers: { userId: fromObjectId(userId), externalId },
|
|
2206
|
-
details: { access_token: plainToken, token_type: 'Bearer' }
|
|
2405
|
+
details: { access_token: plainToken, token_type: 'Bearer' },
|
|
2207
2406
|
});
|
|
2208
2407
|
|
|
2209
2408
|
// Query database directly (bypass repository)
|
|
2210
2409
|
const rawResult = await prisma.$runCommandRaw({
|
|
2211
2410
|
find: 'Credential',
|
|
2212
|
-
filter: { userId, externalId }
|
|
2411
|
+
filter: { userId, externalId },
|
|
2213
2412
|
});
|
|
2214
2413
|
|
|
2215
2414
|
const storedCredential = rawResult.cursor.firstBatch[0];
|
|
2216
2415
|
const storedToken = storedCredential.data.access_token;
|
|
2217
2416
|
|
|
2218
2417
|
// CRITICAL ASSERTIONS
|
|
2219
|
-
expect(storedToken).not.toBe(plainToken);
|
|
2220
|
-
expect(storedToken).toMatch(/^[^:]+:[^:]+:[^:]+:[^:]+$/);
|
|
2221
|
-
expect(storedToken.split(':').length).toBeGreaterThanOrEqual(4);
|
|
2418
|
+
expect(storedToken).not.toBe(plainToken); // NOT plain text
|
|
2419
|
+
expect(storedToken).toMatch(/^[^:]+:[^:]+:[^:]+:[^:]+$/); // Encrypted format
|
|
2420
|
+
expect(storedToken.split(':').length).toBeGreaterThanOrEqual(4); // 4+ parts
|
|
2222
2421
|
|
|
2223
2422
|
// Verify repository returns decrypted
|
|
2224
2423
|
const retrieved = await repository.findCredential({
|
|
2225
2424
|
userId: fromObjectId(userId),
|
|
2226
|
-
externalId
|
|
2425
|
+
externalId,
|
|
2227
2426
|
});
|
|
2228
|
-
expect(retrieved.access_token).toBe(plainToken);
|
|
2427
|
+
expect(retrieved.access_token).toBe(plainToken); // Decrypted
|
|
2229
2428
|
});
|
|
2230
2429
|
|
|
2231
2430
|
it('encrypts refresh_token', async () => {
|
|
@@ -2233,13 +2432,16 @@ describe('CredentialRepositoryDocumentDB - Security', () => {
|
|
|
2233
2432
|
const plainRefresh = '1//0secret_refresh_token';
|
|
2234
2433
|
|
|
2235
2434
|
await repository.upsertCredential({
|
|
2236
|
-
identifiers: {
|
|
2237
|
-
|
|
2435
|
+
identifiers: {
|
|
2436
|
+
userId: fromObjectId(userId),
|
|
2437
|
+
externalId: 'test-456',
|
|
2438
|
+
},
|
|
2439
|
+
details: { refresh_token: plainRefresh },
|
|
2238
2440
|
});
|
|
2239
2441
|
|
|
2240
2442
|
const rawResult = await prisma.$runCommandRaw({
|
|
2241
2443
|
find: 'Credential',
|
|
2242
|
-
filter: { userId }
|
|
2444
|
+
filter: { userId },
|
|
2243
2445
|
});
|
|
2244
2446
|
|
|
2245
2447
|
const stored = rawResult.cursor.firstBatch[0].data.refresh_token;
|
|
@@ -2252,13 +2454,16 @@ describe('CredentialRepositoryDocumentDB - Security', () => {
|
|
|
2252
2454
|
const plainIdToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...';
|
|
2253
2455
|
|
|
2254
2456
|
await repository.upsertCredential({
|
|
2255
|
-
identifiers: {
|
|
2256
|
-
|
|
2457
|
+
identifiers: {
|
|
2458
|
+
userId: fromObjectId(userId),
|
|
2459
|
+
externalId: 'test-789',
|
|
2460
|
+
},
|
|
2461
|
+
details: { id_token: plainIdToken },
|
|
2257
2462
|
});
|
|
2258
2463
|
|
|
2259
2464
|
const rawResult = await prisma.$runCommandRaw({
|
|
2260
2465
|
find: 'Credential',
|
|
2261
|
-
filter: { userId }
|
|
2466
|
+
filter: { userId },
|
|
2262
2467
|
});
|
|
2263
2468
|
|
|
2264
2469
|
const stored = rawResult.cursor.firstBatch[0].data.id_token;
|
|
@@ -2270,18 +2475,21 @@ describe('CredentialRepositoryDocumentDB - Security', () => {
|
|
|
2270
2475
|
const userId = new ObjectId();
|
|
2271
2476
|
|
|
2272
2477
|
await repository.upsertCredential({
|
|
2273
|
-
identifiers: {
|
|
2478
|
+
identifiers: {
|
|
2479
|
+
userId: fromObjectId(userId),
|
|
2480
|
+
externalId: 'test-000',
|
|
2481
|
+
},
|
|
2274
2482
|
details: {
|
|
2275
2483
|
access_token: 'token123',
|
|
2276
|
-
expires_in: 3600,
|
|
2277
|
-
token_type: 'Bearer',
|
|
2278
|
-
scope: 'openid profile'
|
|
2279
|
-
}
|
|
2484
|
+
expires_in: 3600, // Not in encrypted fields registry
|
|
2485
|
+
token_type: 'Bearer', // Not in registry
|
|
2486
|
+
scope: 'openid profile', // Not in registry
|
|
2487
|
+
},
|
|
2280
2488
|
});
|
|
2281
2489
|
|
|
2282
2490
|
const rawResult = await prisma.$runCommandRaw({
|
|
2283
2491
|
find: 'Credential',
|
|
2284
|
-
filter: { userId }
|
|
2492
|
+
filter: { userId },
|
|
2285
2493
|
});
|
|
2286
2494
|
|
|
2287
2495
|
const stored = rawResult.cursor.firstBatch[0].data;
|
|
@@ -2302,13 +2510,16 @@ describe('CredentialRepositoryDocumentDB - Security', () => {
|
|
|
2302
2510
|
const plainData = {
|
|
2303
2511
|
access_token: 'test_access_123',
|
|
2304
2512
|
refresh_token: 'test_refresh_456',
|
|
2305
|
-
expires_in: 7200
|
|
2513
|
+
expires_in: 7200,
|
|
2306
2514
|
};
|
|
2307
2515
|
|
|
2308
2516
|
// Insert
|
|
2309
2517
|
const created = await repository.upsertCredential({
|
|
2310
|
-
identifiers: {
|
|
2311
|
-
|
|
2518
|
+
identifiers: {
|
|
2519
|
+
userId: fromObjectId(userId),
|
|
2520
|
+
externalId: 'flow-test',
|
|
2521
|
+
},
|
|
2522
|
+
details: plainData,
|
|
2312
2523
|
});
|
|
2313
2524
|
|
|
2314
2525
|
// Verify returned data is plain text
|
|
@@ -2318,7 +2529,7 @@ describe('CredentialRepositoryDocumentDB - Security', () => {
|
|
|
2318
2529
|
// Read via repository
|
|
2319
2530
|
const retrieved = await repository.findCredential({
|
|
2320
2531
|
userId: fromObjectId(userId),
|
|
2321
|
-
externalId: 'flow-test'
|
|
2532
|
+
externalId: 'flow-test',
|
|
2322
2533
|
});
|
|
2323
2534
|
|
|
2324
2535
|
// Verify decrypted correctly
|
|
@@ -2328,7 +2539,7 @@ describe('CredentialRepositoryDocumentDB - Security', () => {
|
|
|
2328
2539
|
// Verify database has encrypted values
|
|
2329
2540
|
const rawResult = await prisma.$runCommandRaw({
|
|
2330
2541
|
find: 'Credential',
|
|
2331
|
-
filter: { userId }
|
|
2542
|
+
filter: { userId },
|
|
2332
2543
|
});
|
|
2333
2544
|
const stored = rawResult.cursor.firstBatch[0].data;
|
|
2334
2545
|
expect(stored.access_token).not.toBe('test_access_123');
|
|
@@ -2346,17 +2557,20 @@ describe('CredentialRepositoryDocumentDB - Security', () => {
|
|
|
2346
2557
|
const plainToken = 'dev_token_plain';
|
|
2347
2558
|
|
|
2348
2559
|
await devRepo.upsertCredential({
|
|
2349
|
-
identifiers: {
|
|
2350
|
-
|
|
2560
|
+
identifiers: {
|
|
2561
|
+
userId: fromObjectId(userId),
|
|
2562
|
+
externalId: 'dev-test',
|
|
2563
|
+
},
|
|
2564
|
+
details: { access_token: plainToken },
|
|
2351
2565
|
});
|
|
2352
2566
|
|
|
2353
2567
|
// In dev, should be stored as plain text
|
|
2354
2568
|
const rawResult = await prisma.$runCommandRaw({
|
|
2355
2569
|
find: 'Credential',
|
|
2356
|
-
filter: { userId }
|
|
2570
|
+
filter: { userId },
|
|
2357
2571
|
});
|
|
2358
2572
|
const stored = rawResult.cursor.firstBatch[0].data.access_token;
|
|
2359
|
-
expect(stored).toBe(plainToken);
|
|
2573
|
+
expect(stored).toBe(plainToken); // Plain text in dev!
|
|
2360
2574
|
|
|
2361
2575
|
// Reset to production
|
|
2362
2576
|
process.env.STAGE = 'production';
|
|
@@ -2484,9 +2698,10 @@ sampleCreds.forEach(function(cred) {
|
|
|
2484
2698
|
```
|
|
2485
2699
|
|
|
2486
2700
|
**Estimate Impact**:
|
|
2487
|
-
|
|
2488
|
-
-
|
|
2489
|
-
-
|
|
2701
|
+
|
|
2702
|
+
- Number of affected credentials
|
|
2703
|
+
- Number of affected users
|
|
2704
|
+
- Third-party services (Asana, Frontify, etc.)
|
|
2490
2705
|
|
|
2491
2706
|
---
|
|
2492
2707
|
|
|
@@ -2495,22 +2710,24 @@ sampleCreds.forEach(function(cred) {
|
|
|
2495
2710
|
**Priority Actions**:
|
|
2496
2711
|
|
|
2497
2712
|
1. **Deploy Fix Immediately**:
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2713
|
+
|
|
2714
|
+
```bash
|
|
2715
|
+
# Deploy encryption fix to stop new plain text storage
|
|
2716
|
+
cd backend
|
|
2717
|
+
npm install @friggframework/core@latest # With encryption fix
|
|
2718
|
+
npm run deploy -- --stage production
|
|
2719
|
+
```
|
|
2504
2720
|
|
|
2505
2721
|
2. **Rotate All Affected Tokens**:
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2722
|
+
|
|
2723
|
+
- Force OAuth re-authentication for all users
|
|
2724
|
+
- Revoke old tokens on third-party services
|
|
2725
|
+
- Generate new encrypted tokens
|
|
2509
2726
|
|
|
2510
2727
|
3. **Audit Access**:
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2728
|
+
- Review database access logs
|
|
2729
|
+
- Identify who had access to plain text credentials
|
|
2730
|
+
- Check for unauthorized API usage
|
|
2514
2731
|
|
|
2515
2732
|
---
|
|
2516
2733
|
|
|
@@ -2519,9 +2736,18 @@ sampleCreds.forEach(function(cred) {
|
|
|
2519
2736
|
**Migration Script** (`migrate-encrypt-credentials.js`):
|
|
2520
2737
|
|
|
2521
2738
|
```javascript
|
|
2522
|
-
const {
|
|
2523
|
-
|
|
2524
|
-
|
|
2739
|
+
const {
|
|
2740
|
+
prisma,
|
|
2741
|
+
connectPrisma,
|
|
2742
|
+
disconnectPrisma,
|
|
2743
|
+
} = require('@friggframework/core/database/prisma');
|
|
2744
|
+
const {
|
|
2745
|
+
DocumentDBEncryptionService,
|
|
2746
|
+
} = require('@friggframework/core/database/documentdb-encryption-service');
|
|
2747
|
+
const {
|
|
2748
|
+
toObjectId,
|
|
2749
|
+
fromObjectId,
|
|
2750
|
+
} = require('@friggframework/core/database/documentdb-utils');
|
|
2525
2751
|
|
|
2526
2752
|
/**
|
|
2527
2753
|
* Migrate plain text credentials to encrypted format.
|
|
@@ -2540,14 +2766,16 @@ async function migrateCredentials() {
|
|
|
2540
2766
|
const encryptionService = new DocumentDBEncryptionService();
|
|
2541
2767
|
|
|
2542
2768
|
if (!encryptionService.enabled) {
|
|
2543
|
-
console.error(
|
|
2769
|
+
console.error(
|
|
2770
|
+
'❌ Encryption not enabled! Check environment variables.'
|
|
2771
|
+
);
|
|
2544
2772
|
process.exit(1);
|
|
2545
2773
|
}
|
|
2546
2774
|
|
|
2547
2775
|
// Fetch all credentials
|
|
2548
2776
|
const result = await prisma.$runCommandRaw({
|
|
2549
2777
|
find: 'Credential',
|
|
2550
|
-
filter: {}
|
|
2778
|
+
filter: {},
|
|
2551
2779
|
});
|
|
2552
2780
|
|
|
2553
2781
|
const credentials = result.cursor.firstBatch;
|
|
@@ -2564,7 +2792,9 @@ async function migrateCredentials() {
|
|
|
2564
2792
|
// Check if already encrypted
|
|
2565
2793
|
const token = cred.data?.access_token;
|
|
2566
2794
|
if (!token) {
|
|
2567
|
-
console.log(
|
|
2795
|
+
console.log(
|
|
2796
|
+
`⏭️ Skipping credential ${credId} (no access_token)`
|
|
2797
|
+
);
|
|
2568
2798
|
continue;
|
|
2569
2799
|
}
|
|
2570
2800
|
|
|
@@ -2577,29 +2807,36 @@ async function migrateCredentials() {
|
|
|
2577
2807
|
|
|
2578
2808
|
// Encrypt credential data
|
|
2579
2809
|
console.log(`🔐 Encrypting credential ${credId}...`);
|
|
2580
|
-
const encryptedData = await encryptionService.encryptFields(
|
|
2581
|
-
|
|
2582
|
-
|
|
2810
|
+
const encryptedData = await encryptionService.encryptFields(
|
|
2811
|
+
'Credential',
|
|
2812
|
+
{
|
|
2813
|
+
data: cred.data,
|
|
2814
|
+
}
|
|
2815
|
+
);
|
|
2583
2816
|
|
|
2584
2817
|
// Update database
|
|
2585
2818
|
await prisma.$runCommandRaw({
|
|
2586
2819
|
update: 'Credential',
|
|
2587
|
-
updates: [
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2820
|
+
updates: [
|
|
2821
|
+
{
|
|
2822
|
+
q: { _id: cred._id },
|
|
2823
|
+
u: {
|
|
2824
|
+
$set: {
|
|
2825
|
+
data: encryptedData.data,
|
|
2826
|
+
updatedAt: new Date(),
|
|
2827
|
+
},
|
|
2828
|
+
},
|
|
2829
|
+
},
|
|
2830
|
+
],
|
|
2596
2831
|
});
|
|
2597
2832
|
|
|
2598
2833
|
console.log(`✅ Encrypted credential ${credId}`);
|
|
2599
2834
|
encryptedCount++;
|
|
2600
|
-
|
|
2601
2835
|
} catch (error) {
|
|
2602
|
-
console.error(
|
|
2836
|
+
console.error(
|
|
2837
|
+
`❌ Failed to encrypt credential ${credId}:`,
|
|
2838
|
+
error.message
|
|
2839
|
+
);
|
|
2603
2840
|
errorCount++;
|
|
2604
2841
|
}
|
|
2605
2842
|
}
|
|
@@ -2615,7 +2852,7 @@ async function migrateCredentials() {
|
|
|
2615
2852
|
}
|
|
2616
2853
|
|
|
2617
2854
|
// Run migration
|
|
2618
|
-
migrateCredentials().catch(error => {
|
|
2855
|
+
migrateCredentials().catch((error) => {
|
|
2619
2856
|
console.error('💥 Migration failed:', error);
|
|
2620
2857
|
process.exit(1);
|
|
2621
2858
|
});
|
|
@@ -2642,7 +2879,11 @@ node verify-encryption.js # See verification script below
|
|
|
2642
2879
|
**Verification Script** (`verify-encryption.js`):
|
|
2643
2880
|
|
|
2644
2881
|
```javascript
|
|
2645
|
-
const {
|
|
2882
|
+
const {
|
|
2883
|
+
prisma,
|
|
2884
|
+
connectPrisma,
|
|
2885
|
+
disconnectPrisma,
|
|
2886
|
+
} = require('@friggframework/core/database/prisma');
|
|
2646
2887
|
|
|
2647
2888
|
async function verifyEncryption() {
|
|
2648
2889
|
console.log('🔍 Verifying credential encryption...');
|
|
@@ -2651,7 +2892,7 @@ async function verifyEncryption() {
|
|
|
2651
2892
|
|
|
2652
2893
|
const result = await prisma.$runCommandRaw({
|
|
2653
2894
|
find: 'Credential',
|
|
2654
|
-
filter: {}
|
|
2895
|
+
filter: {},
|
|
2655
2896
|
});
|
|
2656
2897
|
|
|
2657
2898
|
const credentials = result.cursor.firstBatch;
|
|
@@ -2678,14 +2919,16 @@ async function verifyEncryption() {
|
|
|
2678
2919
|
console.log(` Plain text: ${failCount}`);
|
|
2679
2920
|
|
|
2680
2921
|
if (failCount > 0) {
|
|
2681
|
-
console.error(
|
|
2922
|
+
console.error(
|
|
2923
|
+
'\n❌ Verification failed! Plain text credentials still exist.'
|
|
2924
|
+
);
|
|
2682
2925
|
process.exit(1);
|
|
2683
2926
|
} else {
|
|
2684
2927
|
console.log('\n✅ Verification passed! All credentials encrypted.');
|
|
2685
2928
|
}
|
|
2686
2929
|
}
|
|
2687
2930
|
|
|
2688
|
-
verifyEncryption().catch(error => {
|
|
2931
|
+
verifyEncryption().catch((error) => {
|
|
2689
2932
|
console.error('💥 Verification failed:', error);
|
|
2690
2933
|
process.exit(1);
|
|
2691
2934
|
});
|
|
@@ -2696,20 +2939,22 @@ verifyEncryption().catch(error => {
|
|
|
2696
2939
|
### Step 5: Post-Migration Cleanup
|
|
2697
2940
|
|
|
2698
2941
|
1. **Delete Migration Scripts**:
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2942
|
+
|
|
2943
|
+
```bash
|
|
2944
|
+
rm migrate-encrypt-credentials.js
|
|
2945
|
+
rm verify-encryption.js
|
|
2946
|
+
```
|
|
2703
2947
|
|
|
2704
2948
|
2. **Update Documentation**:
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2949
|
+
|
|
2950
|
+
- Document the incident
|
|
2951
|
+
- Document lessons learned
|
|
2952
|
+
- Update security procedures
|
|
2708
2953
|
|
|
2709
2954
|
3. **Monitor**:
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2955
|
+
- Set up alerts for plain text detection
|
|
2956
|
+
- Monitor API error rates (in case decryption fails)
|
|
2957
|
+
- Watch for OAuth re-authentication requests
|
|
2713
2958
|
|
|
2714
2959
|
---
|
|
2715
2960
|
|
|
@@ -2720,18 +2965,20 @@ verifyEncryption().catch(error => {
|
|
|
2720
2965
|
1. **Stop the migration script**
|
|
2721
2966
|
|
|
2722
2967
|
2. **Restore from backup**:
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2968
|
+
|
|
2969
|
+
```bash
|
|
2970
|
+
# Restore MongoDB backup from before migration
|
|
2971
|
+
mongorestore --uri="mongodb://..." --archive=backup-before-migration.archive
|
|
2972
|
+
```
|
|
2727
2973
|
|
|
2728
2974
|
3. **Revert code deployment**:
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2975
|
+
|
|
2976
|
+
```bash
|
|
2977
|
+
# Rollback to previous version
|
|
2978
|
+
cd backend
|
|
2979
|
+
npm install @friggframework/core@<previous-version>
|
|
2980
|
+
npm run deploy -- --stage production
|
|
2981
|
+
```
|
|
2735
2982
|
|
|
2736
2983
|
4. **Investigate and fix issues**
|
|
2737
2984
|
|
|
@@ -2744,28 +2991,31 @@ verifyEncryption().catch(error => {
|
|
|
2744
2991
|
For large deployments:
|
|
2745
2992
|
|
|
2746
2993
|
1. **Phase 1: Deploy encryption fix** (don't migrate yet)
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2994
|
+
|
|
2995
|
+
- New credentials will be encrypted
|
|
2996
|
+
- Old credentials remain as-is
|
|
2997
|
+
- Application handles both encrypted and plain text
|
|
2750
2998
|
|
|
2751
2999
|
2. **Phase 2: Migrate in batches**
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
3000
|
+
|
|
3001
|
+
```javascript
|
|
3002
|
+
// Migrate 100 credentials at a time
|
|
3003
|
+
const batchSize = 100;
|
|
3004
|
+
for (let skip = 0; skip < totalCredentials; skip += batchSize) {
|
|
3005
|
+
await migrateBatch(skip, batchSize);
|
|
3006
|
+
await sleep(1000); // 1 second between batches
|
|
3007
|
+
}
|
|
3008
|
+
```
|
|
2760
3009
|
|
|
2761
3010
|
3. **Phase 3: Verify**
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
3011
|
+
|
|
3012
|
+
- Check random samples
|
|
3013
|
+
- Monitor error rates
|
|
3014
|
+
- Verify API calls still work
|
|
2765
3015
|
|
|
2766
3016
|
4. **Phase 4: Complete**
|
|
2767
|
-
|
|
2768
|
-
|
|
3017
|
+
- Remove backward compatibility code
|
|
3018
|
+
- Update monitoring alerts
|
|
2769
3019
|
|
|
2770
3020
|
---
|
|
2771
3021
|
|
|
@@ -2774,17 +3024,20 @@ For large deployments:
|
|
|
2774
3024
|
### Encryption Format
|
|
2775
3025
|
|
|
2776
3026
|
**Envelope Encryption Pattern**:
|
|
3027
|
+
|
|
2777
3028
|
```
|
|
2778
3029
|
keyId:iv:cipher:encKey
|
|
2779
3030
|
```
|
|
2780
3031
|
|
|
2781
3032
|
**Components**:
|
|
2782
|
-
|
|
2783
|
-
-
|
|
2784
|
-
-
|
|
2785
|
-
-
|
|
3033
|
+
|
|
3034
|
+
- `keyId`: Identifier for the encryption key (e.g., "aes-key-1", KMS key ID)
|
|
3035
|
+
- `iv`: Initialization vector (base64-encoded)
|
|
3036
|
+
- `cipher`: Encrypted data (base64-encoded)
|
|
3037
|
+
- `encKey`: Encrypted data encryption key (base64-encoded)
|
|
2786
3038
|
|
|
2787
3039
|
**Example**:
|
|
3040
|
+
|
|
2788
3041
|
```
|
|
2789
3042
|
aes-key-1:MTIzNDU2Nzg5MGFiY2RlZg==:ZW5jcnlwdGVkX2RhdGFfaGVyZQ==:ZGVrX2VuY3J5cHRlZA==
|
|
2790
3043
|
```
|
|
@@ -2794,6 +3047,7 @@ aes-key-1:MTIzNDU2Nzg5MGFiY2RlZg==:ZW5jcnlwdGVkX2RhdGFfaGVyZQ==:ZGVrX2VuY3J5cHRl
|
|
|
2794
3047
|
### Key Management
|
|
2795
3048
|
|
|
2796
3049
|
**Production (KMS - Recommended)**:
|
|
3050
|
+
|
|
2797
3051
|
```bash
|
|
2798
3052
|
# AWS KMS key is auto-discovered by Frigg infrastructure
|
|
2799
3053
|
# Or set explicitly:
|
|
@@ -2804,13 +3058,15 @@ export STAGE=production
|
|
|
2804
3058
|
```
|
|
2805
3059
|
|
|
2806
3060
|
**Benefits**:
|
|
2807
|
-
|
|
2808
|
-
-
|
|
2809
|
-
-
|
|
2810
|
-
-
|
|
2811
|
-
-
|
|
3061
|
+
|
|
3062
|
+
- ✅ AWS-managed key rotation
|
|
3063
|
+
- ✅ Audit trail via CloudTrail
|
|
3064
|
+
- ✅ Fine-grained IAM permissions
|
|
3065
|
+
- ✅ Hardware security module (HSM) backed
|
|
3066
|
+
- ✅ Compliance-ready (HIPAA, PCI-DSS, etc.)
|
|
2812
3067
|
|
|
2813
3068
|
**Alternative (AES - Any Environment)**:
|
|
3069
|
+
|
|
2814
3070
|
```bash
|
|
2815
3071
|
# Generate a 32-character key
|
|
2816
3072
|
export AES_KEY_ID=my-app-key-v1
|
|
@@ -2821,14 +3077,16 @@ export STAGE=production
|
|
|
2821
3077
|
```
|
|
2822
3078
|
|
|
2823
3079
|
**Benefits**:
|
|
2824
|
-
|
|
2825
|
-
-
|
|
2826
|
-
-
|
|
3080
|
+
|
|
3081
|
+
- ✅ Works in any environment (no AWS required)
|
|
3082
|
+
- ✅ Faster than KMS (no network calls)
|
|
3083
|
+
- ✅ No AWS costs
|
|
2827
3084
|
|
|
2828
3085
|
**Drawbacks**:
|
|
2829
|
-
|
|
2830
|
-
-
|
|
2831
|
-
-
|
|
3086
|
+
|
|
3087
|
+
- ⚠️ Must securely manage key yourself
|
|
3088
|
+
- ⚠️ No automatic key rotation
|
|
3089
|
+
- ⚠️ Key stored in environment/config
|
|
2832
3090
|
|
|
2833
3091
|
---
|
|
2834
3092
|
|
|
@@ -2837,18 +3095,21 @@ export STAGE=production
|
|
|
2837
3095
|
**Purpose**: Skip encryption in local development for easier debugging
|
|
2838
3096
|
|
|
2839
3097
|
**Bypassed Stages**:
|
|
2840
|
-
|
|
2841
|
-
-
|
|
2842
|
-
-
|
|
3098
|
+
|
|
3099
|
+
- `dev`
|
|
3100
|
+
- `test`
|
|
3101
|
+
- `local`
|
|
2843
3102
|
|
|
2844
3103
|
**Production Stages** (encryption enabled):
|
|
2845
|
-
|
|
2846
|
-
-
|
|
2847
|
-
-
|
|
2848
|
-
-
|
|
2849
|
-
-
|
|
3104
|
+
|
|
3105
|
+
- `production`
|
|
3106
|
+
- `prod`
|
|
3107
|
+
- `staging`
|
|
3108
|
+
- `stage`
|
|
3109
|
+
- Any other value
|
|
2850
3110
|
|
|
2851
3111
|
**Configuration**:
|
|
3112
|
+
|
|
2852
3113
|
```bash
|
|
2853
3114
|
# Bypass encryption (dev)
|
|
2854
3115
|
export STAGE=dev
|
|
@@ -2871,6 +3132,7 @@ export KMS_KEY_ARN=...
|
|
|
2871
3132
|
**Location**: `packages/core/database/encryption/encryption-schema-registry.js`
|
|
2872
3133
|
|
|
2873
3134
|
**Current Encrypted Fields**:
|
|
3135
|
+
|
|
2874
3136
|
```javascript
|
|
2875
3137
|
const ENCRYPTED_FIELDS = {
|
|
2876
3138
|
User: ['hashword'],
|
|
@@ -2878,10 +3140,10 @@ const ENCRYPTED_FIELDS = {
|
|
|
2878
3140
|
'data.access_token',
|
|
2879
3141
|
'data.refresh_token',
|
|
2880
3142
|
'data.id_token',
|
|
2881
|
-
'data.domain'
|
|
3143
|
+
'data.domain',
|
|
2882
3144
|
],
|
|
2883
3145
|
IntegrationMapping: ['mapping'],
|
|
2884
|
-
Token: ['token']
|
|
3146
|
+
Token: ['token'],
|
|
2885
3147
|
};
|
|
2886
3148
|
```
|
|
2887
3149
|
|
|
@@ -2889,42 +3151,47 @@ const ENCRYPTED_FIELDS = {
|
|
|
2889
3151
|
|
|
2890
3152
|
1. Open `encryption-schema-registry.js`
|
|
2891
3153
|
2. Add field path to appropriate model:
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
3154
|
+
```javascript
|
|
3155
|
+
Credential: [
|
|
3156
|
+
'data.access_token',
|
|
3157
|
+
'data.refresh_token',
|
|
3158
|
+
'data.id_token',
|
|
3159
|
+
'data.domain',
|
|
3160
|
+
'data.client_secret', // ← NEW
|
|
3161
|
+
];
|
|
3162
|
+
```
|
|
2901
3163
|
3. Deploy - encryption applied automatically (no code changes needed)
|
|
2902
3164
|
|
|
2903
3165
|
**Field Path Examples**:
|
|
2904
|
-
|
|
2905
|
-
-
|
|
2906
|
-
-
|
|
3166
|
+
|
|
3167
|
+
- Top-level: `hashword` → encrypts `document.hashword`
|
|
3168
|
+
- Nested: `data.access_token` → encrypts `document.data.access_token`
|
|
3169
|
+
- Deep nesting supported: `config.secrets.apiKey`
|
|
2907
3170
|
|
|
2908
3171
|
---
|
|
2909
3172
|
|
|
2910
3173
|
### Compliance & Best Practices
|
|
2911
3174
|
|
|
2912
3175
|
**GDPR Compliance**:
|
|
2913
|
-
|
|
2914
|
-
-
|
|
2915
|
-
-
|
|
3176
|
+
|
|
3177
|
+
- ✅ Data encrypted at rest
|
|
3178
|
+
- ✅ Encryption keys managed securely
|
|
3179
|
+
- ✅ User data can be deleted (right to erasure)
|
|
2916
3180
|
|
|
2917
3181
|
**PCI-DSS Compliance** (if storing payment data):
|
|
2918
|
-
|
|
2919
|
-
-
|
|
2920
|
-
-
|
|
3182
|
+
|
|
3183
|
+
- ✅ Encryption of cardholder data
|
|
3184
|
+
- ✅ Key management procedures
|
|
3185
|
+
- ✅ Audit logging (via CloudTrail with KMS)
|
|
2921
3186
|
|
|
2922
3187
|
**HIPAA Compliance** (if storing health data):
|
|
2923
|
-
|
|
2924
|
-
-
|
|
2925
|
-
-
|
|
3188
|
+
|
|
3189
|
+
- ✅ Encryption at rest (required)
|
|
3190
|
+
- ✅ Access controls (AWS KMS IAM)
|
|
3191
|
+
- ✅ Audit trail (CloudTrail)
|
|
2926
3192
|
|
|
2927
3193
|
**Best Practices**:
|
|
3194
|
+
|
|
2928
3195
|
1. **Use KMS in production** - Better security, compliance, key rotation
|
|
2929
3196
|
2. **Rotate keys periodically** - Even with KMS, review and rotate annually
|
|
2930
3197
|
3. **Monitor decryption failures** - Alert on >1% failure rate
|
|
@@ -2938,16 +3205,16 @@ const ENCRYPTED_FIELDS = {
|
|
|
2938
3205
|
|
|
2939
3206
|
Before going to production:
|
|
2940
3207
|
|
|
2941
|
-
-
|
|
2942
|
-
-
|
|
2943
|
-
-
|
|
2944
|
-
-
|
|
2945
|
-
-
|
|
2946
|
-
-
|
|
2947
|
-
-
|
|
2948
|
-
-
|
|
2949
|
-
-
|
|
2950
|
-
-
|
|
3208
|
+
- [ ] Verify `STAGE=production` in environment
|
|
3209
|
+
- [ ] Verify encryption keys configured (`KMS_KEY_ARN` or `AES_KEY_ID`)
|
|
3210
|
+
- [ ] Run security tests (verify encrypted format in database)
|
|
3211
|
+
- [ ] Test credential creation and retrieval end-to-end
|
|
3212
|
+
- [ ] Verify OAuth flows work (tokens decrypted correctly)
|
|
3213
|
+
- [ ] Check logs for decryption errors
|
|
3214
|
+
- [ ] Review IAM permissions (if using KMS)
|
|
3215
|
+
- [ ] Test key rotation procedure (if using KMS)
|
|
3216
|
+
- [ ] Document encryption architecture for auditors
|
|
3217
|
+
- [ ] Set up monitoring alerts (decryption failures, plain text detection)
|
|
2951
3218
|
|
|
2952
3219
|
---
|
|
2953
3220
|
|
|
@@ -2958,44 +3225,51 @@ Before going to production:
|
|
|
2958
3225
|
When creating a new DocumentDB repository that handles encrypted data:
|
|
2959
3226
|
|
|
2960
3227
|
1. **Import DocumentDBEncryptionService**:
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
3228
|
+
|
|
3229
|
+
```javascript
|
|
3230
|
+
const {
|
|
3231
|
+
DocumentDBEncryptionService,
|
|
3232
|
+
} = require('../database/documentdb-encryption-service');
|
|
3233
|
+
```
|
|
2964
3234
|
|
|
2965
3235
|
2. **Initialize in constructor**:
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
3236
|
+
|
|
3237
|
+
```javascript
|
|
3238
|
+
constructor() {
|
|
3239
|
+
this.prisma = prisma;
|
|
3240
|
+
this.encryptionService = new DocumentDBEncryptionService();
|
|
3241
|
+
}
|
|
3242
|
+
```
|
|
2972
3243
|
|
|
2973
3244
|
3. **Encrypt before writes**:
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
3245
|
+
|
|
3246
|
+
```javascript
|
|
3247
|
+
async create(data) {
|
|
3248
|
+
const encrypted = await this.encryptionService.encryptFields('ModelName', data);
|
|
3249
|
+
const id = await insertOne(this.prisma, 'CollectionName', encrypted);
|
|
3250
|
+
// ...
|
|
3251
|
+
}
|
|
3252
|
+
```
|
|
2981
3253
|
|
|
2982
3254
|
4. **Decrypt after reads**:
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
3255
|
+
|
|
3256
|
+
```javascript
|
|
3257
|
+
async findById(id) {
|
|
3258
|
+
const doc = await findOne(this.prisma, 'CollectionName', { _id: toObjectId(id) });
|
|
3259
|
+
const decrypted = await this.encryptionService.decryptFields('ModelName', doc);
|
|
3260
|
+
return this._mapModel(decrypted);
|
|
3261
|
+
}
|
|
3262
|
+
```
|
|
2990
3263
|
|
|
2991
3264
|
5. **Add encrypted fields to registry** (if new model):
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
3265
|
+
|
|
3266
|
+
```javascript
|
|
3267
|
+
// packages/core/database/encryption/encryption-schema-registry.js
|
|
3268
|
+
const ENCRYPTED_FIELDS = {
|
|
3269
|
+
// ... existing models
|
|
3270
|
+
NewModel: ['sensitiveField1', 'nested.field2'],
|
|
3271
|
+
};
|
|
3272
|
+
```
|
|
2999
3273
|
|
|
3000
3274
|
6. **Add tests** (see Phase 5 for test patterns)
|
|
3001
3275
|
|
|
@@ -3006,88 +3280,98 @@ When creating a new DocumentDB repository that handles encrypted data:
|
|
|
3006
3280
|
To encrypt a new field in an existing model:
|
|
3007
3281
|
|
|
3008
3282
|
1. **Update encryption-schema-registry.js**:
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3283
|
+
|
|
3284
|
+
```javascript
|
|
3285
|
+
const ENCRYPTED_FIELDS = {
|
|
3286
|
+
Credential: [
|
|
3287
|
+
'data.access_token',
|
|
3288
|
+
'data.refresh_token',
|
|
3289
|
+
'data.id_token',
|
|
3290
|
+
'data.domain',
|
|
3291
|
+
'data.client_secret', // ← NEW FIELD
|
|
3292
|
+
],
|
|
3293
|
+
};
|
|
3294
|
+
```
|
|
3020
3295
|
|
|
3021
3296
|
2. **No code changes needed** - DocumentDBEncryptionService reads from registry
|
|
3022
3297
|
|
|
3023
3298
|
3. **Deploy** - new field will be encrypted automatically
|
|
3024
3299
|
|
|
3025
3300
|
4. **Migrate existing data** (if field already has plain text values):
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3301
|
+
```javascript
|
|
3302
|
+
// Run migration script to encrypt existing plain text values
|
|
3303
|
+
// Similar to credential migration script
|
|
3304
|
+
```
|
|
3030
3305
|
|
|
3031
3306
|
---
|
|
3032
3307
|
|
|
3033
3308
|
### Known Limitations
|
|
3034
3309
|
|
|
3035
3310
|
1. **Performance**: Encryption/decryption adds latency
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3311
|
+
|
|
3312
|
+
- KMS: ~50ms per field (network call to AWS)
|
|
3313
|
+
- AES: ~5-10ms per field (local crypto)
|
|
3314
|
+
- **Mitigation**: Use bulk operations, consider caching decrypted values
|
|
3039
3315
|
|
|
3040
3316
|
2. **DocumentDB-specific**: Only needed for DocumentDB
|
|
3041
|
-
|
|
3042
|
-
|
|
3317
|
+
|
|
3318
|
+
- MongoDB/PostgreSQL use automatic Prisma Extension
|
|
3319
|
+
- Duplicate logic unavoidable (Prisma raw queries bypass extensions)
|
|
3043
3320
|
|
|
3044
3321
|
3. **Manual encryption required**: Developers must remember to call service
|
|
3045
|
-
|
|
3322
|
+
|
|
3323
|
+
- **Mitigation**: Code reviews, tests, linting rules
|
|
3046
3324
|
|
|
3047
3325
|
4. **No transactional encryption**: Encryption happens outside transactions
|
|
3048
|
-
|
|
3049
|
-
|
|
3326
|
+
|
|
3327
|
+
- **Risk**: If encryption fails mid-operation, could leave inconsistent state
|
|
3328
|
+
- **Mitigation**: Encrypt before transaction starts, handle errors
|
|
3050
3329
|
|
|
3051
3330
|
5. **Field-level only**: Doesn't encrypt entire documents or collections
|
|
3052
|
-
|
|
3331
|
+
- **Alternative**: Use database-level encryption (AWS DocumentDB encryption at rest)
|
|
3053
3332
|
|
|
3054
3333
|
---
|
|
3055
3334
|
|
|
3056
3335
|
### Future Improvements
|
|
3057
3336
|
|
|
3058
3337
|
1. **Automatic Repository Decorator**:
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3338
|
+
|
|
3339
|
+
```javascript
|
|
3340
|
+
// Potential future API
|
|
3341
|
+
@encryptDocumentDB(['User', 'Credential'])
|
|
3342
|
+
class MyRepositoryDocumentDB {
|
|
3343
|
+
// Encryption applied automatically by decorator
|
|
3344
|
+
}
|
|
3345
|
+
```
|
|
3066
3346
|
|
|
3067
3347
|
2. **Encryption Caching**:
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3348
|
+
|
|
3349
|
+
- Cache decrypted values for frequently accessed credentials
|
|
3350
|
+
- Invalidate cache on credential update
|
|
3351
|
+
- Reduce KMS API calls
|
|
3071
3352
|
|
|
3072
3353
|
3. **Field Compression**:
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3354
|
+
|
|
3355
|
+
- Compress large fields before encryption
|
|
3356
|
+
- Reduce storage and transfer costs
|
|
3357
|
+
- Especially useful for `IntegrationMapping.mapping`
|
|
3076
3358
|
|
|
3077
3359
|
4. **Key Versioning**:
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3360
|
+
|
|
3361
|
+
- Support multiple active keys
|
|
3362
|
+
- Gradual key rotation without migration
|
|
3363
|
+
- Store key version with encrypted data
|
|
3081
3364
|
|
|
3082
3365
|
5. **Encryption Metrics**:
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3366
|
+
|
|
3367
|
+
- Track encryption/decryption performance
|
|
3368
|
+
- Monitor failure rates
|
|
3369
|
+
- Alert on anomalies
|
|
3086
3370
|
|
|
3087
3371
|
6. **Integration with Prisma Extension**:
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3372
|
+
- Potential future Prisma feature: Extension support for raw queries
|
|
3373
|
+
- Would eliminate need for DocumentDBEncryptionService
|
|
3374
|
+
- Track: https://github.com/prisma/prisma/issues/...
|
|
3091
3375
|
|
|
3092
3376
|
---
|
|
3093
3377
|
|
|
@@ -3096,39 +3380,43 @@ To encrypt a new field in an existing model:
|
|
|
3096
3380
|
**Recommended Metrics**:
|
|
3097
3381
|
|
|
3098
3382
|
1. **Encryption Failures**:
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3383
|
+
|
|
3384
|
+
```javascript
|
|
3385
|
+
// Log when encryption fails
|
|
3386
|
+
console.error('Encryption failed', { modelName, fieldPath, error });
|
|
3387
|
+
// Alert if >1% of operations fail
|
|
3388
|
+
```
|
|
3104
3389
|
|
|
3105
3390
|
2. **Decryption Failures**:
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3391
|
+
|
|
3392
|
+
```javascript
|
|
3393
|
+
// Log when decryption fails
|
|
3394
|
+
console.error('Decryption failed', { modelName, fieldPath, error });
|
|
3395
|
+
// Alert immediately (could indicate data corruption)
|
|
3396
|
+
```
|
|
3111
3397
|
|
|
3112
3398
|
3. **Plain Text Detection**:
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3399
|
+
|
|
3400
|
+
```javascript
|
|
3401
|
+
// Periodic scan of database
|
|
3402
|
+
// Alert if any plain text credentials found
|
|
3403
|
+
```
|
|
3117
3404
|
|
|
3118
3405
|
4. **Performance Metrics**:
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3406
|
+
```javascript
|
|
3407
|
+
// Track encryption/decryption time
|
|
3408
|
+
const start = Date.now();
|
|
3409
|
+
await service.encryptFields(...);
|
|
3410
|
+
const duration = Date.now() - start;
|
|
3411
|
+
metrics.histogram('encryption_duration_ms', duration);
|
|
3412
|
+
```
|
|
3126
3413
|
|
|
3127
3414
|
**CloudWatch Dashboards** (for AWS deployments):
|
|
3128
|
-
|
|
3129
|
-
-
|
|
3130
|
-
-
|
|
3131
|
-
-
|
|
3415
|
+
|
|
3416
|
+
- Encryption operation count
|
|
3417
|
+
- Average encryption duration
|
|
3418
|
+
- Decryption failure rate
|
|
3419
|
+
- KMS API call count (if using KMS)
|
|
3132
3420
|
|
|
3133
3421
|
---
|
|
3134
3422
|
|
|
@@ -3137,31 +3425,36 @@ To encrypt a new field in an existing model:
|
|
|
3137
3425
|
**Common Issues**:
|
|
3138
3426
|
|
|
3139
3427
|
1. **"No encryption keys configured"**
|
|
3140
|
-
|
|
3141
|
-
|
|
3428
|
+
|
|
3429
|
+
- **Cause**: Missing `KMS_KEY_ARN` or `AES_KEY_ID` in production
|
|
3430
|
+
- **Fix**: Set environment variables, restart application
|
|
3142
3431
|
|
|
3143
3432
|
2. **"Decryption failed"**
|
|
3144
|
-
|
|
3145
|
-
|
|
3433
|
+
|
|
3434
|
+
- **Cause**: Wrong key, corrupted data, or key rotation
|
|
3435
|
+
- **Fix**: Check key configuration, verify data integrity, check key version
|
|
3146
3436
|
|
|
3147
3437
|
3. **"Cannot read property 'access_token' of undefined"**
|
|
3148
|
-
|
|
3149
|
-
|
|
3438
|
+
|
|
3439
|
+
- **Cause**: Credential data is null or decryption returned null
|
|
3440
|
+
- **Fix**: Check if credential exists, verify encryption didn't fail on write
|
|
3150
3441
|
|
|
3151
3442
|
4. **"Encryption too slow"**
|
|
3152
|
-
|
|
3153
|
-
|
|
3443
|
+
|
|
3444
|
+
- **Cause**: Using KMS with high latency
|
|
3445
|
+
- **Fix**: Switch to AES for non-production, optimize KMS calls (batching)
|
|
3154
3446
|
|
|
3155
3447
|
5. **"Credentials not encrypted after deployment"**
|
|
3156
|
-
|
|
3157
|
-
|
|
3448
|
+
- **Cause**: `STAGE=dev` in production, or missing encryption keys
|
|
3449
|
+
- **Fix**: Set `STAGE=production`, configure keys, redeploy
|
|
3158
3450
|
|
|
3159
3451
|
**Getting Help**:
|
|
3160
|
-
|
|
3161
|
-
-
|
|
3162
|
-
-
|
|
3163
|
-
-
|
|
3164
|
-
-
|
|
3452
|
+
|
|
3453
|
+
- Check logs for error details
|
|
3454
|
+
- Review encryption-schema-registry.js configuration
|
|
3455
|
+
- Verify environment variables
|
|
3456
|
+
- Run health check: `curl http://localhost:3000/health/detailed`
|
|
3457
|
+
- Check encryption status in health response
|
|
3165
3458
|
|
|
3166
3459
|
---
|
|
3167
3460
|
|
|
@@ -3170,59 +3463,69 @@ To encrypt a new field in an existing model:
|
|
|
3170
3463
|
### Related Files
|
|
3171
3464
|
|
|
3172
3465
|
**Core Encryption**:
|
|
3173
|
-
|
|
3174
|
-
-
|
|
3175
|
-
-
|
|
3176
|
-
-
|
|
3177
|
-
-
|
|
3466
|
+
|
|
3467
|
+
- `packages/core/database/encryption/README.md` - Main encryption documentation
|
|
3468
|
+
- `packages/core/database/encryption/encryption-schema-registry.js` - Encrypted fields definition
|
|
3469
|
+
- `packages/core/database/encryption/field-encryption-service.js` - Field-level encryption (Prisma Extension)
|
|
3470
|
+
- `packages/core/database/encryption/prisma-encryption-extension.js` - Prisma Client Extension
|
|
3471
|
+
- `packages/core/encrypt/Cryptor.js` - Encryption adapter (KMS/AES)
|
|
3178
3472
|
|
|
3179
3473
|
**DocumentDB**:
|
|
3180
|
-
|
|
3181
|
-
-
|
|
3474
|
+
|
|
3475
|
+
- `packages/core/database/documentdb-utils.js` - Raw query utilities
|
|
3476
|
+
- `packages/core/database/prisma.js` - Prisma client initialization
|
|
3182
3477
|
|
|
3183
3478
|
**Repositories**:
|
|
3184
|
-
|
|
3185
|
-
-
|
|
3186
|
-
-
|
|
3187
|
-
-
|
|
3479
|
+
|
|
3480
|
+
- `packages/core/user/repositories/user-repository-documentdb.js` - User repository
|
|
3481
|
+
- `packages/core/modules/repositories/module-repository-documentdb.js` - Module/Entity repository
|
|
3482
|
+
- `packages/core/credential/repositories/credential-repository-documentdb.js` - Credential repository
|
|
3483
|
+
- `packages/core/integrations/repositories/integration-repository-documentdb.js` - Integration repository
|
|
3188
3484
|
|
|
3189
3485
|
**Tests**:
|
|
3190
|
-
|
|
3191
|
-
-
|
|
3486
|
+
|
|
3487
|
+
- `packages/core/database/encryption/*.test.js` - Encryption unit tests
|
|
3488
|
+
- `packages/core/**/repositories/__tests__/*.test.js` - Repository tests
|
|
3192
3489
|
|
|
3193
3490
|
---
|
|
3194
3491
|
|
|
3195
3492
|
### External Documentation
|
|
3196
3493
|
|
|
3197
3494
|
**Prisma**:
|
|
3198
|
-
|
|
3199
|
-
-
|
|
3200
|
-
-
|
|
3495
|
+
|
|
3496
|
+
- [Prisma Client Extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions)
|
|
3497
|
+
- [Raw Database Access](https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access)
|
|
3498
|
+
- [MongoDB Support](https://www.prisma.io/docs/concepts/database-connectors/mongodb)
|
|
3201
3499
|
|
|
3202
3500
|
**AWS DocumentDB**:
|
|
3203
|
-
|
|
3204
|
-
-
|
|
3501
|
+
|
|
3502
|
+
- [AWS DocumentDB Documentation](https://docs.aws.amazon.com/documentdb/)
|
|
3503
|
+
- [MongoDB Compatibility](https://docs.aws.amazon.com/documentdb/latest/developerguide/functional-differences.html)
|
|
3205
3504
|
|
|
3206
3505
|
**AWS KMS**:
|
|
3207
|
-
|
|
3208
|
-
-
|
|
3506
|
+
|
|
3507
|
+
- [AWS KMS Developer Guide](https://docs.aws.amazon.com/kms/latest/developerguide/)
|
|
3508
|
+
- [Envelope Encryption](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#enveloping)
|
|
3209
3509
|
|
|
3210
3510
|
**Encryption Best Practices**:
|
|
3211
|
-
|
|
3212
|
-
-
|
|
3511
|
+
|
|
3512
|
+
- [OWASP Cryptographic Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html)
|
|
3513
|
+
- [NIST Encryption Standards](https://csrc.nist.gov/projects/cryptographic-standards-and-guidelines)
|
|
3213
3514
|
|
|
3214
3515
|
---
|
|
3215
3516
|
|
|
3216
3517
|
### Frigg Framework
|
|
3217
3518
|
|
|
3218
3519
|
**Core Documentation**:
|
|
3219
|
-
|
|
3220
|
-
-
|
|
3221
|
-
-
|
|
3520
|
+
|
|
3521
|
+
- [Frigg Framework Docs](https://docs.friggframework.org)
|
|
3522
|
+
- [GitHub Repository](https://github.com/friggframework/frigg)
|
|
3523
|
+
- [Community Slack](https://friggframework.org/#contact)
|
|
3222
3524
|
|
|
3223
3525
|
**Related Issues**:
|
|
3224
|
-
|
|
3225
|
-
-
|
|
3526
|
+
|
|
3527
|
+
- GitHub Issue: DocumentDB encryption support [#TBD]
|
|
3528
|
+
- GitHub PR: Implement DocumentDBEncryptionService [#TBD]
|
|
3226
3529
|
|
|
3227
3530
|
---
|
|
3228
3531
|
|
|
@@ -3231,29 +3534,31 @@ To encrypt a new field in an existing model:
|
|
|
3231
3534
|
### Glossary
|
|
3232
3535
|
|
|
3233
3536
|
**Terms**:
|
|
3234
|
-
|
|
3235
|
-
-
|
|
3236
|
-
-
|
|
3237
|
-
-
|
|
3238
|
-
-
|
|
3239
|
-
-
|
|
3240
|
-
-
|
|
3537
|
+
|
|
3538
|
+
- **DocumentDB**: AWS DocumentDB, a MongoDB-compatible database service
|
|
3539
|
+
- **Prisma Extension**: Prisma feature that intercepts and modifies queries
|
|
3540
|
+
- **Raw Query**: Low-level database command that bypasses Prisma ORM
|
|
3541
|
+
- **Envelope Encryption**: Encryption pattern using data keys encrypted by master keys
|
|
3542
|
+
- **KMS**: AWS Key Management Service
|
|
3543
|
+
- **AES**: Advanced Encryption Standard (symmetric encryption)
|
|
3544
|
+
- **Field-Level Encryption**: Encrypting individual fields within documents
|
|
3241
3545
|
|
|
3242
3546
|
**Acronyms**:
|
|
3243
|
-
|
|
3244
|
-
-
|
|
3245
|
-
-
|
|
3246
|
-
-
|
|
3247
|
-
-
|
|
3248
|
-
-
|
|
3547
|
+
|
|
3548
|
+
- **DRY**: Don't Repeat Yourself
|
|
3549
|
+
- **IAM**: Identity and Access Management
|
|
3550
|
+
- **HSM**: Hardware Security Module
|
|
3551
|
+
- **GDPR**: General Data Protection Regulation
|
|
3552
|
+
- **PCI-DSS**: Payment Card Industry Data Security Standard
|
|
3553
|
+
- **HIPAA**: Health Insurance Portability and Accountability Act
|
|
3249
3554
|
|
|
3250
3555
|
---
|
|
3251
3556
|
|
|
3252
3557
|
### Changelog
|
|
3253
3558
|
|
|
3254
|
-
| Version | Date
|
|
3255
|
-
|
|
3256
|
-
| 1.0
|
|
3559
|
+
| Version | Date | Author | Changes |
|
|
3560
|
+
| ------- | ---------- | ------ | --------------------- |
|
|
3561
|
+
| 1.0 | 2025-01-13 | System | Initial documentation |
|
|
3257
3562
|
|
|
3258
3563
|
---
|
|
3259
3564
|
|