Merge pull request #1119 from AStove/Handshake

Use new authentication methods in weechat 2.9
codeql
Lorenz Hübschle-Schneider 5 years ago committed by GitHub
commit faccb8378e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      css/glowingbear.css
  2. 46
      index.html
  3. 206
      js/connection.js
  4. 10
      js/glowingbear.js
  5. 37
      js/utils.js
  6. 65
      js/weechat.js

@ -953,6 +953,10 @@ code {
border: 1pt solid #444;
}
.checkbox.indent {
margin-left: 20px;
}
#bufferlines.hideTime td.time {
display:none;
}

@ -71,6 +71,12 @@
<div class="alert alert-danger" ng-show="securityError" ng-cloak>
<strong>Secure connection error</strong> 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.
</div>
<div class="alert alert-danger" ng-show="oldWeechatError" ng-cloak>
<strong>Weechat version error</strong> Weechat connected but did not respond to a handshake. This could mean weechat < version 2.9. Verify your weechat is 2.8 or older and check "Compatibility with Weechat 2.8 and older" or consider updating weechat.
</div>
<div class="alert alert-danger" ng-show="hashAlgorithmDisagree" ng-cloak>
<strong>Hash algorithm error</strong> Weechat and glowing bear did not agree on a hashing algorithm, please do /set relay.network.password_hash_algo "pbkdf2+sha512" in weechat.
</div>
<div class="panel-group accordion">
<div class="panel" data-state="active" ng-show=false>
<div class="panel-heading">
@ -116,39 +122,42 @@
</div>
</div>
<div class="row no-gutter">
<div ng-class="settings.useTotp ? 'col-sm-9' : 'col-sm-12'" ng>
<div class="col-sm-12">
<label class="control-label" for="password">WeeChat relay password</label>
<input type="password" class="form-control favorite-font" id="password" ng-model="password" placeholder="Password">
</div>
<div class="col-sm-3" ng-Show="settings.useTotp">
<label class="control-label" for="totp">Token</label>
<input type="text" class="form-control favorite-font" id="totp" ng-model="totp" ng-change="parseTotp()" ng-class="{'is-invalid': totpInvalid}" autocomplete="off">
</div>
</div>
<div class="alert alert-danger" ng-show="passwordError" ng-cloak>
Error: wrong password or token
</div>
<div class="checkbox">
<label class="control-label" for="savepassword">
<input type="checkbox" id="savepassword" ng-model="settings.savepassword">
Save password in your browser
<label class="control-label" for="ssl">
<input type="checkbox" id="ssl" ng-model="settings.ssl">
Encryption. <strong>Strongly recommended!</strong> Need help? Check below.
</label>
</div>
<div class="checkbox">
<label class="control-label" for="compatibilityWeechat28">
<input type="checkbox" id="compatibilityWeechat28" ng-model="settings.compatibilityWeechat28">
Compatibility with Weechat 2.8 and older <a href="#plaintext" ng-click="toggleAccordionByName('gettingStartedAccordion')"><i class="glyphicon glyphicon-info-sign"></i></a>
<span style="color: #888;display:block">WeeChat 2.9 is scheduled for release in July 2020, so you&apos;ll likely want to keep this enabled for now.</span>
</label>
</div>
<div class="checkbox indent" ng-show="settings.compatibilityWeechat28">
<label class="control-label" for="useTotp">
<input type="checkbox" id="useTotp" ng-model="settings.useTotp">
Use Time-based One-Time Password <a href="https://blog.weechat.org/post/2019/01/14/Support-of-TOTP" target="_blank"><i class="glyphicon glyphicon-info-sign"></i></a>
Use Time-based One-Time Password (automatic for Weechat >= 2.9)<a href="https://blog.weechat.org/post/2019/01/14/Support-of-TOTP" target="_blank"><i class="glyphicon glyphicon-info-sign"></i></a>
</label>
</div>
<div class="checkbox">
<label class="control-label" for="ssl">
<input type="checkbox" id="ssl" ng-model="settings.ssl">
Encryption. <strong>Strongly recommended!</strong> Need help? Check below.
<label class="control-label" for="savepassword">
<input type="checkbox" id="savepassword" ng-model="settings.savepassword">
Save password in your browser
</label>
</div>
<div class="checkbox" ng-show="settings.savepassword || settings.autoconnect">
<label class="control-label" for="autoconnect">
<input type="checkbox" id="autoconnect" ng-model="settings.autoconnect" ng-disabled="settings.useTotp">
<input type="checkbox" id="autoconnect" ng-model="settings.autoconnect" ng-disabled="settings.compatibilityWeechat28 && settings.useTotp">
Automatically connect
</label>
</div>
@ -158,7 +167,7 @@
</div>
</div>
</div>
<div class="panel" data-state="collapsed">
<div class="panel" data-state="collapsed" id="gettingStartedAccordion">
<div class="panel-heading">
<h4 class="panel-title">
<a class="accordion-toggle" ng-click="toggleAccordion($event)">
@ -168,8 +177,8 @@
</div>
<div id="collapseTwo" class="panel-collapse collapse">
<div class="panel-body">
<p><span class="label label-danger">WeeChat version 0.4.2 or higher is required, but WeeChat 2.9 or later is recommended for the best experience.</p>
<h3>Use TLS encryption</h3>
<p><span class="label label-danger">WeeChat version 0.4.2 or higher is required—we recommend at least 1.0.</p>
<p>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.</p>
<div class="alert alert-warning" ng-show="show_tls_warning"><strong>You're using Glowing Bear over an unencrypted connection (http://). This is not recommended!</strong> We recommend using our secure hosted version at <a href="https://www.glowing-bear.org/">https://www.glowing-bear.org/</a>, or <a href="https://latest.glowing-bear.org/">https://latest.glowing-bear.org</a> for the latest and greatest development version. You can still follow the instructions below to set up an encrypted relay, though.</div>
<p>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 <a href="https://letsencrypt.org/">Let's Encrypt</a>—it's free and easy. We'll walk you through it.</p>
@ -191,6 +200,13 @@ chown -R <strong>username</strong>:<strong>username</strong> ~<strong>username</
<pre>/secure set relay_totp_secret xxxxx
/set relay.network.totp_secret "${sec.data.relay_totp_secret}"</pre>
<p>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.</p>
<h3><a name="plaintext"></a>Compatibility with WeeChat 2.8 and older</h3>
<p><strong>Required for WeeChat <= 2.8</strong></p>
<p>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!</p>
<p>By default, WeeChat 2.9 support several authentication methods. Of these, Glowing Bear only uses the most secure one, <code>pbkdf2+sha512</code>. You can check the list of enabled methods to ensure it is in there:
<code>/set relay.network.password_hash_algo</code>
</p>
</div>
</div>
</div>

@ -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,14 +276,65 @@ 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 (weechatAssumedPre2_9) {
return;
}
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;
// 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) {
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.');
}
}
).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);
// Connection is successful
// Send all the other commands required for initialization
_requestBufferInfos().then(function(bufinfo) {
handlers.handleBufferInfo(bufinfo);
@ -211,7 +358,6 @@ weechat.factory('connection',
}, 60000); // Sync hotlist every 60 second
}
// Fetch weechat time format for displaying timestamps
fetchConfValue('weechat.look.buffer_time_format',
function() {
@ -229,22 +375,15 @@ weechat.factory('connection',
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 +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) {

@ -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

@ -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
};
});

@ -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.
*

Loading…
Cancel
Save