OWASP/SSO_Project

View on GitHub
js-backend/utils/password.js

Summary

Maintainability
A
2 hrs
Test Coverage
const crypto = require("crypto");
const argon2 = require("argon2");
const https = require("https");
const url = require("url");

const isFailSafe = process.env.PWNEDPASSFAILSAFE ? (process.env.PWNEDPASSFAILSAFE==1) : false;

class PwUtil {
    constructor(dbConnection) {
        this.db = dbConnection;
    }
    checkPassword(userId, password) {
        return new Promise((resolve, reject) => {
            // Check password policy
            if(password.length < 8 || password.length > 100) return reject("Password does not match password policy");
            
            this.checkPwnedPasswords(password, isFailSafe).then(() => {
                // Check for older passwords
                if(userId === null || !this.db) {
                    return resolve();
                } else {
                    this.db.execute("SELECT password FROM passwords WHERE userId = ? ORDER BY created DESC LIMIT ?", [userId, process.env.PWHISTORY || 3], (err, results) => {
                        if(err) return reject(err);
                        
                        const taskList = [];
                        for(let i=0;i<results.length;i++) {
                            taskList.push(this.verifyPassword(password, results[i].password));
                        }
                    
                        Promise.all(taskList).then(verifyResults => {
                            for(let i=0;i<verifyResults.length;i++) {
                                if(verifyResults[i] === true) return reject("Password has been previously used");
                            }
                        
                            return resolve();
                        }).catch(reject);
                    });
                }
            }).catch(reject);
        });
    }
    checkPwnedPasswords(password, failSafe) {
        return new Promise((resolve, reject) => {
            //return resolve();
            
            const shasum = crypto.createHash("sha1");
            shasum.update(password);
            const shahex = shasum.digest("hex").toUpperCase();
            const shaprefix = shahex.substr(0, 5);
            const shasuffix = shahex.substr(5);
            
            this.httpGet("https://api.pwnedpasswords.com/range/"+shahex.substr(0, 5)).then(body => {
                body.indexOf(shasuffix) != -1 ? reject("Password has been previously hacked and insecure") : resolve();
            }).catch(err => {
                console.error(err, response ? response.statusCode : null);
                failSafe===true ? reject("Error checking pwned passwords") : resolve();
            });
        });
    }
    hashPassword(password) {
        return argon2.hash(password, {
            type: process.env.ARGON2TYPE || argon2.argon2id,
            parallelism: process.env.ARGON2PARALLEL || 1,
            timeCost: process.env.ARGON2TIME || 5,
            memoryCost: process.env.ARGON2MEMORY || 200000, // 200 MB
        });
    }
    verifyPassword(password, hash) {
        return argon2.verify(hash, password);
    }
    createRandomString(length) {
        return new Promise((resolve, reject) => {
            crypto.randomBytes(length, (err, buffer) => {
                if(err) return reject(err);
                resolve(buffer.toString("hex"));
            });
        });
    }
    httpGet(toUrl) {
        return new Promise((resolve, reject) => {
            https.get(toUrl, response => {
                let body = "";
                response.on("data", (chunk) => body += chunk);
                response.on("end", () => resolve(body));
            }).on("error", reject);
        });
    }
    httpPost(toUrl, postData) {
        return new Promise((resolve, reject) => {
            const urlParts = url.parse(toUrl);
            const options = {
                hostname: urlParts.hostname,
                port: urlParts.port || 443,
                path: urlParts.path,
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                    "Content-Length": postData.length,
                },
            };
            
            const req = https.request(options, response => {
                let body = "";
                response.on("data", (chunk) => body += chunk);
                response.on("end", () => resolve(body));
            });
            
            req.on("error", error => {
                reject(error);
            });

            req.write(postData);
            req.end();
        });
    }
}

exports.PwUtil = PwUtil;