From e0f53df176319030bbabd2443c0df7d44021519a Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Thu, 26 Dec 2024 06:09:49 -0500 Subject: [PATCH] Added 'altcha' captcha system for account and channel creation. --- config.example.json | 1 + package.json | 2 + .../api/account/registerController.js | 18 +++++- .../api/channel/registerController.js | 10 +++ src/controllers/newChannelController.js | 9 ++- src/controllers/registerController.js | 11 +++- src/routers/api/accountRouter.js | 3 +- src/server.js | 13 ++-- src/utils/altchaUtils.js | 61 ++++++++++++++++++ src/validators/accountValidator.js | 2 +- src/validators/channelValidator.js | 2 +- src/views/channel.ejs | 2 +- src/views/newChannel.ejs | 10 ++- src/views/register.ejs | 14 +++-- www/css/newChannel.css | 12 +++- www/css/register.css | 11 +++- www/css/theme/movie-night.css | 23 +++++++ www/js/newChannel.js | 57 ++++++++++++++--- www/js/register.js | 62 +++++++++++++++---- www/js/utils.js | 58 ++++++++++++++--- 20 files changed, 326 insertions(+), 55 deletions(-) create mode 100644 src/utils/altchaUtils.js diff --git a/config.example.json b/config.example.json index f807775..6037e4f 100644 --- a/config.example.json +++ b/config.example.json @@ -2,6 +2,7 @@ "instanceName": "Canopy", "port": 8080, "sessionSecret": "CHANGE_ME", + "altchaSecret": "CHANGE_ME", "db":{ "address": "127.0.0.1", "port": "27017", diff --git a/package.json b/package.json index 3a67d61..542ebd2 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,8 @@ "version": "0.1", "license": "AGPL-3.0-only", "dependencies": { + "altcha": "^1.0.7", + "altcha-lib": "^1.2.0", "bcrypt": "^5.1.1", "bootstrap-icons": "^1.11.3", "connect-mongo": "^5.1.0", diff --git a/src/controllers/api/account/registerController.js b/src/controllers/api/account/registerController.js index a19db6e..e9b66c2 100644 --- a/src/controllers/api/account/registerController.js +++ b/src/controllers/api/account/registerController.js @@ -14,20 +14,34 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ +//Config +const config = require('../../../../config.json'); + //NPM Imports const {validationResult, matchedData} = require('express-validator'); //local imports const {userModel} = require('../../../schemas/userSchema'); const userBanModel = require('../../../schemas/userBanSchema'); +const altchaUtils = require('../../../utils/altchaUtils'); const {exceptionHandler, errorHandler} = require('../../../utils/loggerUtils'); module.exports.post = async function(req, res){ try{ + //Check for validation errors const validResult = validationResult(req); + //If there are none if(validResult.isEmpty()){ + //Get sanatized/validated data const user = matchedData(req); + //Verify Altcha Payload + const verified = await altchaUtils.verify(req.body.verification); + + //If altcha verification failed + if(!verified){ + return errorHandler(res, 'Altcha verification failed, Please refresh the page!', 'unauthorized'); + } //Would prefer to stick this in userModel.statics.register() but we end up with circular dependencies >:( const nukedBans = await userBanModel.checkProcessedBans(user.user); @@ -42,9 +56,9 @@ module.exports.post = async function(req, res){ return res.sendStatus(200); }else{ res.status(400); - res.send({errors: validResult.array()}) + return res.send({errors: validResult.array()}); } }catch(err){ - exceptionHandler(res, err); + return exceptionHandler(res, err); } } \ No newline at end of file diff --git a/src/controllers/api/channel/registerController.js b/src/controllers/api/channel/registerController.js index 977e7ec..896342a 100644 --- a/src/controllers/api/channel/registerController.js +++ b/src/controllers/api/channel/registerController.js @@ -20,6 +20,7 @@ const {validationResult, matchedData} = require('express-validator'); //local imports const {exceptionHandler, errorHandler} = require('../../../utils/loggerUtils'); const {userModel} = require('../../../schemas/userSchema'); +const altchaUtils = require('../../../utils/altchaUtils'); const channelModel = require('../../../schemas/channel/channelSchema'); //api account functions @@ -32,6 +33,15 @@ module.exports.post = async function(req, res){ if(validResult.isEmpty()){ //Set channel object from sanatized/validated data, and get user document from session data const channel = matchedData(req); + //Verify Altcha Payload + const verified = await altchaUtils.verify(req.body.verification); + + //If altcha verification failed + if(!verified){ + return errorHandler(res, 'Altcha verification failed, Please refresh the page!', 'unauthorized'); + } + + //Find current user const userDB = await userModel.findOne({user: req.session.user.user}); //register new channel with requesting user as owner diff --git a/src/controllers/newChannelController.js b/src/controllers/newChannelController.js index cb8d1f8..8531941 100644 --- a/src/controllers/newChannelController.js +++ b/src/controllers/newChannelController.js @@ -14,10 +14,17 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ +//Local Imports +const altchaUtils = require('../utils/altchaUtils'); + //Config const config = require('../../config.json'); //root index functions module.exports.get = async function(req, res){ - res.render('newChannel', {instance: config.instanceName, user: req.session.user}); + //Generate captcha + const challenge = await altchaUtils.genCaptcha(); + + //render the page + return res.render('newChannel', {instance: config.instanceName, user: req.session.user, challenge}); } \ No newline at end of file diff --git a/src/controllers/registerController.js b/src/controllers/registerController.js index e944207..dc303ed 100644 --- a/src/controllers/registerController.js +++ b/src/controllers/registerController.js @@ -17,7 +17,14 @@ along with this program. If not, see .*/ //Config const config = require('../../config.json'); +//Local Imports +const altchaUtils = require('../utils/altchaUtils'); + //register page functions -module.exports.get = function(req, res){ - res.render('register', {instance: config.instanceName, user: req.session.user}); +module.exports.get = async function(req, res){ + //Generate captcha + const challenge = await altchaUtils.genCaptcha(); + + //Render page + return res.render('register', {instance: config.instanceName, user: req.session.user, challenge}); } \ No newline at end of file diff --git a/src/routers/api/accountRouter.js b/src/routers/api/accountRouter.js index df16f34..65e997d 100644 --- a/src/routers/api/accountRouter.js +++ b/src/routers/api/accountRouter.js @@ -34,8 +34,9 @@ router.post('/login', accountValidator.user(), accountValidator.pass(), loginCon router.get('/logout', logoutController.get); + router.post('/register', accountValidator.user(), - accountValidator.pass(), + accountValidator.securePass(), accountValidator.pass('passConfirm'), accountValidator.email(), registerController.post); diff --git a/src/server.js b/src/server.js index 6e0ad85..5c31ca0 100644 --- a/src/server.js +++ b/src/server.js @@ -23,6 +23,9 @@ const path = require('path'); const mongoStore = require('connect-mongo'); const mongoose = require('mongoose'); +//Define global crypto variable for altcha +globalThis.crypto = require('node:crypto').webcrypto; + //Define Local Imports const channelManager = require('./app/channel/channelManager'); const scheduler = require('./utils/scheduler'); @@ -80,6 +83,7 @@ app.set('views', __dirname + '/views'); //Middlware //Enable Express app.use(express.json()); +//app.use(express.urlencoded()); //Enable Express-Sessions app.use(sessionMiddleware); @@ -104,12 +108,11 @@ app.use('/tooltip', tooltipRouter); //Bot-Ready app.use('/api', apiRouter); -//3rd-Party Browser-Side Libraries -app.use('/lib/bootstrap-icons',express.static(path.join(__dirname, '../node_modules/bootstrap-icons'))); -app.use('/lib/socket.io',express.static(path.join(__dirname, '../node_modules/socket.io/client-dist'))); -app.use('/lib/validator',express.static(path.join(__dirname, '../node_modules/validator'))); - //Static File Server +//Serve bootstrap icons +app.use('/lib/bootstrap-icons',express.static(path.join(__dirname, '../node_modules/bootstrap-icons'))); +app.use('/lib/altcha',express.static(path.join(__dirname, '../node_modules/altcha/dist_external'))); +//Server public 'www' folder app.use(express.static(path.join(__dirname, '../www'))); //Increment launch counter diff --git a/src/utils/altchaUtils.js b/src/utils/altchaUtils.js new file mode 100644 index 0000000..b7725a3 --- /dev/null +++ b/src/utils/altchaUtils.js @@ -0,0 +1,61 @@ +/*Canopy - The next generation of stoner streaming software +Copyright (C) 2024 Rainbownapkin and the TTN Community + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see .*/ + +//Config +const config = require('../../config.json'); + +//NPM imports +const { createChallenge, verifySolution } = require('altcha-lib'); + +//Create empty array to hold cache of spent payloades to protect against replay attacks +const spent = []; +//Captcha lifetime in minutes +const lifetime = 2; + +module.exports.genCaptcha = async function(){ + //Set altcha expiration date + const expiration = new Date(); + + //Add four minutes + expiration.setMinutes(expiration.getMinutes() + lifetime); + + //Generate Altcha Challenge + return await createChallenge({ + hmacKey: config.altchaSecret, + maxNumber: 200000, + expires: expiration + }); +} + +module.exports.verify = async function(payload){ + //If we already checked this payload + if(spent.indexOf(payload) != -1){ + //Fuck off and die + return false; + } + + //Get length before pushing payload to get index of next item + const payloadIndex = spent.length; + + //Add payload to cache of spent payloades + spent.push(payload); + + //Set timeout to splice out the used payload after its expired so we're not filling RAM with expired payloads that aren't going to resolve true anyways + setTimeout(() => {spent.splice(payloadIndex,1);}, lifetime * 60 * 1000); + + //Return verification results + return await verifySolution(payload, config.altchaSecret); +} \ No newline at end of file diff --git a/src/validators/accountValidator.js b/src/validators/accountValidator.js index 82bcdf7..1dfa124 100644 --- a/src/validators/accountValidator.js +++ b/src/validators/accountValidator.js @@ -21,7 +21,7 @@ const { check, body, checkSchema, checkExact} = require('express-validator'); const {isRank} = require('./permissionsValidator'); module.exports = { - user: (field = 'user') => check(field).escape().trim().isLength({min: 1, max: 22}), + user: (field = 'user') => check(field).escape().trim().isAlphanumeric().isLength({min: 1, max: 22}), //Password security requirements may change over time, therefore we should only validate against strongPassword() when creating new accounts //that way we don't break old ones upon change diff --git a/src/validators/channelValidator.js b/src/validators/channelValidator.js index 3309c30..04ee960 100644 --- a/src/validators/channelValidator.js +++ b/src/validators/channelValidator.js @@ -21,7 +21,7 @@ const { check, body, checkSchema, checkExact} = require('express-validator'); const accountValidator = require('./accountValidator'); module.exports = { - name: (field = 'name') => check(field).escape().trim().isLength({min: 1, max: 50}), + name: (field = 'name') => check(field).escape().trim().isAlphanumeric().isLength({min: 1, max: 50}), description: (field = 'description') => body(field).escape().trim().isLength({min: 1, max: 1000}), diff --git a/src/views/channel.ejs b/src/views/channel.ejs index c2806d1..912e98f 100644 --- a/src/views/channel.ejs +++ b/src/views/channel.ejs @@ -120,7 +120,7 @@ along with this program. If not, see .-->