]> jfr.im git - irc/weechat/qweechat.git/commitdiff
Add support of TOTP and password hash (WeeChat >= 2.9)
authorSébastien Helleu <redacted>
Sun, 14 Nov 2021 17:40:26 +0000 (18:40 +0100)
committerSébastien Helleu <redacted>
Sun, 14 Nov 2021 17:46:47 +0000 (18:46 +0100)
README.md
qweechat/connection.py
qweechat/data/icons/README
qweechat/data/icons/dialog-password.png [new file with mode: 0644]
qweechat/network.py
qweechat/preferences.py [new file with mode: 0644]
qweechat/qweechat.py

index b836ba305fc6bafb19353f1f105db775a2b17a65..9942a91ca3d3d971a51466d148e7827a11f68e52 100644 (file)
--- a/README.md
+++ b/README.md
@@ -45,6 +45,7 @@ In QWeeChat, click on connect and enter fields:
 - `server`: the IP address or hostname of your machine with WeeChat running
 - `port`: the relay port (defined in WeeChat)
 - `password`: the relay password (defined in WeeChat)
+- `totp`: the Time-Based One-Time Password (optional, to set if required by WeeChat)
 
 Options can be changed in file `~/.config/qweechat/qweechat.conf`.
 
index 9a5adbae47886ddef8b5c0f6d88f12e44f54d9cc..aad350a9c5a301c1354c9eaaca9f5f6496d754e9 100644 (file)
@@ -32,35 +32,91 @@ class ConnectionDialog(QtWidgets.QDialog):
         super().__init__(*args)
         self.values = values
         self.setModal(True)
+        self.setWindowTitle('Connect to WeeChat')
 
         grid = QtWidgets.QGridLayout()
         grid.setSpacing(10)
 
         self.fields = {}
