mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-06-28 16:37:32 -04:00
211 lines
6.4 KiB
C++
211 lines
6.4 KiB
C++
/*
|
|
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
|
|
* 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 "Kdbx3Reader.h"
|
|
|
|
#include "core/Endian.h"
|
|
#include "core/Group.h"
|
|
#include "crypto/CryptoHash.h"
|
|
#include "format/KdbxXmlReader.h"
|
|
#include "format/KeePass2RandomStream.h"
|
|
#include "streams/HashedBlockStream.h"
|
|
#include "streams/QtIOCompressor"
|
|
#include "streams/SymmetricCipherStream.h"
|
|
|
|
#include <QBuffer>
|
|
|
|
bool Kdbx3Reader::readDatabaseImpl(QIODevice* device,
|
|
const QByteArray& headerData,
|
|
QSharedPointer<const CompositeKey> key,
|
|
Database* db)
|
|
{
|
|
Q_ASSERT(m_kdbxVersion <= KeePass2::FILE_VERSION_3_1);
|
|
|
|
if (hasError()) {
|
|
return false;
|
|
}
|
|
|
|
// check if all required headers were present
|
|
if (m_masterSeed.isEmpty() || m_encryptionIV.isEmpty() || m_streamStartBytes.isEmpty()
|
|
|| m_protectedStreamKey.isEmpty() || db->cipher().isNull()) {
|
|
raiseError(tr("missing database headers"));
|
|
return false;
|
|
}
|
|
|
|
if (!db->setKey(key, false)) {
|
|
raiseError(tr("Unable to calculate master key"));
|
|
return false;
|
|
}
|
|
|
|
if (!db->challengeMasterSeed(m_masterSeed)) {
|
|
raiseError(tr("Unable to issue challenge-response."));
|
|
return false;
|
|
}
|
|
|
|
CryptoHash hash(CryptoHash::Sha256);
|
|
hash.addData(m_masterSeed);
|
|
hash.addData(db->challengeResponseKey());
|
|
hash.addData(db->transformedMasterKey());
|
|
QByteArray finalKey = hash.result();
|
|
|
|
SymmetricCipher::Algorithm cipher = SymmetricCipher::cipherToAlgorithm(db->cipher());
|
|
SymmetricCipherStream cipherStream(
|
|
device, cipher, SymmetricCipher::algorithmMode(cipher), SymmetricCipher::Decrypt);
|
|
if (!cipherStream.init(finalKey, m_encryptionIV)) {
|
|
raiseError(cipherStream.errorString());
|
|
return false;
|
|
}
|
|
if (!cipherStream.open(QIODevice::ReadOnly)) {
|
|
raiseError(cipherStream.errorString());
|
|
return false;
|
|
}
|
|
|
|
QByteArray realStart = cipherStream.read(32);
|
|
|
|
if (realStart != m_streamStartBytes) {
|
|
raiseError(tr("Invalid credentials were provided, please try again.\n"
|
|
"If this reoccurs, then your database file may be corrupt."));
|
|
return false;
|
|
}
|
|
|
|
HashedBlockStream hashedStream(&cipherStream);
|
|
if (!hashedStream.open(QIODevice::ReadOnly)) {
|
|
raiseError(hashedStream.errorString());
|
|
return false;
|
|
}
|
|
|
|
QIODevice* xmlDevice = nullptr;
|
|
QScopedPointer<QtIOCompressor> ioCompressor;
|
|
|
|
if (db->compressionAlgorithm() == Database::CompressionNone) {
|
|
xmlDevice = &hashedStream;
|
|
} else {
|
|
ioCompressor.reset(new QtIOCompressor(&hashedStream));
|
|
ioCompressor->setStreamFormat(QtIOCompressor::GzipFormat);
|
|
if (!ioCompressor->open(QIODevice::ReadOnly)) {
|
|
raiseError(ioCompressor->errorString());
|
|
return false;
|
|
}
|
|
xmlDevice = ioCompressor.data();
|
|
}
|
|
|
|
KeePass2RandomStream randomStream(KeePass2::ProtectedStreamAlgo::Salsa20);
|
|
if (!randomStream.init(m_protectedStreamKey)) {
|
|
raiseError(randomStream.errorString());
|
|
return false;
|
|
}
|
|
|
|
Q_ASSERT(xmlDevice);
|
|
|
|
KdbxXmlReader xmlReader(KeePass2::FILE_VERSION_3_1);
|
|
xmlReader.readDatabase(xmlDevice, db, &randomStream);
|
|
|
|
if (xmlReader.hasError()) {
|
|
raiseError(xmlReader.errorString());
|
|
return false;
|
|
}
|
|
|
|
Q_ASSERT(!xmlReader.headerHash().isEmpty() || m_kdbxVersion < KeePass2::FILE_VERSION_3_1);
|
|
|
|
if (!xmlReader.headerHash().isEmpty()) {
|
|
QByteArray headerHash = CryptoHash::hash(headerData, CryptoHash::Sha256);
|
|
if (headerHash != xmlReader.headerHash()) {
|
|
raiseError(tr("Header doesn't match hash"));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Kdbx3Reader::readHeaderField(StoreDataStream& headerStream, Database* db)
|
|
{
|
|
Q_UNUSED(db);
|
|
|
|
QByteArray fieldIDArray = headerStream.read(1);
|
|
if (fieldIDArray.size() != 1) {
|
|
raiseError(tr("Invalid header id size"));
|
|
return false;
|
|
}
|
|
char fieldID = fieldIDArray.at(0);
|
|
|
|
bool ok;
|
|
auto fieldLen = Endian::readSizedInt<quint16>(&headerStream, KeePass2::BYTEORDER, &ok);
|
|
if (!ok) {
|
|
raiseError(tr("Invalid header field length"));
|
|
return false;
|
|
}
|
|
|
|
QByteArray fieldData;
|
|
if (fieldLen != 0) {
|
|
fieldData = headerStream.read(fieldLen);
|
|
if (fieldData.size() != fieldLen) {
|
|
raiseError(tr("Invalid header data length"));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool headerEnd = false;
|
|
switch (static_cast<KeePass2::HeaderFieldID>(fieldID)) {
|
|
case KeePass2::HeaderFieldID::EndOfHeader:
|
|
headerEnd = true;
|
|
break;
|
|
|
|
case KeePass2::HeaderFieldID::CipherID:
|
|
setCipher(fieldData);
|
|
break;
|
|
|
|
case KeePass2::HeaderFieldID::CompressionFlags:
|
|
setCompressionFlags(fieldData);
|
|
break;
|
|
|
|
case KeePass2::HeaderFieldID::MasterSeed:
|
|
setMasterSeed(fieldData);
|
|
break;
|
|
|
|
case KeePass2::HeaderFieldID::TransformSeed:
|
|
setTransformSeed(fieldData);
|
|
break;
|
|
|
|
case KeePass2::HeaderFieldID::TransformRounds:
|
|
setTransformRounds(fieldData);
|
|
break;
|
|
|
|
case KeePass2::HeaderFieldID::EncryptionIV:
|
|
setEncryptionIV(fieldData);
|
|
break;
|
|
|
|
case KeePass2::HeaderFieldID::ProtectedStreamKey:
|
|
setProtectedStreamKey(fieldData);
|
|
break;
|
|
|
|
case KeePass2::HeaderFieldID::StreamStartBytes:
|
|
setStreamStartBytes(fieldData);
|
|
break;
|
|
|
|
case KeePass2::HeaderFieldID::InnerRandomStreamID:
|
|
setInnerRandomStreamID(fieldData);
|
|
break;
|
|
|
|
default:
|
|
qWarning("Unknown header field read: id=%d", fieldID);
|
|
break;
|
|
}
|
|
|
|
return !headerEnd;
|
|
}
|