mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-08-13 16:56:03 -04:00
Add KeePass2Writer.
Support attributes MasterKeyChanged, MasterKeyChangeRec, MasterKeyChangeForce and Tags. Close streams in the dtor.
This commit is contained in:
parent
a9ac4bbf41
commit
e3da80fcc6
22 changed files with 397 additions and 39 deletions
|
@ -31,6 +31,7 @@ set(keepassx_SOURCES
|
||||||
crypto/Random.cpp
|
crypto/Random.cpp
|
||||||
crypto/SymmetricCipher.cpp
|
crypto/SymmetricCipher.cpp
|
||||||
format/KeePass2Reader.cpp
|
format/KeePass2Reader.cpp
|
||||||
|
format/KeePass2Writer.cpp
|
||||||
format/KeePass2XmlReader.cpp
|
format/KeePass2XmlReader.cpp
|
||||||
format/KeePass2XmlWriter.cpp
|
format/KeePass2XmlWriter.cpp
|
||||||
gui/DatabaseWidget.cpp
|
gui/DatabaseWidget.cpp
|
||||||
|
|
|
@ -102,3 +102,61 @@ void Database::addDeletedObject(const DeletedObject& delObj)
|
||||||
{
|
{
|
||||||
m_deletedObjects.append(delObj);
|
m_deletedObjects.append(delObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Uuid Database::cipher() const
|
||||||
|
{
|
||||||
|
return m_cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
Database::CompressionAlgorithm Database::compressionAlgo() const
|
||||||
|
{
|
||||||
|
return m_compressionAlgo;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray Database::transformSeed() const
|
||||||
|
{
|
||||||
|
return m_transformSeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
quint64 Database::transformRounds() const
|
||||||
|
{
|
||||||
|
return m_transformRounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray Database::transformedMasterKey() const
|
||||||
|
{
|
||||||
|
return m_transformedMasterKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Database::setCipher(const Uuid& cipher)
|
||||||
|
{
|
||||||
|
Q_ASSERT(!cipher.isNull());
|
||||||
|
|
||||||
|
m_cipher = cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Database::setCompressionAlgo(Database::CompressionAlgorithm algo)
|
||||||
|
{
|
||||||
|
Q_ASSERT(static_cast<quint32>(algo) <= CompressionAlgorithmMax);
|
||||||
|
|
||||||
|
m_compressionAlgo = algo;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Database::setTransformSeed(const QByteArray& seed)
|
||||||
|
{
|
||||||
|
Q_ASSERT(seed.size() == 32);
|
||||||
|
|
||||||
|
m_transformSeed = seed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Database::setTransformRounds(quint64 rounds)
|
||||||
|
{
|
||||||
|
m_transformRounds = rounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Database::setTransformedMasterKey(QByteArray& key)
|
||||||
|
{
|
||||||
|
Q_ASSERT(key.size() == 32);
|
||||||
|
|
||||||
|
m_transformedMasterKey = key;
|
||||||
|
}
|
||||||
|
|
|
@ -36,6 +36,13 @@ class Database : public QObject
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
enum CompressionAlgorithm
|
||||||
|
{
|
||||||
|
CompressionNone = 0,
|
||||||
|
CompressionGZip = 1
|
||||||
|
};
|
||||||
|
static const quint32 CompressionAlgorithmMax = CompressionGZip;
|
||||||
|
|
||||||
Database();
|
Database();
|
||||||
Group* rootGroup();
|
Group* rootGroup();
|
||||||
const Group* rootGroup() const;
|
const Group* rootGroup() const;
|
||||||
|
@ -47,6 +54,18 @@ public:
|
||||||
QList<DeletedObject> deletedObjects();
|
QList<DeletedObject> deletedObjects();
|
||||||
void addDeletedObject(const DeletedObject& delObj);
|
void addDeletedObject(const DeletedObject& delObj);
|
||||||
|
|
||||||
|
Uuid cipher() const;
|
||||||
|
Database::CompressionAlgorithm compressionAlgo() const;
|
||||||
|
QByteArray transformSeed() const;
|
||||||
|
quint64 transformRounds() const;
|
||||||
|
QByteArray transformedMasterKey() const;
|
||||||
|
|
||||||
|
void setCipher(const Uuid& cipher);
|
||||||
|
void setCompressionAlgo(Database::CompressionAlgorithm algo);
|
||||||
|
void setTransformSeed(const QByteArray& seed);
|
||||||
|
void setTransformRounds(quint64 rounds);
|
||||||
|
void setTransformedMasterKey(QByteArray& key);
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void groupDataChanged(Group* group);
|
void groupDataChanged(Group* group);
|
||||||
void groupAboutToAdd(Group* group, int index);
|
void groupAboutToAdd(Group* group, int index);
|
||||||
|
@ -61,6 +80,12 @@ private:
|
||||||
Metadata* m_metadata;
|
Metadata* m_metadata;
|
||||||
Group* m_rootGroup;
|
Group* m_rootGroup;
|
||||||
QList<DeletedObject> m_deletedObjects;
|
QList<DeletedObject> m_deletedObjects;
|
||||||
|
|
||||||
|
Uuid m_cipher;
|
||||||
|
CompressionAlgorithm m_compressionAlgo;
|
||||||
|
QByteArray m_transformSeed;
|
||||||
|
quint64 m_transformRounds;
|
||||||
|
QByteArray m_transformedMasterKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // KEEPASSX_DATABASE_H
|
#endif // KEEPASSX_DATABASE_H
|
||||||
|
|
|
@ -74,6 +74,11 @@ QString Entry::overrideUrl() const
|
||||||
return m_overrideUrl;
|
return m_overrideUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString Entry::tags() const
|
||||||
|
{
|
||||||
|
return m_tags;
|
||||||
|
}
|
||||||
|
|
||||||
TimeInfo Entry::timeInfo() const
|
TimeInfo Entry::timeInfo() const
|
||||||
{
|
{
|
||||||
return m_timeInfo;
|
return m_timeInfo;
|
||||||
|
@ -172,6 +177,11 @@ void Entry::setOverrideUrl(const QString& url)
|
||||||
m_overrideUrl = url;
|
m_overrideUrl = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Entry::setTags(const QString& tags)
|
||||||
|
{
|
||||||
|
m_tags = tags;
|
||||||
|
}
|
||||||
|
|
||||||
void Entry::setTimeInfo(const TimeInfo& timeInfo)
|
void Entry::setTimeInfo(const TimeInfo& timeInfo)
|
||||||
{
|
{
|
||||||
m_timeInfo = timeInfo;
|
m_timeInfo = timeInfo;
|
||||||
|
|
|
@ -49,6 +49,7 @@ public:
|
||||||
QColor foregroundColor() const;
|
QColor foregroundColor() const;
|
||||||
QColor backgroundColor() const;
|
QColor backgroundColor() const;
|
||||||
QString overrideUrl() const;
|
QString overrideUrl() const;
|
||||||
|
QString tags() const;
|
||||||
TimeInfo timeInfo() const;
|
TimeInfo timeInfo() const;
|
||||||
bool autoTypeEnabled() const;
|
bool autoTypeEnabled() const;
|
||||||
int autoTypeObfuscation() const;
|
int autoTypeObfuscation() const;
|
||||||
|
@ -68,6 +69,7 @@ public:
|
||||||
void setForegroundColor(const QColor& color);
|
void setForegroundColor(const QColor& color);
|
||||||
void setBackgroundColor(const QColor& color);
|
void setBackgroundColor(const QColor& color);
|
||||||
void setOverrideUrl(const QString& url);
|
void setOverrideUrl(const QString& url);
|
||||||
|
void setTags(const QString& tags);
|
||||||
void setTimeInfo(const TimeInfo& timeInfo);
|
void setTimeInfo(const TimeInfo& timeInfo);
|
||||||
void setAutoTypeEnabled(bool enable);
|
void setAutoTypeEnabled(bool enable);
|
||||||
void setAutoTypeObfuscation(int obfuscation);
|
void setAutoTypeObfuscation(int obfuscation);
|
||||||
|
@ -98,6 +100,7 @@ private:
|
||||||
QColor m_foregroundColor;
|
QColor m_foregroundColor;
|
||||||
QColor m_backgroundColor;
|
QColor m_backgroundColor;
|
||||||
QString m_overrideUrl;
|
QString m_overrideUrl;
|
||||||
|
QString m_tags;
|
||||||
TimeInfo m_timeInfo;
|
TimeInfo m_timeInfo;
|
||||||
bool m_autoTypeEnabled;
|
bool m_autoTypeEnabled;
|
||||||
int m_autoTypeObfuscation;
|
int m_autoTypeObfuscation;
|
||||||
|
|
|
@ -145,6 +145,21 @@ const Group* Metadata::lastTopVisibleGroup() const
|
||||||
return m_lastTopVisibleGroup;
|
return m_lastTopVisibleGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QDateTime Metadata::masterKeyChanged() const
|
||||||
|
{
|
||||||
|
return m_masterKeyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Metadata::masterKeyChangeRec() const
|
||||||
|
{
|
||||||
|
return m_masterKeyChangeRec;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Metadata::masterKeyChangeForce() const
|
||||||
|
{
|
||||||
|
return m_masterKeyChangeForce;
|
||||||
|
}
|
||||||
|
|
||||||
QHash<QString, QString> Metadata::customFields() const
|
QHash<QString, QString> Metadata::customFields() const
|
||||||
{
|
{
|
||||||
return m_customFields;
|
return m_customFields;
|
||||||
|
@ -271,6 +286,21 @@ void Metadata::setLastTopVisibleGroup(Group* group)
|
||||||
m_lastTopVisibleGroup = group;
|
m_lastTopVisibleGroup = group;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Metadata::setMasterKeyChanged(const QDateTime& value)
|
||||||
|
{
|
||||||
|
m_masterKeyChanged = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Metadata::setMasterKeyChangeRec(int value)
|
||||||
|
{
|
||||||
|
m_masterKeyChangeRec = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Metadata::setMasterKeyChangeForce(int value)
|
||||||
|
{
|
||||||
|
m_masterKeyChangeForce = value;
|
||||||
|
}
|
||||||
|
|
||||||
void Metadata::addCustomField(const QString& key, const QString& value)
|
void Metadata::addCustomField(const QString& key, const QString& value)
|
||||||
{
|
{
|
||||||
Q_ASSERT(!m_customFields.contains(key));
|
Q_ASSERT(!m_customFields.contains(key));
|
||||||
|
|
|
@ -57,6 +57,9 @@ public:
|
||||||
QDateTime entryTemplatesGroupChanged() const;
|
QDateTime entryTemplatesGroupChanged() const;
|
||||||
const Group* lastSelectedGroup() const;
|
const Group* lastSelectedGroup() const;
|
||||||
const Group* lastTopVisibleGroup() const;
|
const Group* lastTopVisibleGroup() const;
|
||||||
|
QDateTime masterKeyChanged() const;
|
||||||
|
int masterKeyChangeRec() const;
|
||||||
|
int masterKeyChangeForce() const;
|
||||||
QHash<QString, QString> customFields() const;
|
QHash<QString, QString> customFields() const;
|
||||||
|
|
||||||
void setGenerator(const QString& value);
|
void setGenerator(const QString& value);
|
||||||
|
@ -82,6 +85,9 @@ public:
|
||||||
void setEntryTemplatesGroupChanged(const QDateTime& value);
|
void setEntryTemplatesGroupChanged(const QDateTime& value);
|
||||||
void setLastSelectedGroup(Group* group);
|
void setLastSelectedGroup(Group* group);
|
||||||
void setLastTopVisibleGroup(Group* group);
|
void setLastTopVisibleGroup(Group* group);
|
||||||
|
void setMasterKeyChanged(const QDateTime& value);
|
||||||
|
void setMasterKeyChangeRec(int value);
|
||||||
|
void setMasterKeyChangeForce(int value);
|
||||||
void addCustomField(const QString& key, const QString& value);
|
void addCustomField(const QString& key, const QString& value);
|
||||||
void removeCustomField(const QString& key);
|
void removeCustomField(const QString& key);
|
||||||
|
|
||||||
|
@ -112,6 +118,10 @@ private:
|
||||||
Group* m_lastSelectedGroup;
|
Group* m_lastSelectedGroup;
|
||||||
Group* m_lastTopVisibleGroup;
|
Group* m_lastTopVisibleGroup;
|
||||||
|
|
||||||
|
QDateTime m_masterKeyChanged;
|
||||||
|
int m_masterKeyChangeRec;
|
||||||
|
int m_masterKeyChangeForce;
|
||||||
|
|
||||||
QHash<QString, QString> m_customFields;
|
QHash<QString, QString> m_customFields;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,8 @@
|
||||||
|
|
||||||
#include <QtCore/QtGlobal>
|
#include <QtCore/QtGlobal>
|
||||||
|
|
||||||
|
#include "core/Uuid.h"
|
||||||
|
|
||||||
namespace KeePass2
|
namespace KeePass2
|
||||||
{
|
{
|
||||||
const quint32 SIGNATURE_1 = 0x9AA2D903;
|
const quint32 SIGNATURE_1 = 0x9AA2D903;
|
||||||
|
@ -27,6 +29,10 @@ namespace KeePass2
|
||||||
const quint32 FILE_VERSION = 0x00020000;
|
const quint32 FILE_VERSION = 0x00020000;
|
||||||
const quint32 FILE_VERSION_CRITICAL_MASK = 0xFFFF0000;
|
const quint32 FILE_VERSION_CRITICAL_MASK = 0xFFFF0000;
|
||||||
|
|
||||||
|
const QSysInfo::Endian BYTEORDER = QSysInfo::LittleEndian;
|
||||||
|
|
||||||
|
const Uuid CIPHER_AES = Uuid(QByteArray::fromHex("31c1f2e6bf714350be5805216afc5aff"));
|
||||||
|
|
||||||
enum HeaderFieldID
|
enum HeaderFieldID
|
||||||
{
|
{
|
||||||
EndOfHeader = 0,
|
EndOfHeader = 0,
|
||||||
|
@ -41,13 +47,6 @@ namespace KeePass2
|
||||||
StreamStartBytes = 9,
|
StreamStartBytes = 9,
|
||||||
InnerRandomStreamID = 10
|
InnerRandomStreamID = 10
|
||||||
};
|
};
|
||||||
|
|
||||||
enum CompressionAlgorithm
|
|
||||||
{
|
|
||||||
CompressionNone = 0,
|
|
||||||
CompressionGZip = 1,
|
|
||||||
CompressionCount = 2
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // KEEPASSX_KEEPASS2_H
|
#endif // KEEPASSX_KEEPASS2_H
|
||||||
|
|
|
@ -21,37 +21,37 @@
|
||||||
#include <QtCore/QFile>
|
#include <QtCore/QFile>
|
||||||
#include <QtCore/QIODevice>
|
#include <QtCore/QIODevice>
|
||||||
|
|
||||||
#include "KeePass2XmlReader.h"
|
#include "core/Database.h"
|
||||||
#include "crypto/CryptoHash.h"
|
#include "crypto/CryptoHash.h"
|
||||||
|
#include "format/KeePass2.h"
|
||||||
|
#include "format/KeePass2XmlReader.h"
|
||||||
#include "streams/HashedBlockStream.h"
|
#include "streams/HashedBlockStream.h"
|
||||||
#include "streams/QtIOCompressor"
|
#include "streams/QtIOCompressor"
|
||||||
#include "streams/SymmetricCipherStream.h"
|
#include "streams/SymmetricCipherStream.h"
|
||||||
|
|
||||||
const QSysInfo::Endian KeePass2Reader::BYTEORDER = QSysInfo::LittleEndian;
|
|
||||||
|
|
||||||
Database* KeePass2Reader::readDatabase(QIODevice* device, const CompositeKey& key)
|
Database* KeePass2Reader::readDatabase(QIODevice* device, const CompositeKey& key)
|
||||||
{
|
{
|
||||||
|
m_db = new Database();
|
||||||
m_device = device;
|
m_device = device;
|
||||||
m_error = false;
|
m_error = false;
|
||||||
m_errorStr = QString();
|
m_errorStr = QString();
|
||||||
m_headerEnd = false;
|
m_headerEnd = false;
|
||||||
m_cipher = Uuid();
|
|
||||||
|
|
||||||
bool ok;
|
bool ok;
|
||||||
|
|
||||||
quint32 signature1 = Endian::readUInt32(m_device, BYTEORDER, &ok);
|
quint32 signature1 = Endian::readUInt32(m_device, KeePass2::BYTEORDER, &ok);
|
||||||
if (!ok || signature1 != KeePass2::SIGNATURE_1) {
|
if (!ok || signature1 != KeePass2::SIGNATURE_1) {
|
||||||
raiseError("1");
|
raiseError("1");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
quint32 signature2 = Endian::readUInt32(m_device, BYTEORDER, &ok);
|
quint32 signature2 = Endian::readUInt32(m_device, KeePass2::BYTEORDER, &ok);
|
||||||
if (!ok || signature2 != KeePass2::SIGNATURE_2) {
|
if (!ok || signature2 != KeePass2::SIGNATURE_2) {
|
||||||
raiseError("2");
|
raiseError("2");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
quint32 version = Endian::readUInt32(m_device, BYTEORDER, &ok) & KeePass2::FILE_VERSION_CRITICAL_MASK;
|
quint32 version = Endian::readUInt32(m_device, KeePass2::BYTEORDER, &ok) & KeePass2::FILE_VERSION_CRITICAL_MASK;
|
||||||
quint32 expectedVersion = KeePass2::FILE_VERSION & KeePass2::FILE_VERSION_CRITICAL_MASK;
|
quint32 expectedVersion = KeePass2::FILE_VERSION & KeePass2::FILE_VERSION_CRITICAL_MASK;
|
||||||
// TODO do we support old Kdbx versions?
|
// TODO do we support old Kdbx versions?
|
||||||
if (!ok || (version != expectedVersion)) {
|
if (!ok || (version != expectedVersion)) {
|
||||||
|
@ -62,9 +62,12 @@ Database* KeePass2Reader::readDatabase(QIODevice* device, const CompositeKey& ke
|
||||||
while (readHeaderField() && !error()) {
|
while (readHeaderField() && !error()) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QByteArray transformedMasterKey = key.transform(m_db->transformSeed(), m_db->transformRounds());
|
||||||
|
m_db->setTransformedMasterKey(transformedMasterKey);
|
||||||
|
|
||||||
CryptoHash hash(CryptoHash::Sha256);
|
CryptoHash hash(CryptoHash::Sha256);
|
||||||
hash.addData(m_masterSeed);
|
hash.addData(m_masterSeed);
|
||||||
hash.addData(key.transform(m_transformSeed, m_transformRounds));
|
hash.addData(transformedMasterKey);
|
||||||
QByteArray finalKey = hash.result();
|
QByteArray finalKey = hash.result();
|
||||||
|
|
||||||
SymmetricCipherStream cipherStream(device, SymmetricCipher::Aes256, SymmetricCipher::Cbc, SymmetricCipher::Decrypt, finalKey, m_encryptionIV);
|
SymmetricCipherStream cipherStream(device, SymmetricCipher::Aes256, SymmetricCipher::Cbc, SymmetricCipher::Decrypt, finalKey, m_encryptionIV);
|
||||||
|
@ -83,7 +86,7 @@ Database* KeePass2Reader::readDatabase(QIODevice* device, const CompositeKey& ke
|
||||||
QIODevice* xmlDevice;
|
QIODevice* xmlDevice;
|
||||||
QScopedPointer<QtIOCompressor> ioCompressor;
|
QScopedPointer<QtIOCompressor> ioCompressor;
|
||||||
|
|
||||||
if (m_compression == KeePass2::CompressionNone) {
|
if (m_db->compressionAlgo() == Database::CompressionNone) {
|
||||||
xmlDevice = &hashedStream;
|
xmlDevice = &hashedStream;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -94,9 +97,9 @@ Database* KeePass2Reader::readDatabase(QIODevice* device, const CompositeKey& ke
|
||||||
}
|
}
|
||||||
|
|
||||||
KeePass2XmlReader xmlReader;
|
KeePass2XmlReader xmlReader;
|
||||||
Database* db = xmlReader.readDatabase(xmlDevice);
|
xmlReader.readDatabase(xmlDevice, m_db);
|
||||||
// TODO forward error messages from xmlReader
|
// TODO forward error messages from xmlReader
|
||||||
return db;
|
return m_db;
|
||||||
}
|
}
|
||||||
|
|
||||||
Database* KeePass2Reader::readDatabase(const QString& filename, const CompositeKey& key)
|
Database* KeePass2Reader::readDatabase(const QString& filename, const CompositeKey& key)
|
||||||
|
@ -136,7 +139,7 @@ bool KeePass2Reader::readHeaderField()
|
||||||
quint8 fieldID = fieldIDArray.at(0);
|
quint8 fieldID = fieldIDArray.at(0);
|
||||||
|
|
||||||
bool ok;
|
bool ok;
|
||||||
quint16 fieldLen = Endian::readUInt16(m_device, BYTEORDER, &ok);
|
quint16 fieldLen = Endian::readUInt16(m_device, KeePass2::BYTEORDER, &ok);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
raiseError("");
|
raiseError("");
|
||||||
return false;
|
return false;
|
||||||
|
@ -206,7 +209,14 @@ void KeePass2Reader::setCipher(const QByteArray& data)
|
||||||
raiseError("");
|
raiseError("");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
m_cipher = Uuid(data);
|
Uuid uuid(data);
|
||||||
|
|
||||||
|
if (uuid != KeePass2::CIPHER_AES) {
|
||||||
|
raiseError("");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_db->setCipher(uuid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,13 +226,13 @@ void KeePass2Reader::setCompressionFlags(const QByteArray& data)
|
||||||
raiseError("");
|
raiseError("");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
quint32 id = Endian::bytesToUInt32(data, BYTEORDER);
|
quint32 id = Endian::bytesToUInt32(data, KeePass2::BYTEORDER);
|
||||||
|
|
||||||
if (id >= KeePass2::CompressionCount) {
|
if (id > Database::CompressionAlgorithmMax) {
|
||||||
raiseError("");
|
raiseError("");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
m_compression = static_cast<KeePass2::CompressionAlgorithm>(id);
|
m_db->setCompressionAlgo(static_cast<Database::CompressionAlgorithm>(id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -243,7 +253,7 @@ void KeePass2Reader::setTransformSeed(const QByteArray& data)
|
||||||
raiseError("");
|
raiseError("");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
m_transformSeed = data;
|
m_db->setTransformSeed(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,7 +263,7 @@ void KeePass2Reader::setTansformRounds(const QByteArray& data)
|
||||||
raiseError("");
|
raiseError("");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
m_transformRounds = Endian::bytesToUInt64(data, BYTEORDER);
|
m_db->setTransformRounds(Endian::bytesToUInt64(data, KeePass2::BYTEORDER));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,6 @@
|
||||||
#include "core/Endian.h"
|
#include "core/Endian.h"
|
||||||
#include "core/Uuid.h"
|
#include "core/Uuid.h"
|
||||||
#include "keys/CompositeKey.h"
|
#include "keys/CompositeKey.h"
|
||||||
#include "format/KeePass2.h"
|
|
||||||
|
|
||||||
class Database;
|
class Database;
|
||||||
|
|
||||||
|
@ -52,18 +51,13 @@ private:
|
||||||
void setStreamStartBytes(const QByteArray& data);
|
void setStreamStartBytes(const QByteArray& data);
|
||||||
void setInnerRandomStreamID(const QByteArray& data);
|
void setInnerRandomStreamID(const QByteArray& data);
|
||||||
|
|
||||||
static const QSysInfo::Endian BYTEORDER;
|
|
||||||
|
|
||||||
QIODevice* m_device;
|
QIODevice* m_device;
|
||||||
bool m_error;
|
bool m_error;
|
||||||
QString m_errorStr;
|
QString m_errorStr;
|
||||||
bool m_headerEnd;
|
bool m_headerEnd;
|
||||||
|
|
||||||
Uuid m_cipher;
|
Database* m_db;
|
||||||
KeePass2::CompressionAlgorithm m_compression;
|
|
||||||
QByteArray m_masterSeed;
|
QByteArray m_masterSeed;
|
||||||
QByteArray m_transformSeed;
|
|
||||||
quint64 m_transformRounds;
|
|
||||||
QByteArray m_encryptionIV;
|
QByteArray m_encryptionIV;
|
||||||
QByteArray m_streamStartBytes;
|
QByteArray m_streamStartBytes;
|
||||||
};
|
};
|
||||||
|
|
132
src/format/KeePass2Writer.cpp
Normal file
132
src/format/KeePass2Writer.cpp
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
/*
|
||||||
|
* 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 "KeePass2Writer.h"
|
||||||
|
|
||||||
|
#include <QtCore/QFile>
|
||||||
|
|
||||||
|
#include "core/Database.h"
|
||||||
|
#include "core/Endian.h"
|
||||||
|
#include "crypto/CryptoHash.h"
|
||||||
|
#include "crypto/Random.h"
|
||||||
|
#include "format/KeePass2XmlWriter.h"
|
||||||
|
#include "streams/HashedBlockStream.h"
|
||||||
|
#include "streams/QtIOCompressor"
|
||||||
|
#include "streams/SymmetricCipherStream.h"
|
||||||
|
|
||||||
|
#define CHECK_RETURN(x) if (!(x)) return;
|
||||||
|
#define CHECK_RETURN_FALSE(x) if (!(x)) return false;
|
||||||
|
|
||||||
|
KeePass2Writer::KeePass2Writer()
|
||||||
|
: m_error(false)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void KeePass2Writer::writeDatabase(QIODevice* device, Database* db)
|
||||||
|
{
|
||||||
|
m_error = false;
|
||||||
|
m_errorStr = QString();
|
||||||
|
|
||||||
|
m_device = device;
|
||||||
|
|
||||||
|
QByteArray masterSeed = Random::randomArray(32);
|
||||||
|
QByteArray encryptionIV = Random::randomArray(16);
|
||||||
|
QByteArray startBytes = Random::randomArray(32);
|
||||||
|
QByteArray endOfHeader = "\r\n\r\n";
|
||||||
|
|
||||||
|
CryptoHash hash(CryptoHash::Sha256);
|
||||||
|
hash.addData(masterSeed);
|
||||||
|
hash.addData(db->transformedMasterKey());
|
||||||
|
QByteArray finalKey = hash.result();
|
||||||
|
|
||||||
|
|
||||||
|
CHECK_RETURN(writeData(Endian::int32ToBytes(KeePass2::SIGNATURE_1, KeePass2::BYTEORDER)));
|
||||||
|
CHECK_RETURN(writeData(Endian::int32ToBytes(KeePass2::SIGNATURE_2, KeePass2::BYTEORDER)));
|
||||||
|
CHECK_RETURN(writeData(Endian::int32ToBytes(KeePass2::FILE_VERSION, KeePass2::BYTEORDER)));
|
||||||
|
|
||||||
|
CHECK_RETURN(writeHeaderField(KeePass2::CipherID, db->cipher().toByteArray()));
|
||||||
|
CHECK_RETURN(writeHeaderField(KeePass2::CompressionFlags, Endian::int32ToBytes(db->compressionAlgo(), KeePass2::BYTEORDER)));
|
||||||
|
CHECK_RETURN(writeHeaderField(KeePass2::MasterSeed, masterSeed));
|
||||||
|
CHECK_RETURN(writeHeaderField(KeePass2::TransformSeed, db->transformSeed()));
|
||||||
|
CHECK_RETURN(writeHeaderField(KeePass2::TransformRounds, Endian::int64ToBytes(db->transformRounds(), KeePass2::BYTEORDER)));
|
||||||
|
CHECK_RETURN(writeHeaderField(KeePass2::EncryptionIV, encryptionIV));
|
||||||
|
CHECK_RETURN(writeHeaderField(KeePass2::StreamStartBytes, startBytes));
|
||||||
|
CHECK_RETURN(writeHeaderField(KeePass2::EndOfHeader, endOfHeader));
|
||||||
|
|
||||||
|
SymmetricCipherStream cipherStream(device, SymmetricCipher::Aes256, SymmetricCipher::Cbc, SymmetricCipher::Encrypt, finalKey, encryptionIV);
|
||||||
|
cipherStream.open(QIODevice::WriteOnly);
|
||||||
|
m_device = &cipherStream;
|
||||||
|
CHECK_RETURN(writeData(startBytes));
|
||||||
|
|
||||||
|
HashedBlockStream hashedStream(&cipherStream);
|
||||||
|
hashedStream.open(QIODevice::WriteOnly);
|
||||||
|
|
||||||
|
QScopedPointer<QtIOCompressor> ioCompressor;
|
||||||
|
|
||||||
|
if (db->compressionAlgo() == Database::CompressionNone) {
|
||||||
|
m_device = &hashedStream;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ioCompressor.reset(new QtIOCompressor(&hashedStream));
|
||||||
|
ioCompressor->setStreamFormat(QtIOCompressor::GzipFormat);
|
||||||
|
ioCompressor->open(QIODevice::WriteOnly);
|
||||||
|
m_device = ioCompressor.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
KeePass2XmlWriter xmlWriter;
|
||||||
|
xmlWriter.writeDatabase(m_device, db);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool KeePass2Writer::writeData(const QByteArray& data)
|
||||||
|
{
|
||||||
|
if (m_device->write(data) != data.size()) {
|
||||||
|
m_error = true;
|
||||||
|
m_errorStr = m_device->errorString();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool KeePass2Writer::writeHeaderField(KeePass2::HeaderFieldID fieldId, const QByteArray& data)
|
||||||
|
{
|
||||||
|
QByteArray fieldIdArr;
|
||||||
|
fieldIdArr[0] = fieldId;
|
||||||
|
CHECK_RETURN_FALSE(writeData(fieldIdArr));
|
||||||
|
CHECK_RETURN_FALSE(writeData(Endian::int16ToBytes(data.size(), KeePass2::BYTEORDER)));
|
||||||
|
CHECK_RETURN_FALSE(writeData(data));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void KeePass2Writer::writeDatabase(const QString& filename, Database* db)
|
||||||
|
{
|
||||||
|
QFile file(filename);
|
||||||
|
file.open(QIODevice::WriteOnly|QIODevice::Truncate);
|
||||||
|
writeDatabase(&file, db);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool KeePass2Writer::error()
|
||||||
|
{
|
||||||
|
return m_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString KeePass2Writer::errorString()
|
||||||
|
{
|
||||||
|
return m_errorStr;
|
||||||
|
}
|
46
src/format/KeePass2Writer.h
Normal file
46
src/format/KeePass2Writer.h
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* 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_KEEPASS2WRITER_H
|
||||||
|
#define KEEPASSX_KEEPASS2WRITER_H
|
||||||
|
|
||||||
|
#include <QtCore/QIODevice>
|
||||||
|
|
||||||
|
#include "format/KeePass2.h"
|
||||||
|
#include "keys/CompositeKey.h"
|
||||||
|
|
||||||
|
class Database;
|
||||||
|
|
||||||
|
class KeePass2Writer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
KeePass2Writer();
|
||||||
|
void writeDatabase(QIODevice* device, Database* db);
|
||||||
|
void writeDatabase(const QString& filename, Database* db);
|
||||||
|
bool error();
|
||||||
|
QString errorString();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool writeData(const QByteArray& data);
|
||||||
|
bool writeHeaderField(KeePass2::HeaderFieldID fieldId, const QByteArray& data);
|
||||||
|
|
||||||
|
QIODevice* m_device;
|
||||||
|
bool m_error;
|
||||||
|
QString m_errorStr;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // KEEPASSX_KEEPASS2WRITER_H
|
|
@ -29,11 +29,11 @@ KeePass2XmlReader::KeePass2XmlReader()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
Database* KeePass2XmlReader::readDatabase(QIODevice* device)
|
void KeePass2XmlReader::readDatabase(QIODevice* device, Database* db)
|
||||||
{
|
{
|
||||||
m_xml.setDevice(device);
|
m_xml.setDevice(device);
|
||||||
|
|
||||||
m_db = new Database();
|
m_db = db;
|
||||||
m_meta = m_db->metadata();
|
m_meta = m_db->metadata();
|
||||||
|
|
||||||
m_tmpParent = new Group();
|
m_tmpParent = new Group();
|
||||||
|
@ -50,8 +50,13 @@ Database* KeePass2XmlReader::readDatabase(QIODevice* device)
|
||||||
}
|
}
|
||||||
|
|
||||||
delete m_tmpParent;
|
delete m_tmpParent;
|
||||||
|
}
|
||||||
|
|
||||||
return m_db;
|
Database* KeePass2XmlReader::readDatabase(QIODevice* device)
|
||||||
|
{
|
||||||
|
Database* db = new Database();
|
||||||
|
readDatabase(device, db);
|
||||||
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
Database* KeePass2XmlReader::readDatabase(const QString& filename)
|
Database* KeePass2XmlReader::readDatabase(const QString& filename)
|
||||||
|
@ -120,6 +125,15 @@ void KeePass2XmlReader::parseMeta()
|
||||||
else if (m_xml.name() == "MaintenanceHistoryDays") {
|
else if (m_xml.name() == "MaintenanceHistoryDays") {
|
||||||
m_meta->setMaintenanceHistoryDays(readNumber());
|
m_meta->setMaintenanceHistoryDays(readNumber());
|
||||||
}
|
}
|
||||||
|
else if (m_xml.name() == "MasterKeyChanged") {
|
||||||
|
m_meta->setMasterKeyChanged(readDateTime());
|
||||||
|
}
|
||||||
|
else if (m_xml.name() == "MasterKeyChangeRec") {
|
||||||
|
m_meta->setMasterKeyChangeRec(readNumber());
|
||||||
|
}
|
||||||
|
else if (m_xml.name() == "MasterKeyChangeForce") {
|
||||||
|
m_meta->setMasterKeyChangeForce(readNumber());
|
||||||
|
}
|
||||||
else if (m_xml.name() == "MemoryProtection") {
|
else if (m_xml.name() == "MemoryProtection") {
|
||||||
parseMemoryProtection();
|
parseMemoryProtection();
|
||||||
}
|
}
|
||||||
|
@ -461,6 +475,9 @@ Entry* KeePass2XmlReader::parseEntry(bool history)
|
||||||
else if (m_xml.name() == "OverrideURL") {
|
else if (m_xml.name() == "OverrideURL") {
|
||||||
entry->setOverrideUrl(readString());
|
entry->setOverrideUrl(readString());
|
||||||
}
|
}
|
||||||
|
else if (m_xml.name() == "Tags") {
|
||||||
|
entry->setTags(readString());
|
||||||
|
}
|
||||||
else if (m_xml.name() == "Times") {
|
else if (m_xml.name() == "Times") {
|
||||||
entry->setTimeInfo(parseTimes());
|
entry->setTimeInfo(parseTimes());
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ class KeePass2XmlReader
|
||||||
public:
|
public:
|
||||||
KeePass2XmlReader();
|
KeePass2XmlReader();
|
||||||
Database* readDatabase(QIODevice* device);
|
Database* readDatabase(QIODevice* device);
|
||||||
|
void readDatabase(QIODevice* device, Database* db);
|
||||||
Database* readDatabase(const QString& filename);
|
Database* readDatabase(const QString& filename);
|
||||||
bool error();
|
bool error();
|
||||||
QString errorString();
|
QString errorString();
|
||||||
|
|
|
@ -53,7 +53,7 @@ void KeePass2XmlWriter::writeDatabase(QIODevice* device, Database* db)
|
||||||
void KeePass2XmlWriter::writeDatabase(const QString& filename, Database* db)
|
void KeePass2XmlWriter::writeDatabase(const QString& filename, Database* db)
|
||||||
{
|
{
|
||||||
QFile file(filename);
|
QFile file(filename);
|
||||||
file.open(QIODevice::WriteOnly);
|
file.open(QIODevice::WriteOnly|QIODevice::Truncate);
|
||||||
writeDatabase(&file, db);
|
writeDatabase(&file, db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,6 +69,9 @@ void KeePass2XmlWriter::writeMetadata()
|
||||||
writeString("DefaultUserName", m_meta->defaultUserName());
|
writeString("DefaultUserName", m_meta->defaultUserName());
|
||||||
writeDateTime("DefaultUserNameChanged", m_meta->defaultUserNameChanged());
|
writeDateTime("DefaultUserNameChanged", m_meta->defaultUserNameChanged());
|
||||||
writeNumber("MaintenanceHistoryDays", m_meta->maintenanceHistoryDays());
|
writeNumber("MaintenanceHistoryDays", m_meta->maintenanceHistoryDays());
|
||||||
|
writeDateTime("MasterKeyChanged", m_meta->masterKeyChanged());
|
||||||
|
writeNumber("MasterKeyChangeRec", m_meta->masterKeyChangeRec());
|
||||||
|
writeNumber("MasterKeyChangeForce", m_meta->masterKeyChangeForce());
|
||||||
writeMemoryProtection();
|
writeMemoryProtection();
|
||||||
writeCustomIcons();
|
writeCustomIcons();
|
||||||
writeBool("RecycleBinEnabled", m_meta->recycleBinEnabled());
|
writeBool("RecycleBinEnabled", m_meta->recycleBinEnabled());
|
||||||
|
@ -263,16 +266,17 @@ void KeePass2XmlWriter::writeEntry(const Entry* entry)
|
||||||
writeColor("ForegroundColor", entry->foregroundColor());
|
writeColor("ForegroundColor", entry->foregroundColor());
|
||||||
writeColor("BackgroundColor", entry->backgroundColor());
|
writeColor("BackgroundColor", entry->backgroundColor());
|
||||||
writeString("OverrideURL", entry->overrideUrl());
|
writeString("OverrideURL", entry->overrideUrl());
|
||||||
|
writeString("Tags", entry->tags());
|
||||||
writeTimes(entry->timeInfo());
|
writeTimes(entry->timeInfo());
|
||||||
|
|
||||||
Q_FOREACH (const QString& key, entry->attributes()) {
|
Q_FOREACH (const QString& key, entry->attributes().keys()) {
|
||||||
m_xml.writeStartElement("String");
|
m_xml.writeStartElement("String");
|
||||||
writeString("Key", key);
|
writeString("Key", key);
|
||||||
writeString("Value", entry->attributes().value(key));
|
writeString("Value", entry->attributes().value(key));
|
||||||
m_xml.writeEndElement();
|
m_xml.writeEndElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
Q_FOREACH (const QString& key, entry->attachments()) {
|
Q_FOREACH (const QString& key, entry->attachments().keys()) {
|
||||||
m_xml.writeStartElement("Binary");
|
m_xml.writeStartElement("Binary");
|
||||||
writeString("Key", key);
|
writeString("Key", key);
|
||||||
writeBinary("Value", entry->attachments().value(key));
|
writeBinary("Value", entry->attachments().value(key));
|
||||||
|
|
|
@ -38,6 +38,11 @@ HashedBlockStream::HashedBlockStream(QIODevice* baseDevice, qint32 blockSize)
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HashedBlockStream::~HashedBlockStream()
|
||||||
|
{
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
void HashedBlockStream::init()
|
void HashedBlockStream::init()
|
||||||
{
|
{
|
||||||
m_buffer.clear();
|
m_buffer.clear();
|
||||||
|
|
|
@ -29,6 +29,7 @@ class HashedBlockStream : public LayeredStream
|
||||||
public:
|
public:
|
||||||
explicit HashedBlockStream(QIODevice* baseDevice);
|
explicit HashedBlockStream(QIODevice* baseDevice);
|
||||||
HashedBlockStream(QIODevice* baseDevice, qint32 blockSize);
|
HashedBlockStream(QIODevice* baseDevice, qint32 blockSize);
|
||||||
|
~HashedBlockStream();
|
||||||
|
|
||||||
bool reset();
|
bool reset();
|
||||||
void close();
|
void close();
|
||||||
|
|
|
@ -24,6 +24,11 @@ LayeredStream::LayeredStream(QIODevice* baseDevice)
|
||||||
connect(baseDevice, SIGNAL(aboutToClose()), SLOT(closeStream()));
|
connect(baseDevice, SIGNAL(aboutToClose()), SLOT(closeStream()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LayeredStream::~LayeredStream()
|
||||||
|
{
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
bool LayeredStream::isSequential() const
|
bool LayeredStream::isSequential() const
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -26,6 +26,7 @@ class LayeredStream : public QIODevice
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit LayeredStream(QIODevice* baseDevice);
|
explicit LayeredStream(QIODevice* baseDevice);
|
||||||
|
virtual ~LayeredStream();
|
||||||
|
|
||||||
bool isSequential() const;
|
bool isSequential() const;
|
||||||
virtual QString errorString() const;
|
virtual QString errorString() const;
|
||||||
|
|
|
@ -28,6 +28,11 @@ SymmetricCipherStream::SymmetricCipherStream(QIODevice* baseDevice, SymmetricCip
|
||||||
m_cipher = new SymmetricCipher(algo, mode, direction, key, iv);
|
m_cipher = new SymmetricCipher(algo, mode, direction, key, iv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SymmetricCipherStream::~SymmetricCipherStream()
|
||||||
|
{
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
bool SymmetricCipherStream::reset()
|
bool SymmetricCipherStream::reset()
|
||||||
{
|
{
|
||||||
if (isWritable()) {
|
if (isWritable()) {
|
||||||
|
|
|
@ -30,6 +30,7 @@ class SymmetricCipherStream : public LayeredStream
|
||||||
public:
|
public:
|
||||||
SymmetricCipherStream(QIODevice* baseDevice, SymmetricCipher::Algorithm algo, SymmetricCipher::Mode mode,
|
SymmetricCipherStream(QIODevice* baseDevice, SymmetricCipher::Algorithm algo, SymmetricCipher::Mode mode,
|
||||||
SymmetricCipher::Direction direction, const QByteArray& key, const QByteArray& iv);
|
SymmetricCipher::Direction direction, const QByteArray& key, const QByteArray& iv);
|
||||||
|
~SymmetricCipherStream();
|
||||||
bool reset();
|
bool reset();
|
||||||
void close();
|
void close();
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
****************************************************************************/
|
****************************************************************************/
|
||||||
|
|
||||||
#include "qtiocompressor.h"
|
#include "qtiocompressor.h"
|
||||||
#include "zlib.h"
|
#include <zlib.h>
|
||||||
#include <QtCore/QDebug>
|
#include <QtCore/QDebug>
|
||||||
|
|
||||||
typedef Bytef ZlibByte;
|
typedef Bytef ZlibByte;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue