Added 'altcha' captcha system for account and channel creation.

This commit is contained in:
rainbow napkin 2024-12-26 06:09:49 -05:00
parent 60801f0dc2
commit e0f53df176
20 changed files with 326 additions and 55 deletions

View file

@ -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 <https://www.gnu.org/licenses/>.*/
//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);
}
}

View file

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

View file

@ -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 <https://www.gnu.org/licenses/>.*/
//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});
}

View file

@ -17,7 +17,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
//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});
}

View file

@ -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);

View file

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

61
src/utils/altchaUtils.js Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.*/
//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);
}

View file

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

View file

@ -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}),

View file

@ -120,7 +120,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.-->
</body>
<footer>
<%- include('partial/scripts', {user}); %>
<script src="/lib/socket.io/socket.io.min.js"></script>
<script src="/socket.io/socket.io.min.js"></script>
<script src="/js/channel/commandPreprocessor.js"></script>
<script src="/js/channel/chatPostprocessor.js"></script>
<script src="/js/channel/chat.js"></script>

View file

@ -18,21 +18,25 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.-->
<head>
<%- include('partial/styles', {instance, user}); %>
<link rel="stylesheet" type="text/css" href="/css/newChannel.css">
<link rel="stylesheet" type="text/css" href="/lib/altcha/altcha.css">
<title><%= instance %> - New Channel</title>
</head>
<body>
<%- include('partial/navbar', {user}); %>
<form action="javascript:">
<label>Channel Name:</label>
<input id="register-channel-name" placeholder="Required">
<input class="register-prompt" id="register-channel-name" placeholder="Required">
<label>Description:</label>
<input id="register-description" placeholder="Required">
<input class="register-prompt" id="register-description" placeholder="Required">
<label>Thumbnail:</label>
<input id="register-thumbnail" placeholder="Required">
<input class="register-prompt" id="register-thumbnail" placeholder="Required">
<altcha-widget floating challengejson="<%= JSON.stringify(challenge) %>"></altcha-widget>
<button id="register-button" class='positive-button'>Register</button>
</form>
</body>
<footer>
<%- include('partial/scripts', {user}); %>
<script src="js/newChannel.js"></script>
<script src="/lib/altcha/altcha.js" type="module"></script>
</footer>
</html>

View file

@ -19,23 +19,27 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.-->
<head>
<%- include('partial/styles', {instance, user}); %>
<link rel="stylesheet" type="text/css" href="/css/register.css">
<link rel="stylesheet" type="text/css" href="/lib/altcha/altcha.css">
<title><%= instance %> - Account Registration</title>
</head>
<body>
<%- include('partial/navbar', {user}); %>
<form action="javascript:">
<label>Username:</label>
<input id="register-username" placeholder="Required">
<input class="register-prompt" id="register-username" placeholder="Required">
<label>Password:</label>
<input id="register-password" placeholder="Required" type="password">
<input class="register-prompt" id="register-password" placeholder="Required" type="password">
<label>Confirm Password:</label>
<input id="register-password-confirm" placeholder="Required" type="password">
<input class="register-prompt" id="register-password-confirm" placeholder="Required" type="password">
<label>Account Recovery Email:</label>
<input id="register-email" placeholder="Optional">
<input class="register-prompt" id="register-email" placeholder="Optional">
<altcha-widget floating challengejson="<%= JSON.stringify(challenge) %>"></altcha-widget>
<button id="register-button" class='positive-button'>Register</button>
</form>
</body>
<footer>
<%- include('partial/scripts', {user}); %>
<script src="js/register.js"></script>
<script src="/js/register.js"></script>
<script src="/lib/altcha/altcha.js" type="module"></script>
</footer>
</html>