Use new authentication methods in weechat 2.9

codeql
Jeremy Mahieu 5 years ago
parent 574dd49c09
commit f03e011d1a
  1. 4
      css/glowingbear.css
  2. 41
      index.html
  3. 297
      js/connection.js
  4. 12
      js/glowingbear.js
  5. 38
      js/utils.js
  6. 66
      js/weechat.js

@ -960,3 +960,7 @@ code {
color: #444; color: #444;
border: 1pt solid #444; border: 1pt solid #444;
} }
.checkbox.indent {
margin-left: 20px;
}

@ -68,6 +68,9 @@
<div class="alert alert-danger" ng-show="securityError" ng-cloak> <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. <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>
<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 version and check "Allow Plaintext Authentication" if it's < 2.9.
</div>
<div class="panel-group accordion"> <div class="panel-group accordion">
<div class="panel" data-state="active" ng-show=false> <div class="panel" data-state="active" ng-show=false>
<div class="panel-heading"> <div class="panel-heading">
@ -113,39 +116,41 @@
</div> </div>
</div> </div>
<div class="row no-gutter"> <div class="row no-gutter">
<div ng-class="settings.useTotp ? 'col-sm-9' : 'col-sm-12'" ng> <div ng-class="col-sm-12" ng>
<label class="control-label" for="password">WeeChat relay password</label> <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"> <input type="password" class="form-control favorite-font" id="password" ng-model="password" placeholder="Password">
</div> </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>
<div class="alert alert-danger" ng-show="passwordError" ng-cloak> <div class="alert alert-danger" ng-show="passwordError" ng-cloak>
Error: wrong password or token Error: wrong password or token
</div> </div>
<div class="checkbox"> <div class="checkbox">
<label class="control-label" for="savepassword"> <label class="control-label" for="ssl">
<input type="checkbox" id="savepassword" ng-model="settings.savepassword"> <input type="checkbox" id="ssl" ng-model="settings.ssl">
Save password in your browser Encryption. <strong>Strongly recommended!</strong> Need help? Check below.
</label> </label>
</div> </div>
<div class="checkbox"> <div class="checkbox">
<label class="control-label" for="allowPlaintextAuthentication">
<input type="checkbox" id="allowPlaintextAuthentication" ng-model="settings.allowPlaintextAuthentication">
Allow Plaintext Authentication (Weechat < 2.9) <a href="#plaintext" ng-click="toggleAccordionByName('gettingStartedAccordion')"><i class="glyphicon glyphicon-info-sign"></i></a>
</label>
</div>
<div class="checkbox indent" ng-show="settings.allowPlaintextAuthentication">
<label class="control-label" for="useTotp"> <label class="control-label" for="useTotp">
<input type="checkbox" id="useTotp" ng-model="settings.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> </label>
</div> </div>
<div class="checkbox"> <div class="checkbox">
<label class="control-label" for="ssl"> <label class="control-label" for="savepassword">
<input type="checkbox" id="ssl" ng-model="settings.ssl"> <input type="checkbox" id="savepassword" ng-model="settings.savepassword">
Encryption. <strong>Strongly recommended!</strong> Need help? Check below. Save password in your browser
</label> </label>
</div> </div>
<div class="checkbox" ng-show="settings.savepassword || settings.autoconnect"> <div class="checkbox indent" ng-show="settings.savepassword || settings.autoconnect">
<label class="control-label" for="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.allowPlaintextAuthentication && settings.useTotp">
Automatically connect Automatically connect
</label> </label>
</div> </div>
@ -155,7 +160,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="panel" data-state="collapsed"> <div class="panel" data-state="collapsed" id="gettingStartedAccordion">
<div class="panel-heading"> <div class="panel-heading">
<h4 class="panel-title"> <h4 class="panel-title">
<a class="accordion-toggle" ng-click="toggleAccordion($event)"> <a class="accordion-toggle" ng-click="toggleAccordion($event)">
@ -188,6 +193,12 @@ chown -R <strong>username</strong>:<strong>username</strong> ~<strong>username</
<pre>/secure set relay_totp_secret xxxxx <pre>/secure set relay_totp_secret xxxxx
/set relay.network.totp_secret "${sec.data.relay_totp_secret}"</pre> /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> <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>Allow plaintext authentication</h3>
<p><strong>Required for Weechat < 2.9</strong></p>
<p>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</p>
<p>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:</p>
<pre>/relay.network.auth_password "pbkdf2+sha512"</pre>
</div> </div>
</div> </div>
</div> </div>

