diff --git a/css/glowingbear.css b/css/glowingbear.css index 62a10fb..1952962 100644 --- a/css/glowingbear.css +++ b/css/glowingbear.css @@ -960,3 +960,7 @@ code { color: #444; border: 1pt solid #444; } + +.checkbox.indent { + margin-left: 20px; +} \ No newline at end of file diff --git a/index.html b/index.html index 1928512..27098ec 100644 --- a/index.html +++ b/index.html @@ -68,6 +68,9 @@
Secure connection error Unable to connect to unencrypted relay when you are connecting to Glowing Bear over HTTPS. Please use an encrypted relay or load the page without using HTTPS.
+
+ Weechat version error Weechat connected but did not respond to a handshake. This could mean weechat < version 2.9. Verify your weechat version and check "Allow Plaintext Authentication" if it's < 2.9. +
@@ -113,39 +116,41 @@
-
+
-
- - -
Error: wrong password or token
-
+ +
+
-
-
+
@@ -155,7 +160,7 @@
-
+

@@ -188,6 +193,12 @@ chown -R username:username ~username/secure set relay_totp_secret xxxxx /set relay.network.totp_secret "${sec.data.relay_totp_secret}"

Open an authenticator app and create an entry with the same secret. In Glowing Bear check the checkbox for "use Time-based One-Time Password" and fill in the one time password as you see it in the authenticator app.

+ +

Allow plaintext authentication

+

Required for Weechat < 2.9

+

Since weechat version 2.9 the authentication was made more secure and resistant to brute forcing. Glowing bear uses the most secure authentication method by default. However to supports older version of weechat this options allows glowing bear to still send your password in plaintext (wrapped in https if enabled). Only enable this if you are using Weechat < 2.9

+

By default weechat 2.9 support all authentication methods, if you are only using glowing bear you can do the following command to improve security:

+
/relay.network.auth_password "pbkdf2+sha512"

diff --git a/js/connection.js b/js/connection.js index a8f813d..cedba21 100644 --- a/js/connection.js +++ b/js/connection.js @@ -4,12 +4,13 @@ var weechat = angular.module('weechat'); weechat.factory('connection', - ['$rootScope', '$log', 'handlers', 'models', 'settings', 'ngWebsockets', function($rootScope, + ['$rootScope', '$log', 'handlers', 'models', 'settings', 'ngWebsockets', 'utils', function($rootScope, $log, handlers, models, settings, - ngWebsockets) { + ngWebsockets, + utils) { var protocol = new weeChat.Protocol(); @@ -22,6 +23,7 @@ weechat.factory('connection', // Takes care of the connection and websocket hooks var connect = function (host, port, path, passwd, ssl, useTotp, totp, noCompression, successCallback, failCallback) { $rootScope.passwordError = false; + $rootScope.oldWeechatError = false; connectionData = [host, port, path, passwd, ssl, noCompression]; var proto = ssl ? 'wss' : 'ws'; // If host is an IPv6 literal wrap it in brackets @@ -31,31 +33,143 @@ weechat.factory('connection', var url = proto + "://" + host + ":" + port + "/" + path; $log.debug('Connecting to URL: ', url); + + var weechatIsPre2_9 = false; var onopen = function () { + var _performHandshake = function() { + return new Promise(function(resolve) { + + // First a handshake is sent to determine authentication method + // This is only supported for weechat >= 2.9 + // If after 'a while' weechat does not respond + // stop waiting for the handshake and assume it's an old version + // This time is debatable, high latency connections may wrongfully + // think weechat is an older version. This time is purposfully set + // too high, this time should be reduced if determined the weechat + // is lower than 2.9 + // This time also includes the time it takes to generate the hash + const WAIT_TIME_OLD_WEECHAT = 2000; //ms + + // Wait long enough to assume we are on a version < 2.9 + var handShakeTimeout = setTimeout(function () { + weechatIsPre2_9 = true; + console.log('Weechat\'s version is assumed to be < 2.9'); + resolve(); + }, WAIT_TIME_OLD_WEECHAT); + + // Or wait for a response from the handshake + ngWebsockets.send( + weeChat.Protocol.formatHandshake({ + password: "pbkdf2+sha512", compression: noCompression ? 'off' : 'zlib' + }) + ).then(function (message){ + clearTimeout(handShakeTimeout); + resolve(message); + }); + + + + }); + + } + + var _askTotp = function (useTotp) { + return new Promise(function(resolve) { + + // If weechat is < 2.9 the totp will be a setting (checkbox) + // Otherwise the handshake will specify it + if ( useTotp ) { + // Ask the user to input his TOTP + var totp = prompt("Please enter your TOTP Token"); + resolve (totp); + } else { + // User does not use TOTP, don't ask + resolve(null); + } + + }) + } // Helper methods for initialization commands - var _initializeConnection = function(passwd) { + // This method is used to initialize weechat < 2.9 + var _initializeConnectionPre29 = function(passwd, totp) { + + // This is not secure, this has to be specifically allowed with a setting + // Otherwise an attacker could persuade the client to send it's password + // Or due to latency the client could think weechat was an older version + if (!settings.allowPlaintextAuthentication) + { + $rootScope.oldWeechatError = true; + $rootScope.$emit('relayDisconnect'); + $rootScope.$digest() // Have to do this otherwise change detection doesn't see the error. + throw new Error('Plainttext authentication not allowed.'); + } + // Escape comma in password (#937) passwd = passwd.replace(',', '\\,'); - // This is not the proper way to do this. - // WeeChat does not send a confirmation for the init. - // Until it does, We need to "assume" that formatInit - // will be received before formatInfo + ngWebsockets.send( - weeChat.Protocol.formatInit({ + weeChat.Protocol.formatInitPre29({ password: passwd, compression: noCompression ? 'off' : 'zlib', - useTotp: useTotp, totp: totp }) ); - return ngWebsockets.send( - weeChat.Protocol.formatInfo({ - name: 'version' + // Wait a little bit until the init is sent + return new Promise(function(resolve) { + setTimeout(() => resolve(), 5); + }) + + }; + + // Helper methods for initialization commands + // This method is used to initialize weechat >= 2.9 + var salt; + var _initializeConnection29 = function(passwd, nonce, iterations, totp) { + + return window.crypto.subtle.importKey( + + 'raw', + utils.stringToUTF8Array(passwd), + {name: 'PBKDF2'},//{name: 'HMAC', hash: 'SHA-512'}, + false, + ['deriveBits'] + + ).then( function (key) { + + salt = utils.concatenateTypedArray(utils.concatenateTypedArray(nonce, new Uint8Array([0x3A])), window.crypto.getRandomValues(new Uint8Array(16))); //nonce:cnonce, 3A is a ':' in ASCII + return window.crypto.subtle.deriveBits( + { + name: 'PBKDF2', + hash: 'SHA-512', + salt: salt, + iterations: iterations, + }, + key, //your key from generateKey or importKey + 512 + ); + + }).then( function (hash) { + + + ngWebsockets.send( + weeChat.Protocol.formatInit29( + 'pbkdf2+sha512:' + utils.bytetoHexString(salt) + ':100000:' + utils.bytetoHexString(hash), + totp + ) + ); + + // Wait a little bit until the init is sent + return new Promise(function(resolve) { + + setTimeout(() => resolve(), 5); + }) - ); + + }); + }; var _requestHotlist = function() { @@ -180,71 +294,124 @@ weechat.factory('connection', $rootScope.angularTimeFormat = angularFormat; }; + var passwordMethod + var totpRequested; + var nonce; + var iterations; + + _performHandshake().then( + + //Wait for weechat to respond or handshake times out + function (message) + { + // Do nothing if the handshake was received + // after concluding weechat was an old version + // TODO maybe warn the user here + if(weechatIsPre2_9) { + return; + } - // First command asks for the password and issues - // a version command. If it fails, it means the we - // did not provide the proper password. - _initializeConnection(passwd).then( - function(version) { - handlers.handleVersionInfo(version); - // Connection is successful - // Send all the other commands required for initialization - _requestBufferInfos().then(function(bufinfo) { - handlers.handleBufferInfo(bufinfo); - }); + passwordMethod = message.objects[0].content.auth_password; + totpRequested = message.objects[0].content.totp === 'on' ? true : false; + nonce = utils.hexStringToByte(message.objects[0].content.nonce); + iterations = message.objects[0].content.hash_iterations; + + } - _requestHotlist().then(function(hotlist) { - handlers.handleHotlistInfo(hotlist); + ).then( function() { - }); - if (settings.hotlistsync) { - // Schedule hotlist syncing every so often so that this - // client will have unread counts (mostly) in sync with - // other clients or terminal usage directly. - setInterval(function() { - if ($rootScope.connected) { - _requestHotlist().then(function(hotlist) { - handlers.handleHotlistInfo(hotlist); - - }); - } - }, 60000); // Sync hotlist every 60 second - } + if(weechatIsPre2_9) + { + // Ask the user for the TOTP token if this is enabled + return _askTotp(useTotp) + .then( function (totp) { + return _initializeConnectionPre29(passwd, totp) + }) - // Fetch weechat time format for displaying timestamps - fetchConfValue('weechat.look.buffer_time_format', - function() { - // Will set models.wconfig['weechat.look.buffer_time_format'] - _parseWeechatTimeFormat(); - }); + } else { + + // Weechat version >= 2.9 + return _askTotp(totpRequested) + .then( function(totp) { + return _initializeConnection29(passwd, nonce, iterations, totp) + }) - // Fetch nick completion config - fetchConfValue('weechat.completion.nick_completer'); - fetchConfValue('weechat.completion.nick_add_space'); + } - _requestSync(); - $log.info("Connected to relay"); - $rootScope.connected = true; - if (successCallback) { - successCallback(); - } - }, - function() { - handleWrongPassword(); + }).then( function(){ + + // The Init was sent, weechat will not respond + // Wait until either the connection closes + // Or try to send version and see if weechat responds + return ngWebsockets.send( + weeChat.Protocol.formatInfo({ + name: 'version' + }) + ); + + }).then( function(version) { + + // From now on we are assumed initialized + // We don't know for sure because weechat does not respond + // All we know is the socket wasn't closed afer waiting a little bit + console.log('Succesfully connected'); + $rootScope.waseverconnected = true; + handlers.handleVersionInfo(version); + + // Send all the other commands required for initialization + _requestBufferInfos().then(function(bufinfo) { + handlers.handleBufferInfo(bufinfo); + }); + + _requestHotlist().then(function(hotlist) { + handlers.handleHotlistInfo(hotlist); + + }); + if (settings.hotlistsync) { + // Schedule hotlist syncing every so often so that this + // client will have unread counts (mostly) in sync with + // other clients or terminal usage directly. + setInterval(function() { + if ($rootScope.connected) { + _requestHotlist().then(function(hotlist) { + handlers.handleHotlistInfo(hotlist); + + }); + } + }, 60000); // Sync hotlist every 60 second } - ); + // Fetch weechat time format for displaying timestamps + fetchConfValue('weechat.look.buffer_time_format', + function() { + // Will set models.wconfig['weechat.look.buffer_time_format'] + _parseWeechatTimeFormat(); + }); + + // Fetch nick completion config + fetchConfValue('weechat.completion.nick_completer'); + fetchConfValue('weechat.completion.nick_add_space'); + + _requestSync(); + $log.info("Connected to relay"); + $rootScope.connected = true; + if (successCallback) { + successCallback(); + } + + }, + + //Sending version failed + function() { + handleWrongPassword(); + }); }; var onmessage = function() { - // If we recieve a message from WeeChat it means that - // password was OK. Store that result and check for it - // in the failure handler. - $rootScope.waseverconnected = true; + }; - var onclose = function (evt) { /* * Handles websocket disconnection @@ -274,7 +441,7 @@ weechat.factory('connection', var handleWrongPassword = function() { // Connection got closed, lets check if we ever was connected successfully - if (!$rootScope.waseverconnected && !$rootScope.errorMessage) { + if (!$rootScope.waseverconnected && !$rootScope.errorMessage && !$rootScope.oldWeechatError) { $rootScope.passwordError = true; $rootScope.$apply(); } diff --git a/js/glowingbear.js b/js/glowingbear.js index 04519bb..1e8c177 100644 --- a/js/glowingbear.js +++ b/js/glowingbear.js @@ -45,6 +45,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', 'port': 9001, 'path': 'weechat', 'ssl': (window.location.protocol === "https:"), + 'allowPlaintextAuthentication': true, 'useTotp': false, 'savepassword': false, 'autoconnect': false, @@ -773,6 +774,17 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', event.preventDefault(); var target = event.target.parentNode.parentNode.parentNode; + toggleAccordionByTarget(target); + }; + + $scope.toggleAccordionByName = function(name) { + + var target = document.getElementById(name);; + toggleAccordionByTarget(target); + }; + + var toggleAccordionByTarget = function(target) { + target.setAttribute('data-state', target.getAttribute('data-state') === 'active' ? 'collapsed' : 'active'); // Hide all other siblings diff --git a/js/utils.js b/js/utils.js index 854372b..4c1ef15 100644 --- a/js/utils.js +++ b/js/utils.js @@ -45,6 +45,40 @@ weechat.factory('utils', function() { head.appendChild(elem); }; + // Convert string to ByteArray + function hexStringToByte(str) { + if (!str) { + return new Uint8Array(); + } + + var a = []; + for (var i = 0, len = str.length; i < len; i+=2) { + a.push(parseInt(str.substr(i,2),16)); + } + + return new Uint8Array(a); + } + + function bytetoHexString(buffer) { + return Array + .from (new Uint8Array (buffer)) + .map (b => b.toString (16).padStart (2, "0")) + .join (""); + } + + function stringToUTF8Array(string) { + const encoder = new TextEncoder() + const view = encoder.encode(string) + return view; + } + + function concatenateTypedArray(a, b) { // a, b TypedArray of same type + var c = new (a.constructor)(a.length + b.length); + c.set(a, 0); + c.set(b, a.length); + return c; + } + return { changeClassStyle: changeClassStyle, @@ -53,5 +87,9 @@ weechat.factory('utils', function() { isCordova: isCordova, inject_script: inject_script, inject_css: inject_css, + hexStringToByte: hexStringToByte, + bytetoHexString: bytetoHexString, + stringToUTF8Array: stringToUTF8Array, + concatenateTypedArray: concatenateTypedArray }; }); diff --git a/js/weechat.js b/js/weechat.js index f46e3ef..79e70de 100644 --- a/js/weechat.js +++ b/js/weechat.js @@ -628,17 +628,51 @@ }; /** - * Formats an init command. + * Formats a handshake command. + * + * @param params Parameters: + * password: list of supported hash algorithems, colon separated (optional) + * compression: compression ('off' or 'zlib') (optional) + * @return Formatted handshake command string + */ + //https://weechat.org/files/doc/stable/weechat_relay_protocol.en.html#command_handshake + WeeChatProtocol.formatHandshake = function(params) { + var defaultParams = { + password: 'pbkdf2+sha512', + compression: 'zlib' + }; + var keys = []; + var parts = []; + + params = WeeChatProtocol._mergeParams(defaultParams, params); + + if (params.password !== null) { + keys.push('compression=' + params.compression); + } + + if (params.password !== null) { + keys.push('password=' + params.password); + } + + parts.push(keys.join(',')); + + return WeeChatProtocol._formatCmd(null, 'handshake', parts); + }; + + /** + * Formats an init command for weechat versions < 2.9 * * @param params Parameters: * password: password (optional) * compression: compression ('off' or 'zlib') (optional) + * totp: One Time Password (optional) * @return Formatted init command string */ - WeeChatProtocol.formatInit = function(params) { + WeeChatProtocol.formatInitPre29 = function(params) { var defaultParams = { password: null, - compression: 'zlib' + compression: 'zlib', + totp: null }; var keys = []; var parts = []; @@ -648,7 +682,7 @@ if (params.password !== null) { keys.push('password=' + params.password); } - if (params.useTotp) { + if (params.totp !== null) { keys.push('totp=' + params.totp); } parts.push(keys.join(',')); @@ -656,6 +690,30 @@ return WeeChatProtocol._formatCmd(null, 'init', parts); }; + /** + * Formats an init command for weechat versions >= 2.9 + * + * @param params Parameters: + * password_hash: hash of password with method and salt + * totp: One Time Password (can be null) + * @return Formatted init command string + */ + WeeChatProtocol.formatInit29 = function(password_hash, totp) { + + var keys = []; + var parts = []; + + if (totp != null) { + keys.push('totp=' + totp); + } + if (password_hash !== null) { + keys.push('password_hash=' + password_hash); + } + parts.push(keys.join(',')); + + return WeeChatProtocol._formatCmd(null, 'init', parts); + }; + /** * Formats an hdata command. *