Skip to content

Commit

Permalink
CryptographicHash: new class for computing cryptographic hashes.
Browse files Browse the repository at this point in the history
We used to use QCryptographicHash, but Qt 4 doesn't support Sha256.

To remedy that, we've implemented this class on top of OpenSSL's EVP
system.

Besides being able to work on Qt 4, this class also means we can add
additional hash functions in the future, which are not necessarily
supported by Qt, such as BLAKE2.
  • Loading branch information
mkrautz committed Feb 25, 2017
1 parent e6ac067 commit 7c22b84
Show file tree
Hide file tree
Showing 5 changed files with 393 additions and 2 deletions.
164 changes: 164 additions & 0 deletions src/CryptographicHash.cpp
@@ -0,0 +1,164 @@
// Copyright 2005-2017 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.

#include "murmur_pch.h"

#include "CryptographicHash.h"

class CryptographicHashPrivate {
public:
CryptographicHashPrivate(const EVP_MD *type);
~CryptographicHashPrivate();
void addData(const QByteArray &buf);
QByteArray result();

private:
EVP_MD_CTX *m_mdctx;

/// The result of the cryptographic hash.
/// This field is filled out by the result()
/// method.
/// Once this field is set, the m_mdctx is finalized
/// and can't be used anymore, so result() and addData()
/// must guard against that possibility.
QByteArray m_result;

CryptographicHashPrivate();

/// If m_mdctx is set, cleanupMdctx will destroy
/// the EVP_MD_CTX object and set m_mdctx to NULL.
void cleanupMdctx();
};

CryptographicHashPrivate::CryptographicHashPrivate()
: m_mdctx(NULL) {
}

void CryptographicHashPrivate::cleanupMdctx() {
if (m_mdctx) {
EVP_MD_CTX_destroy(m_mdctx);
m_mdctx = NULL;
}
}

CryptographicHashPrivate::CryptographicHashPrivate(const EVP_MD *type)
: m_mdctx(NULL) {

m_mdctx = EVP_MD_CTX_create();
if (m_mdctx == NULL) {
return;
}

int err = EVP_DigestInit_ex(m_mdctx, type, NULL);
if (err != 1) {
cleanupMdctx();
return;
}
}

CryptographicHashPrivate::~CryptographicHashPrivate() {
cleanupMdctx();
}

void CryptographicHashPrivate::addData(const QByteArray &buf) {
if (m_mdctx == NULL) {
return;
}

// If we have a result already, that means our m_mdctx
// object has been finalized, and can't be used anymore.
// In that case, transition into an error state by cleaning
// up the mdctx -- that way, subsequent calls to result()
// will return an empty QByteArray.
if (!m_result.isNull()) {
cleanupMdctx();
return;
}

int err = EVP_DigestUpdate(m_mdctx, buf.constData(), buf.size());
if (err != 1) {
cleanupMdctx();
}
}

QByteArray CryptographicHashPrivate::result() {
if (m_mdctx == NULL) {
return QByteArray();
}

// If we have a result already, that means our m_mdctx
// object has been finalized, and can't be used anymore.
// In that case, just return the already computed result.
if (!m_result.isNull()) {
return m_result;
}

QByteArray digest(EVP_MD_CTX_size(m_mdctx), '\0');
int err = EVP_DigestFinal_ex(m_mdctx, reinterpret_cast<unsigned char *>(digest.data()), NULL);
if (err != 1) {
cleanupMdctx();
return QByteArray();
}

m_result = digest;

return m_result;
}

QByteArray CryptographicHash::hash(const QByteArray &buf, Algorithm algo) {
CryptographicHash h(algo);
h.addData(buf);
return h.result();
}

QString CryptographicHash::humanReadableAlgorithmName(CryptographicHash::Algorithm algo) {
switch (algo) {
case CryptographicHash::Sha1:
return QLatin1String("SHA-1");
case CryptographicHash::Sha256:
return QLatin1String("SHA-256");
}
return QString();
}

QString CryptographicHash::shortAlgorithmName(CryptographicHash::Algorithm algo) {
switch (algo) {
case CryptographicHash::Sha1:
return QLatin1String("sha1");
case CryptographicHash::Sha256:
return QLatin1String("sha256");
}
return QString();
}

CryptographicHash::CryptographicHash()
: d(NULL) {
}

CryptographicHash::CryptographicHash(CryptographicHash::Algorithm algo)
: d(NULL) {

switch (algo) {
case CryptographicHash::Sha1:
d = new CryptographicHashPrivate(EVP_sha1());
break;
case CryptographicHash::Sha256:
d = new CryptographicHashPrivate(EVP_sha256());
break;
}
}

void CryptographicHash::addData(const QByteArray &buf) {
if (d) {
d->addData(buf);
}
}

QByteArray CryptographicHash::result() const {
if (d) {
return d->result();
}
return QByteArray();
}
71 changes: 71 additions & 0 deletions src/CryptographicHash.h
@@ -0,0 +1,71 @@
// Copyright 2005-2017 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.