-        for line, field in enumerate(('server', 'port', 'password', 'lines')):
-            grid.addWidget(QtWidgets.QLabel(field.capitalize()), line, 0)
-            line_edit = QtWidgets.QLineEdit()
-            line_edit.setFixedWidth(200)
-            if field == 'password':
-                line_edit.setEchoMode(QtWidgets.QLineEdit.Password)
-            if field == 'lines':
-                validator = QtGui.QIntValidator(0, 2147483647, self)
-                line_edit.setValidator(validator)
-                line_edit.setFixedWidth(80)
-            line_edit.insert(self.values[field])
-            grid.addWidget(line_edit, line, 1)
-            self.fields[field] = line_edit
-            if field == 'port':
-                ssl = QtWidgets.QCheckBox('SSL')
-                ssl.setChecked(self.values['ssl'] == 'on')
-                grid.addWidget(ssl, line, 2)
-                self.fields['ssl'] = ssl
+        focus = None
+
+        # server
+        grid.addWidget(QtWidgets.QLabel('<b>Server</b>'), 0, 0)
+        line_edit = QtWidgets.QLineEdit()
+        line_edit.setFixedWidth(200)
+        value = self.values.get('server', '')
+        line_edit.insert(value)
+        grid.addWidget(line_edit, 0, 1)
+        self.fields['server'] = line_edit
+        if not focus and not value:
+            focus = 'server'
+
+        # port / SSL
+        grid.addWidget(QtWidgets.QLabel('<b>Port</b>'), 1, 0)
+        line_edit = QtWidgets.QLineEdit()
+        line_edit.setFixedWidth(200)
+        value = self.values.get('port', '')
+        line_edit.insert(value)
+        grid.addWidget(line_edit, 1, 1)
+        self.fields['port'] = line_edit
+        if not focus and not value:
+            focus = 'port'
+
+        ssl = QtWidgets.QCheckBox('SSL')
+        ssl.setChecked(self.values['ssl'] == 'on')
+        grid.addWidget(ssl, 1, 2)
+        self.fields['ssl'] = ssl
+
+        # password
+        grid.addWidget(QtWidgets.QLabel('<b>Password</b>'), 2, 0)
+        line_edit = QtWidgets.QLineEdit()
+        line_edit.setFixedWidth(200)
+        line_edit.setEchoMode(QtWidgets.QLineEdit.Password)
+        value = self.values.get('password', '')
+        line_edit.insert(value)
+        grid.addWidget(line_edit, 2, 1)
+        self.fields['password'] = line_edit
+        if not focus and not value:
+            focus = 'password'
+
+        # TOTP (Time-Based One-Time Password)
+        label = QtWidgets.QLabel('TOTP')
+        label.setToolTip('Time-Based One-Time Password (6 digits)')
+        grid.addWidget(label, 3, 0)
+        line_edit = QtWidgets.QLineEdit()
+        line_edit.setPlaceholderText('6 digits')
+        validator = QtGui.QIntValidator(0, 999999, self)
+        line_edit.setValidator(validator)
+        line_edit.setFixedWidth(80)
+        value = self.values.get('totp', '')
+        line_edit.insert(value)
+        grid.addWidget(line_edit, 3, 1)
+        self.fields['totp'] = line_edit
+        if not focus and not value:
+            focus = 'totp'
+
+        # lines
+        grid.addWidget(QtWidgets.QLabel('Lines'), 4, 0)
+        line_edit = QtWidgets.QLineEdit()
+        line_edit.setFixedWidth(200)
+        validator = QtGui.QIntValidator(0, 2147483647, self)
+        line_edit.setValidator(validator)
+        line_edit.setFixedWidth(80)
+        value = self.values.get('lines', '')
+        line_edit.insert(value)
+        grid.addWidget(line_edit, 4, 1)
+        self.fields['lines'] = line_edit
+        if not focus and not value:
+            focus = 'lines'
 
         self.dialog_buttons = QtWidgets.QDialogButtonBox()
         self.dialog_buttons.setStandardButtons(
             QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
         self.dialog_buttons.rejected.connect(self.close)
 
-        grid.addWidget(self.dialog_buttons, 4, 0, 1, 2)
+        grid.addWidget(self.dialog_buttons, 5, 0, 1, 2)
         self.setLayout(grid)
         self.show()
+
+        if focus:
+            self.fields[focus].setFocus()
index 614a9d9296d107e09b6486a59dc2811e4c47f680..42c617b8fae4eb04f924adcc32fce6e7eb6c21c1 100644 (file)
@@ -10,8 +10,9 @@ Files: weechat.png, bullet_green_8x8.png, bullet_yellow_8x8.png
 
 
 Files: application-exit.png, dialog-close.png, dialog-ok-apply.png,
-       dialog-warning.png, document-save.png, edit-find.png, help-about.png,
-       network-connect.png, network-disconnect.png, preferences-other.png
+       dialog-password.png, dialog-warning.png, document-save.png,
+       edit-find.png, help-about.png, network-connect.png,
+       network-disconnect.png, preferences-other.png
 
   Files come from Debian package "oxygen-icon-theme":
 
diff --git a/qweechat/data/icons/dialog-password.png b/qweechat/data/icons/dialog-password.png
new file mode 100644 (file)
index 0000000..2151029
Binary files /dev/null and b/qweechat/data/icons/dialog-password.png differ
index 68561854006acf102613ad7eb87a9c4c537e14c1..d663dd8b93d2744833365aceed11e5ab94e6cf0f 100644 (file)
 
 """I/O with WeeChat/relay."""
 
+import hashlib
+import secrets
 import struct
 
 from PySide6 import QtCore, QtNetwork
 
 from qweechat import config
+from qweechat.debug import DebugDialog
 
 
-_PROTO_INIT_CMD = [
-    # initialize with the password
-    'init password=%(password)s',
+# list of supported hash algorithms on our side
+# (the hash algorithm will be negotiated with the remote WeeChat)
+_HASH_ALGOS_LIST = [
+    'plain',
+    'sha256',
+    'sha512',
+    'pbkdf2+sha256',
+    'pbkdf2+sha512',
 ]
+_HASH_ALGOS = ':'.join(_HASH_ALGOS_LIST)
 
-_PROTO_SYNC_CMDS = [
+# handshake with remote WeeChat (before init)
+_PROTO_HANDSHAKE = f'(handshake) handshake password_hash_algo={_HASH_ALGOS}\n'
+
+# initialize with the password (plain text)
+_PROTO_INIT_PWD = 'init password=%(password)s%(totp)s\n'
+
+# initialize with the hashed password
+_PROTO_INIT_HASH = ('init password_hash='
+                    '%(algo)s:%(salt)s%(iter)s:%(hash)s%(totp)s\n')
+
+_PROTO_SYNC = [
     # get buffers
     '(listbuffers) hdata buffer:gui_buffers(*) number,full_name,short_name,'
     'type,nicklist,title,local_variables',
@@ -49,26 +68,33 @@ _PROTO_SYNC_CMDS = [
 
 STATUS_DISCONNECTED = 'disconnected'
 STATUS_CONNECTING = 'connecting'
+STATUS_AUTHENTICATING = 'authenticating'
 STATUS_CONNECTED = 'connected'
 
 NETWORK_STATUS = {
-    'disconnected': {
+    STATUS_DISCONNECTED: {
         'label': 'Disconnected',
         'color': '#aa0000',
         'icon': 'dialog-close.png',
     },
-    'connecting': {
+    STATUS_CONNECTING: {
         'label': 'Connecting…',
-        'color': '#ff7f00',
+        'color': '#dd5f00',
         'icon': 'dialog-warning.png',
     },
-    'connected': {
+    STATUS_AUTHENTICATING: {
+        'label': 'Authenticating…',
+        'color': '#007fff',
+        'icon': 'dialog-password.png',
+    },
+    STATUS_CONNECTED: {
         'label': 'Connected',
         'color': 'green',
         'icon': 'dialog-ok-apply.png',
     },
 }
 
+
 class Network(QtCore.QObject):
     """I/O with WeeChat/relay."""
 
@@ -77,10 +103,9 @@ class Network(QtCore.QObject):
 
     def __init__(self, *args):
         super().__init__(*args)
-        self._server = None
-        self._port = None
-        self._ssl = None
-        self._password = None
+        self._init_connection()
+        self.debug_lines = []
+        self.debug_dialog = None
         self._lines = config.CONFIG_DEFAULT_RELAY_LINES
         self._buffer = QtCore.QByteArray()
         self._socket = QtNetwork.QSslSocket()
@@ -88,22 +113,91 @@ class Network(QtCore.QObject):
         self._socket.readyRead.connect(self._socket_read)
         self._socket.disconnected.connect(self._socket_disconnected)
 
+    def _init_connection(self):
+        self.status = STATUS_DISCONNECTED
+        self._server = None
+        self._port = None
+        self._ssl = None
+        self._password = None
+        self._totp = None
+        self._handshake_received = False
+        self._handshake_timer = None
+        self._handshake_timer = False
+        self._pwd_hash_algo = None
+        self._pwd_hash_iter = 0
+        self._server_nonce = None
+
+    def set_status(self, status):
+        """Set current status."""
+        self.status = status
+        self.statusChanged.emit(status, None)
+
+    def pbkdf2(self, hash_name, salt):
+        """Return hashed password with PBKDF2-HMAC."""
+        return hashlib.pbkdf2_hmac(
+            hash_name,
+            password=self._password.encode('utf-8'),
+            salt=salt,
+            iterations=self._pwd_hash_iter,
+        ).hex()
+
     def _build_init_command(self):
         """Build the init command to send to WeeChat."""
-        cmd = '\n'.join(_PROTO_INIT_CMD) + '\n'
-        return cmd % {'password': self._password}
+        totp = f',totp={self._totp}' if self._totp else ''
+        if self._pwd_hash_algo == 'plain':
+            cmd = _PROTO_INIT_PWD % {
+                'password': self._password,
+                'totp': totp,
+            }
+        else:
+            client_nonce = secrets.token_bytes(16)
+            salt = self._server_nonce + client_nonce
+            pwd_hash = None
+            iterations = ''
+            if self._pwd_hash_algo == 'pbkdf2+sha512':
+                pwd_hash = self.pbkdf2('sha512', salt)
+                iterations = f':{self._pwd_hash_iter}'
+            elif self._pwd_hash_algo == 'pbkdf2+sha256':
+                pwd_hash = self.pbkdf2('sha256', salt)
+                iterations = f':{self._pwd_hash_iter}'
+            elif self._pwd_hash_algo == 'sha512':
+                pwd = salt + self._password.encode('utf-8')
+                pwd_hash = hashlib.sha512(pwd).hexdigest()
+            elif self._pwd_hash_algo == 'sha256':
+                pwd = salt + self._password.encode('utf-8')
+                pwd_hash = hashlib.sha256(pwd).hexdigest()
+            if not pwd_hash:
+                return None
+            cmd = _PROTO_INIT_HASH % {
+                'algo': self._pwd_hash_algo,
+                'salt': bytearray(salt).hex(),
+                'iter': iterations,
+                'hash': pwd_hash,
+                'totp': totp,
+            }
+        return cmd
 
     def _build_sync_command(self):
         """Build the sync commands to send to WeeChat."""
-        cmd =  '\n'.join(_PROTO_SYNC_CMDS) + '\n'
+        cmd =  '\n'.join(_PROTO_SYNC) + '\n'
         return cmd % {'lines': self._lines}
 
+    def handshake_timer_expired(self):
+        if self.status == STATUS_AUTHENTICATING:
+            self._pwd_hash_algo = 'plain'
+            self.send_to_weechat(self._build_init_command())
+            self.sync_weechat()
+            self.set_status(STATUS_CONNECTED)
+
     def _socket_connected(self):
         """Slot: socket connected."""
-        self.statusChanged.emit(STATUS_CONNECTED, None)
-        if self._password:
-            cmd = self._build_init_command() + self._build_sync_command()
-            self.send_to_weechat(cmd)
+        self.set_status(STATUS_AUTHENTICATING)
+        self.send_to_weechat(_PROTO_HANDSHAKE)
+        self._handshake_timer = QtCore.QTimer()
+        self._handshake_timer.setSingleShot(True)
+        self._handshake_timer.setInterval(2000)
+        self._handshake_timer.timeout.connect(self.handshake_timer_expired)
+        self._handshake_timer.start()
 
     def _socket_read(self):
         """Slot: data available on socket."""
@@ -129,11 +223,10 @@ class Network(QtCore.QObject):
 
     def _socket_disconnected(self):
         """Slot: socket disconnected."""
-        self._server = None
-        self._port = None
-        self._ssl = None
-        self._password = ""
-        self.statusChanged.emit(STATUS_DISCONNECTED, None)
+        if self._handshake_timer:
+            self._handshake_timer.stop()
+        self._init_connection()
+        self.set_status(STATUS_DISCONNECTED)
 
     def is_connected(self):
         """Return True if the socket is connected, False otherwise."""
@@ -143,7 +236,7 @@ class Network(QtCore.QObject):
         """Return True if SSL is used, False otherwise."""
         return self._ssl
 
-    def connect_weechat(self, server, port, ssl, password, lines):
+    def connect_weechat(self, server, port, ssl, password, totp, lines):
         """Connect to WeeChat."""
         self._server = server
         try:
@@ -152,6 +245,7 @@ class Network(QtCore.QObject):
             self._port = 0
         self._ssl = ssl
         self._password = password
+        self._totp = totp
         try:
             self._lines = int(lines)
         except ValueError:
@@ -165,23 +259,40 @@ class Network(QtCore.QObject):
             self._socket.connectToHostEncrypted(self._server, self._port)
         else:
             self._socket.connectToHost(self._server, self._port)
-        self.statusChanged.emit(STATUS_CONNECTING, "")
+        self.set_status(STATUS_CONNECTING)
 
     def disconnect_weechat(self):
         """Disconnect from WeeChat."""
         if self._socket.state() == QtNetwork.QAbstractSocket.UnconnectedState:
+            self.set_status(STATUS_DISCONNECTED)
             return
         if self._socket.state() == QtNetwork.QAbstractSocket.ConnectedState:
             self.send_to_weechat('quit\n')
             self._socket.waitForBytesWritten(1000)
         else:
-            self.statusChanged.emit(STATUS_DISCONNECTED, None)
+            self.set_status(STATUS_DISCONNECTED)
         self._socket.abort()
 
     def send_to_weechat(self, message):
         """Send a message to WeeChat."""
+        self.debug_print(0, '<==', message, forcecolor='#AA0000')
         self._socket.write(message.encode('utf-8'))
 
+    def init_with_handshake(self, response):
+        """Initialize with WeeChat using the handshake response."""
+        self._pwd_hash_algo = response['password_hash_algo']
+        self._pwd_hash_iter = int(response['password_hash_iterations'])
+        self._server_nonce = bytearray.fromhex(response['nonce'])
+        if self._pwd_hash_algo:
+            cmd = self._build_init_command()
+            if cmd:
+                self.send_to_weechat(cmd)
+                self.sync_weechat()
+                self.set_status(STATUS_CONNECTED)
+                return
+        # failed to initialize: disconnect
+        self.disconnect_weechat()
+
     def desync_weechat(self):
         """Desynchronize from WeeChat."""
         self.send_to_weechat('desync\n')
@@ -211,3 +322,35 @@ class Network(QtCore.QObject):
             'password': self._password,
             'lines': str(self._lines),
         }
+
+    def debug_print(self, *args, **kwargs):
+        """Display a debug message."""
+        self.debug_lines.append((args, kwargs))
+        if self.debug_dialog:
+            self.debug_dialog.chat.display(*args, **kwargs)
+
+    def _debug_dialog_closed(self, result):
+        """Called when debug dialog is closed."""
+        self.debug_dialog = None
+
+    def debug_input_text_sent(self, text):
+        """Send debug buffer input to WeeChat."""
+        if self.network.is_connected():
+            text = str(text)
+            pos = text.find(')')
+            if text.startswith('(') and pos >= 0:
+                text = '(debug_%s)%s' % (text[1:pos], text[pos+1:])
+            else:
+                text = '(debug) %s' % text
+            self.network.debug_print(0, '<==', text, forcecolor='#AA0000')
+            self.network.send_to_weechat(text + '\n')
+
+    def open_debug_dialog(self):
+        """Open a dialog with debug messages."""
+        if not self.debug_dialog:
+            self.debug_dialog = DebugDialog()
+            self.debug_dialog.input.textSent.connect(
+                self.debug_input_text_sent)
+            self.debug_dialog.finished.connect(self._debug_dialog_closed)
+            self.debug_dialog.display_lines(self.debug_lines)
+            self.debug_dialog.chat.scroll_bottom()
diff --git a/qweechat/preferences.py b/qweechat/preferences.py
new file mode 100644 (file)
index 0000000..e8c276f
--- /dev/null
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+#
+# preferences.py - preferences dialog box
+#
+# Copyright (C) 2011-2021 Sébastien Helleu <flashcode@flashtux.org>
+#
+# This file is part of QWeeChat, a Qt remote GUI for WeeChat.
+#
+# QWeeChat is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# QWeeChat is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with QWeeChat.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+"""Preferences dialog box."""
+
+from PySide6 import QtCore, QtWidgets as QtGui
+
+
+class PreferencesDialog(QtGui.QDialog):
+    """Preferences dialog."""
+
+    def __init__(self, *args):
+        QtGui.QDialog.__init__(*(self,) + args)
+        self.setModal(True)
+        self.setWindowTitle('Preferences')
+
+        close_button = QtGui.QPushButton('Close')
+        close_button.pressed.connect(self.close)
+
+        hbox = QtGui.QHBoxLayout()
+        hbox.addStretch(1)
+        hbox.addWidget(close_button)
+        hbox.addStretch(1)
+
+        vbox = QtGui.QVBoxLayout()
+
+        label = QtGui.QLabel('Not yet implemented!')
+        label.setAlignment(QtCore.Qt.AlignHCenter)
+        vbox.addWidget(label)
+
+        label = QtGui.QLabel('')
+        label.setAlignment(QtCore.Qt.AlignHCenter)
+        vbox.addWidget(label)
+
+        vbox.addLayout(hbox)
+
+        self.setLayout(vbox)
+        self.show()
index 1888d6d876633c77d83d492fdea05e7ca1a1d905..66a8d6a12f747460d7c964cd6651d92f71022701 100644 (file)
@@ -40,21 +40,18 @@ from pkg_resources import resource_filename
 from PySide6 import QtCore, QtGui, QtWidgets
 
 from qweechat import config
-from qweechat.weechat import protocol
-from qweechat.network import Network, STATUS_DISCONNECTED, NETWORK_STATUS
-from qweechat.connection import ConnectionDialog
-from qweechat.buffer import BufferListWidget, Buffer
-from qweechat.debug import DebugDialog
 from qweechat.about import AboutDialog
+from qweechat.buffer import BufferListWidget, Buffer
+from qweechat.connection import ConnectionDialog
+from qweechat.network import Network, STATUS_DISCONNECTED, NETWORK_STATUS
+from qweechat.preferences import PreferencesDialog
+from qweechat.weechat import protocol
 
 
 APP_NAME = 'QWeeChat'
 AUTHOR = 'Sébastien Helleu'
 WEECHAT_SITE = 'https://weechat.org/'
 
-# number of lines in buffer for debug window
-DEBUG_NUM_LINES = 50
-
 
 class MainWindow(QtWidgets.QMainWindow):
     """Main window."""
@@ -67,9 +64,6 @@ class MainWindow(QtWidgets.QMainWindow):
         self.resize(1000, 600)
         self.setWindowTitle(APP_NAME)
 
-        self.debug_dialog = None
-        self.debug_lines = []
-
         self.about_dialog = None
         self.connection_dialog = None
         self.preferences_dialog = None
@@ -101,26 +95,47 @@ class MainWindow(QtWidgets.QMainWindow):
         # actions for menu and toolbar
         actions_def = {
             'connect': [
-                'network-connect.png', 'Connect to WeeChat',
-                'Ctrl+O', self.open_connection_dialog],
+                'network-connect.png',
+                'Connect to WeeChat',
+                'Ctrl+O',
+                self.open_connection_dialog,
+            ],
             'disconnect': [
-                'network-disconnect.png', 'Disconnect from WeeChat',
-                'Ctrl+D', self.network.disconnect_weechat],
+                'network-disconnect.png',
+                'Disconnect from WeeChat',
+                'Ctrl+D',
+                self.network.disconnect_weechat,
+            ],
             'debug': [
-                'edit-find.png', 'Debug console window',
-                'Ctrl+B', self.open_debug_dialog],
+                'edit-find.png',
+                'Open debug console window',
+                'Ctrl+B',
+                self.network.open_debug_dialog,
+            ],
             'preferences': [
-                'preferences-other.png', 'Preferences',
-                'Ctrl+P', self.open_preferences_dialog],
+                'preferences-other.png',
+                'Change preferences',
+                'Ctrl+P',
+                self.open_preferences_dialog,
+            ],
             'about': [
-                'help-about.png', 'About',
-                'Ctrl+H', self.open_about_dialog],
+                'help-about.png',
+                'About QWeeChat',
+                'Ctrl+H',
+                self.open_about_dialog,
+            ],
             'save connection': [
-                'document-save.png', 'Save connection configuration',
-                'Ctrl+S', self.save_connection],
+                'document-save.png',
+                'Save connection configuration',
+                'Ctrl+S',
+                self.save_connection,
+            ],
             'quit': [
-                'application-exit.png', 'Quit application',
-                'Ctrl+Q', self.close],
+                'application-exit.png',
+                'Quit application',
+                'Ctrl+Q',
+                self.close,
+            ],
         }
         self.actions = {}
         for name, action in list(actions_def.items()):
