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) {