mask-privacy 4.2.0 → 4.3.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": "4.2.0",
3
+ "version": "4.3.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/core/fpe.ts CHANGED
@@ -91,39 +91,44 @@ async function _getBijectiveTweak(): Promise<Buffer> {
91
91
  async function _encryptBijectiveFF1(text: string): Promise<bigint> {
92
92
  const canonical = text.toLowerCase().trim();
93
93
  const hash = crypto.createHash('sha256').update(canonical, 'utf-8').digest();
94
- // Hash to 64-bit int, then to 20-digit string
95
- const inputInt = hash.readBigUInt64BE(0);
96
- const inputStr = inputInt.toString().padStart(20, '0');
94
+
95
+ // Upgrade to 128-bit entropy (16 bytes) to prevent collisions in enterprise datasets.
96
+ const hash128 = hash.subarray(0, 16);
97
+ // BigInt in JS handles 128-bit integers natively.
98
+ const inputInt = hash128.readBigUInt64BE(0) << 64n | hash128.readBigUInt64BE(8);
99
+ const inputStr = inputInt.toString().padStart(40, '0');
97
100
 
98
101
  const aesKey = await _getAesKey();
99
102
  const tweak = await _getBijectiveTweak();
100
103
  const engine = new FF1(aesKey, tweak, 10);
101
104
 
102
105
  const cipherStr = engine.encrypt(inputStr);
103
- return BigInt(cipherStr) % (2n ** 64n);
106
+ return BigInt(cipherStr);
104
107
  }
105
108
 
106
109
  function _renderBijectivePerson(bits: bigint): string {
107
- const firstIdx = Number(bits & 0x7FFn); // 11 bits (2048)
108
- const connIdx = Number((bits >> 11n) & 0x3Fn); // 6 bits (64)
109
- const rootIdx = Number((bits >> 17n) & 0xFFFn); // 12 bits (4096)
110
- const suffixIdx = Number((bits >> 29n) & 0x1FFn); // 9 bits (512)
111
- const tag = Number((bits >> 38n) & 0x3FFFn); // 14 bits (16384)
112
- const formatIdx = Number((bits >> 52n) & 0xFn); // 4 bits (16)
110
+ // Bit allocation (128-bit space):
111
+ // First(11) + Conn(6) + Root(12) + Suffix(9) + Tag(40) + Format(4) = 82 bits
112
+ const firstIdx = Number(bits & 0x7FFn);
113
+ const connIdx = Number((bits >> 11n) & 0x3Fn);
114
+ const rootIdx = Number((bits >> 17n) & 0xFFFn);
115
+ const suffixIdx = Number((bits >> 29n) & 0x1FFn);
116
+ const tag = bits >> 38n & 0xFFFFFFFFFFn;
117
+ const formatIdx = Number((bits >> 78n) & 0xFn);
113
118
 
114
119
  const first = _BIJECTIVE_NAMES[firstIdx % _BIJECTIVE_NAMES.length];
115
120
  const conn = _BIJECTIVE_CONNECTORS[connIdx % _BIJECTIVE_CONNECTORS.length];
116
121
  const root = _BIJECTIVE_ROOTS[rootIdx % _BIJECTIVE_ROOTS.length];
117
122
  const suffix = _BIJECTIVE_SUFFIXES[suffixIdx % _BIJECTIVE_SUFFIXES.length];
118
123
  const surname = `${root}${suffix}`;
119
- const numeric = tag % 10000;
120
-
121
- const paddedNumeric = numeric.toString().padStart(4, '0');
124
+
125
+ // Expanded 10-digit tag (33 bits of entropy) ensures enterprise-grade collision avoidance.
126
+ const numeric = Number(tag % 10000000000n);
127
+ const paddedNumeric = numeric.toString().padStart(10, '0');
122
128
 
123
129
  if (formatIdx === 0) return `${first} ${conn} ${surname}-${paddedNumeric}`;
124
130
  if (formatIdx === 1) return `${surname}, ${first}-${paddedNumeric}`;
125
131
  if (formatIdx === 2) return `${first[0]}. ${surname}-${paddedNumeric}`;
126
- if (formatIdx === 3) return `${first} ${surname}-${paddedNumeric}`;
127
132
 
128
133
  return `${first} ${surname}-${paddedNumeric}`;
129
134
  }
@@ -132,10 +137,10 @@ function _renderBijectiveLocation(bits: bigint): string {
132
137
  const s1 = Number(bits & 0x3FFn);
133
138
  const s2 = Number((bits >> 10n) & 0x3FFn);
134
139
  const s3 = Number((bits >> 20n) & 0x3FFn);
135
- const tag = Number((bits >> 30n) & 0xFFFn);
140
+ const tag = bits >> 30n & 0xFFFFFFFFFFn;
136
141
 
137
142
  const city = `${_BIJECTIVE_SYLLABLES[s1 % 1000]}${_BIJECTIVE_SYLLABLES[s2 % 1000].toLowerCase()}${_BIJECTIVE_SYLLABLES[s3 % 1000].toLowerCase()}`;
138
- return `${city}-${tag.toString().padStart(3, '0')}`;
143
+ return `${city}-${(Number(tag % 10000000000n)).toString().padStart(10, '0')}`;
139
144
  }
140
145
 
141
146
  function _computeLuhnDigit(partialNum: string): string {
@@ -154,6 +159,22 @@ function _computeLuhnDigit(partialNum: string): string {
154
159
  return ((10 - (sum % 10)) % 10).toString();
155
160
  }
156
161
 
162
+ export function _getLuhnSum(numStr: string): number {
163
+ const digits = numStr.split("").map(Number);
164
+ let sum = 0;
165
+ let shouldDouble = false; // Index 15 is W1
166
+ for (let i = digits.length - 1; i >= 0; i--) {
167
+ let digit = digits[i];
168
+ if (shouldDouble) {
169
+ digit *= 2;
170
+ if (digit > 9) digit -= 9;
171
+ }
172
+ sum += digit;
173
+ shouldDouble = !shouldDouble;
174
+ }
175
+ return sum;
176
+ }
177
+
157
178
  function _computeEsIdCheck(num: number): string {
158
179
  return "TRWAGMYFPDXBNJZSQVHLCKE"[num % 23];
159
180
  }
@@ -206,16 +227,20 @@ export async function generateDPToken(rawText: string, entityType: string = 'UNK
206
227
  if (type === "CREDIT_CARD" || type === "CREDIT_CARD_NUMBER") {
207
228
  const digits = _stripCcSeparators(text);
208
229
  if (digits.length === 16) {
209
- const bin6 = digits.slice(0, 6);
210
- const last4 = digits.slice(12, 16);
211
- const middle6 = digits.slice(6, 12);
230
+ // PCI DSS v4.0.3.3 Compliance: Reveal First 6 and Last 4 ONLY.
231
+ // Luhn-sum correction ensures downstream validity without disclosing middle data.
232
+ const prefix6 = digits.slice(0, 6);
233
+ const suffix4 = digits.slice(12, 16);
234
+ const middle5 = digits.slice(6, 11);
212
235
 
213
236
  const engine = new FF1(await _getAesKey(), Buffer.from("CREDIT_CARD"), 10);
214
- const encMiddle = engine.encrypt(middle6);
237
+ const encMiddle5 = engine.encrypt(middle5);
238
+
239
+ const draft = prefix6 + encMiddle5 + '0' + suffix4;
240
+ const sum = _getLuhnSum(draft);
241
+ const correction = (10 - (sum % 10)) % 10;
215
242
 
216
- const base15 = bin6 + encMiddle + last4.slice(0, 3);
217
- const checkDig = _computeLuhnDigit(base15);
218
- const full = bin6 + encMiddle + last4.slice(0, 3) + checkDig;
243
+ const full = prefix6 + encMiddle5 + correction.toString() + suffix4;
219
244
  return `${full.slice(0, 4)}-${full.slice(4, 8)}-${full.slice(8, 12)}-${full.slice(12, 16)}`;
220
245
  } else {
221
246
  const fallbackDigits = digits.padEnd(16, '0').slice(0, 16);
@@ -18,7 +18,7 @@ export const TOKEN_PATTERN = new RegExp(
18
18
  "|\\b000\\d{5}[A-Z]\\b" + // Spanish DNI token
19
19
  "|[A-Z]{2}00[A-F0-9]{4,16}" + // IBAN token
20
20
  "|<(?:PER|LOC|ORG):[^>]+>" + // NLP Semantic tokens V4
21
- "|\\b[A-Z][a-zA-Z, ]+-[0-9]{3,4}\\b" + // Bijective Name/Loc
21
+ "|\\b[A-Z][a-zA-Z, ]+-[0-9]{3,10}\\b" + // Bijective Name/Loc
22
22
  "|\\[TKN-[^\\]]+\\]", // Opaque
23
23
  "g"
24
24
  );
@@ -84,7 +84,7 @@ export function looksLikeToken(value: string | any): boolean {
84
84
  if (v.includes("-") && v.length >= 6) {
85
85
  const parts = v.split("-");
86
86
  const tag = parts[parts.length - 1];
87
- if (tag.length === 4 && /^\d+$/.test(tag)) {
87
+ if (tag.length >= 3 && tag.length <= 10 && /^\d+$/.test(tag)) {
88
88
  return true;
89
89
  }
90
90
  }
@@ -130,8 +130,8 @@ export function isUnambiguouslySafeToken(value: string | any): boolean {
130
130
  // Opaque fallback tokens: [TKN-...]
131
131
  if (v.startsWith("[TKN-") && v.endsWith("]")) return true;
132
132
 
133
- // Bijective Name/Location tokens: always end -DDDD (synthetic pattern)
134
- if (/^[A-Z][a-zA-Z, ]+-[0-9]{3,4}$/.test(v)) return true;
133
+ // Bijective Name/Location tokens: end with synthetic numeric tag
134
+ if (/^[A-Z][a-zA-Z, ]+-[0-9]{3,10}$/.test(v)) return true;
135
135
 
136
136
  // NOTE: Raw SSN (\d{3}-\d{2}-\d{4}), CC (\d{4}-\d{4}-\d{4}-\d{4}),
137
137
  // and routing (\d{9}) patterns are intentionally EXCLUDED because real
@@ -93,7 +93,7 @@ describe('BijectiveFPEIntegration', () => {
93
93
  // Pattern: Name Surname-Tag (4 digits)
94
94
  expect(res).toContain("-");
95
95
  const parts = res.split("-");
96
- expect(parts[parts.length - 1].length).toBe(4);
96
+ expect(parts[parts.length - 1].length).toBe(10);
97
97
  });
98
98
 
99
99
  test('test_location_synthesis', async () => {
@@ -102,6 +102,6 @@ describe('BijectiveFPEIntegration', () => {
102
102
  // Pattern: CityName-Tag (12 bits -> 3-4 digits)
103
103
  expect(res).toContain("-");
104
104
  const tag = res.split("-").pop()!;
105
- expect(tag.length).toBeGreaterThanOrEqual(3);
105
+ expect(tag.length).toBe(10);
106
106
  });
107
107
  });
@@ -0,0 +1,53 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from '@jest/globals';
2
+ import { generateDPToken, resetMasterKey, _getLuhnSum } from '../src/core/fpe';
3
+
4
+ describe('CompliancePCIChecks', () => {
5
+ beforeEach(() => {
6
+ resetMasterKey();
7
+ process.env.MASK_MASTER_KEY = "compliance-remediation-test-key";
8
+ process.env.MASK_TENANT_ID = "pci-auditor";
9
+ process.env.MASK_BIJECTIVE_MODE = "true";
10
+ });
11
+
12
+ afterEach(() => {
13
+ resetMasterKey();
14
+ });
15
+
16
+ test('test_pci_dss_v4_6_plus_4_compliance', async () => {
17
+ /** Verify that CC tokenization reveals exactly the first 6 and last 4 digits. */
18
+ const rawCc = "4111-2222-3333-4444";
19
+ const token = await generateDPToken(rawCc);
20
+
21
+ const digits = token.replace(/-/g, "");
22
+ // BIN (First 6)
23
+ expect(digits.slice(0, 6)).toBe("411122");
24
+ // Identity (Last 4)
25
+ expect(digits.slice(12)).toBe("4444");
26
+
27
+ // Middle 6 must be masked/encrypted
28
+ expect(digits.slice(6, 12)).not.toBe("223333");
29
+ });
30
+
31
+ test('test_luhn_preservation', async () => {
32
+ /** Verify that the generated CC tokens are Luhn-valid identifiers. */
33
+ const rawCc = "4111-2222-3333-4444";
34
+ const token = await generateDPToken(rawCc);
35
+ const digits = token.replace(/-/g, "");
36
+
37
+ // Standard Luhn check
38
+ const sum = _getLuhnSum(digits);
39
+ expect(sum % 10).toBe(0);
40
+ });
41
+
42
+ test('test_bijective_entropy_expansion', async () => {
43
+ /** Verify that bijective names use 10-digit tags (128-bit entropy). */
44
+ const name = "Robert Oppenheimer";
45
+ const token = await generateDPToken(name, "PERSON");
46
+
47
+ const parts = token.split("-");
48
+ const tag = parts[parts.length - 1];
49
+
50
+ expect(tag.length).toBe(10);
51
+ expect(/^\d+$/.test(tag)).toBe(true);
52
+ });
53
+ });
package/tests/fpe.test.ts CHANGED
@@ -44,8 +44,8 @@ describe('TestFPETokenGeneration', () => {
44
44
  const digits = token.replace(/-/g, '');
45
45
  // BIN (first 6) must be preserved.
46
46
  expect(digits.slice(0, 6)).toBe('411111');
47
- // Last 3 digits of the last 4 must be preserved (the last digit is the Luhn check).
48
- expect(digits.slice(12, 15)).toBe('111');
47
+ // Last 4 digits (1111) must be preserved from original.
48
+ expect(digits.slice(12, 16)).toBe('1111');
49
49
  // Middle 6 must be randomized.
50
50
  expect(digits.slice(6, 12)).not.toBe('111111');
51
51
  });