@xbg.solutions/utils-hashing 1.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/lib/hashed-fields-lookup.d.ts +16 -0
- package/lib/hashed-fields-lookup.d.ts.map +1 -0
- package/lib/hashed-fields-lookup.js +25 -0
- package/lib/hashed-fields-lookup.js.map +1 -0
- package/lib/hasher.d.ts +28 -0
- package/lib/hasher.d.ts.map +1 -0
- package/lib/hasher.js +115 -0
- package/lib/hasher.js.map +1 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +16 -0
- package/lib/index.js.map +1 -0
- package/lib/unhashing.d.ts +31 -0
- package/lib/unhashing.d.ts.map +1 -0
- package/lib/unhashing.js +143 -0
- package/lib/unhashing.js.map +1 -0
- package/package.json +22 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encrypted Fields Registry
|
|
3
|
+
*
|
|
4
|
+
* Central registry of all PII fields that must be encrypted at rest.
|
|
5
|
+
* All fields use AES-256-GCM for authenticated encryption.
|
|
6
|
+
*/
|
|
7
|
+
export declare const HASHED_FIELDS: {
|
|
8
|
+
readonly 'user.email': true;
|
|
9
|
+
readonly 'user.phoneNumber': true;
|
|
10
|
+
};
|
|
11
|
+
export type HashedFieldPath = keyof typeof HASHED_FIELDS;
|
|
12
|
+
/**
|
|
13
|
+
* Check if a field path should be hashed
|
|
14
|
+
*/
|
|
15
|
+
export declare function isHashedField(fieldPath: string): boolean;
|
|
16
|
+
//# sourceMappingURL=hashed-fields-lookup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hashed-fields-lookup.d.ts","sourceRoot":"","sources":["../src/hashed-fields-lookup.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAO,MAAM,aAAa;;;CAQhB,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG,MAAM,OAAO,aAAa,CAAC;AAEzD;;GAEG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAExD"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Encrypted Fields Registry
|
|
4
|
+
*
|
|
5
|
+
* Central registry of all PII fields that must be encrypted at rest.
|
|
6
|
+
* All fields use AES-256-GCM for authenticated encryption.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.HASHED_FIELDS = void 0;
|
|
10
|
+
exports.isHashedField = isHashedField;
|
|
11
|
+
exports.HASHED_FIELDS = {
|
|
12
|
+
// User PII
|
|
13
|
+
'user.email': true,
|
|
14
|
+
'user.phoneNumber': true,
|
|
15
|
+
// Add project-specific PII fields here, e.g.:
|
|
16
|
+
// 'customer.email': true,
|
|
17
|
+
// 'address.addressLine1': true,
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Check if a field path should be hashed
|
|
21
|
+
*/
|
|
22
|
+
function isHashedField(fieldPath) {
|
|
23
|
+
return fieldPath in exports.HASHED_FIELDS;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=hashed-fields-lookup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hashed-fields-lookup.js","sourceRoot":"","sources":["../src/hashed-fields-lookup.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAiBH,sCAEC;AAjBY,QAAA,aAAa,GAAG;IAC3B,WAAW;IACX,YAAY,EAAE,IAAI;IAClB,kBAAkB,EAAE,IAAI;IAExB,8CAA8C;IAC9C,0BAA0B;IAC1B,gCAAgC;CACxB,CAAC;AAIX;;GAEG;AACH,SAAgB,aAAa,CAAC,SAAiB;IAC7C,OAAO,SAAS,IAAI,qBAAa,CAAC;AACpC,CAAC"}
|
package/lib/hasher.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PII Encryption Utilities
|
|
3
|
+
*
|
|
4
|
+
* Encrypt PII fields before storage using AES-256-GCM.
|
|
5
|
+
* AES-256-GCM provides authenticated encryption with reversible decryption.
|
|
6
|
+
*
|
|
7
|
+
* Encrypted format: `${iv}:${encrypted}:${authTag}` (all base64 encoded)
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Encrypt a single value using AES-256-GCM
|
|
11
|
+
*
|
|
12
|
+
* @param value - Plaintext value to encrypt
|
|
13
|
+
* @returns Encrypted value in format: `${iv}:${encrypted}:${authTag}` (base64)
|
|
14
|
+
*/
|
|
15
|
+
export declare function hashValue(value: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Hash all PII fields in an object based on entity type
|
|
18
|
+
*
|
|
19
|
+
* @param data - Object containing fields to hash
|
|
20
|
+
* @param entityType - Type of entity ('user', 'contact', 'address')
|
|
21
|
+
* @returns New object with hashed fields
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* hashFields({ email: 'test@example.com' }, 'user')
|
|
25
|
+
* // Returns { email: '$2b$10$...' }
|
|
26
|
+
*/
|
|
27
|
+
export declare function hashFields<T extends Record<string, unknown>>(data: T, entityType: 'user' | 'contact' | 'address'): T;
|
|
28
|
+
//# sourceMappingURL=hasher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hasher.d.ts","sourceRoot":"","sources":["../src/hasher.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAmCH;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAkB/C;AAGD;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1D,IAAI,EAAE,CAAC,EACP,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GACzC,CAAC,CAkBH"}
|
package/lib/hasher.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* PII Encryption Utilities
|
|
4
|
+
*
|
|
5
|
+
* Encrypt PII fields before storage using AES-256-GCM.
|
|
6
|
+
* AES-256-GCM provides authenticated encryption with reversible decryption.
|
|
7
|
+
*
|
|
8
|
+
* Encrypted format: `${iv}:${encrypted}:${authTag}` (all base64 encoded)
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.hashValue = hashValue;
|
|
45
|
+
exports.hashFields = hashFields;
|
|
46
|
+
const crypto = __importStar(require("crypto"));
|
|
47
|
+
const hashed_fields_lookup_1 = require("./hashed-fields-lookup");
|
|
48
|
+
// AES-256-GCM configuration
|
|
49
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
50
|
+
const IV_LENGTH = 12; // 96 bits for GCM
|
|
51
|
+
const KEY_LENGTH = 32; // 256 bits
|
|
52
|
+
/**
|
|
53
|
+
* Get encryption key from environment
|
|
54
|
+
* Key must be 32 bytes (64 hex characters) for AES-256
|
|
55
|
+
*/
|
|
56
|
+
function getEncryptionKey() {
|
|
57
|
+
const keyHex = process.env.PII_ENCRYPTION_KEY;
|
|
58
|
+
if (!keyHex) {
|
|
59
|
+
throw new Error('PII_ENCRYPTION_KEY not found in environment. ' +
|
|
60
|
+
'Generate with: openssl rand -hex 32');
|
|
61
|
+
}
|
|
62
|
+
// Validate key length
|
|
63
|
+
if (keyHex.length !== KEY_LENGTH * 2) {
|
|
64
|
+
throw new Error(`PII_ENCRYPTION_KEY must be ${KEY_LENGTH * 2} hex characters (${KEY_LENGTH} bytes). ` +
|
|
65
|
+
`Current length: ${keyHex.length}`);
|
|
66
|
+
}
|
|
67
|
+
return Buffer.from(keyHex, 'hex');
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Encrypt a single value using AES-256-GCM
|
|
71
|
+
*
|
|
72
|
+
* @param value - Plaintext value to encrypt
|
|
73
|
+
* @returns Encrypted value in format: `${iv}:${encrypted}:${authTag}` (base64)
|
|
74
|
+
*/
|
|
75
|
+
function hashValue(value) {
|
|
76
|
+
const key = getEncryptionKey();
|
|
77
|
+
// Generate random IV (initialization vector)
|
|
78
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
79
|
+
// Create cipher
|
|
80
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
81
|
+
// Encrypt the value
|
|
82
|
+
let encrypted = cipher.update(value, 'utf8', 'base64');
|
|
83
|
+
encrypted += cipher.final('base64');
|
|
84
|
+
// Get authentication tag
|
|
85
|
+
const authTag = cipher.getAuthTag();
|
|
86
|
+
// Return in format: iv:encrypted:authTag (all base64)
|
|
87
|
+
return `${iv.toString('base64')}:${encrypted}:${authTag.toString('base64')}`;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Hash all PII fields in an object based on entity type
|
|
91
|
+
*
|
|
92
|
+
* @param data - Object containing fields to hash
|
|
93
|
+
* @param entityType - Type of entity ('user', 'contact', 'address')
|
|
94
|
+
* @returns New object with hashed fields
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* hashFields({ email: 'test@example.com' }, 'user')
|
|
98
|
+
* // Returns { email: '$2b$10$...' }
|
|
99
|
+
*/
|
|
100
|
+
function hashFields(data, entityType) {
|
|
101
|
+
const result = Object.assign({}, data);
|
|
102
|
+
// Iterate through top-level fields and check if they should be hashed
|
|
103
|
+
for (const fieldName of Object.keys(result)) {
|
|
104
|
+
const fieldPath = `${entityType}.${fieldName}`;
|
|
105
|
+
if ((0, hashed_fields_lookup_1.isHashedField)(fieldPath)) {
|
|
106
|
+
const value = result[fieldName];
|
|
107
|
+
// Only hash non-null, non-empty strings
|
|
108
|
+
if (value && typeof value === 'string' && value.length > 0) {
|
|
109
|
+
result[fieldName] = hashValue(value);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=hasher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hasher.js","sourceRoot":"","sources":["../src/hasher.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCH,8BAkBC;AAcD,gCAqBC;AA5FD,+CAAiC;AACjC,iEAAuD;AAEvD,4BAA4B;AAC5B,MAAM,SAAS,GAAG,aAAa,CAAC;AAChC,MAAM,SAAS,GAAG,EAAE,CAAC,CAAC,kBAAkB;AACxC,MAAM,UAAU,GAAG,EAAE,CAAC,CAAC,WAAW;AAElC;;;GAGG;AACH,SAAS,gBAAgB;IACvB,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;IAE9C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACb,+CAA+C;YAC/C,qCAAqC,CACtC,CAAC;IACJ,CAAC;IAED,sBAAsB;IACtB,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU,GAAG,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CACb,8BAA8B,UAAU,GAAG,CAAC,oBAAoB,UAAU,WAAW;YACrF,mBAAmB,MAAM,CAAC,MAAM,EAAE,CACnC,CAAC;IACJ,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AACpC,CAAC;AAED;;;;;GAKG;AACH,SAAgB,SAAS,CAAC,KAAa;IACrC,MAAM,GAAG,GAAG,gBAAgB,EAAE,CAAC;IAE/B,6CAA6C;IAC7C,MAAM,EAAE,GAAG,MAAM,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;IAEzC,gBAAgB;IAChB,MAAM,MAAM,GAAG,MAAM,CAAC,cAAc,CAAC,SAAS,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IAEzD,oBAAoB;IACpB,IAAI,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;IACvD,SAAS,IAAI,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAEpC,yBAAyB;IACzB,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;IAEpC,sDAAsD;IACtD,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,SAAS,IAAI,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;AAC/E,CAAC;AAGD;;;;;;;;;;GAUG;AACH,SAAgB,UAAU,CACxB,IAAO,EACP,UAA0C;IAE1C,MAAM,MAAM,qBAAQ,IAAI,CAAE,CAAC;IAE3B,sEAAsE;IACtE,KAAK,MAAM,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5C,MAAM,SAAS,GAAG,GAAG,UAAU,IAAI,SAAS,EAAE,CAAC;QAE/C,IAAI,IAAA,oCAAa,EAAC,SAAS,CAAC,EAAE,CAAC;YAC7B,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;YAEhC,wCAAwC;YACxC,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC1D,MAAkC,CAAC,SAAS,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;YACpE,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hashing utilities barrel export
|
|
3
|
+
*/
|
|
4
|
+
export { HASHED_FIELDS, isHashedField } from './hashed-fields-lookup';
|
|
5
|
+
export type { HashedFieldPath } from './hashed-fields-lookup';
|
|
6
|
+
export { hashValue, hashFields } from './hasher';
|
|
7
|
+
export { unhashValue, unhashFields } from './unhashing';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACtE,YAAY,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Hashing utilities barrel export
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.unhashFields = exports.unhashValue = exports.hashFields = exports.hashValue = exports.isHashedField = exports.HASHED_FIELDS = void 0;
|
|
7
|
+
var hashed_fields_lookup_1 = require("./hashed-fields-lookup");
|
|
8
|
+
Object.defineProperty(exports, "HASHED_FIELDS", { enumerable: true, get: function () { return hashed_fields_lookup_1.HASHED_FIELDS; } });
|
|
9
|
+
Object.defineProperty(exports, "isHashedField", { enumerable: true, get: function () { return hashed_fields_lookup_1.isHashedField; } });
|
|
10
|
+
var hasher_1 = require("./hasher");
|
|
11
|
+
Object.defineProperty(exports, "hashValue", { enumerable: true, get: function () { return hasher_1.hashValue; } });
|
|
12
|
+
Object.defineProperty(exports, "hashFields", { enumerable: true, get: function () { return hasher_1.hashFields; } });
|
|
13
|
+
var unhashing_1 = require("./unhashing");
|
|
14
|
+
Object.defineProperty(exports, "unhashValue", { enumerable: true, get: function () { return unhashing_1.unhashValue; } });
|
|
15
|
+
Object.defineProperty(exports, "unhashFields", { enumerable: true, get: function () { return unhashing_1.unhashFields; } });
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;GAEG;;;AAEH,+DAAsE;AAA7D,qHAAA,aAAa,OAAA;AAAE,qHAAA,aAAa,OAAA;AAErC,mCAAiD;AAAxC,mGAAA,SAAS,OAAA;AAAE,oGAAA,UAAU,OAAA;AAC9B,yCAAwD;AAA/C,wGAAA,WAAW,OAAA;AAAE,yGAAA,YAAY,OAAA"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PII Decryption Utilities (Lazy Pattern)
|
|
3
|
+
*
|
|
4
|
+
* Decrypt PII fields on-demand when explicitly requested.
|
|
5
|
+
* Uses AES-256-GCM for authenticated decryption.
|
|
6
|
+
*
|
|
7
|
+
* Encrypted format: `${iv}:${encrypted}:${authTag}` (all base64 encoded)
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Decrypt a single value using AES-256-GCM
|
|
11
|
+
*
|
|
12
|
+
* @param encryptedValue - Encrypted value in format: `${iv}:${encrypted}:${authTag}` (base64)
|
|
13
|
+
* @returns Decrypted plaintext value
|
|
14
|
+
* @throws Error if decryption fails or format is invalid
|
|
15
|
+
*/
|
|
16
|
+
export declare function unhashValue(value: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Unhash specific fields in an object (lazy pattern)
|
|
19
|
+
*
|
|
20
|
+
* Services explicitly request which fields to unhash.
|
|
21
|
+
* No automatic unhashing on every GET request.
|
|
22
|
+
*
|
|
23
|
+
* @param data - Object containing hashed fields
|
|
24
|
+
* @param fieldsToUnhash - Array of field paths to unhash (e.g., ['contact.email'])
|
|
25
|
+
* @returns New object with unhashed fields
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* unhashFields(contactData, ['contact.email', 'contact.phoneNumber'])
|
|
29
|
+
*/
|
|
30
|
+
export declare function unhashFields<T extends Record<string, unknown>>(data: T, fieldsToUnhash: string[]): T;
|
|
31
|
+
//# sourceMappingURL=unhashing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unhashing.d.ts","sourceRoot":"","sources":["../src/unhashing.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA2CH;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAgCjD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5D,IAAI,EAAE,CAAC,EACP,cAAc,EAAE,MAAM,EAAE,GACvB,CAAC,CA0BH"}
|
package/lib/unhashing.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* PII Decryption Utilities (Lazy Pattern)
|
|
4
|
+
*
|
|
5
|
+
* Decrypt PII fields on-demand when explicitly requested.
|
|
6
|
+
* Uses AES-256-GCM for authenticated decryption.
|
|
7
|
+
*
|
|
8
|
+
* Encrypted format: `${iv}:${encrypted}:${authTag}` (all base64 encoded)
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.unhashValue = unhashValue;
|
|
45
|
+
exports.unhashFields = unhashFields;
|
|
46
|
+
const crypto = __importStar(require("crypto"));
|
|
47
|
+
const hashed_fields_lookup_1 = require("./hashed-fields-lookup");
|
|
48
|
+
// AES-256-GCM configuration
|
|
49
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
50
|
+
const KEY_LENGTH = 32; // 256 bits
|
|
51
|
+
/**
|
|
52
|
+
* Get encryption key from environment
|
|
53
|
+
* Key must be 32 bytes (64 hex characters) for AES-256
|
|
54
|
+
*/
|
|
55
|
+
function getEncryptionKey() {
|
|
56
|
+
const keyHex = process.env.PII_ENCRYPTION_KEY;
|
|
57
|
+
if (!keyHex) {
|
|
58
|
+
throw new Error('PII_ENCRYPTION_KEY not found in environment. ' +
|
|
59
|
+
'Generate with: openssl rand -hex 32');
|
|
60
|
+
}
|
|
61
|
+
// Validate key length
|
|
62
|
+
if (keyHex.length !== KEY_LENGTH * 2) {
|
|
63
|
+
throw new Error(`PII_ENCRYPTION_KEY must be ${KEY_LENGTH * 2} hex characters (${KEY_LENGTH} bytes). ` +
|
|
64
|
+
`Current length: ${keyHex.length}`);
|
|
65
|
+
}
|
|
66
|
+
return Buffer.from(keyHex, 'hex');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Check if a value appears to be AES-256-GCM encrypted
|
|
70
|
+
* Format: iv:encrypted:authTag (base64 strings separated by colons)
|
|
71
|
+
*/
|
|
72
|
+
function isEncrypted(value) {
|
|
73
|
+
const parts = value.split(':');
|
|
74
|
+
return parts.length === 3 && parts.every(part => part.length > 0);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Decrypt a single value using AES-256-GCM
|
|
78
|
+
*
|
|
79
|
+
* @param encryptedValue - Encrypted value in format: `${iv}:${encrypted}:${authTag}` (base64)
|
|
80
|
+
* @returns Decrypted plaintext value
|
|
81
|
+
* @throws Error if decryption fails or format is invalid
|
|
82
|
+
*/
|
|
83
|
+
function unhashValue(value) {
|
|
84
|
+
// Validate format
|
|
85
|
+
if (!isEncrypted(value)) {
|
|
86
|
+
throw new Error('Invalid encrypted value format. ' +
|
|
87
|
+
'Expected: iv:encrypted:authTag (base64)');
|
|
88
|
+
}
|
|
89
|
+
const key = getEncryptionKey();
|
|
90
|
+
// Parse encrypted value components
|
|
91
|
+
const [ivBase64, encryptedBase64, authTagBase64] = value.split(':');
|
|
92
|
+
const iv = Buffer.from(ivBase64, 'base64');
|
|
93
|
+
const authTag = Buffer.from(authTagBase64, 'base64');
|
|
94
|
+
// Create decipher
|
|
95
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
96
|
+
decipher.setAuthTag(authTag);
|
|
97
|
+
try {
|
|
98
|
+
// Decrypt the value
|
|
99
|
+
let decrypted = decipher.update(encryptedBase64, 'base64', 'utf8');
|
|
100
|
+
decrypted += decipher.final('utf8');
|
|
101
|
+
return decrypted;
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
throw new Error('Decryption failed. This may indicate data corruption or wrong encryption key. ' +
|
|
105
|
+
`Original error: ${error instanceof Error ? error.message : String(error)}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Unhash specific fields in an object (lazy pattern)
|
|
110
|
+
*
|
|
111
|
+
* Services explicitly request which fields to unhash.
|
|
112
|
+
* No automatic unhashing on every GET request.
|
|
113
|
+
*
|
|
114
|
+
* @param data - Object containing hashed fields
|
|
115
|
+
* @param fieldsToUnhash - Array of field paths to unhash (e.g., ['contact.email'])
|
|
116
|
+
* @returns New object with unhashed fields
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* unhashFields(contactData, ['contact.email', 'contact.phoneNumber'])
|
|
120
|
+
*/
|
|
121
|
+
function unhashFields(data, fieldsToUnhash) {
|
|
122
|
+
const result = Object.assign({}, data);
|
|
123
|
+
for (const fieldPath of fieldsToUnhash) {
|
|
124
|
+
// Validate that this field is actually an encrypted field
|
|
125
|
+
if (!(0, hashed_fields_lookup_1.isHashedField)(fieldPath)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
// Extract entity type and field name from path
|
|
129
|
+
// e.g., 'contact.email' -> entityType='contact', fieldName='email'
|
|
130
|
+
const parts = fieldPath.split('.');
|
|
131
|
+
if (parts.length !== 2) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const fieldName = parts[1];
|
|
135
|
+
const value = result[fieldName];
|
|
136
|
+
// Only attempt to decrypt string values that appear to be encrypted
|
|
137
|
+
if (value && typeof value === 'string' && isEncrypted(value)) {
|
|
138
|
+
result[fieldName] = unhashValue(value);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=unhashing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unhashing.js","sourceRoot":"","sources":["../src/unhashing.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xbg.solutions/utils-hashing",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "PII encryption with AES-256-GCM and field-level hashing",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "lib/index.d.ts",
|
|
7
|
+
"files": ["lib"],
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"build:watch": "tsc --watch",
|
|
11
|
+
"clean": "rm -rf lib",
|
|
12
|
+
"prepublishOnly": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {"lodash": "^4.17.21"},
|
|
15
|
+
"devDependencies": {"@types/lodash": "^4.14.202", "@types/node": "^20.11.0", "typescript": "^5.3.3"},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": "22"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
}
|
|
22
|
+
}
|