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.
This commit is contained in:
parent
d9edcc4083
commit
448c5b1a37
|
@ -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)
|
||||
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
#include "./entry.h"
|
||||
#include "./parsingexception.h"
|
||||
|
||||
#include "../util/openssl.h"
|
||||
#include "../util/opensslrandomdevice.h"
|
||||
|
||||
#include <c++utilities/conversion/stringbuilder.h>
|
||||
#include <c++utilities/conversion/stringconversion.h>
|
||||
#include <c++utilities/io/catchiofailure.h>
|
||||
|
@ -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<NodeEntry>(*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<size_t>(m_file.tellg()) - headerSize;
|
||||
m_file.seekg(static_cast<streamoff>(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<unsigned const char *>(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<char *>(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<unsigned const char *>(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<unsigned char *>(decryptedData.data()), &outlen1,
|
||||
reinterpret_cast<unsigned char *>(rawData.data()), static_cast<int>(remainingSize))
|
||||
!= 1
|
||||
|
@ -297,8 +324,11 @@ void PasswordFile::load()
|
|||
decryptedStream.rdbuf()->pubsetbuf(decryptedData.data(), static_cast<streamsize>(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<uint16>::max()) {
|
||||
throw runtime_error("Extended header exceeds maximum size.");
|
||||
}
|
||||
m_fwriter.writeUInt16BE(static_cast<uint16>(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<uint16>(m_encryptedExtendedHeader.size()));
|
||||
m_fwriter.writeString(m_encryptedExtendedHeader);
|
||||
if (version >= 0x5U) {
|
||||
if (m_encryptedExtendedHeader.size() > numeric_limits<uint16>::max()) {
|
||||
throw runtime_error("Encrypted extended header exceeds maximum size.");
|
||||
}
|
||||
BinaryWriter buffstrWriter(&buffstr);
|
||||
buffstrWriter.writeUInt16BE(static_cast<uint16>(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<char> encryptedData;
|
||||
|
||||
// compress data
|
||||
if (useCompression) {
|
||||
if (options & PasswordFileSaveFlags::Compression) {
|
||||
uLongf compressedSize = compressBound(size);
|
||||
encryptedData.resize(8 + compressedSize);
|
||||
ConversionUtilities::LE::getBytes(static_cast<uint64>(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<streamsize>(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<unsigned const char *>(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<char *>(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<unsigned const char *>(m_password), iv) != 1
|
||||
|| EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, password.data, iv) != 1
|
||||
|| EVP_EncryptUpdate(ctx, reinterpret_cast<unsigned char *>(encryptedData.data()), &outlen1,
|
||||
reinterpret_cast<unsigned char *>(decryptedData.data()), static_cast<int>(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<char *>(iv), aes256cbcIvSize);
|
||||
m_file.write(encryptedData.data(), static_cast<streamsize>(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.
|
||||
*/
|
||||
|
|
|
@ -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<PasswordFileOpenFlags>(
|
||||
static_cast<std::underlying_type<PasswordFileOpenFlags>::type>(lhs) | static_cast<std::underlying_type<PasswordFileOpenFlags>::type>(rhs));
|
||||
}
|
||||
|
||||
constexpr PasswordFileOpenFlags operator|=(PasswordFileOpenFlags lhs, PasswordFileOpenFlags rhs)
|
||||
{
|
||||
return static_cast<PasswordFileOpenFlags>(
|
||||
static_cast<std::underlying_type<PasswordFileOpenFlags>::type>(lhs) | static_cast<std::underlying_type<PasswordFileOpenFlags>::type>(rhs));
|
||||
}
|
||||
|
||||
constexpr bool operator&(PasswordFileOpenFlags lhs, PasswordFileOpenFlags rhs)
|
||||
{
|
||||
return static_cast<bool>(
|
||||
static_cast<std::underlying_type<PasswordFileOpenFlags>::type>(lhs) & static_cast<std::underlying_type<PasswordFileOpenFlags>::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<PasswordFileSaveFlags>(
|
||||
static_cast<std::underlying_type<PasswordFileSaveFlags>::type>(lhs) | static_cast<std::underlying_type<PasswordFileSaveFlags>::type>(rhs));
|
||||
}
|
||||
|
||||
constexpr PasswordFileSaveFlags operator|=(PasswordFileSaveFlags lhs, PasswordFileSaveFlags rhs)
|
||||
{
|
||||
return static_cast<PasswordFileSaveFlags>(
|
||||
static_cast<std::underlying_type<PasswordFileSaveFlags>::type>(lhs) | static_cast<std::underlying_type<PasswordFileSaveFlags>::type>(rhs));
|
||||
}
|
||||
|
||||
constexpr bool operator&(PasswordFileSaveFlags lhs, PasswordFileSaveFlags rhs)
|
||||
{
|
||||
return static_cast<bool>(
|
||||
static_cast<std::underlying_type<PasswordFileSaveFlags>::type>(lhs) & static_cast<std::underlying_type<PasswordFileSaveFlags>::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<NodeEntry> m_rootEntry;
|
||||
std::string m_extendedHeader;
|
||||
std::string m_encryptedExtendedHeader;
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
#include "../util/openssl.h"
|
||||
|
||||
#include <c++utilities/conversion/stringconversion.h>
|
||||
#include <c++utilities/tests/testutils.h>
|
||||
|
||||
#include <cppunit/TestFixture.h>
|
||||
#include <cppunit/extensions/HelperMacros.h>
|
||||
|
||||
#include <random>
|
||||
|
||||
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<unsigned const char *>(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<uint32_t>(0u), generateRandomNumber(0u, 0u));
|
||||
CPPUNIT_ASSERT_EQUAL(static_cast<uint32_t>(1u), generateRandomNumber(1u, 1u));
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
#include "./openssl.h"
|
||||
#include "./opensslrandomdevice.h"
|
||||
|
||||
#include <c++utilities/conversion/binaryconversion.h>
|
||||
|
||||
#include <openssl/conf.h>
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/sha.h>
|
||||
|
||||
#include <random>
|
||||
|
||||
/*!
|
||||
* \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<uint32_t> dist(min, max);
|
||||
return dist(rng);
|
||||
}
|
||||
|
||||
} // namespace OpenSsl
|
||||
} // namespace Util
|
||||
|
|
|
@ -1,14 +1,24 @@
|
|||
#ifndef PASSWORD_FILE_UTIL_OPENSSL_H
|
||||
#define OPENSSL_H
|
||||
#define PASSWORD_FILE_UTIL_OPENSSL_H
|
||||
|
||||
#include "../global.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -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))) {
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue