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.
@@ -1,6 +1,6 @@
1
1
  // phone.number.crypto.spec.ts
2
2
  import * as crypto from "crypto";
3
- import { encrypt, decrypt } from "../src/security.helper";
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 = encrypt(phone);
20
+ const encrypted = phoneEncrypt(phone);
21
21
 
22
22
  // decrypt should return normalized phone
23
- const decrypted = decrypt(encrypted);
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 = encrypt(phone);
39
- const e2 = encrypt(phone);
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 = encrypt("+85512345678");
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(() => decrypt(tampered)).toThrow();
57
+ expect(() => phoneDecrypt(tampered)).toThrow();
58
58
  });
59
59
 
60
60
  it("should throw when tampering with authTag", () => {
61
- const encrypted = encrypt("+85512345678");
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(() => decrypt(tampered)).toThrow();
67
+ expect(() => phoneDecrypt(tampered)).toThrow();
68
68
  });
69
69
  });
@@ -23,9 +23,9 @@
23
23
  <div class='clearfix'>
24
24
 
25
25
  <div class='fl pad1y space-right2'>
26
- <span class="strong">97.05% </span>
26
+ <span class="strong">97.14% </span>
27
27
  <span class="quiet">Statements</span>
28
- <span class='fraction'>33/34</span>
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">96.96% </span>
47
+ <span class="strong">97.05% </span>
48
48
  <span class="quiet">Lines</span>
49
- <span class='fraction'>32/33</span>
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.29" class="pic high">
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.29" class="pct high">96.29%</td>
102
- <td data-value="27" class="abs high">26/27</td>
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.29" class="pct high">96.29%</td>
108
- <td data-value="27" class="abs high">26/27</td>
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:30:01.192Z
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.05% </span>
26
+ <span class="strong">97.14% </span>
27
27
  <span class="quiet">Statements</span>
28
- <span class='fraction'>33/34</span>
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">96.96% </span>
47
+ <span class="strong">97.05% </span>
48
48
  <span class="quiet">Lines</span>
49
- <span class='fraction'>32/33</span>
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.29" class="pic high">
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.29" class="pct high">96.29%</td>
102
- <td data-value="27" class="abs high">26/27</td>
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.29" class="pct high">96.29%</td>
108
- <td data-value="27" class="abs high">26/27</td>
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:30:01.178Z
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:30:01.178Z
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.29% </span>
26
+ <span class="strong">96.42% </span>
27
27
  <span class="quiet">Statements</span>
28
- <span class='fraction'>26/27</span>
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.29% </span>
47
+ <span class="strong">96.42% </span>
48
48
  <span class="quiet">Lines</span>
49
- <span class='fraction'>26/27</span>
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">&nbsp;</span>
178
178
  <span class="cline-any cline-neutral">&nbsp;</span>
179
179
  <span class="cline-any cline-neutral">&nbsp;</span>
180
- <span class="cline-any cline-neutral">&nbsp;</span>
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 encrypt(
278
+ export function phoneEncrypt(
279
279
  phoneRaw: string
280
280
  ): Omit&lt;PhoneCipherRecord, "hmacIndex"&gt; &amp; { 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 decrypt(
309
+ export function phoneDecrypt(
310
310
  record: Pick&lt;PhoneCipherRecord, "ciphertext" | "iv" | "authTag"&gt;
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:30:01.178Z
337
+ at 2025-08-23T06:47:14.827Z
338
338
  </div>
339
339
  <script src="prettify.js"></script>
340
340
  <script>
@@ -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,encrypt
26
- FN:68,decrypt
25
+ FN:37,phoneEncrypt
26
+ FN:68,phoneDecrypt
27
27
  FNF:3
28
28
  FNH:3
29
29
  FNDA:8,hmacIndex
30
- FNDA:8,encrypt
31
- FNDA:6,decrypt
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:27
60
- LH:26
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:30:01.192Z
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.29% </span>
26
+ <span class="strong">96.42% </span>
27
27
  <span class="quiet">Statements</span>
28
- <span class='fraction'>26/27</span>
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.29% </span>
47
+ <span class="strong">96.42% </span>
48
48
  <span class="quiet">Lines</span>
49
- <span class='fraction'>26/27</span>
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">&nbsp;</span>
178
178
  <span class="cline-any cline-neutral">&nbsp;</span>
179
179
  <span class="cline-any cline-neutral">&nbsp;</span>
180
- <span class="cline-any cline-neutral">&nbsp;</span>
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 encrypt(
278
+ export function phoneEncrypt(
279
279
  phoneRaw: string
280
280
  ): Omit&lt;PhoneCipherRecord, "hmacIndex"&gt; &amp; { 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 decrypt(
309
+ export function phoneDecrypt(
310
310
  record: Pick&lt;PhoneCipherRecord, "ciphertext" | "iv" | "authTag"&gt;
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:30:01.192Z
337
+ at 2025-08-23T06:47:14.841Z
338
338
  </div>
339
339
  <script src="prettify.js"></script>
340
340
  <script>
@@ -0,0 +1 @@
1
+ export declare function sample(): void;
@@ -1 +1,5 @@
1
1
  "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sample = sample;
4
+ function sample() {
5
+ }
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
+ export * from "./date.helper";
1
2
  export * from "./security.helper";
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 encrypt(phoneRaw: string): Omit<PhoneCipherRecord, "hmacIndex"> & {
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 decrypt(record: Pick<PhoneCipherRecord, "ciphertext" | "iv" | "authTag">): string;
20
+ export declare function phoneDecrypt(record: Pick<PhoneCipherRecord, "ciphertext" | "iv" | "authTag">): string;
@@ -33,8 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.encrypt = encrypt;
37
- exports.decrypt = decrypt;
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 encrypt(phoneRaw) {
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 decrypt(record) {
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.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
  }
@@ -0,0 +1,3 @@
1
+ export function sample() {
2
+
3
+ }
package/src/index.ts CHANGED
@@ -1 +1,2 @@
1
+ export * from "./date.helper"
1
2
  export * from "./security.helper"
@@ -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 encrypt(
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 decrypt(
68
+ export function phoneDecrypt(
69
69
  record: Pick<PhoneCipherRecord, "ciphertext" | "iv" | "authTag">
70
70
  ): string {
71
71
  const iv = Buffer.from(record.iv, "base64");
@@ -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:29:58</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.441s</div></summary><div class="suite-tests"><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.helper &gt; 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.005s</div></div></div><div class="test-result passed"><div class="test-info"><div class="test-suitename">phone.number.helper &gt; 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 &gt; 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 &gt; 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 &gt; normalize</div><div class="test-title">should not duplicate + signs</div><div class="test-status">passed</div><div class="test-duration"> </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.469s</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.015s</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>
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 &gt; 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 &gt; 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 &gt; 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 &gt; 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 &gt; 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
- }