diff --git a/agents/MeshService.exe b/agents/MeshService.exe index 7044eb16..fa41c14d 100644 Binary files a/agents/MeshService.exe and b/agents/MeshService.exe differ diff --git a/agents/MeshService64.exe b/agents/MeshService64.exe index 490d18b9..36e8aa29 100644 Binary files a/agents/MeshService64.exe and b/agents/MeshService64.exe differ diff --git a/db.js b/db.js index 25d70d32..2d069a28 100644 --- a/db.js +++ b/db.js @@ -80,7 +80,7 @@ module.exports.CreateDB = function (args, datapath) { */ } - obj.Set = function (data) { obj.file.update({ _id: data._id }, data, { upsert: true }); } + obj.Set = function (data, func) { obj.file.update({ _id: data._id }, data, { upsert: true }, func); } obj.Get = function (id, func) { obj.file.find({ _id: id }, func); } obj.GetAll = function (func) { obj.file.find({}, func); } obj.GetAllTypeNoTypeField = function (type, domain, func) { obj.file.find({ type: type, domain: domain }, { type : 0 }, func); } diff --git a/meshagent.js b/meshagent.js index 741f3786..241ffe35 100644 --- a/meshagent.js +++ b/meshagent.js @@ -38,6 +38,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { obj.close = function (arg) { if ((arg == 1) || (arg == null)) { try { obj.ws.close(); obj.parent.parent.debug(1, 'Soft disconnect ' + obj.nodeid + ' (' + obj.remoteaddr + ')'); } catch (e) { console.log(e); } } // Soft close, close the websocket if (arg == 2) { try { obj.ws._socket._parent.end(); obj.parent.parent.debug(1, 'Hard disconnect ' + obj.nodeid + ' (' + obj.remoteaddr + ')'); } catch (e) { console.log(e); } } // Hard close, close the TCP socket + if (arg == 3) { obj.authenticated = -1; } // Don't communicate with this agent anymore, but don't disconnect (Duplicate agent). if (obj.parent.wsagents[obj.dbNodeKey] == obj) { delete obj.parent.wsagents[obj.dbNodeKey]; obj.parent.parent.ClearConnectivityState(obj.dbMeshKey, obj.dbNodeKey, 1); @@ -293,7 +294,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { if (dupAgent) { // Close the duplicate agent obj.parent.parent.debug(1, 'Duplicate agent ' + obj.nodeid + ' (' + obj.remoteaddr + ')'); - dupAgent.close(); + dupAgent.close(3); } else { // Indicate the agent is connected obj.parent.parent.SetConnectivityState(obj.dbMeshKey, obj.dbNodeKey, obj.connectTime, 1, 1); diff --git a/meshcentral.js b/meshcentral.js index 93847222..3e637bfe 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -40,6 +40,8 @@ function CreateMeshCentralServer() { obj.maintenanceTimer = null; obj.serverId = null; obj.currentVer = null; + obj.serverKey = new Buffer(obj.crypto.randomBytes(32), 'binary'); + obj.loginCookieEncryptionKey = null; try { obj.currentVer = JSON.parse(require('fs').readFileSync(obj.path.join(__dirname, 'package.json'), 'utf8')).version; } catch (e) { } // Fetch server version // Setup the default configuration and files paths @@ -70,7 +72,7 @@ function CreateMeshCentralServer() { try { require('./pass').hash('test', function () { }); } catch (e) { console.log('Old version of node, must upgrade.'); return; } // TODO: Not sure if this test works or not. // Check for invalid arguments - var validArguments = ['_', 'notls', 'user', 'port', 'mpsport', 'redirport', 'cert', 'deletedomain', 'deletedefaultdomain', 'showall', 'showusers', 'shownodes', 'showmeshes', 'showevents', 'showpower', 'showiplocations', 'help', 'exactports', 'install', 'uninstall', 'start', 'stop', 'restart', 'debug', 'filespath', 'datapath', 'noagentupdate', 'launch', 'noserverbackup', 'mongodb', 'mongodbcol', 'wanonly', 'lanonly', 'nousers', 'mpsdebug', 'mpspass', 'ciralocalfqdn', 'dbexport', 'dbimport', 'selfupdate', 'tlsoffload', 'userallowedip', 'fastcert', 'swarmport', 'swarmdebug']; + var validArguments = ['_', 'notls', 'user', 'port', 'mpsport', 'redirport', 'cert', 'deletedomain', 'deletedefaultdomain', 'showall', 'showusers', 'shownodes', 'showmeshes', 'showevents', 'showpower', 'showiplocations', 'help', 'exactports', 'install', 'uninstall', 'start', 'stop', 'restart', 'debug', 'filespath', 'datapath', 'noagentupdate', 'launch', 'noserverbackup', 'mongodb', 'mongodbcol', 'wanonly', 'lanonly', 'nousers', 'mpsdebug', 'mpspass', 'ciralocalfqdn', 'dbexport', 'dbimport', 'selfupdate', 'tlsoffload', 'userallowedip', 'fastcert', 'swarmport', 'swarmdebug', 'logintoken', 'logintokenkey']; for (var arg in obj.args) { obj.args[arg.toLocaleLowerCase()] = obj.args[arg]; if (validArguments.indexOf(arg.toLocaleLowerCase()) == -1) { console.log('Invalid argument "' + arg + '", use --help.'); return; } } if (obj.args.mongodb == true) { console.log('Must specify: --mongodb [connectionstring] \r\nSee https://docs.mongodb.com/manual/reference/connection-string/ for MongoDB connection string.'); return; } @@ -234,6 +236,8 @@ function CreateMeshCentralServer() { if (obj.args.showevents) { obj.db.GetAllType('event', function (err, docs) { console.log(docs); process.exit(); }); return; } if (obj.args.showpower) { obj.db.GetAllType('power', function (err, docs) { console.log(docs); process.exit(); }); return; } if (obj.args.showiplocations) { obj.db.GetAllType('iploc', function (err, docs) { console.log(docs); process.exit(); }); return; } + if (obj.args.logintoken) { obj.getLoginToken(obj.args.logintoken, function (r) { console.log(r); process.exit(); }); return; } + if (obj.args.logintokenkey) { obj.showLoginTokenKey(function (r) { console.log(r); process.exit(); }); return; } if (obj.args.dbexport) { // Export the entire database to a JSON file if (obj.args.dbexport == true) { obj.args.dbexport = obj.path.join(obj.datapath, 'meshcentral.db.json'); } @@ -365,13 +369,18 @@ function CreateMeshCentralServer() { // Dispatch an event that the server is now running obj.DispatchEvent(['*'], obj, { etype: 'server', action: 'started', msg: 'Server started' }) - obj.debug(1, 'Server started'); + // Load the login cookie encryption key from the database if allowed + if ((obj.config) && (obj.config.settings) && (obj.config.settings.loginTokenOk == true)) { + obj.db.Get('LoginCookieEncryptionKey', function (err, docs) { + if ((docs.length > 0) && (docs[0].key != null)) { + obj.loginCookieEncryptionKey = Buffer.from(docs[0].key, 'hex'); + } else { + obj.loginCookieEncryptionKey = obj.generateCookieKey(); obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() }); + } + }); + } - /* - obj.db.GetUserWithVerifiedEmail('', 'ylian.saint-hilaire@intel.com', function (err, docs) { - console.log(JSON.stringify(docs)); - }); - */ + obj.debug(1, 'Server started'); }); }); }); @@ -823,6 +832,79 @@ function CreateMeshCentralServer() { } } + // Generate a time limited user login token + obj.getLoginToken = function (userid, func) { + var x = userid.split('/'); + if (x == null || x.length != 3 || x[0] != 'user') { func('Invalid userid.'); return; } + obj.db.Get(userid, function (err, docs) { + if (err != null || docs == null || docs.length == 0) { + func('User ' + userid + ' not found.'); return; + } else { + // Load the login cookie encryption key from the database + obj.db.Get('LoginCookieEncryptionKey', function (err, docs) { + if ((docs.length > 0) && (docs[0].key != null)) { + // Key is present, use it. + obj.loginCookieEncryptionKey = Buffer.from(docs[0].key, 'hex'); + func(obj.encodeCookie({ u: userid, a: 3 }, obj.loginCookieEncryptionKey)); + } else { + // Key is not present, generate one. + obj.loginCookieEncryptionKey = obj.generateCookieKey(); + obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() }, function () { func(obj.encodeCookie({ u: userid, a: 3 }, obj.loginCookieEncryptionKey)); }); + } + }); + } + }); + } + + // Show the yser login token generation key + obj.showLoginTokenKey = function (func) { + // Load the login cookie encryption key from the database + obj.db.Get('LoginCookieEncryptionKey', function (err, docs) { + if ((docs.length > 0) && (docs[0].key != null)) { + // Key is present, use it. + func(docs[0].key); + } else { + // Key is not present, generate one. + obj.loginCookieEncryptionKey = obj.generateCookieKey(); + obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() }, function () { func(obj.loginCookieEncryptionKey.toString('hex')); }); + } + }); + } + + // Generate a cryptographic key used to encode and decode cookies + obj.generateCookieKey = function () { + return new Buffer(obj.crypto.randomBytes(32), 'binary'); + //return Buffer.alloc(32, 0); // Sets the key to zeros, debug only. + } + + // Encode an object as a cookie using a key. (key must be 32 bytes long) + obj.encodeCookie = function (o, key) { + try { + if (key == null) { key = obj.serverKey; } + o.time = Math.floor(Date.now() / 1000); // Add the cookie creation time + var iv = new Buffer(obj.crypto.randomBytes(12), 'binary'), cipher = obj.crypto.createCipheriv('aes-256-gcm', key, iv); + var crypted = Buffer.concat([cipher.update(JSON.stringify(o), 'utf8'), cipher.final()]); + return Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); + } catch (e) { return null; } + } + + // Decode a cookie back into an object using a key. Return null if it's not a valid cookie. (key must be 32 bytes long) + obj.decodeCookie = function (cookie, key, timeout) { + try { + if (key == null) { key = obj.serverKey; } + cookie = new Buffer(cookie.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64'); + var decipher = obj.crypto.createDecipheriv('aes-256-gcm', key, cookie.slice(0, 12)); + decipher.setAuthTag(cookie.slice(12, 16)); + var o = JSON.parse(decipher.update(cookie.slice(28), 'binary', 'utf8') + decipher.final('utf8')); + if ((o.time == null) || (o.time == null) || (typeof o.time != 'number')) { return null; } + o.time = o.time * 1000; // Decode the cookie creation time + o.dtime = Date.now() - o.time; // Decode how long ago the cookie was created (in milliseconds) + if (timeout == null) { timeout = 2; } + if ((o.dtime > (timeout * 60000)) || (o.dtime < -30000)) return null; // The cookie is only valid 120 seconds, or 30 seconds back in time (in case other server's clock is not quite right) + return o; + } catch (e) { return null; } + } + // Debug obj.debug = function (lvl) { if (lvl > obj.debugLevel) return; diff --git a/meshmail.js b/meshmail.js index ffb8f95c..57c9e27c 100644 --- a/meshmail.js +++ b/meshmail.js @@ -11,6 +11,7 @@ module.exports.CreateMeshMain = function (parent) { obj.parent = parent; obj.retry = 0; obj.sendingMail = false; + obj.mailCookieEncryptionKey = null; const nodemailer = require('nodemailer'); // Default account email validation mail @@ -45,7 +46,7 @@ module.exports.CreateMeshMain = function (parent) { // Send account check mail obj.sendAccountCheckMail = function (domain, username, email) { if ((parent.certificates == null) || (parent.certificates.CommonName == null)) return; // If the server name is not set, no reset possible. - var cookie = obj.parent.webserver.encodeCookie({ u: domain.id + '/' + username, e: email, a: 1 }); + var cookie = obj.parent.encodeCookie({ u: domain.id + '/' + username, e: email, a: 1 }, obj.mailCookieEncryptionKey); obj.pendingMails.push({ to: email, from: parent.config.smtp.from, subject: mailReplacements(accountCheckSubject, domain, username, email), text: mailReplacements(accountCheckMailText, domain, username, email, cookie), html: mailReplacements(accountCheckMailHtml, domain, username, email, cookie) }); sendNextMail(); } @@ -53,7 +54,7 @@ module.exports.CreateMeshMain = function (parent) { // Send account reset mail obj.sendAccountResetMail = function (domain, username, email) { if ((parent.certificates == null) || (parent.certificates.CommonName == null)) return; // If the server name is not set, don't validate the email address. - var cookie = obj.parent.webserver.encodeCookie({ u: domain.id + '/' + username, e: email, a: 2 }); + var cookie = obj.parent.encodeCookie({ u: domain.id + '/' + username, e: email, a: 2 }, obj.mailCookieEncryptionKey); obj.pendingMails.push({ to: email, from: parent.config.smtp.from, subject: mailReplacements(accountResetSubject, domain, username, email), text: mailReplacements(accountResetMailText, domain, username, email, cookie), html: mailReplacements(accountResetMailHtml, domain, username, email, cookie) }); sendNextMail(); } @@ -74,7 +75,7 @@ module.exports.CreateMeshMain = function (parent) { sendNextMail(); // Send the next mail } else { obj.retry++; - console.log('SMTP server failed: ' + err.response); + console.log('SMTP server failed: ' + JSON.stringify(err)); if (obj.retry < 6) { setTimeout(sendNextMail, 60000); } // Wait and try again } }); @@ -86,10 +87,22 @@ module.exports.CreateMeshMain = function (parent) { if (err == null) { console.log('SMTP mail server ' + parent.config.smtp.host + ' working as expected.'); } else { - console.log('SMTP mail server ' + parent.config.smtp.host + ' failed: ' + err.response); + console.log('SMTP mail server ' + parent.config.smtp.host + ' failed: ' + JSON.stringify(err)); } }); } + // Load the cookie encryption key from the database + obj.parent.db.Get('MailCookieEncryptionKey', function (err, docs) { + if ((docs.length > 0) && (docs[0].key != null)) { + // Key is present, use it. + obj.mailCookieEncryptionKey = Buffer.from(docs[0].key, 'hex'); + } else { + // Key is not present, generate one. + obj.mailCookieEncryptionKey = obj.parent.generateCookieKey(); + obj.parent.db.Set({ _id: 'MailCookieEncryptionKey', key: obj.mailCookieEncryptionKey.toString('hex'), time: Date.now() }); + } + }); + return obj; } \ No newline at end of file diff --git a/meshrelay.js b/meshrelay.js index 449707d6..dbf98cda 100644 --- a/meshrelay.js +++ b/meshrelay.js @@ -84,7 +84,7 @@ module.exports.CreateMeshRelay = function (parent, ws, req, domain) { } } else { // Get the session from the cookie - var cookie = obj.parent.parent.webserver.decodeCookie(req.query.auth); + var cookie = obj.parent.parent.decodeCookie(req.query.auth); if (cookie != null) { obj.authenticated = true; if (cookie.tcpport != null) { diff --git a/meshuser.js b/meshuser.js index 1a7b963a..d100b43b 100644 --- a/meshuser.js +++ b/meshuser.js @@ -833,7 +833,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain) { case 'agentdisconnect': { // Force mesh agent disconnection - forceMeshAgentDisconnect(user, domain, command.nodeid, command.disconnectMode); + obj.parent.forceMeshAgentDisconnect(user, domain, command.nodeid, command.disconnectMode); break; } case 'close': @@ -855,7 +855,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain) { if (command.nodeid) { cookieContent.nodeid = command.nodeid; } if (command.tcpaddr) { cookieContent.tcpaddr = command.tcpaddr; } // Indicates the browser want to agent to TCP connect to a remote address if (command.tcpport) { cookieContent.tcpport = command.tcpport; } // Indicates the browser want to agent to TCP connect to a remote port - command.cookie = obj.parent.encodeCookie(cookieContent); + command.cookie = obj.parent.parent.encodeCookie(cookieContent); ws.send(JSON.stringify(command)); } } diff --git a/multiserver.js b/multiserver.js index dfefd73d..d42fc4a0 100644 --- a/multiserver.js +++ b/multiserver.js @@ -561,7 +561,7 @@ module.exports.CreateMultiServer = function (parent, args) { if (path.substring(path.length - 11) == '/.websocket') { path = path.substring(0, path.length - 11); } var queryStr = '' for (var i in req.query) { queryStr += ((queryStr == '') ? '?' : '&') + i + '=' + req.query[i]; } - if (user != null) { queryStr += ((queryStr == '') ? '?' : '&') + 'auth=' + obj.encodeCookie({ userid: user._id, domainid: user.domain }, cookieKey); } + if (user != null) { queryStr += ((queryStr == '') ? '?' : '&') + 'auth=' + obj.parent.encodeCookie({ userid: user._id, domainid: user.domain }, cookieKey); } var url = obj.peerConfig.servers[serverid].url + path + queryStr; // Setup an connect the web socket diff --git a/package.json b/package.json index 49cae205..b3a61d77 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "meshcentral", - "version": "0.1.0-o", + "version": "0.1.0-x", "keywords": [ "Remote Management", "Intel AMT", diff --git a/views/default.handlebars b/views/default.handlebars index ce7abdf8..8cb84f41 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -56,41 +56,43 @@
-

{{{logoutControl}}}

+

{{{logoutControl}}}

-