diff --git a/meshuser.js b/meshuser.js index d440b29a..e97da40f 100644 --- a/meshuser.js +++ b/meshuser.js @@ -1978,6 +1978,60 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use }); break; } + case 'webauthn-startregister': + { + // Check is 2-step login is supported + const twoStepLoginSupported = ((domain.auth != 'sspi') && (parent.parent.certificates.CommonName.indexOf('.') != -1) && (args.lanonly !== true) && (args.nousers !== true)); + if ((twoStepLoginSupported == false) || (command.name == null) || (parent.f2l == null)) break; + + parent.f2l.attestationOptions().then(function (registrationOptions) { + // Convert the challenge to base64 and add user information + registrationOptions.challenge = Buffer(registrationOptions.challenge).toString('base64'); + registrationOptions.user.id = Buffer(parent.crypto.randomBytes(16)).toString('base64'); + registrationOptions.user.name = user._id; + registrationOptions.user.displayName = user._id.split('/')[2]; + + // Send the registration request + obj.webAuthnReqistrationRequest = { action: 'webauthn-startregister', keyname: command.name, request: registrationOptions }; + ws.send(JSON.stringify(obj.webAuthnReqistrationRequest)); + //console.log(obj.webAuthnReqistrationRequest); + }, function (error) { + console.log('webauthn-startregister-error', error); + }); + break; + } + case 'webauthn-endregister': + { + if ((obj.webAuthnReqistrationRequest == null) || (parent.f2l == null)) return; + + var attestationExpectations = { + challenge: obj.webAuthnReqistrationRequest.request.challenge.split('+').join('-').split('/').join('_').split('=').join(''), // Convert to Base64URL + origin: "https://devbox.mesh.meshcentral.com", + factor: "either" + }; + var clientAttestationResponse = command.response; + clientAttestationResponse.id = clientAttestationResponse.rawId; + clientAttestationResponse.rawId = new Uint8Array(Buffer.from(clientAttestationResponse.rawId, 'base64')).buffer; + clientAttestationResponse.response.attestationObject = new Uint8Array(Buffer.from(clientAttestationResponse.response.attestationObject, 'base64')).buffer; + clientAttestationResponse.response.clientDataJSON = new Uint8Array(Buffer.from(clientAttestationResponse.response.clientDataJSON, 'base64')).buffer; + + parent.f2l.attestationResult(clientAttestationResponse, attestationExpectations).then(function (regResult) { + var keyIndex = parent.crypto.randomBytes(4).readUInt32BE(0); + if (user.otphkeys == null) { user.otphkeys = []; } + user.otphkeys.push({ name: obj.webAuthnReqistrationRequest.keyname, type: 3, publicKey: regResult.authnrData.get('credentialPublicKeyPem'), counter: regResult.authnrData.get('counter'), keyIndex: keyIndex, keyId: clientAttestationResponse.id }); + parent.db.SetUser(user); + ws.send(JSON.stringify({ action: 'otp-hkey-setup-response', result: true, name: command.name, index: keyIndex })); + + // Notify change + 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 }); + }, function (error) { + console.log('webauthn-endregister-error', error); + ws.send(JSON.stringify({ action: 'otp-hkey-setup-response', result: false, error: error, name: command.name, index: keyIndex })); + }); + + delete obj.hardwareKeyRegistrationRequest; + break; + } case 'getClip': { if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check nodeid diff --git a/package.json b/package.json index 3194ea33..184af60b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "meshcentral", - "version": "0.3.0-t", + "version": "0.3.0-u", "keywords": [ "Remote Management", "Intel AMT", diff --git a/public/images/hardware-key-WebAuthn-24.png b/public/images/hardware-key-WebAuthn-24.png new file mode 100644 index 00000000..c7222da5 Binary files /dev/null and b/public/images/hardware-key-WebAuthn-24.png differ diff --git a/readme.txt b/readme.txt index 5aa9e7c5..78350cfe 100644 --- a/readme.txt +++ b/readme.txt @@ -3,13 +3,11 @@ MeshCentral For more information, [visit MeshCommander.com/MeshCentral2](http://www.meshcommander.com/meshcentral2). -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. +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. 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. -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. - -This version is BETA and should not be used in production. +To test this server, feel free to try [MeshCentral.com](http://meshcentral.com). Installation @@ -37,7 +35,7 @@ To run MeshCentral you may need to use "nodejs" instead of "node" on Linux. node meshcentral [arguments] ``` -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. +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. ``` node meshcentral --cert servername.domain.com diff --git a/views/default.handlebars b/views/default.handlebars index 547e3738..0574839e 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -1486,7 +1486,7 @@ x += "
"; if (message.keys && message.keys.length > 0) { for (var i in message.keys) { - var key = message.keys[i], type = (key.type == 1)?'U2F':'OTP'; + var key = message.keys[i], type = ((key.type == 1)?'U2F':(key.type == 2)?'OTP':'WebAuthn'); x += start + '' + key.name + "" + end; } } else { @@ -1496,6 +1496,7 @@ x += "
"; x += ""; if ((features & 0x4000) != 0) { x += ""; } + x += ""; x += "

"; setDialogMode(2, "Manage Security Keys", 8, null, x, 'otpauth-hardware-manage'); if (u2fSupported() == false) { QE('d2addkey1', false); } @@ -1533,6 +1534,26 @@ } break; } + case 'webauthn-startregister': { + if (xxdialogMode && (xxdialogTag != 'otpauth-hardware-manage')) return; + var x = "Press the key button now.

"; + setDialogMode(2, "Add Security Key", 2, null, x); + + var publicKey = message.request; + message.request.challenge = Uint8Array.from(atob(message.request.challenge), c => c.charCodeAt(0)) + message.request.user.id = Uint8Array.from(atob(message.request.user.id), c => c.charCodeAt(0)) + navigator.credentials.create({ publicKey }) + .then((newCredentialInfo) => { + // Public key credential + 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 }; + meshserver.send({ action: 'webauthn-endregister', response: r }); + setDialogMode(0); + }).catch((error) => { + // Error + setDialogMode(2, "Add Security Key", 1, null, "ERROR: " + error); + }); + break; + } case 'event': { if (!message.event.nolog) { events.unshift(message.event); @@ -5611,7 +5632,7 @@ } function account_addhkey(type) { - if (type == 1) { + if (type == 1 || type == 3) { var x = "Type in the name of the key to add.

"; x += addHtmlValue('Key Name', ''); } else if (type == 2) { @@ -5635,6 +5656,8 @@ } else if (type == 2) { meshserver.send({ action: 'otp-hkey-yubikey-add', name: name, otp: Q('dp1key').value }); setDialogMode(2, "Add Security Key", 0, null, "
Checking...


", 'otpauth-hardware-manage'); + } else if (type == 3) { + meshserver.send({ action: 'webauthn-startregister', name: name }); } } diff --git a/views/login.handlebars b/views/login.handlebars index 46ec525f..320d1eda 100644 --- a/views/login.handlebars +++ b/views/login.handlebars @@ -372,7 +372,37 @@ if ('{{loginmode}}' == '4') { try { if (hardwareKeyChallenge.length > 0) { hardwareKeyChallenge = JSON.parse(hardwareKeyChallenge); } else { hardwareKeyChallenge = null; } } catch (ex) { hardwareKeyChallenge = null } - if ((hardwareKeyChallenge != null) && u2fSupported()) { + if ((hardwareKeyChallenge != null) && (hardwareKeyChallenge.type == 'webAuthn')) { + hardwareKeyChallenge.challenge = Uint8Array.from(atob(hardwareKeyChallenge.challenge), c => c.charCodeAt(0)).buffer; + + const publicKeyCredentialRequestOptions = { challenge: hardwareKeyChallenge.challenge, allowCredentials: [], timeout: hardwareKeyChallenge.timeout } + for (var i = 0; i < hardwareKeyChallenge.keyIds.length; i++) { + publicKeyCredentialRequestOptions.allowCredentials.push( + { id: Uint8Array.from(atob(hardwareKeyChallenge.keyIds[i]), c => c.charCodeAt(0)), type: 'public-key', transports: ['usb', 'ble', 'nfc'], } + ); + } + + // New WebAuthn hardware keys + navigator.credentials.get({ publicKey: publicKeyCredentialRequestOptions }).then( + function (rawAssertion) { + console.log(rawAssertion); + /* + var assertion = { + id: base64encode(rawAssertion.rawId), + clientDataJSON: arrayBufferToString(rawAssertion.response.clientDataJSON), + userHandle: base64encode(rawAssertion.response.userHandle), + signature: base64encode(rawAssertion.response.signature), + authenticatorData: base64encode(rawAssertion.response.authenticatorData) + }; + console.log(assertion); + */ + }, + function (error) { + console.log('credentials-get error', error); + } + ); + } else if ((hardwareKeyChallenge != null) && u2fSupported()) { + // Old U2F hardware keys window.u2f.sign(hardwareKeyChallenge.appId, hardwareKeyChallenge.challenge, hardwareKeyChallenge.registeredKeys, function (authResponse) { if ((currentpanel == 4) && authResponse.signatureData) { Q('hwtokenInput').value = JSON.stringify(authResponse); diff --git a/webserver.js b/webserver.js index af00291b..359acc0d 100644 --- a/webserver.js +++ b/webserver.js @@ -61,6 +61,13 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { obj.interceptor = require('./interceptor'); const constants = (obj.crypto.constants ? obj.crypto.constants : require('constants')); // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead. + // Setup WebAuthn, this is an optional install. + // "npm install @davedoesdev/fido2-lib" + try { + const { Fido2Lib } = require("@davedoesdev/fido2-lib"); + obj.f2l = new Fido2Lib({ attestation: "none" }); + } catch (ex) { console.log(ex); } + // Variables obj.parent = parent; obj.filespath = parent.filespath; @@ -385,6 +392,25 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { function getHardwareKeyChallenge(req, domain, user, func) { if (req.session.u2fchallenge) { delete req.session.u2fchallenge; }; if (user.otphkeys && (user.otphkeys.length > 0)) { + // Get all WebAuthn keys + if (obj.f2l != null) { + var webAuthnKeys = []; + for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 3) { webAuthnKeys.push(user.otphkeys[i]); } } + if (webAuthnKeys.length > 0) { + obj.f2l.assertionOptions().then(function (authnOptions) { + authnOptions.type = 'webAuthn'; + authnOptions.keyIds = []; + for (var i = 0; i < webAuthnKeys.length; i++) { authnOptions.keyIds.push(webAuthnKeys[0].keyId); } + req.session.u2fchallenge = authnOptions.challenge = Buffer(authnOptions.challenge).toString('base64'); + func(JSON.stringify(authnOptions)); + }, function (error) { + console.log('assertionOptions-Error', error); + func(''); + }); + return; + } + } + // Get all U2F keys var u2fKeys = []; for (var i = 0; i < user.otphkeys.length; i++) { if (user.otphkeys[i].type == 1) { u2fKeys.push(user.otphkeys[i]); } }