core-mb 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +2 -0
- package/README.md +9 -0
- package/__unitest__/phone.number.crypto.spec.ts +69 -0
- package/__unitest__/phone.number.helper.spec.ts +30 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +131 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +131 -0
- package/coverage/lcov-report/phone.number.helper.ts.html +124 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/security.helper.ts.html +349 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov.info +67 -0
- package/coverage/phone.number.helper.ts.html +124 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/security.helper.ts.html +349 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/dist/aes.helper.d.ts +6 -0
- package/dist/aes.helper.js +105 -0
- package/dist/date.helper.d.ts +0 -0
- package/dist/date.helper.js +1 -0
- package/dist/document.helper.d.ts +0 -0
- package/dist/document.helper.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +17 -0
- package/dist/logging.helper.d.ts +0 -0
- package/dist/logging.helper.js +1 -0
- package/dist/money.helper.d.ts +0 -0
- package/dist/money.helper.js +1 -0
- package/dist/phone.number.helper.d.ts +7 -0
- package/dist/phone.number.helper.js +17 -0
- package/dist/security.helper.d.ts +16 -0
- package/dist/security.helper.js +94 -0
- package/dist/validation.helper.d.ts +0 -0
- package/dist/validation.helper.js +1 -0
- package/jest.config.js +17 -0
- package/jest.setup.ts +2 -0
- package/package.json +32 -0
- package/src/aes.helper.ts +101 -0
- package/src/date.helper.ts +0 -0
- package/src/document.helper.ts +0 -0
- package/src/index.ts +1 -0
- package/src/logging.helper.ts +0 -0
- package/src/money.helper.ts +0 -0
- package/src/phone.number.helper.ts +13 -0
- package/src/security.helper.ts +88 -0
- package/src/validation.helper.ts +0 -0
- package/test-report.html +277 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
var addSorting = (function() {
|
|
3
|
+
'use strict';
|
|
4
|
+
var cols,
|
|
5
|
+
currentSort = {
|
|
6
|
+
index: 0,
|
|
7
|
+
desc: false
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// returns the summary table element
|
|
11
|
+
function getTable() {
|
|
12
|
+
return document.querySelector('.coverage-summary');
|
|
13
|
+
}
|
|
14
|
+
// returns the thead element of the summary table
|
|
15
|
+
function getTableHeader() {
|
|
16
|
+
return getTable().querySelector('thead tr');
|
|
17
|
+
}
|
|
18
|
+
// returns the tbody element of the summary table
|
|
19
|
+
function getTableBody() {
|
|
20
|
+
return getTable().querySelector('tbody');
|
|
21
|
+
}
|
|
22
|
+
// returns the th element for nth column
|
|
23
|
+
function getNthColumn(n) {
|
|
24
|
+
return getTableHeader().querySelectorAll('th')[n];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function onFilterInput() {
|
|
28
|
+
const searchValue = document.getElementById('fileSearch').value;
|
|
29
|
+
const rows = document.getElementsByTagName('tbody')[0].children;
|
|
30
|
+
|
|
31
|
+
// Try to create a RegExp from the searchValue. If it fails (invalid regex),
|
|
32
|
+
// it will be treated as a plain text search
|
|
33
|
+
let searchRegex;
|
|
34
|
+
try {
|
|
35
|
+
searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive
|
|
36
|
+
} catch (error) {
|
|
37
|
+
searchRegex = null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (let i = 0; i < rows.length; i++) {
|
|
41
|
+
const row = rows[i];
|
|
42
|
+
let isMatch = false;
|
|
43
|
+
|
|
44
|
+
if (searchRegex) {
|
|
45
|
+
// If a valid regex was created, use it for matching
|
|
46
|
+
isMatch = searchRegex.test(row.textContent);
|
|
47
|
+
} else {
|
|
48
|
+
// Otherwise, fall back to the original plain text search
|
|
49
|
+
isMatch = row.textContent
|
|
50
|
+
.toLowerCase()
|
|
51
|
+
.includes(searchValue.toLowerCase());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
row.style.display = isMatch ? '' : 'none';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// loads the search box
|
|
59
|
+
function addSearchBox() {
|
|
60
|
+
var template = document.getElementById('filterTemplate');
|
|
61
|
+
var templateClone = template.content.cloneNode(true);
|
|
62
|
+
templateClone.getElementById('fileSearch').oninput = onFilterInput;
|
|
63
|
+
template.parentElement.appendChild(templateClone);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// loads all columns
|
|
67
|
+
function loadColumns() {
|
|
68
|
+
var colNodes = getTableHeader().querySelectorAll('th'),
|
|
69
|
+
colNode,
|
|
70
|
+
cols = [],
|
|
71
|
+
col,
|
|
72
|
+
i;
|
|
73
|
+
|
|
74
|
+
for (i = 0; i < colNodes.length; i += 1) {
|
|
75
|
+
colNode = colNodes[i];
|
|
76
|
+
col = {
|
|
77
|
+
key: colNode.getAttribute('data-col'),
|
|
78
|
+
sortable: !colNode.getAttribute('data-nosort'),
|
|
79
|
+
type: colNode.getAttribute('data-type') || 'string'
|
|
80
|
+
};
|
|
81
|
+
cols.push(col);
|
|
82
|
+
if (col.sortable) {
|
|
83
|
+
col.defaultDescSort = col.type === 'number';
|
|
84
|
+
colNode.innerHTML =
|
|
85
|
+
colNode.innerHTML + '<span class="sorter"></span>';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return cols;
|
|
89
|
+
}
|
|
90
|
+
// attaches a data attribute to every tr element with an object
|
|
91
|
+
// of data values keyed by column name
|
|
92
|
+
function loadRowData(tableRow) {
|
|
93
|
+
var tableCols = tableRow.querySelectorAll('td'),
|
|
94
|
+
colNode,
|
|
95
|
+
col,
|
|
96
|
+
data = {},
|
|
97
|
+
i,
|
|
98
|
+
val;
|
|
99
|
+
for (i = 0; i < tableCols.length; i += 1) {
|
|
100
|
+
colNode = tableCols[i];
|
|
101
|
+
col = cols[i];
|
|
102
|
+
val = colNode.getAttribute('data-value');
|
|
103
|
+
if (col.type === 'number') {
|
|
104
|
+
val = Number(val);
|
|
105
|
+
}
|
|
106
|
+
data[col.key] = val;
|
|
107
|
+
}
|
|
108
|
+
return data;
|
|
109
|
+
}
|
|
110
|
+
// loads all row data
|
|
111
|
+
function loadData() {
|
|
112
|
+
var rows = getTableBody().querySelectorAll('tr'),
|
|
113
|
+
i;
|
|
114
|
+
|
|
115
|
+
for (i = 0; i < rows.length; i += 1) {
|
|
116
|
+
rows[i].data = loadRowData(rows[i]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// sorts the table using the data for the ith column
|
|
120
|
+
function sortByIndex(index, desc) {
|
|
121
|
+
var key = cols[index].key,
|
|
122
|
+
sorter = function(a, b) {
|
|
123
|
+
a = a.data[key];
|
|
124
|
+
b = b.data[key];
|
|
125
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
126
|
+
},
|
|
127
|
+
finalSorter = sorter,
|
|
128
|
+
tableBody = document.querySelector('.coverage-summary tbody'),
|
|
129
|
+
rowNodes = tableBody.querySelectorAll('tr'),
|
|
130
|
+
rows = [],
|
|
131
|
+
i;
|
|
132
|
+
|
|
133
|
+
if (desc) {
|
|
134
|
+
finalSorter = function(a, b) {
|
|
135
|
+
return -1 * sorter(a, b);
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (i = 0; i < rowNodes.length; i += 1) {
|
|
140
|
+
rows.push(rowNodes[i]);
|
|
141
|
+
tableBody.removeChild(rowNodes[i]);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
rows.sort(finalSorter);
|
|
145
|
+
|
|
146
|
+
for (i = 0; i < rows.length; i += 1) {
|
|
147
|
+
tableBody.appendChild(rows[i]);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// removes sort indicators for current column being sorted
|
|
151
|
+
function removeSortIndicators() {
|
|
152
|
+
var col = getNthColumn(currentSort.index),
|
|
153
|
+
cls = col.className;
|
|
154
|
+
|
|
155
|
+
cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
|
|
156
|
+
col.className = cls;
|
|
157
|
+
}
|
|
158
|
+
// adds sort indicators for current column being sorted
|
|
159
|
+
function addSortIndicators() {
|
|
160
|
+
getNthColumn(currentSort.index).className += currentSort.desc
|
|
161
|
+
? ' sorted-desc'
|
|
162
|
+
: ' sorted';
|
|
163
|
+
}
|
|
164
|
+
// adds event listeners for all sorter widgets
|
|
165
|
+
function enableUI() {
|
|
166
|
+
var i,
|
|
167
|
+
el,
|
|
168
|
+
ithSorter = function ithSorter(i) {
|
|
169
|
+
var col = cols[i];
|
|
170
|
+
|
|
171
|
+
return function() {
|
|
172
|
+
var desc = col.defaultDescSort;
|
|
173
|
+
|
|
174
|
+
if (currentSort.index === i) {
|
|
175
|
+
desc = !currentSort.desc;
|
|
176
|
+
}
|
|
177
|
+
sortByIndex(i, desc);
|
|
178
|
+
removeSortIndicators();
|
|
179
|
+
currentSort.index = i;
|
|
180
|
+
currentSort.desc = desc;
|
|
181
|
+
addSortIndicators();
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
for (i = 0; i < cols.length; i += 1) {
|
|
185
|
+
if (cols[i].sortable) {
|
|
186
|
+
// add the click event handler on the th so users
|
|
187
|
+
// dont have to click on those tiny arrows
|
|
188
|
+
el = getNthColumn(i).querySelector('.sorter').parentElement;
|
|
189
|
+
if (el.addEventListener) {
|
|
190
|
+
el.addEventListener('click', ithSorter(i));
|
|
191
|
+
} else {
|
|
192
|
+
el.attachEvent('onclick', ithSorter(i));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// adds sorting functionality to the UI
|
|
198
|
+
return function() {
|
|
199
|
+
if (!getTable()) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
cols = loadColumns();
|
|
203
|
+
loadData();
|
|
204
|
+
addSearchBox();
|
|
205
|
+
addSortIndicators();
|
|
206
|
+
enableUI();
|
|
207
|
+
};
|
|
208
|
+
})();
|
|
209
|
+
|
|
210
|
+
window.addEventListener('load', addSorting);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const crypto = __importStar(require("crypto"));
|
|
37
|
+
const aesKeyB64 = process.env.PHONE_AES_KEY;
|
|
38
|
+
const hmacKeyB64 = process.env.PHONE_HMAC_KEY;
|
|
39
|
+
if (!aesKeyB64 || !hmacKeyB64) {
|
|
40
|
+
throw new Error("PHONE_AES_KEY and PHONE_HMAC_KEY must be set (base64-encoded).");
|
|
41
|
+
}
|
|
42
|
+
const aesKey = Buffer.from(aesKeyB64, "base64");
|
|
43
|
+
const hmacKey = Buffer.from(hmacKeyB64, "base64");
|
|
44
|
+
/** Normalize to a consistent representation (approx. E.164-like):
|
|
45
|
+
* - Keep leading '+' if present
|
|
46
|
+
* - Remove spaces, dashes, parentheses
|
|
47
|
+
* - Remove any non-digit characters except leading '+'
|
|
48
|
+
* - You can adapt to your locale/validation as needed
|
|
49
|
+
*/
|
|
50
|
+
function normalize(phoneRaw) {
|
|
51
|
+
if (!phoneRaw)
|
|
52
|
+
return "";
|
|
53
|
+
const trimmed = phoneRaw.trim();
|
|
54
|
+
const hasPlus = trimmed.startsWith("+");
|
|
55
|
+
const digitsOnly = trimmed.replace(/[^\d]/g, "");
|
|
56
|
+
return hasPlus ? `+${digitsOnly}` : digitsOnly; // store either "+855123..." or "0123..."
|
|
57
|
+
}
|
|
58
|
+
/** Create an HMAC index for lookups without revealing the number.
|
|
59
|
+
* Store this alongside the encrypted data and index it in the DB.
|
|
60
|
+
*/
|
|
61
|
+
function hmacIndex(phoneRaw) {
|
|
62
|
+
const normalized = normalize(phoneRaw);
|
|
63
|
+
const h = crypto.createHmac("sha256", hmacKey);
|
|
64
|
+
h.update(normalized, "utf8");
|
|
65
|
+
return h.digest("base64");
|
|
66
|
+
}
|
|
67
|
+
/** Encrypt a phone number using AES-256-GCM.
|
|
68
|
+
* Returns base64-encoded ciphertext, iv and authTag suitable for storage.
|
|
69
|
+
*/
|
|
70
|
+
function encrypt(phoneRaw) {
|
|
71
|
+
const normalized = normalize(phoneRaw);
|
|
72
|
+
// 12-byte IV is recommended for GCM
|
|
73
|
+
const iv = crypto.randomBytes(12);
|
|
74
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv);
|
|
75
|
+
// Optional: bind additional data (AAD), e.g., tenant ID to prevent cross-tenant swaps
|
|
76
|
+
// cipher.setAAD(Buffer.from(tenantId, 'utf8'));
|
|
77
|
+
const ciphertextBuf = Buffer.concat([
|
|
78
|
+
cipher.update(normalized, "utf8"),
|
|
79
|
+
cipher.final(),
|
|
80
|
+
]);
|
|
81
|
+
const authTag = cipher.getAuthTag();
|
|
82
|
+
return {
|
|
83
|
+
ciphertext: ciphertextBuf.toString("base64"),
|
|
84
|
+
iv: iv.toString("base64"),
|
|
85
|
+
authTag: authTag.toString("base64"),
|
|
86
|
+
hmacIndex: hmacIndex(normalized),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Decrypt a previously stored record. Throws if tampered.
|
|
90
|
+
* Returns the normalized phone string.
|
|
91
|
+
*/
|
|
92
|
+
function decrypt(record) {
|
|
93
|
+
const iv = Buffer.from(record.iv, "base64");
|
|
94
|
+
const authTag = Buffer.from(record.authTag, "base64");
|
|
95
|
+
const ciphertext = Buffer.from(record.ciphertext, "base64");
|
|
96
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv);
|
|
97
|
+
// If you used setAAD on encrypt, you MUST set the same AAD here
|
|
98
|
+
// decipher.setAAD(Buffer.from(tenantId, 'utf8'));
|
|
99
|
+
decipher.setAuthTag(authTag);
|
|
100
|
+
const plaintext = Buffer.concat([
|
|
101
|
+
decipher.update(ciphertext),
|
|
102
|
+
decipher.final(),
|
|
103
|
+
]).toString("utf8");
|
|
104
|
+
return plaintext; // normalized phone
|
|
105
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./security.helper";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./security.helper"), exports);
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Normalize to a consistent representation (approx. E.164-like):
|
|
2
|
+
* - Keep leading '+' if present
|
|
3
|
+
* - Remove spaces, dashes, parentheses
|
|
4
|
+
* - Remove any non-digit characters except leading '+'
|
|
5
|
+
* - You can adapt to your locale/validation as needed
|
|
6
|
+
*/
|
|
7
|
+
export declare function normalize(phoneRaw: string): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalize = normalize;
|
|
4
|
+
/** Normalize to a consistent representation (approx. E.164-like):
|
|
5
|
+
* - Keep leading '+' if present
|
|
6
|
+
* - Remove spaces, dashes, parentheses
|
|
7
|
+
* - Remove any non-digit characters except leading '+'
|
|
8
|
+
* - You can adapt to your locale/validation as needed
|
|
9
|
+
*/
|
|
10
|
+
function normalize(phoneRaw) {
|
|
11
|
+
if (!phoneRaw)
|
|
12
|
+
return "";
|
|
13
|
+
const trimmed = phoneRaw.trim();
|
|
14
|
+
const hasPlus = trimmed.startsWith("+");
|
|
15
|
+
const digitsOnly = trimmed.replace(/[^\d]/g, "");
|
|
16
|
+
return hasPlus ? `+${digitsOnly}` : digitsOnly; // store either "+855123..." or "0123..."
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type PhoneCipherRecord = {
|
|
2
|
+
ciphertext: string;
|
|
3
|
+
iv: string;
|
|
4
|
+
authTag: string;
|
|
5
|
+
hmacIndex: string;
|
|
6
|
+
};
|
|
7
|
+
/** Encrypt a phone number using AES-256-GCM.
|
|
8
|
+
* Returns base64-encoded ciphertext, iv and authTag suitable for storage.
|
|
9
|
+
*/
|
|
10
|
+
export declare function encrypt(phoneRaw: string): Omit<PhoneCipherRecord, "hmacIndex"> & {
|
|
11
|
+
hmacIndex: string;
|
|
12
|
+
};
|
|
13
|
+
/** Decrypt a previously stored record. Throws if tampered.
|
|
14
|
+
* Returns the normalized phone string.
|
|
15
|
+
*/
|
|
16
|
+
export declare function decrypt(record: Pick<PhoneCipherRecord, "ciphertext" | "iv" | "authTag">): string;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.encrypt = encrypt;
|
|
37
|
+
exports.decrypt = decrypt;
|
|
38
|
+
const crypto = __importStar(require("crypto"));
|
|
39
|
+
const phone_number_helper_1 = require("./phone.number.helper");
|
|
40
|
+
const aesKeyB64 = process.env.PHONE_AES_KEY;
|
|
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");
|
|
47
|
+
/** Create an HMAC index for lookups without revealing the number.
|
|
48
|
+
* Store this alongside the encrypted data and index it in the DB.
|
|
49
|
+
*/
|
|
50
|
+
function hmacIndex(phoneRaw) {
|
|
51
|
+
const normalized = (0, phone_number_helper_1.normalize)(phoneRaw);
|
|
52
|
+
const h = crypto.createHmac("sha256", hmacKey);
|
|
53
|
+
h.update(normalized, "utf8");
|
|
54
|
+
return h.digest("base64");
|
|
55
|
+
}
|
|
56
|
+
/** Encrypt a phone number using AES-256-GCM.
|
|
57
|
+
* Returns base64-encoded ciphertext, iv and authTag suitable for storage.
|
|
58
|
+
*/
|
|
59
|
+
function encrypt(phoneRaw) {
|
|
60
|
+
const normalized = (0, phone_number_helper_1.normalize)(phoneRaw);
|
|
61
|
+
// 12-byte IV is recommended for GCM
|
|
62
|
+
const iv = crypto.randomBytes(12);
|
|
63
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv);
|
|
64
|
+
// Optional: bind additional data (AAD), e.g., tenant ID to prevent cross-tenant swaps
|
|
65
|
+
// cipher.setAAD(Buffer.from(tenantId, 'utf8'));
|
|
66
|
+
const ciphertextBuf = Buffer.concat([
|
|
67
|
+
cipher.update(normalized, "utf8"),
|
|
68
|
+
cipher.final(),
|
|
69
|
+
]);
|
|
70
|
+
const authTag = cipher.getAuthTag();
|
|
71
|
+
return {
|
|
72
|
+
ciphertext: ciphertextBuf.toString("base64"),
|
|
73
|
+
iv: iv.toString("base64"),
|
|
74
|
+
authTag: authTag.toString("base64"),
|
|
75
|
+
hmacIndex: hmacIndex(normalized),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** Decrypt a previously stored record. Throws if tampered.
|
|
79
|
+
* Returns the normalized phone string.
|
|
80
|
+
*/
|
|
81
|
+
function decrypt(record) {
|
|
82
|
+
const iv = Buffer.from(record.iv, "base64");
|
|
83
|
+
const authTag = Buffer.from(record.authTag, "base64");
|
|
84
|
+
const ciphertext = Buffer.from(record.ciphertext, "base64");
|
|
85
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv);
|
|
86
|
+
// If you used setAAD on encrypt, you MUST set the same AAD here
|
|
87
|
+
// decipher.setAAD(Buffer.from(tenantId, 'utf8'));
|
|
88
|
+
decipher.setAuthTag(authTag);
|
|
89
|
+
const plaintext = Buffer.concat([
|
|
90
|
+
decipher.update(ciphertext),
|
|
91
|
+
decipher.final(),
|
|
92
|
+
]).toString("utf8");
|
|
93
|
+
return plaintext; // normalized phone
|
|
94
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
preset: "ts-jest",
|
|
3
|
+
testEnvironment: "node",
|
|
4
|
+
setupFiles: ["<rootDir>/jest.setup.ts"],
|
|
5
|
+
collectCoverage: true, // enable coverage
|
|
6
|
+
coverageDirectory: "coverage", // output folder
|
|
7
|
+
coverageReporters: ["text", "lcov", "html"], // text in terminal + HTML report
|
|
8
|
+
testMatch: ["**/*.spec.ts"], // only run *.spec.ts files
|
|
9
|
+
reporters: [
|
|
10
|
+
"default",
|
|
11
|
+
["jest-html-reporter", {
|
|
12
|
+
pageTitle: "Test Report",
|
|
13
|
+
outputPath: "test-report.html",
|
|
14
|
+
includeFailureMsg: true
|
|
15
|
+
}]
|
|
16
|
+
]
|
|
17
|
+
};
|
package/jest.setup.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "core-mb",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Core utility functions for the MB ecosystem in TypeScript",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"test": "jest",
|
|
10
|
+
"test:watch": "jest --watch",
|
|
11
|
+
"test:coverage": "jest --coverage",
|
|
12
|
+
"coverage": "jest --coverage --ci"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"core",
|
|
16
|
+
"mb",
|
|
17
|
+
"ecosystem",
|
|
18
|
+
"utility",
|
|
19
|
+
"typescript"
|
|
20
|
+
],
|
|
21
|
+
"author": "Marco Bytes",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/jest": "^30.0.0",
|
|
25
|
+
"@types/node": "^20.19.11",
|
|
26
|
+
"dotenv": "^17.2.1",
|
|
27
|
+
"jest": "^30.0.5",
|
|
28
|
+
"jest-html-reporter": "^4.3.0",
|
|
29
|
+
"ts-jest": "^29.4.1",
|
|
30
|
+
"typescript": "^5.9.2"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
}
|
|
File without changes
|
|
File without changes
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./security.helper"
|
|
File without changes
|