Page MenuHomePhorge

D289.1772516621.diff
No OneTemporary

Size
38 KB
Referenced Files
None
Subscribers
None

D289.1772516621.diff

diff --git a/src/crypto/CMakeLists.txt b/src/crypto/CMakeLists.txt
--- a/src/crypto/CMakeLists.txt
+++ b/src/crypto/CMakeLists.txt
@@ -8,6 +8,8 @@
base64.cpp
sha256.cpp
key-export.cpp
+ verification-process.cpp
+ sas-desc.cpp
)
add_library(kazvcrypto ${kazvcrypto_SRCS})
diff --git a/src/crypto/sas-desc.hpp b/src/crypto/sas-desc.hpp
new file mode 100644
--- /dev/null
+++ b/src/crypto/sas-desc.hpp
@@ -0,0 +1,114 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libkazv-config.hpp>
+#include "crypto-util.hpp"
+#include <copy-helper.hpp>
+#include <immer/array.hpp>
+#include <set>
+
+namespace Kazv
+{
+ class SasDesc
+ {
+ public:
+ static std::size_t constructRandomSize();
+
+ /**
+ * Construct an invalid SAS description.
+ */
+ SasDesc();
+
+ /**
+ * Construct a random SAS description.
+ */
+ SasDesc(RandomTag, RandomData r);
+
+ SasDesc(SasDesc &&that);
+
+ SasDesc &operator=(SasDesc &&that);
+
+ ~SasDesc();
+
+ /// Replace this with a random SAS description
+ void emplace(RandomTag, RandomData r);
+
+ /// @return Whether this SAS is in a valid state.
+ bool valid() const;
+
+ std::string publicKey() const;
+
+ /**
+ * Set the key of the other party.
+ */
+ void setTheirKey(std::string theirPublicKey);
+
+ std::pair<immer::array<int> /* emoji */, immer::array<int> /* decimal */> getDisplayCodesCurve25519HkdfSha256(
+ std::string starterUserId,
+ std::string starterDeviceId,
+ std::string starterKey,
+ std::string accepterUserId,
+ std::string accepterDeviceId,
+ std::string accepterKey,
+ std::string txnId
+ ) const;
+
+ /**
+ * Get the mac for a key using hkdf-hmac-sha256.v2.
+ *
+ * @param key The unpadded base64-encoded public key.
+ * @param keyId The key id for the key.
+ */
+ std::string getKeyMacHkdfHMacSha256V2(
+ std::string key,
+ std::string ourUserId,
+ std::string ourDeviceId,
+ std::string theirUserId,
+ std::string theirDeviceId,
+ std::string txnId,
+ std::string keyId
+ ) const;
+
+ bool verifyKeyMacHkdfHMacSha256V2(
+ std::string mac,
+ std::string key,
+ std::string ourUserId,
+ std::string ourDeviceId,
+ std::string theirUserId,
+ std::string theirDeviceId,
+ std::string txnId,
+ std::string keyId
+ ) const;
+
+ /**
+ * Get the mac for a key list using hkdf-hmac-sha256.v2.
+ *
+ * @param keyList A set containing the key ids.
+ */
+ std::string getKeyListMacHkdfHMacSha256V2(
+ std::set<std::string> keyList,
+ std::string ourUserId,
+ std::string ourDeviceId,
+ std::string theirUserId,
+ std::string theirDeviceId,
+ std::string txnId
+ ) const;
+
+ bool verifyKeyListMacHkdfHMacSha256V2(
+ std::string mac,
+ std::set<std::string> keyList,
+ std::string ourUserId,
+ std::string ourDeviceId,
+ std::string theirUserId,
+ std::string theirDeviceId,
+ std::string txnId
+ ) const;
+
+ private:
+ struct Private;
+ std::unique_ptr<Private> m_d;
+ };
+}
diff --git a/src/crypto/sas-desc.cpp b/src/crypto/sas-desc.cpp
new file mode 100644
--- /dev/null
+++ b/src/crypto/sas-desc.cpp
@@ -0,0 +1,225 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include "sas-desc.hpp"
+#include <crypto-util-p.hpp>
+#include <debug.hpp>
+#include <vodozemac.h>
+
+namespace Kazv
+{
+ struct SasDesc::Private
+ {
+ Private(RandomTag)
+ : sas(vodozemac::sas::new_sas())
+ , publicKey(sas.value()->public_key()->to_base64())
+ , establishedSas(std::nullopt)
+ {
+ }
+ std::optional<rust::Box<vodozemac::sas::Sas>> sas;
+ std::string publicKey;
+ std::optional<rust::Box<vodozemac::sas::EstablishedSas>> establishedSas;
+ };
+
+ SasDesc::SasDesc()
+ : m_d(nullptr)
+ {}
+
+ SasDesc::SasDesc(RandomTag, RandomData)
+ : m_d(std::make_unique<Private>(RandomTag{}))
+ {
+ }
+
+ SasDesc::SasDesc(SasDesc &&that) = default;
+
+ SasDesc &SasDesc::operator=(SasDesc &&that) = default;
+
+ std::size_t SasDesc::constructRandomSize()
+ {
+ return 0;
+ }
+
+ SasDesc::~SasDesc() = default;
+
+ bool SasDesc::valid() const
+ {
+ return !!m_d && (m_d->sas || m_d->establishedSas);
+ }
+
+ void SasDesc::emplace(RandomTag, RandomData)
+ {
+ m_d = std::make_unique<Private>(RandomTag{});
+ }
+
+ std::string SasDesc::publicKey() const
+ {
+ if (!valid()) {
+ return std::string();
+ }
+ return m_d->publicKey;
+ }
+
+ void SasDesc::setTheirKey(std::string theirPublicKey)
+ {
+ if (!(m_d && m_d->sas)) {
+ return;
+ }
+ m_d->establishedSas = checkVodozemacError([&]() {
+ auto key = vodozemac::types::curve_key_from_base64(theirPublicKey);
+ auto sas = std::move(m_d->sas).value();
+ return sas->diffie_hellman(*key);
+ });
+ }
+
+ std::pair<immer::array<int> /* emoji */, immer::array<int> /* decimal */> SasDesc::getDisplayCodesCurve25519HkdfSha256(
+ std::string starterUserId,
+ std::string starterDeviceId,
+ std::string starterKey,
+ std::string accepterUserId,
+ std::string accepterDeviceId,
+ std::string accepterKey,
+ std::string txnId
+ ) const
+ {
+ if (!(m_d && m_d->establishedSas)) {
+ kzo.crypto.warn() << "no establishedSas" << std::endl;
+ return {{}, {}};
+ }
+ auto bytes = checkVodozemacError([&]() {
+ return m_d->establishedSas.value()->bytes(
+ "MATRIX_KEY_VERIFICATION_SAS|"
+ + starterUserId + "|"
+ + starterDeviceId + "|"
+ + starterKey + "|"
+ + accepterUserId + "|"
+ + accepterDeviceId + "|"
+ + accepterKey + "|"
+ + txnId
+ );
+ });
+ if (!bytes) {
+ kzo.crypto.warn() << "vodozemac exception" << bytes.reason() << std::endl;
+ return {{}, {}};
+ }
+ auto emoji = bytes.value()->emoji_indices();
+ auto decimals = bytes.value()->decimals();
+ return {
+ immer::array<int>(emoji.begin(), emoji.end()),
+ immer::array<int>(decimals.begin(), decimals.end()),
+ };
+ }
+
+ std::string SasDesc::getKeyMacHkdfHMacSha256V2(
+ std::string key,
+ std::string ourUserId,
+ std::string ourDeviceId,
+ std::string theirUserId,
+ std::string theirDeviceId,
+ std::string txnId,
+ std::string keyId
+ ) const
+ {
+ // https://spec.matrix.org/v1.17/client-server-api/#mac-calculation
+ auto info = "MATRIX_KEY_VERIFICATION_MAC"
+ + ourUserId
+ + ourDeviceId
+ + theirUserId
+ + theirDeviceId
+ + txnId
+ + keyId;
+ auto mac = m_d->establishedSas.value()->calculate_mac(key, info)->to_base64();
+ return std::string(mac);
+ }
+
+ bool SasDesc::verifyKeyMacHkdfHMacSha256V2(
+ std::string mac,
+ std::string key,
+ std::string ourUserId,
+ std::string ourDeviceId,
+ std::string theirUserId,
+ std::string theirDeviceId,
+ std::string txnId,
+ std::string keyId
+ ) const
+ {
+ // https://spec.matrix.org/v1.17/client-server-api/#mac-calculation
+ auto info = "MATRIX_KEY_VERIFICATION_MAC"
+ + theirUserId
+ + theirDeviceId
+ + ourUserId
+ + ourDeviceId
+ + txnId
+ + keyId;
+ auto res = checkVodozemacError([this, &key, &info, &mac]() {
+ m_d->establishedSas.value()->verify_mac(key, info, *vodozemac::sas::mac_from_base64(mac));
+ return true;
+ });
+ return res.has_value();
+ }
+
+ static std::string keyListToStr(const std::set<std::string> &keyList)
+ {
+ if (keyList.empty()) {
+ return std::string();
+ }
+ return std::accumulate(
+ keyList.begin(),
+ keyList.end(),
+ std::string(),
+ [](std::string acc, std::string cur) {
+ std::string joiner = acc.size() ? "," : "";
+ return std::move(acc) + joiner + std::move(cur);
+ });
+ }
+
+ std::string SasDesc::getKeyListMacHkdfHMacSha256V2(
+ std::set<std::string> keyList,
+ std::string ourUserId,
+ std::string ourDeviceId,
+ std::string theirUserId,
+ std::string theirDeviceId,
+ std::string txnId
+ ) const
+ {
+ auto info = "MATRIX_KEY_VERIFICATION_MAC"
+ + ourUserId
+ + ourDeviceId
+ + theirUserId
+ + theirDeviceId
+ + txnId
+ + "KEY_IDS";
+ auto str = keyListToStr(keyList);
+ auto mac = m_d->establishedSas.value()->calculate_mac(str, info)->to_base64();
+ return std::string(mac);
+ }
+
+ bool SasDesc::verifyKeyListMacHkdfHMacSha256V2(
+ std::string mac,
+ std::set<std::string> keyList,
+ std::string ourUserId,
+ std::string ourDeviceId,
+ std::string theirUserId,
+ std::string theirDeviceId,
+ std::string txnId
+ ) const
+ {
+ // https://spec.matrix.org/v1.17/client-server-api/#mac-calculation
+ auto info = "MATRIX_KEY_VERIFICATION_MAC"
+ + theirUserId
+ + theirDeviceId
+ + ourUserId
+ + ourDeviceId
+ + txnId
+ + "KEY_IDS";
+ auto str = keyListToStr(keyList);
+ auto res = checkVodozemacError([this, &str, &info, &mac]() {
+ m_d->establishedSas.value()->verify_mac(str, info, *vodozemac::sas::mac_from_base64(mac));
+ return true;
+ });
+ return res.has_value();
+ }
+
+}
diff --git a/src/crypto/verification-process.hpp b/src/crypto/verification-process.hpp
new file mode 100644
--- /dev/null
+++ b/src/crypto/verification-process.hpp
@@ -0,0 +1,194 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libkazv-config.hpp>
+#include "sas-desc.hpp"
+#include <crypto-util.hpp>
+#include <types.hpp>
+#include <immer/array.hpp>
+
+namespace Kazv
+{
+ struct VerificationSendEvent
+ {
+ Event event;
+ friend bool operator==(const VerificationSendEvent &a, const VerificationSendEvent &b) = default;
+ };
+
+ struct VerificationDisplayCodes
+ {
+ /// The indices of emojis to display
+ /// They can be converted to emojis using the table at
+ /// https://spec.matrix.org/v1.17/client-server-api/#sas-method-emoji
+ immer::array<int> emojiIndices;
+ /// The numbers to display
+ immer::array<int> decimalCode;
+ friend bool operator==(const VerificationDisplayCodes &a, const VerificationDisplayCodes &b) = default;
+ };
+
+ struct VerificationShowStatus
+ {
+ enum Status
+ {
+ /// A party has cancelled
+ Cancelled,
+ /// We have verified the other party's keys
+ VerifiedThem,
+ /// Both parties have verified each other
+ VerifiedBoth,
+ /// Verification failed
+ VerificationFailed,
+ /// Someone requested us to verify
+ Requested,
+ /// We requested to verify someone else, and are waiting
+ /// for them to accept the request
+ Waiting,
+ };
+
+ Status status;
+ friend bool operator==(const VerificationShowStatus &a, const VerificationShowStatus &b) = default;
+ };
+
+ using VerificationMessage = std::variant<
+ VerificationSendEvent,
+ VerificationDisplayCodes,
+ VerificationShowStatus
+ >;
+ using VerificationMessages = immer::flex_vector<VerificationMessage>;
+
+ struct VerificationProcess
+ {
+ enum Party {
+ Us,
+ Them,
+ };
+ inline static const immer::flex_vector<std::string> supportedMethods = {"m.sas.v1"};
+ inline static const immer::flex_vector<std::string> supportedHashes = {"sha256"};
+ inline static const immer::flex_vector<std::string> supportedKeyAgreementProtocols = {"curve25519-hkdf-sha256"};
+ inline static const immer::flex_vector<std::string> supportedMessageAuthenticationCodes = {"hkdf-hmac-sha256.v2"};
+ inline static const immer::flex_vector<std::string> defaultShortAuthenticationString = {"emoji", "decimal"};
+
+ VerificationProcess(std::string ourUserId, std::string ourDeviceId, std::string theirUserId, std::string theirDeviceId, std::string ourDeviceKey);
+
+ std::string ourUserId;
+ std::string ourDeviceId;
+ std::string theirUserId;
+ std::string theirDeviceId;
+ /// A list of events that have been transmitted between the parties.
+ immer::flex_vector<std::pair<Party, Event>> events;
+ bool confirmedMatch{false};
+ std::string ourDeviceKey;
+ std::string theirDeviceKey;
+
+ SasDesc sas;
+
+ /// @return Whether the process is finished.
+ bool finished() const;
+
+ // Modification functions
+ /**
+ * Process an incoming event.
+ */
+ [[nodiscard]] VerificationMessages processIncoming(Event e);
+
+ /**
+ * Add an outgoing event to the process.
+ */
+ void addOutgoing(Event e);
+
+ /**
+ * Set the device key of the other party.
+ */
+ void setTheirDeviceKey(std::string key);
+
+ /**
+ * Signal that the user is ready for an incoming verification.
+ */
+ [[nodiscard]] VerificationMessages userReady();
+
+ /**
+ * Signal that the user has confirmed that the codes match.
+ */
+ [[nodiscard]] VerificationMessages userConfirmMatch();
+
+ /**
+ * Get the transaction id for this process.
+ */
+ [[nodiscard]] std::string txnId() const;
+
+ /**
+ * Make a m.key.verification.ready event for this verification process.
+ *
+ * It is the caller's responsibility to use AddOutgoingVerificationEventAction
+ * to add the event after it has been sent.
+ */
+ [[nodiscard]] Event makeReadyEvent() const;
+
+ /**
+ * Make a m.key.verification.start event for this verification process.
+ *
+ * It is the caller's responsibility to use AddOutgoingVerificationEventAction
+ * to add the event after it has been sent.
+ *
+ * @param method The method to use for verification.
+ */
+ [[nodiscard]] Event makeStartEvent(std::string method) const;
+
+ /**
+ * Make a m.key.verification.accept event with method equal to `m.sas.v1`
+ * for this verification process.
+ *
+ * This function assumes that the last event is a m.key.verification.start
+ * event.
+ */
+ [[nodiscard]] Event makeSasAcceptEvent();
+
+ /**
+ * Make a m.key.verification.key event for sas.
+ *
+ * This function assumes that the last event is a m.key.verification.accept
+ * event if we started the process, or a m.key.verification.key event
+ * if they started the process.
+ */
+ [[nodiscard]] Event makeSasKeyEvent();
+
+ /**
+ * Make a m.key.verification.mac event for sas.
+ *
+ * This function assumes that the last event is a m.key.verification.key
+ * event if we started the process, or a m.key.verification.mac event
+ * if they started the process.
+ */
+ [[nodiscard]] Event makeSasMacEvent();
+
+ /**
+ * Get the start event to use for this process.
+ *
+ * If two start events are present, the one to be used is resolved
+ * through the algorithm as per the spec.
+ */
+ [[nodiscard]] std::pair<Party, Event> getStartEvent() const;
+
+ /// Get the party whose start event is used.
+ [[nodiscard]] Party getStartingParty() const;
+
+ /// Add the commitment to the content of the accept event.
+ [[nodiscard]] json addCommitmentToAcceptContent(json content, Event startEvent);
+
+ /// Verify the commitment in the accept event.
+ [[nodiscard]] bool verifySasCommitment();
+
+ /// Get the codes to display to the user.
+ /// This function assumes that the SasDesc already has keys from both parties.
+ [[nodiscard]] VerificationDisplayCodes makeDisplayCodes();
+
+ /// Get the key from the other party from a m.key.verification.key event.
+ [[nodiscard]] std::string getTheirKey() const;
+
+ /// Verify the key mac and return the corresponding done/cancel event.
+ [[nodiscard]] VerificationMessages verifyKeyMac(Event macEvent);
+ };
+}
diff --git a/src/crypto/verification-process.cpp b/src/crypto/verification-process.cpp
new file mode 100644
--- /dev/null
+++ b/src/crypto/verification-process.cpp
@@ -0,0 +1,369 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include "verification-process.hpp"
+#include "sha256.hpp"
+#include <cursorutil.hpp>
+#include <debug.hpp>
+#include <lager/util.hpp>
+#include <zug/transducer/filter.hpp>
+
+namespace Kazv
+{
+ /// Get the type of the verification event
+ static std::string typeOfVEvent(const Event &e)
+ {
+ return e.type();
+ }
+
+ /// Get the transaction id of the verification event
+ static std::string txnIdOfVEvent(const Event &e)
+ {
+ const auto c = e.content().get();
+ return c.contains("transaction_id") ? c["transaction_id"] : "";
+ }
+
+ static Event makeVEvent(std::string type, std::string txnId, json content)
+ {
+ content["transaction_id"] = txnId;
+ return Event{json{
+ {"type", type},
+ {"content", content},
+ }};
+ }
+
+ template<class Cont1, class Cont2, class Conv>
+ static auto firstIn(Cont1 &&wanted, Cont2 &&supported, Conv &&conv) -> std::optional<std::decay_t<decltype(supported[0])>>
+ {
+ auto it = std::find_if(supported.begin(), supported.end(), [&conv, &wanted](const auto &v) {
+ return std::find(wanted.begin(), wanted.end(), conv(v)) != wanted.end();
+ });
+ if (it == supported.end()) {
+ return std::nullopt;
+ } else {
+ return *it;
+ }
+ }
+
+ static json strToJsonValue(const std::string &s)
+ {
+ return json(s);
+ }
+
+ VerificationProcess::VerificationProcess(std::string ourUserId, std::string ourDeviceId, std::string theirUserId, std::string theirDeviceId, std::string ourDeviceKey)
+ : ourUserId(ourUserId)
+ , ourDeviceId(ourDeviceId)
+ , theirUserId(theirUserId)
+ , theirDeviceId(theirDeviceId)
+ , ourDeviceKey(ourDeviceKey)
+ {}
+
+ void VerificationProcess::setTheirDeviceKey(std::string key)
+ {
+ theirDeviceKey = key;
+ }
+
+ VerificationMessages VerificationProcess::processIncoming(Event e)
+ {
+ events = std::move(events).push_back({Them, e});
+ auto type = typeOfVEvent(e);
+ kzo.crypto.dbg() << "Handling event:" << e.raw().get().dump() << std::endl;
+ if (type == "m.key.verification.request") {
+ return {VerificationShowStatus{VerificationShowStatus::Requested}};
+ } else if (type == "m.key.verification.ready") {
+ try {
+ auto methods = e.content().get().at("methods");
+ auto selected = firstIn(methods, VerificationProcess::supportedMethods, strToJsonValue);
+ if (selected) {
+ return {VerificationSendEvent{makeStartEvent(selected.value())}};
+ }
+ } catch (const std::exception &e) {
+
+ }
+ } else if (type == "m.key.verification.start") {
+ try {
+ auto method = e.content().get().at("method");
+ if (method == "m.sas.v1") {
+ return {VerificationSendEvent{makeSasAcceptEvent()}};
+ }
+ } catch (const std::exception &e) {
+ }
+ } else if (type == "m.key.verification.accept") {
+ try {
+ auto method = e.content().get().at("method");
+ if (method == "m.sas.v1") {
+ return {VerificationSendEvent{makeSasKeyEvent()}};
+ }
+ } catch (const std::exception &e) {
+ }
+ } else if (type == "m.key.verification.key") {
+ try {
+ auto starter = getStartingParty();
+ if (starter == Them) {
+ // us <-start-- them
+ // -accept->
+ // <- key --
+ auto keyEvent = makeSasKeyEvent();
+ return {makeDisplayCodes(), VerificationSendEvent(keyEvent)};
+ } else {
+ // us --start-> them
+ // <-accept-
+ // -- key ->
+ // <- key --
+ // From this point, ask the user to verify the codes match.
+ // Only after the user confirmed the match
+ // do we begin to send mac events.
+ if (verifySasCommitment()) {
+ return {makeDisplayCodes()};
+ } else {
+ // TODO: send error
+ return {};
+ }
+ }
+ } catch (const std::exception &e) {
+ kzo.crypto.dbg() << "Error handling key event:" << e.what() << std::endl;
+ }
+ } else if (type == "m.key.verification.mac") {
+ try {
+ return verifyKeyMac(e);
+ } catch (const std::exception &e) {
+ }
+ } else if (type == "m.key.verification.done") {
+ return {VerificationShowStatus{VerificationShowStatus::VerifiedBoth}};
+ }
+ return {};
+ }
+
+ void VerificationProcess::addOutgoing(Event e)
+ {
+ events = std::move(events).push_back({Us, std::move(e)});
+ }
+
+ VerificationMessages VerificationProcess::userReady()
+ {
+ return {
+ VerificationSendEvent{makeReadyEvent()},
+ VerificationShowStatus{VerificationShowStatus::Waiting},
+ };
+ }
+
+ VerificationMessages VerificationProcess::userConfirmMatch()
+ {
+ confirmedMatch = true;
+ return {
+ VerificationSendEvent{makeSasMacEvent()},
+ };
+ }
+
+ std::string VerificationProcess::txnId() const
+ {
+ return txnIdOfVEvent(events[0].second);
+ }
+
+ Event VerificationProcess::makeReadyEvent() const
+ {
+ return makeVEvent("m.key.verification.ready", txnId(), json{
+ {"from_device", ourDeviceId},
+ {"methods", supportedMethods},
+ });
+ }
+
+ Event VerificationProcess::makeStartEvent(std::string method) const
+ {
+ if (method == "m.sas.v1") {
+ return makeVEvent("m.key.verification.start", txnId(), json{
+ {"from_device", ourDeviceId},
+ {"method", std::move(method)},
+ {"hashes", supportedHashes},
+ {"key_agreement_protocols", supportedKeyAgreementProtocols},
+ {"message_authentication_codes", supportedMessageAuthenticationCodes},
+ {"short_authentication_string", defaultShortAuthenticationString},
+ });
+ } else {
+ return Event();
+ }
+ }
+
+ Event VerificationProcess::makeSasAcceptEvent()
+ {
+ auto [party, lastEvent] = getStartEvent();
+ auto lastContent = lastEvent.content().get();
+ auto content = addCommitmentToAcceptContent(json{
+ {"from_device", ourDeviceId},
+ {"method", "m.sas.v1"},
+ {"hash", firstIn(lastContent.at("hashes"), supportedHashes, strToJsonValue)},
+ {"key_agreement_protocol", firstIn(lastContent.at("key_agreement_protocols"), supportedKeyAgreementProtocols, strToJsonValue)},
+ {"message_authentication_code", firstIn(lastContent.at("message_authentication_codes"), supportedMessageAuthenticationCodes, strToJsonValue)},
+ {"short_authentication_string", defaultShortAuthenticationString},
+ }, lastEvent);
+ return makeVEvent("m.key.verification.accept", txnId(), content);
+ }
+
+ Event VerificationProcess::makeSasKeyEvent()
+ {
+ auto [lastSender, lastEvent] = events.back();
+ auto type = typeOfVEvent(lastEvent);
+ if (type == "m.key.verification.accept" || type == "m.key.verification.key") {
+ if (!sas.valid()) {
+ sas.emplace(RandomTag{}, {});
+ }
+ auto publicKey = sas.publicKey();
+ auto content = json{
+ {"key", publicKey},
+ };
+ return makeVEvent("m.key.verification.key", txnId(), content);
+ } else {
+ return Event();
+ }
+ }
+
+ Event VerificationProcess::makeSasMacEvent()
+ {
+ auto makeMac = [this](auto &&macFunc, auto k, auto &&...as) {
+ return std::invoke(macFunc,
+ sas, k,
+ ourUserId, ourDeviceId, theirUserId, theirDeviceId,
+ txnId(),
+ std::forward<decltype(as)>(as)...
+ );
+ };
+ if (confirmedMatch) {
+ auto deviceKeyId = "ed25519:" + ourDeviceId;
+ auto keyList = std::set<std::string>{deviceKeyId};
+ auto content = json{
+ {"mac", {
+ {deviceKeyId, makeMac(&SasDesc::getKeyMacHkdfHMacSha256V2, ourDeviceKey, deviceKeyId)},
+ }},
+ {"keys", makeMac(&SasDesc::getKeyListMacHkdfHMacSha256V2, keyList)},
+ };
+ return makeVEvent("m.key.verification.mac", txnId(), content);
+ } else {
+ return Event();
+ }
+ }
+
+ auto VerificationProcess::getStartEvent() const -> std::pair<Party, Event>
+ {
+ auto startEvents = intoImmer(immer::flex_vector<std::pair<Party, Event>>{}, zug::filter([](const auto &p) {
+ return typeOfVEvent(p.second) == "m.key.verification.start";
+ }), events);
+ if (startEvents.empty()) {
+ return std::make_pair(Us, Event());
+ }
+ auto minPair = std::min_element(startEvents.begin(), startEvents.end(), [this](const auto &a, const auto &b) {
+ auto aKey = a.first == Us ? std::make_pair(ourUserId, ourDeviceId) : std::make_pair(theirUserId, theirDeviceId);
+ auto bKey = b.first == Us ? std::make_pair(ourUserId, ourDeviceId) : std::make_pair(theirUserId, theirDeviceId);
+ return aKey < bKey;
+ });
+ return *minPair;
+ }
+
+ auto VerificationProcess::getStartingParty() const -> Party
+ {
+ return getStartEvent().first;
+ }
+
+ json VerificationProcess::addCommitmentToAcceptContent(json content, Event startEvent)
+ {
+ SHA256Desc hash;
+ sas.emplace(RandomTag{}, {});
+ hash.processInPlace(sas.publicKey());
+ hash.processInPlace(startEvent.content().get().dump());
+ content["commitment"] = hash.get();
+ return content;
+ }
+
+ bool VerificationProcess::verifySasCommitment()
+ {
+ auto theirAcceptEventIt = std::find_if(events.begin(), events.end(), [](const auto &p) {
+ return p.first == Them && typeOfVEvent(p.second) == "m.key.verification.accept";
+ });
+ if (theirAcceptEventIt == events.end()) {
+ kzo.crypto.warn() << "No accept event found" << std::endl;
+ return false;
+ }
+ auto acceptEvent = theirAcceptEventIt->second;
+ SHA256Desc hash;
+ hash.processInPlace(getTheirKey());
+ hash.processInPlace(getStartEvent().second.content().get().dump());
+ auto commitment = hash.get();
+
+ return commitment == acceptEvent.content().get().at("commitment").template get<std::string>();
+ }
+
+ VerificationDisplayCodes VerificationProcess::makeDisplayCodes()
+ {
+ auto starter = getStartingParty();
+ auto starterUserId = starter == Us ? ourUserId : theirUserId;
+ auto starterDeviceId = starter == Us ? ourDeviceId : theirDeviceId;
+ auto starterKey = starter == Us ? sas.publicKey() : getTheirKey();
+ auto accepterUserId = starter == Us ? theirUserId : ourUserId;
+ auto accepterDeviceId = starter == Us ? theirDeviceId : ourDeviceId;
+ auto accepterKey = starter == Us ? getTheirKey() : sas.publicKey();
+ sas.setTheirKey(getTheirKey());
+ auto [emoji, decimal] = sas.getDisplayCodesCurve25519HkdfSha256(
+ starterUserId,
+ starterDeviceId,
+ starterKey,
+ accepterUserId,
+ accepterDeviceId,
+ accepterKey,
+ txnId()
+ );
+ return {emoji, decimal};
+ }
+
+ std::string VerificationProcess::getTheirKey() const
+ {
+ auto it = std::find_if(events.begin(), events.end(), [](const auto &p) {
+ return p.first == Them && typeOfVEvent(p.second) == "m.key.verification.key";
+ });
+ if (it == events.end()) {
+ return std::string();
+ }
+ return it->second.content().get().at("key").template get<std::string>();
+ }
+
+ VerificationMessages VerificationProcess::verifyKeyMac(Event macEvent)
+ {
+ auto checkMac = [this](auto &&macFunc, auto mac, auto k, auto &&...as) {
+ return std::invoke(macFunc,
+ sas, mac, k,
+ ourUserId, ourDeviceId, theirUserId, theirDeviceId,
+ txnId(),
+ std::forward<decltype(as)>(as)...
+ );
+ };
+ auto content = macEvent.content().get();
+ std::set<std::string> keyList;
+ for (auto [k, v]: content.at("mac").items()) {
+ keyList.insert(k);
+ }
+ if (!checkMac(
+ &SasDesc::verifyKeyListMacHkdfHMacSha256V2,
+ content.at("keys").template get<std::string>(),
+ keyList
+ )) {
+ return {VerificationSendEvent(Event())};
+ }
+ auto keyId = "ed25519:" + theirDeviceId;
+ if (!checkMac(
+ &SasDesc::verifyKeyMacHkdfHMacSha256V2,
+ content.at("mac").at(keyId).template get<std::string>(),
+ theirDeviceKey,
+ keyId
+ )) {
+ return {VerificationSendEvent(Event())};
+ }
+ return {
+ VerificationSendEvent{
+ makeVEvent("m.key.verification.done", txnId(), json::object())
+ },
+ // From this point we verified their mac to be valid.
+ VerificationShowStatus{VerificationShowStatus::VerifiedThem},
+ };
+ }
+}
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -120,6 +120,7 @@
crypto/outbound-group-session-test.cpp
crypto/session-test.cpp
crypto/key-export-test.cpp
+ crypto/verification-process-test.cpp
EXTRA_LINK_LIBRARIES kazvcrypto
)
diff --git a/src/tests/crypto/verification-process-test.cpp b/src/tests/crypto/verification-process-test.cpp
new file mode 100644
--- /dev/null
+++ b/src/tests/crypto/verification-process-test.cpp
@@ -0,0 +1,148 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2026 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include "verification-process.hpp"
+#include "crypto.hpp"
+#include <catch2/catch_test_macros.hpp>
+
+using namespace Kazv;
+using VMsgs = immer::flex_vector<VerificationMessage>;
+
+// Examples from v1.17 client-server api
+// https://spec.matrix.org/v1.17/client-server-api/#key-verification-framework
+Event requestEvent = R"({
+ "content": {
+ "from_device": "AliceDevice2",
+ "methods": [
+ "m.sas.v1"
+ ],
+ "timestamp": 1559598944869,
+ "transaction_id": "S0meUniqueAndOpaqueString"
+ },
+ "type": "m.key.verification.request"
+})"_json;
+
+const std::string txnId = "S0meUniqueAndOpaqueString";
+const Timestamp ts = 1559598944869;
+
+TEST_CASE("Full verification flow", "[crypto][verification]")
+{
+ auto ca = Crypto(RandomTag{}, {});
+ auto cb = Crypto(RandomTag{}, {});
+
+ auto pa = VerificationProcess(
+ "@alice:example.com",
+ "AliceDevice2",
+ "@bob:example.com",
+ "BobDevice1",
+ ca.ed25519IdentityKey()
+ );
+ auto pb = VerificationProcess(
+ "@bob:example.com",
+ "BobDevice1",
+ "@alice:example.com",
+ "AliceDevice2",
+ cb.ed25519IdentityKey()
+ );
+ pa.setTheirDeviceKey(cb.ed25519IdentityKey());
+ pa.addOutgoing(requestEvent);
+ auto msgs = pb.processIncoming(requestEvent);
+ REQUIRE(msgs == VMsgs{VerificationShowStatus{VerificationShowStatus::Requested}});
+
+ msgs = pb.userReady();
+ REQUIRE(msgs.size() == 2);
+ REQUIRE(std::holds_alternative<VerificationSendEvent>(msgs[0]));
+ REQUIRE(std::get<VerificationShowStatus>(msgs[1]) == VerificationShowStatus{VerificationShowStatus::Waiting});
+ auto readyEvent = std::get<VerificationSendEvent>(msgs[0]).event;
+ REQUIRE(readyEvent.content().get()["transaction_id"] == txnId);
+
+ pb.addOutgoing(readyEvent);
+ msgs = pa.processIncoming(readyEvent);
+ REQUIRE(msgs.size() == 1);
+ REQUIRE(std::holds_alternative<VerificationSendEvent>(msgs[0]));
+ auto startEvent = std::get<VerificationSendEvent>(msgs[0]).event;
+ REQUIRE(startEvent.type() == "m.key.verification.start");
+ REQUIRE(startEvent.content().get()["method"] == "m.sas.v1");
+
+ pa.addOutgoing(startEvent);
+ msgs = pb.processIncoming(startEvent);
+ REQUIRE(msgs.size() == 1);
+ REQUIRE(std::holds_alternative<VerificationSendEvent>(msgs[0]));
+ auto acceptEvent = std::get<VerificationSendEvent>(msgs[0]).event;
+
+ REQUIRE(acceptEvent.type() == "m.key.verification.accept");
+ REQUIRE(acceptEvent.content().get()["commitment"].template get<std::string>().size());
+
+ pb.setTheirDeviceKey(ca.ed25519IdentityKey());
+ pb.addOutgoing(acceptEvent);
+ msgs = pa.processIncoming(acceptEvent);
+ REQUIRE(msgs.size() == 1);
+ REQUIRE(std::holds_alternative<VerificationSendEvent>(msgs[0]));
+ auto keyEventA = std::get<VerificationSendEvent>(msgs[0]).event;
+ REQUIRE(keyEventA.type() == "m.key.verification.key");
+ REQUIRE(keyEventA.content().get()["key"].template get<std::string>().size());
+
+ pa.addOutgoing(keyEventA);
+ msgs = pb.processIncoming(keyEventA);
+ REQUIRE(msgs.size() == 2);
+ REQUIRE(std::holds_alternative<VerificationDisplayCodes>(msgs[0]));
+ auto codesB = std::get<VerificationDisplayCodes>(msgs[0]);
+ REQUIRE(codesB.decimalCode.size());
+ REQUIRE(std::holds_alternative<VerificationSendEvent>(msgs[1]));
+ auto keyEventB = std::get<VerificationSendEvent>(msgs[1]).event;
+ REQUIRE(keyEventB.type() == "m.key.verification.key");
+ REQUIRE(keyEventB.content().get()["key"].template get<std::string>().size());
+
+ pb.addOutgoing(keyEventB);
+ msgs = pa.processIncoming(keyEventB);
+ REQUIRE(msgs.size() == 1);
+ REQUIRE(std::holds_alternative<VerificationDisplayCodes>(msgs[0]));
+ auto codesA = std::get<VerificationDisplayCodes>(msgs[0]);
+ REQUIRE(codesA == codesB);
+ REQUIRE(codesA.decimalCode.size());
+
+ msgs = pa.userConfirmMatch();
+ REQUIRE(msgs.size() == 1);
+ REQUIRE(std::holds_alternative<VerificationSendEvent>(msgs[0]));
+ auto macEventA = std::get<VerificationSendEvent>(msgs[0]).event;
+ REQUIRE(macEventA.type() == "m.key.verification.mac");
+
+ msgs = pb.userConfirmMatch();
+ REQUIRE(msgs.size() == 1);
+ REQUIRE(std::holds_alternative<VerificationSendEvent>(msgs[0]));
+ auto macEventB = std::get<VerificationSendEvent>(msgs[0]).event;
+ REQUIRE(macEventB.type() == "m.key.verification.mac");
+
+ pb.addOutgoing(macEventB);
+ msgs = pa.processIncoming(macEventB);
+ REQUIRE(msgs.size() == 2);
+ REQUIRE(std::holds_alternative<VerificationSendEvent>(msgs[0]));
+ auto doneEventA = std::get<VerificationSendEvent>(msgs[0]).event;
+ REQUIRE(doneEventA.type() == "m.key.verification.done");
+ REQUIRE(std::holds_alternative<VerificationShowStatus>(msgs[1]));
+ REQUIRE(std::get<VerificationShowStatus>(msgs[1]).status == VerificationShowStatus::VerifiedThem);
+
+ pa.addOutgoing(macEventA);
+ msgs = pb.processIncoming(macEventA);
+ REQUIRE(msgs.size() == 2);
+ REQUIRE(std::holds_alternative<VerificationSendEvent>(msgs[0]));
+ auto doneEventB = std::get<VerificationSendEvent>(msgs[0]).event;
+ REQUIRE(doneEventB.type() == "m.key.verification.done");
+ REQUIRE(std::holds_alternative<VerificationShowStatus>(msgs[1]));
+ REQUIRE(std::get<VerificationShowStatus>(msgs[1]).status == VerificationShowStatus::VerifiedThem);
+
+ pb.addOutgoing(doneEventB);
+ msgs = pa.processIncoming(doneEventB);
+ REQUIRE(msgs.size() == 1);
+ REQUIRE(std::holds_alternative<VerificationShowStatus>(msgs[0]));
+ REQUIRE(std::get<VerificationShowStatus>(msgs[0]).status == VerificationShowStatus::VerifiedBoth);
+
+ pa.addOutgoing(doneEventA);
+ msgs = pb.processIncoming(doneEventA);
+ REQUIRE(msgs.size() == 1);
+ REQUIRE(std::holds_alternative<VerificationShowStatus>(msgs[0]));
+ REQUIRE(std::get<VerificationShowStatus>(msgs[0]).status == VerificationShowStatus::VerifiedBoth);
+}

File Metadata

Mime Type
text/plain
Expires
Mon, Mar 2, 9:43 PM (21 h, 24 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1155691
Default Alt Text
D289.1772516621.diff (38 KB)

Event Timeline