core-mb 1.0.1 → 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/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 +16 -43
- package/coverage/lcov.info +56 -35
- package/coverage/phone.number.helper.ts.html +1 -1
- package/coverage/security.helper.ts.html +16 -43
- package/dist/config.d.ts +12 -0
- package/dist/config.js +24 -0
- package/dist/security.helper.js +5 -10
- package/jest.config.js +2 -1
- package/jest.setup.ts +8 -0
- package/package.json +3 -2
- package/src/config.ts +31 -0
- package/src/security.helper.ts +6 -15
- package/test-report.html +1 -1
|
@@ -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,17 +155,6 @@
|
|
|
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
|
-
<span class="cline-any cline-neutral"> </span>
|
|
171
|
-
<span class="cline-any cline-neutral"> </span>
|
|
172
|
-
<span class="cline-any cline-neutral"> </span>
|
|
173
|
-
<span class="cline-any cline-neutral"> </span>
|
|
174
|
-
<span class="cline-any cline-yes">1x</span>
|
|
175
|
-
<span class="cline-any cline-yes">1x</span>
|
|
176
158
|
<span class="cline-any cline-neutral"> </span>
|
|
177
159
|
<span class="cline-any cline-neutral"> </span>
|
|
178
160
|
<span class="cline-any cline-neutral"> </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
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
|
}
|
|
@@ -283,7 +256,7 @@ export function phoneEncrypt(
|
|
|
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'));
|
|
@@ -313,7 +286,7 @@ export function phoneDecrypt(
|
|
|
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 phoneDecrypt(
|
|
|
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/security.helper.js
CHANGED
|
@@ -37,20 +37,15 @@ exports.hmacIndex = hmacIndex;
|
|
|
37
37
|
exports.phoneEncrypt = phoneEncrypt;
|
|
38
38
|
exports.phoneDecrypt = phoneDecrypt;
|
|
39
39
|
const crypto = __importStar(require("crypto"));
|
|
40
|
+
const config_1 = require("./config");
|
|
40
41
|
const phone_number_helper_1 = require("./phone.number.helper");
|
|
41
|
-
const
|
|
42
|
-
const hmacKeyB64 = process.env.PHONE_HMAC_KEY;
|
|
43
|
-
if (!aesKeyB64 || !hmacKeyB64) {
|
|
44
|
-
throw new Error("PHONE_AES_KEY and PHONE_HMAC_KEY must be set (base64-encoded).");
|
|
45
|
-
}
|
|
46
|
-
const aesKey = Buffer.from(aesKeyB64, "base64");
|
|
47
|
-
const hmacKey = Buffer.from(hmacKeyB64, "base64");
|
|
42
|
+
const config = config_1.CoreMBConfig.getInstance();
|
|
48
43
|
/** Create an HMAC index for lookups without revealing the number.
|
|
49
44
|
* Store this alongside the encrypted data and index it in the DB.
|
|
50
45
|
*/
|
|
51
46
|
function hmacIndex(phoneRaw) {
|
|
52
47
|
const normalized = (0, phone_number_helper_1.normalize)(phoneRaw);
|
|
53
|
-
const h = crypto.createHmac("sha256", hmacKey);
|
|
48
|
+
const h = crypto.createHmac("sha256", config.hmacKey);
|
|
54
49
|
h.update(normalized, "utf8");
|
|
55
50
|
return h.digest("base64");
|
|
56
51
|
}
|
|
@@ -61,7 +56,7 @@ function phoneEncrypt(phoneRaw) {
|
|
|
61
56
|
const normalized = (0, phone_number_helper_1.normalize)(phoneRaw);
|
|
62
57
|
// 12-byte IV is recommended for GCM
|
|
63
58
|
const iv = crypto.randomBytes(12);
|
|
64
|
-
const cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv);
|
|
59
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", config.aesKey, iv);
|
|
65
60
|
// Optional: bind additional data (AAD), e.g., tenant ID to prevent cross-tenant swaps
|
|
66
61
|
// cipher.setAAD(Buffer.from(tenantId, 'utf8'));
|
|
67
62
|
const ciphertextBuf = Buffer.concat([
|
|
@@ -83,7 +78,7 @@ function phoneDecrypt(record) {
|
|
|
83
78
|
const iv = Buffer.from(record.iv, "base64");
|
|
84
79
|
const authTag = Buffer.from(record.authTag, "base64");
|
|
85
80
|
const ciphertext = Buffer.from(record.ciphertext, "base64");
|
|
86
|
-
const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv);
|
|
81
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", config.aesKey, iv);
|
|
87
82
|
// If you used setAAD on encrypt, you MUST set the same AAD here
|
|
88
83
|
// decipher.setAAD(Buffer.from(tenantId, 'utf8'));
|
|
89
84
|
decipher.setAuthTag(authTag);
|
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,6 +26,7 @@
|
|
|
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"
|
|
31
32
|
},
|
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/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
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
|
}
|
|
@@ -42,7 +33,7 @@ export function phoneEncrypt(
|
|
|
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'));
|
|
@@ -72,7 +63,7 @@ export function phoneDecrypt(
|
|
|
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/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>
|