password.js (2321B)
1 import crypto from 'node:crypto'; 2 3 const SCRYPT_N = 32768; 4 const SCRYPT_R = 8; 5 const SCRYPT_P = 1; 6 const KEY_LENGTH = 64; 7 const SALT_LENGTH = 16; 8 const MAX_MEMORY = 64 * 1024 * 1024; 9 const MAX_PASSWORD_LENGTH = 1024; 10 11 function derive(password, salt, params) { 12 return new Promise((resolve, reject) => { 13 crypto.scrypt( 14 password, 15 salt, 16 KEY_LENGTH, 17 { 18 N: params.N, 19 r: params.r, 20 p: params.p, 21 maxmem: MAX_MEMORY, 22 }, 23 (error, key) => { 24 if (error) reject(error); 25 else resolve(key); 26 } 27 ); 28 }); 29 } 30 31 export async function hashPassword(password) { 32 if (typeof password !== 'string') throw new Error('password must be a string'); 33 if (password.length > MAX_PASSWORD_LENGTH) throw new Error('password is too long'); 34 35 const salt = crypto.randomBytes(SALT_LENGTH); 36 const key = await derive(password, salt, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }); 37 return [ 38 'scrypt', 39 SCRYPT_N, 40 SCRYPT_R, 41 SCRYPT_P, 42 salt.toString('base64url'), 43 key.toString('base64url'), 44 ].join('$'); 45 } 46 47 export async function verifyPassword(password, encoded) { 48 if (typeof password !== 'string' || typeof encoded !== 'string') return false; 49 if (password.length > MAX_PASSWORD_LENGTH) return false; 50 51 const parts = encoded.split('$'); 52 if (parts.length !== 6 || parts[0] !== 'scrypt') return false; 53 54 const params = { 55 N: Number(parts[1]), 56 r: Number(parts[2]), 57 p: Number(parts[3]), 58 }; 59 if ( 60 !Number.isInteger(params.N) || 61 !Number.isInteger(params.r) || 62 !Number.isInteger(params.p) || 63 params.N <= 1 || 64 params.r <= 0 || 65 params.p <= 0 66 ) { 67 return false; 68 } 69 70 let salt; 71 let expected; 72 try { 73 salt = Buffer.from(parts[4], 'base64url'); 74 expected = Buffer.from(parts[5], 'base64url'); 75 } catch { 76 return false; 77 } 78 if (salt.length === 0 || expected.length !== KEY_LENGTH) return false; 79 80 let actual; 81 try { 82 actual = await derive(password, salt, params); 83 } catch { 84 return false; 85 } 86 87 return crypto.timingSafeEqual(actual, expected); 88 } 89 90 let dummyHash = null; 91 92 export async function verifyDummyPassword(password) { 93 if (!dummyHash) { 94 dummyHash = hashPassword(crypto.randomBytes(24).toString('base64url')); 95 } 96 return verifyPassword(password, await dummyHash); 97 }