Added support for AES128/HMAC-SHA256 login tokens and online token generation.
Ylian Saint-Hilaire committed
Dec 28, 2018 at 21:55 UTC
caed053d2de04b4d7cda6259d7bcb8effc841174
3 files changed
+66
-16
meshcentral.js
+43
-15
@@ -58,7 +58,7 @@ function CreateMeshCentralServer(config, args) {
58
obj.maintenanceTimer = null;
59
obj.serverId = null;
60
obj.currentVer = null;
61
- obj.serverKey = new Buffer(obj.crypto.randomBytes(32), 'binary');
61
+ obj.serverKey = new Buffer(obj.crypto.randomBytes(48), 'binary');
62
obj.loginCookieEncryptionKey = null;
63
obj.serverSelfWriteAllowed = true;
64
try { obj.currentVer = JSON.parse(obj.fs.readFileSync(obj.path.join(__dirname, 'package.json'), 'utf8')).version; } catch (e) { } // Fetch server version
@@ -528,7 +528,7 @@ function CreateMeshCentralServer(config, args) {
528
// Load the login cookie encryption key from the database if allowed
529
if ((obj.config) && (obj.config.settings) && (obj.config.settings.allowlogintoken == true)) {
530
obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
531
- if ((docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null)) {
531
+ if ((docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 96)) {
532
obj.loginCookieEncryptionKey = Buffer.from(docs[0].key, 'hex');
533
} else {
534
obj.loginCookieEncryptionKey = obj.generateCookieKey(); obj.db.Set({ _id: 'LoginCookieEncryptionKey', key: obj.loginCookieEncryptionKey.toString('hex'), time: Date.now() });
@@ -1067,7 +1067,7 @@ function CreateMeshCentralServer(config, args) {
1067
} else {
1068
// Load the login cookie encryption key from the database
1069
obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
1070
- if ((docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null)) {
1070
+ if ((docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 96)) {
1071
// Key is present, use it.
1072
obj.loginCookieEncryptionKey = Buffer.from(docs[0].key, 'hex');
1073
func(obj.encodeCookie({ u: userid, a: 3 }, obj.loginCookieEncryptionKey));
@@ -1081,11 +1081,11 @@ function CreateMeshCentralServer(config, args) {
1081
});
1082
};
1083
1084
- // Show the yser login token generation key
1084
+ // Show the user login token generation key
1085
obj.showLoginTokenKey = function (func) {
1086
// Load the login cookie encryption key from the database
1087
obj.db.Get('LoginCookieEncryptionKey', function (err, docs) {
1088
- if ((docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null)) {
1088
+ if ((docs.length > 0) && (docs[0].key != null) && (obj.args.logintokengen == null) && (docs[0].key.length >= 96)) {
1089
// Key is present, use it.
1090
func(docs[0].key);
1091
} else {
@@ -1098,30 +1098,36 @@ function CreateMeshCentralServer(config, args) {
1098
1099
// Generate a cryptographic key used to encode and decode cookies
1100
obj.generateCookieKey = function () {
1101
- return new Buffer(obj.crypto.randomBytes(32), 'binary');
1102
- //return Buffer.alloc(32, 0); // Sets the key to zeros, debug only.
1101
+ return new Buffer(obj.crypto.randomBytes(48), 'binary');
1102
+ //return Buffer.alloc(48, 0); // Sets the key to zeros, debug only.
1103
};
1104
1105
- // Encode an object as a cookie using a key. (key must be 32 bytes long)
1105
+ // Encode an object as a cookie using a key using AES-GCM. (key must be 32 bytes or more)
1106
obj.encodeCookie = function (o, key) {
1107
try {
1108
if (key == null) { key = obj.serverKey; }
1109
o.time = Math.floor(Date.now() / 1000); // Add the cookie creation time
1110
- var iv = new Buffer(obj.crypto.randomBytes(12), 'binary'), cipher = obj.crypto.createCipheriv('aes-256-gcm', key, iv);
1111
- var crypted = Buffer.concat([cipher.update(JSON.stringify(o), 'utf8'), cipher.final()]);
1112
- var cookie = Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
1113
- return cookie;
1110
+ const iv = new Buffer(obj.crypto.randomBytes(12), 'binary'), cipher = obj.crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv);
1111
+ const crypted = Buffer.concat([cipher.update(JSON.stringify(o), 'utf8'), cipher.final()]);
1112
+ return Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
1113
} catch (e) { return null; }
1114
};
1115
1117
- // 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)
1116
+ // Decode a cookie back into an object using a key using AES-GCM or AES128-CBC/HMAC-SHA386. Return null if it's not a valid cookie. (key must be 32 bytes or more)
1117
obj.decodeCookie = function (cookie, key, timeout) {
1118
+ const r = obj.decodeCookieAESGCM(cookie, key, timeout);
1119
+ if (r == null) { return obj.decodeCookieAESSHA(cookie, key, timeout); }
1120
+ return r;
1121
+ }
1122
+
1123
+ // Decode a cookie back into an object using a key using AES-GCM. Return null if it's not a valid cookie. (key must be 32 bytes or more)
1124
+ obj.decodeCookieAESGCM = function (cookie, key, timeout) {
1125
try {
1126
if (key == null) { key = obj.serverKey; }
1127
cookie = new Buffer(cookie.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64');
1122
- var decipher = obj.crypto.createDecipheriv('aes-256-gcm', key, cookie.slice(0, 12));
1128
+ const decipher = obj.crypto.createDecipheriv('aes-256-gcm', key.slice(0, 32), cookie.slice(0, 12));
1129
decipher.setAuthTag(cookie.slice(12, 16));
1124
- var o = JSON.parse(decipher.update(cookie.slice(28), 'binary', 'utf8') + decipher.final('utf8'));
1130
+ const o = JSON.parse(decipher.update(cookie.slice(28), 'binary', 'utf8') + decipher.final('utf8'));
1131
if ((o.time == null) || (o.time == null) || (typeof o.time != 'number')) { Debug(1, 'ERR: Bad cookie due to invalid time'); return null; }
1132
o.time = o.time * 1000; // Decode the cookie creation time
1133
o.dtime = Date.now() - o.time; // Decode how long ago the cookie was created (in milliseconds)
@@ -1131,6 +1137,28 @@ function CreateMeshCentralServer(config, args) {
1137
} catch (e) { return null; }
1138
};
1139
1140
+ // Decode a cookie back into an object using a key using AES128 / HMAC-SHA256. Return null if it's not a valid cookie. (key must be 48 bytes or more)
1141
+ // We do this because poor .NET does not support AES-GCM.
1142
+ obj.decodeCookieAESSHA = function (cookie, key, timeout) {
1143
+ try {
1144
+ if (key == null) { key = obj.serverKey; }
1145
+ if (key.length < 48) return null;
1146
+ cookie = new Buffer(cookie.replace(/\@/g, '+').replace(/\$/g, '/'), 'base64');
1147
+ const decipher = obj.crypto.createDecipheriv('aes-128-cbc', key.slice(32, 48), cookie.slice(0, 16));
1148
+ const rawmsg = decipher.update(cookie.slice(16), 'binary', 'binary') + decipher.final('binary');
1149
+ const hmac = obj.crypto.createHmac('sha256', key.slice(0, 32));
1150
+ hmac.update(rawmsg.slice(32));
1151
+ if (Buffer.compare(hmac.digest(), Buffer.from(rawmsg.slice(0, 32))) == false) { return null; }
1152
+ const o = JSON.parse(rawmsg.slice(32).toString('utf8'));
1153
+ if ((o.time == null) || (o.time == null) || (typeof o.time != 'number')) { Debug(1, 'ERR: Bad cookie due to invalid time'); return null; }
1154
+ o.time = o.time * 1000; // Decode the cookie creation time
1155
+ o.dtime = Date.now() - o.time; // Decode how long ago the cookie was created (in milliseconds)
1156
+ if (timeout == null) { timeout = 2; }
1157
+ if ((o.dtime > (timeout * 60000)) || (o.dtime < -30000)) { obj.debug(1, 'ERR: Bad cookie due to timeout'); 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)
1158
+ return o;
1159
+ } catch (ex) { console.log(ex); return null; }
1160
+ };
1161
+
1162
// Debug
1163
obj.debug = function (lvl) {
1164
if (lvl > obj.debugLevel) return;
package.json
+1
-1
@@ -1,6 +1,6 @@
1
{
2
"name": "meshcentral",
3
- "version": "0.2.5-e",
3
+ "version": "0.2.5-f",
4
"keywords": [
5
"Remote Management",
6
"Intel AMT",
webserver.js
+22
@@ -1922,6 +1922,28 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
1922
try { obj.meshAgentHandler.CreateMeshAgent(obj, obj.db, ws, req, obj.args, getDomain(req)); } catch (e) { console.log(e); }
1923
});
1924
1925
+ // Creates a login token using the user/pass that is passed in as URL arguments.
1926
+ // For example: https://localhost/createLoginToken.ashx?user=admin&pass=admin&a=3
1927
+ // It's not advised to use this to create login tokens since the URL is often logged and you got credentials in the URL.
1928
+ // However, people want it so here it is.
1929
+ obj.app.get(url + 'createLoginToken.ashx', function (req, res) {
1930
+ // A web socket session can be authenticated in many ways (Default user, session, user/pass and cookie). Check authentication here.
1931
+ if ((req.query.user != null) && (req.query.pass != null)) {
1932
+ // A user/pass is provided in URL arguments
1933
+ obj.authenticate(req.query.user, req.query.pass, getDomain(req), function (err, userid) {
1934
+ if ((err == null) && (obj.users[userid])) {
1935
+ // User is authenticated, create a token
1936
+ var x = { a: 3 }; for (var i in req.query) { if ((i != 'user') && (i != 'pass')) { x[i] = obj.common.toNumber(req.query[i]); } } x.u = userid;
1937
+ res.send(obj.parent.encodeCookie(x, obj.parent.loginCookieEncryptionKey));
1938
+ } else {
1939
+ res.sendStatus(404);
1940
+ }
1941
+ });
1942
+ } else {
1943
+ res.sendStatus(404);
1944
+ }
1945
+ });
1946
+
1947
obj.app.get(url + 'stop', function (req, res) { res.send('Stopping Server, <a href="' + url + '">click here to login</a>.'); setTimeout(function () { parent.Stop(); }, 500); });
1948
1949
// Indicates to ExpressJS that the public folder should be used to serve static files.