mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-06-29 08:57:43 -04:00
Implements KDBX4 format with Argon2 KDF
* Adds KDBX4 reader/writer interfaces * Adds KDBX4 XML reader/write interfaces * Implements test cases for KDBX4 * Fully compatible with KeePass2 * Corrects minor issues with Argon2 KDF
This commit is contained in:
parent
7dba788d09
commit
bef7ba2cfe
36 changed files with 3305 additions and 51 deletions
|
@ -91,6 +91,10 @@ set(keepassx_SOURCES
|
||||||
format/Kdbx3Writer.cpp
|
format/Kdbx3Writer.cpp
|
||||||
format/Kdbx3XmlReader.cpp
|
format/Kdbx3XmlReader.cpp
|
||||||
format/Kdbx3XmlWriter.cpp
|
format/Kdbx3XmlWriter.cpp
|
||||||
|
format/Kdbx4Reader.cpp
|
||||||
|
format/Kdbx4Writer.cpp
|
||||||
|
format/Kdbx4XmlReader.cpp
|
||||||
|
format/Kdbx4XmlWriter.cpp
|
||||||
gui/AboutDialog.cpp
|
gui/AboutDialog.cpp
|
||||||
gui/Application.cpp
|
gui/Application.cpp
|
||||||
gui/CategoryListWidget.cpp
|
gui/CategoryListWidget.cpp
|
||||||
|
|
|
@ -494,6 +494,14 @@ void Database::setKdf(QSharedPointer<Kdf> kdf)
|
||||||
m_data.kdf = std::move(kdf);
|
m_data.kdf = std::move(kdf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Database::setPublicCustomData(QByteArray data) {
|
||||||
|
m_data.publicCustomData = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray Database::publicCustomData() const {
|
||||||
|
return m_data.publicCustomData;
|
||||||
|
}
|
||||||
|
|
||||||
bool Database::changeKdf(QSharedPointer<Kdf> kdf)
|
bool Database::changeKdf(QSharedPointer<Kdf> kdf)
|
||||||
{
|
{
|
||||||
kdf->randomizeSeed();
|
kdf->randomizeSeed();
|
||||||
|
|
|
@ -58,6 +58,7 @@ public:
|
||||||
Uuid cipher;
|
Uuid cipher;
|
||||||
CompressionAlgorithm compressionAlgo;
|
CompressionAlgorithm compressionAlgo;
|
||||||
QByteArray transformedMasterKey;
|
QByteArray transformedMasterKey;
|
||||||
|
QByteArray publicCustomData;
|
||||||
QSharedPointer<Kdf> kdf;
|
QSharedPointer<Kdf> kdf;
|
||||||
CompositeKey key;
|
CompositeKey key;
|
||||||
bool hasKey;
|
bool hasKey;
|
||||||
|
@ -91,6 +92,7 @@ public:
|
||||||
Uuid cipher() const;
|
Uuid cipher() const;
|
||||||
Database::CompressionAlgorithm compressionAlgo() const;
|
Database::CompressionAlgorithm compressionAlgo() const;
|
||||||
QSharedPointer<Kdf> kdf() const;
|
QSharedPointer<Kdf> kdf() const;
|
||||||
|
QByteArray publicCustomData() const;
|
||||||
QByteArray transformedMasterKey() const;
|
QByteArray transformedMasterKey() const;
|
||||||
const CompositeKey& key() const;
|
const CompositeKey& key() const;
|
||||||
QByteArray challengeResponseKey() const;
|
QByteArray challengeResponseKey() const;
|
||||||
|
@ -99,6 +101,7 @@ public:
|
||||||
void setCipher(const Uuid& cipher);
|
void setCipher(const Uuid& cipher);
|
||||||
void setCompressionAlgo(Database::CompressionAlgorithm algo);
|
void setCompressionAlgo(Database::CompressionAlgorithm algo);
|
||||||
void setKdf(QSharedPointer<Kdf> kdf);
|
void setKdf(QSharedPointer<Kdf> kdf);
|
||||||
|
void setPublicCustomData(QByteArray data);
|
||||||
bool setKey(const CompositeKey& key, bool updateChangedTime = true,
|
bool setKey(const CompositeKey& key, bool updateChangedTime = true,
|
||||||
bool updateTransformSalt = false);
|
bool updateTransformSalt = false);
|
||||||
bool hasKey() const;
|
bool hasKey() const;
|
||||||
|
|
|
@ -49,6 +49,7 @@ Metadata::Metadata(QObject* parent)
|
||||||
m_recycleBinChanged = now;
|
m_recycleBinChanged = now;
|
||||||
m_entryTemplatesGroupChanged = now;
|
m_entryTemplatesGroupChanged = now;
|
||||||
m_masterKeyChanged = now;
|
m_masterKeyChanged = now;
|
||||||
|
m_settingsChanged = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <class P, class V> bool Metadata::set(P& property, const V& value)
|
template <class P, class V> bool Metadata::set(P& property, const V& value)
|
||||||
|
@ -525,3 +526,12 @@ void Metadata::removeCustomField(const QString& key)
|
||||||
m_customFields.remove(key);
|
m_customFields.remove(key);
|
||||||
emit modified();
|
emit modified();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QDateTime Metadata::settingsChanged() const {
|
||||||
|
return m_settingsChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Metadata::setSettingsChanged(const QDateTime& value) {
|
||||||
|
Q_ASSERT(value.timeSpec() == Qt::UTC);
|
||||||
|
m_settingsChanged = value;
|
||||||
|
}
|
||||||
|
|
|
@ -69,6 +69,7 @@ public:
|
||||||
QDateTime descriptionChanged() const;
|
QDateTime descriptionChanged() const;
|
||||||
QString defaultUserName() const;
|
QString defaultUserName() const;
|
||||||
QDateTime defaultUserNameChanged() const;
|
QDateTime defaultUserNameChanged() const;
|
||||||
|
QDateTime settingsChanged() const;
|
||||||
int maintenanceHistoryDays() const;
|
int maintenanceHistoryDays() const;
|
||||||
QColor color() const;
|
QColor color() const;
|
||||||
bool protectTitle() const;
|
bool protectTitle() const;
|
||||||
|
@ -108,6 +109,7 @@ public:
|
||||||
void setDescriptionChanged(const QDateTime& value);
|
void setDescriptionChanged(const QDateTime& value);
|
||||||
void setDefaultUserName(const QString& value);
|
void setDefaultUserName(const QString& value);
|
||||||
void setDefaultUserNameChanged(const QDateTime& value);
|
void setDefaultUserNameChanged(const QDateTime& value);
|
||||||
|
void setSettingsChanged(const QDateTime& value);
|
||||||
void setMaintenanceHistoryDays(int value);
|
void setMaintenanceHistoryDays(int value);
|
||||||
void setColor(const QColor& value);
|
void setColor(const QColor& value);
|
||||||
void setProtectTitle(bool value);
|
void setProtectTitle(bool value);
|
||||||
|
@ -141,6 +143,7 @@ public:
|
||||||
* - Master key changed date
|
* - Master key changed date
|
||||||
* - Custom icons
|
* - Custom icons
|
||||||
* - Custom fields
|
* - Custom fields
|
||||||
|
* - Settings changed date
|
||||||
*/
|
*/
|
||||||
void copyAttributesFrom(const Metadata* other);
|
void copyAttributesFrom(const Metadata* other);
|
||||||
|
|
||||||
|
@ -170,6 +173,7 @@ private:
|
||||||
QPointer<Group> m_lastTopVisibleGroup;
|
QPointer<Group> m_lastTopVisibleGroup;
|
||||||
|
|
||||||
QDateTime m_masterKeyChanged;
|
QDateTime m_masterKeyChanged;
|
||||||
|
QDateTime m_settingsChanged;
|
||||||
|
|
||||||
QHash<QString, QString> m_customFields;
|
QHash<QString, QString> m_customFields;
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,31 @@ AesKdf::AesKdf()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool AesKdf::processParameters(const QVariantMap &p)
|
||||||
|
{
|
||||||
|
bool ok;
|
||||||
|
int rounds = p.value(KeePass2::KDFPARAM_AES_ROUNDS).toInt(&ok);
|
||||||
|
if (!ok || !setRounds(rounds)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray seed = p.value(KeePass2::KDFPARAM_AES_SEED).toByteArray();
|
||||||
|
if (!setSeed(seed)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap AesKdf::writeParameters()
|
||||||
|
{
|
||||||
|
QVariantMap p;
|
||||||
|
p.insert(KeePass2::KDFPARAM_UUID, KeePass2::KDF_AES.toByteArray());
|
||||||
|
p.insert(KeePass2::KDFPARAM_AES_ROUNDS, rounds());
|
||||||
|
p.insert(KeePass2::KDFPARAM_AES_SEED, seed());
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
bool AesKdf::transform(const QByteArray& raw, QByteArray& result) const
|
bool AesKdf::transform(const QByteArray& raw, QByteArray& result) const
|
||||||
{
|
{
|
||||||
QByteArray resultLeft;
|
QByteArray resultLeft;
|
||||||
|
|
|
@ -25,6 +25,8 @@ class AesKdf: public Kdf
|
||||||
public:
|
public:
|
||||||
AesKdf();
|
AesKdf();
|
||||||
|
|
||||||
|
bool processParameters(const QVariantMap& p) override;
|
||||||
|
QVariantMap writeParameters() override;
|
||||||
bool transform(const QByteArray& raw, QByteArray& result) const override;
|
bool transform(const QByteArray& raw, QByteArray& result) const override;
|
||||||
QSharedPointer<Kdf> clone() const override;
|
QSharedPointer<Kdf> clone() const override;
|
||||||
|
|
||||||
|
|
|
@ -32,24 +32,43 @@
|
||||||
*/
|
*/
|
||||||
Argon2Kdf::Argon2Kdf()
|
Argon2Kdf::Argon2Kdf()
|
||||||
: Kdf::Kdf(KeePass2::KDF_ARGON2)
|
: Kdf::Kdf(KeePass2::KDF_ARGON2)
|
||||||
|
, m_version(0x13)
|
||||||
, m_memory(1<<16)
|
, m_memory(1<<16)
|
||||||
, m_parallelism(2)
|
, m_parallelism(2)
|
||||||
{
|
{
|
||||||
m_rounds = 1;
|
m_rounds = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
quint32 Argon2Kdf::memory() const
|
quint32 Argon2Kdf::version() const
|
||||||
{
|
{
|
||||||
// Convert to Megabytes
|
return m_version;
|
||||||
return m_memory / (1<<10);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Argon2Kdf::setMemory(quint32 memoryMegabytes)
|
bool Argon2Kdf::setVersion(quint32 version)
|
||||||
{
|
{
|
||||||
// TODO: add bounds check
|
// MIN=0x10; MAX=0x13)
|
||||||
// Convert to Kibibytes
|
if (version >= 0x10 && version <= 0x13) {
|
||||||
m_memory = (1<<10) * memoryMegabytes;
|
m_version = version;
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
m_version = 0x13;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
quint64 Argon2Kdf::memory() const
|
||||||
|
{
|
||||||
|
return m_memory;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Argon2Kdf::setMemory(quint64 kibibytes)
|
||||||
|
{
|
||||||
|
// MIN=8KB; MAX=2,147,483,648KB
|
||||||
|
if (kibibytes >= 8 && kibibytes < (1ULL<<32)) {
|
||||||
|
m_memory = kibibytes;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
m_memory = 16;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
quint32 Argon2Kdf::parallelism() const
|
quint32 Argon2Kdf::parallelism() const
|
||||||
|
@ -59,30 +78,97 @@ quint32 Argon2Kdf::parallelism() const
|
||||||
|
|
||||||
bool Argon2Kdf::setParallelism(quint32 threads)
|
bool Argon2Kdf::setParallelism(quint32 threads)
|
||||||
{
|
{
|
||||||
// TODO: add bounds check
|
// MIN=1; MAX=16,777,215
|
||||||
m_parallelism = threads;
|
if (threads >= 1 && threads < (1<<24)) {
|
||||||
|
m_parallelism = threads;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
m_parallelism = 1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Argon2Kdf::processParameters(const QVariantMap &p)
|
||||||
|
{
|
||||||
|
QByteArray salt = p.value(KeePass2::KDFPARAM_ARGON2_SALT).toByteArray();
|
||||||
|
if (!setSeed(salt)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ok;
|
||||||
|
quint32 version = p.value(KeePass2::KDFPARAM_ARGON2_VERSION).toUInt(&ok);
|
||||||
|
if (!ok || !setVersion(version)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
quint32 lanes = p.value(KeePass2::KDFPARAM_ARGON2_PARALLELISM).toUInt(&ok);
|
||||||
|
if (!ok || !setParallelism(lanes)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
quint64 memory = p.value(KeePass2::KDFPARAM_ARGON2_MEMORY).toULongLong(&ok) / 1024ULL;
|
||||||
|
if (!ok || !setMemory(memory)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
quint64 iterations = p.value(KeePass2::KDFPARAM_ARGON2_ITERATIONS).toULongLong(&ok);
|
||||||
|
if (!ok || !setRounds(iterations)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* KeePass2 does not currently implement these parameters
|
||||||
|
*
|
||||||
|
QByteArray secret = p.value(KeePass2::KDFPARAM_ARGON2_SECRET).toByteArray();
|
||||||
|
if (!argon2Kdf->setSecret(secret)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray ad = p.value(KeePass2::KDFPARAM_ARGON2_ASSOCDATA).toByteArray();
|
||||||
|
if (!argon2Kdf->setAssocData(ad)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QVariantMap Argon2Kdf::writeParameters()
|
||||||
|
{
|
||||||
|
QVariantMap p;
|
||||||
|
p.insert(KeePass2::KDFPARAM_UUID, KeePass2::KDF_ARGON2.toByteArray());
|
||||||
|
p.insert(KeePass2::KDFPARAM_ARGON2_VERSION, version());
|
||||||
|
p.insert(KeePass2::KDFPARAM_ARGON2_PARALLELISM, parallelism());
|
||||||
|
p.insert(KeePass2::KDFPARAM_ARGON2_MEMORY, memory() * 1024);
|
||||||
|
p.insert(KeePass2::KDFPARAM_ARGON2_ITERATIONS, static_cast<quint64>(rounds()));
|
||||||
|
p.insert(KeePass2::KDFPARAM_ARGON2_SALT, seed());
|
||||||
|
|
||||||
|
/* KeePass2 does not currently implement these
|
||||||
|
*
|
||||||
|
if (!assocData().isEmpty()) {
|
||||||
|
p.insert(KeePass2::KDFPARAM_ARGON2_ASSOCDATA, argon2Kdf.assocData());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!secret().isEmpty()) {
|
||||||
|
p.insert(KeePass2::KDFPARAM_ARGON2_SECRET, argon2Kdf.secret());
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
bool Argon2Kdf::transform(const QByteArray& raw, QByteArray& result) const
|
bool Argon2Kdf::transform(const QByteArray& raw, QByteArray& result) const
|
||||||
{
|
{
|
||||||
result.clear();
|
result.clear();
|
||||||
result.resize(32);
|
result.resize(32);
|
||||||
|
return transformKeyRaw(raw, seed(), version(), rounds(), memory(), parallelism(), result);
|
||||||
if (!transformKeyRaw(raw, seed(), rounds(), memory(), parallelism(), result)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = CryptoHash::hash(result, CryptoHash::Sha256);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Argon2Kdf::transformKeyRaw(const QByteArray& key, const QByteArray& seed, int rounds,
|
bool Argon2Kdf::transformKeyRaw(const QByteArray& key, const QByteArray& seed, quint32 version,
|
||||||
quint32 memory, quint32 parallelism, QByteArray& result)
|
quint32 rounds, quint64 memory, quint32 parallelism, QByteArray& result)
|
||||||
{
|
{
|
||||||
// Time Cost, Mem Cost, Threads/Lanes, Password, length, Salt, length, out, length
|
// Time Cost, Mem Cost, Threads/Lanes, Password, length, Salt, length, out, length
|
||||||
int rc = argon2d_hash_raw(rounds, memory, parallelism, key.data(), key.size(),
|
int rc = argon2_hash(rounds, memory, parallelism, key.data(), key.size(),
|
||||||
seed.data(), seed.size(), result.data(), result.size());
|
seed.data(), seed.size(), result.data(), result.size(),
|
||||||
|
nullptr, 0, Argon2_d, version);
|
||||||
if (rc != ARGON2_OK) {
|
if (rc != ARGON2_OK) {
|
||||||
qWarning("Argon2 error: %s", argon2_error_message(rc));
|
qWarning("Argon2 error: %s", argon2_error_message(rc));
|
||||||
return false;
|
return false;
|
||||||
|
@ -105,12 +191,9 @@ int Argon2Kdf::benchmarkImpl(int msec) const
|
||||||
timer.start();
|
timer.start();
|
||||||
|
|
||||||
int rounds = 4;
|
int rounds = 4;
|
||||||
|
if (transformKeyRaw(key, seed, version(), rounds, memory(), parallelism(), key)) {
|
||||||
int rc = argon2d_hash_raw(rounds, m_memory, m_parallelism, key.data(), key.size(), seed.data(), seed.size(), key.data(), key.size());
|
return static_cast<int>(rounds * (static_cast<float>(msec) / timer.elapsed()));
|
||||||
if (rc != ARGON2_OK) {
|
|
||||||
qWarning("Argon2 error: %s", argon2_error_message(rc));
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return static_cast<int>(rounds * (static_cast<float>(msec) / timer.elapsed()));
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,25 +24,31 @@ class Argon2Kdf : public Kdf {
|
||||||
public:
|
public:
|
||||||
Argon2Kdf();
|
Argon2Kdf();
|
||||||
|
|
||||||
|
bool processParameters(const QVariantMap& p) override;
|
||||||
|
QVariantMap writeParameters() override;
|
||||||
bool transform(const QByteArray& raw, QByteArray& result) const override;
|
bool transform(const QByteArray& raw, QByteArray& result) const override;
|
||||||
QSharedPointer<Kdf> clone() const override;
|
QSharedPointer<Kdf> clone() const override;
|
||||||
|
|
||||||
quint32 memory() const;
|
quint32 version() const;
|
||||||
bool setMemory(quint32 memory_kb);
|
bool setVersion(quint32 version);
|
||||||
|
quint64 memory() const;
|
||||||
|
bool setMemory(quint64 kibibytes);
|
||||||
quint32 parallelism() const;
|
quint32 parallelism() const;
|
||||||
bool setParallelism(quint32 threads);
|
bool setParallelism(quint32 threads);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
int benchmarkImpl(int msec) const override;
|
int benchmarkImpl(int msec) const override;
|
||||||
|
|
||||||
quint32 m_memory;
|
quint32 m_version;
|
||||||
|
quint64 m_memory;
|
||||||
quint32 m_parallelism;
|
quint32 m_parallelism;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static bool transformKeyRaw(const QByteArray& key,
|
static bool transformKeyRaw(const QByteArray& key,
|
||||||
const QByteArray& seed,
|
const QByteArray& seed,
|
||||||
int rounds,
|
quint32 version,
|
||||||
quint32 memory,
|
quint32 rounds,
|
||||||
|
quint64 memory,
|
||||||
quint32 parallelism,
|
quint32 parallelism,
|
||||||
QByteArray& result) Q_REQUIRED_RESULT;
|
QByteArray& result) Q_REQUIRED_RESULT;
|
||||||
};
|
};
|
||||||
|
|
|
@ -46,8 +46,12 @@ QByteArray Kdf::seed() const
|
||||||
|
|
||||||
bool Kdf::setRounds(int rounds)
|
bool Kdf::setRounds(int rounds)
|
||||||
{
|
{
|
||||||
m_rounds = rounds;
|
if (rounds >= 1 && rounds < INT_MAX) {
|
||||||
return true;
|
m_rounds = rounds;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
m_rounds = 1;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Kdf::setSeed(const QByteArray& seed)
|
bool Kdf::setSeed(const QByteArray& seed)
|
||||||
|
|
|
@ -39,6 +39,8 @@ public:
|
||||||
virtual bool setSeed(const QByteArray& seed);
|
virtual bool setSeed(const QByteArray& seed);
|
||||||
virtual void randomizeSeed();
|
virtual void randomizeSeed();
|
||||||
|
|
||||||
|
virtual bool processParameters(const QVariantMap& p) = 0;
|
||||||
|
virtual QVariantMap writeParameters() = 0;
|
||||||
virtual bool transform(const QByteArray& raw, QByteArray& result) const = 0;
|
virtual bool transform(const QByteArray& raw, QByteArray& result) const = 0;
|
||||||
virtual QSharedPointer<Kdf> clone() const = 0;
|
virtual QSharedPointer<Kdf> clone() const = 0;
|
||||||
|
|
||||||
|
|
|
@ -82,7 +82,7 @@ Database* Kdbx3Reader::readDatabase(QIODevice* device, const CompositeKey& key,
|
||||||
|
|
||||||
quint32 version = Endian::readSizedInt<quint32>(m_headerStream, KeePass2::BYTEORDER, &ok)
|
quint32 version = Endian::readSizedInt<quint32>(m_headerStream, KeePass2::BYTEORDER, &ok)
|
||||||
& KeePass2::FILE_VERSION_CRITICAL_MASK;
|
& KeePass2::FILE_VERSION_CRITICAL_MASK;
|
||||||
quint32 maxVersion = KeePass2::FILE_VERSION & KeePass2::FILE_VERSION_CRITICAL_MASK;
|
quint32 maxVersion = KeePass2::FILE_VERSION_3 & KeePass2::FILE_VERSION_CRITICAL_MASK;
|
||||||
if (!ok || (version < KeePass2::FILE_VERSION_MIN) || (version > maxVersion)) {
|
if (!ok || (version < KeePass2::FILE_VERSION_MIN) || (version > maxVersion)) {
|
||||||
raiseError(tr("Unsupported KeePass KDBX 2 or 3 database version."));
|
raiseError(tr("Unsupported KeePass KDBX 2 or 3 database version."));
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
|
@ -74,7 +74,7 @@ bool Kdbx3Writer::writeDatabase(QIODevice* device, Database* db)
|
||||||
|
|
||||||
CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes<qint32>(KeePass2::SIGNATURE_1, KeePass2::BYTEORDER)));
|
CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes<qint32>(KeePass2::SIGNATURE_1, KeePass2::BYTEORDER)));
|
||||||
CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes<qint32>(KeePass2::SIGNATURE_2, KeePass2::BYTEORDER)));
|
CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes<qint32>(KeePass2::SIGNATURE_2, KeePass2::BYTEORDER)));
|
||||||
CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes<qint32>(KeePass2::FILE_VERSION, KeePass2::BYTEORDER)));
|
CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes<qint32>(KeePass2::FILE_VERSION_3, KeePass2::BYTEORDER)));
|
||||||
|
|
||||||
CHECK_RETURN_FALSE(writeHeaderField(KeePass2::CipherID, db->cipher().toByteArray()));
|
CHECK_RETURN_FALSE(writeHeaderField(KeePass2::CipherID, db->cipher().toByteArray()));
|
||||||
CHECK_RETURN_FALSE(writeHeaderField(KeePass2::CompressionFlags,
|
CHECK_RETURN_FALSE(writeHeaderField(KeePass2::CompressionFlags,
|
||||||
|
|
522
src/format/Kdbx4Reader.cpp
Normal file
522
src/format/Kdbx4Reader.cpp
Normal file
|
@ -0,0 +1,522 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Kdbx4Reader.h"
|
||||||
|
|
||||||
|
#include <QBuffer>
|
||||||
|
#include <QFile>
|
||||||
|
|
||||||
|
#include "crypto/kdf/AesKdf.h"
|
||||||
|
#include "streams/HmacBlockStream.h"
|
||||||
|
#include "core/Database.h"
|
||||||
|
#include "core/Endian.h"
|
||||||
|
#include "crypto/CryptoHash.h"
|
||||||
|
#include "format/KeePass1.h"
|
||||||
|
#include "format/KeePass2.h"
|
||||||
|
#include "format/KeePass2RandomStream.h"
|
||||||
|
#include "format/Kdbx4XmlReader.h"
|
||||||
|
#include "streams/HashedBlockStream.h"
|
||||||
|
#include "streams/QtIOCompressor"
|
||||||
|
#include "streams/StoreDataStream.h"
|
||||||
|
#include "streams/SymmetricCipherStream.h"
|
||||||
|
|
||||||
|
Kdbx4Reader::Kdbx4Reader()
|
||||||
|
: m_device(nullptr)
|
||||||
|
, m_db(nullptr)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Database* Kdbx4Reader::readDatabase(QIODevice* device, const CompositeKey& key, bool keepDatabase)
|
||||||
|
{
|
||||||
|
QScopedPointer<Database> db(new Database());
|
||||||
|
m_db = db.data();
|
||||||
|
m_device = device;
|
||||||
|
m_error = false;
|
||||||
|
m_errorStr.clear();
|
||||||
|
m_xmlData.clear();
|
||||||
|
m_masterSeed.clear();
|
||||||
|
m_encryptionIV.clear();
|
||||||
|
m_protectedStreamKey.clear();
|
||||||
|
m_binaryPool.clear();
|
||||||
|
|
||||||
|
StoreDataStream headerStream(m_device);
|
||||||
|
headerStream.open(QIODevice::ReadOnly);
|
||||||
|
QIODevice* headerIo = &headerStream;
|
||||||
|
|
||||||
|
bool ok;
|
||||||
|
|
||||||
|
quint32 signature1 = Endian::readSizedInt<quint32>(headerIo, KeePass2::BYTEORDER, &ok);
|
||||||
|
if (!ok || signature1 != KeePass2::SIGNATURE_1) {
|
||||||
|
raiseError(tr("Not a KeePass database."));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
quint32 signature2 = Endian::readSizedInt<quint32>(headerIo, KeePass2::BYTEORDER, &ok);
|
||||||
|
if (ok && signature2 == KeePass1::SIGNATURE_2) {
|
||||||
|
raiseError(tr("The selected file is an old KeePass 1 database (.kdb).\n\n"
|
||||||
|
"You can import it by clicking on Database > 'Import KeePass 1 database...'.\n"
|
||||||
|
"This is a one-way migration. You won't be able to open the imported "
|
||||||
|
"database with the old KeePassX 0.4 version."));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
else if (!ok || signature2 != KeePass2::SIGNATURE_2) {
|
||||||
|
raiseError(tr("Not a KeePass database."));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
quint32 version = Endian::readSizedInt<quint32>(headerIo, KeePass2::BYTEORDER, &ok)
|
||||||
|
& KeePass2::FILE_VERSION_CRITICAL_MASK;
|
||||||
|
if (!ok || version != (KeePass2::FILE_VERSION_4 & KeePass2::FILE_VERSION_CRITICAL_MASK)) {
|
||||||
|
raiseError(tr("Unsupported KeePass KDBX 4 database version."));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (readHeaderField(headerIo) && !hasError()) {
|
||||||
|
}
|
||||||
|
|
||||||
|
headerStream.close();
|
||||||
|
|
||||||
|
if (hasError()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if all required headers were present
|
||||||
|
if (m_masterSeed.isEmpty()
|
||||||
|
|| m_encryptionIV.isEmpty()
|
||||||
|
|| m_db->cipher().isNull()) {
|
||||||
|
raiseError("missing database headers");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_db->setKey(key, false)) {
|
||||||
|
raiseError(tr("Unable to calculate master key"));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_db->challengeMasterSeed(m_masterSeed) == false) {
|
||||||
|
raiseError(tr("Unable to issue challenge-response."));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
CryptoHash hash(CryptoHash::Sha256);
|
||||||
|
hash.addData(m_masterSeed);
|
||||||
|
hash.addData(m_db->challengeResponseKey());
|
||||||
|
hash.addData(m_db->transformedMasterKey());
|
||||||
|
QByteArray finalKey = hash.result();
|
||||||
|
|
||||||
|
QByteArray headerSha256 = m_device->read(32);
|
||||||
|
QByteArray headerHmac = m_device->read(32);
|
||||||
|
if (headerSha256.size() != 32 || headerHmac.size() != 32) {
|
||||||
|
raiseError("Invalid header checksum size");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (headerSha256 != CryptoHash::hash(headerStream.storedData(), CryptoHash::Sha256)) {
|
||||||
|
raiseError("Header SHA256 mismatch");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray hmacKey = KeePass2::hmacKey(m_masterSeed, m_db->transformedMasterKey());
|
||||||
|
if (headerHmac != CryptoHash::hmac(headerStream.storedData(),
|
||||||
|
HmacBlockStream::getHmacKey(UINT64_MAX, hmacKey), CryptoHash::Sha256)) {
|
||||||
|
raiseError(tr("Wrong key or database file is corrupt. (HMAC mismatch)"));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
HmacBlockStream hmacStream(m_device, hmacKey);
|
||||||
|
if (!hmacStream.open(QIODevice::ReadOnly)) {
|
||||||
|
raiseError(hmacStream.errorString());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
SymmetricCipher::Algorithm cipher = SymmetricCipher::cipherToAlgorithm(m_db->cipher());
|
||||||
|
if (cipher == SymmetricCipher::InvalidAlgorithm) {
|
||||||
|
raiseError("Unknown cipher");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
SymmetricCipherStream cipherStream(&hmacStream, cipher,
|
||||||
|
SymmetricCipher::algorithmMode(cipher), SymmetricCipher::Decrypt);
|
||||||
|
if (!cipherStream.init(finalKey, m_encryptionIV)) {
|
||||||
|
raiseError(cipherStream.errorString());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (!cipherStream.open(QIODevice::ReadOnly)) {
|
||||||
|
raiseError(cipherStream.errorString());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
QIODevice* xmlDevice;
|
||||||
|
QScopedPointer<QtIOCompressor> ioCompressor;
|
||||||
|
|
||||||
|
if (m_db->compressionAlgo() == Database::CompressionNone) {
|
||||||
|
xmlDevice = &cipherStream;
|
||||||
|
} else {
|
||||||
|
ioCompressor.reset(new QtIOCompressor(&cipherStream));
|
||||||
|
ioCompressor->setStreamFormat(QtIOCompressor::GzipFormat);
|
||||||
|
if (!ioCompressor->open(QIODevice::ReadOnly)) {
|
||||||
|
raiseError(ioCompressor->errorString());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
xmlDevice = ioCompressor.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
while (readInnerHeaderField(xmlDevice) && !hasError()) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasError()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeePass2RandomStream randomStream(m_irsAlgo);
|
||||||
|
if (!randomStream.init(m_protectedStreamKey)) {
|
||||||
|
raiseError(randomStream.errorString());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
QScopedPointer<QBuffer> buffer;
|
||||||
|
|
||||||
|
if (m_saveXml) {
|
||||||
|
m_xmlData = xmlDevice->readAll();
|
||||||
|
buffer.reset(new QBuffer(&m_xmlData));
|
||||||
|
buffer->open(QIODevice::ReadOnly);
|
||||||
|
xmlDevice = buffer.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
Kdbx4XmlReader xmlReader(m_binaryPool);
|
||||||
|
xmlReader.readDatabase(xmlDevice, m_db, &randomStream);
|
||||||
|
|
||||||
|
if (xmlReader.hasError()) {
|
||||||
|
raiseError(xmlReader.errorString());
|
||||||
|
if (keepDatabase) {
|
||||||
|
return db.take();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.take();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Kdbx4Reader::readHeaderField(QIODevice* device)
|
||||||
|
{
|
||||||
|
QByteArray fieldIDArray = device->read(1);
|
||||||
|
if (fieldIDArray.size() != 1) {
|
||||||
|
raiseError("Invalid header id size");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
quint8 fieldID = fieldIDArray.at(0);
|
||||||
|
|
||||||
|
bool ok;
|
||||||
|
quint32 fieldLen = Endian::readSizedInt<quint32>(device, KeePass2::BYTEORDER, &ok);
|
||||||
|
if (!ok) {
|
||||||
|
raiseError("Invalid header field length");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray fieldData;
|
||||||
|
if (fieldLen != 0) {
|
||||||
|
fieldData = device->read(fieldLen);
|
||||||
|
if (static_cast<quint32>(fieldData.size()) != fieldLen) {
|
||||||
|
raiseError("Invalid header data length");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (fieldID) {
|
||||||
|
case KeePass2::EndOfHeader:
|
||||||
|
return false;
|
||||||
|
|
||||||
|
case KeePass2::CipherID:
|
||||||
|
setCipher(fieldData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeePass2::CompressionFlags:
|
||||||
|
setCompressionFlags(fieldData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeePass2::MasterSeed:
|
||||||
|
setMasterSeed(fieldData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeePass2::EncryptionIV:
|
||||||
|
setEncryptionIV(fieldData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeePass2::KdfParameters: {
|
||||||
|
QBuffer bufIoDevice(&fieldData);
|
||||||
|
if (!bufIoDevice.open(QIODevice::ReadOnly)) {
|
||||||
|
raiseError("Failed to open buffer for KDF parameters in header");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QVariantMap kdfParams = readVariantMap(&bufIoDevice);
|
||||||
|
QSharedPointer<Kdf> kdf = KeePass2::kdfFromParameters(kdfParams);
|
||||||
|
if (kdf == nullptr) {
|
||||||
|
raiseError("Invalid KDF parameters");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
m_db->setKdf(kdf);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case KeePass2::PublicCustomData:
|
||||||
|
m_db->setPublicCustomData(fieldData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeePass2::ProtectedStreamKey:
|
||||||
|
case KeePass2::TransformRounds:
|
||||||
|
case KeePass2::TransformSeed:
|
||||||
|
case KeePass2::StreamStartBytes:
|
||||||
|
case KeePass2::InnerRandomStreamID:
|
||||||
|
raiseError("Legacy header fields found in KDBX4 file.");
|
||||||
|
return false;
|
||||||
|
|
||||||
|
default:
|
||||||
|
qWarning("Unknown header field read: id=%d", fieldID);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Kdbx4Reader::readInnerHeaderField(QIODevice* device)
|
||||||
|
{
|
||||||
|
QByteArray fieldIDArray = device->read(1);
|
||||||
|
if (fieldIDArray.size() != 1) {
|
||||||
|
raiseError("Invalid inner header id size");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
KeePass2::InnerHeaderFieldID fieldID = static_cast<KeePass2::InnerHeaderFieldID>(fieldIDArray.at(0));
|
||||||
|
|
||||||
|
bool ok;
|
||||||
|
quint32 fieldLen = Endian::readSizedInt<quint32>(device, KeePass2::BYTEORDER, &ok);
|
||||||
|
if (!ok) {
|
||||||
|
raiseError("Invalid inner header field length");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray fieldData;
|
||||||
|
if (fieldLen != 0) {
|
||||||
|
fieldData = device->read(fieldLen);
|
||||||
|
if (static_cast<quint32>(fieldData.size()) != fieldLen) {
|
||||||
|
raiseError("Invalid header data length");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (fieldID) {
|
||||||
|
case KeePass2::InnerHeaderFieldID::End:
|
||||||
|
return false;
|
||||||
|
|
||||||
|
case KeePass2::InnerHeaderFieldID::InnerRandomStreamID:
|
||||||
|
setInnerRandomStreamID(fieldData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeePass2::InnerHeaderFieldID::InnerRandomStreamKey:
|
||||||
|
setProtectedStreamKey(fieldData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeePass2::InnerHeaderFieldID::Binary:
|
||||||
|
if (fieldLen < 1) {
|
||||||
|
raiseError("Invalid inner header binary size");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
m_binaryPool.insert(QString::number(m_binaryPool.size()), fieldData.mid(1));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
qWarning("Unknown inner header field read: id=%hhu", static_cast<quint8>(fieldID));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap Kdbx4Reader::readVariantMap(QIODevice* device)
|
||||||
|
{
|
||||||
|
bool ok;
|
||||||
|
quint16 version = Endian::readSizedInt<quint16>(device, KeePass2::BYTEORDER, &ok)
|
||||||
|
& KeePass2::VARIANTMAP_CRITICAL_MASK;
|
||||||
|
quint16 maxVersion = KeePass2::VARIANTMAP_VERSION & KeePass2::VARIANTMAP_CRITICAL_MASK;
|
||||||
|
if (!ok || (version > maxVersion)) {
|
||||||
|
raiseError(tr("Unsupported KeePass variant map version."));
|
||||||
|
return QVariantMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap vm;
|
||||||
|
QByteArray fieldTypeArray;
|
||||||
|
KeePass2::VariantMapFieldType fieldType;
|
||||||
|
while (((fieldTypeArray = device->read(1)).size() == 1)
|
||||||
|
&& ((fieldType = static_cast<KeePass2::VariantMapFieldType>(fieldTypeArray.at(0)))
|
||||||
|
!= KeePass2::VariantMapFieldType::End)) {
|
||||||
|
quint32 nameLen = Endian::readSizedInt<quint32>(device, KeePass2::BYTEORDER, &ok);
|
||||||
|
if (!ok) {
|
||||||
|
raiseError("Invalid variant map entry name length");
|
||||||
|
return QVariantMap();
|
||||||
|
}
|
||||||
|
QByteArray nameBytes;
|
||||||
|
if (nameLen != 0) {
|
||||||
|
nameBytes = device->read(nameLen);
|
||||||
|
if (static_cast<quint32>(nameBytes.size()) != nameLen) {
|
||||||
|
raiseError("Invalid variant map entry name data");
|
||||||
|
return QVariantMap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QString name = QString::fromUtf8(nameBytes);
|
||||||
|
|
||||||
|
quint32 valueLen = Endian::readSizedInt<quint32>(device, KeePass2::BYTEORDER, &ok);
|
||||||
|
if (!ok) {
|
||||||
|
raiseError("Invalid variant map entry value length");
|
||||||
|
return QVariantMap();
|
||||||
|
}
|
||||||
|
QByteArray valueBytes;
|
||||||
|
if (valueLen != 0) {
|
||||||
|
valueBytes = device->read(valueLen);
|
||||||
|
if (static_cast<quint32>(valueBytes.size()) != valueLen) {
|
||||||
|
raiseError("Invalid variant map entry value data");
|
||||||
|
return QVariantMap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (fieldType) {
|
||||||
|
case KeePass2::VariantMapFieldType::Bool:
|
||||||
|
if (valueLen == 1) {
|
||||||
|
vm.insert(name, QVariant(valueBytes.at(0) != 0));
|
||||||
|
} else {
|
||||||
|
raiseError("Invalid variant map Bool entry value length");
|
||||||
|
return QVariantMap();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case KeePass2::VariantMapFieldType::Int32:
|
||||||
|
if (valueLen == 4) {
|
||||||
|
vm.insert(name, QVariant(Endian::bytesToSizedInt<qint32>(valueBytes, KeePass2::BYTEORDER)));
|
||||||
|
} else {
|
||||||
|
raiseError("Invalid variant map Int32 entry value length");
|
||||||
|
return QVariantMap();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case KeePass2::VariantMapFieldType::UInt32:
|
||||||
|
if (valueLen == 4) {
|
||||||
|
vm.insert(name, QVariant(Endian::bytesToSizedInt<quint32>(valueBytes, KeePass2::BYTEORDER)));
|
||||||
|
} else {
|
||||||
|
raiseError("Invalid variant map UInt32 entry value length");
|
||||||
|
return QVariantMap();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case KeePass2::VariantMapFieldType::Int64:
|
||||||
|
if (valueLen == 8) {
|
||||||
|
vm.insert(name, QVariant(Endian::bytesToSizedInt<qint64>(valueBytes, KeePass2::BYTEORDER)));
|
||||||
|
} else {
|
||||||
|
raiseError("Invalid variant map Int64 entry value length");
|
||||||
|
return QVariantMap();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case KeePass2::VariantMapFieldType::UInt64:
|
||||||
|
if (valueLen == 8) {
|
||||||
|
vm.insert(name, QVariant(Endian::bytesToSizedInt<quint64>(valueBytes, KeePass2::BYTEORDER)));
|
||||||
|
} else {
|
||||||
|
raiseError("Invalid variant map UInt64 entry value length");
|
||||||
|
return QVariantMap();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case KeePass2::VariantMapFieldType::String:
|
||||||
|
vm.insert(name, QVariant(QString::fromUtf8(valueBytes)));
|
||||||
|
break;
|
||||||
|
case KeePass2::VariantMapFieldType::ByteArray:
|
||||||
|
vm.insert(name, QVariant(valueBytes));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
raiseError("Invalid variant map entry type");
|
||||||
|
return QVariantMap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldTypeArray.size() != 1) {
|
||||||
|
raiseError("Invalid variant map field type size");
|
||||||
|
return QVariantMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
return vm;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4Reader::setCipher(const QByteArray& data)
|
||||||
|
{
|
||||||
|
if (data.size() != Uuid::Length) {
|
||||||
|
raiseError("Invalid cipher uuid length");
|
||||||
|
} else {
|
||||||
|
Uuid uuid(data);
|
||||||
|
|
||||||
|
if (SymmetricCipher::cipherToAlgorithm(uuid) == SymmetricCipher::InvalidAlgorithm) {
|
||||||
|
raiseError("Unsupported cipher");
|
||||||
|
} else {
|
||||||
|
m_db->setCipher(uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4Reader::setCompressionFlags(const QByteArray& data)
|
||||||
|
{
|
||||||
|
if (data.size() != 4) {
|
||||||
|
raiseError("Invalid compression flags length");
|
||||||
|
} else {
|
||||||
|
quint32 id = Endian::bytesToSizedInt<quint32>(data, KeePass2::BYTEORDER);
|
||||||
|
|
||||||
|
if (id > Database::CompressionAlgorithmMax) {
|
||||||
|
raiseError("Unsupported compression algorithm");
|
||||||
|
} else {
|
||||||
|
m_db->setCompressionAlgo(static_cast<Database::CompressionAlgorithm>(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4Reader::setMasterSeed(const QByteArray& data)
|
||||||
|
{
|
||||||
|
if (data.size() != 32) {
|
||||||
|
raiseError("Invalid master seed size");
|
||||||
|
} else {
|
||||||
|
m_masterSeed = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4Reader::setEncryptionIV(const QByteArray& data)
|
||||||
|
{
|
||||||
|
m_encryptionIV = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4Reader::setProtectedStreamKey(const QByteArray& data)
|
||||||
|
{
|
||||||
|
m_protectedStreamKey = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4Reader::setInnerRandomStreamID(const QByteArray& data)
|
||||||
|
{
|
||||||
|
if (data.size() != 4) {
|
||||||
|
raiseError("Invalid random stream id size");
|
||||||
|
} else {
|
||||||
|
quint32 id = Endian::bytesToSizedInt<quint32>(data, KeePass2::BYTEORDER);
|
||||||
|
KeePass2::ProtectedStreamAlgo irsAlgo = KeePass2::idToProtectedStreamAlgo(id);
|
||||||
|
if (irsAlgo == KeePass2::InvalidProtectedStreamAlgo || irsAlgo == KeePass2::ArcFourVariant) {
|
||||||
|
raiseError("Invalid inner random stream cipher");
|
||||||
|
} else {
|
||||||
|
m_irsAlgo = irsAlgo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<QString, QByteArray> Kdbx4Reader::binaryPool()
|
||||||
|
{
|
||||||
|
return m_binaryPool;
|
||||||
|
}
|
66
src/format/Kdbx4Reader.h
Normal file
66
src/format/Kdbx4Reader.h
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef KEEPASSX_KDBX4READER_H
|
||||||
|
#define KEEPASSX_KDBX4READER_H
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QString>
|
||||||
|
#include <QByteArray>
|
||||||
|
|
||||||
|
#include "format/KeePass2.h"
|
||||||
|
#include "format/KeePass2Reader.h"
|
||||||
|
#include "crypto/SymmetricCipher.h"
|
||||||
|
#include "keys/CompositeKey.h"
|
||||||
|
|
||||||
|
class Database;
|
||||||
|
class QIODevice;
|
||||||
|
|
||||||
|
class Kdbx4Reader : public BaseKeePass2Reader
|
||||||
|
{
|
||||||
|
Q_DECLARE_TR_FUNCTIONS(Kdbx4Reader)
|
||||||
|
|
||||||
|
public:
|
||||||
|
Kdbx4Reader();
|
||||||
|
|
||||||
|
using BaseKeePass2Reader::readDatabase;
|
||||||
|
virtual Database* readDatabase(QIODevice* device, const CompositeKey& key, bool keepDatabase = false) override;
|
||||||
|
|
||||||
|
QHash<QString, QByteArray> binaryPool();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool readHeaderField(QIODevice* device);
|
||||||
|
bool readInnerHeaderField(QIODevice* device);
|
||||||
|
QVariantMap readVariantMap(QIODevice* device);
|
||||||
|
|
||||||
|
void setCipher(const QByteArray& data);
|
||||||
|
void setCompressionFlags(const QByteArray& data);
|
||||||
|
void setMasterSeed(const QByteArray& data);
|
||||||
|
void setEncryptionIV(const QByteArray& data);
|
||||||
|
void setProtectedStreamKey(const QByteArray& data);
|
||||||
|
void setInnerRandomStreamID(const QByteArray& data);
|
||||||
|
|
||||||
|
QIODevice* m_device;
|
||||||
|
|
||||||
|
Database* m_db;
|
||||||
|
QByteArray m_masterSeed;
|
||||||
|
QByteArray m_encryptionIV;
|
||||||
|
QHash<QString, QByteArray> m_binaryPool;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // KEEPASSX_KDBX4READER_H
|
326
src/format/Kdbx4Writer.cpp
Normal file
326
src/format/Kdbx4Writer.cpp
Normal file
|
@ -0,0 +1,326 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
|
||||||
|
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Kdbx4Writer.h"
|
||||||
|
|
||||||
|
#include <QBuffer>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QIODevice>
|
||||||
|
#include <QList>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "streams/HmacBlockStream.h"
|
||||||
|
#include "core/Database.h"
|
||||||
|
#include "core/Endian.h"
|
||||||
|
#include "crypto/CryptoHash.h"
|
||||||
|
#include "crypto/Random.h"
|
||||||
|
#include "format/KeePass2RandomStream.h"
|
||||||
|
#include "format/Kdbx4XmlWriter.h"
|
||||||
|
#include "streams/QtIOCompressor"
|
||||||
|
#include "streams/SymmetricCipherStream.h"
|
||||||
|
|
||||||
|
#define CHECK_RETURN_FALSE(x) if (!(x)) return false;
|
||||||
|
|
||||||
|
Kdbx4Writer::Kdbx4Writer()
|
||||||
|
: m_device(nullptr)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Kdbx4Writer::writeDatabase(QIODevice* device, Database* db)
|
||||||
|
{
|
||||||
|
m_error = false;
|
||||||
|
m_errorStr.clear();
|
||||||
|
|
||||||
|
SymmetricCipher::Algorithm algo = SymmetricCipher::cipherToAlgorithm(db->cipher());
|
||||||
|
if (algo == SymmetricCipher::InvalidAlgorithm) {
|
||||||
|
raiseError("Invalid symmetric cipher algorithm.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int ivSize = SymmetricCipher::algorithmIvSize(algo);
|
||||||
|
if (ivSize < 0) {
|
||||||
|
raiseError("Invalid symmetric cipher IV size.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray masterSeed = randomGen()->randomArray(32);
|
||||||
|
QByteArray encryptionIV = randomGen()->randomArray(ivSize);
|
||||||
|
QByteArray protectedStreamKey = randomGen()->randomArray(64);
|
||||||
|
QByteArray startBytes;
|
||||||
|
QByteArray endOfHeader = "\r\n\r\n";
|
||||||
|
|
||||||
|
if (db->challengeMasterSeed(masterSeed) == false) {
|
||||||
|
raiseError(tr("Unable to issue challenge-response."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!db->setKey(db->key(), false, true)) {
|
||||||
|
raiseError(tr("Unable to calculate master key"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CryptoHash hash(CryptoHash::Sha256);
|
||||||
|
hash.addData(masterSeed);
|
||||||
|
hash.addData(db->challengeResponseKey());
|
||||||
|
Q_ASSERT(!db->transformedMasterKey().isEmpty());
|
||||||
|
hash.addData(db->transformedMasterKey());
|
||||||
|
QByteArray finalKey = hash.result();
|
||||||
|
|
||||||
|
QByteArray headerData;
|
||||||
|
{
|
||||||
|
QBuffer header;
|
||||||
|
header.open(QIODevice::WriteOnly);
|
||||||
|
m_device = &header;
|
||||||
|
CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes(KeePass2::SIGNATURE_1, KeePass2::BYTEORDER)));
|
||||||
|
CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes(KeePass2::SIGNATURE_2, KeePass2::BYTEORDER)));
|
||||||
|
CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes(KeePass2::FILE_VERSION_4, KeePass2::BYTEORDER)));
|
||||||
|
CHECK_RETURN_FALSE(writeHeaderField(KeePass2::CipherID, db->cipher().toByteArray()));
|
||||||
|
CHECK_RETURN_FALSE(writeHeaderField(KeePass2::CompressionFlags,
|
||||||
|
Endian::sizedIntToBytes(static_cast<int>(db->compressionAlgo()),
|
||||||
|
KeePass2::BYTEORDER)));
|
||||||
|
CHECK_RETURN_FALSE(writeHeaderField(KeePass2::MasterSeed, masterSeed));
|
||||||
|
CHECK_RETURN_FALSE(writeHeaderField(KeePass2::EncryptionIV, encryptionIV));
|
||||||
|
|
||||||
|
// Convert current Kdf to basic parameters
|
||||||
|
QVariantMap kdfParams = KeePass2::kdfToParameters(db->kdf());
|
||||||
|
|
||||||
|
QByteArray kdfParamBytes;
|
||||||
|
if (!serializeVariantMap(kdfParams, kdfParamBytes)) {
|
||||||
|
raiseError("Failed to serialise KDF parameters variant map");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QByteArray publicCustomData = db->publicCustomData();
|
||||||
|
CHECK_RETURN_FALSE(writeHeaderField(KeePass2::KdfParameters, kdfParamBytes));
|
||||||
|
if (!publicCustomData.isEmpty()) {
|
||||||
|
CHECK_RETURN_FALSE(writeHeaderField(KeePass2::PublicCustomData, publicCustomData));
|
||||||
|
}
|
||||||
|
|
||||||
|
CHECK_RETURN_FALSE(writeHeaderField(KeePass2::EndOfHeader, endOfHeader));
|
||||||
|
header.close();
|
||||||
|
m_device = device;
|
||||||
|
headerData = header.data();
|
||||||
|
}
|
||||||
|
CHECK_RETURN_FALSE(writeData(headerData));
|
||||||
|
QByteArray headerHash = CryptoHash::hash(headerData, CryptoHash::Sha256);
|
||||||
|
|
||||||
|
QScopedPointer<QIODevice> firstLayer, secondLayer;
|
||||||
|
|
||||||
|
QByteArray hmacKey = KeePass2::hmacKey(masterSeed, db->transformedMasterKey());
|
||||||
|
QByteArray headerHmac = CryptoHash::hmac(headerData, HmacBlockStream::getHmacKey(UINT64_MAX, hmacKey),
|
||||||
|
CryptoHash::Sha256);
|
||||||
|
CHECK_RETURN_FALSE(writeData(headerHash));
|
||||||
|
CHECK_RETURN_FALSE(writeData(headerHmac));
|
||||||
|
|
||||||
|
HmacBlockStream* hmacStream = new HmacBlockStream(device, hmacKey);
|
||||||
|
if (!hmacStream->open(QIODevice::WriteOnly)) {
|
||||||
|
raiseError(hmacStream->errorString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
firstLayer.reset(static_cast<QIODevice*>(hmacStream));
|
||||||
|
|
||||||
|
SymmetricCipherStream* cipherStream = new SymmetricCipherStream(hmacStream, algo,
|
||||||
|
SymmetricCipher::algorithmMode(algo),
|
||||||
|
SymmetricCipher::Encrypt);
|
||||||
|
if (!cipherStream->init(finalKey, encryptionIV)) {
|
||||||
|
raiseError(cipherStream->errorString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!cipherStream->open(QIODevice::WriteOnly)) {
|
||||||
|
raiseError(cipherStream->errorString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
secondLayer.reset(static_cast<QIODevice*>(cipherStream));
|
||||||
|
|
||||||
|
QScopedPointer<QtIOCompressor> ioCompressor;
|
||||||
|
if (db->compressionAlgo() == Database::CompressionNone) {
|
||||||
|
m_device = secondLayer.data();
|
||||||
|
} else {
|
||||||
|
ioCompressor.reset(new QtIOCompressor(secondLayer.data()));
|
||||||
|
ioCompressor->setStreamFormat(QtIOCompressor::GzipFormat);
|
||||||
|
if (!ioCompressor->open(QIODevice::WriteOnly)) {
|
||||||
|
raiseError(ioCompressor->errorString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
m_device = ioCompressor.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<QByteArray, int> idMap;
|
||||||
|
|
||||||
|
CHECK_RETURN_FALSE(writeInnerHeaderField(KeePass2::InnerHeaderFieldID::InnerRandomStreamID,
|
||||||
|
Endian::sizedIntToBytes(static_cast<int>(KeePass2::ChaCha20),
|
||||||
|
KeePass2::BYTEORDER)));
|
||||||
|
CHECK_RETURN_FALSE(writeInnerHeaderField(KeePass2::InnerHeaderFieldID::InnerRandomStreamKey,
|
||||||
|
protectedStreamKey));
|
||||||
|
const QList<Entry*> allEntries = db->rootGroup()->entriesRecursive(true);
|
||||||
|
int nextId = 0;
|
||||||
|
|
||||||
|
for (Entry* entry : allEntries) {
|
||||||
|
const QList<QString> attachmentKeys = entry->attachments()->keys();
|
||||||
|
for (const QString& key : attachmentKeys) {
|
||||||
|
QByteArray data = entry->attachments()->value(key);
|
||||||
|
if (!idMap.contains(data)) {
|
||||||
|
CHECK_RETURN_FALSE(writeBinary(data));
|
||||||
|
idMap.insert(data, nextId++);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CHECK_RETURN_FALSE(writeInnerHeaderField(KeePass2::InnerHeaderFieldID::End, QByteArray()));
|
||||||
|
|
||||||
|
KeePass2RandomStream randomStream(KeePass2::ChaCha20);
|
||||||
|
if (!randomStream.init(protectedStreamKey)) {
|
||||||
|
raiseError(randomStream.errorString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Kdbx4XmlWriter xmlWriter(KeePass2::FILE_VERSION_4, idMap);
|
||||||
|
xmlWriter.writeDatabase(m_device, db, &randomStream, headerHash);
|
||||||
|
|
||||||
|
// Explicitly close/reset streams so they are flushed and we can detect
|
||||||
|
// errors. QIODevice::close() resets errorString() etc.
|
||||||
|
if (ioCompressor) {
|
||||||
|
ioCompressor->close();
|
||||||
|
}
|
||||||
|
if (!secondLayer->reset()) {
|
||||||
|
raiseError(secondLayer->errorString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!firstLayer->reset()) {
|
||||||
|
raiseError(firstLayer->errorString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xmlWriter.hasError()) {
|
||||||
|
raiseError(xmlWriter.errorString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Kdbx4Writer::writeData(const QByteArray& data)
|
||||||
|
{
|
||||||
|
if (m_device->write(data) != data.size()) {
|
||||||
|
raiseError(m_device->errorString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Kdbx4Writer::writeHeaderField(KeePass2::HeaderFieldID fieldId, const QByteArray& data)
|
||||||
|
{
|
||||||
|
QByteArray fieldIdArr;
|
||||||
|
fieldIdArr[0] = fieldId;
|
||||||
|
CHECK_RETURN_FALSE(writeData(fieldIdArr));
|
||||||
|
CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes(static_cast<quint32>(data.size()), KeePass2::BYTEORDER)));
|
||||||
|
CHECK_RETURN_FALSE(writeData(data));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Kdbx4Writer::writeInnerHeaderField(KeePass2::InnerHeaderFieldID fieldId, const QByteArray& data)
|
||||||
|
{
|
||||||
|
QByteArray fieldIdArr;
|
||||||
|
fieldIdArr[0] = static_cast<char>(fieldId);
|
||||||
|
CHECK_RETURN_FALSE(writeData(fieldIdArr));
|
||||||
|
CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes(static_cast<quint32>(data.size()), KeePass2::BYTEORDER)));
|
||||||
|
CHECK_RETURN_FALSE(writeData(data));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Kdbx4Writer::writeBinary(const QByteArray& data)
|
||||||
|
{
|
||||||
|
QByteArray fieldIdArr;
|
||||||
|
fieldIdArr[0] = static_cast<char>(KeePass2::InnerHeaderFieldID::Binary);
|
||||||
|
CHECK_RETURN_FALSE(writeData(fieldIdArr));
|
||||||
|
CHECK_RETURN_FALSE(writeData(Endian::sizedIntToBytes(static_cast<quint32>(data.size() + 1), KeePass2::BYTEORDER)));
|
||||||
|
CHECK_RETURN_FALSE(writeData(QByteArray(1, '\1')));
|
||||||
|
CHECK_RETURN_FALSE(writeData(data));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Kdbx4Writer::serializeVariantMap(const QVariantMap& p, QByteArray& o)
|
||||||
|
{
|
||||||
|
QBuffer buf(&o);
|
||||||
|
buf.open(QIODevice::WriteOnly);
|
||||||
|
CHECK_RETURN_FALSE(buf.write(Endian::sizedIntToBytes(KeePass2::VARIANTMAP_VERSION, KeePass2::BYTEORDER)) == 2);
|
||||||
|
|
||||||
|
bool ok;
|
||||||
|
QList<QString> keys = p.keys();
|
||||||
|
for (int i = 0; i < keys.size(); ++i) {
|
||||||
|
QString k = keys.at(i);
|
||||||
|
KeePass2::VariantMapFieldType fieldType;
|
||||||
|
QByteArray data;
|
||||||
|
QVariant v = p.value(k);
|
||||||
|
switch (static_cast<QMetaType::Type>(v.type())) {
|
||||||
|
case QMetaType::Type::Int:
|
||||||
|
fieldType = KeePass2::VariantMapFieldType::Int32;
|
||||||
|
data = Endian::sizedIntToBytes(v.toInt(&ok), KeePass2::BYTEORDER);
|
||||||
|
CHECK_RETURN_FALSE(ok);
|
||||||
|
break;
|
||||||
|
case QMetaType::Type::UInt:
|
||||||
|
fieldType = KeePass2::VariantMapFieldType::UInt32;
|
||||||
|
data = Endian::sizedIntToBytes(v.toUInt(&ok), KeePass2::BYTEORDER);
|
||||||
|
CHECK_RETURN_FALSE(ok);
|
||||||
|
break;
|
||||||
|
case QMetaType::Type::LongLong:
|
||||||
|
fieldType = KeePass2::VariantMapFieldType::Int64;
|
||||||
|
data = Endian::sizedIntToBytes(v.toLongLong(&ok), KeePass2::BYTEORDER);
|
||||||
|
CHECK_RETURN_FALSE(ok);
|
||||||
|
break;
|
||||||
|
case QMetaType::Type::ULongLong:
|
||||||
|
fieldType = KeePass2::VariantMapFieldType::UInt64;
|
||||||
|
data = Endian::sizedIntToBytes(v.toULongLong(&ok), KeePass2::BYTEORDER);
|
||||||
|
CHECK_RETURN_FALSE(ok);
|
||||||
|
break;
|
||||||
|
case QMetaType::Type::QString:
|
||||||
|
fieldType = KeePass2::VariantMapFieldType::String;
|
||||||
|
data = v.toString().toUtf8();
|
||||||
|
break;
|
||||||
|
case QMetaType::Type::Bool:
|
||||||
|
fieldType = KeePass2::VariantMapFieldType::Bool;
|
||||||
|
data = QByteArray(1, (v.toBool() ? '\1' : '\0'));
|
||||||
|
break;
|
||||||
|
case QMetaType::Type::QByteArray:
|
||||||
|
fieldType = KeePass2::VariantMapFieldType::ByteArray;
|
||||||
|
data = v.toByteArray();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
qWarning("Unknown object type %d in QVariantMap", v.type());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QByteArray typeBytes;
|
||||||
|
typeBytes[0] = static_cast<char>(fieldType);
|
||||||
|
QByteArray nameBytes = k.toUtf8();
|
||||||
|
QByteArray nameLenBytes = Endian::sizedIntToBytes(nameBytes.size(), KeePass2::BYTEORDER);
|
||||||
|
QByteArray dataLenBytes = Endian::sizedIntToBytes(data.size(), KeePass2::BYTEORDER);
|
||||||
|
|
||||||
|
CHECK_RETURN_FALSE(buf.write(typeBytes) == 1);
|
||||||
|
CHECK_RETURN_FALSE(buf.write(nameLenBytes) == 4);
|
||||||
|
CHECK_RETURN_FALSE(buf.write(nameBytes) == nameBytes.size());
|
||||||
|
CHECK_RETURN_FALSE(buf.write(dataLenBytes) == 4);
|
||||||
|
CHECK_RETURN_FALSE(buf.write(data) == data.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray endBytes;
|
||||||
|
endBytes[0] = static_cast<char>(KeePass2::VariantMapFieldType::End);
|
||||||
|
CHECK_RETURN_FALSE(buf.write(endBytes) == 1);
|
||||||
|
return true;
|
||||||
|
}
|
52
src/format/Kdbx4Writer.h
Normal file
52
src/format/Kdbx4Writer.h
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef KEEPASSX_KDBX4WRITER_H
|
||||||
|
#define KEEPASSX_KDBX4WRITER_H
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
|
||||||
|
#include "format/KeePass2.h"
|
||||||
|
#include "format/KeePass2Writer.h"
|
||||||
|
#include "keys/CompositeKey.h"
|
||||||
|
|
||||||
|
class Database;
|
||||||
|
class QIODevice;
|
||||||
|
|
||||||
|
class Kdbx4Writer : public BaseKeePass2Writer
|
||||||
|
{
|
||||||
|
Q_DECLARE_TR_FUNCTIONS(Kdbx4Writer)
|
||||||
|
|
||||||
|
public:
|
||||||
|
Kdbx4Writer();
|
||||||
|
|
||||||
|
using BaseKeePass2Writer::writeDatabase;
|
||||||
|
bool writeDatabase(QIODevice* device, Database* db);
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool writeData(const QByteArray& data);
|
||||||
|
bool writeHeaderField(KeePass2::HeaderFieldID fieldId, const QByteArray& data);
|
||||||
|
bool writeInnerHeaderField(KeePass2::InnerHeaderFieldID fieldId, const QByteArray& data);
|
||||||
|
|
||||||
|
QIODevice* m_device;
|
||||||
|
|
||||||
|
bool writeBinary(const QByteArray& data);
|
||||||
|
|
||||||
|
static bool serializeVariantMap(const QVariantMap& p, QByteArray& o);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // KEEPASSX_KDBX4WRITER_H
|
1080
src/format/Kdbx4XmlReader.cpp
Normal file
1080
src/format/Kdbx4XmlReader.cpp
Normal file
File diff suppressed because it is too large
Load diff
102
src/format/Kdbx4XmlReader.h
Normal file
102
src/format/Kdbx4XmlReader.h
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef KEEPASSX_KDBX4XMLREADER_H
|
||||||
|
#define KEEPASSX_KDBX4XMLREADER_H
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QPair>
|
||||||
|
#include <QXmlStreamReader>
|
||||||
|
|
||||||
|
#include "core/TimeInfo.h"
|
||||||
|
#include "core/Uuid.h"
|
||||||
|
|
||||||
|
class Database;
|
||||||
|
class Entry;
|
||||||
|
class Group;
|
||||||
|
class KeePass2RandomStream;
|
||||||
|
class Metadata;
|
||||||
|
|
||||||
|
class Kdbx4XmlReader
|
||||||
|
{
|
||||||
|
Q_DECLARE_TR_FUNCTIONS(Kdbx4XmlReader)
|
||||||
|
|
||||||
|
public:
|
||||||
|
Kdbx4XmlReader();
|
||||||
|
Kdbx4XmlReader(QHash<QString, QByteArray>& binaryPool);
|
||||||
|
Database* readDatabase(QIODevice* device);
|
||||||
|
void readDatabase(QIODevice* device, Database* db, KeePass2RandomStream* randomStream = nullptr);
|
||||||
|
Database* readDatabase(const QString& filename);
|
||||||
|
bool hasError();
|
||||||
|
QString errorString();
|
||||||
|
QByteArray headerHash();
|
||||||
|
void setStrictMode(bool strictMode);
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool parseKeePassFile();
|
||||||
|
void parseMeta();
|
||||||
|
void parseMemoryProtection();
|
||||||
|
void parseCustomIcons();
|
||||||
|
void parseIcon();
|
||||||
|
void parseBinaries();
|
||||||
|
void parseCustomData();
|
||||||
|
void parseCustomDataItem();
|
||||||
|
bool parseRoot();
|
||||||
|
Group* parseGroup();
|
||||||
|
void parseDeletedObjects();
|
||||||
|
void parseDeletedObject();
|
||||||
|
Entry* parseEntry(bool history);
|
||||||
|
void parseEntryString(Entry* entry);
|
||||||
|
QPair<QString, QString> parseEntryBinary(Entry* entry);
|
||||||
|
void parseAutoType(Entry* entry);
|
||||||
|
void parseAutoTypeAssoc(Entry* entry);
|
||||||
|
QList<Entry*> parseEntryHistory();
|
||||||
|
TimeInfo parseTimes();
|
||||||
|
|
||||||
|
QString readString();
|
||||||
|
bool readBool();
|
||||||
|
QDateTime readDateTime();
|
||||||
|
QColor readColor();
|
||||||
|
int readNumber();
|
||||||
|
Uuid readUuid();
|
||||||
|
QByteArray readBinary();
|
||||||
|
QByteArray readCompressedBinary();
|
||||||
|
|
||||||
|
Group* getGroup(const Uuid& uuid);
|
||||||
|
Entry* getEntry(const Uuid& uuid);
|
||||||
|
void raiseError(const QString& errorMessage);
|
||||||
|
void skipCurrentElement();
|
||||||
|
|
||||||
|
QXmlStreamReader m_xml;
|
||||||
|
KeePass2RandomStream* m_randomStream;
|
||||||
|
Database* m_db;
|
||||||
|
Metadata* m_meta;
|
||||||
|
Group* m_tmpParent;
|
||||||
|
QHash<Uuid, Group*> m_groups;
|
||||||
|
QHash<Uuid, Entry*> m_entries;
|
||||||
|
QHash<QString, QByteArray> m_binaryPool;
|
||||||
|
QHash<QString, QPair<Entry*, QString> > m_binaryMap;
|
||||||
|
QByteArray m_headerHash;
|
||||||
|
bool m_error;
|
||||||
|
QString m_errorStr;
|
||||||
|
bool m_strictMode;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // KEEPASSX_KDBX4XMLREADER_H
|
611
src/format/Kdbx4XmlWriter.cpp
Normal file
611
src/format/Kdbx4XmlWriter.cpp
Normal file
|
@ -0,0 +1,611 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Kdbx4XmlWriter.h"
|
||||||
|
|
||||||
|
#include <QBuffer>
|
||||||
|
#include <QFile>
|
||||||
|
|
||||||
|
#include "core/Endian.h"
|
||||||
|
#include "core/Metadata.h"
|
||||||
|
#include "format/KeePass2RandomStream.h"
|
||||||
|
#include "streams/QtIOCompressor"
|
||||||
|
|
||||||
|
Kdbx4XmlWriter::Kdbx4XmlWriter()
|
||||||
|
: Kdbx4XmlWriter(KeePass2::FILE_VERSION_3)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Kdbx4XmlWriter::Kdbx4XmlWriter(quint32 version)
|
||||||
|
: Kdbx4XmlWriter(version, QHash<QByteArray, int>())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Kdbx4XmlWriter::Kdbx4XmlWriter(quint32 version, QHash<QByteArray, int> idMap)
|
||||||
|
: m_db(nullptr)
|
||||||
|
, m_meta(nullptr)
|
||||||
|
, m_randomStream(nullptr)
|
||||||
|
, m_idMap(idMap)
|
||||||
|
, m_error(false)
|
||||||
|
, m_version(version)
|
||||||
|
{
|
||||||
|
m_xml.setAutoFormatting(true);
|
||||||
|
m_xml.setAutoFormattingIndent(-1); // 1 tab
|
||||||
|
m_xml.setCodec("UTF-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeDatabase(QIODevice* device, Database* db, KeePass2RandomStream* randomStream, const QByteArray& headerHash)
|
||||||
|
{
|
||||||
|
m_db = db;
|
||||||
|
m_meta = db->metadata();
|
||||||
|
m_randomStream = randomStream;
|
||||||
|
m_headerHash = headerHash;
|
||||||
|
|
||||||
|
if (m_version < KeePass2::FILE_VERSION_4 && m_idMap.isEmpty()) {
|
||||||
|
generateIdMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_xml.setDevice(device);
|
||||||
|
|
||||||
|
m_xml.writeStartDocument("1.0", true);
|
||||||
|
|
||||||
|
m_xml.writeStartElement("KeePassFile");
|
||||||
|
|
||||||
|
writeMetadata();
|
||||||
|
writeRoot();
|
||||||
|
|
||||||
|
m_xml.writeEndElement();
|
||||||
|
|
||||||
|
m_xml.writeEndDocument();
|
||||||
|
|
||||||
|
if (m_xml.hasError()) {
|
||||||
|
raiseError(device->errorString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeDatabase(const QString& filename, Database* db)
|
||||||
|
{
|
||||||
|
QFile file(filename);
|
||||||
|
file.open(QIODevice::WriteOnly|QIODevice::Truncate);
|
||||||
|
writeDatabase(&file, db);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Kdbx4XmlWriter::hasError()
|
||||||
|
{
|
||||||
|
return m_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Kdbx4XmlWriter::errorString()
|
||||||
|
{
|
||||||
|
return m_errorStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::generateIdMap()
|
||||||
|
{
|
||||||
|
const QList<Entry*> allEntries = m_db->rootGroup()->entriesRecursive(true);
|
||||||
|
int nextId = 0;
|
||||||
|
|
||||||
|
for (Entry* entry : allEntries) {
|
||||||
|
const QList<QString> attachmentKeys = entry->attachments()->keys();
|
||||||
|
for (const QString& key : attachmentKeys) {
|
||||||
|
QByteArray data = entry->attachments()->value(key);
|
||||||
|
if (!m_idMap.contains(data)) {
|
||||||
|
m_idMap.insert(data, nextId++);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeMetadata()
|
||||||
|
{
|
||||||
|
m_xml.writeStartElement("Meta");
|
||||||
|
writeString("Generator", m_meta->generator());
|
||||||
|
if (m_version < KeePass2::FILE_VERSION_4 && !m_headerHash.isEmpty()) {
|
||||||
|
writeBinary("HeaderHash", m_headerHash);
|
||||||
|
}
|
||||||
|
writeString("DatabaseName", m_meta->name());
|
||||||
|
writeDateTime("DatabaseNameChanged", m_meta->nameChanged());
|
||||||
|
writeString("DatabaseDescription", m_meta->description());
|
||||||
|
writeDateTime("DatabaseDescriptionChanged", m_meta->descriptionChanged());
|
||||||
|
writeString("DefaultUserName", m_meta->defaultUserName());
|
||||||
|
writeDateTime("DefaultUserNameChanged", m_meta->defaultUserNameChanged());
|
||||||
|
writeNumber("MaintenanceHistoryDays", m_meta->maintenanceHistoryDays());
|
||||||
|
writeColor("Color", m_meta->color());
|
||||||
|
writeDateTime("MasterKeyChanged", m_meta->masterKeyChanged());
|
||||||
|
writeNumber("MasterKeyChangeRec", m_meta->masterKeyChangeRec());
|
||||||
|
writeNumber("MasterKeyChangeForce", m_meta->masterKeyChangeForce());
|
||||||
|
writeMemoryProtection();
|
||||||
|
writeCustomIcons();
|
||||||
|
writeBool("RecycleBinEnabled", m_meta->recycleBinEnabled());
|
||||||
|
writeUuid("RecycleBinUUID", m_meta->recycleBin());
|
||||||
|
writeDateTime("RecycleBinChanged", m_meta->recycleBinChanged());
|
||||||
|
writeUuid("EntryTemplatesGroup", m_meta->entryTemplatesGroup());
|
||||||
|
writeDateTime("EntryTemplatesGroupChanged", m_meta->entryTemplatesGroupChanged());
|
||||||
|
writeUuid("LastSelectedGroup", m_meta->lastSelectedGroup());
|
||||||
|
writeUuid("LastTopVisibleGroup", m_meta->lastTopVisibleGroup());
|
||||||
|
writeNumber("HistoryMaxItems", m_meta->historyMaxItems());
|
||||||
|
writeNumber("HistoryMaxSize", m_meta->historyMaxSize());
|
||||||
|
if (m_version >= KeePass2::FILE_VERSION_4) {
|
||||||
|
writeDateTime("SettingsChanged", m_meta->settingsChanged());
|
||||||
|
}
|
||||||
|
if (m_version < KeePass2::FILE_VERSION_4) {
|
||||||
|
writeBinaries();
|
||||||
|
}
|
||||||
|
writeCustomData();
|
||||||
|
|
||||||
|
m_xml.writeEndElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeMemoryProtection()
|
||||||
|
{
|
||||||
|
m_xml.writeStartElement("MemoryProtection");
|
||||||
|
|
||||||
|
writeBool("ProtectTitle", m_meta->protectTitle());
|
||||||
|
writeBool("ProtectUserName", m_meta->protectUsername());
|
||||||
|
writeBool("ProtectPassword", m_meta->protectPassword());
|
||||||
|
writeBool("ProtectURL", m_meta->protectUrl());
|
||||||
|
writeBool("ProtectNotes", m_meta->protectNotes());
|
||||||
|
|
||||||
|
m_xml.writeEndElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeCustomIcons()
|
||||||
|
{
|
||||||
|
m_xml.writeStartElement("CustomIcons");
|
||||||
|
|
||||||
|
const QList<Uuid> customIconsOrder = m_meta->customIconsOrder();
|
||||||
|
for (const Uuid& uuid : customIconsOrder) {
|
||||||
|
writeIcon(uuid, m_meta->customIcon(uuid));
|
||||||
|
}
|
||||||
|
|
||||||
|
m_xml.writeEndElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeIcon(const Uuid& uuid, const QImage& icon)
|
||||||
|
{
|
||||||
|
m_xml.writeStartElement("Icon");
|
||||||
|
|
||||||
|
writeUuid("UUID", uuid);
|
||||||
|
|
||||||
|
QByteArray ba;
|
||||||
|
QBuffer buffer(&ba);
|
||||||
|
buffer.open(QIODevice::WriteOnly);
|
||||||
|
// TODO: check !icon.save()
|
||||||
|
icon.save(&buffer, "PNG");
|
||||||
|
buffer.close();
|
||||||
|
writeBinary("Data", ba);
|
||||||
|
|
||||||
|
m_xml.writeEndElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeBinaries()
|
||||||
|
{
|
||||||
|
m_xml.writeStartElement("Binaries");
|
||||||
|
|
||||||
|
QHash<QByteArray, int>::const_iterator i;
|
||||||
|
for (i = m_idMap.constBegin(); i != m_idMap.constEnd(); ++i) {
|
||||||
|
m_xml.writeStartElement("Binary");
|
||||||
|
|
||||||
|
m_xml.writeAttribute("ID", QString::number(i.value()));
|
||||||
|
|
||||||
|
QByteArray data;
|
||||||
|
if (m_db->compressionAlgo() == Database::CompressionGZip) {
|
||||||
|
m_xml.writeAttribute("Compressed", "True");
|
||||||
|
|
||||||
|
QBuffer buffer;
|
||||||
|
buffer.open(QIODevice::ReadWrite);
|
||||||
|
|
||||||
|
QtIOCompressor compressor(&buffer);
|
||||||
|
compressor.setStreamFormat(QtIOCompressor::GzipFormat);
|
||||||
|
compressor.open(QIODevice::WriteOnly);
|
||||||
|
|
||||||
|
qint64 bytesWritten = compressor.write(i.key());
|
||||||
|
Q_ASSERT(bytesWritten == i.key().size());
|
||||||
|
Q_UNUSED(bytesWritten);
|
||||||
|
compressor.close();
|
||||||
|
|
||||||
|
buffer.seek(0);
|
||||||
|
data = buffer.readAll();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
data = i.key();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.isEmpty()) {
|
||||||
|
m_xml.writeCharacters(QString::fromLatin1(data.toBase64()));
|
||||||
|
}
|
||||||
|
m_xml.writeEndElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_xml.writeEndElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeCustomData()
|
||||||
|
{
|
||||||
|
m_xml.writeStartElement("CustomData");
|
||||||
|
|
||||||
|
QHash<QString, QString> customFields = m_meta->customFields();
|
||||||
|
const QList<QString> keyList = customFields.keys();
|
||||||
|
for (const QString& key : keyList) {
|
||||||
|
writeCustomDataItem(key, customFields.value(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
m_xml.writeEndElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeCustomDataItem(const QString& key, const QString& value)
|
||||||
|
{
|
||||||
|
m_xml.writeStartElement("Item");
|
||||||
|
|
||||||
|
writeString("Key", key);
|
||||||
|
writeString("Value", value);
|
||||||
|
|
||||||
|
m_xml.writeEndElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeRoot()
|
||||||
|
{
|
||||||
|
Q_ASSERT(m_db->rootGroup());
|
||||||
|
|
||||||
|
m_xml.writeStartElement("Root");
|
||||||
|
|
||||||
|
writeGroup(m_db->rootGroup());
|
||||||
|
writeDeletedObjects();
|
||||||
|
|
||||||
|
m_xml.writeEndElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeGroup(const Group* group)
|
||||||
|
{
|
||||||
|
Q_ASSERT(!group->uuid().isNull());
|
||||||
|
|
||||||
|
m_xml.writeStartElement("Group");
|
||||||
|
|
||||||
|
writeUuid("UUID", group->uuid());
|
||||||
|
writeString("Name", group->name());
|
||||||
|
writeString("Notes", group->notes());
|
||||||
|
writeNumber("IconID", group->iconNumber());
|
||||||
|
|
||||||
|
if (!group->iconUuid().isNull()) {
|
||||||
|
writeUuid("CustomIconUUID", group->iconUuid());
|
||||||
|
}
|
||||||
|
writeTimes(group->timeInfo());
|
||||||
|
writeBool("IsExpanded", group->isExpanded());
|
||||||
|
writeString("DefaultAutoTypeSequence", group->defaultAutoTypeSequence());
|
||||||
|
|
||||||
|
writeTriState("EnableAutoType", group->autoTypeEnabled());
|
||||||
|
|
||||||
|
writeTriState("EnableSearching", group->searchingEnabled());
|
||||||
|
|
||||||
|
writeUuid("LastTopVisibleEntry", group->lastTopVisibleEntry());
|
||||||
|
|
||||||
|
const QList<Entry*> entryList = group->entries();
|
||||||
|
for (const Entry* entry : entryList) {
|
||||||
|
writeEntry(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QList<Group*> children = group->children();
|
||||||
|
for (const Group* child : children) {
|
||||||
|
writeGroup(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_xml.writeEndElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeTimes(const TimeInfo& ti)
|
||||||
|
{
|
||||||
|
m_xml.writeStartElement("Times");
|
||||||
|
|
||||||
|
writeDateTime("LastModificationTime", ti.lastModificationTime());
|
||||||
|
writeDateTime("CreationTime", ti.creationTime());
|
||||||
|
writeDateTime("LastAccessTime", ti.lastAccessTime());
|
||||||
|
writeDateTime("ExpiryTime", ti.expiryTime());
|
||||||
|
writeBool("Expires", ti.expires());
|
||||||
|
writeNumber("UsageCount", ti.usageCount());
|
||||||
|
writeDateTime("LocationChanged", ti.locationChanged());
|
||||||
|
|
||||||
|
m_xml.writeEndElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeDeletedObjects()
|
||||||
|
{
|
||||||
|
m_xml.writeStartElement("DeletedObjects");
|
||||||
|
|
||||||
|
const QList<DeletedObject> delObjList = m_db->deletedObjects();
|
||||||
|
for (const DeletedObject& delObj : delObjList) {
|
||||||
|
writeDeletedObject(delObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_xml.writeEndElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeDeletedObject(const DeletedObject& delObj)
|
||||||
|
{
|
||||||
|
m_xml.writeStartElement("DeletedObject");
|
||||||
|
|
||||||
|
writeUuid("UUID", delObj.uuid);
|
||||||
|
writeDateTime("DeletionTime", delObj.deletionTime);
|
||||||
|
|
||||||
|
m_xml.writeEndElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeEntry(const Entry* entry)
|
||||||
|
{
|
||||||
|
Q_ASSERT(!entry->uuid().isNull());
|
||||||
|
|
||||||
|
m_xml.writeStartElement("Entry");
|
||||||
|
|
||||||
|
writeUuid("UUID", entry->uuid());
|
||||||
|
writeNumber("IconID", entry->iconNumber());
|
||||||
|
if (!entry->iconUuid().isNull()) {
|
||||||
|
writeUuid("CustomIconUUID", entry->iconUuid());
|
||||||
|
}
|
||||||
|
writeColor("ForegroundColor", entry->foregroundColor());
|
||||||
|
writeColor("BackgroundColor", entry->backgroundColor());
|
||||||
|
writeString("OverrideURL", entry->overrideUrl());
|
||||||
|
writeString("Tags", entry->tags());
|
||||||
|
writeTimes(entry->timeInfo());
|
||||||
|
|
||||||
|
const QList<QString> attributesKeyList = entry->attributes()->keys();
|
||||||
|
for (const QString& key : attributesKeyList) {
|
||||||
|
m_xml.writeStartElement("String");
|
||||||
|
|
||||||
|
bool protect = ( ((key == "Title") && m_meta->protectTitle()) ||
|
||||||
|
((key == "UserName") && m_meta->protectUsername()) ||
|
||||||
|
((key == "Password") && m_meta->protectPassword()) ||
|
||||||
|
((key == "URL") && m_meta->protectUrl()) ||
|
||||||
|
((key == "Notes") && m_meta->protectNotes()) ||
|
||||||
|
entry->attributes()->isProtected(key) );
|
||||||
|
|
||||||
|
writeString("Key", key);
|
||||||
|
|
||||||
|
m_xml.writeStartElement("Value");
|
||||||
|
QString value;
|
||||||
|
|
||||||
|
if (protect) {
|
||||||
|
if (m_randomStream) {
|
||||||
|
m_xml.writeAttribute("Protected", "True");
|
||||||
|
bool ok;
|
||||||
|
QByteArray rawData = m_randomStream->process(entry->attributes()->value(key).toUtf8(), &ok);
|
||||||
|
if (!ok) {
|
||||||
|
raiseError(m_randomStream->errorString());
|
||||||
|
}
|
||||||
|
value = QString::fromLatin1(rawData.toBase64());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_xml.writeAttribute("ProtectInMemory", "True");
|
||||||
|
value = entry->attributes()->value(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
value = entry->attributes()->value(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value.isEmpty()) {
|
||||||
|
m_xml.writeCharacters(stripInvalidXml10Chars(value));
|
||||||
|
}
|
||||||
|
m_xml.writeEndElement();
|
||||||
|
|
||||||
|
m_xml.writeEndElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
const QList<QString> attachmentsKeyList = entry->attachments()->keys();
|
||||||
|
for (const QString& key : attachmentsKeyList) {
|
||||||
|
m_xml.writeStartElement("Binary");
|
||||||
|
|
||||||
|
writeString("Key", key);
|
||||||
|
|
||||||
|
m_xml.writeStartElement("Value");
|
||||||
|
m_xml.writeAttribute("Ref", QString::number(m_idMap[entry->attachments()->value(key)]));
|
||||||
|
m_xml.writeEndElement();
|
||||||
|
|
||||||
|
m_xml.writeEndElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
writeAutoType(entry);
|
||||||
|
// write history only for entries that are not history items
|
||||||
|
if (entry->parent()) {
|
||||||
|
writeEntryHistory(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_xml.writeEndElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeAutoType(const Entry* entry)
|
||||||
|
{
|
||||||
|
m_xml.writeStartElement("AutoType");
|
||||||
|
|
||||||
|
writeBool("Enabled", entry->autoTypeEnabled());
|
||||||
|
writeNumber("DataTransferObfuscation", entry->autoTypeObfuscation());
|
||||||
|
writeString("DefaultSequence", entry->defaultAutoTypeSequence());
|
||||||
|
|
||||||
|
const QList<AutoTypeAssociations::Association> autoTypeAssociations = entry->autoTypeAssociations()->getAll();
|
||||||
|
for (const AutoTypeAssociations::Association& assoc : autoTypeAssociations) {
|
||||||
|
writeAutoTypeAssoc(assoc);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_xml.writeEndElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeAutoTypeAssoc(const AutoTypeAssociations::Association& assoc)
|
||||||
|
{
|
||||||
|
m_xml.writeStartElement("Association");
|
||||||
|
|
||||||
|
writeString("Window", assoc.window);
|
||||||
|
writeString("KeystrokeSequence", assoc.sequence);
|
||||||
|
|
||||||
|
m_xml.writeEndElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeEntryHistory(const Entry* entry)
|
||||||
|
{
|
||||||
|
m_xml.writeStartElement("History");
|
||||||
|
|
||||||
|
const QList<Entry*>& historyItems = entry->historyItems();
|
||||||
|
for (const Entry* item : historyItems) {
|
||||||
|
writeEntry(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_xml.writeEndElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeString(const QString& qualifiedName, const QString& string)
|
||||||
|
{
|
||||||
|
if (string.isEmpty()) {
|
||||||
|
m_xml.writeEmptyElement(qualifiedName);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_xml.writeTextElement(qualifiedName, stripInvalidXml10Chars(string));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeNumber(const QString& qualifiedName, int number)
|
||||||
|
{
|
||||||
|
writeString(qualifiedName, QString::number(number));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeBool(const QString& qualifiedName, bool b)
|
||||||
|
{
|
||||||
|
if (b) {
|
||||||
|
writeString(qualifiedName, "True");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
writeString(qualifiedName, "False");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeDateTime(const QString& qualifiedName, const QDateTime& dateTime)
|
||||||
|
{
|
||||||
|
Q_ASSERT(dateTime.isValid());
|
||||||
|
Q_ASSERT(dateTime.timeSpec() == Qt::UTC);
|
||||||
|
|
||||||
|
QString dateTimeStr;
|
||||||
|
if (m_version < KeePass2::FILE_VERSION_4) {
|
||||||
|
dateTimeStr = dateTime.toString(Qt::ISODate);
|
||||||
|
|
||||||
|
// Qt < 4.8 doesn't append a 'Z' at the end
|
||||||
|
if (!dateTimeStr.isEmpty() && dateTimeStr[dateTimeStr.size() - 1] != 'Z') {
|
||||||
|
dateTimeStr.append('Z');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
qint64 secs = QDateTime(QDate(1, 1, 1), QTime(0, 0, 0, 0), Qt::UTC).secsTo(dateTime);
|
||||||
|
QByteArray secsBytes = Endian::sizedIntToBytes(secs, KeePass2::BYTEORDER);
|
||||||
|
dateTimeStr = QString::fromLatin1(secsBytes.toBase64());
|
||||||
|
}
|
||||||
|
writeString(qualifiedName, dateTimeStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeUuid(const QString& qualifiedName, const Uuid& uuid)
|
||||||
|
{
|
||||||
|
writeString(qualifiedName, uuid.toBase64());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeUuid(const QString& qualifiedName, const Group* group)
|
||||||
|
{
|
||||||
|
if (group) {
|
||||||
|
writeUuid(qualifiedName, group->uuid());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
writeUuid(qualifiedName, Uuid());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeUuid(const QString& qualifiedName, const Entry* entry)
|
||||||
|
{
|
||||||
|
if (entry) {
|
||||||
|
writeUuid(qualifiedName, entry->uuid());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
writeUuid(qualifiedName, Uuid());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeBinary(const QString& qualifiedName, const QByteArray& ba)
|
||||||
|
{
|
||||||
|
writeString(qualifiedName, QString::fromLatin1(ba.toBase64()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeColor(const QString& qualifiedName, const QColor& color)
|
||||||
|
{
|
||||||
|
QString colorStr;
|
||||||
|
|
||||||
|
if (color.isValid()) {
|
||||||
|
colorStr = QString("#%1%2%3").arg(colorPartToString(color.red()),
|
||||||
|
colorPartToString(color.green()),
|
||||||
|
colorPartToString(color.blue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
writeString(qualifiedName, colorStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::writeTriState(const QString& qualifiedName, Group::TriState triState)
|
||||||
|
{
|
||||||
|
QString value;
|
||||||
|
|
||||||
|
if (triState == Group::Inherit) {
|
||||||
|
value = "null";
|
||||||
|
}
|
||||||
|
else if (triState == Group::Enable) {
|
||||||
|
value = "true";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
value = "false";
|
||||||
|
}
|
||||||
|
|
||||||
|
writeString(qualifiedName, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Kdbx4XmlWriter::colorPartToString(int value)
|
||||||
|
{
|
||||||
|
QString str = QString::number(value, 16).toUpper();
|
||||||
|
if (str.length() == 1) {
|
||||||
|
str.prepend("0");
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Kdbx4XmlWriter::stripInvalidXml10Chars(QString str)
|
||||||
|
{
|
||||||
|
for (int i = str.size() - 1; i >= 0; i--) {
|
||||||
|
const QChar ch = str.at(i);
|
||||||
|
const ushort uc = ch.unicode();
|
||||||
|
|
||||||
|
if (ch.isLowSurrogate() && i != 0 && str.at(i - 1).isHighSurrogate()) {
|
||||||
|
// keep valid surrogate pair
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
else if ((uc < 0x20 && uc != 0x09 && uc != 0x0A && uc != 0x0D) // control characters
|
||||||
|
|| (uc >= 0x7F && uc <= 0x84) // control characters, valid but discouraged by XML
|
||||||
|
|| (uc >= 0x86 && uc <= 0x9F) // control characters, valid but discouraged by XML
|
||||||
|
|| (uc > 0xFFFD) // noncharacter
|
||||||
|
|| ch.isLowSurrogate() // single low surrogate
|
||||||
|
|| ch.isHighSurrogate()) // single high surrogate
|
||||||
|
{
|
||||||
|
qWarning("Stripping invalid XML 1.0 codepoint %x", uc);
|
||||||
|
str.remove(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Kdbx4XmlWriter::raiseError(const QString& errorMessage)
|
||||||
|
{
|
||||||
|
m_error = true;
|
||||||
|
m_errorStr = errorMessage;
|
||||||
|
}
|
93
src/format/Kdbx4XmlWriter.h
Normal file
93
src/format/Kdbx4XmlWriter.h
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef KEEPASSX_KDBX4XMLWRITER_H
|
||||||
|
#define KEEPASSX_KDBX4XMLWRITER_H
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QXmlStreamWriter>
|
||||||
|
|
||||||
|
#include "core/Database.h"
|
||||||
|
#include "core/Entry.h"
|
||||||
|
#include "core/Group.h"
|
||||||
|
#include "core/TimeInfo.h"
|
||||||
|
#include "core/Uuid.h"
|
||||||
|
|
||||||
|
class KeePass2RandomStream;
|
||||||
|
class Metadata;
|
||||||
|
|
||||||
|
class Kdbx4XmlWriter
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Kdbx4XmlWriter();
|
||||||
|
Kdbx4XmlWriter(quint32 version);
|
||||||
|
Kdbx4XmlWriter(quint32 version, QHash<QByteArray, int> idMap);
|
||||||
|
void writeDatabase(QIODevice* device, Database* db, KeePass2RandomStream* randomStream = nullptr,
|
||||||
|
const QByteArray& headerHash = QByteArray());
|
||||||
|
void writeDatabase(const QString& filename, Database* db);
|
||||||
|
bool hasError();
|
||||||
|
QString errorString();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void generateIdMap();
|
||||||
|
|
||||||
|
void writeMetadata();
|
||||||
|
void writeMemoryProtection();
|
||||||
|
void writeCustomIcons();
|
||||||
|
void writeIcon(const Uuid& uuid, const QImage& icon);
|
||||||
|
void writeBinaries();
|
||||||
|
void writeCustomData();
|
||||||
|
void writeCustomDataItem(const QString& key, const QString& value);
|
||||||
|
void writeRoot();
|
||||||
|
void writeGroup(const Group* group);
|
||||||
|
void writeTimes(const TimeInfo& ti);
|
||||||
|
void writeDeletedObjects();
|
||||||
|
void writeDeletedObject(const DeletedObject& delObj);
|
||||||
|
void writeEntry(const Entry* entry);
|
||||||
|
void writeAutoType(const Entry* entry);
|
||||||
|
void writeAutoTypeAssoc(const AutoTypeAssociations::Association& assoc);
|
||||||
|
void writeEntryHistory(const Entry* entry);
|
||||||
|
|
||||||
|
void writeString(const QString& qualifiedName, const QString& string);
|
||||||
|
void writeNumber(const QString& qualifiedName, int number);
|
||||||
|
void writeBool(const QString& qualifiedName, bool b);
|
||||||
|
void writeDateTime(const QString& qualifiedName, const QDateTime& dateTime);
|
||||||
|
void writeUuid(const QString& qualifiedName, const Uuid& uuid);
|
||||||
|
void writeUuid(const QString& qualifiedName, const Group* group);
|
||||||
|
void writeUuid(const QString& qualifiedName, const Entry* entry);
|
||||||
|
void writeBinary(const QString& qualifiedName, const QByteArray& ba);
|
||||||
|
void writeColor(const QString& qualifiedName, const QColor& color);
|
||||||
|
void writeTriState(const QString& qualifiedName, Group::TriState triState);
|
||||||
|
QString colorPartToString(int value);
|
||||||
|
QString stripInvalidXml10Chars(QString str);
|
||||||
|
|
||||||
|
void raiseError(const QString& errorMessage);
|
||||||
|
|
||||||
|
QXmlStreamWriter m_xml;
|
||||||
|
Database* m_db;
|
||||||
|
Metadata* m_meta;
|
||||||
|
KeePass2RandomStream* m_randomStream;
|
||||||
|
QHash<QByteArray, int> m_idMap;
|
||||||
|
bool m_error;
|
||||||
|
QString m_errorStr;
|
||||||
|
quint32 m_version;
|
||||||
|
QByteArray m_headerHash;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // KEEPASSX_KDBX4XMLWRITER_H
|
|
@ -19,6 +19,7 @@
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
#include "crypto/kdf/AesKdf.h"
|
#include "crypto/kdf/AesKdf.h"
|
||||||
#include "crypto/kdf/Argon2Kdf.h"
|
#include "crypto/kdf/Argon2Kdf.h"
|
||||||
|
#include "crypto/CryptoHash.h"
|
||||||
|
|
||||||
const Uuid KeePass2::CIPHER_AES = Uuid(QByteArray::fromHex("31c1f2e6bf714350be5805216afc5aff"));
|
const Uuid KeePass2::CIPHER_AES = Uuid(QByteArray::fromHex("31c1f2e6bf714350be5805216afc5aff"));
|
||||||
const Uuid KeePass2::CIPHER_TWOFISH = Uuid(QByteArray::fromHex("ad68f29f576f4bb9a36ad47af965346c"));
|
const Uuid KeePass2::CIPHER_TWOFISH = Uuid(QByteArray::fromHex("ad68f29f576f4bb9a36ad47af965346c"));
|
||||||
|
@ -29,6 +30,19 @@ const Uuid KeePass2::KDF_ARGON2 = Uuid(QByteArray::fromHex("EF636DDF8C29444B91F7
|
||||||
|
|
||||||
const QByteArray KeePass2::INNER_STREAM_SALSA20_IV("\xE8\x30\x09\x4B\x97\x20\x5D\x2A");
|
const QByteArray KeePass2::INNER_STREAM_SALSA20_IV("\xE8\x30\x09\x4B\x97\x20\x5D\x2A");
|
||||||
|
|
||||||
|
const QString KeePass2::KDFPARAM_UUID("$UUID");
|
||||||
|
// AES parameters
|
||||||
|
const QString KeePass2::KDFPARAM_AES_ROUNDS("R");
|
||||||
|
const QString KeePass2::KDFPARAM_AES_SEED("S");
|
||||||
|
// Argon2 parameters
|
||||||
|
const QString KeePass2::KDFPARAM_ARGON2_SALT("S");
|
||||||
|
const QString KeePass2::KDFPARAM_ARGON2_PARALLELISM("P");
|
||||||
|
const QString KeePass2::KDFPARAM_ARGON2_MEMORY("M");
|
||||||
|
const QString KeePass2::KDFPARAM_ARGON2_ITERATIONS("I");
|
||||||
|
const QString KeePass2::KDFPARAM_ARGON2_VERSION("V");
|
||||||
|
const QString KeePass2::KDFPARAM_ARGON2_SECRET("K");
|
||||||
|
const QString KeePass2::KDFPARAM_ARGON2_ASSOCDATA("A");
|
||||||
|
|
||||||
const QList<QPair<Uuid, QString>> KeePass2::CIPHERS {
|
const QList<QPair<Uuid, QString>> KeePass2::CIPHERS {
|
||||||
qMakePair(KeePass2::CIPHER_AES, QObject::tr("AES: 256-bit")),
|
qMakePair(KeePass2::CIPHER_AES, QObject::tr("AES: 256-bit")),
|
||||||
qMakePair(KeePass2::CIPHER_TWOFISH, QObject::tr("Twofish: 256-bit")),
|
qMakePair(KeePass2::CIPHER_TWOFISH, QObject::tr("Twofish: 256-bit")),
|
||||||
|
@ -39,6 +53,38 @@ const QList<QPair<Uuid, QString>> KeePass2::KDFS {
|
||||||
qMakePair(KeePass2::KDF_ARGON2, QObject::tr("Argon2")),
|
qMakePair(KeePass2::KDF_ARGON2, QObject::tr("Argon2")),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
QByteArray KeePass2::hmacKey(QByteArray masterSeed, QByteArray transformedMasterKey) {
|
||||||
|
CryptoHash hmacKeyHash(CryptoHash::Sha512);
|
||||||
|
hmacKeyHash.addData(masterSeed);
|
||||||
|
hmacKeyHash.addData(transformedMasterKey);
|
||||||
|
hmacKeyHash.addData(QByteArray(1, '\x01'));
|
||||||
|
return hmacKeyHash.result();
|
||||||
|
}
|
||||||
|
|
||||||
|
QSharedPointer<Kdf> KeePass2::kdfFromParameters(const QVariantMap &p)
|
||||||
|
{
|
||||||
|
QByteArray uuidBytes = p.value(KDFPARAM_UUID).toByteArray();
|
||||||
|
if (uuidBytes.size() != Uuid::Length) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
QSharedPointer<Kdf> kdf(uuidToKdf(Uuid(uuidBytes)));
|
||||||
|
if (kdf.isNull()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!kdf->processParameters(p)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return kdf;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap KeePass2::kdfToParameters(QSharedPointer<Kdf> kdf)
|
||||||
|
{
|
||||||
|
return kdf->writeParameters();
|
||||||
|
}
|
||||||
|
|
||||||
QSharedPointer<Kdf> KeePass2::uuidToKdf(const Uuid& uuid)
|
QSharedPointer<Kdf> KeePass2::uuidToKdf(const Uuid& uuid)
|
||||||
{
|
{
|
||||||
if (uuid == KDF_AES) {
|
if (uuid == KDF_AES) {
|
||||||
|
|
|
@ -19,6 +19,8 @@
|
||||||
#define KEEPASSX_KEEPASS2_H
|
#define KEEPASSX_KEEPASS2_H
|
||||||
|
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
|
#include <QMap>
|
||||||
|
#include <QVariantMap>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
|
|
||||||
#include "crypto/SymmetricCipher.h"
|
#include "crypto/SymmetricCipher.h"
|
||||||
|
@ -29,9 +31,14 @@ namespace KeePass2
|
||||||
{
|
{
|
||||||
const quint32 SIGNATURE_1 = 0x9AA2D903;
|
const quint32 SIGNATURE_1 = 0x9AA2D903;
|
||||||
const quint32 SIGNATURE_2 = 0xB54BFB67;
|
const quint32 SIGNATURE_2 = 0xB54BFB67;
|
||||||
const quint32 FILE_VERSION = 0x00030001;
|
|
||||||
const quint32 FILE_VERSION_MIN = 0x00020000;
|
const quint32 FILE_VERSION_MIN = 0x00020000;
|
||||||
const quint32 FILE_VERSION_CRITICAL_MASK = 0xFFFF0000;
|
const quint32 FILE_VERSION_CRITICAL_MASK = 0xFFFF0000;
|
||||||
|
const quint32 FILE_VERSION_4 = 0x00040000;
|
||||||
|
const quint32 FILE_VERSION_3 = 0x00030001;
|
||||||
|
|
||||||
|
const quint16 VARIANTMAP_VERSION = 0x0100;
|
||||||
|
const quint16 VARIANTMAP_CRITICAL_MASK = 0xFF00;
|
||||||
|
|
||||||
const QSysInfo::Endian BYTEORDER = QSysInfo::LittleEndian;
|
const QSysInfo::Endian BYTEORDER = QSysInfo::LittleEndian;
|
||||||
|
|
||||||
|
@ -44,6 +51,17 @@ namespace KeePass2
|
||||||
|
|
||||||
extern const QByteArray INNER_STREAM_SALSA20_IV;
|
extern const QByteArray INNER_STREAM_SALSA20_IV;
|
||||||
|
|
||||||
|
extern const QString KDFPARAM_UUID;
|
||||||
|
extern const QString KDFPARAM_AES_ROUNDS;
|
||||||
|
extern const QString KDFPARAM_AES_SEED;
|
||||||
|
extern const QString KDFPARAM_ARGON2_SALT;
|
||||||
|
extern const QString KDFPARAM_ARGON2_PARALLELISM;
|
||||||
|
extern const QString KDFPARAM_ARGON2_MEMORY;
|
||||||
|
extern const QString KDFPARAM_ARGON2_ITERATIONS;
|
||||||
|
extern const QString KDFPARAM_ARGON2_VERSION;
|
||||||
|
extern const QString KDFPARAM_ARGON2_SECRET;
|
||||||
|
extern const QString KDFPARAM_ARGON2_ASSOCDATA;
|
||||||
|
|
||||||
extern const QList<QPair<Uuid, QString>> CIPHERS;
|
extern const QList<QPair<Uuid, QString>> CIPHERS;
|
||||||
extern const QList<QPair<Uuid, QString>> KDFS;
|
extern const QList<QPair<Uuid, QString>> KDFS;
|
||||||
|
|
||||||
|
@ -59,7 +77,17 @@ namespace KeePass2
|
||||||
EncryptionIV = 7,
|
EncryptionIV = 7,
|
||||||
ProtectedStreamKey = 8,
|
ProtectedStreamKey = 8,
|
||||||
StreamStartBytes = 9,
|
StreamStartBytes = 9,
|
||||||
InnerRandomStreamID = 10
|
InnerRandomStreamID = 10,
|
||||||
|
KdfParameters = 11,
|
||||||
|
PublicCustomData = 12
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class InnerHeaderFieldID : quint8
|
||||||
|
{
|
||||||
|
End = 0,
|
||||||
|
InnerRandomStreamID = 1,
|
||||||
|
InnerRandomStreamKey = 2,
|
||||||
|
Binary = 3
|
||||||
};
|
};
|
||||||
|
|
||||||
enum ProtectedStreamAlgo
|
enum ProtectedStreamAlgo
|
||||||
|
@ -70,7 +98,33 @@ namespace KeePass2
|
||||||
InvalidProtectedStreamAlgo = -1
|
InvalidProtectedStreamAlgo = -1
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class VariantMapFieldType : quint8
|
||||||
|
{
|
||||||
|
End = 0,
|
||||||
|
// Byte = 0x02,
|
||||||
|
// UInt16 = 0x03,
|
||||||
|
UInt32 = 0x04,
|
||||||
|
UInt64 = 0x05,
|
||||||
|
// Signed mask: 0x08
|
||||||
|
Bool = 0x08,
|
||||||
|
// SByte = 0x0A,
|
||||||
|
// Int16 = 0x0B,
|
||||||
|
Int32 = 0x0C,
|
||||||
|
Int64 = 0x0D,
|
||||||
|
// Float = 0x10,
|
||||||
|
// Double = 0x11,
|
||||||
|
// Decimal = 0x12,
|
||||||
|
// Char = 0x17, // 16-bit Unicode character
|
||||||
|
String = 0x18,
|
||||||
|
// Array mask: 0x40
|
||||||
|
ByteArray = 0x42
|
||||||
|
};
|
||||||
|
|
||||||
|
QByteArray hmacKey(QByteArray masterSeed, QByteArray transformedMasterKey);
|
||||||
|
QSharedPointer<Kdf> kdfFromParameters(const QVariantMap &p);
|
||||||
|
QVariantMap kdfToParameters(QSharedPointer<Kdf> kdf);
|
||||||
QSharedPointer<Kdf> uuidToKdf(const Uuid& uuid);
|
QSharedPointer<Kdf> uuidToKdf(const Uuid& uuid);
|
||||||
|
Uuid kdfToUuid(QSharedPointer<Kdf> kdf);
|
||||||
ProtectedStreamAlgo idToProtectedStreamAlgo(quint32 id);
|
ProtectedStreamAlgo idToProtectedStreamAlgo(quint32 id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
#include "format/KeePass1.h"
|
#include "format/KeePass1.h"
|
||||||
#include "format/KeePass2.h"
|
#include "format/KeePass2.h"
|
||||||
#include "format/Kdbx3Reader.h"
|
#include "format/Kdbx3Reader.h"
|
||||||
|
#include "format/Kdbx4Reader.h"
|
||||||
|
|
||||||
BaseKeePass2Reader::BaseKeePass2Reader()
|
BaseKeePass2Reader::BaseKeePass2Reader()
|
||||||
: m_error(false)
|
: m_error(false)
|
||||||
|
@ -118,14 +119,21 @@ Database* KeePass2Reader::readDatabase(QIODevice* device, const CompositeKey& ke
|
||||||
|
|
||||||
m_version = Endian::readSizedInt<quint32>(device, KeePass2::BYTEORDER, &ok)
|
m_version = Endian::readSizedInt<quint32>(device, KeePass2::BYTEORDER, &ok)
|
||||||
& KeePass2::FILE_VERSION_CRITICAL_MASK;
|
& KeePass2::FILE_VERSION_CRITICAL_MASK;
|
||||||
quint32 maxVersion = KeePass2::FILE_VERSION & KeePass2::FILE_VERSION_CRITICAL_MASK;
|
quint32 maxVersion = KeePass2::FILE_VERSION_4 & KeePass2::FILE_VERSION_CRITICAL_MASK;
|
||||||
if (!ok || (m_version < KeePass2::FILE_VERSION_MIN) || (m_version > maxVersion)) {
|
if (!ok || (m_version < KeePass2::FILE_VERSION_MIN) || (m_version > maxVersion)) {
|
||||||
raiseError(tr("Unsupported KeePass 2 database version."));
|
raiseError(tr("Unsupported KeePass 2 database version."));
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
device->seek(0);
|
device->seek(0);
|
||||||
m_reader.reset(static_cast<BaseKeePass2Reader*>(new Kdbx3Reader()));
|
|
||||||
|
// Determine KDBX3 vs KDBX4
|
||||||
|
if (m_version < KeePass2::FILE_VERSION_4) {
|
||||||
|
m_reader.reset(new Kdbx3Reader());
|
||||||
|
} else {
|
||||||
|
m_reader.reset(new Kdbx4Reader());
|
||||||
|
}
|
||||||
|
|
||||||
m_reader->setSaveXml(m_saveXml);
|
m_reader->setSaveXml(m_saveXml);
|
||||||
return m_reader->readDatabase(device, key, keepDatabase);
|
return m_reader->readDatabase(device, key, keepDatabase);
|
||||||
}
|
}
|
||||||
|
@ -159,3 +167,8 @@ quint32 KeePass2Reader::version() const
|
||||||
{
|
{
|
||||||
return m_version;
|
return m_version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QSharedPointer<BaseKeePass2Reader> KeePass2Reader::reader()
|
||||||
|
{
|
||||||
|
return m_reader;
|
||||||
|
}
|
|
@ -70,11 +70,12 @@ public:
|
||||||
QString errorString() override;
|
QString errorString() override;
|
||||||
QByteArray xmlData() override;
|
QByteArray xmlData() override;
|
||||||
QByteArray streamKey() override;
|
QByteArray streamKey() override;
|
||||||
|
QSharedPointer<BaseKeePass2Reader> reader();
|
||||||
KeePass2::ProtectedStreamAlgo protectedStreamAlgo() const override;
|
KeePass2::ProtectedStreamAlgo protectedStreamAlgo() const override;
|
||||||
|
|
||||||
quint32 version() const;
|
quint32 version() const;
|
||||||
private:
|
private:
|
||||||
QScopedPointer<BaseKeePass2Reader> m_reader;
|
QSharedPointer<BaseKeePass2Reader> m_reader;
|
||||||
quint32 m_version;
|
quint32 m_version;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,9 @@
|
||||||
#include "format/KeePass2.h"
|
#include "format/KeePass2.h"
|
||||||
#include "format/KeePass2RandomStream.h"
|
#include "format/KeePass2RandomStream.h"
|
||||||
#include "format/KeePass2Reader.h"
|
#include "format/KeePass2Reader.h"
|
||||||
|
#include "format/Kdbx4Reader.h"
|
||||||
#include "format/Kdbx3XmlReader.h"
|
#include "format/Kdbx3XmlReader.h"
|
||||||
|
#include "format/Kdbx4XmlReader.h"
|
||||||
|
|
||||||
KeePass2Repair::RepairOutcome KeePass2Repair::repairDatabase(QIODevice* device, const CompositeKey& key)
|
KeePass2Repair::RepairOutcome KeePass2Repair::repairDatabase(QIODevice* device, const CompositeKey& key)
|
||||||
{
|
{
|
||||||
|
@ -74,12 +76,23 @@ KeePass2Repair::RepairOutcome KeePass2Repair::repairDatabase(QIODevice* device,
|
||||||
|
|
||||||
KeePass2RandomStream randomStream(reader.protectedStreamAlgo());
|
KeePass2RandomStream randomStream(reader.protectedStreamAlgo());
|
||||||
randomStream.init(reader.streamKey());
|
randomStream.init(reader.streamKey());
|
||||||
Kdbx3XmlReader xmlReader;
|
bool hasError;
|
||||||
|
|
||||||
QBuffer buffer(&xmlData);
|
QBuffer buffer(&xmlData);
|
||||||
buffer.open(QIODevice::ReadOnly);
|
buffer.open(QIODevice::ReadOnly);
|
||||||
xmlReader.readDatabase(&buffer, db.data(), &randomStream);
|
if ((reader.version() & KeePass2::FILE_VERSION_CRITICAL_MASK) < KeePass2::FILE_VERSION_4) {
|
||||||
|
Kdbx3XmlReader xmlReader;
|
||||||
|
xmlReader.readDatabase(&buffer, db.data(), &randomStream);
|
||||||
|
hasError = xmlReader.hasError();
|
||||||
|
} else {
|
||||||
|
auto reader4 = reader.reader().staticCast<Kdbx4Reader>();
|
||||||
|
QHash<QString, QByteArray> pool = reader4->binaryPool();
|
||||||
|
Kdbx4XmlReader xmlReader(pool);
|
||||||
|
xmlReader.readDatabase(&buffer, db.data(), &randomStream);
|
||||||
|
hasError = xmlReader.hasError();
|
||||||
|
}
|
||||||
|
|
||||||
if (xmlReader.hasError()) {
|
if (hasError) {
|
||||||
return qMakePair(RepairFailed, nullptr);
|
return qMakePair(RepairFailed, nullptr);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
#include "format/KeePass2Writer.h"
|
#include "format/KeePass2Writer.h"
|
||||||
#include "core/Database.h"
|
#include "core/Database.h"
|
||||||
#include "format/Kdbx3Writer.h"
|
#include "format/Kdbx3Writer.h"
|
||||||
|
#include "format/Kdbx4Writer.h"
|
||||||
|
|
||||||
BaseKeePass2Writer::BaseKeePass2Writer() : m_error(false)
|
BaseKeePass2Writer::BaseKeePass2Writer() : m_error(false)
|
||||||
{
|
{
|
||||||
|
@ -67,6 +68,22 @@ QString KeePass2Writer::errorString()
|
||||||
}
|
}
|
||||||
|
|
||||||
bool KeePass2Writer::writeDatabase(QIODevice* device, Database* db) {
|
bool KeePass2Writer::writeDatabase(QIODevice* device, Database* db) {
|
||||||
m_writer.reset(static_cast<BaseKeePass2Writer*>(new Kdbx3Writer()));
|
bool useKdbx4 = false;
|
||||||
|
|
||||||
|
if (db->kdf()->uuid() != KeePass2::KDF_AES) {
|
||||||
|
useKdbx4 = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (db->publicCustomData().size() > 0) {
|
||||||
|
useKdbx4 = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine KDBX3 vs KDBX4
|
||||||
|
if (useKdbx4) {
|
||||||
|
m_writer.reset(new Kdbx4Writer());
|
||||||
|
} else {
|
||||||
|
m_writer.reset(new Kdbx3Writer());
|
||||||
|
}
|
||||||
|
|
||||||
return m_writer->writeDatabase(device, db);
|
return m_writer->writeDatabase(device, db);
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,7 +105,7 @@ void DatabaseSettingsWidget::load(Database* db)
|
||||||
m_ui->transformRoundsSpinBox->setValue(kdf->rounds());
|
m_ui->transformRoundsSpinBox->setValue(kdf->rounds());
|
||||||
if (kdfUuid == KeePass2::KDF_ARGON2) {
|
if (kdfUuid == KeePass2::KDF_ARGON2) {
|
||||||
auto argon2Kdf = kdf.staticCast<Argon2Kdf>();
|
auto argon2Kdf = kdf.staticCast<Argon2Kdf>();
|
||||||
m_ui->memorySpinBox->setValue(argon2Kdf->memory());
|
m_ui->memorySpinBox->setValue(argon2Kdf->memory() / (1<<10));
|
||||||
m_ui->parallelismSpinBox->setValue(argon2Kdf->parallelism());
|
m_ui->parallelismSpinBox->setValue(argon2Kdf->parallelism());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,6 +120,7 @@ void DatabaseSettingsWidget::save()
|
||||||
meta->setDescription(m_ui->dbDescriptionEdit->text());
|
meta->setDescription(m_ui->dbDescriptionEdit->text());
|
||||||
meta->setDefaultUserName(m_ui->defaultUsernameEdit->text());
|
meta->setDefaultUserName(m_ui->defaultUsernameEdit->text());
|
||||||
meta->setRecycleBinEnabled(m_ui->recycleBinEnabledCheckBox->isChecked());
|
meta->setRecycleBinEnabled(m_ui->recycleBinEnabledCheckBox->isChecked());
|
||||||
|
meta->setSettingsChanged(QDateTime::currentDateTimeUtc());
|
||||||
|
|
||||||
bool truncate = false;
|
bool truncate = false;
|
||||||
|
|
||||||
|
@ -156,7 +157,7 @@ void DatabaseSettingsWidget::save()
|
||||||
kdf->setRounds(m_ui->transformRoundsSpinBox->value());
|
kdf->setRounds(m_ui->transformRoundsSpinBox->value());
|
||||||
if (kdf->uuid() == KeePass2::KDF_ARGON2) {
|
if (kdf->uuid() == KeePass2::KDF_ARGON2) {
|
||||||
auto argon2Kdf = kdf.staticCast<Argon2Kdf>();
|
auto argon2Kdf = kdf.staticCast<Argon2Kdf>();
|
||||||
argon2Kdf->setMemory(m_ui->memorySpinBox->value());
|
argon2Kdf->setMemory(m_ui->memorySpinBox->value() * (1<<10));
|
||||||
argon2Kdf->setParallelism(m_ui->parallelismSpinBox->value());
|
argon2Kdf->setParallelism(m_ui->parallelismSpinBox->value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,8 +190,12 @@ void DatabaseSettingsWidget::transformRoundsBenchmark()
|
||||||
kdf->setRounds(m_ui->transformRoundsSpinBox->value());
|
kdf->setRounds(m_ui->transformRoundsSpinBox->value());
|
||||||
if (kdf->uuid() == KeePass2::KDF_ARGON2) {
|
if (kdf->uuid() == KeePass2::KDF_ARGON2) {
|
||||||
auto argon2Kdf = kdf.staticCast<Argon2Kdf>();
|
auto argon2Kdf = kdf.staticCast<Argon2Kdf>();
|
||||||
argon2Kdf->setMemory(m_ui->memorySpinBox->value());
|
if (!argon2Kdf->setMemory(m_ui->memorySpinBox->value() * (1<<10))) {
|
||||||
argon2Kdf->setParallelism(m_ui->parallelismSpinBox->value());
|
m_ui->memorySpinBox->setValue(argon2Kdf->memory() / (1<<10));
|
||||||
|
}
|
||||||
|
if (!argon2Kdf->setParallelism(m_ui->parallelismSpinBox->value())) {
|
||||||
|
m_ui->parallelismSpinBox->setValue(argon2Kdf->parallelism());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the number of rounds required to meet 1 second delay
|
// Determine the number of rounds required to meet 1 second delay
|
||||||
|
|
|
@ -809,7 +809,7 @@ void DatabaseWidget::updateMasterKey(bool accepted)
|
||||||
|
|
||||||
if (accepted) {
|
if (accepted) {
|
||||||
QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
|
QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
|
||||||
bool result = m_db->setKey(m_changeMasterKeyWidget->newMasterKey());
|
bool result = m_db->setKey(m_changeMasterKeyWidget->newMasterKey(), true, true);
|
||||||
QApplication::restoreOverrideCursor();
|
QApplication::restoreOverrideCursor();
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
|
|
|
@ -110,6 +110,9 @@ add_unit_test(NAME testgroup SOURCES TestGroup.cpp
|
||||||
add_unit_test(NAME testkdbx3xmlreader SOURCES TestKeePass2XmlReader.cpp TestKdbx3XmlReader.cpp
|
add_unit_test(NAME testkdbx3xmlreader SOURCES TestKeePass2XmlReader.cpp TestKdbx3XmlReader.cpp
|
||||||
LIBS ${TEST_LIBRARIES})
|
LIBS ${TEST_LIBRARIES})
|
||||||
|
|
||||||
|
add_unit_test(NAME testkdbx4xmlreader SOURCES TestKeePass2XmlReader.cpp TestKdbx4XmlReader.cpp
|
||||||
|
LIBS ${TEST_LIBRARIES})
|
||||||
|
|
||||||
add_unit_test(NAME testkeys SOURCES TestKeys.cpp
|
add_unit_test(NAME testkeys SOURCES TestKeys.cpp
|
||||||
LIBS ${TEST_LIBRARIES})
|
LIBS ${TEST_LIBRARIES})
|
||||||
|
|
||||||
|
|
22
tests/TestKdbx4XmlReader.cpp
Normal file
22
tests/TestKdbx4XmlReader.cpp
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <QTest>
|
||||||
|
|
||||||
|
#include "TestKeePass2XmlReader.h"
|
||||||
|
|
||||||
|
QTEST_GUILESS_MAIN(TestKdbx4XmlReader)
|
|
@ -155,3 +155,26 @@ void TestKeePass2Reader::testFormat300()
|
||||||
|
|
||||||
delete db;
|
delete db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestKeePass2Reader::testFormat400()
|
||||||
|
{
|
||||||
|
QString filename = QString(KEEPASSX_TEST_DATA_DIR).append("/Format400.kdbx");
|
||||||
|
CompositeKey key;
|
||||||
|
key.addKey(PasswordKey("t"));
|
||||||
|
KeePass2Reader reader;
|
||||||
|
Database* db = reader.readDatabase(filename, key);
|
||||||
|
QVERIFY(db);
|
||||||
|
QVERIFY(!reader.hasError());
|
||||||
|
|
||||||
|
QCOMPARE(db->rootGroup()->name(), QString("Format400"));
|
||||||
|
QCOMPARE(db->metadata()->name(), QString("Format400"));
|
||||||
|
QCOMPARE(db->rootGroup()->entries().size(), 1);
|
||||||
|
Entry* entry = db->rootGroup()->entries().at(0);
|
||||||
|
|
||||||
|
QCOMPARE(entry->title(), QString("Format400"));
|
||||||
|
QCOMPARE(entry->username(), QString("Format400"));
|
||||||
|
QCOMPARE(entry->attributes()->keys().size(), 6);
|
||||||
|
QCOMPARE(entry->attributes()->value("Format400"), QString("Format400"));
|
||||||
|
QCOMPARE(entry->attachments()->keys().size(), 1);
|
||||||
|
QCOMPARE(entry->attachments()->value("Format400"), QByteArray("Format400\n"));
|
||||||
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ private slots:
|
||||||
void testBrokenHeaderHash();
|
void testBrokenHeaderHash();
|
||||||
void testFormat200();
|
void testFormat200();
|
||||||
void testFormat300();
|
void testFormat300();
|
||||||
|
void testFormat400();
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // KEEPASSX_TESTKEEPASS2READER_H
|
#endif // KEEPASSX_TESTKEEPASS2READER_H
|
||||||
|
|
|
@ -27,6 +27,8 @@
|
||||||
#include "crypto/Crypto.h"
|
#include "crypto/Crypto.h"
|
||||||
#include "format/Kdbx3XmlReader.h"
|
#include "format/Kdbx3XmlReader.h"
|
||||||
#include "format/Kdbx3XmlWriter.h"
|
#include "format/Kdbx3XmlWriter.h"
|
||||||
|
#include "format/Kdbx4XmlReader.h"
|
||||||
|
#include "format/Kdbx4XmlWriter.h"
|
||||||
#include "config-keepassx-tests.h"
|
#include "config-keepassx-tests.h"
|
||||||
|
|
||||||
namespace QTest {
|
namespace QTest {
|
||||||
|
@ -89,6 +91,18 @@ void TestKdbx3XmlReader::initTestCase()
|
||||||
QVERIFY(!reader.hasError());
|
QVERIFY(!reader.hasError());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestKdbx4XmlReader::initTestCase()
|
||||||
|
{
|
||||||
|
QVERIFY(Crypto::init());
|
||||||
|
|
||||||
|
Kdbx4XmlReader reader;
|
||||||
|
reader.setStrictMode(true);
|
||||||
|
QString xmlFile = QString(KEEPASSX_TEST_DATA_DIR).append("/NewDatabase.xml");
|
||||||
|
m_db = reader.readDatabase(xmlFile);
|
||||||
|
QVERIFY(m_db);
|
||||||
|
QVERIFY(!reader.hasError());
|
||||||
|
}
|
||||||
|
|
||||||
void TestKdbx3XmlReader::readDatabase(QString path, bool strictMode, Database*& db, bool& hasError, QString& errorString)
|
void TestKdbx3XmlReader::readDatabase(QString path, bool strictMode, Database*& db, bool& hasError, QString& errorString)
|
||||||
{
|
{
|
||||||
Kdbx3XmlReader reader;
|
Kdbx3XmlReader reader;
|
||||||
|
@ -115,6 +129,32 @@ void TestKdbx3XmlReader::writeDatabase(QBuffer* buf, Database* db, bool& hasErro
|
||||||
errorString = writer.errorString();
|
errorString = writer.errorString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestKdbx4XmlReader::readDatabase(QString path, bool strictMode, Database*& db, bool& hasError, QString& errorString)
|
||||||
|
{
|
||||||
|
Kdbx4XmlReader reader;
|
||||||
|
reader.setStrictMode(strictMode);
|
||||||
|
db = reader.readDatabase(path);
|
||||||
|
hasError = reader.hasError();
|
||||||
|
errorString = reader.errorString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestKdbx4XmlReader::readDatabase(QBuffer* buf, bool strictMode, Database*& db, bool& hasError, QString& errorString)
|
||||||
|
{
|
||||||
|
Kdbx4XmlReader reader;
|
||||||
|
reader.setStrictMode(strictMode);
|
||||||
|
db = reader.readDatabase(buf);
|
||||||
|
hasError = reader.hasError();
|
||||||
|
errorString = reader.errorString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestKdbx4XmlReader::writeDatabase(QBuffer* buf, Database* db, bool& hasError, QString& errorString)
|
||||||
|
{
|
||||||
|
Kdbx4XmlWriter writer;
|
||||||
|
writer.writeDatabase(buf, db);
|
||||||
|
hasError = writer.hasError();
|
||||||
|
errorString = writer.errorString();
|
||||||
|
}
|
||||||
|
|
||||||
void TestKeePass2XmlReader::testMetadata()
|
void TestKeePass2XmlReader::testMetadata()
|
||||||
{
|
{
|
||||||
QCOMPARE(m_db->metadata()->generator(), QString("KeePass"));
|
QCOMPARE(m_db->metadata()->generator(), QString("KeePass"));
|
||||||
|
|
|
@ -70,4 +70,17 @@ protected:
|
||||||
virtual void writeDatabase(QBuffer* buf, Database* db, bool& hasError, QString& errorString) override;
|
virtual void writeDatabase(QBuffer* buf, Database* db, bool& hasError, QString& errorString) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class TestKdbx4XmlReader : public TestKeePass2XmlReader
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
virtual void initTestCase() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void readDatabase(QBuffer* buf, bool strictMode, Database*& db, bool& hasError, QString& errorString) override;
|
||||||
|
virtual void readDatabase(QString path, bool strictMode, Database*& db, bool& hasError, QString& errorString) override;
|
||||||
|
virtual void writeDatabase(QBuffer* buf, Database* db, bool& hasError, QString& errorString) override;
|
||||||
|
};
|
||||||
|
|
||||||
#endif // KEEPASSX_TESTKEEPASS2XMLREADER_H
|
#endif // KEEPASSX_TESTKEEPASS2XMLREADER_H
|
||||||
|
|
BIN
tests/data/Format400.kdbx
Normal file
BIN
tests/data/Format400.kdbx
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue