@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/dist/sbom.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.5",
4
- "serialNumber": "urn:uuid:45a7c8f7-12f3-4e25-ab52-81be917b4646",
4
+ "serialNumber": "urn:uuid:54f5fdbd-84b0-47fe-95c1-e86fe565dcf8",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-06-11T16:17:36.609Z",
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.2",
16
+ "bomRef": "pkg:npm/@vollcrypt/db-guard@0.9.1",
17
17
  "type": "library",
18
18
  "name": "@vollcrypt/db-guard",
19
- "version": "0.1.2",
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": "7e613b58cf47bc14d647cfbfa2a9cc0f9e805dbab7562daa00ac679c30830ba7"
24
+ "content": "54df5b23b93c61111ba6124b9557fb4fe3f5e4af8ef18d1aeb5477826679363f"
25
25
  },
26
26
  {
27
27
  "alg": "SHA-256",
28
- "content": "fb28a7038cc818346aeb212ece2e5d4f1db4b72fd3ed9dda78a1a7f5744d7455"
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": "bf91d9b9d820796590d8b030e409031623d177e45cc487da8bcb66af13078ec0"
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": "44623a5cf632c22a41b8196b809a3c00d50276bde28c2fbde13d6f4bef3177ce"
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": "aad49d4c9dd41f2e44175465da6ae7f4a7ae35337ce12f7b3a87113ec0f25424"
60
+ "content": "3686b5a03b627e3262257f6a6d2d3addcb7f2cf3377ece458c434aad3b15d2ba"
61
61
  },
62
62
  {
63
63
  "alg": "SHA-256",
64
- "content": "a4864c4ea0bb9e9cc9bc1f58cdbc31863e5543a572e6049ee900b53b9fecb003"
64
+ "content": "ed2935e0d96814bcf3192294462ae707acfe6934ce2aed9f859683aed88a3ea8"
65
65
  },
66
66
  {
67
67
  "alg": "SHA-256",
68
- "content": "496292f1edd16bd779f6e8712291c4b90e5b3f404d71b54f5c369c6736c26814"
68
+ "content": "37b74d238138bb80b65341520257b9390be3cc5b9b1f5adf579894eb4995c2ab"
69
69
  },
70
70
  {
71
71
  "alg": "SHA-256",
72
- "content": "822f37608176917f438febaaea06a3b65508c45474bd7e137c4df815c50b4854"
72
+ "content": "757dba651cf48b6ab68e11b5c92e8ccc81c1ca6eb9129f60a2a3b224fc4f712e"
73
73
  },
74
74
  {
75
75
  "alg": "SHA-256",
76
- "content": "4b1a32b35545e2e7944ebba3f84272b7421ffedd1cc3c7eb569e1a6b2fcc4a02"
76
+ "content": "1619f068398e57c2044457d18d24ee1c626b54e12153246093d4c51add64368c"
77
77
  },
78
78
  {
79
79
  "alg": "SHA-256",
80
- "content": "a3bf00d62fecf9266325a9dd18c4ec272de9559355dbf3f9f43836a4688fe86b"
80
+ "content": "18f77aa838f2cd861cd31dd5a03fdfd86d34563a090340177dba1420b5ea9b53"
81
81
  },
82
82
  {
83
83
  "alg": "SHA-256",
84
- "content": "42b2177825713fa1c618b80af6cf7a5d9f4055399c8e95b67018a0410add2a3f"
84
+ "content": "0b7c921d254a3c12d6d0924f3778ac39d7e95383086f1f8a893ff361a29c380b"
85
85
  },
86
86
  {
87
87
  "alg": "SHA-256",
88
- "content": "f6b85a786a9bb964f0620df4bcad0787e5c972e853df35551cf8fc902bb79691"
88
+ "content": "d55f39802671b594be9c0a0512c06cb1828569dea9024c78e890c53085cf0232"
89
89
  },
