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.
Files changed (3) hide show
  1. package/README.md +36 -0
  2. package/index.js +161 -46
  3. 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
- // Check version (new format includes version at index 2)
76
- const version = parts[2] || "1";
77
-
78
- if (version === "1") {
79
- // Legacy format
80
- const [ , , algorithm, iterations, saltLength, hashLength, salt, hash, history ] = parts;
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 encoding
93
- const [ , , , algorithm, iterations, saltLength, hashLength, salt, hash, history, encoding ] = parts;
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
- // Enhanced: Hash function with more options
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
- 完全相同: meta1.hash === meta2.hash && meta1.salt === meta2.salt
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.1.0"
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.0",
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",