mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-05-09 18:15:11 -04:00
Rename KeePass2{,Xml}{R,W} to Kdbx3{,Xml}{R,W}, and add a redirection class
This class will in future select Kdbx4{R,W} as appropriate.
This commit is contained in:
parent
e5ec585f98
commit
3461cbfb06
20 changed files with 1099 additions and 734 deletions
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* 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
|
||||
|
@ -15,198 +15,30 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "KeePass2Reader.h"
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QtGlobal>
|
||||
#include <QString>
|
||||
#include <QFile>
|
||||
#include <QIODevice>
|
||||
|
||||
#include "core/Database.h"
|
||||
#include "core/Endian.h"
|
||||
#include "crypto/CryptoHash.h"
|
||||
#include "crypto/kdf/AesKdf.h"
|
||||
#include "keys/CompositeKey.h"
|
||||
#include "format/KeePass2Reader.h"
|
||||
#include "format/KeePass1.h"
|
||||
#include "format/KeePass2.h"
|
||||
#include "format/KeePass2RandomStream.h"
|
||||
#include "format/KeePass2XmlReader.h"
|
||||
#include "streams/HashedBlockStream.h"
|
||||
#include "streams/QtIOCompressor"
|
||||
#include "streams/StoreDataStream.h"
|
||||
#include "streams/SymmetricCipherStream.h"
|
||||
#include "format/Kdbx3Reader.h"
|
||||
|
||||
KeePass2Reader::KeePass2Reader()
|
||||
: m_device(nullptr)
|
||||
, m_headerStream(nullptr)
|
||||
, m_error(false)
|
||||
, m_headerEnd(false)
|
||||
, m_saveXml(false)
|
||||
, m_db(nullptr)
|
||||
, m_irsAlgo(KeePass2::InvalidProtectedStreamAlgo)
|
||||
BaseKeePass2Reader::BaseKeePass2Reader() :
|
||||
m_error(false),
|
||||
m_saveXml(false),
|
||||
m_irsAlgo(KeePass2::InvalidProtectedStreamAlgo)
|
||||
{
|
||||
}
|
||||
|
||||
Database* KeePass2Reader::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_headerEnd = false;
|
||||
m_xmlData.clear();
|
||||
m_masterSeed.clear();
|
||||
m_encryptionIV.clear();
|
||||
m_streamStartBytes.clear();
|
||||
m_protectedStreamKey.clear();
|
||||
|
||||
StoreDataStream headerStream(m_device);
|
||||
headerStream.open(QIODevice::ReadOnly);
|
||||
m_headerStream = &headerStream;
|
||||
|
||||
bool ok;
|
||||
|
||||
quint32 signature1 = Endian::readUInt32(m_headerStream, KeePass2::BYTEORDER, &ok);
|
||||
if (!ok || signature1 != KeePass2::SIGNATURE_1) {
|
||||
raiseError(tr("Not a KeePass database."));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
quint32 signature2 = Endian::readUInt32(m_headerStream, 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::readUInt32(m_headerStream, KeePass2::BYTEORDER, &ok)
|
||||
& KeePass2::FILE_VERSION_CRITICAL_MASK;
|
||||
quint32 maxVersion = KeePass2::FILE_VERSION & KeePass2::FILE_VERSION_CRITICAL_MASK;
|
||||
if (!ok || (version < KeePass2::FILE_VERSION_MIN) || (version > maxVersion)) {
|
||||
raiseError(tr("Unsupported KeePass database version."));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
while (readHeaderField() && !hasError()) {
|
||||
}
|
||||
|
||||
headerStream.close();
|
||||
|
||||
if (hasError()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// check if all required headers were present
|
||||
if (m_masterSeed.isEmpty() || m_encryptionIV.isEmpty()
|
||||
|| m_streamStartBytes.isEmpty() || m_protectedStreamKey.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();
|
||||
|
||||
SymmetricCipherStream cipherStream(m_device, SymmetricCipher::cipherToAlgorithm(m_db->cipher()),
|
||||
SymmetricCipher::Cbc, SymmetricCipher::Decrypt);
|
||||
if (!cipherStream.init(finalKey, m_encryptionIV)) {
|
||||
raiseError(cipherStream.errorString());
|
||||
return nullptr;
|
||||
}
|
||||
if (!cipherStream.open(QIODevice::ReadOnly)) {
|
||||
raiseError(cipherStream.errorString());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QByteArray realStart = cipherStream.read(32);
|
||||
|
||||
if (realStart != m_streamStartBytes) {
|
||||
raiseError(tr("Wrong key or database file is corrupt."));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
HashedBlockStream hashedStream(&cipherStream);
|
||||
if (!hashedStream.open(QIODevice::ReadOnly)) {
|
||||
raiseError(hashedStream.errorString());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QIODevice* xmlDevice;
|
||||
QScopedPointer<QtIOCompressor> ioCompressor;
|
||||
|
||||
if (m_db->compressionAlgo() == Database::CompressionNone) {
|
||||
xmlDevice = &hashedStream;
|
||||
}
|
||||
else {
|
||||
ioCompressor.reset(new QtIOCompressor(&hashedStream));
|
||||
ioCompressor->setStreamFormat(QtIOCompressor::GzipFormat);
|
||||
if (!ioCompressor->open(QIODevice::ReadOnly)) {
|
||||
raiseError(ioCompressor->errorString());
|
||||
return nullptr;
|
||||
}
|
||||
xmlDevice = ioCompressor.data();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
KeePass2XmlReader xmlReader;
|
||||
xmlReader.readDatabase(xmlDevice, m_db, &randomStream);
|
||||
|
||||
if (xmlReader.hasError()) {
|
||||
raiseError(xmlReader.errorString());
|
||||
if (keepDatabase) {
|
||||
return db.take();
|
||||
}
|
||||
else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Q_ASSERT(version < 0x00030001 || !xmlReader.headerHash().isEmpty());
|
||||
|
||||
if (!xmlReader.headerHash().isEmpty()) {
|
||||
QByteArray headerHash = CryptoHash::hash(headerStream.storedData(), CryptoHash::Sha256);
|
||||
if (headerHash != xmlReader.headerHash()) {
|
||||
raiseError("Header doesn't match hash");
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
return db.take();
|
||||
}
|
||||
|
||||
Database* KeePass2Reader::readDatabase(const QString& filename, const CompositeKey& key)
|
||||
BaseKeePass2Reader::~BaseKeePass2Reader() {}
|
||||
|
||||
Database* BaseKeePass2Reader::readDatabase(const QString& filename, const CompositeKey& key)
|
||||
{
|
||||
QFile file(filename);
|
||||
if (!file.open(QFile::ReadOnly)) {
|
||||
|
@ -224,238 +56,113 @@ Database* KeePass2Reader::readDatabase(const QString& filename, const CompositeK
|
|||
return db.take();
|
||||
}
|
||||
|
||||
bool KeePass2Reader::hasError()
|
||||
bool BaseKeePass2Reader::hasError()
|
||||
{
|
||||
return m_error;
|
||||
}
|
||||
|
||||
QString KeePass2Reader::errorString()
|
||||
QString BaseKeePass2Reader::errorString()
|
||||
{
|
||||
return m_errorStr;
|
||||
}
|
||||
|
||||
void KeePass2Reader::setSaveXml(bool save)
|
||||
void BaseKeePass2Reader::setSaveXml(bool save)
|
||||
{
|
||||
m_saveXml = save;
|
||||
}
|
||||
|
||||
QByteArray KeePass2Reader::xmlData()
|
||||
QByteArray BaseKeePass2Reader::xmlData()
|
||||
{
|
||||
return m_xmlData;
|
||||
}
|
||||
|
||||
QByteArray KeePass2Reader::streamKey()
|
||||
QByteArray BaseKeePass2Reader::streamKey()
|
||||
{
|
||||
return m_protectedStreamKey;
|
||||
}
|
||||
|
||||
void KeePass2Reader::raiseError(const QString& errorMessage)
|
||||
KeePass2::ProtectedStreamAlgo BaseKeePass2Reader::protectedStreamAlgo() const {
|
||||
return m_irsAlgo;
|
||||
}
|
||||
|
||||
|
||||
void BaseKeePass2Reader::raiseError(const QString& errorMessage)
|
||||
{
|
||||
m_error = true;
|
||||
m_errorStr = errorMessage;
|
||||
}
|
||||
|
||||
bool KeePass2Reader::readHeaderField()
|
||||
Database* KeePass2Reader::readDatabase(QIODevice* device, const CompositeKey& key, bool keepDatabase)
|
||||
{
|
||||
QByteArray fieldIDArray = m_headerStream->read(1);
|
||||
if (fieldIDArray.size() != 1) {
|
||||
raiseError("Invalid header id size");
|
||||
return false;
|
||||
}
|
||||
quint8 fieldID = fieldIDArray.at(0);
|
||||
m_error = false;
|
||||
m_errorStr.clear();
|
||||
|
||||
bool ok;
|
||||
quint16 fieldLen = Endian::readUInt16(m_headerStream, KeePass2::BYTEORDER, &ok);
|
||||
if (!ok) {
|
||||
raiseError("Invalid header field length");
|
||||
return false;
|
||||
|
||||
quint32 signature1 = Endian::readUInt32(device, KeePass2::BYTEORDER, &ok);
|
||||
if (!ok || signature1 != KeePass2::SIGNATURE_1) {
|
||||
raiseError(tr("Not a KeePass database."));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QByteArray fieldData;
|
||||
if (fieldLen != 0) {
|
||||
fieldData = m_headerStream->read(fieldLen);
|
||||
if (fieldData.size() != fieldLen) {
|
||||
raiseError("Invalid header data length");
|
||||
return false;
|
||||
}
|
||||
quint32 signature2 = Endian::readUInt32(device, 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;
|
||||
}
|
||||
|
||||
switch (fieldID) {
|
||||
case KeePass2::EndOfHeader:
|
||||
m_headerEnd = true;
|
||||
break;
|
||||
|
||||
case KeePass2::CipherID:
|
||||
setCipher(fieldData);
|
||||
break;
|
||||
|
||||
case KeePass2::CompressionFlags:
|
||||
setCompressionFlags(fieldData);
|
||||
break;
|
||||
|
||||
case KeePass2::MasterSeed:
|
||||
setMasterSeed(fieldData);
|
||||
break;
|
||||
|
||||
case KeePass2::TransformSeed:
|
||||
setTransformSeed(fieldData);
|
||||
break;
|
||||
|
||||
case KeePass2::TransformRounds:
|
||||
setTransformRounds(fieldData);
|
||||
break;
|
||||
|
||||
case KeePass2::EncryptionIV:
|
||||
setEncryptionIV(fieldData);
|
||||
break;
|
||||
|
||||
case KeePass2::ProtectedStreamKey:
|
||||
setProtectedStreamKey(fieldData);
|
||||
break;
|
||||
|
||||
case KeePass2::StreamStartBytes:
|
||||
setStreamStartBytes(fieldData);
|
||||
break;
|
||||
|
||||
case KeePass2::InnerRandomStreamID:
|
||||
setInnerRandomStreamID(fieldData);
|
||||
break;
|
||||
|
||||
default:
|
||||
qWarning("Unknown header field read: id=%d", fieldID);
|
||||
break;
|
||||
m_version = Endian::readUInt32(device, KeePass2::BYTEORDER, &ok)
|
||||
& KeePass2::FILE_VERSION_CRITICAL_MASK;
|
||||
quint32 maxVersion = KeePass2::FILE_VERSION & KeePass2::FILE_VERSION_CRITICAL_MASK;
|
||||
if (!ok || (m_version < KeePass2::FILE_VERSION_MIN) || (m_version > maxVersion)) {
|
||||
raiseError(tr("Unsupported KeePass 2 database version."));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return !m_headerEnd;
|
||||
device->seek(0);
|
||||
m_reader.reset(static_cast<BaseKeePass2Reader*>(new Kdbx3Reader()));
|
||||
m_reader->setSaveXml(m_saveXml);
|
||||
return m_reader->readDatabase(device, key, keepDatabase);
|
||||
}
|
||||
|
||||
void KeePass2Reader::setCipher(const QByteArray& data)
|
||||
bool KeePass2Reader::hasError()
|
||||
{
|
||||
if (data.size() != Uuid::Length) {
|
||||
raiseError("Invalid cipher uuid length");
|
||||
}
|
||||
else {
|
||||
Uuid uuid(data);
|
||||
|
||||
if (uuid != KeePass2::CIPHER_AES && uuid != KeePass2::CIPHER_TWOFISH) {
|
||||
raiseError("Unsupported cipher");
|
||||
}
|
||||
else {
|
||||
m_db->setCipher(uuid);
|
||||
}
|
||||
}
|
||||
return m_error || (m_reader && m_reader->hasError());
|
||||
}
|
||||
|
||||
void KeePass2Reader::setCompressionFlags(const QByteArray& data)
|
||||
QString KeePass2Reader::errorString()
|
||||
{
|
||||
if (data.size() != 4) {
|
||||
raiseError("Invalid compression flags length");
|
||||
}
|
||||
else {
|
||||
quint32 id = Endian::bytesToUInt32(data, KeePass2::BYTEORDER);
|
||||
|
||||
if (id > Database::CompressionAlgorithmMax) {
|
||||
raiseError("Unsupported compression algorithm");
|
||||
}
|
||||
else {
|
||||
m_db->setCompressionAlgo(static_cast<Database::CompressionAlgorithm>(id));
|
||||
}
|
||||
}
|
||||
return m_reader ? m_reader->errorString() : m_errorStr;
|
||||
}
|
||||
|
||||
void KeePass2Reader::setMasterSeed(const QByteArray& data)
|
||||
QByteArray KeePass2Reader::xmlData()
|
||||
{
|
||||
if (data.size() != 32) {
|
||||
raiseError("Invalid master seed size");
|
||||
}
|
||||
else {
|
||||
m_masterSeed = data;
|
||||
}
|
||||
return m_reader ? m_reader->xmlData() : m_xmlData;
|
||||
}
|
||||
|
||||
void KeePass2Reader::setTransformSeed(const QByteArray& data)
|
||||
QByteArray KeePass2Reader::streamKey()
|
||||
{
|
||||
if (data.size() != 32) {
|
||||
raiseError("Invalid transform seed size");
|
||||
}
|
||||
else {
|
||||
AesKdf* aesKdf;
|
||||
if (m_db->kdf()->type() == Kdf::Type::AES) {
|
||||
aesKdf = static_cast<AesKdf*>(m_db->kdf());
|
||||
} else {
|
||||
aesKdf = new AesKdf();
|
||||
m_db->setKdf(aesKdf);
|
||||
}
|
||||
|
||||
aesKdf->setSeed(data);
|
||||
}
|
||||
}
|
||||
|
||||
void KeePass2Reader::setTransformRounds(const QByteArray& data)
|
||||
{
|
||||
if (data.size() != 8) {
|
||||
raiseError("Invalid transform rounds size");
|
||||
}
|
||||
else {
|
||||
quint64 rounds = Endian::bytesToUInt64(data, KeePass2::BYTEORDER);
|
||||
|
||||
AesKdf* aesKdf;
|
||||
if (m_db->kdf()->type() == Kdf::Type::AES) {
|
||||
aesKdf = static_cast<AesKdf*>(m_db->kdf());
|
||||
} else {
|
||||
aesKdf = new AesKdf();
|
||||
m_db->setKdf(aesKdf);
|
||||
}
|
||||
|
||||
aesKdf->setRounds(rounds);
|
||||
}
|
||||
}
|
||||
|
||||
void KeePass2Reader::setEncryptionIV(const QByteArray& data)
|
||||
{
|
||||
if (data.size() != 16) {
|
||||
raiseError("Invalid encryption iv size");
|
||||
}
|
||||
else {
|
||||
m_encryptionIV = data;
|
||||
}
|
||||
}
|
||||
|
||||
void KeePass2Reader::setProtectedStreamKey(const QByteArray& data)
|
||||
{
|
||||
if (data.size() != 32) {
|
||||
raiseError("Invalid stream key size");
|
||||
}
|
||||
else {
|
||||
m_protectedStreamKey = data;
|
||||
}
|
||||
}
|
||||
|
||||
void KeePass2Reader::setStreamStartBytes(const QByteArray& data)
|
||||
{
|
||||
if (data.size() != 32) {
|
||||
raiseError("Invalid start bytes size");
|
||||
}
|
||||
else {
|
||||
m_streamStartBytes = data;
|
||||
}
|
||||
}
|
||||
|
||||
void KeePass2Reader::setInnerRandomStreamID(const QByteArray& data)
|
||||
{
|
||||
if (data.size() != 4) {
|
||||
raiseError("Invalid random stream id size");
|
||||
}
|
||||
else {
|
||||
quint32 id = Endian::bytesToUInt32(data, KeePass2::BYTEORDER);
|
||||
m_irsAlgo = KeePass2::idToProtectedStreamAlgo(id);
|
||||
if (m_irsAlgo == KeePass2::ArcFourVariant || m_irsAlgo == KeePass2::InvalidProtectedStreamAlgo) {
|
||||
raiseError("Unsupported random stream algorithm");
|
||||
}
|
||||
}
|
||||
return m_reader ? m_reader->streamKey() : m_protectedStreamKey;
|
||||
}
|
||||
|
||||
KeePass2::ProtectedStreamAlgo KeePass2Reader::protectedStreamAlgo() const
|
||||
{
|
||||
return m_irsAlgo;
|
||||
return m_reader ? m_reader->protectedStreamAlgo() : m_irsAlgo;
|
||||
}
|
||||
|
||||
quint32 KeePass2Reader::version() const
|
||||
{
|
||||
return m_version;
|
||||
}
|
||||
|
||||
BaseKeePass2Reader* KeePass2Reader::reader() const
|
||||
{
|
||||
return m_reader.data();
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue