Good progress on QR support

This commit is contained in:
Bryan Ashby 2019-05-09 19:56:04 -06:00
parent 3c6c8d2a5c
commit 6070bc94e7
No known key found for this signature in database
GPG Key ID: B49EB437951D2542
5 changed files with 139 additions and 38 deletions

View File

@ -16,6 +16,7 @@ const UserProps = require('../user_property.js');
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const moment = require('moment'); const moment = require('moment');
const fs = require('fs-extra');
exports.handleUserCommand = handleUserCommand; exports.handleUserCommand = handleUserCommand;
@ -343,6 +344,62 @@ Affiliations : ${propOrNA(UserProps.Affiliations)}
`); `);
} }
function twoFactorAuth(user) {
if(argv._.length < 4) {
return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
}
const {
OTPTypes,
prepareOTP,
} = require('../../core/user_2fa_otp.js');
async.waterfall(
[
function validate(callback) {
// :TODO: Prompt for if not supplied
let otpType = argv._[argv._.length - 1];
otpType = _.find(OTPTypes, t => {
return t.toLowerCase() === otpType;
});
if(!otpType) {
return callback(Errors.Invalid('Invalid OTP type'));
}
return callback(null, otpType);
},
function prepare(otpType, callback) {
const otpOpts = {
username : user.username,
qrType : argv['qr-type'] || 'ascii',
};
prepareOTP(otpType, otpOpts, (err, otpInfo) => {
return callback(err, otpInfo);
});
},
function storeOrDisplayQR(otpInfo, callback) {
if(!argv.out) {
return callback(null, otpInfo);
}
if('-' === argv.out) {
console.info(otpInfo.qr);
return callback(null, otpInfo);
}
fs.writeFile(argv.out, otpInfo.qr, 'utf8', err => {
return callback(err, otpInfo);
});
}
],
(err) => {
if(err) {
console.error(err.message);
} else {
}
}
);
}
function handleUserCommand() { function handleUserCommand() {
function errUsage() { function errUsage() {
return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR); return printUsageAndSetExitCode(getHelpFor('User'), ExitCodes.ERROR);
@ -356,7 +413,8 @@ function handleUserCommand() {
const usernameIdx = [ const usernameIdx = [
'pw', 'pass', 'passwd', 'password', 'pw', 'pass', 'passwd', 'password',
'group', 'group',
'mv', 'rename' 'mv', 'rename',
'2fa',
].includes(action) ? argv._.length - 2 : argv._.length - 1; ].includes(action) ? argv._.length - 2 : argv._.length - 1;
const userName = argv._[usernameIdx]; const userName = argv._[usernameIdx];
@ -391,6 +449,8 @@ function handleUserCommand() {
group : modUserGroups, group : modUserGroups,
info : showUserInfo, info : showUserInfo,
'2fa' : twoFactorAuth,
}[action] || errUsage)(user, action); }[action] || errUsage)(user, action);
}); });
} }

View File