90
90
  {
91
91
  "alg": "SHA-256",
92
- "content": "e2b7a8fa24c120f59ab48ebb508a18c01e3e24245e04c197b08ad0e327a2d9a8"
92
+ "content": "de57df0c3fc6657a996c804d6deee6ff83f889f457c82f55ea35064df6372eab"
93
93
  },
94
94
  {
95
95
  "alg": "SHA-256",
96
- "content": "554d9ba00f800470666ab66127079df889b8d717a9cf7b28ab252f0adad64304"
96
+ "content": "dbe2dce83e417dc20d476e5bd476e31c3cf43e58fd363011a77dbc56ab82386f"
97
97
  },
98
98
  {
99
99
  "alg": "SHA-256",
100
- "content": "5e907c9f5ec9fb93ef07f6360d7f8572aa5a1ea1c0a32d0b10496477d0b8fd33"
100
+ "content": "390eb8dbf9f2eab968445dadc43e1dfa6a13217851aa2b5138a5215059c487ed"
101
101
  },
102
102
  {
103
103
  "alg": "SHA-256",
104
- "content": "6cc82bbbe82e260abcc789495fe85f984138c5c8368901881edaece1ffa46d8c"
104
+ "content": "47200df068a368cdd80b5818ae8dafc749686f8a09a0be8bd9847b175dd89fe6"
105
105
  },
106
106
  {
107
107
  "alg": "SHA-256",
108
- "content": "154827069a5496e043a9f69b3249bd5dddd62690783dfa8675d06d0f574ac76b"
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",
@@ -1,2 +1 @@
1
- ��7gu
2
- �
1
+ �0��cWA��8��c.*�+�F֛�U��g{s_7O�=��$�V�¿��Я�n9�Ҧ�
@@ -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>): void;
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 = `${tenantId || 'global'}:${version}`;
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 = `${tenantId || 'global'}:${version}`;
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
- if (!globalKeysToZeroize.includes(keys)) {
332
- globalKeysToZeroize.push(keys);
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
- function triggerFailClosed(onFailClosedCallback) {
336
- isFailClosed = true;
337
- // Zeroize all registered keys immediately in memory
338
- for (const keyMap of globalKeysToZeroize) {
339
- for (const key of Object.values(keyMap)) {
340
- key.fill(0);
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.values()) {
345
- entry.wrappedKey.fill(0);
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
- secureKeyCache.clear();
348
- ephemeralMasterKey.fill(0);
349
- if (breakGlassEmergencyKey) {
350
- breakGlassEmergencyKey.fill(0);
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('Vollcrypt Security: Decryption rate limit exceeded. Fail-Closed mode triggered. Keys zeroized.');
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
- if (context) {
374
- if (context.windowStart === undefined || context.decryptCount === undefined) {
375
- context.windowStart = now;
376
- context.decryptCount = 0;
377
- }
378
- if (now - context.windowStart > 1000) {
379
- context.decryptCount = 0;
380
- context.windowStart = now;
381
- }
382
- context.decryptCount++;
383
- if (context.decryptCount > limit) {
384
- if (mode === 'fail_closed') {
385
- triggerFailClosed(options?.onFailClosed);
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
- else {
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
- return isFailClosed;
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
- isFailClosed = false;
438
- decryptCount = 0;
439
- windowStart = Date.now();
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
- try {
501
- fs.appendFileSync(auditLogPath, JSON.stringify(fullEntry) + '\n', 'utf8');
502
- }
503
- catch {
504
- // prevent filesystem errors from throwing
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
- return null;
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] || '1';
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
- return null;
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
@@ -1,4 +1,4 @@
1
- import { InsertEvent, UpdateEvent } from 'typeorm';
1
+ import type { InsertEvent, UpdateEvent } from 'typeorm';
2
2
  import { RateLimiterOptions } from './security';
3
3
  export interface TypeOrmDbGuardOptions {
4
4
  key: Buffer | Record<string, Buffer>;