inslash 1.0.3 → 1.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.
Files changed (4) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/index.d.ts +122 -0
  3. package/index.js +289 -27
  4. package/package.json +23 -6
package/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ ## [1.1.0] - 2026-02-18
4
+
5
+ ### Added
6
+ - 🚀 **Version 2 passport format** with encoding support
7
+ - 🔐 **API Key generation** utility (`generateApiKey()`)
8
+ - 📊 **Batch verification** for multiple values (`batchVerify()`)
9
+ - 🔍 **Passport inspection** without verification (`inspectPassport()`)
10
+ - ⚖️ **Passport comparison** utility (`comparePassports()`)
11
+ - 📈 **Security strength estimation** (`estimateSecurity()`)
12
+ - 🎨 **Multiple encoding support** (hex, base64, base64url, latin1)
13
+ - ✅ **Algorithm validation** for supported algorithms
14
+ - 📝 **Detailed upgrade reasons** in verify response
15
+ - ⏱️ **Timing information** for debugging
16
+
17
+ ### Enhanced
18
+ - 🔧 More detailed verification response with metadata
19
+ - 📚 Better error messages with suggestions
20
+ - 🔄 Backward compatibility with v1 passports
21
+ - ⚡ Performance improvements in hashWithSalt
22
+
23
+ ### Fixed
24
+ - 🐛 Timing attack protection improvements
25
+ - 🔒 Better input validation
26
+
27
+ ## [1.0.3] - 2026-02-16
28
+ - Initial release with core functionality
package/index.d.ts ADDED
@@ -0,0 +1,122 @@
1
+ declare module 'inslash' {
2
+ export interface HashOptions {
3
+ saltLength?: number;
4
+ hashLength?: number;
5
+ iterations?: number;
6
+ algorithm?: 'sha256' | 'sha512' | 'sha384';
7
+ encoding?: 'hex' | 'base64' | 'base64url' | 'latin1';
8
+ }
9
+
10
+ export interface HashResult {
11
+ passport: string;
12
+ algorithm: string;
13
+ iterations: number;
14
+ saltLength: number;
15
+ hashLength: number;
16
+ salt: string;
17
+ hash: string;
18
+ encoding: string;
19
+ history: Array<{
20
+ date: string;
21
+ algorithm: string;
22
+ iterations: number;
23
+ encoding?: string;
24
+ }>;
25
+ }
26
+
27
+ export interface VerifyResult {
28
+ valid: boolean;
29
+ needsUpgrade: boolean;
30
+ upgradeReasons?: string[];
31
+ upgradedPassport: string | null;
32
+ upgradedMetadata: {
33
+ algorithm: string;
34
+ iterations: number;
35
+ encoding: string;
36
+ } | null;
37
+ metadata: {
38
+ algorithm: string;
39
+ iterations: number;
40
+ encoding: string;
41
+ hashLength: number;
42
+ saltLength: number;
43
+ };
44
+ }
45
+
46
+ export interface InspectResult {
47
+ valid: boolean;
48
+ algorithm?: string;
49
+ iterations?: number;
50
+ saltLength?: number;
51
+ hashLength?: number;
52
+ salt?: string;
53
+ hash?: string;
54
+ history?: any[];
55
+ encoding?: string;
56
+ error?: string;
57
+ }
58
+
59
+ export interface CompareResult {
60
+ sameAlgorithm: boolean;
61
+ sameIterations: boolean;
62
+ sameSalt: boolean;
63
+ sameHash: boolean;
64
+ sameEncoding: boolean;
65
+ 完全相同: boolean;
66
+ error?: string;
67
+ }
68
+
69
+ export interface SecurityEstimate {
70
+ score: number;
71
+ level: 'Excellent' | 'Strong' | 'Good' | 'Fair' | 'Weak' | 'Invalid';
72
+ recommendations: string[];
73
+ metadata: {
74
+ algorithm: string;
75
+ iterations: number;
76
+ saltLength: number;
77
+ hashLength: number;
78
+ };
79
+ error?: string;
80
+ }
81
+
82
+ export interface ApiKeyOptions {
83
+ prefix?: string;
84
+ length?: number;
85
+ encoding?: 'hex' | 'base64' | 'base64url';
86
+ }
87
+
88
+ export function hash(
89
+ value: string,
90
+ secret: string,
91
+ options?: HashOptions
92
+ ): Promise<HashResult>;
93
+
94
+ export function verify(
95
+ value: string,
96
+ passport: string,
97
+ secret: string,
98
+ options?: Partial<HashOptions>
99
+ ): Promise<VerifyResult>;
100
+
101
+ export function encodePassport(meta: any): string;
102
+ export function decodePassport(passport: string): any;
103
+
104
+ // New functions
105
+ export function batchVerify(
106
+ values: string[],
107
+ passport: string,
108
+ secret: string,
109
+ options?: Partial<HashOptions>
110
+ ): Promise<Array<{ value: string; valid: boolean; needsUpgrade?: boolean; error?: string }>>;
111
+
112
+ export function inspectPassport(passport: string): InspectResult;
113
+ export function comparePassports(passport1: string, passport2: string): CompareResult;
114
+ export function estimateSecurity(passport: string): SecurityEstimate;
115
+ export function generateApiKey(options?: ApiKeyOptions): string;
116
+
117
+ // Constants
118
+ export const DEFAULTS: Required<HashOptions>;
119
+ export const SUPPORTED_ALGORITHMS: string[];
120
+ export const SUPPORTED_ENCODINGS: string[];
121
+ export const VERSION: string;
122
+ }
package/index.js CHANGED
@@ -4,27 +4,54 @@ const DEFAULTS = {
4
4
  saltLength: 16,
5
5
  hashLength: 32,
6
6
  iterations: 100_000,
7
- algorithm: "sha256"
7
+ algorithm: "sha256",
8
+ encoding: "hex" // New: support for different encodings
8
9
  };
