core-mb 1.0.0 → 1.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.
- package/__unitest__/phone.number.crypto.spec.ts +9 -9
- package/coverage/index.html +10 -10
- package/coverage/lcov-report/index.html +10 -10
- package/coverage/lcov-report/phone.number.helper.ts.html +1 -1
- package/coverage/lcov-report/security.helper.ts.html +9 -9
- package/coverage/lcov.info +7 -6
- package/coverage/phone.number.helper.ts.html +1 -1
- package/coverage/security.helper.ts.html +9 -9
- 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 +5 -4
- package/dist/validation.helper.d.ts +6 -0
- package/dist/validation.helper.js +32 -0
- package/package.json +4 -1
- package/src/date.helper.ts +3 -0
- package/src/index.ts +1 -0
- package/src/security.helper.ts +3 -3
- package/src/validation.helper.ts +36 -0
- package/test-report.html +1 -1
- package/src/aes.helper.ts +0 -101
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// phone.number.crypto.spec.ts
|
|
2
2
|
import * as crypto from "crypto";
|
|
3
|
-
import {
|
|
3
|
+
import { phoneEncrypt, phoneDecrypt } from "../src/security.helper";
|
|
4
4
|
import { normalize } from "../src/phone.number.helper";
|
|
5
5
|
|
|
6
6
|
// Mock environment variables for testing
|
|
@@ -17,10 +17,10 @@ describe("phone.number.crypto", () => {
|
|
|
17
17
|
|
|
18
18
|
it("should encrypt and decrypt correctly", () => {
|
|
19
19
|
samplePhones.forEach((phone) => {
|
|
20
|
-
const encrypted =
|
|
20
|
+
const encrypted = phoneEncrypt(phone);
|
|
21
21
|
|
|
22
22
|
// decrypt should return normalized phone
|
|
23
|
-
const decrypted =
|
|
23
|
+
const decrypted = phoneDecrypt(encrypted);
|
|
24
24
|
expect(decrypted).toBe(normalize(phone));
|
|
25
25
|
|
|
26
26
|
// ciphertext, iv, authTag should be base64 strings
|
|
@@ -35,8 +35,8 @@ describe("phone.number.crypto", () => {
|
|
|
35
35
|
|
|
36
36
|
it("should produce different ciphertexts for same phone (unique IV)", () => {
|
|
37
37
|
const phone = "+85512345678";
|
|
38
|
-
const e1 =
|
|
39
|
-
const e2 =
|
|
38
|
+
const e1 = phoneEncrypt(phone);
|
|
39
|
+
const e2 = phoneEncrypt(phone);
|
|
40
40
|
|
|
41
41
|
expect(e1.ciphertext).not.toBe(e2.ciphertext);
|
|
42
42
|
expect(e1.iv).not.toBe(e2.iv);
|
|
@@ -47,23 +47,23 @@ describe("phone.number.crypto", () => {
|
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
it("should throw when tampering with ciphertext", () => {
|
|
50
|
-
const encrypted =
|
|
50
|
+
const encrypted = phoneEncrypt("+85512345678");
|
|
51
51
|
|
|
52
52
|
// tamper with ciphertext
|
|
53
53
|
const tampered = {
|
|
54
54
|
...encrypted,
|
|
55
55
|
ciphertext: encrypted.ciphertext.slice(0, -2) + "AA",
|
|
56
56
|
};
|
|
57
|
-
expect(() =>
|
|
57
|
+
expect(() => phoneDecrypt(tampered)).toThrow();
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
it("should throw when tampering with authTag", () => {
|
|
61
|
-
const encrypted =
|
|
61
|
+
const encrypted = phoneEncrypt("+85512345678");
|
|
62
62
|
|
|
63
63
|
const tampered = {
|
|
64
64
|
...encrypted,
|
|
65
65
|
authTag: encrypted.authTag.slice(0, -2) + "AA",
|
|
66
66
|
};
|
|
67
|
-
expect(() =>
|
|
67
|
+
expect(() => phoneDecrypt(tampered)).toThrow();
|
|
68
68
|
});
|
|
69
69
|
});
|
package/coverage/index.html
CHANGED
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
<div class='clearfix'>
|
|
24
24
|
|
|
25
25
|
<div class='fl pad1y space-right2'>
|
|
26
|
-
<span class="strong">97.
|
|
26
|
+
<span class="strong">97.14% </span>
|
|
27
27
|
<span class="quiet">Statements</span>
|
|
28
|
-
<span class='fraction'>
|
|
28
|
+
<span class='fraction'>34/35</span>
|
|
29
29
|
</div>
|
|
30
30
|
|
|
31
31
|
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
<div class='fl pad1y space-right2'>
|
|
47
|
-
<span class="strong">
|
|
47
|
+
<span class="strong">97.05% </span>
|
|
48
48
|
<span class="quiet">Lines</span>
|
|
49
|
-
<span class='fraction'>
|
|
49
|
+
<span class='fraction'>33/34</span>
|
|
50
50
|
</div>
|
|
51
51
|
|
|
52
52
|
|
|
@@ -95,17 +95,17 @@
|
|
|
95
95
|
|
|
96
96
|
<tr>
|
|
97
97
|
<td class="file high" data-value="security.helper.ts"><a href="security.helper.ts.html">security.helper.ts</a></td>
|
|
98
|
-
<td data-value="96.
|
|
98
|
+
<td data-value="96.42" class="pic high">
|
|
99
99
|
<div class="chart"><div class="cover-fill" style="width: 96%"></div><div class="cover-empty" style="width: 4%"></div></div>
|
|
100
100
|
</td>
|
|
101
|
-
<td data-value="96.
|
|
102
|
-
<td data-value="
|
|
101
|
+
<td data-value="96.42" class="pct high">96.42%</td>
|
|
102
|
+
<td data-value="28" class="abs high">27/28</td>
|
|
103
103
|
<td data-value="75" class="pct medium">75%</td>
|
|
104
104
|
<td data-value="4" class="abs medium">3/4</td>
|
|
105
105
|
<td data-value="100" class="pct high">100%</td>
|
|
106
106
|
<td data-value="3" class="abs high">3/3</td>
|
|
107
|
-
<td data-value="96.
|
|
108
|
-
<td data-value="
|
|
107
|
+
<td data-value="96.42" class="pct high">96.42%</td>
|
|
108
|
+
<td data-value="28" class="abs high">27/28</td>
|
|
109
109
|
</tr>
|
|
110
110
|
|
|
111
111
|
</tbody>
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
117
117
|
Code coverage generated by
|
|
118
118
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
119
|
-
at 2025-08-23T06:
|
|
119
|
+
at 2025-08-23T06:47:14.841Z
|
|
120
120
|
</div>
|
|
121
121
|
<script src="prettify.js"></script>
|
|
122
122
|
<script>
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
<div class='clearfix'>
|
|
24
24
|
|
|
25
25
|
<div class='fl pad1y space-right2'>
|
|
26
|
-
<span class="strong">97.
|
|
26
|
+
<span class="strong">97.14% </span>
|
|
27
27
|
<span class="quiet">Statements</span>
|
|
28
|
-
<span class='fraction'>
|
|
28
|
+
<span class='fraction'>34/35</span>
|
|
29
29
|
</div>
|
|
30
30
|
|
|
31
31
|
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
<div class='fl pad1y space-right2'>
|
|
47
|
-
<span class="strong">
|
|
47
|
+
<span class="strong">97.05% </span>
|
|
48
48
|
<span class="quiet">Lines</span>
|
|
49
|
-
<span class='fraction'>
|
|
49
|
+
<span class='fraction'>33/34</span>
|
|
50
50
|
</div>
|
|
51
51
|
|
|
52
52
|
|
|
@@ -95,17 +95,17 @@
|
|
|
95
95
|
|
|
96
96
|
<tr>
|
|
97
97
|
<td class="file high" data-value="security.helper.ts"><a href="security.helper.ts.html">security.helper.ts</a></td>
|
|
98
|
-
<td data-value="96.
|
|
98
|
+
<td data-value="96.42" class="pic high">
|
|
99
99
|
<div class="chart"><div class="cover-fill" style="width: 96%"></div><div class="cover-empty" style="width: 4%"></div></div>
|
|
100
100
|
</td>
|
|
101
|
-
<td data-value="96.
|
|
102
|
-
<td data-value="
|
|
101
|
+
<td data-value="96.42" class="pct high">96.42%</td>
|
|
102
|
+
<td data-value="28" class="abs high">27/28</td>
|
|
103
103
|
<td data-value="75" class="pct medium">75%</td>
|
|
104
104
|
<td data-value="4" class="abs medium">3/4</td>
|
|
105
105
|
<td data-value="100" class="pct high">100%</td>
|
|
106
106
|
<td data-value="3" class="abs high">3/3</td>
|
|
107
|
-
<td data-value="96.
|
|
108
|
-
<td data-value="
|
|
107
|
+
<td data-value="96.42" class="pct high">96.42%</td>
|
|
108
|
+
<td data-value="28" class="abs high">27/28</td>
|
|
109
109
|
</tr>
|
|
110
110
|
|
|
111
111
|
</tbody>
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
117
117
|
Code coverage generated by
|
|
118
118
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
119
|
-
at 2025-08-23T06:
|
|
119
|
+
at 2025-08-23T06:47:14.827Z
|
|
120
120
|
</div>
|
|
121
121
|
<script src="prettify.js"></script>
|
|
122
122
|
<script>
|
|
@@ -109,7 +109,7 @@ export function normalize(phoneRaw: string): string {
|
|
|
109
109
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
110
110
|
Code coverage generated by
|
|
111
111
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
112
|
-
at 2025-08-23T06:
|
|
112
|
+
at 2025-08-23T06:47:14.827Z
|
|
113
113
|
</div>
|
|
114
114
|
<script src="prettify.js"></script>
|
|
115
115
|
<script>
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
<div class='clearfix'>
|
|
24
24
|
|
|
25
25
|
<div class='fl pad1y space-right2'>
|
|
26
|
-
<span class="strong">96.
|
|
26
|
+
<span class="strong">96.42% </span>
|
|
27
27
|
<span class="quiet">Statements</span>
|
|
28
|
-
<span class='fraction'>
|
|
28
|
+
<span class='fraction'>27/28</span>
|
|
29
29
|
</div>
|
|
30
30
|
|
|
31
31
|
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
<div class='fl pad1y space-right2'>
|
|
47
|
-
<span class="strong">96.
|
|
47
|
+
<span class="strong">96.42% </span>
|
|
48
48
|
<span class="quiet">Lines</span>
|
|
49
|
-
<span class='fraction'>
|
|
49
|
+
<span class='fraction'>27/28</span>
|
|
50
50
|
</div>
|
|
51
51
|
|
|
52
52
|
|
|
@@ -177,7 +177,7 @@
|
|
|
177
177
|
<span class="cline-any cline-neutral"> </span>
|
|
178
178
|
<span class="cline-any cline-neutral"> </span>
|
|
179
179
|
<span class="cline-any cline-neutral"> </span>
|
|
180
|
-
<span class="cline-any cline-
|
|
180
|
+
<span class="cline-any cline-yes">1x</span>
|
|
181
181
|
<span class="cline-any cline-yes">8x</span>
|
|
182
182
|
<span class="cline-any cline-yes">8x</span>
|
|
183
183
|
<span class="cline-any cline-yes">8x</span>
|
|
@@ -265,7 +265,7 @@ const hmacKey = Buffer.from(hmacKeyB64, "base64");
|
|
|
265
265
|
/** Create an HMAC index for lookups without revealing the number.
|
|
266
266
|
* Store this alongside the encrypted data and index it in the DB.
|
|
267
267
|
*/
|
|
268
|
-
function hmacIndex(phoneRaw: string): string {
|
|
268
|
+
export function hmacIndex(phoneRaw: string): string {
|
|
269
269
|
const normalized = normalize(phoneRaw);
|
|
270
270
|
const h = crypto.createHmac("sha256", hmacKey);
|
|
271
271
|
h.update(normalized, "utf8");
|
|
@@ -275,7 +275,7 @@ function hmacIndex(phoneRaw: string): string {
|
|
|
275
275
|
/** Encrypt a phone number using AES-256-GCM.
|
|
276
276
|
* Returns base64-encoded ciphertext, iv and authTag suitable for storage.
|
|
277
277
|
*/
|
|
278
|
-
export function
|
|
278
|
+
export function phoneEncrypt(
|
|
279
279
|
phoneRaw: string
|
|
280
280
|
): Omit<PhoneCipherRecord, "hmacIndex"> & { hmacIndex: string } {
|
|
281
281
|
const normalized = normalize(phoneRaw);
|
|
@@ -306,7 +306,7 @@ export function encrypt(
|
|
|
306
306
|
/** Decrypt a previously stored record. Throws if tampered.
|
|
307
307
|
* Returns the normalized phone string.
|
|
308
308
|
*/
|
|
309
|
-
export function
|
|
309
|
+
export function phoneDecrypt(
|
|
310
310
|
record: Pick<PhoneCipherRecord, "ciphertext" | "iv" | "authTag">
|
|
311
311
|
): string {
|
|
312
312
|
const iv = Buffer.from(record.iv, "base64");
|
|
@@ -334,7 +334,7 @@ export function decrypt(
|
|
|
334
334
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
335
335
|
Code coverage generated by
|
|
336
336
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
337
|
-
at 2025-08-23T06:
|
|
337
|
+
at 2025-08-23T06:47:14.827Z
|
|
338
338
|
</div>
|
|
339
339
|
<script src="prettify.js"></script>
|
|
340
340
|
<script>
|
package/coverage/lcov.info
CHANGED
|
@@ -22,13 +22,13 @@ end_of_record
|
|
|
22
22
|
TN:
|
|
23
23
|
SF:src/security.helper.ts
|
|
24
24
|
FN:27,hmacIndex
|
|
25
|
-
FN:37,
|
|
26
|
-
FN:68,
|
|
25
|
+
FN:37,phoneEncrypt
|
|
26
|
+
FN:68,phoneDecrypt
|
|
27
27
|
FNF:3
|
|
28
28
|
FNH:3
|
|
29
29
|
FNDA:8,hmacIndex
|
|
30
|
-
FNDA:8,
|
|
31
|
-
FNDA:6,
|
|
30
|
+
FNDA:8,phoneEncrypt
|
|
31
|
+
FNDA:6,phoneDecrypt
|
|
32
32
|
DA:1,1
|
|
33
33
|
DA:2,1
|
|
34
34
|
DA:12,1
|
|
@@ -37,6 +37,7 @@ DA:15,1
|
|
|
37
37
|
DA:16,0
|
|
38
38
|
DA:21,1
|
|
39
39
|
DA:22,1
|
|
40
|
+
DA:27,1
|
|
40
41
|
DA:28,8
|
|
41
42
|
DA:29,8
|
|
42
43
|
DA:30,8
|
|
@@ -56,8 +57,8 @@ DA:75,6
|
|
|
56
57
|
DA:80,6
|
|
57
58
|
DA:82,5
|
|
58
59
|
DA:87,4
|
|
59
|
-
LF:
|
|
60
|
-
LH:
|
|
60
|
+
LF:28
|
|
61
|
+
LH:27
|
|
61
62
|
BRDA:15,0,0,0
|
|
62
63
|
BRDA:15,0,1,1
|
|
63
64
|
BRDA:15,1,0,1
|
|
@@ -109,7 +109,7 @@ export function normalize(phoneRaw: string): string {
|
|
|
109
109
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
110
110
|
Code coverage generated by
|
|
111
111
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
112
|
-
at 2025-08-23T06:
|
|
112
|
+
at 2025-08-23T06:47:14.841Z
|
|
113
113
|
</div>
|
|
114
114
|
<script src="prettify.js"></script>
|
|
115
115
|
<script>
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
<div class='clearfix'>
|
|
24
24
|
|
|
25
25
|
<div class='fl pad1y space-right2'>
|
|
26
|
-
<span class="strong">96.
|
|
26
|
+
<span class="strong">96.42% </span>
|
|
27
27
|
<span class="quiet">Statements</span>
|
|
28
|
-
<span class='fraction'>
|
|
28
|
+
<span class='fraction'>27/28</span>
|
|
29
29
|
</div>
|
|
30
30
|
|
|
31
31
|
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
<div class='fl pad1y space-right2'>
|
|
47
|
-
<span class="strong">96.
|
|
47
|
+
<span class="strong">96.42% </span>
|
|
48
48
|
<span class="quiet">Lines</span>
|
|
49
|
-
<span class='fraction'>
|
|
49
|
+
<span class='fraction'>27/28</span>
|
|
50
50
|
</div>
|
|
51
51
|
|
|
52
52
|
|
|
@@ -177,7 +177,7 @@
|
|
|
177
177
|
<span class="cline-any cline-neutral"> </span>
|
|
178
178
|
<span class="cline-any cline-neutral"> </span>
|
|
179
179
|
<span class="cline-any cline-neutral"> </span>
|
|
180
|
-
<span class="cline-any cline-
|
|
180
|
+
<span class="cline-any cline-yes">1x</span>
|
|
181
181
|
<span class="cline-any cline-yes">8x</span>
|
|
182
182
|
<span class="cline-any cline-yes">8x</span>
|
|
183
183
|
<span class="cline-any cline-yes">8x</span>
|
|
@@ -265,7 +265,7 @@ const hmacKey = Buffer.from(hmacKeyB64, "base64");
|
|
|
265
265
|
/** Create an HMAC index for lookups without revealing the number.
|
|
266
266
|
* Store this alongside the encrypted data and index it in the DB.
|
|
267
267
|
*/
|
|
268
|
-
function hmacIndex(phoneRaw: string): string {
|
|
268
|
+
export function hmacIndex(phoneRaw: string): string {
|
|
269
269
|
const normalized = normalize(phoneRaw);
|
|
270
270
|
const h = crypto.createHmac("sha256", hmacKey);
|
|
271
271
|
h.update(normalized, "utf8");
|
|
@@ -275,7 +275,7 @@ function hmacIndex(phoneRaw: string): string {
|
|
|
275
275
|
/** Encrypt a phone number using AES-256-GCM.
|
|
276
276
|
* Returns base64-encoded ciphertext, iv and authTag suitable for storage.
|
|
277
277
|
*/
|
|
278
|
-
export function
|
|
278
|
+
export function phoneEncrypt(
|
|
279
279
|
phoneRaw: string
|
|
280
280
|
): Omit<PhoneCipherRecord, "hmacIndex"> & { hmacIndex: string } {
|
|
281
281
|
const normalized = normalize(phoneRaw);
|
|
@@ -306,7 +306,7 @@ export function encrypt(
|
|
|
306
306
|
/** Decrypt a previously stored record. Throws if tampered.
|
|
307
307
|
* Returns the normalized phone string.
|
|
308
308
|
*/
|
|
309
|
-
export function
|
|
309
|
+
export function phoneDecrypt(
|
|
310
310
|
record: Pick<PhoneCipherRecord, "ciphertext" | "iv" | "authTag">
|
|
311
311
|
): string {
|
|
312
312
|
const iv = Buffer.from(record.iv, "base64");
|
|
@@ -334,7 +334,7 @@ export function decrypt(
|
|
|
334
334
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
335
335
|
Code coverage generated by
|
|
336
336
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
337
|
-
at 2025-08-23T06:
|
|
337
|
+
at 2025-08-23T06:47:14.841Z
|
|
338
338
|
</div>
|
|
339
339
|
<script src="prettify.js"></script>
|
|
340
340
|
<script>
|
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,8 +33,9 @@ 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"));
|
|
39
40
|
const phone_number_helper_1 = require("./phone.number.helper");
|
|
40
41
|
const aesKeyB64 = process.env.PHONE_AES_KEY;
|
|
@@ -56,7 +57,7 @@ function hmacIndex(phoneRaw) {
|
|
|
56
57
|
/** Encrypt a phone number using AES-256-GCM.
|
|
57
58
|
* Returns base64-encoded ciphertext, iv and authTag suitable for storage.
|
|
58
59
|
*/
|
|
59
|
-
function
|
|
60
|
+
function phoneEncrypt(phoneRaw) {
|
|
60
61
|
const normalized = (0, phone_number_helper_1.normalize)(phoneRaw);
|
|
61
62
|
// 12-byte IV is recommended for GCM
|
|
62
63
|
const iv = crypto.randomBytes(12);
|
|
@@ -78,7 +79,7 @@ function encrypt(phoneRaw) {
|
|
|
78
79
|
/** Decrypt a previously stored record. Throws if tampered.
|
|
79
80
|
* Returns the normalized phone string.
|
|
80
81
|
*/
|
|
81
|
-
function
|
|
82
|
+
function phoneDecrypt(record) {
|
|
82
83
|
const iv = Buffer.from(record.iv, "base64");
|
|
83
84
|
const authTag = Buffer.from(record.authTag, "base64");
|
|
84
85
|
const ciphertext = Buffer.from(record.ciphertext, "base64");
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "core-mb",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
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",
|
|
@@ -28,5 +28,8 @@
|
|
|
28
28
|
"jest-html-reporter": "^4.3.0",
|
|
29
29
|
"ts-jest": "^29.4.1",
|
|
30
30
|
"typescript": "^5.9.2"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"class-validator": "^0.14.2"
|
|
31
34
|
}
|
|
32
35
|
}
|
package/src/date.helper.ts
CHANGED
package/src/index.ts
CHANGED
package/src/security.helper.ts
CHANGED
|
@@ -24,7 +24,7 @@ const hmacKey = Buffer.from(hmacKeyB64, "base64");
|
|
|
24
24
|
/** Create an HMAC index for lookups without revealing the number.
|
|
25
25
|
* Store this alongside the encrypted data and index it in the DB.
|
|
26
26
|
*/
|
|
27
|
-
function hmacIndex(phoneRaw: string): string {
|
|
27
|
+
export function hmacIndex(phoneRaw: string): string {
|
|
28
28
|
const normalized = normalize(phoneRaw);
|
|
29
29
|
const h = crypto.createHmac("sha256", hmacKey);
|
|
30
30
|
h.update(normalized, "utf8");
|
|
@@ -34,7 +34,7 @@ function hmacIndex(phoneRaw: string): string {
|
|
|
34
34
|
/** Encrypt a phone number using AES-256-GCM.
|
|
35
35
|
* Returns base64-encoded ciphertext, iv and authTag suitable for storage.
|
|
36
36
|
*/
|
|
37
|
-
export function
|
|
37
|
+
export function phoneEncrypt(
|
|
38
38
|
phoneRaw: string
|
|
39
39
|
): Omit<PhoneCipherRecord, "hmacIndex"> & { hmacIndex: string } {
|
|
40
40
|
const normalized = normalize(phoneRaw);
|
|
@@ -65,7 +65,7 @@ export function encrypt(
|
|
|
65
65
|
/** Decrypt a previously stored record. Throws if tampered.
|
|
66
66
|
* Returns the normalized phone string.
|
|
67
67
|
*/
|
|
68
|
-
export function
|
|
68
|
+
export function phoneDecrypt(
|
|
69
69
|
record: Pick<PhoneCipherRecord, "ciphertext" | "iv" | "authTag">
|
|
70
70
|
): string {
|
|
71
71
|
const iv = Buffer.from(record.iv, "base64");
|
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 13:
|
|
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 13:47:11</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">1.364s</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.004s</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">0.001s</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">1.87s</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.008s</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.004s</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.002s</div></div></div></div></details></main></body></html>
|
package/src/aes.helper.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import * as crypto from "crypto";
|
|
2
|
-
|
|
3
|
-
export type PhoneCipherRecord = {
|
|
4
|
-
// Base64 strings suitable for storage in text columns
|
|
5
|
-
ciphertext: string; // Encrypted phone number
|
|
6
|
-
iv: string; // Initialization Vector (nonce) for GCM (12 bytes recommended)
|
|
7
|
-
authTag: string; // Authentication tag returned by GCM (ensures integrity)
|
|
8
|
-
hmacIndex: string; // HMAC-SHA256(phoneNormalized) for search/dedup
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const aesKeyB64 = process.env.PHONE_AES_KEY;
|
|
12
|
-
const hmacKeyB64 = process.env.PHONE_HMAC_KEY;
|
|
13
|
-
|
|
14
|
-
if (!aesKeyB64 || !hmacKeyB64) {
|
|
15
|
-
throw new Error(
|
|
16
|
-
"PHONE_AES_KEY and PHONE_HMAC_KEY must be set (base64-encoded)."
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const aesKey = Buffer.from(aesKeyB64, "base64");
|
|
21
|
-
const hmacKey = Buffer.from(hmacKeyB64, "base64");
|
|
22
|
-
|
|
23
|
-
/** Normalize to a consistent representation (approx. E.164-like):
|
|
24
|
-
* - Keep leading '+' if present
|
|
25
|
-
* - Remove spaces, dashes, parentheses
|
|
26
|
-
* - Remove any non-digit characters except leading '+'
|
|
27
|
-
* - You can adapt to your locale/validation as needed
|
|
28
|
-
*/
|
|
29
|
-
function normalize(phoneRaw: string): string {
|
|
30
|
-
if (!phoneRaw) return "";
|
|
31
|
-
const trimmed = phoneRaw.trim();
|
|
32
|
-
const hasPlus = trimmed.startsWith("+");
|
|
33
|
-
const digitsOnly = trimmed.replace(/[^\d]/g, "");
|
|
34
|
-
return hasPlus ? `+${digitsOnly}` : digitsOnly; // store either "+855123..." or "0123..."
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Create an HMAC index for lookups without revealing the number.
|
|
38
|
-
* Store this alongside the encrypted data and index it in the DB.
|
|
39
|
-
*/
|
|
40
|
-
function hmacIndex(phoneRaw: string): string {
|
|
41
|
-
const normalized = normalize(phoneRaw);
|
|
42
|
-
const h = crypto.createHmac("sha256", hmacKey);
|
|
43
|
-
h.update(normalized, "utf8");
|
|
44
|
-
return h.digest("base64");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Encrypt a phone number using AES-256-GCM.
|
|
48
|
-
* Returns base64-encoded ciphertext, iv and authTag suitable for storage.
|
|
49
|
-
*/
|
|
50
|
-
function encrypt(
|
|
51
|
-
phoneRaw: string
|
|
52
|
-
): Omit<PhoneCipherRecord, "hmacIndex"> & { hmacIndex: string } {
|
|
53
|
-
const normalized = normalize(phoneRaw);
|
|
54
|
-
|
|
55
|
-
// 12-byte IV is recommended for GCM
|
|
56
|
-
const iv = crypto.randomBytes(12);
|
|
57
|
-
|
|
58
|
-
const cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv);
|
|
59
|
-
|
|
60
|
-
// Optional: bind additional data (AAD), e.g., tenant ID to prevent cross-tenant swaps
|
|
61
|
-
// cipher.setAAD(Buffer.from(tenantId, 'utf8'));
|
|
62
|
-
|
|
63
|
-
const ciphertextBuf = Buffer.concat([
|
|
64
|
-
cipher.update(normalized, "utf8"),
|
|
65
|
-
cipher.final(),
|
|
66
|
-
]);
|
|
67
|
-
|
|
68
|
-
const authTag = cipher.getAuthTag();
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
ciphertext: ciphertextBuf.toString("base64"),
|
|
72
|
-
iv: iv.toString("base64"),
|
|
73
|
-
authTag: authTag.toString("base64"),
|
|
74
|
-
hmacIndex: hmacIndex(normalized),
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Decrypt a previously stored record. Throws if tampered.
|
|
79
|
-
* Returns the normalized phone string.
|
|
80
|
-
*/
|
|
81
|
-
function decrypt(
|
|
82
|
-
record: Pick<PhoneCipherRecord, "ciphertext" | "iv" | "authTag">
|
|
83
|
-
): string {
|
|
84
|
-
const iv = Buffer.from(record.iv, "base64");
|
|
85
|
-
const authTag = Buffer.from(record.authTag, "base64");
|
|
86
|
-
const ciphertext = Buffer.from(record.ciphertext, "base64");
|
|
87
|
-
|
|
88
|
-
const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv);
|
|
89
|
-
|
|
90
|
-
// If you used setAAD on encrypt, you MUST set the same AAD here
|
|
91
|
-
// decipher.setAAD(Buffer.from(tenantId, 'utf8'));
|
|
92
|
-
|
|
93
|
-
decipher.setAuthTag(authTag);
|
|
94
|
-
|
|
95
|
-
const plaintext = Buffer.concat([
|
|
96
|
-
decipher.update(ciphertext),
|
|
97
|
-
decipher.final(),
|
|
98
|
-
]).toString("utf8");
|
|
99
|
-
|
|
100
|
-
return plaintext; // normalized phone
|
|
101
|
-
}
|