typesecure 0.1.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/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/index.d.mts +237 -0
- package/dist/index.d.ts +237 -0
- package/dist/index.js +720 -0
- package/dist/index.mjs +662 -0
- package/package.json +68 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
// src/utils/hash.ts
|
|
2
|
+
import CryptoJS from "crypto-js";
|
|
3
|
+
|
|
4
|
+
// src/types/index.ts
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
var PasswordStrengthSchema = z.object({
|
|
7
|
+
score: z.number().min(0).max(4),
|
|
8
|
+
feedback: z.object({
|
|
9
|
+
warning: z.string().optional(),
|
|
10
|
+
suggestions: z.array(z.string()).optional()
|
|
11
|
+
}),
|
|
12
|
+
isStrong: z.boolean()
|
|
13
|
+
});
|
|
14
|
+
var HashOptionsSchema = z.object({
|
|
15
|
+
algorithm: z.enum(["md5", "sha1", "sha256", "sha512", "sha3"]).default("sha256"),
|
|
16
|
+
encoding: z.enum(["hex", "base64"]).default("hex")
|
|
17
|
+
});
|
|
18
|
+
var PasswordHashOptionsSchema = z.object({
|
|
19
|
+
algorithm: z.enum(["pbkdf2", "argon2", "bcrypt"]).default("pbkdf2"),
|
|
20
|
+
iterations: z.number().min(1e3).default(1e4),
|
|
21
|
+
saltLength: z.number().min(16).default(32),
|
|
22
|
+
keyLength: z.number().min(16).default(64)
|
|
23
|
+
});
|
|
24
|
+
var TimingSafeOptionsSchema = z.object({
|
|
25
|
+
encoding: z.enum(["utf8", "hex", "base64"]).default("utf8")
|
|
26
|
+
});
|
|
27
|
+
var EncryptionOptionsSchema = z.object({
|
|
28
|
+
mode: z.enum(["aes-cbc", "aes-ctr", "aes-ecb", "aes-gcm"]).default("aes-cbc"),
|
|
29
|
+
padding: z.enum(["Pkcs7", "NoPadding", "ZeroPadding"]).default("Pkcs7"),
|
|
30
|
+
iv: z.string().optional(),
|
|
31
|
+
authTag: z.string().optional(),
|
|
32
|
+
aad: z.string().optional()
|
|
33
|
+
// Additional authenticated data for GCM mode
|
|
34
|
+
});
|
|
35
|
+
var ConfigSchema = z.object({
|
|
36
|
+
minimumPasswordLength: z.number().min(8).default(12),
|
|
37
|
+
requireNumbers: z.boolean().default(true),
|
|
38
|
+
requireSymbols: z.boolean().default(true),
|
|
39
|
+
requireUppercase: z.boolean().default(true),
|
|
40
|
+
requireLowercase: z.boolean().default(true)
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// src/utils/hash.ts
|
|
44
|
+
function hash(input, options) {
|
|
45
|
+
const validatedOptions = HashOptionsSchema.parse({
|
|
46
|
+
algorithm: options?.algorithm || "sha256",
|
|
47
|
+
encoding: options?.encoding || "hex"
|
|
48
|
+
});
|
|
49
|
+
let hashedValue;
|
|
50
|
+
switch (validatedOptions.algorithm) {
|
|
51
|
+
case "md5":
|
|
52
|
+
hashedValue = CryptoJS.MD5(input);
|
|
53
|
+
break;
|
|
54
|
+
case "sha1":
|
|
55
|
+
hashedValue = CryptoJS.SHA1(input);
|
|
56
|
+
break;
|
|
57
|
+
case "sha256":
|
|
58
|
+
hashedValue = CryptoJS.SHA256(input);
|
|
59
|
+
break;
|
|
60
|
+
case "sha512":
|
|
61
|
+
hashedValue = CryptoJS.SHA512(input);
|
|
62
|
+
break;
|
|
63
|
+
case "sha3":
|
|
64
|
+
hashedValue = CryptoJS.SHA3(input);
|
|
65
|
+
break;
|
|
66
|
+
default:
|
|
67
|
+
hashedValue = CryptoJS.SHA256(input);
|
|
68
|
+
}
|
|
69
|
+
return validatedOptions.encoding === "base64" ? hashedValue.toString(CryptoJS.enc.Base64) : hashedValue.toString(CryptoJS.enc.Hex);
|
|
70
|
+
}
|
|
71
|
+
function verifyHash(input, hashedValue, options) {
|
|
72
|
+
const newHash = hash(input, options);
|
|
73
|
+
return newHash === hashedValue;
|
|
74
|
+
}
|
|
75
|
+
function hmac(input, key, options) {
|
|
76
|
+
const validatedOptions = HashOptionsSchema.parse({
|
|
77
|
+
algorithm: options?.algorithm || "sha256",
|
|
78
|
+
encoding: options?.encoding || "hex"
|
|
79
|
+
});
|
|
80
|
+
let hmacValue;
|
|
81
|
+
switch (validatedOptions.algorithm) {
|
|
82
|
+
case "md5":
|
|
83
|
+
hmacValue = CryptoJS.HmacMD5(input, key);
|
|
84
|
+
break;
|
|
85
|
+
case "sha1":
|
|
86
|
+
hmacValue = CryptoJS.HmacSHA1(input, key);
|
|
87
|
+
break;
|
|
88
|
+
case "sha256":
|
|
89
|
+
hmacValue = CryptoJS.HmacSHA256(input, key);
|
|
90
|
+
break;
|
|
91
|
+
case "sha512":
|
|
92
|
+
hmacValue = CryptoJS.HmacSHA512(input, key);
|
|
93
|
+
break;
|
|
94
|
+
case "sha3":
|
|
95
|
+
hmacValue = CryptoJS.HmacSHA3(input, key);
|
|
96
|
+
break;
|
|
97
|
+
default:
|
|
98
|
+
hmacValue = CryptoJS.HmacSHA256(input, key);
|
|
99
|
+
}
|
|
100
|
+
return validatedOptions.encoding === "base64" ? hmacValue.toString(CryptoJS.enc.Base64) : hmacValue.toString(CryptoJS.enc.Hex);
|
|
101
|
+
}
|
|
102
|
+
function hashPassword(password, options) {
|
|
103
|
+
const validatedOptions = PasswordHashOptionsSchema.parse({
|
|
104
|
+
algorithm: options?.algorithm || "pbkdf2",
|
|
105
|
+
iterations: options?.iterations || 1e4,
|
|
106
|
+
saltLength: options?.saltLength || 32,
|
|
107
|
+
keyLength: options?.keyLength || 64
|
|
108
|
+
});
|
|
109
|
+
const salt = CryptoJS.lib.WordArray.random(validatedOptions.saltLength / 2).toString(
|
|
110
|
+
CryptoJS.enc.Hex
|
|
111
|
+
);
|
|
112
|
+
let hashedPassword;
|
|
113
|
+
switch (validatedOptions.algorithm) {
|
|
114
|
+
case "pbkdf2":
|
|
115
|
+
hashedPassword = CryptoJS.PBKDF2(password, salt, {
|
|
116
|
+
keySize: validatedOptions.keyLength / 4,
|
|
117
|
+
// keySize is in 32-bit words
|
|
118
|
+
iterations: validatedOptions.iterations
|
|
119
|
+
}).toString(CryptoJS.enc.Hex);
|
|
120
|
+
break;
|
|
121
|
+
case "argon2":
|
|
122
|
+
hashedPassword = CryptoJS.PBKDF2(password, salt, {
|
|
123
|
+
keySize: validatedOptions.keyLength / 4,
|
|
124
|
+
iterations: validatedOptions.iterations * 2
|
|
125
|
+
// Higher iterations to simulate Argon2 costs
|
|
126
|
+
}).toString(CryptoJS.enc.Hex);
|
|
127
|
+
break;
|
|
128
|
+
case "bcrypt":
|
|
129
|
+
hashedPassword = CryptoJS.PBKDF2(password, salt, {
|
|
130
|
+
keySize: validatedOptions.keyLength / 4,
|
|
131
|
+
iterations: 1024 + validatedOptions.iterations % 1024
|
|
132
|
+
// bcrypt simulation
|
|
133
|
+
}).toString(CryptoJS.enc.Hex);
|
|
134
|
+
break;
|
|
135
|
+
default:
|
|
136
|
+
hashedPassword = CryptoJS.PBKDF2(password, salt, {
|
|
137
|
+
keySize: validatedOptions.keyLength / 4,
|
|
138
|
+
iterations: validatedOptions.iterations
|
|
139
|
+
}).toString(CryptoJS.enc.Hex);
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
hash: hashedPassword,
|
|
143
|
+
salt,
|
|
144
|
+
params: validatedOptions
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function verifyPassword(password, hash2, salt, options) {
|
|
148
|
+
const validatedOptions = PasswordHashOptionsSchema.parse({
|
|
149
|
+
algorithm: options?.algorithm || "pbkdf2",
|
|
150
|
+
iterations: options?.iterations || 1e4,
|
|
151
|
+
saltLength: options?.saltLength || 32,
|
|
152
|
+
keyLength: options?.keyLength || 64
|
|
153
|
+
});
|
|
154
|
+
const { hash: generatedHash } = hashPassword(password, validatedOptions);
|
|
155
|
+
return timingSafeEqual(generatedHash, hash2);
|
|
156
|
+
}
|
|
157
|
+
function timingSafeEqual(a, b, options) {
|
|
158
|
+
const validatedOptions = TimingSafeOptionsSchema.parse({
|
|
159
|
+
encoding: options?.encoding || "utf8"
|
|
160
|
+
});
|
|
161
|
+
if (a.length !== b.length) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
let bufferA = a;
|
|
165
|
+
let bufferB = b;
|
|
166
|
+
if (validatedOptions.encoding === "hex") {
|
|
167
|
+
try {
|
|
168
|
+
bufferA = CryptoJS.enc.Hex.parse(a).toString(CryptoJS.enc.Utf8);
|
|
169
|
+
bufferB = CryptoJS.enc.Hex.parse(b).toString(CryptoJS.enc.Utf8);
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
} else if (validatedOptions.encoding === "base64") {
|
|
174
|
+
try {
|
|
175
|
+
bufferA = CryptoJS.enc.Base64.parse(a).toString(CryptoJS.enc.Utf8);
|
|
176
|
+
bufferB = CryptoJS.enc.Base64.parse(b).toString(CryptoJS.enc.Utf8);
|
|
177
|
+
} catch {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
let result = 0;
|
|
182
|
+
for (let i = 0; i < bufferA.length; i++) {
|
|
183
|
+
result |= bufferA.charCodeAt(i) ^ bufferB.charCodeAt(i);
|
|
184
|
+
}
|
|
185
|
+
return result === 0;
|
|
186
|
+
}
|
|
187
|
+
function generateRandomBytes(length = 32, encoding = "hex") {
|
|
188
|
+
const randomBytes = CryptoJS.lib.WordArray.random(length);
|
|
189
|
+
return encoding === "base64" ? randomBytes.toString(CryptoJS.enc.Base64) : randomBytes.toString(CryptoJS.enc.Hex);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/utils/encryption.ts
|
|
193
|
+
import CryptoJS2 from "crypto-js";
|
|
194
|
+
var SecurityLevel = /* @__PURE__ */ ((SecurityLevel2) => {
|
|
195
|
+
SecurityLevel2["HIGH"] = "HIGH";
|
|
196
|
+
SecurityLevel2["MEDIUM"] = "MEDIUM";
|
|
197
|
+
SecurityLevel2["LOW"] = "LOW";
|
|
198
|
+
SecurityLevel2["INSECURE"] = "INSECURE";
|
|
199
|
+
return SecurityLevel2;
|
|
200
|
+
})(SecurityLevel || {});
|
|
201
|
+
function getSecurityLevel(options) {
|
|
202
|
+
if (options.mode === "aes-gcm") {
|
|
203
|
+
return "HIGH" /* HIGH */;
|
|
204
|
+
}
|
|
205
|
+
if (options.mode === "aes-cbc" || options.mode === "aes-ctr") {
|
|
206
|
+
return "HIGH" /* HIGH */;
|
|
207
|
+
}
|
|
208
|
+
if (options.mode === "aes-ecb") {
|
|
209
|
+
return "INSECURE" /* INSECURE */;
|
|
210
|
+
}
|
|
211
|
+
return "LOW" /* LOW */;
|
|
212
|
+
}
|
|
213
|
+
function logSecurityWarning(options) {
|
|
214
|
+
const securityLevel = getSecurityLevel(options);
|
|
215
|
+
switch (securityLevel) {
|
|
216
|
+
case "INSECURE" /* INSECURE */:
|
|
217
|
+
console.warn(
|
|
218
|
+
`\u26A0\uFE0F SECURITY WARNING: ${options.mode} mode is insecure and should not be used in production!`
|
|
219
|
+
);
|
|
220
|
+
break;
|
|
221
|
+
case "LOW" /* LOW */:
|
|
222
|
+
console.warn(
|
|
223
|
+
`\u26A0\uFE0F Security Warning: ${options.mode} with ${options.padding} padding provides low security.`
|
|
224
|
+
);
|
|
225
|
+
break;
|
|
226
|
+
case "MEDIUM" /* MEDIUM */:
|
|
227
|
+
console.info(
|
|
228
|
+
`\u2139\uFE0F Security Note: ${options.mode} with ${options.padding} padding provides adequate security for most use cases.`
|
|
229
|
+
);
|
|
230
|
+
break;
|
|
231
|
+
case "HIGH" /* HIGH */:
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function encrypt(text, key, options) {
|
|
236
|
+
const validatedOptions = EncryptionOptionsSchema.parse({
|
|
237
|
+
mode: options?.mode || "aes-cbc",
|
|
238
|
+
padding: options?.padding || "Pkcs7",
|
|
239
|
+
iv: options?.iv,
|
|
240
|
+
authTag: options?.authTag,
|
|
241
|
+
aad: options?.aad
|
|
242
|
+
});
|
|
243
|
+
logSecurityWarning(validatedOptions);
|
|
244
|
+
const keyWordArray = CryptoJS2.enc.Utf8.parse(key);
|
|
245
|
+
let encrypted;
|
|
246
|
+
const paddingOption = CryptoJS2.pad[validatedOptions.padding];
|
|
247
|
+
switch (validatedOptions.mode) {
|
|
248
|
+
case "aes-gcm": {
|
|
249
|
+
const iv = validatedOptions.iv ? CryptoJS2.enc.Utf8.parse(validatedOptions.iv) : CryptoJS2.lib.WordArray.random(12);
|
|
250
|
+
const aad = validatedOptions.aad ? CryptoJS2.enc.Utf8.parse(validatedOptions.aad) : void 0;
|
|
251
|
+
encrypted = CryptoJS2.AES.encrypt(text, keyWordArray, {
|
|
252
|
+
iv,
|
|
253
|
+
mode: CryptoJS2.mode.CTR,
|
|
254
|
+
// Use CTR as a base for GCM simulation
|
|
255
|
+
padding: paddingOption
|
|
256
|
+
});
|
|
257
|
+
const authData = aad ? aad.concat(encrypted.ciphertext) : encrypted.ciphertext;
|
|
258
|
+
const authTag = CryptoJS2.HmacSHA256(authData, keyWordArray).toString().substring(0, 32);
|
|
259
|
+
const ivHex = iv.toString(CryptoJS2.enc.Hex);
|
|
260
|
+
return `${ivHex}:${authTag}:${encrypted.toString()}`;
|
|
261
|
+
}
|
|
262
|
+
case "aes-cbc": {
|
|
263
|
+
const iv = validatedOptions.iv ? CryptoJS2.enc.Utf8.parse(validatedOptions.iv) : CryptoJS2.lib.WordArray.random(16);
|
|
264
|
+
encrypted = CryptoJS2.AES.encrypt(text, keyWordArray, {
|
|
265
|
+
iv,
|
|
266
|
+
padding: paddingOption,
|
|
267
|
+
mode: CryptoJS2.mode.CBC
|
|
268
|
+
});
|
|
269
|
+
if (!validatedOptions.iv) {
|
|
270
|
+
const ivHex = iv.toString(CryptoJS2.enc.Hex);
|
|
271
|
+
return `${ivHex}:${encrypted.toString()}`;
|
|
272
|
+
}
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
case "aes-ctr": {
|
|
276
|
+
const iv = validatedOptions.iv ? CryptoJS2.enc.Utf8.parse(validatedOptions.iv) : CryptoJS2.lib.WordArray.random(16);
|
|
277
|
+
encrypted = CryptoJS2.AES.encrypt(text, keyWordArray, {
|
|
278
|
+
iv,
|
|
279
|
+
padding: paddingOption,
|
|
280
|
+
mode: CryptoJS2.mode.CTR
|
|
281
|
+
});
|
|
282
|
+
if (!validatedOptions.iv) {
|
|
283
|
+
const ivHex = iv.toString(CryptoJS2.enc.Hex);
|
|
284
|
+
return `${ivHex}:${encrypted.toString()}`;
|
|
285
|
+
}
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
case "aes-ecb":
|
|
289
|
+
encrypted = CryptoJS2.AES.encrypt(text, keyWordArray, {
|
|
290
|
+
padding: paddingOption,
|
|
291
|
+
mode: CryptoJS2.mode.ECB
|
|
292
|
+
});
|
|
293
|
+
break;
|
|
294
|
+
default:
|
|
295
|
+
throw new Error(`Unsupported encryption mode: ${validatedOptions.mode}`);
|
|
296
|
+
}
|
|
297
|
+
return encrypted.toString();
|
|
298
|
+
}
|
|
299
|
+
function decrypt(encryptedText, key, options) {
|
|
300
|
+
const validatedOptions = EncryptionOptionsSchema.parse({
|
|
301
|
+
mode: options?.mode || "aes-cbc",
|
|
302
|
+
padding: options?.padding || "Pkcs7",
|
|
303
|
+
iv: options?.iv,
|
|
304
|
+
authTag: options?.authTag,
|
|
305
|
+
aad: options?.aad
|
|
306
|
+
});
|
|
307
|
+
const keyWordArray = CryptoJS2.enc.Utf8.parse(key);
|
|
308
|
+
let cipherText = encryptedText;
|
|
309
|
+
let iv;
|
|
310
|
+
let authTag;
|
|
311
|
+
if (validatedOptions.mode === "aes-gcm" && encryptedText.includes(":")) {
|
|
312
|
+
const parts = encryptedText.split(":");
|
|
313
|
+
if (parts.length >= 3) {
|
|
314
|
+
const ivHex = parts[0];
|
|
315
|
+
authTag = parts[1];
|
|
316
|
+
cipherText = parts[2];
|
|
317
|
+
iv = CryptoJS2.enc.Hex.parse(ivHex);
|
|
318
|
+
}
|
|
319
|
+
} else if (encryptedText.includes(":") && !validatedOptions.iv) {
|
|
320
|
+
const parts = encryptedText.split(":");
|
|
321
|
+
const ivHex = parts[0];
|
|
322
|
+
cipherText = parts[1];
|
|
323
|
+
iv = CryptoJS2.enc.Hex.parse(ivHex);
|
|
324
|
+
} else if (validatedOptions.iv) {
|
|
325
|
+
iv = CryptoJS2.enc.Utf8.parse(validatedOptions.iv);
|
|
326
|
+
}
|
|
327
|
+
const paddingOption = CryptoJS2.pad[validatedOptions.padding];
|
|
328
|
+
let decrypted;
|
|
329
|
+
switch (validatedOptions.mode) {
|
|
330
|
+
case "aes-gcm": {
|
|
331
|
+
if (!iv) {
|
|
332
|
+
throw new Error("IV is required for GCM mode decryption");
|
|
333
|
+
}
|
|
334
|
+
if (!authTag && !validatedOptions.authTag) {
|
|
335
|
+
throw new Error("Authentication tag is required for GCM mode decryption");
|
|
336
|
+
}
|
|
337
|
+
const actualAuthTag = authTag || validatedOptions.authTag;
|
|
338
|
+
const aad = validatedOptions.aad ? CryptoJS2.enc.Utf8.parse(validatedOptions.aad) : void 0;
|
|
339
|
+
decrypted = CryptoJS2.AES.decrypt(cipherText, keyWordArray, {
|
|
340
|
+
iv,
|
|
341
|
+
mode: CryptoJS2.mode.CTR,
|
|
342
|
+
padding: paddingOption
|
|
343
|
+
});
|
|
344
|
+
const cipherTextObj = CryptoJS2.enc.Base64.parse(cipherText);
|
|
345
|
+
const authData = aad ? aad.concat(cipherTextObj) : cipherTextObj;
|
|
346
|
+
const calculatedAuthTag = CryptoJS2.HmacSHA256(authData, keyWordArray).toString().substring(0, 32);
|
|
347
|
+
if (calculatedAuthTag !== actualAuthTag) {
|
|
348
|
+
throw new Error("Authentication failed: Invalid authentication tag");
|
|
349
|
+
}
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
case "aes-cbc":
|
|
353
|
+
if (!iv && validatedOptions.mode === "aes-cbc") {
|
|
354
|
+
throw new Error("IV is required for CBC mode decryption");
|
|
355
|
+
}
|
|
356
|
+
decrypted = CryptoJS2.AES.decrypt(cipherText, keyWordArray, {
|
|
357
|
+
iv,
|
|
358
|
+
padding: paddingOption,
|
|
359
|
+
mode: CryptoJS2.mode.CBC
|
|
360
|
+
});
|
|
361
|
+
break;
|
|
362
|
+
case "aes-ctr":
|
|
363
|
+
if (!iv && validatedOptions.mode === "aes-ctr") {
|
|
364
|
+
throw new Error("IV is required for CTR mode decryption");
|
|
365
|
+
}
|
|
366
|
+
decrypted = CryptoJS2.AES.decrypt(cipherText, keyWordArray, {
|
|
367
|
+
iv,
|
|
368
|
+
padding: paddingOption,
|
|
369
|
+
mode: CryptoJS2.mode.CTR
|
|
370
|
+
});
|
|
371
|
+
break;
|
|
372
|
+
case "aes-ecb":
|
|
373
|
+
console.warn(
|
|
374
|
+
"\u26A0\uFE0F WARNING: ECB mode does not provide semantic security and should not be used in most applications"
|
|
375
|
+
);
|
|
376
|
+
decrypted = CryptoJS2.AES.decrypt(cipherText, keyWordArray, {
|
|
377
|
+
padding: paddingOption,
|
|
378
|
+
mode: CryptoJS2.mode.ECB
|
|
379
|
+
});
|
|
380
|
+
break;
|
|
381
|
+
default:
|
|
382
|
+
throw new Error(`Unsupported encryption mode: ${validatedOptions.mode}`);
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
return decrypted.toString(CryptoJS2.enc.Utf8);
|
|
386
|
+
} catch {
|
|
387
|
+
throw new Error("Failed to decrypt: Invalid key or corrupted data");
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function generateKey(length = 32) {
|
|
391
|
+
return CryptoJS2.lib.WordArray.random(length).toString(CryptoJS2.enc.Hex);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/utils/password.ts
|
|
395
|
+
function calculatePasswordEntropy(password) {
|
|
396
|
+
if (!password || password.length === 0) return 0;
|
|
397
|
+
let poolSize = 0;
|
|
398
|
+
const hasLowercase = /[a-z]/.test(password);
|
|
399
|
+
const hasUppercase = /[A-Z]/.test(password);
|
|
400
|
+
const hasDigits = /\d/.test(password);
|
|
401
|
+
const hasSymbols = /[^A-Za-z0-9]/.test(password);
|
|
402
|
+
if (hasLowercase) poolSize += 26;
|
|
403
|
+
if (hasUppercase) poolSize += 26;
|
|
404
|
+
if (hasDigits) poolSize += 10;
|
|
405
|
+
if (hasSymbols) poolSize += 33;
|
|
406
|
+
const entropy = password.length * Math.log2(poolSize);
|
|
407
|
+
return Math.round(entropy * 100) / 100;
|
|
408
|
+
}
|
|
409
|
+
function analyzePasswordPatterns(password) {
|
|
410
|
+
const patterns = [];
|
|
411
|
+
let entropyReduction = 0;
|
|
412
|
+
const keyboardPatterns = [
|
|
413
|
+
"qwerty",
|
|
414
|
+
"asdfgh",
|
|
415
|
+
"zxcvbn",
|
|
416
|
+
"qwertz",
|
|
417
|
+
"azerty",
|
|
418
|
+
"123456",
|
|
419
|
+
"654321"
|
|
420
|
+
];
|
|
421
|
+
const sequentialCheck = (pwd) => {
|
|
422
|
+
for (let i = 0; i < pwd.length - 2; i++) {
|
|
423
|
+
const c1 = pwd.charCodeAt(i);
|
|
424
|
+
const c2 = pwd.charCodeAt(i + 1);
|
|
425
|
+
const c3 = pwd.charCodeAt(i + 2);
|
|
426
|
+
if (c1 + 1 === c2 && c2 + 1 === c3 || c1 - 1 === c2 && c2 - 1 === c3) {
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return false;
|
|
431
|
+
};
|
|
432
|
+
const repeatedPatternCheck = (pwd) => {
|
|
433
|
+
if (pwd.length <= 2) return false;
|
|
434
|
+
for (let len = 2; len <= pwd.length / 2; len++) {
|
|
435
|
+
for (let i = 0; i <= pwd.length - len * 2; i++) {
|
|
436
|
+
const pattern = pwd.substring(i, i + len);
|
|
437
|
+
const nextPart = pwd.substring(i + len, i + len * 2);
|
|
438
|
+
if (pattern === nextPart) {
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return false;
|
|
444
|
+
};
|
|
445
|
+
for (const pattern of keyboardPatterns) {
|
|
446
|
+
if (password.toLowerCase().includes(pattern)) {
|
|
447
|
+
patterns.push("Contains keyboard pattern");
|
|
448
|
+
entropyReduction += 0.2;
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (sequentialCheck(password)) {
|
|
453
|
+
patterns.push("Contains sequential characters");
|
|
454
|
+
entropyReduction += 0.15;
|
|
455
|
+
}
|
|
456
|
+
if (repeatedPatternCheck(password)) {
|
|
457
|
+
patterns.push("Contains repeated patterns");
|
|
458
|
+
entropyReduction += 0.2;
|
|
459
|
+
}
|
|
460
|
+
const l33tMap = {
|
|
461
|
+
"4": "a",
|
|
462
|
+
"@": "a",
|
|
463
|
+
"8": "b",
|
|
464
|
+
"3": "e",
|
|
465
|
+
"6": "g",
|
|
466
|
+
"1": "i",
|
|
467
|
+
"!": "i",
|
|
468
|
+
"0": "o",
|
|
469
|
+
"9": "g",
|
|
470
|
+
$: "s",
|
|
471
|
+
"5": "s",
|
|
472
|
+
"+": "t",
|
|
473
|
+
"7": "t",
|
|
474
|
+
"%": "x",
|
|
475
|
+
"2": "z"
|
|
476
|
+
};
|
|
477
|
+
let transformedPassword = password.toLowerCase();
|
|
478
|
+
for (const [digit, letter] of Object.entries(l33tMap)) {
|
|
479
|
+
const safeDigit = digit.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
480
|
+
transformedPassword = transformedPassword.replace(
|
|
481
|
+
new RegExp(safeDigit, "g"),
|
|
482
|
+
letter
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
const commonWords = [
|
|
486
|
+
"password",
|
|
487
|
+
"admin",
|
|
488
|
+
"user",
|
|
489
|
+
"login",
|
|
490
|
+
"welcome",
|
|
491
|
+
"secure",
|
|
492
|
+
"secret"
|
|
493
|
+
];
|
|
494
|
+
for (const word of commonWords) {
|
|
495
|
+
if (transformedPassword.includes(word)) {
|
|
496
|
+
patterns.push("Contains common word with substitutions");
|
|
497
|
+
entropyReduction += 0.25;
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (/^[a-z]+$/.test(password) || /^[A-Z]+$/.test(password) || /^[0-9]+$/.test(password) || /^[^A-Za-z0-9]+$/.test(password)) {
|
|
502
|
+
patterns.push("Uses only one character type");
|
|
503
|
+
entropyReduction += 0.3;
|
|
504
|
+
}
|
|
505
|
+
entropyReduction = Math.min(entropyReduction, 0.9);
|
|
506
|
+
return { entropyReduction, patterns };
|
|
507
|
+
}
|
|
508
|
+
function checkPasswordStrength(password, config) {
|
|
509
|
+
const validatedConfig = ConfigSchema.parse({
|
|
510
|
+
minimumPasswordLength: config?.minimumPasswordLength || 12,
|
|
511
|
+
requireNumbers: config?.requireNumbers !== void 0 ? config.requireNumbers : true,
|
|
512
|
+
requireSymbols: config?.requireSymbols !== void 0 ? config.requireSymbols : true,
|
|
513
|
+
requireUppercase: config?.requireUppercase !== void 0 ? config.requireUppercase : true,
|
|
514
|
+
requireLowercase: config?.requireLowercase !== void 0 ? config.requireLowercase : true
|
|
515
|
+
});
|
|
516
|
+
const hasMinLength = password.length >= validatedConfig.minimumPasswordLength;
|
|
517
|
+
const hasNumbers = /\d/.test(password);
|
|
518
|
+
const hasSymbols = /[^A-Za-z0-9]/.test(password);
|
|
519
|
+
const hasUppercase = /[A-Z]/.test(password);
|
|
520
|
+
const hasLowercase = /[a-z]/.test(password);
|
|
521
|
+
const suggestions = [];
|
|
522
|
+
let warning = "";
|
|
523
|
+
let criteriaCount = 0;
|
|
524
|
+
if (hasMinLength) criteriaCount++;
|
|
525
|
+
if (hasNumbers && validatedConfig.requireNumbers) criteriaCount++;
|
|
526
|
+
if (hasSymbols && validatedConfig.requireSymbols) criteriaCount++;
|
|
527
|
+
if (hasUppercase && validatedConfig.requireUppercase) criteriaCount++;
|
|
528
|
+
if (hasLowercase && validatedConfig.requireLowercase) criteriaCount++;
|
|
529
|
+
if (!hasMinLength) {
|
|
530
|
+
warning = "Password is too short";
|
|
531
|
+
suggestions.push(
|
|
532
|
+
`Password should be at least ${validatedConfig.minimumPasswordLength} characters long`
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
if (validatedConfig.requireNumbers && !hasNumbers) {
|
|
536
|
+
suggestions.push("Add numbers to make your password stronger");
|
|
537
|
+
}
|
|
538
|
+
if (validatedConfig.requireSymbols && !hasSymbols) {
|
|
539
|
+
suggestions.push("Add symbols to make your password stronger");
|
|
540
|
+
}
|
|
541
|
+
if (validatedConfig.requireUppercase && !hasUppercase) {
|
|
542
|
+
suggestions.push("Add uppercase letters to make your password stronger");
|
|
543
|
+
}
|
|
544
|
+
if (validatedConfig.requireLowercase && !hasLowercase) {
|
|
545
|
+
suggestions.push("Add lowercase letters to make your password stronger");
|
|
546
|
+
}
|
|
547
|
+
const entropy = calculatePasswordEntropy(password);
|
|
548
|
+
const { entropyReduction, patterns } = analyzePasswordPatterns(password);
|
|
549
|
+
const effectiveEntropy = entropy * (1 - entropyReduction);
|
|
550
|
+
suggestions.push(
|
|
551
|
+
...patterns.map((p) => `${p}: Consider a more random pattern`)
|
|
552
|
+
);
|
|
553
|
+
let score;
|
|
554
|
+
if (effectiveEntropy < 28) {
|
|
555
|
+
score = 0;
|
|
556
|
+
if (!warning) warning = "Very weak password";
|
|
557
|
+
} else if (effectiveEntropy < 36) {
|
|
558
|
+
score = 1;
|
|
559
|
+
if (!warning) warning = "Weak password";
|
|
560
|
+
} else if (effectiveEntropy < 60) {
|
|
561
|
+
score = 2;
|
|
562
|
+
} else if (effectiveEntropy < 80) {
|
|
563
|
+
score = 3;
|
|
564
|
+
} else {
|
|
565
|
+
score = 4;
|
|
566
|
+
}
|
|
567
|
+
const criteriaBoost = criteriaCount >= 5 ? 1 : 0;
|
|
568
|
+
score = Math.min(4, score + criteriaBoost);
|
|
569
|
+
if (!hasMinLength || validatedConfig.requireNumbers && !hasNumbers || validatedConfig.requireSymbols && !hasSymbols || validatedConfig.requireUppercase && !hasUppercase || validatedConfig.requireLowercase && !hasLowercase) {
|
|
570
|
+
score = Math.min(score, 2);
|
|
571
|
+
}
|
|
572
|
+
if (effectiveEntropy < 50) {
|
|
573
|
+
suggestions.push(
|
|
574
|
+
`Increase password entropy (currently ~${Math.round(
|
|
575
|
+
effectiveEntropy
|
|
576
|
+
)} bits)`
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
const isStrong = hasMinLength && (!validatedConfig.requireNumbers || hasNumbers) && (!validatedConfig.requireSymbols || hasSymbols) && (!validatedConfig.requireUppercase || hasUppercase) && (!validatedConfig.requireLowercase || hasLowercase) && score >= 3;
|
|
580
|
+
return PasswordStrengthSchema.parse({
|
|
581
|
+
score,
|
|
582
|
+
feedback: {
|
|
583
|
+
warning,
|
|
584
|
+
suggestions: suggestions.length > 0 ? suggestions : void 0
|
|
585
|
+
},
|
|
586
|
+
isStrong
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
function generateSecurePassword(config) {
|
|
590
|
+
const validatedConfig = ConfigSchema.parse({
|
|
591
|
+
minimumPasswordLength: config?.minimumPasswordLength || 12,
|
|
592
|
+
requireNumbers: config?.requireNumbers !== void 0 ? config.requireNumbers : true,
|
|
593
|
+
requireSymbols: config?.requireSymbols !== void 0 ? config.requireSymbols : true,
|
|
594
|
+
requireUppercase: config?.requireUppercase !== void 0 ? config.requireUppercase : true,
|
|
595
|
+
requireLowercase: config?.requireLowercase !== void 0 ? config.requireLowercase : true
|
|
596
|
+
});
|
|
597
|
+
const length = validatedConfig.minimumPasswordLength;
|
|
598
|
+
const lowercaseChars = "abcdefghijklmnopqrstuvwxyz";
|
|
599
|
+
const uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
600
|
+
const numbers = "0123456789";
|
|
601
|
+
const symbols = "!@#$%^&*()_+-=[]{}|;:,.<>?";
|
|
602
|
+
let characters = "";
|
|
603
|
+
let password = "";
|
|
604
|
+
if (validatedConfig.requireLowercase) characters += lowercaseChars;
|
|
605
|
+
if (validatedConfig.requireUppercase) characters += uppercaseChars;
|
|
606
|
+
if (validatedConfig.requireNumbers) characters += numbers;
|
|
607
|
+
if (validatedConfig.requireSymbols) characters += symbols;
|
|
608
|
+
if (validatedConfig.requireLowercase) {
|
|
609
|
+
password += lowercaseChars.charAt(
|
|
610
|
+
Math.floor(Math.random() * lowercaseChars.length)
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
if (validatedConfig.requireUppercase) {
|
|
614
|
+
password += uppercaseChars.charAt(
|
|
615
|
+
Math.floor(Math.random() * uppercaseChars.length)
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
if (validatedConfig.requireNumbers) {
|
|
619
|
+
password += numbers.charAt(Math.floor(Math.random() * numbers.length));
|
|
620
|
+
}
|
|
621
|
+
if (validatedConfig.requireSymbols) {
|
|
622
|
+
password += symbols.charAt(Math.floor(Math.random() * symbols.length));
|
|
623
|
+
}
|
|
624
|
+
for (let i = password.length; i < length; i++) {
|
|
625
|
+
password += characters.charAt(
|
|
626
|
+
Math.floor(Math.random() * characters.length)
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
return shuffleString(password);
|
|
630
|
+
}
|
|
631
|
+
function shuffleString(str) {
|
|
632
|
+
const array = str.split("");
|
|
633
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
634
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
635
|
+
[array[i], array[j]] = [array[j], array[i]];
|
|
636
|
+
}
|
|
637
|
+
return array.join("");
|
|
638
|
+
}
|
|
639
|
+
export {
|
|
640
|
+
ConfigSchema,
|
|
641
|
+
EncryptionOptionsSchema,
|
|
642
|
+
HashOptionsSchema,
|
|
643
|
+
PasswordHashOptionsSchema,
|
|
644
|
+
PasswordStrengthSchema,
|
|
645
|
+
SecurityLevel,
|
|
646
|
+
TimingSafeOptionsSchema,
|
|
647
|
+
analyzePasswordPatterns,
|
|
648
|
+
calculatePasswordEntropy,
|
|
649
|
+
checkPasswordStrength,
|
|
650
|
+
decrypt,
|
|
651
|
+
encrypt,
|
|
652
|
+
generateKey,
|
|
653
|
+
generateRandomBytes,
|
|
654
|
+
generateSecurePassword,
|
|
655
|
+
getSecurityLevel,
|
|
656
|
+
hash,
|
|
657
|
+
hashPassword,
|
|
658
|
+
hmac,
|
|
659
|
+
timingSafeEqual,
|
|
660
|
+
verifyHash,
|
|
661
|
+
verifyPassword
|
|
662
|
+
};
|