+ Implement SSH PubKey authentication

* Security related items to config/security dir
This commit is contained in:
Bryan Ashby 2019-02-20 23:55:09 -07:00
parent 65ef1feb6c
commit 57938e761e
No known key found for this signature in database
GPG Key ID: B49EB437951D2542
7 changed files with 164 additions and 96 deletions

View File

@ -41,7 +41,7 @@ Report your issue on Xibalba BBS, hop in #enigma-bbs on Freenet and chat, or
[file a issue on GitHub](https://github.com/NuSkooler/enigma-bbs/issues). [file a issue on GitHub](https://github.com/NuSkooler/enigma-bbs/issues).
# 0.0.9-alpha to 0.0.10-alpha # 0.0.9-alpha to 0.0.10-alpha
* Security related files such as private keys and certs are now looked for in `config/security` by default.
# 0.0.8-alpha to 0.0.9-alpha # 0.0.8-alpha to 0.0.9-alpha
* Development is now against Node.js 10.x LTS. Follow your standard upgrade path to update to Node 10.x before using 0.0.9-alpha! * Development is now against Node.js 10.x LTS. Follow your standard upgrade path to update to Node 10.x before using 0.0.9-alpha!

View File

@ -250,6 +250,7 @@ function getDefaultConfig() {
paths : { paths : {
config : paths.join(__dirname, './../config/'), config : paths.join(__dirname, './../config/'),
security : paths.join(__dirname, './../config/security'), // certs, keys, etc.
mods : paths.join(__dirname, './../mods/'), mods : paths.join(__dirname, './../mods/'),
loginServers : paths.join(__dirname, './servers/login/'), loginServers : paths.join(__dirname, './servers/login/'),
contentServers : paths.join(__dirname, './servers/content/'), contentServers : paths.join(__dirname, './servers/content/'),
@ -259,7 +260,7 @@ function getDefaultConfig() {
art : paths.join(__dirname, './../art/general/'), art : paths.join(__dirname, './../art/general/'),
themes : paths.join(__dirname, './../art/themes/'), themes : paths.join(__dirname, './../art/themes/'),
logs : paths.join(__dirname, './../logs/'), // :TODO: set up based on system, e.g. /var/logs/enigmabbs or such logs : paths.join(__dirname, './../logs/'),
db : paths.join(__dirname, './../db/'), db : paths.join(__dirname, './../db/'),
modsDb : paths.join(__dirname, './../db/mods/'), modsDb : paths.join(__dirname, './../db/mods/'),
dropFiles : paths.join(__dirname, './../drop/'), // + "/node<x>/ dropFiles : paths.join(__dirname, './../drop/'), // + "/node<x>/
@ -284,10 +285,10 @@ function getDefaultConfig() {
// //
// > openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \ // > openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \
// -pkeyopt rsa_keygen_pubexp:65537 | openssl rsa \ // -pkeyopt rsa_keygen_pubexp:65537 | openssl rsa \
// -out ./config/ssh_private_key.pem -aes128 // -out ./config/security/ssh_private_key.pem -aes128
// //
// (The above is a more modern equivelant of the following): // (The above is a more modern equivalent of the following):
// > openssl genrsa -aes128 -out ./config/ssh_private_key.pem 2048 // > openssl genrsa -aes128 -out ./config/security/ssh_private_key.pem 2048
// //
// 2 - Set 'privateKeyPass' to the password you used in step #1 // 2 - Set 'privateKeyPass' to the password you used in step #1
// //
@ -297,7 +298,7 @@ function getDefaultConfig() {
// - https://blog.sleeplessbeastie.eu/2017/12/28/how-to-generate-private-key/ // - https://blog.sleeplessbeastie.eu/2017/12/28/how-to-generate-private-key/
// - https://gist.github.com/briansmith/2ee42439923d8e65a266994d0f70180b // - https://gist.github.com/briansmith/2ee42439923d8e65a266994d0f70180b
// //
privateKeyPem : paths.join(__dirname, './../config/ssh_private_key.pem'), privateKeyPem : paths.join(__dirname, './../config/security/ssh_private_key.pem'),
firstMenu : 'sshConnected', firstMenu : 'sshConnected',
firstMenuNewUser : 'sshConnectedNewUser', firstMenuNewUser : 'sshConnectedNewUser',

View File

@ -14,6 +14,8 @@ const {
Errors, Errors,
ErrorReasons ErrorReasons
} = require('../../enig_error.js'); } = require('../../enig_error.js');
const User = require('../../user.js');
const UserProps = require('../../user_property.js');
// deps // deps
const ssh2 = require('ssh2'); const ssh2 = require('ssh2');
@ -21,6 +23,7 @@ const fs = require('graceful-fs');
const util = require('util'); const util = require('util');
const _ = require('lodash'); const _ = require('lodash');
const assert = require('assert'); const assert = require('assert');
const crypto = require('crypto');
const ModuleInfo = exports.moduleInfo = { const ModuleInfo = exports.moduleInfo = {
name : 'SSH', name : 'SSH',
@ -42,8 +45,6 @@ function SSHClient(clientConn) {
clientConn.on('authentication', function authAttempt(ctx) { clientConn.on('authentication', function authAttempt(ctx) {
const username = ctx.username || ''; const username = ctx.username || '';
const password = ctx.password || '';
const config = Config(); const config = Config();
self.isNewUser = (config.users.newUserNames || []).indexOf(username) > -1; self.isNewUser = (config.users.newUserNames || []).indexOf(username) > -1;
@ -106,37 +107,36 @@ function SSHClient(clientConn) {
} }
}; };
// const authWithPasswordOrPubKey = (authType) => {
// If the system is open and |isNewUser| is true, the login if('pubKey' !== authType || !self.user.isAuthenticated() || !ctx.signature) {
// sequence is hijacked in order to start the application process. // step 1: login/auth using PubKey
// userLogin(self, ctx.username, ctx.password, { authType, ctx }, (err) => {
if(false === config.general.closedSystem && self.isNewUser) { if(err) {
return ctx.accept(); if(isSpecialHandleError(err)) {
} return handleSpecialError(err, username);
}
if(username.length > 0 && password.length > 0) { if(Errors.BadLogin().code === err.code) {
userLogin(self, ctx.username, ctx.password, function authResult(err) { return slowTerminateConnection();
if(err) { }
if(isSpecialHandleError(err)) {
return handleSpecialError(err, username); return safeContextReject(SSHClient.ValidAuthMethods);
} }
if(Errors.BadLogin().code === err.code) { ctx.accept();
return slowTerminateConnection(); });
} } else {
// step 2: verify signature
return safeContextReject(SSHClient.ValidAuthMethods); const pubKeyActual = ssh2.utils.parseKey(self.user.getProperty(UserProps.LoginPubKey));
if(!pubKeyActual || !pubKeyActual.verify(ctx.blob, ctx.signature)) {
return slowTerminateConnection();
} }
return ctx.accept();
ctx.accept();
});
} else {
if(-1 === SSHClient.ValidAuthMethods.indexOf(ctx.method)) {
return safeContextReject(SSHClient.ValidAuthMethods);
} }
};
const authKeyboardInteractive = () => {
if(0 === username.length) { if(0 === username.length) {
// :TODO: can we display something here?
return safeContextReject(); return safeContextReject();
} }
@ -176,6 +176,30 @@ function SSHClient(clientConn) {
} }
}); });
}); });
};
//
// If the system is open and |isNewUser| is true, the login
// sequence is hijacked in order to start the application process.
//
if(false === config.general.closedSystem && self.isNewUser) {
return ctx.accept();
}
switch(ctx.method) {
case 'password' :
return authWithPasswordOrPubKey('password');
//return authWithPassword();
case 'publickey' :
return authWithPasswordOrPubKey('pubKey');
//return authWithPubKey();
case 'keyboard-interactive' :
return authKeyboardInteractive();
default :
return safeContextReject(SSHClient.ValidAuthMethods);
} }
}); });
@ -293,7 +317,11 @@ function SSHClient(clientConn) {
util.inherits(SSHClient, baseClient.Client); util.inherits(SSHClient, baseClient.Client);
SSHClient.ValidAuthMethods = [ 'password', 'keyboard-interactive' ]; SSHClient.ValidAuthMethods = [
'password',
'keyboard-interactive',
'publickey',
];
exports.getModule = class SSHServerModule extends LoginServerModule { exports.getModule = class SSHServerModule extends LoginServerModule {
constructor() { constructor() {

View File

@ -21,6 +21,7 @@ const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const moment = require('moment'); const moment = require('moment');
const sanatizeFilename = require('sanitize-filename'); const sanatizeFilename = require('sanitize-filename');
const ssh2 = require('ssh2');
exports.isRootUserId = function(id) { return 1 === id; }; exports.isRootUserId = function(id) { return 1 === id; };
@ -47,7 +48,10 @@ module.exports = class User {
static get StandardPropertyGroups() { static get StandardPropertyGroups() {
return { return {
password : [ UserProps.PassPbkdf2Salt, UserProps.PassPbkdf2Dk ], auth : [
UserProps.PassPbkdf2Salt, UserProps.PassPbkdf2Dk,
UserProps.LoginPubKey, UserProps.LoginPubKeyFingerprintSHA256,
],
}; };
} }
@ -174,10 +178,49 @@ module.exports = class User {
}); });
} }
authenticate(username, password, cb) { authenticate(username, password, options, cb) {
if(!cb && _.isFunction(options)) {
cb = options;
options = {};
}
const self = this; const self = this;
const tempAuthInfo = {}; const tempAuthInfo = {};
const validatePassword = (props, callback) => {
User.generatePasswordDerivedKey(password, props[UserProps.PassPbkdf2Salt], (err, dk) => {
if(err) {
return callback(err);
}
//
// Use constant time comparison here for security feel-goods
//
const passDkBuf = Buffer.from(dk, 'hex');
const propsDkBuf = Buffer.from(props[UserProps.PassPbkdf2Dk], 'hex');
return callback(crypto.timingSafeEqual(passDkBuf, propsDkBuf) ?
null :
Errors.AccessDenied('Invalid password')
);
});
};
const validatePubKey = (props, callback) => {
const pubKeyActual = ssh2.utils.parseKey(props[UserProps.LoginPubKey]);
if(!pubKeyActual) {
return callback(Errors.AccessDenied('Invalid public key'));
}
if(options.ctx.key.algo != pubKeyActual.type ||
!crypto.timingSafeEqual(options.ctx.key.data, pubKeyActual.getPublicSSH()))
{
return callback(Errors.AccessDenied('Invalid public key'));
}
return callback(null);
};
async.waterfall( async.waterfall(
[ [
function fetchUserId(callback) { function fetchUserId(callback) {
@ -191,27 +234,15 @@ module.exports = class User {
}, },
function getRequiredAuthProperties(callback) { function getRequiredAuthProperties(callback) {
// fetch properties required for authentication // fetch properties required for authentication
User.loadProperties(tempAuthInfo.userId, { names : User.StandardPropertyGroups.password }, (err, props) => { User.loadProperties( tempAuthInfo.userId, { names : User.StandardPropertyGroups.auth }, (err, props) => {
return callback(err, props); return callback(err, props);
}); });
}, },
function getDkWithSalt(props, callback) { function validatePassOrPubKey(props, callback) {
// get DK from stored salt and password provided if('pubKey' === options.authType) {
User.generatePasswordDerivedKey(password, props[UserProps.PassPbkdf2Salt], (err, dk) => { return validatePubKey(props, callback);
return callback(err, dk, props[UserProps.PassPbkdf2Dk]); }
}); return validatePassword(props, callback);
},
function validateAuth(passDk, propsDk, callback) {
//
// Use constant time comparison here for security feel-goods
//
const passDkBuf = Buffer.from(passDk, 'hex');
const propsDkBuf = Buffer.from(propsDk, 'hex');
return callback(crypto.timingSafeEqual(passDkBuf, propsDkBuf) ?
null :
Errors.AccessDenied('Invalid password')
);
}, },
function initProps(callback) { function initProps(callback) {
User.loadProperties(tempAuthInfo.userId, (err, allProps) => { User.loadProperties(tempAuthInfo.userId, (err, allProps) => {

View File

@ -22,7 +22,12 @@ const _ = require('lodash');
exports.userLogin = userLogin; exports.userLogin = userLogin;
function userLogin(client, username, password, cb) { function userLogin(client, username, password, options, cb) {
if(!cb && _.isFunction(options)) {
cb = options;
options = {};
}
const config = Config(); const config = Config();
if(config.users.badUserNames.includes(username.toLowerCase())) { if(config.users.badUserNames.includes(username.toLowerCase())) {
@ -34,7 +39,7 @@ function userLogin(client, username, password, cb) {
}, 2000); }, 2000);
} }
client.user.authenticate(username, password, err => { client.user.authenticate(username, password, options, err => {
if(err) { if(err) {
client.user.sessionFailedLoginAttempts = _.get(client.user, 'sessionFailedLoginAttempts', 0) + 1; client.user.sessionFailedLoginAttempts = _.get(client.user, 'sessionFailedLoginAttempts', 0) + 1;
const disconnect = config.users.failedLogin.disconnect; const disconnect = config.users.failedLogin.disconnect;

View File

@ -8,54 +8,57 @@
// can utilize their own properties as well! // can utilize their own properties as well!
// //
module.exports = { module.exports = {
PassPbkdf2Salt : 'pw_pbkdf2_salt', PassPbkdf2Salt : 'pw_pbkdf2_salt',
PassPbkdf2Dk : 'pw_pbkdf2_dk', PassPbkdf2Dk : 'pw_pbkdf2_dk',
AccountStatus : 'account_status', // See User.AccountStatus enum AccountStatus : 'account_status', // See User.AccountStatus enum
RealName : 'real_name', RealName : 'real_name',
Sex : 'sex', Sex : 'sex',
Birthdate : 'birthdate', Birthdate : 'birthdate',
Location : 'location', Location : 'location',
Affiliations : 'affiliation', Affiliations : 'affiliation',
EmailAddress : 'email_address', EmailAddress : 'email_address',
WebAddress : 'web_address', WebAddress : 'web_address',
TermHeight : 'term_height', TermHeight : 'term_height',
TermWidth : 'term_width', TermWidth : 'term_width',
ThemeId : 'theme_id', ThemeId : 'theme_id',
AccountCreated : 'account_created', AccountCreated : 'account_created',
LastLoginTs : 'last_login_timestamp', LastLoginTs : 'last_login_timestamp',
LoginCount : 'login_count', LoginCount : 'login_count',
UserComment : 'user_comment', // NYI UserComment : 'user_comment', // NYI
DownloadQueue : 'dl_queue', // download_queue.js DownloadQueue : 'dl_queue', // download_queue.js
FailedLoginAttempts : 'failed_login_attempts', FailedLoginAttempts : 'failed_login_attempts',
AccountLockedTs : 'account_locked_timestamp', AccountLockedTs : 'account_locked_timestamp',
AccountLockedPrevStatus : 'account_locked_prev_status', // previous account status before lock out AccountLockedPrevStatus : 'account_locked_prev_status', // previous account status before lock out
EmailPwResetToken : 'email_password_reset_token', EmailPwResetToken : 'email_password_reset_token',
EmailPwResetTokenTs : 'email_password_reset_token_ts', EmailPwResetTokenTs : 'email_password_reset_token_ts',
FileAreaTag : 'file_area_tag', FileAreaTag : 'file_area_tag',
FileBaseFilters : 'file_base_filters', FileBaseFilters : 'file_base_filters',
FileBaseFilterActiveUuid : 'file_base_filter_active_uuid', FileBaseFilterActiveUuid : 'file_base_filter_active_uuid',
FileBaseLastViewedId : 'user_file_base_last_viewed', FileBaseLastViewedId : 'user_file_base_last_viewed',
FileDlTotalCount : 'dl_total_count', FileDlTotalCount : 'dl_total_count',
FileUlTotalCount : 'ul_total_count', FileUlTotalCount : 'ul_total_count',
FileDlTotalBytes : 'dl_total_bytes', FileDlTotalBytes : 'dl_total_bytes',
FileUlTotalBytes : 'ul_total_bytes', FileUlTotalBytes : 'ul_total_bytes',
MessageConfTag : 'message_conf_tag', MessageConfTag : 'message_conf_tag',
MessageAreaTag : 'message_area_tag', MessageAreaTag : 'message_area_tag',
MessagePostCount : 'post_count', MessagePostCount : 'post_count',
DoorRunTotalCount : 'door_run_total_count', DoorRunTotalCount : 'door_run_total_count',
DoorRunTotalMinutes : 'door_run_total_minutes', DoorRunTotalMinutes : 'door_run_total_minutes',
AchievementTotalCount : 'achievement_total_count', AchievementTotalCount : 'achievement_total_count',
AchievementTotalPoints : 'achievement_total_points', AchievementTotalPoints : 'achievement_total_points',
MinutesOnlineTotalCount : 'minutes_online_total_count', MinutesOnlineTotalCount : 'minutes_online_total_count',
LoginPubKey : 'login_public_key', // OpenSSL format
//LoginPubKeyFingerprintSHA256 : 'login_public_key_fp_sha256', // hint: ssh-kegen -lf id_rsa.pub
}; };

View File

@ -118,10 +118,10 @@
// //
// > openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \ // > openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \
// -pkeyopt rsa_keygen_pubexp:65537 | openssl rsa \ // -pkeyopt rsa_keygen_pubexp:65537 | openssl rsa \
// -out ./config/ssh_private_key.pem -aes128 // -out ./config/security/ssh_private_key.pem -aes128
// //
// (The above is a more modern equivelant of the following): // (The above is a more modern equivelant of the following):
// > openssl genrsa -aes128 -out ./config/ssh_private_key.pem 2048 // > openssl genrsa -aes128 -out ./config/security/ssh_private_key.pem 2048
// //
// 2 - Set 'privateKeyPass' to the password you used in step #1 // 2 - Set 'privateKeyPass' to the password you used in step #1
// //