diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d5c198ee..34ac87f04 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -262,7 +262,7 @@ set_property(DIRECTORY APPEND PROPERTY COMPILE_DEFINITIONS_NONE QT_NO_DEBUG) find_package(LibGPGError REQUIRED) -find_package(Gcrypt 1.6.0 REQUIRED) +find_package(Gcrypt 1.7.0 REQUIRED) find_package(ZLIB REQUIRED) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c04901a11..2e10cdcf4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -148,6 +148,7 @@ set(keepassx_SOURCES keys/PasswordKey.cpp keys/YkChallengeResponseKey.cpp streams/HashedBlockStream.cpp + streams/HmacBlockStream.cpp streams/LayeredStream.cpp streams/qtiocompressor.cpp streams/StoreDataStream.cpp diff --git a/src/crypto/Crypto.cpp b/src/crypto/Crypto.cpp index d00be720b..7ba78a6b3 100644 --- a/src/crypto/Crypto.cpp +++ b/src/crypto/Crypto.cpp @@ -95,18 +95,28 @@ bool Crypto::checkAlgorithms() qWarning("Crypto::checkAlgorithms: %s", qPrintable(m_errorStr)); return false; } + if (gcry_cipher_algo_info(GCRY_CIPHER_CHACHA20, GCRYCTL_TEST_ALGO, nullptr, nullptr) != 0) { + m_errorStr = "GCRY_CIPHER_CHACHA20 not found."; + qWarning("Crypto::checkAlgorithms: %s", qPrintable(m_errorStr)); + return false; + } if (gcry_md_test_algo(GCRY_MD_SHA256) != 0) { m_errorStr = "GCRY_MD_SHA256 not found."; qWarning("Crypto::checkAlgorithms: %s", qPrintable(m_errorStr)); return false; } + if (gcry_md_test_algo(GCRY_MD_SHA512) != 0) { + m_errorStr = "GCRY_MD_SHA512 not found."; + qWarning("Crypto::checkAlgorithms: %s", qPrintable(m_errorStr)); + return false; + } return true; } bool Crypto::selfTest() { - return testSha256() && testAes256Cbc() && testAes256Ecb() && testTwofish() && testSalsa20(); + return testSha256() && testSha512() && testAes256Cbc() && testAes256Ecb() && testTwofish() && testSalsa20() && testChaCha20(); } void Crypto::raiseError(const QString& str) @@ -128,6 +138,19 @@ bool Crypto::testSha256() return true; } +bool Crypto::testSha512() +{ + QByteArray sha512Test = CryptoHash::hash("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", + CryptoHash::Sha512); + + if (sha512Test != QByteArray::fromHex("204a8fc6dda82f0a0ced7beb8e08a41657c16ef468b228a8279be331a703c33596fd15c13b1b07f9aa1d3bea57789ca031ad85c7a71dd70354ec631238ca3445")) { + raiseError("SHA-512 mismatch."); + return false; + } + + return true; +} + bool Crypto::testAes256Cbc() { QByteArray key = QByteArray::fromHex("603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"); @@ -285,3 +308,30 @@ bool Crypto::testSalsa20() return true; } + +bool Crypto::testChaCha20() { + QByteArray chacha20Key = QByteArray::fromHex("0000000000000000000000000000000000000000000000000000000000000000"); + QByteArray chacha20iv = QByteArray::fromHex("0000000000000000"); + QByteArray chacha20Plain = QByteArray::fromHex("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); + QByteArray chacha20Cipher = QByteArray::fromHex("76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586"); + bool ok; + + SymmetricCipher chacha20Stream(SymmetricCipher::ChaCha20, SymmetricCipher::Stream, + SymmetricCipher::Encrypt); + if (!chacha20Stream.init(chacha20Key, chacha20iv)) { + raiseError(chacha20Stream.errorString()); + return false; + } + + QByteArray chacha20Processed = chacha20Stream.process(chacha20Plain, &ok); + if (!ok) { + raiseError(chacha20Stream.errorString()); + return false; + } + if (chacha20Processed != chacha20Cipher) { + raiseError("ChaCha20 stream cipher mismatch."); + return false; + } + + return true; +} \ No newline at end of file diff --git a/src/crypto/Crypto.h b/src/crypto/Crypto.h index 0ce2903c6..379068eb4 100644 --- a/src/crypto/Crypto.h +++ b/src/crypto/Crypto.h @@ -35,10 +35,12 @@ private: static bool selfTest(); static void raiseError(const QString& str); static bool testSha256(); + static bool testSha512(); static bool testAes256Cbc(); static bool testAes256Ecb(); static bool testTwofish(); static bool testSalsa20(); + static bool testChaCha20(); static bool m_initalized; static QString m_errorStr; diff --git a/src/crypto/CryptoHash.cpp b/src/crypto/CryptoHash.cpp index d116451fc..a3ffb648e 100644 --- a/src/crypto/CryptoHash.cpp +++ b/src/crypto/CryptoHash.cpp @@ -29,27 +29,42 @@ public: }; CryptoHash::CryptoHash(CryptoHash::Algorithm algo) + : CryptoHash::CryptoHash(algo, false) {} + +CryptoHash::CryptoHash(CryptoHash::Algorithm algo, bool hmac) : d_ptr(new CryptoHashPrivate()) { Q_D(CryptoHash); Q_ASSERT(Crypto::initalized()); - int algoGcrypt; + int algoGcrypt = -1; + unsigned int flagsGcrypt = 0; switch (algo) { case CryptoHash::Sha256: algoGcrypt = GCRY_MD_SHA256; break; + case CryptoHash::Sha512: + algoGcrypt = GCRY_MD_SHA512; + break; + default: Q_ASSERT(false); break; } - gcry_error_t error = gcry_md_open(&d->ctx, algoGcrypt, 0); + if (hmac) { + flagsGcrypt |= GCRY_MD_FLAG_HMAC; + } + + gcry_error_t error = gcry_md_open(&d->ctx, algoGcrypt, flagsGcrypt); + if (error) { + qWarning("Gcrypt error (ctor): %s", gcry_strerror(error)); + qWarning("Gcrypt error (ctor): %s", gcry_strsource(error)); + } Q_ASSERT(error == 0); // TODO: error handling - Q_UNUSED(error); d->hashLen = gcry_md_get_algo_dlen(algoGcrypt); } @@ -74,6 +89,18 @@ void CryptoHash::addData(const QByteArray& data) gcry_md_write(d->ctx, data.constData(), data.size()); } +void CryptoHash::setKey(const QByteArray& data) +{ + Q_D(CryptoHash); + + gcry_error_t error = gcry_md_setkey(d->ctx, data.constData(), data.size()); + if (error) { + qWarning("Gcrypt error (setKey): %s", gcry_strerror(error)); + qWarning("Gcrypt error (setKey): %s", gcry_strsource(error)); + } + Q_ASSERT(error == 0); +} + void CryptoHash::reset() { Q_D(CryptoHash); @@ -96,3 +123,12 @@ QByteArray CryptoHash::hash(const QByteArray& data, CryptoHash::Algorithm algo) cryptoHash.addData(data); return cryptoHash.result(); } + +QByteArray CryptoHash::hmac(const QByteArray& data, const QByteArray& key, CryptoHash::Algorithm algo) +{ + // replace with gcry_md_hash_buffer()? + CryptoHash cryptoHash(algo, true); + cryptoHash.setKey(key); + cryptoHash.addData(data); + return cryptoHash.result(); +} diff --git a/src/crypto/CryptoHash.h b/src/crypto/CryptoHash.h index 80df056f1..cf027e0f3 100644 --- a/src/crypto/CryptoHash.h +++ b/src/crypto/CryptoHash.h @@ -27,16 +27,20 @@ class CryptoHash public: enum Algorithm { - Sha256 + Sha256, + Sha512 }; explicit CryptoHash(CryptoHash::Algorithm algo); + explicit CryptoHash(CryptoHash::Algorithm algo, bool hmac); ~CryptoHash(); void addData(const QByteArray& data); void reset(); QByteArray result() const; + void setKey(const QByteArray& data); static QByteArray hash(const QByteArray& data, CryptoHash::Algorithm algo); + static QByteArray hmac(const QByteArray& data, const QByteArray& key, Algorithm algo); private: CryptoHashPrivate* const d_ptr; diff --git a/src/crypto/SymmetricCipher.cpp b/src/crypto/SymmetricCipher.cpp index 016103b27..9bd605b43 100644 --- a/src/crypto/SymmetricCipher.cpp +++ b/src/crypto/SymmetricCipher.cpp @@ -24,6 +24,7 @@ SymmetricCipher::SymmetricCipher(SymmetricCipher::Algorithm algo, SymmetricCiphe SymmetricCipher::Direction direction) : m_backend(createBackend(algo, mode, direction)) , m_initialized(false) + , m_algo(algo) { } @@ -61,6 +62,7 @@ SymmetricCipherBackend* SymmetricCipher::createBackend(SymmetricCipher::Algorith case SymmetricCipher::Aes256: case SymmetricCipher::Twofish: case SymmetricCipher::Salsa20: + case SymmetricCipher::ChaCha20: return new SymmetricCipherGcrypt(algo, mode, direction); default: @@ -93,10 +95,14 @@ SymmetricCipher::Algorithm SymmetricCipher::cipherToAlgorithm(Uuid cipher) { if (cipher == KeePass2::CIPHER_AES) { return SymmetricCipher::Aes256; - } - else { + } else if (cipher == KeePass2::CIPHER_CHACHA20) { + return SymmetricCipher::ChaCha20; + } else if (cipher == KeePass2::CIPHER_TWOFISH) { return SymmetricCipher::Twofish; } + + qWarning("SymmetricCipher::cipherToAlgorithm: invalid Uuid %s", cipher.toByteArray().toHex().data()); + return InvalidAlgorithm; } Uuid SymmetricCipher::algorithmToCipher(SymmetricCipher::Algorithm algo) @@ -104,7 +110,42 @@ Uuid SymmetricCipher::algorithmToCipher(SymmetricCipher::Algorithm algo) switch (algo) { case SymmetricCipher::Aes256: return KeePass2::CIPHER_AES; - default: + case SymmetricCipher::ChaCha20: + return KeePass2::CIPHER_CHACHA20; + case SymmetricCipher::Twofish: return KeePass2::CIPHER_TWOFISH; + default: + qWarning("SymmetricCipher::algorithmToCipher: invalid algorithm %d", algo); + return Uuid(); } } + +int SymmetricCipher::algorithmIvSize(SymmetricCipher::Algorithm algo) { + switch (algo) { + case SymmetricCipher::ChaCha20: + return 12; + case SymmetricCipher::Aes256: + case SymmetricCipher::Twofish: + return 16; + default: + qWarning("SymmetricCipher::algorithmIvSize: invalid algorithm %d", algo); + return -1; + } +} + +SymmetricCipher::Mode SymmetricCipher::algorithmMode(SymmetricCipher::Algorithm algo) { + switch (algo) { + case SymmetricCipher::ChaCha20: + return SymmetricCipher::Stream; + case SymmetricCipher::Aes256: + case SymmetricCipher::Twofish: + return SymmetricCipher::Cbc; + default: + qWarning("SymmetricCipher::algorithmMode: invalid algorithm %d", algo); + return SymmetricCipher::InvalidMode; + } +} + +SymmetricCipher::Algorithm SymmetricCipher::algorithm() const { + return m_algo; +} diff --git a/src/crypto/SymmetricCipher.h b/src/crypto/SymmetricCipher.h index 31d10466b..981ef320e 100644 --- a/src/crypto/SymmetricCipher.h +++ b/src/crypto/SymmetricCipher.h @@ -33,7 +33,9 @@ public: { Aes256, Twofish, - Salsa20 + Salsa20, + ChaCha20, + InvalidAlgorithm = -1 }; enum Mode @@ -41,7 +43,8 @@ public: Cbc, Ctr, Ecb, - Stream + Stream, + InvalidMode = -1 }; enum Direction @@ -74,9 +77,12 @@ public: int keySize() const; int blockSize() const; QString errorString() const; + Algorithm algorithm() const; - static SymmetricCipher::Algorithm cipherToAlgorithm(Uuid cipher); - static Uuid algorithmToCipher(SymmetricCipher::Algorithm algo); + static Algorithm cipherToAlgorithm(Uuid cipher); + static Uuid algorithmToCipher(Algorithm algo); + static int algorithmIvSize(Algorithm algo); + static Mode algorithmMode(Algorithm algo); private: static SymmetricCipherBackend* createBackend(SymmetricCipher::Algorithm algo, SymmetricCipher::Mode mode, @@ -84,6 +90,7 @@ private: const QScopedPointer m_backend; bool m_initialized; + Algorithm m_algo; Q_DISABLE_COPY(SymmetricCipher) }; diff --git a/src/crypto/SymmetricCipherGcrypt.cpp b/src/crypto/SymmetricCipherGcrypt.cpp index 0b291e693..bbb80bf60 100644 --- a/src/crypto/SymmetricCipherGcrypt.cpp +++ b/src/crypto/SymmetricCipherGcrypt.cpp @@ -46,6 +46,9 @@ int SymmetricCipherGcrypt::gcryptAlgo(SymmetricCipher::Algorithm algo) case SymmetricCipher::Salsa20: return GCRY_CIPHER_SALSA20; + case SymmetricCipher::ChaCha20: + return GCRY_CIPHER_CHACHA20; + default: Q_ASSERT(false); return -1; diff --git a/src/format/KeePass2.cpp b/src/format/KeePass2.cpp index 01c15a871..8cf3df555 100644 --- a/src/format/KeePass2.cpp +++ b/src/format/KeePass2.cpp @@ -22,6 +22,7 @@ const Uuid KeePass2::CIPHER_AES = Uuid(QByteArray::fromHex("31c1f2e6bf714350be5805216afc5aff")); const Uuid KeePass2::CIPHER_TWOFISH = Uuid(QByteArray::fromHex("ad68f29f576f4bb9a36ad47af965346c")); +const Uuid KeePass2::CIPHER_CHACHA20 = Uuid(QByteArray::fromHex("D6038A2B8B6F4CB5A524339A31DBB59A")); const Uuid KeePass2::KDF_AES = Uuid(QByteArray::fromHex("C9D9F39A628A4460BF740D08C18A4FEA")); @@ -30,6 +31,7 @@ const QByteArray KeePass2::INNER_STREAM_SALSA20_IV("\xE8\x30\x09\x4B\x97\x20\x5D const QList KeePass2::CIPHERS { KeePass2::UuidNamePair(KeePass2::CIPHER_AES, "AES: 256-bit"), KeePass2::UuidNamePair(KeePass2::CIPHER_TWOFISH, "Twofish: 256-bit"), + KeePass2::UuidNamePair(KeePass2::CIPHER_CHACHA20, "ChaCha20: 256-bit") }; const QList KeePass2::KDFS { KeePass2::UuidNamePair(KeePass2::KDF_AES, "AES-KDF"), @@ -53,6 +55,20 @@ Uuid KeePass2::kdfToUuid(const Kdf& kdf) } } +KeePass2::ProtectedStreamAlgo KeePass2::idToProtectedStreamAlgo(quint32 id) +{ + switch (id) { + case static_cast(KeePass2::ArcFourVariant): + return KeePass2::ArcFourVariant; + case static_cast(KeePass2::Salsa20): + return KeePass2::Salsa20; + case static_cast(KeePass2::ChaCha20): + return KeePass2::ChaCha20; + default: + return KeePass2::InvalidProtectedStreamAlgo; + } +} + KeePass2::UuidNamePair::UuidNamePair(const Uuid& uuid, const QString& name) : m_uuid(uuid) , m_name(name) diff --git a/src/format/KeePass2.h b/src/format/KeePass2.h index b3c7ee559..6356e15da 100644 --- a/src/format/KeePass2.h +++ b/src/format/KeePass2.h @@ -37,6 +37,7 @@ namespace KeePass2 extern const Uuid CIPHER_AES; extern const Uuid CIPHER_TWOFISH; + extern const Uuid CIPHER_CHACHA20; extern const Uuid KDF_AES; @@ -75,11 +76,14 @@ namespace KeePass2 enum ProtectedStreamAlgo { ArcFourVariant = 1, - Salsa20 = 2 + Salsa20 = 2, + ChaCha20 = 3, + InvalidProtectedStreamAlgo = -1 }; Kdf* uuidToKdf(const Uuid& uuid); Uuid kdfToUuid(const Kdf& kdf); + ProtectedStreamAlgo idToProtectedStreamAlgo(quint32 id); } #endif // KEEPASSX_KEEPASS2_H diff --git a/src/format/KeePass2RandomStream.cpp b/src/format/KeePass2RandomStream.cpp index 1944e5d5b..5f80fc151 100644 --- a/src/format/KeePass2RandomStream.cpp +++ b/src/format/KeePass2RandomStream.cpp @@ -20,16 +20,27 @@ #include "crypto/CryptoHash.h" #include "format/KeePass2.h" -KeePass2RandomStream::KeePass2RandomStream() - : m_cipher(SymmetricCipher::Salsa20, SymmetricCipher::Stream, SymmetricCipher::Encrypt) +KeePass2RandomStream::KeePass2RandomStream(KeePass2::ProtectedStreamAlgo algo) + : m_cipher(mapAlgo(algo), SymmetricCipher::Stream, SymmetricCipher::Encrypt) , m_offset(0) { } bool KeePass2RandomStream::init(const QByteArray& key) { - return m_cipher.init(CryptoHash::hash(key, CryptoHash::Sha256), - KeePass2::INNER_STREAM_SALSA20_IV); + switch (m_cipher.algorithm()) { + case SymmetricCipher::Salsa20: + return m_cipher.init(CryptoHash::hash(key, CryptoHash::Sha256), + KeePass2::INNER_STREAM_SALSA20_IV); + case SymmetricCipher::ChaCha20: { + QByteArray keyIv = CryptoHash::hash(key, CryptoHash::Sha512); + return m_cipher.init(keyIv.left(32), keyIv.mid(32, 12)); + } + default: + qWarning("Invalid stream algorithm (%d)", m_cipher.algorithm()); + break; + } + return false; } QByteArray KeePass2RandomStream::randomBytes(int size, bool* ok) @@ -109,3 +120,14 @@ bool KeePass2RandomStream::loadBlock() return true; } + +SymmetricCipher::Algorithm KeePass2RandomStream::mapAlgo(KeePass2::ProtectedStreamAlgo algo) { + switch (algo) { + case KeePass2::ChaCha20: + return SymmetricCipher::ChaCha20; + case KeePass2::Salsa20: + return SymmetricCipher::Salsa20; + default: + return SymmetricCipher::InvalidAlgorithm; + } +} \ No newline at end of file diff --git a/src/format/KeePass2RandomStream.h b/src/format/KeePass2RandomStream.h index 584d738b3..1e341bacc 100644 --- a/src/format/KeePass2RandomStream.h +++ b/src/format/KeePass2RandomStream.h @@ -21,11 +21,13 @@ #include #include "crypto/SymmetricCipher.h" +#include "KeePass2.h" class KeePass2RandomStream { public: - KeePass2RandomStream(); + KeePass2RandomStream(KeePass2::ProtectedStreamAlgo algo); + bool init(const QByteArray& key); QByteArray randomBytes(int size, bool* ok); QByteArray process(const QByteArray& data, bool* ok); @@ -38,6 +40,8 @@ private: SymmetricCipher m_cipher; QByteArray m_buffer; int m_offset; + + static SymmetricCipher::Algorithm mapAlgo(KeePass2::ProtectedStreamAlgo algo); }; #endif // KEEPASSX_KEEPASS2RANDOMSTREAM_H diff --git a/src/format/KeePass2Reader.cpp b/src/format/KeePass2Reader.cpp index 60ebfdc82..f42e7b9bb 100644 --- a/src/format/KeePass2Reader.cpp +++ b/src/format/KeePass2Reader.cpp @@ -41,6 +41,7 @@ KeePass2Reader::KeePass2Reader() , m_headerEnd(false) , m_saveXml(false) , m_db(nullptr) + , m_irsAlgo(KeePass2::InvalidProtectedStreamAlgo) { } @@ -164,7 +165,7 @@ Database* KeePass2Reader::readDatabase(QIODevice* device, const CompositeKey& ke xmlDevice = ioCompressor.data(); } - KeePass2RandomStream randomStream; + KeePass2RandomStream randomStream(m_irsAlgo); if (!randomStream.init(m_protectedStreamKey)) { raiseError(randomStream.errorString()); return nullptr; @@ -447,9 +448,14 @@ void KeePass2Reader::setInnerRandomStreamID(const QByteArray& data) } else { quint32 id = Endian::bytesToUInt32(data, KeePass2::BYTEORDER); - - if (id != KeePass2::Salsa20) { + m_irsAlgo = KeePass2::idToProtectedStreamAlgo(id); + if (m_irsAlgo == KeePass2::ArcFourVariant || m_irsAlgo == KeePass2::InvalidProtectedStreamAlgo) { raiseError("Unsupported random stream algorithm"); } } } + +KeePass2::ProtectedStreamAlgo KeePass2Reader::protectedStreamAlgo() const +{ + return m_irsAlgo; +} diff --git a/src/format/KeePass2Reader.h b/src/format/KeePass2Reader.h index f82b4464a..e9a947284 100644 --- a/src/format/KeePass2Reader.h +++ b/src/format/KeePass2Reader.h @@ -21,6 +21,7 @@ #include #include "keys/CompositeKey.h" +#include "format/KeePass2.h" class Database; class QIODevice; @@ -38,6 +39,7 @@ public: void setSaveXml(bool save); QByteArray xmlData(); QByteArray streamKey(); + KeePass2::ProtectedStreamAlgo protectedStreamAlgo() const; private: void raiseError(const QString& errorMessage); @@ -67,6 +69,7 @@ private: QByteArray m_encryptionIV; QByteArray m_streamStartBytes; QByteArray m_protectedStreamKey; + KeePass2::ProtectedStreamAlgo m_irsAlgo; }; #endif // KEEPASSX_KEEPASS2READER_H diff --git a/src/format/KeePass2Repair.cpp b/src/format/KeePass2Repair.cpp index 8d18457d4..5f2732e2a 100644 --- a/src/format/KeePass2Repair.cpp +++ b/src/format/KeePass2Repair.cpp @@ -71,7 +71,7 @@ KeePass2Repair::RepairOutcome KeePass2Repair::repairDatabase(QIODevice* device, return qMakePair(RepairFailed, nullptr); } - KeePass2RandomStream randomStream; + KeePass2RandomStream randomStream(reader.protectedStreamAlgo()); randomStream.init(reader.streamKey()); KeePass2XmlReader xmlReader; QBuffer buffer(&xmlData); diff --git a/src/format/KeePass2Writer.cpp b/src/format/KeePass2Writer.cpp index 0d78dc9a8..6dc89c36d 100644 --- a/src/format/KeePass2Writer.cpp +++ b/src/format/KeePass2Writer.cpp @@ -131,7 +131,7 @@ void KeePass2Writer::writeDatabase(QIODevice* device, Database* db) m_device = ioCompressor.data(); } - KeePass2RandomStream randomStream; + KeePass2RandomStream randomStream(KeePass2::Salsa20); if (!randomStream.init(protectedStreamKey)) { raiseError(randomStream.errorString()); return; diff --git a/src/streams/HmacBlockStream.cpp b/src/streams/HmacBlockStream.cpp new file mode 100644 index 000000000..c01c973b1 --- /dev/null +++ b/src/streams/HmacBlockStream.cpp @@ -0,0 +1,267 @@ +/* +* Copyright (C) 2017 KeePassXC Team +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 2 or (at your option) +* version 3 of the License. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +#include "HmacBlockStream.h" + +#include "core/Endian.h" +#include "crypto/CryptoHash.h" + +const QSysInfo::Endian HmacBlockStream::ByteOrder = QSysInfo::LittleEndian; + +HmacBlockStream::HmacBlockStream(QIODevice* baseDevice, QByteArray key) + : LayeredStream(baseDevice) + , m_blockSize(1024*1024) + , m_key(key) +{ + init(); +} + +HmacBlockStream::HmacBlockStream(QIODevice* baseDevice, QByteArray key, qint32 blockSize) + : LayeredStream(baseDevice) + , m_blockSize(blockSize) + , m_key(key) +{ + init(); +} + +HmacBlockStream::~HmacBlockStream() +{ + close(); +} + +void HmacBlockStream::init() +{ + m_buffer.clear(); + m_bufferPos = 0; + m_blockIndex = 0; + m_eof = false; + m_error = false; +} + +bool HmacBlockStream::reset() +{ + // Write final block(s) only if device is writable and we haven't + // already written a final block. + if (isWritable() && (!m_buffer.isEmpty() || m_blockIndex != 0)) { + if (!m_buffer.isEmpty()) { + if (!writeHashedBlock()) { + return false; + } + } + + // write empty final block + if (!writeHashedBlock()) { + return false; + } + } + + init(); + + return true; +} + +void HmacBlockStream::close() +{ + // Write final block(s) only if device is writable and we haven't + // already written a final block. + if (isWritable() && (!m_buffer.isEmpty() || m_blockIndex != 0)) { + if (!m_buffer.isEmpty()) { + writeHashedBlock(); + } + + // write empty final block + writeHashedBlock(); + } + + LayeredStream::close(); +} + +qint64 HmacBlockStream::readData(char* data, qint64 maxSize) +{ + if (m_error) { + return -1; + } + else if (m_eof) { + return 0; + } + + qint64 bytesRemaining = maxSize; + qint64 offset = 0; + + while (bytesRemaining > 0) { + if (m_bufferPos == m_buffer.size()) { + if (!readHashedBlock()) { + if (m_error) { + return -1; + } + else { + return maxSize - bytesRemaining; + } + } + } + + int bytesToCopy = qMin(bytesRemaining, static_cast(m_buffer.size() - m_bufferPos)); + + memcpy(data + offset, m_buffer.constData() + m_bufferPos, bytesToCopy); + + offset += bytesToCopy; + m_bufferPos += bytesToCopy; + bytesRemaining -= bytesToCopy; + } + + return maxSize; +} + +bool HmacBlockStream::readHashedBlock() +{ + if (m_eof) { + return false; + } + QByteArray hmac = m_baseDevice->read(32); + if (hmac.size() != 32) { + m_error = true; + setErrorString("Invalid HMAC size."); + return false; + } + + QByteArray blockSizeBytes = m_baseDevice->read(4); + if (blockSizeBytes.size() != 4) { + m_error = true; + setErrorString("Invalid block size size."); + return false; + } + qint32 blockSize = Endian::bytesToInt32(blockSizeBytes, ByteOrder); + if (blockSize < 0) { + m_error = true; + setErrorString("Invalid block size."); + return false; + } + + m_buffer = m_baseDevice->read(blockSize); + if (m_buffer.size() != blockSize) { + m_error = true; + setErrorString("Block too short."); + return false; + } + + CryptoHash hasher(CryptoHash::Sha256, true); + hasher.setKey(getCurrentHmacKey()); + hasher.addData(Endian::uint64ToBytes(m_blockIndex, ByteOrder)); + hasher.addData(blockSizeBytes); + hasher.addData(m_buffer); + + if (hmac != hasher.result()) { + m_error = true; + setErrorString("Mismatch between hash and data."); + return false; + } + + m_bufferPos = 0; + m_blockIndex++; + + if (blockSize == 0) { + m_eof = true; + return false; + } + + return true; +} + +qint64 HmacBlockStream::writeData(const char* data, qint64 maxSize) +{ + Q_ASSERT(maxSize >= 0); + + if (m_error) { + return 0; + } + + qint64 bytesRemaining = maxSize; + qint64 offset = 0; + + while (bytesRemaining > 0) { + int bytesToCopy = qMin(bytesRemaining, static_cast(m_blockSize - m_buffer.size())); + + m_buffer.append(data + offset, bytesToCopy); + + offset += bytesToCopy; + bytesRemaining -= bytesToCopy; + + if (m_buffer.size() == m_blockSize) { + if (!writeHashedBlock()) { + if (m_error) { + return -1; + } + else { + return maxSize - bytesRemaining; + } + } + } + } + + return maxSize; +} + +bool HmacBlockStream::writeHashedBlock() +{ + CryptoHash hasher(CryptoHash::Sha256, true); + hasher.setKey(getCurrentHmacKey()); + hasher.addData(Endian::uint64ToBytes(m_blockIndex, ByteOrder)); + hasher.addData(Endian::int32ToBytes(m_buffer.size(), ByteOrder)); + hasher.addData(m_buffer); + QByteArray hash = hasher.result(); + + if (m_baseDevice->write(hash) != hash.size()) { + m_error = true; + setErrorString(m_baseDevice->errorString()); + return false; + } + + if (!Endian::writeInt32(m_buffer.size(), m_baseDevice, ByteOrder)) { + m_error = true; + setErrorString(m_baseDevice->errorString()); + return false; + } + + if (!m_buffer.isEmpty()) { + if (m_baseDevice->write(m_buffer) != m_buffer.size()) { + m_error = true; + setErrorString(m_baseDevice->errorString()); + return false; + } + + m_buffer.clear(); + } + m_blockIndex++; + return true; +} + +QByteArray HmacBlockStream::getCurrentHmacKey() const { + return getHmacKey(m_blockIndex, m_key); +} + +QByteArray HmacBlockStream::getHmacKey(quint64 blockIndex, QByteArray key) { + Q_ASSERT(key.size() == 64); + QByteArray indexBytes = Endian::uint64ToBytes(blockIndex, ByteOrder); + CryptoHash hasher(CryptoHash::Sha512); + hasher.addData(indexBytes); + hasher.addData(key); + return hasher.result(); +} + +bool HmacBlockStream::atEnd() const { + return m_eof; +} diff --git a/src/streams/HmacBlockStream.h b/src/streams/HmacBlockStream.h new file mode 100644 index 000000000..eecd8fe92 --- /dev/null +++ b/src/streams/HmacBlockStream.h @@ -0,0 +1,61 @@ +/* +* Copyright (C) 2017 KeePassXC Team +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 2 or (at your option) +* version 3 of the License. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +#ifndef KEEPASSX_HMACBLOCKSTREAM_H +#define KEEPASSX_HMACBLOCKSTREAM_H + +#include + +#include "streams/LayeredStream.h" + +class HmacBlockStream : public LayeredStream +{ + Q_OBJECT + +public: + explicit HmacBlockStream(QIODevice* baseDevice, QByteArray key); + HmacBlockStream(QIODevice* baseDevice, QByteArray key, qint32 blockSize); + ~HmacBlockStream(); + + bool reset() override; + void close() override; + + static QByteArray getHmacKey(quint64 blockIndex, QByteArray key); + + bool atEnd() const override; + +protected: + qint64 readData(char* data, qint64 maxSize) override; + qint64 writeData(const char* data, qint64 maxSize) override; + +private: + void init(); + bool readHashedBlock(); + bool writeHashedBlock(); + QByteArray getCurrentHmacKey() const; + + static const QSysInfo::Endian ByteOrder; + qint32 m_blockSize; + QByteArray m_buffer; + QByteArray m_key; + int m_bufferPos; + quint64 m_blockIndex; + bool m_eof; + bool m_error; +}; + +#endif // KEEPASSX_HMACBLOCKSTREAM_H diff --git a/tests/TestCryptoHash.cpp b/tests/TestCryptoHash.cpp index c166f5595..469ce8192 100644 --- a/tests/TestCryptoHash.cpp +++ b/tests/TestCryptoHash.cpp @@ -44,4 +44,17 @@ void TestCryptoHash::test() cryptoHash3.addData(QString("ssX").toLatin1()); QCOMPARE(cryptoHash3.result(), QByteArray::fromHex("0b56e5f65263e747af4a833bd7dd7ad26a64d7a4de7c68e52364893dca0766b4")); + + CryptoHash cryptoHash2(CryptoHash::Sha512); + QCOMPARE(cryptoHash2.result(), + QByteArray::fromHex("cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e")); + + QByteArray result3 = CryptoHash::hash(source2, CryptoHash::Sha512); + QCOMPARE(result3, QByteArray::fromHex("0d41b612584ed39ff72944c29494573e40f4bb95283455fae2e0be1e3565aa9f48057d59e6ffd777970e282871c25a549a2763e5b724794f312c97021c42f91d")); + + CryptoHash cryptoHash4(CryptoHash::Sha512); + cryptoHash4.addData(QString("KeePa").toLatin1()); + cryptoHash4.addData(QString("ssX").toLatin1()); + QCOMPARE(cryptoHash4.result(), + QByteArray::fromHex("0d41b612584ed39ff72944c29494573e40f4bb95283455fae2e0be1e3565aa9f48057d59e6ffd777970e282871c25a549a2763e5b724794f312c97021c42f91d")); } diff --git a/tests/TestKeePass2RandomStream.cpp b/tests/TestKeePass2RandomStream.cpp index 03dfbe507..bef7af540 100644 --- a/tests/TestKeePass2RandomStream.cpp +++ b/tests/TestKeePass2RandomStream.cpp @@ -58,7 +58,7 @@ void TestKeePass2RandomStream::test() } - KeePass2RandomStream randomStream; + KeePass2RandomStream randomStream(KeePass2::Salsa20); bool ok; QVERIFY(randomStream.init(key)); QByteArray randomStreamData; diff --git a/tests/TestSymmetricCipher.cpp b/tests/TestSymmetricCipher.cpp index c1e947063..bfa3c3db8 100644 --- a/tests/TestSymmetricCipher.cpp +++ b/tests/TestSymmetricCipher.cpp @@ -342,6 +342,56 @@ void TestSymmetricCipher::testSalsa20() QCOMPARE(cipherTextB.mid(448, 64), expectedCipherText4); } +void TestSymmetricCipher::testChaCha20() +{ + // https://tools.ietf.org/html/draft-agl-tls-chacha20poly1305-04#section-7 + bool ok; + + { + QByteArray key = QByteArray::fromHex("0000000000000000000000000000000000000000000000000000000000000000"); + QByteArray iv = QByteArray::fromHex("0000000000000000"); + SymmetricCipher cipher(SymmetricCipher::ChaCha20, SymmetricCipher::Stream, SymmetricCipher::Encrypt); + QVERIFY(cipher.init(key, iv)); + QCOMPARE(cipher.process(QByteArray(64, 0), &ok), + QByteArray::fromHex( + "76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586")); + QVERIFY(ok); + } + + { + QByteArray key = QByteArray::fromHex("0000000000000000000000000000000000000000000000000000000000000001"); + QByteArray iv = QByteArray::fromHex("0000000000000000"); + SymmetricCipher cipher(SymmetricCipher::ChaCha20, SymmetricCipher::Stream, SymmetricCipher::Encrypt); + QVERIFY(cipher.init(key, iv)); + QCOMPARE(cipher.process(QByteArray(64, 0), &ok), + QByteArray::fromHex( + "4540f05a9f1fb296d7736e7b208e3c96eb4fe1834688d2604f450952ed432d41bbe2a0b6ea7566d2a5d1e7e20d42af2c53d792b1c43fea817e9ad275ae546963")); + QVERIFY(ok); + } + + { + QByteArray key = QByteArray::fromHex("0000000000000000000000000000000000000000000000000000000000000000"); + QByteArray iv = QByteArray::fromHex("0000000000000001"); + SymmetricCipher cipher(SymmetricCipher::ChaCha20, SymmetricCipher::Stream, SymmetricCipher::Encrypt); + QVERIFY(cipher.init(key, iv)); + QCOMPARE(cipher.process(QByteArray(60, 0), &ok), + QByteArray::fromHex( + "de9cba7bf3d69ef5e786dc63973f653a0b49e015adbff7134fcb7df137821031e85a050278a7084527214f73efc7fa5b5277062eb7a0433e445f41e3")); + QVERIFY(ok); + } + + { + QByteArray key = QByteArray::fromHex("0000000000000000000000000000000000000000000000000000000000000000"); + QByteArray iv = QByteArray::fromHex("0100000000000000"); + SymmetricCipher cipher(SymmetricCipher::ChaCha20, SymmetricCipher::Stream, SymmetricCipher::Encrypt); + QVERIFY(cipher.init(key, iv)); + QCOMPARE(cipher.process(QByteArray(64, 0), &ok), + QByteArray::fromHex( + "ef3fdfd6c61578fbf5cf35bd3dd33b8009631634d21e42ac33960bd138e50d32111e4caf237ee53ca8ad6426194a88545ddc497a0b466e7d6bbdb0041b2f586b")); + QVERIFY(ok); + } +} + void TestSymmetricCipher::testPadding() { QByteArray key = QByteArray::fromHex("603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"); diff --git a/tests/TestSymmetricCipher.h b/tests/TestSymmetricCipher.h index cad13841a..40e3b49cf 100644 --- a/tests/TestSymmetricCipher.h +++ b/tests/TestSymmetricCipher.h @@ -34,6 +34,7 @@ private slots: void testTwofish256CbcEncryption(); void testTwofish256CbcDecryption(); void testSalsa20(); + void testChaCha20(); void testPadding(); void testStreamReset(); };