core-mb 1.0.0 → 1.0.2
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/.github/workflows/node.js.yml +58 -0
- package/.nyc_output/0393f58b-d3a0-4d83-8010-334fc96e8c86.json +1 -0
- package/.nyc_output/0edf14d3-019a-4204-aae9-8d7d2491500b.json +1 -0
- package/.nyc_output/1db7cb73-4e51-46c0-96e8-3d1edd65f98c.json +1 -0
- package/.nyc_output/29ead10f-a723-4574-aeaf-5eff2ebcafac.json +1 -0
- package/.nyc_output/30473bab-10b9-4d55-8f30-42cbd631a50e.json +1 -0
- package/.nyc_output/4514ee29-1cb6-4243-abab-937ae44e87fe.json +1 -0
- package/.nyc_output/5268e9ea-7320-41e0-8dbf-37139a2b07e9.json +1 -0
- package/.nyc_output/581a75aa-21fc-470f-a998-ad9752b568ff.json +1 -0
- package/.nyc_output/6492905d-a035-44ac-b40d-84d04d64d3c2.json +1 -0
- package/.nyc_output/67ba5e3c-ca1d-4dbf-9155-6a154788cb55.json +1 -0
- package/.nyc_output/688e0703-cbfb-4f94-ac42-66472ac00a0c.json +1 -0
- package/.nyc_output/6c1e2427-a36b-407d-96d2-929a885e565d.json +1 -0
- package/.nyc_output/781a9e98-6dff-42dc-9bb2-126b65619cf3.json +1 -0
- package/.nyc_output/960ffee6-798c-4670-b1fa-39f6cebaa70b.json +1 -0
- package/.nyc_output/9661b441-490d-4793-881a-570493cf4d77.json +1 -0
- package/.nyc_output/9fe3622e-5b2a-42b2-a7ec-6f42c052afc9.json +1 -0
- package/.nyc_output/c37e92ff-6103-4b4b-be8e-2e23adc44ba1.json +1 -0
- package/.nyc_output/cfc973a7-b63b-4ed5-a568-019c4813486f.json +1 -0
- package/.nyc_output/d4490352-b4a2-45e0-beb3-a146203ce7a1.json +1 -0
- package/.nyc_output/da65eb21-2244-42c2-a24a-136a49caa10b.json +1 -0
- package/.nyc_output/e58bd293-d9b2-4a7a-bd45-b6f603743e81.json +1 -0
- package/.nyc_output/e5ef7a91-bcbd-46a0-848e-423e6aec9090.json +1 -0
- package/.nyc_output/f73cc576-f3eb-476a-bb88-9030aaaca351.json +1 -0
- package/.nyc_output/processinfo/0393f58b-d3a0-4d83-8010-334fc96e8c86.json +1 -0
- package/.nyc_output/processinfo/0edf14d3-019a-4204-aae9-8d7d2491500b.json +1 -0
- package/.nyc_output/processinfo/1db7cb73-4e51-46c0-96e8-3d1edd65f98c.json +1 -0
- package/.nyc_output/processinfo/29ead10f-a723-4574-aeaf-5eff2ebcafac.json +1 -0
- package/.nyc_output/processinfo/30473bab-10b9-4d55-8f30-42cbd631a50e.json +1 -0
- package/.nyc_output/processinfo/4514ee29-1cb6-4243-abab-937ae44e87fe.json +1 -0
- package/.nyc_output/processinfo/5268e9ea-7320-41e0-8dbf-37139a2b07e9.json +1 -0
- package/.nyc_output/processinfo/581a75aa-21fc-470f-a998-ad9752b568ff.json +1 -0
- package/.nyc_output/processinfo/6492905d-a035-44ac-b40d-84d04d64d3c2.json +1 -0
- package/.nyc_output/processinfo/67ba5e3c-ca1d-4dbf-9155-6a154788cb55.json +1 -0
- package/.nyc_output/processinfo/688e0703-cbfb-4f94-ac42-66472ac00a0c.json +1 -0
- package/.nyc_output/processinfo/6c1e2427-a36b-407d-96d2-929a885e565d.json +1 -0
- package/.nyc_output/processinfo/781a9e98-6dff-42dc-9bb2-126b65619cf3.json +1 -0
- package/.nyc_output/processinfo/960ffee6-798c-4670-b1fa-39f6cebaa70b.json +1 -0
- package/.nyc_output/processinfo/9661b441-490d-4793-881a-570493cf4d77.json +1 -0
- package/.nyc_output/processinfo/9fe3622e-5b2a-42b2-a7ec-6f42c052afc9.json +1 -0
- package/.nyc_output/processinfo/c37e92ff-6103-4b4b-be8e-2e23adc44ba1.json +1 -0
- package/.nyc_output/processinfo/cfc973a7-b63b-4ed5-a568-019c4813486f.json +1 -0
- package/.nyc_output/processinfo/d4490352-b4a2-45e0-beb3-a146203ce7a1.json +1 -0
- package/.nyc_output/processinfo/da65eb21-2244-42c2-a24a-136a49caa10b.json +1 -0
- package/.nyc_output/processinfo/e58bd293-d9b2-4a7a-bd45-b6f603743e81.json +1 -0
- package/.nyc_output/processinfo/e5ef7a91-bcbd-46a0-848e-423e6aec9090.json +1 -0
- package/.nyc_output/processinfo/f73cc576-f3eb-476a-bb88-9030aaaca351.json +1 -0
- package/.nyc_output/processinfo/index.json +1 -0
- package/README.md +12 -4
- package/__unitest__/phone.number.crypto.spec.ts +9 -9
- package/coverage/config.ts.html +178 -0
- package/coverage/index.html +31 -16
- package/coverage/lcov-report/config.ts.html +178 -0
- package/coverage/lcov-report/index.html +31 -16
- package/coverage/lcov-report/phone.number.helper.ts.html +1 -1
- package/coverage/lcov-report/security.helper.ts.html +19 -46
- package/coverage/lcov.info +58 -36
- package/coverage/phone.number.helper.ts.html +1 -1
- package/coverage/security.helper.ts.html +19 -46
- package/dist/config.d.ts +12 -0
- package/dist/config.js +24 -0
- package/dist/date.helper.d.ts +1 -0
- package/dist/date.helper.js +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/security.helper.d.ts +6 -2
- package/dist/security.helper.js +10 -14
- package/dist/validation.helper.d.ts +6 -0
- package/dist/validation.helper.js +32 -0
- package/jest.config.js +2 -1
- package/jest.setup.ts +8 -0
- package/package.json +6 -2
- package/src/config.ts +31 -0
- package/src/date.helper.ts +3 -0
- package/src/index.ts +1 -0
- package/src/security.helper.ts +9 -18
- package/src/validation.helper.ts +36 -0
- package/test-report.html +1 -1
- package/src/aes.helper.ts +0 -101
|
@@ -23,16 +23,16 @@
|
|
|
23
23
|
<div class='clearfix'>
|
|
24
24
|
|
|
25
25
|
<div class='fl pad1y space-right2'>
|
|
26
|
-
<span class="strong">
|
|
26
|
+
<span class="strong">100% </span>
|
|
27
27
|
<span class="quiet">Statements</span>
|
|
28
|
-
<span class='fraction'>
|
|
28
|
+
<span class='fraction'>24/24</span>
|
|
29
29
|
</div>
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
<div class='fl pad1y space-right2'>
|
|
33
|
-
<span class="strong">
|
|
33
|
+
<span class="strong">100% </span>
|
|
34
34
|
<span class="quiet">Branches</span>
|
|
35
|
-
<span class='fraction'>
|
|
35
|
+
<span class='fraction'>0/0</span>
|
|
36
36
|
</div>
|
|
37
37
|
|
|
38
38
|
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
<div class='fl pad1y space-right2'>
|
|
47
|
-
<span class="strong">
|
|
47
|
+
<span class="strong">100% </span>
|
|
48
48
|
<span class="quiet">Lines</span>
|
|
49
|
-
<span class='fraction'>
|
|
49
|
+
<span class='fraction'>24/24</span>
|
|
50
50
|
</div>
|
|
51
51
|
|
|
52
52
|
|
|
@@ -142,18 +142,11 @@
|
|
|
142
142
|
<a name='L77'></a><a href='#L77'>77</a>
|
|
143
143
|
<a name='L78'></a><a href='#L78'>78</a>
|
|
144
144
|
<a name='L79'></a><a href='#L79'>79</a>
|
|
145
|
-
<a name='L80'></a><a href='#L80'>80</a>
|
|
146
|
-
<
|
|
147
|
-
<a name='L82'></a><a href='#L82'>82</a>
|
|
148
|
-
<a name='L83'></a><a href='#L83'>83</a>
|
|
149
|
-
<a name='L84'></a><a href='#L84'>84</a>
|
|
150
|
-
<a name='L85'></a><a href='#L85'>85</a>
|
|
151
|
-
<a name='L86'></a><a href='#L86'>86</a>
|
|
152
|
-
<a name='L87'></a><a href='#L87'>87</a>
|
|
153
|
-
<a name='L88'></a><a href='#L88'>88</a>
|
|
154
|
-
<a name='L89'></a><a href='#L89'>89</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
|
|
145
|
+
<a name='L80'></a><a href='#L80'>80</a></td><td class="line-coverage quiet"><span class="cline-any cline-yes">1x</span>
|
|
146
|
+
<span class="cline-any cline-yes">1x</span>
|
|
155
147
|
<span class="cline-any cline-yes">1x</span>
|
|
156
148
|
<span class="cline-any cline-neutral"> </span>
|
|
149
|
+
<span class="cline-any cline-yes">1x</span>
|
|
157
150
|
<span class="cline-any cline-neutral"> </span>
|
|
158
151
|
<span class="cline-any cline-neutral"> </span>
|
|
159
152
|
<span class="cline-any cline-neutral"> </span>
|
|
@@ -162,22 +155,11 @@
|
|
|
162
155
|
<span class="cline-any cline-neutral"> </span>
|
|
163
156
|
<span class="cline-any cline-neutral"> </span>
|
|
164
157
|
<span class="cline-any cline-neutral"> </span>
|
|
165
|
-
<span class="cline-any cline-yes">1x</span>
|
|
166
|
-
<span class="cline-any cline-yes">1x</span>
|
|
167
|
-
<span class="cline-any cline-neutral"> </span>
|
|
168
|
-
<span class="cline-any cline-yes">1x</span>
|
|
169
|
-
<span class="cline-any cline-no"> </span>
|
|
170
158
|
<span class="cline-any cline-neutral"> </span>
|
|
171
159
|
<span class="cline-any cline-neutral"> </span>
|
|
172
160
|
<span class="cline-any cline-neutral"> </span>
|
|
173
161
|
<span class="cline-any cline-neutral"> </span>
|
|
174
162
|
<span class="cline-any cline-yes">1x</span>
|
|
175
|
-
<span class="cline-any cline-yes">1x</span>
|
|
176
|
-
<span class="cline-any cline-neutral"> </span>
|
|
177
|
-
<span class="cline-any cline-neutral"> </span>
|
|
178
|
-
<span class="cline-any cline-neutral"> </span>
|
|
179
|
-
<span class="cline-any cline-neutral"> </span>
|
|
180
|
-
<span class="cline-any cline-neutral"> </span>
|
|
181
163
|
<span class="cline-any cline-yes">8x</span>
|
|
182
164
|
<span class="cline-any cline-yes">8x</span>
|
|
183
165
|
<span class="cline-any cline-yes">8x</span>
|
|
@@ -240,8 +222,11 @@
|
|
|
240
222
|
<span class="cline-any cline-yes">4x</span>
|
|
241
223
|
<span class="cline-any cline-neutral"> </span>
|
|
242
224
|
<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">import * as crypto from "crypto";
|
|
225
|
+
import { CoreMBConfig } from "./config";
|
|
243
226
|
import { normalize } from "./phone.number.helper";
|
|
244
227
|
|
|
228
|
+
const config = CoreMBConfig.getInstance();
|
|
229
|
+
|
|
245
230
|
export type PhoneCipherRecord = {
|
|
246
231
|
// Base64 strings suitable for storage in text columns
|
|
247
232
|
ciphertext: string; // Encrypted phone number
|
|
@@ -250,24 +235,12 @@ export type PhoneCipherRecord = {
|
|
|
250
235
|
hmacIndex: string; // HMAC-SHA256(phoneNormalized) for search/dedup
|
|
251
236
|
};
|
|
252
237
|
|
|
253
|
-
const aesKeyB64 = process.env.PHONE_AES_KEY;
|
|
254
|
-
const hmacKeyB64 = process.env.PHONE_HMAC_KEY;
|
|
255
|
-
|
|
256
|
-
<span class="missing-if-branch" title="if path not taken" >I</span>if (!aesKeyB64 || !hmacKeyB64) {
|
|
257
|
-
<span class="cstat-no" title="statement not covered" > throw new Error(</span>
|
|
258
|
-
"PHONE_AES_KEY and PHONE_HMAC_KEY must be set (base64-encoded)."
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const aesKey = Buffer.from(aesKeyB64, "base64");
|
|
263
|
-
const hmacKey = Buffer.from(hmacKeyB64, "base64");
|
|
264
|
-
|
|
265
238
|
/** Create an HMAC index for lookups without revealing the number.
|
|
266
239
|
* Store this alongside the encrypted data and index it in the DB.
|
|
267
240
|
*/
|
|
268
|
-
function hmacIndex(phoneRaw: string): string {
|
|
241
|
+
export function hmacIndex(phoneRaw: string): string {
|
|
269
242
|
const normalized = normalize(phoneRaw);
|
|
270
|
-
const h = crypto.createHmac("sha256", hmacKey);
|
|
243
|
+
const h = crypto.createHmac("sha256", config.hmacKey);
|
|
271
244
|
h.update(normalized, "utf8");
|
|
272
245
|
return h.digest("base64");
|
|
273
246
|
}
|
|
@@ -275,7 +248,7 @@ function hmacIndex(phoneRaw: string): string {
|
|
|
275
248
|
/** Encrypt a phone number using AES-256-GCM.
|
|
276
249
|
* Returns base64-encoded ciphertext, iv and authTag suitable for storage.
|
|
277
250
|
*/
|
|
278
|
-
export function
|
|
251
|
+
export function phoneEncrypt(
|
|
279
252
|
phoneRaw: string
|
|
280
253
|
): Omit<PhoneCipherRecord, "hmacIndex"> & { hmacIndex: string } {
|
|
281
254
|
const normalized = normalize(phoneRaw);
|
|
@@ -283,7 +256,7 @@ export function encrypt(
|
|
|
283
256
|
// 12-byte IV is recommended for GCM
|
|
284
257
|
const iv = crypto.randomBytes(12);
|
|
285
258
|
|
|
286
|
-
const cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv);
|
|
259
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", config.aesKey, iv);
|
|
287
260
|
|
|
288
261
|
// Optional: bind additional data (AAD), e.g., tenant ID to prevent cross-tenant swaps
|
|
289
262
|
// cipher.setAAD(Buffer.from(tenantId, 'utf8'));
|
|
@@ -306,14 +279,14 @@ export function encrypt(
|
|
|
306
279
|
/** Decrypt a previously stored record. Throws if tampered.
|
|
307
280
|
* Returns the normalized phone string.
|
|
308
281
|
*/
|
|
309
|
-
export function
|
|
282
|
+
export function phoneDecrypt(
|
|
310
283
|
record: Pick<PhoneCipherRecord, "ciphertext" | "iv" | "authTag">
|
|
311
284
|
): string {
|
|
312
285
|
const iv = Buffer.from(record.iv, "base64");
|
|
313
286
|
const authTag = Buffer.from(record.authTag, "base64");
|
|
314
287
|
const ciphertext = Buffer.from(record.ciphertext, "base64");
|
|
315
288
|
|
|
316
|
-
const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv);
|
|
289
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", config.aesKey, iv);
|
|
317
290
|
|
|
318
291
|
// If you used setAAD on encrypt, you MUST set the same AAD here
|
|
319
292
|
// decipher.setAAD(Buffer.from(tenantId, 'utf8'));
|
|
@@ -334,7 +307,7 @@ export function decrypt(
|
|
|
334
307
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
335
308
|
Code coverage generated by
|
|
336
309
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
337
|
-
at 2025-08-
|
|
310
|
+
at 2025-08-23T08:06:17.198Z
|
|
338
311
|
</div>
|
|
339
312
|
<script src="prettify.js"></script>
|
|
340
313
|
<script>
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ConfigProperties {
|
|
2
|
+
aesKey: string;
|
|
3
|
+
hmacKey: string;
|
|
4
|
+
}
|
|
5
|
+
export declare class CoreMBConfig {
|
|
6
|
+
private static instance;
|
|
7
|
+
readonly aesKey: Buffer;
|
|
8
|
+
readonly hmacKey: Buffer;
|
|
9
|
+
private constructor();
|
|
10
|
+
static initialize(props: ConfigProperties): CoreMBConfig;
|
|
11
|
+
static getInstance(): CoreMBConfig;
|
|
12
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CoreMBConfig = void 0;
|
|
4
|
+
class CoreMBConfig {
|
|
5
|
+
constructor(props) {
|
|
6
|
+
this.aesKey = Buffer.from(props.aesKey, "base64");
|
|
7
|
+
this.hmacKey = Buffer.from(props.hmacKey, "base64");
|
|
8
|
+
}
|
|
9
|
+
static initialize(props) {
|
|
10
|
+
if (CoreMBConfig.instance) {
|
|
11
|
+
throw new Error("CoreMBConfig has already been initialized");
|
|
12
|
+
}
|
|
13
|
+
CoreMBConfig.instance = new CoreMBConfig(props);
|
|
14
|
+
return CoreMBConfig.instance;
|
|
15
|
+
}
|
|
16
|
+
// Access the singleton instance
|
|
17
|
+
static getInstance() {
|
|
18
|
+
if (!CoreMBConfig.instance) {
|
|
19
|
+
throw new Error("CoreMBConfig has not been initialized yet");
|
|
20
|
+
}
|
|
21
|
+
return CoreMBConfig.instance;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.CoreMBConfig = CoreMBConfig;
|
package/dist/date.helper.d.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function sample(): void;
|
package/dist/date.helper.js
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -14,4 +14,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./date.helper"), exports);
|
|
17
18
|
__exportStar(require("./security.helper"), exports);
|
|
@@ -4,13 +4,17 @@ export type PhoneCipherRecord = {
|
|
|
4
4
|
authTag: string;
|
|
5
5
|
hmacIndex: string;
|
|
6
6
|
};
|
|
7
|
+
/** Create an HMAC index for lookups without revealing the number.
|
|
8
|
+
* Store this alongside the encrypted data and index it in the DB.
|
|
9
|
+
*/
|
|
10
|
+
export declare function hmacIndex(phoneRaw: string): string;
|
|
7
11
|
/** Encrypt a phone number using AES-256-GCM.
|
|
8
12
|
* Returns base64-encoded ciphertext, iv and authTag suitable for storage.
|
|
9
13
|
*/
|
|
10
|
-
export declare function
|
|
14
|
+
export declare function phoneEncrypt(phoneRaw: string): Omit<PhoneCipherRecord, "hmacIndex"> & {
|
|
11
15
|
hmacIndex: string;
|
|
12
16
|
};
|
|
13
17
|
/** Decrypt a previously stored record. Throws if tampered.
|
|
14
18
|
* Returns the normalized phone string.
|
|
15
19
|
*/
|
|
16
|
-
export declare function
|
|
20
|
+
export declare function phoneDecrypt(record: Pick<PhoneCipherRecord, "ciphertext" | "iv" | "authTag">): string;
|
package/dist/security.helper.js
CHANGED
|
@@ -33,34 +33,30 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.
|
|
37
|
-
exports.
|
|
36
|
+
exports.hmacIndex = hmacIndex;
|
|
37
|
+
exports.phoneEncrypt = phoneEncrypt;
|
|
38
|
+
exports.phoneDecrypt = phoneDecrypt;
|
|
38
39
|
const crypto = __importStar(require("crypto"));
|
|
40
|
+
const config_1 = require("./config");
|
|
39
41
|
const phone_number_helper_1 = require("./phone.number.helper");
|
|
40
|
-
const
|
|
41
|
-
const hmacKeyB64 = process.env.PHONE_HMAC_KEY;
|
|
42
|
-
if (!aesKeyB64 || !hmacKeyB64) {
|
|
43
|
-
throw new Error("PHONE_AES_KEY and PHONE_HMAC_KEY must be set (base64-encoded).");
|
|
44
|
-
}
|
|
45
|
-
const aesKey = Buffer.from(aesKeyB64, "base64");
|
|
46
|
-
const hmacKey = Buffer.from(hmacKeyB64, "base64");
|
|
42
|
+
const config = config_1.CoreMBConfig.getInstance();
|
|
47
43
|
/** Create an HMAC index for lookups without revealing the number.
|
|
48
44
|
* Store this alongside the encrypted data and index it in the DB.
|
|
49
45
|
*/
|
|
50
46
|
function hmacIndex(phoneRaw) {
|
|
51
47
|
const normalized = (0, phone_number_helper_1.normalize)(phoneRaw);
|
|
52
|
-
const h = crypto.createHmac("sha256", hmacKey);
|
|
48
|
+
const h = crypto.createHmac("sha256", config.hmacKey);
|
|
53
49
|
h.update(normalized, "utf8");
|
|
54
50
|
return h.digest("base64");
|
|
55
51
|
}
|
|
56
52
|
/** Encrypt a phone number using AES-256-GCM.
|
|
57
53
|
* Returns base64-encoded ciphertext, iv and authTag suitable for storage.
|
|
58
54
|
*/
|
|
59
|
-
function
|
|
55
|
+
function phoneEncrypt(phoneRaw) {
|
|
60
56
|
const normalized = (0, phone_number_helper_1.normalize)(phoneRaw);
|
|
61
57
|
// 12-byte IV is recommended for GCM
|
|
62
58
|
const iv = crypto.randomBytes(12);
|
|
63
|
-
const cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv);
|
|
59
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", config.aesKey, iv);
|
|
64
60
|
// Optional: bind additional data (AAD), e.g., tenant ID to prevent cross-tenant swaps
|
|
65
61
|
// cipher.setAAD(Buffer.from(tenantId, 'utf8'));
|
|
66
62
|
const ciphertextBuf = Buffer.concat([
|
|
@@ -78,11 +74,11 @@ function encrypt(phoneRaw) {
|
|
|
78
74
|
/** Decrypt a previously stored record. Throws if tampered.
|
|
79
75
|
* Returns the normalized phone string.
|
|
80
76
|
*/
|
|
81
|
-
function
|
|
77
|
+
function phoneDecrypt(record) {
|
|
82
78
|
const iv = Buffer.from(record.iv, "base64");
|
|
83
79
|
const authTag = Buffer.from(record.authTag, "base64");
|
|
84
80
|
const ciphertext = Buffer.from(record.ciphertext, "base64");
|
|
85
|
-
const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv);
|
|
81
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", config.aesKey, iv);
|
|
86
82
|
// If you used setAAD on encrypt, you MUST set the same AAD here
|
|
87
83
|
// decipher.setAAD(Buffer.from(tenantId, 'utf8'));
|
|
88
84
|
decipher.setAuthTag(authTag);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ValidationOptions } from "class-validator";
|
|
2
|
+
/**
|
|
3
|
+
* Checks that the string is likely an encrypted phone number
|
|
4
|
+
* (e.g., base64 or hex encoded string). Adjust pattern as needed.
|
|
5
|
+
*/
|
|
6
|
+
export declare function IsEncryptedPhone(validationOptions?: ValidationOptions): (object: Object, propertyName: string) => void;
|
|
@@ -1 +1,33 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IsEncryptedPhone = IsEncryptedPhone;
|
|
4
|
+
const class_validator_1 = require("class-validator");
|
|
5
|
+
/**
|
|
6
|
+
* Checks that the string is likely an encrypted phone number
|
|
7
|
+
* (e.g., base64 or hex encoded string). Adjust pattern as needed.
|
|
8
|
+
*/
|
|
9
|
+
function IsEncryptedPhone(validationOptions) {
|
|
10
|
+
return function (object, propertyName) {
|
|
11
|
+
(0, class_validator_1.registerDecorator)({
|
|
12
|
+
name: "isEncryptedPhone",
|
|
13
|
+
target: object.constructor,
|
|
14
|
+
propertyName: propertyName,
|
|
15
|
+
options: validationOptions,
|
|
16
|
+
validator: {
|
|
17
|
+
validate(value, args) {
|
|
18
|
+
if (typeof value !== "string")
|
|
19
|
+
return false;
|
|
20
|
+
// Example: allow hex or base64 strings
|
|
21
|
+
// Hex pattern (32+ chars, adjust length as needed)
|
|
22
|
+
const hexPattern = /^[a-fA-F0-9]+$/;
|
|
23
|
+
// Base64 pattern
|
|
24
|
+
const base64Pattern = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
25
|
+
return hexPattern.test(value) || base64Pattern.test(value);
|
|
26
|
+
},
|
|
27
|
+
defaultMessage(args) {
|
|
28
|
+
return `${args.property} must be an encrypted phone string (hex or base64)`;
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
}
|
package/jest.config.js
CHANGED
|
@@ -4,8 +4,9 @@ module.exports = {
|
|
|
4
4
|
setupFiles: ["<rootDir>/jest.setup.ts"],
|
|
5
5
|
collectCoverage: true, // enable coverage
|
|
6
6
|
coverageDirectory: "coverage", // output folder
|
|
7
|
-
coverageReporters: ["text", "lcov", "html"], // text in terminal + HTML report
|
|
7
|
+
coverageReporters: ["text-summary", "lcov", "html"], // text in terminal + HTML report
|
|
8
8
|
testMatch: ["**/*.spec.ts"], // only run *.spec.ts files
|
|
9
|
+
coveragePathIgnorePatterns: ["/node_modules/"],
|
|
9
10
|
reporters: [
|
|
10
11
|
"default",
|
|
11
12
|
["jest-html-reporter", {
|
package/jest.setup.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "core-mb",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Core utility functions for the MB ecosystem in TypeScript",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"test": "jest",
|
|
10
10
|
"test:watch": "jest --watch",
|
|
11
11
|
"test:coverage": "jest --coverage",
|
|
12
|
-
"coverage": "jest
|
|
12
|
+
"coverage": "nyc jest"
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
15
15
|
"core",
|
|
@@ -26,7 +26,11 @@
|
|
|
26
26
|
"dotenv": "^17.2.1",
|
|
27
27
|
"jest": "^30.0.5",
|
|
28
28
|
"jest-html-reporter": "^4.3.0",
|
|
29
|
+
"nyc": "^17.1.0",
|
|
29
30
|
"ts-jest": "^29.4.1",
|
|
30
31
|
"typescript": "^5.9.2"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"class-validator": "^0.14.2"
|
|
31
35
|
}
|
|
32
36
|
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface ConfigProperties {
|
|
2
|
+
aesKey: string;
|
|
3
|
+
hmacKey: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export class CoreMBConfig {
|
|
7
|
+
private static instance: CoreMBConfig;
|
|
8
|
+
public readonly aesKey: Buffer;
|
|
9
|
+
public readonly hmacKey: Buffer;
|
|
10
|
+
|
|
11
|
+
private constructor(props: ConfigProperties) {
|
|
12
|
+
this.aesKey = Buffer.from(props.aesKey, "base64");
|
|
13
|
+
this.hmacKey = Buffer.from(props.hmacKey, "base64");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public static initialize(props: ConfigProperties): CoreMBConfig {
|
|
17
|
+
if (CoreMBConfig.instance) {
|
|
18
|
+
throw new Error("CoreMBConfig has already been initialized");
|
|
19
|
+
}
|
|
20
|
+
CoreMBConfig.instance = new CoreMBConfig(props);
|
|
21
|
+
return CoreMBConfig.instance;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Access the singleton instance
|
|
25
|
+
public static getInstance(): CoreMBConfig {
|
|
26
|
+
if (!CoreMBConfig.instance) {
|
|
27
|
+
throw new Error("CoreMBConfig has not been initialized yet");
|
|
28
|
+
}
|
|
29
|
+
return CoreMBConfig.instance;
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/date.helper.ts
CHANGED
package/src/index.ts
CHANGED
package/src/security.helper.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import * as crypto from "crypto";
|
|
2
|
+
import { CoreMBConfig } from "./config";
|
|
2
3
|
import { normalize } from "./phone.number.helper";
|
|
3
4
|
|
|
5
|
+
const config = CoreMBConfig.getInstance();
|
|
6
|
+
|
|
4
7
|
export type PhoneCipherRecord = {
|
|
5
8
|
// Base64 strings suitable for storage in text columns
|
|
6
9
|
ciphertext: string; // Encrypted phone number
|
|
@@ -9,24 +12,12 @@ export type PhoneCipherRecord = {
|
|
|
9
12
|
hmacIndex: string; // HMAC-SHA256(phoneNormalized) for search/dedup
|
|
10
13
|
};
|
|
11
14
|
|
|
12
|
-
const aesKeyB64 = process.env.PHONE_AES_KEY;
|
|
13
|
-
const hmacKeyB64 = process.env.PHONE_HMAC_KEY;
|
|
14
|
-
|
|
15
|
-
if (!aesKeyB64 || !hmacKeyB64) {
|
|
16
|
-
throw new Error(
|
|
17
|
-
"PHONE_AES_KEY and PHONE_HMAC_KEY must be set (base64-encoded)."
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const aesKey = Buffer.from(aesKeyB64, "base64");
|
|
22
|
-
const hmacKey = Buffer.from(hmacKeyB64, "base64");
|
|
23
|
-
|
|
24
15
|
/** Create an HMAC index for lookups without revealing the number.
|
|
25
16
|
* Store this alongside the encrypted data and index it in the DB.
|
|
26
17
|
*/
|
|
27
|
-
function hmacIndex(phoneRaw: string): string {
|
|
18
|
+
export function hmacIndex(phoneRaw: string): string {
|
|
28
19
|
const normalized = normalize(phoneRaw);
|
|
29
|
-
const h = crypto.createHmac("sha256", hmacKey);
|
|
20
|
+
const h = crypto.createHmac("sha256", config.hmacKey);
|
|
30
21
|
h.update(normalized, "utf8");
|
|
31
22
|
return h.digest("base64");
|
|
32
23
|
}
|
|
@@ -34,7 +25,7 @@ function hmacIndex(phoneRaw: string): string {
|
|
|
34
25
|
/** Encrypt a phone number using AES-256-GCM.
|
|
35
26
|
* Returns base64-encoded ciphertext, iv and authTag suitable for storage.
|
|
36
27
|
*/
|
|
37
|
-
export function
|
|
28
|
+
export function phoneEncrypt(
|
|
38
29
|
phoneRaw: string
|
|
39
30
|
): Omit<PhoneCipherRecord, "hmacIndex"> & { hmacIndex: string } {
|
|
40
31
|
const normalized = normalize(phoneRaw);
|
|
@@ -42,7 +33,7 @@ export function encrypt(
|
|
|
42
33
|
// 12-byte IV is recommended for GCM
|
|
43
34
|
const iv = crypto.randomBytes(12);
|
|
44
35
|
|
|
45
|
-
const cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv);
|
|
36
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", config.aesKey, iv);
|
|
46
37
|
|
|
47
38
|
// Optional: bind additional data (AAD), e.g., tenant ID to prevent cross-tenant swaps
|
|
48
39
|
// cipher.setAAD(Buffer.from(tenantId, 'utf8'));
|
|
@@ -65,14 +56,14 @@ export function encrypt(
|
|
|
65
56
|
/** Decrypt a previously stored record. Throws if tampered.
|
|
66
57
|
* Returns the normalized phone string.
|
|
67
58
|
*/
|
|
68
|
-
export function
|
|
59
|
+
export function phoneDecrypt(
|
|
69
60
|
record: Pick<PhoneCipherRecord, "ciphertext" | "iv" | "authTag">
|
|
70
61
|
): string {
|
|
71
62
|
const iv = Buffer.from(record.iv, "base64");
|
|
72
63
|
const authTag = Buffer.from(record.authTag, "base64");
|
|
73
64
|
const ciphertext = Buffer.from(record.ciphertext, "base64");
|
|
74
65
|
|
|
75
|
-
const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv);
|
|
66
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", config.aesKey, iv);
|
|
76
67
|
|
|
77
68
|
// If you used setAAD on encrypt, you MUST set the same AAD here
|
|
78
69
|
// decipher.setAAD(Buffer.from(tenantId, 'utf8'));
|
package/src/validation.helper.ts
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
registerDecorator,
|
|
3
|
+
ValidationOptions,
|
|
4
|
+
ValidationArguments,
|
|
5
|
+
} from "class-validator";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Checks that the string is likely an encrypted phone number
|
|
9
|
+
* (e.g., base64 or hex encoded string). Adjust pattern as needed.
|
|
10
|
+
*/
|
|
11
|
+
export function IsEncryptedPhone(validationOptions?: ValidationOptions) {
|
|
12
|
+
return function (object: Object, propertyName: string) {
|
|
13
|
+
registerDecorator({
|
|
14
|
+
name: "isEncryptedPhone",
|
|
15
|
+
target: object.constructor,
|
|
16
|
+
propertyName: propertyName,
|
|
17
|
+
options: validationOptions,
|
|
18
|
+
validator: {
|
|
19
|
+
validate(value: any, args: ValidationArguments) {
|
|
20
|
+
if (typeof value !== "string") return false;
|
|
21
|
+
|
|
22
|
+
// Example: allow hex or base64 strings
|
|
23
|
+
// Hex pattern (32+ chars, adjust length as needed)
|
|
24
|
+
const hexPattern = /^[a-fA-F0-9]+$/;
|
|
25
|
+
// Base64 pattern
|
|
26
|
+
const base64Pattern = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
27
|
+
|
|
28
|
+
return hexPattern.test(value) || base64Pattern.test(value);
|
|
29
|
+
},
|
|
30
|
+
defaultMessage(args: ValidationArguments) {
|
|
31
|
+
return `${args.property} must be an encrypted phone string (hex or base64)`;
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
}
|
package/test-report.html
CHANGED
|
@@ -274,4 +274,4 @@ header {
|
|
|
274
274
|
font-size: 1rem;
|
|
275
275
|
padding: 0 0.5rem;
|
|
276
276
|
}
|
|
277
|
-
</style></head><body><main class="jesthtml-content"><header><h1 id="title">Test Report</h1></header><section id="metadata-container"><div id="timestamp">Started: 2025-08-23
|
|
277
|
+
</style></head><body><main class="jesthtml-content"><header><h1 id="title">Test Report</h1></header><section id="metadata-container"><div id="timestamp">Started: 2025-08-23 15:06:13</div><div id="summary"><div id="suite-summary"><div class="summary-total">Suites (2)</div><div class="summary-passed ">2 passed</div><div class="summary-failed summary-empty">0 failed</div><div class="summary-pending summary-empty">0 pending</div></div><div id="test-summary"><div class="summary-total">Tests (9)</div><div class="summary-passed ">9 passed</div><div class="summary-failed summary-empty">0 failed</div><div class="summary-pending summary-empty">0 pending</div></div></div></section><details id="suite-1" class="suite-container" open=""><summary class="suite-info"><div class="suite-path">/Volumes/Projects/RND/microservices/vieltalk-chat/core-mb/__unitest__/phone.number.helper.spec.ts</div><div class="suite-time">2.333s</div></summary><div class="suite-tests"><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.helper > normalize</div><div class="test-title">should return empty string for null/undefined/empty input</div><div class="test-status">passed</div><div class="test-duration">0.006s</div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.helper > normalize</div><div class="test-title">should keep leading + and strip non-digit characters</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.helper > normalize</div><div class="test-title">should remove all non-digits if no leading +</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.helper > normalize</div><div class="test-title">should handle already clean input</div><div class="test-status">passed</div><div class="test-duration"> </div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.helper > normalize</div><div class="test-title">should not duplicate + signs</div><div class="test-status">passed</div><div class="test-duration">0.001s</div></div></div></div></details><details id="suite-2" class="suite-container" open=""><summary class="suite-info"><div class="suite-path">/Volumes/Projects/RND/microservices/vieltalk-chat/core-mb/__unitest__/phone.number.crypto.spec.ts</div><div class="suite-time">2.611s</div></summary><div class="suite-tests"><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.crypto</div><div class="test-title">should encrypt and decrypt correctly</div><div class="test-status">passed</div><div class="test-duration">0.009s</div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.crypto</div><div class="test-title">should produce different ciphertexts for same phone (unique IV)</div><div class="test-status">passed</div><div class="test-duration">0.001s</div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.crypto</div><div class="test-title">should throw when tampering with ciphertext</div><div class="test-status">passed</div><div class="test-duration">0.009s</div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.crypto</div><div class="test-title">should throw when tampering with authTag</div><div class="test-status">passed</div><div class="test-duration">0.001s</div></div></div></div></details></main></body></html>
|