#ifndef MUMBLE_CRYPTOGRAPHICHASH_H_
#define MUMBLE_CRYPTOGRAPHICHASH_H_

#include <QByteArray>

class CryptographicHashPrivate;

/// CryptographicHash is used for computing cryptographic hashes.
class CryptographicHash {
public:
/// Algorithm specifies an algorithm for use with CryptographicHash.
enum Algorithm {
/// The SHA-1 hashing algorithm.
Sha1,
/// The SHA-256 hashing algorithm.
Sha256
};

/// Compute the cryptographic hash of |buf| using algorithm |algo|
/// and return the result.
/// On failure, the returned QByteArray will be empty.
static QByteArray hash(const QByteArray &buf, Algorithm algo);

/// Return the human readable name of |algo|.
///
/// Examples:
/// CryptographicHash::Sha1 -> "SHA-1"
/// CryptographicHash::Sha256 -> "SHA-256"
static QString humanReadableAlgorithmName(CryptographicHash::Algorithm algo);

/// Return a machine-readable short name of |algo|.
/// This variant is a short, lower-case name for the algorithm, suitable
/// for prefixing when serializing a cryptographic hash.
///
/// Examples:
/// CryptographicHash::Sha1 -> "sha1"
/// CryptographicHash::Sha256 -> "sha256"
static QString shortAlgorithmName(CryptographicHash::Algorithm algo);

/// Consruct a CryptographicHash object that can be used
/// to incrementally compute a cryptographic hash using the
/// hash function specified in |algo|.
CryptographicHash(Algorithm algo);

/// addData adds the content of |buf| to the hash computation.
///
/// Attempting to add data to the CryptographicHash after result()
/// has been called is an error, and will cause the CryptographicHash
/// object to transition to an error state. Subsequent calls to result()
/// will return an empty QByteArray.
void addData(const QByteArray &buf);

/// result() finalizes the CryptographicHash object,
/// and returns the computed hash.
/// After calling this method, it is no longer possible
/// to add more data to the CryptographicHash object.
/// If an error occurred during the computation, the
/// returned QByteArray will be empty.
QByteArray result() const;

private:
CryptographicHash();
CryptographicHashPrivate *d;
};

#endif
4 changes: 2 additions & 2 deletions src/mumble.pri
Expand Up @@ -14,8 +14,8 @@ CONFIG += qt thread debug_and_release warn_on
DEFINES *= MUMBLE_VERSION_STRING=$$VERSION
INCLUDEPATH += $$PWD . ../mumble_proto
VPATH += $$PWD
HEADERS *= ACL.h Channel.h CryptState.h Connection.h Group.h HTMLFilter.h User.h Net.h OSInfo.h Timer.h SSL.h Version.h SSLCipherInfo.h SSLCipherInfoTable.h licenses.h License.h LogEmitter.h
SOURCES *= ACL.cpp Group.cpp Channel.cpp Connection.cpp HTMLFilter.cpp User.cpp Timer.cpp CryptState.cpp OSInfo.cpp Net.cpp SSL.cpp Version.cpp SSLCipherInfo.cpp License.cpp LogEmitter.cpp
HEADERS *= ACL.h Channel.h CryptState.h Connection.h Group.h HTMLFilter.h User.h Net.h OSInfo.h Timer.h SSL.h Version.h SSLCipherInfo.h SSLCipherInfoTable.h licenses.h License.h LogEmitter.h CryptographicHash.h
SOURCES *= ACL.cpp Group.cpp Channel.cpp Connection.cpp HTMLFilter.cpp User.cpp Timer.cpp CryptState.cpp OSInfo.cpp Net.cpp SSL.cpp Version.cpp SSLCipherInfo.cpp License.cpp LogEmitter.cpp CryptographicHash.cpp
LIBS *= -lmumble_proto
# Note: Protobuf generates into its own directory so we can mark it as a
# system include folder for unix. Otherwise the generated code creates
Expand Down
139 changes: 139 additions & 0 deletions src/tests/TestCryptographicHash.cpp
@@ -0,0 +1,139 @@
// Copyright 2005-2017 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.

#include <QtCore>
#include <QtTest>
#include <QLatin1String>

#include "CryptographicHash.h"

class TestCryptographicHash : public QObject {
Q_OBJECT
private slots:
void sha1_data();
void sha1();

void sha256_data();
void sha256();

void staticHashVsObject();
void multipleResultCalls();
void addDataAfterResult();
};

/// normalizeHash removes all whitespace from the hex-encoded hash string.
static QString normalizeHash(QString str) {
str.replace(QLatin1String(" "), QLatin1String(""));
str.replace(QLatin1String("\t"), QLatin1String(""));
str.replace(QLatin1String("\r"), QLatin1String(""));
str.replace(QLatin1String("\n"), QLatin1String(""));
return str;
}

/// normalizeHash removes all whitespace from the hex-encoded hash byte array.
static QByteArray normalizeHash(QByteArray buf) {
buf.replace(" ", "");
buf.replace("\t", "");
buf.replace("\r", "");
buf.replace("\n", "");
return buf;
}

