auth

Main authentication module for my projects
git clone git://git.finwo.net/app/auth
Log | Files | Refs

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 }