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',
|
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}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue