@vollcrypt/db-guard 0.1.2 → 0.9.1
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/LICENSE +16 -0
- package/LICENSE-COMMERCIAL.md +71 -0
- package/LICENSE-GPL +674 -0
- package/dist/blind-index.d.ts +1 -7
- package/dist/blind-index.js +3 -22
- package/dist/cli.js +3 -3
- package/dist/drivers.js +52 -16
- package/dist/drizzle.js +23 -22
- package/dist/index.d.ts +2 -6
- package/dist/index.js +4 -12
- package/dist/kms.d.ts +10 -0
- package/dist/kms.js +58 -2
- package/dist/mongoose.d.ts +1 -1
- package/dist/mongoose.js +33 -24
- package/dist/prisma.d.ts +4 -24
- package/dist/prisma.js +44 -176
- package/dist/provenance.json +37 -21
- package/dist/provenance.json.sig +2 -1
- package/dist/sbom.json +45 -21
- package/dist/sbom.json.sig +1 -2
- package/dist/security.d.ts +11 -3
- package/dist/security.js +223 -76
- package/dist/typeorm.d.ts +1 -1
- package/dist/typeorm.js +12 -11
- package/package.json +105 -50
package/dist/sbom.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bomFormat": "CycloneDX",
|
|
3
3
|
"specVersion": "1.5",
|
|
4
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"serialNumber": "urn:uuid:54f5fdbd-84b0-47fe-95c1-e86fe565dcf8",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "2026-
|
|
7
|
+
"timestamp": "2026-07-09T19:07:03.950Z",
|
|
8
8
|
"tools": [
|
|
9
9
|
{
|
|
10
10
|
"vendor": "Vollcrypt",
|
|
@@ -13,19 +13,19 @@
|
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"component": {
|
|
16
|
-
"bomRef": "pkg:npm/@vollcrypt/db-guard@0.1
|
|
16
|
+
"bomRef": "pkg:npm/@vollcrypt/db-guard@0.9.1",
|
|
17
17
|
"type": "library",
|
|
18
18
|
"name": "@vollcrypt/db-guard",
|
|
19
|
-
"version": "0.1
|
|
19
|
+
"version": "0.9.1",
|
|
20
20
|
"description": "Database field-level encryption integrations for Vollcrypt (Prisma, Mongoose, Drizzle, TypeORM)",
|
|
21
21
|
"hashes": [
|
|
22
22
|
{
|
|
23
23
|
"alg": "SHA-256",
|
|
24
|
-
"content": "
|
|
24
|
+
"content": "54df5b23b93c61111ba6124b9557fb4fe3f5e4af8ef18d1aeb5477826679363f"
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"alg": "SHA-256",
|
|
28
|
-
"content": "
|
|
28
|
+
"content": "d417966c32408676efa5399b6e960b68d6015d66837b9b2bad9fa0bb9396b4be"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"alg": "SHA-256",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"alg": "SHA-256",
|
|
36
|
-
"content": "
|
|
36
|
+
"content": "33c313c9195a9e00099652c3e8fac9eac169ac3b94088222fbe97e7a2e51442a"
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
"alg": "SHA-256",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
51
|
"alg": "SHA-256",
|
|
52
|
-
"content": "
|
|
52
|
+
"content": "fac1fd60cc16c2dafa9daf234177fd2f17d41cf1015d4fcd1821adb2cdbdb970"
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
"alg": "SHA-256",
|
|
@@ -57,55 +57,55 @@
|
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
59
|
"alg": "SHA-256",
|
|
60
|
-
"content": "
|
|
60
|
+
"content": "3686b5a03b627e3262257f6a6d2d3addcb7f2cf3377ece458c434aad3b15d2ba"
|
|
61
61
|
},
|
|
62
62
|
{
|
|
63
63
|
"alg": "SHA-256",
|
|
64
|
-
"content": "
|
|
64
|
+
"content": "ed2935e0d96814bcf3192294462ae707acfe6934ce2aed9f859683aed88a3ea8"
|
|
65
65
|
},
|
|
66
66
|
{
|
|
67
67
|
"alg": "SHA-256",
|
|
68
|
-
"content": "
|
|
68
|
+
"content": "37b74d238138bb80b65341520257b9390be3cc5b9b1f5adf579894eb4995c2ab"
|
|
69
69
|
},
|
|
70
70
|
{
|
|
71
71
|
"alg": "SHA-256",
|
|
72
|
-
"content": "
|
|
72
|
+
"content": "757dba651cf48b6ab68e11b5c92e8ccc81c1ca6eb9129f60a2a3b224fc4f712e"
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
75
|
"alg": "SHA-256",
|
|
76
|
-
"content": "
|
|
76
|
+
"content": "1619f068398e57c2044457d18d24ee1c626b54e12153246093d4c51add64368c"
|
|
77
77
|
},
|
|
78
78
|
{
|
|
79
79
|
"alg": "SHA-256",
|
|
80
|
-
"content": "
|
|
80
|
+
"content": "18f77aa838f2cd861cd31dd5a03fdfd86d34563a090340177dba1420b5ea9b53"
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
"alg": "SHA-256",
|
|
84
|
-
"content": "
|
|
84
|
+
"content": "0b7c921d254a3c12d6d0924f3778ac39d7e95383086f1f8a893ff361a29c380b"
|
|
85
85
|
},
|
|
86
86
|
{
|
|
87
87
|
"alg": "SHA-256",
|
|
88
|
-
"content": "
|
|
88
|
+
"content": "d55f39802671b594be9c0a0512c06cb1828569dea9024c78e890c53085cf0232"
|
|
89
89
|
},
|
|
90
90
|
{
|
|
91
91
|
"alg": "SHA-256",
|
|
92
|
-
"content": "
|
|
92
|
+
"content": "de57df0c3fc6657a996c804d6deee6ff83f889f457c82f55ea35064df6372eab"
|
|
93
93
|
},
|
|
94
94
|
{
|
|
95
95
|
"alg": "SHA-256",
|
|
96
|
-
"content": "
|
|
96
|
+
"content": "dbe2dce83e417dc20d476e5bd476e31c3cf43e58fd363011a77dbc56ab82386f"
|
|
97
97
|
},
|
|
98
98
|
{
|
|
99
99
|
"alg": "SHA-256",
|
|
100
|
-
"content": "
|
|
100
|
+
"content": "390eb8dbf9f2eab968445dadc43e1dfa6a13217851aa2b5138a5215059c487ed"
|
|
101
101
|
},
|
|
102
102
|
{
|
|
103
103
|
"alg": "SHA-256",
|
|
104
|
-
"content": "
|
|
104
|
+
"content": "47200df068a368cdd80b5818ae8dafc749686f8a09a0be8bd9847b175dd89fe6"
|
|
105
105
|
},
|
|
106
106
|
{
|
|
107
107
|
"alg": "SHA-256",
|
|
108
|
-
"content": "
|
|
108
|
+
"content": "80e1908e97ffc89c426e89c71efbbd7dbea98191ec7da8ecc9d37cc439a262d8"
|
|
109
109
|
}
|
|
110
110
|
]
|
|
111
111
|
},
|
|
@@ -141,6 +141,30 @@
|
|
|
141
141
|
"version": ">=0.3.0",
|
|
142
142
|
"purl": "pkg:npm/typeorm@0.3.0"
|
|
143
143
|
},
|
|
144
|
+
{
|
|
145
|
+
"type": "library",
|
|
146
|
+
"name": "@aws-sdk/client-kms",
|
|
147
|
+
"version": ">=3.0.0",
|
|
148
|
+
"purl": "pkg:npm/@aws-sdk/client-kms@3.0.0"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"type": "library",
|
|
152
|
+
"name": "@google-cloud/kms",
|
|
153
|
+
"version": ">=3.0.0",
|
|
154
|
+
"purl": "pkg:npm/@google-cloud/kms@3.0.0"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"type": "library",
|
|
158
|
+
"name": "node-vault",
|
|
159
|
+
"version": ">=0.9.0",
|
|
160
|
+
"purl": "pkg:npm/node-vault@0.9.0"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"type": "library",
|
|
164
|
+
"name": "pkcs11js",
|
|
165
|
+
"version": ">=1.0.0",
|
|
166
|
+
"purl": "pkg:npm/pkcs11js@1.0.0"
|
|
167
|
+
},
|
|
144
168
|
{
|
|
145
169
|
"type": "library",
|
|
146
170
|
"name": "rust-crate:aes-gcm",
|
package/dist/sbom.json.sig
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
�
|
|
1
|
+
�0��cWA��8��c.*�+�F֛�U��g{s_7O�=��$�V�¿��Я�n9�Ҧ�
|
package/dist/security.d.ts
CHANGED
|
@@ -46,11 +46,11 @@ export declare function activateBreakGlass(signatures: {
|
|
|
46
46
|
signature: string;
|
|
47
47
|
timestamp: number;
|
|
48
48
|
}[], emergencyBackupKey: Buffer): void;
|
|
49
|
-
export declare function registerKeysForZeroization(keys: Record<string, Buffer
|
|
50
|
-
export declare function triggerFailClosed(onFailClosedCallback?: () => void): void;
|
|
49
|
+
export declare function registerKeysForZeroization(keys: Record<string, Buffer>, tenantId?: string): void;
|
|
50
|
+
export declare function triggerFailClosed(onFailClosedCallback?: () => void, tenantId?: string): void;
|
|
51
51
|
export declare function checkRateLimit(options?: RateLimiterOptions): void;
|
|
52
52
|
export declare function checkPageSize(count: number, options?: RateLimiterOptions): 'ok' | 'warn' | 'bypass' | 'error';
|
|
53
|
-
export declare function getFailClosedStatus(): boolean;
|
|
53
|
+
export declare function getFailClosedStatus(tenantId?: string): boolean;
|
|
54
54
|
export declare function resetFailClosedStatusForTesting(): void;
|
|
55
55
|
export interface AuditLogEntry {
|
|
56
56
|
timestamp: string;
|
|
@@ -88,3 +88,11 @@ export declare function parseCiphertext(stored: string): {
|
|
|
88
88
|
version: string;
|
|
89
89
|
base64Data: string;
|
|
90
90
|
} | null;
|
|
91
|
+
/**
|
|
92
|
+
* Computes a hardened, frequency-resistant blind index for a database field.
|
|
93
|
+
*/
|
|
94
|
+
export declare function computeBlindIndex(value: any, rootSalt: Buffer, columnName: string): string;
|
|
95
|
+
export declare function encryptValue(val: any, key: Buffer, version: string): string;
|
|
96
|
+
export declare function decryptValue(stored: any, keys: Record<string, Buffer>): any;
|
|
97
|
+
export declare function rewriteQueryWhere(where: any, fields: string[], rootSalt: Buffer, modelName: string): void;
|
|
98
|
+
export declare function addBlindIndexes(data: any, fields: string[], rootSalt: Buffer, modelName: string): void;
|
package/dist/security.js
CHANGED
|
@@ -65,6 +65,11 @@ exports.resetAuditLoggerForTesting = resetAuditLoggerForTesting;
|
|
|
65
65
|
exports.logDecryption = logDecryption;
|
|
66
66
|
exports.decryptWithSecurity = decryptWithSecurity;
|
|
67
67
|
exports.parseCiphertext = parseCiphertext;
|
|
68
|
+
exports.computeBlindIndex = computeBlindIndex;
|
|
69
|
+
exports.encryptValue = encryptValue;
|
|
70
|
+
exports.decryptValue = decryptValue;
|
|
71
|
+
exports.rewriteQueryWhere = rewriteQueryWhere;
|
|
72
|
+
exports.addBlindIndexes = addBlindIndexes;
|
|
68
73
|
const async_hooks_1 = require("async_hooks");
|
|
69
74
|
const crypto = __importStar(require("crypto"));
|
|
70
75
|
const fs = __importStar(require("fs"));
|
|
@@ -209,15 +214,14 @@ function maskValue(val, rule) {
|
|
|
209
214
|
return '***';
|
|
210
215
|
}
|
|
211
216
|
}
|
|
212
|
-
let decryptCount = 0;
|
|
213
|
-
let windowStart = Date.now();
|
|
214
|
-
let isFailClosed = false;
|
|
215
|
-
const globalKeysToZeroize = [];
|
|
216
217
|
// Ephemeral Master Key generated randomly on startup
|
|
217
218
|
let ephemeralMasterKey = crypto.randomBytes(32);
|
|
219
|
+
const tenantFailClosed = new Map();
|
|
220
|
+
const tenantKeys = new Map();
|
|
221
|
+
const tenantRateLimitStates = new Map();
|
|
218
222
|
const secureKeyCache = new Map();
|
|
219
223
|
function getCachedKey(tenantId, version) {
|
|
220
|
-
const cacheKey =
|
|
224
|
+
const cacheKey = JSON.stringify([tenantId || 'global', version]);
|
|
221
225
|
const entry = secureKeyCache.get(cacheKey);
|
|
222
226
|
if (!entry)
|
|
223
227
|
return undefined;
|
|
@@ -234,7 +238,7 @@ function getCachedKey(tenantId, version) {
|
|
|
234
238
|
}
|
|
235
239
|
}
|
|
236
240
|
function setCachedKey(tenantId, version, plaintextKey, ttlMs = 120000) {
|
|
237
|
-
const cacheKey =
|
|
241
|
+
const cacheKey = JSON.stringify([tenantId || 'global', version]);
|
|
238
242
|
const existing = secureKeyCache.get(cacheKey);
|
|
239
243
|
if (existing) {
|
|
240
244
|
existing.wrappedKey.fill(0);
|
|
@@ -327,27 +331,58 @@ function activateBreakGlass(signatures, emergencyBackupKey) {
|
|
|
327
331
|
isBreakGlassActiveFlag = true;
|
|
328
332
|
logDecryption('SYSTEM', 'BREAK_GLASS_ACTIVATED', undefined);
|
|
329
333
|
}
|
|
330
|
-
function registerKeysForZeroization(keys) {
|
|
331
|
-
|
|
332
|
-
|
|
334
|
+
function registerKeysForZeroization(keys, tenantId) {
|
|
335
|
+
const tId = tenantId || 'global';
|
|
336
|
+
let list = tenantKeys.get(tId);
|
|
337
|
+
if (!list) {
|
|
338
|
+
list = [];
|
|
339
|
+
tenantKeys.set(tId, list);
|
|
340
|
+
}
|
|
341
|
+
if (!list.includes(keys)) {
|
|
342
|
+
list.push(keys);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function triggerFailClosed(onFailClosedCallback, tenantId) {
|
|
346
|
+
const tId = tenantId || exports.dbGuardContextStore.getStore()?.tenantId || 'global';
|
|
347
|
+
tenantFailClosed.set(tId, true);
|
|
348
|
+
// Zeroize all registered keys immediately in memory for this tenant
|
|
349
|
+
const list = tenantKeys.get(tId);
|
|
350
|
+
if (list && list.length > 0) {
|
|
351
|
+
for (const keyMap of list) {
|
|
352
|
+
for (const key of Object.values(keyMap)) {
|
|
353
|
+
key.fill(0);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
333
356
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
357
|
+
else {
|
|
358
|
+
// Fallback to global keys if no tenant-specific keys are registered
|
|
359
|
+
const globalList = tenantKeys.get('global');
|
|
360
|
+
if (globalList) {
|
|
361
|
+
for (const keyMap of globalList) {
|
|
362
|
+
for (const key of Object.values(keyMap)) {
|
|
363
|
+
key.fill(0);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
341
366
|
}
|
|
342
367
|
}
|
|
343
368
|
// Zeroize cache and ephemeral master key
|
|
344
|
-
for (const entry of secureKeyCache.
|
|
345
|
-
|
|
369
|
+
for (const [cacheKey, entry] of secureKeyCache.entries()) {
|
|
370
|
+
try {
|
|
371
|
+
const parsed = JSON.parse(cacheKey);
|
|
372
|
+
if (Array.isArray(parsed) && parsed[0] === tId) {
|
|
373
|
+
entry.wrappedKey.fill(0);
|
|
374
|
+
secureKeyCache.delete(cacheKey);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
// fallback
|
|
379
|
+
}
|
|
346
380
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
381
|
+
if (tId === 'global') {
|
|
382
|
+
ephemeralMasterKey.fill(0);
|
|
383
|
+
if (breakGlassEmergencyKey) {
|
|
384
|
+
breakGlassEmergencyKey.fill(0);
|
|
385
|
+
}
|
|
351
386
|
}
|
|
352
387
|
if (onFailClosedCallback) {
|
|
353
388
|
try {
|
|
@@ -357,59 +392,45 @@ function triggerFailClosed(onFailClosedCallback) {
|
|
|
357
392
|
// prevent user callback crash from blocking zeroization
|
|
358
393
|
}
|
|
359
394
|
}
|
|
360
|
-
throw new Error(
|
|
395
|
+
throw new Error(`Vollcrypt Security: Decryption rate limit exceeded. Fail-Closed mode triggered for tenant "${tId}". Keys zeroized.`);
|
|
361
396
|
}
|
|
362
397
|
function checkRateLimit(options) {
|
|
363
|
-
if (isFailClosed) {
|
|
364
|
-
throw new Error('Vollcrypt Security: Fail-Closed mode is active. Decryption blocked.');
|
|
365
|
-
}
|
|
366
398
|
const context = exports.dbGuardContextStore.getStore();
|
|
399
|
+
const tId = context?.tenantId || 'global';
|
|
400
|
+
if (tenantFailClosed.get(tId)) {
|
|
401
|
+
throw new Error(`Vollcrypt Security: Fail-Closed mode is active for tenant "${tId}". Decryption blocked.`);
|
|
402
|
+
}
|
|
367
403
|
if (context?.bypassRateLimit) {
|
|
368
404
|
return; // Rate limit check bypassed for this request context
|
|
369
405
|
}
|
|
370
406
|
const limit = context?.maxDecryptionsPerSecond || options?.maxDecryptionsPerSecond || 500;
|
|
371
407
|
const mode = context?.rateLimiterMode || options?.mode || 'fail_closed';
|
|
372
408
|
const now = Date.now();
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
else if (mode === 'warn') {
|
|
388
|
-
console.warn(`Vollcrypt Warning: Decryption rate limit exceeded. ${context.decryptCount} decryptions in the current window (limit: ${limit}).`);
|
|
389
|
-
}
|
|
409
|
+
let state = tenantRateLimitStates.get(tId);
|
|
410
|
+
if (!state) {
|
|
411
|
+
state = { decryptCount: 0, windowStart: now };
|
|
412
|
+
tenantRateLimitStates.set(tId, state);
|
|
413
|
+
}
|
|
414
|
+
if (now - state.windowStart > 1000) {
|
|
415
|
+
state.decryptCount = 0;
|
|
416
|
+
state.windowStart = now;
|
|
417
|
+
}
|
|
418
|
+
state.decryptCount++;
|
|
419
|
+
if (state.decryptCount > limit) {
|
|
420
|
+
if (mode === 'fail_closed') {
|
|
421
|
+
triggerFailClosed(options?.onFailClosed, tId);
|
|
390
422
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if (now - windowStart > 1000) {
|
|
394
|
-
decryptCount = 0;
|
|
395
|
-
windowStart = now;
|
|
396
|
-
}
|
|
397
|
-
decryptCount++;
|
|
398
|
-
if (decryptCount > limit) {
|
|
399
|
-
if (mode === 'fail_closed') {
|
|
400
|
-
triggerFailClosed(options?.onFailClosed);
|
|
401
|
-
}
|
|
402
|
-
else if (mode === 'warn') {
|
|
403
|
-
console.warn(`Vollcrypt Warning: Decryption rate limit exceeded. ${decryptCount} decryptions in the current window (limit: ${limit}).`);
|
|
404
|
-
}
|
|
423
|
+
else if (mode === 'warn') {
|
|
424
|
+
console.warn(`Vollcrypt Warning: Decryption rate limit exceeded for tenant "${tId}". ${state.decryptCount} decryptions in the current window (limit: ${limit}).`);
|
|
405
425
|
}
|
|
406
426
|
}
|
|
407
427
|
}
|
|
408
428
|
function checkPageSize(count, options) {
|
|
409
|
-
if (isFailClosed) {
|
|
410
|
-
throw new Error('Vollcrypt Security: Fail-Closed mode is active. Decryption blocked.');
|
|
411
|
-
}
|
|
412
429
|
const context = exports.dbGuardContextStore.getStore();
|
|
430
|
+
const tId = context?.tenantId || 'global';
|
|
431
|
+
if (tenantFailClosed.get(tId)) {
|
|
432
|
+
throw new Error(`Vollcrypt Security: Fail-Closed mode is active for tenant "${tId}". Decryption blocked.`);
|
|
433
|
+
}
|
|
413
434
|
const maxPageSize = context?.maxPageSize !== undefined
|
|
414
435
|
? context.maxPageSize
|
|
415
436
|
: (options?.maxPageSize !== undefined ? options.maxPageSize : 250);
|
|
@@ -430,17 +451,19 @@ function checkPageSize(count, options) {
|
|
|
430
451
|
}
|
|
431
452
|
return 'ok';
|
|
432
453
|
}
|
|
433
|
-
function getFailClosedStatus() {
|
|
434
|
-
|
|
454
|
+
function getFailClosedStatus(tenantId) {
|
|
455
|
+
const tId = tenantId || exports.dbGuardContextStore.getStore()?.tenantId || 'global';
|
|
456
|
+
return tenantFailClosed.get(tId) || false;
|
|
435
457
|
}
|
|
436
458
|
function resetFailClosedStatusForTesting() {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
459
|
+
tenantFailClosed.clear();
|
|
460
|
+
tenantRateLimitStates.clear();
|
|
461
|
+
tenantKeys.clear();
|
|
440
462
|
}
|
|
441
463
|
let lastLogHash = '0'.repeat(64);
|
|
442
464
|
let auditLogPath;
|
|
443
465
|
let onAuditLogCallback;
|
|
466
|
+
let auditWriteQueue = Promise.resolve();
|
|
444
467
|
function configureAuditLogger(options) {
|
|
445
468
|
if (options?.path) {
|
|
446
469
|
auditLogPath = options.path;
|
|
@@ -470,6 +493,7 @@ function resetAuditLoggerForTesting() {
|
|
|
470
493
|
lastLogHash = '0'.repeat(64);
|
|
471
494
|
auditLogPath = undefined;
|
|
472
495
|
onAuditLogCallback = undefined;
|
|
496
|
+
auditWriteQueue = Promise.resolve();
|
|
473
497
|
}
|
|
474
498
|
function logDecryption(model, field, recordId) {
|
|
475
499
|
const context = exports.dbGuardContextStore.getStore();
|
|
@@ -497,12 +521,11 @@ function logDecryption(model, field, recordId) {
|
|
|
497
521
|
}
|
|
498
522
|
}
|
|
499
523
|
if (auditLogPath) {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
}
|
|
524
|
+
const line = JSON.stringify(fullEntry) + '\n';
|
|
525
|
+
const currentPath = auditLogPath;
|
|
526
|
+
auditWriteQueue = auditWriteQueue.then(() => {
|
|
527
|
+
return fs.promises.appendFile(currentPath, line, 'utf8').catch(() => { });
|
|
528
|
+
});
|
|
506
529
|
}
|
|
507
530
|
}
|
|
508
531
|
function decryptWithSecurity(stored, decryptRawFn, modelName, fieldName, recordId, options) {
|
|
@@ -543,7 +566,8 @@ function decryptWithSecurity(stored, decryptRawFn, modelName, fieldName, recordI
|
|
|
543
566
|
return result;
|
|
544
567
|
}
|
|
545
568
|
exports.VERSION_ALGORITHMS = {
|
|
546
|
-
'1': '1'
|
|
569
|
+
'1': '1',
|
|
570
|
+
'2': '1'
|
|
547
571
|
};
|
|
548
572
|
exports.CRYPTO_ALGORITHMS = {
|
|
549
573
|
'1': {
|
|
@@ -557,12 +581,135 @@ function parseCiphertext(stored) {
|
|
|
557
581
|
const content = stored.slice('VOLLVALT:'.length);
|
|
558
582
|
if (content.startsWith('v')) {
|
|
559
583
|
const colon = content.indexOf(':');
|
|
560
|
-
if (colon === -1)
|
|
561
|
-
|
|
584
|
+
if (colon === -1) {
|
|
585
|
+
throw new Error("Vollcrypt Security: Malformed ciphertext format.");
|
|
586
|
+
}
|
|
562
587
|
const versionPart = content.slice(1, colon);
|
|
563
588
|
const base64Part = content.slice(colon + 1);
|
|
564
|
-
const algoId = exports.VERSION_ALGORITHMS[versionPart]
|
|
589
|
+
const algoId = exports.VERSION_ALGORITHMS[versionPart];
|
|
590
|
+
if (!algoId) {
|
|
591
|
+
throw new Error(`Vollcrypt Security: Deprecated or unsupported encryption version "v${versionPart}".`);
|
|
592
|
+
}
|
|
565
593
|
return { algoId, version: versionPart, base64Data: base64Part };
|
|
566
594
|
}
|
|
567
|
-
|
|
595
|
+
throw new Error("Vollcrypt Security: Legacy unversioned ciphertexts are deprecated and unsupported.");
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Computes a hardened, frequency-resistant blind index for a database field.
|
|
599
|
+
*/
|
|
600
|
+
function computeBlindIndex(value, rootSalt, columnName) {
|
|
601
|
+
if (value === null || value === undefined)
|
|
602
|
+
return value;
|
|
603
|
+
const plaintext = typeof value === 'string' ? value : JSON.stringify(value);
|
|
604
|
+
const columnNameBuf = Buffer.from(columnName, 'utf8');
|
|
605
|
+
// 1. Derive column-specific key using HKDF-SHA256
|
|
606
|
+
const derivedColumnKey = deriveHkdf(rootSalt, null, columnNameBuf, 32);
|
|
607
|
+
try {
|
|
608
|
+
// 2. Compute the final blind index using the derived column key
|
|
609
|
+
const plaintextBuf = Buffer.from(plaintext, 'utf8');
|
|
610
|
+
const blindIndex = deriveHkdf(derivedColumnKey, null, plaintextBuf, 32);
|
|
611
|
+
return blindIndex.toString('hex');
|
|
612
|
+
}
|
|
613
|
+
finally {
|
|
614
|
+
// 3. RAM Security: Zeroize the derived key immediately (Anti-Core Dump)
|
|
615
|
+
derivedColumnKey.fill(0);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
function encryptValue(val, key, version) {
|
|
619
|
+
if (val === null || val === undefined)
|
|
620
|
+
return val;
|
|
621
|
+
const context = exports.dbGuardContextStore.getStore();
|
|
622
|
+
const tId = context?.tenantId || 'global';
|
|
623
|
+
if (key.every(b => b === 0) || getFailClosedStatus(tId)) {
|
|
624
|
+
throw new Error(`Vollcrypt Security: Fail-Closed mode is active for tenant "${tId}". Encryption blocked.`);
|
|
625
|
+
}
|
|
626
|
+
const plaintext = typeof val === 'string' ? val : JSON.stringify(val);
|
|
627
|
+
const plaintextBuf = Buffer.from(plaintext, 'utf8');
|
|
628
|
+
const encrypted = exports.CRYPTO_ALGORITHMS['1'].encrypt(plaintextBuf, key);
|
|
629
|
+
// RAM Security: Zeroize the plaintext buffer immediately
|
|
630
|
+
plaintextBuf.fill(0);
|
|
631
|
+
return `VOLLVALT:v${version}:${encrypted.toString('base64')}`;
|
|
632
|
+
}
|
|
633
|
+
function decryptValue(stored, keys) {
|
|
634
|
+
if (typeof stored !== 'string') {
|
|
635
|
+
return stored;
|
|
636
|
+
}
|
|
637
|
+
const parsed = parseCiphertext(stored);
|
|
638
|
+
if (!parsed) {
|
|
639
|
+
return stored;
|
|
640
|
+
}
|
|
641
|
+
const { algoId, version, base64Data } = parsed;
|
|
642
|
+
const key = keys[version];
|
|
643
|
+
if (!key) {
|
|
644
|
+
throw new Error(`Decryption key version "${version}" not found in registered keys`);
|
|
645
|
+
}
|
|
646
|
+
if (key.every(b => b === 0)) {
|
|
647
|
+
throw new Error(`Vollcrypt Security: Decryption blocked. Key version "${version}" is zeroized due to a Fail-Closed event.`);
|
|
648
|
+
}
|
|
649
|
+
try {
|
|
650
|
+
const encryptedBuf = Buffer.from(base64Data, 'base64');
|
|
651
|
+
const decryptor = exports.CRYPTO_ALGORITHMS[algoId];
|
|
652
|
+
if (!decryptor) {
|
|
653
|
+
throw new Error(`Unsupported decryption algorithm ID "${algoId}"`);
|
|
654
|
+
}
|
|
655
|
+
const decrypted = decryptor.decrypt(encryptedBuf, key);
|
|
656
|
+
const plaintext = decrypted.toString('utf8');
|
|
657
|
+
// RAM Security: Zeroize the decrypted buffer
|
|
658
|
+
decrypted.fill(0);
|
|
659
|
+
try {
|
|
660
|
+
return JSON.parse(plaintext);
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
return plaintext;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
catch (err) {
|
|
667
|
+
throw new Error(`Failed to decrypt field value: ${err.message}`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
function rewriteQueryWhere(where, fields, rootSalt, modelName) {
|
|
671
|
+
if (!where || typeof where !== 'object')
|
|
672
|
+
return;
|
|
673
|
+
for (const field of fields) {
|
|
674
|
+
if (where[field] !== undefined) {
|
|
675
|
+
const val = where[field];
|
|
676
|
+
const bidxField = `${field}_bidx`;
|
|
677
|
+
if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {
|
|
678
|
+
where[bidxField] = computeBlindIndex(val, rootSalt, `${modelName}.${field}`);
|
|
679
|
+
delete where[field];
|
|
680
|
+
}
|
|
681
|
+
else if (val && typeof val === 'object') {
|
|
682
|
+
if (val.equals !== undefined) {
|
|
683
|
+
where[bidxField] = {
|
|
684
|
+
equals: computeBlindIndex(val.equals, rootSalt, `${modelName}.${field}`),
|
|
685
|
+
};
|
|
686
|
+
delete where[field];
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
// Recurse into compound logical operators
|
|
692
|
+
const operators = ['AND', 'OR', 'NOT'];
|
|
693
|
+
for (const op of operators) {
|
|
694
|
+
if (Array.isArray(where[op])) {
|
|
695
|
+
where[op].forEach((item) => rewriteQueryWhere(item, fields, rootSalt, modelName));
|
|
696
|
+
}
|
|
697
|
+
else if (where[op] && typeof where[op] === 'object') {
|
|
698
|
+
rewriteQueryWhere(where[op], fields, rootSalt, modelName);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
function addBlindIndexes(data, fields, rootSalt, modelName) {
|
|
703
|
+
if (!data || typeof data !== 'object')
|
|
704
|
+
return;
|
|
705
|
+
if (Array.isArray(data)) {
|
|
706
|
+
data.forEach((item) => addBlindIndexes(item, fields, rootSalt, modelName));
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
for (const field of fields) {
|
|
710
|
+
if (data[field] !== undefined && data[field] !== null) {
|
|
711
|
+
const bidxField = `${field}_bidx`;
|
|
712
|
+
data[bidxField] = computeBlindIndex(data[field], rootSalt, `${modelName}.${field}`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
568
715
|
}
|
package/dist/typeorm.d.ts
CHANGED