diff --git a/src/CryptographicHash.cpp b/src/CryptographicHash.cpp new file mode 100644 index 00000000000..2046d53829f --- /dev/null +++ b/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 . + +#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(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(); +} diff --git a/src/CryptographicHash.h b/src/CryptographicHash.h new file mode 100644 index 00000000000..590551f13e6 --- /dev/null +++ b/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 . + +#ifndef MUMBLE_CRYPTOGRAPHICHASH_H_ +#define MUMBLE_CRYPTOGRAPHICHASH_H_ + +#include + +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 diff --git a/src/mumble.pri b/src/mumble.pri index 69fcd75600c..efca083e9fd 100644 --- a/src/mumble.pri +++ b/src/mumble.pri @@ -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 diff --git a/src/tests/TestCryptographicHash.cpp b/src/tests/TestCryptographicHash.cpp new file mode 100644 index 00000000000..bf5657ecf6f --- /dev/null +++ b/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 . + +#include +#include +#include + +#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("input"); + QTest::addColumn("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("input"); + QTest::addColumn("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" diff --git a/src/tests/TestCryptographicHash.pro b/src/tests/TestCryptographicHash.pro new file mode 100644 index 00000000000..a6b151f736c --- /dev/null +++ b/src/tests/TestCryptographicHash.pro @@ -0,0 +1,17 @@ +# 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 . + +include(../../compiler.pri) +include(../../qmake/openssl.pri) + +TEMPLATE = app +CONFIG += qt warn_on qtestlib +CONFIG -= app_bundle +LANGUAGE = C++ +TARGET = TestCryptographicHash +SOURCES = TestCryptographicHash.cpp CryptographicHash.cpp +HEADERS = CryptographicHash.h +VPATH += .. +INCLUDEPATH += .. ../murmur ../mumble