9
10
 
11
+ const SUPPORTED_ALGORITHMS = ["sha256", "sha512", "sha384"];
12
+ const SUPPORTED_ENCODINGS = ["hex", "base64", "base64url", "latin1"];
13
+
10
14
  const createSalt = (length) => crypto.randomBytes(length).toString("hex");
11
15
 
16
+ // New: Generate a secure API key
17
+ const generateApiKey = (options = {}) => {
18
+ const {
19
+ prefix = "inslash",
20
+ length = 32,
21
+ encoding = "hex"
22
+ } = options;
23
+
24
+ const random = crypto.randomBytes(length).toString(encoding);
25
+ return prefix ? `${prefix}_${random}` : random;
26
+ };
27
+
28
+ // New: Hash with timing attack protection info
12
29
  const hashWithSalt = async (value, salt, secret, options) => {
13
- const { iterations, hashLength, algorithm } = options;
30
+ const { iterations, hashLength, algorithm, encoding = "hex" } = options;
14
31
  let data = value + salt;
15
32
  let digest = Buffer.from(data);
16
33
 
34
+ console.time(`Hash operation (${iterations} iterations)`);
17
35
  for (let i = 0; i < iterations; i++) {
18
36
  digest = crypto.createHmac(algorithm, secret).update(digest).digest();
19
37
  }
38
+ console.timeEnd(`Hash operation (${iterations} iterations)`);
20
39
 
21
- return digest.toString("hex").slice(0, hashLength);
40
+ const result = digest.toString(encoding).slice(0, hashLength);
41
+
42
+ return {
43
+ hash: result,
44
+ timing: iterations, // For informational purposes
45
+ algorithm
46
+ };
22
47
  };
23
48
 
49
+ // Enhanced: Passport encoding with versioning
24
50
  const encodePassport = (meta) => {
25
51
  const history = Buffer.from(JSON.stringify(meta.history || [])).toString("base64");
26
- return [
52
+ const parts = [
27
53
  "$inslash",
54
+ meta.version || "1",
28
55
  meta.algorithm,
29
56
  meta.iterations,
30
57
  meta.saltLength,
@@ -32,45 +59,89 @@ const encodePassport = (meta) => {
32
59
  meta.salt,
33
60
  meta.hash,
34
61
  history
35
- ].join("$");
62
+ ];
63
+
64
+ // Add optional metadata if present
65
+ if (meta.encoding) parts.push(meta.encoding);
66
+
67
+ return parts.join("$");
36
68
  };
37
69
 
70
+ // Enhanced: Decode with backward compatibility
38
71
  const decodePassport = (passport) => {
39
72
  const parts = passport.split("$");
40
73
  if (parts[1] !== "inslash") throw new Error("Invalid passport format");
41
- const [ , , algorithm, iterations, saltLength, hashLength, salt, hash, history ] = parts;
42
- return {
43
- algorithm,
44
- iterations: Number(iterations),
45
- saltLength: Number(saltLength),
46
- hashLength: Number(hashLength),
47
- salt,
48
- hash,
49
- history: JSON.parse(Buffer.from(history, "base64").toString())
50
- };
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;
81
+ return {
82
+ version: "1",
83
+ algorithm,
84
+ iterations: Number(iterations),
85
+ saltLength: Number(saltLength),
86
+ hashLength: Number(hashLength),
87
+ salt,
88
+ hash,
89
+ history: JSON.parse(Buffer.from(history, "base64").toString())
90
+ };
91
+ } else {
92
+ // New format with encoding
93
+ const [ , , , algorithm, iterations, saltLength, hashLength, salt, hash, history, encoding ] = parts;
94
+ return {
95
+ version,
96
+ algorithm,
97
+ iterations: Number(iterations),
98
+ saltLength: Number(saltLength),
99
+ hashLength: Number(hashLength),
100
+ salt,
101
+ hash,
102
+ history: JSON.parse(Buffer.from(history, "base64").toString()),
103
+ encoding: encoding || "hex"
104
+ };
105
+ }
51
106
  };
