Added support for user inner server authentication.
Ylian Saint-Hilaire committed
Apr 2, 2021 at 17:20 UTC
72799f0346df3830f1c476152590ec95eeac828c
3 files changed
+172
-34
agents/meshcmd.js
+39
-11
@@ -68,7 +68,11 @@ var FullSite_IntelAmtLocalWebApp = "H4sIAAAAAAAEAMQ5h3ajvNKvwu/9SnI2JICNa7zn4JLu
68
// Check the server certificate fingerprint
69
function onVerifyServer(clientName, certs) {
70
if (certs == null) { certs = clientName; } // Temporary thing until we fix duktape
71
- settings.meshServerTlsHash = certs[certs.length - 1].fingerprint.split(':').join(''); // This is used to delayed server authentication
71
+
72
+ // If we have the serverid, used delayed server authentication
73
+ if (settings.serverid != null) { settings.meshServerTlsHash = certs[certs.length - 1].fingerprint.split(':').join(''); return; }
74
+
75
+ // Otherwise, use server HTTPS certificate hash
76
try { for (var i in certs) { if (certs[i].fingerprint.replace(/:/g, '') == settings.serverhttpshash) { return; } } } catch (e) { }
77
if (settings.serverhttpshash != null) {
78
console.log('Error: Failed to verify server certificate.');
@@ -1981,14 +1985,17 @@ function startRouter() {
1985
1986
// Complete the URL and add a x-meshauth header if needed
1987
var xurlargs = [];
1984
- if (settings.authcookie != null) {
1985
- xurlargs.push('auth=' + settings.authcookie);
1986
- if (xtoken != null) { xurlargs.push('token=' + xtoken); }
1987
- } else {
1988
- if (xtoken != null) {
1989
- options.headers = { 'x-meshauth': Buffer.from(settings.username,'binary').toString('base64') + ',' + Buffer.from(settings.password,'binary').toString('base64') + ',' + Buffer.from(xtoken,'binary').toString('base64') };
1988
+ if (settings.serverid == null) {
1989
+ // Authenticate the server using HTTPS cert hash
1990
+ if (settings.authcookie != null) {
1991
+ xurlargs.push('auth=' + settings.authcookie);
1992
+ if (xtoken != null) { xurlargs.push('token=' + xtoken); }
1993
} else {
1991
- options.headers = { 'x-meshauth': Buffer.from(settings.username,'binary').toString('base64') + ',' + Buffer.from(settings.password,'binary').toString('base64') };
1994
+ if (xtoken != null) {
1995
+ options.headers = { 'x-meshauth': Buffer.from(settings.username, 'binary').toString('base64') + ',' + Buffer.from(settings.password, 'binary').toString('base64') + ',' + Buffer.from(xtoken, 'binary').toString('base64') };
1996
+ } else {
1997
+ options.headers = { 'x-meshauth': Buffer.from(settings.username, 'binary').toString('base64') + ',' + Buffer.from(settings.password, 'binary').toString('base64') };
1998
+ }
1999
}
2000
}
2001
if (settings.loginkey) { xurlargs.push('key=' + settings.loginkey); }
@@ -2050,7 +2057,26 @@ function OnServerWebSocket(msg, s, head) {
2057
var signDataHash = hasher.syncHash(Buffer.concat([Buffer.from(settings.serverAuthClientNonce, 'base64'), Buffer.from(settings.meshServerTlsHash, 'hex'), Buffer.from(command.nonce, 'base64')]));
2058
if (require('RSA').verify(require('RSA').TYPES.SHA384, cert, signDataHash, Buffer.from(command.signature, 'base64')) == false) { console.log("Unable to authenticate the server, invalid signature."); process.exit(1); return; }
2059
2053
- console.log('Server is authenticated'); // TODO: Send username/password to server.
2060
+ // Figure out the 2FA token to use if any
2061
+ var xtoken = null;
2062
+ if (settings.emailtoken) { xtoken = '**email**'; }
2063
+ else if (settings.smstoken) { xtoken = '**sms**'; }
2064
+ else if (settings.token != null) { xtoken = settings.token; }
2065
+
2066
+ // Authenticate the server using HTTPS cert hash
2067
+ if (settings.authcookie != null) {
2068
+ if (xtoken != null) {
2069
+ s.write("{\"action\":\"userAuth\",\"auth\":\"" + settings.authcookie + "\",\"token\":\"" + xtoken + "\"}");
2070
+ } else {
2071
+ s.write("{\"action\":\"userAuth\",\"auth\":\"" + settings.authcookie + "\"}");
2072
+ }
2073
+ } else {
2074
+ if (xtoken != null) {
2075
+ s.write("{\"action\":\"userAuth\",\"username\":\"" + Buffer.from(settings.username, 'binary').toString('base64') + "\",\"password\":\"" + Buffer.from(settings.password, 'binary').toString('base64') + "\",\"token\":\"" + xtoken + "\"}");
2076
+ } else {
2077
+ s.write("{\"action\":\"userAuth\",\"username\":\"" + Buffer.from(settings.username, 'binary').toString('base64') + "\",\"password\":\"" + Buffer.from(settings.password, 'binary').toString('base64') + "\"}");
2078
+ }
2079
+ }
2080
break;
2081
}
2082
}
@@ -2059,8 +2085,10 @@ function OnServerWebSocket(msg, s, head) {
2085
s.on('close', function () { console.log("Server closed the connection."); process.exit(1); return; });
2086
2087
// Perform inner server authentication
2062
- //settings.serverAuthClientNonce = require('EncryptionStream').GenerateRandom(48).toString('base64');
2063
- //s.write("{\"action\":\"serverAuth\",\"cnonce\":\"" + settings.serverAuthClientNonce + "\",\"tlshash\":\"" + settings.meshServerTlsHash + "\"}"); // Ask for server authentication
2088
+ if (settings.serverid != null) {
2089
+ settings.serverAuthClientNonce = require('EncryptionStream').GenerateRandom(48).toString('base64');
2090
+ s.write("{\"action\":\"serverAuth\",\"cnonce\":\"" + settings.serverAuthClientNonce + "\",\"tlshash\":\"" + settings.meshServerTlsHash + "\"}"); // Ask for server authentication
2091
+ }
2092
}
2093
2094
function startRouterEx() {
meshuser.js
+2
-20
@@ -102,8 +102,8 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
102
function cleanRemoteAddr(addr) { if (addr.startsWith('::ffff:')) { return addr.substring(7); } else { return addr; } }
103
104
// Send a PING/PONG message
105
- function sendPing() { obj.ws.send('{"action":"ping"}'); }
106
- function sendPong() { obj.ws.send('{"action":"pong"}'); }
105
+ function sendPing() { try { obj.ws.send('{"action":"ping"}'); } catch (ex) { } }
106
+ function sendPong() { try { obj.ws.send('{"action":"pong"}'); } catch (ex) { } }
107
108
// Setup the agent PING/PONG timers
109
if ((typeof args.browserping == 'number') && (obj.pingtimer == null)) { obj.pingtimer = setInterval(sendPing, args.browserping * 1000); }
@@ -5544,24 +5544,6 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
5544
//console.log(command, file);
5545
break;
5546
}
5547
- case 'serverAuth': { // This command is used to perform server "inner" authentication.
5548
- if (common.validateString(command.cnonce, 1, 256) == false) break; // Check the client nonce
5549
- if (common.validateString(command.tlshash, 1, 512) == false) break; // Check the TLS hash
5550
-
5551
- // Check that the TLS hash is an acceptable one.
5552
- var h = Buffer.from(command.tlshash, 'hex').toString('binary');
5553
- if ((parent.webCertificateHashs[domain.id] != h) && (parent.webCertificateFullHashs[domain.id] != h) && (parent.defaultWebCertificateHash != h) && (parent.defaultWebCertificateFullHash != h)) { obj.close(); return; }
5554
-
5555
- // TLS hash check is a success, sign the request.
5556
- // Perform the hash signature using the server agent certificate
5557
- var nonce = parent.crypto.randomBytes(48);
5558
- var signData = Buffer.from(command.cnonce, 'base64').toString('binary') + h + nonce.toString('binary'); // Client Nonce + TLS Hash + Server Nonce
5559
- parent.parent.certificateOperations.acceleratorPerformSignature(0, signData, null, function (tag, signature) {
5560
- // Send back our certificate + nonce + signature
5561
- ws.send(JSON.stringify({ 'action': 'serverAuth', 'cert': Buffer.from(parent.agentCertificateAsn1, 'binary').toString('base64'), 'nonce': nonce.toString('base64'), 'signature': Buffer.from(signature,'binary').toString('base64') }));
5562
- });
5563
- break;
5564
- }
5547
default: {
5548
// Unknown user action
5549
console.log('Unknown action from user ' + user.name + ': ' + command.action + '.');
webserver.js
+131
-3
@@ -4585,7 +4585,6 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
4585
res.send(JSON.stringify(meshaction, null, ' '));
4586
return;
4587
} else if (req.query.meshaction == 'winrouter') {
4588
- console.log('t2');
4588
var p = obj.path.join(__dirname, 'agents', 'MeshCentralRouter.exe');
4589
if (obj.fs.existsSync(p)) {
4590
setContentDispositionHeader(res, 'application/octet-stream', 'MeshCentralRouter.exe', null, 'MeshCentralRouter.exe');
@@ -5071,7 +5070,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
5070
name: 'xid', // Recommended security practice to not use the default cookie name
5071
httpOnly: true,
5072
keys: [obj.args.sessionkey], // If multiple instances of this server are behind a load-balancer, this secret must be the same for all instances
5074
- secure: (obj.args.tlsoffload == null) // Use this cookie only over TLS (Check this: https://expressjs.com/en/guide/behind-proxies.html)
5073
+ secure: true // Use this cookie only over TLS (Check this: https://expressjs.com/en/guide/behind-proxies.html)
5074
}
5075
if (obj.args.sessionsamesite != null) { sessionOptions.sameSite = obj.args.sessionsamesite; } else { sessionOptions.sameSite = 'strict'; }
5076
if (obj.args.sessiontime != null) { sessionOptions.maxAge = (obj.args.sessiontime * 60 * 1000); }
@@ -5262,7 +5261,13 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
5261
obj.app.ws(url + 'control.ashx', function (ws, req) {
5262
const domain = getDomain(req);
5263
if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { ws.close(); return; } // Check 3FA URL key
5265
- PerformWSSessionAuth(ws, req, false, function (ws1, req1, domain, user, cookie) { obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws1, req1, obj.args, domain, user); });
5264
+ PerformWSSessionAuth(ws, req, true, function (ws1, req1, domain, user, cookie) {
5265
+ if (user == null) { // User is not authenticated, perform inner server authentication
5266
+ PerformWSSessionInnerAuth(ws, req, domain, function (ws1, req1, domain, user) { obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws1, req1, obj.args, domain, user); }); // User is authenticated
5267
+ } else {
5268
+ obj.meshUserHandler.CreateMeshUser(obj, obj.db, ws1, req1, obj.args, domain, user); // User is authenticated
5269
+ }
5270
+ });
5271
});
5272
obj.app.ws(url + 'devicefile.ashx', function (ws, req) { obj.meshDeviceFileHandler.CreateMeshDeviceFile(obj, ws, null, req, domain); });
5273
obj.app.get(url + 'devicefile.ashx', handleDeviceFile);
@@ -5735,6 +5740,129 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
5740
if (obj.args.agentport) { CheckListenPort(obj.args.agentport, obj.args.agentportbind, StartAltWebServer); }
5741
}
5742
5743
+ // Perform server inner authentication
5744
+ // This is a type of server authentication where the client will open the socket regardless of the TLS certificate and request that the server
5745
+ // sign a client nonce with the server agent cert and return the response. Only after that will the client send the client authentication username
5746
+ // and password or authentication cookie.
5747
+ function PerformWSSessionInnerAuth(ws, req, domain, func) {
5748
+ // When data is received from the web socket
5749
+ ws.on('message', function (data) {
5750
+ var command;
5751
+ try { command = JSON.parse(data.toString('utf8')); } catch (e) { return; }
5752
+ if (obj.common.validateString(command.action, 3, 32) == false) return; // Action must be a string between 3 and 32 chars
5753
+
5754
+ switch (command.action) {
5755
+ case 'serverAuth': { // This command is used to perform server "inner" authentication.
5756
+ if (obj.common.validateString(command.cnonce, 1, 256) == false) break; // Check the client nonce
5757
+ if (obj.common.validateString(command.tlshash, 1, 512) == false) break; // Check the TLS hash
5758
+
5759
+ // Check that the TLS hash is an acceptable one.
5760
+ var h = Buffer.from(command.tlshash, 'hex').toString('binary');
5761
+ if ((obj.webCertificateHashs[domain.id] != h) && (obj.webCertificateFullHashs[domain.id] != h) && (obj.defaultWebCertificateHash != h) && (obj.defaultWebCertificateFullHash != h)) { try { ws.close(); } catch (ex) { } return; }
5762
+
5763
+ // TLS hash check is a success, sign the request.
5764
+ // Perform the hash signature using the server agent certificate
5765
+ var nonce = obj.crypto.randomBytes(48);
5766
+ var signData = Buffer.from(command.cnonce, 'base64').toString('binary') + h + nonce.toString('binary'); // Client Nonce + TLS Hash + Server Nonce
5767
+ parent.certificateOperations.acceleratorPerformSignature(0, signData, null, function (tag, signature) {
5768
+ // Send back our certificate + nonce + signature
5769
+ ws.send(JSON.stringify({ 'action': 'serverAuth', 'cert': Buffer.from(obj.agentCertificateAsn1, 'binary').toString('base64'), 'nonce': nonce.toString('base64'), 'signature': Buffer.from(signature, 'binary').toString('base64') }));
5770
+ });
5771
+ break;
5772
+ }
5773
+ case 'userAuth': { // This command is used to perform user authentication.
5774
+ // Check username and password authentication
5775
+ if ((typeof command.username == 'string') && (typeof command.password == 'string')) {
5776
+ obj.authenticate(Buffer.from(command.username, 'base64').toString(), Buffer.from(command.password, 'base64').toString(), domain, function (err, userid) {
5777
+ var user = obj.users[userid];
5778
+ if ((err == null) && (user)) {
5779
+ // Check if a 2nd factor is needed
5780
+ var emailcheck = ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap'))
5781
+ if (checkUserOneTimePasswordRequired(domain, user, req) == true) {
5782
+ // Figure out if email 2FA is allowed
5783
+ var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
5784
+ var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
5785
+ if ((typeof req.query.token != 'string') || (req.query.token == '**email**') || (req.query.token == '**sms**')) {
5786
+ if ((req.query.token == '**email**') && (email2fa == true)) {
5787
+ // Cause a token to be sent to the user's registered email
5788
+ user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() };
5789
+ obj.db.SetUser(user);
5790
+ parent.debug('web', 'Sending 2FA email to: ' + user.email);
5791
+ domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key);
5792
+ // Ask for a login token & confirm email was sent
5793
+ try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, email2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
5794
+ } else if ((req.query.token == '**sms**') && (sms2fa == true)) {
5795
+ // Cause a token to be sent to the user's phone number
5796
+ user.otpsms = { k: obj.common.zeroPad(getRandomSixDigitInteger(), 6), d: Date.now() };
5797
+ obj.db.SetUser(user);
5798
+ parent.debug('web', 'Sending 2FA SMS to: ' + user.phone);
5799
+ parent.smsserver.sendToken(domain, user.phone, user.otpsms.k, obj.getLanguageCodes(req));
5800
+ // Ask for a login token & confirm sms was sent
5801
+ try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, sms2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
5802
+ } else {
5803
+ // Ask for a login token
5804
+ parent.debug('web', 'Asking for login token');
5805
+ try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
5806
+ }
5807
+ } else {
5808
+ checkUserOneTimePassword(req, domain, user, req.query.token, null, function (result) {
5809
+ if (result == false) {
5810
+ // Failed, ask for a login token again
5811
+ parent.debug('web', 'Invalid login token, asking again');
5812
+ try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { }
5813
+ } else {
5814
+ // We are authenticated with 2nd factor.
5815
+ // Check email verification
5816
+ if (emailcheck && (user.email != null) && (user.emailVerified !== true)) {
5817
+ parent.debug('web', 'Invalid login, asking for email validation');
5818
+ try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, sms2fa: sms2fa, email2fasent: true })); ws.close(); } catch (e) { }
5819
+ } else {
5820
+ // We are authenticated
5821
+ ws._socket.pause();
5822
+ ws.removeAllListeners(['message', 'close', 'error']);
5823
+ func(ws, req, domain, user);
5824
+ }
5825
+ }
5826
+ });
5827
+ }
5828
+ } else {
5829
+ // Check email verification
5830
+ if (emailcheck && (user.email != null) && (user.emailVerified !== true)) {
5831
+ parent.debug('web', 'Invalid login, asking for email validation');
5832
+ var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null));
5833
+ var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null));
5834
+ try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, sms2fa: sms2fa, email2fasent: true })); ws.close(); } catch (e) { }
5835
+ } else {
5836
+ // We are authenticated
5837
+ ws._socket.pause();
5838
+ ws.removeAllListeners(['message', 'close', 'error']);
5839
+ func(ws, req, domain, user);
5840
+ }
5841
+ }
5842
+
5843
+ }
5844
+ });
5845
+ } else {
5846
+ // Invalid authentication
5847
+ try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'noauth-2c' })); } catch (ex) { }
5848
+ try { ws.close(); } catch (ex) { }
5849
+ }
5850
+ break;
5851
+ }
5852
+ }
5853
+
5854
+ });
5855
+
5856
+ // If error, do nothing
5857
+ ws.on('error', function (err) { try { ws.close(); } catch (e) { console.log(e); } });
5858
+
5859
+ // If the web socket is closed
5860
+ ws.on('close', function (req) { try { ws.close(); } catch (e) { console.log(e); } });
5861
+
5862
+ // Resume the socket to perform inner authentication
5863
+ try { ws._socket.resume(); } catch (ex) { }
5864
+ }
5865
+
5866
// Authenticates a session and forwards
5867
function PerformWSSessionAuth(ws, req, noAuthOk, func) {
5868
// Check if this is a banned ip address