inslash 1.0.2 → 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.
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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Inslash
3
+ Copyright (c) 2026 Reshuk Sapkota
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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 ADDED
@@ -0,0 +1,386 @@
1
+ const crypto = require("crypto");
2
+
3
+ const DEFAULTS = {
4
+ saltLength: 16,
5
+ hashLength: 32,
6
+ iterations: 100_000,
7
+ algorithm: "sha256",
8
+ encoding: "hex" // New: support for different encodings
9
+ };
10
+
11
+ const SUPPORTED_ALGORITHMS = ["sha256", "sha512", "sha384"];
12
+ const SUPPORTED_ENCODINGS = ["hex", "base64", "base64url", "latin1"];
13
+
14
+ const createSalt = (length) => crypto.randomBytes(length).toString("hex");
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
29
+ const hashWithSalt = async (value, salt, secret, options) => {
30
+ const { iterations, hashLength, algorithm, encoding = "hex" } = options;
31
+ let data = value + salt;
32
+ let digest = Buffer.from(data);
33
+
34
+ console.time(`Hash operation (${iterations} iterations)`);
35
+ for (let i = 0; i < iterations; i++) {
36
+ digest = crypto.createHmac(algorithm, secret).update(digest).digest();
37
+ }
38
+ console.timeEnd(`Hash operation (${iterations} iterations)`);
39
+
40
+ const result = digest.toString(encoding).slice(0, hashLength);
41
+
42
+ return {
43
+ hash: result,
44
+ timing: iterations, // For informational purposes
45
+ algorithm
46
+ };
47
+ };
48
+
49
+ // Enhanced: Passport encoding with versioning
50
+ const encodePassport = (meta) => {
51
+ const history = Buffer.from(JSON.stringify(meta.history || [])).toString("base64");
52
+ const parts = [
53
+ "$inslash",
54
+ meta.version || "1",
55
+ meta.algorithm,
56
+ meta.iterations,
57
+ meta.saltLength,
58
+ meta.hashLength,
59
+ meta.salt,
60
+ meta.hash,
61
+ history
62
+ ];
63
+
64
+ // Add optional metadata if present
65
+ if (meta.encoding) parts.push(meta.encoding);
66
+
67
+ return parts.join("$");
68
+ };
69
+
70
+ // Enhanced: Decode with backward compatibility
71
+ const decodePassport = (passport) => {
72
+ const parts = passport.split("$");
73
+ 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;
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
+ }
106
+ };
107
+
108
+ // Enhanced: Hash function with more options
109
+ const hash = async (value, secret, opts = {}) => {
110
+ if (!secret) throw new Error("Secret key is required");
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
+
123
+ const options = { ...DEFAULTS, ...opts };
124
+ const salt = createSalt(options.saltLength);
125
+ const pepper = process.env.HASH_PEPPER || "";
126
+ const valueWithPepper = value + pepper;
127
+
128
+ const { hash: hashed } = await hashWithSalt(valueWithPepper, salt, secret, options);
129
+
130
+ const meta = {
131
+ version: "2",
132
+ algorithm: options.algorithm,
133
+ iterations: options.iterations,
134
+ saltLength: options.saltLength,
135
+ hashLength: options.hashLength,
136
+ salt,
137
+ hash: hashed,
138
+ encoding: options.encoding,
139
+ history: [
140
+ {
141
+ date: new Date().toISOString(),
142
+ algorithm: options.algorithm,
143
+ iterations: options.iterations,
144
+ encoding: options.encoding
145
+ }
146
+ ]
147
+ };
148
+
149
+ return {
150
+ passport: encodePassport(meta),
151
+ ...meta
152
+ };
153
+ };
154
+
155
+ // Enhanced: Verify with more detailed response
156
+ const verify = async (value, passport, secret, opts = {}) => {
157
+ const meta = decodePassport(passport);
158
+ const options = {
159
+ algorithm: meta.algorithm,
160
+ iterations: meta.iterations,
161
+ saltLength: meta.saltLength,
162
+ hashLength: meta.hashLength,
163
+ encoding: meta.encoding || "hex",
164
+ ...opts
165
+ };
166
+
167
+ const pepper = process.env.HASH_PEPPER || "";
168
+ const valueWithPepper = value + pepper;
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
+
178
+ let needsUpgrade = false;
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
+
194
+ let upgradedPassport = null;
195
+ let upgradedMetadata = null;
196
+
197
+ if (valid && needsUpgrade) {
198
+ const newMeta = { ...meta, ...opts };
199
+ newMeta.history = (meta.history || []).concat([
200
+ {
201
+ date: new Date().toISOString(),
202
+ algorithm: opts.algorithm || meta.algorithm,
203
+ iterations: opts.iterations || meta.iterations,
204
+ encoding: opts.encoding || meta.encoding,
205
+ reason: "security upgrade"
206
+ }
207
+ ]);
208
+
209
+ const newSalt = createSalt(newMeta.saltLength);
210
+ const { hash: newHash } = await hashWithSalt(valueWithPepper, newSalt, secret, newMeta);
211
+
212
+ newMeta.salt = newSalt;
213
+ newMeta.hash = newHash;
214
+ newMeta.version = "2";
215
+
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
+ }
258
+ }
259
+ return results;
260
+ };
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
365
+ module.exports = {
366
+ // Core functions
367
+ hash,
368
+ verify,
369
+ encodePassport,
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,15 +1,56 @@
1
- {
2
- "name": "inslash",
3
- "version": "1.0.2",
4
- "main": "index.js",
5
- "scripts": {
6
- "test": "echo \"Error: no test specified\" && exit 1"
7
- },
8
- "keywords": [],
9
- "author": "Reshuk Sapkota",
10
- "license": "MIT",
11
- "description": "",
12
- "dependencies": {
13
- "crypto": "^1.0.1"
14
- }
15
- }
1
+ {
2
+ "name": "inslash",
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
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "scripts": {
8
+ "test": "node test.js",
9
+ "benchmark": "node benchmark.js",
10
+ "prepublishOnly": "npm test"
11
+ },
12
+ "keywords": [
13
+ "password",
14
+ "hash",
15
+ "security",
16
+ "crypto",
17
+ "salt",
18
+ "pepper",
19
+ "upgradeable",
20
+ "passport",
21
+ "nodejs",
22
+ "authentication",
23
+ "hashing",
24
+ "hmac",
25
+ "key-derivation",
26
+ "security-tools"
27
+ ],
28
+ "author": "Reshuk Sapkota",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/reshuk-code/inslash"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/reshuk-code/inslash/issues"
36
+ },
37
+ "homepage": "https://github.com/reshuk-code/inslash#readme",
38
+ "files": [
39
+ "index.js",
40
+ "index.d.ts",
41
+ "README.md",
42
+ "LICENSE",
43
+ "CHANGELOG.md"
44
+ ],
45
+ "engines": {
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"
55
+ }
56
+ }
package/cstest.js DELETED
@@ -1,49 +0,0 @@
1
- const { hash, verify } = require("./script");
2
-
3
- const SECRET_KEY = "supersecret";
4
-
5
- // 1. Rainbow Table Attack
6
- (async () => {
7
- const a = await hash("password123", SECRET_KEY);
8
- const b = await hash("password123", SECRET_KEY);
9
- console.log("Rainbow Table Test:", a.hash !== b.hash ? "PASS" : "FAIL");
10
-
11
- // 2. Brute Force Attack (timing)
12
- console.time("Low Iterations");
13
- await hash("password123", SECRET_KEY, { iterations: 1000 });
14
- console.timeEnd("Low Iterations");
15
-
16
- console.time("High Iterations");
17
- await hash("password123", SECRET_KEY, { iterations: 200_000 });
18
- console.timeEnd("High Iterations");
19
-
20
- // 3. Timing Attack (should use timingSafeEqual)
21
- const v = await verify("password123", a.passport, SECRET_KEY);
22
- console.log("Timing Safe Equal Test:", v.valid ? "PASS" : "FAIL");
23
-
24
- // 4. Salt Storage
25
- console.log("Salt Unique Test:", a.salt !== b.salt ? "PASS" : "FAIL");
26
-
27
- // 5. Pepper Security
28
- process.env.HASH_PEPPER = "pepper";
29
- const withPepper = await hash("password123", SECRET_KEY);
30
- process.env.HASH_PEPPER = "";
31
- const vPepper = await verify("password123", withPepper.passport, SECRET_KEY);
32
- console.log("Pepper Security Test:", vPepper.valid ? "FAIL" : "PASS");
33
-
34
- // 6. Upgrade Path
35
- const vUpgrade = await verify("password123", a.passport, SECRET_KEY, { iterations: 200_000 });
36
- console.log("Upgrade Path Test:", vUpgrade.needsUpgrade ? "PASS" : "FAIL");
37
-
38
- // 7. Input Validation
39
- try {
40
- await hash(null, SECRET_KEY);
41
- console.log("Null Input Test: FAIL");
42
- } catch {
43
- console.log("Null Input Test: PASS");
44
- }
45
-
46
- // 8. Collision Resistance
47
- const c = await hash("passwordABC", SECRET_KEY);
48
- console.log("Collision Resistance Test:", a.hash !== c.hash ? "PASS" : "FAIL");
49
- })();
package/script.js DELETED
@@ -1,124 +0,0 @@
1
- const crypto = require("crypto");
2
-
3
- const DEFAULTS = {
4
- saltLength: 16,
5
- hashLength: 32,
6
- iterations: 100_000,
7
- algorithm: "sha256"
8
- };
9
-
10
- const createSalt = (length) => crypto.randomBytes(length).toString("hex");
11
-
12
- const hashWithSalt = async (value, salt, secret, options) => {
13
- const { iterations, hashLength, algorithm } = options;
14
- let data = value + salt;
15
- let digest = Buffer.from(data);
16
-
17
- for (let i = 0; i < iterations; i++) {
18
- digest = crypto.createHmac(algorithm, secret).update(digest).digest();
19
- }
20
-
21
- return digest.toString("hex").slice(0, hashLength);
22
- };
23
-
24
- const encodePassport = (meta) => {
25
- const history = Buffer.from(JSON.stringify(meta.history || [])).toString("base64");
26
- return [
27
- "$inslash",
28
- meta.algorithm,
29
- meta.iterations,
30
- meta.saltLength,
31
- meta.hashLength,
32
- meta.salt,
33
- meta.hash,
34
- history
35
- ].join("$");
36
- };
37
-
38
- const decodePassport = (passport) => {
39
- const parts = passport.split("$");
40
- 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
- };
51
- };
52
-
53
- const hash = async (value, secret, opts = {}) => {
54
- if (!secret) throw new Error("Secret key is required");
55
- if (typeof value !== "string" || !value) throw new Error("Value to hash must be a non-empty string");
56
- const options = { ...DEFAULTS, ...opts };
57
- const salt = createSalt(options.saltLength);
58
- const pepper = process.env.HASH_PEPPER || "";
59
- const valueWithPepper = value + pepper;
60
- const hashed = await hashWithSalt(valueWithPepper, salt, secret, options);
61
-
62
- const meta = {
63
- algorithm: options.algorithm,
64
- iterations: options.iterations,
65
- saltLength: options.saltLength,
66
- hashLength: options.hashLength,
67
- salt,
68
- hash: hashed,
69
- history: [
70
- {
71
- date: new Date().toISOString(),
72
- algorithm: options.algorithm,
73
- iterations: options.iterations
74
- }
75
- ]
76
- };
77
-
78
- return {
79
- passport: encodePassport(meta),
80
- ...meta
81
- };
82
- };
83
-
84
- const verify = async (value, passport, secret, opts = {}) => {
85
- const meta = decodePassport(passport);
86
- const options = {
87
- algorithm: meta.algorithm,
88
- iterations: meta.iterations,
89
- saltLength: meta.saltLength,
90
- hashLength: meta.hashLength,
91
- ...opts
92
- };
93
- const pepper = process.env.HASH_PEPPER || "";
94
- 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));
97
- let needsUpgrade = false;
98
- if (opts.iterations && opts.iterations > meta.iterations) needsUpgrade = true;
99
- if (opts.algorithm && opts.algorithm !== meta.algorithm) needsUpgrade = true;
100
- let upgradedPassport = null;
101
- if (valid && needsUpgrade) {
102
- const newMeta = { ...meta, ...opts };
103
- newMeta.history = meta.history.concat([
104
- {
105
- date: new Date().toISOString(),
106
- algorithm: opts.algorithm || meta.algorithm,
107
- iterations: opts.iterations || meta.iterations
108
- }
109
- ]);
110
- const newSalt = createSalt(newMeta.saltLength);
111
- const newHash = await hashWithSalt(valueWithPepper, newSalt, secret, newMeta);
112
- newMeta.salt = newSalt;
113
- newMeta.hash = newHash;
114
- upgradedPassport = encodePassport(newMeta);
115
- }
116
- return { valid, needsUpgrade, upgradedPassport };
117
- };
118
-
119
- module.exports = {
120
- hash,
121
- verify,
122
- encodePassport,
123
- decodePassport
124
- };
package/test.js DELETED
@@ -1,22 +0,0 @@
1
- const { hash, verify } = require("./script");
2
-
3
- const SECRET_KEY = process.env.HASH_SECRET || "abcd";
4
-
5
- // create hash
6
- (async () => {
7
- const result = await hash("Happy", SECRET_KEY, {
8
- iterations: 150_000
9
- });
10
-
11
- console.log(result);
12
-
13
- // verify
14
- const verifyResult = await verify(
15
- "Happy",
16
- result.passport, // <-- use passport, not salt/hash
17
- SECRET_KEY,
18
- { iterations: result.iterations }
19
- );
20
-
21
- console.log(verifyResult); // { valid: true, needsUpgrade: false, upgradedPassport: null }
22
- })();