From b22a4c8ca3dc1870d0bc88fb94cc381eacbfe45d Mon Sep 17 00:00:00 2001 From: vae Date: Wed, 12 May 2021 12:33:35 +0300 Subject: [PATCH] feat(OMEMO): add Device, DeviceList, PreKey, Bundle + XML --- qomemo/CMakeLists.txt | 7 +- qomemo/qomemo.cpp | 243 ++++++++++++++++++++++++++++++++++++++++++ qomemo/qomemo.h | 56 ++++++++++ 3 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 qomemo/qomemo.cpp create mode 100644 qomemo/qomemo.h diff --git a/qomemo/CMakeLists.txt b/qomemo/CMakeLists.txt index dc0e0ea..f17475c 100644 --- a/qomemo/CMakeLists.txt +++ b/qomemo/CMakeLists.txt @@ -1 +1,6 @@ -target_sources(squawk PRIVATE signal.h signal.cpp) \ No newline at end of file +target_sources(squawk PRIVATE + signal.h + signal.cpp + qomemo.cpp + qomemo.h + ) \ No newline at end of file diff --git a/qomemo/qomemo.cpp b/qomemo/qomemo.cpp new file mode 100644 index 0000000..1695c04 --- /dev/null +++ b/qomemo/qomemo.cpp @@ -0,0 +1,243 @@ +/* + * Created by victoria on 2021-05-12. + */ + +#include "qomemo.h" + +#include + +static bool elementMatches(const QXmppElement &element, const QString &tagName, + const QString &xmlns = QStringLiteral("")) { + if (element.tagName() != tagName) { + qWarning() << "tag name: expected = " << tagName + << ", got = " << element.tagName(); + return false; + } + + if (!xmlns.isEmpty() && element.attribute("xmlns") != xmlns) { + qWarning() << "xmlns: expected = " << xmlns + << ", got = " << element.attribute("xmlns"); + return false; + } + + return true; +} + +static QXmppElement createElement(const QString &tagName, + const QString &xmlns = QStringLiteral("")) { + QXmppElement el{}; + el.setTagName(tagName); + + if (!xmlns.isEmpty()) + el.setAttribute("xmlns", xmlns); + + return el; +} + +static QXmppElement createValue(const QString &value) { + auto el = createElement("value"); + el.setValue(value); + + return el; +} + +static QXmppElement createField(const QString &key, const QString &value, + bool hidden = false) { + auto field = createElement("field"); + field.setAttribute("var", key); + if (hidden) + field.setAttribute("type", "hidden"); + field.appendChild(createValue(value)); + + return field; +} + +static QXmppElement +createOpenPublishOptions(const QString &maxItems = QStringLiteral("")) { + auto formType = createField( + "FORM_TYPE", "http://jabber.org/protocol/pubsub#publish-options", true); + auto accessModel = createField("pubsub#access_model", "open"); + + auto x = createElement("x", "jabber:x:data"); + x.setAttribute("type", "submit"); + + x.appendChild(formType); + x.appendChild(accessModel); + + if (!maxItems.isEmpty()) + x.appendChild(createField("pubsub#max_items", maxItems)); + + auto publishOptions = createElement("publish-options"); + publishOptions.appendChild(x); + + return publishOptions; +} + +QXmppElement QOmemo::DeviceList::toXml() const { + auto element = createElement("devices", "urn:xmpp:omemo:1"); + + for (const auto &d : devices) { + element.appendChild(d.toXml()); + } + + return element; +} + +QXmppIq QOmemo::DeviceList::toIq() const { + QXmppIq iq{}; + + iq.setType(QXmppIq::Set); + + auto item = createElement("item"); + item.setAttribute("id", "current"); + item.appendChild(toXml()); + + auto publish = createElement("publish"); + publish.setAttribute("node", "urn:xmpp:omemo:1:devices"); + publish.appendChild(item); + + auto pubSub = createElement("pubsub", "http://jabber.org/protocol/pubsub"); + pubSub.appendChild(publish); + pubSub.appendChild(createOpenPublishOptions()); + + iq.extensions().push_back(pubSub); + + return iq; +} + +void QOmemo::DeviceList::fromXml(const QXmppElement &element) { + if (!elementMatches(element, "devices", "urn:xmpp:omemo:1")) + return; + + devices.clear(); + + auto deviceElement = element.firstChildElement("device"); + while (!deviceElement.isNull()) { + Device device{}; + device.fromXml(deviceElement); + devices.push_back(device); + + deviceElement = deviceElement.nextSiblingElement("device"); + } +} + +QXmppElement QOmemo::Device::toXml() const { + auto result = createElement("device"); + result.setAttribute("id", QString::number(id)); + + if (!label.isEmpty()) { + result.setAttribute("label", label); + } + + return result; +} + +void QOmemo::Device::fromXml(const QXmppElement &element) { + if (!elementMatches(element, "device")) + return; + + id = element.attribute("id").toInt(); + label = element.attribute("label"); +} + +QXmppElement QOmemo::PreKey::toXml() const { + auto pk = createElement("pk"); + pk.setAttribute("id", QString::number(id)); + // TODO: Base64 + pk.setValue(data); + + return pk; +} + +void QOmemo::PreKey::fromXml(const QXmppElement &element) { + if (!elementMatches(element, "pk")) + return; + + id = element.attribute("id").toInt(); + // TODO: Base64 + data = element.value(); +} + +QXmppElement QOmemo::Bundle::toXml() const { + auto spkNode = createElement("spk"); + spkNode.setAttribute("id", QString::number(spkId)); + spkNode.setValue(spk); + + auto spksNode = createElement("spks"); + spksNode.setValue(spks); + + auto ikNode = createElement("ik"); + ikNode.setValue(ik); + + auto prekeysNode = createElement("prekeys"); + for (const auto &pk : prekeys) { + prekeysNode.appendChild(pk.toXml()); + } + + auto result = createElement("bundle", "urn:xmpp:omemo:1"); + result.appendChild(spkNode); + result.appendChild(spksNode); + result.appendChild(ikNode); + result.appendChild(prekeysNode); + + return result; +} + +QXmppIq QOmemo::Bundle::toIq() const { + QXmppIq iq{}; + + iq.setType(QXmppIq::Set); + + auto item = createElement("item"); + item.setAttribute("id", QString::number(deviceId)); + item.appendChild(toXml()); + + auto publish = createElement("publish"); + publish.setAttribute("node", "urn:xmpp:omemo:1:bundles"); + publish.appendChild(item); + + auto pubSub = createElement("pubsub", "http://jabber.org/protocol/pubsub"); + pubSub.appendChild(publish); + pubSub.appendChild(createOpenPublishOptions("max")); + + iq.extensions().push_back(pubSub); + + return iq; +} + +void QOmemo::Bundle::fromXml(const QXmppElement &element) { + if (!elementMatches(element, "bundle", "urn:xmpp:omemo:1")) + return; + + auto spkNode = element.firstChildElement("spk"); + if (spkNode.isNull()) { + qWarning() << "'bundle': missing 'spk'"; + return; + } + spk = spkNode.value(); + spkId = spkNode.attribute("id").toInt(); + + auto spksNode = element.firstChildElement("spks"); + if (spksNode.isNull()) { + qWarning() << "'bundle': missing 'spks'"; + return; + } + spks = spksNode.value(); + + auto ikNode = element.firstChildElement("ik"); + if (ikNode.isNull()) { + qWarning() << "'bundle': missing 'ik'"; + return; + } + ik = ikNode.value(); + + auto prekeysNode = element.firstChildElement("prekeys"); + auto pkNode = prekeysNode.firstChildElement("pk"); + + prekeys.clear(); + while (!pkNode.isNull()) { + PreKey pk{}; + pk.fromXml(pkNode); + prekeys.push_back(pk); + } +} diff --git a/qomemo/qomemo.h b/qomemo/qomemo.h new file mode 100644 index 0000000..8955d1e --- /dev/null +++ b/qomemo/qomemo.h @@ -0,0 +1,56 @@ +/* + * Created by victoria on 2021-05-12. + */ + +#pragma once + +#include + +namespace QOmemo { + +class Device { +public: + [[nodiscard]] QXmppElement toXml() const; + void fromXml(const QXmppElement &element); + + int id; + QString label; +}; + +class DeviceList { +public: + [[nodiscard]] QXmppElement toXml() const; + [[nodiscard]] QXmppIq toIq() const; + /// Expects a urn:xmpp:omemo:1:devices node + void fromXml(const QXmppElement &element); + +private: + QList devices; +}; + +class PreKey { +public: + [[nodiscard]] QXmppElement toXml() const; + /// Expects a + void fromXml(const QXmppElement &element); + + int id; + QString data; +}; + +class Bundle { +public: + [[nodiscard]] QXmppElement toXml() const; + [[nodiscard]] QXmppIq toIq() const; + void fromXml(const QXmppElement &element); + + int deviceId; + + QString spk; + int spkId; + QString spks; + QString ik; + QList prekeys; +}; + +} // namespace QOmemo