Permalink
Browse files

[sasl] add support for SCRAM-SHA-1 and SCRAM-SHA-256

  • Loading branch information...
jlaine committed Jan 16, 2019
1 parent e520306 commit bce9ca477709ae0876e7b7682034f49cdd010f27
Showing with 233 additions and 4 deletions.
  1. +2 −1 CHANGELOG.md
  2. +120 −2 src/base/QXmppSasl.cpp
  3. +19 −0 src/base/QXmppSasl_p.h
  4. +92 −1 tests/qxmppsasl/tst_qxmppsasl.cpp
@@ -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)
-------------------------
@@ -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)
@@ -26,6 +26,7 @@
#define QXMPPSASL_P_H

#include <QByteArray>
#include <QCryptographicHash>
#include <QMap>

#include "QXmppGlobal.h"
@@ -262,6 +263,24 @@ class QXmppSaslClientPlain : public QXmppSaslClient
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:
@@ -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");

0 comments on commit bce9ca4

Please sign in to comment.