diff --git a/README.md b/README.md index d44bab9b..ee551706 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Written in Qt/QML + Python with [nio](https://github.com/poljar/matrix-nio), - **Multiple accounts** in one client - Set your display name and profile picture - Import/export **E2E** key files -- Inspect, rename and manually verify your sessions +- Inspect, rename, manually verify and sign out one or multiple sessions ### Rooms diff --git a/TODO.md b/TODO.md index 1fcc9657..a34bc593 100644 --- a/TODO.md +++ b/TODO.md @@ -185,7 +185,6 @@ - E2E - List and verify other users's devices - SAS verification - - Delete devices (do that in key verification popup instead of blacklisting) - Request room keys from own other devices - Auto-trust accounts within the same client - Provide help when undecryptable messages occur, including: diff --git a/src/backend/errors.py b/src/backend/errors.py index 5d3a3923..dcb6aa6e 100644 --- a/src/backend/errors.py +++ b/src/backend/errors.py @@ -33,6 +33,12 @@ class MatrixError(Exception): return cls(response.transport_response.status, response.status_code) +@dataclass +class MatrixUnauthorized(MatrixError): + http_code: int = 401 + m_code: str = "M_UNAUTHORIZED" + + @dataclass class MatrixForbidden(MatrixError): http_code: int = 403 diff --git a/src/backend/matrix_client.py b/src/backend/matrix_client.py index 9fcb4f83..a1e2f307 100644 --- a/src/backend/matrix_client.py +++ b/src/backend/matrix_client.py @@ -34,8 +34,8 @@ from nio.crypto import async_generator_from_data from . import __app_name__, __display_name__, utils from .errors import ( BadMimeType, InvalidUserId, InvalidUserInContext, MatrixBadGateway, - MatrixError, MatrixNotFound, MatrixTooLarge, UneededThumbnail, - UserFromOtherServerDisallowed, + MatrixError, MatrixNotFound, MatrixTooLarge, MatrixUnauthorized, + UneededThumbnail, UserFromOtherServerDisallowed, ) from .html_markdown import HTML_PROCESSOR as HTML from .media_cache import Media, Thumbnail @@ -1347,6 +1347,23 @@ class MatrixClient(nio.AsyncClient): self.blacklist_device(self.olm.device_store[user_id][device_id]) + async def delete_devices_with_password( + self, device_ids: List[str], password: str, + ) -> None: + """Delete devices, authentifying using the account's password.""" + + auth = { + "type": "m.login.password", + "user": self.user_id, + "password": password, + } + + resp = await super().delete_devices(device_ids, auth) + + if isinstance(resp, nio.DeleteDevicesAuthResponse): + raise MatrixUnauthorized() + + # Functions to register/modify data into models async def update_account_unread_counts(self) -> None: diff --git a/src/gui/Pages/AccountSettings/Sessions.qml b/src/gui/Pages/AccountSettings/Sessions.qml index 27e8065d..768af4b3 100644 --- a/src/gui/Pages/AccountSettings/Sessions.qml +++ b/src/gui/Pages/AccountSettings/Sessions.qml @@ -53,16 +53,32 @@ HColumnPage { } function deleteDevices(...indice) { - const deviceIds = [] + if (indice.length === 1 && indice[0] === 0) { + utils.makePopup("Popups/SignOutPopup.qml", { userId: page.userId }) + return + } - for (const i of indice.sort()) + const deviceIds = [] + let deleteOwnDevice = false + + for (const i of indice.sort()) { + i === 0 ? + deleteOwnDevice = true : deviceIds.push(deviceList.model.get(i).id) + } utils.makePopup( - "Popups/AuthentificationPopup.qml", + "Popups/DeleteDevicesPopup.qml", { userId: page.userId, deviceIds, + deletedCallback: () => { + deleteOwnDevice ? + utils.makePopup( + "Popups/SignOutPopup.qml", { userId: page.userId }, + ) : + page.loadDevices() + }, }, ) } diff --git a/src/gui/Popups/AuthentificationPopup.qml b/src/gui/Popups/AuthentificationPopup.qml deleted file mode 100644 index 929d0d63..00000000 --- a/src/gui/Popups/AuthentificationPopup.qml +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later - -import QtQuick 2.12 -import "../Base" -import "../Base/ButtonLayout" - -HFlickableColumnPopup { - id: popup - - - property string userId - property string deviceIds - - - page.footer: ButtonLayout { - CancelButton { - id: cancelButton - onClicked: popup.close() - } - } - - onOpened: cancelButton.forceActiveFocus() - - SummaryLabel { text: qsTr("Not implemented yet") } -} diff --git a/src/gui/Popups/DeleteDevicesPopup.qml b/src/gui/Popups/DeleteDevicesPopup.qml new file mode 100644 index 00000000..ae2422a6 --- /dev/null +++ b/src/gui/Popups/DeleteDevicesPopup.qml @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +import QtQuick 2.12 +import "../Base" +import "../Base/ButtonLayout" + +PasswordPopup { + id: popup + + + property string userId + property var deviceIds // array + property var deletedCallback: null + + + function verifyPassword(pass, callback) { + py.callClientCoro( + userId, + "delete_devices_with_password", + [deviceIds, pass], + () => callback(true), + (type, args) => { + callback( + type === "MatrixUnauthorized" ? + false : + qsTr("Unknown error: %1 - %2").arg(type).arg(args) + ) + }, + ) + } + + summary.text: + qsTr("Enter your account's password to continue:") + + + validateButton.text: qsTr("Sign out") + validateButton.icon.name: "sign-out" + + onClosed: if (acceptedPassword && deletedCallback) deletedCallback() +}