@friggframework/core 2.0.0--canary.499.897122d.0 → 2.0.0--canary.499.5d51f03.0
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/credential/repositories/credential-repository-documentdb.js +95 -23
- package/database/documentdb-encryption-service.js +326 -0
- package/database/encryption/README.md +50 -0
- package/database/repositories/health-check-repository-mongodb.js +1 -3
- package/integrations/repositories/integration-mapping-repository-documentdb.js +1 -0
- package/integrations/repositories/integration-repository-documentdb.js +1 -0
- package/integrations/repositories/process-repository-documentdb.js +1 -0
- package/modules/repositories/module-repository-documentdb.js +38 -123
- package/package.json +5 -5
- package/syncs/repositories/sync-repository-documentdb.js +1 -0
- package/token/repositories/token-repository-documentdb.js +1 -0
- package/user/repositories/user-repository-documentdb.js +30 -151
- package/websocket/repositories/websocket-connection-repository-documentdb.js +1 -0
|
@@ -10,18 +10,39 @@ const {
|
|
|
10
10
|
const {
|
|
11
11
|
CredentialRepositoryInterface,
|
|
12
12
|
} = require('./credential-repository-interface');
|
|
13
|
+
const { DocumentDBEncryptionService } = require('../../database/documentdb-encryption-service');
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Credential repository for DocumentDB.
|
|
17
|
+
* Uses DocumentDBEncryptionService for field-level encryption.
|
|
18
|
+
*
|
|
19
|
+
* Encrypted fields:
|
|
20
|
+
* - Credential.data.access_token
|
|
21
|
+
* - Credential.data.refresh_token
|
|
22
|
+
* - Credential.data.id_token
|
|
23
|
+
* - Credential.data.domain
|
|
24
|
+
*
|
|
25
|
+
* SECURITY CRITICAL: All OAuth credentials must be encrypted at rest.
|
|
26
|
+
*
|
|
27
|
+
* @see DocumentDBEncryptionService
|
|
28
|
+
* @see encryption-schema-registry.js
|
|
29
|
+
*/
|
|
14
30
|
class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
|
|
15
31
|
constructor() {
|
|
16
32
|
super();
|
|
17
33
|
this.prisma = prisma;
|
|
34
|
+
this.encryptionService = new DocumentDBEncryptionService();
|
|
18
35
|
}
|
|
19
36
|
|
|
20
37
|
async findCredentialById(id) {
|
|
21
38
|
const objectId = toObjectId(id);
|
|
22
39
|
if (!objectId) return null;
|
|
23
40
|
const doc = await findOne(this.prisma, 'Credential', { _id: objectId });
|
|
24
|
-
|
|
41
|
+
if (!doc) return null;
|
|
42
|
+
|
|
43
|
+
// Decrypt sensitive fields using service
|
|
44
|
+
const decryptedCredential = await this.encryptionService.decryptFields('Credential', doc);
|
|
45
|
+
return this._mapCredentialById(decryptedCredential);
|
|
25
46
|
}
|
|
26
47
|
|
|
27
48
|
async updateAuthenticationStatus(credentialId, authIsValid) {
|
|
@@ -73,28 +94,48 @@ class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
|
|
|
73
94
|
const now = new Date();
|
|
74
95
|
|
|
75
96
|
if (existing) {
|
|
76
|
-
|
|
97
|
+
// Decrypt existing credential data first
|
|
98
|
+
const decryptedExisting = await this.encryptionService.decryptFields('Credential', existing);
|
|
99
|
+
const mergedData = { ...(decryptedExisting.data || {}), ...oauthData };
|
|
100
|
+
|
|
101
|
+
// Build update document
|
|
102
|
+
const updateDocument = {
|
|
103
|
+
userId: toObjectId(userId || user) || existing.userId || null,
|
|
104
|
+
externalId: externalId !== undefined ? externalId : existing.externalId,
|
|
105
|
+
authIsValid: authIsValid !== undefined ? authIsValid : existing.authIsValid,
|
|
106
|
+
data: mergedData,
|
|
107
|
+
updatedAt: now,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Encrypt before storing
|
|
111
|
+
const encryptedUpdate = await this.encryptionService.encryptFields(
|
|
112
|
+
'Credential',
|
|
113
|
+
{ data: updateDocument.data }
|
|
114
|
+
);
|
|
115
|
+
|
|
77
116
|
await updateOne(
|
|
78
117
|
this.prisma,
|
|
79
118
|
'Credential',
|
|
80
119
|
{ _id: existing._id },
|
|
81
120
|
{
|
|
82
121
|
$set: {
|
|
83
|
-
userId:
|
|
84
|
-
externalId:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
data: mergedData,
|
|
89
|
-
updatedAt: now,
|
|
122
|
+
userId: updateDocument.userId,
|
|
123
|
+
externalId: updateDocument.externalId,
|
|
124
|
+
authIsValid: updateDocument.authIsValid,
|
|
125
|
+
data: encryptedUpdate.data,
|
|
126
|
+
updatedAt: updateDocument.updatedAt,
|
|
90
127
|
},
|
|
91
128
|
}
|
|
92
129
|
);
|
|
130
|
+
|
|
131
|
+
// Read back and decrypt
|
|
93
132
|
const updated = await findOne(this.prisma, 'Credential', { _id: existing._id });
|
|
94
|
-
|
|
133
|
+
const decryptedCredential = await this.encryptionService.decryptFields('Credential', updated);
|
|
134
|
+
return this._mapCredential(decryptedCredential);
|
|
95
135
|
}
|
|
96
136
|
|
|
97
|
-
|
|
137
|
+
// Build plain text document
|
|
138
|
+
const plainDocument = {
|
|
98
139
|
userId: toObjectId(userId || user || identifiers.user),
|
|
99
140
|
externalId: externalId !== undefined ? externalId : identifiers.externalId,
|
|
100
141
|
authIsValid: authIsValid ?? null,
|
|
@@ -103,15 +144,28 @@ class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
|
|
|
103
144
|
updatedAt: now,
|
|
104
145
|
};
|
|
105
146
|
|
|
106
|
-
|
|
147
|
+
// Encrypt before storing
|
|
148
|
+
const encryptedDocument = await this.encryptionService.encryptFields(
|
|
149
|
+
'Credential',
|
|
150
|
+
plainDocument
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const insertedId = await insertOne(this.prisma, 'Credential', encryptedDocument);
|
|
154
|
+
|
|
155
|
+
// Read back and decrypt
|
|
107
156
|
const created = await findOne(this.prisma, 'Credential', { _id: insertedId });
|
|
108
|
-
|
|
157
|
+
const decryptedCredential = await this.encryptionService.decryptFields('Credential', created);
|
|
158
|
+
return this._mapCredential(decryptedCredential);
|
|
109
159
|
}
|
|
110
160
|
|
|
111
161
|
async findCredential(filter) {
|
|
112
162
|
const query = this._buildFilter(filter);
|
|
113
163
|
const credential = await findOne(this.prisma, 'Credential', query);
|
|
114
|
-
|
|
164
|
+
if (!credential) return null;
|
|
165
|
+
|
|
166
|
+
// Decrypt sensitive fields using service
|
|
167
|
+
const decryptedCredential = await this.encryptionService.decryptFields('Credential', credential);
|
|
168
|
+
return this._mapCredential(decryptedCredential);
|
|
115
169
|
}
|
|
116
170
|
|
|
117
171
|
async updateCredential(credentialId, updates) {
|
|
@@ -128,26 +182,44 @@ class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
|
|
|
128
182
|
...oauthData
|
|
129
183
|
} = updates || {};
|
|
130
184
|
|
|
131
|
-
|
|
185
|
+
// Decrypt existing credential data first
|
|
186
|
+
const decryptedExisting = await this.encryptionService.decryptFields('Credential', existing);
|
|
187
|
+
const mergedData = { ...(decryptedExisting.data || {}), ...oauthData };
|
|
188
|
+
|
|
189
|
+
// Build update document
|
|
190
|
+
const updateDocument = {
|
|
191
|
+
userId: toObjectId(userId || user) || existing.userId || null,
|
|
192
|
+
externalId: externalId !== undefined ? externalId : existing.externalId,
|
|
193
|
+
authIsValid: authIsValid !== undefined ? authIsValid : existing.authIsValid,
|
|
194
|
+
data: mergedData,
|
|
195
|
+
updatedAt: new Date(),
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Encrypt before storing
|
|
199
|
+
const encryptedUpdate = await this.encryptionService.encryptFields(
|
|
200
|
+
'Credential',
|
|
201
|
+
{ data: updateDocument.data }
|
|
202
|
+
);
|
|
203
|
+
|
|
132
204
|
await updateOne(
|
|
133
205
|
this.prisma,
|
|
134
206
|
'Credential',
|
|
135
207
|
{ _id: objectId },
|
|
136
208
|
{
|
|
137
209
|
$set: {
|
|
138
|
-
userId:
|
|
139
|
-
externalId:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
data: mergedData,
|
|
144
|
-
updatedAt: new Date(),
|
|
210
|
+
userId: updateDocument.userId,
|
|
211
|
+
externalId: updateDocument.externalId,
|
|
212
|
+
authIsValid: updateDocument.authIsValid,
|
|
213
|
+
data: encryptedUpdate.data,
|
|
214
|
+
updatedAt: updateDocument.updatedAt,
|
|
145
215
|
},
|
|
146
216
|
}
|
|
147
217
|
);
|
|
148
218
|
|
|
219
|
+
// Read back and decrypt
|
|
149
220
|
const updated = await findOne(this.prisma, 'Credential', { _id: objectId });
|
|
150
|
-
|
|
221
|
+
const decryptedCredential = await this.encryptionService.decryptFields('Credential', updated);
|
|
222
|
+
return this._mapCredential(decryptedCredential);
|
|
151
223
|
}
|
|
152
224
|
|
|
153
225
|
_buildIdentifierFilter(identifiers) {
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
const { Cryptor } = require('../encrypt/Cryptor');
|
|
2
|
+
const { getEncryptedFields } = require('./encryption/encryption-schema-registry');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Encryption service specifically for DocumentDB repositories
|
|
6
|
+
* that use $runCommandRaw and bypass Prisma Client Extensions.
|
|
7
|
+
*
|
|
8
|
+
* Provides document-level encryption/decryption, handling nested fields
|
|
9
|
+
* according to the encryption schema registry.
|
|
10
|
+
*
|
|
11
|
+
* @class DocumentDBEncryptionService
|
|
12
|
+
* @example
|
|
13
|
+
* const service = new DocumentDBEncryptionService();
|
|
14
|
+
*
|
|
15
|
+
* // Encrypt before write
|
|
16
|
+
* const encrypted = await service.encryptFields('Credential', document);
|
|
17
|
+
* await insertOne(prisma, 'Credential', encrypted);
|
|
18
|
+
*
|
|
19
|
+
* // Decrypt after read
|
|
20
|
+
* const doc = await findOne(prisma, 'Credential', filter);
|
|
21
|
+
* const decrypted = await service.decryptFields('Credential', doc);
|
|
22
|
+
*/
|
|
23
|
+
class DocumentDBEncryptionService {
|
|
24
|
+
/**
|
|
25
|
+
* @param {Object} options - Configuration options
|
|
26
|
+
* @param {Cryptor} [options.cryptor] - Optional Cryptor instance for dependency injection (useful for testing)
|
|
27
|
+
*/
|
|
28
|
+
constructor({ cryptor = null } = {}) {
|
|
29
|
+
if (cryptor) {
|
|
30
|
+
// Dependency injection - use provided Cryptor (for testing)
|
|
31
|
+
this.cryptor = cryptor;
|
|
32
|
+
this.enabled = true;
|
|
33
|
+
} else {
|
|
34
|
+
// Default behavior - create Cryptor from environment
|
|
35
|
+
this._initializeCryptor();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Initialize Cryptor with environment-based configuration.
|
|
41
|
+
* Matches the logic from @friggframework/core/database/prisma.js
|
|
42
|
+
*
|
|
43
|
+
* Encryption is bypassed in dev/test/local stages.
|
|
44
|
+
* Production uses AWS KMS (if available) or AES encryption.
|
|
45
|
+
*
|
|
46
|
+
* @private
|
|
47
|
+
*/
|
|
48
|
+
_initializeCryptor() {
|
|
49
|
+
// Match logic from packages/core/database/prisma.js
|
|
50
|
+
const stage = process.env.STAGE || process.env.NODE_ENV || 'development';
|
|
51
|
+
const bypassEncryption = ['dev', 'test', 'local'].includes(stage.toLowerCase());
|
|
52
|
+
|
|
53
|
+
if (bypassEncryption) {
|
|
54
|
+
this.cryptor = null;
|
|
55
|
+
this.enabled = false;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Determine encryption method (ensure boolean values)
|
|
60
|
+
const hasKMS = !!(process.env.KMS_KEY_ARN && process.env.KMS_KEY_ARN.trim() !== '');
|
|
61
|
+
const hasAES = !!(process.env.AES_KEY_ID && process.env.AES_KEY_ID.trim() !== '');
|
|
62
|
+
|
|
63
|
+
if (!hasKMS && !hasAES) {
|
|
64
|
+
console.warn('[DocumentDBEncryptionService] No encryption keys configured. Encryption disabled.');
|
|
65
|
+
this.cryptor = null;
|
|
66
|
+
this.enabled = false;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// KMS takes precedence over AES
|
|
71
|
+
const shouldUseAws = hasKMS;
|
|
72
|
+
this.cryptor = new Cryptor({ shouldUseAws });
|
|
73
|
+
this.enabled = true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Encrypt sensitive fields in a document before storing to DocumentDB.
|
|
78
|
+
*
|
|
79
|
+
* Reads field paths from encryption-schema-registry.js and encrypts
|
|
80
|
+
* only the fields defined for the given model.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} modelName - Model name from schema registry (e.g., 'User', 'Credential')
|
|
83
|
+
* @param {Object} document - Document to encrypt
|
|
84
|
+
* @returns {Promise<Object>} - New document with encrypted fields (original unchanged)
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* const plainDoc = {
|
|
88
|
+
* userId: '123',
|
|
89
|
+
* data: { access_token: 'plain_secret' }
|
|
90
|
+
* };
|
|
91
|
+
* const encrypted = await service.encryptFields('Credential', plainDoc);
|
|
92
|
+
* // encrypted.data.access_token = "keyId:iv:cipher:encKey"
|
|
93
|
+
*/
|
|
94
|
+
async encryptFields(modelName, document) {
|
|
95
|
+
// Bypass if encryption disabled
|
|
96
|
+
if (!this.enabled || !this.cryptor) {
|
|
97
|
+
return document;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Validate input
|
|
101
|
+
if (!document || typeof document !== 'object') {
|
|
102
|
+
return document;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Get encrypted fields from registry
|
|
106
|
+
const encryptedFieldsConfig = getEncryptedFields(modelName);
|
|
107
|
+
if (!encryptedFieldsConfig || !encryptedFieldsConfig.fields || encryptedFieldsConfig.fields.length === 0) {
|
|
108
|
+
return document;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Deep clone to prevent mutation (preserves Date, RegExp, Buffer)
|
|
112
|
+
const result = structuredClone(document);
|
|
113
|
+
|
|
114
|
+
// Encrypt each field path
|
|
115
|
+
for (const fieldPath of encryptedFieldsConfig.fields) {
|
|
116
|
+
await this._encryptFieldPath(result, fieldPath, modelName);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Decrypt sensitive fields in a document after reading from DocumentDB.
|
|
124
|
+
*
|
|
125
|
+
* Reads field paths from encryption-schema-registry.js and decrypts
|
|
126
|
+
* only the fields defined for the given model.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} modelName - Model name from schema registry
|
|
129
|
+
* @param {Object} document - Document to decrypt
|
|
130
|
+
* @returns {Promise<Object>} - New document with decrypted fields (original unchanged)
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* const encryptedDoc = {
|
|
134
|
+
* userId: '123',
|
|
135
|
+
* data: { access_token: 'keyId:iv:cipher:encKey' }
|
|
136
|
+
* };
|
|
137
|
+
* const decrypted = await service.decryptFields('Credential', encryptedDoc);
|
|
138
|
+
* // decrypted.data.access_token = "plain_secret"
|
|
139
|
+
*/
|
|
140
|
+
async decryptFields(modelName, document) {
|
|
141
|
+
// Bypass if encryption disabled
|
|
142
|
+
if (!this.enabled || !this.cryptor) {
|
|
143
|
+
return document;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Validate input
|
|
147
|
+
if (!document || typeof document !== 'object') {
|
|
148
|
+
return document;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Get encrypted fields from registry
|
|
152
|
+
const encryptedFieldsConfig = getEncryptedFields(modelName);
|
|
153
|
+
if (!encryptedFieldsConfig || !encryptedFieldsConfig.fields || encryptedFieldsConfig.fields.length === 0) {
|
|
154
|
+
return document;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Deep clone to prevent mutation (preserves Date, RegExp, Buffer)
|
|
158
|
+
const result = structuredClone(document);
|
|
159
|
+
|
|
160
|
+
// Decrypt each field path
|
|
161
|
+
for (const fieldPath of encryptedFieldsConfig.fields) {
|
|
162
|
+
await this._decryptFieldPath(result, fieldPath, modelName);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Encrypt a specific field path in a document (handles nested fields).
|
|
170
|
+
*
|
|
171
|
+
* @private
|
|
172
|
+
* @param {Object} document - Document to modify (mutated in place)
|
|
173
|
+
* @param {string} fieldPath - Field path from schema registry (e.g., 'data.access_token')
|
|
174
|
+
* @param {string} modelName - For error logging context
|
|
175
|
+
*/
|
|
176
|
+
async _encryptFieldPath(document, fieldPath, modelName) {
|
|
177
|
+
// Parse field path
|
|
178
|
+
const parts = fieldPath.split('.');
|
|
179
|
+
|
|
180
|
+
// Navigate to parent object
|
|
181
|
+
let current = document;
|
|
182
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
183
|
+
if (!current[parts[i]]) {
|
|
184
|
+
// Path doesn't exist, nothing to encrypt
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
current = current[parts[i]];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Get field name and value
|
|
191
|
+
const fieldName = parts[parts.length - 1];
|
|
192
|
+
const value = current[fieldName];
|
|
193
|
+
|
|
194
|
+
// Skip if already encrypted or empty
|
|
195
|
+
if (!value || this._isEncryptedValue(value)) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
// Convert to string if needed
|
|
201
|
+
const stringValue = typeof value === 'string'
|
|
202
|
+
? value
|
|
203
|
+
: JSON.stringify(value);
|
|
204
|
+
|
|
205
|
+
// Encrypt using Cryptor
|
|
206
|
+
current[fieldName] = await this.cryptor.encrypt(stringValue);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.error(`[DocumentDBEncryptionService] Failed to encrypt ${modelName}.${fieldPath}:`, error.message);
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Decrypt a specific field path in a document (handles nested fields).
|
|
215
|
+
*
|
|
216
|
+
* @private
|
|
217
|
+
* @param {Object} document - Document to modify (mutated in place)
|
|
218
|
+
* @param {string} fieldPath - Field path from schema registry
|
|
219
|
+
* @param {string} modelName - For error logging context
|
|
220
|
+
*/
|
|
221
|
+
async _decryptFieldPath(document, fieldPath, modelName) {
|
|
222
|
+
// Parse field path
|
|
223
|
+
const parts = fieldPath.split('.');
|
|
224
|
+
|
|
225
|
+
// Navigate to parent object
|
|
226
|
+
let current = document;
|
|
227
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
228
|
+
if (!current[parts[i]]) {
|
|
229
|
+
// Path doesn't exist, nothing to decrypt
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
current = current[parts[i]];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Get field name and encrypted value
|
|
236
|
+
const fieldName = parts[parts.length - 1];
|
|
237
|
+
const encryptedValue = current[fieldName];
|
|
238
|
+
|
|
239
|
+
// Skip if not encrypted format
|
|
240
|
+
if (!encryptedValue || !this._isEncryptedValue(encryptedValue)) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
// Decrypt using Cryptor
|
|
246
|
+
const decryptedString = await this.cryptor.decrypt(encryptedValue);
|
|
247
|
+
|
|
248
|
+
// Try to parse as JSON (for objects/arrays)
|
|
249
|
+
try {
|
|
250
|
+
current[fieldName] = JSON.parse(decryptedString);
|
|
251
|
+
} catch {
|
|
252
|
+
// Not JSON, return as string
|
|
253
|
+
current[fieldName] = decryptedString;
|
|
254
|
+
}
|
|
255
|
+
} catch (error) {
|
|
256
|
+
const errorContext = {
|
|
257
|
+
modelName,
|
|
258
|
+
fieldPath,
|
|
259
|
+
encryptedValuePrefix: encryptedValue.substring(0, 20),
|
|
260
|
+
errorMessage: error.message
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
console.error(
|
|
264
|
+
`[DocumentDBEncryptionService] Failed to decrypt ${modelName}.${fieldPath}:`,
|
|
265
|
+
JSON.stringify(errorContext)
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Throw error to fail fast - don't silently corrupt data
|
|
269
|
+
throw new Error(`Decryption failed for ${modelName}.${fieldPath}: ${error.message}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Check if a value is in encrypted format.
|
|
275
|
+
*
|
|
276
|
+
* Encrypted format: "keyId:iv:cipher:encKey" (envelope encryption)
|
|
277
|
+
* All parts are base64-encoded strings.
|
|
278
|
+
*
|
|
279
|
+
* @private
|
|
280
|
+
* @param {any} value - Value to check
|
|
281
|
+
* @returns {boolean} - True if value is encrypted
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* _isEncryptedValue("plain_text") // false
|
|
285
|
+
* _isEncryptedValue("YWVzLWtleS0x:TXlJVkhlcmU=:QWN0dWFsQ2lwaGVy:RW5jcnlwdGVk") // true
|
|
286
|
+
* _isEncryptedValue(null) // false
|
|
287
|
+
* _isEncryptedValue({}) // false
|
|
288
|
+
*/
|
|
289
|
+
_isEncryptedValue(value) {
|
|
290
|
+
// Must be string
|
|
291
|
+
if (typeof value !== 'string') {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Must have exactly 4 colon-separated parts
|
|
296
|
+
const parts = value.split(':');
|
|
297
|
+
if (parts.length !== 4) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Enhanced validation: check for base64 pattern
|
|
302
|
+
// This prevents false positives on URLs, connection strings, etc.
|
|
303
|
+
const base64Pattern = /^[A-Za-z0-9+/=]+$/;
|
|
304
|
+
|
|
305
|
+
// All parts should be base64-encoded
|
|
306
|
+
if (!parts.every(part => base64Pattern.test(part))) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Encrypted values should be sufficiently long to be valid
|
|
311
|
+
// Real encrypted values from Cryptor are always >50 chars due to envelope encryption format:
|
|
312
|
+
// - keyId (base64): ~12 chars minimum
|
|
313
|
+
// - iv (base64): ~24 chars for 16-byte IV
|
|
314
|
+
// - ciphertext (base64): varies, minimum ~16 chars for small values
|
|
315
|
+
// - encryptedKey (base64): ~44 chars for 32-byte data key
|
|
316
|
+
// Total minimum: ~96 chars, so 50 is a safe lower bound
|
|
317
|
+
// This prevents false positives on non-encrypted strings that happen to have 4 colons
|
|
318
|
+
if (value.length < 50) {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
module.exports = { DocumentDBEncryptionService };
|
|
@@ -267,6 +267,14 @@ const CORE_ENCRYPTION_SCHEMA = {
|
|
|
267
267
|
- Integration-specific sensitive data (use custom schema instead)
|
|
268
268
|
- Temporary/experimental encryption (use custom schema instead)
|
|
269
269
|
|
|
270
|
+
#### After Adding Encrypted Fields
|
|
271
|
+
|
|
272
|
+
After adding fields to `encryption-schema-registry.js`:
|
|
273
|
+
|
|
274
|
+
1. **For MongoDB/PostgreSQL**: No code changes needed (automatic via Prisma Extension)
|
|
275
|
+
2. **For DocumentDB**: Encryption is automatic via DocumentDBEncryptionService
|
|
276
|
+
(service reads from same registry)
|
|
277
|
+
|
|
270
278
|
## How It Works
|
|
271
279
|
|
|
272
280
|
### Write Operation (Create/Update)
|
|
@@ -403,6 +411,48 @@ return entities.map((e) => ({
|
|
|
403
411
|
|
|
404
412
|
See `modules/repositories/module-repository-postgres.js` and `module-repository-mongo.js` for complete implementation examples using `_fetchCredential()` and `_fetchCredentialsBulk()` helper methods.
|
|
405
413
|
|
|
414
|
+
## DocumentDB Encryption
|
|
415
|
+
|
|
416
|
+
### Why DocumentDB Needs Manual Encryption
|
|
417
|
+
|
|
418
|
+
DocumentDB repositories use `$runCommandRaw()` for MongoDB protocol compatibility, which bypasses Prisma Client Extensions. This means the automatic encryption extension does not apply.
|
|
419
|
+
|
|
420
|
+
### DocumentDBEncryptionService
|
|
421
|
+
|
|
422
|
+
For DocumentDB repositories, use `DocumentDBEncryptionService` to manually encrypt/decrypt documents before/after database operations.
|
|
423
|
+
|
|
424
|
+
#### Usage Example
|
|
425
|
+
|
|
426
|
+
```javascript
|
|
427
|
+
const { DocumentDBEncryptionService } = require('../documentdb-encryption-service');
|
|
428
|
+
const { insertOne, findOne } = require('../documentdb-utils');
|
|
429
|
+
|
|
430
|
+
class MyRepositoryDocumentDB {
|
|
431
|
+
constructor() {
|
|
432
|
+
this.encryptionService = new DocumentDBEncryptionService();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async create(data) {
|
|
436
|
+
// Encrypt before write
|
|
437
|
+
const encrypted = await this.encryptionService.encryptFields('ModelName', data);
|
|
438
|
+
const id = await insertOne(this.prisma, 'CollectionName', encrypted);
|
|
439
|
+
|
|
440
|
+
// Decrypt after read
|
|
441
|
+
const doc = await findOne(this.prisma, 'CollectionName', { _id: id });
|
|
442
|
+
const decrypted = await this.encryptionService.decryptFields('ModelName', doc);
|
|
443
|
+
|
|
444
|
+
return decrypted;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
#### Configuration
|
|
450
|
+
|
|
451
|
+
Uses the same environment variables and Cryptor as the Prisma Extension:
|
|
452
|
+
- `STAGE`: Bypasses encryption for dev/test/local
|
|
453
|
+
- `KMS_KEY_ARN`: AWS KMS encryption (production)
|
|
454
|
+
- `AES_KEY_ID` + `AES_KEY`: AES encryption (fallback)
|
|
455
|
+
|
|
406
456
|
## Usage Examples
|
|
407
457
|
|
|
408
458
|
### Repository Code (No Changes Needed!)
|
|
@@ -9,111 +9,25 @@ const {
|
|
|
9
9
|
deleteOne,
|
|
10
10
|
} = require('../../database/documentdb-utils');
|
|
11
11
|
const { ModuleRepositoryInterface } = require('./module-repository-interface');
|
|
12
|
-
const {
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const { DocumentDBEncryptionService } = require('../../database/documentdb-encryption-service');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Module/Entity repository for DocumentDB.
|
|
16
|
+
* Uses DocumentDBEncryptionService for credential decryption.
|
|
17
|
+
*
|
|
18
|
+
* Encrypted fields: Credential.data.*
|
|
19
|
+
*
|
|
20
|
+
* Note: This repository only reads credentials. CredentialRepository
|
|
21
|
+
* handles credential creation/updates with encryption.
|
|
22
|
+
*
|
|
23
|
+
* @see DocumentDBEncryptionService
|
|
24
|
+
* @see CredentialRepositoryDocumentDB
|
|
25
|
+
*/
|
|
15
26
|
class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
|
|
16
27
|
constructor() {
|
|
17
28
|
super();
|
|
18
29
|
this.prisma = prisma;
|
|
19
|
-
this.
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
_initializeCryptor() {
|
|
23
|
-
// Match logic from @friggframework/core/database/prisma.js
|
|
24
|
-
const stage = process.env.STAGE || process.env.NODE_ENV || 'development';
|
|
25
|
-
const bypassEncryption = ['dev', 'test', 'local'].includes(stage.toLowerCase());
|
|
26
|
-
|
|
27
|
-
if (bypassEncryption) {
|
|
28
|
-
this.cryptor = null;
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Determine encryption method
|
|
33
|
-
const hasKMS = process.env.KMS_KEY_ARN && process.env.KMS_KEY_ARN.trim() !== '';
|
|
34
|
-
const hasAES = process.env.AES_KEY_ID && process.env.AES_KEY_ID.trim() !== '';
|
|
35
|
-
|
|
36
|
-
if (!hasKMS && !hasAES) {
|
|
37
|
-
console.warn('No encryption keys configured. Encryption disabled.');
|
|
38
|
-
this.cryptor = null;
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const shouldUseAws = hasKMS;
|
|
43
|
-
this.cryptor = new Cryptor({ shouldUseAws });
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
_isEncryptedValue(value) {
|
|
47
|
-
// Envelope encryption format: "keyId:encryptedPart1:encryptedPart2:encryptedKey"
|
|
48
|
-
// Must be string with at least 4 colon-separated parts
|
|
49
|
-
if (typeof value !== 'string') {
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const parts = value.split(':');
|
|
54
|
-
return parts.length >= 4;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async _decryptField(encryptedValue, context = '') {
|
|
58
|
-
// If encryption is disabled, return as-is
|
|
59
|
-
if (!this.cryptor) {
|
|
60
|
-
return encryptedValue;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// If not encrypted format, return as-is
|
|
64
|
-
if (!this._isEncryptedValue(encryptedValue)) {
|
|
65
|
-
return encryptedValue;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
// Decrypt using Cryptor
|
|
70
|
-
const decryptedString = await this.cryptor.decrypt(encryptedValue);
|
|
71
|
-
|
|
72
|
-
// Try to parse as JSON (for objects/arrays)
|
|
73
|
-
try {
|
|
74
|
-
return JSON.parse(decryptedString);
|
|
75
|
-
} catch {
|
|
76
|
-
// Not JSON, return as string
|
|
77
|
-
return decryptedString;
|
|
78
|
-
}
|
|
79
|
-
} catch (error) {
|
|
80
|
-
console.error(`Failed to decrypt field${context ? ` (${context})` : ''}:`, error.message);
|
|
81
|
-
// Return null on decryption failure to avoid exposing encrypted data
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async _decryptCredentialData(rawData) {
|
|
87
|
-
if (!rawData || typeof rawData !== 'object') {
|
|
88
|
-
return rawData;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Get encrypted fields from registry
|
|
92
|
-
const encryptedFieldsConfig = getEncryptedFields('Credential');
|
|
93
|
-
if (!encryptedFieldsConfig || !encryptedFieldsConfig.fields) {
|
|
94
|
-
return rawData;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const decrypted = {};
|
|
98
|
-
|
|
99
|
-
for (const [key, value] of Object.entries(rawData)) {
|
|
100
|
-
// Check if this field is in the encrypted fields list
|
|
101
|
-
const isEncrypted = encryptedFieldsConfig.fields.some(field => {
|
|
102
|
-
// Support both top-level and nested fields (e.g., 'data.access_token')
|
|
103
|
-
const fieldPath = field.split('.');
|
|
104
|
-
return fieldPath[fieldPath.length - 1] === key;
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
if (isEncrypted) {
|
|
108
|
-
// Decrypt encrypted fields
|
|
109
|
-
decrypted[key] = await this._decryptField(value, `Credential.data.${key}`);
|
|
110
|
-
} else {
|
|
111
|
-
// Pass through non-encrypted fields
|
|
112
|
-
decrypted[key] = value;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return decrypted;
|
|
30
|
+
this.encryptionService = new DocumentDBEncryptionService();
|
|
117
31
|
}
|
|
118
32
|
|
|
119
33
|
async findEntityById(entityId) {
|
|
@@ -254,25 +168,28 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
|
|
|
254
168
|
|
|
255
169
|
if (!rawCredential) return null;
|
|
256
170
|
|
|
257
|
-
//
|
|
258
|
-
const
|
|
259
|
-
rawCredential.data || {}
|
|
260
|
-
);
|
|
171
|
+
// Decrypt sensitive fields using service
|
|
172
|
+
const decryptedCredential = await this.encryptionService.decryptFields('Credential', rawCredential);
|
|
261
173
|
|
|
262
174
|
// Return in same format
|
|
263
175
|
const credential = {
|
|
264
|
-
id: fromObjectId(
|
|
265
|
-
userId: fromObjectId(
|
|
266
|
-
externalId:
|
|
267
|
-
authIsValid:
|
|
268
|
-
createdAt:
|
|
269
|
-
updatedAt:
|
|
270
|
-
data:
|
|
176
|
+
id: fromObjectId(decryptedCredential._id),
|
|
177
|
+
userId: fromObjectId(decryptedCredential.userId),
|
|
178
|
+
externalId: decryptedCredential.externalId ?? null,
|
|
179
|
+
authIsValid: decryptedCredential.authIsValid ?? null,
|
|
180
|
+
createdAt: decryptedCredential.createdAt,
|
|
181
|
+
updatedAt: decryptedCredential.updatedAt,
|
|
182
|
+
data: decryptedCredential.data
|
|
271
183
|
};
|
|
272
184
|
|
|
273
185
|
return this._convertCredentialIds(credential);
|
|
274
186
|
} catch (error) {
|
|
275
187
|
console.error(`Failed to fetch/decrypt credential ${id}:`, error.message);
|
|
188
|
+
// Return null instead of throwing to allow graceful degradation
|
|
189
|
+
// This repository is read-only (doesn't create/update credentials)
|
|
190
|
+
// Entities can still be loaded even if their credential is corrupted/unreadable
|
|
191
|
+
// The entity will have null credential, which calling code must handle
|
|
192
|
+
// This is intentional behavior: prefer partial data over complete failure
|
|
276
193
|
return null;
|
|
277
194
|
}
|
|
278
195
|
}
|
|
@@ -296,20 +213,18 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
|
|
|
296
213
|
// Decrypt all credentials in parallel
|
|
297
214
|
const decryptionPromises = rawCredentials.map(async (rawCredential) => {
|
|
298
215
|
try {
|
|
299
|
-
//
|
|
300
|
-
const
|
|
301
|
-
rawCredential.data || {}
|
|
302
|
-
);
|
|
216
|
+
// Decrypt sensitive fields using service
|
|
217
|
+
const decryptedCredential = await this.encryptionService.decryptFields('Credential', rawCredential);
|
|
303
218
|
|
|
304
219
|
// Build credential object in same format as Prisma would return
|
|
305
220
|
const credential = {
|
|
306
|
-
id: fromObjectId(
|
|
307
|
-
userId: fromObjectId(
|
|
308
|
-
externalId:
|
|
309
|
-
authIsValid:
|
|
310
|
-
createdAt:
|
|
311
|
-
updatedAt:
|
|
312
|
-
data:
|
|
221
|
+
id: fromObjectId(decryptedCredential._id),
|
|
222
|
+
userId: fromObjectId(decryptedCredential.userId),
|
|
223
|
+
externalId: decryptedCredential.externalId ?? null,
|
|
224
|
+
authIsValid: decryptedCredential.authIsValid ?? null,
|
|
225
|
+
createdAt: decryptedCredential.createdAt,
|
|
226
|
+
updatedAt: decryptedCredential.updatedAt,
|
|
227
|
+
data: decryptedCredential.data
|
|
313
228
|
};
|
|
314
229
|
|
|
315
230
|
return this._convertCredentialIds(credential);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0--canary.499.
|
|
4
|
+
"version": "2.0.0--canary.499.5d51f03.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.588.0",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@friggframework/eslint-config": "2.0.0--canary.499.
|
|
42
|
-
"@friggframework/prettier-config": "2.0.0--canary.499.
|
|
43
|
-
"@friggframework/test": "2.0.0--canary.499.
|
|
41
|
+
"@friggframework/eslint-config": "2.0.0--canary.499.5d51f03.0",
|
|
42
|
+
"@friggframework/prettier-config": "2.0.0--canary.499.5d51f03.0",
|
|
43
|
+
"@friggframework/test": "2.0.0--canary.499.5d51f03.0",
|
|
44
44
|
"@prisma/client": "^6.17.0",
|
|
45
45
|
"@types/lodash": "4.17.15",
|
|
46
46
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -80,5 +80,5 @@
|
|
|
80
80
|
"publishConfig": {
|
|
81
81
|
"access": "public"
|
|
82
82
|
},
|
|
83
|
-
"gitHead": "
|
|
83
|
+
"gitHead": "5d51f03b5b8de0c3d48bf6055f492ad5c761e5fa"
|
|
84
84
|
}
|
|
@@ -10,141 +10,23 @@ const {
|
|
|
10
10
|
} = require('../../database/documentdb-utils');
|
|
11
11
|
const { createTokenRepository } = require('../../token/repositories/token-repository-factory');
|
|
12
12
|
const { UserRepositoryInterface } = require('./user-repository-interface');
|
|
13
|
-
const {
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
const { DocumentDBEncryptionService } = require('../../database/documentdb-encryption-service');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* User repository for DocumentDB.
|
|
17
|
+
* Uses DocumentDBEncryptionService for field-level encryption.
|
|
18
|
+
*
|
|
19
|
+
* Encrypted fields: User.hashword
|
|
20
|
+
*
|
|
21
|
+
* @see DocumentDBEncryptionService
|
|
22
|
+
* @see encryption-schema-registry.js
|
|
23
|
+
*/
|
|
16
24
|
class UserRepositoryDocumentDB extends UserRepositoryInterface {
|
|
17
25
|
constructor() {
|
|
18
26
|
super();
|
|
19
27
|
this.prisma = prisma;
|
|
20
28
|
this.tokenRepository = createTokenRepository();
|
|
21
|
-
this.
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
_initializeCryptor() {
|
|
25
|
-
// Match logic from @friggframework/core/database/prisma.js
|
|
26
|
-
const stage = process.env.STAGE || process.env.NODE_ENV || 'development';
|
|
27
|
-
const bypassEncryption = ['dev', 'test', 'local'].includes(stage.toLowerCase());
|
|
28
|
-
|
|
29
|
-
if (bypassEncryption) {
|
|
30
|
-
this.cryptor = null;
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Determine encryption method
|
|
35
|
-
const hasKMS = process.env.KMS_KEY_ARN && process.env.KMS_KEY_ARN.trim() !== '';
|
|
36
|
-
const hasAES = process.env.AES_KEY_ID && process.env.AES_KEY_ID.trim() !== '';
|
|
37
|
-
|
|
38
|
-
if (!hasKMS && !hasAES) {
|
|
39
|
-
console.warn('No encryption keys configured. Encryption disabled.');
|
|
40
|
-
this.cryptor = null;
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const shouldUseAws = hasKMS;
|
|
45
|
-
this.cryptor = new Cryptor({ shouldUseAws });
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
_isEncryptedValue(value) {
|
|
49
|
-
// Envelope encryption format: "keyId:encryptedPart1:encryptedPart2:encryptedKey"
|
|
50
|
-
// Must be string with at least 4 colon-separated parts
|
|
51
|
-
if (typeof value !== 'string') {
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const parts = value.split(':');
|
|
56
|
-
return parts.length >= 4;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async _decryptField(encryptedValue, context = '') {
|
|
60
|
-
// If encryption is disabled, return as-is
|
|
61
|
-
if (!this.cryptor) {
|
|
62
|
-
return encryptedValue;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// If not encrypted format, return as-is
|
|
66
|
-
if (!this._isEncryptedValue(encryptedValue)) {
|
|
67
|
-
return encryptedValue;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
// Decrypt using Cryptor
|
|
72
|
-
const decryptedString = await this.cryptor.decrypt(encryptedValue);
|
|
73
|
-
|
|
74
|
-
// Try to parse as JSON (for objects/arrays)
|
|
75
|
-
try {
|
|
76
|
-
return JSON.parse(decryptedString);
|
|
77
|
-
} catch {
|
|
78
|
-
// Not JSON, return as string
|
|
79
|
-
return decryptedString;
|
|
80
|
-
}
|
|
81
|
-
} catch (error) {
|
|
82
|
-
console.error(`Failed to decrypt field${context ? ` (${context})` : ''}:`, error.message);
|
|
83
|
-
// Return null on decryption failure to avoid exposing encrypted data
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async _encryptField(plainValue, context = '') {
|
|
89
|
-
// If encryption is disabled, return as-is
|
|
90
|
-
if (!this.cryptor) {
|
|
91
|
-
return plainValue;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Don't encrypt null/undefined
|
|
95
|
-
if (plainValue === null || plainValue === undefined) {
|
|
96
|
-
return plainValue;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
// Convert objects/arrays to JSON string
|
|
101
|
-
const stringValue = typeof plainValue === 'string'
|
|
102
|
-
? plainValue
|
|
103
|
-
: JSON.stringify(plainValue);
|
|
104
|
-
|
|
105
|
-
// Encrypt using Cryptor
|
|
106
|
-
return await this.cryptor.encrypt(stringValue);
|
|
107
|
-
} catch (error) {
|
|
108
|
-
console.error(`Failed to encrypt field${context ? ` (${context})` : ''}:`, error.message);
|
|
109
|
-
throw error;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async _decryptHashword(rawUser) {
|
|
114
|
-
if (!rawUser || !rawUser.hashword) {
|
|
115
|
-
return rawUser;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Get encrypted fields from registry
|
|
119
|
-
const encryptedFieldsConfig = getEncryptedFields('User');
|
|
120
|
-
const shouldDecrypt = encryptedFieldsConfig?.fields?.includes('hashword');
|
|
121
|
-
|
|
122
|
-
if (!shouldDecrypt) {
|
|
123
|
-
return rawUser;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const decryptedHashword = await this._decryptField(rawUser.hashword, 'User.hashword');
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
...rawUser,
|
|
130
|
-
hashword: decryptedHashword
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async _encryptHashword(hashword) {
|
|
135
|
-
if (!hashword) {
|
|
136
|
-
return hashword;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Get encrypted fields from registry
|
|
140
|
-
const encryptedFieldsConfig = getEncryptedFields('User');
|
|
141
|
-
const shouldEncrypt = encryptedFieldsConfig?.fields?.includes('hashword');
|
|
142
|
-
|
|
143
|
-
if (!shouldEncrypt) {
|
|
144
|
-
return hashword;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return await this._encryptField(hashword, 'User.hashword');
|
|
29
|
+
this.encryptionService = new DocumentDBEncryptionService();
|
|
148
30
|
}
|
|
149
31
|
|
|
150
32
|
async getSessionToken(token) {
|
|
@@ -158,7 +40,7 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
|
|
|
158
40
|
_id: toObjectId(userId),
|
|
159
41
|
type: 'ORGANIZATION',
|
|
160
42
|
});
|
|
161
|
-
const decrypted = await this.
|
|
43
|
+
const decrypted = await this.encryptionService.decryptFields('User', doc);
|
|
162
44
|
return this._mapUser(decrypted);
|
|
163
45
|
}
|
|
164
46
|
|
|
@@ -167,7 +49,7 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
|
|
|
167
49
|
_id: toObjectId(userId),
|
|
168
50
|
type: 'INDIVIDUAL',
|
|
169
51
|
});
|
|
170
|
-
const decrypted = await this.
|
|
52
|
+
const decrypted = await this.encryptionService.decryptFields('User', doc);
|
|
171
53
|
return this._mapUser(decrypted);
|
|
172
54
|
}
|
|
173
55
|
|
|
@@ -212,13 +94,12 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
|
|
|
212
94
|
}
|
|
213
95
|
|
|
214
96
|
// Bcrypt hash the password
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
// Encrypt the bcrypt hash if encryption is enabled
|
|
218
|
-
document.hashword = await this._encryptHashword(hashedPassword);
|
|
97
|
+
document.hashword = await bcrypt.hash(params.hashword, 10);
|
|
219
98
|
}
|
|
220
99
|
|
|
221
|
-
|
|
100
|
+
// Encrypt sensitive fields before insert
|
|
101
|
+
const encryptedDocument = await this.encryptionService.encryptFields('User', document);
|
|
102
|
+
const insertedId = await insertOne(this.prisma, 'User', encryptedDocument);
|
|
222
103
|
const created = await findOne(this.prisma, 'User', { _id: insertedId });
|
|
223
104
|
|
|
224
105
|
// Defensive check: verify document was found after insert
|
|
@@ -237,8 +118,8 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
|
|
|
237
118
|
);
|
|
238
119
|
}
|
|
239
120
|
|
|
240
|
-
// Decrypt
|
|
241
|
-
const decrypted = await this.
|
|
121
|
+
// Decrypt sensitive fields after read
|
|
122
|
+
const decrypted = await this.encryptionService.decryptFields('User', created);
|
|
242
123
|
|
|
243
124
|
return this._mapUser(decrypted);
|
|
244
125
|
}
|
|
@@ -263,7 +144,7 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
|
|
|
263
144
|
type: 'INDIVIDUAL',
|
|
264
145
|
username,
|
|
265
146
|
});
|
|
266
|
-
const decrypted = await this.
|
|
147
|
+
const decrypted = await this.encryptionService.decryptFields('User', doc);
|
|
267
148
|
return this._mapUser(decrypted);
|
|
268
149
|
}
|
|
269
150
|
|
|
@@ -272,7 +153,7 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
|
|
|
272
153
|
type: 'INDIVIDUAL',
|
|
273
154
|
appUserId,
|
|
274
155
|
});
|
|
275
|
-
const decrypted = await this.
|
|
156
|
+
const decrypted = await this.encryptionService.decryptFields('User', doc);
|
|
276
157
|
return this._mapUser(decrypted);
|
|
277
158
|
}
|
|
278
159
|
|
|
@@ -281,13 +162,13 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
|
|
|
281
162
|
type: 'ORGANIZATION',
|
|
282
163
|
appOrgId,
|
|
283
164
|
});
|
|
284
|
-
const decrypted = await this.
|
|
165
|
+
const decrypted = await this.encryptionService.decryptFields('User', doc);
|
|
285
166
|
return this._mapUser(decrypted);
|
|
286
167
|
}
|
|
287
168
|
|
|
288
169
|
async findUserById(userId) {
|
|
289
170
|
const doc = await findOne(this.prisma, 'User', { _id: toObjectId(userId) });
|
|
290
|
-
const decrypted = await this.
|
|
171
|
+
const decrypted = await this.encryptionService.decryptFields('User', doc);
|
|
291
172
|
return this._mapUser(decrypted);
|
|
292
173
|
}
|
|
293
174
|
|
|
@@ -296,7 +177,7 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
|
|
|
296
177
|
type: 'INDIVIDUAL',
|
|
297
178
|
email,
|
|
298
179
|
});
|
|
299
|
-
const decrypted = await this.
|
|
180
|
+
const decrypted = await this.encryptionService.decryptFields('User', doc);
|
|
300
181
|
return this._mapUser(decrypted);
|
|
301
182
|
}
|
|
302
183
|
|
|
@@ -307,20 +188,18 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
|
|
|
307
188
|
const payload = await this._prepareUpdatePayload(updates);
|
|
308
189
|
payload.updatedAt = new Date();
|
|
309
190
|
|
|
310
|
-
// Encrypt
|
|
311
|
-
|
|
312
|
-
payload.hashword = await this._encryptHashword(payload.hashword);
|
|
313
|
-
}
|
|
191
|
+
// Encrypt sensitive fields before update
|
|
192
|
+
const encryptedPayload = await this.encryptionService.encryptFields('User', payload);
|
|
314
193
|
|
|
315
194
|
await updateOne(
|
|
316
195
|
this.prisma,
|
|
317
196
|
'User',
|
|
318
197
|
{ _id: objectId, type: 'INDIVIDUAL' },
|
|
319
|
-
{ $set:
|
|
198
|
+
{ $set: encryptedPayload }
|
|
320
199
|
);
|
|
321
200
|
|
|
322
201
|
const updated = await findOne(this.prisma, 'User', { _id: objectId });
|
|
323
|
-
const decrypted = await this.
|
|
202
|
+
const decrypted = await this.encryptionService.decryptFields('User', updated);
|
|
324
203
|
return this._mapUser(decrypted);
|
|
325
204
|
}
|
|
326
205
|
|
|
@@ -338,7 +217,7 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
|
|
|
338
217
|
);
|
|
339
218
|
|
|
340
219
|
const updated = await findOne(this.prisma, 'User', { _id: objectId });
|
|
341
|
-
const decrypted = await this.
|
|
220
|
+
const decrypted = await this.encryptionService.decryptFields('User', updated);
|
|
342
221
|
return this._mapUser(decrypted);
|
|
343
222
|
}
|
|
344
223
|
|