@@ -128,7 +143,7 @@ class MainWindow(QtWidgets.QMainWindow):
                 QtGui.QIcon(
                     resource_filename(__name__, 'data/icons/%s' % action[0])),
                 name.capitalize(), self)
-            self.actions[name].setStatusTip(action[1])
+            self.actions[name].setToolTip(f'{action[1]} ({action[2]})')
             self.actions[name].setShortcut(action[2])
             self.actions[name].triggered.connect(action[3])
 
@@ -168,16 +183,18 @@ class MainWindow(QtWidgets.QMainWindow):
 
         # open debug dialog
         if self.config.getboolean('look', 'debug'):
-            self.open_debug_dialog()
+            self.network.open_debug_dialog()
 
         # auto-connect to relay
         if self.config.getboolean('relay', 'autoconnect'):
-            self.network.connect_weechat(self.config.get('relay', 'server'),
-                                         self.config.get('relay', 'port'),
-                                         self.config.getboolean('relay',
-                                                                'ssl'),
-                                         self.config.get('relay', 'password'),
-                                         self.config.get('relay', 'lines'))
+            self.network.connect_weechat(
+                server=self.config.get('relay', 'server'),
+                port=self.config.get('relay', 'port'),
+                ssl=self.config.getboolean('relay', 'ssl'),
+                password=self.config.get('relay', 'password'),
+                totp=None,
+                lines=self.config.get('relay', 'lines'),
+            )
 
         self.show()
 
@@ -192,14 +209,12 @@ class MainWindow(QtWidgets.QMainWindow):
         if self.network.is_connected():
             message = 'input %s %s\n' % (full_name, text)
             self.network.send_to_weechat(message)
-            self.debug_display(0, '<==', message, forcecolor='#AA0000')
+            self.network.debug_print(0, '<==', message, forcecolor='#AA0000')
 
     def open_preferences_dialog(self):
         """Open a dialog with preferences."""
         # TODO: implement the preferences dialog box
-        messages = ['Not yet implemented!',
-                    '']
-        self.preferences_dialog = AboutDialog('Preferences', messages, self)
+        self.preferences_dialog = PreferencesDialog(self)
 
     def save_connection(self):
         """Save connection configuration."""
@@ -208,39 +223,6 @@ class MainWindow(QtWidgets.QMainWindow):
             for option in options:
                 self.config.set('relay', option, options[option])
 
-    def debug_display(self, *args, **kwargs):
-        """Display a debug message."""
-        self.debug_lines.append((args, kwargs))
-        self.debug_lines = self.debug_lines[-DEBUG_NUM_LINES:]
-        if self.debug_dialog:
-            self.debug_dialog.chat.display(*args, **kwargs)
-
-    def open_debug_dialog(self):
-        """Open a dialog with debug messages."""
-        if not self.debug_dialog:
-            self.debug_dialog = DebugDialog(self)
-            self.debug_dialog.input.textSent.connect(
-                self.debug_input_text_sent)
-            self.debug_dialog.finished.connect(self._debug_dialog_closed)
-            self.debug_dialog.display_lines(self.debug_lines)
-            self.debug_dialog.chat.scroll_bottom()
-
-    def debug_input_text_sent(self, text):
-        """Send debug buffer input to WeeChat."""
-        if self.network.is_connected():
-            text = str(text)
-            pos = text.find(')')
-            if text.startswith('(') and pos >= 0:
-                text = '(debug_%s)%s' % (text[1:pos], text[pos+1:])
-            else:
-                text = '(debug) %s' % text
-            self.debug_display(0, '<==', text, forcecolor='#AA0000')
-            self.network.send_to_weechat(text + '\n')
-
-    def _debug_dialog_closed(self, result):
-        """Called when debug dialog is closed."""
-        self.debug_dialog = None
-
     def open_about_dialog(self):
         """Open a dialog with info about QWeeChat."""
         self.about_dialog = AboutDialog(APP_NAME, AUTHOR, WEECHAT_SITE, self)
