From 16d4f4e48304543a0ab59b235edba07f5f2c2204 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 18 Apr 2022 20:07:12 +0200 Subject: Implement key verification --- lib/keyverificationsession.cpp | 522 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 522 insertions(+) create mode 100644 lib/keyverificationsession.cpp (limited to 'lib/keyverificationsession.cpp') diff --git a/lib/keyverificationsession.cpp b/lib/keyverificationsession.cpp new file mode 100644 index 00000000..3b3b7627 --- /dev/null +++ b/lib/keyverificationsession.cpp @@ -0,0 +1,522 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "connection.h" +#include "keyverificationsession.h" +#include "olm/sas.h" +#include "e2ee/qolmaccount.h" +#include "e2ee/qolmutils.h" +#include "events/event.h" +#include +#include +#include +#include "database.h" + +using namespace Quotient; + +KeyVerificationSession::KeyVerificationSession(const QString& remoteUserId, const KeyVerificationRequestEvent& event, Connection *connection, bool encrypted, QObject* parent) + : QObject(parent) + , m_remoteUserId(remoteUserId) + , m_remoteDeviceId(event.fromDevice()) + , m_transactionId(event.transactionId()) + , m_connection(connection) + , m_encrypted(encrypted) + , m_remoteSupportedMethods(event.methods()) +{ + auto timeoutTime = std::min(event.timestamp() + 600000, QDateTime::currentDateTime().addSecs(120).toMSecsSinceEpoch()); + m_timeout = timeoutTime - QDateTime::currentMSecsSinceEpoch(); + if (m_timeout <= 5000) { + return; + } + init(); + setState(INCOMING); +} + +KeyVerificationSession::KeyVerificationSession(const QString& userId, const QString& deviceId, Connection* connection, QObject* parent) + : QObject(parent) + , m_remoteUserId(userId) + , m_remoteDeviceId(deviceId) + , m_transactionId(QUuid::createUuid().toString()) + , m_connection(connection) + , m_encrypted(false) +{ + m_timeout = 600000; + init(); + QMetaObject::invokeMethod(this, &KeyVerificationSession::sendRequest); +} + +void KeyVerificationSession::init() +{ + connect(m_connection, &Connection::incomingKeyVerificationReady, this, [this](const KeyVerificationReadyEvent& event) { + if (event.transactionId() == m_transactionId && event.fromDevice() == m_remoteDeviceId) { + handleReady(event); + } + }); + connect(m_connection, &Connection::incomingKeyVerificationStart, this, [this](const KeyVerificationStartEvent& event) { + if (event.transactionId() == m_transactionId && event.fromDevice() == m_remoteDeviceId) { + handleStart(event); + } + }); + connect(m_connection, &Connection::incomingKeyVerificationAccept, this, [this](const KeyVerificationAcceptEvent& event) { + if (event.transactionId() == m_transactionId) { + handleAccept(event); + } + }); + connect(m_connection, &Connection::incomingKeyVerificationKey, this, [this](const KeyVerificationKeyEvent& event) { + if (event.transactionId() == m_transactionId) { + handleKey(event); + } + }); + connect(m_connection, &Connection::incomingKeyVerificationMac, this, [this](const KeyVerificationMacEvent& event) { + if (event.transactionId() == m_transactionId) { + handleMac(event); + } + }); + connect(m_connection, &Connection::incomingKeyVerificationDone, this, [this](const KeyVerificationDoneEvent& event) { + if (event.transactionId() == m_transactionId) { + handleDone(event); + } + }); + connect(m_connection, &Connection::incomingKeyVerificationCancel, this, [this](const KeyVerificationCancelEvent& event) { + if (event.transactionId() == m_transactionId) { + handleCancel(event); + } + }); + + QTimer::singleShot(m_timeout, this, [this] { + cancelVerification(TIMEOUT); + }); + + + m_sas = olm_sas(new uint8_t[olm_sas_size()]); + auto randomSize = olm_create_sas_random_length(m_sas); + auto random = getRandom(randomSize); + olm_create_sas(m_sas, random.data(), randomSize); + + m_language = QLocale::system().uiLanguages()[0]; + m_language = m_language.left(m_language.indexOf('-')); +} + +KeyVerificationSession::~KeyVerificationSession() +{ + delete[] reinterpret_cast(m_sas); +} + +void KeyVerificationSession::handleKey(const KeyVerificationKeyEvent& event) +{ + if (state() != WAITINGFORKEY && state() != WAITINGFORVERIFICATION) { + cancelVerification(UNEXPECTED_MESSAGE); + return; + } + olm_sas_set_their_key(m_sas, event.key().toLatin1().data(), event.key().toLatin1().size()); + + if (startSentByUs) { + auto commitment = QString(QCryptographicHash::hash((event.key() % m_startEvent).toLatin1(), QCryptographicHash::Sha256).toBase64()); + commitment = commitment.left(commitment.indexOf('=')); + if (commitment != m_commitment) { + qCWarning(E2EE) << "Commitment mismatch; aborting verification"; + cancelVerification(MISMATCHED_COMMITMENT); + return; + } + } else { + sendKey(); + } + setState(WAITINGFORVERIFICATION); + + QByteArray keyBytes(olm_sas_pubkey_length(m_sas), '\0'); + olm_sas_get_pubkey(m_sas, keyBytes.data(), keyBytes.size()); + QString key = QString(keyBytes); + + QByteArray output(6, '\0'); + QString infoTemplate = startSentByUs ? "MATRIX_KEY_VERIFICATION_SAS|%1|%2|%3|%4|%5|%6|%7"_ls : "MATRIX_KEY_VERIFICATION_SAS|%4|%5|%6|%1|%2|%3|%7"_ls; + + auto info = infoTemplate.arg(m_connection->userId()).arg(m_connection->deviceId()).arg(key).arg(m_remoteUserId).arg(m_remoteDeviceId).arg(event.key()).arg(m_transactionId); + olm_sas_generate_bytes(m_sas, info.toLatin1().data(), info.toLatin1().size(), output.data(), output.size()); + + QVector code(7, 0); + const auto& data = (uint8_t *) output.data(); + + code[0] = data[0] >> 2; + code[1] = (data[0] << 4 & 0x3f) | data[1] >> 4; + code[2] = (data[1] << 2 & 0x3f) | data[2] >> 6; + code[3] = data[2] & 0x3f; + code[4] = data[3] >> 2; + code[5] = (data[3] << 4 & 0x3f) | data[4] >> 4; + code[6] = (data[4] << 2 & 0x3f) | data[5] >> 6; + + for (const auto& c : code) { + auto [emoji, description] = emojiForCode(c); + QVariantMap map; + map["emoji"] = emoji; + map["description"] = description; + m_sasEmojis += map; + } + emit sasEmojisChanged(); + emit keyReceived(); +} + +QByteArray KeyVerificationSession::macInfo(bool verifying, const QString& key) +{ + return (verifying ? "MATRIX_KEY_VERIFICATION_MAC%3%4%1%2%5%6"_ls : "MATRIX_KEY_VERIFICATION_MAC%1%2%3%4%5%6"_ls).arg(m_connection->userId()).arg(m_connection->deviceId()).arg(m_remoteUserId).arg(m_remoteDeviceId).arg(m_transactionId).arg(key).toLatin1(); +} + +QString KeyVerificationSession::calculateMac(const QString& input, bool verifying, const QString& keyId) +{ + QByteArray inputBytes = input.toLatin1(); + QByteArray outputBytes(olm_sas_mac_length(m_sas), '\0'); + olm_sas_calculate_mac(m_sas, inputBytes.data(), inputBytes.size(), macInfo(verifying, keyId).data(), macInfo(verifying, keyId).size(), outputBytes.data(), outputBytes.size()); + auto output = QString(outputBytes); + return output.left(output.indexOf('=')); +} + +void KeyVerificationSession::sendMac() +{ + QString edKeyId = "ed25519:" % m_connection->deviceId(); + + auto keys = calculateMac(edKeyId, false); + + QJsonObject mac; + auto key = m_connection->olmAccount()->deviceKeys().keys[edKeyId]; + mac[edKeyId] = calculateMac(key, false, edKeyId); + + auto event = makeEvent(QJsonObject { + {"type", "m.key.verification.mac"}, + {"content", QJsonObject{ + {"transaction_id", m_transactionId}, + {"keys", keys}, + {"mac", mac}, + }} + }); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + setState (macReceived ? DONE : WAITINGFORMAC); +} + +void KeyVerificationSession::sendDone() +{ + auto event = makeEvent(QJsonObject { + {"type", "m.key.verification.done"}, + {"content", QJsonObject{ + {"transaction_id", m_transactionId}, + }} + }); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); +} + +void KeyVerificationSession::sendKey() +{ + QByteArray keyBytes(olm_sas_pubkey_length(m_sas), '\0'); + olm_sas_get_pubkey(m_sas, keyBytes.data(), keyBytes.size()); + QString key = QString(keyBytes); + auto event = makeEvent(QJsonObject { + {"type", "m.key.verification.key"}, + {"content", QJsonObject{ + {"transaction_id", m_transactionId}, + {"key", key}, + }} + }); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); +} + + +void KeyVerificationSession::cancelVerification(Error error) +{ + auto event = makeEvent(QJsonObject { + {"type", "m.key.verification.cancel"}, + {"content", QJsonObject{ + {"code", errorToString(error)}, + {"reason", errorToString(error)}, + {"transaction_id", m_transactionId} + }} + }); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + setState(CANCELED); + setError(error); + emit finished(); + deleteLater(); +} + +void KeyVerificationSession::sendReady() +{ + auto methods = commonSupportedMethods(m_remoteSupportedMethods); + + if (methods.isEmpty()) { + cancelVerification(UNKNOWN_METHOD); + return; + } + + auto event = makeEvent(QJsonObject { + {"type", "m.key.verification.ready"}, + {"content", QJsonObject { + {"from_device", m_connection->deviceId()}, + {"methods", toJson(methods)}, + {"transaction_id", m_transactionId}, + }} + }); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + setState(READY); + + if (methods.size() == 1) { + sendStartSas(); + } +} + +void KeyVerificationSession::sendStartSas() +{ + startSentByUs = true; + auto event = makeEvent(QJsonObject { + {"type", "m.key.verification.start"}, + {"content", QJsonObject { + {"from_device", m_connection->deviceId()}, + {"hashes", QJsonArray {"sha256"}}, + {"key_agreement_protocols", QJsonArray { "curve25519-hkdf-sha256" }}, + {"message_authentication_codes", QJsonArray { "hkdf-hmac-sha256" }}, + {"method", "m.sas.v1"}, + {"short_authentication_string", QJsonArray { "decimal", "emoji" }}, + {"transaction_id", m_transactionId}, + }} + }); + m_startEvent = QJsonDocument(event->contentJson()).toJson(QJsonDocument::Compact); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + setState(WAITINGFORACCEPT); +} + +void KeyVerificationSession::handleReady(const KeyVerificationReadyEvent& event) +{ + if (state() != WAITINGFORREADY) { + cancelVerification(UNEXPECTED_MESSAGE); + return; + } + setState(READY); + m_remoteSupportedMethods = event.methods(); + auto methods = commonSupportedMethods(m_remoteSupportedMethods); + + if (methods.isEmpty()) { + cancelVerification(UNKNOWN_METHOD); + return; + } + + if (methods.size() == 1) { + sendStartSas(); + } +} + +void KeyVerificationSession::handleStart(const KeyVerificationStartEvent& event) +{ + if (state() != READY) { + cancelVerification(UNEXPECTED_MESSAGE); + return; + } + if (startSentByUs) { + if (m_remoteUserId > m_connection->userId() || (m_remoteUserId == m_connection->userId() && m_remoteDeviceId > m_connection->deviceId())) { + return; + } else { + startSentByUs = false; + } + } + QByteArray publicKey(olm_sas_pubkey_length(m_sas), '\0'); + olm_sas_get_pubkey(m_sas, publicKey.data(), publicKey.size()); + const auto canonicalEvent = QString(QJsonDocument(event.contentJson()).toJson(QJsonDocument::Compact)); + auto commitment = QString(QCryptographicHash::hash((QString(publicKey) % canonicalEvent).toLatin1(), QCryptographicHash::Sha256).toBase64()); + commitment = commitment.left(commitment.indexOf('=')); + + auto acceptEvent = makeEvent(QJsonObject { + {"type", "m.key.verification.accept"}, + {"content", QJsonObject { + {"commitment", commitment}, + {"hash", "sha256"}, + {"key_agreement_protocol", "curve25519-hkdf-sha256"}, + {"message_authentication_code", "hkdf-hmac-sha256"}, + {"method", "m.sas.v1"}, + {"short_authentication_string", QJsonArray { + "decimal", + "emoji", + }}, + {"transaction_id", m_transactionId}, + }} + }); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(acceptEvent), m_encrypted); + setState(ACCEPTED); +} + +void KeyVerificationSession::handleAccept(const KeyVerificationAcceptEvent& event) +{ + if(state() != WAITINGFORACCEPT) { + cancelVerification(UNEXPECTED_MESSAGE); + return; + } + m_commitment = event.commitment(); + sendKey(); + setState(WAITINGFORKEY); +} + +void KeyVerificationSession::handleMac(const KeyVerificationMacEvent& event) +{ + QStringList keys = event.mac().keys(); + keys.sort(); + const auto& key = keys.join(","); + const QString edKeyId = "ed25519:"_ls % m_remoteDeviceId; + + if (calculateMac(m_connection->edKeyForUserDevice(m_remoteUserId, m_remoteDeviceId), true, edKeyId) != event.mac()[edKeyId]) { + cancelVerification(KEY_MISMATCH); + return; + } + + if (calculateMac(key, true) != event.keys()) { + cancelVerification(KEY_MISMATCH); + return; + } + + m_connection->database()->setSessionVerified(edKeyId); + emit m_connection->sessionVerified(m_remoteUserId, m_remoteDeviceId); + macReceived = true; + + if (state() == WAITINGFORMAC) { + setState(DONE); + sendDone(); + emit finished(); + deleteLater(); + } +} + +void KeyVerificationSession::handleDone(const KeyVerificationDoneEvent& event) +{ + if (state() != DONE) { + cancelVerification(UNEXPECTED_MESSAGE); + } +} + +void KeyVerificationSession::handleCancel(const KeyVerificationCancelEvent& event) +{ + setError(stringToError(event.code())); + setState(CANCELED); +} + +std::pair KeyVerificationSession::emojiForCode(int code) +{ + static QJsonArray data; + if (data.isEmpty()) { + QFile dataFile(":/sas-emoji.json"); + dataFile.open(QFile::ReadOnly); + data = QJsonDocument::fromJson(dataFile.readAll()).array(); + } + if (data[code].toObject()["translated_descriptions"].toObject().contains(m_language)) { + return {data[code].toObject()["emoji"].toString(), data[code].toObject()["translated_descriptions"].toObject()[m_language].toString()}; + } + return {data[code].toObject()["emoji"].toString(), data[code].toObject()["description"].toString()}; +} + +QList KeyVerificationSession::sasEmojis() const +{ + return m_sasEmojis; +} + +void KeyVerificationSession::sendRequest() +{ + QJsonArray methods = toJson(m_supportedMethods); + auto event = makeEvent(QJsonObject { + {"type", "m.key.verification.request"}, + {"content", QJsonObject { + {"from_device", m_connection->deviceId()}, + {"transaction_id", m_transactionId}, + {"methods", methods}, + {"timestamp", QDateTime::currentMSecsSinceEpoch()}, + }}, + }); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + setState(WAITINGFORREADY); +} + +KeyVerificationSession::State KeyVerificationSession::state() const +{ + return m_state; +} + +void KeyVerificationSession::setState(KeyVerificationSession::State state) +{ + m_state = state; + emit stateChanged(); +} + +KeyVerificationSession::Error KeyVerificationSession::error() const +{ + return m_error; +} + +void KeyVerificationSession::setError(Error error) +{ + m_error = error; + emit errorChanged(); +} + +QString KeyVerificationSession::errorToString(Error error) const +{ + switch(error) { + case NONE: + return "none"_ls; + case TIMEOUT: + return "m.timeout"_ls; + case USER: + return "m.user"_ls; + case UNEXPECTED_MESSAGE: + return "m.unexpected_message"_ls; + case UNKNOWN_TRANSACTION: + return "m.unknown_transaction"_ls; + case UNKNOWN_METHOD: + return "m.unknown_method"_ls; + case KEY_MISMATCH: + return "m.key_mismatch"_ls; + case USER_MISMATCH: + return "m.user_mismatch"_ls; + case INVALID_MESSAGE: + return "m.invalid_message"_ls; + case SESSION_ACCEPTED: + return "m.accepted"_ls; + case MISMATCHED_COMMITMENT: + return "m.mismatched_commitment"_ls; + case MISMATCHED_SAS: + return "m.mismatched_sas"_ls; + default: + return "m.user"_ls; + } +} + +KeyVerificationSession::Error KeyVerificationSession::stringToError(const QString& error) const +{ + if (error == "m.timeout"_ls) { + return REMOTE_TIMEOUT; + } else if (error == "m.user"_ls) { + return REMOTE_USER; + } else if (error == "m.unexpected_message"_ls) { + return REMOTE_UNEXPECTED_MESSAGE; + } else if (error == "m.unknown_message"_ls) { + return REMOTE_UNEXPECTED_MESSAGE; + } else if (error == "m.unknown_transaction"_ls) { + return REMOTE_UNKNOWN_TRANSACTION; + } else if (error == "m.unknown_method"_ls) { + return REMOTE_UNKNOWN_METHOD; + } else if (error == "m.key_mismatch"_ls) { + return REMOTE_KEY_MISMATCH; + } else if (error == "m.user_mismatch"_ls) { + return REMOTE_USER_MISMATCH; + } else if (error == "m.invalid_message"_ls) { + return REMOTE_INVALID_MESSAGE; + } else if (error == "m.accepted"_ls) { + return REMOTE_SESSION_ACCEPTED; + } else if (error == "m.mismatched_commitment"_ls) { + return REMOTE_MISMATCHED_COMMITMENT; + } else if (error == "m.mismatched_sas"_ls) { + return REMOTE_MISMATCHED_SAS; + } + return NONE; +} + +QStringList KeyVerificationSession::commonSupportedMethods(const QStringList& remoteMethods) const +{ + QStringList result; + for (const auto& method : remoteMethods) { + if (m_supportedMethods.contains(method)) { + result += method; + } + } + return result; +} -- cgit v1.2.3 From 1c94d1b41eb352b31b2dc915fea95e26f6138284 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 25 Aug 2022 19:28:10 +0200 Subject: KeyVerificationSession: cleanup - Use std::chrono for the timeout (it's more readable and less ambiguous) and make it a local variable - Only pass a Connection object once to constructors - Ensure buildability even without E2EE (key verification is disabled in that case) - Reorder #includes - Other cleanup following clang-tidy warnings --- lib/keyverificationsession.cpp | 59 ++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 28 deletions(-) (limited to 'lib/keyverificationsession.cpp') diff --git a/lib/keyverificationsession.cpp b/lib/keyverificationsession.cpp index d889b465..1ee489ea 100644 --- a/lib/keyverificationsession.cpp +++ b/lib/keyverificationsession.cpp @@ -1,53 +1,59 @@ // SPDX-FileCopyrightText: 2022 Tobias Fella // SPDX-License-Identifier: LGPL-2.1-or-later -#include "connection.h" #include "keyverificationsession.h" -#include "olm/sas.h" + +#include "connection.h" +#include "database.h" #include "e2ee/qolmaccount.h" #include "e2ee/qolmutils.h" +#include "olm/sas.h" + #include "events/event.h" + #include -#include #include -#include "database.h" +#include + +#include using namespace Quotient; +using namespace std::chrono; -KeyVerificationSession::KeyVerificationSession(const QString& remoteUserId, const KeyVerificationRequestEvent& event, Connection *connection, bool encrypted, QObject* parent) - : QObject(parent) - , m_remoteUserId(remoteUserId) +KeyVerificationSession::KeyVerificationSession( + QString remoteUserId, const KeyVerificationRequestEvent& event, + Connection* connection, bool encrypted) + : QObject(connection) + , m_remoteUserId(std::move(remoteUserId)) , m_remoteDeviceId(event.fromDevice()) , m_transactionId(event.transactionId()) , m_connection(connection) , m_encrypted(encrypted) , m_remoteSupportedMethods(event.methods()) { - auto timeoutTime = std::min(event.timestamp().addSecs(600), - QDateTime::currentDateTime().addSecs(120)); - m_timeout = - timeoutTime.toMSecsSinceEpoch() - QDateTime::currentMSecsSinceEpoch(); - if (m_timeout <= 5000) { - return; - } - init(); - setState(INCOMING); + const auto& currentTime = QDateTime::currentDateTime(); + const auto timeoutTime = + std::min(event.timestamp().addSecs(600), currentTime.addSecs(120)); + const milliseconds timeout{ currentTime.msecsTo(timeoutTime) }; + if (timeout > 5s) + init(timeout); + // Otherwise don't even bother starting up } -KeyVerificationSession::KeyVerificationSession(const QString& userId, const QString& deviceId, Connection* connection, QObject* parent) - : QObject(parent) - , m_remoteUserId(userId) - , m_remoteDeviceId(deviceId) +KeyVerificationSession::KeyVerificationSession(QString userId, QString deviceId, + Connection* connection) + : QObject(connection) + , m_remoteUserId(std::move(userId)) + , m_remoteDeviceId(std::move(deviceId)) , m_transactionId(QUuid::createUuid().toString()) , m_connection(connection) , m_encrypted(false) { - m_timeout = 600000; - init(); + init(600s); QMetaObject::invokeMethod(this, &KeyVerificationSession::sendRequest); } -void KeyVerificationSession::init() +void KeyVerificationSession::init(milliseconds timeout) { connect(m_connection, &Connection::incomingKeyVerificationReady, this, [this](const KeyVerificationReadyEvent& event) { if (event.transactionId() == m_transactionId && event.fromDevice() == m_remoteDeviceId) { @@ -85,10 +91,7 @@ void KeyVerificationSession::init() } }); - QTimer::singleShot(m_timeout, this, [this] { - cancelVerification(TIMEOUT); - }); - + QTimer::singleShot(timeout, this, [this] { cancelVerification(TIMEOUT); }); m_sas = olm_sas(new uint8_t[olm_sas_size()]); auto randomSize = olm_create_sas_random_length(m_sas); @@ -380,7 +383,7 @@ void KeyVerificationSession::handleMac(const KeyVerificationMacEvent& event) } } -void KeyVerificationSession::handleDone(const KeyVerificationDoneEvent& event) +void KeyVerificationSession::handleDone(const KeyVerificationDoneEvent&) { if (state() != DONE) { cancelVerification(UNEXPECTED_MESSAGE); -- cgit v1.2.3 From 376da43a29f3ebad807da2761e7a0c0b105587ec Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 25 Aug 2022 19:58:18 +0200 Subject: More code reorganisation - Common switchOnType() piece for key verification events is factored out into processIfVerificationEvent() - Bare event JSON removed from KeyVerificationSession into constructors of respective events - Connection::sendToDevice() uses assembleEncryptedContent() introduced in the previous commit - commonSupportedMethods() moved out to .cpp; error/string converters made static --- lib/keyverificationsession.cpp | 173 ++++++++++++++++------------------------- 1 file changed, 65 insertions(+), 108 deletions(-) (limited to 'lib/keyverificationsession.cpp') diff --git a/lib/keyverificationsession.cpp b/lib/keyverificationsession.cpp index 1ee489ea..caf5071a 100644 --- a/lib/keyverificationsession.cpp +++ b/lib/keyverificationsession.cpp @@ -20,6 +20,19 @@ using namespace Quotient; using namespace std::chrono; +const QStringList supportedMethods = { SasV1Method }; + +QStringList commonSupportedMethods(const QStringList& remoteMethods) +{ + QStringList result; + for (const auto& method : remoteMethods) { + if (supportedMethods.contains(method)) { + result += method; + } + } + return result; +} + KeyVerificationSession::KeyVerificationSession( QString remoteUserId, const KeyVerificationRequestEvent& event, Connection* connection, bool encrypted) @@ -93,7 +106,7 @@ void KeyVerificationSession::init(milliseconds timeout) QTimer::singleShot(timeout, this, [this] { cancelVerification(TIMEOUT); }); - m_sas = olm_sas(new uint8_t[olm_sas_size()]); + m_sas = olm_sas(new std::byte[olm_sas_size()]); auto randomSize = olm_create_sas_random_length(m_sas); auto random = getRandom(randomSize); olm_create_sas(m_sas, random.data(), randomSize); @@ -104,7 +117,8 @@ void KeyVerificationSession::init(milliseconds timeout) KeyVerificationSession::~KeyVerificationSession() { - delete[] reinterpret_cast(m_sas); + olm_clear_sas(m_sas); + delete[] reinterpret_cast(m_sas); } void KeyVerificationSession::handleKey(const KeyVerificationKeyEvent& event) @@ -160,18 +174,22 @@ void KeyVerificationSession::handleKey(const KeyVerificationKeyEvent& event) emit keyReceived(); } -QByteArray KeyVerificationSession::macInfo(bool verifying, const QString& key) -{ - return (verifying ? "MATRIX_KEY_VERIFICATION_MAC%3%4%1%2%5%6"_ls : "MATRIX_KEY_VERIFICATION_MAC%1%2%3%4%5%6"_ls).arg(m_connection->userId()).arg(m_connection->deviceId()).arg(m_remoteUserId).arg(m_remoteDeviceId).arg(m_transactionId).arg(key).toLatin1(); -} - -QString KeyVerificationSession::calculateMac(const QString& input, bool verifying, const QString& keyId) +QString KeyVerificationSession::calculateMac(const QString& input, + bool verifying, + const QString& keyId) { QByteArray inputBytes = input.toLatin1(); QByteArray outputBytes(olm_sas_mac_length(m_sas), '\0'); - olm_sas_calculate_mac(m_sas, inputBytes.data(), inputBytes.size(), macInfo(verifying, keyId).data(), macInfo(verifying, keyId).size(), outputBytes.data(), outputBytes.size()); - auto output = QString(outputBytes); - return output.left(output.indexOf('=')); + const auto macInfo = + (verifying ? "MATRIX_KEY_VERIFICATION_MAC%3%4%1%2%5%6"_ls + : "MATRIX_KEY_VERIFICATION_MAC%1%2%3%4%5%6"_ls) + .arg(m_connection->userId(), m_connection->deviceId(), + m_remoteUserId, m_remoteDeviceId, m_transactionId, keyId) + .toLatin1(); + olm_sas_calculate_mac(m_sas, inputBytes.data(), inputBytes.size(), + macInfo.data(), macInfo.size(), outputBytes.data(), + outputBytes.size()); + return QString::fromLatin1(outputBytes.data(), outputBytes.indexOf('=')); } void KeyVerificationSession::sendMac() @@ -184,56 +202,37 @@ void KeyVerificationSession::sendMac() auto key = m_connection->olmAccount()->deviceKeys().keys[edKeyId]; mac[edKeyId] = calculateMac(key, false, edKeyId); - auto event = makeEvent(QJsonObject { - {"type", "m.key.verification.mac"}, - {"content", QJsonObject{ - {"transaction_id", m_transactionId}, - {"keys", keys}, - {"mac", mac}, - }} - }); - m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationMacEvent(m_transactionId, keys, + mac), + m_encrypted); setState (macReceived ? DONE : WAITINGFORMAC); } void KeyVerificationSession::sendDone() { - auto event = makeEvent(QJsonObject { - {"type", "m.key.verification.done"}, - {"content", QJsonObject{ - {"transaction_id", m_transactionId}, - }} - }); - m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationDoneEvent(m_transactionId), + m_encrypted); } void KeyVerificationSession::sendKey() { QByteArray keyBytes(olm_sas_pubkey_length(m_sas), '\0'); olm_sas_get_pubkey(m_sas, keyBytes.data(), keyBytes.size()); - QString key = QString(keyBytes); - auto event = makeEvent(QJsonObject { - {"type", "m.key.verification.key"}, - {"content", QJsonObject{ - {"transaction_id", m_transactionId}, - {"key", key}, - }} - }); - m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationKeyEvent(m_transactionId, + keyBytes), + m_encrypted); } void KeyVerificationSession::cancelVerification(Error error) { - auto event = makeEvent(QJsonObject { - {"type", "m.key.verification.cancel"}, - {"content", QJsonObject{ - {"code", errorToString(error)}, - {"reason", errorToString(error)}, - {"transaction_id", m_transactionId} - }} - }); - m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationCancelEvent(m_transactionId, + errorToString(error)), + m_encrypted); setState(CANCELED); setError(error); emit finished(); @@ -249,15 +248,11 @@ void KeyVerificationSession::sendReady() return; } - auto event = makeEvent(QJsonObject { - {"type", "m.key.verification.ready"}, - {"content", QJsonObject { - {"from_device", m_connection->deviceId()}, - {"methods", toJson(methods)}, - {"transaction_id", m_transactionId}, - }} - }); - m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + m_connection->sendToDevice( + m_remoteUserId, m_remoteDeviceId, + KeyVerificationReadyEvent(m_transactionId, m_connection->deviceId(), + methods), + m_encrypted); setState(READY); if (methods.size() == 1) { @@ -268,20 +263,10 @@ void KeyVerificationSession::sendReady() void KeyVerificationSession::sendStartSas() { startSentByUs = true; - auto event = makeEvent(QJsonObject { - {"type", "m.key.verification.start"}, - {"content", QJsonObject { - {"from_device", m_connection->deviceId()}, - {"hashes", QJsonArray {"sha256"}}, - {"key_agreement_protocols", QJsonArray { "curve25519-hkdf-sha256" }}, - {"message_authentication_codes", QJsonArray { "hkdf-hmac-sha256" }}, - {"method", "m.sas.v1"}, - {"short_authentication_string", QJsonArray { "decimal", "emoji" }}, - {"transaction_id", m_transactionId}, - }} - }); - m_startEvent = QJsonDocument(event->contentJson()).toJson(QJsonDocument::Compact); - m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + KeyVerificationStartEvent event(m_transactionId, m_connection->deviceId()); + m_startEvent = QJsonDocument(event.contentJson()).toJson(QJsonDocument::Compact); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + std::move(event), m_encrypted); setState(WAITINGFORACCEPT); } @@ -324,22 +309,10 @@ void KeyVerificationSession::handleStart(const KeyVerificationStartEvent& event) auto commitment = QString(QCryptographicHash::hash((QString(publicKey) % canonicalEvent).toLatin1(), QCryptographicHash::Sha256).toBase64()); commitment = commitment.left(commitment.indexOf('=')); - auto acceptEvent = makeEvent(QJsonObject { - {"type", "m.key.verification.accept"}, - {"content", QJsonObject { - {"commitment", commitment}, - {"hash", "sha256"}, - {"key_agreement_protocol", "curve25519-hkdf-sha256"}, - {"message_authentication_code", "hkdf-hmac-sha256"}, - {"method", "m.sas.v1"}, - {"short_authentication_string", QJsonArray { - "decimal", - "emoji", - }}, - {"transaction_id", m_transactionId}, - }} - }); - m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(acceptEvent), m_encrypted); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationAcceptEvent(m_transactionId, + commitment), + m_encrypted); setState(ACCEPTED); } @@ -417,17 +390,12 @@ QList KeyVerificationSession::sasEmojis() const void KeyVerificationSession::sendRequest() { - QJsonArray methods = toJson(m_supportedMethods); - auto event = makeEvent(QJsonObject { - {"type", "m.key.verification.request"}, - {"content", QJsonObject { - {"from_device", m_connection->deviceId()}, - {"transaction_id", m_transactionId}, - {"methods", methods}, - {"timestamp", QDateTime::currentMSecsSinceEpoch()}, - }}, - }); - m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, std::move(event), m_encrypted); + m_connection->sendToDevice( + m_remoteUserId, m_remoteDeviceId, + KeyVerificationRequestEvent(m_transactionId, m_connection->deviceId(), + supportedMethods, + QDateTime::currentDateTime()), + m_encrypted); setState(WAITINGFORREADY); } @@ -453,7 +421,7 @@ void KeyVerificationSession::setError(Error error) emit errorChanged(); } -QString KeyVerificationSession::errorToString(Error error) const +QString KeyVerificationSession::errorToString(Error error) { switch(error) { case NONE: @@ -485,7 +453,7 @@ QString KeyVerificationSession::errorToString(Error error) const } } -KeyVerificationSession::Error KeyVerificationSession::stringToError(const QString& error) const +KeyVerificationSession::Error KeyVerificationSession::stringToError(const QString& error) { if (error == "m.timeout"_ls) { return REMOTE_TIMEOUT; @@ -514,14 +482,3 @@ KeyVerificationSession::Error KeyVerificationSession::stringToError(const QStrin } return NONE; } - -QStringList KeyVerificationSession::commonSupportedMethods(const QStringList& remoteMethods) const -{ - QStringList result; - for (const auto& method : remoteMethods) { - if (m_supportedMethods.contains(method)) { - result += method; - } - } - return result; -} -- cgit v1.2.3 From 6404b8cd4d57468b810538da04f8017fb13ccc37 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Thu, 25 Aug 2022 19:58:48 +0200 Subject: Refactor the code handling emoji - Use a dedicated structure, EmojiEntry, instead of QVariantMap (it's Q_GADGET, should be readable from QML just fine) - While we're at it, QVector is better than QList in Qt 5.15 - Remove language from the session state - it's used in a single method - Modernise handleKey() code --- lib/keyverificationsession.cpp | 128 +++++++++++++++++++++++++---------------- 1 file changed, 78 insertions(+), 50 deletions(-) (limited to 'lib/keyverificationsession.cpp') diff --git a/lib/keyverificationsession.cpp b/lib/keyverificationsession.cpp index caf5071a..2c468c3e 100644 --- a/lib/keyverificationsession.cpp +++ b/lib/keyverificationsession.cpp @@ -110,9 +110,6 @@ void KeyVerificationSession::init(milliseconds timeout) auto randomSize = olm_create_sas_random_length(m_sas); auto random = getRandom(randomSize); olm_create_sas(m_sas, random.data(), randomSize); - - m_language = QLocale::system().uiLanguages()[0]; - m_language = m_language.left(m_language.indexOf('-')); } KeyVerificationSession::~KeyVerificationSession() @@ -121,18 +118,57 @@ KeyVerificationSession::~KeyVerificationSession() delete[] reinterpret_cast(m_sas); } +struct EmojiStoreEntry : EmojiEntry { + QHash translatedDescriptions; + + explicit EmojiStoreEntry(const QJsonObject& json) + : EmojiEntry{ fromJson(json["emoji"]), + fromJson(json["description"]) } + , translatedDescriptions{ fromJson>( + json["translated_descriptions"]) } + {} +}; + +using EmojiStore = QVector; + +EmojiStore loadEmojiStore() +{ + QFile dataFile(":/sas-emoji.json"); + dataFile.open(QFile::ReadOnly); + return fromJson( + QJsonDocument::fromJson(dataFile.readAll()).array()); +} + +EmojiEntry emojiForCode(int code, const QString& language) +{ + static const EmojiStore emojiStore = loadEmojiStore(); + const auto& entry = emojiStore[code]; + if (!language.isEmpty()) + if (const auto translatedDescription = + emojiStore[code].translatedDescriptions.value(language); + !translatedDescription.isNull()) + return { entry.emoji, translatedDescription }; + + return SLICE(entry, EmojiEntry); +} + void KeyVerificationSession::handleKey(const KeyVerificationKeyEvent& event) { if (state() != WAITINGFORKEY && state() != WAITINGFORVERIFICATION) { cancelVerification(UNEXPECTED_MESSAGE); return; } - olm_sas_set_their_key(m_sas, event.key().toLatin1().data(), event.key().toLatin1().size()); + auto eventKey = event.key().toLatin1(); + olm_sas_set_their_key(m_sas, eventKey.data(), eventKey.size()); if (startSentByUs) { - auto commitment = QString(QCryptographicHash::hash((event.key() % m_startEvent).toLatin1(), QCryptographicHash::Sha256).toBase64()); - commitment = commitment.left(commitment.indexOf('=')); - if (commitment != m_commitment) { + const auto paddedCommitment = + QCryptographicHash::hash((eventKey % m_startEvent).toLatin1(), + QCryptographicHash::Sha256) + .toBase64(); + const QLatin1String unpaddedCommitment(paddedCommitment.constData(), + paddedCommitment.indexOf('=')); + if (unpaddedCommitment != m_commitment) { qCWarning(E2EE) << "Commitment mismatch; aborting verification"; cancelVerification(MISMATCHED_COMMITMENT); return; @@ -142,34 +178,40 @@ void KeyVerificationSession::handleKey(const KeyVerificationKeyEvent& event) } setState(WAITINGFORVERIFICATION); - QByteArray keyBytes(olm_sas_pubkey_length(m_sas), '\0'); - olm_sas_get_pubkey(m_sas, keyBytes.data(), keyBytes.size()); - QString key = QString(keyBytes); - - QByteArray output(6, '\0'); - QString infoTemplate = startSentByUs ? "MATRIX_KEY_VERIFICATION_SAS|%1|%2|%3|%4|%5|%6|%7"_ls : "MATRIX_KEY_VERIFICATION_SAS|%4|%5|%6|%1|%2|%3|%7"_ls; - - auto info = infoTemplate.arg(m_connection->userId()).arg(m_connection->deviceId()).arg(key).arg(m_remoteUserId).arg(m_remoteDeviceId).arg(event.key()).arg(m_transactionId); - olm_sas_generate_bytes(m_sas, info.toLatin1().data(), info.toLatin1().size(), output.data(), output.size()); - - QVector code(7, 0); - const auto& data = (uint8_t *) output.data(); - - code[0] = data[0] >> 2; - code[1] = (data[0] << 4 & 0x3f) | data[1] >> 4; - code[2] = (data[1] << 2 & 0x3f) | data[2] >> 6; - code[3] = data[2] & 0x3f; - code[4] = data[3] >> 2; - code[5] = (data[3] << 4 & 0x3f) | data[4] >> 4; - code[6] = (data[4] << 2 & 0x3f) | data[5] >> 6; - - for (const auto& c : code) { - auto [emoji, description] = emojiForCode(c); - QVariantMap map; - map["emoji"] = emoji; - map["description"] = description; - m_sasEmojis += map; - } + std::string key(olm_sas_pubkey_length(m_sas), '\0'); + olm_sas_get_pubkey(m_sas, key.data(), key.size()); + + std::array output{}; + const auto infoTemplate = + startSentByUs ? "MATRIX_KEY_VERIFICATION_SAS|%1|%2|%3|%4|%5|%6|%7"_ls + : "MATRIX_KEY_VERIFICATION_SAS|%4|%5|%6|%1|%2|%3|%7"_ls; + + const auto info = infoTemplate + .arg(m_connection->userId(), m_connection->deviceId(), + key.data(), m_remoteUserId, m_remoteDeviceId, + eventKey, m_transactionId) + .toLatin1(); + olm_sas_generate_bytes(m_sas, info.data(), info.size(), output.data(), + output.size()); + + static constexpr auto x3f = std::byte{ 0x3f }; + const std::array code{ + output[0] >> 2, + (output[0] << 4 & x3f) | output[1] >> 4, + (output[1] << 2 & x3f) | output[2] >> 6, + output[2] & x3f, + output[3] >> 2, + (output[3] << 4 & x3f) | output[4] >> 4, + (output[4] << 2 & x3f) | output[5] >> 6 + }; + + const auto uiLanguages = QLocale().uiLanguages(); + const auto preferredLanguage = uiLanguages.isEmpty() + ? QString() + : uiLanguages.front().section('-', 0, 0); + for (const auto& c : code) + m_sasEmojis += emojiForCode(std::to_integer(c), preferredLanguage); + emit sasEmojisChanged(); emit keyReceived(); } @@ -369,21 +411,7 @@ void KeyVerificationSession::handleCancel(const KeyVerificationCancelEvent& even setState(CANCELED); } -std::pair KeyVerificationSession::emojiForCode(int code) -{ - static QJsonArray data; - if (data.isEmpty()) { - QFile dataFile(":/sas-emoji.json"); - dataFile.open(QFile::ReadOnly); - data = QJsonDocument::fromJson(dataFile.readAll()).array(); - } - if (data[code].toObject()["translated_descriptions"].toObject().contains(m_language)) { - return {data[code].toObject()["emoji"].toString(), data[code].toObject()["translated_descriptions"].toObject()[m_language].toString()}; - } - return {data[code].toObject()["emoji"].toString(), data[code].toObject()["description"].toString()}; -} - -QList KeyVerificationSession::sasEmojis() const +QVector KeyVerificationSession::sasEmojis() const { return m_sasEmojis; } -- cgit v1.2.3