From de898bda26d925913070c840dfc10e5d82b10b54 Mon Sep 17 00:00:00 2001 From: Dissidente Date: Wed, 22 Apr 2020 19:55:00 +0100 Subject: [PATCH 1/5] ADD: alias command attribute --- TalkerNode.js | 60 ++++++++++++++++++++++++-------------------- commands/template.js | 1 + 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/TalkerNode.js b/TalkerNode.js index 5d00822..ae7dcd8 100644 --- a/TalkerNode.js +++ b/TalkerNode.js @@ -461,36 +461,42 @@ function setCmdRank(command, rank) { */ function findCommand(socket, command) { var c = command; - var userRank = socket.db.rank; + var userRank = socket.db.rank; + // hierarchy: direct match > weighted partial match > alias direct match if(commands[c] && userRank >= getCmdRank(c)) { - return [commands[c]]; + // direct match + return [commands[c]]; } else { - // when we have more than one possible command, we - // choose the most heavier from the ones with lower - // getCmdRank - var results = []; - var weigth = 0; - var rank = ranks.list.length - 1; - for (var cmd in commands) { - if(cmd.substr(0, c.length) == c && userRank >= getCmdRank(cmd)) { - var cweigth = 0; - if (typeof commands[cmd].weigth !== 'undefined') - cweigth = commands[cmd].weigth; - if (getCmdRank(cmd) < rank) { - rank = getCmdRank(cmd); - weigth = cweigth; - results = [commands[cmd]]; - } else if (getCmdRank(cmd) === rank) { - if (cweigth > weigth) { - weigth = commands[cmd].weigth; - results = [commands[cmd]]; - } else if (cweigth === weigth) { - results.push(commands[cmd]); - } + // when we have more than one possible command, we + // choose the most heavier from the ones with lower + // getCmdRank + var results = []; + var weigth = 0; + var rank = ranks.list.length - 1; + for (var cmd in commands) { + if(cmd.substr(0, c.length) == c && userRank >= getCmdRank(cmd)) { + // partial match + var cweigth = 0; + if (typeof commands[cmd].weigth !== 'undefined') + cweigth = commands[cmd].weigth; + if (getCmdRank(cmd) < rank) { + rank = getCmdRank(cmd); + weigth = cweigth; + results = [commands[cmd]]; + } else if (getCmdRank(cmd) === rank) { + if (cweigth > weigth) { + weigth = commands[cmd].weigth; + results = [commands[cmd]]; + } else if (cweigth === weigth) { + results.push(commands[cmd]); + } + } + } else if (commands[cmd].alias != 'undefined' && commands[cmd] != "" && commands[cmd].alias == c) { + // alias direct match + return [commands[cmd]] + } } - } - } - return results; + return results; } } diff --git a/commands/template.js b/commands/template.js index da1e8e3..0421c20 100644 --- a/commands/template.js +++ b/commands/template.js @@ -1,5 +1,6 @@ exports.command = { name: "template", // Name of command to be executed (Max 10 chars) + alias: "ctemplate", // Alias for the command (partial match not supported; Max 10 chars) autoload: false, // Should the command be autoloaded at startup unloadable: true, // Can the command be unloaded dynamically min_rank: 0, // Minimum rank to use to execute the command From 74856ed047730985bf38107d83dff873b7b234a1 Mon Sep 17 00:00:00 2001 From: Dissidente Date: Sun, 26 Apr 2020 16:38:22 +0100 Subject: [PATCH 2/5] ADD: main 2FA functions and .2fa command --- TalkerNode.js | 108 +++++++++++++++++++---------- commands/2fa.js | 176 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 250 insertions(+), 34 deletions(-) create mode 100644 commands/2fa.js diff --git a/TalkerNode.js b/TalkerNode.js index ae7dcd8..7c12b19 100644 --- a/TalkerNode.js +++ b/TalkerNode.js @@ -99,6 +99,43 @@ function echo(bool) { return new Buffer.from(bytes); } +/* +* Generates 2FA secret keys and backup codes +*/ +function auth2faGenerateSecretKey() { + var auth2fa = require('speakeasy'); + return auth2fa.generateSecret({length: 20}).base32; +} + +/* +* Validates 2FA OTP code +*/ +function auth2faValidateOTP(username,userToken) { + var userRecord = usersdb.get(username).value() + userToken = userToken.replace('-','') // remove possible dashes from backup code + if(userToken === userRecord.auth2fa_backupCode) { + // user is using a backup code -> burn code and validate ok + auth2faBurnBackupCode(username); + return true; + } else { + // check provided OTP code is valid + var auth2fa = require('speakeasy'); + return (auth2fa.totp.verify({ secret: userRecord.auth2fa_secretKey, + encoding: 'base32', + window: 2, + token: userToken })); + } +} + +/* +* Burn 2FA backup code +*/ +function auth2faBurnBackupCode(username) { + var userRecord = usersdb.get(username).value() + userRecord.auth2fa_backupCode = '!' + userRecord.auth2fa_backupCode + // update db +} + /* * Method used to send data to a socket */ @@ -596,7 +633,10 @@ function command_utility() { sockets: sockets, commands: commands, ranks: ranks, - echo: echo, + echo: echo, + auth2faGenerateSecretKey: auth2faGenerateSecretKey, + auth2faValidateOTP: auth2faValidateOTP, + auth2faBurnBackupCode: auth2faBurnBackupCode, loadCommands: loadCommands, getCmdRank: getCmdRank, setCmdRank: setCmdRank, @@ -668,39 +708,39 @@ function command_utility() { return usersdb.get(name).value(); }, - // returns the username of an "aproximate" user - // read 'getAproxOnlineUser' to understand the difference between - // 'getOnlineUser' and it, same happens here between 'getUser' and - // 'getAproxUser'. - getAproxUser: function getAproxUser(name) { - if (this.getUser(name) !== undefined) return [name]; - var possibilities = []; - for (var key in usersdb.getState()) { - if (name.toLowerCase() === key.toLowerCase().substr(0,name.length) && (name.length < key.length)) { - possibilities.push(key); - } - } - if (possibilities.length === 0) return []; - return possibilities; - }, - - // updates a user in the database - // TODO: argh, we surely don't want this! harden it! - updateUser: function updateUser(username, userObj) { - username = username.toLowerCase().charAt(0).toUpperCase() + username.toLowerCase().slice(1); - usersdb.set(username,userObj).write(); - }, - - // get users list, only insensitive information - getUsersList: function getUsersList() { - var list = []; - for (var key in usersdb.getState()) { - // retrieving username, rank and loginTime. If needed, we can always add stuff later - var val = usersdb.get(key).value(); - list.push({username:key, rank:val.rank, loginTime:val.loginTime}); - } - return list; - }, + // returns the username of an "aproximate" user + // read 'getAproxOnlineUser' to understand the difference between + // 'getOnlineUser' and it, same happens here between 'getUser' and + // 'getAproxUser'. + getAproxUser: function getAproxUser(name) { + if (this.getUser(name) !== undefined) return [name]; + var possibilities = []; + for (var key in usersdb.getState()) { + if (name.toLowerCase() === key.toLowerCase().substr(0,name.length) && (name.length < key.length)) { + possibilities.push(key); + } + } + if (possibilities.length === 0) return []; + return possibilities; + }, + + // updates a user in the database + // TODO: argh, we surely don't want this! harden it! + updateUser: function updateUser(username, userObj) { + username = username.toLowerCase().charAt(0).toUpperCase() + username.toLowerCase().slice(1); + usersdb.set(username,userObj).write(); + }, + + // get users list, only insensitive information + getUsersList: function getUsersList() { + var list = []; + for (var key in usersdb.getState()) { + // retrieving username, rank and loginTime. If needed, we can always add stuff later + var val = usersdb.get(key).value(); + list.push({username:key, rank:val.rank, loginTime:val.loginTime}); + } + return list; + }, // gives a full view of the universe; TODO: we surely don't want this // TODO: in the meantime, we don't need to define a function for this! diff --git a/commands/2fa.js b/commands/2fa.js new file mode 100644 index 0000000..f127cdc --- /dev/null +++ b/commands/2fa.js @@ -0,0 +1,176 @@ +exports.command = { + name: "2fa", + alias: "", + autoload: true, + unloadable: true, + min_rank: 2, + display: "Manages 2 factor authentication for your account", + help: "To enroll your account for 2FA: .2fa enroll + .2fa verify \r\n" + + "To generate a new backup code: .2fa newbackupcode\r\n" + + "To check your account 2FA status: .2fa status\r\n" + + "To disable 2FA for your account: .2fa disable \r\n\r\n" + + "For more detailed help use .2fa help", + usage: ".2fa |disable |newbackupcode|status>", + weigth: 0, + + // returns secret key or backup code in a friendly easy-to-read format + formatFriendlyKeyString: function(keyString) { + return keyString.match(/.{1,4}/g).join('-') + }, + + // outputs command extended help with information on 2fa enrollment procedure + outputDetailedHelp: function(socket, command_access) { + var chalk = require('chalk'); + + command_access.sendData(socket, chalk.blue("+-----------------------------------------------------------------------------+\r\n")); + command_access.sendData(socket, " Detailed help for ." + this.name + " command\r\n"); + command_access.sendData(socket, chalk.blue("+-----------------------------------------------------------------------------+\r\n")); + command_access.sendData(socket, " 1. Enrolling into 2 factor authentication on " + chalk.bold(command_access.talkername) + "\r\n\r\n"); + command_access.sendData(socket, " Enrollment starts by using " + chalk.yellow(chalk.bold("." + this.name + " enroll")) + " command.\r\n\r\n"); + command_access.sendData(socket, " This command will generate a secret key that you will need to configure\r\n"); + command_access.sendData(socket, " on your soft token software, to generate a token to authenticate. Check\r\n"); + command_access.sendData(socket, " https://en.wikipedia.org/wiki/Google_Authenticator#Other_authentication_software\r\n"); + command_access.sendData(socket, " for a list of soft woken software you can use for this purpose.\r\n\r\n"); + command_access.sendData(socket, " After configuring your soft token software with the generated secret key\r\n"); + command_access.sendData(socket, " you need to confirm that it's generating correct tokens, by using the\r\n"); + command_access.sendData(socket, " following command: " + chalk.yellow(chalk.bold("." + this.name + " verify ")) + "\r\n\r\n"); + command_access.sendData(socket, " This command will certify that your 2FA software is properly configured\r\n"); + command_access.sendData(socket, " and will enable 2FA for your account. It will also generate a backup code\r\n"); + command_access.sendData(socket, " which you can use if you are unable to authenticate using 2FA. Using this\r\n"); + command_access.sendData(socket, " backup code will require generating a new one using " + chalk.yellow(chalk.bold("." + this.name + " newbackupcode")) + "\r\n\r\n"); + command_access.sendData(socket, " 2. Checking 2FA status for your user account\r\n\r\n"); + command_access.sendData(socket, " To check the status of 2FA for your account use " + chalk.yellow(chalk.bold("." + this.name + " status")) + "\r\n\r\n"); + command_access.sendData(socket, " 3. Disable 2 factor authentication\r\n\r\n"); + command_access.sendData(socket, " You can disable 2FA at any time by using " + chalk.yellow(chalk.bold("." + this.name + " disable ")) + "\r\n"); + command_access.sendData(socket, " To reenable it on your account you will be required to enroll again\r\n"); + command_access.sendData(socket, chalk.blue("+-----------------------------------------------------------------------------+\r\n")); + }, + + // Function to execute the command + execute: function(socket, command, command_access) { + var chalk = require('chalk'); + var operation = command.split(' ')[0]; + var token = command.split(' ')[1]; + switch(operation) + { + case 'enroll': // starts 2fa enrollment (generates secret key and waits for token verification) + if(socket.db.auth2fa_status) + { + command_access.sendData(socket, "2 factor authentication is already enabled for your account!\r\n"); + command_access.sendData(socket, "If you really want to start the enrollment procedure please disable 2FA first.\r\n"); + } else { + if(typeof socket.db.auth2fa_status === 'undefined') + { + // start enrollment + var newSecretKey = command_access.auth2faGenerateSecretKey(); + socket.db.auth2fa_secretKey = newSecretKey; + socket.db.auth2fa_status = false; + command_access.sendData(socket, "Your secret key is: " + chalk.yellow(chalk.bold(this.formatFriendlyKeyString(newSecretKey))) + "\r\n"); + command_access.sendData(socket, "Please add it to your soft token software (just the letters, the dashes are for\r\n"); + command_access.sendData(socket, "easy reading), and then verify the configuration using " + chalk.yellow(chalk.bold("." + this.name + " verify ")) + "\r\n"); + command_access.sendData(socket, "to complete enrollment.\r\n"); + command_access.updateUser(socket.username, socket.db); + + } else { // enrollment is already ongoing and expects token verification + command_access.sendData(socket, "2 factor authentication enrollment is already taking place for your account!\r\n"); + command_access.sendData(socket, "Either complete token verification with " + chalk.yellow(chalk.bold("." + this.name + " verify ")) + " or disable 2FA.\r\n"); + } + } + break; + case 'verify': // closes 2fa enrollment (verifies token, generates backup code and sets account configuration) + if(socket.db.auth2fa_status) + { + command_access.sendData(socket, "2 factor authentication is already enabled for your account!\r\n"); + command_access.sendData(socket, "If you really want to start the enrollment procedure please disable 2FA first.\r\n"); + } else { + if(typeof socket.db.auth2fa_status === 'undefined') + { + // enrollment has not yet been started + command_access.sendData(socket, "2 factor authentication enrollment has not been started for your account!\r\n"); + command_access.sendData(socket, "To begin enrollment use " + chalk.yellow(chalk.bold("." + this.name + " enroll")) + ".\r\n"); + } else { // verify 2fa configuration and enable it for the account + if(typeof token !== 'undefined' && token.length == 6) + { + if(command_access.auth2faValidateOTP(socket.username,token)) + { // token is valid; enable 2FA for the account + var newBackupCode = command_access.auth2faGenerateSecretKey(); + socket.db.auth2fa_backupCode = newBackupCode; + socket.db.auth2fa_status = true; + command_access.updateUser(socket.username, socket.db); + command_access.sendData(socket, "Your token has been validated. Your account is now enabled for 2FA.\r\n"); + command_access.sendData(socket, "This is your backup code which you can use to login if you are unable to \r\n"); + command_access.sendData(socket, "use 2FA during login: >>>> " + chalk.yellow(chalk.bold(this.formatFriendlyKeyString(newBackupCode))) + " <<<<\r\n"); + command_access.sendData(socket, "Once you use your backup code you will need to generate a new one using.\r\n"); + command_access.sendData(socket, chalk.yellow(chalk.bold("." + this.name + " newbackupcode") + "\r\n")); + } else { + command_access.sendData(socket, chalk.red("Your token failed validation!\r\n")); + command_access.sendData(socket, "Please provide a valid token generated from your secret key!\r\n"); + command_access.sendData(socket, "If you wish to restart the enrollment process please use " + chalk.yellow(chalk.bold("." + this.name + " disable")) + "\r\n"); + } + } else { + command_access.sendData(socket, "Please provide a valid token generated from your secret key!\r\n"); + } + } + } + break; + case 'disable': // disables 2FA when enrollment has taken place + if(typeof socket.db.auth2fa_status !== 'undefined') + { // 2FA is enabled or in the enrollment process + var continueDisable = false; + if(socket.db.auth2fa_status) + { + if(typeof token !== 'undefined' && command_access.auth2faValidateOTP(socket.username,token)) + { + continueDisable = true; + command_access.sendData(socket, "2 factor authentication has been disabled for your account.\r\n"); + } else { + continueDisable = false; + command_access.sendData(socket, "Invalid token!\r\n"); + } + } else { + continueDisable = true; + command_access.sendData(socket, "2 factor authentication enrollment process has been canceled.\r\n"); + } + if(continueDisable) + { + delete socket.db.auth2fa_status; + delete socket.db.auth2fa_secretKey; + delete socket.db.auth2fa_backupCode; + command_access.updateUser(socket.username, socket.db); + } + } else { + command_access.sendData(socket, "You don't have 2 factor authentication enabled for your account!\r\n"); + } + break; + case 'newbackupcode': // generates a new backup code + if(socket.db.auth2fa_status) + { + var newBackupCode = command_access.auth2faGenerateSecretKey(); + socket.db.auth2fa_backupCode = newBackupCode; + command_access.sendData(socket, "Your new backup code is: >>>> " + chalk.yellow(chalk.bold(this.formatFriendlyKeyString(newBackupCode))) + " <<<<\r\n"); + command_access.updateUser(socket.username, socket.db); + } else { + command_access.sendData(socket, "You don't have 2 factor authentication enabled for your account!\r\n"); + } + break; + case 'status': // queries 2fa configuration state + command_access.sendData(socket, "+----------------------------------------------------------------------------+\r\n"); + if(typeof socket.db.auth2fa_status != 'undefined') // 2FA is not configured + { + if(socket.db.auth2fa_status) { // enabled + command_access.sendData(socket, ("+ 2 factor authentication " + chalk.bold("is enabled") + " for your account").padEnd(77) + "+ \r\n"); + } else { // enrolling + command_access.sendData(socket, ("+ " + chalk.yellow("You are currently enrolling into 2FA")).padEnd(77) + "+\r\n"); + command_access.sendData(socket, ("+ Next step: verify soft token configuration using " + chalk.bold("." + this.name + " verify ")).padEnd(77) + "+\r\n"); + command_access.sendData(socket, ("+ For further help use " + chalk.bold("." + this.name + " help")).padEnd(77) + "+\r\n"); + } + } else { + command_access.sendData(socket, ("+ 2 factor authentication is " + chalk.bold("not enabled") + " for your account").padEnd(77) + "+\r\n"); + } + command_access.sendData(socket, "+----------------------------------------------------------------------------+\r\n"); + break; + default: // handles help command too + this.outputDetailedHelp(socket, command_access); + } + } +} \ No newline at end of file From 2b743bab5a5083d81a56c022943c87206d4b7813 Mon Sep 17 00:00:00 2001 From: Dissidente Date: Mon, 27 Apr 2020 20:21:02 +0100 Subject: [PATCH 3/5] ADD: .2faadmin --- TalkerNode.js | 4 +- commands/2fa.js | 10 ++--- commands/2faadmin.js | 98 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 7 deletions(-) create mode 100644 commands/2faadmin.js diff --git a/TalkerNode.js b/TalkerNode.js index 7c12b19..f3a83db 100644 --- a/TalkerNode.js +++ b/TalkerNode.js @@ -131,9 +131,9 @@ function auth2faValidateOTP(username,userToken) { * Burn 2FA backup code */ function auth2faBurnBackupCode(username) { - var userRecord = usersdb.get(username).value() + var userRecord = usersdb.get(username.toLowerCase().charAt(0).toUpperCase() + username.toLowerCase().slice(1)).value(); userRecord.auth2fa_backupCode = '!' + userRecord.auth2fa_backupCode - // update db + usersdb.set(username,userRecord).write(); } /* diff --git a/commands/2fa.js b/commands/2fa.js index f127cdc..e7dd7f9 100644 --- a/commands/2fa.js +++ b/commands/2fa.js @@ -3,13 +3,13 @@ exports.command = { alias: "", autoload: true, unloadable: true, - min_rank: 2, + min_rank: 0, display: "Manages 2 factor authentication for your account", help: "To enroll your account for 2FA: .2fa enroll + .2fa verify \r\n" + - "To generate a new backup code: .2fa newbackupcode\r\n" + - "To check your account 2FA status: .2fa status\r\n" + - "To disable 2FA for your account: .2fa disable \r\n\r\n" + - "For more detailed help use .2fa help", + "To generate a new backup code: .2fa newbackupcode\r\n" + + "To check your account 2FA status: .2fa status\r\n" + + "To disable 2FA for your account: .2fa disable \r\n\r\n" + + "For more detailed help use .2fa help", usage: ".2fa |disable |newbackupcode|status>", weigth: 0, diff --git a/commands/2faadmin.js b/commands/2faadmin.js new file mode 100644 index 0000000..93af56b --- /dev/null +++ b/commands/2faadmin.js @@ -0,0 +1,98 @@ +exports.command = { + name: "2faadmin", + alias: "", + autoload: true, + unloadable: true, + min_rank: 7, + display: "Manages 2 factor authentication for users", + help: "To reset 2FA configuration for a user: .2faadmin resetuser \r\n" + + "To burn a backup code for a user: .2faadmin burnbackupcode \r\n" + + "To return the general 2FA stats: .2faadmin status\r\n" + + "To return the 2FA status for a user: .2faadmin status ", + usage: ".2faadmin |burnbackupcode |status []>", + weigth: 10, + + // Function to execute the command + execute: function(socket, command, command_access) { + var chalk = require('chalk'); + var operation = command.split(' ')[0]; + var argument = command.split(' ')[1]; + switch(operation) + { + case 'resetuser': + if(typeof argument !== 'undefined') { + var userRecord = command_access.getUser(argument); + if(typeof userRecord !== 'undefined') { + delete userRecord.auth2fa_status; + delete userRecord.auth2fa_secretKey; + delete userRecord.auth2fa_backupCode; + command_access.updateUser(argument, socket.db); + command_access.sendData(socket, "2FA configuration for user " + chalk.bold(argument) + " has been resetted.\r\n"); + } else { + command_access.sendData(socket, "Username not found!\r\n"); + } + } else { + command_access.sendData(socket, "Please provide a username!\r\n"); + } + break; + case 'burnbackupcode': + if(typeof argument !== 'undefined') { + var userRecord = command_access.getUser(argument); + if(typeof userRecord !== 'undefined') { + if(typeof userRecord.auth2fa_backupCode !== 'undefined') + { + if(userRecord.auth2fa_backupCode.substr(0,1) !== '!') { + command_access.auth2faBurnBackupCode(argument); + command_access.sendData(socket, "Backup code for user " + chalk.bold(argument) + " has been burned. User will need to generate a new one.\r\n"); + } else { + command_access.sendData(socket, "Backup code for user " + chalk.bold(argument) + " is already burned.\r\n"); + } + } else { + command_access.sendData(socket, "User "+ argument +" does not have a backup code! (2FA disabled or enrollment not completed)\r\n"); + } + } else { + command_access.sendData(socket, "Username not found!\r\n"); + } + } else { + command_access.sendData(socket, "Please provide a username!\r\n"); + } + break; + case 'status': + if(typeof argument === 'undefined') { // general status + var userList = command_access.getUsersList(); + var countEnabled = 0, countDisabled = 0, countEnrolling = 0; + for (let index = 0; index < userList.length; index++) { + var userRecord = command_access.getUser(userList[index].username); + if(typeof userRecord.auth2fa_status === 'undefined') { + countDisabled++; + } else if(userRecord.auth2fa_status) { + countEnabled++; + } else { + countEnrolling++; + } + } + command_access.sendData(socket, chalk.green("User accounts with 2FA enabled : " + countEnabled + "\r\n")); + command_access.sendData(socket, chalk.yellow("User accounts with 2FA ongoing enrollment: " + countEnrolling + "\r\n")); + command_access.sendData(socket, chalk.red("User accounts with 2FA disabled : " + countDisabled + "\r\n")); + } else { // user status + var userRecord = command_access.getUser(argument); + if(typeof userRecord !== 'undefined') { + if(typeof userRecord.auth2fa_status !== 'undefined') { + if(userRecord.auth2fa_status) { + command_access.sendData(socket, "2 factor authentication " + chalk.bold("is enabled") + " for user " + argument + "\r\n"); + } else { + command_access.sendData(socket, "2 factor authentication is in " + chalk.bold("enrollment process") + " for user " + argument + "\r\n"); + } + } else { + command_access.sendData(socket, "2 factor authentication is " + chalk.bold("not enabled") + " for user " + argument + "\r\n"); + } + } else { + command_access.sendData(socket, "Username not found!\r\n"); + } + } + break; + default: + command_access.sendData(socket, "Invalid operation for ." + this.name + ". Check syntax using .help 2faadmin\r\n"); + } + } +} \ No newline at end of file From d7b672267377273b9e263f449bc0dd20c00b64bc Mon Sep 17 00:00:00 2001 From: Dissidente Date: Mon, 27 Apr 2020 21:39:49 +0100 Subject: [PATCH 4/5] ADD: 2FA login stage + burned backup code info on .2fa(admin) status --- TalkerNode.js | 27 ++++++++++++++++++++++++--- commands/2fa.js | 3 +++ commands/2faadmin.js | 11 +++++++++-- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/TalkerNode.js b/TalkerNode.js index f3a83db..1e55b5f 100644 --- a/TalkerNode.js +++ b/TalkerNode.js @@ -112,7 +112,7 @@ function auth2faGenerateSecretKey() { */ function auth2faValidateOTP(username,userToken) { var userRecord = usersdb.get(username).value() - userToken = userToken.replace('-','') // remove possible dashes from backup code + userToken = userToken.replace(/-/g,'') // remove possible dashes from backup code if(userToken === userRecord.auth2fa_backupCode) { // user is using a backup code -> burn code and validate ok auth2faBurnBackupCode(username); @@ -122,7 +122,7 @@ function auth2faValidateOTP(username,userToken) { var auth2fa = require('speakeasy'); return (auth2fa.totp.verify({ secret: userRecord.auth2fa_secretKey, encoding: 'base32', - window: 2, + window: 3, token: userToken })); } } @@ -297,12 +297,33 @@ function receiveData(socket, data) { return; } } - } else if (socket.db.password !== crypto.createHash('sha512').update(cleanData).digest('hex')) { + } else if (typeof socket.auth2faOK === 'undefined' && socket.db.password !== crypto.createHash('sha512').update(cleanData).digest('hex')) { require("fs").appendFileSync('auth.log', new Date().toISOString() + " " + socket.remoteAddress + " with port " + socket.remotePort + " failed to log in, username attempted was: " + socket.username + "\r\n"); delete socket.username; delete socket.db; sendData(socket, chalk.red("\r\nWrong password! ") + "\r\nLet's start from the beggining...\r\n" + chalk.cyan("Tell me your name: ")); return; + } else { + // password authentication sucessfull + if(socket.db.auth2fa_status && typeof socket.auth2faOK === 'undefined') { + // 2FA challenge + sendData(socket, chalk.cyan("\r\nGive me your 2FA token or backup code: ")); + sendData(socket, echo(false)); + socket.auth2faOK = false; + return; + } else if(socket.db.auth2fa_status && !socket.auth2faOK) { + // 2FA verification + if(!auth2faValidateOTP(socket.username,cleanData)) { + require("fs").appendFileSync('auth.log', new Date().toISOString() + " " + socket.remoteAddress + " with port " + socket.remotePort + " failed to log in with a valid 2fa token, username attempted was: " + socket.username + "\r\n"); + delete socket.username; + delete socket.auth2faOK; + delete socket.db; + sendData(socket, chalk.red("\r\nInvalid token! ") + "\r\nLet's start from the beggining...\r\n" + chalk.cyan("Tell me your name: ")); + return; + } else { + socket.auth2faOK = true; + } + } } // entering the talker... diff --git a/commands/2fa.js b/commands/2fa.js index e7dd7f9..6955492 100644 --- a/commands/2fa.js +++ b/commands/2fa.js @@ -159,6 +159,9 @@ exports.command = { { if(socket.db.auth2fa_status) { // enabled command_access.sendData(socket, ("+ 2 factor authentication " + chalk.bold("is enabled") + " for your account").padEnd(77) + "+ \r\n"); + if(socket.db.auth2fa_backupCode.substr(0,1) === '!') { + command_access.sendData(socket, ("+ " + chalk.red("Your backup code is burned!") + " Type " + chalk.bold("." + this.name + " newbackupcode") + " to generate a new one.").padEnd(77) + "+ \r\n"); + } } else { // enrolling command_access.sendData(socket, ("+ " + chalk.yellow("You are currently enrolling into 2FA")).padEnd(77) + "+\r\n"); command_access.sendData(socket, ("+ Next step: verify soft token configuration using " + chalk.bold("." + this.name + " verify ")).padEnd(77) + "+\r\n"); diff --git a/commands/2faadmin.js b/commands/2faadmin.js index 93af56b..bb8a491 100644 --- a/commands/2faadmin.js +++ b/commands/2faadmin.js @@ -60,26 +60,33 @@ exports.command = { case 'status': if(typeof argument === 'undefined') { // general status var userList = command_access.getUsersList(); - var countEnabled = 0, countDisabled = 0, countEnrolling = 0; + var countEnabled = 0, countDisabled = 0, countEnrolling = 0, countBurnedBackupCode = 0; for (let index = 0; index < userList.length; index++) { var userRecord = command_access.getUser(userList[index].username); if(typeof userRecord.auth2fa_status === 'undefined') { countDisabled++; } else if(userRecord.auth2fa_status) { countEnabled++; + if(userRecord.auth2fa_backupCode.substr(0,1) === '!') { + countBurnedBackupCode++; + } } else { countEnrolling++; } } command_access.sendData(socket, chalk.green("User accounts with 2FA enabled : " + countEnabled + "\r\n")); command_access.sendData(socket, chalk.yellow("User accounts with 2FA ongoing enrollment: " + countEnrolling + "\r\n")); - command_access.sendData(socket, chalk.red("User accounts with 2FA disabled : " + countDisabled + "\r\n")); + command_access.sendData(socket, chalk.red("User accounts with burned backup codes : " + countBurnedBackupCode + "\r\n")); + command_access.sendData(socket, chalk.blueBright("User accounts with 2FA disabled : " + countDisabled + "\r\n")); } else { // user status var userRecord = command_access.getUser(argument); if(typeof userRecord !== 'undefined') { if(typeof userRecord.auth2fa_status !== 'undefined') { if(userRecord.auth2fa_status) { command_access.sendData(socket, "2 factor authentication " + chalk.bold("is enabled") + " for user " + argument + "\r\n"); + if(userRecord.auth2fa_backupCode.substr(0,1) === '!') { + command_access.sendData(socket, chalk.red("Backup code for user " + argument + " is burned!\r\n")); + } } else { command_access.sendData(socket, "2 factor authentication is in " + chalk.bold("enrollment process") + " for user " + argument + "\r\n"); } From 3588d1c099f353a6e0a907115b5ca0675ea71d57 Mon Sep 17 00:00:00 2001 From: Dissidente Date: Fri, 1 May 2020 12:46:58 +0100 Subject: [PATCH 5/5] CHANGE: Replaced speakeasy library by otpauth for 2FA --- TalkerNode.js | 22 +++++++++++++--------- commands/2fa.js | 4 ++-- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/TalkerNode.js b/TalkerNode.js index 1e55b5f..0762f92 100644 --- a/TalkerNode.js +++ b/TalkerNode.js @@ -103,15 +103,15 @@ function echo(bool) { * Generates 2FA secret keys and backup codes */ function auth2faGenerateSecretKey() { - var auth2fa = require('speakeasy'); - return auth2fa.generateSecret({length: 20}).base32; + var OTPAuth = require('otpauth'); + return (new OTPAuth.Secret().b32); } /* * Validates 2FA OTP code */ function auth2faValidateOTP(username,userToken) { - var userRecord = usersdb.get(username).value() + var userRecord = usersdb.get(username).value(); userToken = userToken.replace(/-/g,'') // remove possible dashes from backup code if(userToken === userRecord.auth2fa_backupCode) { // user is using a backup code -> burn code and validate ok @@ -119,11 +119,15 @@ function auth2faValidateOTP(username,userToken) { return true; } else { // check provided OTP code is valid - var auth2fa = require('speakeasy'); - return (auth2fa.totp.verify({ secret: userRecord.auth2fa_secretKey, - encoding: 'base32', - window: 3, - token: userToken })); + const OTPAuth = require('otpauth'); + var secretKey = userRecord.auth2fa_secretKey; + var TOTP = new OTPAuth.TOTP({ secret: secretKey }); + + var authDelta = TOTP.validate({ + token: userToken, + window: 3 + }); + return !(authDelta === null); } } @@ -132,7 +136,7 @@ function auth2faValidateOTP(username,userToken) { */ function auth2faBurnBackupCode(username) { var userRecord = usersdb.get(username.toLowerCase().charAt(0).toUpperCase() + username.toLowerCase().slice(1)).value(); - userRecord.auth2fa_backupCode = '!' + userRecord.auth2fa_backupCode + userRecord.auth2fa_backupCode = '!' + userRecord.auth2fa_backupCode; usersdb.set(username,userRecord).write(); } diff --git a/commands/2fa.js b/commands/2fa.js index 6955492..7e3fff7 100644 --- a/commands/2fa.js +++ b/commands/2fa.js @@ -66,8 +66,8 @@ exports.command = { socket.db.auth2fa_secretKey = newSecretKey; socket.db.auth2fa_status = false; command_access.sendData(socket, "Your secret key is: " + chalk.yellow(chalk.bold(this.formatFriendlyKeyString(newSecretKey))) + "\r\n"); - command_access.sendData(socket, "Please add it to your soft token software (just the letters, the dashes are for\r\n"); - command_access.sendData(socket, "easy reading), and then verify the configuration using " + chalk.yellow(chalk.bold("." + this.name + " verify ")) + "\r\n"); + command_access.sendData(socket, "Please add it to your soft token software (dashes are for easy reading only),\r\n"); + command_access.sendData(socket, "and then verify the configuration using " + chalk.yellow(chalk.bold("." + this.name + " verify ")) + "\r\n"); command_access.sendData(socket, "to complete enrollment.\r\n"); command_access.updateUser(socket.username, socket.db);