mask-privacy 3.5.0 → 4.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mask-privacy",
3
- "version": "3.5.0",
3
+ "version": "4.0.0",
4
4
  "description": "Enterprise-grade AI Data Loss Prevention (DLP) SDK for TypeScript",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/config.ts CHANGED
@@ -36,6 +36,11 @@ export const config = {
36
36
  get MASK_DEV_MODE() { return getEnvBool('MASK_DEV_MODE', false); },
37
37
  get MASK_LOG_LEVEL() { return (process.env.MASK_LOG_LEVEL || 'info').toLowerCase(); },
38
38
 
39
+ // --- COMPLIANCE & PRIVACY (Bijective) ---
40
+ get MASK_TENANT_ID() { return process.env.MASK_TENANT_ID || 'global-default-tenant'; },
41
+ get MASK_SALT_ROTATION() { return (process.env.MASK_SALT_ROTATION || 'NONE').toUpperCase(); },
42
+ get MASK_BIJECTIVE_MODE() { return getEnvBool('MASK_BIJECTIVE_MODE', true); },
43
+
39
44
  // --- SECURITY & CRYPTOGRAPHY ---
40
45
  get MASK_ENCRYPTION_KEY() { return process.env.MASK_ENCRYPTION_KEY || null; },
41
46
  get MASK_MASTER_KEY() {
@@ -35,6 +35,8 @@ export interface PatternDescriptor {
35
35
  validatorTag: string | null;
36
36
  isHighEntropy: boolean;
37
37
  supportedLocales: string[];
38
+ ruleId: string; // Unique ID for compliance audit trail
39
+ complianceScope: ReadonlySet<string>; // Compliance frameworks: {"PCI-DSS", "HIPAA", ...}
38
40
  }
39
41
 
40
42
  // ── Locale-specific auxiliary patterns ──────────────────────────────────────
@@ -71,6 +73,8 @@ type RawEntry = [
71
73
  validatorTag: string | null,
72
74
  isHighEntropy?: boolean,
73
75
  supportedLocales?: string[],
76
+ ruleId?: string,
77
+ complianceScope?: string[],
74
78
  ];
75
79
 
76
80
  const RAW_PATTERNS: RawEntry[] = [
@@ -291,7 +295,7 @@ export class DLPPatternRegistry {
291
295
 
292
296
  private buildCatalogue(restrict: ReadonlySet<SensitiveCategory> | null): void {
293
297
  for (const entry of RAW_PATTERNS) {
294
- const [typeName, regexSource, terms, risk, cat, vtag, isHighEntropy, supportedLocales] = entry;
298
+ const [typeName, regexSource, terms, risk, cat, vtag, isHighEntropy, supportedLocales, ruleId, complianceScope] = entry;
295
299
  if (restrict !== null && !restrict.has(cat)) continue;
296
300
 
297
301
  let re: RegExp;
@@ -309,6 +313,8 @@ export class DLPPatternRegistry {
309
313
  validatorTag: vtag,
310
314
  isHighEntropy: isHighEntropy ?? (vtag !== null),
311
315
  supportedLocales: supportedLocales ?? ["*"],
316
+ ruleId: ruleId ?? `MASK-${cat.slice(0, 3)}-${typeName}`,
317
+ complianceScope: new Set(complianceScope ?? []),
312
318
  });
313
319
  }
314
320
  }
package/src/core/fpe.ts CHANGED
@@ -10,6 +10,14 @@ import * as crypto from 'crypto';
10
10
  import { config } from '../config';
11
11
  import { getKeyProvider } from './key_provider';
12
12
  import { MaskSecurityError } from './exceptions';
13
+ import {
14
+ FIRST_NAMES as _BIJECTIVE_NAMES,
15
+ CONNECTORS as _BIJECTIVE_CONNECTORS,
16
+ SURNAME_ROOTS as _BIJECTIVE_ROOTS,
17
+ SURNAME_SUFFIXES as _BIJECTIVE_SUFFIXES,
18
+ SYLLABLES as _BIJECTIVE_SYLLABLES
19
+ } from './synthesisLibrary';
20
+
13
21
 
14
22
  // Master key management
15
23
 
@@ -51,7 +59,7 @@ export function resetMasterKey(): void {
51
59
  // Detectors — order matters: first match wins
52
60
 
53
61
  const _EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
54
- const _PHONE_RE = /(?<!\d)(?:\+?1?[\s\-.]?\(?\d{3}\)?[\s\-.]?\d{3}[\s\-.]?\d{4}|\d{3}[\s\-.]?\d{4})(?!\d)/;
62
+ const _PHONE_RE = /(?<!\d)(?:\+?1?[\s\-.]?\(?\d{3}\)?[\s\-.]?\d{3}[\s\-.]?\d{4}|\d{3}[\s\-.]?\d{4}|\+\d{2,3}[\s\-.]?\d{3}[\s\-.]?\d{3}[\s\-.]?\d{3,4})(?!\d)/;
55
63
  const _PHONE_INTL_RE = /(?<!\d)\+(?:[1-9]\d{0,3})[-.\s]?\(?\d{1,5}\)?(?:[-.\s]?\d{2,4}){2,4}(?!\d)/;
56
64
  const _SSN_RE = /^\d{3}-\d{2}-\d{4}$/;
57
65
  const _CC_RE = /^(?:\d{4}[ \-]?){3}\d{4}$/;
@@ -71,39 +79,165 @@ async function _hmacHex(plaintext: string, n: number = 8): Promise<string> {
71
79
  return digest.slice(0, n);
72
80
  }
73
81
 
74
- /** Return *n* deterministic decimal digits derived from HMAC(key, plaintext). */
75
- async function _hmacDigits(plaintext: string, n: number, offset: number = 0): Promise<string> {
82
+ /**
83
+ * Return a deterministic 128-bit BigInt from HMAC(key, plaintext).
84
+ *
85
+ * Uses the first 16 bytes (128 bits) of the SHA-256 HMAC digest,
86
+ * providing a namespace of 2^128 (~3.4 × 10^38). This replaces the
87
+ * old nibble-by-nibble modulo-10 approach which suffered from severe
88
+ * distribution bias in short fields (3-4 digits).
89
+ */
90
+ async function _hmacInt(plaintext: string): Promise<bigint> {
76
91
  const masterKey = await _getMasterKey();
77
- const digest = crypto
92
+ const raw = crypto
78
93
  .createHmac('sha256', masterKey)
79
94
  .update(plaintext, 'utf-8')
80
- .digest('hex');
95
+ .digest();
96
+ // Read first 16 bytes as a big-endian unsigned integer
97
+ let result = 0n;
98
+ for (let i = 0; i < 16; i++) {
99
+ result = (result << 8n) | BigInt(raw[i]);
100
+ }
101
+ return result;
102
+ }
103
+
104
+ /**
105
+ * Return *n* deterministic decimal digits from HMAC(key, plaintext).
106
+ *
107
+ * Uses full-integer division of a 128-bit HMAC-derived seed instead of
108
+ * per-nibble modulo-10, which eliminates the distribution bias that
109
+ * caused collisions in short numeric fields (routing numbers, SSN
110
+ * suffixes). The offset parameter salts the input to derive
111
+ * independent digit sequences from the same plaintext.
112
+ */
113
+ async function _hmacDigits(plaintext: string, n: number, offset: number = 0): Promise<string> {
114
+ const salted = offset ? `${plaintext}::${offset}` : plaintext;
115
+ const seed = await _hmacInt(salted);
116
+ const modulus = 10n ** BigInt(n);
117
+ return (seed % modulus).toString().padStart(n, '0');
118
+ }
81
119
 
82
- const result: string[] = [];
83
- for (let i = offset; i < digest.length; i++) {
84
- const ch = digest[i];
85
- result.push((parseInt(ch, 16) % 10).toString());
86
- if (result.length === n) break;
120
+ // ── Bijective Synthesis Engine ─────────────────────────────────────────────
121
+
122
+ export class FF1 {
123
+ /** NIST SP 800-38G FF1 implementation (simplified for 64-bit domains). */
124
+ constructor(private key: Buffer, private tweak: Buffer) {}
125
+
126
+ encrypt(n: bigint): bigint {
127
+ /** Encrypts 64-bit bigint n using FF1 (10 rounds). */
128
+ let A = n >> 32n;
129
+ let B = n & 0xFFFFFFFFn;
130
+ const radix = 2n ** 32n;
131
+
132
+ for (let i = 0; i < 10; i++) {
133
+ const tweakInfoBuffer = Buffer.alloc(8);
134
+ tweakInfoBuffer.writeUInt32BE(i, 0);
135
+ tweakInfoBuffer.writeUInt32BE(Number(B), 4);
136
+ const tweakInfoCombined = Buffer.concat([this.tweak, tweakInfoBuffer]);
137
+
138
+ const h = crypto.createHmac('sha256', this.key)
139
+ .update(tweakInfoCombined)
140
+ .digest();
141
+
142
+ const roundVal = BigInt(h.readUInt32BE(0));
143
+
144
+ const Anext = B;
145
+ const Bnext = (A + roundVal) % radix;
146
+ A = Anext;
147
+ B = Bnext;
148
+ }
149
+
150
+ return (A << 32n) | B;
87
151
  }
88
152
 
89
- while (result.length < n) {
90
- result.push("0");
153
+ decrypt(n: bigint): bigint {
154
+ /** Decrypts 64-bit bigint n using FF1 (10 rounds in reverse). */
155
+ let A = n >> 32n;
156
+ let B = n & 0xFFFFFFFFn;
157
+ const radix = 2n ** 32n;
158
+
159
+ for (let i = 9; i >= 0; i--) {
160
+ const tweakInfoBuffer = Buffer.alloc(8);
161
+ tweakInfoBuffer.writeUInt32BE(i, 0);
162
+ tweakInfoBuffer.writeUInt32BE(Number(A), 4);
163
+ const tweakInfoCombined = Buffer.concat([this.tweak, tweakInfoBuffer]);
164
+
165
+ const h = crypto.createHmac('sha256', this.key)
166
+ .update(tweakInfoCombined)
167
+ .digest();
168
+
169
+ const roundVal = BigInt(h.readUInt32BE(0));
170
+
171
+ const Bprev = A;
172
+ const Aprev = (B - roundVal + radix) % radix;
173
+ A = Aprev;
174
+ B = Bprev;
175
+ }
176
+
177
+ return (A << 32n) | B;
91
178
  }
92
- return result.join("");
93
179
  }
94
180
 
95
- // Public API
181
+ async function _getBijectiveTweak(): Promise<Buffer> {
182
+ const masterKey = await _getMasterKey();
183
+ let base = config.MASK_TENANT_ID;
184
+ if (config.MASK_SALT_ROTATION !== 'NONE') {
185
+ const now = new Date();
186
+ if (config.MASK_SALT_ROTATION === 'MONTHLY') {
187
+ base += `-${now.getUTCFullYear()}-${now.getUTCMonth() + 1}`;
188
+ } else if (config.MASK_SALT_ROTATION === 'YEARLY') {
189
+ base += `-${now.getUTCFullYear()}`;
190
+ }
191
+ }
192
+ return crypto.createHmac('sha256', masterKey).update(base, 'utf-8').digest();
193
+ }
96
194
 
97
- // Dictionary for Semantic NLP Faker Generation
98
- const _FIRST_NAMES = ["Taylor", "Jordan", "Casey", "Morgan", "Riley", "Avery", "Rowan", "Quinn", "Charlie", "Peyton", "Blake", "Dakota", "Reese", "Skyler", "Finley", "Eden", "Harley", "Rory", "Emerson", "Remi"];
99
- const _LAST_NAMES = ["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez", "Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin"];
100
- const _CITIES = ["London", "Paris", "Berlin", "Tokyo", "Rome", "Madrid", "Vienna", "Sydney", "Toronto", "Chicago", "Seattle", "Austin", "Boston", "Denver", "Dallas", "Miami", "Seoul", "Dubai", "Mumbai", "Cairo"];
195
+ function _renderBijectivePerson(bits: bigint): string {
196
+ /** Render a 64-bit cipher into a human-readable name (Bijective Synthesis). */
197
+ const firstIdx = Number(bits & 0x7FFn); // 11 bits (2048)
198
+ const connIdx = Number((bits >> 11n) & 0x3Fn); // 6 bits (64)
199
+ const rootIdx = Number((bits >> 17n) & 0xFFFn); // 12 bits (4096)
200
+ const suffixIdx = Number((bits >> 29n) & 0x1FFn); // 9 bits (512)
201
+ const tag = Number((bits >> 38n) & 0x3FFFn); // 14 bits (16384)
202
+ const formatIdx = Number((bits >> 52n) & 0xFn); // 4 bits (16)
203
+
204
+ const first = _BIJECTIVE_NAMES[firstIdx % _BIJECTIVE_NAMES.length];
205
+ const conn = _BIJECTIVE_CONNECTORS[connIdx % _BIJECTIVE_CONNECTORS.length];
206
+ const root = _BIJECTIVE_ROOTS[rootIdx % _BIJECTIVE_ROOTS.length];
207
+ const suffix = _BIJECTIVE_SUFFIXES[suffixIdx % _BIJECTIVE_SUFFIXES.length];
208
+ const surname = `${root}${suffix}`;
209
+ const numeric = tag % 10000;
210
+
211
+ const paddedNumeric = numeric.toString().padStart(4, '0');
212
+
213
+ // Format Shuffle
214
+ if (formatIdx === 0) return `${first} ${conn} ${surname}-${paddedNumeric}`;
215
+ if (formatIdx === 1) return `${surname}, ${first}-${paddedNumeric}`;
216
+ if (formatIdx === 2) return `${first[0]}. ${surname}-${paddedNumeric}`;
217
+ if (formatIdx === 3) return `${first} ${surname}-${paddedNumeric}`;
218
+
219
+ return `${first} ${surname}-${paddedNumeric}`;
220
+ }
221
+
222
+ function _renderBijectiveLocation(bits: bigint): string {
223
+ /** Render a 64-bit cipher into a bijective location name. */
224
+ const s1 = Number(bits & 0x3FFn);
225
+ const s2 = Number((bits >> 10n) & 0x3FFn);
226
+ const s3 = Number((bits >> 20n) & 0x3FFn);
227
+ const tag = Number((bits >> 30n) & 0xFFFn);
228
+
229
+ const city = `${_BIJECTIVE_SYLLABLES[s1 % 1000]}${_BIJECTIVE_SYLLABLES[s2 % 1000].toLowerCase()}${_BIJECTIVE_SYLLABLES[s3 % 1000].toLowerCase()}`;
230
+ return `${city}-${tag.toString().padStart(3, '0')}`;
231
+ }
101
232
 
102
- /** Return a deterministic item from an array. */
233
+ // ── Legacy Semantic Token Banks (Redirected in Bijective Mode) ──────────────
234
+ // Seed lists are imported from semanticBanks.ts, maintaining architecture
235
+ // parity with python/semantic_banks.py
236
+
237
+ /** Return a deterministic item from an array using full 128-bit entropy. */
103
238
  async function _pickFromArray(plaintext: string, array: string[]): Promise<string> {
104
- const digits = await _hmacDigits(plaintext, 8);
105
- const num = parseInt(digits, 10);
106
- return array[num % array.length];
239
+ const seed = await _hmacInt(plaintext);
240
+ return array[Number(seed % BigInt(array.length))];
107
241
  }
108
242
 
109
243
  /** Compute Luhn check digit */
@@ -143,7 +277,7 @@ export async function generateFPEToken(rawText: string, entityType: string = 'UN
143
277
  else if (_SSN_RE.test(text)) type = "US_SSN";
144
278
  else if (_CC_RE.test(text)) type = "CREDIT_CARD";
145
279
  else if (_ROUTING_RE.test(text)) type = "US_ROUTING_NUMBER";
146
- else if (_ES_ID_RE.test(text)) type = "ES_DNI";
280
+ else if (_ES_ID_RE.test(text)) type = "ES_ID";
147
281
  else if (_IBAN_RE.test(text)) type = "INTL_BANK_IBAN";
148
282
  else if (_PHONE_RE.test(text)) type = "PHONE_NUMBER";
149
283
  }
@@ -180,23 +314,37 @@ export async function generateFPEToken(rawText: string, entityType: string = 'UN
180
314
  return `${countryCode}00${(await _hmacHex(text, 8)).toUpperCase()}`;
181
315
  }
182
316
 
183
- if (type === "ES_DNI") {
317
+ if (type === "ES_ID" || type === "ES_DNI") {
184
318
  const digits = `000${await _hmacDigits(text, 5)}`;
185
319
  return digits + _computeEsIdCheck(parseInt(digits, 10));
186
320
  }
187
321
 
188
322
  if (type === "PERSON" || type === "PERSON_NAME") {
189
- const f = await _pickFromArray(text, _FIRST_NAMES);
190
- const l = await _pickFromArray(text + "last", _LAST_NAMES);
191
- return `<PER:${f}_${l}>`;
323
+ if (config.MASK_BIJECTIVE_MODE) {
324
+ const canonical = text.toLowerCase().trim();
325
+ const hash = crypto.createHash('sha256').update(canonical, 'utf-8').digest();
326
+ const inputInt = hash.readBigUInt64BE(0);
327
+ const masterKey = await _getMasterKey();
328
+ const engine = new FF1(masterKey.slice(0, 16), await _getBijectiveTweak());
329
+ const cipher = engine.encrypt(inputInt);
330
+ return _renderBijectivePerson(cipher);
331
+ }
332
+ return `[TKN-PERSON-${await _hmacHex(text)}]`;
192
333
  }
193
334
  if (type === "LOCATION" || type === "PHYS_ADDRESS") {
194
- const c = await _pickFromArray(text, _CITIES);
195
- return `<LOC:${c}>`;
335
+ if (config.MASK_BIJECTIVE_MODE) {
336
+ const canonical = text.toLowerCase().trim();
337
+ const hash = crypto.createHash('sha256').update(canonical, 'utf-8').digest();
338
+ const inputInt = hash.readBigUInt64BE(0);
339
+ const masterKey = await _getMasterKey();
340
+ const engine = new FF1(masterKey.slice(0, 16), await _getBijectiveTweak());
341
+ const cipher = engine.encrypt(inputInt);
342
+ return _renderBijectiveLocation(cipher);
343
+ }
344
+ return `[TKN-LOC-${await _hmacHex(text)}]`;
196
345
  }
197
346
  if (type === "ORGANIZATION") {
198
- const c = await _pickFromArray(text, _LAST_NAMES);
199
- return `<ORG:${c}_Inc>`;
347
+ return `[TKN-ORG-${await _hmacHex(text)}]`;
200
348
  }
201
349
 
202
350
  return `[TKN-${await _hmacHex(text)}]`;
@@ -11,14 +11,15 @@
11
11
  */
12
12
  export const TOKEN_PATTERN = new RegExp(
13
13
  "tkn-[a-f0-9]{8,64}@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}" + // Email
14
- "|\\+[1-9]\\d{0,3}-555-\\d{7}" + // Phone
15
- "|000-00-\\d{4}" + // SSN
16
- "|4000-0000-0000-\\d{4}" + // CC
17
- "|000000\\d{3}" + // Routing
18
- "|000\\d{5}[A-Z]" + // Spanish DNI token
19
- "|[A-Z]{2}00[A-F0-9]{4,16}" + // IBAN token
20
- "|<(?:PER|LOC|ORG):[^>]+>" + // NLP Semantic tokens
21
- "|\\[TKN-[a-f0-9]{8,64}\\]", // Opaque
14
+ "|\\+[1-9]\\d{0,3}-555-\\d{7}" + // Phone
15
+ "|000-00-\\d{4}" + // SSN
16
+ "|4000-0000-0000-\\d{4}" + // CC
17
+ "|000000\\d{3}" + // Routing
18
+ "|000\\d{5}[A-Z]" + // Spanish DNI token
19
+ "|[A-Z]{2}00[A-F0-9]{4,16}" + // IBAN token
20
+ "|<(?:PER|LOC|ORG):[^>]+>" + // NLP Semantic tokens V4
21
+ "|\\b[A-Z][a-zA-Z, ]+-[0-9]{3,4}\\b" + // Bijective Name/Loc
22
+ "|\\\\[TKN-[a-f0-9]{8,64}\\\\]", // Opaque
22
23
  "g"
23
24
  );
24
25
 
@@ -78,6 +79,15 @@ export function looksLikeToken(value: string | any): boolean {
78
79
  if (v.startsWith("[TKN-") && v.endsWith("]")) {
79
80
  return true;
80
81
  }
82
+
83
+ // Bijective Name: Word Word-1234
84
+ if (v.includes("-") && v.length >= 6) {
85
+ const parts = v.split("-");
86
+ const tag = parts[parts.length - 1];
87
+ if (tag.length === 4 && /^\d+$/.test(tag)) {
88
+ return true;
89
+ }
90
+ }
81
91
 
82
92
  return false;
83
93
  }
@@ -132,7 +132,8 @@ export class BaseScanner {
132
132
  if (conf >= confidenceThreshold) {
133
133
  spans.push({ start: m.index, end: m.index + matchedStr.length,
134
134
  entityType: typeTag, originalValue: matchedStr,
135
- confidence: conf, method: 'dlp_heuristic', language: detectedLanguage });
135
+ confidence: conf, method: 'dlp_heuristic', language: detectedLanguage,
136
+ ruleId: descriptor.ruleId, complianceScope: descriptor.complianceScope });
136
137
  }
137
138
  }
138
139
  }
package/src/core/span.ts CHANGED
@@ -14,6 +14,9 @@ export interface Span {
14
14
  confidence: number;
15
15
  method: string; // "dlp_heuristic" | "regex" | "nlp"
16
16
  language?: string;
17
+ // Audit trail fields (DSPM compliance)
18
+ ruleId?: string; // e.g. "MASK-FIN-001"
19
+ complianceScope?: ReadonlySet<string>; // e.g. {"PCI-DSS", "HIPAA"}
17
20
  maskedValue?: string;
18
21
  }
19
22
 
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Synthesis Library — Identical components to the Python SDK.
3
+ */
4
+
5
+ export const FIRST_NAMES: string[] = [
6
+ "James", "Mary", "Robert", "Patricia", "John", "Jennifer", "Michael", "Linda",
7
+ "David", "Elizabeth", "William", "Barbara", "Richard", "Susan", "Joseph", "Jessica"
8
+ ];
9
+ while (FIRST_NAMES.length < 2048) { FIRST_NAMES.push(`NameEx_${FIRST_NAMES.length}`); }
10
+
11
+ export const CONNECTORS: string[] = [
12
+ "of", "from", "van", "del", "di", "von", "le", "la", "de", "el",
13
+ "the", "near", "at", "by", "under", "over", "across", "beyond", "within", "without",
14
+ "and", "with", "aka", "alias", "formally", "lately", "born", "styled", "known", "called",
15
+ "st", "al", "bin", "ibn", "abu", "ben", "bar", "fitz", "mac", "mc",
16
+ "o", "da", "dos", "das", "do", "du", "della", "degli", "dei", "delle",
17
+ "sur", "ter", "ten", "zu", "zum", "zur", "auf", "an", "der", "die", "das",
18
+ "pro", "anti", "ex", "quasi"
19
+ ];
20
+
21
+ export const SURNAME_ROOTS: string[] = [
22
+ "Silver", "Gold", "Iron", "Stone", "Rock", "Wood", "Leaf", "Rain", "Snow", "Wind",
23
+ "Storm", "Cloud", "Sun", "Moon", "Star", "Sky", "Sea", "Lake", "River", "Brook",
24
+ "Hill", "Mount", "Vale", "Glen", "Dale", "Field", "Meadow", "Forest", "Grove", "Wild"
25
+ ];
26
+ while (SURNAME_ROOTS.length < 4096) { SURNAME_ROOTS.push(`RootEx_${SURNAME_ROOTS.length}`); }
27
+
28
+ export const SURNAME_SUFFIXES: string[] = [
29
+ "son", "man", "field", "wood", "berg", "stein", "ov", "ova", "ski", "ska",
30
+ "ez", "ez", "ia", "ic", "os", "as", "is", "us", "er", "en",
31
+ "ard", "ier", "eau", "oux", "ly", "ley", "ton", "don", "ham", "ford",
32
+ "wick", "shire", "land", "way", "side", "gate", "bridge", "well", "pool", "cliff",
33
+ "bank", "shore", "hart", "foot", "head", "more", "less", "ness", "ship", "ward"
34
+ ];
35
+ while (SURNAME_SUFFIXES.length < 512) { SURNAME_SUFFIXES.push(`SuffixEx_${SURNAME_SUFFIXES.length}`); }
36
+
37
+ export const SYLLABLES: string[] = [
38
+ "San", "Ver", "Dina", "Lon", "Don", "Chi", "Ca", "Go", "New", "York",
39
+ "Los", "An", "Ge", "Les", "Pa", "Ris", "Ber", "Lin", "Mad", "Rid"
40
+ ];
41
+ while (SYLLABLES.length < 1000) { SYLLABLES.push(`Syl_${SYLLABLES.length}`); }
package/src/index.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * and framework-agnostic tool interception hooks.
7
7
  */
8
8
 
9
- export const VERSION = "3.5.0";
9
+ export const VERSION = "3.5.1";
10
10
 
11
11
  export {
12
12
  getVault,
@@ -200,7 +200,9 @@ export class AuditLogger {
200
200
  this._bufferFullWarned = false;
201
201
 
202
202
  for (const evt of events) {
203
- console.info(JSON.stringify(evt));
203
+ // Use a replacer to handle BigInt values which are not JSON-serializable by default
204
+ const json = JSON.stringify(evt, (_, v) => typeof v === 'bigint' ? v.toString() : v);
205
+ console.info(json);
204
206
  }
205
207
  } finally {
206
208
  this._isFlushing = false;
@@ -0,0 +1,103 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from '@jest/globals';
2
+ import { generateFPEToken, resetMasterKey, FF1 } from '../src/core/fpe';
3
+ import { config } from '../src/config';
4
+ import * as process from 'process';
5
+ import * as crypto from 'crypto';
6
+
7
+ describe('BijectiveFPEIntegration', () => {
8
+ const TEST_KEY = "fixed-test-key-for-bijective-proof";
9
+ const TENANT_ID = "tenant-a";
10
+
11
+ let originalBijective: string | undefined;
12
+ let originalTenant: string | undefined;
13
+
14
+ beforeEach(() => {
15
+ resetMasterKey();
16
+ originalBijective = process.env.MASK_BIJECTIVE_MODE;
17
+ originalTenant = process.env.MASK_TENANT_ID;
18
+
19
+ process.env.MASK_BIJECTIVE_MODE = "true";
20
+ process.env.MASK_MASTER_KEY = TEST_KEY;
21
+ process.env.MASK_TENANT_ID = TENANT_ID;
22
+ });
23
+
24
+ afterEach(() => {
25
+ resetMasterKey();
26
+ if (originalBijective === undefined) delete process.env.MASK_BIJECTIVE_MODE;
27
+ else process.env.MASK_BIJECTIVE_MODE = originalBijective;
28
+
29
+ if (originalTenant === undefined) delete process.env.MASK_TENANT_ID;
30
+ else process.env.MASK_TENANT_ID = originalTenant;
31
+ });
32
+
33
+ test('test_ff1_bijective_property', async () => {
34
+ /** Verify that FF1 is a true bijection (decrypt(encrypt(x)) == x). */
35
+ const masterKeyFull = Buffer.from(TEST_KEY, 'utf-8');
36
+ const masterKey16 = masterKeyFull.slice(0, 16);
37
+ const tenantTweak = crypto.createHmac('sha256', masterKeyFull).update(TENANT_ID, 'utf-8').digest();
38
+
39
+ const engine = new FF1(masterKey16, tenantTweak);
40
+
41
+ const testValues = [
42
+ 0n, 1n, 100n, BigInt(2**31 - 1), BigInt(2**32), BigInt(2**32 + 1),
43
+ (1n << 63n) - 1n, (1n << 64n) - 1n,
44
+ 1234567890123456789n
45
+ ];
46
+
47
+ for (const val of testValues) {
48
+ const cipher = engine.encrypt(val);
49
+ const decrypted = engine.decrypt(cipher);
50
+ expect(decrypted).toBe(val);
51
+ }
52
+ });
53
+
54
+ test('test_cross_sdk_parity_golden_vector', async () => {
55
+ /**
56
+ * Verify bit-for-bit parity with Python implementation.
57
+ * Input 0 with TEST_KEY and TENANT_ID should match exactly.
58
+ */
59
+ const masterKeyFull = Buffer.from(TEST_KEY, 'utf-8');
60
+ const masterKey16 = masterKeyFull.slice(0, 16);
61
+ const tenantTweak = crypto.createHmac('sha256', masterKeyFull).update(TENANT_ID, 'utf-8').digest();
62
+ const engine = new FF1(masterKey16, tenantTweak);
63
+
64
+ const cipher = engine.encrypt(0n);
65
+ // This value matches Python output for the same test keys
66
+ expect(cipher.toString()).toBe("14723038793896035711");
67
+ });
68
+
69
+ test('test_tenant_isolation', async () => {
70
+ /** Verify that different tenants produce unique tokens. */
71
+ const name = "John Doe";
72
+
73
+ process.env.MASK_TENANT_ID = "tenant-a";
74
+ const tokenA = await generateFPEToken(name, "PERSON");
75
+
76
+ process.env.MASK_TENANT_ID = "tenant-b";
77
+ const tokenB = await generateFPEToken(name, "PERSON");
78
+
79
+ expect(tokenA).not.toBe(tokenB);
80
+
81
+ process.env.MASK_TENANT_ID = "tenant-a";
82
+ const tokenA2 = await generateFPEToken(name, "PERSON");
83
+ expect(tokenA).toBe(tokenA2);
84
+ });
85
+
86
+ test('test_human_readable_synthesis', async () => {
87
+ /** Verify synthesis pattern matches Bijective expectations. */
88
+ const res = await generateFPEToken("Jane Doe", "PERSON");
89
+ // Pattern: Name Surname-Tag (4 digits)
90
+ expect(res).toContain("-");
91
+ const parts = res.split("-");
92
+ expect(parts[parts.length - 1].length).toBe(4);
93
+ });
94
+
95
+ test('test_location_synthesis', async () => {
96
+ /** Verify bijective location synthesis. */
97
+ const res = await generateFPEToken("San Francisco", "LOCATION");
98
+ // Pattern: CityName-Tag (12 bits -> 3-4 digits)
99
+ expect(res).toContain("-");
100
+ const tag = res.split("-").pop()!;
101
+ expect(tag.length).toBeGreaterThanOrEqual(3);
102
+ });
103
+ });