inslash 1.1.0 → 1.2.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/README.md +36 -0
- package/index.js +161 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,8 +35,44 @@ const verifyResult = await verify("myPassword", result.passport, secret);
|
|
|
35
35
|
console.log(verifyResult.valid); // true or false
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
## API Mode (Hosted Hashing)
|
|
39
|
+
|
|
40
|
+
`inslash` can connect to a hosted API for password hashing, with automatic fallback to local crypto if the API is unavailable.
|
|
41
|
+
|
|
42
|
+
### Setup
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
const inslash = require('inslash');
|
|
46
|
+
|
|
47
|
+
// Configure once with your API key
|
|
48
|
+
inslash.configure({
|
|
49
|
+
apiKey: 'inslash_your_api_key_here',
|
|
50
|
+
apiUrl: 'https://api.inslash.com' // or http://localhost:3001 for testing
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Now hash() and verify() automatically use the API
|
|
54
|
+
const result = await inslash.hash('myPassword');
|
|
55
|
+
const verified = await inslash.verify('myPassword', result.passport);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### How It Works
|
|
59
|
+
|
|
60
|
+
1. **API First**: When configured, `hash()` and `verify()` call your hosted API
|
|
61
|
+
2. **Silent Fallback**: If the API is down or slow, falls back to local crypto automatically
|
|
62
|
+
3. **Zero Config Local**: If not configured, uses local crypto only (no secret needed from you)
|
|
63
|
+
|
|
64
|
+
### Get an API Key
|
|
65
|
+
|
|
66
|
+
Visit [https://inslash.com](https://inslash.com) to create a project and get your API key.
|
|
67
|
+
|
|
38
68
|
## API
|
|
39
69
|
|
|
70
|
+
### `configure(options)`
|
|
71
|
+
- `options.apiKey` (string): Your Inslash API key.
|
|
72
|
+
- `options.apiUrl` (string): API endpoint URL.
|
|
73
|
+
- **Returns:** Current configuration object.
|
|
74
|
+
- **Note:** Call this once before using `hash()` or `verify()` to enable API mode.
|
|
75
|
+
|
|
40
76
|
### `async hash(value, secret, options?)`
|
|
41
77
|
- `value` (string): The value to hash.
|
|
42
78
|
- `secret` (string): Secret key for HMAC.
|
package/index.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
const crypto = require("crypto");
|
|
2
|
+
const https = require("https");
|
|
3
|
+
const http = require("http");
|
|
4
|
+
|
|
5
|
+
// Module-level configuration for API mode
|
|
6
|
+
let CONFIG = {
|
|
7
|
+
apiKey: null,
|
|
8
|
+
apiUrl: null,
|
|
9
|
+
strictMode: false // If true, throw error instead of falling back to local
|
|
10
|
+
};
|
|
2
11
|
|
|
3
12
|
const DEFAULTS = {
|
|
4
13
|
saltLength: 16,
|
|
@@ -20,7 +29,7 @@ const generateApiKey = (options = {}) => {
|
|
|
20
29
|
length = 32,
|
|
21
30
|
encoding = "hex"
|
|
22
31
|
} = options;
|
|
23
|
-
|
|
32
|
+
|
|
24
33
|
const random = crypto.randomBytes(length).toString(encoding);
|
|
25
34
|
return prefix ? `${prefix}_${random}` : random;
|
|
26
35
|
};
|
|
@@ -38,7 +47,7 @@ const hashWithSalt = async (value, salt, secret, options) => {
|
|
|
38
47
|
console.timeEnd(`Hash operation (${iterations} iterations)`);
|
|
39
48
|
|
|
40
49
|
const result = digest.toString(encoding).slice(0, hashLength);
|
|
41
|
-
|
|
50
|
+
|
|
42
51
|
return {
|
|
43
52
|
hash: result,
|
|
44
53
|
timing: iterations, // For informational purposes
|
|
@@ -60,10 +69,10 @@ const encodePassport = (meta) => {
|
|
|
60
69
|
meta.hash,
|
|
61
70
|
history
|
|
62
71
|
];
|
|
63
|
-
|
|
72
|
+
|
|
64
73
|
// Add optional metadata if present
|
|
65
74
|
if (meta.encoding) parts.push(meta.encoding);
|
|
66
|
-
|
|
75
|
+
|
|
67
76
|
return parts.join("$");
|
|
68
77
|
};
|
|
69
78
|
|
|
@@ -71,13 +80,15 @@ const encodePassport = (meta) => {
|
|
|
71
80
|
const decodePassport = (passport) => {
|
|
72
81
|
const parts = passport.split("$");
|
|
73
82
|
if (parts[1] !== "inslash") throw new Error("Invalid passport format");
|
|
74
|
-
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
|
|
84
|
+
// Detect format: check if parts[2] is a numeric version or an algorithm name
|
|
85
|
+
// Legacy format: $inslash$algorithm$iterations$...
|
|
86
|
+
// New format: $inslash$version$algorithm$iterations$...
|
|
87
|
+
const isLegacyFormat = SUPPORTED_ALGORITHMS.includes(parts[2]);
|
|
88
|
+
|
|
89
|
+
if (isLegacyFormat) {
|
|
90
|
+
// Legacy format without explicit version
|
|
91
|
+
const [, , algorithm, iterations, saltLength, hashLength, salt, hash, history] = parts;
|
|
81
92
|
return {
|
|
82
93
|
version: "1",
|
|
83
94
|
algorithm,
|
|
@@ -86,11 +97,12 @@ const decodePassport = (passport) => {
|
|
|
86
97
|
hashLength: Number(hashLength),
|
|
87
98
|
salt,
|
|
88
99
|
hash,
|
|
89
|
-
history: JSON.parse(Buffer.from(history, "base64").toString())
|
|
100
|
+
history: history ? JSON.parse(Buffer.from(history, "base64").toString()) : []
|
|
90
101
|
};
|
|
91
102
|
} else {
|
|
92
|
-
// New format with
|
|
93
|
-
const
|
|
103
|
+
// New format with explicit version
|
|
104
|
+
const version = parts[2];
|
|
105
|
+
const [, , , algorithm, iterations, saltLength, hashLength, salt, hash, history, encoding] = parts;
|
|
94
106
|
return {
|
|
95
107
|
version,
|
|
96
108
|
algorithm,
|
|
@@ -99,32 +111,111 @@ const decodePassport = (passport) => {
|
|
|
99
111
|
hashLength: Number(hashLength),
|
|
100
112
|
salt,
|
|
101
113
|
hash,
|
|
102
|
-
history: JSON.parse(Buffer.from(history, "base64").toString()),
|
|
114
|
+
history: history ? JSON.parse(Buffer.from(history, "base64").toString()) : [],
|
|
103
115
|
encoding: encoding || "hex"
|
|
104
116
|
};
|
|
105
117
|
}
|
|
106
118
|
};
|
|
107
119
|
|
|
108
|
-
//
|
|
120
|
+
// Helper: Call API endpoint
|
|
121
|
+
const callAPI = (endpoint, body) => {
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
const url = new URL(endpoint, CONFIG.apiUrl);
|
|
124
|
+
const client = url.protocol === 'https:' ? https : http;
|
|
125
|
+
|
|
126
|
+
const postData = JSON.stringify(body);
|
|
127
|
+
const options = {
|
|
128
|
+
hostname: url.hostname,
|
|
129
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
130
|
+
path: url.pathname,
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: {
|
|
133
|
+
'Content-Type': 'application/json',
|
|
134
|
+
'x-api-key': CONFIG.apiKey,
|
|
135
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
136
|
+
},
|
|
137
|
+
timeout: 10000
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const req = client.request(options, (res) => {
|
|
141
|
+
let data = '';
|
|
142
|
+
res.on('data', chunk => data += chunk);
|
|
143
|
+
res.on('end', () => {
|
|
144
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
145
|
+
try {
|
|
146
|
+
resolve(JSON.parse(data));
|
|
147
|
+
} catch (e) {
|
|
148
|
+
reject(new Error('Invalid JSON response'));
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
reject(new Error(`API error: ${res.statusCode}`));
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
req.on('error', reject);
|
|
157
|
+
req.on('timeout', () => {
|
|
158
|
+
req.destroy();
|
|
159
|
+
reject(new Error('API request timeout'));
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
req.write(postData);
|
|
163
|
+
req.end();
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Configure API mode
|
|
168
|
+
const configure = (options = {}) => {
|
|
169
|
+
const { apiKey, apiUrl, strictMode } = options;
|
|
170
|
+
|
|
171
|
+
if (apiKey) CONFIG.apiKey = apiKey;
|
|
172
|
+
if (apiUrl) CONFIG.apiUrl = apiUrl;
|
|
173
|
+
if (typeof strictMode !== 'undefined') CONFIG.strictMode = strictMode;
|
|
174
|
+
|
|
175
|
+
return CONFIG;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Enhanced: Hash function with more options and API support
|
|
109
179
|
const hash = async (value, secret, opts = {}) => {
|
|
110
|
-
if (!secret) throw new Error("Secret key is required");
|
|
111
180
|
if (typeof value !== "string" || !value) throw new Error("Value to hash must be a non-empty string");
|
|
112
|
-
|
|
181
|
+
|
|
182
|
+
// API Mode: Try API first if configured
|
|
183
|
+
if (CONFIG.apiKey && CONFIG.apiUrl) {
|
|
184
|
+
try {
|
|
185
|
+
const apiResult = await callAPI('/api/hash', {
|
|
186
|
+
value,
|
|
187
|
+
secret,
|
|
188
|
+
options: opts
|
|
189
|
+
});
|
|
190
|
+
return apiResult;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
// In strict mode, throw the error instead of falling back
|
|
193
|
+
if (CONFIG.strictMode) {
|
|
194
|
+
throw new Error(`API hash failed: ${error.message}`);
|
|
195
|
+
}
|
|
196
|
+
// Silent fallback to local crypto
|
|
197
|
+
console.warn('API hash failed, falling back to local:', error.message);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Local Mode: Original crypto implementation
|
|
202
|
+
if (!secret) throw new Error("Secret key is required");
|
|
203
|
+
|
|
113
204
|
// Validate algorithm
|
|
114
205
|
if (opts.algorithm && !SUPPORTED_ALGORITHMS.includes(opts.algorithm)) {
|
|
115
206
|
throw new Error(`Unsupported algorithm: ${opts.algorithm}. Supported: ${SUPPORTED_ALGORITHMS.join(", ")}`);
|
|
116
207
|
}
|
|
117
|
-
|
|
208
|
+
|
|
118
209
|
// Validate encoding
|
|
119
210
|
if (opts.encoding && !SUPPORTED_ENCODINGS.includes(opts.encoding)) {
|
|
120
211
|
throw new Error(`Unsupported encoding: ${opts.encoding}. Supported: ${SUPPORTED_ENCODINGS.join(", ")}`);
|
|
121
212
|
}
|
|
122
|
-
|
|
213
|
+
|
|
123
214
|
const options = { ...DEFAULTS, ...opts };
|
|
124
215
|
const salt = createSalt(options.saltLength);
|
|
125
216
|
const pepper = process.env.HASH_PEPPER || "";
|
|
126
217
|
const valueWithPepper = value + pepper;
|
|
127
|
-
|
|
218
|
+
|
|
128
219
|
const { hash: hashed } = await hashWithSalt(valueWithPepper, salt, secret, options);
|
|
129
220
|
|
|
130
221
|
const meta = {
|
|
@@ -152,8 +243,29 @@ const hash = async (value, secret, opts = {}) => {
|
|
|
152
243
|
};
|
|
153
244
|
};
|
|
154
245
|
|
|
155
|
-
// Enhanced: Verify with more detailed response
|
|
246
|
+
// Enhanced: Verify with more detailed response and API support
|
|
156
247
|
const verify = async (value, passport, secret, opts = {}) => {
|
|
248
|
+
// API Mode: Try API first if configured
|
|
249
|
+
if (CONFIG.apiKey && CONFIG.apiUrl) {
|
|
250
|
+
try {
|
|
251
|
+
const apiResult = await callAPI('/api/verify', {
|
|
252
|
+
value,
|
|
253
|
+
passport,
|
|
254
|
+
secret,
|
|
255
|
+
options: opts
|
|
256
|
+
});
|
|
257
|
+
return apiResult;
|
|
258
|
+
} catch (error) {
|
|
259
|
+
// In strict mode, throw the error instead of falling back
|
|
260
|
+
if (CONFIG.strictMode) {
|
|
261
|
+
throw new Error(`API verify failed: ${error.message}`);
|
|
262
|
+
}
|
|
263
|
+
// Silent fallback to local crypto
|
|
264
|
+
console.warn('API verify failed, falling back to local:', error.message);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Local Mode: Original crypto implementation
|
|
157
269
|
const meta = decodePassport(passport);
|
|
158
270
|
const options = {
|
|
159
271
|
algorithm: meta.algorithm,
|
|
@@ -163,21 +275,21 @@ const verify = async (value, passport, secret, opts = {}) => {
|
|
|
163
275
|
encoding: meta.encoding || "hex",
|
|
164
276
|
...opts
|
|
165
277
|
};
|
|
166
|
-
|
|
278
|
+
|
|
167
279
|
const pepper = process.env.HASH_PEPPER || "";
|
|
168
280
|
const valueWithPepper = value + pepper;
|
|
169
|
-
|
|
281
|
+
|
|
170
282
|
const { hash: computed } = await hashWithSalt(valueWithPepper, meta.salt, secret, options);
|
|
171
|
-
|
|
283
|
+
|
|
172
284
|
// Use timing-safe comparison
|
|
173
285
|
const valid = crypto.timingSafeEqual(
|
|
174
286
|
Buffer.from(computed, options.encoding),
|
|
175
287
|
Buffer.from(meta.hash, meta.encoding || "hex")
|
|
176
288
|
);
|
|
177
|
-
|
|
289
|
+
|
|
178
290
|
let needsUpgrade = false;
|
|
179
291
|
let upgradeReasons = [];
|
|
180
|
-
|
|
292
|
+
|
|
181
293
|
if (opts.iterations && opts.iterations > meta.iterations) {
|
|
182
294
|
needsUpgrade = true;
|
|
183
295
|
upgradeReasons.push(`iterations (${meta.iterations} -> ${opts.iterations})`);
|
|
@@ -190,10 +302,10 @@ const verify = async (value, passport, secret, opts = {}) => {
|
|
|
190
302
|
needsUpgrade = true;
|
|
191
303
|
upgradeReasons.push(`encoding (${meta.encoding} -> ${opts.encoding})`);
|
|
192
304
|
}
|
|
193
|
-
|
|
305
|
+
|
|
194
306
|
let upgradedPassport = null;
|
|
195
307
|
let upgradedMetadata = null;
|
|
196
|
-
|
|
308
|
+
|
|
197
309
|
if (valid && needsUpgrade) {
|
|
198
310
|
const newMeta = { ...meta, ...opts };
|
|
199
311
|
newMeta.history = (meta.history || []).concat([
|
|
@@ -205,14 +317,14 @@ const verify = async (value, passport, secret, opts = {}) => {
|
|
|
205
317
|
reason: "security upgrade"
|
|
206
318
|
}
|
|
207
319
|
]);
|
|
208
|
-
|
|
320
|
+
|
|
209
321
|
const newSalt = createSalt(newMeta.saltLength);
|
|
210
322
|
const { hash: newHash } = await hashWithSalt(valueWithPepper, newSalt, secret, newMeta);
|
|
211
|
-
|
|
323
|
+
|
|
212
324
|
newMeta.salt = newSalt;
|
|
213
325
|
newMeta.hash = newHash;
|
|
214
326
|
newMeta.version = "2";
|
|
215
|
-
|
|
327
|
+
|
|
216
328
|
upgradedPassport = encodePassport(newMeta);
|
|
217
329
|
upgradedMetadata = {
|
|
218
330
|
algorithm: newMeta.algorithm,
|
|
@@ -220,7 +332,7 @@ const verify = async (value, passport, secret, opts = {}) => {
|
|
|
220
332
|
encoding: newMeta.encoding
|
|
221
333
|
};
|
|
222
334
|
}
|
|
223
|
-
|
|
335
|
+
|
|
224
336
|
return {
|
|
225
337
|
valid,
|
|
226
338
|
needsUpgrade,
|
|
@@ -281,14 +393,14 @@ const comparePassports = (passport1, passport2) => {
|
|
|
281
393
|
try {
|
|
282
394
|
const meta1 = decodePassport(passport1);
|
|
283
395
|
const meta2 = decodePassport(passport2);
|
|
284
|
-
|
|
396
|
+
|
|
285
397
|
return {
|
|
286
398
|
sameAlgorithm: meta1.algorithm === meta2.algorithm,
|
|
287
399
|
sameIterations: meta1.iterations === meta2.iterations,
|
|
288
400
|
sameSalt: meta1.salt === meta2.salt,
|
|
289
401
|
sameHash: meta1.hash === meta2.hash,
|
|
290
402
|
sameEncoding: (meta1.encoding || "hex") === (meta2.encoding || "hex"),
|
|
291
|
-
|
|
403
|
+
完全相同: meta1.hash === meta2.hash && meta1.salt === meta2.salt
|
|
292
404
|
};
|
|
293
405
|
} catch (error) {
|
|
294
406
|
return {
|
|
@@ -304,16 +416,16 @@ const estimateSecurity = (passport) => {
|
|
|
304
416
|
const meta = decodePassport(passport);
|
|
305
417
|
const now = new Date();
|
|
306
418
|
const year = now.getFullYear();
|
|
307
|
-
|
|
419
|
+
|
|
308
420
|
// Rough estimate of security level
|
|
309
421
|
let score = 0;
|
|
310
422
|
let recommendations = [];
|
|
311
|
-
|
|
423
|
+
|
|
312
424
|
// Algorithm score
|
|
313
425
|
if (meta.algorithm === "sha512") score += 40;
|
|
314
426
|
else if (meta.algorithm === "sha384") score += 35;
|
|
315
427
|
else if (meta.algorithm === "sha256") score += 30;
|
|
316
|
-
|
|
428
|
+
|
|
317
429
|
// Iterations score (based on year)
|
|
318
430
|
if (meta.iterations >= 300000) score += 40;
|
|
319
431
|
else if (meta.iterations >= 200000) score += 35;
|
|
@@ -323,7 +435,7 @@ const estimateSecurity = (passport) => {
|
|
|
323
435
|
score += 15;
|
|
324
436
|
recommendations.push("Increase iterations (current: " + meta.iterations + ")");
|
|
325
437
|
}
|
|
326
|
-
|
|
438
|
+
|
|
327
439
|
// Salt length
|
|
328
440
|
if (meta.saltLength >= 32) score += 20;
|
|
329
441
|
else if (meta.saltLength >= 16) score += 15;
|
|
@@ -331,16 +443,16 @@ const estimateSecurity = (passport) => {
|
|
|
331
443
|
score += 5;
|
|
332
444
|
recommendations.push("Increase salt length (current: " + meta.saltLength + ")");
|
|
333
445
|
}
|
|
334
|
-
|
|
446
|
+
|
|
335
447
|
// Hash length
|
|
336
448
|
if (meta.hashLength >= 32) score += 10;
|
|
337
|
-
|
|
449
|
+
|
|
338
450
|
let level = "Weak";
|
|
339
451
|
if (score >= 90) level = "Excellent";
|
|
340
452
|
else if (score >= 75) level = "Strong";
|
|
341
453
|
else if (score >= 60) level = "Good";
|
|
342
454
|
else if (score >= 40) level = "Fair";
|
|
343
|
-
|
|
455
|
+
|
|
344
456
|
return {
|
|
345
457
|
score,
|
|
346
458
|
level,
|
|
@@ -368,19 +480,22 @@ module.exports = {
|
|
|
368
480
|
verify,
|
|
369
481
|
encodePassport,
|
|
370
482
|
decodePassport,
|
|
371
|
-
|
|
483
|
+
|
|
484
|
+
// API configuration
|
|
485
|
+
configure,
|
|
486
|
+
|
|
372
487
|
// New enhanced functions
|
|
373
488
|
batchVerify,
|
|
374
489
|
inspectPassport,
|
|
375
490
|
comparePassports,
|
|
376
491
|
estimateSecurity,
|
|
377
492
|
generateApiKey,
|
|
378
|
-
|
|
493
|
+
|
|
379
494
|
// Utilities
|
|
380
495
|
DEFAULTS,
|
|
381
496
|
SUPPORTED_ALGORITHMS,
|
|
382
497
|
SUPPORTED_ENCODINGS,
|
|
383
|
-
|
|
498
|
+
|
|
384
499
|
// Version info
|
|
385
|
-
VERSION: "1.
|
|
500
|
+
VERSION: "1.2.0"
|
|
386
501
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "inslash",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.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",
|