diff --git a/css/glowingbear.css b/css/glowingbear.css
index 1aac1f8..9c5c6ca 100644
--- a/css/glowingbear.css
+++ b/css/glowingbear.css
@@ -953,6 +953,10 @@ code {
border: 1pt solid #444;
}
+.checkbox.indent {
+ margin-left: 20px;
+}
+
#bufferlines.hideTime td.time {
display:none;
}
diff --git a/index.html b/index.html
index 0c664fc..86e8da5 100644
--- a/index.html
+++ b/index.html
@@ -71,6 +71,12 @@
+
+
WeeChat version 0.4.2 or higher is required, but WeeChat 2.9 or later is recommended for the best experience.
Use TLS encryption
-
WeeChat version 0.4.2 or higher is required—we recommend at least 1.0.
To start using Glowing Bear, follow the instructions below to set up an encrypted relay. All communication goes directly between your browser and your WeeChat relay! This means that your server must be accessible. We never see any of your data or your password, and you don't need to trust a "cloud". All settings, including your password, are saved locally in your own browser between sessions.
You're using Glowing Bear over an unencrypted connection (http://). This is not recommended! We recommend using our secure hosted version at
https://www.glowing-bear.org/, or
https://latest.glowing-bear.org for the latest and greatest development version. You can still follow the instructions below to set up an encrypted relay, though.
When using encryption, all communication between your browser and WeeChat will be securely encrypted with TLS. This means that you have to set up a certificate. While it's possible to use a self-signed cert, we recommend against it, because it's handled poorly in browsers, and may not work at all on mobile devices. If you don't already have a certificate for your domain (or you don't have a domain), we strongly encourage you to get a certificate from Let's Encrypt—it's free and easy. We'll walk you through it.
@@ -191,6 +200,13 @@ 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.
+
+ Compatibility with WeeChat 2.8 and older
+ Required for WeeChat <= 2.8
+ With WeeChat 2.9—scheduled for release in July 2020—relay client authentication was made more secure and resistant to brute forcing. Glowing Bear uses the most secure authentication method by default. However, to support older versions of WeeChat, this option allows Glowing Bear to still use the old authentication method, sending your password to WeeChat (in plain text if you are not using encryption!). Only enable this if you are using a WeeChat version before 2.9!
+ By default, WeeChat 2.9 support several authentication methods. Of these, Glowing Bear only uses the most secure one, pbkdf2+sha512
. You can check the list of enabled methods to ensure it is in there:
+ /set relay.network.password_hash_algo
+
diff --git a/js/connection.js b/js/connection.js
index 1d150ca..8a1e230 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,8 @@ 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;
+ $rootScope.hashAlgorithmDisagree = 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 +34,124 @@ weechat.factory('connection',
var url = proto + "://" + host + ":" + port + "/" + path;
$log.debug('Connecting to URL: ', url);
+
+ var weechatAssumedPre2_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 = 200; //ms
+
+ // Wait long enough to assume we are on a version < 2.9
+ var handShakeTimeout = setTimeout(function () {
+ weechatAssumedPre2_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_hash_algo: "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.compatibilityWeechat28) {
+ $rootScope.oldWeechatError = true;
+ $rootScope.$emit('relayDisconnect');
+ $rootScope.$digest(); // Have to do this otherwise change detection doesn't see the error.
+ throw new Error('Plaintext 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(function() { 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) {
+ var clientnonce = window.crypto.getRandomValues(new Uint8Array(16));
+ //nonce:clientnonce, 3A is a ':' in ASCII
+ salt = utils.concatenateTypedArrays(
+ nonce, new Uint8Array([0x3A]), clientnonce);
+ return window.crypto.subtle.deriveBits(
+ {
+ name: 'PBKDF2',
+ hash: 'SHA-512',
+ salt: salt,
+ iterations: iterations,
+ }, key, 512
+ );
+ }).then(function (hash) {
+ ngWebsockets.send(
+ weeChat.Protocol.formatInit29(
+ 'pbkdf2+sha512:' + utils.bytetoHexString(salt) + ':' +
+ iterations + ':' + utils.bytetoHexString(hash),
+ totp
+ )
+ );
+
+ // Wait a little bit until the init is sent
+ return new Promise(function(resolve) {
+ setTimeout(function() { resolve(); }, 5);
+ });
+ });
};
var _requestHotlist = function() {
@@ -180,71 +276,114 @@ weechat.factory('connection',
$rootScope.angularTimeFormat = angularFormat;
};
-
- // 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);
- });
-
- _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
+ 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 (weechatAssumedPre2_9) {
+ return;
}
-
- // 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();
+ var content = message.objects[0].content;
+ passwordMethod = content.password_hash_algo;
+ totpRequested = (content.totp === 'on');
+ nonce = utils.hexStringToByte(content.nonce);
+ iterations = content.password_hash_iterations;
+
+ if (passwordMethod != "pbkdf2+sha512") {
+ $rootScope.hashAlgorithmDisagree = true;
+ $rootScope.$emit('relayDisconnect');
+ $rootScope.$digest(); // Have to do this otherwise change detection doesn't see the error.
+ throw new Error('No supported password hash algorithm returned.');
}
- },
- function() {
- handleWrongPassword();
}
- );
+ ).then(function() {
+ if (weechatAssumedPre2_9) {
+ // Ask the user for the TOTP token if this is enabled
+ return _askTotp(useTotp)
+ .then(function (totp) {
+ return _initializeConnectionPre29(passwd, totp);
+ });
+ } else {
+ // Weechat version >= 2.9
+ return _askTotp(totpRequested)
+ .then(function(totp) {
+ return _initializeConnection29(passwd, nonce, iterations, totp);
+ });
+ }
+ }).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();
+ }
- 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;
+ },
+
+ //Sending version failed
+ function() {
+ handleWrongPassword();
+ });
};
-
var onclose = function (evt) {
/*
* Handles websocket disconnection
@@ -274,7 +413,9 @@ 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.hashAlgorithmDisagree)
+ {
$rootScope.passwordError = true;
$rootScope.$apply();
}
@@ -309,7 +450,6 @@ weechat.factory('connection',
'binaryType': "arraybuffer",
'onopen': onopen,
'onclose': onclose,
- 'onmessage': onmessage,
'onerror': onerror
});
} catch(e) {
diff --git a/js/glowingbear.js b/js/glowingbear.js
index 2888d4b..40bfe3a 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:"),
+ 'compatibilityWeechat28': true,
'useTotp': false,
'savepassword': false,
'autoconnect': false,
@@ -764,6 +765,15 @@ 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..2c03e7f 100644
--- a/js/utils.js
+++ b/js/utils.js
@@ -45,6 +45,39 @@ 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(function(b) { return b.toString(16).padStart(2, "0"); })
+ .join("");
+ }
+
+ function stringToUTF8Array(string) {
+ return new TextEncoder().encode(string);
+ }
+
+ // Concatenate three TypedArrays of the same type
+ function concatenateTypedArrays(a, b, c) {
+ var res = new (a.constructor)(a.length + b.length + c.length);
+ res.set(a, 0);
+ res.set(b, a.length);
+ res.set(c, a.length + b.length);
+ return res;
+ }
return {
changeClassStyle: changeClassStyle,
@@ -53,5 +86,9 @@ weechat.factory('utils', function() {
isCordova: isCordova,
inject_script: inject_script,
inject_css: inject_css,
+ hexStringToByte: hexStringToByte,
+ bytetoHexString: bytetoHexString,
+ stringToUTF8Array: stringToUTF8Array,
+ concatenateTypedArrays: concatenateTypedArrays
};
});
diff --git a/js/weechat.js b/js/weechat.js
index 2b99c25..2c33c6e 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 algorithms, 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_hash_algo: 'pbkdf2+sha512',
+ compression: 'zlib'
+ };
+ var keys = [];
+ var parts = [];
+
+ params = WeeChatProtocol._mergeParams(defaultParams, params);
+
+ if (params.compression !== null) {
+ keys.push('compression=' + params.compression);
+ }
+
+ if (params.password_hash_algo !== null) {
+ keys.push('password_hash_algo=' + params.password_hash_algo);
+ }
+
+ 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,29 @@
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.
*