@ -4,12 +4,13 @@
var weechat = angular.module('weechat'); var weechat = angular.module('weechat');
weechat.factory('connection', weechat.factory('connection',
['$rootScope', '$log', 'handlers', 'models', 'settings', 'ngWebsockets', function($rootScope, ['$rootScope', '$log', 'handlers', 'models', 'settings', 'ngWebsockets', 'utils', function($rootScope,
$log, $log,
handlers, handlers,
models, models,
settings, settings,
ngWebsockets) { ngWebsockets,
utils) {
var protocol = new weeChat.Protocol(); var protocol = new weeChat.Protocol();
@ -22,6 +23,7 @@ weechat.factory('connection',
// Takes care of the connection and websocket hooks // Takes care of the connection and websocket hooks
var connect = function (host, port, path, passwd, ssl, useTotp, totp, noCompression, successCallback, failCallback) { var connect = function (host, port, path, passwd, ssl, useTotp, totp, noCompression, successCallback, failCallback) {
$rootScope.passwordError = false; $rootScope.passwordError = false;
$rootScope.oldWeechatError = false;
connectionData = [host, port, path, passwd, ssl, noCompression]; connectionData = [host, port, path, passwd, ssl, noCompression];
var proto = ssl ? 'wss' : 'ws'; var proto = ssl ? 'wss' : 'ws';
// If host is an IPv6 literal wrap it in brackets // If host is an IPv6 literal wrap it in brackets
@ -31,31 +33,143 @@ weechat.factory('connection',
var url = proto + "://" + host + ":" + port + "/" + path; var url = proto + "://" + host + ":" + port + "/" + path;
$log.debug('Connecting to URL: ', url); $log.debug('Connecting to URL: ', url);
var weechatIsPre2_9 = false;
var onopen = function () { 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 // 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) // Escape comma in password (#937)
passwd = passwd.replace(',', '\\,'); 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( ngWebsockets.send(
weeChat.Protocol.formatInit({ weeChat.Protocol.formatInitPre29({
password: passwd, password: passwd,
compression: noCompression ? 'off' : 'zlib', compression: noCompression ? 'off' : 'zlib',
useTotp: useTotp,
totp: totp totp: totp
}) })
); );
return ngWebsockets.send( // Wait a little bit until the init is sent
weeChat.Protocol.formatInfo({ return new Promise(function(resolve) {
name: 'version' 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() { var _requestHotlist = function() {
@ -180,70 +294,123 @@ weechat.factory('connection',
$rootScope.angularTimeFormat = angularFormat; $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 passwordMethod = message.objects[0].content.auth_password;
// a version command. If it fails, it means the we totpRequested = message.objects[0].content.totp === 'on' ? true : false;
// did not provide the proper password. nonce = utils.hexStringToByte(message.objects[0].content.nonce);
_initializeConnection(passwd).then( iterations = message.objects[0].content.hash_iterations;
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);
}); ).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();
});
// Fetch nick completion config } else {
fetchConfValue('weechat.completion.nick_completer');
fetchConfValue('weechat.completion.nick_add_space'); // Weechat version >= 2.9
return _askTotp(totpRequested)
.then( function(totp) {
return _initializeConnection29(passwd, nonce, iterations, totp)
})
_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() { 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) { var onclose = function (evt) {
/* /*
@ -274,7 +441,7 @@ weechat.factory('connection',
var handleWrongPassword = function() { var handleWrongPassword = function() {
// Connection got closed, lets check if we ever was connected successfully // 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.passwordError = true;
$rootScope.$apply(); $rootScope.$apply();
} }

@ -45,6 +45,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
'port': 9001, 'port': 9001,
'path': 'weechat', 'path': 'weechat',
'ssl': (window.location.protocol === "https:"), 'ssl': (window.location.protocol === "https:"),
'allowPlaintextAuthentication': true,
'useTotp': false, 'useTotp': false,
'savepassword': false, 'savepassword': false,
'autoconnect': false, 'autoconnect': false,
@ -773,6 +774,17 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
event.preventDefault(); event.preventDefault();
var target = event.target.parentNode.parentNode.parentNode; 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'); target.setAttribute('data-state', target.getAttribute('data-state') === 'active' ? 'collapsed' : 'active');
// Hide all other siblings // Hide all other siblings

@ -45,6 +45,40 @@ weechat.factory('utils', function() {
head.appendChild(elem); 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 { return {
changeClassStyle: changeClassStyle, changeClassStyle: changeClassStyle,
@ -53,5 +87,9 @@ weechat.factory('utils', function() {
isCordova: isCordova, isCordova: isCordova,
inject_script: inject_script, inject_script: inject_script,
inject_css: inject_css, inject_css: inject_css,
hexStringToByte: hexStringToByte,
bytetoHexString: bytetoHexString,
stringToUTF8Array: stringToUTF8Array,
concatenateTypedArray: concatenateTypedArray
}; };
}); });

@ -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: * @param params Parameters:
* password: password (optional) * password: password (optional)
* compression: compression ('off' or 'zlib') (optional) * compression: compression ('off' or 'zlib') (optional)
* totp: One Time Password (optional)
* @return Formatted init command string * @return Formatted init command string
*/ */
WeeChatProtocol.formatInit = function(params) { WeeChatProtocol.formatInitPre29 = function(params) {
var defaultParams = { var defaultParams = {
password: null, password: null,
compression: 'zlib' compression: 'zlib',
totp: null
}; };
var keys = []; var keys = [];
var parts = []; var parts = [];
@ -648,7 +682,7 @@
if (params.password !== null) { if (params.password !== null) {
keys.push('password=' + params.password); keys.push('password=' + params.password);
} }
if (params.useTotp) { if (params.totp !== null) {
keys.push('totp=' + params.totp); keys.push('totp=' + params.totp);
} }
parts.push(keys.join(',')); parts.push(keys.join(','));
@ -656,6 +690,30 @@
return WeeChatProtocol._formatCmd(null, 'init', parts); 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. * Formats an hdata command.
* *

Loading…
Cancel
Save