mask-privacy 4.0.0 → 4.2.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/dist/index.d.mts +37 -31
- package/dist/index.d.ts +37 -31
- package/dist/index.js +794 -370
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +767 -342
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/src/config.ts +5 -0
- package/src/core/crypto.ts +171 -87
- package/src/core/exceptions.ts +25 -0
- package/src/core/ff1.ts +196 -0
- package/src/core/fpe.ts +97 -175
- package/src/core/fpe_utils.ts +57 -11
- package/src/core/key_provider.ts +80 -0
- package/src/core/vault.ts +152 -78
- package/src/telemetry/audit_logger.ts +136 -16
- package/tests/bijective_fpe.test.ts +16 -12
- package/tests/fpe.test.ts +17 -8
- package/tests/security_hardening.test.ts +117 -0
- package/tests/vault.test.ts +67 -0
- package/tests/vault_backends.test.ts +7 -7
package/src/core/vault.ts
CHANGED
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
|
|
13
13
|
import * as crypto from 'crypto';
|
|
14
14
|
import { config } from '../config';
|
|
15
|
-
import {
|
|
15
|
+
import { generateDPToken } from './fpe';
|
|
16
16
|
import { looksLikeToken, TOKEN_PATTERN } from './fpe_utils';
|
|
17
17
|
import { getCryptoEngineAsync } from './crypto';
|
|
18
|
-
import { MaskVaultConnectionError } from './exceptions';
|
|
18
|
+
import { MaskVaultConnectionError, TokenCollisionError } from './exceptions';
|
|
19
19
|
import { getAuditLogger } from '../telemetry/audit_logger';
|
|
20
20
|
import { BucketManager } from './search';
|
|
21
21
|
|
|
@@ -50,8 +50,8 @@ export function _getFailStrategy(): string {
|
|
|
50
50
|
|
|
51
51
|
/** Interface every vault backend must implement. */
|
|
52
52
|
export abstract class BaseVault {
|
|
53
|
-
/** Persist a token → plaintext mapping with a TTL
|
|
54
|
-
abstract store(token: string, plaintext: string, ttlSeconds: number, ptHash?: string | null): Promise<void>;
|
|
53
|
+
/** Persist a token → encrypted plaintext mapping with a TTL and optional compliance metadata. */
|
|
54
|
+
abstract store(token: string, plaintext: string, ttlSeconds: number, ptHash?: string | null, metadata?: Record<string, string> | null): Promise<void>;
|
|
55
55
|
|
|
56
56
|
/** Return the existing unexpired token for a given plaintext hash, or null. */
|
|
57
57
|
abstract getTokenByPlaintextHash(ptHash: string): Promise<string | null>;
|
|
@@ -59,6 +59,9 @@ export abstract class BaseVault {
|
|
|
59
59
|
/** Return the plaintext for token, or null if missing/expired. */
|
|
60
60
|
abstract retrieve(token: string): Promise<string | null>;
|
|
61
61
|
|
|
62
|
+
/** Return the plaintext hash stored for this token (used for collision detection), or null. */
|
|
63
|
+
abstract getPtHashForToken(token: string): Promise<string | null>;
|
|
64
|
+
|
|
62
65
|
/** Delete a token and its reverse mapping. */
|
|
63
66
|
abstract delete(token: string): Promise<void>;
|
|
64
67
|
}
|
|
@@ -78,29 +81,57 @@ export function _hashPlaintext(plaintext: string, secret?: Buffer): string {
|
|
|
78
81
|
return crypto.createHash('sha256').update(trimmed, 'utf-8').digest('hex');
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Tenant-namespaced cache key helpers
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
function _vaultKey(token: string): string {
|
|
89
|
+
return `mask:${config.MASK_TENANT_ID}:${token}`;
|
|
90
|
+
}
|
|
91
|
+
function _vaultRevKey(ptHash: string): string {
|
|
92
|
+
return `mask-rev:${config.MASK_TENANT_ID}:${ptHash}`;
|
|
93
|
+
}
|
|
94
|
+
function _vaultHashKey(token: string): string {
|
|
95
|
+
return `mask-hash:${config.MASK_TENANT_ID}:${token}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Extract the ciphertext from a JSON compliance envelope {ct, meta}, or return raw.
|
|
100
|
+
* Fixes the fatal bug where tokens stored with metadata could never be decrypted.
|
|
101
|
+
*/
|
|
102
|
+
function _unwrapPayload(raw: string): string {
|
|
103
|
+
if (raw && raw.startsWith('{')) {
|
|
104
|
+
try {
|
|
105
|
+
const obj = JSON.parse(raw);
|
|
106
|
+
if (obj.ct) return obj.ct;
|
|
107
|
+
} catch {}
|
|
108
|
+
}
|
|
109
|
+
return raw;
|
|
110
|
+
}
|
|
111
|
+
|
|
81
112
|
/**
|
|
82
113
|
* In-memory implementation (single-process, dev / testing)
|
|
83
114
|
*
|
|
84
115
|
* Map-backed vault. Fast, but state is lost across processes.
|
|
85
116
|
*/
|
|
86
117
|
export class MemoryVault extends BaseVault {
|
|
87
|
-
private _store: Map<string, { plaintext: string; expiry: number; ptHash: string | null }>;
|
|
118
|
+
private _store: Map<string, { plaintext: string; expiry: number; ptHash: string | null; metadata: Record<string, string> }>;
|
|
88
119
|
private _reverseStore: Map<string, string>;
|
|
120
|
+
private _cleanupTimer: NodeJS.Timeout | null = null;
|
|
89
121
|
|
|
90
122
|
constructor() {
|
|
91
123
|
super();
|
|
92
124
|
this._store = new Map();
|
|
93
125
|
this._reverseStore = new Map();
|
|
126
|
+
// Replace probabilistic inline cleanup with a background timer (unreffed
|
|
127
|
+
// so it never prevents process exit) to eliminate request-path latency jitter.
|
|
128
|
+
this._cleanupTimer = setInterval(() => this._cleanup(), 60_000);
|
|
129
|
+
if (this._cleanupTimer && typeof this._cleanupTimer.unref === 'function') {
|
|
130
|
+
this._cleanupTimer.unref();
|
|
131
|
+
}
|
|
94
132
|
}
|
|
95
133
|
|
|
96
134
|
private _cleanup(): void {
|
|
97
|
-
// Probabilistic cleanup to prevent memory bloat
|
|
98
|
-
// Configurable frequency (default 1%) balances CPU vs Memory usage
|
|
99
|
-
const cleanupFreq = config.MASK_VAULT_CLEANUP_FREQUENCY;
|
|
100
|
-
if (Math.random() > cleanupFreq) {
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
135
|
const now = Date.now() / 1000;
|
|
105
136
|
for (const [token, entry] of this._store.entries()) {
|
|
106
137
|
if (now > entry.expiry) {
|
|
@@ -112,12 +143,28 @@ export class MemoryVault extends BaseVault {
|
|
|
112
143
|
}
|
|
113
144
|
}
|
|
114
145
|
|
|
115
|
-
async store(token: string,
|
|
116
|
-
|
|
146
|
+
async store(token: string, ciphertext: string, ttlSeconds: number, ptHash: string | null = null, metadata: Record<string, string> | null = null): Promise<void> {
|
|
147
|
+
// Enforce max memory keys bound (OOM prevention)
|
|
148
|
+
if (!this._store.has(token) && this._store.size >= config.MASK_VAULT_MAX_MEMORY_KEYS) {
|
|
149
|
+
const firstKey = this._store.keys().next().value;
|
|
150
|
+
if (firstKey !== undefined) {
|
|
151
|
+
const oldEntry = this._store.get(firstKey);
|
|
152
|
+
this._store.delete(firstKey);
|
|
153
|
+
if (oldEntry?.ptHash && this._reverseStore.get(oldEntry.ptHash) === firstKey) {
|
|
154
|
+
this._reverseStore.delete(oldEntry.ptHash);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const existing = this._store.get(token);
|
|
160
|
+
// Delete to reset insertion order (implementing true LRU over simple FIFO)
|
|
161
|
+
if (existing) this._store.delete(token);
|
|
162
|
+
|
|
117
163
|
this._store.set(token, {
|
|
118
|
-
plaintext,
|
|
164
|
+
plaintext: ciphertext,
|
|
119
165
|
expiry: (Date.now() / 1000) + ttlSeconds,
|
|
120
|
-
ptHash
|
|
166
|
+
ptHash,
|
|
167
|
+
metadata: { ...(existing?.metadata || {}), ...(metadata || {}) },
|
|
121
168
|
});
|
|
122
169
|
if (ptHash) {
|
|
123
170
|
this._reverseStore.set(ptHash, token);
|
|
@@ -125,7 +172,6 @@ export class MemoryVault extends BaseVault {
|
|
|
125
172
|
}
|
|
126
173
|
|
|
127
174
|
async getTokenByPlaintextHash(ptHash: string): Promise<string | null> {
|
|
128
|
-
this._cleanup();
|
|
129
175
|
const token = this._reverseStore.get(ptHash);
|
|
130
176
|
if (token && this._store.has(token)) {
|
|
131
177
|
return token;
|
|
@@ -133,8 +179,12 @@ export class MemoryVault extends BaseVault {
|
|
|
133
179
|
return null;
|
|
134
180
|
}
|
|
135
181
|
|
|
182
|
+
async getPtHashForToken(token: string): Promise<string | null> {
|
|
183
|
+
const entry = this._store.get(token);
|
|
184
|
+
return entry?.ptHash ?? null;
|
|
185
|
+
}
|
|
186
|
+
|
|
136
187
|
async retrieve(token: string): Promise<string | null> {
|
|
137
|
-
this._cleanup();
|
|
138
188
|
const entry = this._store.get(token);
|
|
139
189
|
if (!entry) {
|
|
140
190
|
return null;
|
|
@@ -204,13 +254,14 @@ export class RedisVault extends BaseVault {
|
|
|
204
254
|
}
|
|
205
255
|
}
|
|
206
256
|
|
|
207
|
-
async store(token: string, ciphertext: string, ttlSeconds: number, ptHash: string | null = null): Promise<void> {
|
|
257
|
+
async store(token: string, ciphertext: string, ttlSeconds: number, ptHash: string | null = null, metadata: Record<string, string> | null = null): Promise<void> {
|
|
208
258
|
try {
|
|
209
259
|
const pipeline = this._client.pipeline();
|
|
210
|
-
|
|
260
|
+
const payload = metadata ? JSON.stringify({ ct: ciphertext, meta: metadata }) : ciphertext;
|
|
261
|
+
pipeline.set(_vaultKey(token), payload, 'EX', ttlSeconds);
|
|
211
262
|
if (ptHash) {
|
|
212
|
-
pipeline.set(
|
|
213
|
-
pipeline.set(
|
|
263
|
+
pipeline.set(_vaultRevKey(ptHash), token, 'EX', ttlSeconds);
|
|
264
|
+
pipeline.set(_vaultHashKey(token), ptHash, 'EX', ttlSeconds);
|
|
214
265
|
}
|
|
215
266
|
const results = await pipeline.exec();
|
|
216
267
|
if (results) {
|
|
@@ -223,14 +274,22 @@ export class RedisVault extends BaseVault {
|
|
|
223
274
|
}
|
|
224
275
|
}
|
|
225
276
|
|
|
277
|
+
async getPtHashForToken(token: string): Promise<string | null> {
|
|
278
|
+
try {
|
|
279
|
+
return await this._client.get(_vaultHashKey(token));
|
|
280
|
+
} catch {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
226
285
|
async getTokenByPlaintextHash(ptHash: string): Promise<string | null> {
|
|
227
286
|
try {
|
|
228
|
-
const token = await this._client.get(
|
|
287
|
+
const token = await this._client.get(_vaultRevKey(ptHash));
|
|
229
288
|
if (token) {
|
|
230
|
-
if (await this._client.exists(
|
|
289
|
+
if (await this._client.exists(_vaultKey(token))) {
|
|
231
290
|
return token;
|
|
232
291
|
} else {
|
|
233
|
-
await this._client.del(
|
|
292
|
+
await this._client.del(_vaultRevKey(ptHash));
|
|
234
293
|
}
|
|
235
294
|
}
|
|
236
295
|
return null;
|
|
@@ -244,7 +303,8 @@ export class RedisVault extends BaseVault {
|
|
|
244
303
|
|
|
245
304
|
async retrieve(token: string): Promise<string | null> {
|
|
246
305
|
try {
|
|
247
|
-
|
|
306
|
+
const raw = await this._client.get(_vaultKey(token));
|
|
307
|
+
return raw ? _unwrapPayload(raw) : null;
|
|
248
308
|
} catch (e) {
|
|
249
309
|
if (_getFailStrategy() === 'closed') {
|
|
250
310
|
throw new MaskVaultConnectionError(`Redis read failed: ${e}`);
|
|
@@ -255,12 +315,12 @@ export class RedisVault extends BaseVault {
|
|
|
255
315
|
|
|
256
316
|
async delete(token: string): Promise<void> {
|
|
257
317
|
try {
|
|
258
|
-
const ptHash = await this._client.get(
|
|
318
|
+
const ptHash = await this._client.get(_vaultHashKey(token));
|
|
259
319
|
const pipeline = this._client.pipeline();
|
|
260
|
-
pipeline.del(
|
|
261
|
-
pipeline.del(
|
|
320
|
+
pipeline.del(_vaultKey(token));
|
|
321
|
+
pipeline.del(_vaultHashKey(token));
|
|
262
322
|
if (ptHash) {
|
|
263
|
-
pipeline.del(
|
|
323
|
+
pipeline.del(_vaultRevKey(ptHash));
|
|
264
324
|
}
|
|
265
325
|
await pipeline.exec();
|
|
266
326
|
} catch (e) {
|
|
@@ -312,54 +372,38 @@ export class DynamoDBVault extends BaseVault {
|
|
|
312
372
|
console.info(`DynamoDBVault connected to table ${this._tableName} in ${this._region}`);
|
|
313
373
|
}
|
|
314
374
|
|
|
315
|
-
async store(token: string, ciphertext: string, ttlSeconds: number, ptHash: string | null = null): Promise<void> {
|
|
375
|
+
async store(token: string, ciphertext: string, ttlSeconds: number, ptHash: string | null = null, metadata: Record<string, string> | null = null): Promise<void> {
|
|
316
376
|
const { TransactWriteCommand, PutCommand } = require('@aws-sdk/lib-dynamodb');
|
|
317
377
|
const now = Math.floor(Date.now() / 1000);
|
|
318
378
|
const ttlVal = now + ttlSeconds;
|
|
319
|
-
const
|
|
320
|
-
token:
|
|
379
|
+
const primaryItem: Record<string, any> = {
|
|
380
|
+
token: _vaultKey(token),
|
|
321
381
|
ciphertext: ciphertext,
|
|
322
382
|
ttl: ttlVal,
|
|
323
|
-
ptr_hash: ptHash || undefined
|
|
324
383
|
};
|
|
384
|
+
if (ptHash) primaryItem.ptr_hash = ptHash;
|
|
385
|
+
if (metadata) primaryItem.meta_json = JSON.stringify(metadata);
|
|
325
386
|
|
|
326
387
|
if (ptHash) {
|
|
327
388
|
try {
|
|
328
389
|
await this._client.send(new TransactWriteCommand({
|
|
329
390
|
TransactItems: [
|
|
391
|
+
{ Put: { TableName: this._tableName, Item: primaryItem } },
|
|
330
392
|
{
|
|
331
393
|
Put: {
|
|
332
394
|
TableName: this._tableName,
|
|
333
|
-
Item: {
|
|
334
|
-
token: `mask:${token}`,
|
|
335
|
-
ciphertext: ciphertext,
|
|
336
|
-
ttl: ttlVal,
|
|
337
|
-
ptr_hash: ptHash
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
},
|
|
341
|
-
{
|
|
342
|
-
Put: {
|
|
343
|
-
TableName: this._tableName,
|
|
344
|
-
Item: {
|
|
345
|
-
token: `mask-rev:${ptHash}`,
|
|
346
|
-
ciphertext: token,
|
|
347
|
-
ttl: ttlVal
|
|
348
|
-
}
|
|
395
|
+
Item: { token: _vaultRevKey(ptHash), ciphertext: token, ttl: ttlVal }
|
|
349
396
|
}
|
|
350
397
|
}
|
|
351
398
|
]
|
|
352
399
|
}));
|
|
353
400
|
} catch (e: any) {
|
|
354
401
|
console.error(`DynamoDB transact_write_items failed: ${e}`);
|
|
355
|
-
// In both strategies, we must raise for DynamoDB failures to prevent data loss
|
|
356
|
-
// since we didn't perform the atomic write.
|
|
357
402
|
throw new MaskVaultConnectionError(`DynamoDB atomic write failed: ${e}`);
|
|
358
403
|
}
|
|
359
404
|
} else {
|
|
360
|
-
// Single store (no reverse index)
|
|
361
405
|
try {
|
|
362
|
-
await this._client.send(new PutCommand({ TableName: this._tableName, Item:
|
|
406
|
+
await this._client.send(new PutCommand({ TableName: this._tableName, Item: primaryItem }));
|
|
363
407
|
} catch (e: any) {
|
|
364
408
|
throw new MaskVaultConnectionError(`DynamoDB individual write failed: ${e}`);
|
|
365
409
|
}
|
|
@@ -372,12 +416,12 @@ export class DynamoDBVault extends BaseVault {
|
|
|
372
416
|
const now = Math.floor(Date.now() / 1000);
|
|
373
417
|
const resp = await this._client.send(new GetCommand({
|
|
374
418
|
TableName: this._tableName,
|
|
375
|
-
Key: { token:
|
|
419
|
+
Key: { token: _vaultRevKey(ptHash) }
|
|
376
420
|
}));
|
|
377
421
|
const item = resp.Item;
|
|
378
422
|
if (!item) return null;
|
|
379
423
|
if (now > (item.ttl || 0)) {
|
|
380
|
-
await this._client.send(new DeleteCommand({ TableName: this._tableName, Key: { token:
|
|
424
|
+
await this._client.send(new DeleteCommand({ TableName: this._tableName, Key: { token: _vaultRevKey(ptHash) } }));
|
|
381
425
|
return null;
|
|
382
426
|
}
|
|
383
427
|
const token = item.ciphertext;
|
|
@@ -394,16 +438,16 @@ export class DynamoDBVault extends BaseVault {
|
|
|
394
438
|
const now = Math.floor(Date.now() / 1000);
|
|
395
439
|
const resp = await this._client.send(new GetCommand({
|
|
396
440
|
TableName: this._tableName,
|
|
397
|
-
Key: { token:
|
|
441
|
+
Key: { token: _vaultKey(token) }
|
|
398
442
|
}));
|
|
399
443
|
const item = resp.Item;
|
|
400
444
|
if (!item) return null;
|
|
401
445
|
if (now > (item.ttl || 0)) {
|
|
402
446
|
const ptHash = item.ptr_hash;
|
|
403
447
|
if (ptHash) {
|
|
404
|
-
await this._client.send(new DeleteCommand({ TableName: this._tableName, Key: { token:
|
|
448
|
+
await this._client.send(new DeleteCommand({ TableName: this._tableName, Key: { token: _vaultRevKey(ptHash) } }));
|
|
405
449
|
}
|
|
406
|
-
await this._client.send(new DeleteCommand({ TableName: this._tableName, Key: { token:
|
|
450
|
+
await this._client.send(new DeleteCommand({ TableName: this._tableName, Key: { token: _vaultKey(token) } }));
|
|
407
451
|
return null;
|
|
408
452
|
}
|
|
409
453
|
return item.ciphertext;
|
|
@@ -413,18 +457,29 @@ export class DynamoDBVault extends BaseVault {
|
|
|
413
457
|
}
|
|
414
458
|
}
|
|
415
459
|
|
|
460
|
+
async getPtHashForToken(token: string): Promise<string | null> {
|
|
461
|
+
try {
|
|
462
|
+
const { GetCommand } = require('@aws-sdk/lib-dynamodb');
|
|
463
|
+
const resp = await this._client.send(new GetCommand({
|
|
464
|
+
TableName: this._tableName,
|
|
465
|
+
Key: { token: _vaultKey(token) }
|
|
466
|
+
}));
|
|
467
|
+
return resp.Item?.ptr_hash ?? null;
|
|
468
|
+
} catch { return null; }
|
|
469
|
+
}
|
|
470
|
+
|
|
416
471
|
async delete(token: string): Promise<void> {
|
|
417
472
|
try {
|
|
418
473
|
const { GetCommand, DeleteCommand } = require('@aws-sdk/lib-dynamodb');
|
|
419
474
|
const resp = await this._client.send(new GetCommand({
|
|
420
475
|
TableName: this._tableName,
|
|
421
|
-
Key: { token:
|
|
476
|
+
Key: { token: _vaultKey(token) }
|
|
422
477
|
}));
|
|
423
478
|
const item = resp.Item;
|
|
424
479
|
if (item && item.ptr_hash) {
|
|
425
|
-
await this._client.send(new DeleteCommand({ TableName: this._tableName, Key: { token:
|
|
480
|
+
await this._client.send(new DeleteCommand({ TableName: this._tableName, Key: { token: _vaultRevKey(item.ptr_hash) } }));
|
|
426
481
|
}
|
|
427
|
-
await this._client.send(new DeleteCommand({ TableName: this._tableName, Key: { token:
|
|
482
|
+
await this._client.send(new DeleteCommand({ TableName: this._tableName, Key: { token: _vaultKey(token) } }));
|
|
428
483
|
} catch (e: any) {
|
|
429
484
|
if (_getFailStrategy() === 'closed') throw new MaskVaultConnectionError(`DynamoDB delete failed: ${e}`);
|
|
430
485
|
}
|
|
@@ -450,21 +505,29 @@ export class MemcachedVault extends BaseVault {
|
|
|
450
505
|
}
|
|
451
506
|
}
|
|
452
507
|
|
|
453
|
-
async store(token: string, ciphertext: string, ttlSeconds: number, ptHash: string | null = null): Promise<void> {
|
|
508
|
+
async store(token: string, ciphertext: string, ttlSeconds: number, ptHash: string | null = null, metadata: Record<string, string> | null = null): Promise<void> {
|
|
454
509
|
try {
|
|
455
|
-
|
|
510
|
+
const payload = metadata ? JSON.stringify({ ct: ciphertext, meta: metadata }) : ciphertext;
|
|
511
|
+
await this._client.set(_vaultKey(token), Buffer.from(payload), { expires: ttlSeconds });
|
|
456
512
|
if (ptHash) {
|
|
457
|
-
await this._client.set(
|
|
458
|
-
await this._client.set(
|
|
513
|
+
await this._client.set(_vaultRevKey(ptHash), Buffer.from(token), { expires: ttlSeconds });
|
|
514
|
+
await this._client.set(_vaultHashKey(token), Buffer.from(ptHash), { expires: ttlSeconds });
|
|
459
515
|
}
|
|
460
516
|
} catch (e) {
|
|
461
517
|
throw new MaskVaultConnectionError(`Memcached error: ${e}`);
|
|
462
518
|
}
|
|
463
519
|
}
|
|
464
520
|
|
|
521
|
+
async getPtHashForToken(token: string): Promise<string | null> {
|
|
522
|
+
try {
|
|
523
|
+
const { value } = await this._client.get(_vaultHashKey(token));
|
|
524
|
+
return value ? value.toString() : null;
|
|
525
|
+
} catch { return null; }
|
|
526
|
+
}
|
|
527
|
+
|
|
465
528
|
async getTokenByPlaintextHash(ptHash: string): Promise<string | null> {
|
|
466
529
|
try {
|
|
467
|
-
const { value } = await this._client.get(
|
|
530
|
+
const { value } = await this._client.get(_vaultRevKey(ptHash));
|
|
468
531
|
if (!value) return null;
|
|
469
532
|
const token = value.toString();
|
|
470
533
|
return (await this.retrieve(token)) !== null ? token : null;
|
|
@@ -476,8 +539,9 @@ export class MemcachedVault extends BaseVault {
|
|
|
476
539
|
|
|
477
540
|
async retrieve(token: string): Promise<string | null> {
|
|
478
541
|
try {
|
|
479
|
-
const { value } = await this._client.get(
|
|
480
|
-
|
|
542
|
+
const { value } = await this._client.get(_vaultKey(token));
|
|
543
|
+
if (!value) return null;
|
|
544
|
+
return _unwrapPayload(value.toString());
|
|
481
545
|
} catch (e) {
|
|
482
546
|
if (_getFailStrategy() === 'closed') throw new MaskVaultConnectionError(`Memcached read failed: ${e}`);
|
|
483
547
|
return null;
|
|
@@ -486,12 +550,12 @@ export class MemcachedVault extends BaseVault {
|
|
|
486
550
|
|
|
487
551
|
async delete(token: string): Promise<void> {
|
|
488
552
|
try {
|
|
489
|
-
const { value } = await this._client.get(
|
|
553
|
+
const { value } = await this._client.get(_vaultHashKey(token));
|
|
490
554
|
const ptHash = value ? value.toString() : null;
|
|
491
|
-
await this._client.delete(
|
|
492
|
-
await this._client.delete(
|
|
555
|
+
await this._client.delete(_vaultKey(token));
|
|
556
|
+
await this._client.delete(_vaultHashKey(token));
|
|
493
557
|
if (ptHash) {
|
|
494
|
-
await this._client.delete(
|
|
558
|
+
await this._client.delete(_vaultRevKey(ptHash));
|
|
495
559
|
}
|
|
496
560
|
} catch (e) {
|
|
497
561
|
if (_getFailStrategy() === 'closed') throw new MaskVaultConnectionError(`Memcached delete failed: ${e}`);
|
|
@@ -539,6 +603,7 @@ export type EncodeOptions = {
|
|
|
539
603
|
searchBuckets?: ('year' | 'month' | 'day' | 'numeric')[];
|
|
540
604
|
searchBucketSize?: number;
|
|
541
605
|
entityType?: string;
|
|
606
|
+
metadata?: Record<string, string> | null;
|
|
542
607
|
};
|
|
543
608
|
|
|
544
609
|
/**
|
|
@@ -566,16 +631,26 @@ export async function encode(rawText: string, options: EncodeOptions = {}): Prom
|
|
|
566
631
|
}
|
|
567
632
|
|
|
568
633
|
// 2. Generate new token
|
|
569
|
-
const token = await
|
|
634
|
+
const token = await generateDPToken(text, options.entityType || 'UNKNOWN');
|
|
570
635
|
|
|
571
636
|
// 3. Encrypt the plaintext before it touches the vault
|
|
572
637
|
const ciphertext = cryptoEngine.encrypt(text);
|
|
573
638
|
|
|
574
|
-
// 4.
|
|
639
|
+
// 4. Collision Detection — refuse to overwrite a different plaintext under the same token
|
|
640
|
+
const existingCiphertext = await vault.retrieve(token);
|
|
641
|
+
if (existingCiphertext !== null) {
|
|
642
|
+
const existingHash = await vault.getPtHashForToken(token);
|
|
643
|
+
if (existingHash && existingHash !== ptHash) {
|
|
644
|
+
throw new TokenCollisionError(token, existingHash, ptHash);
|
|
645
|
+
}
|
|
646
|
+
// Same plaintext re-encoded (hash matches) — safe to proceed/update
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// 5. Store with primary reverse lookup hash and optional compliance metadata
|
|
575
650
|
const ttl = options.ttl || DEFAULT_TTL;
|
|
576
|
-
await vault.store(token, ciphertext, ttl, ptHash);
|
|
651
|
+
await vault.store(token, ciphertext, ttl, ptHash, options.metadata || null);
|
|
577
652
|
|
|
578
|
-
//
|
|
653
|
+
// 6. Store additional blind indices if buckets are requested
|
|
579
654
|
if (options.searchBuckets && options.searchBuckets.length > 0) {
|
|
580
655
|
for (const bType of options.searchBuckets) {
|
|
581
656
|
let bucketVal: string;
|
|
@@ -586,7 +661,6 @@ export async function encode(rawText: string, options: EncodeOptions = {}): Prom
|
|
|
586
661
|
}
|
|
587
662
|
const bHash = await BucketManager.getBucketIndex(bucketVal);
|
|
588
663
|
await vault.store(token, ciphertext, ttl, bHash);
|
|
589
|
-
|
|
590
664
|
}
|
|
591
665
|
}
|
|
592
666
|
|