@63klabs/cache-data 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,305 @@
1
+
2
+ const crypto = require("crypto"); // included by aws so don't need to add to package.json
3
+
4
+ /* *****************************************************************************
5
+ -----------------------------------------------------------------------------
6
+ HELPER FUNCTIONS
7
+ -----------------------------------------------------------------------------
8
+ */
9
+
10
+ const printMsg = function() {
11
+ console.log("This is a message from the demo package");
12
+ };
13
+
14
+ /**
15
+ * Given a secret string, returns a string padded out at the beginning
16
+ * with * or passed character leaving only the specified number of characters unobfuscated.
17
+ *
18
+ * For example, if 123456789123456 was passed with default keep, padding character, and length,
19
+ * ******3456 would be returned.
20
+ *
21
+ * No more than 25% of the string, or 6 characters may be kept, whichever is lesser.
22
+ * @param {string} str The secret string to obfuscate
23
+ * @param {Object} options
24
+ * @param {number} options.keep The number of characters to keep unobfuscated on the end. 4 is default
25
+ * @param {string} options.char The character to pad out with. '*' is default
26
+ * @param {number} options.len Length of the result string
27
+ * @returns Last few characters padded by * or (passed character) from start
28
+ */
29
+ const obfuscate = function(str, options = {}) {
30
+ if ( !( "keep" in options) ) { options.keep = 4; }
31
+ if ( !( "char" in options) ) { options.char = '*'; }
32
+ if ( !( "len" in options) ) { options.len = 10; }
33
+
34
+ // don't show more than 25% of the string, and show no more than a max of 6;
35
+ if ((options.keep / str.length) > .25 || str.length <= 6) { options.keep = Math.min(Math.ceil(str.length * .25), 6); }
36
+
37
+ // we allow any length greater than padding of 4
38
+ if ( options.keep + 4 > options.len ) { options.len = options.keep + 4; }
39
+
40
+ return str.slice(-options.keep).padStart(options.len, options.char);
41
+ };
42
+
43
+ const SANITIZE_MAX_INPUT_LENGTH = 200000; // Adjustable
44
+
45
+ const sanitizeInput = function (strObj) {
46
+
47
+ if (typeof strObj !== 'string') {
48
+ throw new Error('Invalid input');
49
+ }
50
+
51
+ // Early length check to prevent ReDoS
52
+ if (strObj.length > SANITIZE_MAX_INPUT_LENGTH) {
53
+ let trunc = strObj.substring(0, SANITIZE_MAX_INPUT_LENGTH);
54
+ strObj = JSON.stringify({message: 'Input exceeds maximum allowed length', truncated_input: trunc});
55
+ }
56
+
57
+ return strObj;
58
+ }
59
+
60
+ /**
61
+ * Given an object such as a Lambda event which may hold secret keys in the query string or
62
+ * Authorization headers, it will attempt to find and obfuscate them. It searches for any object keys,
63
+ * string patterns that have 'key', 'secret', or 'token' in the label and obfuscates its value.
64
+ * @param {Object} obj The object to sanitize
65
+ * @returns {Object} A sanitized object
66
+ */
67
+ const sanitize = function (obj) {
68
+
69
+ let sanitizedObj = {};
70
+
71
+ // If obj is already a string, convert it to an object
72
+ if (typeof obj === 'string') {
73
+ try {
74
+ obj = JSON.parse(obj);
75
+ } catch(e) {
76
+ // If it's not JSON, wrap it in an object
77
+ obj = { value: obj };
78
+ }
79
+ }
80
+
81
+ try {
82
+
83
+ // convert object to a string which is much easier to perform a search/replace on and we avoid changing original
84
+ let strObj = JSON.stringify(obj);
85
+
86
+ /**
87
+ * Find and replace secret values for secrets, keys, tokens, and authorization headers
88
+ * @param {string} strObj
89
+ * @returns stringified object with secret values replaced (except arrays)
90
+ */
91
+ const sanitizeRoundOne = function (strObj) {
92
+
93
+ strObj = sanitizeInput(strObj);
94
+
95
+ /*
96
+ This regex will produce 2 groups for each match.
97
+ Group 1 will have object key/values and = param value pairs from strings such as query strings.
98
+ Group 2 will have authorization header keys
99
+ View/Edit this regex: https://regex101.com/r/IJp35p/3
100
+ */
101
+ const regex1 = new RegExp(/(?:"?[a-z0-9_\-]{0,256}(?:key|secret|token)[a-z0-9_\-]{0,256}"?\s{0,10}(?::|=)\s{0,10}\"?(?!null|true|false)([a-z0-9+_:\.\-\/]{1,1024})|"Authorization":"[a-z0-9+:_\-\/]{1,1024}\s(.{1,1024}?(?<!\\)(?=")))/, "gi");
102
+ //const _regex1 = new RegExp(/(?:"?[a-z0-9_\-]{0,256}(?:key|secret|token)[a-z0-9_\-]{0,256}"?\s*(?::|=)\s*\"?(?!null|true|false)([a-z0-9+_:\.\-\/]{1,1024})|"Authorization":"[a-z0-9+:_\-\/]{1,1024}\s([^"]{1,1024}))/, "gi");
103
+
104
+ // find matches
105
+ let matches = strObj.matchAll(regex1);
106
+
107
+ /*
108
+ We will do a loop, sort, then another loop,
109
+ but we don't expect 100s of matches anyway.
110
+ */
111
+
112
+ // simplify the array of matches
113
+ let matchList = [];
114
+ for (const match of matches) {
115
+ let segment = match[0];
116
+ let secret = (match[1] !== undefined) ? match[1] : match[2]; // we only expect a result in Group 1 or Group 2, not both
117
+ matchList.push({ segment, secret});
118
+ }
119
+
120
+ // sort so we are replacing the largest strings first
121
+ matchList.sort(function (a, b) {
122
+ return b.segment.length - a.segment.length;
123
+ });
124
+
125
+ // Perform replacecements
126
+ for (const match of matchList) {
127
+
128
+ /*
129
+ Determine if we should obfuscate as string or number
130
+ If we have an object such as: { pin:37832481234 }
131
+ We will get a JSON parse error if we replace a number as *****1234
132
+ So we need to replace it as a number such as 99999991234 so that
133
+ when it parses from a string back to an object it looks like: { pin:99999991234 }
134
+ However, we want to treat strings as strings:
135
+ { pin:"3783281234" } => { pin:"**********1234" }
136
+ */
137
+
138
+ // see if character right before secret is : (stringify will place a number right after : without quotes, and we'll ignore =)
139
+ let obf = (match.segment.charAt(match.segment.length - match.secret.length-1) === ':')
140
+ ? obfuscate(match.secret, {char: 9}) // pad with 9
141
+ : obfuscate(match.secret); // pad normally
142
+
143
+ /*
144
+ 2 steps. Replace secret in match, then replace match in strObj
145
+ This ensures we keep the stringified object true to form for
146
+ converting back to obj
147
+ */
148
+ let str = match.segment.replace(match.secret, obf); // replace secret in match
149
+ strObj = strObj.replace(match.segment, str); // find the old match and replace it with the new one
150
+
151
+ }
152
+
153
+ return strObj;
154
+ };
155
+
156
+ /**
157
+ * Find secret, key, and token arrays in stringified object
158
+ * @param {string} strObj
159
+ * @returns stringified object with array of secrets replaced
160
+ */
161
+ const sanitizeRoundTwo = function(strObj) {
162
+
163
+ strObj = sanitizeInput(strObj);
164
+
165
+ /*
166
+ This regex will grab object keys matching the key|secret|token names which have arrays
167
+ https://regex101.com/r/dFNu4x/3
168
+ */
169
+ const regex2 = new RegExp(/\"[a-z0-9_\-]{0,256}(?:key|secret|token)[a-z0-9_\-]{0,256}\":\[([a-z0-9+_:\.\-\/\",]{1,1024})\]/, "gi");
170
+ const regex3 = new RegExp(/[^,\"]{1,1024}/, "gi");
171
+
172
+ // find matches
173
+ let arrayMatches = strObj.matchAll(regex2);
174
+
175
+ // simplify the array of matches
176
+ let matchList2 = [];
177
+ for (const match of arrayMatches) {
178
+ let segment = match[0];
179
+ let secrets = match[1];
180
+ matchList2.push({ segment, secrets});
181
+ }
182
+
183
+ // sort so we are replacing the largest strings first
184
+ matchList2.sort(function (a, b) {
185
+ return b.segment.length - a.segment.length;
186
+ });
187
+
188
+ for (const match of matchList2) {
189
+ let secrets = match.secrets.matchAll(regex3);
190
+ let list = [];
191
+ for (const secret of secrets) {
192
+ list.push(obfuscate(secret[0]));
193
+ }
194
+ let csv = `"${list.join('","')}"`;
195
+ let str = match.segment.replace(match.secrets, csv);
196
+ strObj = strObj.replace(match.segment, str);
197
+ };
198
+
199
+ return strObj;
200
+ };
201
+
202
+ // convert back to object
203
+ sanitizedObj = JSON.parse(sanitizeRoundTwo(sanitizeRoundOne(strObj)));
204
+
205
+ } catch (error) {
206
+ //DebugAndLog.error(`Error sanitizing object. Skipping: ${error.message}`, error.stack);
207
+ sanitizedObj = {"message": "Error sanitizing object"};
208
+ }
209
+
210
+ return sanitizedObj;
211
+ };
212
+
213
+ /**
214
+ * Hash JSON objects and arrays to determine matches (contain
215
+ * the same keys, values, and nesting.
216
+ *
217
+ * Works best with JSON data objects that survive JSON.stringify().
218
+ * If the data object passed to it contains classes or specialized
219
+ * objects (like Date), JSON.stringify() will attempt to use a
220
+ * .toJSON() method to convert the object. DataTypes of Symbols and
221
+ * Functions will not survive this process.
222
+
223
+ * @param {string} algorithm
224
+ * @param {Object|Array|BigInt|Number|String|Boolean} data to hash
225
+ * @param {{salt: string, iterations: number}} options
226
+ * @returns {string} Reproducible hash in hex
227
+ */
228
+ const hashThisData = function(algorithm, data, options = {}) {
229
+
230
+ // set default values for options
231
+ if ( !( "salt" in options) ) { options.salt = ""; }
232
+ if ( !( "iterations" in options) || options.iterations < 1 ) { options.iterations = 1; }
233
+ if ( !( "skipParse" in options) ) { options.skipParse = false; } // used so we don't parse during recursion
234
+
235
+ // if it is an object or array, then parse it to remove non-data elements (functions, etc)
236
+ if ( !options.skipParse && (typeof data === "object" || Array.isArray(data))) {
237
+ data = JSON.parse(JSON.stringify(data, (key, value) => {
238
+ switch (typeof value) {
239
+ case 'bigint':
240
+ return value.toString();
241
+ case 'undefined':
242
+ return 'undefined';
243
+ default:
244
+ return value;
245
+ }
246
+ }));
247
+ options.skipParse = true; // set to true so we don't parse during recursion
248
+ }
249
+
250
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof
251
+ const dataType = (data !== null && !Array.isArray(data)) ? typeof data : (Array.isArray(data)) ? "array" : "null";
252
+
253
+ if (data === null) { data = "null" }
254
+ if (data === undefined) { data = "undefined" }
255
+
256
+ let valueStr = "";
257
+
258
+ if (dataType === "array" || dataType === "object") {
259
+
260
+ /*
261
+ We will iterate through the keys and values and generate a reproducible data string.
262
+ (sorted by object key or array value)
263
+ */
264
+
265
+ let arrayOfStuff = [];
266
+
267
+ // copy the named keys and alphabetize (or generate index for array) .
268
+ let keys = (dataType === "array")
269
+ ? Array.from({ length: data.length }, (value, index) => index)
270
+ : Object.keys(data).sort();
271
+
272
+ // iterate through the keys alphabetically and add the key and value to the arrayOfStuff
273
+ keys.forEach((key) => {
274
+ // clone options
275
+ const opts = JSON.parse(JSON.stringify(options));
276
+ opts.iterations = 1; // don't iterate during recursion, only at end
277
+
278
+ const value = hashThisData(algorithm, data[key], opts);
279
+ arrayOfStuff.push( `${(dataType !== "array" ? key : "$array")}:::${dataType}:::${value}` );
280
+ })
281
+
282
+ valueStr = arrayOfStuff.sort().join("|||");
283
+
284
+ } else {
285
+ valueStr = `-:::${dataType}:::${data.toString()}`;
286
+ }
287
+
288
+ const hash = crypto.createHash(algorithm);
289
+ let hashOfData = "";
290
+
291
+ // hash for the number of iterations
292
+ for (let i = 0; i < options.iterations; i++) {
293
+ hash.update(valueStr + hashOfData + options.salt);
294
+ hashOfData = hash.digest('hex');
295
+ }
296
+
297
+ return hashOfData;
298
+ };
299
+
300
+ module.exports = {
301
+ printMsg,
302
+ sanitize,
303
+ obfuscate,
304
+ hashThisData
305
+ };
@@ -0,0 +1,34 @@
1
+
2
+ const {AWS} = require('./AWS.classes');
3
+
4
+ /**
5
+ * Node version in 0.0.0 format retrieved from process.versions.node if present. '0.0.0' if not present.
6
+ * @type {string}
7
+ */
8
+ const nodeVer = AWS.NODE_VER;
9
+
10
+ /**
11
+ * Node Major version. This is the first number in the version string. '20.1.6' would return 20 as a number.
12
+ * @type {number}
13
+ */
14
+ const nodeVerMajor = AWS.NODE_VER_MAJOR;
15
+
16
+ /**
17
+ * Node Minor version. This is the second number in the version string. '20.31.6' would return 31 as a number.
18
+ * @type {number}
19
+ */
20
+ const nodeVerMinor = AWS.NODE_VER_MINOR;
21
+
22
+ const nodeVerMajorMinor = AWS.NODE_VER_MAJOR_MINOR;
23
+
24
+ if (nodeVerMajor < 16) {
25
+ console.error(`Node.js version 16 or higher is required for @chadkluck/cache-data. Version ${nodeVer} detected. Please install at least Node version 16 (>18 preferred) in your environment.`);
26
+ process.exit(1);
27
+ }
28
+
29
+ module.exports = {
30
+ nodeVer,
31
+ nodeVerMajor,
32
+ nodeVerMinor,
33
+ nodeVerMajorMinor
34
+ }