@ -12,14 +12,16 @@ const {
recordLogin, recordLogin,
transformLoginError, transformLoginError,
} = require('./user_login.js'); } = require('./user_login.js');
const Config = require('./config.js').get;
// deps // deps
const _ = require('lodash'); const _ = require('lodash');
const crypto = require('crypto'); const crypto = require('crypto');
const async = require('async'); const async = require('async');
const qrGen = require('qrcode-generator');
exports.loginFactor2_OTP = loginFactor2_OTP; exports.prepareOTP = prepareOTP;
exports.generateNewBackupCodes = generateNewBackupCodes; exports.loginFactor2_OTP = loginFactor2_OTP;
const OTPTypes = exports.OTPTypes = { const OTPTypes = exports.OTPTypes = {
RFC6238_TOTP : 'rfc6238_TOTP', // Time-Based, SHA-512 RFC6238_TOTP : 'rfc6238_TOTP', // Time-Based, SHA-512
@ -28,23 +30,27 @@ const OTPTypes = exports.OTPTypes = {
}; };
function otpFromType(otpType) { function otpFromType(otpType) {
return { try {
[ OTPTypes.RFC6238_TOTP ] : () => { return {
const totp = require('otplib/totp'); [ OTPTypes.RFC6238_TOTP ] : () => {
totp.options = { crypto, algorithm : 'sha256' }; const totp = require('otplib/totp');
return totp; totp.options = { crypto, algorithm : 'sha256' };
}, return totp;
[ OTPTypes.RFC4266_HOTP ] : () => { },
const hotp = require('otplib/hotp'); [ OTPTypes.RFC4266_HOTP ] : () => {
hotp.options = { crypto, algorithm : 'sha256' }; const hotp = require('otplib/hotp');
return hotp; hotp.options = { crypto, algorithm : 'sha256' };
}, return hotp;
[ OTPTypes.GoogleAuthenticator ] : () => { },
const googleAuth = require('otplib/authenticator'); [ OTPTypes.GoogleAuthenticator ] : () => {
googleAuth.options = { crypto }; const googleAuth = require('otplib/authenticator');
return googleAuth; googleAuth.options = { crypto };
}, return googleAuth;
}[otpType](); },
}[otpType]();
} catch(e) {
// nothing
}
} }
function generateOTPBackupCode() { function generateOTPBackupCode() {
@ -79,13 +85,9 @@ function backupCodePBKDF2(secret, salt, cb) {
return crypto.pbkdf2(secret, salt, 1000, 128, 'sha1', cb); return crypto.pbkdf2(secret, salt, 1000, 128, 'sha1', cb);
} }
function generateNewBackupCodes(user, cb) { function generateNewBackupCodes(cb) {
// const plainTextCodes = [...Array(6)].map(() => generateOTPBackupCode());
// Backup codes are not stored in plain text, but rather async.map(plainTextCodes, (code, nextCode) => {
// an array of objects: [{salt, code}, ...]
//
const plainCodes = [...Array(6)].map(() => generateOTPBackupCode());
async.map(plainCodes, (code, nextCode) => {
crypto.randomBytes(16, (err, salt) => { crypto.randomBytes(16, (err, salt) => {
if(err) { if(err) {
return nextCode(err); return nextCode(err);
@ -101,14 +103,7 @@ function generateNewBackupCodes(user, cb) {
}); });
}, },
(err, codes) => { (err, codes) => {
if(err) { return cb(err, codes, plainTextCodes);
return cb(err);
}
codes = JSON.stringify(codes);
user.persistProperty(UserProps.AuthFactor2OTPBackupCodes, codes, err => {
return cb(err, plainCodes);
});
}); });
} }
@ -149,13 +144,51 @@ function validateAndConsumeBackupCode(user, token, cb) {
} }
} }
function createQRCode(otp, options, secret) {
const uri = otp.keyuri(options.username || 'user', Config().general.boardName, secret);
const qrCode = qrGen(0, 'L');
qrCode.addData(uri);
qrCode.make();
options.qrType = options.qrType || 'ascii';
return {
ascii : qrCode.createASCII,
data : qrCode.createDataURL,
img : qrCode.createImgTag,
svg : qrCode.createSvgTag,
}[options.qrType]();
}
function prepareOTP(otpType, options, cb) {
if(!_.isFunction(cb)) {
cb = options;
options = {};
}
const otp = otpFromType(otpType);
if(!otp) {
return cb(Errors.Invalid(`Unknown OTP type: ${otpType}`));
}
const secret = OTPTypes.GoogleAuthenticator === otpType ?
otp.generateSecret() :
crypto.randomBytes(64).toString('base64').substr(0, 32);
generateNewBackupCodes((err, codes, plainTextCodes) => {
const qr = createQRCode(otp, options, secret);
return cb(err, { secret, codes, plainTextCodes, qr } );
});
}
function loginFactor2_OTP(client, token, cb) { function loginFactor2_OTP(client, token, cb) {
if(client.user.authFactor < User.AuthFactors.Factor1) { if(client.user.authFactor < User.AuthFactors.Factor1) {
return cb(Errors.AccessDenied('OTP requires prior authentication factor 1')); return cb(Errors.AccessDenied('OTP requires prior authentication factor 1'));
} }
const otpType = client.user.getProperty(UserProps.AuthFactor2OTP); const otpType = client.user.getProperty(UserProps.AuthFactor2OTP);
if(!_.values(OTPTypes).includes(otpType)) { const otp = otpFromType(otpType);
if(!otp) {
return cb(Errors.Invalid(`Unknown OTP type: ${otpType}`)); return cb(Errors.Invalid(`Unknown OTP type: ${otpType}`));
} }
@ -164,7 +197,6 @@ function loginFactor2_OTP(client, token, cb) {
return cb(Errors.Invalid('Missing OTP secret')); return cb(Errors.Invalid('Missing OTP secret'));
} }
const otp = otpFromType(otpType);
const valid = otp.verify( { token, secret } ); const valid = otp.verify( { token, secret } );
const allowLogin = () => { const allowLogin = () => {

View File

@ -20,6 +20,7 @@ const User = require('./user.js');
// deps // deps
const async = require('async'); const async = require('async');
const _ = require('lodash'); const _ = require('lodash');
const assert = require('assert');
exports.userLogin = userLogin; exports.userLogin = userLogin;
exports.recordLogin = recordLogin; exports.recordLogin = recordLogin;
@ -110,6 +111,8 @@ function userLogin(client, username, password, options, cb) {
} }
function recordLogin(client, cb) { function recordLogin(client, cb) {
assert(client.user.authenticated); // don't get in situations where this isn't true
const user = client.user; const user = client.user;
async.parallel( async.parallel(
[ [

View File

@ -55,7 +55,8 @@
"ws": "^6.1.3", "ws": "^6.1.3",
"xxhash": "^0.2.4", "xxhash": "^0.2.4",
"yazl": "^2.5.1", "yazl": "^2.5.1",
"otplib": "^10.0.1" "otplib": "^10.0.1",
"qrcode-generator": "1.4.3"
}, },
"devDependencies": {}, "devDependencies": {},
"engines": { "engines": {

View File

@ -1570,6 +1570,11 @@ punycode@^1.4.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
qrcode-generator@1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/qrcode-generator/-/qrcode-generator-1.4.3.tgz#4876e8f280e65b6c94615f4c19c484f6b964b199"
integrity sha512-++rVRvMRq5BlHfmAafl8a4ppUntzUxCCUTT2t0siUgqKwdnqRzY8IH6f6WSX5dZUhD2Ul5/MIKuTJddflwrGzw==
qs@~6.5.2: qs@~6.5.2:
version "6.5.2" version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"