From 5caa679b9230b81a9a0ba3c6dfd4039865402123 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sat, 18 Oct 2025 09:42:08 -0400 Subject: [PATCH] Upgraded password hashing algo to argon2id. --- config.example.json | 10 +++++++--- config.example.jsonc | 23 +++++++++++++++-------- src/schemas/user/userSchema.js | 12 ++++++------ src/server.js | 2 +- src/utils/altchaUtils.js | 4 ++-- src/utils/configCheck.js | 16 +++++++++++++--- src/utils/hashUtils.js | 22 ++++++++++++---------- 7 files changed, 56 insertions(+), 33 deletions(-) diff --git a/config.example.json b/config.example.json index 5e3eb01..cb43df6 100644 --- a/config.example.json +++ b/config.example.json @@ -6,11 +6,15 @@ "protocol": "http", "domain": "localhost", "ytdlpPath": "/home/canopy/.local/pipx/venvs/yt-dlp/bin/yt-dlp", - "sessionSecret": "CHANGE_ME", - "altchaSecret": "CHANGE_ME", - "ipSecret": "CHANGE_ME", "migrate": false, "dropLegacyTokes": false, + "secrets":{ + "passwordSecret": "CHANGE_ME", + "rememberMeSecret": "CHANGE_ME", + "sessionSecret": "CHANGE_ME", + "altchaSecret": "CHANGE_ME", + "ipSecret": "CHANGE_ME" + }, "ssl":{ "cert": "./server.cert", "key": "./server.key" diff --git a/config.example.jsonc b/config.example.jsonc index 01eeab1..02fb88e 100644 --- a/config.example.jsonc +++ b/config.example.jsonc @@ -16,14 +16,6 @@ //Path to YT-DLP Executable for scraping youtube, dailymotion, and vimeo //Dailymotion and Vimeo could work using official apis w/o keys, but you wouldn't have any raw file playback options :P "ytdlpPath": "/home/canopy/.local/pipx/venvs/yt-dlp/bin/yt-dlp", - //Be careful with what you keep in secrets, you should use special chars, but test your deployment, as some chars may break account registration - //An update to either kill the server and bitch about the issue in console is planned so it's not so confusing for new admins - //Session secret used to secure session keys - "sessionSecret": "CHANGE_ME", - //Altacha secret used to generate altcha challenges - "altchaSecret": "CHANGE_ME", - //IP Secret used to salt IP Hashes - "ipSecret": "CHANGE_ME", //Enable to migrate legacy DB and toke files dumped into the ./migration/ directory //WARNING: The migration folder is cleared after server boot, whether or not a migration took place or this option is enabled. //Keep your backups in a safe place, preferably a machine that DOESN'T have open inbound ports exposed to the internet/a publically accessible reverse proxy! @@ -32,6 +24,21 @@ //Requires migration to be disabled before it takes effect. //WARNING: this does NOT affect user toke counts, migrated or otherwise. Use carefully! "dropLegacyTokes": false, + //Server Secrets + //Be careful with what you keep in secrets, you should use special chars, but test your deployment, as some chars may break account registration + //An update to either kill the server and bitch about the issue in console is planned so it's not so confusing for new admins + "secrets":{ + //Password secret used to pepper password hashes + "passwordSecret": "CHANGE_ME", + //Password secret used to pepper rememberMe token hashes + "rememberMeSecret": "CHANGE_ME", + //Session secret used to secure session keys + "sessionSecret": "CHANGE_ME", + //Altacha secret used to generate altcha challenges + "altchaSecret": "CHANGE_ME", + //IP Secret used to pepper IP Hashes + "ipSecret": "CHANGE_ME" + }, //SSL cert and key locations "ssl":{ "cert": "./server.cert", diff --git a/src/schemas/user/userSchema.js b/src/schemas/user/userSchema.js index 209312e..e9368a3 100644 --- a/src/schemas/user/userSchema.js +++ b/src/schemas/user/userSchema.js @@ -164,7 +164,7 @@ userSchema.pre('save', async function (next){ //If the password was changed if(this.isModified("pass")){ //Hash that sunnovabitch, no questions asked. - this.pass = hashUtil.hashPassword(this.pass); + this.pass = await hashUtil.hashPassword(this.pass); } //If the flair was changed @@ -321,7 +321,7 @@ userSchema.statics.authenticate = async function(user, pass, failLine = "Bad Use } //Check our password is correct - if(userDB.checkPass(pass)){ + if(await userDB.checkPass(pass)){ return userDB; }else{ //if not scream and shout @@ -492,8 +492,8 @@ userSchema.statics.processAgedIPRecords = async function(){ * @param {String} pass - Password to authenticate * @returns {Boolean} True if authenticated */ -userSchema.methods.checkPass = function(pass){ - return hashUtil.comparePassword(pass, this.pass) +userSchema.methods.checkPass = async function(pass){ + return await hashUtil.comparePassword(pass, this.pass) } /** @@ -824,7 +824,7 @@ userSchema.methods.killAllSessions = async function(reason = "A full log-out fro * @param {Object} passChange - passChange object handed down from Browser */ userSchema.methods.changePassword = async function(passChange){ - if(this.checkPass(passChange.oldPass)){ + if(await this.checkPass(passChange.oldPass)){ if(passChange.newPass == passChange.confirmPass){ //Note: We don't have to worry about hashing here because the schema is written to do it auto-magically this.pass = passChange.newPass; @@ -877,7 +877,7 @@ userSchema.methods.nuke = async function(pass){ } //Check that the password is correct - if(this.checkPass(pass)){ + if(await this.checkPass(pass)){ //delete the user var oldUser = await this.deleteOne(); }else{ diff --git a/src/server.js b/src/server.js index 336071c..8aedf91 100644 --- a/src/server.js +++ b/src/server.js @@ -84,7 +84,7 @@ module.exports.store = mongoStore.create({mongoUrl: dbUrl}); //define sessionMiddleware const sessionMiddleware = session({ - secret: config.sessionSecret, + secret: config.secrets.sessionSecret, resave: false, saveUninitialized: false, store: module.exports.store diff --git a/src/utils/altchaUtils.js b/src/utils/altchaUtils.js index 4c7754f..68ed7e1 100644 --- a/src/utils/altchaUtils.js +++ b/src/utils/altchaUtils.js @@ -44,7 +44,7 @@ module.exports.genCaptcha = async function(difficulty = 2, uniqueSecret = ''){ //Generate Altcha Challenge return await createChallenge({ - hmacKey: [config.altchaSecret, uniqueSecret].join(''), + hmacKey: [config.secrets.altchaSecret, uniqueSecret].join(''), maxNumber: 100000 * difficulty, expires: expiration }); @@ -73,5 +73,5 @@ module.exports.verify = async function(payload, uniqueSecret = ''){ setTimeout(() => {spent.splice(payloadIndex,1);}, lifetime * 60 * 1000); //Return verification results - return await verifySolution(payload, [config.altchaSecret, uniqueSecret].join('')); + return await verifySolution(payload, [config.secrets.altchaSecret, uniqueSecret].join('')); } \ No newline at end of file diff --git a/src/utils/configCheck.js b/src/utils/configCheck.js index a705195..d979b32 100644 --- a/src/utils/configCheck.js +++ b/src/utils/configCheck.js @@ -40,18 +40,28 @@ module.exports.securityCheck = function(){ loggerUtil.consoleWarn("Mail transport security disabled! This server should be used for development purposes only!"); } + //check password pepper + if(!validator.isStrongPassword(config.secrets.passwordSecret) || config.secrets.passwordSecret == "CHANGE_ME"){ + loggerUtil.consoleWarn("Insecure Password Secret! Change Password Secret!"); + } + + //check RememberMe pepper + if(!validator.isStrongPassword(config.secrets.rememberMeSecret) || config.secrets.rememberMeSecret == "CHANGE_ME"){ + loggerUtil.consoleWarn("Insecure RememberMe Secret! Change RememberMe Secret!"); + } + //check session secret - if(!validator.isStrongPassword(config.sessionSecret) || config.sessionSecret == "CHANGE_ME"){ + if(!validator.isStrongPassword(config.secrets.sessionSecret) || config.secrets.sessionSecret == "CHANGE_ME"){ loggerUtil.consoleWarn("Insecure Session Secret! Change Session Secret!"); } //check altcha secret - if(!validator.isStrongPassword(config.altchaSecret) || config.altchaSecret == "CHANGE_ME"){ + if(!validator.isStrongPassword(config.secrets.altchaSecret) || config.secrets.altchaSecret == "CHANGE_ME"){ loggerUtil.consoleWarn("Insecure Altcha Secret! Change Altcha Secret!"); } //check ipHash secret - if(!validator.isStrongPassword(config.ipSecret) || config.ipSecret == "CHANGE_ME"){ + if(!validator.isStrongPassword(config.secrets.ipSecret) || config.secrets.ipSecret == "CHANGE_ME"){ loggerUtil.consoleWarn("Insecure IP Hashing Secret! Change IP Hashing Secret!"); } diff --git a/src/utils/hashUtils.js b/src/utils/hashUtils.js index b9086cc..e60d81e 100644 --- a/src/utils/hashUtils.js +++ b/src/utils/hashUtils.js @@ -29,9 +29,9 @@ const bcrypt = require('bcrypt'); * @param {String} pass - Password to hash * @returns {String} Hashed/Salted password */ -module.exports.hashPassword = function(pass){ - const salt = bcrypt.genSaltSync(); - return bcrypt.hashSync(pass, salt); +module.exports.hashPassword = async function(pass){ + //Hash password with argon2id + return await argon2.hash(pass, {secret: Buffer.from(config.secrets.passwordSecret)}); } /** @@ -40,8 +40,9 @@ module.exports.hashPassword = function(pass){ * @param {String} hash - Salty Hash * @returns {Boolean} True if authentication success */ -module.exports.comparePassword = function(pass, hash){ - return bcrypt.compareSync(pass, hash); +module.exports.comparePassword = async function(pass, hash){ + //Verify password against argon2 hash + return await argon2.verify(hash, pass, {secret: Buffer.from(config.secrets.passwordSecret)}); } /** @@ -59,14 +60,14 @@ module.exports.compareLegacyPassword = function(pass, hash){ * * Provides a basic level of privacy by only logging salted hashes of IP's * @param {String} ip - IP to hash - * @returns {String} Hashed/Salted IP Adress + * @returns {String} Hashed/Peppered IP Adress */ module.exports.hashIP = function(ip){ //Create hash object const hashObj = crypto.createHash('sha512'); - //add IP and salt to the hash - hashObj.update(`${ip}${config.ipSecret}`); + //add IP and pepper to the hash + hashObj.update(`${ip}${config.secrets.ipSecret}`); //return the IP hash as a string return hashObj.digest('hex'); @@ -78,7 +79,8 @@ module.exports.hashIP = function(ip){ * @returns {String} - Hashed token */ module.exports.hashRememberMeToken = async function(token){ - return await argon2.hash(token); + //hash token with argon2id + return await argon2.hash(token, {secret: Buffer.from(config.secrets.rememberMeSecret)}); } /** @@ -89,5 +91,5 @@ module.exports.hashRememberMeToken = async function(token){ */ module.exports.compareRememberMeToken = async function(token, hash){ //Compare hash and return result - return await argon2.verify(hash, token); + return await argon2.verify(hash, token, {secret: Buffer.from(config.secrets.rememberMeSecret)}); } \ No newline at end of file