inslash 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +45 -42
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -20,7 +20,7 @@ const generateApiKey = (options = {}) => {
|
|
|
20
20
|
length = 32,
|
|
21
21
|
encoding = "hex"
|
|
22
22
|
} = options;
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
const random = crypto.randomBytes(length).toString(encoding);
|
|
25
25
|
return prefix ? `${prefix}_${random}` : random;
|
|
26
26
|
};
|
|
@@ -38,7 +38,7 @@ const hashWithSalt = async (value, salt, secret, options) => {
|
|
|
38
38
|
console.timeEnd(`Hash operation (${iterations} iterations)`);
|
|
39
39
|
|
|
40
40
|
const result = digest.toString(encoding).slice(0, hashLength);
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
return {
|
|
43
43
|
hash: result,
|
|
44
44
|
timing: iterations, // For informational purposes
|
|
@@ -60,10 +60,10 @@ const encodePassport = (meta) => {
|
|
|
60
60
|
meta.hash,
|
|
61
61
|
history
|
|
62
62
|
];
|
|
63
|
-
|
|
63
|
+
|
|
64
64
|
// Add optional metadata if present
|
|
65
65
|
if (meta.encoding) parts.push(meta.encoding);
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
return parts.join("$");
|
|
68
68
|
};
|
|
69
69
|
|
|
@@ -71,13 +71,15 @@ const encodePassport = (meta) => {
|
|
|
71
71
|
const decodePassport = (passport) => {
|
|
72
72
|
const parts = passport.split("$");
|
|
73
73
|
if (parts[1] !== "inslash") throw new Error("Invalid passport format");
|
|
74
|
-
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
|
|
75
|
+
// Detect format: check if parts[2] is a numeric version or an algorithm name
|
|
76
|
+
// Legacy format: $inslash$algorithm$iterations$...
|
|
77
|
+
// New format: $inslash$version$algorithm$iterations$...
|
|
78
|
+
const isLegacyFormat = SUPPORTED_ALGORITHMS.includes(parts[2]);
|
|
79
|
+
|
|
80
|
+
if (isLegacyFormat) {
|
|
81
|
+
// Legacy format without explicit version
|
|
82
|
+
const [, , algorithm, iterations, saltLength, hashLength, salt, hash, history] = parts;
|
|
81
83
|
return {
|
|
82
84
|
version: "1",
|
|
83
85
|
algorithm,
|
|
@@ -86,11 +88,12 @@ const decodePassport = (passport) => {
|
|
|
86
88
|
hashLength: Number(hashLength),
|
|
87
89
|
salt,
|
|
88
90
|
hash,
|
|
89
|
-
history: JSON.parse(Buffer.from(history, "base64").toString())
|
|
91
|
+
history: history ? JSON.parse(Buffer.from(history, "base64").toString()) : []
|
|
90
92
|
};
|
|
91
93
|
} else {
|
|
92
|
-
// New format with
|
|
93
|
-
const
|
|
94
|
+
// New format with explicit version
|
|
95
|
+
const version = parts[2];
|
|
96
|
+
const [, , , algorithm, iterations, saltLength, hashLength, salt, hash, history, encoding] = parts;
|
|
94
97
|
return {
|
|
95
98
|
version,
|
|
96
99
|
algorithm,
|
|
@@ -99,7 +102,7 @@ const decodePassport = (passport) => {
|
|
|
99
102
|
hashLength: Number(hashLength),
|
|
100
103
|
salt,
|
|
101
104
|
hash,
|
|
102
|
-
history: JSON.parse(Buffer.from(history, "base64").toString()),
|
|
105
|
+
history: history ? JSON.parse(Buffer.from(history, "base64").toString()) : [],
|
|
103
106
|
encoding: encoding || "hex"
|
|
104
107
|
};
|
|
105
108
|
}
|
|
@@ -109,22 +112,22 @@ const decodePassport = (passport) => {
|
|
|
109
112
|
const hash = async (value, secret, opts = {}) => {
|
|
110
113
|
if (!secret) throw new Error("Secret key is required");
|
|
111
114
|
if (typeof value !== "string" || !value) throw new Error("Value to hash must be a non-empty string");
|
|
112
|
-
|
|
115
|
+
|
|
113
116
|
// Validate algorithm
|
|
114
117
|
if (opts.algorithm && !SUPPORTED_ALGORITHMS.includes(opts.algorithm)) {
|
|
115
118
|
throw new Error(`Unsupported algorithm: ${opts.algorithm}. Supported: ${SUPPORTED_ALGORITHMS.join(", ")}`);
|
|
116
119
|
}
|
|
117
|
-
|
|
120
|
+
|
|
118
121
|
// Validate encoding
|
|
119
122
|
if (opts.encoding && !SUPPORTED_ENCODINGS.includes(opts.encoding)) {
|
|
120
123
|
throw new Error(`Unsupported encoding: ${opts.encoding}. Supported: ${SUPPORTED_ENCODINGS.join(", ")}`);
|
|
121
124
|
}
|
|
122
|
-
|
|
125
|
+
|
|
123
126
|
const options = { ...DEFAULTS, ...opts };
|
|
124
127
|
const salt = createSalt(options.saltLength);
|
|
125
128
|
const pepper = process.env.HASH_PEPPER || "";
|
|
126
129
|
const valueWithPepper = value + pepper;
|
|
127
|
-
|
|
130
|
+
|
|
128
131
|
const { hash: hashed } = await hashWithSalt(valueWithPepper, salt, secret, options);
|
|
129
132
|
|
|
130
133
|
const meta = {
|
|
@@ -163,21 +166,21 @@ const verify = async (value, passport, secret, opts = {}) => {
|
|
|
163
166
|
encoding: meta.encoding || "hex",
|
|
164
167
|
...opts
|
|
165
168
|
};
|
|
166
|
-
|
|
169
|
+
|
|
167
170
|
const pepper = process.env.HASH_PEPPER || "";
|
|
168
171
|
const valueWithPepper = value + pepper;
|
|
169
|
-
|
|
172
|
+
|
|
170
173
|
const { hash: computed } = await hashWithSalt(valueWithPepper, meta.salt, secret, options);
|
|
171
|
-
|
|
174
|
+
|
|
172
175
|
// Use timing-safe comparison
|
|
173
176
|
const valid = crypto.timingSafeEqual(
|
|
174
177
|
Buffer.from(computed, options.encoding),
|
|
175
178
|
Buffer.from(meta.hash, meta.encoding || "hex")
|
|
176
179
|
);
|
|
177
|
-
|
|
180
|
+
|
|
178
181
|
let needsUpgrade = false;
|
|
179
182
|
let upgradeReasons = [];
|
|
180
|
-
|
|
183
|
+
|
|
181
184
|
if (opts.iterations && opts.iterations > meta.iterations) {
|
|
182
185
|
needsUpgrade = true;
|
|
183
186
|
upgradeReasons.push(`iterations (${meta.iterations} -> ${opts.iterations})`);
|
|
@@ -190,10 +193,10 @@ const verify = async (value, passport, secret, opts = {}) => {
|
|
|
190
193
|
needsUpgrade = true;
|
|
191
194
|
upgradeReasons.push(`encoding (${meta.encoding} -> ${opts.encoding})`);
|
|
192
195
|
}
|
|
193
|
-
|
|
196
|
+
|
|
194
197
|
let upgradedPassport = null;
|
|
195
198
|
let upgradedMetadata = null;
|
|
196
|
-
|
|
199
|
+
|
|
197
200
|
if (valid && needsUpgrade) {
|
|
198
201
|
const newMeta = { ...meta, ...opts };
|
|
199
202
|
newMeta.history = (meta.history || []).concat([
|
|
@@ -205,14 +208,14 @@ const verify = async (value, passport, secret, opts = {}) => {
|
|
|
205
208
|
reason: "security upgrade"
|
|
206
209
|
}
|
|
207
210
|
]);
|
|
208
|
-
|
|
211
|
+
|
|
209
212
|
const newSalt = createSalt(newMeta.saltLength);
|
|
210
213
|
const { hash: newHash } = await hashWithSalt(valueWithPepper, newSalt, secret, newMeta);
|
|
211
|
-
|
|
214
|
+
|
|
212
215
|
newMeta.salt = newSalt;
|
|
213
216
|
newMeta.hash = newHash;
|
|
214
217
|
newMeta.version = "2";
|
|
215
|
-
|
|
218
|
+
|
|
216
219
|
upgradedPassport = encodePassport(newMeta);
|
|
217
220
|
upgradedMetadata = {
|
|
218
221
|
algorithm: newMeta.algorithm,
|
|
@@ -220,7 +223,7 @@ const verify = async (value, passport, secret, opts = {}) => {
|
|
|
220
223
|
encoding: newMeta.encoding
|
|
221
224
|
};
|
|
222
225
|
}
|
|
223
|
-
|
|
226
|
+
|
|
224
227
|
return {
|
|
225
228
|
valid,
|
|
226
229
|
needsUpgrade,
|
|
@@ -281,14 +284,14 @@ const comparePassports = (passport1, passport2) => {
|
|
|
281
284
|
try {
|
|
282
285
|
const meta1 = decodePassport(passport1);
|
|
283
286
|
const meta2 = decodePassport(passport2);
|
|
284
|
-
|
|
287
|
+
|
|
285
288
|
return {
|
|
286
289
|
sameAlgorithm: meta1.algorithm === meta2.algorithm,
|
|
287
290
|
sameIterations: meta1.iterations === meta2.iterations,
|
|
288
291
|
sameSalt: meta1.salt === meta2.salt,
|
|
289
292
|
sameHash: meta1.hash === meta2.hash,
|
|
290
293
|
sameEncoding: (meta1.encoding || "hex") === (meta2.encoding || "hex"),
|
|
291
|
-
|
|
294
|
+
完全相同: meta1.hash === meta2.hash && meta1.salt === meta2.salt
|
|
292
295
|
};
|
|
293
296
|
} catch (error) {
|
|
294
297
|
return {
|
|
@@ -304,16 +307,16 @@ const estimateSecurity = (passport) => {
|
|
|
304
307
|
const meta = decodePassport(passport);
|
|
305
308
|
const now = new Date();
|
|
306
309
|
const year = now.getFullYear();
|
|
307
|
-
|
|
310
|
+
|
|
308
311
|
// Rough estimate of security level
|
|
309
312
|
let score = 0;
|
|
310
313
|
let recommendations = [];
|
|
311
|
-
|
|
314
|
+
|
|
312
315
|
// Algorithm score
|
|
313
316
|
if (meta.algorithm === "sha512") score += 40;
|
|
314
317
|
else if (meta.algorithm === "sha384") score += 35;
|
|
315
318
|
else if (meta.algorithm === "sha256") score += 30;
|
|
316
|
-
|
|
319
|
+
|
|
317
320
|
// Iterations score (based on year)
|
|
318
321
|
if (meta.iterations >= 300000) score += 40;
|
|
319
322
|
else if (meta.iterations >= 200000) score += 35;
|
|
@@ -323,7 +326,7 @@ const estimateSecurity = (passport) => {
|
|
|
323
326
|
score += 15;
|
|
324
327
|
recommendations.push("Increase iterations (current: " + meta.iterations + ")");
|
|
325
328
|
}
|
|
326
|
-
|
|
329
|
+
|
|
327
330
|
// Salt length
|
|
328
331
|
if (meta.saltLength >= 32) score += 20;
|
|
329
332
|
else if (meta.saltLength >= 16) score += 15;
|
|
@@ -331,16 +334,16 @@ const estimateSecurity = (passport) => {
|
|
|
331
334
|
score += 5;
|
|
332
335
|
recommendations.push("Increase salt length (current: " + meta.saltLength + ")");
|
|
333
336
|
}
|
|
334
|
-
|
|
337
|
+
|
|
335
338
|
// Hash length
|
|
336
339
|
if (meta.hashLength >= 32) score += 10;
|
|
337
|
-
|
|
340
|
+
|
|
338
341
|
let level = "Weak";
|
|
339
342
|
if (score >= 90) level = "Excellent";
|
|
340
343
|
else if (score >= 75) level = "Strong";
|
|
341
344
|
else if (score >= 60) level = "Good";
|
|
342
345
|
else if (score >= 40) level = "Fair";
|
|
343
|
-
|
|
346
|
+
|
|
344
347
|
return {
|
|
345
348
|
score,
|
|
346
349
|
level,
|
|
@@ -368,19 +371,19 @@ module.exports = {
|
|
|
368
371
|
verify,
|
|
369
372
|
encodePassport,
|
|
370
373
|
decodePassport,
|
|
371
|
-
|
|
374
|
+
|
|
372
375
|
// New enhanced functions
|
|
373
376
|
batchVerify,
|
|
374
377
|
inspectPassport,
|
|
375
378
|
comparePassports,
|
|
376
379
|
estimateSecurity,
|
|
377
380
|
generateApiKey,
|
|
378
|
-
|
|
381
|
+
|
|
379
382
|
// Utilities
|
|
380
383
|
DEFAULTS,
|
|
381
384
|
SUPPORTED_ALGORITHMS,
|
|
382
385
|
SUPPORTED_ENCODINGS,
|
|
383
|
-
|
|
386
|
+
|
|
384
387
|
// Version info
|
|
385
388
|
VERSION: "1.1.0"
|
|
386
389
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "inslash",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "A modern, upgradeable, and secure password hashing utility with passport encoding, hash ancestry, and comprehensive security features.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|