Skip to content

Commit

Permalink
[sasl] add support for SCRAM-SHA-1 and SCRAM-SHA-256
Browse files Browse the repository at this point in the history
  • Loading branch information
jlaine committed Jan 17, 2019
1 parent e520306 commit bce9ca4
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 4 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
@@ -1,7 +1,8 @@
QXmpp 1.0.1 (UNRELEASED) 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) QXmpp 1.0.0 (Jan 8, 2019)
------------------------- -------------------------
Expand Down
122 changes: 120 additions & 2 deletions src/base/QXmppSasl.cpp
Expand Up @@ -24,9 +24,10 @@


#include <cstdlib> #include <cstdlib>


#include <QCryptographicHash>
#include <QDomElement> #include <QDomElement>
#include <QMessageAuthenticationCode>
#include <QStringList> #include <QStringList>
#include <QtEndian>
#include <QUrlQuery> #include <QUrlQuery>


#include "QXmppSasl_p.h" #include "QXmppSasl_p.h"
Expand All @@ -49,6 +50,36 @@ static QByteArray calculateDigest(const QByteArray &method, const QByteArray &di
return QCryptographicHash::hash(KD, QCryptographicHash::Md5).toHex(); 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() static QByteArray generateNonce()
{ {
if (!forcedNonce.isEmpty()) if (!forcedNonce.isEmpty())
Expand All @@ -61,6 +92,17 @@ static QByteArray generateNonce()
return nonce.toBase64(); 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) QXmppSaslAuth::QXmppSaslAuth(const QString &mechanism, const QByteArray &value)
: m_mechanism(mechanism) : m_mechanism(mechanism)
, m_value(value) , m_value(value)
Expand Down Expand Up @@ -230,7 +272,9 @@ QXmppSaslClient::~QXmppSaslClient()


QStringList QXmppSaslClient::availableMechanisms() 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. /// Creates an SASL client for the given mechanism.
Expand All @@ -243,6 +287,10 @@ QXmppSaslClient* QXmppSaslClient::create(const QString &mechanism, QObject *pare
return new QXmppSaslClientDigestMd5(parent); return new QXmppSaslClientDigestMd5(parent);
} else if (mechanism == "ANONYMOUS") { } else if (mechanism == "ANONYMOUS") {
return new QXmppSaslClientAnonymous(parent); 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") { } else if (mechanism == "X-FACEBOOK-PLATFORM") {
return new QXmppSaslClientFacebook(parent); return new QXmppSaslClientFacebook(parent);
} else if (mechanism == "X-MESSENGER-OAUTH2") { } else if (mechanism == "X-MESSENGER-OAUTH2") {
Expand Down Expand Up @@ -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) QXmppSaslClientWindowsLive::QXmppSaslClientWindowsLive(QObject *parent)
: QXmppSaslClient(parent) : QXmppSaslClient(parent)
, m_step(0) , m_step(0)
Expand Down
19 changes: 19 additions & 0 deletions src/base/QXmppSasl_p.h
Expand Up @@ -26,6 +26,7 @@
#define QXMPPSASL_P_H #define QXMPPSASL_P_H


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


#include "QXmppGlobal.h" #include "QXmppGlobal.h"
Expand Down Expand Up @@ -262,6 +263,24 @@ class QXmppSaslClientPlain : public QXmppSaslClient
int m_step; 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 class QXmppSaslClientWindowsLive : public QXmppSaslClient
{ {
public: public:
Expand Down
93 changes: 92 additions & 1 deletion tests/qxmppsasl/tst_qxmppsasl.cpp
Expand Up @@ -49,6 +49,9 @@ private slots:
void testClientFacebook(); void testClientFacebook();
void testClientGoogle(); void testClientGoogle();
void testClientPlain(); void testClientPlain();
void testClientScramSha1();
void testClientScramSha1_bad();
void testClientScramSha256();
void testClientWindowsLive(); void testClientWindowsLive();


// server // server
Expand Down Expand Up @@ -186,7 +189,7 @@ void tst_QXmppSasl::testSuccess()


void tst_QXmppSasl::testClientAvailableMechanisms() 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() void tst_QXmppSasl::testClientBadMechanism()
Expand Down Expand Up @@ -316,6 +319,94 @@ void tst_QXmppSasl::testClientPlain()
delete client; 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() void tst_QXmppSasl::testClientWindowsLive()
{ {
QXmppSaslClient *client = QXmppSaslClient::create("X-MESSENGER-OAUTH2"); QXmppSaslClient *client = QXmppSaslClient::create("X-MESSENGER-OAUTH2");
Expand Down

0 comments on commit bce9ca4

Please sign in to comment.