@@ -257,18 +239,20 @@ class MainWindow(QtWidgets.QMainWindow):
     def connect_weechat(self):
         """Connect to WeeChat."""
         self.network.connect_weechat(
-            self.connection_dialog.fields['server'].text(),
-            self.connection_dialog.fields['port'].text(),
-            self.connection_dialog.fields['ssl'].isChecked(),
-            self.connection_dialog.fields['password'].text(),
-            int(self.connection_dialog.fields['lines'].text()))
+            server=self.connection_dialog.fields['server'].text(),
+            port=self.connection_dialog.fields['port'].text(),
+            ssl=self.connection_dialog.fields['ssl'].isChecked(),
+            password=self.connection_dialog.fields['password'].text(),
+            totp=self.connection_dialog.fields['totp'].text(),
+            lines=int(self.connection_dialog.fields['lines'].text()),
+        )
         self.connection_dialog.close()
 
     def _network_status_changed(self, status, extra):
         """Called when the network status has changed."""
         if self.config.getboolean('look', 'statusbar'):
             self.statusBar().showMessage(status)
-        self.debug_display(0, '', status, forcecolor='#0000AA')
+        self.network.debug_print(0, '', status, forcecolor='#0000AA')
         self.network_status_set(status)
 
     def network_status_set(self, status):
