Started work on adding FIDO2 support.

Ylian Saint-Hilaire committed Mar 22, 2019 at 22:33 UTC 2b81e1243806004900e68eb32f8d2fe0350ec510
7 files changed +140 -9
meshuser.js
+54
@@ -1978,6 +1978,60 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
1978 });
1979 break;
1980 }
1981 + case 'webauthn-startregister':
1982 + {
1983 + // Check is 2-step login is supported
1984 + const twoStepLoginSupported = ((domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.lanonly !== true) && (args.nousers !== true));
1985 + if ((twoStepLoginSupported == false) || (command.name == null) || (parent.f2l == null)) break;
1986 +
1987 + parent.f2l.attestationOptions().then(function (registrationOptions) {
1988 + // Convert the challenge to base64 and add user information
1989 + registrationOptions.challenge = Buffer(registrationOptions.challenge).toString('base64');
1990 + registrationOptions.user.id = Buffer(parent.crypto.randomBytes(16)).toString('base64');
1991 + registrationOptions.user.name = user._id;
1992 + registrationOptions.user.displayName = user._id.split('/')[2];
1993 +
1994 + // Send the registration request
1995 + obj.webAuthnReqistrationRequest = { action: 'webauthn-startregister', keyname: command.name, request: registrationOptions };
1996 + ws.send(JSON.stringify(obj.webAuthnReqistrationRequest));
1997 + //console.log(obj.webAuthnReqistrationRequest);
1998 + }, function (error) {
1999 + console.log('webauthn-startregister-error', error);
2000 + });
2001 + break;
2002 + }
2003 + case 'webauthn-endregister':
2004 + {
2005 + if ((obj.webAuthnReqistrationRequest == null) || (parent.f2l == null)) return;
2006 +
2007 + var attestationExpectations = {
2008 + challenge: obj.webAuthnReqistrationRequest.request.challenge.split('+').join('-').split('/').join('_').split('=').join(''), // Convert to Base64URL
2009 + origin: "https://devbox.mesh.meshcentral.com",
2010 + factor: "either"
2011 + };
2012 + var clientAttestationResponse = command.response;
2013 + clientAttestationResponse.id = clientAttestationResponse.rawId;
2014 + clientAttestationResponse.rawId = new Uint8Array(Buffer.from(clientAttestationResponse.rawId, 'base64')).buffer;
2015 + clientAttestationResponse.response.attestationObject = new Uint8Array(Buffer.from(clientAttestationResponse.response.attestationObject, 'base64')).buffer;
2016 + clientAttestationResponse.response.clientDataJSON = new Uint8Array(Buffer.from(clientAttestationResponse.response.clientDataJSON, 'base64')).buffer;
2017 +
2018 + parent.f2l.attestationResult(clientAttestationResponse, attestationExpectations).then(function (regResult) {
2019 + var keyIndex = parent.crypto.randomBytes(4).readUInt32BE(0);
2020 + if (user.otphkeys == null) { user.otphkeys = []; }
2021 + user.otphkeys.push({ name: obj.webAuthnReqistrationRequest.keyname, type: 3, publicKey: regResult.authnrData.get('credentialPublicKeyPem'), counter: regResult.authnrData.get('counter'), keyIndex: keyIndex, keyId: clientAttestationResponse.id });
2022 + parent.db.SetUser(user);
2023 + ws.send(JSON.stringify({ action: 'otp-hkey-setup-response', result: true, name: command.name, index: keyIndex }));
2024 +
2025 + // Notify change
2026 + parent.parent.DispatchEvent(['*', 'server-users', user._id], obj, { etype: 'user', username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', msg: 'Added security key.', domain: domain.id });
2027 + }, function (error) {
2028 + console.log('webauthn-endregister-error', error);
2029 + ws.send(JSON.stringify({ action: 'otp-hkey-setup-response', result: false, error: error, name: command.name, index: keyIndex }));
2030 + });
2031 +
2032 + delete obj.hardwareKeyRegistrationRequest;
2033 + break;
2034 + }
2035 case 'getClip': {
2036 if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check nodeid
2037
package.json
+1 -1
@@ -1,6 +1,6 @@
1 {
2 "name": "meshcentral",
3 - "version": "0.3.0-t",
3 + "version": "0.3.0-u",
4 "keywords": [
5 "Remote Management",
6 "Intel AMT",
public/images/hardware-key-WebAuthn-24.png
Binary files /dev/null and b/public/images/hardware-key-WebAuthn-24.png differ
readme.txt
+3 -5
@@ -3,13 +3,11 @@ MeshCentral
3
4 For more information, [visit MeshCommander.com/MeshCentral2](http://www.meshcommander.com/meshcentral2).
5
6 -Download the [full PDF user's guide](http://info.meshcentral.com/downloads/meshcentral2/MeshCentral2UserGuide.pdf) with more information on configuring and running MeshCentral2. In addition, the [installation guide](http://info.meshcentral.com/downloads/meshcentral2/MeshCentral2InstallGuide.pdf) can help get MeshCentral installed on Amazon AWS, Microsoft Azure, Ubuntu and the Raspberry Pi.
6 +Download the [full PDF user's guide](http://info.meshcentral.com/downloads/MeshCentral2/MeshCentral2UserGuide.pdf) with more information on configuring and running MeshCentral2. In addition, the [installation guide](http://info.meshcentral.com/downloads/MeshCentral2/MeshCentral2InstallGuide.pdf) can help get MeshCentral installed on Amazon AWS, Microsoft Azure, Ubuntu and the Raspberry Pi.
7
8 This is a full computer management web site. With MeshCentral, you can run your own web server to remotely manage and control computers on a local network or anywhere on the internet. Once you get the server started, create a mesh (a group of computers) and then download and install a mesh agent on each computer you want to manage. A minute later, the new computer will show up on the web site and you can take control of it. MeshCentral includes full web-based remote desktop, terminal and file management capability.
9
10 -This version of MeshCentral that is completely rebuild of the original MeshCentral coded in C#. It's simpler and includes many other design improvements over the original. At some point in the future, [MeshCentral.com](http://meshcentral.com) that is still running the older code will switch to using this code base.
11 -
12 -This version is BETA and should not be used in production.
10 +To test this server, feel free to try [MeshCentral.com](http://meshcentral.com).
11
12
13 Installation
@@ -37,7 +35,7 @@ To run MeshCentral you may need to use "nodejs" instead of "node" on Linux.
35 node meshcentral [arguments]
36 ```
37
40 -One of the first things you will want to do is set a server name or IP address. This will be used by mesh agents to connect back to the server. So, make sure you set **a name that will resolve back to your server**. MeshCentral will not register this name for you. You must make sure to setup the DNS name yourself first, or use the right IP address. If you are just taking a quick look at MeshCentral, you can skip this step and do it at later time.
38 +You can launch MeshCentral with no arguments to start it in LAN mode. In LAN mode only devices on the local network can be managed. To setup a more seciour server, use --cert to specify an IP address or name that resolves to your server. This name will be used by mesh agents to connect back to the server. So, make sure you set **a name that will resolve back to your server**. MeshCentral will not register this name for you. You must make sure to setup the DNS name yourself first, or use the right IP address. If you are just taking a quick look at MeshCentral, you can skip this step and do it at later time.
39
40 ```
41 node meshcentral --cert servername.domain.com
views/default.handlebars
+25 -2
@@ -1486,7 +1486,7 @@
1486 x += "<div style='max-height:150px;overflow-y:auto;overflow-x:hidden;margin-top:6px;margin-bottom:6px'>";
1487 if (message.keys && message.keys.length > 0) {
1488 for (var i in message.keys) {
1489 - var key = message.keys[i], type = (key.type == 1)?'U2F':'OTP';
1489 + var key = message.keys[i], type = ((key.type == 1)?'U2F':(key.type == 2)?'OTP':'WebAuthn');
1490 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;
1491 }
1492 } else {
@@ -1496,6 +1496,7 @@
1496 x += "<div><input type=button value='Close' onclick=setDialogMode(0) style=float:right></input>";
1497 x += "<input id=d2addkey1 type=button value='Add U2F Key' onclick='account_addhkey(1);'></input>";
1498 if ((features & 0x4000) != 0) { x += "<input id=d2addkey2 type=button value='Add OTP Key' onclick='account_addhkey(2);'></input>"; }
1499 + x += "<input id=d2addkey3 type=button value='Add FIDO2 Key' onclick='account_addhkey(3);'></input>";
1500 x += "</div><br />";
1501 setDialogMode(2, "Manage Security Keys", 8, null, x, 'otpauth-hardware-manage');
1502 if (u2fSupported() == false) { QE('d2addkey1', false); }
@@ -1533,6 +1534,26 @@
1534 }
1535 break;
1536 }
1537 + case 'webauthn-startregister': {
1538 + if (xxdialogMode && (xxdialogTag != 'otpauth-hardware-manage')) return;
1539 + 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 + " />";
1540 + setDialogMode(2, "Add Security Key", 2, null, x);
1541 +
1542 + var publicKey = message.request;
1543 + message.request.challenge = Uint8Array.from(atob(message.request.challenge), c => c.charCodeAt(0))
1544 + message.request.user.id = Uint8Array.from(atob(message.request.user.id), c => c.charCodeAt(0))
1545 + navigator.credentials.create({ publicKey })
1546 + .then((newCredentialInfo) => {
1547 + // Public key credential
1548 + var r = { rawId: btoa(String.fromCharCode.apply(null, new Uint8Array(newCredentialInfo.rawId))), response: { attestationObject: btoa(String.fromCharCode.apply(null, new Uint8Array(newCredentialInfo.response.attestationObject))), clientDataJSON: btoa(String.fromCharCode.apply(null, new Uint8Array(newCredentialInfo.response.clientDataJSON))) }, type: newCredentialInfo.type };
1549 + meshserver.send({ action: 'webauthn-endregister', response: r });
1550 + setDialogMode(0);
1551 + }).catch((error) => {
1552 + // Error
1553 + setDialogMode(2, "Add Security Key", 1, null, "ERROR: " + error);
1554 + });
1555 + break;
1556 + }
1557 case 'event': {
1558 if (!message.event.nolog) {
1559 events.unshift(message.event);
@@ -5611,7 +5632,7 @@
5632 }
5633
5634 function account_addhkey(type) {
5614 - if (type == 1) {
5635 + if (type == 1 || type == 3) {
5636 var x = "Type in the name of the key to add.<br /><br />";
5637 x += addHtmlValue('Key Name', '<input id=dp1keyname style=width:230px maxlength=20 autocomplete=off placeholder="MyKey" onkeyup=account_addhkeyValidate(event,2) />');
5638 } else if (type == 2) {
@@ -5635,6 +5656,8 @@
5656 } else if (type == 2) {
5657 meshserver.send({ action: 'otp-hkey-yubikey-add', name: name, otp: Q('dp1key').value });
5658 setDialogMode(2, "Add Security Key", 0, null, "<br />Checking...<br /><br /><br />", 'otpauth-hardware-manage');
5659 + } else if (type == 3) {
5660 + meshserver.send({ action: 'webauthn-startregister', name: name });
5661 }
5662 }
5663
views/login.handlebars
+31 -1
@@ -372,7 +372,37 @@
372
373 if ('{{loginmode}}' == '4') {
374 try { if (hardwareKeyChallenge.length > 0) { hardwareKeyChallenge = JSON.parse(hardwareKeyChallenge); } else { hardwareKeyChallenge = null; } } catch (ex) { hardwareKeyChallenge = null }
375 - if ((hardwareKeyChallenge != null) && u2fSupported()) {
375 + if ((hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn')) {
376 + hardwareKeyChallenge.challenge = Uint8Array.from(atob(hardwareKeyChallenge.challenge), c => c.charCodeAt(0)).buffer;
377 +
378 + const publicKeyCredentialRequestOptions = { challenge: hardwareKeyChallenge.challenge, allowCredentials: [], timeout: hardwareKeyChallenge.timeout }
379 + for (var i = 0; i < hardwareKeyChallenge.keyIds.length; i++) {
380 + publicKeyCredentialRequestOptions.allowCredentials.push(
381 + { id: Uint8Array.from(atob(hardwareKeyChallenge.keyIds[i]), c => c.charCodeAt(0)), type: 'public-key', transports: ['usb', 'ble', 'nfc'], }
382 + );
383 + }
384 +
385 + // New WebAuthn hardware keys
386 + navigator.credentials.get({ publicKey: publicKeyCredentialRequestOptions }).then(
387 + function (rawAssertion) {
388 + console.log(rawAssertion);
389 + /*
390 + var assertion = {
391 + id: base64encode(rawAssertion.rawId),
392 + clientDataJSON: arrayBufferToString(rawAssertion.response.clientDataJSON),
393 + userHandle: base64encode(rawAssertion.response.userHandle),
394 + signature: base64encode(rawAssertion.response.signature),
395 + authenticatorData: base64encode(rawAssertion.response.authenticatorData)
396 + };
397 + console.log(assertion);
398 + */
399 + },
400 + function (error) {
401 + console.log('credentials-get error', error);
402 + }
403 + );
404 + } else if ((hardwareKeyChallenge != null) && u2fSupported()) {
405 + // Old U2F hardware keys
406 window.u2f.sign(hardwareKeyChallenge.appId, hardwareKeyChallenge.challenge, hardwareKeyChallenge.registeredKeys, function (authResponse) {
407 if ((currentpanel == 4) && authResponse.signatureData) {
408 Q('hwtokenInput').value = JSON.stringify(authResponse);
webserver.js
+26
@@ -61,6 +61,13 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
61 obj.interceptor = require('./interceptor');
62 const constants = (obj.crypto.constants ? obj.crypto.constants : require('constants')); // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead.
63
64 + // Setup WebAuthn, this is an optional install.
65 + // "npm install @davedoesdev/fido2-lib"
66 + try {
67 + const { Fido2Lib } = require("@davedoesdev/fido2-lib");
68 + obj.f2l = new Fido2Lib({ attestation: "none" });
69 + } catch (ex) { console.log(ex); }
70 +
71 // Variables
72 obj.parent = parent;
73 obj.filespath = parent.filespath;
@@ -385,6 +392,25 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
392 function getHardwareKeyChallenge(req, domain, user, func) {
393 if (req.session.u2fchallenge) { delete req.session.u2fchallenge; };
394 if (user.otphkeys && (user.otphkeys.length > 0)) {
395 + // Get all WebAuthn keys
396 + if (obj.f2l != null) {
397 + var webAuthnKeys = [];
398 + for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 3) { webAuthnKeys.push(user.otphkeys[i]); } }
399 + if (webAuthnKeys.length > 0) {
400 + obj.f2l.assertionOptions().then(function (authnOptions) {
401 + authnOptions.type = 'webAuthn';
402 + authnOptions.keyIds = [];
403 + for (var i = 0; i < webAuthnKeys.length; i++) { authnOptions.keyIds.push(webAuthnKeys[0].keyId); }
404 + req.session.u2fchallenge = authnOptions.challenge = Buffer(authnOptions.challenge).toString('base64');
405 + func(JSON.stringify(authnOptions));
406 + }, function (error) {
407 + console.log('assertionOptions-Error', error);
408 + func('');
409 + });
410 + return;
411 + }
412 + }
413 +
414 // Get all U2F keys
415 var u2fKeys = [];
416 for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 1) { u2fKeys.push(user.otphkeys[i]); } }