From 1db0899a7d4807834a7c23f46b3d39f8ae9f8c88 Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Sat, 5 Oct 2019 14:24:40 -0700 Subject: [PATCH 1/2] Added MQTT authentication. --- certoperations.js | 15 +++++++++++++ meshcentral.js | 2 +- meshuser.js | 48 ++++++++++++++++++++++++++++++++++++++++ mqttbroker.js | 40 +++++++++++++++++++++++++-------- views/default.handlebars | 24 ++++++++++++++++++++ webserver.js | 7 +++--- 6 files changed, 123 insertions(+), 13 deletions(-) diff --git a/certoperations.js b/certoperations.js index dcc1663d..56e27460 100644 --- a/certoperations.js +++ b/certoperations.js @@ -241,6 +241,21 @@ module.exports.CertificateOperations = function (parent) { return obj.pki.getPublicKeyFingerprint(publickey, { encoding: "hex", md: obj.forge.md.sha384.create() }); }; + // Return the SHA384 hash of the certificate, return hex + obj.getCertHashSha1 = function (cert) { + try { + var md = obj.forge.md.sha1.create(); + md.update(obj.forge.asn1.toDer(obj.pki.certificateToAsn1(obj.pki.certificateFromPem(cert))).getBytes()); + return md.digest().toHex(); + } catch (ex) { + // If this is not an RSA certificate, hash the raw PKCS7 out of the PEM file + var x1 = cert.indexOf('-----BEGIN CERTIFICATE-----'), x2 = cert.indexOf('-----END CERTIFICATE-----'); + if ((x1 >= 0) && (x2 > x1)) { + return obj.crypto.createHash('sha1').update(Buffer.from(cert.substring(x1 + 27, x2), 'base64')).digest('hex'); + } else { console.log('ERROR: Unable to decode certificate.'); return null; } + } + }; + // Return the SHA384 hash of the certificate, return hex obj.getCertHash = function (cert) { try { diff --git a/meshcentral.js b/meshcentral.js index 98f09b44..919ca9b0 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -826,7 +826,7 @@ function CreateMeshCentralServer(config, args) { obj.apfserver = require('./apfserver.js').CreateApfServer(obj, obj.db, obj.args); // Create MQTT Broker to hook into webserver and mpsserver - if (obj.config.settings.mqtt != null) { obj.mqttbroker = require("./mqttbroker.js").CreateMQTTBroker(obj, obj.db, obj.args); } + if ((typeof obj.config.settings.mqtt == 'object') && (typeof obj.config.settings.mqtt.auth == 'object') && (typeof obj.config.settings.mqtt.auth.keyid == 'string') && (typeof obj.config.settings.mqtt.auth.key == 'string')) { obj.mqttbroker = require("./mqttbroker.js").CreateMQTTBroker(obj, obj.db, obj.args); } // Start the web server and if needed, the redirection web server. obj.webserver = require('./webserver.js').CreateWebServer(obj, obj.db, obj.args, obj.certificates); diff --git a/meshuser.js b/meshuser.js index cdfde4bc..bbe1de43 100644 --- a/meshuser.js +++ b/meshuser.js @@ -2876,6 +2876,54 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } break; } + case 'getmqttlogin': { + var err = null; + if (parent.parent.mqttbroker == null) { err = 'MQTT not supported on this server'; } + if (common.validateString(command.nodeid, 1, 1024) == false) { err = 'Invalid nodeid'; } // Check the nodeid + + // Handle any errors + if (err != null) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'getmqttlogin', responseid: command.responseid, result: err })); } catch (ex) { } } break; } + + var nodeid = command.nodeid; + if ((nodeid.split('/').length == 3) && (nodeid.split('/')[1] == domain.id)) { // Validate the domain, operation only valid for current domain + // Get the device + db.Get(nodeid, function (err, nodes) { + if ((nodes == null) || (nodes.length != 1)) { if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'getmqttlogin', responseid: command.responseid, result: 'Invalid node id' })); } catch (ex) { } return; } } + var node = nodes[0]; + + // Get the device group for this node + var mesh = parent.meshes[node.meshid]; + if (mesh) { + // Check if this user has rights to do this + if ((mesh.links[user._id] != null) && (mesh.links[user._id].rights == 0xFFFFFFFF)) { + var token = parent.parent.mqttbroker.generateLogin(mesh._id, node._id); + var r = { action: 'getmqttlogin', responseid: command.responseid, nodeid: node._id, user: token.user, pass: token.pass }; + const serverName = parent.getWebServerName(domain); + + // Add MPS URL + if (parent.parent.mpsserver != null) { + r.mpsCertHashSha384 = parent.parent.certificateOperations.getCertHash(parent.parent.mpsserver.certificates.mps.cert); + r.mpsCertHashSha1 = parent.parent.certificateOperations.getCertHashSha1(parent.parent.mpsserver.certificates.mps.cert); + r.mpsUrl = 'mqtts://' + serverName + ':' + ((args.mpsaliasport != null) ? args.mpsaliasport : args.mpsport) + '/'; + } + + // Add WS URL + var xdomain = (domain.dns == null) ? domain.id : ''; + if (xdomain != '') xdomain += "/"; + var httpsPort = ((args.aliasport == null) ? args.port : args.aliasport); // Use HTTPS alias port is specified + r.wsUrl = "ws" + (args.notls ? '' : 's') + "://" + serverName + ":" + httpsPort + "/" + xdomain + "mqtt.ashx"; + r.wsTrustedCert = parent.isTrustedCert(domain); + + try { ws.send(JSON.stringify(r)); } catch (ex) { } + } else { + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'getmqttlogin', responseid: command.responseid, result: 'Unable to perform this operation' })); } catch (ex) { } } + } + } + }); + } + + break; + } case 'amt': { if (common.validateString(command.nodeid, 1, 1024) == false) break; // Check nodeid if (common.validateInt(command.mode, 0, 3) == false) break; // Check connection mode diff --git a/mqttbroker.js b/mqttbroker.js index 42ea6cee..238a6a74 100644 --- a/mqttbroker.js +++ b/mqttbroker.js @@ -16,26 +16,46 @@ module.exports.CreateMQTTBroker = function (parent, db, args) { obj.handle = obj.aedes.handle; obj.connections = {}; // NodesID --> client array + // Generate a username and password for MQTT login + obj.generateLogin = function (meshid, nodeid) { + const meshidsplit = meshid.split('/'), nodeidsplit = nodeid.split('/'); + const xmeshid = meshidsplit[2], xnodeid = nodeidsplit[2], xdomainid = meshidsplit[1]; + const username = 'MCAuth1:' + xnodeid + ':' + xmeshid + ':' + xdomainid; + const nonce = Buffer.from(parent.crypto.randomBytes(9), 'binary').toString('base64'); + return { meshid: meshid, nodeid: nodeid, user: username, pass: parent.config.settings.mqtt.auth.keyid + ':' + nonce + ':' + parent.crypto.createHash('sha384').update(username + ':' + nonce + ':' + parent.config.settings.mqtt.auth.key).digest("base64") }; + } + // Connection Authentication obj.aedes.authenticate = function (client, username, password, callback) { - // TODO: add authentication handler - obj.parent.debug("mqtt", "Authentication with " + username + ":" + password + ":" + client.id + ", " + client.conn.xtransport + "://" + cleanRemoteAddr(client.conn.xip)); + obj.parent.debug("mqtt", "Authentication User:" + username + ", Pass:" + password.toString() + ", ClientID:" + client.id + ", " + client.conn.xtransport + "://" + cleanRemoteAddr(client.conn.xip)); + // Parse the username and password var usersplit = username.split(':'); - if (usersplit.length != 5) { callback(null, false); return; } + var passsplit = password.toString().split(':'); + if ((usersplit.length !== 4) || (passsplit.length !== 3)) { obj.parent.debug("mqtt", "Invalid user/pass format, " + client.conn.xtransport + "://" + cleanRemoteAddr(client.conn.xip)); callback(null, false); return; } + if (usersplit[0] !== 'MCAuth1') { obj.parent.debug("mqtt", "Invalid auth method, " + client.conn.xtransport + "://" + cleanRemoteAddr(client.conn.xip)); callback(null, false); return; } + + // Check authentication + if (passsplit[0] !== parent.config.settings.mqtt.auth.keyid) { obj.parent.debug("mqtt", "Invalid auth keyid, " + client.conn.xtransport + "://" + cleanRemoteAddr(client.conn.xip)); callback(null, false); return; } + if (parent.crypto.createHash('sha384').update(username + ':' + passsplit[1] + ':' + parent.config.settings.mqtt.auth.key).digest("base64") !== passsplit[2]) { obj.parent.debug("mqtt", "Invalid password, " + client.conn.xtransport + "://" + cleanRemoteAddr(client.conn.xip)); callback(null, false); return; } // Setup the identifiers - var xnodeid = usersplit[1]; + const xnodeid = usersplit[1]; var xmeshid = usersplit[2]; - var xdomainid = usersplit[3]; + const xdomainid = usersplit[3]; + + // Check the domain + if ((typeof client.conn.xdomain == 'object') && (xdomainid != client.conn.xdomain.id)) { obj.parent.debug("mqtt", "Invalid domain connection, " + client.conn.xtransport + "://" + cleanRemoteAddr(client.conn.xip)); callback(null, false); return; } // Convert meshid from HEX to Base64 if needed - if (xmeshid.length == 96) { xmeshid = Buffer.from(xmeshid, 'hex').toString('base64'); } - if ((xmeshid.length != 64) || (xnodeid.length != 64)) { callback(null, false); return; } + if (xmeshid.length === 96) { xmeshid = Buffer.from(xmeshid, 'hex').toString('base64'); } + if ((xmeshid.length !== 64) || (xnodeid.length != 64)) { callback(null, false); return; } client.xdbNodeKey = 'node/' + xdomainid + '/' + xnodeid; client.xdbMeshKey = 'mesh/' + xdomainid + '/' + xmeshid; + //console.log(obj.generateLogin(client.xdbMeshKey, client.xdbNodeKey)); + // Check if this node exists in the database db.Get(client.xdbNodeKey, function (err, nodes) { if ((nodes == null) || (nodes.length != 1)) { callback(null, false); return; } // Node does not exist @@ -75,6 +95,7 @@ module.exports.CreateMQTTBroker = function (parent, db, args) { // Check if a client can publish a packet obj.aedes.authorizePublish = function (client, packet, callback) { // TODO: add authorized publish control + //console.log(packet); obj.parent.debug("mqtt", "AuthorizePublish, " + client.conn.xtransport + "://" + cleanRemoteAddr(client.conn.xip)); callback(null); } @@ -82,13 +103,14 @@ module.exports.CreateMQTTBroker = function (parent, db, args) { // Check if a client can publish a packet obj.aedes.authorizeSubscribe = function (client, sub, callback) { // TODO: add subscription control here - obj.parent.debug("mqtt", "AuthorizeSubscribe, " + client.conn.xtransport + "://" + cleanRemoteAddr(client.conn.xip)); + obj.parent.debug("mqtt", "AuthorizeSubscribe \"" + sub.topic + "\", " + client.conn.xtransport + "://" + cleanRemoteAddr(client.conn.xip)); callback(null, sub); } - // Check if a client can publish a packet + // Check if a client can forward a packet obj.aedes.authorizeForward = function (client, packet) { // TODO: add forwarding control + //console.log(packet); obj.parent.debug("mqtt", "AuthorizeForward, " + client.conn.xtransport + "://" + cleanRemoteAddr(client.conn.xip)); //return packet; return packet; diff --git a/views/default.handlebars b/views/default.handlebars index 9cf5d6e1..cb43c827 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -2274,6 +2274,24 @@ QV('agentInvitationLinkDiv', true); break; } + case 'getmqttlogin': { + if ((currentNode == null) || (currentNode._id != message.nodeid) || (xxdialogMode != null)) return; + var x = "These settings can be used to connect MQTT for this device.

