diff options
| author | Linus Jahn <lnj@kaidan.im> | 2022-09-16 19:08:02 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-09-16 19:08:02 +0200 |
| commit | d858e4ae6e0adeaad8d03b055883f411e6d19ab0 (patch) | |
| tree | e306f607339bd553ba79030fcc90686c5d1422e3 | |
| parent | 7878aeb22cc49b31ae527dfc683f2f16b0f79075 (diff) | |
| download | qxmpp-d858e4ae6e0adeaad8d03b055883f411e6d19ab0.tar.gz | |
Implement XEP-0448: Encryption for stateless file sharing parsing (#463)
https://xmpp.org/extensions/xep-0448.html
Co-authored-by: Jonah Brüchert <jbb@kaidan.im>
| -rw-r--r-- | doc/doap.xml | 7 | ||||
| -rw-r--r-- | src/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | src/base/QXmppConstants.cpp | 2 | ||||
| -rw-r--r-- | src/base/QXmppConstants_p.h | 2 | ||||
| -rw-r--r-- | src/base/QXmppEncryptedFileSource.cpp | 157 | ||||
| -rw-r--r-- | src/base/QXmppEncryptedFileSource_p.h | 59 | ||||
| -rw-r--r-- | src/base/QXmppHash.cpp | 1 | ||||
| -rw-r--r-- | tests/qxmppmessage/tst_qxmppmessage.cpp | 61 |
8 files changed, 290 insertions, 0 deletions
diff --git a/doc/doap.xml b/doc/doap.xml index 97826d73..bbad8a58 100644 --- a/doc/doap.xml +++ b/doc/doap.xml @@ -579,6 +579,13 @@ SPDX-License-Identifier: CC0-1.0 </implements> <implements> <xmpp:SupportedXep> + <xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0448.html'/> + <xmpp:status>partial</xmpp:status> + <xmpp:version>0.2</xmpp:version> + <xmpp:since>1.5</xmpp:since> + </implements> + <implements> + <xmpp:SupportedXep> <xmpp:xep rdf:resource='https://xmpp.org/extensions/xep-0450.html'/> <xmpp:status>complete</xmpp:status> <xmpp:version>0.4</xmpp:version> diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c719dc3f..fb4273c9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -161,6 +161,7 @@ set(SOURCE_FILES base/QXmppDataFormBase.cpp base/QXmppDiscoveryIq.cpp base/QXmppElement.cpp + base/QXmppEncryptedFileSource.cpp base/QXmppEntityTimeIq.cpp base/QXmppError.cpp base/QXmppFileMetadata.cpp diff --git a/src/base/QXmppConstants.cpp b/src/base/QXmppConstants.cpp index 7f58911c..196ee89b 100644 --- a/src/base/QXmppConstants.cpp +++ b/src/base/QXmppConstants.cpp @@ -192,5 +192,7 @@ const char *ns_tm = "urn:xmpp:tm:1"; const char *ns_file_metadata = "urn:xmpp:file:metadata:0"; // XEP-0447: Stateless file sharing const char *ns_sfs = "urn:xmpp:sfs:0"; +// XEP-0448: Encryption for stateless file sharing +const char *ns_esfs = "urn:xmpp:esfs:0"; // XEP-0450: Automatic Trust Management (ATM) const char *ns_atm = "urn:xmpp:atm:1"; diff --git a/src/base/QXmppConstants_p.h b/src/base/QXmppConstants_p.h index d040ed27..83521c5a 100644 --- a/src/base/QXmppConstants_p.h +++ b/src/base/QXmppConstants_p.h @@ -204,6 +204,8 @@ extern const char *ns_tm; extern const char *ns_file_metadata; // XEP-0447: Stateless file sharing extern const char *ns_sfs; +// XEP-0448: Encryption for stateless file sharing +extern const char *ns_esfs; // XEP-0450: Automatic Trust Management (ATM) extern const char *ns_atm; diff --git a/src/base/QXmppEncryptedFileSource.cpp b/src/base/QXmppEncryptedFileSource.cpp new file mode 100644 index 00000000..d2c2b592 --- /dev/null +++ b/src/base/QXmppEncryptedFileSource.cpp @@ -0,0 +1,157 @@ +// SPDX-FileCopyrightText: 2022 Linus Jahn <lnj@kaidan.im> +// SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "QXmppConstants_p.h" +#include "QXmppEncryptedFileSource_p.h" +#include "QXmppHttpFileSource.h" + +#include <optional> + +#include <QDomElement> +#include <QXmlStreamWriter> + +/// \cond +static QString cipherToString(QXmppEncryptedFileSource::Cipher cipher) +{ + switch (cipher) { + case QXmppEncryptedFileSource::Aes128GcmNopadding: + return "urn:xmpp:ciphers:aes-128-gcm-nopadding:0"; + case QXmppEncryptedFileSource::Aes256GcmNopadding: + return "urn:xmpp:ciphers:aes-256-gcm-nopadding:0"; + case QXmppEncryptedFileSource::Aes256CbcPkcs7: + return "urn:xmpp:ciphers:aes-256-cbc-pkcs7:0"; + } + Q_UNREACHABLE(); +} + +static std::optional<QXmppEncryptedFileSource::Cipher> cipherFromString(const QString &cipher) +{ + if (cipher == "urn:xmpp:ciphers:aes-128-gcm-nopadding:0") { + return QXmppEncryptedFileSource::Aes128GcmNopadding; + } else if (cipher == "urn:xmpp:ciphers:aes-256-gcm-nopadding:0") { + return QXmppEncryptedFileSource::Aes256GcmNopadding; + } else if (cipher == "urn:xmpp:ciphers:aes-256-cbc-pkcs7:0") { + return QXmppEncryptedFileSource::Aes256CbcPkcs7; + } + return {}; +} + +QXmppEncryptedFileSource::QXmppEncryptedFileSource() = default; + +QXmppEncryptedFileSource::Cipher QXmppEncryptedFileSource::cipher() const +{ + return m_cipher; +} + +void QXmppEncryptedFileSource::setCipher(Cipher newCipher) +{ + m_cipher = newCipher; +} + +const QByteArray &QXmppEncryptedFileSource::key() const +{ + return m_key; +} + +void QXmppEncryptedFileSource::setKey(const QByteArray &newKey) +{ + m_key = newKey; +} + +const QByteArray &QXmppEncryptedFileSource::iv() const +{ + return m_iv; +} + +void QXmppEncryptedFileSource::setIv(const QByteArray &newIv) +{ + m_iv = newIv; +} + +const QVector<QXmppHash> &QXmppEncryptedFileSource::hashes() const +{ + return m_hashes; +} + +void QXmppEncryptedFileSource::setHashes(const QVector<QXmppHash> &newHashes) +{ + m_hashes = newHashes; +} + +const QVector<QXmppHttpFileSource> &QXmppEncryptedFileSource::httpSources() const +{ + return m_httpSources; +} + +void QXmppEncryptedFileSource::setHttpSources(const QVector<QXmppHttpFileSource> &newHttpSources) +{ + m_httpSources = newHttpSources; +} + +bool QXmppEncryptedFileSource::parse(const QDomElement &el) +{ + QString cipher = el.attribute(QStringLiteral("cipher")); + if (auto parsedCipher = cipherFromString(cipher)) { + m_cipher = *parsedCipher; + } else { + return false; + } + + auto keyEl = el.firstChildElement(QStringLiteral("key")); + if (keyEl.isNull()) { + return false; + } + m_key = QByteArray::fromBase64(keyEl.text().toUtf8()); + + auto ivEl = el.firstChildElement(QStringLiteral("iv")); + if (ivEl.isNull()) { + return false; + } + m_iv = QByteArray::fromBase64(ivEl.text().toUtf8()); + + for (auto childEl = el.firstChildElement(QStringLiteral("hash")); + !childEl.isNull(); + childEl = childEl.nextSiblingElement(QStringLiteral("hash"))) { + QXmppHash hash; + if (!hash.parse(childEl)) { + return false; + } + m_hashes.push_back(std::move(hash)); + } + + auto sourcesEl = el.firstChildElement(QStringLiteral("sources")); + if (sourcesEl.isNull()) { + return false; + } + for (auto childEl = sourcesEl.firstChildElement(QStringLiteral("url-data")); + !childEl.isNull(); + childEl = childEl.nextSiblingElement(QStringLiteral("url-data"))) { + QXmppHttpFileSource source; + source.parse(childEl); + m_httpSources.push_back(std::move(source)); + } + + return true; +} + +void QXmppEncryptedFileSource::toXml(QXmlStreamWriter *writer) const +{ + writer->writeStartElement(QStringLiteral("encrypted")); + writer->writeDefaultNamespace(ns_esfs); + writer->writeAttribute(QStringLiteral("cipher"), cipherToString(m_cipher)); + writer->writeTextElement(QStringLiteral("key"), m_key.toBase64()); + writer->writeTextElement(QStringLiteral("iv"), m_iv.toBase64()); + for (const auto &hash : m_hashes) { + hash.toXml(writer); + } + writer->writeStartElement(QStringLiteral("sources")); + writer->writeDefaultNamespace(ns_sfs); + for (const auto &source : m_httpSources) { + source.toXml(writer); + } + writer->writeEndElement(); + writer->writeEndElement(); +} +/// \endcond diff --git a/src/base/QXmppEncryptedFileSource_p.h b/src/base/QXmppEncryptedFileSource_p.h new file mode 100644 index 00000000..21270de2 --- /dev/null +++ b/src/base/QXmppEncryptedFileSource_p.h @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2022 Linus Jahn <lnj@kaidan.im> +// SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#ifndef QXMPPENCRYPTEDFILESOURCE_H +#define QXMPPENCRYPTEDFILESOURCE_H + +#include "QXmppGlobal.h" +#include "QXmppHash.h" +#include "QXmppHttpFileSource.h" + +#include <QSharedDataPointer> +#include <QUrl> +#include <QVector> + +class QXmppEncryptedFileSourcePrivate; + +// exported for tests +class QXMPP_EXPORT QXmppEncryptedFileSource +{ +public: + enum Cipher { + Aes128GcmNopadding, + Aes256GcmNopadding, + Aes256CbcPkcs7, + }; + + QXmppEncryptedFileSource(); + + Cipher cipher() const; + void setCipher(Cipher newCipher); + + const QByteArray &key() const; + void setKey(const QByteArray &newKey); + + const QByteArray &iv() const; + void setIv(const QByteArray &newIv); + + const QVector<QXmppHash> &hashes() const; + void setHashes(const QVector<QXmppHash> &newHashes); + + const QVector<QXmppHttpFileSource> &httpSources() const; + void setHttpSources(const QVector<QXmppHttpFileSource> &newHttpSources); + + /// \cond + bool parse(const QDomElement &el); + void toXml(QXmlStreamWriter *writer) const; + /// \endcond + +private: + Cipher m_cipher = Aes128GcmNopadding; + QByteArray m_key; + QByteArray m_iv; + QVector<QXmppHash> m_hashes; + QVector<QXmppHttpFileSource> m_httpSources; +}; + +#endif // QXMPPENCRYPTEDFILESOURCE_H diff --git a/src/base/QXmppHash.cpp b/src/base/QXmppHash.cpp index c246d200..6d1dcb07 100644 --- a/src/base/QXmppHash.cpp +++ b/src/base/QXmppHash.cpp @@ -123,6 +123,7 @@ bool QXmppHash::parse(const QDomElement &el) #else m_hash = QByteArray::fromBase64(el.text().toUtf8()); #endif + return true; } return false; } diff --git a/tests/qxmppmessage/tst_qxmppmessage.cpp b/tests/qxmppmessage/tst_qxmppmessage.cpp index 35bf2625..c9eaa786 100644 --- a/tests/qxmppmessage/tst_qxmppmessage.cpp +++ b/tests/qxmppmessage/tst_qxmppmessage.cpp @@ -6,6 +6,7 @@ #include "QXmppBitsOfBinaryContentId.h" #include "QXmppBitsOfBinaryDataList.h" +#include "QXmppEncryptedFileSource_p.h" #include "QXmppMessage.h" #include "QXmppMixInvitation.h" #include "QXmppTrustMessageElement.h" @@ -53,6 +54,7 @@ private slots: void testTrustMessageElement(); void testE2eeFallbackBody(); void testFileSharing(); + void testEncryptedFileSource(); }; void tst_QXmppMessage::testBasic_data() @@ -1186,5 +1188,64 @@ void tst_QXmppMessage::testFileSharing() serializePacket(message1, xml); } +void tst_QXmppMessage::testEncryptedFileSource() +{ + { + QByteArray xml( + "<encrypted xmlns='urn:xmpp:esfs:0' cipher='urn:xmpp:ciphers:aes-256-gcm-nopadding:0'>" + "<key>SuRJ2agVm/pQbJQlPq/B23Xt1YOOJCcEGJA5HrcYOGQ=</key>" + "<iv>T8RDMBaiqn6Ci4Nw</iv>" + "<hash xmlns='urn:xmpp:hashes:2' algo='sha3-256'>BgKI2gp2kNCRsARNvhFmw5kFf9BBo2pTbV2D8XHTMWI=</hash>" + "<hash xmlns='urn:xmpp:hashes:2' algo='blake2b-256'>id4cnqqy9/ssfCkM4vYSkiXXrlE=</hash>" + "<sources xmlns='urn:xmpp:sfs:0'>" + "<url-data xmlns='http://jabber.org/protocol/url-data' target='https://download.montague.lit/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/encrypted.jpg'/>" + "</sources>" + "</encrypted>"); + + QXmppEncryptedFileSource encryptedSource; + parsePacket(encryptedSource, xml); + QCOMPARE(encryptedSource.key(), QByteArray::fromBase64("SuRJ2agVm/pQbJQlPq/B23Xt1YOOJCcEGJA5HrcYOGQ=")); + QCOMPARE(encryptedSource.iv(), QByteArray::fromBase64("T8RDMBaiqn6Ci4Nw")); + QCOMPARE(encryptedSource.httpSources().front().url(), QUrl("https://download.montague.lit/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/encrypted.jpg")); + QCOMPARE(encryptedSource.cipher(), QXmppEncryptedFileSource::Aes256GcmNopadding); + QVERIFY(!encryptedSource.hashes().empty()); + serializePacket(encryptedSource, xml); + } + + { + QByteArray xml( + "<encrypted xmlns='urn:xmpp:esfs:0' cipher='urn:xmpp:ciphers:aes-128-gcm-nopadding:0'>" + "<key>SuRJ2agVm/pQbJQlPq/B23Xt1YOOJCcEGJA5HrcYOGQ=</key>" + "<iv>T8RDMBaiqn6Ci4Nw</iv>" + "<hash xmlns='urn:xmpp:hashes:2' algo='sha3-256'>BgKI2gp2kNCRsARNvhFmw5kFf9BBo2pTbV2D8XHTMWI=</hash>" + "<hash xmlns='urn:xmpp:hashes:2' algo='blake2b-256'>id4cnqqy9/ssfCkM4vYSkiXXrlE=</hash>" + "<sources xmlns='urn:xmpp:sfs:0'>" + "<url-data xmlns='http://jabber.org/protocol/url-data' target='https://download.montague.lit/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/encrypted.jpg'/>" + "</sources>" + "</encrypted>"); + + QXmppEncryptedFileSource encryptedSource; + parsePacket(encryptedSource, xml); + serializePacket(encryptedSource, xml); + } + + { + QByteArray xml( + "<encrypted xmlns='urn:xmpp:esfs:0' cipher='urn:xmpp:ciphers:aes-256-cbc-pkcs7:0'>" + "<key>SuRJ2agVm/pQbJQlPq/B23Xt1YOOJCcEGJA5HrcYOGQ=</key>" + "<iv>T8RDMBaiqn6Ci4Nw</iv>" + "<hash xmlns='urn:xmpp:hashes:2' algo='sha3-256'>BgKI2gp2kNCRsARNvhFmw5kFf9BBo2pTbV2D8XHTMWI=</hash>" + "<hash xmlns='urn:xmpp:hashes:2' algo='blake2b-256'>id4cnqqy9/ssfCkM4vYSkiXXrlE=</hash>" + "<sources xmlns='urn:xmpp:sfs:0'>" + "<url-data xmlns='http://jabber.org/protocol/url-data' target='https://download.montague.lit/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/encrypted.jpg'/>" + "</sources>" + "</encrypted>"); + + QXmppEncryptedFileSource encryptedSource; + parsePacket(encryptedSource, xml); + serializePacket(encryptedSource, xml); + } +} + QTEST_MAIN(tst_QXmppMessage) #include "tst_qxmppmessage.moc" |
