diff --git a/meshuser.js b/meshuser.js index a322d89e..318e9b16 100644 --- a/meshuser.js +++ b/meshuser.js @@ -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)); diff --git a/package.json b/package.json index fe6a4ddd..2b957f8d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "meshcentral", - "version": "0.2.7-n", + "version": "0.2.7-o", "keywords": [ "Remote Management", "Intel AMT", diff --git a/public/images/hardware-key-24.png b/public/images/hardware-key-24.png deleted file mode 100644 index f2ecfb7f..00000000 Binary files a/public/images/hardware-key-24.png and /dev/null differ diff --git a/public/images/hardware-key-32.png b/public/images/hardware-key-32.png deleted file mode 100644 index 1ad6f6a3..00000000 Binary files a/public/images/hardware-key-32.png and /dev/null differ diff --git a/public/images/hardware-key-OTP-24.png b/public/images/hardware-key-OTP-24.png new file mode 100644 index 00000000..2cdedb90 Binary files /dev/null and b/public/images/hardware-key-OTP-24.png differ diff --git a/public/images/hardware-key-U2F-24.png b/public/images/hardware-key-U2F-24.png new file mode 100644 index 00000000..7d11dd81 Binary files /dev/null and b/public/images/hardware-key-U2F-24.png differ diff --git a/public/images/hardware-keypress-120 - Copy.png b/public/images/hardware-keypress-120 - Copy.png new file mode 100644 index 00000000..c1d804b1 Binary files /dev/null and b/public/images/hardware-keypress-120 - Copy.png differ diff --git a/public/images/hardware-keypress-120.png b/public/images/hardware-keypress-120.png index c1d804b1..bec0f394 100644 Binary files a/public/images/hardware-keypress-120.png and b/public/images/hardware-keypress-120.png differ diff --git a/views/default-min.handlebars b/views/default-min.handlebars index 31a6e765..d688c160 100644 --- a/views/default-min.handlebars +++ b/views/default-min.handlebars @@ -1 +1 @@ - MeshCentral
{{{title}}}
{{{title2}}}

{{{logoutControl}}}

 

\ No newline at end of file + MeshCentral
{{{title}}}
{{{title2}}}

{{{logoutControl}}}

 

\ No newline at end of file diff --git a/views/default.handlebars b/views/default.handlebars index 05bc7abf..76a44d3a 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -1447,38 +1447,44 @@ var end = ""; var x = "Hardware keys 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 + '' + key.name + "" + end; + var type = 'OTP'; + if (key.type == 1) { keyType1++; type = 'U2F'; } + x += start + '' + key.name + "" + end; } } else { x += start + 'No Hardware Keys Configured' + end; } x += "
"; x += "
"; - //x += ""; - - if (u2fSupported()) { - x += ""; - } else { - x += "No hardware key support on this browser."; - } + x += ""; + if ((features & 0x4000) != 0) { x += ""; } x += "

"; 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, '
Error, Unable to add key.

'); + } break; } case 'otp-hkey-setup-request': { if (xxdialogMode && (xxdialogTag != 'otpauth-hardware-manage')) return; - var x = "Press the key button now.

"; + var x = "Press the key button now.

"; 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, '
Checking...


', 'otpauth-hardware-manage'); } else { - setDialogMode(0); + setDialogMode(2, "Add Hardware Login Key", 1, null, '
Error code ' + registrationResponse.errorCode + '

'); } }); break; @@ -5310,38 +5316,32 @@ meshserver.send({ action: 'otp-hkey-get' }); } - function account_addhkey() { - var x = "Type in the name of the key to add.

"; - x += addHtmlValue('Key Name', ''); - setDialogMode(2, "Add Hardware Login Key", 3, account_addhkeyEx, x); + function account_addhkey(type) { + if (type == 1) { + var x = "Type in the name of the key to add.

"; + x += addHtmlValue('Key Name', ''); + } else if (type == 2) { + var x = "Type in a key name, select the OTP box and press the USB key button

"; + x += addHtmlValue('Key Name', ''); + x += addHtmlValue('OTP from key', ''); + } + 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'; } - meshserver.send({ action: 'otp-hkey-setup-request', name: name }); - } - - 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.

"; - x += addHtmlValue('Key Name', ''); - x += addHtmlValue('Key Token', ''); - 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 }); + 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, "
Checking...


", 'otpauth-hardware-manage'); + } } function account_removehkey(index) { diff --git a/views/login-min.handlebars b/views/login-min.handlebars index 4a09c04c..2e45fd30 100644 --- a/views/login-min.handlebars +++ b/views/login-min.handlebars @@ -1 +1 @@ - MeshCentral - Login
{{{title}}}
{{{title2}}}

Welcome

Connect to your home or office devices from anywhere in the world using MeshCentral, the real time, open source remote monitoring and management web site. You will need to download and install a management agent on your computers. Once installed, computers will show up in the "My Devices" section of this web site and you will be able to monitor them and take control of them.


\ No newline at end of file + MeshCentral - Login
{{{title}}}
{{{title2}}}

Welcome

Connect to your home or office devices from anywhere in the world using MeshCentral, the real time, open source remote monitoring and management web site. You will need to download and install a management agent on your computers. Once installed, computers will show up in the "My Devices" section of this web site and you will be able to monitor them and take control of them.


\ No newline at end of file diff --git a/views/login-mobile-min.handlebars b/views/login-mobile-min.handlebars index 16a50e47..baeadf9c 100644 --- a/views/login-mobile-min.handlebars +++ b/views/login-mobile-min.handlebars @@ -1 +1 @@ - MeshCentral - Login
{{{title}}}
{{{title2}}}
\ No newline at end of file + MeshCentral - Login
{{{title}}}
{{{title2}}}
\ No newline at end of file diff --git a/views/login-mobile.handlebars b/views/login-mobile.handlebars index 9edf2d0d..c8aef775 100644 --- a/views/login-mobile.handlebars +++ b/views/login-mobile.handlebars @@ -150,7 +150,7 @@ Login token: - + @@ -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)); } // diff --git a/views/login.handlebars b/views/login.handlebars index c833f28f..3457af8a 100644 --- a/views/login.handlebars +++ b/views/login.handlebars @@ -223,7 +223,7 @@ Login token: - + @@ -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)); } // diff --git a/webserver.js b/webserver.js index 7687abe5..f8addf87 100644 --- a/webserver.js +++ b/webserver.js @@ -342,43 +342,76 @@ 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')) { - // 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; + 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, 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; + } + } + + func(false); } - // Return a hardware key challenge + // 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)) { - 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)); } - return JSON.stringify(requests); + // 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, u2fKeyHandle)); } + return JSON.stringify(requests); + } } return ''; } @@ -398,89 +431,24 @@ 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) { - // 2-step auth is required, but the token is not present or not valid. - if (user.otpsecret != null) { req.session.error = 'Invalid token, try again.'; } - 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) { + 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 = 'Invalid token, try again.'; } req.session.loginmode = '4'; req.session.tokenusername = xusername; req.session.tokenpassword = xpassword; res.redirect(domain.url); - return; + } else { + // Login succesful + completeLoginRequest(req, res, domain, user, userid); } - } else { - // 2-step auth is required, but the token is not present or not valid. - if (user.otpsecret != null) { req.session.error = 'Invalid token, try again.'; } - req.session.loginmode = '4'; - req.session.tokenusername = xusername; - req.session.tokenpassword = xpassword; - res.redirect(domain.url); - return; - } - } - */ - - // Save login time - user.login = Math.floor(Date.now() / 1000); - obj.db.SetUser(user); - - // Regenerate session when signing in to prevent fixation - //req.session.regenerate(function () { - // Store the user's primary key in the session store to be retrieved, or in this case the entire user object - // req.session.success = 'Authenticated as ' + user.name + 'click to logout. You may now access /restricted.'; - delete req.session.loginmode; - delete req.session.tokenusername; - delete req.session.tokenpassword; - req.session.userid = userid; - req.session.domainid = domain.id; - req.session.currentNode = ''; - if (req.session.passhint) { delete req.session.passhint; } - if (req.body.viewmode) { req.session.viewmode = req.body.viewmode; } - if (req.body.host) { - // TODO: This is a terrible search!!! FIX THIS. - /* - obj.db.GetAllType('node', function (err, docs) { - for (var i = 0; i < docs.length; i++) { - if (docs[i].name == req.body.host) { - req.session.currentNode = docs[i]._id; - break; - } - } - console.log("CurrentNode: " + req.session.currentNode); - // This redirect happens after finding node is completed - res.redirect(domain.url); }); - */ - res.redirect(domain.url); // Temporary - } else { - res.redirect(domain.url); + return; } - //}); - obj.parent.DispatchEvent(['*'], obj, { etype: 'user', username: user.name, action: 'login', msg: 'Account login', domain: domain.id }); + // Login succesful + completeLoginRequest(req, res, domain, user, userid); } else { delete req.session.loginmode; if (err == 'locked') { req.session.error = 'Account locked.'; } else { req.session.error = 'Login failed, check username and password.'; } @@ -494,6 +462,47 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { }); } + function completeLoginRequest(req, res, domain, user, userid) { + // Save login time + user.login = Math.floor(Date.now() / 1000); + obj.db.SetUser(user); + + // Regenerate session when signing in to prevent fixation + //req.session.regenerate(function () { + // Store the user's primary key in the session store to be retrieved, or in this case the entire user object + // req.session.success = 'Authenticated as ' + user.name + 'click to logout. You may now access /restricted.'; + delete req.session.loginmode; + delete req.session.tokenusername; + delete req.session.tokenpassword; + req.session.userid = userid; + req.session.domainid = domain.id; + req.session.currentNode = ''; + if (req.session.passhint) { delete req.session.passhint; } + if (req.body.viewmode) { req.session.viewmode = req.body.viewmode; } + if (req.body.host) { + // TODO: This is a terrible search!!! FIX THIS. + /* + obj.db.GetAllType('node', function (err, docs) { + for (var i = 0; i < docs.length; i++) { + if (docs[i].name == req.body.host) { + req.session.currentNode = docs[i]._id; + break; + } + } + console.log("CurrentNode: " + req.session.currentNode); + // This redirect happens after finding node is completed + res.redirect(domain.url); + }); + */ + res.redirect(domain.url); // Temporary + } else { + res.redirect(domain.url); + } + //}); + + obj.parent.DispatchEvent(['*'], obj, { etype: 'user', username: user.name, action: 'login', msg: 'Account login', domain: domain.id }); + } + function handleCreateAccountRequest(req, res) { const domain = checkUserIpAddress(req, res); if ((domain == null) || (domain.auth == 'sspi')) return;