52
107
 
108
+ // Enhanced: Hash function with more options
53
109
  const hash = async (value, secret, opts = {}) => {
54
110
  if (!secret) throw new Error("Secret key is required");
55
111
  if (typeof value !== "string" || !value) throw new Error("Value to hash must be a non-empty string");
112
+
113
+ // Validate algorithm
114
+ if (opts.algorithm && !SUPPORTED_ALGORITHMS.includes(opts.algorithm)) {
115
+ throw new Error(`Unsupported algorithm: ${opts.algorithm}. Supported: ${SUPPORTED_ALGORITHMS.join(", ")}`);
116
+ }
117
+
118
+ // Validate encoding
119
+ if (opts.encoding && !SUPPORTED_ENCODINGS.includes(opts.encoding)) {
120
+ throw new Error(`Unsupported encoding: ${opts.encoding}. Supported: ${SUPPORTED_ENCODINGS.join(", ")}`);
121
+ }
122
+
56
123
  const options = { ...DEFAULTS, ...opts };
57
124
  const salt = createSalt(options.saltLength);
58
125
  const pepper = process.env.HASH_PEPPER || "";
59
126
  const valueWithPepper = value + pepper;
60
- const hashed = await hashWithSalt(valueWithPepper, salt, secret, options);
127
+
128
+ const { hash: hashed } = await hashWithSalt(valueWithPepper, salt, secret, options);
61
129
 
62
130
  const meta = {
131
+ version: "2",
63
132
  algorithm: options.algorithm,
64
133
  iterations: options.iterations,
65
134
  saltLength: options.saltLength,
66
135
  hashLength: options.hashLength,
67
136
  salt,
68
137
  hash: hashed,
138
+ encoding: options.encoding,
69
139
  history: [
70
140
  {
71
141
  date: new Date().toISOString(),
72
142
  algorithm: options.algorithm,
73
- iterations: options.iterations
143
+ iterations: options.iterations,
144
+ encoding: options.encoding
74
145
  }
75
146
  ]
76
147
  };
@@ -81,6 +152,7 @@ const hash = async (value, secret, opts = {}) => {
81
152
  };
82
153
  };
83
154
 
