Don't store hashed versions of backup codes

* Really no point; secret must be in plain-text and only ever used in conjunction with pass/etc.
* Better oputil handling
This commit is contained in:
Bryan Ashby 2019-05-09 20:25:47 -06:00
parent 6070bc94e7
commit 2767f3c4e3
No known key found for this signature in database
GPG Key ID: B49EB437951D2542
3 changed files with 39 additions and 62 deletions

View File

@ -373,7 +373,7 @@ function twoFactorAuth(user) {
qrType : argv['qr-type'] || 'ascii', qrType : argv['qr-type'] || 'ascii',
}; };
prepareOTP(otpType, otpOpts, (err, otpInfo) => { prepareOTP(otpType, otpOpts, (err, otpInfo) => {
return callback(err, otpInfo); return callback(err, Object.assign(otpInfo, { otpType }));
}); });
}, },
function storeOrDisplayQR(otpInfo, callback) { function storeOrDisplayQR(otpInfo, callback) {
@ -381,20 +381,35 @@ function twoFactorAuth(user) {
return callback(null, otpInfo); return callback(null, otpInfo);
} }
if('-' === argv.out) {
console.info(otpInfo.qr);
return callback(null, otpInfo);
}
fs.writeFile(argv.out, otpInfo.qr, 'utf8', err => { fs.writeFile(argv.out, otpInfo.qr, 'utf8', err => {
return callback(err, otpInfo); 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) { if(err) {
console.error(err.message); console.error(err.message);
} else { } 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}`);
}
} }
} }
); );

View File

@ -81,64 +81,26 @@ function generateOTPBackupCode() {
return bits.join('-'); return bits.join('-');
} }
function backupCodePBKDF2(secret, salt, cb) { function generateNewBackupCodes() {
return crypto.pbkdf2(secret, salt, 1000, 128, 'sha1', cb); const codes = [...Array(6)].map(() => generateOTPBackupCode());
} return codes;
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 validateAndConsumeBackupCode(user, token, cb) { function validateAndConsumeBackupCode(user, token, cb) {
try try
{ {
let validCodes = JSON.parse(user.getProperty(UserProps.AuthFactor2OTPBackupCodes)); let validCodes = JSON.parse(user.getProperty(UserProps.AuthFactor2OTPBackupCodes));
async.detect(validCodes, (entry, nextEntry) => { const matchingCode = validCodes.find(c => c === token);
backupCodePBKDF2(token, entry.salt, (err, code) => { if(!matchingCode) {
if(err) {
return nextEntry(err);
}
code = code.toString('base64');
return nextEntry(null, code === entry.code);
});
},
(err, matchingEntry) => {
if(err) {
return cb(err);
}
if(!matchingEntry) {
return cb(Errors.BadLogin('Invalid OTP value supplied', ErrorReasons.Invalid2FA)); return cb(Errors.BadLogin('Invalid OTP value supplied', ErrorReasons.Invalid2FA));
} }
// We're consuming a match - remove it from available backup codes // We're consuming a match - remove it from available backup codes
validCodes = validCodes.filter(entry => { validCodes = validCodes.filter(c => c !== matchingCode);
return entry.code != matchingEntry.code && entry.salt != matchingEntry.salt;
});
validCodes = JSON.stringify(validCodes); validCodes = JSON.stringify(validCodes);
user.persistProperty(UserProps.AuthFactor2OTPBackupCodes, validCodes, err => { user.persistProperty(UserProps.AuthFactor2OTPBackupCodes, validCodes, err => {
return cb(err); return cb(err);
}); });
});
} catch(e) { } catch(e) {
return cb(e); return cb(e);
} }
@ -174,10 +136,10 @@ function prepareOTP(otpType, options, cb) {
otp.generateSecret() : otp.generateSecret() :
crypto.randomBytes(64).toString('base64').substr(0, 32); crypto.randomBytes(64).toString('base64').substr(0, 32);
generateNewBackupCodes((err, codes, plainTextCodes) => { const backupCodes = generateNewBackupCodes();
const qr = createQRCode(otp, options, secret); const qr = createQRCode(otp, options, secret);
return cb(err, { secret, codes, plainTextCodes, qr } );
}); return cb(null, { secret, backupCodes, qr } );
} }
function loginFactor2_OTP(client, token, cb) { function loginFactor2_OTP(client, token, cb) {

View File

@ -62,6 +62,6 @@ module.exports = {
AuthFactor1Types : 'auth_factor1_types', // List of User.AuthFactor1Types value(s) AuthFactor1Types : 'auth_factor1_types', // List of User.AuthFactor1Types value(s)
AuthFactor2OTP : 'auth_factor2_otp', // If present, OTP type for 2FA AuthFactor2OTP : 'auth_factor2_otp', // If present, OTP type for 2FA
AuthFactor2OTPSecret : 'auth_factor2_otp_secret', // Secret used in conjunction with OTP 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
}; };