@vollcrypt/db-guard 0.1.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/README.md +271 -0
- package/compliance-config.json +39 -0
- package/dist/blind-index.d.ts +7 -0
- package/dist/blind-index.js +24 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +211 -0
- package/dist/compliance-report.html +562 -0
- package/dist/compliance.d.ts +40 -0
- package/dist/compliance.js +659 -0
- package/dist/drizzle.d.ts +65 -0
- package/dist/drizzle.js +118 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +44 -0
- package/dist/kms.d.ts +46 -0
- package/dist/kms.js +154 -0
- package/dist/mongoose.d.ts +30 -0
- package/dist/mongoose.js +317 -0
- package/dist/prisma.d.ts +54 -0
- package/dist/prisma.js +390 -0
- package/dist/provenance.json +222 -0
- package/dist/provenance.json.sig +1 -0
- package/dist/sbom.json +209 -0
- package/dist/sbom.json.sig +1 -0
- package/dist/security.d.ts +88 -0
- package/dist/security.js +547 -0
- package/dist/typeorm.d.ts +26 -0
- package/dist/typeorm.js +149 -0
- package/package.json +50 -0
package/dist/security.js
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CRYPTO_ALGORITHMS = exports.VERSION_ALGORITHMS = exports.dbGuardContextStore = void 0;
|
|
37
|
+
exports.wrapKey = wrapKey;
|
|
38
|
+
exports.unwrapKey = unwrapKey;
|
|
39
|
+
exports.calculatePadding = calculatePadding;
|
|
40
|
+
exports.padMessageWithLen = padMessageWithLen;
|
|
41
|
+
exports.unpadMessageWithLen = unpadMessageWithLen;
|
|
42
|
+
exports.encryptAesGcmPadded = encryptAesGcmPadded;
|
|
43
|
+
exports.decryptAesGcmPadded = decryptAesGcmPadded;
|
|
44
|
+
exports.verifySignature = verifySignature;
|
|
45
|
+
exports.deriveHkdf = deriveHkdf;
|
|
46
|
+
exports.generateEd25519Keypair = generateEd25519Keypair;
|
|
47
|
+
exports.signMessage = signMessage;
|
|
48
|
+
exports.maskValue = maskValue;
|
|
49
|
+
exports.getCachedKey = getCachedKey;
|
|
50
|
+
exports.setCachedKey = setCachedKey;
|
|
51
|
+
exports.resetSecureKeyCacheForTesting = resetSecureKeyCacheForTesting;
|
|
52
|
+
exports.configureBreakGlass = configureBreakGlass;
|
|
53
|
+
exports.deactivateBreakGlass = deactivateBreakGlass;
|
|
54
|
+
exports.isBreakGlassActive = isBreakGlassActive;
|
|
55
|
+
exports.getBreakGlassKey = getBreakGlassKey;
|
|
56
|
+
exports.activateBreakGlass = activateBreakGlass;
|
|
57
|
+
exports.registerKeysForZeroization = registerKeysForZeroization;
|
|
58
|
+
exports.triggerFailClosed = triggerFailClosed;
|
|
59
|
+
exports.checkRateLimit = checkRateLimit;
|
|
60
|
+
exports.checkPageSize = checkPageSize;
|
|
61
|
+
exports.getFailClosedStatus = getFailClosedStatus;
|
|
62
|
+
exports.resetFailClosedStatusForTesting = resetFailClosedStatusForTesting;
|
|
63
|
+
exports.configureAuditLogger = configureAuditLogger;
|
|
64
|
+
exports.resetAuditLoggerForTesting = resetAuditLoggerForTesting;
|
|
65
|
+
exports.logDecryption = logDecryption;
|
|
66
|
+
exports.decryptWithSecurity = decryptWithSecurity;
|
|
67
|
+
exports.parseCiphertext = parseCiphertext;
|
|
68
|
+
const async_hooks_1 = require("async_hooks");
|
|
69
|
+
const crypto = __importStar(require("crypto"));
|
|
70
|
+
const fs = __importStar(require("fs"));
|
|
71
|
+
const KEY_WRAP_IV = Buffer.from('A6A6A6A6A6A6A6A6', 'hex');
|
|
72
|
+
function wrapKey(kek, keyToWrap) {
|
|
73
|
+
if (kek.length !== 32) {
|
|
74
|
+
throw new Error('KEK must be exactly 32 bytes');
|
|
75
|
+
}
|
|
76
|
+
const cipher = crypto.createCipheriv('id-aes256-wrap', kek, KEY_WRAP_IV);
|
|
77
|
+
return Buffer.concat([cipher.update(keyToWrap), cipher.final()]);
|
|
78
|
+
}
|
|
79
|
+
function unwrapKey(kek, wrappedKey) {
|
|
80
|
+
if (kek.length !== 32) {
|
|
81
|
+
throw new Error('KEK must be exactly 32 bytes');
|
|
82
|
+
}
|
|
83
|
+
const decipher = crypto.createDecipheriv('id-aes256-wrap', kek, KEY_WRAP_IV);
|
|
84
|
+
return Buffer.concat([decipher.update(wrappedKey), decipher.final()]);
|
|
85
|
+
}
|
|
86
|
+
function calculatePadding(contentLen) {
|
|
87
|
+
const sizes = [64, 128, 256, 512, 1024, 2048];
|
|
88
|
+
const minPadding = 2;
|
|
89
|
+
let target = sizes.find(s => s >= contentLen + minPadding);
|
|
90
|
+
if (target === undefined) {
|
|
91
|
+
const remainder = (contentLen + minPadding) % 1024;
|
|
92
|
+
if (remainder === 0) {
|
|
93
|
+
target = contentLen + minPadding;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
target = contentLen + minPadding + (1024 - remainder);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const paddingLen = target - contentLen;
|
|
100
|
+
return crypto.randomBytes(paddingLen);
|
|
101
|
+
}
|
|
102
|
+
function padMessageWithLen(content) {
|
|
103
|
+
const lenBytes = Buffer.alloc(4);
|
|
104
|
+
lenBytes.writeUInt32BE(content.length, 0);
|
|
105
|
+
const baseLen = 4 + content.length;
|
|
106
|
+
const paddingBytes = calculatePadding(baseLen);
|
|
107
|
+
return Buffer.concat([lenBytes, content, paddingBytes]);
|
|
108
|
+
}
|
|
109
|
+
function unpadMessageWithLen(padded) {
|
|
110
|
+
if (padded.length < 4) {
|
|
111
|
+
throw new Error('Padded message too short');
|
|
112
|
+
}
|
|
113
|
+
const len = padded.readUInt32BE(0);
|
|
114
|
+
if (len > padded.length - 4) {
|
|
115
|
+
throw new Error('Invalid padded message length');
|
|
116
|
+
}
|
|
117
|
+
return padded.subarray(4, 4 + len);
|
|
118
|
+
}
|
|
119
|
+
function encryptAesGcmPadded(key, plaintext, aad = null) {
|
|
120
|
+
const padded = padMessageWithLen(plaintext);
|
|
121
|
+
const iv = crypto.randomBytes(12);
|
|
122
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
123
|
+
if (aad) {
|
|
124
|
+
cipher.setAAD(aad);
|
|
125
|
+
}
|
|
126
|
+
const ciphertext = Buffer.concat([cipher.update(padded), cipher.final()]);
|
|
127
|
+
const tag = cipher.getAuthTag();
|
|
128
|
+
return Buffer.concat([iv, ciphertext, tag]);
|
|
129
|
+
}
|
|
130
|
+
function decryptAesGcmPadded(key, encryptedData, aad = null) {
|
|
131
|
+
const iv = encryptedData.subarray(0, 12);
|
|
132
|
+
const tag = encryptedData.subarray(encryptedData.length - 16);
|
|
133
|
+
const ciphertext = encryptedData.subarray(12, encryptedData.length - 16);
|
|
134
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
135
|
+
decipher.setAuthTag(tag);
|
|
136
|
+
if (aad) {
|
|
137
|
+
decipher.setAAD(aad);
|
|
138
|
+
}
|
|
139
|
+
const padded = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
140
|
+
return unpadMessageWithLen(padded);
|
|
141
|
+
}
|
|
142
|
+
function verifySignature(publicKey, message, signature) {
|
|
143
|
+
try {
|
|
144
|
+
const spkiHeader = Buffer.from('302a300506032b6570032100', 'hex');
|
|
145
|
+
const pubKey = crypto.createPublicKey({
|
|
146
|
+
key: Buffer.concat([spkiHeader, publicKey]),
|
|
147
|
+
format: 'der',
|
|
148
|
+
type: 'spki'
|
|
149
|
+
});
|
|
150
|
+
return crypto.verify(null, message, pubKey, signature);
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function deriveHkdf(ikm, salt, info, keyLen) {
|
|
157
|
+
return Buffer.from(crypto.hkdfSync('sha256', ikm, salt || Buffer.alloc(0), info || Buffer.alloc(0), keyLen));
|
|
158
|
+
}
|
|
159
|
+
function generateEd25519Keypair() {
|
|
160
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
|
|
161
|
+
const pkBytes = publicKey.export({ type: 'spki', format: 'der' }).subarray(12);
|
|
162
|
+
const skBytes = privateKey.export({ type: 'pkcs8', format: 'der' }).subarray(16);
|
|
163
|
+
return [skBytes, pkBytes];
|
|
164
|
+
}
|
|
165
|
+
function signMessage(secretKey, message) {
|
|
166
|
+
const pkcs8Header = Buffer.from('302e020100300506032b657004220420', 'hex');
|
|
167
|
+
const privateKeyObj = crypto.createPrivateKey({
|
|
168
|
+
key: Buffer.concat([pkcs8Header, secretKey]),
|
|
169
|
+
format: 'der',
|
|
170
|
+
type: 'pkcs8'
|
|
171
|
+
});
|
|
172
|
+
return crypto.sign(null, message, privateKeyObj);
|
|
173
|
+
}
|
|
174
|
+
// 1. Request Context Store (AsyncLocalStorage)
|
|
175
|
+
exports.dbGuardContextStore = new async_hooks_1.AsyncLocalStorage();
|
|
176
|
+
// 2. Dynamic Data Masking (DDM)
|
|
177
|
+
function maskValue(val, rule) {
|
|
178
|
+
if (val === null || val === undefined)
|
|
179
|
+
return val;
|
|
180
|
+
const str = typeof val === 'string' ? val : String(val);
|
|
181
|
+
if (typeof rule === 'function') {
|
|
182
|
+
return rule(str);
|
|
183
|
+
}
|
|
184
|
+
switch (rule) {
|
|
185
|
+
case 'credit_card':
|
|
186
|
+
if (str.length >= 12) {
|
|
187
|
+
return str.slice(0, 4) + '-XXXX-XXXX-' + str.slice(-4);
|
|
188
|
+
}
|
|
189
|
+
return 'XXXX-XXXX-XXXX-XXXX';
|
|
190
|
+
case 'email':
|
|
191
|
+
const parts = str.split('@');
|
|
192
|
+
if (parts.length === 2) {
|
|
193
|
+
const name = parts[0];
|
|
194
|
+
if (name.length > 3) {
|
|
195
|
+
return name.slice(0, 3) + '***@' + parts[1];
|
|
196
|
+
}
|
|
197
|
+
return name + '***@' + parts[1];
|
|
198
|
+
}
|
|
199
|
+
return '***@***.***';
|
|
200
|
+
case 'tc_no':
|
|
201
|
+
if (str.length >= 11) {
|
|
202
|
+
return str.slice(0, 3) + 'XXXXXX' + str.slice(-2);
|
|
203
|
+
}
|
|
204
|
+
return 'XXXXXXXXXXX';
|
|
205
|
+
default:
|
|
206
|
+
if (typeof rule === 'string' && rule !== 'credit_card' && rule !== 'email' && rule !== 'tc_no') {
|
|
207
|
+
return rule; // static mask string
|
|
208
|
+
}
|
|
209
|
+
return '***';
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
let decryptCount = 0;
|
|
213
|
+
let windowStart = Date.now();
|
|
214
|
+
let isFailClosed = false;
|
|
215
|
+
const globalKeysToZeroize = [];
|
|
216
|
+
// Ephemeral Master Key generated randomly on startup
|
|
217
|
+
let ephemeralMasterKey = crypto.randomBytes(32);
|
|
218
|
+
const secureKeyCache = new Map();
|
|
219
|
+
function getCachedKey(tenantId, version) {
|
|
220
|
+
const cacheKey = `${tenantId || 'global'}:${version}`;
|
|
221
|
+
const entry = secureKeyCache.get(cacheKey);
|
|
222
|
+
if (!entry)
|
|
223
|
+
return undefined;
|
|
224
|
+
if (Date.now() > entry.expiresAt) {
|
|
225
|
+
entry.wrappedKey.fill(0);
|
|
226
|
+
secureKeyCache.delete(cacheKey);
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
return unwrapKey(ephemeralMasterKey, entry.wrappedKey);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function setCachedKey(tenantId, version, plaintextKey, ttlMs = 120000) {
|
|
237
|
+
const cacheKey = `${tenantId || 'global'}:${version}`;
|
|
238
|
+
const existing = secureKeyCache.get(cacheKey);
|
|
239
|
+
if (existing) {
|
|
240
|
+
existing.wrappedKey.fill(0);
|
|
241
|
+
}
|
|
242
|
+
const wrapped = wrapKey(ephemeralMasterKey, plaintextKey);
|
|
243
|
+
secureKeyCache.set(cacheKey, {
|
|
244
|
+
wrappedKey: wrapped,
|
|
245
|
+
expiresAt: Date.now() + ttlMs
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
// Background cleanup worker (scans every 30s)
|
|
249
|
+
const cacheCleanupInterval = setInterval(() => {
|
|
250
|
+
const now = Date.now();
|
|
251
|
+
for (const [key, entry] of secureKeyCache.entries()) {
|
|
252
|
+
if (now > entry.expiresAt) {
|
|
253
|
+
entry.wrappedKey.fill(0);
|
|
254
|
+
secureKeyCache.delete(key);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}, 30000);
|
|
258
|
+
if (typeof cacheCleanupInterval.unref === 'function') {
|
|
259
|
+
cacheCleanupInterval.unref();
|
|
260
|
+
}
|
|
261
|
+
function resetSecureKeyCacheForTesting() {
|
|
262
|
+
for (const entry of secureKeyCache.values()) {
|
|
263
|
+
entry.wrappedKey.fill(0);
|
|
264
|
+
}
|
|
265
|
+
secureKeyCache.clear();
|
|
266
|
+
ephemeralMasterKey = crypto.randomBytes(32);
|
|
267
|
+
isBreakGlassActiveFlag = false;
|
|
268
|
+
if (breakGlassEmergencyKey) {
|
|
269
|
+
breakGlassEmergencyKey.fill(0);
|
|
270
|
+
breakGlassEmergencyKey = undefined;
|
|
271
|
+
}
|
|
272
|
+
breakGlassThreshold = 0;
|
|
273
|
+
breakGlassPublicKeys = [];
|
|
274
|
+
}
|
|
275
|
+
// Emergency Break-Glass variables
|
|
276
|
+
let breakGlassThreshold = 0;
|
|
277
|
+
let breakGlassPublicKeys = [];
|
|
278
|
+
let breakGlassEmergencyKey;
|
|
279
|
+
let isBreakGlassActiveFlag = false;
|
|
280
|
+
function configureBreakGlass(options) {
|
|
281
|
+
breakGlassThreshold = options.threshold;
|
|
282
|
+
breakGlassPublicKeys = options.publicKeys;
|
|
283
|
+
}
|
|
284
|
+
function deactivateBreakGlass() {
|
|
285
|
+
isBreakGlassActiveFlag = false;
|
|
286
|
+
if (breakGlassEmergencyKey) {
|
|
287
|
+
breakGlassEmergencyKey.fill(0);
|
|
288
|
+
breakGlassEmergencyKey = undefined;
|
|
289
|
+
}
|
|
290
|
+
logDecryption('SYSTEM', 'BREAK_GLASS_DEACTIVATED', undefined);
|
|
291
|
+
}
|
|
292
|
+
function isBreakGlassActive() {
|
|
293
|
+
return isBreakGlassActiveFlag;
|
|
294
|
+
}
|
|
295
|
+
function getBreakGlassKey() {
|
|
296
|
+
return breakGlassEmergencyKey;
|
|
297
|
+
}
|
|
298
|
+
function activateBreakGlass(signatures, emergencyBackupKey) {
|
|
299
|
+
if (breakGlassThreshold <= 0 || breakGlassPublicKeys.length === 0) {
|
|
300
|
+
throw new Error('Vollcrypt Security: Break-Glass protocol is not configured.');
|
|
301
|
+
}
|
|
302
|
+
if (signatures.length < breakGlassThreshold) {
|
|
303
|
+
throw new Error(`Vollcrypt Security: Insufficient signatures. Required: ${breakGlassThreshold}, Provided: ${signatures.length}`);
|
|
304
|
+
}
|
|
305
|
+
const verifiedKeys = new Set();
|
|
306
|
+
for (const sig of signatures) {
|
|
307
|
+
if (!breakGlassPublicKeys.includes(sig.publicKey)) {
|
|
308
|
+
throw new Error(`Vollcrypt Security: Public key ${sig.publicKey} is not in the authorized break-glass list.`);
|
|
309
|
+
}
|
|
310
|
+
if (verifiedKeys.has(sig.publicKey)) {
|
|
311
|
+
throw new Error(`Vollcrypt Security: Duplicate signature from public key ${sig.publicKey}.`);
|
|
312
|
+
}
|
|
313
|
+
if (Math.abs(Date.now() - sig.timestamp) > 15 * 60 * 1000) {
|
|
314
|
+
throw new Error(`Vollcrypt Security: Signature timestamp ${sig.timestamp} is outside the allowed 15-minute window.`);
|
|
315
|
+
}
|
|
316
|
+
const message = `break-glass-activate|${sig.timestamp}`;
|
|
317
|
+
const pubKeyBuf = Buffer.from(sig.publicKey, 'hex');
|
|
318
|
+
const msgBuf = Buffer.from(message, 'utf8');
|
|
319
|
+
const sigBuf = Buffer.from(sig.signature, 'hex');
|
|
320
|
+
const isValid = verifySignature(pubKeyBuf, msgBuf, sigBuf);
|
|
321
|
+
if (!isValid) {
|
|
322
|
+
throw new Error(`Vollcrypt Security: Invalid signature for public key ${sig.publicKey}.`);
|
|
323
|
+
}
|
|
324
|
+
verifiedKeys.add(sig.publicKey);
|
|
325
|
+
}
|
|
326
|
+
breakGlassEmergencyKey = Buffer.from(emergencyBackupKey);
|
|
327
|
+
isBreakGlassActiveFlag = true;
|
|
328
|
+
logDecryption('SYSTEM', 'BREAK_GLASS_ACTIVATED', undefined);
|
|
329
|
+
}
|
|
330
|
+
function registerKeysForZeroization(keys) {
|
|
331
|
+
if (!globalKeysToZeroize.includes(keys)) {
|
|
332
|
+
globalKeysToZeroize.push(keys);
|
|
333
|
+
}
|
|
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);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// Zeroize cache and ephemeral master key
|
|
344
|
+
for (const entry of secureKeyCache.values()) {
|
|
345
|
+
entry.wrappedKey.fill(0);
|
|
346
|
+
}
|
|
347
|
+
secureKeyCache.clear();
|
|
348
|
+
ephemeralMasterKey.fill(0);
|
|
349
|
+
if (breakGlassEmergencyKey) {
|
|
350
|
+
breakGlassEmergencyKey.fill(0);
|
|
351
|
+
}
|
|
352
|
+
if (onFailClosedCallback) {
|
|
353
|
+
try {
|
|
354
|
+
onFailClosedCallback();
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
// prevent user callback crash from blocking zeroization
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
throw new Error('Vollcrypt Security: Decryption rate limit exceeded. Fail-Closed mode triggered. Keys zeroized.');
|
|
361
|
+
}
|
|
362
|
+
function checkRateLimit(options) {
|
|
363
|
+
if (isFailClosed) {
|
|
364
|
+
throw new Error('Vollcrypt Security: Fail-Closed mode is active. Decryption blocked.');
|
|
365
|
+
}
|
|
366
|
+
const context = exports.dbGuardContextStore.getStore();
|
|
367
|
+
if (context?.bypassRateLimit) {
|
|
368
|
+
return; // Rate limit check bypassed for this request context
|
|
369
|
+
}
|
|
370
|
+
const limit = context?.maxDecryptionsPerSecond || options?.maxDecryptionsPerSecond || 500;
|
|
371
|
+
const mode = context?.rateLimiterMode || options?.mode || 'fail_closed';
|
|
372
|
+
const now = Date.now();
|
|
373
|
+
if (now - windowStart > 1000) {
|
|
374
|
+
decryptCount = 0;
|
|
375
|
+
windowStart = now;
|
|
376
|
+
}
|
|
377
|
+
decryptCount++;
|
|
378
|
+
if (decryptCount > limit) {
|
|
379
|
+
if (mode === 'fail_closed') {
|
|
380
|
+
triggerFailClosed(options?.onFailClosed);
|
|
381
|
+
}
|
|
382
|
+
else if (mode === 'warn') {
|
|
383
|
+
console.warn(`Vollcrypt Warning: Decryption rate limit exceeded. ${decryptCount} decryptions in the current window (limit: ${limit}).`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function checkPageSize(count, options) {
|
|
388
|
+
if (isFailClosed) {
|
|
389
|
+
throw new Error('Vollcrypt Security: Fail-Closed mode is active. Decryption blocked.');
|
|
390
|
+
}
|
|
391
|
+
const context = exports.dbGuardContextStore.getStore();
|
|
392
|
+
const maxPageSize = context?.maxPageSize !== undefined
|
|
393
|
+
? context.maxPageSize
|
|
394
|
+
: (options?.maxPageSize !== undefined ? options.maxPageSize : 250);
|
|
395
|
+
const behavior = context?.onPageSizeExceeded
|
|
396
|
+
? context.onPageSizeExceeded
|
|
397
|
+
: (options?.onPageSizeExceeded || 'warn');
|
|
398
|
+
if (count > maxPageSize) {
|
|
399
|
+
if (behavior === 'error') {
|
|
400
|
+
throw new Error(`Vollcrypt Security: Query returned ${count} records, which exceeds the max allowed page size of ${maxPageSize}. Decryption blocked to prevent rate limit execution.`);
|
|
401
|
+
}
|
|
402
|
+
else if (behavior === 'warn') {
|
|
403
|
+
console.warn(`Vollcrypt Warning: Query returned ${count} records, which exceeds the recommended page size limit of ${maxPageSize}. This may trigger the decryption rate limiter.`);
|
|
404
|
+
return 'warn';
|
|
405
|
+
}
|
|
406
|
+
else if (behavior === 'bypass') {
|
|
407
|
+
return 'bypass';
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return 'ok';
|
|
411
|
+
}
|
|
412
|
+
function getFailClosedStatus() {
|
|
413
|
+
return isFailClosed;
|
|
414
|
+
}
|
|
415
|
+
function resetFailClosedStatusForTesting() {
|
|
416
|
+
isFailClosed = false;
|
|
417
|
+
decryptCount = 0;
|
|
418
|
+
windowStart = Date.now();
|
|
419
|
+
}
|
|
420
|
+
let lastLogHash = '0'.repeat(64);
|
|
421
|
+
let auditLogPath;
|
|
422
|
+
let onAuditLogCallback;
|
|
423
|
+
function configureAuditLogger(options) {
|
|
424
|
+
if (options?.path) {
|
|
425
|
+
auditLogPath = options.path;
|
|
426
|
+
try {
|
|
427
|
+
if (fs.existsSync(auditLogPath)) {
|
|
428
|
+
const content = fs.readFileSync(auditLogPath, 'utf8').trim();
|
|
429
|
+
if (content) {
|
|
430
|
+
const lines = content.split('\n');
|
|
431
|
+
const lastLine = lines[lines.length - 1];
|
|
432
|
+
if (lastLine) {
|
|
433
|
+
const entry = JSON.parse(lastLine);
|
|
434
|
+
if (entry && entry.hash) {
|
|
435
|
+
lastLogHash = entry.hash;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
// fallback to genesis hash on error
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (options?.onAuditLog)
|
|
446
|
+
onAuditLogCallback = options.onAuditLog;
|
|
447
|
+
}
|
|
448
|
+
function resetAuditLoggerForTesting() {
|
|
449
|
+
lastLogHash = '0'.repeat(64);
|
|
450
|
+
auditLogPath = undefined;
|
|
451
|
+
onAuditLogCallback = undefined;
|
|
452
|
+
}
|
|
453
|
+
function logDecryption(model, field, recordId) {
|
|
454
|
+
const context = exports.dbGuardContextStore.getStore();
|
|
455
|
+
const timestamp = new Date().toISOString();
|
|
456
|
+
const entry = {
|
|
457
|
+
timestamp,
|
|
458
|
+
userId: context?.userId,
|
|
459
|
+
role: context?.role,
|
|
460
|
+
model,
|
|
461
|
+
field,
|
|
462
|
+
recordId: recordId ? String(recordId) : undefined,
|
|
463
|
+
action: 'decrypt',
|
|
464
|
+
prevHash: lastLogHash
|
|
465
|
+
};
|
|
466
|
+
const payload = `${entry.timestamp}|${entry.userId || ''}|${entry.role || ''}|${entry.model}|${entry.field}|${entry.recordId || ''}|${entry.action}|${entry.prevHash}`;
|
|
467
|
+
const hash = crypto.createHash('sha256').update(payload).digest('hex');
|
|
468
|
+
const fullEntry = { ...entry, hash };
|
|
469
|
+
lastLogHash = hash;
|
|
470
|
+
if (onAuditLogCallback) {
|
|
471
|
+
try {
|
|
472
|
+
onAuditLogCallback(fullEntry);
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
// prevent callback errors from stopping application flow
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (auditLogPath) {
|
|
479
|
+
try {
|
|
480
|
+
fs.appendFileSync(auditLogPath, JSON.stringify(fullEntry) + '\n', 'utf8');
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
// prevent filesystem errors from throwing
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function decryptWithSecurity(stored, decryptRawFn, modelName, fieldName, recordId, options) {
|
|
488
|
+
if (typeof stored !== 'string' || !stored.startsWith('VOLLVALT:')) {
|
|
489
|
+
// Dual-read fallback: if the value is not encrypted, return as is.
|
|
490
|
+
return stored;
|
|
491
|
+
}
|
|
492
|
+
const fieldKey = `${modelName}.${fieldName}`;
|
|
493
|
+
// 1. Check if Crypto-RBAC is configured
|
|
494
|
+
if (options?.cryptoRbac) {
|
|
495
|
+
const context = exports.dbGuardContextStore.getStore();
|
|
496
|
+
const role = context?.role;
|
|
497
|
+
const roleConfig = role ? options.cryptoRbac.roles[role] : undefined;
|
|
498
|
+
const isAuthorized = roleConfig?.decrypt.includes(fieldKey) || false;
|
|
499
|
+
if (!isAuthorized) {
|
|
500
|
+
// Unauthorized. Check for masking rules
|
|
501
|
+
const maskRule = roleConfig?.mask?.[fieldKey];
|
|
502
|
+
if (maskRule !== undefined) {
|
|
503
|
+
if (typeof maskRule === 'string' && maskRule !== 'credit_card' && maskRule !== 'email' && maskRule !== 'tc_no') {
|
|
504
|
+
// Static mask bypasses decryption completely
|
|
505
|
+
return maskRule;
|
|
506
|
+
}
|
|
507
|
+
// Dynamic mask requires internal decryption
|
|
508
|
+
checkRateLimit(options.rateLimiter);
|
|
509
|
+
const plaintext = decryptRawFn(stored);
|
|
510
|
+
const masked = maskValue(plaintext, maskRule);
|
|
511
|
+
logDecryption(modelName, fieldName, recordId);
|
|
512
|
+
return masked;
|
|
513
|
+
}
|
|
514
|
+
// No mask defined for unauthorized access -> block decryption
|
|
515
|
+
throw new Error(`Vollcrypt Security: Role "${role || 'GUEST'}" is not authorized to decrypt field "${fieldKey}".`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// 2. Authorized or RBAC disabled: proceed with normal decryption
|
|
519
|
+
checkRateLimit(options?.rateLimiter);
|
|
520
|
+
const result = decryptRawFn(stored);
|
|
521
|
+
logDecryption(modelName, fieldName, recordId);
|
|
522
|
+
return result;
|
|
523
|
+
}
|
|
524
|
+
exports.VERSION_ALGORITHMS = {
|
|
525
|
+
'1': '1'
|
|
526
|
+
};
|
|
527
|
+
exports.CRYPTO_ALGORITHMS = {
|
|
528
|
+
'1': {
|
|
529
|
+
encrypt: (plaintext, key) => encryptAesGcmPadded(key, plaintext, null),
|
|
530
|
+
decrypt: (ciphertext, key) => decryptAesGcmPadded(key, ciphertext, null),
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
function parseCiphertext(stored) {
|
|
534
|
+
if (!stored.startsWith('VOLLVALT:'))
|
|
535
|
+
return null;
|
|
536
|
+
const content = stored.slice('VOLLVALT:'.length);
|
|
537
|
+
if (content.startsWith('v')) {
|
|
538
|
+
const colon = content.indexOf(':');
|
|
539
|
+
if (colon === -1)
|
|
540
|
+
return null;
|
|
541
|
+
const versionPart = content.slice(1, colon);
|
|
542
|
+
const base64Part = content.slice(colon + 1);
|
|
543
|
+
const algoId = exports.VERSION_ALGORITHMS[versionPart] || '1';
|
|
544
|
+
return { algoId, version: versionPart, base64Data: base64Part };
|
|
545
|
+
}
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { InsertEvent, UpdateEvent } from 'typeorm';
|
|
2
|
+
import { RateLimiterOptions } from './security';
|
|
3
|
+
export interface TypeOrmDbGuardOptions {
|
|
4
|
+
key: Buffer | Record<string, Buffer>;
|
|
5
|
+
activeKeyVersion?: string;
|
|
6
|
+
entities: Record<string, string[]>;
|
|
7
|
+
blindIndexes?: {
|
|
8
|
+
rootSalt: Buffer;
|
|
9
|
+
entities: Record<string, string[]>;
|
|
10
|
+
};
|
|
11
|
+
cryptoRbac?: {
|
|
12
|
+
roles: Record<string, {
|
|
13
|
+
decrypt: string[];
|
|
14
|
+
mask?: Record<string, 'credit_card' | 'email' | 'tc_no' | ((v: any) => any) | string>;
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
rateLimiter?: RateLimiterOptions;
|
|
18
|
+
}
|
|
19
|
+
export declare function createTypeOrmSubscriber(options: TypeOrmDbGuardOptions): {
|
|
20
|
+
new (): {
|
|
21
|
+
listenTo(): ObjectConstructor;
|
|
22
|
+
beforeInsert(event: InsertEvent<any>): void;
|
|
23
|
+
beforeUpdate(event: UpdateEvent<any>): void;
|
|
24
|
+
afterLoad(entity: any, event: any): void;
|
|
25
|
+
};
|
|
26
|
+
};
|