155
+ // Enhanced: Verify with more detailed response
84
156
  const verify = async (value, passport, secret, opts = {}) => {
85
157
  const meta = decodePassport(passport);
86
158
  const options = {
@@ -88,37 +160,227 @@ const verify = async (value, passport, secret, opts = {}) => {
88
160
  iterations: meta.iterations,
89
161
  saltLength: meta.saltLength,
90
162
  hashLength: meta.hashLength,
163
+ encoding: meta.encoding || "hex",
91
164
  ...opts
92
165
  };
166
+
93
167
  const pepper = process.env.HASH_PEPPER || "";
94
168
  const valueWithPepper = value + pepper;
95
- const computed = await hashWithSalt(valueWithPepper, meta.salt, secret, options);
96
- const valid = crypto.timingSafeEqual(Buffer.from(computed), Buffer.from(meta.hash));
169
+
170
+ const { hash: computed } = await hashWithSalt(valueWithPepper, meta.salt, secret, options);
171
+
172
+ // Use timing-safe comparison
173
+ const valid = crypto.timingSafeEqual(
174
+ Buffer.from(computed, options.encoding),
175
+ Buffer.from(meta.hash, meta.encoding || "hex")
176
+ );
177
+
97
178
  let needsUpgrade = false;
98
- if (opts.iterations && opts.iterations > meta.iterations) needsUpgrade = true;
99
- if (opts.algorithm && opts.algorithm !== meta.algorithm) needsUpgrade = true;
179
+ let upgradeReasons = [];
180
+
181
+ if (opts.iterations && opts.iterations > meta.iterations) {
182
+ needsUpgrade = true;
183
+ upgradeReasons.push(`iterations (${meta.iterations} -> ${opts.iterations})`);
184
+ }
185
+ if (opts.algorithm && opts.algorithm !== meta.algorithm) {
186
+ needsUpgrade = true;
187
+ upgradeReasons.push(`algorithm (${meta.algorithm} -> ${opts.algorithm})`);
188
+ }
189
+ if (opts.encoding && opts.encoding !== meta.encoding) {
190
+ needsUpgrade = true;
191
+ upgradeReasons.push(`encoding (${meta.encoding} -> ${opts.encoding})`);
192
+ }
193
+
100
194
  let upgradedPassport = null;
195
+ let upgradedMetadata = null;
196
+
101
197
  if (valid && needsUpgrade) {
102
198
  const newMeta = { ...meta, ...opts };
103
- newMeta.history = meta.history.concat([
199
+ newMeta.history = (meta.history || []).concat([
104
200
  {
105
201
  date: new Date().toISOString(),
106
202
  algorithm: opts.algorithm || meta.algorithm,
107
- iterations: opts.iterations || meta.iterations
203
+ iterations: opts.iterations || meta.iterations,
204
+ encoding: opts.encoding || meta.encoding,
205
+ reason: "security upgrade"
108
206
  }
109
207
  ]);
208
+
110
209
  const newSalt = createSalt(newMeta.saltLength);
111
- const newHash = await hashWithSalt(valueWithPepper, newSalt, secret, newMeta);
210
+ const { hash: newHash } = await hashWithSalt(valueWithPepper, newSalt, secret, newMeta);
211
+
112
212
  newMeta.salt = newSalt;
113
213
  newMeta.hash = newHash;
214
+ newMeta.version = "2";
215
+
114
216
  upgradedPassport = encodePassport(newMeta);
217
+ upgradedMetadata = {
218
+ algorithm: newMeta.algorithm,
219
+ iterations: newMeta.iterations,
220
+ encoding: newMeta.encoding
221
+ };
222
+ }
223
+
224
+ return {
225
+ valid,
226
+ needsUpgrade,
227
+ upgradeReasons,
228
+ upgradedPassport,
229
+ upgradedMetadata,
230
+ metadata: {
231
+ algorithm: meta.algorithm,
232
+ iterations: meta.iterations,
233
+ encoding: meta.encoding,
234
+ hashLength: meta.hashLength,
235
+ saltLength: meta.saltLength
236
+ }
237
+ };
238
+ };
239
+
240
+ // New: Batch verify multiple values against same passport
241
+ const batchVerify = async (values, passport, secret, opts = {}) => {
242
+ const results = [];
243
+ for (const value of values) {
244
+ try {
245
+ const result = await verify(value, passport, secret, opts);
246
+ results.push({
247
+ value,
248
+ valid: result.valid,
249
+ needsUpgrade: result.needsUpgrade
250
+ });
251
+ } catch (error) {
252
+ results.push({
253
+ value,
254
+ error: error.message,
255
+ valid: false
256
+ });
257
+ }
115
258
  }
116
- return { valid, needsUpgrade, upgradedPassport };
259
+ return results;
117
260
  };
118
261
 
262
+ // New: Extract metadata without verification
263
+ const inspectPassport = (passport) => {
264
+ try {
265
+ const meta = decodePassport(passport);
266
+ return {
267
+ valid: true,
268
+ ...meta,
269
+ history: meta.history || []
270
+ };
271
+ } catch (error) {
272
+ return {
273
+ valid: false,
274
+ error: error.message
275
+ };
276
+ }
277
+ };
278
+
279
+ // New: Compare two passports
280
+ const comparePassports = (passport1, passport2) => {
281
+ try {
282
+ const meta1 = decodePassport(passport1);
283
+ const meta2 = decodePassport(passport2);
284
+
285
+ return {
286
+ sameAlgorithm: meta1.algorithm === meta2.algorithm,
287
+ sameIterations: meta1.iterations === meta2.iterations,
288
+ sameSalt: meta1.salt === meta2.salt,
289
+ sameHash: meta1.hash === meta2.hash,
290
+ sameEncoding: (meta1.encoding || "hex") === (meta2.encoding || "hex"),
291
+ 完全相同: meta1.hash === meta2.hash && meta1.salt === meta2.salt
292
+ };
293
+ } catch (error) {
294
+ return {
295
+ error: error.message,
296
+ identical: false
297
+ };
298
+ }
299
+ };
300
+
301
+ // New: Estimate security strength
302
+ const estimateSecurity = (passport) => {
303
+ try {
304
+ const meta = decodePassport(passport);
305
+ const now = new Date();
306
+ const year = now.getFullYear();
307
+
308
+ // Rough estimate of security level
309
+ let score = 0;
310
+ let recommendations = [];
311
+
312
+ // Algorithm score
313
+ if (meta.algorithm === "sha512") score += 40;
314
+ else if (meta.algorithm === "sha384") score += 35;
315
+ else if (meta.algorithm === "sha256") score += 30;
316
+
317
+ // Iterations score (based on year)
318
+ if (meta.iterations >= 300000) score += 40;
319
+ else if (meta.iterations >= 200000) score += 35;
320
+ else if (meta.iterations >= 150000) score += 30;
321
+ else if (meta.iterations >= 100000) score += 25;
322
+ else {
323
+ score += 15;
324
+ recommendations.push("Increase iterations (current: " + meta.iterations + ")");
325
+ }
326
+
327
+ // Salt length
328
+ if (meta.saltLength >= 32) score += 20;
329
+ else if (meta.saltLength >= 16) score += 15;
330
+ else {
331
+ score += 5;
332
+ recommendations.push("Increase salt length (current: " + meta.saltLength + ")");
333
+ }
334
+
335
+ // Hash length
336
+ if (meta.hashLength >= 32) score += 10;
337
+
338
+ let level = "Weak";
339
+ if (score >= 90) level = "Excellent";
340
+ else if (score >= 75) level = "Strong";
341
+ else if (score >= 60) level = "Good";
342
+ else if (score >= 40) level = "Fair";
343
+
344
+ return {
345
+ score,
346
+ level,
347
+ recommendations,
348
+ metadata: {
349
+ algorithm: meta.algorithm,
350
+ iterations: meta.iterations,
351
+ saltLength: meta.saltLength,
352
+ hashLength: meta.hashLength
353
+ }
354
+ };
355
+ } catch (error) {
356
+ return {
357
+ error: error.message,
358
+ score: 0,
359
+ level: "Invalid"
360
+ };
361
+ }
362
+ };
363
+
364
+ // Export everything
119
365
  module.exports = {
366
+ // Core functions
120
367
  hash,
121
368
  verify,
122
369
  encodePassport,
123
- decodePassport
124
- };
370
+ decodePassport,
371
+
372
+ // New enhanced functions
373
+ batchVerify,
374
+ inspectPassport,
375
+ comparePassports,
376
+ estimateSecurity,
377
+ generateApiKey,
378
+
379
+ // Utilities
380
+ DEFAULTS,
381
+ SUPPORTED_ALGORITHMS,
382
+ SUPPORTED_ENCODINGS,
383
+
384
+ // Version info
385
+ VERSION: "1.1.0"
386
+ };
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "inslash",
3
- "version": "1.0.3",
4
- "description": "A modern, upgradeable, and secure password hashing utility with passport encoding and hash ancestry.",
3
+ "version": "1.1.0",
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
- "types": "index.d.ts",
6
+ "types": "index.d.ts",
7
7
  "scripts": {
8
- "test": "node cstest.js"
8
+ "test": "node test.js",
9
+ "benchmark": "node benchmark.js",
10
+ "prepublishOnly": "npm test"
9
11
  },
10
12
  "keywords": [
11
13
  "password",
@@ -16,7 +18,12 @@
16
18
  "pepper",
17
19
  "upgradeable",
18
20
  "passport",
19
- "nodejs"
21
+ "nodejs",
22
+ "authentication",
23
+ "hashing",
24
+ "hmac",
25
+ "key-derivation",
26
+ "security-tools"
20
27
  ],
21
28
  "author": "Reshuk Sapkota",
22
29
  "license": "MIT",
@@ -30,10 +37,20 @@
30
37
  "homepage": "https://github.com/reshuk-code/inslash#readme",
31
38
  "files": [
32
39
  "index.js",
40
+ "index.d.ts",
33
41
  "README.md",
34
- "LICENSE"
42
+ "LICENSE",
43
+ "CHANGELOG.md"
35
44
  ],
36
45
  "engines": {
37
46
  "node": ">=16"
47
+ },
48
+ "devDependencies": {
49
+ "concurrently": "^9.2.1"
50
+ },
51
+ "dependencies": {},
52
+ "funding": {
53
+ "type": "individual",
54
+ "url": "https://github.com/sponsors/reshuk-code"
38
55
  }
39
56
  }