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/dist/index.js +25 -24
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +25 -24
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/core/fpe.ts +48 -23
- package/src/core/fpe_utils.ts +4 -4
- package/tests/bijective_fpe.test.ts +2 -2
- package/tests/compliance_pci.test.ts +53 -0
- package/tests/fpe.test.ts +2 -2
package/package.json
CHANGED
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
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
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)
|
|
106
|
+
return BigInt(cipherStr);
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
function _renderBijectivePerson(bits: bigint): string {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
const
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
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
|
|
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
|
|
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);
|
package/src/core/fpe_utils.ts
CHANGED
|
@@ -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,
|
|
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
|
|
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:
|
|
134
|
-
if (/^[A-Z][a-zA-Z, ]+-[0-9]{3,
|
|
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(
|
|
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).
|
|
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
|
|
48
|
-
expect(digits.slice(12,
|
|
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
|
});
|