Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F44969341
D289.1772516621.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
38 KB
Referenced Files
None
Subscribers
None
D289.1772516621.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D289: Implement single SAS verification process
Attached
Detach File
Event Timeline
Log In to Comment