Added support for both U2F and OTP hardware login keys.

This commit is contained in:
Ylian Saint-Hilaire 2019-02-08 09:24:00 -08:00
parent 70bc543699
commit 8c068505cf
15 changed files with 171 additions and 155 deletions

View File

@ -1503,9 +1503,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
// Send back the list of keys we have, just send the list of names and index
var hkeys = [];
if (user.otphkeys != null) { for (var i = 0; i < user.otphkeys.length; i++) { hkeys.push({ i: user.otphkeys[i].keyIndex, name: user.otphkeys[i].name }); } }
//hkeys = [{ i: 1234, name: 'My Normal Key' }, { i: 5678, name: 'Backup Key' }, { i: 90122, name: 'Blue Extra Key' }];
if (user.otphkeys != null) { for (var i = 0; i < user.otphkeys.length; i++) { hkeys.push({ i: user.otphkeys[i].keyIndex, name: user.otphkeys[i].name, type: user.otphkeys[i].type }); } }
ws.send(JSON.stringify({ action: 'otp-hkey-get', keys: hkeys }));
break;
@ -1539,22 +1537,31 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
// Check if Yubikey support is present
if ((typeof domain.yubikey != 'object') || (typeof domain.yubikey.id != 'string') || (typeof domain.yubikey.secret != 'string')) break;
/*
var yub = require('yubikey-client');
yub.init(domain.yubikey.id, domain.yubikey.secret);
yub.verify(command.otp, function (err, data) {
console.log(err, data);
});
*/
// Query the YubiKey server to validate the OTP
var yubikeyotp = require('yubikeyotp');
//var request = { otp: command.otp, id: domain.yubikey.id, key: domain.yubikey.secret, sl: '100', timestamp: true }
var request = { otp: command.otp, id: domain.yubikey.id, key: domain.yubikey.secret, timestamp: true }
if (domain.yubikey.proxy) { request.requestParams = { proxy: domain.yubikey.proxy }; }
console.log('YubiKey Request: ' + JSON.stringify(request));
yubikeyotp.verifyOTP(request, function (err, results) {
console.log(err, results);
if (results.status == 'OK') {
var keyIndex = obj.parent.crypto.randomBytes(4).readUInt32BE(0);
var keyId = command.otp.substring(0, 12);
if (user.otphkeys == null) { user.otphkeys = []; }
// Check if this key was already registered, if so, remove it.
var foundAtIndex = -1;
for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].keyid == keyId) { foundAtIndex = i; } }
if (foundAtIndex != -1) { user.otphkeys.splice(foundAtIndex, 1); }
// Add the new key and notify
user.otphkeys.push({ name: command.name, type: 2, keyid: keyId, keyIndex: keyIndex });
obj.parent.db.SetUser(user);
ws.send(JSON.stringify({ action: 'otp-hkey-yubikey-add', result: true, name: command.name, index: keyIndex }));
// Notify change TODO: Should be done on all sessions/servers for this user.
try { ws.send(JSON.stringify({ action: 'userinfo', userinfo: obj.parent.CloneSafeUser(user) })); } catch (ex) { }
} else {
ws.send(JSON.stringify({ action: 'otp-hkey-yubikey-add', result: false, name: command.name }));
}
});
break;
@ -1587,7 +1594,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
ws.send(JSON.stringify({ action: 'otp-hkey-setup-response', result: result.successful, name: command.name, index: keyIndex }));
if (result.successful) {
if (user.otphkeys == null) { user.otphkeys = []; }
user.otphkeys.push({ name: command.name, publicKey: result.publicKey, keyHandle: result.keyHandle, keyIndex: keyIndex });
user.otphkeys.push({ name: command.name, type: 1, publicKey: result.publicKey, keyHandle: result.keyHandle, keyIndex: keyIndex });
obj.parent.db.SetUser(user);
//console.log('KEYS', JSON.stringify(user.otphkeys));

View File

@ -1,6 +1,6 @@
{
"name": "meshcentral",
"version": "0.2.7-n",
"version": "0.2.7-o",
"keywords": [
"Remote Management",
"Intel AMT",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 362 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 578 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 988 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

File diff suppressed because one or more lines are too long

View File

@ -1447,38 +1447,44 @@
var end = "</table></div></div>";
var x = "<a href='https://www.yubico.com/' rel='noreferrer noopener' target='_blank'>Hardware keys</a> are used as secondary login authentication.";
x += "";
var keyType1 = 0;
if (message.keys && message.keys.length > 0) {
for (var i in message.keys) {
var key = message.keys[i];
x += start + '<tr style=margin:5px><td style=width:30px><img src="images/hardware-key-24.png"><td style=width:250px>' + key.name + "<td><input type=button value='Remove' onclick=account_removehkey(" + key.i + ")></input>" + end;
var type = 'OTP';
if (key.type == 1) { keyType1++; type = 'U2F'; }
x += start + '<tr style=margin:5px><td style=width:30px><img width=24 height=18 src="images/hardware-key-' + type + '-24.png" style=margin-top:4px><td style=width:250px>' + key.name + "<td><input type=button value='Remove' onclick=account_removehkey(" + key.i + ")></input>" + end;
}
} else {
x += start + '<tr style=text-align:center><td>No Hardware Keys Configured' + end;
}
x += "<br />";
x += "<div><input type=button value='Close' onclick=setDialogMode(0) style=float:right></input>";
//x += "<input type=button value='Add YubiKey' onclick='account_addYubiKey();'></input>";
if (u2fSupported()) {
x += "<input id=d2addkey type=button value='Add Key' onclick='account_addhkey();'></input>";
} else {
x += "No hardware key support on this browser.";
}
x += "<input id=d2addkey1 type=button value='Add U2F Key' onclick='account_addhkey(1);'></input>";
if ((features & 0x4000) != 0) { x += "<input id=d2addkey2 type=button value='Add OTP Key' onclick='account_addhkey(2);'></input>"; }
x += "</div><br />";
setDialogMode(2, "Manage Hardware Login Keys", 8, null, x, 'otpauth-hardware-manage');
if (u2fSupported() && (message.keys.length > 0)) { QE('d2addkey', false); }
if ((u2fSupported() == false) || (keyType1 > 0)) { QE('d2addkey1', false); }
break;
}
case 'otp-hkey-yubikey-add': {
if (message.result) {
meshserver.send({ action: 'otp-hkey-get' }); // Success, ask for the full list of keys.
} else {
setDialogMode(2, "Add Hardware Login Key", 1, null, '<br />Error, Unable to add key.<br /><br />');
}
break;
}
case 'otp-hkey-setup-request': {
if (xxdialogMode && (xxdialogTag != 'otpauth-hardware-manage')) return;
var x = "Press the key button now.<br /><br /><div style=width:100%;text-align:center><img src='images/hardware-keypress-120.png' /></div><input id=dp1keyname style=display:none value=" + message.name + " />";
var x = "Press the key button now.<br /><br /><div style=width:100%;text-align:center><img width=120 height=117 src='images/hardware-keypress-120.png' /></div><input id=dp1keyname style=display:none value=" + message.name + " />";
setDialogMode(2, "Add Hardware Login Key", 2, null, x);
window.u2f.register(message.request.appId, [message.request], [], function (registrationResponse) {
if (registrationResponse.registrationData) {
meshserver.send({ action: 'otp-hkey-setup-response', request: message.request, response: registrationResponse, name: Q('dp1keyname').value });
setDialogMode(2, "Add Hardware Login Key", 0, null, '<br />Checking...<br /><br /><br />', 'otpauth-hardware-manage');
} else {
setDialogMode(0);
setDialogMode(2, "Add Hardware Login Key", 1, null, '<br />Error code ' + registrationResponse.errorCode + '<br /><br />');
}
});
break;
@ -5310,38 +5316,32 @@
meshserver.send({ action: 'otp-hkey-get' });
}
function account_addhkey() {
function account_addhkey(type) {
if (type == 1) {
var x = "Type in the name of the key to add.<br /><br />";
x += addHtmlValue('Key Name', '<input id=dp1keyname style=width:230px maxlength=20 autocomplete=off placeholder="MyKey" onkeyup=account_addhkeyValidate(event) />');
setDialogMode(2, "Add Hardware Login Key", 3, account_addhkeyEx, x);
x += addHtmlValue('Key Name', '<input id=dp1keyname style=width:230px maxlength=20 autocomplete=off placeholder="MyKey" onkeyup=account_addhkeyValidate(event,2) />');
} else if (type == 2) {
var x = "Type in a key name, select the OTP box and press the USB key button<br /><br />";
x += addHtmlValue('Key Name', '<input id=dp1keyname style=width:230px maxlength=20 autocomplete=off placeholder="MyKey" onkeyup=account_addhkeyValidate(event,1) />');
x += addHtmlValue('OTP from key', '<input id=dp1key style=width:230px autocomplete=off onkeyup=account_addhkeyValidate(event,2) />');
}
setDialogMode(2, "Add Hardware Login Key", 3, account_addhkeyEx, x, type);
Q('dp1keyname').focus();
}
function account_addhkeyValidate(e) {
if ((e != null) && (e.keyCode == 13)) { dialogclose(1); }
function account_addhkeyValidate(e,action) {
if ((e != null) && (e.keyCode == 13)) { if (action == 2) { dialogclose(1); } else { Q('dp1key').focus(); } }
}
function account_addhkeyEx() {
function account_addhkeyEx(button, type) {
var name = Q('dp1keyname').value;
if (name == '') { name = 'MyKey'; }
if (type == 1) {
meshserver.send({ action: 'otp-hkey-setup-request', name: name });
} else if (type == 2) {
meshserver.send({ action: 'otp-hkey-yubikey-add', name: name, otp: Q('dp1key').value });
setDialogMode(2, "Add Hardware Login Key", 0, null, "<br />Checking...<br /><br /><br />", 'otpauth-hardware-manage');
}
function account_addYubiKey() {
if (xxdialogMode && (xxdialogTag != 'otpauth-hardware-manage')) return;
var x = "Type in a name for the key and press button on the key to register the new hardware key.<br /><br />";
x += addHtmlValue('Key Name', '<input id=dp1keyname style=width:230px maxlength=20 autocomplete=off onchange=account_addYubiKeyValidate() onkeyup=account_addYubiKeyValidate() />');
x += addHtmlValue('Key Token', '<input id=dp1keytoken style=width:230px maxlength=2048 autocomplete=off onchange=account_addYubiKeyValidate() onkeyup=account_addYubiKeyValidate() />');
setDialogMode(2, "Add Yubikey", 3, account_addYubiKeyEx, x);
account_addYubiKeyValidate();
}
function account_addYubiKeyValidate() {
QE('idx_dlgOkButton', (Q('dp1keyname').value.length > 0) && (Q('dp1keytoken').value.length > 0));
}
function account_addYubiKeyEx() {
meshserver.send({ action: 'otp-hkey-yubikey-add', name: Q('dp1keyname').value, otp: Q('dp1keytoken').value });
}
function account_removehkey(index) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -150,7 +150,7 @@
<tr>
<td align=right width=100>Login token:</td>
<td>
<input id=tokenInput type=text name=token maxlength=12 onkeypress="return (event.keyCode == 8) || (event.keyCode == 13) || (event.charCode >= 48 && event.charCode <= 57)" onkeyup=checkToken(event) onkeydown=checkToken(event) />
<input id=tokenInput type=text name=token maxlength=50 onkeyup=checkToken(event) onkeydown=checkToken(event) />
<input id=hwtokenInput1 type=text name=hwtoken1 style="display:none" />
<input id=hwtokenInput2 type=text name=hwtoken2 style="display:none" />
</td>
@ -351,9 +351,9 @@
function checkToken() {
var t1 = Q('tokenInput').value;
var t2 = t1.replace(/\D/g, '');
var t2 = t1.split(' ').join('');
if (t1 != t2) { Q('tokenInput').value = t2; }
QE('tokenOkButton', (Q('tokenInput').value.length == 6) || (Q('tokenInput').value.length == 8));
QE('tokenOkButton', (Q('tokenInput').value.length == 6) || (Q('tokenInput').value.length == 8) || (Q('tokenInput').value.length == 44));
}
//

View File

@ -223,7 +223,7 @@
<tr>
<td align=right width=100>Login token:</td>
<td>
<input id=tokenInput type=text name=token maxlength=12 onkeypress="return (event.keyCode == 8) || (event.keyCode == 13) || (event.charCode >= 48 && event.charCode <= 57)" onkeyup=checkToken(event) onkeydown=checkToken(event) />
<input id=tokenInput type=text name=token maxlength=50 onkeyup=checkToken(event) onkeydown=checkToken(event) />
<input id=hwtokenInput1 type=text name=hwtoken1 style="display:none" />
<input id=hwtokenInput2 type=text name=hwtoken2 style="display:none" />
</td>
@ -446,9 +446,9 @@
function checkToken() {
var t1 = Q('tokenInput').value;
var t2 = t1.replace(/\D/g, '');
var t2 = t1.split(' ').join('');
if (t1 != t2) { Q('tokenInput').value = t2; }
QE('tokenOkButton', (Q('tokenInput').value.length == 6) || (Q('tokenInput').value.length == 8));
QE('tokenOkButton', (Q('tokenInput').value.length == 6) || (Q('tokenInput').value.length == 8) || (Q('tokenInput').value.length == 44));
}
//

View File

@ -342,44 +342,77 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
}
// Check the 2-step auth token
function checkUserOneTimePassword(domain, user, token, hwtoken1, hwtoken2) {
function checkUserOneTimePassword(domain, user, token, hwtoken1, hwtoken2, func) {
const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.certificates.CommonName != 'un-configured') && (obj.args.lanonly !== true) && (obj.args.nousers !== true));
if (twoStepLoginSupported == false) return true;
if (twoStepLoginSupported == false) { func(true); return; };
// Check hardware key
// Check U2F hardware key
if (user.otphkeys && (user.otphkeys.length > 0) && (typeof (hwtoken1) == 'string') && (typeof (hwtoken2) == 'string')) {
var u2fpublicKey = null;
// Find a U2F key
for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 1) { u2fpublicKey = user.otphkeys[i].publicKey; } }
if (u2fpublicKey != null) {
// Check hardware token
var authRequest = null, authResponse = null;
try { authRequest = JSON.parse(hwtoken1); } catch (ex) { }
try { authResponse = JSON.parse(hwtoken2); } catch (ex) { }
if ((authRequest != null) && (authResponse != null)) {
const u2f = require('u2f');
const result = u2f.checkSignature(authRequest[0], authResponse, user.otphkeys[0].publicKey);
if (result.successful === true) return true;
const result = u2f.checkSignature(authRequest[0], authResponse, u2fpublicKey);
if (result.successful === true) { func(true); return; };
}
}
}
// Check Google Authenticator
const otplib = require('otplib')
if (user.otpsecret && (typeof (token) == 'string') && (otplib.authenticator.check(token, user.otpsecret) == true)) return true;
if (user.otpsecret && (typeof (token) == 'string') && (token.length == 6) && (otplib.authenticator.check(token, user.otpsecret) == true)) { func(true); return; };
// Check written down keys
if ((user.otpkeys != null) && (user.otpkeys.keys != null)) {
if ((user.otpkeys != null) && (user.otpkeys.keys != null) && (typeof (token) == 'string') && (token.length == 8)) {
var tokenNumber = parseInt(token);
for (var i = 0; i < user.otpkeys.keys.length; i++) { if ((tokenNumber === user.otpkeys.keys[i].p) && (user.otpkeys.keys[i].u === true)) { user.otpkeys.keys[i].u = false; return true; } }
for (var i = 0; i < user.otpkeys.keys.length; i++) { if ((tokenNumber === user.otpkeys.keys[i].p) && (user.otpkeys.keys[i].u === true)) { user.otpkeys.keys[i].u = false; func(true); return; } }
}
return false;
// Check OTP hardware key
if (domain.yubikey.id && domain.yubikey.secret && user.otphkeys && (user.otphkeys.length > 0) && (typeof (token) == 'string') && (token.length == 44)) {
var keyId = token.substring(0, 12);
// Find a matching OPT key
var match = false;
for (var i = 0; i < user.otphkeys.length; i++) { if ((user.otphkeys[i].type === 2) && (user.otphkeys[i].keyid === keyId)) { match = true; } }
// If we have a match, check the OTP
if (match === true) {
var yubikeyotp = require('yubikeyotp');
var request = { otp: token, id: domain.yubikey.id, key: domain.yubikey.secret, timestamp: true }
if (domain.yubikey.proxy) { request.requestParams = { proxy: domain.yubikey.proxy }; }
yubikeyotp.verifyOTP(request, function (err, results) { func(results.status == 'OK'); });
return;
}
}
// Return a hardware key challenge
func(false);
}
// Return a U2F hardware key challenge
// TODO: Figure out how to support many U2F keys at the same time.
function getHardwareKeyChallenge(domain, user) {
if (user.otphkeys && (user.otphkeys.length > 0)) {
// Find a U2F key
var u2fKeyHandle = null;
for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 1) { u2fKeyHandle = user.otphkeys[i].keyHandle; } }
// Generate a U2F challenge
if (u2fKeyHandle != null) {
var requests = [];
const u2f = require('u2f');
for (var i in user.otphkeys) { requests.push(u2f.request('https://' + obj.parent.certificates.CommonName, user.otphkeys[i].keyHandle)); }
for (var i in user.otphkeys) { requests.push(u2f.request('https://' + obj.parent.certificates.CommonName, u2fKeyHandle)); }
return JSON.stringify(requests);
}
}
return '';
}
@ -398,51 +431,38 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
// Check if this user has 2-step login active
if (checkUserOneTimePasswordRequired(domain, user)) {
if (checkUserOneTimePassword(domain, user, req.body.token, req.body.hwtoken1, req.body.hwtoken2) == false) {
checkUserOneTimePassword(domain, user, req.body.token, req.body.hwtoken1, req.body.hwtoken2, function (result) {
if (result == false) {
// 2-step auth is required, but the token is not present or not valid.
if (user.otpsecret != null) { req.session.error = '<b style=color:#8C001A>Invalid token, try again.</b>'; }
req.session.loginmode = '4';
req.session.tokenusername = xusername;
req.session.tokenpassword = xpassword;
res.redirect(domain.url);
return;
}
}
/*
// Check if this user has 2-step login active
var tokenValid = 0;
const twoStepLoginSupported = ((domain.auth != 'sspi') && (obj.parent.certificates.CommonName != 'un-configured') && (obj.args.lanonly !== true) && (obj.args.nousers !== true));
const otplib = require('otplib')
otplib.authenticator.options = { window: 6 }; // Set +/- 3 minute window
if (twoStepLoginSupported && user.otpsecret && ((typeof (req.body.token) != 'string') || ((tokenValid = otplib.authenticator.check(req.body.token, user.otpsecret)) !== true))) {
// Failed OTP, check user's one time passwords
console.log(user);
if ((req.body.token != null) && ((user.otpkeys != null) && (user.otpkeys.keys != null)) || (user.otphkeys && user.otphkeys.length > 0)) {
var found = null;
var tokenNumber = parseInt(req.body.token);
for (var i = 0; i < user.otpkeys.keys.length; i++) { if ((tokenNumber === user.otpkeys.keys[i].p) && (user.otpkeys.keys[i].u === true)) { user.otpkeys.keys[i].u = false; found = i; } }
if (found == null) {
// 2-step auth is required, but the token is not present or not valid.
if (user.otpsecret != null) { req.session.error = '<b style=color:#8C001A>Invalid token, try again.</b>'; }
req.session.loginmode = '4';
req.session.tokenusername = xusername;
req.session.tokenpassword = xpassword;
res.redirect(domain.url);
return;
}
} else {
// 2-step auth is required, but the token is not present or not valid.
if (user.otpsecret != null) { req.session.error = '<b style=color:#8C001A>Invalid token, try again.</b>'; }
req.session.loginmode = '4';
req.session.tokenusername = xusername;
req.session.tokenpassword = xpassword;
res.redirect(domain.url);
// Login succesful
completeLoginRequest(req, res, domain, user, userid);
}
});
return;
}
}
*/
// Login succesful
completeLoginRequest(req, res, domain, user, userid);
} else {
delete req.session.loginmode;
if (err == 'locked') { req.session.error = '<b style=color:#8C001A>Account locked.</b>'; } else { req.session.error = '<b style=color:#8C001A>Login failed, check username and password.</b>'; }
if ((passhint != null) && (passhint.length > 0)) {
req.session.passhint = passhint;
} else {
if (req.session.passhint) { delete req.session.passhint; }
}
res.redirect(domain.url);
}
});
}
function completeLoginRequest(req, res, domain, user, userid) {
// Save login time
user.login = Math.floor(Date.now() / 1000);
obj.db.SetUser(user);
@ -481,17 +501,6 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
//});
obj.parent.DispatchEvent(['*'], obj, { etype: 'user', username: user.name, action: 'login', msg: 'Account login', domain: domain.id });
} else {
delete req.session.loginmode;
if (err == 'locked') { req.session.error = '<b style=color:#8C001A>Account locked.</b>'; } else { req.session.error = '<b style=color:#8C001A>Login failed, check username and password.</b>'; }
if ((passhint != null) && (passhint.length > 0)) {
req.session.passhint = passhint;
} else {
if (req.session.passhint) { delete req.session.passhint; }
}
res.redirect(domain.url);
}
});
}
function handleCreateAccountRequest(req, res) {