diff options
| author | Jeremy Lainé <jeremy.laine@m4x.org> | 2019-01-17 00:23:27 +0100 |
|---|---|---|
| committer | Jeremy Lainé <jeremy.laine@m4x.org> | 2019-01-17 20:35:09 +0100 |
| commit | bce9ca477709ae0876e7b7682034f49cdd010f27 (patch) | |
| tree | 114591927851493247dd93bdbf0c94510b8adebf | |
| parent | e52030614d935dfb044b0e3fc57a30d812d626f3 (diff) | |
| download | qxmpp-bce9ca477709ae0876e7b7682034f49cdd010f27.tar.gz | |
[sasl] add support for SCRAM-SHA-1 and SCRAM-SHA-256
| -rw-r--r-- | CHANGELOG.md | 3 | ||||
| -rw-r--r-- | src/base/QXmppSasl.cpp | 122 | ||||
| -rw-r--r-- | src/base/QXmppSasl_p.h | 19 | ||||
| -rw-r--r-- | tests/qxmppsasl/tst_qxmppsasl.cpp | 93 |
4 files changed, 233 insertions, 4 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index d02973e6..34514f47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ QXmpp 1.0.1 (UNRELEASED) ------------------------ -*under development* +New features: + - Add support for SCRAM-SHA-1 and SCRAM-SHA-256 (#183, @jlaine) QXmpp 1.0.0 (Jan 8, 2019) ------------------------- diff --git a/src/base/QXmppSasl.cpp b/src/base/QXmppSasl.cpp index d6dbb04a..b47886f7 100644 --- a/src/base/QXmppSasl.cpp +++ b/src/base/QXmppSasl.cpp @@ -24,9 +24,10 @@ #include <cstdlib> -#include <QCryptographicHash> #include <QDomElement> +#include <QMessageAuthenticationCode> #include <QStringList> +#include <QtEndian> #include <QUrlQuery> #include "QXmppSasl_p.h" @@ -49,6 +50,36 @@ 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, quint64 dkLen) +{ + QByteArray key; + quint32 currentIteration = 1; + QMessageAuthenticationCode hmac(algorithm, data); + QByteArray index(4, Qt::Uninitialized); + while (quint64(key.length()) < dkLen) { + hmac.addData(salt); + qToBigEndian(currentIteration, reinterpret_cast<uchar*>(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<char>()); + } + key += tkey; + currentIteration++; + } + return key.left(dkLen); +} + static QByteArray generateNonce() { if (!forcedNonce.isEmpty()) @@ -61,6 +92,17 @@ static QByteArray generateNonce() return nonce.toBase64(); } +static QMap<char, QByteArray> parseGS2(const QByteArray &ba) +{ + QMap<char, QByteArray> map; + foreach (const QByteArray &keyValue, ba.split(',')) { + if (keyValue.size() >= 2 && keyValue[1] == '=') { + map[keyValue[0]] = keyValue.mid(2); + } + } + return map; +} + QXmppSaslAuth::QXmppSaslAuth(const QString &mechanism, const QByteArray &value) : m_mechanism(mechanism) , m_value(value) @@ -230,7 +272,9 @@ QXmppSaslClient::~QXmppSaslClient() QStringList QXmppSaslClient::availableMechanisms() { - return QStringList() << "PLAIN" << "DIGEST-MD5" << "ANONYMOUS" << "X-FACEBOOK-PLATFORM" << "X-MESSENGER-OAUTH2" << "X-OAUTH2"; + return QStringList() << "PLAIN" << "DIGEST-MD5" << "ANONYMOUS" + << "SCRAM-SHA-1" << "SCRAM-SHA-256" + << "X-FACEBOOK-PLATFORM" << "X-MESSENGER-OAUTH2" << "X-OAUTH2"; } /// Creates an SASL client for the given mechanism. @@ -243,6 +287,10 @@ QXmppSaslClient* QXmppSaslClient::create(const QString &mechanism, QObject *pare return new QXmppSaslClientDigestMd5(parent); } else if (mechanism == "ANONYMOUS") { return new QXmppSaslClientAnonymous(parent); + } else if (mechanism == "SCRAM-SHA-1") { + return new QXmppSaslClientScram(QCryptographicHash::Sha1, parent); + } else if (mechanism == "SCRAM-SHA-256") { + return new QXmppSaslClientScram(QCryptographicHash::Sha256, parent); } else if (mechanism == "X-FACEBOOK-PLATFORM") { return new QXmppSaslClientFacebook(parent); } else if (mechanism == "X-MESSENGER-OAUTH2") { @@ -507,6 +555,76 @@ bool QXmppSaslClientPlain::respond(const QByteArray &challenge, QByteArray &resp } } +QXmppSaslClientScram::QXmppSaslClientScram(QCryptographicHash::Algorithm algorithm, QObject *parent) + : QXmppSaslClient(parent) + , m_algorithm(algorithm) + , m_step(0) +{ + Q_ASSERT(m_algorithm == QCryptographicHash::Sha1 || m_algorithm == QCryptographicHash::Sha256); + m_nonce = generateNonce(); + + if (m_algorithm == QCryptographicHash::Sha256) { + m_dklen = 32; + m_mechanism = "SCRAM-SHA-256"; + } else { + m_dklen = 20; + m_mechanism = "SCRAM-SHA-1"; + } +} + +QString QXmppSaslClientScram::mechanism() const +{ + return m_mechanism; +} + +bool QXmppSaslClientScram::respond(const QByteArray &challenge, QByteArray &response) +{ + Q_UNUSED(challenge); + if (m_step == 0) { + m_gs2Header = "n,,"; + m_clientFirstMessageBare = "n=" + username().toUtf8() + ",r=" + m_nonce; + + response = m_gs2Header + m_clientFirstMessageBare; + m_step++; + return true; + } else if (m_step == 1) { + // validate input + const QMap<char, QByteArray> input = parseGS2(challenge); + const QByteArray nonce = input.value('r'); + const QByteArray salt = QByteArray::fromBase64(input.value('s')); + const int iterations = input.value('i').toInt(); + if (!nonce.startsWith(m_nonce) || salt.isEmpty() || iterations < 1) { + return false; + } + + // calculate proofs + const QByteArray clientFinalMessageBare = "c=" + m_gs2Header.toBase64() + ",r=" + nonce; + const QByteArray saltedPassword = deriveKeyPbkdf2(m_algorithm, password().toUtf8(), salt, + iterations, m_dklen); + const QByteArray clientKey = QMessageAuthenticationCode::hash("Client Key", saltedPassword, m_algorithm); + const QByteArray storedKey = QCryptographicHash::hash(clientKey, m_algorithm); + const QByteArray authMessage = m_clientFirstMessageBare + "," + challenge + "," + clientFinalMessageBare; + QByteArray clientProof = QMessageAuthenticationCode::hash(authMessage, storedKey, m_algorithm); + std::transform(clientProof.cbegin(), clientProof.cend(), clientKey.cbegin(), + clientProof.begin(), std::bit_xor<char>()); + + const QByteArray serverKey = QMessageAuthenticationCode::hash("Server Key", saltedPassword, m_algorithm); + m_serverSignature = QMessageAuthenticationCode::hash(authMessage, serverKey, m_algorithm); + + response = clientFinalMessageBare + ",p=" + clientProof.toBase64(); + m_step++; + return true; + } else if (m_step == 2) { + const QMap<char, QByteArray> input = parseGS2(challenge); + response = QByteArray(); + m_step++; + return QByteArray::fromBase64(input.value('v')) == m_serverSignature; + } else { + warning("QXmppSaslClientPlain : Invalid step"); + return false; + } +} + QXmppSaslClientWindowsLive::QXmppSaslClientWindowsLive(QObject *parent) : QXmppSaslClient(parent) , m_step(0) diff --git a/src/base/QXmppSasl_p.h b/src/base/QXmppSasl_p.h index ac8d911b..90c2638c 100644 --- a/src/base/QXmppSasl_p.h +++ b/src/base/QXmppSasl_p.h @@ -26,6 +26,7 @@ #define QXMPPSASL_P_H #include <QByteArray> +#include <QCryptographicHash> #include <QMap> #include "QXmppGlobal.h" @@ -262,6 +263,24 @@ private: int m_step; }; +class QXmppSaslClientScram : public QXmppSaslClient +{ +public: + QXmppSaslClientScram(QCryptographicHash::Algorithm algorithm, QObject *parent = 0); + QString mechanism() const; + bool respond(const QByteArray &challenge, QByteArray &response); + +private: + QCryptographicHash::Algorithm m_algorithm; + int m_step; + int m_dklen; + QString m_mechanism; + QByteArray m_gs2Header; + QByteArray m_clientFirstMessageBare; + QByteArray m_serverSignature; + QByteArray m_nonce; +}; + class QXmppSaslClientWindowsLive : public QXmppSaslClient { public: diff --git a/tests/qxmppsasl/tst_qxmppsasl.cpp b/tests/qxmppsasl/tst_qxmppsasl.cpp index 3beabf00..4d5bc5dd 100644 --- a/tests/qxmppsasl/tst_qxmppsasl.cpp +++ b/tests/qxmppsasl/tst_qxmppsasl.cpp @@ -49,6 +49,9 @@ private slots: void testClientFacebook(); void testClientGoogle(); void testClientPlain(); + void testClientScramSha1(); + void testClientScramSha1_bad(); + void testClientScramSha256(); void testClientWindowsLive(); // server @@ -186,7 +189,7 @@ void tst_QXmppSasl::testSuccess() void tst_QXmppSasl::testClientAvailableMechanisms() { - QCOMPARE(QXmppSaslClient::availableMechanisms(), QStringList() << "PLAIN" << "DIGEST-MD5" << "ANONYMOUS" << "X-FACEBOOK-PLATFORM" << "X-MESSENGER-OAUTH2" << "X-OAUTH2"); + QCOMPARE(QXmppSaslClient::availableMechanisms(), QStringList() << "PLAIN" << "DIGEST-MD5" << "ANONYMOUS" << "SCRAM-SHA-1" << "SCRAM-SHA-256" << "X-FACEBOOK-PLATFORM" << "X-MESSENGER-OAUTH2" << "X-OAUTH2"); } void tst_QXmppSasl::testClientBadMechanism() @@ -316,6 +319,94 @@ void tst_QXmppSasl::testClientPlain() delete client; } +void tst_QXmppSasl::testClientScramSha1() +{ + QXmppSaslDigestMd5::setNonce("fyko+d2lbbFgONRv9qkxdawL"); + + QXmppSaslClient *client = QXmppSaslClient::create("SCRAM-SHA-1"); + QVERIFY(client != 0); + QCOMPARE(client->mechanism(), QLatin1String("SCRAM-SHA-1")); + + client->setUsername("user"); + client->setPassword("pencil"); + + // first step + QByteArray response; + QVERIFY(client->respond(QByteArray(), response)); + QCOMPARE(response, QByteArray("n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL")); + + // second step + QVERIFY(client->respond(QByteArray("r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096"), response)); + QCOMPARE(response, QByteArray("c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts=")); + + // third step + QVERIFY(client->respond(QByteArray("v=rmF9pqV8S7suAoZWja4dJRkFsKQ"), response)); + QCOMPARE(response, QByteArray()); + + // any further step is an error + QVERIFY(!client->respond(QByteArray(), response)); + + delete client; +} + +void tst_QXmppSasl::testClientScramSha1_bad() +{ + QXmppSaslDigestMd5::setNonce("fyko+d2lbbFgONRv9qkxdawL"); + + QXmppSaslClient *client = QXmppSaslClient::create("SCRAM-SHA-1"); + QVERIFY(client != 0); + QCOMPARE(client->mechanism(), QLatin1String("SCRAM-SHA-1")); + + client->setUsername("user"); + client->setPassword("pencil"); + + // first step + QByteArray response; + QVERIFY(client->respond(QByteArray(), response)); + QCOMPARE(response, QByteArray("n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL")); + + // no nonce + QVERIFY(!client->respond(QByteArray("s=QSXCR+Q6sek8bf92,i=4096"), response)); + + // no salt + QVERIFY(!client->respond(QByteArray("r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,i=4096"), response)); + + // no iterations + QVERIFY(!client->respond(QByteArray("r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92"), response)); + + delete client; +} + +void tst_QXmppSasl::testClientScramSha256() +{ + QXmppSaslDigestMd5::setNonce("rOprNGfwEbeRWgbNEkqO"); + + QXmppSaslClient *client = QXmppSaslClient::create("SCRAM-SHA-256"); + QVERIFY(client != 0); + QCOMPARE(client->mechanism(), QLatin1String("SCRAM-SHA-256")); + + client->setUsername("user"); + client->setPassword("pencil"); + + // first step + QByteArray response; + QVERIFY(client->respond(QByteArray(), response)); + QCOMPARE(response, QByteArray("n,,n=user,r=rOprNGfwEbeRWgbNEkqO")); + + // second step + QVERIFY(client->respond(QByteArray("r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,s=W22ZaJ0SNY7soEsUEjb6gQ==,i=4096"), response)); + QCOMPARE(response, QByteArray("c=biws,r=rOprNGfwEbeRWgbNEkqO%hvYDpWUa2RaTCAfuxFIlj)hNlF$k0,p=dHzbZapWIk4jUhN+Ute9ytag9zjfMHgsqmmiz7AndVQ=")); + + // third step + QVERIFY(client->respond(QByteArray("v=6rriTRBi23WpRR/wtup+mMhUZUn/dB5nLTJRsjl95G4="), response)); + QCOMPARE(response, QByteArray()); + + // any further step is an error + QVERIFY(!client->respond(QByteArray(), response)); + + delete client; +} + void tst_QXmppSasl::testClientWindowsLive() { QXmppSaslClient *client = QXmppSaslClient::create("X-MESSENGER-OAUTH2"); |
