From 448c5b1a371c8cc30e4444d494e8702f7579ed95 Mon Sep 17 00:00:00 2001 From: Martchus Date: Tue, 18 Dec 2018 23:17:19 +0100 Subject: [PATCH] Improve reading/writing overall file * Allow to hash password N times using SHA-256 * Use flags instead of bool parameter * Expose extended header * Fix bugs when reading/writing extended headers * Store password as std::string Reading files written with previous versions is still possible. If new features are not used it is also possible to read new files with previous versions. --- CMakeLists.txt | 7 +- io/passwordfile.cpp | 186 +++++++++++++++++++++++++++-------- io/passwordfile.h | 69 +++++++++++-- tests/opensslutils.cpp | 64 ++++++++++++ tests/passwordfiletests.cpp | 100 ++++++++++++++----- util/openssl.cpp | 38 +++++++ util/openssl.h | 12 ++- util/opensslrandomdevice.cpp | 2 +- util/opensslrandomdevice.h | 2 +- 9 files changed, 404 insertions(+), 76 deletions(-) create mode 100644 tests/opensslutils.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 367c8c2..4f59d93 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,7 @@ set(TEST_SRC_FILES tests/entrytests.cpp tests/fieldtests.cpp tests/opensslrandomdevice.cpp + tests/opensslutils.cpp ) set(DOC_FILES @@ -42,9 +43,9 @@ set(META_APP_NAME "Passwordfile library") set(META_APP_AUTHOR "Martchus") set(META_APP_URL "https://github.com/${META_APP_AUTHOR}/${META_PROJECT_NAME}") set(META_APP_DESCRIPTION "C++ library to read/write passwords from/to encrypted files") -set(META_VERSION_MAJOR 3) -set(META_VERSION_MINOR 2) -set(META_VERSION_PATCH 1) +set(META_VERSION_MAJOR 4) +set(META_VERSION_MINOR 0) +set(META_VERSION_PATCH 0) set(META_PUBLIC_SHARED_LIB_DEPENDS c++utilities) set(META_PUBLIC_STATIC_LIB_DEPENDS c++utilities_static) diff --git a/io/passwordfile.cpp b/io/passwordfile.cpp index 8d5574b..9f287ef 100644 --- a/io/passwordfile.cpp +++ b/io/passwordfile.cpp @@ -3,6 +3,9 @@ #include "./entry.h" #include "./parsingexception.h" +#include "../util/openssl.h" +#include "../util/opensslrandomdevice.h" + #include #include #include @@ -63,6 +66,7 @@ PasswordFile::PasswordFile(const string &path, const string &password) */ PasswordFile::PasswordFile(const PasswordFile &other) : m_path(other.m_path) + , m_password(other.m_password) , m_rootEntry(other.m_rootEntry ? make_unique(*other.m_rootEntry) : nullptr) , m_extendedHeader(other.m_extendedHeader) , m_encryptedExtendedHeader(other.m_encryptedExtendedHeader) @@ -70,7 +74,6 @@ PasswordFile::PasswordFile(const PasswordFile &other) , m_fwriter(BinaryWriter(&m_file)) { m_file.exceptions(ios_base::failbit | ios_base::badbit); - memcpy(m_password, other.m_password, 32); } /*! @@ -78,6 +81,7 @@ PasswordFile::PasswordFile(const PasswordFile &other) */ PasswordFile::PasswordFile(PasswordFile &&other) : m_path(move(other.m_path)) + , m_password(move(other.m_password)) , m_rootEntry(move(other.m_rootEntry)) , m_extendedHeader(move(other.m_extendedHeader)) , m_encryptedExtendedHeader(move(other.m_encryptedExtendedHeader)) @@ -85,7 +89,6 @@ PasswordFile::PasswordFile(PasswordFile &&other) , m_freader(BinaryReader(&m_file)) , m_fwriter(BinaryWriter(&m_file)) { - memcpy(m_password, other.m_password, 32); } /*! @@ -99,13 +102,14 @@ PasswordFile::~PasswordFile() * \brief Opens the file. Does not load the contents (see load()). * \throws Throws ios_base::failure when an IO error occurs. */ -void PasswordFile::open(bool readOnly) +void PasswordFile::open(PasswordFileOpenFlags options) { close(); if (m_path.empty()) { throwIoFailure("Unable to open file because path is emtpy."); } - m_file.open(m_path, readOnly ? ios_base::in | ios_base::binary : ios_base::in | ios_base::out | ios_base::binary); + m_file.open( + m_path, options & PasswordFileOpenFlags::ReadOnly ? ios_base::in | ios_base::binary : ios_base::in | ios_base::out | ios_base::binary); opened(); } @@ -169,11 +173,11 @@ void PasswordFile::load() // check version and flags (used in version 0x3 only) const auto version = m_freader.readUInt32LE(); - if (version != 0x0U && version != 0x1U && version != 0x2U && version != 0x3U && version != 0x4U && version != 0x5U) { - throw ParsingException("Version is unknown."); + if (version > 0x6U) { + throw ParsingException(argsToString("Version \"", version, "\" is unknown. Only versions 0 to 6 are supported.")); } bool decrypterUsed, ivUsed, compressionUsed; - if (version == 0x3U) { + if (version >= 0x3U) { const auto flags = m_freader.readByte(); decrypterUsed = flags & 0x80; ivUsed = flags & 0x40; @@ -190,6 +194,8 @@ void PasswordFile::load() if (version >= 0x4U) { uint16 extendedHeaderSize = m_freader.readUInt16BE(); m_extendedHeader = m_freader.readString(extendedHeaderSize); + } else { + m_extendedHeader.clear(); } // get length @@ -198,7 +204,17 @@ void PasswordFile::load() auto remainingSize = static_cast(m_file.tellg()) - headerSize; m_file.seekg(static_cast(headerSize), ios_base::beg); - // read file + // read hash count + uint32_t hashCount = 0U; + if (version >= 0x6U && decrypterUsed) { + if (remainingSize < 4) { + throw ParsingException("Hash count truncated."); + } + hashCount = m_freader.readUInt32BE(); + remainingSize -= 4; + } + + // read IV unsigned char iv[aes256cbcIvSize] = { 0 }; if (decrypterUsed && ivUsed) { if (remainingSize < aes256cbcIvSize) { @@ -220,12 +236,23 @@ void PasswordFile::load() throw CryptoException("Size exceeds limit."); } + // prepare password + Util::OpenSsl::Sha256Sum password; + if (hashCount) { + // hash the password as often as it has been hashed when writing the file + password = Util::OpenSsl::computeSha256Sum(reinterpret_cast(m_password.data()), m_password.size()); + for (uint32_t i = 1; i < hashCount; ++i) { + password = Util::OpenSsl::computeSha256Sum(password.data, Util::OpenSsl::Sha256Sum::size); + } + } else { + m_password.copy(reinterpret_cast(password.data), Util::OpenSsl::Sha256Sum::size); + } + // initiate ctx, decrypt data EVP_CIPHER_CTX *ctx = nullptr; decryptedData.resize(remainingSize + 32); int outlen1, outlen2; - if ((ctx = EVP_CIPHER_CTX_new()) == nullptr - || EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, reinterpret_cast(m_password), iv) != 1 + if ((ctx = EVP_CIPHER_CTX_new()) == nullptr || EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, password.data, iv) != 1 || EVP_DecryptUpdate(ctx, reinterpret_cast(decryptedData.data()), &outlen1, reinterpret_cast(rawData.data()), static_cast(remainingSize)) != 1 @@ -297,8 +324,11 @@ void PasswordFile::load() decryptedStream.rdbuf()->pubsetbuf(decryptedData.data(), static_cast(remainingSize)); #endif if (version >= 0x5u) { - const auto extendedHeaderSize = m_freader.readUInt16BE(); - m_encryptedExtendedHeader = m_freader.readString(extendedHeaderSize); + BinaryReader reader(&decryptedStream); + const auto extendedHeaderSize = reader.readUInt16BE(); + m_encryptedExtendedHeader = reader.readString(extendedHeaderSize); + } else { + m_encryptedExtendedHeader.clear(); } m_rootEntry.reset(new NodeEntry(decryptedStream)); } catch (...) { @@ -310,15 +340,29 @@ void PasswordFile::load() } } +/*! + * \brief Returns the minimum file version required to write the current instance with the specified \a options. + */ +uint32 PasswordFile::mininumVersion(PasswordFileSaveFlags options) const +{ + if (options & PasswordFileSaveFlags::PasswordHashing) { + return 0x6U; // password hashing requires at least version 6 + } else if (!m_encryptedExtendedHeader.empty()) { + return 0x5U; // encrypted extended header requires at least version 5 + } else if (!m_extendedHeader.empty()) { + return 0x4U; // regular extended header requires at least version 4 + } + return 0x3U; // lowest supported version by the serializer +} + /*! * \brief Writes the current root entry to the file under path() replacing its previous contents. - * \param useEncryption Specifies whether encryption should be used. - * \param useCompression Specifies whether compression should be used. + * \param options Specify the features (like encryption and compression) to be used. * \throws Throws ios_base::failure when an IO error occurs. * \throws Throws runtime_error when no root entry is present or a compression error occurs. * \throws Throws Io::CryptoException when an encryption error occurs. */ -void PasswordFile::save(bool useEncryption, bool useCompression) +void PasswordFile::save(PasswordFileSaveFlags options) { if (!m_rootEntry) { throw runtime_error("Root entry has not been created."); @@ -333,19 +377,18 @@ void PasswordFile::save(bool useEncryption, bool useCompression) m_file.open(m_path, ios_base::in | ios_base::out | ios_base::trunc | ios_base::binary); } - write(useEncryption, useCompression); + write(options); m_file.flush(); } /*! * \brief Writes the current root entry to the file which is assumed to be opened and writeable. - * \param useEncryption Specifies whether encryption should be used. - * \param useCompression Specifies whether compression should be used. + * \param options Specify the features (like encryption and compression) to be used. * \throws Throws ios_base::failure when an IO error occurs. * \throws Throws runtime_error when no root entry is present or a compression error occurs. * \throws Throws Io::CryptoException when an encryption error occurs. */ -void PasswordFile::write(bool useEncryption, bool useCompression) +void PasswordFile::write(PasswordFileSaveFlags options) { if (!m_rootEntry) { throw runtime_error("Root entry has not been created."); @@ -354,19 +397,25 @@ void PasswordFile::write(bool useEncryption, bool useCompression) // write magic number m_fwriter.writeUInt32LE(0x7770616DU); - // write version, extended header requires version 4, encrypted extended header required version 5 - m_fwriter.writeUInt32LE(m_extendedHeader.empty() && m_encryptedExtendedHeader.empty() ? 0x3U : (m_encryptedExtendedHeader.empty() ? 0x4U : 0x5U)); + // write version + const auto version = mininumVersion(options); + m_fwriter.writeUInt32LE(version); + + // write flags byte flags = 0x00; - if (useEncryption) { + if (options & PasswordFileSaveFlags::Encryption) { flags |= 0x80 | 0x40; } - if (useCompression) { + if (options & PasswordFileSaveFlags::Compression) { flags |= 0x20; } m_fwriter.writeByte(flags); // write extened header - if (!m_extendedHeader.empty()) { + if (version >= 0x4U) { + if (m_extendedHeader.size() > numeric_limits::max()) { + throw runtime_error("Extended header exceeds maximum size."); + } m_fwriter.writeUInt16BE(static_cast(m_extendedHeader.size())); m_fwriter.writeString(m_extendedHeader); } @@ -376,9 +425,13 @@ void PasswordFile::write(bool useEncryption, bool useCompression) buffstr.exceptions(ios_base::failbit | ios_base::badbit); // write encrypted extened header - if (!m_encryptedExtendedHeader.empty()) { - m_fwriter.writeUInt16BE(static_cast(m_encryptedExtendedHeader.size())); - m_fwriter.writeString(m_encryptedExtendedHeader); + if (version >= 0x5U) { + if (m_encryptedExtendedHeader.size() > numeric_limits::max()) { + throw runtime_error("Encrypted extended header exceeds maximum size."); + } + BinaryWriter buffstrWriter(&buffstr); + buffstrWriter.writeUInt16BE(static_cast(m_encryptedExtendedHeader.size())); + buffstrWriter.writeString(m_encryptedExtendedHeader); } m_rootEntry->make(buffstr); buffstr.seekp(0, ios_base::end); @@ -391,7 +444,7 @@ void PasswordFile::write(bool useEncryption, bool useCompression) vector encryptedData; // compress data - if (useCompression) { + if (options & PasswordFileSaveFlags::Compression) { uLongf compressedSize = compressBound(size); encryptedData.resize(8 + compressedSize); ConversionUtilities::LE::getBytes(static_cast(size), encryptedData.data()); @@ -412,19 +465,32 @@ void PasswordFile::write(bool useEncryption, bool useCompression) } // write data without encryption - if (!useEncryption) { + if (!(options & PasswordFileSaveFlags::Encryption)) { // write data to file m_file.write(decryptedData.data(), static_cast(size)); return; } + // prepare password + Util::OpenSsl::Sha256Sum password; + const uint32_t hashCount = (options & PasswordFileSaveFlags::PasswordHashing) ? Util::OpenSsl::generateRandomNumber(1, 100) : 0u; + if (hashCount) { + // hash password a few times + password = Util::OpenSsl::computeSha256Sum(reinterpret_cast(m_password.data()), m_password.size()); + for (uint32_t i = 1; i < hashCount; ++i) { + password = Util::OpenSsl::computeSha256Sum(password.data, Util::OpenSsl::Sha256Sum::size); + } + } else { + m_password.copy(reinterpret_cast(password.data), Util::OpenSsl::Sha256Sum::size); + } + // initiate ctx, encrypt data EVP_CIPHER_CTX *ctx = nullptr; unsigned char iv[aes256cbcIvSize]; int outlen1, outlen2; encryptedData.resize(size + 32); if (RAND_bytes(iv, aes256cbcIvSize) != 1 || (ctx = EVP_CIPHER_CTX_new()) == nullptr - || EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, reinterpret_cast(m_password), iv) != 1 + || EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, password.data, iv) != 1 || EVP_EncryptUpdate(ctx, reinterpret_cast(encryptedData.data()), &outlen1, reinterpret_cast(decryptedData.data()), static_cast(size)) != 1 @@ -450,6 +516,9 @@ void PasswordFile::write(bool useEncryption, bool useCompression) } // write encrypted data to file + if (version >= 0x6U) { + m_fwriter.writeUInt32BE(hashCount); + } m_file.write(reinterpret_cast(iv), aes256cbcIvSize); m_file.write(encryptedData.data(), static_cast(outlen1 + outlen2)); } @@ -610,18 +679,25 @@ void PasswordFile::clearPath() /*! * \brief Returns the current password. It will be used when loading or saving using encryption. */ -const char *PasswordFile::password() const +const std::string &PasswordFile::password() const { return m_password; } /*! - * \brief Sets the current password. It will be used when loading or saving using encryption. + * \brief Sets the current password. It will be used when loading an encrypted file or when saving using encryption. */ -void PasswordFile::setPassword(const string &value) +void PasswordFile::setPassword(const string &password) { - clearPassword(); - value.copy(m_password, 32, 0); + m_password = password; +} + +/*! + * \brief Sets the current password. It will be used when loading an encrypted file or when saving using encryption. + */ +void PasswordFile::setPassword(const char *password, const size_t passwordSize) +{ + m_password.assign(password, passwordSize); } /*! @@ -629,7 +705,7 @@ void PasswordFile::setPassword(const string &value) */ void PasswordFile::clearPassword() { - memset(m_password, 0, 32); + m_password.clear(); } /*! @@ -642,16 +718,16 @@ bool PasswordFile::isEncryptionUsed() } m_file.seekg(0); - //check magic number + // check magic number if (m_freader.readUInt32LE() != 0x7770616DU) { return false; } - //check version + // check version const auto version = m_freader.readUInt32LE(); if (version == 0x1U || version == 0x2U) { return true; - } else if (version == 0x3U) { + } else if (version >= 0x3U) { return m_freader.readByte() & 0x80; } else { return false; @@ -666,6 +742,38 @@ bool PasswordFile::isOpen() const return m_file.is_open(); } +/*! + * \brief Returns the extended header. + */ +string &PasswordFile::extendedHeader() +{ + return m_extendedHeader; +} + +/*! + * \brief Returns the extended header. + */ +const string &PasswordFile::extendedHeader() const +{ + return m_extendedHeader; +} + +/*! + * \brief Returns the encrypted extended header. + */ +string &PasswordFile::encryptedExtendedHeader() +{ + return m_encryptedExtendedHeader; +} + +/*! + * \brief Returns the encrypted extended header. + */ +const string &PasswordFile::encryptedExtendedHeader() const +{ + return m_encryptedExtendedHeader; +} + /*! * \brief Returns the size of the file if the file is open; otherwise returns zero. */ diff --git a/io/passwordfile.h b/io/passwordfile.h index b4f26e5..cd32967 100644 --- a/io/passwordfile.h +++ b/io/passwordfile.h @@ -16,6 +16,56 @@ namespace Io { class NodeEntry; +enum class PasswordFileOpenFlags : uint64 { + None = 0, + ReadOnly = 1, + Default = None, +}; + +constexpr PasswordFileOpenFlags operator|(PasswordFileOpenFlags lhs, PasswordFileOpenFlags rhs) +{ + return static_cast( + static_cast::type>(lhs) | static_cast::type>(rhs)); +} + +constexpr PasswordFileOpenFlags operator|=(PasswordFileOpenFlags lhs, PasswordFileOpenFlags rhs) +{ + return static_cast( + static_cast::type>(lhs) | static_cast::type>(rhs)); +} + +constexpr bool operator&(PasswordFileOpenFlags lhs, PasswordFileOpenFlags rhs) +{ + return static_cast( + static_cast::type>(lhs) & static_cast::type>(rhs)); +} + +enum class PasswordFileSaveFlags : uint64 { + None = 0, + Encryption = 1, + Compression = 2, + PasswordHashing = 4, + Default = Encryption | Compression | PasswordHashing, +}; + +constexpr PasswordFileSaveFlags operator|(PasswordFileSaveFlags lhs, PasswordFileSaveFlags rhs) +{ + return static_cast( + static_cast::type>(lhs) | static_cast::type>(rhs)); +} + +constexpr PasswordFileSaveFlags operator|=(PasswordFileSaveFlags lhs, PasswordFileSaveFlags rhs) +{ + return static_cast( + static_cast::type>(lhs) | static_cast::type>(rhs)); +} + +constexpr bool operator&(PasswordFileSaveFlags lhs, PasswordFileSaveFlags rhs) +{ + return static_cast( + static_cast::type>(lhs) & static_cast::type>(rhs)); +} + class PASSWORD_FILE_EXPORT PasswordFile { public: explicit PasswordFile(); @@ -24,15 +74,15 @@ public: PasswordFile(PasswordFile &&other); ~PasswordFile(); IoUtilities::NativeFileStream &fileStream(); - void open(bool readOnly = false); + void open(PasswordFileOpenFlags options = PasswordFileOpenFlags::Default); void opened(); void generateRootEntry(); void create(); void close(); void load(); - // FIXME: use flags in v4 - void save(bool useEncryption = true, bool useCompression = true); - void write(bool useEncryption = true, bool useCompression = true); + uint32 mininumVersion(PasswordFileSaveFlags options) const; + void save(PasswordFileSaveFlags options = PasswordFileSaveFlags::Default); + void write(PasswordFileSaveFlags options = PasswordFileSaveFlags::Default); void clearEntries(); void clear(); void exportToTextfile(const std::string &targetPath) const; @@ -41,18 +91,23 @@ public: const NodeEntry *rootEntry() const; NodeEntry *rootEntry(); const std::string &path() const; - const char *password() const; + const std::string &password() const; void setPath(const std::string &value); void clearPath(); - void setPassword(const std::string &value); + void setPassword(const std::string &password); + void setPassword(const char *password, const std::size_t passwordSize); void clearPassword(); bool isEncryptionUsed(); bool isOpen() const; + std::string &extendedHeader(); + const std::string &extendedHeader() const; + std::string &encryptedExtendedHeader(); + const std::string &encryptedExtendedHeader() const; std::size_t size(); private: std::string m_path; - char m_password[32]; + std::string m_password; std::unique_ptr m_rootEntry; std::string m_extendedHeader; std::string m_encryptedExtendedHeader; diff --git a/tests/opensslutils.cpp b/tests/opensslutils.cpp new file mode 100644 index 0000000..d1bb3f1 --- /dev/null +++ b/tests/opensslutils.cpp @@ -0,0 +1,64 @@ +#include "../util/openssl.h" + +#include +#include + +#include +#include + +#include + +using namespace std; +using namespace Util::OpenSsl; +using namespace ConversionUtilities; +using namespace TestUtilities::Literals; + +using namespace CPPUNIT_NS; + +/*! + * \brief The OpenSslUtilsTests class tests the functions in the Util::OpenSsl namespace. + */ +class OpenSslUtilsTests : public TestFixture { + CPPUNIT_TEST_SUITE(OpenSslUtilsTests); + CPPUNIT_TEST(testComputeSha256Sum); + CPPUNIT_TEST(testGenerateRandomNumber); + CPPUNIT_TEST_SUITE_END(); + +public: + void setUp(); + void tearDown(); + + void testComputeSha256Sum(); + void testGenerateRandomNumber(); +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(OpenSslUtilsTests); + +void OpenSslUtilsTests::setUp() +{ +} + +void OpenSslUtilsTests::tearDown() +{ +} + +void OpenSslUtilsTests::testComputeSha256Sum() +{ + const char someString[] = "hello world"; + Sha256Sum sum = computeSha256Sum(reinterpret_cast(someString), sizeof(someString)); + string sumAsHex; + sumAsHex.reserve(64); + for (unsigned char hashNumber : sum.data) { + const string digits = numberToString(hashNumber, 16); + sumAsHex.push_back(digits.size() < 2 ? '0' : digits.front()); + sumAsHex.push_back(digits.back()); + } + CPPUNIT_ASSERT_EQUAL("430646847E70344C09F58739E99D5BC96EAC8D5FE7295CF196B986279876BF9B"s, sumAsHex); + // note that the termination char is hashed as well +} + +void OpenSslUtilsTests::testGenerateRandomNumber() +{ + CPPUNIT_ASSERT_EQUAL(static_cast(0u), generateRandomNumber(0u, 0u)); + CPPUNIT_ASSERT_EQUAL(static_cast(1u), generateRandomNumber(1u, 1u)); +} diff --git a/tests/passwordfiletests.cpp b/tests/passwordfiletests.cpp index ad4ca13..38d5ec5 100644 --- a/tests/passwordfiletests.cpp +++ b/tests/passwordfiletests.cpp @@ -19,9 +19,8 @@ using namespace CPPUNIT_NS; class PasswordFileTests : public TestFixture { CPPUNIT_TEST_SUITE(PasswordFileTests); CPPUNIT_TEST(testReading); -#ifdef PLATFORM_UNIX - CPPUNIT_TEST(testWriting); -#endif + CPPUNIT_TEST(testBasicWriting); + CPPUNIT_TEST(testExtendedWriting); CPPUNIT_TEST_SUITE_END(); public: @@ -29,11 +28,10 @@ public: void tearDown(); void testReading(); - void testReading( - const string &testfile1path, const string &testfile1password, const string &testfile2, const string &testfile2password, bool testfile2Mod); -#ifdef PLATFORM_UNIX - void testWriting(); -#endif + void testReading(const string &context, const string &testfile1path, const string &testfile1password, const string &testfile2, + const string &testfile2password, bool testfile2Mod, bool extendedHeaderMod); + void testBasicWriting(); + void testExtendedWriting(); }; CPPUNIT_TEST_SUITE_REGISTRATION(PasswordFileTests); @@ -51,19 +49,20 @@ void PasswordFileTests::tearDown() */ void PasswordFileTests::testReading() { - testReading(TestUtilities::testFilePath("testfile1.pwmgr"), "123456", TestUtilities::testFilePath("testfile2.pwmgr"), string(), false); + testReading( + "read", TestUtilities::testFilePath("testfile1.pwmgr"), "123456", TestUtilities::testFilePath("testfile2.pwmgr"), string(), false, false); } -void PasswordFileTests::testReading( - const string &testfile1path, const string &testfile1password, const string &testfile2, const string &testfile2password, bool testfile2Mod) +void PasswordFileTests::testReading(const string &context, const string &testfile1path, const string &testfile1password, const string &testfile2, + const string &testfile2password, bool testfile2Mod, bool extendedHeaderMod) { PasswordFile file; // open testfile 1 ... file.setPath(testfile1path); - file.open(true); + file.open(PasswordFileOpenFlags::ReadOnly); - CPPUNIT_ASSERT_EQUAL(!testfile1password.empty(), file.isEncryptionUsed()); + CPPUNIT_ASSERT_EQUAL_MESSAGE(context, !testfile1password.empty(), file.isEncryptionUsed()); // attempt to decrypt using a wrong password file.setPassword(testfile1password + "asdf"); if (!testfile1password.empty()) { @@ -104,9 +103,16 @@ void PasswordFileTests::testReading( // test testaccount3 CPPUNIT_ASSERT_EQUAL("testaccount3"s, rootEntry->children()[3]->label()); + if (extendedHeaderMod) { + CPPUNIT_ASSERT_EQUAL("foo"s, file.extendedHeader()); + } else { + CPPUNIT_ASSERT_EQUAL(""s, file.extendedHeader()); + } + CPPUNIT_ASSERT_EQUAL(""s, file.encryptedExtendedHeader()); + // open testfile 2 file.setPath(testfile2); - file.open(true); + file.open(PasswordFileOpenFlags::ReadOnly); CPPUNIT_ASSERT_EQUAL(!testfile2password.empty(), file.isEncryptionUsed()); file.setPassword(testfile2password); @@ -120,13 +126,19 @@ void PasswordFileTests::testReading( CPPUNIT_ASSERT_EQUAL("testfile2"s, rootEntry2->label()); CPPUNIT_ASSERT_EQUAL(1_st, rootEntry2->children().size()); } + if (extendedHeaderMod) { + CPPUNIT_ASSERT_EQUAL("foo"s, file.extendedHeader()); + CPPUNIT_ASSERT_EQUAL("bar"s, file.encryptedExtendedHeader()); + } else { + CPPUNIT_ASSERT_EQUAL(""s, file.extendedHeader()); + CPPUNIT_ASSERT_EQUAL(""s, file.encryptedExtendedHeader()); + } } -#ifdef PLATFORM_UNIX /*! - * \brief Tests writing and reading. + * \brief Tests writing (and reading again) using basic features. */ -void PasswordFileTests::testWriting() +void PasswordFileTests::testBasicWriting() { const string testfile1 = TestUtilities::workingCopyPath("testfile1.pwmgr"); const string testfile2 = TestUtilities::workingCopyPath("testfile2.pwmgr"); @@ -134,26 +146,66 @@ void PasswordFileTests::testWriting() // resave testfile 1 file.setPath(testfile1); - file.open(false); + file.open(); file.setPassword("123456"); file.load(); file.doBackup(); - file.save(false, true); + file.save(PasswordFileSaveFlags::Compression); // resave testfile 2 file.setPath(testfile2); - file.open(false); + file.open(); file.load(); file.rootEntry()->setLabel("testfile2 - modified"); new AccountEntry("newAccount", file.rootEntry()); file.setPassword("654321"); file.doBackup(); - file.save(true, false); + file.save(PasswordFileSaveFlags::Encryption); // check results using the reading test - testReading(testfile1, string(), testfile2, "654321", true); + testReading("basic writing", testfile1, string(), testfile2, "654321", true, false); // check backup files - testReading(testfile1 + ".backup", "123456", testfile2 + ".backup", string(), false); + testReading("basic writing", testfile1 + ".backup", "123456", testfile2 + ".backup", string(), false, false); +} + +/*! + * \brief Tests writing (and reading again) using extended features. + */ +void PasswordFileTests::testExtendedWriting() +{ + const string testfile1 = TestUtilities::workingCopyPath("testfile1.pwmgr"); + const string testfile2 = TestUtilities::workingCopyPath("testfile2.pwmgr"); + PasswordFile file; + + // resave testfile 1 + file.setPath(testfile1); + file.open(); + file.setPassword("123456"); + file.load(); + CPPUNIT_ASSERT_EQUAL(""s, file.extendedHeader()); + CPPUNIT_ASSERT_EQUAL(""s, file.encryptedExtendedHeader()); + file.doBackup(); + file.extendedHeader() = "foo"; + file.save(PasswordFileSaveFlags::Encryption | PasswordFileSaveFlags::PasswordHashing); + + // resave testfile 2 + file.setPath(testfile2); + file.open(); + file.load(); + CPPUNIT_ASSERT_EQUAL(""s, file.extendedHeader()); + CPPUNIT_ASSERT_EQUAL(""s, file.encryptedExtendedHeader()); + file.rootEntry()->setLabel("testfile2 - modified"); + new AccountEntry("newAccount", file.rootEntry()); + file.setPassword("654321"); + file.extendedHeader() = "foo"; + file.encryptedExtendedHeader() = "bar"; + file.doBackup(); + file.save(PasswordFileSaveFlags::Encryption | PasswordFileSaveFlags::PasswordHashing); + + // check results using the reading test + testReading("extended writing", testfile1, "123456", testfile2, "654321", true, true); + + // check backup files + testReading("extended writing", testfile1 + ".backup", "123456", testfile2 + ".backup", string(), false, false); } -#endif diff --git a/util/openssl.cpp b/util/openssl.cpp index f8e39f3..e65c648 100644 --- a/util/openssl.cpp +++ b/util/openssl.cpp @@ -1,8 +1,14 @@ #include "./openssl.h" +#include "./opensslrandomdevice.h" + +#include #include #include #include +#include + +#include /*! * \brief Contains utility classes and functions. @@ -14,6 +20,8 @@ namespace Util { */ namespace OpenSsl { +static_assert(Sha256Sum::size == SHA256_DIGEST_LENGTH, "SHA-256 sum fits into Sha256Sum struct"); + /*! * \brief Initializes OpenSSL. */ @@ -35,5 +43,35 @@ void clean() // remove error strings ERR_free_strings(); } + +/*! + * \brief Computes a SHA-256 sum using OpenSSL. + */ +Sha256Sum computeSha256Sum(const unsigned char *buffer, std::size_t size) +{ + // init sha256 hashing + SHA256_CTX sha256; + SHA256_Init(&sha256); + + // do the actual hashing + SHA256_Update(&sha256, buffer, size); + + // finalize the hashing + Sha256Sum hash; + SHA256_Final(hash.data, &sha256); + return hash; +} + +/*! + * \brief Generates a random number using OpenSSL. + */ +uint32_t generateRandomNumber(uint32_t min, uint32_t max) +{ + OpenSslRandomDevice dev; + std::default_random_engine rng(dev()); + std::uniform_int_distribution dist(min, max); + return dist(rng); +} + } // namespace OpenSsl } // namespace Util diff --git a/util/openssl.h b/util/openssl.h index 2f5d02a..965b2b7 100644 --- a/util/openssl.h +++ b/util/openssl.h @@ -1,14 +1,24 @@ #ifndef PASSWORD_FILE_UTIL_OPENSSL_H -#define OPENSSL_H +#define PASSWORD_FILE_UTIL_OPENSSL_H #include "../global.h" +#include + namespace Util { namespace OpenSsl { +struct Sha256Sum { + static constexpr std::size_t size = 32; + unsigned char data[size] = { 0 }; +}; + void PASSWORD_FILE_EXPORT init(); void PASSWORD_FILE_EXPORT clean(); +Sha256Sum PASSWORD_FILE_EXPORT computeSha256Sum(const unsigned char *buffer, std::size_t size); +uint32_t PASSWORD_FILE_EXPORT generateRandomNumber(uint32_t min, uint32_t max); + } // namespace OpenSsl } // namespace Util diff --git a/util/opensslrandomdevice.cpp b/util/opensslrandomdevice.cpp index 77a2844..adf21c7 100644 --- a/util/opensslrandomdevice.cpp +++ b/util/opensslrandomdevice.cpp @@ -28,7 +28,7 @@ OpenSslRandomDevice::OpenSslRandomDevice() /*! * \brief Generates a new random number. */ -uint32 OpenSslRandomDevice::operator()() const +OpenSslRandomDevice::result_type OpenSslRandomDevice::operator()() const { unsigned char buf[4]; if (RAND_bytes(buf, sizeof(buf))) { diff --git a/util/opensslrandomdevice.h b/util/opensslrandomdevice.h index 808b610..7133fc4 100644 --- a/util/opensslrandomdevice.h +++ b/util/opensslrandomdevice.h @@ -14,7 +14,7 @@ public: using result_type = uint32; OpenSslRandomDevice(); - uint32 operator()() const; + result_type operator()() const; bool status() const; static constexpr result_type min(); static constexpr result_type max();