mirror of
https://github.com/nodemailer/wildduck.git
synced 2024-11-10 17:47:07 +08:00
177 lines
4.9 KiB
JavaScript
177 lines
4.9 KiB
JavaScript
'use strict';
|
|
|
|
const bcrypt = require('bcryptjs');
|
|
const pbkdf2 = require('@phc/pbkdf2'); // see https://www.npmjs.com/package/@phc/pbkdf2
|
|
const unixcrypt = require('unixcrypt');
|
|
// unefficient but we are rehashing immediatelly after successful verification
|
|
const { argon2Verify } = require('hash-wasm');
|
|
// this crap is only needed to support legacy users imported from some older system
|
|
const cryptMD5 = require('./md5/cryptmd5').cryptMD5;
|
|
const consts = require('./consts');
|
|
const unixCryptTD = require('unix-crypt-td-js');
|
|
|
|
// just pass hashing through to bcrypt
|
|
module.exports.hash = async password => {
|
|
password = (password || '').toString();
|
|
|
|
switch (consts.DEFAULT_HASH_ALGO) {
|
|
case 'pbkdf2':
|
|
return await pbkdf2.hash(password, {
|
|
iterations: consts.PDKDF2_ITERATIONS,
|
|
saltSize: consts.PDKDF2_SALT_SIZE,
|
|
digest: consts.PDKDF2_DIGEST
|
|
});
|
|
case 'bcrypt':
|
|
default:
|
|
return await bcrypt.hash(password, consts.BCRYPT_ROUNDS);
|
|
}
|
|
};
|
|
|
|
// compare against known hashing algos
|
|
module.exports.compare = async (password, hash) => {
|
|
password = (password || '').toString();
|
|
hash = (hash || '').toString();
|
|
|
|
let algo = checkHashSupport(hash);
|
|
if (!algo.result) {
|
|
throw new Error('Invalid algo: ' + JSON.stringify(algo.algo));
|
|
}
|
|
|
|
switch (algo.algo) {
|
|
case 'pbkdf2':
|
|
return await pbkdf2.verify(hash, password);
|
|
|
|
case 'bcrypt':
|
|
return await bcrypt.compare(password, hash);
|
|
|
|
case 'unixcrypt':
|
|
return await unixcryptCompareAsync(password, hash);
|
|
|
|
case 'argon2':
|
|
try {
|
|
// throws if does not match
|
|
return await argon2Verify({
|
|
password,
|
|
hash
|
|
});
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
|
|
case 'md5': {
|
|
let result;
|
|
|
|
let salt = hash.split('$')[2] || '';
|
|
result = cryptMD5(password, salt) === hash;
|
|
|
|
return result;
|
|
}
|
|
|
|
case 'des': {
|
|
let result;
|
|
|
|
let salt = hash.substr(0, 2);
|
|
result = unixCryptTD(password, salt) === hash;
|
|
|
|
return result;
|
|
}
|
|
|
|
default:
|
|
throw new Error('Invalid algo: ' + JSON.stringify(algo));
|
|
}
|
|
};
|
|
|
|
function checkHashSupport(hash) {
|
|
hash = (hash || '').toString();
|
|
|
|
if (/^[0-9a-z./]{13}$/i.test(hash)) {
|
|
// DES https://passlib.readthedocs.io/en/stable/lib/passlib.hash.des_crypt.html#format
|
|
return { result: true, algo: 'des' };
|
|
}
|
|
|
|
let algo = [].concat(hash.match(/^\$([^$]+)\$/) || [])[1];
|
|
algo = (algo || '').toString().toLowerCase();
|
|
|
|
switch (algo) {
|
|
case 'pbkdf2-sha512':
|
|
case 'pbkdf2-sha256':
|
|
case 'pbkdf2-sha1':
|
|
return { result: true, algo: 'pbkdf2' };
|
|
case '2a':
|
|
case '2b':
|
|
case '2y':
|
|
return { result: true, algo: 'bcrypt' };
|
|
case '6': // sha512crypt
|
|
case '5': // sha256crypt
|
|
return { result: true, algo: 'unixcrypt' };
|
|
|
|
case 'argon2d':
|
|
case 'argon2i':
|
|
case 'argon2id':
|
|
return { result: true, algo: 'argon2' };
|
|
|
|
case '1': {
|
|
return { result: true, algo: 'md5' };
|
|
}
|
|
|
|
default:
|
|
return { result: false, algo };
|
|
}
|
|
}
|
|
|
|
module.exports.checkHashSupport = checkHashSupport;
|
|
|
|
module.exports.shouldRehash = hash => {
|
|
hash = (hash || '').toString();
|
|
|
|
if (/^[0-9a-z./]{13}$/i.test(hash)) {
|
|
// DES https://passlib.readthedocs.io/en/stable/lib/passlib.hash.des_crypt.html#format
|
|
return true;
|
|
}
|
|
|
|
let algo = [].concat(hash.match(/^\$([^$]+)\$/) || [])[1];
|
|
algo = (algo || '').toString().toLowerCase();
|
|
|
|
switch (algo) {
|
|
case 'pbkdf2-sha512':
|
|
case 'pbkdf2-sha256': {
|
|
let [, iterations] = hash.match(/^\$[^$]+\$i=(\d+)\$/) || [];
|
|
|
|
if (consts.DEFAULT_HASH_ALGO !== 'pbkdf2') {
|
|
return true;
|
|
}
|
|
|
|
iterations = (iterations && Number(iterations)) || 0;
|
|
|
|
if (iterations && consts.PDKDF2_ITERATIONS > iterations) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
case '2a':
|
|
case '2b':
|
|
case '2y':
|
|
return consts.DEFAULT_HASH_ALGO !== 'bcrypt';
|
|
|
|
// Always rehash the following algos
|
|
case '6': // sha512crypt
|
|
case '5': // sha256crypt
|
|
case '1': // md5
|
|
case 'argon2d': // Argon2 (mostly because we are using an inefficient implementation)
|
|
case 'argon2i':
|
|
case 'argon2id':
|
|
case 'pbkdf2-sha1':
|
|
return true;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
};
|
|
|
|
async function unixcryptCompareAsync(password, hash) {
|
|
password = (password || '').toString();
|
|
hash = (hash || '').toString();
|
|
|
|
return unixcrypt.verify(password, hash);
|
|
}
|