aboutsummaryrefslogtreecommitdiff
path: root/lib/keyverificationsession.cpp
diff options
context:
space:
mode:
authorTobias Fella <fella@posteo.de>2022-04-18 20:07:12 +0200
committerTobias Fella <fella@posteo.de>2022-05-02 20:25:51 +0200
commit16d4f4e48304543a0ab59b235edba07f5f2c2204 (patch)
tree932845a7b9a6179c4a19ffe6954c6420bf406bbc /lib/keyverificationsession.cpp
parent3a81aea545ec014d597f3756c362913aad4421df (diff)
downloadlibquotient-16d4f4e48304543a0ab59b235edba07f5f2c2204.tar.gz
libquotient-16d4f4e48304543a0ab59b235edba07f5f2c2204.zip
Implement key verification
Diffstat (limited to 'lib/keyverificationsession.cpp')
-rw-r--r--lib/keyverificationsession.cpp522
1 files changed, 522 insertions, 0 deletions
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 <fella@posteo.de>
+// 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 <QtCore/QCryptographicHash>
+#include <QtCore/QUuid>
+#include <QtCore/QTimer>
+#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<long int>(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<uint8_t*>(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<uint8_t> 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<KeyVerificationMacEvent>(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<KeyVerificationDoneEvent>(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<KeyVerificationKeyEvent>(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<KeyVerificationCancelEvent>(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<KeyVerificationReadyEvent>(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<KeyVerificationStartEvent>(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<KeyVerificationAcceptEvent>(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<QString, QString> 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<QVariantMap> KeyVerificationSession::sasEmojis() const
+{
+ return m_sasEmojis;
+}
+
+void KeyVerificationSession::sendRequest()
+{
+ QJsonArray methods = toJson(m_supportedMethods);
+ auto event = makeEvent<KeyVerificationRequestEvent>(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;
+}