diff --git a/core/oputil/oputil_user.js b/core/oputil/oputil_user.js index a2725a71..77e0be68 100644 --- a/core/oputil/oputil_user.js +++ b/core/oputil/oputil_user.js @@ -373,7 +373,7 @@ function twoFactorAuth(user) { qrType : argv['qr-type'] || 'ascii', }; prepareOTP(otpType, otpOpts, (err, otpInfo) => { - return callback(err, otpInfo); + return callback(err, Object.assign(otpInfo, { otpType })); }); }, function storeOrDisplayQR(otpInfo, callback) { @@ -381,20 +381,35 @@ function twoFactorAuth(user) { 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); }); + }, + function persist(otpInfo, callback) { + const props = { + [ UserProps.AuthFactor2OTP ] : otpInfo.otpType, + [ UserProps.AuthFactor2OTPSecret ] : otpInfo.secret, + [ UserProps.AuthFactor2OTPBackupCodes ] : JSON.stringify(otpInfo.backupCodes), + }; + user.persistProperties(props, err => { + return callback(err, otpInfo); + }); } ], - (err) => { + (err, otpInfo) => { if(err) { console.error(err.message); } else { + console.info(`OTP enabled for ${user.username}.`); + console.info(`Secret: ${otpInfo.secret}`); + console.info(`Backup codes: ${otpInfo.backupCodes.join(', ')}`); + + if(!argv.out) { + console.info('QR code:'); + console.info(otpInfo.qr); + } else { + console.info(`QR code saved to ${argv.out}`); + } } } ); diff --git a/core/user_2fa_otp.js b/core/user_2fa_otp.js index eadb09eb..83524750 100644 --- a/core/user_2fa_otp.js +++ b/core/user_2fa_otp.js @@ -81,63 +81,25 @@ function generateOTPBackupCode() { return bits.join('-'); } -function backupCodePBKDF2(secret, salt, cb) { - return crypto.pbkdf2(secret, salt, 1000, 128, 'sha1', cb); -} - -function generateNewBackupCodes(cb) { - const plainTextCodes = [...Array(6)].map(() => generateOTPBackupCode()); - async.map(plainTextCodes, (code, nextCode) => { - crypto.randomBytes(16, (err, salt) => { - if(err) { - return nextCode(err); - } - salt = salt.toString('base64'); - backupCodePBKDF2(code, salt, (err, code) => { - if(err) { - return nextCode(err); - } - code = code.toString('base64'); - return nextCode(null, { salt, code }); - }); - }); - }, - (err, codes) => { - return cb(err, codes, plainTextCodes); - }); +function generateNewBackupCodes() { + const codes = [...Array(6)].map(() => generateOTPBackupCode()); + return codes; } function validateAndConsumeBackupCode(user, token, cb) { try { let validCodes = JSON.parse(user.getProperty(UserProps.AuthFactor2OTPBackupCodes)); - async.detect(validCodes, (entry, nextEntry) => { - backupCodePBKDF2(token, entry.salt, (err, code) => { - if(err) { - return nextEntry(err); - } - code = code.toString('base64'); - return nextEntry(null, code === entry.code); - }); - }, - (err, matchingEntry) => { - if(err) { - return cb(err); - } + const matchingCode = validCodes.find(c => c === token); + if(!matchingCode) { + return cb(Errors.BadLogin('Invalid OTP value supplied', ErrorReasons.Invalid2FA)); + } - if(!matchingEntry) { - return cb(Errors.BadLogin('Invalid OTP value supplied', ErrorReasons.Invalid2FA)); - } - - // We're consuming a match - remove it from available backup codes - validCodes = validCodes.filter(entry => { - return entry.code != matchingEntry.code && entry.salt != matchingEntry.salt; - }); - - validCodes = JSON.stringify(validCodes); - user.persistProperty(UserProps.AuthFactor2OTPBackupCodes, validCodes, err => { - return cb(err); - }); + // We're consuming a match - remove it from available backup codes + validCodes = validCodes.filter(c => c !== matchingCode); + validCodes = JSON.stringify(validCodes); + user.persistProperty(UserProps.AuthFactor2OTPBackupCodes, validCodes, err => { + return cb(err); }); } catch(e) { return cb(e); @@ -174,10 +136,10 @@ function prepareOTP(otpType, options, cb) { 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 } ); - }); + const backupCodes = generateNewBackupCodes(); + const qr = createQRCode(otp, options, secret); + + return cb(null, { secret, backupCodes, qr } ); } function loginFactor2_OTP(client, token, cb) { diff --git a/core/user_property.js b/core/user_property.js index 02fac923..00f640fb 100644 --- a/core/user_property.js +++ b/core/user_property.js @@ -62,6 +62,6 @@ module.exports = { AuthFactor1Types : 'auth_factor1_types', // List of User.AuthFactor1Types value(s) AuthFactor2OTP : 'auth_factor2_otp', // If present, OTP type for 2FA AuthFactor2OTPSecret : 'auth_factor2_otp_secret', // Secret used in conjunction with OTP 2FA - AuthFactor2OTPBackupCodes : 'auth_factor2_otp_backup', // JSON array of backup codes: [{salt,code}, ...] + AuthFactor2OTPBackupCodes : 'auth_factor2_otp_backup', // JSON array of backup codes };