/* * Copyright (C) 2017 KeePassXC Team * Copyright (C) 2010 Felix Geyer * * 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 . */ #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 bool Kdbx3Reader::readDatabaseImpl(QIODevice* device, const QByteArray& headerData, QSharedPointer 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 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(&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(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; }