@@ -296,30 +280,40 @@ class MainWindow(QtWidgets.QMainWindow):
 
     def _network_weechat_msg(self, message):
         """Called when a message is received from WeeChat."""
-        self.debug_display(0, '==>',
-                           'message (%d bytes):\n%s'
-                           % (len(message),
-                              protocol.hex_and_ascii(message.data(), 20)),
-                           forcecolor='#008800')
+        self.network.debug_print(
+            0, '==>',
+            'message (%d bytes):\n%s'
+            % (len(message),
+               protocol.hex_and_ascii(message.data(), 20)),
+            forcecolor='#008800',
+        )
         try:
             proto = protocol.Protocol()
             message = proto.decode(message.data())
             if message.uncompressed:
-                self.debug_display(
+                self.network.debug_print(
                     0, '==>',
                     'message uncompressed (%d bytes):\n%s'
                     % (message.size_uncompressed,
                        protocol.hex_and_ascii(message.uncompressed, 20)),
                     forcecolor='#008800')
-            self.debug_display(0, '', 'Message: %s' % message)
+            self.network.debug_print(0, '', 'Message: %s' % message)
             self.parse_message(message)
         except Exception:  # noqa: E722
             print('Error while decoding message from WeeChat:\n%s'
                   % traceback.format_exc())
             self.network.disconnect_weechat()
 
+    def _parse_handshake(self, message):
+        """Parse a WeeChat message with handshake response."""
+        for obj in message.objects:
+            if obj.objtype != 'htb':
+                continue
+            self.network.init_with_handshake(obj.value)
+            break
+
     def _parse_listbuffers(self, message):
-        """Parse a WeeChat with list of buffers."""
+        """Parse a WeeChat message with list of buffers."""
         for obj in message.objects:
             if obj.objtype != 'hda' or obj.value['path'][-1] != 'buffer':
                 continue
@@ -462,7 +456,9 @@ class MainWindow(QtWidgets.QMainWindow):
     def parse_message(self, message):
         """Parse a WeeChat message."""
         if message.msgid.startswith('debug'):
-            self.debug_display(0, '', '(debug message, ignored)')
+            self.network.debug_print(0, '', '(debug message, ignored)')
+        elif message.msgid == 'handshake':
+            self._parse_handshake(message)
         elif message.msgid == 'listbuffers':
             self._parse_listbuffers(message)
         elif message.msgid in ('listlines', '_buffer_line_added'):
@@ -526,8 +522,8 @@ class MainWindow(QtWidgets.QMainWindow):
     def closeEvent(self, event):
         """Called when QWeeChat window is closed."""
         self.network.disconnect_weechat()
-        if self.debug_dialog:
-            self.debug_dialog.close()
+        if self.network.debug_dialog:
+            self.network.debug_dialog.close()
         config.write(self.config)
         QtWidgets.QMainWindow.closeEvent(self, event)