diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b9b7a18c..c8c97ace7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ QXmpp 1.0.1 (UNRELEASED) New features: - Add support for SCRAM-SHA-1 and SCRAM-SHA-256 (#183, @jlaine) - Add XEP-0367: Message Attaching (v0.3.0) (#196, @lnjX) + - Add XEP-0380: Explicit Message Encryption (v0.3.0) (#199, @lnjX) - Add XEP-0382: Spoiler messages (v0.2.0) (#195, @lnjX) QXmpp 1.0.0 (Jan 8, 2019) diff --git a/doc/xep.doc b/doc/xep.doc index 3daf09362..130436fc3 100644 --- a/doc/xep.doc +++ b/doc/xep.doc @@ -42,6 +42,7 @@ Complete: - XEP-0319: Last User Interaction in Presence - XEP-0352: Client State Indication - XEP-0367: Message Attaching (v0.3.0) +- XEP-0380: Explicit Message Encryption (v0.3.0) - XEP-0382: Spoiler messages (v0.2.0) Ongoing: diff --git a/src/base/QXmppConstants.cpp b/src/base/QXmppConstants.cpp index b6fab47d8..80c97b563 100644 --- a/src/base/QXmppConstants.cpp +++ b/src/base/QXmppConstants.cpp @@ -37,6 +37,8 @@ const char* ns_stanza = "urn:ietf:params:xml:ns:xmpp-stanzas"; const char* ns_rpc = "jabber:iq:rpc"; // XEP-0020: Feature Negotiation const char* ns_feature_negotiation = "http://jabber.org/protocol/feature-neg"; +// XEP-0027: Current Jabber OpenPGP Usage +const char* ns_legacy_openpgp = "jabber:x:encrypted"; // XEP-0030: Service Discovery const char* ns_disco_info = "http://jabber.org/protocol/disco#info"; const char* ns_disco_items = "http://jabber.org/protocol/disco#items"; @@ -135,6 +137,8 @@ const char* ns_chat_markers = "urn:xmpp:chat-markers:0"; const char* ns_csi = "urn:xmpp:csi:0"; // XEP-0363: HTTP File Upload const char* ns_http_upload = "urn:xmpp:http:upload:0"; +// XEP-0364: Current Off-the-Record Messaging Usage +const char* ns_otr = "urn:xmpp:otr:0"; // XEP-0367: Message Attaching const char* ns_message_attaching = "urn:xmpp:message-attaching:1"; // XEP-0369: Mediated Information eXchange (MIX) @@ -146,8 +150,14 @@ const char* ns_mix_node_participants = "urn:xmpp:mix:nodes:participants"; const char* ns_mix_node_presence = "urn:xmpp:mix:nodes:presence"; const char* ns_mix_node_config = "urn:xmpp:mix:nodes:config"; const char* ns_mix_node_info = "urn:xmpp:mix:nodes:info"; +// XEP-0373: OpenPGP for XMPP +const char* ns_ox = "urn:xmpp:openpgp:0"; +// XEP-0380: Explicit Message Encryption +const char* ns_eme = "urn:xmpp:eme:0"; // XEP-0382: Spoiler messages const char* ns_spoiler = "urn:xmpp:spoiler:0"; +// XEP-0384: OMEMO Encryption +const char* ns_omemo = "eu.siacs.conversations.axolotl"; // XEP-0405: Mediated Information eXchange (MIX): Participant Server Requirements const char* ns_mix_pam = "urn:xmpp:mix:pam:1"; const char* ns_mix_roster = "urn:xmpp:mix:roster:0"; diff --git a/src/base/QXmppConstants_p.h b/src/base/QXmppConstants_p.h index 5304ada4d..86003970a 100644 --- a/src/base/QXmppConstants_p.h +++ b/src/base/QXmppConstants_p.h @@ -49,6 +49,8 @@ extern const char* ns_stanza; extern const char* ns_rpc; // XEP-0020: Feature Negotiation extern const char* ns_feature_negotiation; +// XEP-0027: Current Jabber OpenPGP Usage +extern const char* ns_legacy_openpgp; // XEP-0030: Service Discovery extern const char* ns_disco_info; extern const char* ns_disco_items; @@ -147,6 +149,8 @@ extern const char* ns_chat_markers; extern const char* ns_csi; // XEP-0363: HTTP File Upload extern const char* ns_http_upload; +// XEP-0364: Current Off-the-Record Messaging Usage +extern const char* ns_otr; // XEP-0367: Message Attaching extern const char* ns_message_attaching; // XEP-0369: Mediated Information eXchange (MIX) @@ -158,8 +162,14 @@ extern const char* ns_mix_node_participants; extern const char* ns_mix_node_presence; extern const char* ns_mix_node_config; extern const char* ns_mix_node_info; +// XEP-0373: OpenPGP for XMPP +extern const char* ns_ox; +// XEP-0380: Explicit Message Encryption +extern const char* ns_eme; // XEP-0382: Spoiler messages extern const char* ns_spoiler; +// XEP-0384: OMEMO Encryption +extern const char* ns_omemo; // XEP-0405: Mediated Information eXchange (MIX): Participant Server Requirements extern const char* ns_mix_pam; extern const char* ns_mix_roster; diff --git a/src/base/QXmppMessage.cpp b/src/base/QXmppMessage.cpp index af4b2a6f7..54ef4cd61 100644 --- a/src/base/QXmppMessage.cpp +++ b/src/base/QXmppMessage.cpp @@ -55,6 +55,24 @@ static const char* marker_types[] = { "acknowledged" }; +static const QStringList ENCRYPTION_NAMESPACES = { + QString(), + QString(), + ns_otr, + ns_legacy_openpgp, + ns_ox, + ns_omemo +}; + +static const QStringList ENCRYPTION_NAMES = { + QString(), + QString(), + QStringLiteral("OTR"), + QStringLiteral("Legacy OpenPGP"), + QStringLiteral("OpenPGP for XMPP (OX)"), + QStringLiteral("OMEMO") +}; + static const char *ns_xhtml = "http://www.w3.org/1999/xhtml"; enum StampType @@ -110,6 +128,10 @@ class QXmppMessagePrivate : public QSharedData QString mixUserJid; QString mixUserNick; + // XEP-0380: Explicit Message Encryption + QString encryptionMethod; + QString encryptionName; + // XEP-0382: Spoiler messages bool isSpoiler = false; QString spoilerHint; @@ -123,7 +145,7 @@ class QXmppMessagePrivate : public QSharedData /// \param thread QXmppMessage::QXmppMessage(const QString& from, const QString& to, const - QString& body, const QString& thread) + QString& body, const QString& thread) : QXmppStanza(from, to) , d(new QXmppMessagePrivate) { @@ -571,6 +593,74 @@ void QXmppMessage::setMixUserNick(const QString& mixUserNick) d->mixUserNick = mixUserNick; } +/// Returns the encryption method this message is advertised to be encrypted +/// with. +/// +/// \note QXmppMessage::NoEncryption does not necesserily mean that the message +/// is not encrypted; it may also be that the author of the message does not +/// support XEP-0380: Explicit Message Encryption. +/// +/// \note If this returns QXmppMessage::UnknownEncryption, you can still get +/// the namespace of the encryption with \c encryptionMethodNs() and possibly +/// also a name with \c encryptionName(). + +QXmppMessage::EncryptionMethod QXmppMessage::encryptionMethod() const +{ + if (d->encryptionMethod.isEmpty()) + return QXmppMessage::NoEncryption; + + int index = ENCRYPTION_NAMESPACES.indexOf(d->encryptionMethod); + if (index < 0) + return QXmppMessage::UnknownEncryption; + return static_cast(index); +} + +/// Advertises that this message is encrypted with the given encryption method. +/// See XEP-0380: Explicit Message Encryption for details. + +void QXmppMessage::setEncryptionMethod(QXmppMessage::EncryptionMethod method) +{ + d->encryptionMethod = ENCRYPTION_NAMESPACES.at(int(method)); +} + +/// Returns the namespace of the advertised encryption method via. XEP-0380: +/// Explicit Message Encryption. + +QString QXmppMessage::encryptionMethodNs() const +{ + return d->encryptionMethod; +} + +/// Sets the namespace of the encryption method this message advertises to be +/// encrypted with. See XEP-0380: Explicit Message Encryption for details. + +void QXmppMessage::setEncryptionMethodNs(const QString &encryptionMethod) +{ + d->encryptionMethod = encryptionMethod; +} + +/// Returns the associated name of the encryption method this message +/// advertises to be encrypted with. See XEP-0380: Explicit Message Encryption +/// for details. + +QString QXmppMessage::encryptionName() const +{ + if (!d->encryptionName.isEmpty()) + return d->encryptionName; + return ENCRYPTION_NAMES.at(int(encryptionMethod())); +} + +/// Sets the name of the encryption method for XEP-0380: Explicit Message +/// Encryption. +/// +/// \note This should only be used, if the encryption method is custom and is +/// not one of the methods listed in the XEP. + +void QXmppMessage::setEncryptionName(const QString &encryptionName) +{ + d->encryptionName = encryptionName; +} + /// Returns true, if this is a spoiler message according to XEP-0382: Spoiler /// messages. The spoiler hint however can still be empty. /// @@ -765,6 +855,10 @@ void QXmppMessage::parse(const QDomElement &element) } else if (xElement.tagName() == "mix" && xElement.namespaceURI() == ns_mix) { d->mixUserJid = xElement.firstChildElement("jid").text(); d->mixUserNick = xElement.firstChildElement("nick").text(); + // XEP-0380: Explicit Message Encryption + } else if (xElement.tagName() == "encryption" && xElement.namespaceURI() == ns_eme) { + d->encryptionMethod = xElement.attribute("namespace"); + d->encryptionName = xElement.attribute("name"); // XEP-0382: Spoiler messages } else if (xElement.tagName() == "spoiler" && xElement.namespaceURI() == ns_spoiler) { d->isSpoiler = true; @@ -923,6 +1017,15 @@ void QXmppMessage::toXml(QXmlStreamWriter *xmlWriter) const xmlWriter->writeEndElement(); } + // XEP-0380: Explicit Message Encryption + if (!d->encryptionMethod.isEmpty()) { + xmlWriter->writeStartElement("encryption"); + xmlWriter->writeAttribute("xmlns", ns_eme); + xmlWriter->writeAttribute("namespace", d->encryptionMethod); + helperToXmlAddAttribute(xmlWriter, "name", d->encryptionName); + xmlWriter->writeEndElement(); + } + // XEP-0382: Spoiler messages if (d->isSpoiler) { xmlWriter->writeStartElement("spoiler"); diff --git a/src/base/QXmppMessage.h b/src/base/QXmppMessage.h index 330c70feb..69e2d451e 100644 --- a/src/base/QXmppMessage.h +++ b/src/base/QXmppMessage.h @@ -66,6 +66,18 @@ class QXMPP_EXPORT QXmppMessage : public QXmppStanza Acknowledged }; + /// This enum describes different end-to-end encryption methods. These can + /// be used to mark a message explicitly as encrypted with a specific + /// algothim. See XEP-0380: Explicit Message Encryption for details. + enum EncryptionMethod { + NoEncryption, ///< No encryption + UnknownEncryption, ///< Unknown encryption + OTR, ///< XEP-0364: Current Off-the-Record Messaging Usage + LegacyOpenPGP, ///< XEP-0027: Current Jabber OpenPGP Usage + OX, ///< XEP-0373: OpenPGP for XMPP + OMEMO ///< XEP-0384: OMEMO Encryption + }; + QXmppMessage(const QString& from = QString(), const QString& to = QString(), const QString& body = QString(), const QString& thread = QString()); @@ -151,6 +163,15 @@ class QXMPP_EXPORT QXmppMessage : public QXmppStanza QString mixUserNick() const; void setMixUserNick(const QString&); + // XEP-0380: Explicit Message Encryption + EncryptionMethod encryptionMethod() const; + void setEncryptionMethod(EncryptionMethod); + QString encryptionMethodNs() const; + void setEncryptionMethodNs(const QString&); + + QString encryptionName() const; + void setEncryptionName(const QString&); + // XEP-0382: Spoiler messages bool isSpoiler() const; void setIsSpoiler(bool); diff --git a/tests/qxmppmessage/tst_qxmppmessage.cpp b/tests/qxmppmessage/tst_qxmppmessage.cpp index 9441acef0..5fe4b9644 100644 --- a/tests/qxmppmessage/tst_qxmppmessage.cpp +++ b/tests/qxmppmessage/tst_qxmppmessage.cpp @@ -50,6 +50,7 @@ private slots: void testMessageCorrect(); void testMessageAttaching(); void testMix(); + void testEme(); void testSpoiler(); }; @@ -114,6 +115,7 @@ void tst_QXmppMessage::testBasic() QCOMPARE(message.isReceiptRequested(), false); QCOMPARE(message.receiptId(), QString()); QCOMPARE(message.xhtml(), QString()); + QCOMPARE(message.encryptionMethod(), QXmppMessage::NoEncryption); QVERIFY(!message.isSpoiler()); serializePacket(message, xml); } @@ -663,6 +665,50 @@ void tst_QXmppMessage::testMix() QCOMPARE(message.mixUserNick(), QString("erik")); } +void tst_QXmppMessage::testEme() +{ + // test standard encryption: OMEMO + const QByteArray xmlOmemo( + "" + "This message is encrypted with OMEMO, but your client doesn't seem to support that." + "" + ""); + + QXmppMessage messageOmemo; + parsePacket(messageOmemo, xmlOmemo); + QCOMPARE(messageOmemo.encryptionMethodNs(), QString("eu.siacs.conversations.axolotl")); + QCOMPARE(messageOmemo.encryptionMethod(), QXmppMessage::OMEMO); + QCOMPARE(messageOmemo.encryptionName(), QString("OMEMO")); + serializePacket(messageOmemo, xmlOmemo); + + // test custom encryption + const QByteArray xmlCustom( + "" + "This message is encrypted with CustomCrypt, but your client doesn't seem to support that." + "" + ""); + + QXmppMessage messageCustom; + parsePacket(messageCustom, xmlCustom); + QCOMPARE(messageCustom.encryptionMethodNs(), QString("im:example:customcrypt:1")); + QCOMPARE(messageCustom.encryptionMethod(), QXmppMessage::UnknownEncryption); + QCOMPARE(messageCustom.encryptionName(), QString("CustomCrypt")); + serializePacket(messageCustom, xmlCustom); + + // test setters/getters + QXmppMessage message; + message.setEncryptionMethod(QXmppMessage::LegacyOpenPGP); + QCOMPARE(message.encryptionMethod(), QXmppMessage::LegacyOpenPGP); + QCOMPARE(message.encryptionMethodNs(), QString("jabber:x:encrypted")); + QCOMPARE(message.encryptionName(), QString("Legacy OpenPGP")); + + message.setEncryptionMethodNs("fancyorg:encryption:fancycrypt:0"); + message.setEncryptionName("FancyCrypt"); + QCOMPARE(message.encryptionMethod(), QXmppMessage::UnknownEncryption); + QCOMPARE(message.encryptionMethodNs(), QString("fancyorg:encryption:fancycrypt:0")); + QCOMPARE(message.encryptionName(), QString("FancyCrypt")); +} + void tst_QXmppMessage::testSpoiler() { // test parsing with hint