aboutsummaryrefslogtreecommitdiff
path: root/src/base/QXmppSasl.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/base/QXmppSasl.cpp')
-rw-r--r--src/base/QXmppSasl.cpp742
1 files changed, 742 insertions, 0 deletions
diff --git a/src/base/QXmppSasl.cpp b/src/base/QXmppSasl.cpp
new file mode 100644
index 00000000..d37644ca
--- /dev/null
+++ b/src/base/QXmppSasl.cpp
@@ -0,0 +1,742 @@
+/*
+ * Copyright (C) 2008-2012 The QXmpp developers
+ *
+ * Authors:
+ * Manjeet Dahiya
+ * Jeremy Lainé
+ *
+ * Source:
+ * http://code.google.com/p/qxmpp
+ *
+ * This file is a part of QXmpp library.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ */
+
+#include <cstdlib>
+
+#include <QCryptographicHash>
+#include <QDomElement>
+#include <QStringList>
+#include <QUrl>
+
+#include "QXmppSasl_p.h"
+#include "QXmppUtils.h"
+
+const char *ns_xmpp_sasl = "urn:ietf:params:xml:ns:xmpp-sasl";
+
+static QByteArray forcedNonce;
+
+// 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)
+{
+ const QByteArray A1 = secret + ':' + nonce + ':' + cnonce;
+ const QByteArray A2 = method + ':' + digestUri;
+
+ QByteArray HA1 = QCryptographicHash::hash(A1, QCryptographicHash::Md5).toHex();
+ QByteArray HA2 = QCryptographicHash::hash(A2, QCryptographicHash::Md5).toHex();
+ const QByteArray KD = HA1 + ':' + nonce + ':' + nc + ':' + cnonce + ":auth:" + HA2;
+ return QCryptographicHash::hash(KD, QCryptographicHash::Md5).toHex();
+}
+
+static QByteArray generateNonce()
+{
+ if (!forcedNonce.isEmpty())
+ return forcedNonce;
+
+ QByteArray nonce = QXmppUtils::generateRandomBytes(32);
+
+ // The random data can the '=' char is not valid as it is a delimiter,
+ // so to be safe, base64 the nonce
+ return nonce.toBase64();
+}
+
+QXmppSaslAuth::QXmppSaslAuth(const QString &mechanism, const QByteArray &value)
+ : QXmppSaslStanza("auth", value)
+ , m_mechanism(mechanism)
+{
+}
+
+QString QXmppSaslAuth::mechanism() const
+{
+ return m_mechanism;
+}
+
+void QXmppSaslAuth::setMechanism(const QString &mechanism)
+{
+ m_mechanism = mechanism;
+}
+
+void QXmppSaslAuth::parse(const QDomElement &element)
+{
+ m_mechanism = element.attribute("mechanism");
+ setValue(QByteArray::fromBase64(element.text().toAscii()));
+}
+
+void QXmppSaslAuth::toXml(QXmlStreamWriter *writer) const
+{
+ writer->writeStartElement("auth");
+ writer->writeAttribute("xmlns", ns_xmpp_sasl);
+ writer->writeAttribute("mechanism", m_mechanism);
+ if (!value().isEmpty())
+ writer->writeCharacters(value().toBase64());
+ writer->writeEndElement();
+}
+
+QXmppSaslChallenge::QXmppSaslChallenge(const QByteArray &value)
+ : QXmppSaslStanza("challenge", value)
+{
+}
+
+QXmppSaslFailure::QXmppSaslFailure(const QString &condition)
+ : QXmppSaslStanza("failure")
+ , m_condition(condition)
+{
+}
+
+QString QXmppSaslFailure::condition() const
+{
+ return m_condition;
+}
+
+void QXmppSaslFailure::setCondition(const QString &condition)
+{
+ m_condition = condition;
+}
+
+void QXmppSaslFailure::parse(const QDomElement &element)
+{
+ m_condition = element.firstChildElement().tagName();
+}
+
+void QXmppSaslFailure::toXml(QXmlStreamWriter *writer) const
+{
+ writer->writeStartElement("failure");
+ writer->writeAttribute("xmlns", ns_xmpp_sasl);
+ if (!m_condition.isEmpty())
+ writer->writeEmptyElement(m_condition);
+ writer->writeEndElement();
+}
+
+QXmppSaslResponse::QXmppSaslResponse(const QByteArray &value)
+ : QXmppSaslStanza("response", value)
+{
+}
+
+QXmppSaslSuccess::QXmppSaslSuccess()
+ : QXmppSaslStanza("success")
+{
+}
+
+QXmppSaslStanza::QXmppSaslStanza(const QString &type, const QByteArray &value)
+ : m_type(type)
+ , m_value(value)
+{
+}
+
+QByteArray QXmppSaslStanza::value() const
+{
+ return m_value;
+}
+
+void QXmppSaslStanza::setValue(const QByteArray &value)
+{
+ m_value = value;
+}
+
+
+void QXmppSaslStanza::parse(const QDomElement &element)
+{
+ m_type = element.nodeName();
+ m_value = QByteArray::fromBase64(element.text().toAscii());
+}
+
+void QXmppSaslStanza::toXml(QXmlStreamWriter *writer) const
+{
+ if (!m_type.isEmpty()) {
+ writer->writeStartElement(m_type);
+ writer->writeAttribute("xmlns", ns_xmpp_sasl);
+ if (!m_value.isEmpty())
+ writer->writeCharacters(m_value.toBase64());
+ writer->writeEndElement();
+ }
+}
+
+class QXmppSaslClientPrivate
+{
+public:
+ QString host;
+ QString serviceType;
+ QString username;
+ QString password;
+};
+
+QXmppSaslClient::QXmppSaslClient(QObject *parent)
+ : QXmppLoggable(parent)
+ , d(new QXmppSaslClientPrivate)
+{
+}
+
+QXmppSaslClient::~QXmppSaslClient()
+{
+ delete d;
+}
+
+/// Returns a list of supported mechanisms.
+
+QStringList QXmppSaslClient::availableMechanisms()
+{
+ return QStringList() << "PLAIN" << "DIGEST-MD5" << "ANONYMOUS" << "X-FACEBOOK-PLATFORM";
+}
+
+/// Creates an SASL client for the given mechanism.
+
+QXmppSaslClient* QXmppSaslClient::create(const QString &mechanism, QObject *parent)
+{
+ if (mechanism == "PLAIN") {
+ return new QXmppSaslClientPlain(parent);
+ } else if (mechanism == "DIGEST-MD5") {
+ return new QXmppSaslClientDigestMd5(parent);
+ } else if (mechanism == "ANONYMOUS") {
+ return new QXmppSaslClientAnonymous(parent);
+ } else if (mechanism == "X-FACEBOOK-PLATFORM") {
+ return new QXmppSaslClientFacebook(parent);
+ } else {
+ return 0;
+ }
+}
+
+/// 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;
+}
+
+QXmppSaslClientAnonymous::QXmppSaslClientAnonymous(QObject *parent)
+ : QXmppSaslClient(parent)
+ , m_step(0)
+{
+}
+
+QString QXmppSaslClientAnonymous::mechanism() const
+{
+ return "ANONYMOUS";
+}
+
+bool QXmppSaslClientAnonymous::respond(const QByteArray &challenge, QByteArray &response)
+{
+ Q_UNUSED(challenge);
+ if (m_step == 0) {
+ response = QByteArray();
+ m_step++;
+ return true;
+ } else {
+ warning("QXmppSaslClientAnonymous : Invalid step");
+ return false;
+ }
+}
+
+QXmppSaslClientDigestMd5::QXmppSaslClientDigestMd5(QObject *parent)
+ : QXmppSaslClient(parent)
+ , m_nc("00000001")
+ , m_step(0)
+{
+ m_cnonce = generateNonce();
+}
+
+QString QXmppSaslClientDigestMd5::mechanism() const
+{
+ return "DIGEST-MD5";
+}
+
+bool QXmppSaslClientDigestMd5::respond(const QByteArray &challenge, QByteArray &response)
+{
+ Q_UNUSED(challenge);
+ const QByteArray digestUri = QString("%1/%2").arg(serviceType(), host()).toUtf8();
+
+ if (m_step == 0) {
+ response = QByteArray();
+ m_step++;
+ return true;
+ } else if (m_step == 1) {
+ const QMap<QByteArray, QByteArray> input = QXmppSaslDigestMd5::parseMessage(challenge);
+
+ if (!input.contains("nonce")) {
+ warning("QXmppSaslClientDigestMd5 : Invalid input on step 1");
+ return false;
+ }
+
+ // determine realm
+ const QByteArray realm = input.value("realm");
+
+ // determine quality of protection
+ const QList<QByteArray> qops = input.value("qop", "auth").split(',');
+ if (!qops.contains("auth")) {
+ warning("QXmppSaslClientDigestMd5 : Invalid quality of protection");
+ return false;
+ }
+
+ m_nonce = input.value("nonce");
+ m_secret = QCryptographicHash::hash(
+ username().toUtf8() + ":" + realm + ":" + password().toUtf8(),
+ QCryptographicHash::Md5);
+
+ // Build response
+ QMap<QByteArray, QByteArray> output;
+ output["username"] = username().toUtf8();
+ if (!realm.isEmpty())
+ output["realm"] = realm;
+ output["nonce"] = m_nonce;
+ output["qop"] = "auth";
+ output["cnonce"] = m_cnonce;
+ output["nc"] = m_nc;
+ output["digest-uri"] = digestUri;
+ output["response"] = calculateDigest("AUTHENTICATE", digestUri, m_secret, m_nonce, m_cnonce, m_nc);
+ output["charset"] = "utf-8";
+
+ response = QXmppSaslDigestMd5::serializeMessage(output);
+ m_step++;
+ return true;
+ } else if (m_step == 2) {
+ const QMap<QByteArray, QByteArray> input = QXmppSaslDigestMd5::parseMessage(challenge);
+
+ // check new challenge
+ if (input.value("rspauth") != calculateDigest(QByteArray(), digestUri, m_secret, m_nonce, m_cnonce, m_nc)) {
+ warning("QXmppSaslClientDigestMd5 : Invalid challenge on step 2");
+ return false;
+ }
+
+ response = QByteArray();
+ m_step++;
+ return true;
+ } else {
+ warning("QXmppSaslClientDigestMd5 : Invalid step");
+ return false;
+ }
+}
+
+QXmppSaslClientFacebook::QXmppSaslClientFacebook(QObject *parent)
+ : QXmppSaslClient(parent)
+ , m_step(0)
+{
+}
+
+QString QXmppSaslClientFacebook::mechanism() const
+{
+ return "X-FACEBOOK-PLATFORM";
+}
+
+bool QXmppSaslClientFacebook::respond(const QByteArray &challenge, QByteArray &response)
+{
+ if (m_step == 0) {
+ // no initial response
+ response = QByteArray();
+ m_step++;
+ return true;
+ } else if (m_step == 1) {
+ // parse request
+ QUrl requestUrl;
+ requestUrl.setEncodedQuery(challenge);
+ if (!requestUrl.hasQueryItem("method") || !requestUrl.hasQueryItem("nonce")) {
+ warning("QXmppSaslClientFacebook : Invalid challenge, nonce or method missing");
+ return false;
+ }
+
+ // build response
+ QUrl responseUrl;
+ responseUrl.addQueryItem("access_token", username());
+ responseUrl.addQueryItem("api_key", password());
+ responseUrl.addQueryItem("call_id", 0);
+ responseUrl.addQueryItem("method", requestUrl.queryItemValue("method"));
+ responseUrl.addQueryItem("nonce", requestUrl.queryItemValue("nonce"));
+ responseUrl.addQueryItem("v", "1.0");
+
+ response = responseUrl.encodedQuery();
+ m_step++;
+ return true;
+ } else {
+ warning("QXmppSaslClientFacebook : Invalid step");
+ return false;
+ }
+}
+
+QXmppSaslClientPlain::QXmppSaslClientPlain(QObject *parent)
+ : QXmppSaslClient(parent)
+ , m_step(0)
+{
+}
+
+QString QXmppSaslClientPlain::mechanism() const
+{
+ return "PLAIN";
+}
+
+bool QXmppSaslClientPlain::respond(const QByteArray &challenge, QByteArray &response)
+{
+ Q_UNUSED(challenge);
+ if (m_step == 0) {
+ response = QString('\0' + username() + '\0' + password()).toUtf8();
+ m_step++;
+ return true;
+ } else {
+ warning("QXmppSaslClientPlain : Invalid step");
+ return false;
+ }
+}
+
+class QXmppSaslServerPrivate
+{
+public:
+ QString username;
+ QString password;
+ QByteArray passwordDigest;
+ QString realm;
+};
+
+QXmppSaslServer::QXmppSaslServer(QObject *parent)
+ : QXmppLoggable(parent)
+ , d(new QXmppSaslServerPrivate)
+{
+}
+
+QXmppSaslServer::~QXmppSaslServer()
+{
+ delete d;
+}
+
+/// Creates an SASL server for the given mechanism.
+
+QXmppSaslServer* QXmppSaslServer::create(const QString &mechanism, QObject *parent)
+{
+ if (mechanism == "PLAIN") {
+ return new QXmppSaslServerPlain(parent);
+ } else if (mechanism == "DIGEST-MD5") {
+ return new QXmppSaslServerDigestMd5(parent);
+ } else if (mechanism == "ANONYMOUS") {
+ return new QXmppSaslServerAnonymous(parent);
+ } else {
+ return 0;
+ }
+}
+
+/// Returns the username.
+
+QString QXmppSaslServer::username() const
+{
+ return d->username;
+}
+
+/// Sets the username.
+
+void QXmppSaslServer::setUsername(const QString &username)
+{
+ d->username = username;
+}
+
+/// Returns the password.
+
+QString QXmppSaslServer::password() const
+{
+ return d->password;
+}
+
+/// Sets the password.
+
+void QXmppSaslServer::setPassword(const QString &password)
+{
+ d->password = password;
+}
+
+/// Returns the password digest.
+
+QByteArray QXmppSaslServer::passwordDigest() const
+{
+ return d->passwordDigest;
+}
+
+/// Sets the password digest.
+
+void QXmppSaslServer::setPasswordDigest(const QByteArray &digest)
+{
+ d->passwordDigest = digest;
+}
+
+/// Returns the realm.
+
+QString QXmppSaslServer::realm() const
+{
+ return d->realm;
+}
+
+/// Sets the realm.
+
+void QXmppSaslServer::setRealm(const QString &realm)
+{
+ d->realm = realm;
+}
+
+QXmppSaslServerAnonymous::QXmppSaslServerAnonymous(QObject *parent)
+ : QXmppSaslServer(parent)
+ , m_step(0)
+{
+}
+
+QString QXmppSaslServerAnonymous::mechanism() const
+{
+ return "ANONYMOUS";
+}
+
+QXmppSaslServer::Response QXmppSaslServerAnonymous::respond(const QByteArray &request, QByteArray &response)
+{
+ Q_UNUSED(request);
+ if (m_step == 0) {
+ m_step++;
+ response = QByteArray();
+ return Succeeded;
+ } else {
+ warning("QXmppSaslServerAnonymous : Invalid step");
+ return Failed;
+ }
+}
+
+QXmppSaslServerDigestMd5::QXmppSaslServerDigestMd5(QObject *parent)
+ : QXmppSaslServer(parent)
+ , m_step(0)
+{
+ m_nonce = generateNonce();
+}
+
+QString QXmppSaslServerDigestMd5::mechanism() const
+{
+ return "DIGEST-MD5";
+}
+
+QXmppSaslServer::Response QXmppSaslServerDigestMd5::respond(const QByteArray &request, QByteArray &response)
+{
+ if (m_step == 0) {
+ QMap<QByteArray, QByteArray> output;
+ output["nonce"] = m_nonce;
+ if (!realm().isEmpty())
+ output["realm"] = realm().toUtf8();
+ output["qop"] = "auth";
+ output["charset"] = "utf-8";
+ output["algorithm"] = "md5-sess";
+
+ m_step++;
+ response = QXmppSaslDigestMd5::serializeMessage(output);
+ return Challenge;
+ } else if (m_step == 1) {
+ const QMap<QByteArray, QByteArray> input = QXmppSaslDigestMd5::parseMessage(request);
+ const QByteArray realm = input.value("realm");
+ const QByteArray digestUri = input.value("digest-uri");
+
+ if (input.value("qop") != "auth") {
+ warning("QXmppSaslServerDigestMd5 : Invalid quality of protection");
+ return Failed;
+ }
+
+ setUsername(QString::fromUtf8(input.value("username")));
+ if (password().isEmpty() && passwordDigest().isEmpty())
+ return InputNeeded;
+
+ m_nc = input.value("nc");
+ m_cnonce = input.value("cnonce");
+ if (!password().isEmpty()) {
+ m_secret = QCryptographicHash::hash(
+ username().toUtf8() + ":" + realm + ":" + password().toUtf8(),
+ QCryptographicHash::Md5);
+ } else {
+ m_secret = passwordDigest();
+ }
+
+ if (input.value("response") != calculateDigest("AUTHENTICATE", digestUri, m_secret, m_nonce, m_cnonce, m_nc))
+ return Failed;
+
+ QMap<QByteArray, QByteArray> output;
+ output["rspauth"] = calculateDigest(QByteArray(), digestUri, m_secret, m_nonce, m_cnonce, m_nc);
+
+ m_step++;
+ response = QXmppSaslDigestMd5::serializeMessage(output);
+ return Challenge;
+ } else if (m_step == 2) {
+ m_step++;
+ response = QByteArray();
+ return Succeeded;
+ } else {
+ warning("QXmppSaslServerDigestMd5 : Invalid step");
+ return Failed;
+ }
+}
+
+QXmppSaslServerPlain::QXmppSaslServerPlain(QObject *parent)
+ : QXmppSaslServer(parent)
+ , m_step(0)
+{
+}
+
+QString QXmppSaslServerPlain::mechanism() const
+{
+ return "PLAIN";
+}
+
+QXmppSaslServer::Response QXmppSaslServerPlain::respond(const QByteArray &request, QByteArray &response)
+{
+ if (m_step == 0) {
+ QList<QByteArray> auth = request.split('\0');
+ if (auth.size() != 3) {
+ warning("QXmppSaslServerPlain : Invalid input");
+ return Failed;
+ }
+ setUsername(QString::fromUtf8(auth[1]));
+ setPassword(QString::fromUtf8(auth[2]));
+
+ m_step++;
+ response = QByteArray();
+ return InputNeeded;
+ } else {
+ warning("QXmppSaslServerPlain : Invalid step");
+ return Failed;
+ }
+}
+
+void QXmppSaslDigestMd5::setNonce(const QByteArray &nonce)
+{
+ forcedNonce = nonce;
+}
+
+QMap<QByteArray, QByteArray> QXmppSaslDigestMd5::parseMessage(const QByteArray &ba)
+{
+ QMap<QByteArray, QByteArray> map;
+ int startIndex = 0;
+ int pos = 0;
+ while ((pos = ba.indexOf("=", startIndex)) >= 0)
+ {
+ // key get name and skip equals
+ const QByteArray key = ba.mid(startIndex, pos - startIndex).trimmed();
+ pos++;
+
+ // check whether string is quoted
+ if (ba.at(pos) == '"')
+ {
+ // skip opening quote
+ pos++;
+ int endPos = ba.indexOf('"', pos);
+ // skip quoted quotes
+ while (endPos >= 0 && ba.at(endPos - 1) == '\\')
+ endPos = ba.indexOf('"', endPos + 1);
+ if (endPos < 0)
+ {
+ qWarning("Unfinished quoted string");
+ return map;
+ }
+ // unquote
+ QByteArray value = ba.mid(pos, endPos - pos);
+ value.replace("\\\"", "\"");
+ value.replace("\\\\", "\\");
+ map[key] = value;
+ // skip closing quote and comma
+ startIndex = endPos + 2;
+ } else {
+ // non-quoted string
+ int endPos = ba.indexOf(',', pos);
+ if (endPos < 0)
+ endPos = ba.size();
+ map[key] = ba.mid(pos, endPos - pos);
+ // skip comma
+ startIndex = endPos + 1;
+ }
+ }
+ return map;
+}
+
+QByteArray QXmppSaslDigestMd5::serializeMessage(const QMap<QByteArray, QByteArray> &map)
+{
+ QByteArray ba;
+ foreach (const QByteArray &key, map.keys())
+ {
+ if (!ba.isEmpty())
+ ba.append(',');
+ ba.append(key + "=");
+ QByteArray value = map[key];
+ const char *separators = "()<>@,;:\\\"/[]?={} \t";
+ bool quote = false;
+ for (const char *c = separators; *c; c++)
+ {
+ if (value.contains(*c))
+ {
+ quote = true;
+ break;
+ }
+ }
+ if (quote)
+ {
+ value.replace("\\", "\\\\");
+ value.replace("\"", "\\\"");
+ ba.append("\"" + value + "\"");
+ }
+ else
+ ba.append(value);
+ }
+ return ba;
+}
+