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 .-->