@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/mongoose.js
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mongooseDbGuard = mongooseDbGuard;
|
|
4
|
+
const prisma_1 = require("./prisma");
|
|
5
|
+
const blind_index_1 = require("./blind-index");
|
|
6
|
+
const security_1 = require("./security");
|
|
7
|
+
function mongooseDbGuard(schema, options) {
|
|
8
|
+
const { fields } = options;
|
|
9
|
+
let keys;
|
|
10
|
+
let activeVersion;
|
|
11
|
+
if (Buffer.isBuffer(options.key)) {
|
|
12
|
+
keys = { '1': options.key };
|
|
13
|
+
activeVersion = '1';
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
keys = options.key;
|
|
17
|
+
activeVersion = options.activeKeyVersion || Object.keys(keys)[0];
|
|
18
|
+
}
|
|
19
|
+
(0, security_1.registerKeysForZeroization)(keys);
|
|
20
|
+
const activeKey = keys[activeVersion];
|
|
21
|
+
if (!activeKey) {
|
|
22
|
+
throw new Error(`Active encryption key version "${activeVersion}" is not present in the key map.`);
|
|
23
|
+
}
|
|
24
|
+
const resolveTenantKeysAndActiveKeySyncOrAsync = (tenantId) => {
|
|
25
|
+
if ((0, security_1.isBreakGlassActive)()) {
|
|
26
|
+
const bgKey = (0, security_1.getBreakGlassKey)();
|
|
27
|
+
if (bgKey) {
|
|
28
|
+
return { keys: { '1': bgKey }, activeKey: bgKey, activeVersion: '1' };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!tenantId || !options.multiTenant) {
|
|
32
|
+
return { keys, activeKey, activeVersion };
|
|
33
|
+
}
|
|
34
|
+
// Check Secure TTL Cache
|
|
35
|
+
const cachedActiveKey = (0, security_1.getCachedKey)(tenantId, activeVersion);
|
|
36
|
+
if (cachedActiveKey) {
|
|
37
|
+
return { keys: { [activeVersion]: cachedActiveKey }, activeKey: cachedActiveKey, activeVersion };
|
|
38
|
+
}
|
|
39
|
+
// Cache miss: resolve configuration (this part is async)
|
|
40
|
+
const resolveAsync = async () => {
|
|
41
|
+
const multiTenant = options.multiTenant;
|
|
42
|
+
if (!multiTenant) {
|
|
43
|
+
throw new Error(`Vollcrypt Security: Multi-tenant configuration is not defined.`);
|
|
44
|
+
}
|
|
45
|
+
let tenantConfig;
|
|
46
|
+
if (multiTenant.tenants) {
|
|
47
|
+
tenantConfig = multiTenant.tenants[tenantId];
|
|
48
|
+
}
|
|
49
|
+
else if (multiTenant.getTenantConfig) {
|
|
50
|
+
tenantConfig = await multiTenant.getTenantConfig(tenantId);
|
|
51
|
+
}
|
|
52
|
+
if (!tenantConfig) {
|
|
53
|
+
throw new Error(`Vollcrypt Security: Configuration not found for tenantId "${tenantId}".`);
|
|
54
|
+
}
|
|
55
|
+
const resolvedTenantKeys = await (0, prisma_1.resolveKeys)({
|
|
56
|
+
...options,
|
|
57
|
+
key: tenantConfig.key,
|
|
58
|
+
kms: tenantConfig.kms
|
|
59
|
+
});
|
|
60
|
+
(0, security_1.registerKeysForZeroization)(resolvedTenantKeys);
|
|
61
|
+
const tActiveVersion = tenantConfig.kms?.activeKeyVersion || '1';
|
|
62
|
+
const tActiveKey = resolvedTenantKeys[tActiveVersion];
|
|
63
|
+
if (!tActiveKey) {
|
|
64
|
+
throw new Error(`Vollcrypt Security: Active key version "${tActiveVersion}" not found for tenantId "${tenantId}".`);
|
|
65
|
+
}
|
|
66
|
+
for (const [ver, keyBuf] of Object.entries(resolvedTenantKeys)) {
|
|
67
|
+
(0, security_1.setCachedKey)(tenantId, ver, keyBuf);
|
|
68
|
+
}
|
|
69
|
+
return { keys: resolvedTenantKeys, activeKey: tActiveKey, activeVersion: tActiveVersion };
|
|
70
|
+
};
|
|
71
|
+
return resolveAsync();
|
|
72
|
+
};
|
|
73
|
+
// Pre-save document middleware (handles document.save(), Model.create())
|
|
74
|
+
schema.pre('save', function (next) {
|
|
75
|
+
const doc = this;
|
|
76
|
+
const modelName = options.blindIndexes?.modelName || this.constructor.modelName || 'Model';
|
|
77
|
+
const context = security_1.dbGuardContextStore.getStore();
|
|
78
|
+
const runSync = (resolved) => {
|
|
79
|
+
// 1. Encrypt fields
|
|
80
|
+
for (const field of fields) {
|
|
81
|
+
if (doc.isModified(field) && doc[field] !== undefined && doc[field] !== null) {
|
|
82
|
+
doc[field] = (0, prisma_1.encryptValue)(doc[field], resolved.activeKey, resolved.activeVersion);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// 2. Compute blind indexes
|
|
86
|
+
if (options.blindIndexes && options.blindIndexes.rootSalt) {
|
|
87
|
+
for (const field of options.blindIndexes.fields) {
|
|
88
|
+
if (doc.isModified(field) && doc[field] !== undefined && doc[field] !== null) {
|
|
89
|
+
const bidxField = `${field}_bidx`;
|
|
90
|
+
// Decrypt temporary value if it was already encrypted in the previous step
|
|
91
|
+
const rawVal = doc.isModified(field) && doc[field].startsWith('VOLLVALT:')
|
|
92
|
+
? (0, prisma_1.decryptValue)(doc[field], keys)
|
|
93
|
+
: doc[field];
|
|
94
|
+
doc[bidxField] = (0, blind_index_1.computeBlindIndex)(rawVal, options.blindIndexes.rootSalt, `${modelName}.${field}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (typeof next === 'function') {
|
|
99
|
+
next();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const resOrPromise = resolveTenantKeysAndActiveKeySyncOrAsync(context?.tenantId);
|
|
103
|
+
if (resOrPromise instanceof Promise) {
|
|
104
|
+
resOrPromise.then(runSync).catch((err) => {
|
|
105
|
+
if (typeof next === 'function')
|
|
106
|
+
next(err);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
try {
|
|
111
|
+
runSync(resOrPromise);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
if (typeof next === 'function')
|
|
115
|
+
next(err);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
// Helper to encrypt and calculate blind indexes on update payloads
|
|
120
|
+
const encryptAndIndexUpdates = (update, modelName, encKey, encVer) => {
|
|
121
|
+
if (!update || typeof update !== 'object')
|
|
122
|
+
return;
|
|
123
|
+
const encryptPathRecursive = (obj, pathParts, fullPath) => {
|
|
124
|
+
if (!obj || typeof obj !== 'object')
|
|
125
|
+
return;
|
|
126
|
+
const currentPart = pathParts[0];
|
|
127
|
+
if (pathParts.length === 1) {
|
|
128
|
+
if (obj[currentPart] !== undefined && obj[currentPart] !== null) {
|
|
129
|
+
if (options.blindIndexes && options.blindIndexes.fields.includes(fullPath) && options.blindIndexes.rootSalt) {
|
|
130
|
+
const bidxField = `${currentPart}_bidx`;
|
|
131
|
+
obj[bidxField] = (0, blind_index_1.computeBlindIndex)(obj[currentPart], options.blindIndexes.rootSalt, `${modelName}.${fullPath}`);
|
|
132
|
+
}
|
|
133
|
+
obj[currentPart] = (0, prisma_1.encryptValue)(obj[currentPart], encKey, encVer);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
if (obj[currentPart] && typeof obj[currentPart] === 'object') {
|
|
138
|
+
encryptPathRecursive(obj[currentPart], pathParts.slice(1), fullPath);
|
|
139
|
+
}
|
|
140
|
+
const dotNotatedPath = pathParts.join('.');
|
|
141
|
+
if (obj[dotNotatedPath] !== undefined && obj[dotNotatedPath] !== null) {
|
|
142
|
+
if (options.blindIndexes && options.blindIndexes.fields.includes(fullPath) && options.blindIndexes.rootSalt) {
|
|
143
|
+
const bidxField = `${dotNotatedPath}_bidx`;
|
|
144
|
+
obj[bidxField] = (0, blind_index_1.computeBlindIndex)(obj[dotNotatedPath], options.blindIndexes.rootSalt, `${modelName}.${fullPath}`);
|
|
145
|
+
}
|
|
146
|
+
obj[dotNotatedPath] = (0, prisma_1.encryptValue)(obj[dotNotatedPath], encKey, encVer);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const encryptTargetFields = (target) => {
|
|
151
|
+
for (const field of fields) {
|
|
152
|
+
encryptPathRecursive(target, field.split('.'), field);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
// Handle direct object properties
|
|
156
|
+
encryptTargetFields(update);
|
|
157
|
+
// Handle MongoDB update operators ($set, $setOnInsert)
|
|
158
|
+
const operators = ['$set', '$setOnInsert'];
|
|
159
|
+
for (const op of operators) {
|
|
160
|
+
if (update[op] && typeof update[op] === 'object') {
|
|
161
|
+
encryptTargetFields(update[op]);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
// Pre-query update middleware (handles Model.updateOne(), Model.findOneAndUpdate(), etc.)
|
|
166
|
+
const updateHooks = ['updateOne', 'updateMany', 'findOneAndUpdate', 'update'];
|
|
167
|
+
updateHooks.forEach((hook) => {
|
|
168
|
+
schema.pre(hook, function (next) {
|
|
169
|
+
const query = this;
|
|
170
|
+
const modelName = options.blindIndexes?.modelName || query.model?.modelName || 'Model';
|
|
171
|
+
const update = typeof query.getUpdate === 'function' ? query.getUpdate() : null;
|
|
172
|
+
const context = security_1.dbGuardContextStore.getStore();
|
|
173
|
+
const runSync = (resolved) => {
|
|
174
|
+
// 1. Process writes
|
|
175
|
+
if (update) {
|
|
176
|
+
encryptAndIndexUpdates(update, modelName, resolved.activeKey, resolved.activeVersion);
|
|
177
|
+
}
|
|
178
|
+
// 2. Process query criteria (rewrite exact match search queries on conditions)
|
|
179
|
+
const conditions = typeof query.getQuery === 'function' ? query.getQuery() : null;
|
|
180
|
+
if (conditions && options.blindIndexes && options.blindIndexes.rootSalt) {
|
|
181
|
+
(0, prisma_1.rewriteQueryWhere)(conditions, options.blindIndexes.fields, options.blindIndexes.rootSalt, modelName);
|
|
182
|
+
}
|
|
183
|
+
if (typeof next === 'function') {
|
|
184
|
+
next();
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
const resOrPromise = resolveTenantKeysAndActiveKeySyncOrAsync(context?.tenantId);
|
|
188
|
+
if (resOrPromise instanceof Promise) {
|
|
189
|
+
resOrPromise.then(runSync).catch((err) => {
|
|
190
|
+
if (typeof next === 'function')
|
|
191
|
+
next(err);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
try {
|
|
196
|
+
runSync(resOrPromise);
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
if (typeof next === 'function')
|
|
200
|
+
next(err);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
// Pre-query read middleware (handles Model.find(), Model.findOne(), etc.)
|
|
206
|
+
const readHooks = ['find', 'findOne', 'countDocuments', 'distinct'];
|
|
207
|
+
readHooks.forEach((hook) => {
|
|
208
|
+
schema.pre(hook, function (next) {
|
|
209
|
+
const query = this;
|
|
210
|
+
const modelName = options.blindIndexes?.modelName || query.model?.modelName || 'Model';
|
|
211
|
+
const conditions = typeof query.getQuery === 'function' ? query.getQuery() : null;
|
|
212
|
+
if (conditions && options.blindIndexes && options.blindIndexes.rootSalt) {
|
|
213
|
+
(0, prisma_1.rewriteQueryWhere)(conditions, options.blindIndexes.fields, options.blindIndexes.rootSalt, modelName);
|
|
214
|
+
}
|
|
215
|
+
next();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
// Helper to decrypt documents
|
|
219
|
+
const decryptDoc = (doc, modelName, decKeys) => {
|
|
220
|
+
if (!doc)
|
|
221
|
+
return;
|
|
222
|
+
for (const field of fields) {
|
|
223
|
+
if (doc[field] !== undefined && doc[field] !== null) {
|
|
224
|
+
try {
|
|
225
|
+
doc[field] = (0, security_1.decryptWithSecurity)(doc[field], (val) => (0, prisma_1.decryptValue)(val, decKeys), modelName, field, doc.id || doc._id, options);
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
throw new Error(`Mongoose db-guard failed to decrypt field "${field}": ${err.message}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
// Post-find query middleware (handles Model.find())
|
|
234
|
+
schema.post('find', function (docs, next) {
|
|
235
|
+
try {
|
|
236
|
+
const modelName = options.blindIndexes?.modelName || this.model?.modelName || 'Model';
|
|
237
|
+
if (!Array.isArray(docs)) {
|
|
238
|
+
if (typeof next === 'function')
|
|
239
|
+
next();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const context = security_1.dbGuardContextStore.getStore();
|
|
243
|
+
const resOrPromise = resolveTenantKeysAndActiveKeySyncOrAsync(context?.tenantId);
|
|
244
|
+
const runSync = (resolved) => {
|
|
245
|
+
const pageSizeStatus = (0, security_1.checkPageSize)(docs.length, options.rateLimiter);
|
|
246
|
+
if (pageSizeStatus === 'bypass') {
|
|
247
|
+
const currentCtx = security_1.dbGuardContextStore.getStore() || {};
|
|
248
|
+
security_1.dbGuardContextStore.run({ ...currentCtx, bypassRateLimit: true }, () => {
|
|
249
|
+
docs.forEach((d) => decryptDoc(d, modelName, resolved.keys));
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
docs.forEach((d) => decryptDoc(d, modelName, resolved.keys));
|
|
254
|
+
}
|
|
255
|
+
if (typeof next === 'function') {
|
|
256
|
+
next();
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
if (resOrPromise instanceof Promise) {
|
|
260
|
+
resOrPromise.then(runSync).catch((err) => {
|
|
261
|
+
if (typeof next === 'function')
|
|
262
|
+
next(err);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
runSync(resOrPromise);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
if (typeof next === 'function') {
|
|
271
|
+
next(err);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
throw err;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
// Post-findOne query middleware (handles Model.findOne(), Model.findOneAndUpdate(), etc.)
|
|
279
|
+
const singleReadHooks = ['findOne', 'findOneAndUpdate', 'findOneAndDelete'];
|
|
280
|
+
singleReadHooks.forEach((hook) => {
|
|
281
|
+
schema.post(hook, function (doc, next) {
|
|
282
|
+
try {
|
|
283
|
+
const modelName = options.blindIndexes?.modelName || this.model?.modelName || 'Model';
|
|
284
|
+
if (!doc) {
|
|
285
|
+
if (typeof next === 'function')
|
|
286
|
+
next();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const context = security_1.dbGuardContextStore.getStore();
|
|
290
|
+
const resOrPromise = resolveTenantKeysAndActiveKeySyncOrAsync(context?.tenantId);
|
|
291
|
+
const runSync = (resolved) => {
|
|
292
|
+
decryptDoc(doc, modelName, resolved.keys);
|
|
293
|
+
if (typeof next === 'function') {
|
|
294
|
+
next();
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
if (resOrPromise instanceof Promise) {
|
|
298
|
+
resOrPromise.then(runSync).catch((err) => {
|
|
299
|
+
if (typeof next === 'function')
|
|
300
|
+
next(err);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
runSync(resOrPromise);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
if (typeof next === 'function') {
|
|
309
|
+
next(err);
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
throw err;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
}
|
package/dist/prisma.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { KmsProvider } from './kms';
|
|
2
|
+
import { RateLimiterOptions } from './security';
|
|
3
|
+
export interface PrismaDbGuardOptions {
|
|
4
|
+
key?: Buffer | Record<string, Buffer>;
|
|
5
|
+
kms?: {
|
|
6
|
+
provider: KmsProvider;
|
|
7
|
+
wrappedKey: Buffer | Record<string, Buffer>;
|
|
8
|
+
wrappedKek?: Buffer | Record<string, Buffer>;
|
|
9
|
+
activeKeyVersion?: string;
|
|
10
|
+
};
|
|
11
|
+
models: Record<string, string[]>;
|
|
12
|
+
blindIndexes?: {
|
|
13
|
+
rootSalt: Buffer;
|
|
14
|
+
models: Record<string, string[]>;
|
|
15
|
+
};
|
|
16
|
+
cryptoRbac?: {
|
|
17
|
+
roles: Record<string, {
|
|
18
|
+
decrypt: string[];
|
|
19
|
+
mask?: Record<string, 'credit_card' | 'email' | 'tc_no' | ((v: any) => any) | string>;
|
|
20
|
+
}>;
|
|
21
|
+
};
|
|
22
|
+
rateLimiter?: RateLimiterOptions;
|
|
23
|
+
multiTenant?: {
|
|
24
|
+
tenants?: Record<string, {
|
|
25
|
+
key?: Buffer | Record<string, Buffer>;
|
|
26
|
+
kms?: any;
|
|
27
|
+
}>;
|
|
28
|
+
getTenantConfig?: (tenantId: string) => Promise<{
|
|
29
|
+
key?: Buffer | Record<string, Buffer>;
|
|
30
|
+
kms?: any;
|
|
31
|
+
} | undefined>;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolves the plaintext keys asynchronously from local config or KMS provider.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveKeys(options: PrismaDbGuardOptions): Promise<Record<string, Buffer>>;
|
|
38
|
+
export declare function encryptValue(val: any, key: Buffer, version: string): string;
|
|
39
|
+
export declare function decryptValue(stored: any, keys: Record<string, Buffer>): any;
|
|
40
|
+
/**
|
|
41
|
+
* Traverses query `where` arguments to rewrite exact match queries on encrypted fields
|
|
42
|
+
* to target shadow `_bidx` columns using dynamic HKDF-SHA256 blind indexing.
|
|
43
|
+
*/
|
|
44
|
+
export declare function rewriteQueryWhere(where: any, fields: string[], rootSalt: Buffer, modelName: string): void;
|
|
45
|
+
/**
|
|
46
|
+
* Appends calculated blind indexes to the write payload (create/update).
|
|
47
|
+
*/
|
|
48
|
+
export declare function addBlindIndexes(data: any, fields: string[], rootSalt: Buffer, modelName: string): void;
|
|
49
|
+
/**
|
|
50
|
+
* Prisma DbGuard Extension Factory
|
|
51
|
+
*
|
|
52
|
+
* Bootstraps client-level field encryption, query translation, and automatic decryption.
|
|
53
|
+
*/
|
|
54
|
+
export declare const prismaDbGuard: (options: PrismaDbGuardOptions, resolvedKeys?: Record<string, Buffer>) => (client: any) => import("@prisma/client").PrismaClientExtends<import("@prisma/client/runtime/library").InternalArgs<{}, {}, {}, {}> & import("@prisma/client/runtime/library").DefaultArgs>;
|