diff options
author | Alexey Rusakov <Kitsune-Ral@users.sf.net> | 2022-08-26 08:34:09 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-08-26 08:34:09 +0200 |
commit | b257cf4e38d540a0063f06ebdb51dcd2574cc809 (patch) | |
tree | 68e43acc8a0577110c0d03b8d04265622a895a3e /lib/keyverificationsession.cpp | |
parent | 4ad2f6e165a4eb486155eae652e187dc4d6b7d99 (diff) | |
parent | 6404b8cd4d57468b810538da04f8017fb13ccc37 (diff) | |
download | libquotient-b257cf4e38d540a0063f06ebdb51dcd2574cc809.tar.gz libquotient-b257cf4e38d540a0063f06ebdb51dcd2574cc809.zip |
Merge #547: Implement device verification
Diffstat (limited to 'lib/keyverificationsession.cpp')
-rw-r--r-- | lib/keyverificationsession.cpp | 512 |
1 files changed, 512 insertions, 0 deletions
diff --git a/lib/keyverificationsession.cpp b/lib/keyverificationsession.cpp new file mode 100644 index 00000000..2c468c3e --- /dev/null +++ b/lib/keyverificationsession.cpp @@ -0,0 +1,512 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "keyverificationsession.h" + +#include "connection.h" +#include "database.h" +#include "e2ee/qolmaccount.h" +#include "e2ee/qolmutils.h" +#include "olm/sas.h" + +#include "events/event.h" + +#include <QtCore/QCryptographicHash> +#include <QtCore/QTimer> +#include <QtCore/QUuid> + +#include <chrono> + +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) + : 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()) +{ + 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(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) +{ + init(600s); + QMetaObject::invokeMethod(this, &KeyVerificationSession::sendRequest); +} + +void KeyVerificationSession::init(milliseconds timeout) +{ + 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(timeout, this, [this] { cancelVerification(TIMEOUT); }); + + 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); +} + +KeyVerificationSession::~KeyVerificationSession() +{ + olm_clear_sas(m_sas); + delete[] reinterpret_cast<std::byte*>(m_sas); +} + +struct EmojiStoreEntry : EmojiEntry { + QHash<QString, QString> translatedDescriptions; + + explicit EmojiStoreEntry(const QJsonObject& json) + : EmojiEntry{ fromJson<QString>(json["emoji"]), + fromJson<QString>(json["description"]) } + , translatedDescriptions{ fromJson<QHash<QString, QString>>( + json["translated_descriptions"]) } + {} +}; + +using EmojiStore = QVector<EmojiStoreEntry>; + +EmojiStore loadEmojiStore() +{ + QFile dataFile(":/sas-emoji.json"); + dataFile.open(QFile::ReadOnly); + return fromJson<EmojiStore>( + 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; + } + auto eventKey = event.key().toLatin1(); + olm_sas_set_their_key(m_sas, eventKey.data(), eventKey.size()); + + if (startSentByUs) { + 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; + } + } else { + sendKey(); + } + setState(WAITINGFORVERIFICATION); + + std::string key(olm_sas_pubkey_length(m_sas), '\0'); + olm_sas_get_pubkey(m_sas, key.data(), key.size()); + + std::array<std::byte, 6> 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<std::byte, 7> 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<int>(c), preferredLanguage); + + emit sasEmojisChanged(); + emit keyReceived(); +} + +QString KeyVerificationSession::calculateMac(const QString& input, + bool verifying, + const QString& keyId) +{ + QByteArray inputBytes = input.toLatin1(); + QByteArray outputBytes(olm_sas_mac_length(m_sas), '\0'); + 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() +{ + 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); + + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationMacEvent(m_transactionId, keys, + mac), + m_encrypted); + setState (macReceived ? DONE : WAITINGFORMAC); +} + +void KeyVerificationSession::sendDone() +{ + 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()); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationKeyEvent(m_transactionId, + keyBytes), + m_encrypted); +} + + +void KeyVerificationSession::cancelVerification(Error error) +{ + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationCancelEvent(m_transactionId, + errorToString(error)), + m_encrypted); + setState(CANCELED); + setError(error); + emit finished(); + deleteLater(); +} + +void KeyVerificationSession::sendReady() +{ + auto methods = commonSupportedMethods(m_remoteSupportedMethods); + + if (methods.isEmpty()) { + cancelVerification(UNKNOWN_METHOD); + return; + } + + m_connection->sendToDevice( + m_remoteUserId, m_remoteDeviceId, + KeyVerificationReadyEvent(m_transactionId, m_connection->deviceId(), + methods), + m_encrypted); + setState(READY); + + if (methods.size() == 1) { + sendStartSas(); + } +} + +void KeyVerificationSession::sendStartSas() +{ + startSentByUs = true; + 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); +} + +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('=')); + + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationAcceptEvent(m_transactionId, + commitment), + 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&) +{ + if (state() != DONE) { + cancelVerification(UNEXPECTED_MESSAGE); + } +} + +void KeyVerificationSession::handleCancel(const KeyVerificationCancelEvent& event) +{ + setError(stringToError(event.code())); + setState(CANCELED); +} + +QVector<EmojiEntry> KeyVerificationSession::sasEmojis() const +{ + return m_sasEmojis; +} + +void KeyVerificationSession::sendRequest() +{ + m_connection->sendToDevice( + m_remoteUserId, m_remoteDeviceId, + KeyVerificationRequestEvent(m_transactionId, m_connection->deviceId(), + supportedMethods, + QDateTime::currentDateTime()), + 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) +{ + 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) +{ + 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; +} |