Files
keepassxc/src/format/Kdbx3Reader.cpp
Jonathan White 5142981018 Significantly enhance hardware key robustness
* Significantly improve user experience when using hardware keys on databases in both GUI and CLI modes. Prevent locking up the YubiKey USB interface for prolonged periods of time. Allows for other apps to use the key concurrently with KeePassXC.

* Improve messages displayed to user when finding keys and when user interaction is required. Output specific error messages when handling hardware keys during database read/write.

* Only poll for keys when previously used or upon user request. Prevent continuously polling keys when accessing the UI such as switching tabs and minimize/maximize.

* Add support for using multiple hardware keys simultaneously. Keys are identified by their serial number which prevents using the wrong key during open and save operations.

* Fixes #4400
* Fixes #4065
* Fixes #1050
* Fixes #1215
* Fixes #3087
* Fixes #1088
* Fixes #1869
2020-05-14 20:19:56 -04:00

214 lines
6.5 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/AsyncTask.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;
}
bool ok = AsyncTask::runAndWaitForFuture([&] { return db->setKey(key, false); });
if (!ok) {
raiseError(tr("Unable to calculate master key"));
return false;
}
if (!db->challengeMasterSeed(m_masterSeed)) {
raiseError(tr("Unable to issue challenge-response: %1").arg(db->keyError()));
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;
}