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:
parent
6070bc94e7
commit
2767f3c4e3
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue