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',
};
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}`);
}
}
}
);

View File

@ -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) {

View File

@ -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
};