"; + delete message.action; + delete message.nodeid; + x += ''; + /* + x += addHtmlValue('Username', ''); + x += addHtmlValue('Password', ''); + x += addHtmlValue('WS URL', ''); + if (message.mpsUrl && message.mpsCertHash) { + x += addHtmlValue('MPS URL', ''); + x += addHtmlValue('MPS Cert Hash', ''); + } + */ + setDialogMode(2, "MQTT Credentials", 1, null, x); + break; + } case 'stopped': { // Server is stopping. // Disconnect autoReconnect = false; @@ -4280,6 +4298,9 @@ x += 'WinSCP '; } } + + // MQTT options + if ((meshrights == 0xFFFFFFFF) && (features & 0x00400000)) { x += 'MQTT Login '; } x += '
' QH('p10html3', x); @@ -4664,6 +4685,9 @@ setDialogMode(2, "MeshCentral Router", 1, null, x, "fileDownload"); } + // Request MQTT login credentials + function p10showMqttLoginDialog(nodeid) { meshserver.send({ action: 'getmqttlogin', nodeid: nodeid }); } + // Show MeshCmd dialog function p10showMeshCmdDialog(mode, nodeid) { if (xxdialogMode) return; diff --git a/webserver.js b/webserver.js index 98506eed..8de985a5 100644 --- a/webserver.js +++ b/webserver.js @@ -1505,6 +1505,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { if ((domain.auth == 'sspi') || (domain.auth == 'ldap')) { features += 0x00080000; } // LDAP or SSPI in use, warn that users must login first before adding a user to a group. if (domain.amtacmactivation) { features += 0x00100000; } // Intel AMT ACM activation/upgrade is possible if (domain.usernameisemail) { features += 0x00200000; } // Username is email address + if (parent.mqttbroker != null) { features += 0x00400000; } // This server supports MQTT channels // Create a authentication cookie const authCookie = obj.parent.encodeCookie({ userid: user._id, domainid: domain.id, ip: cleanRemoteAddr(req.ip) }, obj.parent.loginCookieEncryptionKey); @@ -1617,7 +1618,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { } // Return true if it looks like we are using a real TLS certificate. - function isTrustedCert(domain) { + obj.isTrustedCert = function(domain) { if (obj.args.notls == true) return false; // We are not using TLS, so not trusted cert. if ((domain != null) && (typeof domain.trustedcert == 'boolean')) return domain.trustedcert; // If the status of the cert specified, use that. if (typeof obj.args.trustedcert == 'boolean') return obj.args.trustedcert; // If the status of the cert specified, use that. @@ -2886,7 +2887,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { res.set({ 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0', 'Content-Type': 'text/plain', 'Content-Disposition': 'attachment; filename="' + scriptInfo.rname + '"' }); var data = scriptInfo.data; var cmdoptions = { wgetoptionshttp: '', wgetoptionshttps: '', curloptionshttp: '-L ', curloptionshttps: '-L ' } - if (isTrustedCert(domain) != true) { + if (obj.isTrustedCert(domain) != true) { cmdoptions.wgetoptionshttps += '--no-check-certificate '; cmdoptions.curloptionshttps += '-k '; } @@ -3350,7 +3351,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { // For example: https://localhost/createLoginToken.ashx?user=admin&pass=admin&a=3 // It's not advised to use this to create login tokens since the URL is often logged and you got credentials in the URL. // Since it's bad, it's only offered when an untrusted certificate is used as a way to help developers get started. - if (isTrustedCert() == false) { + if (obj.isTrustedCert() == false) { obj.app.get(url + 'createLoginToken.ashx', function (req, res) { // A web socket session can be authenticated in many ways (Default user, session, user/pass and cookie). Check authentication here. if ((req.query.user != null) && (req.query.pass != null)) { From 07c6415f850b9dabf725a5a4b7c9e42377888c68 Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Sat, 5 Oct 2019 21:48:17 -0700 Subject: [PATCH 2/2] Fixed last connect time and user consent dialog. --- meshagent.js | 10 ++++++---- package.json | 2 +- views/default-min.handlebars | 2 +- views/default.handlebars | 6 +++--- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/meshagent.js b/meshagent.js index 45498310..d7e58fa7 100644 --- a/meshagent.js +++ b/meshagent.js @@ -44,7 +44,6 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { // Disconnect this agent obj.close = function (arg) { - obj.authenticated = -1; if ((arg == 1) || (arg == null)) { try { ws.close(); if (obj.nodeid != null) { parent.parent.debug('agent', 'Soft disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ')'); } } catch (e) { console.log(e); } } // Soft close, close the websocket if (arg == 2) { try { ws._socket._parent.end(); if (obj.nodeid != null) { parent.parent.debug('agent', 'Hard disconnect ' + obj.nodeid + ' (' + obj.remoteaddrport + ')'); } } catch (e) { console.log(e); } } // Hard close, close the TCP socket // If arg == 3, don't communicate with this agent anymore, but don't disconnect (Duplicate agent). @@ -81,9 +80,12 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { } } else { // Update the last connect time - if (obj.authenticated == 2) { db.Set({ _id: 'lc' + obj.dbNodeKey, type: 'lastconnect', domain: domain.id, time: obj.connectTime, addr: obj.remoteaddrport }); } + if (obj.authenticated == 2) { db.Set({ _id: 'lc' + obj.dbNodeKey, type: 'lastconnect', domain: domain.id, time: obj.connectTime, addr: obj.remoteaddrport, cause: 1 }); } } + // Set this agent as no longer authenticated + obj.authenticated = -1; + // If we where updating the agent, clean that up. if (obj.agentUpdate != null) { if (obj.agentUpdate.fd) { try { parent.fs.close(obj.agentUpdate.fd); } catch (ex) { } } @@ -682,7 +684,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { // Mark when this device connected obj.connectTime = Date.now(); - db.Set({ _id: 'lc' + obj.dbNodeKey, type: 'lastconnect', domain: domain.id, time: obj.connectTime, addr: obj.remoteaddrport }); + db.Set({ _id: 'lc' + obj.dbNodeKey, type: 'lastconnect', domain: domain.id, time: obj.connectTime, addr: obj.remoteaddrport, cause: 1 }); // Device already exists, look if changes have occured var changes = [], change = 0, log = 0; @@ -743,7 +745,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { // Mark when this device connected obj.connectTime = Date.now(); - db.Set({ _id: 'lc' + obj.dbNodeKey, type: 'lastconnect', domain: domain.id, time: obj.connectTime, addr: obj.remoteaddrport }); + db.Set({ _id: 'lc' + obj.dbNodeKey, type: 'lastconnect', domain: domain.id, time: obj.connectTime, addr: obj.remoteaddrport, cause: 1 }); // This node does not exist, create it. var device = { type: 'node', mtype: mesh.mtype, _id: obj.dbNodeKey, icon: obj.agentInfo.platformType, meshid: obj.dbMeshKey, name: obj.agentInfo.computerName, rname: obj.agentInfo.computerName, domain: domain.id, agent: { ver: obj.agentInfo.agentVersion, id: obj.agentInfo.agentId, caps: obj.agentInfo.capabilities }, host: null }; diff --git a/package.json b/package.json index f7cf3e00..62c4ac79 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "meshcentral", - "version": "0.4.1-u", + "version": "0.4.1-v", "keywords": [ "Remote Management", "Intel AMT", diff --git a/views/default-min.handlebars b/views/default-min.handlebars index 9bf81382..59fc8798 100644 --- a/views/default-min.handlebars +++ b/views/default-min.handlebars @@ -1 +1 @@ - {{{title}}}
{{{title}}}
{{{title2}}}

{{{logoutControl}}}

 

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

{{{logoutControl}}}

 

\ No newline at end of file diff --git a/views/default.handlebars b/views/default.handlebars index cb43c827..096cafee 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -7183,14 +7183,14 @@ if (serverinfo.consent & 0x0010) { Q('d20flag4').checked = true; } if (serverinfo.consent & 0x0004) { Q('d20flag5').checked = true; } if (serverinfo.consent & 0x0020) { Q('d20flag6').checked = true; } - if (serverinfo.consent & 0x0040) { Q('d20flag7').checked = true; } + if (debugmode) { if (serverinfo.consent & 0x0040) { Q('d20flag7').checked = true; } } QE('d20flag1', !(serverinfo.consent & 0x0001)); QE('d20flag2', !(serverinfo.consent & 0x0008)); QE('d20flag3', !(serverinfo.consent & 0x0002)); QE('d20flag4', !(serverinfo.consent & 0x0010)); QE('d20flag5', !(serverinfo.consent & 0x0004)); QE('d20flag6', !(serverinfo.consent & 0x0020)); - QE('d20flag7', !(serverinfo.consent & 0x0040)); + if (debugmode) { QE('d20flag7', !(serverinfo.consent & 0x0040)); } } } @@ -7202,7 +7202,7 @@ if (Q('d20flag4').checked) { consent += 0x0010; } if (Q('d20flag5').checked) { consent += 0x0004; } if (Q('d20flag6').checked) { consent += 0x0020; } - if (Q('d20flag7').checked) { consent += 0x0040; } + if (debugmode) { if (Q('d20flag7').checked) { consent += 0x0040; } } meshserver.send({ action: 'editmesh', meshid: currentMesh._id, consent: consent }); }