void TestCryptographicHash::sha1_data() {
QTest::addColumn<QString>("input");
QTest::addColumn<QString>("expectedOutput");

// First 4 SHA1 test vectors from http://www.di-mgt.com.au/sha_testvectors.html
// ...The rest are a bit too excessive to put in a table-based test.
QTest::newRow("1") << QString(QLatin1String("abc")) << normalizeHash(QLatin1String("a9993e36 4706816a ba3e2571 7850c26c 9cd0d89d"));
QTest::newRow("2") << QString(QLatin1String("")) << normalizeHash(QLatin1String("da39a3ee 5e6b4b0d 3255bfef 95601890 afd80709"));
QTest::newRow("3") << QString(QLatin1String("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq")) << normalizeHash(QLatin1String("84983e44 1c3bd26e baae4aa1 f95129e5 e54670f1"));
QTest::newRow("4") << QString(QLatin1String("abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu")) << normalizeHash(QLatin1String("a49b2446 a02c645b f419f995 b6709125 3a04a259"));
}

void TestCryptographicHash::sha1() {
QFETCH(QString, input);
QFETCH(QString, expectedOutput);

QByteArray outBuf = CryptographicHash::hash(input.toLatin1(), CryptographicHash::Sha1);
QByteArray hexBuf = outBuf.toHex();
QString output = QString::fromLatin1(hexBuf);

QCOMPARE(output, expectedOutput);
}

void TestCryptographicHash::sha256_data() {
QTest::addColumn<QString>("input");
QTest::addColumn<QString>("expectedOutput");

// First 4 SHA256 test vectors from http://www.di-mgt.com.au/sha_testvectors.html
// ...The rest are a bit too excessive to put in a table-based test.
QTest::newRow("1") << QString(QLatin1String("abc")) << normalizeHash(QLatin1String("ba7816bf 8f01cfea 414140de 5dae2223 b00361a3 96177a9c b410ff61 f20015ad"));
QTest::newRow("2") << QString(QLatin1String("")) << normalizeHash(QLatin1String("e3b0c442 98fc1c14 9afbf4c8 996fb924 27ae41e4 649b934c a495991b 7852b855"));
QTest::newRow("3") << QString(QLatin1String("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq")) << normalizeHash(QLatin1String("248d6a61 d20638b8 e5c02693 0c3e6039 a33ce459 64ff2167 f6ecedd4 19db06c1"));
QTest::newRow("4") << QString(QLatin1String("abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu")) << normalizeHash(QLatin1String("cf5b16a7 78af8380 036ce59e 7b049237 0b249b11 e8f07a51 afac4503 7afee9d1"));
}

void TestCryptographicHash::sha256() {
QFETCH(QString, input);
QFETCH(QString, expectedOutput);

QByteArray outBuf = CryptographicHash::hash(input.toLatin1(), CryptographicHash::Sha256);

QVERIFY(!outBuf.isEmpty());

QByteArray hexBuf = outBuf.toHex();
QString output = QString::fromLatin1(hexBuf);

QCOMPARE(output, expectedOutput);
}

void TestCryptographicHash::staticHashVsObject() {
CryptographicHash h(CryptographicHash::Sha256);
h.addData(QByteArray("abc"));
QByteArray objectResult = h.result();

QByteArray staticResult = CryptographicHash::hash(QByteArray("abc"), CryptographicHash::Sha256);

QVERIFY(!objectResult.isEmpty());
QVERIFY(!staticResult.isEmpty());

QCOMPARE(objectResult, staticResult);
}

void TestCryptographicHash::multipleResultCalls() {
CryptographicHash h(CryptographicHash::Sha256);
h.addData(QByteArray("abc"));

QByteArray result1 = h.result();
QByteArray result2 = h.result();
QByteArray result3 = h.result();

QVERIFY(!result1.isEmpty());
QVERIFY(!result2.isEmpty());
QVERIFY(!result3.isEmpty());

QCOMPARE(result1.toHex(), normalizeHash(QByteArray("ba7816bf 8f01cfea 414140de 5dae2223 b00361a3 96177a9c b410ff61 f20015ad")));
QCOMPARE(result1, result2);
QCOMPARE(result1, result3);
}

void TestCryptographicHash::addDataAfterResult() {
CryptographicHash h(CryptographicHash::Sha256);
h.addData(QByteArray("abc"));

QByteArray result1 = h.result();

QCOMPARE(result1.toHex(), normalizeHash(QByteArray("ba7816bf 8f01cfea 414140de 5dae2223 b00361a3 96177a9c b410ff61 f20015ad")));

// Adding data after getting the result of a
// CryptographicHash is an error.
h.addData(QByteArray("abc"));
QByteArray result2 = h.result();
QVERIFY(result2.isEmpty());
}

QTEST_MAIN(TestCryptographicHash)
#include "TestCryptographicHash.moc"

0 comments on commit 7c22b84

Please sign in to comment.