diff --git a/.clang-format b/.clang-format
index 272f06b9f..80e9df818 100644
--- a/.clang-format
+++ b/.clang-format
@@ -29,6 +29,7 @@ IncludeCategories:
Priority: 4
InsertBraces: true
Language: Cpp
+NamespaceIndentation: None
PointerAlignment: Right
ReflowComments: true
SortIncludes: true
diff --git a/doc/doap.xml b/doc/doap.xml
index bfc0c2615..785c7e6a1 100644
--- a/doc/doap.xml
+++ b/doc/doap.xml
@@ -41,6 +41,7 @@ SPDX-License-Identifier: CC0-1.0
+
@@ -684,6 +685,14 @@ SPDX-License-Identifier: CC0-1.0
1.6
+
+
+
+ complete
+ 0.1.0
+ 1.8
+
+
1.7.0
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 21763e2d4..e3d32018e 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -114,6 +114,7 @@ set(INSTALL_HEADER_FILES
client/QXmppClient.h
client/QXmppClientExtension.h
client/QXmppConfiguration.h
+ client/QXmppCredentials.h
client/QXmppDiscoveryManager.h
client/QXmppE2eeExtension.h
client/QXmppEntityTimeManager.h
diff --git a/src/base/Algorithms.h b/src/base/Algorithms.h
index 759485513..27eecdd37 100644
--- a/src/base/Algorithms.h
+++ b/src/base/Algorithms.h
@@ -6,15 +6,25 @@
#define ALGORITHMS_H
#include
+#include
+
+namespace std {
+template
+class optional;
+}
namespace QXmpp::Private {
template
-auto transform(InputVector &input, Converter convert)
+auto transform(const InputVector &input, Converter convert)
{
OutputVector output;
- output.reserve(input.size());
- std::transform(input.begin(), input.end(), std::back_inserter(output), std::forward(convert));
+ if constexpr (std::ranges::sized_range) {
+ output.reserve(input.size());
+ }
+ for (const auto &value : input) {
+ output.push_back(std::invoke(convert, value));
+ }
return output;
}
@@ -24,6 +34,24 @@ auto contains(const Vec &vec, const T &value)
return std::find(std::begin(vec), std::end(vec), value) != std::end(vec);
}
+template
+auto map(Function mapValue, std::optional &&optValue) -> std::optional>
+{
+ if (optValue) {
+ return mapValue(std::move(*optValue));
+ }
+ return {};
+}
+
+template
+auto into(std::optional &&value) -> std::optional
+{
+ if (value) {
+ return To { *value };
+ }
+ return {};
+}
+
} // namespace QXmpp::Private
#endif // ALGORITHMS_H
diff --git a/src/base/QXmppConstants_p.h b/src/base/QXmppConstants_p.h
index 8c3517840..89a1aff03 100644
--- a/src/base/QXmppConstants_p.h
+++ b/src/base/QXmppConstants_p.h
@@ -25,6 +25,7 @@ constexpr int XMPP_DEFAULT_PORT = 5222;
}
// QXmpp
+inline constexpr QStringView ns_qxmpp_credentials = u"org.qxmpp.credentials";
inline constexpr QStringView ns_qxmpp_export = u"org.qxmpp.export";
// XMPP
inline constexpr QStringView ns_stream = u"http://etherx.jabber.org/streams";
@@ -258,5 +259,7 @@ inline constexpr QStringView ns_esfs = u"urn:xmpp:esfs:0";
inline constexpr QStringView ns_atm = u"urn:xmpp:atm:1";
// XEP-0482: Call Invites
inline constexpr QStringView ns_call_invites = u"urn:xmpp:call-invites:0";
+// XEP-0484: Fast Authentication Streamlining Tokens
+inline constexpr auto ns_fast = u"urn:xmpp:fast:0";
#endif // QXMPPCONSTANTS_H
diff --git a/src/base/QXmppSasl.cpp b/src/base/QXmppSasl.cpp
index 5b3c8d93e..0b495ace7 100644
--- a/src/base/QXmppSasl.cpp
+++ b/src/base/QXmppSasl.cpp
@@ -11,16 +11,24 @@
#include "QXmppUtils.h"
#include "QXmppUtils_p.h"
+#include "Algorithms.h"
#include "StringLiterals.h"
#include
#include
+#include
#include
#include
#include
+using std::visit;
using namespace QXmpp::Private;
+template
+struct overloaded : Ts... {
+ using Ts::operator()...;
+};
+
static QByteArray forcedNonce;
constexpr auto SASL_ERROR_CONDITIONS = to_array({
@@ -37,7 +45,25 @@ constexpr auto SASL_ERROR_CONDITIONS = to_array({
u"temporary-auth-failure",
});
-namespace QXmpp::Private::Sasl {
+// https://www.iana.org/assignments/named-information/named-information.xhtml#hash-alg
+constexpr auto ianaHashAlgorithms = to_array({
+ u"SHA-256",
+ u"SHA-384",
+ u"SHA-512",
+ u"SHA3-224",
+ u"SHA3-256",
+ u"SHA3-384",
+ u"SHA3-512",
+#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+ u"BLAKE2S-256",
+ u"BLAKE2B-256",
+ u"BLAKE2B-512",
+#endif
+});
+
+namespace QXmpp::Private {
+
+namespace Sasl {
QString errorConditionToString(ErrorCondition c)
{
@@ -170,9 +196,7 @@ void Success::toXml(QXmlStreamWriter *writer) const
writer->writeEndElement();
}
-} // namespace QXmpp::Private::Sasl
-
-namespace QXmpp::Private {
+} // namespace Sasl
std::optional Bind2Feature::fromDom(const QDomElement &el)
{
@@ -262,9 +286,92 @@ void Bind2Bound::toXml(QXmlStreamWriter *writer) const
writer->writeEndElement();
}
-} // namespace QXmpp::Private
+std::optional FastFeature::fromDom(const QDomElement &el)
+{
+ if (el.tagName() != u"fast" || el.namespaceURI() != ns_fast) {
+ return {};
+ }
+
+ return FastFeature {
+ .mechanisms = parseTextElements(iterChildElements(el, u"mechanism", ns_fast)),
+ .tls0rtt = parseBoolean(el.attribute(QStringLiteral("tls-0rtt"))).value_or(false),
+ };
+}
+
+void FastFeature::toXml(QXmlStreamWriter *writer) const
+{
+ writer->writeStartElement(QSL65("fast"));
+ writer->writeDefaultNamespace(toString65(ns_fast));
+ for (const auto &mechanism : mechanisms) {
+ writer->writeStartElement(QSL65("mechanism"));
+ writer->writeCharacters(mechanism);
+ writer->writeEndElement();
+ }
+ writer->writeEndElement();
+}
+
+std::optional FastTokenRequest::fromDom(const QDomElement &el)
+{
+ if (el.tagName() != u"request-token" || el.namespaceURI() != ns_fast) {
+ return {};
+ }
+ return FastTokenRequest { el.attribute(QStringLiteral("mechanism")) };
+}
+
+void FastTokenRequest::toXml(QXmlStreamWriter *writer) const
+{
+ writer->writeStartElement(QSL65("request-token"));
+ writer->writeDefaultNamespace(toString65(ns_fast));
+ writer->writeAttribute(QSL65("mechanism"), mechanism);
+ writer->writeEndElement();
+}
+
+std::optional FastToken::fromDom(const QDomElement &el)
+{
+ if (el.tagName() != u"token" || el.namespaceURI() != ns_fast) {
+ return {};
+ }
+
+ return FastToken {
+ QXmppUtils::datetimeFromString(el.attribute(QStringLiteral("expiry"))),
+ el.attribute(QStringLiteral("token")),
+ };
+}
+
+void FastToken::toXml(QXmlStreamWriter *writer) const
+{
+ writer->writeStartElement(QSL65("token"));
+ writer->writeDefaultNamespace(toString65(ns_fast));
+ writer->writeAttribute(QSL65("expiry"), QXmppUtils::datetimeToString(expiry));
+ writer->writeAttribute(QSL65("token"), token);
+ writer->writeEndElement();
+}
+
+std::optional FastRequest::fromDom(const QDomElement &el)
+{
+ if (el.tagName() != u"fast" || el.namespaceURI() != ns_fast) {
+ return {};
+ }
+ return FastRequest {
+ parseInt(el.attribute(QStringLiteral("count"))),
+ parseBoolean(el.attribute(QStringLiteral("invalidate"))).value_or(false),
+ };
+}
+
+void FastRequest::toXml(QXmlStreamWriter *writer) const
+{
+ writer->writeStartElement(QSL65("fast"));
+ writer->writeDefaultNamespace(toString65(ns_fast));
+ if (count) {
+ writer->writeAttribute(QSL65("count"), QString::number(*count));
+ }
+ if (invalidate) {
+ writer->writeAttribute(QSL65("invalidate"), QSL65("true"));
+ }
+ writer->writeEndElement();
+}
-namespace QXmpp::Private::Sasl2 {
+namespace Sasl2 {
std::optional StreamFeature::fromDom(const QDomElement &el)
{
@@ -280,6 +387,7 @@ std::optional StreamFeature::fromDom(const QDomElement &el)
if (auto inlineEl = firstChildElement(el, u"inline", ns_sasl_2); !inlineEl.isNull()) {
feature.bind2Feature = Bind2Feature::fromDom(firstChildElement(inlineEl, u"bind", ns_bind2));
+ feature.fast = FastFeature::fromDom(firstChildElement(inlineEl, u"fast", ns_fast));
feature.streamResumptionAvailable = !firstChildElement(inlineEl, u"sm", ns_stream_management).isNull();
}
return feature;
@@ -292,11 +400,12 @@ void StreamFeature::toXml(QXmlStreamWriter *writer) const
for (const auto &mechanism : mechanisms) {
writeXmlTextElement(writer, u"mechanism", mechanism);
}
- if (bind2Feature || streamResumptionAvailable) {
+ if (bind2Feature || fast || streamResumptionAvailable) {
writer->writeStartElement(QSL65("inline"));
if (bind2Feature) {
bind2Feature->toXml(writer);
}
+ writeOptional(writer, fast);
if (streamResumptionAvailable) {
writeEmptyElement(writer, u"sm", ns_stream_management);
}
@@ -340,6 +449,8 @@ std::optional Authenticate::fromDom(const QDomElement &el)
UserAgent::fromDom(firstChildElement(el, u"user-agent", ns_sasl_2)),
Bind2Request::fromDom(firstChildElement(el, u"bind", ns_bind2)),
SmResume::fromDom(firstChildElement(el, u"resume", ns_stream_management)),
+ FastTokenRequest::fromDom(firstChildElement(el, u"request-token", ns_fast)),
+ FastRequest::fromDom(firstChildElement(el, u"fast", ns_fast)),
};
}
@@ -358,6 +469,8 @@ void Authenticate::toXml(QXmlStreamWriter *writer) const
if (smResume) {
smResume->toXml(writer);
}
+ writeOptional(writer, tokenRequest);
+ writeOptional(writer, fast);
writer->writeEndElement();
}
@@ -416,6 +529,7 @@ std::optional Success::fromDom(const QDomElement &el)
output.bound = Bind2Bound::fromDom(firstChildElement(el, u"bound", ns_bind2));
output.smResumed = SmResumed::fromDom(firstChildElement(el, u"resumed", ns_stream_management));
output.smFailed = SmFailed::fromDom(firstChildElement(el, u"failed", ns_stream_management));
+ output.token = FastToken::fromDom(firstChildElement(el, u"token", ns_fast));
return output;
}
@@ -437,6 +551,7 @@ void Success::toXml(QXmlStreamWriter *writer) const
if (smFailed) {
smFailed->toXml(writer);
}
+ writeOptional(writer, token);
writer->writeEndElement();
}
@@ -527,7 +642,208 @@ void Abort::toXml(QXmlStreamWriter *writer) const
writer->writeEndElement();
}
-} // namespace QXmpp::Private::Sasl2
+} // namespace Sasl2
+
+QCryptographicHash::Algorithm ianaHashAlgorithmToQt(IanaHashAlgorithm alg)
+{
+#define CASE(_algorithm) \
+ case IanaHashAlgorithm::_algorithm: \
+ return QCryptographicHash::_algorithm;
+
+ switch (alg) {
+ CASE(Sha256)
+ CASE(Sha384)
+ CASE(Sha512)
+ CASE(Sha3_224)
+ CASE(Sha3_256)
+ CASE(Sha3_384)
+ CASE(Sha3_512)
+#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+ CASE(Blake2s_256)
+ CASE(Blake2b_256)
+ CASE(Blake2b_512)
+#endif
+ }
+ Q_UNREACHABLE();
+#undef CASE
+}
+
+std::optional SaslScramMechanism::fromString(QStringView str)
+{
+ if (str == u"SCRAM-SHA-1") {
+ return { { Sha1 } };
+ }
+ if (str == u"SCRAM-SHA-256") {
+ return { { Sha256 } };
+ }
+ if (str == u"SCRAM-SHA-512") {
+ return { { Sha512 } };
+ }
+ if (str == u"SCRAM-SHA3-512") {
+ return { { Sha3_512 } };
+ }
+ return {};
+}
+
+QString SaslScramMechanism::toString() const
+{
+ switch (algorithm) {
+ case Sha1:
+ return u"SCRAM-SHA-1"_s;
+ case Sha256:
+ return u"SCRAM-SHA-256"_s;
+ case Sha512:
+ return u"SCRAM-SHA-512"_s;
+ case Sha3_512:
+ return u"SCRAM-SHA3-512"_s;
+ }
+ Q_UNREACHABLE();
+}
+
+QCryptographicHash::Algorithm SaslScramMechanism::qtAlgorithm() const
+{
+ switch (algorithm) {
+ case Sha1:
+ return QCryptographicHash::Sha1;
+ case Sha256:
+ return QCryptographicHash::Sha256;
+ case Sha512:
+ return QCryptographicHash::Sha512;
+ case Sha3_512:
+ return QCryptographicHash::Sha3_512;
+ }
+ Q_UNREACHABLE();
+}
+
+std::optional SaslHtMechanism::fromString(QStringView string)
+{
+ // prefix
+ static constexpr QStringView prefix = u"HT-";
+ if (!string.startsWith(prefix)) {
+ return {};
+ }
+ string = string.mid(prefix.size());
+
+ // hash algorithm
+ // C++23: use enumerate view
+ std::optional algorithm;
+ for (size_t i = 0; i < ianaHashAlgorithms.size(); ++i) {
+ if (string.startsWith(ianaHashAlgorithms.at(i))) {
+ algorithm = IanaHashAlgorithm(i);
+ string = string.mid(ianaHashAlgorithms.at(i).size());
+ }
+ }
+ if (!algorithm) {
+ return {};
+ }
+
+ // channel-binding type
+ if (string == u"-ENDP") {
+ return SaslHtMechanism { *algorithm, TlsServerEndpoint };
+ }
+ if (string == u"-UNIQ") {
+ return SaslHtMechanism { *algorithm, TlsUnique };
+ }
+ if (string == u"-EXPR") {
+ return SaslHtMechanism { *algorithm, TlsExporter };
+ }
+ if (string == u"-NONE") {
+ return SaslHtMechanism { *algorithm, None };
+ }
+ return {};
+}
+
+static QStringView channelBindingTypeToString(SaslHtMechanism::ChannelBindingType t)
+{
+ switch (t) {
+ case SaslHtMechanism::TlsServerEndpoint:
+ return u"ENDP";
+ case SaslHtMechanism::TlsUnique:
+ return u"UNIQ";
+ case SaslHtMechanism::TlsExporter:
+ return u"EXPR";
+ case SaslHtMechanism::None:
+ return u"NONE";
+ }
+ Q_UNREACHABLE();
+}
+
+QString SaslHtMechanism::toString() const
+{
+ return u"HT-" + ianaHashAlgorithms.at(size_t(hashAlgorithm)) + u'-' + channelBindingTypeToString(channelBindingType);
+}
+
+std::optional SaslMechanism::fromString(QStringView str)
+{
+ if (str.startsWith(u"SCRAM-")) {
+ return into(SaslScramMechanism::fromString(str));
+ }
+ if (str.startsWith(u"HT-")) {
+ return into(SaslHtMechanism::fromString(str));
+ }
+ if (str == u"DIGEST-MD5") {
+ return { { SaslDigestMd5Mechanism() } };
+ }
+ if (str == u"PLAIN") {
+ return { { SaslPlainMechanism() } };
+ }
+ if (str == u"ANONYMOUS") {
+ return { { SaslAnonymousMechanism() } };
+ }
+ if (str == u"X-FACEBOOK-PLATFORM") {
+ return { { SaslXFacebookMechanism() } };
+ }
+ if (str == u"X-MESSENGER-OAUTH2") {
+ return { { SaslXWindowsLiveMechanism() } };
+ }
+ if (str == u"X-OAUTH2") {
+ return { { SaslXGoogleMechanism() } };
+ }
+ return {};
+}
+
+QString SaslMechanism::toString() const
+{
+ return visit(
+ overloaded {
+ [](SaslScramMechanism scram) { return scram.toString(); },
+ [](SaslHtMechanism ht) { return ht.toString(); },
+ [](SaslDigestMd5Mechanism) { return u"DIGEST-MD5"_s; },
+ [](SaslPlainMechanism) { return u"PLAIN"_s; },
+ [](SaslAnonymousMechanism) { return u"ANONYMOUS"_s; },
+ [](SaslXFacebookMechanism) { return u"X-FACEBOOK-PLATFORM"_s; },
+ [](SaslXWindowsLiveMechanism) { return u"X-MESSENGER-OAUTH2"_s; },
+ [](SaslXGoogleMechanism) { return u"X-OAUTH2"_s; },
+ },
+ *this);
+}
+
+std::optional HtToken::fromXml(QXmlStreamReader &r)
+{
+ if (r.name() != u"ht-token" || r.namespaceUri() != ns_qxmpp_credentials) {
+ return {};
+ }
+ const auto &attrs = r.attributes();
+ if (auto mechanism = SaslHtMechanism::fromString(attrs.value("mechanism"_L1))) {
+ return HtToken {
+ *mechanism,
+ attrs.value("secret"_L1).toString(),
+ QXmppUtils::datetimeFromString(toString60(attrs.value("expiry"_L1))),
+ };
+ }
+ return {};
+}
+
+void HtToken::toXml(QXmlStreamWriter &w) const
+{
+ w.writeStartElement(QSL65("ht-token"));
+ w.writeAttribute(QSL65("mechanism"), mechanism.toString());
+ w.writeAttribute(QSL65("secret"), secret);
+ w.writeAttribute(QSL65("expiry"), expiry.toString(Qt::ISODate));
+ w.writeEndElement();
+}
+
+} // namespace QXmpp::Private
///
/// \class QXmppSasl2UserAgent
@@ -606,14 +922,6 @@ void QXmppSasl2UserAgent::setDeviceName(const QString &device)
d->device = device;
}
-// When adding new algorithms, also add them to QXmppSaslClient::availableMechanisms().
-static const QMap SCRAM_ALGORITHMS = {
- { u"SCRAM-SHA-1", QCryptographicHash::Sha1 },
- { u"SCRAM-SHA-256", QCryptographicHash::Sha256 },
- { u"SCRAM-SHA-512", QCryptographicHash::Sha512 },
- { u"SCRAM-SHA3-512", QCryptographicHash::RealSha3_512 },
-};
-
// Calculate digest response for use with XMPP/SASL.
static QByteArray calculateDigest(const QByteArray &method, const QByteArray &digestUri, const QByteArray &secret, const QByteArray &nonce, const QByteArray &cnonce, const QByteArray &nc)
@@ -627,36 +935,6 @@ static QByteArray calculateDigest(const QByteArray &method, const QByteArray &di
return QCryptographicHash::hash(KD, QCryptographicHash::Md5).toHex();
}
-// Perform PBKFD2 key derivation, code taken from Qt 5.12
-
-static QByteArray deriveKeyPbkdf2(QCryptographicHash::Algorithm algorithm,
- const QByteArray &data, const QByteArray &salt,
- int iterations, uint32_t dkLen)
-{
- QByteArray key;
- quint32 currentIteration = 1;
- QMessageAuthenticationCode hmac(algorithm, data);
- QByteArray index(4, Qt::Uninitialized);
- while (key.length() < dkLen) {
- hmac.addData(salt);
- qToBigEndian(currentIteration, reinterpret_cast(index.data()));
- hmac.addData(index);
- QByteArray u = hmac.result();
- hmac.reset();
- QByteArray tkey = u;
- for (int iter = 1; iter < iterations; iter++) {
- hmac.addData(u);
- u = hmac.result();
- hmac.reset();
- std::transform(tkey.cbegin(), tkey.cend(), u.cbegin(), tkey.begin(),
- std::bit_xor());
- }
- key += tkey;
- currentIteration++;
- }
- return key.left(dkLen);
-}
-
static QByteArray generateNonce()
{
if (!forcedNonce.isEmpty()) {
@@ -682,112 +960,74 @@ static QMap parseGS2(const QByteArray &ba)
return map;
}
-class QXmppSaslClientPrivate
-{
-public:
- QString host;
- QString serviceType;
- QString username;
- QString password;
-};
-
-QXmppSaslClient::QXmppSaslClient(QObject *parent)
- : QXmppLoggable(parent),
- d(std::make_unique())
-{
-}
-
-QXmppSaslClient::~QXmppSaslClient() = default;
-
-///
-/// Returns a list of supported mechanisms.
-///
-QStringList QXmppSaslClient::availableMechanisms()
-{
- return {
- u"SCRAM-SHA3-512"_s,
- u"SCRAM-SHA-512"_s,
- u"SCRAM-SHA-256"_s,
- u"SCRAM-SHA-1"_s,
- u"DIGEST-MD5"_s,
- u"PLAIN"_s,
- u"ANONYMOUS"_s,
- u"X-FACEBOOK-PLATFORM"_s,
- u"X-MESSENGER-OAUTH2"_s,
- u"X-OAUTH2"_s,
- };
+bool QXmppSaslClient::isMechanismAvailable(SaslMechanism mechanism, const Credentials &credentials)
+{
+ return visit(
+ overloaded {
+ [&](SaslHtMechanism ht) {
+ return credentials.htToken &&
+ credentials.htToken->mechanism == ht &&
+ ht.channelBindingType == SaslHtMechanism::None;
+ },
+ [&](std::variant) {
+ return !credentials.password.isEmpty();
+ },
+ [&](SaslXFacebookMechanism) {
+ return !credentials.facebookAccessToken.isEmpty() && !credentials.facebookAppId.isEmpty();
+ },
+ [&](SaslXWindowsLiveMechanism) {
+ return !credentials.windowsLiveAccessToken.isEmpty();
+ },
+ [&](SaslXGoogleMechanism) {
+ return !credentials.googleAccessToken.isEmpty();
+ },
+ [](SaslAnonymousMechanism) {
+ return true;
+ } },
+ mechanism);
}
///
/// Creates an SASL client for the given mechanism.
///
-std::unique_ptr QXmppSaslClient::create(const QString &mechanism, QObject *parent)
-{
- if (mechanism == u"PLAIN") {
- return std::make_unique(parent);
- } else if (mechanism == u"DIGEST-MD5") {
- return std::make_unique(parent);
- } else if (mechanism == u"ANONYMOUS") {
- return std::make_unique(parent);
- } else if (SCRAM_ALGORITHMS.contains(mechanism)) {
- return std::make_unique(SCRAM_ALGORITHMS.value(mechanism), parent);
- } else if (mechanism == u"X-FACEBOOK-PLATFORM") {
- return std::make_unique(parent);
- } else if (mechanism == u"X-MESSENGER-OAUTH2") {
- return std::make_unique(parent);
- } else if (mechanism == u"X-OAUTH2") {
- return std::make_unique(parent);
- } else {
- return nullptr;
- }
-}
-
-/// Returns the host.
-QString QXmppSaslClient::host() const
-{
- return d->host;
-}
-
-/// Sets the host.
-void QXmppSaslClient::setHost(const QString &host)
-{
- d->host = host;
-}
-
-/// Returns the service type, e.g. "xmpp".
-QString QXmppSaslClient::serviceType() const
-{
- return d->serviceType;
-}
-
-/// Sets the service type, e.g. "xmpp".
-void QXmppSaslClient::setServiceType(const QString &serviceType)
-{
- d->serviceType = serviceType;
-}
-
-/// Returns the username.
-QString QXmppSaslClient::username() const
-{
- return d->username;
-}
-
-/// Sets the username.
-void QXmppSaslClient::setUsername(const QString &username)
-{
- d->username = username;
-}
-
-/// Returns the password.
-QString QXmppSaslClient::password() const
-{
- return d->password;
-}
-
-/// Sets the password.
-void QXmppSaslClient::setPassword(const QString &password)
-{
- d->password = password;
+std::unique_ptr QXmppSaslClient::create(const QString &string, QObject *parent)
+{
+ if (auto mechanism = SaslMechanism::fromString(string)) {
+ return create(*mechanism, parent);
+ }
+ return nullptr;
+}
+
+std::unique_ptr QXmppSaslClient::create(SaslMechanism mechanism, QObject *parent)
+{
+ return visit>(
+ overloaded {
+ [&](SaslScramMechanism scram) {
+ return std::make_unique(scram, parent);
+ },
+ [&](SaslHtMechanism ht) {
+ return std::make_unique(ht, parent);
+ },
+ [&](SaslPlainMechanism) {
+ return std::make_unique(parent);
+ },
+ [&](SaslDigestMd5Mechanism) {
+ return std::make_unique(parent);
+ },
+ [&](SaslAnonymousMechanism) {
+ return std::make_unique(parent);
+ },
+ [&](SaslXFacebookMechanism) {
+ return std::make_unique(parent);
+ },
+ [&](SaslXWindowsLiveMechanism) {
+ return std::make_unique(parent);
+ },
+ [&](SaslXGoogleMechanism) {
+ return std::make_unique(parent);
+ },
+ },
+ mechanism);
}
QXmppSaslClientAnonymous::QXmppSaslClientAnonymous(QObject *parent)
@@ -795,11 +1035,6 @@ QXmppSaslClientAnonymous::QXmppSaslClientAnonymous(QObject *parent)
{
}
-QString QXmppSaslClientAnonymous::mechanism() const
-{
- return u"ANONYMOUS"_s;
-}
-
std::optional QXmppSaslClientAnonymous::respond(const QByteArray &)
{
if (m_step == 0) {
@@ -817,9 +1052,9 @@ QXmppSaslClientDigestMd5::QXmppSaslClientDigestMd5(QObject *parent)
m_cnonce = generateNonce();
}
-QString QXmppSaslClientDigestMd5::mechanism() const
+void QXmppSaslClientDigestMd5::setCredentials(const QXmpp::Private::Credentials &credentials)
{
- return u"DIGEST-MD5"_s;
+ m_password = credentials.password;
}
std::optional QXmppSaslClientDigestMd5::respond(const QByteArray &challenge)
@@ -849,7 +1084,7 @@ std::optional QXmppSaslClientDigestMd5::respond(const QByteArray &ch
m_nonce = input.value(QByteArrayLiteral("nonce"));
m_secret = QCryptographicHash::hash(
- QByteArray(username().toUtf8() + QByteArrayLiteral(":") + realm + QByteArrayLiteral(":") + password().toUtf8()),
+ QByteArray(username().toUtf8() + QByteArrayLiteral(":") + realm + QByteArrayLiteral(":") + m_password.toUtf8()),
QCryptographicHash::Md5);
// Build response
@@ -890,11 +1125,11 @@ QXmppSaslClientFacebook::QXmppSaslClientFacebook(QObject *parent)
{
}
-QString QXmppSaslClientFacebook::mechanism() const
+void QXmppSaslClientFacebook::setCredentials(const QXmpp::Private::Credentials &credentials)
{
- return u"X-FACEBOOK-PLATFORM"_s;
+ m_accessToken = credentials.facebookAccessToken;
+ m_appId = credentials.facebookAppId;
}
-
std::optional QXmppSaslClientFacebook::respond(const QByteArray &challenge)
{
if (m_step == 0) {
@@ -911,8 +1146,8 @@ std::optional QXmppSaslClientFacebook::respond(const QByteArray &cha
// build response
QUrlQuery responseUrl;
- responseUrl.addQueryItem(u"access_token"_s, password());
- responseUrl.addQueryItem(u"api_key"_s, username());
+ responseUrl.addQueryItem(u"access_token"_s, m_accessToken);
+ responseUrl.addQueryItem(u"api_key"_s, m_appId);
responseUrl.addQueryItem(u"call_id"_s, QString());
responseUrl.addQueryItem(u"method"_s, requestUrl.queryItemValue(u"method"_s));
responseUrl.addQueryItem(u"nonce"_s, requestUrl.queryItemValue(u"nonce"_s));
@@ -931,9 +1166,9 @@ QXmppSaslClientGoogle::QXmppSaslClientGoogle(QObject *parent)
{
}
-QString QXmppSaslClientGoogle::mechanism() const
+void QXmppSaslClientGoogle::setCredentials(const QXmpp::Private::Credentials &credentials)
{
- return u"X-OAUTH2"_s;
+ m_accessToken = credentials.googleAccessToken;
}
std::optional QXmppSaslClientGoogle::respond(const QByteArray &)
@@ -941,7 +1176,7 @@ std::optional QXmppSaslClientGoogle::respond(const QByteArray &)
if (m_step == 0) {
// send initial response
m_step++;
- return QString(u'\0' + username() + u'\0' + password()).toUtf8();
+ return QString(u'\0' + username() + u'\0' + m_accessToken).toUtf8();
} else {
warning(u"QXmppSaslClientGoogle : Invalid step"_s);
return {};
@@ -953,37 +1188,34 @@ QXmppSaslClientPlain::QXmppSaslClientPlain(QObject *parent)
{
}
-QString QXmppSaslClientPlain::mechanism() const
+void QXmppSaslClientPlain::setCredentials(const QXmpp::Private::Credentials &credentials)
{
- return u"PLAIN"_s;
+ m_password = credentials.password;
}
std::optional QXmppSaslClientPlain::respond(const QByteArray &)
{
if (m_step == 0) {
m_step++;
- return QString(u'\0' + username() + u'\0' + password()).toUtf8();
+ return QString(u'\0' + username() + u'\0' + m_password).toUtf8();
} else {
warning(u"QXmppSaslClientPlain : Invalid step"_s);
return {};
}
}
-QXmppSaslClientScram::QXmppSaslClientScram(QCryptographicHash::Algorithm algorithm, QObject *parent)
+QXmppSaslClientScram::QXmppSaslClientScram(SaslScramMechanism mechanism, QObject *parent)
: QXmppSaslClient(parent),
- m_algorithm(algorithm),
+ m_mechanism(mechanism),
m_step(0),
- m_dklen(QCryptographicHash::hashLength(algorithm))
+ m_dklen(QCryptographicHash::hashLength(m_mechanism.qtAlgorithm()))
{
- const auto itr = std::find(SCRAM_ALGORITHMS.cbegin(), SCRAM_ALGORITHMS.cend(), algorithm);
- Q_ASSERT(itr != SCRAM_ALGORITHMS.cend());
-
m_nonce = generateNonce();
}
-QString QXmppSaslClientScram::mechanism() const
+void QXmppSaslClientScram::setCredentials(const QXmpp::Private::Credentials &credentials)
{
- return SCRAM_ALGORITHMS.key(m_algorithm).toString();
+ m_password = credentials.password;
}
std::optional QXmppSaslClientScram::respond(const QByteArray &challenge)
@@ -1006,17 +1238,17 @@ std::optional QXmppSaslClientScram::respond(const QByteArray &challe
// calculate proofs
const QByteArray clientFinalMessageBare = QByteArrayLiteral("c=") + m_gs2Header.toBase64() + QByteArrayLiteral(",r=") + nonce;
- const QByteArray saltedPassword = deriveKeyPbkdf2(m_algorithm, password().toUtf8(), salt,
- iterations, m_dklen);
- const QByteArray clientKey = QMessageAuthenticationCode::hash(QByteArrayLiteral("Client Key"), saltedPassword, m_algorithm);
- const QByteArray storedKey = QCryptographicHash::hash(clientKey, m_algorithm);
+ const QByteArray saltedPassword = QPasswordDigestor::deriveKeyPbkdf2(
+ m_mechanism.qtAlgorithm(), m_password.toUtf8(), salt, iterations, m_dklen);
+ const QByteArray clientKey = QMessageAuthenticationCode::hash(QByteArrayLiteral("Client Key"), saltedPassword, m_mechanism.qtAlgorithm());
+ const QByteArray storedKey = QCryptographicHash::hash(clientKey, m_mechanism.qtAlgorithm());
const QByteArray authMessage = m_clientFirstMessageBare + QByteArrayLiteral(",") + challenge + QByteArrayLiteral(",") + clientFinalMessageBare;
- QByteArray clientProof = QMessageAuthenticationCode::hash(authMessage, storedKey, m_algorithm);
+ QByteArray clientProof = QMessageAuthenticationCode::hash(authMessage, storedKey, m_mechanism.qtAlgorithm());
std::transform(clientProof.cbegin(), clientProof.cend(), clientKey.cbegin(),
clientProof.begin(), std::bit_xor());
- const QByteArray serverKey = QMessageAuthenticationCode::hash(QByteArrayLiteral("Server Key"), saltedPassword, m_algorithm);
- m_serverSignature = QMessageAuthenticationCode::hash(authMessage, serverKey, m_algorithm);
+ const QByteArray serverKey = QMessageAuthenticationCode::hash(QByteArrayLiteral("Server Key"), saltedPassword, m_mechanism.qtAlgorithm());
+ m_serverSignature = QMessageAuthenticationCode::hash(authMessage, serverKey, m_mechanism.qtAlgorithm());
m_step++;
return clientFinalMessageBare + QByteArrayLiteral(",p=") + clientProof.toBase64();
@@ -1038,9 +1270,9 @@ QXmppSaslClientWindowsLive::QXmppSaslClientWindowsLive(QObject *parent)
{
}
-QString QXmppSaslClientWindowsLive::mechanism() const
+void QXmppSaslClientWindowsLive::setCredentials(const QXmpp::Private::Credentials &credentials)
{
- return u"X-MESSENGER-OAUTH2"_s;
+ m_accessToken = credentials.windowsLiveAccessToken;
}
std::optional QXmppSaslClientWindowsLive::respond(const QByteArray &)
@@ -1048,7 +1280,7 @@ std::optional QXmppSaslClientWindowsLive::respond(const QByteArray &
if (m_step == 0) {
// send initial response
m_step++;
- return QByteArray::fromBase64(password().toLatin1());
+ return QByteArray::fromBase64(m_accessToken.toLatin1());
} else {
warning(u"QXmppSaslClientWindowsLive : Invalid step"_s);
return {};
@@ -1343,3 +1575,23 @@ QByteArray QXmppSaslDigestMd5::serializeMessage(const QMap QXmppSaslClientHt::respond(const QByteArray &challenge)
+{
+ // TODO: verify provided by SASL 2 (hmac of 'Responder' + cb data).
+
+ Q_ASSERT(m_mechanism.channelBindingType == QXmpp::Private::SaslHtMechanism::None);
+
+ if (m_done || !challenge.isEmpty() || !m_token || m_mechanism != m_token->mechanism) {
+ return {};
+ }
+
+ // calculate token hash
+ QMessageAuthenticationCode hmac(
+ ianaHashAlgorithmToQt(m_mechanism.hashAlgorithm),
+ m_token->secret.toUtf8());
+ hmac.addData("Initiator");
+
+ m_done = true;
+ return username().toUtf8() + char(0) + hmac.result();
+}
diff --git a/src/base/QXmppSasl_p.h b/src/base/QXmppSasl_p.h
index a13600ec4..faa9a69da 100644
--- a/src/base/QXmppSasl_p.h
+++ b/src/base/QXmppSasl_p.h
@@ -16,12 +16,12 @@
#include
#include
+#include
#include
#include
class QDomElement;
class QXmlStreamWriter;
-class QXmppSaslClientPrivate;
class QXmppSaslServerPrivate;
namespace QXmpp::Private {
@@ -41,7 +41,9 @@ class SaslManager;
// We mean it.
//
-namespace QXmpp::Private::Sasl {
+namespace QXmpp::Private {
+
+namespace Sasl {
enum class ErrorCondition {
Aborted,
@@ -95,9 +97,7 @@ struct Success {
void toXml(QXmlStreamWriter *writer) const;
};
-} // namespace QXmpp::Private::Sasl
-
-namespace QXmpp::Private {
+} // namespace Sasl
struct Bind2Feature {
static std::optional fromDom(const QDomElement &);
@@ -126,9 +126,38 @@ struct Bind2Bound {
std::optional smEnabled;
};
-} // namespace QXmpp::Private
+struct FastFeature {
+ static std::optional fromDom(const QDomElement &);
+ void toXml(QXmlStreamWriter *) const;
+
+ std::vector mechanisms;
+ bool tls0rtt = false;
+};
+
+struct FastTokenRequest {
+ static std::optional fromDom(const QDomElement &);
+ void toXml(QXmlStreamWriter *) const;
+
+ QString mechanism;
+};
+
+struct FastToken {
+ static std::optional fromDom(const QDomElement &);
+ void toXml(QXmlStreamWriter *) const;
+
+ QDateTime expiry;
+ QString token;
+};
+
+struct FastRequest {
+ static std::optional fromDom(const QDomElement &);
+ void toXml(QXmlStreamWriter *) const;
+
+ std::optional count;
+ bool invalidate = false;
+};
-namespace QXmpp::Private::Sasl2 {
+namespace Sasl2 {
struct StreamFeature {
static std::optional fromDom(const QDomElement &);
@@ -136,6 +165,7 @@ struct StreamFeature {
QList mechanisms;
std::optional bind2Feature;
+ std::optional fast;
bool streamResumptionAvailable = false;
};
@@ -157,6 +187,8 @@ struct Authenticate {
std::optional userAgent;
std::optional bindRequest;
std::optional smResume;
+ std::optional tokenRequest;
+ std::optional fast;
};
struct Challenge {
@@ -183,6 +215,7 @@ struct Success {
std::optional bound;
std::optional smResumed;
std::optional smFailed;
+ std::optional token;
};
struct Failure {
@@ -210,37 +243,156 @@ struct Abort {
QString text;
};
-} // namespace QXmpp::Private::Sasl2
+} // namespace Sasl2
+
+enum class IanaHashAlgorithm {
+ Sha256,
+ Sha384,
+ Sha512,
+ Sha3_224,
+ Sha3_256,
+ Sha3_384,
+ Sha3_512,
+#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+ Blake2s_256,
+ Blake2b_256,
+ Blake2b_512,
+#endif
+};
+
+QCryptographicHash::Algorithm ianaHashAlgorithmToQt(IanaHashAlgorithm alg);
+
+//
+// SASL mechanisms
+//
+
+struct SaslScramMechanism {
+ static std::optional fromString(QStringView str);
+ QString toString() const;
+
+ QCryptographicHash::Algorithm qtAlgorithm() const;
+
+ auto operator<=>(const SaslScramMechanism &) const = default;
+
+ enum Algorithm {
+ Sha1,
+ Sha256,
+ Sha512,
+ Sha3_512,
+ } algorithm;
+};
+
+struct SaslHtMechanism {
+ static std::optional fromString(QStringView);
+ QString toString() const;
+
+ auto operator<=>(const SaslHtMechanism &) const = default;
+
+ enum ChannelBindingType {
+ TlsServerEndpoint,
+ TlsUnique,
+ TlsExporter,
+ None,
+ };
+
+ IanaHashAlgorithm hashAlgorithm;
+ ChannelBindingType channelBindingType;
+};
+
+struct SaslDigestMd5Mechanism {
+ auto operator<=>(const SaslDigestMd5Mechanism &) const = default;
+};
+struct SaslPlainMechanism {
+ auto operator<=>(const SaslPlainMechanism &) const = default;
+};
+struct SaslAnonymousMechanism {
+ auto operator<=>(const SaslAnonymousMechanism &) const = default;
+};
+struct SaslXFacebookMechanism {
+ auto operator<=>(const SaslXFacebookMechanism &) const = default;
+};
+struct SaslXWindowsLiveMechanism {
+ auto operator<=>(const SaslXWindowsLiveMechanism &) const = default;
+};
+struct SaslXGoogleMechanism {
+ auto operator<=>(const SaslXGoogleMechanism &) const = default;
+};
+
+// Note that the order of the variant alternatives defines the preference/strength of the mechanisms.
+struct SaslMechanism
+ : std::variant {
+ static std::optional fromString(QStringView str);
+ QString toString() const;
+};
+
+inline QDebug operator<<(QDebug dbg, SaslMechanism mechanism) { return dbg << mechanism.toString(); }
+
+//
+// Credentials
+//
+
+struct HtToken {
+ static std::optional fromXml(QXmlStreamReader &);
+ void toXml(QXmlStreamWriter &) const;
+ bool operator==(const HtToken &other) const = default;
+
+ SaslHtMechanism mechanism;
+ QString secret;
+ QDateTime expiry;
+};
+
+struct Credentials {
+ QString password;
+ std::optional htToken;
+
+ // Facebook
+ QString facebookAccessToken;
+ QString facebookAppId;
+ // Google
+ QString googleAccessToken;
+ // Windows Live
+ QString windowsLiveAccessToken;
+};
+
+} // namespace QXmpp::Private
class QXMPP_AUTOTEST_EXPORT QXmppSaslClient : public QXmppLoggable
{
Q_OBJECT
public:
- QXmppSaslClient(QObject *parent = nullptr);
- ~QXmppSaslClient() override;
-
- QString host() const;
- void setHost(const QString &host);
+ QXmppSaslClient(QObject *parent) : QXmppLoggable(parent) { }
- QString serviceType() const;
- void setServiceType(const QString &serviceType);
+ QString host() const { return m_host; }
+ void setHost(const QString &host) { m_host = host; }
- QString username() const;
- void setUsername(const QString &username);
+ QString serviceType() const { return m_serviceType; }
+ void setServiceType(const QString &serviceType) { m_serviceType = serviceType; }
- QString password() const;
- void setPassword(const QString &password);
+ QString username() const { return m_username; }
+ void setUsername(const QString &username) { m_username = username; }
- virtual QString mechanism() const = 0;
+ virtual void setCredentials(const QXmpp::Private::Credentials &) = 0;
+ virtual QXmpp::Private::SaslMechanism mechanism() const = 0;
virtual std::optional respond(const QByteArray &challenge) = 0;
- static QStringList availableMechanisms();
+ static bool isMechanismAvailable(QXmpp::Private::SaslMechanism, const QXmpp::Private::Credentials &);
static std::unique_ptr create(const QString &mechanism, QObject *parent = nullptr);
+ static std::unique_ptr create(QXmpp::Private::SaslMechanism mechanism, QObject *parent = nullptr);
private:
friend class QXmpp::Private::SaslManager;
- const std::unique_ptr d;
+ QString m_host;
+ QString m_serviceType;
+ QString m_username;
+ QString m_password;
};
class QXMPP_AUTOTEST_EXPORT QXmppSaslServer : public QXmppLoggable
@@ -293,7 +445,8 @@ class QXmppSaslClientAnonymous : public QXmppSaslClient
Q_OBJECT
public:
QXmppSaslClientAnonymous(QObject *parent = nullptr);
- QString mechanism() const override;
+ void setCredentials(const QXmpp::Private::Credentials &) override { }
+ QXmpp::Private::SaslMechanism mechanism() const override { return { QXmpp::Private::SaslAnonymousMechanism() }; }
std::optional respond(const QByteArray &challenge) override;
private:
@@ -305,10 +458,12 @@ class QXmppSaslClientDigestMd5 : public QXmppSaslClient
Q_OBJECT
public:
QXmppSaslClientDigestMd5(QObject *parent = nullptr);
- QString mechanism() const override;
+ void setCredentials(const QXmpp::Private::Credentials &) override;
+ QXmpp::Private::SaslMechanism mechanism() const override { return { QXmpp::Private::SaslDigestMd5Mechanism() }; }
std::optional respond(const QByteArray &challenge) override;
private:
+ QString m_password;
QByteArray m_cnonce;
QByteArray m_nc;
QByteArray m_nonce;
@@ -321,11 +476,14 @@ class QXmppSaslClientFacebook : public QXmppSaslClient
Q_OBJECT
public:
QXmppSaslClientFacebook(QObject *parent = nullptr);
- QString mechanism() const override;
+ void setCredentials(const QXmpp::Private::Credentials &) override;
+ QXmpp::Private::SaslMechanism mechanism() const override { return { QXmpp::Private::SaslXFacebookMechanism() }; }
std::optional respond(const QByteArray &challenge) override;
private:
int m_step;
+ QString m_accessToken;
+ QString m_appId;
};
class QXmppSaslClientGoogle : public QXmppSaslClient
@@ -333,10 +491,12 @@ class QXmppSaslClientGoogle : public QXmppSaslClient
Q_OBJECT
public:
QXmppSaslClientGoogle(QObject *parent = nullptr);
- QString mechanism() const override;
+ void setCredentials(const QXmpp::Private::Credentials &) override;
+ QXmpp::Private::SaslMechanism mechanism() const override { return { QXmpp::Private::SaslXGoogleMechanism() }; }
std::optional respond(const QByteArray &challenge) override;
private:
+ QString m_accessToken;
int m_step;
};
@@ -345,10 +505,12 @@ class QXmppSaslClientPlain : public QXmppSaslClient
Q_OBJECT
public:
QXmppSaslClientPlain(QObject *parent = nullptr);
- QString mechanism() const override;
+ void setCredentials(const QXmpp::Private::Credentials &) override;
+ QXmpp::Private::SaslMechanism mechanism() const override { return { QXmpp::Private::SaslPlainMechanism() }; }
std::optional respond(const QByteArray &challenge) override;
private:
+ QString m_password;
int m_step;
};
@@ -356,13 +518,15 @@ class QXmppSaslClientScram : public QXmppSaslClient
{
Q_OBJECT
public:
- QXmppSaslClientScram(QCryptographicHash::Algorithm algorithm, QObject *parent = nullptr);
- QString mechanism() const override;
+ QXmppSaslClientScram(QXmpp::Private::SaslScramMechanism mechanism, QObject *parent = nullptr);
+ void setCredentials(const QXmpp::Private::Credentials &) override;
+ QXmpp::Private::SaslMechanism mechanism() const override { return { m_mechanism }; }
std::optional respond(const QByteArray &challenge) override;
private:
- QCryptographicHash::Algorithm m_algorithm;
+ QXmpp::Private::SaslScramMechanism m_mechanism;
int m_step;
+ QString m_password;
uint32_t m_dklen;
QByteArray m_gs2Header;
QByteArray m_clientFirstMessageBare;
@@ -370,15 +534,38 @@ class QXmppSaslClientScram : public QXmppSaslClient
QByteArray m_nonce;
};
+class QXmppSaslClientHt : public QXmppSaslClient
+{
+ Q_OBJECT
+ using HtMechanism = QXmpp::Private::SaslHtMechanism;
+
+public:
+ QXmppSaslClientHt(HtMechanism mechanism, QObject *parent)
+ : QXmppSaslClient(parent), m_mechanism(mechanism)
+ {
+ }
+
+ void setCredentials(const QXmpp::Private::Credentials &credentials) override { m_token = credentials.htToken; }
+ QXmpp::Private::SaslMechanism mechanism() const override { return { m_mechanism }; }
+ std::optional respond(const QByteArray &challenge) override;
+
+private:
+ std::optional m_token;
+ HtMechanism m_mechanism;
+ bool m_done = false;
+};
+
class QXmppSaslClientWindowsLive : public QXmppSaslClient
{
Q_OBJECT
public:
QXmppSaslClientWindowsLive(QObject *parent = nullptr);
- QString mechanism() const override;
+ void setCredentials(const QXmpp::Private::Credentials &) override;
+ QXmpp::Private::SaslMechanism mechanism() const override { return { QXmpp::Private::SaslXWindowsLiveMechanism() }; }
std::optional respond(const QByteArray &challenge) override;
private:
+ QString m_accessToken;
int m_step;
};
diff --git a/src/base/QXmppUtils.cpp b/src/base/QXmppUtils.cpp
index a2a89f73d..980d33d90 100644
--- a/src/base/QXmppUtils.cpp
+++ b/src/base/QXmppUtils.cpp
@@ -8,6 +8,7 @@
#include "QXmppNonza.h"
#include "QXmppUtils_p.h"
+#include "Algorithms.h"
#include "StringLiterals.h"
#include
@@ -23,6 +24,8 @@
#include
#include
+using namespace QXmpp::Private;
+
// adapted from public domain source by Ross Williams and Eric Durbin
// FIXME : is this valid for big-endian machines?
static quint32 crctable[256] = {
@@ -93,14 +96,22 @@ static quint32 crctable[256] = {
};
///
-/// Parses a date-time from a string according to
-/// \xep{0082}: XMPP Date and Time Profiles.
+/// Parses a date-time from a string according to \xep{0082, XMPP Date and Time Profiles}.
///
-QDateTime QXmppUtils::datetimeFromString(const QString &str)
+/// Takes QStringView since QXmpp 1.8.
+///
+QDateTime QXmppUtils::datetimeFromString(QStringView str)
{
// Qt::ISODate parses milliseconds, but doesn't output them
+ return QDateTime::fromString(toString60(str), Qt::ISODate).toUTC();
+}
+
+/// \cond
+QDateTime QXmppUtils::datetimeFromString(const QString &str)
+{
return QDateTime::fromString(str, Qt::ISODate).toUTC();
}
+/// \endcond
///
/// Serializes a date-time to a string according to
@@ -409,6 +420,22 @@ template std::optional QXmpp::Private::parseInt(QStringView)
template std::optional QXmpp::Private::parseInt(QStringView);
template std::optional QXmpp::Private::parseInt(QStringView);
+std::optional QXmpp::Private::parseBoolean(const QString &str)
+{
+ if (str == u"1" || str == u"true") {
+ return true;
+ }
+ if (str == u"0" || str == u"false") {
+ return false;
+ }
+ return {};
+}
+
+QString QXmpp::Private::serializeBoolean(bool value)
+{
+ return value ? QStringLiteral("true") : QStringLiteral("false");
+}
+
bool QXmpp::Private::isIqType(const QDomElement &element, QStringView tagName, QStringView xmlns)
{
// IQs must have only one child element, so we do not need to iterate over the child elements.
@@ -442,6 +469,11 @@ QDomElement QXmpp::Private::nextSiblingElement(const QDomElement &el, QStringVie
return {};
}
+std::vector QXmpp::Private::parseTextElements(DomChildElements elements)
+{
+ return transform>(elements, &QDomElement::text);
+}
+
QByteArray QXmpp::Private::serializeXml(const void *packet, void (*toXml)(const void *, QXmlStreamWriter *))
{
QByteArray data;
diff --git a/src/base/QXmppUtils.h b/src/base/QXmppUtils.h
index 00b77a71e..5b14cd617 100644
--- a/src/base/QXmppUtils.h
+++ b/src/base/QXmppUtils.h
@@ -25,7 +25,10 @@ class QXMPP_EXPORT QXmppUtils
{
public:
// XEP-0082: XMPP Date and Time Profiles
+ static QDateTime datetimeFromString(QStringView str);
+ /// \cond
static QDateTime datetimeFromString(const QString &str);
+ /// \endcond
static QString datetimeToString(const QDateTime &dt);
static int timezoneOffsetFromString(const QString &str);
static QString timezoneOffsetToString(int secs);
diff --git a/src/base/QXmppUtils_p.h b/src/base/QXmppUtils_p.h
index 704248468..6b14d90e3 100644
--- a/src/base/QXmppUtils_p.h
+++ b/src/base/QXmppUtils_p.h
@@ -80,6 +80,13 @@ void writeXmlTextElement(QXmlStreamWriter *stream, QStringView name, QStringView
void writeXmlTextElement(QXmlStreamWriter *writer, QStringView name, QStringView xmlns, QStringView value);
void writeOptionalXmlTextElement(QXmlStreamWriter *writer, QStringView name, QStringView value);
void writeEmptyElement(QXmlStreamWriter *writer, QStringView name, QStringView xmlns);
+template
+inline void writeOptional(QXmlStreamWriter *writer, const std::optional &value)
+{
+ if (value) {
+ value->toXml(writer);
+ }
+}
// Base64
std::optional parseBase64(const QString &);
@@ -91,6 +98,10 @@ std::optional parseInt(QStringView str);
template
inline QString serializeInt(Int value) { return QString::number(value); }
+// Booleans
+std::optional parseBoolean(const QString &str);
+QString serializeBoolean(bool);
+
//
// DOM
//
@@ -125,6 +136,8 @@ struct DomChildElements {
inline DomChildElements iterChildElements(const QDomElement &el, QStringView tagName = {}, QStringView namespaceUri = {}) { return DomChildElements { el, tagName, namespaceUri }; }
+std::vector parseTextElements(DomChildElements elements);
+
QByteArray serializeXml(const void *packet, void (*toXml)(const void *, QXmlStreamWriter *));
template
inline QByteArray serializeXml(const T &packet)
diff --git a/src/base/StringLiterals.h b/src/base/StringLiterals.h
index 91b80ddde..de03e08fb 100644
--- a/src/base/StringLiterals.h
+++ b/src/base/StringLiterals.h
@@ -14,6 +14,11 @@ inline QString operator"" _s(const char16_t *str, size_t size) noexcept
{
return QString(QStringPrivate(nullptr, const_cast(str), qsizetype(size)));
}
+
+constexpr inline QLatin1String operator"" _L1(const char *str, size_t size) noexcept
+{
+ return QLatin1String { str, int(size) };
+}
#else
namespace QXmpp::Private {
@@ -52,6 +57,11 @@ QString operator""_s()
static const auto staticData = QXmpp::Private::StaticStringData(str.data);
return QString(QStringDataPtr { staticData.data_ptr() });
}
+
+constexpr inline QLatin1String operator"" _L1(const char *str, size_t size) noexcept
+{
+ return QLatin1String { str, int(size) };
+}
#endif
#endif // STRINGLITERALS_H
diff --git a/src/client/QXmppClient.cpp b/src/client/QXmppClient.cpp
index 308e6423a..6df1cc078 100644
--- a/src/client/QXmppClient.cpp
+++ b/src/client/QXmppClient.cpp
@@ -196,6 +196,56 @@ bool process(QXmppClient *client, const QList &extension
} // namespace QXmpp::Private::MessagePipeline
+///
+/// \class QXmppClient
+///
+/// \brief Main class for starting and managing connections to XMPP servers.
+///
+/// It provides the user all the required functionality to connect to the
+/// server and perform operations afterwards.
+///
+/// This class will provide the handle/reference to QXmppRosterManager
+/// (roster management), QXmppVCardManager (vCard manager), and
+/// QXmppVersionManager (software version information).
+///
+/// By default, the client will automatically try reconnecting to the server.
+/// You can change that behaviour using
+/// QXmppConfiguration::setAutoReconnectionEnabled().
+///
+/// Not all the managers or extensions have been enabled by default. One can
+/// enable/disable the managers using the functions \c addExtension() and
+/// \c removeExtension(). \c findExtension() can be used to find a
+/// reference/pointer to a particular instantiated and enabled manager.
+///
+/// List of managers enabled by default:
+/// - QXmppRosterManager
+/// - QXmppVCardManager
+/// - QXmppVersionManager
+/// - QXmppDiscoveryManager
+/// - QXmppEntityTimeManager
+///
+/// ## Usage of FAST token-based authentication
+///
+/// QXmpp uses \xep{0484, Fast Authentication Streamlining Tokens} if enabled and supported by the
+/// server. FAST tokens can be requested after a first time authentication using a password or
+/// another strong authentication mechanism. The tokens can then be used to log in, without a
+/// password. The tokens are linked to a specific device ID (set via the SASL 2 user agent) and
+/// only this device can use the token. Tokens also expire and are rotated by the server.
+///
+/// The advantage of this mechanism is that a client does not necessarily need to store the
+/// password of an account and in the future clients that are logged in could be listed and logged
+/// out manually. FAST also allows for performance improvements as it only requires one round trip
+/// for authentication (and may be included in TLS 0-RTT data although that is not implemented in
+/// QXmpp) while other mechanisms like SCRAM need multiple round trips.
+///
+/// FAST itself is enabled by default (see QXmppConfiguration::useFastTokenAuthentication()), but
+/// you also need to set a SASL user agent with a stable device ID, so FAST can be used.
+/// After that you can login and use QXmppCredentials to serialize the token data and store it
+/// permanently. Note that the token may change over time, though.
+///
+/// \ingroup Core
+///
+
///
/// \typedef QXmppClient::IqResult
///
@@ -895,12 +945,15 @@ void QXmppClient::_q_socketStateChanged(QAbstractSocket::SocketState socketState
}
/// At connection establishment, send initial presence.
-void QXmppClient::_q_streamConnected()
+void QXmppClient::_q_streamConnected(const QXmpp::Private::SessionBegin &session)
{
d->receivedConflict = false;
d->reconnectionTries = 0;
// notify managers
+ if (session.fastTokenChanged) {
+ Q_EMIT credentialsChanged();
+ }
Q_EMIT connected();
Q_EMIT stateChanged(QXmppClient::ConnectedState);
diff --git a/src/client/QXmppClient.h b/src/client/QXmppClient.h
index 5beb3a488..7c610d19c 100644
--- a/src/client/QXmppClient.h
+++ b/src/client/QXmppClient.h
@@ -37,6 +37,10 @@ class QXmppRosterManager;
class QXmppVCardManager;
class QXmppVersionManager;
+namespace QXmpp::Private {
+struct SessionBegin;
+}
+
///
/// \defgroup Core Core classes
///
@@ -53,34 +57,6 @@ class QXmppVersionManager;
/// QXmppClient::addExtension().
///
-///
-/// \brief The QXmppClient class is the main class for using QXmpp.
-///
-/// It provides the user all the required functionality to connect to the
-/// server and perform operations afterwards.
-///
-/// This class will provide the handle/reference to QXmppRosterManager
-/// (roster management), QXmppVCardManager (vCard manager), and
-/// QXmppVersionManager (software version information).
-///
-/// By default, the client will automatically try reconnecting to the server.
-/// You can change that behaviour using
-/// QXmppConfiguration::setAutoReconnectionEnabled().
-///
-/// Not all the managers or extensions have been enabled by default. One can
-/// enable/disable the managers using the functions \c addExtension() and
-/// \c removeExtension(). \c findExtension() can be used to find a
-/// reference/pointer to a particular instantiated and enabled manager.
-///
-/// List of managers enabled by default:
-/// - QXmppRosterManager
-/// - QXmppVCardManager
-/// - QXmppVersionManager
-/// - QXmppDiscoveryManager
-/// - QXmppEntityTimeManager
-///
-/// \ingroup Core
-///
class QXMPP_EXPORT QXmppClient : public QXmppLoggable
{
Q_OBJECT
@@ -324,6 +300,13 @@ class QXMPP_EXPORT QXmppClient : public QXmppLoggable
/// This signal is emitted when the client state changes.
void stateChanged(QXmppClient::State state);
+ /// Emitted when the credentials, e.g. tokens have changed.
+ ///
+ /// This means that the QXmppCredentials in the QXmppConfiguration of this client has changed.
+ ///
+ /// \since QXmpp 1.8
+ Q_SIGNAL void credentialsChanged();
+
public Q_SLOTS:
void connectToServer(const QXmppConfiguration &,
const QXmppPresence &initialPresence =
@@ -343,7 +326,7 @@ private Q_SLOTS:
void _q_elementReceived(const QDomElement &element, bool &handled);
void _q_reconnect();
void _q_socketStateChanged(QAbstractSocket::SocketState state);
- void _q_streamConnected();
+ void _q_streamConnected(const QXmpp::Private::SessionBegin &);
void _q_streamDisconnected();
private:
diff --git a/src/client/QXmppConfiguration.cpp b/src/client/QXmppConfiguration.cpp
index 79bc654a9..29b60de92 100644
--- a/src/client/QXmppConfiguration.cpp
+++ b/src/client/QXmppConfiguration.cpp
@@ -5,8 +5,11 @@
#include "QXmppConfiguration.h"
#include "QXmppConstants_p.h"
+#include "QXmppCredentials.h"
#include "QXmppSasl2UserAgent.h"
+#include "QXmppSasl_p.h"
#include "QXmppUtils.h"
+#include "QXmppUtils_p.h"
#include "StringLiterals.h"
@@ -16,26 +19,81 @@
using namespace QXmpp::Private;
+struct QXmppCredentialsPrivate : QSharedData, Credentials { };
+
+///
+/// \class QXmppCredentials
+///
+/// \brief Stores different kinds of credentials used for authentication.
+///
+/// QXmppCredentials can be serialized to XML and parsed from XML again. This can be useful to
+/// store credentials permanently without needing to handle all the details of the different
+/// authentication methods. QXmpp can for example request and use \xep{0484, Fast Authentication
+/// Streamlining Tokens} tokens and might support other mechanisms in the future.
+/// The XML format is QXmpp specific and is not specified.
+///
+/// The XML output currently may contain:
+/// * an HT token for \xep{0484, Fast Authentication Streamlining Tokens}
+///
+/// \since QXmpp 1.8
+///
+
+/// Default constructor.
+QXmppCredentials::QXmppCredentials()
+ : d(new QXmppCredentialsPrivate)
+{
+}
+
+QXMPP_PRIVATE_DEFINE_RULE_OF_SIX(QXmppCredentials)
+
+///
+/// Tries to parse XML-serialized credentials.
+///
+std::optional QXmppCredentials::fromXml(QXmlStreamReader &r)
+{
+ if (!r.isStartElement() || r.name() != u"credentials" || r.namespaceUri() != ns_qxmpp_credentials) {
+ return {};
+ }
+
+ QXmppCredentials credentials;
+ while (r.readNextStartElement()) {
+ if (r.name() == u"ht-token") {
+ if (auto htToken = HtToken::fromXml(r)) {
+ credentials.d->htToken = std::move(*htToken);
+ }
+ }
+ }
+ return credentials;
+}
+
+///
+/// Serializes the credentials to XML.
+///
+void QXmppCredentials::toXml(QXmlStreamWriter &writer) const
+{
+ writer.writeStartElement(QSL65("credentials"));
+ writer.writeDefaultNamespace(toString65(ns_qxmpp_credentials));
+ if (d->htToken) {
+ d->htToken->toXml(writer);
+ }
+ writer.writeEndElement();
+}
+
+bool QXmppCredentials::operator==(const QXmppCredentials &other) const
+{
+ return d->htToken == other.d->htToken;
+}
+
class QXmppConfigurationPrivate : public QSharedData
{
public:
QString host;
int port = XMPP_DEFAULT_PORT;
QString user;
- QString password;
QString domain;
QString resource = u"QXmpp"_s;
QString resourcePrefix;
-
- // Facebook
- QString facebookAccessToken;
- QString facebookAppId;
-
- // Google
- QString googleAccessToken;
-
- // Windows Live
- QString windowsLiveAccessToken;
+ QXmppCredentials credentials;
bool autoAcceptSubscriptions = false;
bool sendIntialPresence = true;
@@ -48,6 +106,7 @@ class QXmppConfigurationPrivate : public QSharedData
bool autoReconnectionEnabled = true;
// which authentication systems to use (if any)
bool useSasl2Authentication = true;
+ bool useFastTokenAuthentication = true;
bool useSASLAuthentication = true;
bool useNonSASLAuthentication = true;
bool ignoreSslErrors = false;
@@ -131,7 +190,7 @@ void QXmppConfiguration::setUser(const QString &user)
///
void QXmppConfiguration::setPassword(const QString &password)
{
- d->password = password;
+ credentialData().password = password;
}
///
@@ -213,7 +272,7 @@ QString QXmppConfiguration::user() const
/// Returns the password.
QString QXmppConfiguration::password() const
{
- return d->password;
+ return credentialData().password;
}
/// Returns the resource identifier.
@@ -252,10 +311,30 @@ QString QXmppConfiguration::jidBare() const
}
}
+///
+/// Returns the credentials of this configuration.
+///
+/// \since QXmpp 1.8
+///
+QXmppCredentials QXmppConfiguration::credentials() const
+{
+ return d->credentials;
+}
+
+///
+/// Sets the credentials for this configuration.
+///
+/// \since QXmpp 1.8
+///
+void QXmppConfiguration::setCredentials(const QXmppCredentials &credentials)
+{
+ d->credentials = credentials;
+}
+
/// Returns the access token used for X-FACEBOOK-PLATFORM authentication.
QString QXmppConfiguration::facebookAccessToken() const
{
- return d->facebookAccessToken;
+ return credentialData().facebookAccessToken;
}
///
@@ -266,25 +345,25 @@ QString QXmppConfiguration::facebookAccessToken() const
///
void QXmppConfiguration::setFacebookAccessToken(const QString &accessToken)
{
- d->facebookAccessToken = accessToken;
+ credentialData().facebookAccessToken = accessToken;
}
/// Returns the application ID used for X-FACEBOOK-PLATFORM authentication.
QString QXmppConfiguration::facebookAppId() const
{
- return d->facebookAppId;
+ return credentialData().facebookAppId;
}
/// Sets the application ID used for X-FACEBOOK-PLATFORM authentication.
void QXmppConfiguration::setFacebookAppId(const QString &appId)
{
- d->facebookAppId = appId;
+ credentialData().facebookAppId = appId;
}
/// Returns the access token used for X-OAUTH2 authentication.
QString QXmppConfiguration::googleAccessToken() const
{
- return d->googleAccessToken;
+ return credentialData().googleAccessToken;
}
///
@@ -295,13 +374,13 @@ QString QXmppConfiguration::googleAccessToken() const
///
void QXmppConfiguration::setGoogleAccessToken(const QString &accessToken)
{
- d->googleAccessToken = accessToken;
+ credentialData().googleAccessToken = accessToken;
}
/// Returns the access token used for X-MESSENGER-OAUTH2 authentication.
QString QXmppConfiguration::windowsLiveAccessToken() const
{
- return d->windowsLiveAccessToken;
+ return credentialData().windowsLiveAccessToken;
}
///
@@ -312,7 +391,7 @@ QString QXmppConfiguration::windowsLiveAccessToken() const
///
void QXmppConfiguration::setWindowsLiveAccessToken(const QString &accessToken)
{
- d->windowsLiveAccessToken = accessToken;
+ credentialData().windowsLiveAccessToken = accessToken;
}
///
@@ -378,6 +457,32 @@ void QXmppConfiguration::setUseSasl2Authentication(bool enabled)
d->useSasl2Authentication = enabled;
}
+///
+/// Returns whether to use FAST token-based authentication from \xep{0484, Fast Authentication
+/// Streamlining Tokens} if available.
+///
+/// Note that FAST requires a valid SASL 2 user-agent to be set.
+///
+/// \since QXmpp 1.8
+///
+bool QXmppConfiguration::useFastTokenAuthentication() const
+{
+ return d->useFastTokenAuthentication;
+}
+
+///
+/// Sets whether to use FAST token-based authentication from \xep{0484, Fast Authentication
+/// Streamlining Tokens} if available.
+///
+/// Note that FAST requires a valid SASL 2 user-agent to be set.
+///
+/// \since QXmpp 1.8
+///
+void QXmppConfiguration::setUseFastTokenAuthentication(bool useFast)
+{
+ d->useFastTokenAuthentication = useFast;
+}
+
/// Returns whether SSL errors (such as certificate validation errors)
/// are to be ignored when connecting to the XMPP server.
bool QXmppConfiguration::ignoreSslErrors() const
@@ -603,3 +708,15 @@ QList QXmppConfiguration::caCertificates() const
{
return d->caCertificates;
}
+
+/// \cond
+const Credentials &QXmppConfiguration::credentialData() const
+{
+ return *(d->credentials.d);
+}
+
+Credentials &QXmppConfiguration::credentialData()
+{
+ return *(d->credentials.d);
+}
+/// \endcond
diff --git a/src/client/QXmppConfiguration.h b/src/client/QXmppConfiguration.h
index e6a1ae4ab..37ac3bd34 100644
--- a/src/client/QXmppConfiguration.h
+++ b/src/client/QXmppConfiguration.h
@@ -15,8 +15,13 @@
class QNetworkProxy;
class QSslCertificate;
class QXmppConfigurationPrivate;
+class QXmppCredentials;
class QXmppSasl2UserAgent;
+namespace QXmpp::Private {
+struct Credentials;
+}
+
///
/// \brief The QXmppConfiguration class holds configuration options.
///
@@ -50,11 +55,6 @@ class QXMPP_EXPORT QXmppConfiguration
NonSASLDigest ///< Digest (default)
};
- /// An enumeration for various SASL authentication mechanisms available.
- /// The server may or may not allow any particular mechanism. So depending
- /// upon the availability of mechanisms on the server the library will choose
- /// a mechanism.
-
QXmppConfiguration();
QXmppConfiguration(const QXmppConfiguration &other);
~QXmppConfiguration();
@@ -86,6 +86,9 @@ class QXMPP_EXPORT QXmppConfiguration
QString jidBare() const;
+ QXmppCredentials credentials() const;
+ void setCredentials(const QXmppCredentials &);
+
QString facebookAccessToken() const;
void setFacebookAccessToken(const QString &);
@@ -107,6 +110,9 @@ class QXMPP_EXPORT QXmppConfiguration
bool useSasl2Authentication() const;
void setUseSasl2Authentication(bool);
+ bool useFastTokenAuthentication() const;
+ void setUseFastTokenAuthentication(bool);
+
bool useSASLAuthentication() const;
void setUseSASLAuthentication(bool);
@@ -144,6 +150,11 @@ class QXMPP_EXPORT QXmppConfiguration
QList caCertificates() const;
void setCaCertificates(const QList &);
+ /// \cond
+ const QXmpp::Private::Credentials &credentialData() const;
+ QXmpp::Private::Credentials &credentialData();
+ /// \endcond
+
private:
QSharedDataPointer d;
};
diff --git a/src/client/QXmppCredentials.h b/src/client/QXmppCredentials.h
new file mode 100644
index 000000000..3eed16c48
--- /dev/null
+++ b/src/client/QXmppCredentials.h
@@ -0,0 +1,38 @@
+// SPDX-FileCopyrightText: 2024 Linus Jahn
+//
+// SPDX-License-Identifier: LGPL-2.1-or-later
+
+#ifndef QXMPPCREDENTIALS_H
+#define QXMPPCREDENTIALS_H
+
+#include "QXmppGlobal.h"
+
+#include
+
+#include
+
+struct QXmppCredentialsPrivate;
+class QXmlStreamReader;
+class QXmlStreamWriter;
+
+class QXMPP_EXPORT QXmppCredentials
+{
+public:
+ QXmppCredentials();
+ QXMPP_PRIVATE_DECLARE_RULE_OF_SIX(QXmppCredentials)
+
+ static std::optional fromXml(QXmlStreamReader &);
+ void toXml(QXmlStreamWriter &) const;
+
+ /// Comparison operator
+ bool operator==(const QXmppCredentials &other) const;
+ /// Comparison operator
+ bool operator!=(const QXmppCredentials &other) const = default;
+
+private:
+ friend class QXmppConfiguration;
+
+ QSharedDataPointer d;
+};
+
+#endif // QXMPPCREDENTIALS_H
diff --git a/src/client/QXmppOutgoingClient.cpp b/src/client/QXmppOutgoingClient.cpp
index 6302898ef..b65feaef0 100644
--- a/src/client/QXmppOutgoingClient.cpp
+++ b/src/client/QXmppOutgoingClient.cpp
@@ -61,6 +61,7 @@ QXmppOutgoingClientPrivate::QXmppOutgoingClientPrivate(QXmppOutgoingClient *qq)
streamAckManager(socket),
iqManager(qq, streamAckManager),
listener(qq),
+ fastTokenManager(config),
c2sStreamManager(qq),
csiManager(qq),
pingManager(qq),
@@ -310,6 +311,7 @@ void QXmppOutgoingClient::startSasl2Auth(const Sasl2::StreamFeature &sasl2Featur
sasl2Request.bindRequest = createBind2Request(sasl2Feature.bind2Feature->features);
}
// other extensions
+ d->fastTokenManager.onSasl2Authenticate(sasl2Request, sasl2Feature);
d->c2sStreamManager.onSasl2Authenticate(sasl2Request, sasl2Feature);
// start authentication
@@ -317,10 +319,12 @@ void QXmppOutgoingClient::startSasl2Auth(const Sasl2::StreamFeature &sasl2Featur
if (auto success = std::get_if(&result)) {
debug(u"Authenticated"_s);
d->isAuthenticated = true;
+ d->authenticationMethod = AuthenticationMethod::Sasl2;
d->config.setJid(success->authorizationIdentifier);
d->bind2Bound = std::move(success->bound);
// extensions
+ d->fastTokenManager.onSasl2Success(*success);
d->c2sStreamManager.onSasl2Success(*success);
if (d->bind2Bound) {
d->c2sStreamManager.onBind2Bound(*d->bind2Bound);
@@ -366,6 +370,7 @@ void QXmppOutgoingClient::startNonSaslAuth()
// successful Non-SASL Authentication
debug(u"Authenticated (Non-SASL)"_s);
d->isAuthenticated = true;
+ d->authenticationMethod = AuthenticationMethod::NonSasl;
// xmpp connection made
openSession();
@@ -451,6 +456,8 @@ void QXmppOutgoingClient::openSession()
d->c2sStreamManager.enabled(),
d->c2sStreamManager.streamResumed(),
d->bind2Bound.has_value(),
+ d->authenticationMethod == AuthenticationMethod::Sasl2 && d->fastTokenManager.tokenChanged(),
+ d->authenticationMethod,
};
d->bind2Bound.reset();
@@ -642,6 +649,7 @@ void QXmppOutgoingClient::handleStreamFeatures(const QXmppStreamFeatures &featur
if (std::holds_alternative(result)) {
debug(u"Authenticated"_s);
d->isAuthenticated = true;
+ d->authenticationMethod = AuthenticationMethod::Sasl;
handleStart();
} else {
auto [text, err] = std::get(std::move(result));
diff --git a/src/client/QXmppOutgoingClient.h b/src/client/QXmppOutgoingClient.h
index 13a014b0e..145c7891e 100644
--- a/src/client/QXmppOutgoingClient.h
+++ b/src/client/QXmppOutgoingClient.h
@@ -50,10 +50,18 @@ enum HandleElementResult {
Finished,
};
+enum class AuthenticationMethod {
+ NonSasl,
+ Sasl,
+ Sasl2,
+};
+
struct SessionBegin {
bool smEnabled;
bool smResumed;
bool bind2Used;
+ bool fastTokenChanged;
+ AuthenticationMethod authenticationMethod;
};
struct SessionEnd {
diff --git a/src/client/QXmppOutgoingClient_p.h b/src/client/QXmppOutgoingClient_p.h
index 7fbd39882..7210d6d53 100644
--- a/src/client/QXmppOutgoingClient_p.h
+++ b/src/client/QXmppOutgoingClient_p.h
@@ -191,9 +191,11 @@ class QXmppOutgoingClientPrivate
bool isAuthenticated = false;
bool bindModeAvailable = false;
bool sessionStarted = false;
+ AuthenticationMethod authenticationMethod = AuthenticationMethod::Sasl;
std::optional bind2Bound;
std::variant listener;
+ FastTokenManager fastTokenManager;
C2sStreamManager c2sStreamManager;
CarbonManager carbonManager;
CsiManager csiManager;
diff --git a/src/client/QXmppSaslManager.cpp b/src/client/QXmppSaslManager.cpp
index c708b6f0d..00b3a94af 100644
--- a/src/client/QXmppSaslManager.cpp
+++ b/src/client/QXmppSaslManager.cpp
@@ -5,6 +5,7 @@
#include "QXmppConfiguration.h"
#include "QXmppConstants_p.h"
+#include "QXmppCredentials.h"
#include "QXmppFutureUtils_p.h"
#include "QXmppSasl2UserAgent.h"
#include "QXmppSaslManager_p.h"
@@ -13,69 +14,63 @@
#include "QXmppStreamFeatures.h"
#include "QXmppUtils_p.h"
+#include "Algorithms.h"
#include "StringLiterals.h"
#include "XmppSocket.h"
+#include
+
#include
-#ifndef QXMPP_DOC
+using namespace std::placeholders;
+namespace views = std::views;
+using std::ranges::copy;
+using std::ranges::empty;
+using std::ranges::max;
namespace QXmpp::Private {
-static std::tuple chooseMechanism(const QXmppConfiguration &config, const QList &availableMechanisms)
+static auto chooseMechanism(const QXmppConfiguration &config, const QList &availableMechanisms)
+ -> std::tuple, QStringList>
{
- // supported and preferred SASL auth mechanisms
- const QString preferredMechanism = config.saslAuthMechanism();
- QStringList supportedMechanisms = QXmppSaslClient::availableMechanisms();
- if (supportedMechanisms.contains(preferredMechanism)) {
- supportedMechanisms.removeAll(preferredMechanism);
- supportedMechanisms.prepend(preferredMechanism);
- }
- if (config.facebookAppId().isEmpty() || config.facebookAccessToken().isEmpty()) {
- supportedMechanisms.removeAll(u"X-FACEBOOK-PLATFORM"_s);
- }
- if (config.windowsLiveAccessToken().isEmpty()) {
- supportedMechanisms.removeAll(u"X-MESSENGER-OAUTH2"_s);
- }
- if (config.googleAccessToken().isEmpty()) {
- supportedMechanisms.removeAll(u"X-OAUTH2"_s);
- }
-
- // determine SASL Authentication mechanism to use
- QStringList commonMechanisms;
- for (const auto &mechanism : std::as_const(supportedMechanisms)) {
- if (availableMechanisms.contains(mechanism)) {
- commonMechanisms << mechanism;
- }
- }
-
- // Remove disabled mechanisms and add to disabledAvailable
const auto disabled = config.disabledSaslMechanisms();
QStringList disabledAvailable;
- for (const auto &m : disabled) {
- if (commonMechanisms.removeAll(m)) {
- disabledAvailable.push_back(m);
+ auto isEnabled = [&](const QString &mechanism) {
+ if (disabled.contains(mechanism)) {
+ disabledAvailable.push_back(mechanism);
+ return false;
}
- }
+ return true;
+ };
- return { commonMechanisms.empty() ? QString() : commonMechanisms.first(), disabledAvailable };
-}
+ // mechanisms that are available and supported by us
+ auto mechanismsView = availableMechanisms |
+ views::filter(isEnabled) |
+ views::transform(&SaslMechanism::fromString) |
+ views::filter(&std::optional::has_value) |
+ views::transform([](const auto &v) { return *v; }) |
+ views::filter(std::bind(&QXmppSaslClient::isMechanismAvailable, _1, config.credentialData()));
-static void setCredentials(QXmppSaslClient *saslClient, const QXmppConfiguration &config)
-{
- auto mechanism = saslClient->mechanism();
- if (mechanism == u"X-FACEBOOK-PLATFORM") {
- saslClient->setUsername(config.facebookAppId());
- saslClient->setPassword(config.facebookAccessToken());
- } else if (mechanism == u"X-MESSENGER-OAUTH2") {
- saslClient->setPassword(config.windowsLiveAccessToken());
- } else if (mechanism == u"X-OAUTH2") {
- saslClient->setUsername(config.user());
- saslClient->setPassword(config.googleAccessToken());
- } else {
- saslClient->setUsername(config.user());
- saslClient->setPassword(config.password());
+ std::vector mechanisms(mechanismsView.begin(), mechanismsView.end());
+
+ // no mechanisms supported
+ if (mechanisms.empty()) {
+ return { std::nullopt, disabledAvailable };
+ }
+
+ // try to use configured mechanism
+ if (auto preferredString = config.saslAuthMechanism();
+ !preferredString.isEmpty()) {
+ // parse
+ if (auto preferred = SaslMechanism::fromString(preferredString)) {
+ if (contains(mechanisms, *preferred)) {
+ return { *preferred, {} };
+ }
+ }
}
+
+ // max can be used: mechanisms is not empty (checked above)
+ return { max(mechanisms), disabledAvailable };
}
struct InitSaslAuthResult {
@@ -96,7 +91,7 @@ static InitSaslAuthResult initSaslAuthentication(const QXmppConfiguration &confi
};
auto [mechanism, disabled] = chooseMechanism(config, availableMechanisms);
- if (mechanism.isEmpty()) {
+ if (!mechanism) {
auto text = disabled.empty()
? u"No supported SASL mechanism available"_s
: u"No supported SASL mechanism available (%1 is disabled)"_s.arg(disabled.join(u", "));
@@ -104,15 +99,16 @@ static InitSaslAuthResult initSaslAuthentication(const QXmppConfiguration &confi
return error(std::move(text), { AuthenticationError::MechanismMismatch, {}, {} });
}
- auto saslClient = QXmppSaslClient::create(mechanism, parent);
+ auto saslClient = QXmppSaslClient::create(*mechanism, parent);
if (!saslClient) {
return error(u"SASL mechanism negotiation failed"_s,
AuthenticationError { AuthenticationError::ProcessingError, {}, {} });
}
- info(u"SASL mechanism '%1' selected"_s.arg(saslClient->mechanism()));
+ info(u"SASL mechanism '%1' selected"_s.arg(saslClient->mechanism().toString()));
saslClient->setHost(config.domain());
saslClient->setServiceType(u"xmpp"_s);
- setCredentials(saslClient.get(), config);
+ saslClient->setUsername(config.user());
+ saslClient->setCredentials(config.credentialData());
// send SASL auth request
if (auto response = saslClient->respond(QByteArray())) {
@@ -158,7 +154,7 @@ QXmppTask SaslManager::authenticate(const QXmppConfigur
return makeReadyTask(std::move(*result.error));
}
- m_socket->sendData(serializeXml(Sasl::Auth { result.saslClient->mechanism(), result.initialResponse }));
+ m_socket->sendData(serializeXml(Sasl::Auth { result.saslClient->mechanism().toString(), result.initialResponse }));
m_promise = QXmppPromise();
m_saslClient = std::move(result.saslClient);
@@ -167,22 +163,24 @@ QXmppTask SaslManager::authenticate(const QXmppConfigur
HandleElementResult SaslManager::handleElement(const QDomElement &el)
{
+ using namespace Sasl;
+
auto finish = [this](auto &&value) {
auto p = std::move(*m_promise);
m_promise.reset();
p.finish(value);
};
- if (!m_promise.has_value() || el.namespaceURI() != ns_sasl) {
+ if (!m_promise.has_value()) {
return Rejected;
}
- if (el.tagName() == u"success") {
- finish(Success());
+ if (Success::fromDom(el)) {
+ finish(QXmpp::Success());
return Finished;
- } else if (auto challenge = Sasl::Challenge::fromDom(el)) {
+ } else if (auto challenge = Challenge::fromDom(el)) {
if (auto response = m_saslClient->respond(challenge->value)) {
- m_socket->sendData(serializeXml(Sasl::Response { *response }));
+ m_socket->sendData(serializeXml(Response { *response }));
return Accepted;
} else {
finish(AuthError {
@@ -191,9 +189,9 @@ HandleElementResult SaslManager::handleElement(const QDomElement &el)
});
return Finished;
}
- } else if (auto failure = Sasl::Failure::fromDom(el)) {
+ } else if (auto failure = Failure::fromDom(el)) {
auto text = failure->text.isEmpty()
- ? Sasl::errorConditionToString(failure->condition.value_or(Sasl::ErrorCondition::NotAuthorized))
+ ? errorConditionToString(failure->condition.value_or(ErrorCondition::NotAuthorized))
: failure->text;
finish(AuthError {
@@ -209,14 +207,27 @@ QXmppTask Sasl2Manager::authenticate(Sasl2::Authentica
{
Q_ASSERT(!m_state.has_value());
- auto result = initSaslAuthentication(config, feature.mechanisms, loggable);
+ // collect mechanisms
+ auto mechanisms = feature.mechanisms;
+
+ // additional mechanisms from extensions
+ bool fastAvailable = feature.fast && FastTokenManager::isFastEnabled(config);
+ if (fastAvailable) {
+ copy(feature.fast->mechanisms, std::back_inserter(mechanisms));
+ }
+
+ auto result = initSaslAuthentication(config, mechanisms, loggable);
if (result.error) {
return makeReadyTask(std::move(*result.error));
}
// create request
- auth.mechanism = result.saslClient->mechanism();
+ auth.mechanism = result.saslClient->mechanism().toString();
auth.initialResponse = result.initialResponse;
+ // indicate usage of FAST
+ if (fastAvailable && contains(feature.fast->mechanisms, auth.mechanism)) {
+ auth.fast = FastRequest {};
+ }
// set user-agent if enabled
if (auto userAgent = config.sasl2UserAgent()) {
@@ -249,13 +260,13 @@ HandleElementResult Sasl2Manager::handleElement(const QDomElement &el)
state.p.finish(value);
};
- if (!m_state || el.namespaceURI() != ns_sasl_2) {
+ if (!m_state) {
return Rejected;
}
- if (auto challenge = Sasl2::Challenge::fromDom(el)) {
+ if (auto challenge = Challenge::fromDom(el)) {
if (auto response = m_state->sasl->respond(challenge->data)) {
- m_socket->sendData(serializeXml(Sasl2::Response { *response }));
+ m_socket->sendData(serializeXml(Response { *response }));
return Accepted;
} else {
finish(AuthError {
@@ -291,12 +302,63 @@ HandleElementResult Sasl2Manager::handleElement(const QDomElement &el)
} else if (auto continueElement = Continue::fromDom(el)) {
// no SASL 2 tasks are currently implemented
m_state->unsupportedContinue = continueElement;
- m_socket->sendData(serializeXml(Sasl2::Abort { u"SASL 2 tasks are not supported."_s }));
+ m_socket->sendData(serializeXml(Abort { u"SASL 2 tasks are not supported."_s }));
return Accepted;
}
return Rejected;
}
-} // namespace QXmpp::Private
+FastTokenManager::FastTokenManager(QXmppConfiguration &config)
+ : config(config)
+{
+}
+
+bool FastTokenManager::isFastEnabled(const QXmppConfiguration &config)
+{
+ return config.useFastTokenAuthentication() && config.sasl2UserAgent().has_value();
+}
+
+bool FastTokenManager::hasToken() const
+{
+ return config.credentialData().htToken.has_value();
+}
-#endif // QXMPP_DOC
+void FastTokenManager::onSasl2Authenticate(Sasl2::Authenticate &auth, const Sasl2::StreamFeature &feature)
+{
+ auto selectMechanism = [](const auto &availableMechanisms) {
+ // find mechanisms supported by us
+ auto mechanisms = availableMechanisms |
+ views::transform(&SaslHtMechanism::fromString) |
+ views::filter([](const auto &v) { return v.has_value(); }) |
+ views::transform([](const auto &v) { return *v; }) |
+ views::filter([](const auto &m) { return m.channelBindingType == SaslHtMechanism::None; });
+
+ return empty(mechanisms) ? std::optional() : max(mechanisms);
+ };
+
+ requestedMechanism.reset();
+ m_tokenChanged = false;
+
+ if (feature.fast && isFastEnabled(config) && !hasToken()) {
+ // request token
+ if (auto mechanism = selectMechanism(feature.fast->mechanisms)) {
+ requestedMechanism = mechanism;
+ auth.tokenRequest = FastTokenRequest { mechanism->toString() };
+ }
+ }
+}
+
+void FastTokenManager::onSasl2Success(const Sasl2::Success &success)
+{
+ if (success.token && (requestedMechanism || config.credentialData().htToken)) {
+ // use requested mechanism (new) or the one from the old token (token rotation)
+ config.credentialData().htToken = HtToken {
+ requestedMechanism ? *requestedMechanism : config.credentialData().htToken->mechanism,
+ success.token->token,
+ success.token->expiry,
+ };
+ m_tokenChanged = true;
+ }
+}
+
+} // namespace QXmpp::Private
diff --git a/src/client/QXmppSaslManager_p.h b/src/client/QXmppSaslManager_p.h
index e72b750a6..283bdb265 100644
--- a/src/client/QXmppSaslManager_p.h
+++ b/src/client/QXmppSaslManager_p.h
@@ -62,6 +62,24 @@ class Sasl2Manager
std::optional m_state;
};
+// Authentication token management
+class FastTokenManager
+{
+public:
+ explicit FastTokenManager(QXmppConfiguration &config);
+
+ static bool isFastEnabled(const QXmppConfiguration &);
+ bool hasToken() const;
+ void onSasl2Authenticate(Sasl2::Authenticate &auth, const Sasl2::StreamFeature &feature);
+ void onSasl2Success(const Sasl2::Success &success);
+ bool tokenChanged() const { return m_tokenChanged; }
+
+private:
+ QXmppConfiguration &config;
+ std::optional requestedMechanism;
+ bool m_tokenChanged = false;
+};
+
} // namespace QXmpp::Private
#endif // QXMPPSASLMANAGER_P_H
diff --git a/src/server/QXmppIncomingClient.cpp b/src/server/QXmppIncomingClient.cpp
index cf1ed14b8..22dfebb3e 100644
--- a/src/server/QXmppIncomingClient.cpp
+++ b/src/server/QXmppIncomingClient.cpp
@@ -217,6 +217,7 @@ void QXmppIncomingClient::sendStreamFeatures()
features.setSasl2Feature(Sasl2::StreamFeature {
mechanisms,
d->resource.isEmpty() ? Bind2Feature {} : std::optional(),
+ {},
false,
});
}
diff --git a/tests/qxmppclient/tst_qxmppclient.cpp b/tests/qxmppclient/tst_qxmppclient.cpp
index 5a061fead..94ba60231 100644
--- a/tests/qxmppclient/tst_qxmppclient.cpp
+++ b/tests/qxmppclient/tst_qxmppclient.cpp
@@ -4,6 +4,7 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "QXmppClient.h"
+#include "QXmppCredentials.h"
#include "QXmppE2eeExtension.h"
#include "QXmppFutureUtils_p.h"
#include "QXmppLogger.h"
@@ -39,6 +40,8 @@ class tst_QXmppClient : public QObject
#if BUILD_INTERNAL_TESTS
Q_SLOT void csiManager();
#endif
+
+ Q_SLOT void credentialsSerialization();
};
void tst_QXmppClient::testSendMessage()
@@ -266,5 +269,21 @@ void tst_QXmppClient::csiManager()
}
#endif
+void tst_QXmppClient::credentialsSerialization()
+{
+ QByteArray xml =
+ ""
+ ""
+ "";
+ QXmlStreamReader r(xml);
+ r.readNextStartElement();
+ auto credentials = unwrap(QXmppCredentials::fromXml(r));
+
+ QString output;
+ QXmlStreamWriter w(&output);
+ credentials.toXml(w);
+ QCOMPARE(output, xml);
+}
+
QTEST_MAIN(tst_QXmppClient)
#include "tst_qxmppclient.moc"
diff --git a/tests/qxmppsasl/tst_qxmppsasl.cpp b/tests/qxmppsasl/tst_qxmppsasl.cpp
index 98a48deaa..1028d8dce 100644
--- a/tests/qxmppsasl/tst_qxmppsasl.cpp
+++ b/tests/qxmppsasl/tst_qxmppsasl.cpp
@@ -8,6 +8,7 @@
#include "QXmppSasl2UserAgent.h"
#include "QXmppSaslManager_p.h"
#include "QXmppSasl_p.h"
+#include "QXmppUtils_p.h"
#include "XmppSocket.h"
#include "util.h"
@@ -64,6 +65,8 @@ class tst_QXmppSasl : public QObject
Q_SLOT void sasl2ContinueElement();
Q_SLOT void sasl2Abort();
+ Q_SLOT void htAlgorithmParsing();
+
// client
Q_SLOT void testClientAvailableMechanisms();
Q_SLOT void testClientBadMechanism();
@@ -78,6 +81,7 @@ class tst_QXmppSasl : public QObject
Q_SLOT void testClientScramSha1_bad();
Q_SLOT void testClientScramSha256();
Q_SLOT void testClientWindowsLive();
+ Q_SLOT void clientHtSha256();
// server
Q_SLOT void testServerBadMechanism();
@@ -93,6 +97,9 @@ class tst_QXmppSasl : public QObject
Q_SLOT void sasl2ManagerPlain();
Q_SLOT void sasl2ManagerFailure();
Q_SLOT void sasl2ManagerUnsupportedTasks();
+
+ // SASL 2 + FAST
+ Q_SLOT void sasl2Fast();
};
void tst_QXmppSasl::testParsing()
@@ -399,8 +406,30 @@ void tst_QXmppSasl::sasl2Abort()
serializePacket(*abort, xml);
}
+void tst_QXmppSasl::htAlgorithmParsing()
+{
+ constexpr auto testValues = to_array>({
+ { u"HT-SHA-256-ENDP", { IanaHashAlgorithm::Sha256, SaslHtMechanism::TlsServerEndpoint } },
+ { u"HT-SHA-256-EXPR", { IanaHashAlgorithm::Sha256, SaslHtMechanism::TlsExporter } },
+ { u"HT-SHA-256-UNIQ", { IanaHashAlgorithm::Sha256, SaslHtMechanism::TlsUnique } },
+ { u"HT-SHA-256-NONE", { IanaHashAlgorithm::Sha256, SaslHtMechanism::None } },
+ { u"HT-SHA3-256-ENDP", { IanaHashAlgorithm::Sha3_256, SaslHtMechanism::TlsServerEndpoint } },
+ { u"HT-SHA3-512-EXPR", { IanaHashAlgorithm::Sha3_512, SaslHtMechanism::TlsExporter } },
+ { u"HT-SHA-512-UNIQ", { IanaHashAlgorithm::Sha512, SaslHtMechanism::TlsUnique } },
+#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+ { u"HT-BLAKE2B-256-NONE", { IanaHashAlgorithm::Blake2b_256, SaslHtMechanism::None } },
+#endif
+ });
+
+ for (const auto &[string, htAlg] : testValues) {
+ QCOMPARE(htAlg.toString(), string);
+ QCOMPARE(unwrap(SaslHtMechanism::fromString(string)), htAlg);
+ }
+}
+
void tst_QXmppSasl::testClientAvailableMechanisms()
{
+ QObject context;
const QStringList expectedMechanisms = {
"SCRAM-SHA3-512",
"SCRAM-SHA-512",
@@ -414,7 +443,11 @@ void tst_QXmppSasl::testClientAvailableMechanisms()
"X-OAUTH2"
};
- QCOMPARE(QXmppSaslClient::availableMechanisms(), expectedMechanisms);
+ for (const auto &mechanism : expectedMechanisms) {
+ auto parsed = SaslMechanism::fromString(mechanism);
+ QVERIFY(parsed);
+ QVERIFY(QXmppSaslClient::create(*parsed, &context) != nullptr);
+ }
}
void tst_QXmppSasl::testClientBadMechanism()
@@ -426,7 +459,7 @@ void tst_QXmppSasl::testClientAnonymous()
{
auto client = QXmppSaslClient::create("ANONYMOUS");
QVERIFY(client);
- QCOMPARE(client->mechanism(), "ANONYMOUS");
+ QCOMPARE(client->mechanism().toString(), u"ANONYMOUS");
// initial step returns nothing
QCOMPARE(client->respond(QByteArray()), QByteArray());
@@ -460,10 +493,10 @@ void tst_QXmppSasl::testClientDigestMd5()
auto client = QXmppSaslClient::create("DIGEST-MD5");
QVERIFY(client);
- QCOMPARE(client->mechanism(), "DIGEST-MD5");
+ QCOMPARE(client->mechanism().toString(), "DIGEST-MD5");
client->setUsername("qxmpp1");
- client->setPassword("qxmpp123");
+ client->setCredentials(Credentials { .password = "qxmpp123" });
client->setHost("jabber.ru");
client->setServiceType("xmpp");
@@ -483,10 +516,12 @@ void tst_QXmppSasl::testClientFacebook()
{
auto client = QXmppSaslClient::create("X-FACEBOOK-PLATFORM");
QVERIFY(client);
- QCOMPARE(client->mechanism(), QLatin1String("X-FACEBOOK-PLATFORM"));
+ QCOMPARE(client->mechanism().toString(), u"X-FACEBOOK-PLATFORM");
- client->setUsername("123456789012345");
- client->setPassword("abcdefghijlkmno");
+ client->setCredentials(Credentials {
+ .facebookAccessToken = "abcdefghijlkmno",
+ .facebookAppId = "123456789012345",
+ });
// initial step returns nothing
QCOMPARE(client->respond(QByteArray()), QByteArray());
@@ -503,10 +538,10 @@ void tst_QXmppSasl::testClientGoogle()
{
auto client = QXmppSaslClient::create("X-OAUTH2");
QVERIFY(client);
- QCOMPARE(client->mechanism(), QLatin1String("X-OAUTH2"));
+ QCOMPARE(client->mechanism().toString(), u"X-OAUTH2");
client->setUsername("foo");
- client->setPassword("bar");
+ client->setCredentials(Credentials { .googleAccessToken = "bar" });
// initial step returns data
QCOMPARE(client->respond(QByteArray()), QByteArray("\0foo\0bar", 8));
@@ -519,10 +554,10 @@ void tst_QXmppSasl::testClientPlain()
{
auto client = QXmppSaslClient::create("PLAIN");
QVERIFY(client);
- QCOMPARE(client->mechanism(), QLatin1String("PLAIN"));
+ QCOMPARE(client->mechanism().toString(), u"PLAIN");
client->setUsername("foo");
- client->setPassword("bar");
+ client->setCredentials(Credentials { .password = "bar" });
// initial step returns data
QCOMPARE(client->respond(QByteArray()), QByteArray("\0foo\0bar", 8));
@@ -536,10 +571,10 @@ void tst_QXmppSasl::testClientScramSha1()
QXmppSaslDigestMd5::setNonce("fyko+d2lbbFgONRv9qkxdawL");
auto client = QXmppSaslClient::create("SCRAM-SHA-1");
- QCOMPARE(client->mechanism(), QLatin1String("SCRAM-SHA-1"));
+ QCOMPARE(client->mechanism().toString(), u"SCRAM-SHA-1");
client->setUsername("user");
- client->setPassword("pencil");
+ client->setCredentials(Credentials { .password = "pencil" });
// first step
QCOMPARE(client->respond(QByteArray()), QByteArray("n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL"));
@@ -560,10 +595,10 @@ void tst_QXmppSasl::testClientScramSha1_bad()
QXmppSaslDigestMd5::setNonce("fyko+d2lbbFgONRv9qkxdawL");
auto client = QXmppSaslClient::create("SCRAM-SHA-1");
- QCOMPARE(client->mechanism(), QLatin1String("SCRAM-SHA-1"));
+ QCOMPARE(client->mechanism().toString(), u"SCRAM-SHA-1");
client->setUsername("user");
- client->setPassword("pencil");
+ client->setCredentials(Credentials { .password = "pencil" });
// first step
QCOMPARE(client->respond(QByteArray()), QByteArray("n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL"));
@@ -584,10 +619,10 @@ void tst_QXmppSasl::testClientScramSha256()
auto client = QXmppSaslClient::create("SCRAM-SHA-256");
QVERIFY(client != 0);
- QCOMPARE(client->mechanism(), QLatin1String("SCRAM-SHA-256"));
+ QCOMPARE(client->mechanism().toString(), u"SCRAM-SHA-256");
client->setUsername("user");
- client->setPassword("pencil");
+ client->setCredentials(Credentials { .password = "pencil" });
// first step
QCOMPARE(client->respond(QByteArray()), QByteArray("n,,n=user,r=rOprNGfwEbeRWgbNEkqO"));
@@ -607,9 +642,11 @@ void tst_QXmppSasl::testClientWindowsLive()
{
auto client = QXmppSaslClient::create("X-MESSENGER-OAUTH2");
QVERIFY(client != 0);
- QCOMPARE(client->mechanism(), QLatin1String("X-MESSENGER-OAUTH2"));
+ QCOMPARE(client->mechanism().toString(), u"X-MESSENGER-OAUTH2");
- client->setPassword(QByteArray("footoken").toBase64());
+ client->setCredentials(Credentials {
+ .windowsLiveAccessToken = QByteArray("footoken").toBase64(),
+ });
// initial step returns data
QCOMPARE(client->respond(QByteArray()), QByteArray("footoken", 8));
@@ -618,6 +655,29 @@ void tst_QXmppSasl::testClientWindowsLive()
QVERIFY(!client->respond(QByteArray()));
}
+void tst_QXmppSasl::clientHtSha256()
+{
+ auto client = QXmppSaslClient::create({ SaslHtMechanism(IanaHashAlgorithm::Sha256, SaslHtMechanism::None) });
+ QVERIFY(client != nullptr);
+ QCOMPARE(client->mechanism().toString(), u"HT-SHA-256-NONE"_s);
+
+ client->setUsername(u"lnj"_s);
+ client->setCredentials(Credentials {
+ .htToken = HtToken {
+ SaslHtMechanism(IanaHashAlgorithm::Sha256, SaslHtMechanism::None),
+ u"secret-token:fast-Oeie4nmlUoLHXca_YhkjwkEBgCEKKHKCArT8"_s,
+ QDateTime(),
+ },
+ });
+
+ auto response = client->respond({});
+ QVERIFY(response.has_value());
+ QCOMPARE(response->toBase64(), "bG5qAKq/BuI7mZiZ6fByiqP1ARkYUI/WyFSh7tsYik1uUiB5");
+
+ // any further step is an error
+ QVERIFY(!client->respond({}));
+}
+
void tst_QXmppSasl::testServerBadMechanism()
{
QVERIFY(!QXmppSaslServer::create("BAD-MECH"));
@@ -717,7 +777,7 @@ void tst_QXmppSasl::saslManagerNoMechanisms()
config.setPassword("1234");
config.setDisabledSaslMechanisms({ "SCRAM-SHA-1" });
- QVERIFY(QXmppSaslClient::availableMechanisms().contains("SCRAM-SHA-1"));
+ QVERIFY(QXmppSaslClient::isMechanismAvailable({ SaslScramMechanism(SaslScramMechanism::Sha1) }, config.credentialData()));
auto task = test.manager.authenticate(config, { "SCRAM-SHA-1" }, test.loggable.get());
@@ -745,7 +805,7 @@ void tst_QXmppSasl::sasl2ManagerPlain()
auto task = test.manager.authenticate(
Sasl2::Authenticate(),
config,
- Sasl2::StreamFeature { { "PLAIN", "SCRAM-SHA-1" }, {}, false },
+ Sasl2::StreamFeature { { "PLAIN", "SCRAM-SHA-1" }, {}, {}, false },
test.loggable.get());
QVERIFY(!task.isFinished());
@@ -773,7 +833,7 @@ void tst_QXmppSasl::sasl2ManagerFailure()
auto task = test.manager.authenticate(
Sasl2::Authenticate(),
config,
- Sasl2::StreamFeature { { "SCRAM-SHA-1" }, {}, false },
+ Sasl2::StreamFeature { { "SCRAM-SHA-1" }, {}, {}, false },
test.loggable.get());
QVERIFY(!task.isFinished());
@@ -805,7 +865,7 @@ void tst_QXmppSasl::sasl2ManagerUnsupportedTasks()
auto task = test.manager.authenticate(
Sasl2::Authenticate(),
config,
- Sasl2::StreamFeature { { "SCRAM-SHA-1" }, {}, false },
+ Sasl2::StreamFeature { { "SCRAM-SHA-1" }, {}, {}, false },
test.loggable.get());
auto handled = test.manager.handleElement(xmlToDom(
@@ -834,5 +894,81 @@ void tst_QXmppSasl::sasl2ManagerUnsupportedTasks()
QCOMPARE(err.text, "This account requires 2FA");
}
+void tst_QXmppSasl::sasl2Fast()
+{
+ Sasl2ManagerTest test;
+ auto &sent = test.socket.sent;
+
+ QXmppConfiguration config;
+ config.setUser("bowman");
+ config.setPassword("1234");
+ config.setDisabledSaslMechanisms({});
+ config.setSasl2UserAgent(QXmppSasl2UserAgent {
+ QUuid::fromString(u"d4565fa7-4d72-4749-b3d3-740edbf87770"_s),
+ "QXmpp",
+ "HAL 9000",
+ });
+
+ Sasl2::StreamFeature sasl2Feature {
+ { "PLAIN" },
+ {},
+ FastFeature { { "HT-SHA-256-NONE", "HT-SHA3-512-NONE" }, false },
+ false
+ };
+
+ Sasl2::Authenticate auth;
+
+ FastTokenManager fast(config);
+ fast.onSasl2Authenticate(auth, sasl2Feature);
+
+ // first: authenticate without fast, but request token
+ auto task = test.manager.authenticate(std::move(auth), config, sasl2Feature, test.loggable.get());
+
+ QVERIFY(!task.isFinished());
+ QCOMPARE(sent.size(), 1);
+ QByteArray authenticateXml =
+ ""
+ "AGJvd21hbgAxMjM0"
+ "QXmppHAL 9000"
+ ""
+ "";
+ QCOMPARE(sent.at(0), authenticateXml);
+
+ test.manager.handleElement(xmlToDom("bowman@example.org"));
+
+ QVERIFY(task.isFinished());
+ auto success = expectFutureVariant(task);
+ fast.onSasl2Success(success);
+ QVERIFY(fast.tokenChanged());
+
+ QVERIFY(config.credentialData().htToken.has_value());
+ auto token = unwrap(config.credentialData().htToken);
+ QCOMPARE(token.secret, u"s3cr3tt0k3n");
+ QCOMPARE(token.mechanism, SaslHtMechanism(IanaHashAlgorithm::Sha3_512, SaslHtMechanism::None));
+
+ // Now authenticate with FAST token
+ auth = Sasl2::Authenticate();
+ fast.onSasl2Authenticate(auth, sasl2Feature);
+ task = test.manager.authenticate(std::move(auth), config, sasl2Feature, test.loggable.get());
+ QVERIFY(!task.isFinished());
+ QCOMPARE(sent.size(), 2);
+ authenticateXml =
+ ""
+ "Ym93bWFuAJvHQZJynTMTHwKpXP0AYsGYWSIJMiQn/esiN1G6daGDry+2Fruyr11JLvyWPEmP1VxEZ6qBdNd/es7G1pRpmDg="
+ "QXmppHAL 9000"
+ ""
+ "";
+ QCOMPARE(sent.at(1), authenticateXml);
+ test.manager.handleElement(xmlToDom("bowman@example.org"));
+
+ QVERIFY(task.isFinished());
+ success = expectFutureVariant(task);
+ fast.onSasl2Success(success);
+ QVERIFY(fast.tokenChanged());
+ token = unwrap(config.credentialData().htToken);
+ QCOMPARE(token.secret, u"t0k3n-rotation-token");
+ QCOMPARE(token.mechanism, SaslHtMechanism(IanaHashAlgorithm::Sha3_512, SaslHtMechanism::None));
+}
+
QTEST_MAIN(tst_QXmppSasl)
#include "tst_qxmppsasl.moc"
diff --git a/tests/util.h b/tests/util.h
index 75fd273a9..d9a007027 100644
--- a/tests/util.h
+++ b/tests/util.h
@@ -124,6 +124,13 @@ T expectFutureVariant(QXmppTask &task)
return expectVariant(task.result());
}
+template
+const T &unwrap(const std::optional &v)
+{
+ VERIFY2(v.has_value(), "Expected value, got empty optional");
+ return *v;
+}
+
template
T unwrap(std::optional &&v)
{