From 06f2911ddcc2b2fedf6bafcfc4a18c76a8a3ddc5 Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Fri, 22 Mar 2019 22:33:53 -0700 Subject: [PATCH] Started work on adding FIDO2 support. --- meshuser.js | 54 +++++++++++++++++++++ package.json | 2 +- public/images/hardware-key-WebAuthn-24.png | Bin 0 -> 976 bytes readme.txt | 8 ++- views/default.handlebars | 27 ++++++++++- views/login.handlebars | 32 +++++++++++- webserver.js | 26 ++++++++++ 7 files changed, 140 insertions(+), 9 deletions(-) create mode 100644 public/images/hardware-key-WebAuthn-24.png 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 0000000000000000000000000000000000000000..c7222da5ce0925ec71a5d345ccc5f1f109a6c885 GIT binary patch literal 976 zcmV;>126oEP)<{97<5HgbW?9;ba!ELWdLwtX>N2bZe?^J zG%heMIczh2P5=M{PDw;TR5(wql>bl6aU91#)s?i+lufzr&g6S_cXf(fm%jNr%nJE3 zHfBxNMP&S7XV`p=TvSSnqR@>=B28T}Dywv{jJ_TE!Y?ZSz|-sVu1|MG%Mae$^$)T=LPd2 zPCX0prL*u(Jp=E{=ZSL+2~XO2hBWO0W~W_*j8t}o;wu+1dExw#rx*tgQx+|NJktPq zmH~>a^bwTTuaO!sH(3X9%qGZo?S!0$tFo^_m6J(iV4Ck@Hpp}J%3-1zlRVt53#R+| zBJx%)k%P#BJVfQ^BARps4R9YGARuyaf$C@qR^86SYQn?ckNIov+``(z0<0xncjqQ% z`1=Yy9A+x~v9t!I754=H+6?!(wVo5l_wbA`#qWQfNybLHD!-Nl!}= zq213KH8kiTbpxB-WsKeP2Y%n4coePspoRfOpUQGq0ST>ehAVLeWoDsi%U z2wnb7<}Ztl!RZBQjGU-ysQ*wUR7gWaFBJu!evX5URO)JVO*_yjntY- zh#Vc^<>Q0&H?NRcUyF>lwb*v-C~J&8l#DFO7l(uhgBamkHY2;C9==wegIujfp}7f# zEzKxmyhG7@Ga~3+^7o2RHIgr1LfnBQIMHv$#nlxzURJ~Sp#@6nTZK&#OM^KoC`KGQ8 z)O2;B+R}v@iv__e!h{|UA>rX@==p|*-X1jaP~6zpi>BUg%Da(6pYI{P9;XZjy!_aT z=Dr@7zxSc##}B9@hQ4Wgbcxuj(-FKTLM)Y97lcJc!qVT5FN9^_Cn+&7K=kA5;2@)$ z_{MZ#h+l)~A;P1hg+b%N!OhK${x*hU9w8p<+2(@B_uAf%g+C(&g4)LaqC)&PMkhAK y|Ed!)I 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]); } }