@liuhange/dsh-data-masking 2.0.0 → 2.0.1

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.
Files changed (2) hide show
  1. package/lib/index.js +362 -2
  2. package/package.json +2 -2
package/lib/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { defineTool } from "@deepseek-ai/dsh-tools";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
- import { AuditLogger, BusinessRulesLoader, FileFormatAdapter, PathValidator, ReportGenerator } from "@deepseek-ai/dsh-data-asset-shared";
4
+ import { AuditLogger, BusinessRulesLoader, FileFormatAdapter, PathValidator, ReportGenerator } from "@liuhange/dsh-data-asset-shared";
5
+ import * as crypto from "crypto";
5
6
  //#region lib/types/sensitiveFieldScanner.js
6
7
  var SensitiveFieldScanner = class {
7
8
  scan(lines, sensitivePatterns) {
@@ -50,6 +51,332 @@ var MaskingStrategyExecutor = class {
50
51
  }
51
52
  };
52
53
  //#endregion
54
+ //#region lib/types/algorithms/fpeAlgorithm.js
55
+ var FpeAlgorithm = class {
56
+ encrypt(plaintext, key) {
57
+ if (!key) throw new Error("错误:FPE密钥未配置");
58
+ const chars = plaintext.split("");
59
+ const radix = 10;
60
+ if (!chars.every((c) => /\d/.test(c))) return this.encryptGeneric(plaintext, key);
61
+ const n = plaintext.length;
62
+ if (n < 2) return plaintext;
63
+ const keyHash = crypto.createHash("sha256").update(key).digest();
64
+ const rounds = 10;
65
+ const inputNums = chars.map((c) => parseInt(c, 10));
66
+ for (let round = 0; round < rounds; round++) {
67
+ const roundKey = crypto.createHash("sha256").update(keyHash).update(Buffer.from([round])).digest();
68
+ for (let i = 0; i < n - 1; i++) {
69
+ const mod = radix ** (n - i - 1);
70
+ const prfInput = Buffer.alloc(4);
71
+ prfInput.writeUInt32BE(inputNums[i], 0);
72
+ const prfNum = crypto.createHmac("sha256", roundKey).update(prfInput).digest().readUInt32BE(0) % mod;
73
+ inputNums[i + 1] = (inputNums[i + 1] + prfNum) % radix;
74
+ }
75
+ for (let i = n - 2; i >= 0; i--) {
76
+ const mod = radix ** (n - i - 1);
77
+ const prfInput = Buffer.alloc(4);
78
+ prfInput.writeUInt32BE(inputNums[i + 1], 0);
79
+ const prfNum = crypto.createHmac("sha256", roundKey).update(prfInput).digest().readUInt32BE(0) % mod;
80
+ inputNums[i] = (inputNums[i] + prfNum) % radix;
81
+ }
82
+ }
83
+ return inputNums.join("");
84
+ }
85
+ decrypt(ciphertext, key) {
86
+ if (!key) throw new Error("错误:FPE密钥未配置");
87
+ const chars = ciphertext.split("");
88
+ const radix = 10;
89
+ if (!chars.every((c) => /\d/.test(c))) return this.decryptGeneric(ciphertext, key);
90
+ const n = ciphertext.length;
91
+ if (n < 2) return ciphertext;
92
+ const keyHash = crypto.createHash("sha256").update(key).digest();
93
+ const rounds = 10;
94
+ const outputNums = chars.map((c) => parseInt(c, 10));
95
+ for (let round = rounds - 1; round >= 0; round--) {
96
+ const roundKey = crypto.createHash("sha256").update(keyHash).update(Buffer.from([round])).digest();
97
+ for (let i = 0; i < n - 1; i++) {
98
+ const mod = radix ** (n - i - 1);
99
+ const prfInput = Buffer.alloc(4);
100
+ prfInput.writeUInt32BE(outputNums[i + 1], 0);
101
+ const prfNum = crypto.createHmac("sha256", roundKey).update(prfInput).digest().readUInt32BE(0) % mod;
102
+ outputNums[i] = ((outputNums[i] - prfNum) % radix + radix) % radix;
103
+ }
104
+ for (let i = n - 2; i >= 0; i--) {
105
+ const mod = radix ** (n - i - 1);
106
+ const prfInput = Buffer.alloc(4);
107
+ prfInput.writeUInt32BE(outputNums[i], 0);
108
+ const prfNum = crypto.createHmac("sha256", roundKey).update(prfInput).digest().readUInt32BE(0) % mod;
109
+ outputNums[i + 1] = ((outputNums[i + 1] - prfNum) % radix + radix) % radix;
110
+ }
111
+ }
112
+ return outputNums.join("");
113
+ }
114
+ encryptGeneric(plaintext, key) {
115
+ const keyHash = crypto.createHash("sha256").update(key).digest();
116
+ const result = [];
117
+ for (let i = 0; i < plaintext.length; i++) {
118
+ const charCode = plaintext.charCodeAt(i);
119
+ const offset = crypto.createHmac("sha256", keyHash).update(Buffer.from([i & 255])).digest()[0] % 256;
120
+ result.push(String.fromCharCode((charCode + offset) % 256));
121
+ }
122
+ return result.join("");
123
+ }
124
+ decryptGeneric(ciphertext, key) {
125
+ const keyHash = crypto.createHash("sha256").update(key).digest();
126
+ const result = [];
127
+ for (let i = 0; i < ciphertext.length; i++) {
128
+ const charCode = ciphertext.charCodeAt(i);
129
+ const offset = crypto.createHmac("sha256", keyHash).update(Buffer.from([i & 255])).digest()[0] % 256;
130
+ result.push(String.fromCharCode((charCode - offset + 256) % 256));
131
+ }
132
+ return result.join("");
133
+ }
134
+ };
135
+ //#endregion
136
+ //#region lib/types/algorithms/kAnonymityAlgorithm.js
137
+ var KAnonymityAlgorithm = class {
138
+ anonymize(records, kValue, quasiIdentifiers) {
139
+ if (records.length === 0 || quasiIdentifiers.length === 0) return {
140
+ anonymizedRecords: records,
141
+ actualMinEquivalenceClass: 0,
142
+ suppressedCount: 0
143
+ };
144
+ let workingRecords = [...records];
145
+ let generalized = true;
146
+ let generalizationLevel = 0;
147
+ while (generalized) {
148
+ generalized = false;
149
+ const groups = this.groupByQuasiIdentifiers(workingRecords, quasiIdentifiers);
150
+ if (Math.min(...groups.map((g) => g.length)) >= kValue) {
151
+ const minClass = Math.min(...groups.map((g) => g.length));
152
+ return {
153
+ anonymizedRecords: workingRecords,
154
+ actualMinEquivalenceClass: minClass,
155
+ suppressedCount: 0
156
+ };
157
+ }
158
+ if (generalizationLevel < 3) {
159
+ workingRecords = this.generalize(workingRecords, quasiIdentifiers, generalizationLevel);
160
+ generalizationLevel++;
161
+ generalized = true;
162
+ }
163
+ }
164
+ const groups = this.groupByQuasiIdentifiers(workingRecords, quasiIdentifiers);
165
+ const validGroups = groups.filter((g) => g.length >= kValue);
166
+ const suppressedRecords = groups.filter((g) => g.length < kValue).flat();
167
+ const anonymizedRecords = validGroups.flat();
168
+ const allGroups = [...validGroups, ...groups.filter((g) => g.length < kValue)];
169
+ return {
170
+ anonymizedRecords,
171
+ actualMinEquivalenceClass: allGroups.length > 0 ? Math.min(...allGroups.map((g) => g.length)) : 0,
172
+ suppressedCount: suppressedRecords.length
173
+ };
174
+ }
175
+ groupByQuasiIdentifiers(records, quasiIdentifiers) {
176
+ const groups = /* @__PURE__ */ new Map();
177
+ for (const record of records) {
178
+ const key = quasiIdentifiers.map((qi) => String(record[qi] ?? "")).join("|");
179
+ const group = groups.get(key);
180
+ if (group) group.push(record);
181
+ else groups.set(key, [record]);
182
+ }
183
+ return Array.from(groups.values());
184
+ }
185
+ generalize(records, quasiIdentifiers, level) {
186
+ return records.map((record) => {
187
+ const generalized = { ...record };
188
+ for (const qi of quasiIdentifiers) {
189
+ const value = String(generalized[qi] ?? "");
190
+ if (value.length > level + 1) generalized[qi] = value.substring(0, value.length - level - 1) + "*".repeat(level + 1);
191
+ else generalized[qi] = "*".repeat(value.length);
192
+ }
193
+ return generalized;
194
+ });
195
+ }
196
+ };
197
+ //#endregion
198
+ //#region lib/types/algorithms/differentialPrivacyAlgorithm.js
199
+ var DifferentialPrivacyAlgorithm = class {
200
+ addNoise(trueValue, epsilon, sensitivity, budgetTracker, totalBudget) {
201
+ if (!budgetTracker.canConsume(epsilon, totalBudget)) throw new Error("隐私预算已耗尽,拒绝查询");
202
+ const scale = sensitivity / epsilon;
203
+ const noise = this.generateLaplaceNoise(scale);
204
+ budgetTracker.consume(epsilon);
205
+ return trueValue + noise;
206
+ }
207
+ generateLaplaceNoise(scale) {
208
+ const adjustedU = this.generateSecureUniform() - .5;
209
+ return -scale * Math.sign(adjustedU) * Math.log(1 - 2 * Math.abs(adjustedU));
210
+ }
211
+ generateSecureUniform() {
212
+ const uint64 = crypto.randomBytes(8).readBigUInt64BE(0);
213
+ const maxUint64 = BigInt(2) ** BigInt(64) - BigInt(1);
214
+ return Number(uint64) / Number(maxUint64);
215
+ }
216
+ };
217
+ //#endregion
218
+ //#region lib/types/algorithms/hashAlgorithm.js
219
+ var HashAlgorithm = class {
220
+ hash(plaintext, hashAlgorithm, salt) {
221
+ const input = salt ? salt + plaintext : plaintext;
222
+ const algorithm = hashAlgorithm === "SHA-512" ? "SHA-512" : "SHA-256";
223
+ const hash = crypto.createHash(algorithm);
224
+ hash.update(input, "utf-8");
225
+ const digest = hash.digest("hex");
226
+ return {
227
+ digest,
228
+ salted: salt !== void 0 && salt !== "",
229
+ digestLength: digest.length
230
+ };
231
+ }
232
+ };
233
+ //#endregion
234
+ //#region lib/types/budgetTracker.js
235
+ var BudgetTracker = class {
236
+ consumed = 0;
237
+ queryCount = 0;
238
+ totalBudget;
239
+ constructor(totalBudget) {
240
+ this.totalBudget = totalBudget;
241
+ }
242
+ consume(epsilon) {
243
+ this.consumed += epsilon;
244
+ this.queryCount++;
245
+ }
246
+ canConsume(epsilon, totalBudget) {
247
+ return this.consumed + epsilon <= totalBudget;
248
+ }
249
+ getState() {
250
+ return {
251
+ totalBudget: this.totalBudget,
252
+ consumed: this.consumed,
253
+ remaining: this.totalBudget - this.consumed,
254
+ queryCount: this.queryCount
255
+ };
256
+ }
257
+ };
258
+ //#endregion
259
+ //#region lib/types/advancedMaskingExecutor.js
260
+ var AdvancedMaskingExecutor = class {
261
+ fpeAlgorithm = new FpeAlgorithm();
262
+ kAnonymityAlgorithm = new KAnonymityAlgorithm();
263
+ differentialPrivacyAlgorithm = new DifferentialPrivacyAlgorithm();
264
+ hashAlgorithm = new HashAlgorithm();
265
+ execute(data, algorithm, fieldName, config) {
266
+ switch ((fieldName ? config.fieldAlgorithms[fieldName] : void 0)?.algorithm ?? algorithm) {
267
+ case "FPE": return this.executeFpe(data, config);
268
+ case "k-anonymity": return this.executeKAnonymity(data, config);
269
+ case "differential-privacy": return this.executeDifferentialPrivacy(data, config);
270
+ case "hash": return this.executeHash(data, config);
271
+ default: return this.executeHash(data, config);
272
+ }
273
+ }
274
+ executeFpe(data, config) {
275
+ const key = this.resolveEnvVar(config.fpe.key);
276
+ return {
277
+ maskedData: this.fpeAlgorithm.encrypt(data, key),
278
+ algorithm: "FPE",
279
+ details: {
280
+ algorithm: "FPE",
281
+ reversible: true
282
+ }
283
+ };
284
+ }
285
+ executeKAnonymity(data, config) {
286
+ const kValue = config.kAnonymity.kValue;
287
+ const quasiIdentifiers = config.kAnonymity.quasiIdentifiers;
288
+ let records;
289
+ try {
290
+ records = JSON.parse(data);
291
+ } catch {
292
+ records = [{ value: data }];
293
+ }
294
+ const result = this.kAnonymityAlgorithm.anonymize(records, kValue, quasiIdentifiers);
295
+ const details = {
296
+ algorithm: "k-anonymity",
297
+ kValue,
298
+ actualMinEquivalenceClass: result.actualMinEquivalenceClass,
299
+ suppressedCount: result.suppressedCount
300
+ };
301
+ return {
302
+ maskedData: JSON.stringify(result.anonymizedRecords),
303
+ algorithm: "k-anonymity",
304
+ details
305
+ };
306
+ }
307
+ executeDifferentialPrivacy(data, config) {
308
+ const epsilon = config.differentialPrivacy.epsilon;
309
+ const totalBudget = config.differentialPrivacy.totalBudget;
310
+ const sensitivity = config.differentialPrivacy.sensitivity;
311
+ const budgetTracker = new BudgetTracker(totalBudget);
312
+ const trueValue = Number(data);
313
+ if (isNaN(trueValue)) throw new Error("差分隐私要求输入为数字");
314
+ const noisyValue = this.differentialPrivacyAlgorithm.addNoise(trueValue, epsilon, sensitivity, budgetTracker, totalBudget);
315
+ const budgetState = budgetTracker.getState();
316
+ const details = {
317
+ algorithm: "differential-privacy",
318
+ epsilon,
319
+ budgetConsumed: budgetState.consumed,
320
+ budgetRemaining: budgetState.remaining
321
+ };
322
+ return {
323
+ maskedData: String(noisyValue),
324
+ algorithm: "differential-privacy",
325
+ details
326
+ };
327
+ }
328
+ executeHash(data, config) {
329
+ const hashAlgo = config.hash.algorithm;
330
+ const salt = this.resolveEnvVar(config.hash.salt);
331
+ const result = this.hashAlgorithm.hash(data, hashAlgo, salt || void 0);
332
+ const details = {
333
+ algorithm: "hash",
334
+ hashAlgorithm: hashAlgo,
335
+ salted: result.salted,
336
+ digestLength: result.digestLength
337
+ };
338
+ return {
339
+ maskedData: result.digest,
340
+ algorithm: "hash",
341
+ details
342
+ };
343
+ }
344
+ resolveEnvVar(value) {
345
+ if (value.startsWith("${") && value.endsWith("}")) {
346
+ const envVar = value.slice(2, -1);
347
+ return process.env[envVar] ?? "";
348
+ }
349
+ return value;
350
+ }
351
+ };
352
+ //#endregion
353
+ //#region lib/types/defaultAdvancedMaskingConfig.js
354
+ const defaultAdvancedMaskingConfig = {
355
+ defaultAlgorithm: "hash",
356
+ fieldAlgorithms: {},
357
+ fpe: {
358
+ key: "",
359
+ radix: 10
360
+ },
361
+ kAnonymity: {
362
+ kValue: 2,
363
+ quasiIdentifiers: [
364
+ "zipCode",
365
+ "age",
366
+ "gender"
367
+ ]
368
+ },
369
+ differentialPrivacy: {
370
+ epsilon: 1,
371
+ totalBudget: 5,
372
+ sensitivity: 1
373
+ },
374
+ hash: {
375
+ algorithm: "SHA-256",
376
+ salt: ""
377
+ }
378
+ };
379
+ //#endregion
53
380
  //#region lib/types/index.js
54
381
  const name = "data-masking";
55
382
  const inject = ["tools"];
@@ -61,9 +388,10 @@ function apply(ctx) {
61
388
  const auditLogger = new AuditLogger();
62
389
  const scanner = new SensitiveFieldScanner();
63
390
  const strategyExecutor = new MaskingStrategyExecutor();
391
+ const advancedExecutor = new AdvancedMaskingExecutor();
64
392
  ctx.tools.register(defineTool({
65
393
  name: "mask_sensitive_data",
66
- description: "识别并脱敏数据中的敏感字段(身份证、手机号、银行卡、邮箱)",
394
+ description: "识别并脱敏数据中的敏感字段(身份证、手机号、银行卡、邮箱),支持高级算法(FPE/k-匿名/差分隐私/哈希)",
67
395
  parameters: {
68
396
  filePath: {
69
397
  type: "string",
@@ -73,6 +401,14 @@ function apply(ctx) {
73
401
  strategy: {
74
402
  type: "string",
75
403
  description: "脱敏策略: FULL | PARTIAL | GENERALIZE"
404
+ },
405
+ algorithm: {
406
+ type: "string",
407
+ description: "高级脱敏算法: FPE | k-anonymity | differential-privacy | hash"
408
+ },
409
+ fieldName: {
410
+ type: "string",
411
+ description: "字段名(用于字段级算法配置覆盖)"
76
412
  }
77
413
  },
78
414
  output: {
@@ -87,6 +423,8 @@ function apply(ctx) {
87
423
  const workingDir = process.cwd();
88
424
  const filePath = args.filePath;
89
425
  const strategy = args.strategy ?? "PARTIAL";
426
+ const algorithm = args.algorithm;
427
+ const fieldName = args.fieldName;
90
428
  try {
91
429
  pathValidator.validate(filePath, workingDir);
92
430
  } catch {
@@ -94,6 +432,28 @@ function apply(ctx) {
94
432
  }
95
433
  const fullPath = path.resolve(filePath);
96
434
  if (!fs.existsSync(fullPath)) return `错误:文件不存在 - ${fullPath}`;
435
+ if (algorithm) {
436
+ const advancedConfig = config.advancedMasking ?? defaultAdvancedMaskingConfig;
437
+ const readResult = await formatAdapter.read(fullPath);
438
+ const lines = readResult.lines;
439
+ const maskedLines = [];
440
+ for (const line of lines) {
441
+ const result = advancedExecutor.execute(line, algorithm, fieldName, advancedConfig);
442
+ maskedLines.push(typeof result.maskedData === "string" ? result.maskedData : JSON.stringify(result.maskedData));
443
+ }
444
+ const ext = path.extname(fullPath);
445
+ const baseName = path.basename(fullPath, ext);
446
+ const outputPath = path.join(path.dirname(fullPath), `${baseName}_masked${ext}`);
447
+ await formatAdapter.write(outputPath, maskedLines, readResult.format);
448
+ auditLogger.log({
449
+ pluginName: "data-masking",
450
+ operation: "mask_sensitive_data",
451
+ inputPath: fullPath,
452
+ outputPath,
453
+ result: "SUCCESS"
454
+ });
455
+ return `高级脱敏完成(算法: ${algorithm})\n输入: ${fullPath}\n输出: ${outputPath}\n处理行数: ${maskedLines.length}`;
456
+ }
97
457
  const readResult = await formatAdapter.read(fullPath);
98
458
  const lines = readResult.lines;
99
459
  const scanResult = scanner.scan(lines, config.sensitivePatterns);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
- {
1
+ {
2
2
  "name": "@liuhange/dsh-data-masking",
3
3
  "description": "Data masking plugin: sensitive field identification and graded masking (FULL/PARTIAL/GENERALIZE)",
4
- "version": "2.0.0",
4
+ "version": "2.0.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },