Add parsing of authentication factor headers

This adds the ability to parse and validate (but not use) authentication factor
information contained within the KDBX outer header.

Use authentication factor headers if present for opening database

Saving the database will still discard the header information, but read-only
access works.
This commit is contained in:
Bryan Jacobs 2024-02-15 16:55:00 +11:00 committed by Jonathan White
parent 9a63e80386
commit 8a3985959d
No known key found for this signature in database
GPG Key ID: 440FC65F2E0C6E01
35 changed files with 1835 additions and 33 deletions

View File

@ -1,6 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="en_US">
<context>
<name>AESCBCFactorKeyDerivation</name>
<message>
<source>Performing AES-CBC decryption on wrapped key</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>AboutDialog</name>
<message>
@ -635,6 +642,13 @@
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>AuthenticationFactor</name>
<message>
<source>Validation failed when unwrapping factor &apos;%1&apos;: %2</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>AutoType</name>
<message>
@ -4908,6 +4922,14 @@ If this reoccurs, then your database file may be corrupt.</source>
<extracomment>Translation: variant map = data structure for storing meta data</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Parsing authentication factors</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Parsed authentication factors, got %1 group</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>Kdbx4Writer</name>
@ -4996,6 +5018,109 @@ This is a one-way migration. You won&apos;t be able to open the imported databas
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>KdbxXmlAuthenticationFactorReader</name>
<message>
<source>Read authentication factor XML: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>XML parsing failure on authentication factors: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to parse authentication factor info</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Read authentication factor compat version: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Incompatible authentication factor version</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Secondary authentication factors are comprehensive</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Comprehensive set to unknown value %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unknown element type while processing authentication factor info: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unable to decode validation input for authentication factor</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unable to decode validation output for authentication factor</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unknown authentication validation type %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unable to decode challenge for authentication factor</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unknown element type while processing authentication factor group: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Authentication factor group is empty!</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>An authentication factor group contains only unsupported factors</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Factor is a SHA256-hashed password</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Factor is a FIDO credential with type ES256</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unrecognized factor UUID %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unrecognized factor key type %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unable to decode key salt for authentication factor</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unable to decode wrapped key for authentication factor</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Encountered a CredentialID element on factor of non-FIDO type %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unable to decode FIDO credential ID for authentication factor</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unknown element type while processing generic authentication factor: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Factor %1 is missing required fields</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>KdbxXmlReader</name>
<message>
@ -6700,6 +6825,13 @@ The following data is missing:
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>PasswordAuthenticationFactor</name>
<message>
<source>Falling back to default user password for factor &apos;%1&apos;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>PasswordEditWidget</name>
<message>
@ -8908,6 +9040,22 @@ This option is deprecated, use --set-key-file instead.</source>
<source>Failed to decrypt key data.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Factor &apos;%1&apos; did not contribute key material</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Got a key part from factor &apos;%1&apos;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Attempting to add key material from extra authentication factors</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unable to get keying material from an authentication factor group</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Origin is empty or not allowed</source>
<translation type="unfinished"></translation>
@ -8928,6 +9076,10 @@ This option is deprecated, use --set-key-file instead.</source>
<source>Wait for timer to expire</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unknown passkeys error</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Challenge is shorter than required minimum length</source>
<translation type="unfinished"></translation>
@ -8936,6 +9088,10 @@ This option is deprecated, use --set-key-file instead.</source>
<source>user.id does not match the required length</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Cannot generate valid passphrases because the wordlist is too short</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Favorite</source>
<comment>Tag for favorite entries</comment>
@ -8957,6 +9113,18 @@ This option is deprecated, use --set-key-file instead.</source>
<source>Failed to decrypt json file: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unsupported format, ensure your Bitwarden export is password-protected</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Invalid KDF iterations, cannot decrypt json file</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Only PBKDF and Argon2 are supported, cannot decrypt json file</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Invalid encKeyValidation field</source>
<translation type="unfinished"></translation>
@ -9006,6 +9174,17 @@ This option is deprecated, use --set-key-file instead.</source>
<source>1Password Import</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Delete plugin data?</source>
<translation type="unfinished"></translation>
</message>
<message numerus="yes">
<source>Delete plugin data from Entry(s)?</source>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
</translation>
</message>
<message>
<source>Enter Shortcut</source>
<translation type="unfinished"></translation>
@ -9019,19 +9198,7 @@ This option is deprecated, use --set-key-file instead.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unknown passkeys error</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Invalid KDF iterations, cannot decrypt json file</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unsupported format, ensure your Bitwarden export is password-protected</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Only PBKDF and Argon2 are supported, cannot decrypt json file</source>
<source>Passkey</source>
<translation type="unfinished"></translation>
</message>
<message>
@ -9054,25 +9221,6 @@ This option is deprecated, use --set-key-file instead.</source>
<source>Shortcut %1 conflicts with &apos;%2&apos;. Overwrite shortcut?</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Cannot generate valid passphrases because the wordlist is too short</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Delete plugin data?</source>
<translation type="unfinished"></translation>
</message>
<message numerus="yes">
<source>Delete plugin data from Entry(s)?</source>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
</translation>
</message>
<message>
<source>Passkey</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtIOCompressor</name>

View File

@ -30,6 +30,7 @@ endif()
set(core_SOURCES
core/Alloc.cpp
core/AuthenticationFactorUserData.cpp
core/AutoTypeAssociations.cpp
core/Base32.cpp
core/Bootstrap.cpp
@ -68,6 +69,13 @@ set(core_SOURCES
crypto/kdf/Kdf.cpp
crypto/kdf/AesKdf.cpp
crypto/kdf/Argon2Kdf.cpp
format/multifactor/AESCBCFactorKeyDerivation.cpp
format/multifactor/AuthenticationFactor.cpp
format/multifactor/AuthenticationFactorGroup.cpp
format/multifactor/AuthenticationFactorInfo.cpp
format/multifactor/FactorKeyDerivation.cpp
format/multifactor/FidoAuthenticationFactor.cpp
format/multifactor/PasswordAuthenticationFactor.cpp
format/BitwardenReader.cpp
format/CsvExporter.cpp
format/CsvParser.cpp
@ -77,6 +85,7 @@ set(core_SOURCES
format/KdbxReader.cpp
format/KdbxWriter.cpp
format/KdbxXmlReader.cpp
format/KdbxXmlAuthenticationFactorReader.cpp
format/KeePass2Reader.cpp
format/KeePass2Writer.cpp
format/Kdbx3Reader.cpp
@ -94,6 +103,7 @@ set(core_SOURCES
keys/FileKey.cpp
keys/PasswordKey.cpp
keys/ChallengeResponseKey.cpp
keys/MultiAuthenticationHeaderKey.cpp
streams/HashedBlockStream.cpp
streams/HmacBlockStream.cpp
streams/LayeredStream.cpp

View File

@ -0,0 +1,34 @@
/*
* Copyright (C) 2024 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
* 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 "AuthenticationFactorUserData.h"
void AuthenticationFactorUserData::addDataItem(const QString& key, const QSharedPointer<QByteArray>& value)
{
m_data.insert(key, value);
}
QSharedPointer<QByteArray> AuthenticationFactorUserData::getDataItem(const QString& key) const
{
const auto& v = m_data.find(key);
if (v == m_data.end()) {
return {nullptr};
}
return *v;
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (C) 2024 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
* 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 KEEPASSXC_AUTHENTICATION_FACTOR_USER_DATA_H
#define KEEPASSXC_AUTHENTICATION_FACTOR_USER_DATA_H
#include <QCoreApplication>
#include <QHash>
#include <QSharedPointer>
class AuthenticationFactorUserData : public QObject
{
Q_OBJECT
public:
explicit AuthenticationFactorUserData() = default;
~AuthenticationFactorUserData() override = default;
void addDataItem(const QString& key, const QSharedPointer<QByteArray>& value);
QSharedPointer<QByteArray> getDataItem(const QString& key) const;
protected:
QHash<QString, QSharedPointer<QByteArray>> m_data;
};
#endif // KEEPASSXC_AUTHENTICATION_FACTOR_USER_DATA_H

View File

@ -1108,3 +1108,19 @@ bool Database::isTemporaryDatabase()
{
return m_isTemporaryDatabase;
}
QSharedPointer<AuthenticationFactorInfo> Database::authenticationFactorInfo()
{
return m_data.authenticationFactorInfo;
}
const QSharedPointer<AuthenticationFactorInfo>& Database::authenticationFactorInfo() const
{
return m_data.authenticationFactorInfo;
}
void Database::setAuthenticationFactorInfo(const QSharedPointer<AuthenticationFactorInfo>& authenticationFactorInfo)
{
m_data.authenticationFactorInfo = authenticationFactorInfo;
authenticationFactorInfo->setParent(this);
}

View File

@ -20,6 +20,7 @@
#define KEEPASSX_DATABASE_H
#include <QDateTime>
#include <QDebug>
#include <QHash>
#include <QMutex>
#include <QPointer>
@ -29,6 +30,7 @@
#include "core/ModifiableObject.h"
#include "crypto/kdf/AesKdf.h"
#include "format/KeePass2.h"
#include "format/multifactor/AuthenticationFactorInfo.h"
#include "keys/CompositeKey.h"
#include "keys/PasswordKey.h"
@ -160,6 +162,10 @@ public:
void markAsTemporaryDatabase();
bool isTemporaryDatabase();
void setAuthenticationFactorInfo(const QSharedPointer<AuthenticationFactorInfo>& authenticationFactorInfo);
QSharedPointer<AuthenticationFactorInfo> authenticationFactorInfo();
const QSharedPointer<AuthenticationFactorInfo>& authenticationFactorInfo() const;
static Database* databaseByUuid(const QUuid& uuid);
public slots:
@ -202,6 +208,8 @@ private:
QVariantMap publicCustomData;
QSharedPointer<AuthenticationFactorInfo> authenticationFactorInfo;
DatabaseData()
{
clear();
@ -222,6 +230,9 @@ private:
key.reset();
publicCustomData.clear();
authenticationFactorInfo.clear();
// Default to AES KDF, KDBX4 databases overwrite this
kdf.reset(new AesKdf(true));
kdf->randomizeSeed();

View File

@ -201,6 +201,8 @@ QString SymmetricCipher::modeToString(const Mode mode)
return QStringLiteral("AES-128/CBC");
case Aes256_CBC:
return QStringLiteral("AES-256/CBC");
case Aes256_CBC_UNPADDED:
return QStringLiteral("AES-256/CBC/NoPadding");
case Aes128_CTR:
return QStringLiteral("CTR(AES-128)");
case Aes256_CTR:

View File

@ -40,6 +40,7 @@ public:
ChaCha20,
Salsa20,
Aes256_GCM,
Aes256_CBC_UNPADDED,
InvalidMode = -1,
};

View File

@ -16,9 +16,9 @@
*/
#include "Kdbx4Reader.h"
#include "KdbxXmlAuthenticationFactorReader.h"
#include <QBuffer>
#include <QJsonObject>
#include "core/AsyncTask.h"
#include "core/Endian.h"
@ -224,6 +224,24 @@ bool Kdbx4Reader::readHeaderField(StoreDataStream& device, Database* db)
variantBuffer.open(QBuffer::ReadOnly);
QVariantMap data = readVariantMap(&variantBuffer);
db->setPublicCustomData(data);
auto it = data.constFind(AUTHENTICATION_FACTORS_HEADER_KEY);
if (it != data.constEnd()) {
qDebug() << tr("Parsing authentication factors");
auto authFactorReader =
QScopedPointer<KdbxXmlAuthenticationFactorReader>(new KdbxXmlAuthenticationFactorReader());
authFactorReader->readAuthenticationFactors(db, it.value().toString());
if (authFactorReader->hasError()) {
raiseError(authFactorReader->errorString());
return false;
}
qDebug() << tr("Parsed authentication factors, got %1 group")
.arg(db->authenticationFactorInfo()->getGroups().size());
}
break;
}

View File

@ -18,6 +18,8 @@
#ifndef KEEPASSX_KDBX4READER_H
#define KEEPASSX_KDBX4READER_H
#define AUTHENTICATION_FACTORS_HEADER_KEY "authentication_factors"
#include "format/KdbxReader.h"
/**

View File

@ -20,6 +20,7 @@
#include "core/Database.h"
#include "core/Endian.h"
#include "crypto/SymmetricCipher.h"
#include "keys/MultiAuthenticationHeaderKey.h"
#include "streams/StoreDataStream.h"
#define UUID_LENGTH 16
@ -95,6 +96,29 @@ bool KdbxReader::readDatabase(QIODevice* device, QSharedPointer<const CompositeK
return false;
}
auto authenticationFactorInfo = m_db->authenticationFactorInfo();
if (!authenticationFactorInfo.isNull()) {
// Augment (or replace) the given composite key with factors from the header
auto newCompositeKey = QSharedPointer<CompositeKey>::create();
if (!authenticationFactorInfo->isComprehensive() && !key.isNull()) {
// New composite should start with old key info
for (const auto& keyPart : key->keys()) {
newCompositeKey->addKey(keyPart);
}
}
auto headerInfoKey = QSharedPointer<MultiAuthenticationHeaderKey>::create(authenticationFactorInfo, key);
if (!headerInfoKey->process()) {
m_error = true;
m_errorStr = headerInfoKey->error();
return false;
}
newCompositeKey->addKey(headerInfoKey);
key = newCompositeKey;
}
// No key provided - don't proceed to load payload
if (key.isNull()) {
return true;

View File

@ -0,0 +1,334 @@
/*
* Copyright (C) 2024 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
* 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 "KdbxXmlAuthenticationFactorReader.h"
#include "format/multifactor/FidoAuthenticationFactor.h"
#include "format/multifactor/PasswordAuthenticationFactor.h"
#include <QDebug>
/**
* Read XML contents from a file into a new database.
*
* @param authenticationFactorXml A blob of XML describing authentication factors
* @return pointer to authentication factor information
*/
QSharedPointer<AuthenticationFactorInfo>
KdbxXmlAuthenticationFactorReader::readAuthenticationFactors(Database* db, const QString& authenticationFactorXml)
{
m_error = false;
m_errorStr.clear();
m_xml.clear();
qDebug() << tr("Read authentication factor XML: %1").arg(authenticationFactorXml);
auto result = QSharedPointer<AuthenticationFactorInfo>::create();
m_xml.addData(authenticationFactorXml);
if (m_xml.hasError()) {
raiseError(tr("XML parsing failure on authentication factors: %1").arg(m_xml.error()));
return result;
}
bool factorInfoParsed = false;
if (m_xml.readNextStartElement() && m_xml.name() == "FactorInfo") {
factorInfoParsed = parseFactorInfo(result);
}
if (!factorInfoParsed) {
if (!m_error) {
raiseError(tr("Failed to parse authentication factor info"));
}
return result;
}
if (db != nullptr) {
db->setAuthenticationFactorInfo(result);
}
return result;
}
bool KdbxXmlAuthenticationFactorReader::hasError() const
{
return m_error;
}
QString KdbxXmlAuthenticationFactorReader::errorString() const
{
return m_errorStr;
}
void KdbxXmlAuthenticationFactorReader::raiseError(const QString& errorMessage)
{
m_error = true;
m_errorStr = errorMessage;
}
bool KdbxXmlAuthenticationFactorReader::parseFactorInfo(const QSharedPointer<AuthenticationFactorInfo>& info)
{
Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "FactorInfo");
bool compatVersionFound = false;
while (!m_xml.hasError() && m_xml.readNextStartElement()) {
if (m_xml.name() == "CompatVersion") {
const auto compatVersion = m_xml.readElementText();
qDebug() << tr("Read authentication factor compat version: %1").arg(compatVersion);
if (compatVersion != "1") {
raiseError(tr("Incompatible authentication factor version"));
return false;
}
compatVersionFound = true;
continue;
}
if (m_xml.name() == "Comprehensive") {
const auto comprehensive = m_xml.readElementText();
if (comprehensive == "true") {
qDebug() << tr("Secondary authentication factors are comprehensive");
info.data()->setComprehensive(true);
} else {
raiseError(tr("Comprehensive set to unknown value %1").arg(comprehensive));
return false;
}
continue;
}
if (m_xml.name() == "Group") {
parseFactorGroup(info);
if (m_error) {
return false;
}
continue;
}
raiseError(
tr("Unknown element type while processing authentication factor info: %1").arg(m_xml.name().toString()));
return false;
}
return compatVersionFound;
}
bool KdbxXmlAuthenticationFactorReader::parseFactorGroup(const QSharedPointer<AuthenticationFactorInfo>& info)
{
Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Group");
auto group = QSharedPointer<AuthenticationFactorGroup>::create();
while (!m_xml.hasError() && m_xml.readNextStartElement()) {
if (m_xml.name() == "ValidationIn") {
QByteArray value = QByteArray::fromBase64(m_xml.readElementText().toLatin1());
if (value.isEmpty()) {
raiseError(tr("Unable to decode validation input for authentication factor"));
return false;
}
group->setValidationIn(value);
continue;
}
if (m_xml.name() == "ValidationOut") {
QByteArray value = QByteArray::fromBase64(m_xml.readElementText().toLatin1());
if (value.isEmpty()) {
raiseError(tr("Unable to decode validation output for authentication factor"));
return false;
}
group->setValidationOut(value);
continue;
}
if (m_xml.name() == "ValidationType") {
const auto& text = m_xml.readElementText();
AuthenticationFactorGroupValidationType validationType = AuthenticationFactorGroupValidationType::NONE;
if (text == "HMAC-SHA512") {
validationType = AuthenticationFactorGroupValidationType::HMAC_SHA512;
}
if (validationType == AuthenticationFactorGroupValidationType::NONE) {
qWarning() << tr("Unknown authentication validation type %1").arg(text);
}
group->setValidationType(validationType);
continue;
}
if (m_xml.name() == "Challenge") {
QByteArray value = QByteArray::fromBase64(m_xml.readElementText().toLatin1());
if (value.isEmpty()) {
raiseError(tr("Unable to decode challenge for authentication factor"));
return false;
}
group->setChallenge(value);
continue;
}
if (m_xml.name() == "Factor") {
parseFactor(group.data());
if (m_error) {
return false;
}
continue;
}
raiseError(
tr("Unknown element type while processing authentication factor group: %1").arg(m_xml.name().toString()));
return group;
}
if (group->getFactors().isEmpty()) {
raiseError(tr("Authentication factor group is empty!"));
return false;
}
bool foundCompatibleFactor = false;
for (auto& factor : group->getFactors()) {
if (factor->getFactorType() != FACTOR_TYPE_NULL && factor->getKeyType() != AuthenticationFactorKeyType::NONE) {
foundCompatibleFactor = true;
break;
}
}
if (!foundCompatibleFactor) {
raiseError(tr("An authentication factor group contains only unsupported factors"));
return false;
}
info->addGroup(group);
return true;
}
/**
* Parses the <Factor> XML element from a header-stored FactorInfo block.
*
* @param group The group to which the factor belongs
* @return true if parse successful; false on error
*/
bool KdbxXmlAuthenticationFactorReader::parseFactor(AuthenticationFactorGroup* group)
{
Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Factor");
auto factor = QSharedPointer<AuthenticationFactor>::create();
bool foundFactorType = false;
bool foundWrappedKey = false;
bool foundKeyType = false;
while (!m_xml.hasError() && m_xml.readNextStartElement()) {
if (m_xml.name() == "Name") {
const auto factorName = m_xml.readElementText();
factor->setName(factorName);
continue;
}
if (m_xml.name() == "TypeUUID") {
// Lowercase to not care about how the UUID is formatted as much
const auto& text = m_xml.readElementText().toLower();
if (text == FACTOR_TYPE_PASSWORD_SHA256) {
qDebug() << tr("Factor is a SHA256-hashed password");
factor = QSharedPointer<PasswordAuthenticationFactor>::create(factor);
} else if (text == FACTOR_TYPE_FIDO_ES256) {
qDebug() << tr("Factor is a FIDO credential with type ES256");
factor = QSharedPointer<FidoAuthenticationFactor>::create(factor);
} else {
qWarning() << tr("Unrecognized factor UUID %1").arg(text);
}
foundFactorType = true;
continue;
}
if (m_xml.name() == "KeyType") {
const auto& text = m_xml.readElementText();
AuthenticationFactorKeyType type = AuthenticationFactorKeyType::NONE;
if (text == "AES-CBC") {
type = AuthenticationFactorKeyType::AES_CBC;
}
if (type == AuthenticationFactorKeyType::NONE) {
qWarning() << tr("Unrecognized factor key type %1").arg(text);
}
// Note: unknown types get AuthenticationFactorKeyType::NONE - in other words, unusable
factor->setKeyType(type);
foundKeyType = true;
continue;
}
if (m_xml.name() == "KeySalt") {
QByteArray value = QByteArray::fromBase64(m_xml.readElementText().toLatin1());
if (value.isEmpty()) {
raiseError(tr("Unable to decode key salt for authentication factor"));
return false;
}
factor->setKeySalt(value);
continue;
}
if (m_xml.name() == "WrappedKey") {
QByteArray value = QByteArray::fromBase64(m_xml.readElementText().toLatin1());
if (value.isEmpty()) {
raiseError(tr("Unable to decode wrapped key for authentication factor"));
return false;
}
factor->setWrappedKey(value);
foundWrappedKey = true;
continue;
}
if (m_xml.name() == "CredentialID") {
// This block should move to a FIDO-factor-type-specified code location eventually, but right now
// since this is the only thing that isn't generic to all factors, it's here
auto factorType = factor->getFactorType();
if (factorType != FACTOR_TYPE_FIDO_ES256) {
raiseError(tr("Encountered a CredentialID element on factor of non-FIDO type %1").arg(factorType));
return false;
}
QByteArray value = QByteArray::fromBase64(m_xml.readElementText().toLatin1());
if (value.isEmpty()) {
raiseError(tr("Unable to decode FIDO credential ID for authentication factor"));
return false;
}
factor.dynamicCast<FidoAuthenticationFactor>()->setCredentialID(value);
continue;
}
raiseError(
tr("Unknown element type while processing generic authentication factor: %1").arg(m_xml.name().toString()));
return false;
}
if (!foundFactorType || !foundWrappedKey || !foundKeyType) {
// Missing a required field (or several...)
raiseError(tr("Factor %1 is missing required fields").arg(factor->getName()));
return false;
}
group->addFactor(factor);
return true;
}

View File

@ -0,0 +1,59 @@
/*
* Copyright (C) 2024 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
* 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 KEEPASSXC_KDBXXMLAUTHENTICATIONFACTORREADER_H
#define KEEPASSXC_KDBXXMLAUTHENTICATIONFACTORREADER_H
#include <QCoreApplication>
#include <QPointer>
#include <QXmlStreamReader>
#include "core/Database.h"
#include "format/multifactor/AuthenticationFactor.h"
#include "format/multifactor/AuthenticationFactorGroup.h"
#include "format/multifactor/AuthenticationFactorInfo.h"
/**
* KDBX XML payload reader.
*/
class KdbxXmlAuthenticationFactorReader
{
Q_DECLARE_TR_FUNCTIONS(KdbxXmlAuthenticationFactorReader)
public:
explicit KdbxXmlAuthenticationFactorReader() = default;
virtual ~KdbxXmlAuthenticationFactorReader() = default;
virtual QSharedPointer<AuthenticationFactorInfo> readAuthenticationFactors(Database* db,
const QString& authenticationFactorXml);
[[nodiscard]] bool hasError() const;
[[nodiscard]] QString errorString() const;
protected:
void raiseError(const QString& errorMessage);
bool parseFactorInfo(const QSharedPointer<AuthenticationFactorInfo>& info);
bool parseFactorGroup(const QSharedPointer<AuthenticationFactorInfo>& info);
bool parseFactor(AuthenticationFactorGroup* group);
bool m_error = false;
QString m_errorStr = "";
QXmlStreamReader m_xml;
};
#endif // KEEPASSXC_KDBXXMLAUTHENTICATIONFACTORREADER_H

View File

@ -0,0 +1,38 @@
/*
* Copyright (C) 2024 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
* 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 "AESCBCFactorKeyDerivation.h"
#include "crypto/SymmetricCipher.h"
#include <QDebug>
bool AESCBCFactorKeyDerivation::derive(QByteArray& data, const QByteArray& key, const QByteArray& salt)
{
qDebug() << tr("Performing AES-CBC decryption on wrapped key");
SymmetricCipher aes256;
if (!aes256.init(SymmetricCipher::Aes256_CBC_UNPADDED, SymmetricCipher::Decrypt, key, salt)) {
m_error = aes256.errorString();
return false;
}
if (!aes256.finish(data)) {
m_error = aes256.errorString();
return false;
}
return true;
}

View File

@ -0,0 +1,38 @@
/*
* Copyright (C) 2024 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
* 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 KEEPASSXC_AESCBC_DERIVATION_H
#define KEEPASSXC_AESCBC_DERIVATION_H
#include "FactorKeyDerivation.h"
#include <QCoreApplication>
class AESCBCFactorKeyDerivation : public FactorKeyDerivation
{
Q_OBJECT
public:
explicit AESCBCFactorKeyDerivation() = default;
virtual ~AESCBCFactorKeyDerivation() override = default;
virtual bool derive(QByteArray& data, const QByteArray& key, const QByteArray& salt) override;
protected:
};
#endif // KEEPASSXC_AESCBC_DERIVATION_H

View File

@ -0,0 +1,98 @@
/*
* Copyright (C) 2024 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
* 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 "AuthenticationFactor.h"
#include "AESCBCFactorKeyDerivation.h"
#include <QDebug>
#include <QSharedPointer>
void AuthenticationFactor::setName(const QString& name)
{
m_name = name;
}
const QString& AuthenticationFactor::getName() const
{
return m_name;
}
void AuthenticationFactor::setKeyType(AuthenticationFactorKeyType type)
{
m_keyType = type;
if (m_keyType == AuthenticationFactorKeyType::AES_CBC) {
m_derivation = QSharedPointer<AESCBCFactorKeyDerivation>::create();
} else {
m_derivation = QSharedPointer<AESCBCFactorKeyDerivation>(nullptr);
}
}
void AuthenticationFactor::setKeySalt(const QByteArray& salt)
{
m_keySalt = salt;
}
void AuthenticationFactor::setWrappedKey(const QByteArray& key)
{
m_wrappedKey = key;
}
const QByteArray& AuthenticationFactor::getWrappedKey() const
{
return m_wrappedKey;
}
const QByteArray& AuthenticationFactor::getKeySalt() const
{
return m_keySalt;
}
AuthenticationFactorKeyType AuthenticationFactor::getKeyType() const
{
return m_keyType;
}
const QString& AuthenticationFactor::getFactorType() const
{
return m_factorType;
}
QSharedPointer<QByteArray>
AuthenticationFactor::unwrapKey(const QSharedPointer<AuthenticationFactorUserData>& userData) const
{
auto unwrappingKey = getUnwrappingKey(userData);
auto wrappedKey = getWrappedKey();
if (m_derivation->derive(wrappedKey, unwrappingKey, getKeySalt())) {
// "wrappedKey" is now unwrapped!
return QSharedPointer<QByteArray>::create(wrappedKey);
} else {
qWarning() << tr("Validation failed when unwrapping factor '%1': %2").arg(getName(), m_derivation->getError());
}
return {nullptr};
}
QByteArray AuthenticationFactor::getUnwrappingKey(const QSharedPointer<AuthenticationFactorUserData>& userData) const
{
Q_UNUSED(userData);
// This shouldn't happen - it means we didn't understand the factor type?
qWarning() << "Attempted to get unwrapping key from generic AuthenticationFactor";
return QByteArray();
}

View File

@ -0,0 +1,74 @@
/*
* Copyright (C) 2024 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
* 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 KEEPASSXC_AUTHENTICATIONFACTOR_H
#define KEEPASSXC_AUTHENTICATIONFACTOR_H
#include "AuthenticationFactorGroup.h"
#include "FactorKeyDerivation.h"
#include "core/AuthenticationFactorUserData.h"
#include <QCoreApplication>
class AuthenticationFactorGroup;
#define FACTOR_TYPE_PASSWORD_SHA256 "c127a67f-be51-4bba-ac6f-7351e8a70ba0"
#define FACTOR_TYPE_KEY_FILE "6b9746c7-ca8d-430b-986d-1afaf689c4e4"
#define FACTOR_TYPE_FIDO_ES256 "15f77f9d-a65c-4a2e-b2b5-171f7b2df41a"
#define FACTOR_TYPE_YK_CHALRESP "0e6803a0-915e-4ebf-95ee-f9ddd8c97eea"
#define FACTOR_TYPE_NULL "618636bf-e202-4e0b-bb7c-e2514be00f5a"
enum class AuthenticationFactorKeyType
{
NONE,
AES_CBC,
};
class AuthenticationFactor : public QObject
{
Q_OBJECT
public:
explicit AuthenticationFactor() = default;
~AuthenticationFactor() override = default;
virtual QSharedPointer<QByteArray> unwrapKey(const QSharedPointer<AuthenticationFactorUserData>& userData) const;
const QString& getFactorType() const;
const QString& getName() const;
void setName(const QString& name);
AuthenticationFactorKeyType getKeyType() const;
void setKeyType(AuthenticationFactorKeyType type);
const QByteArray& getKeySalt() const;
void setKeySalt(const QByteArray& salt);
const QByteArray& getWrappedKey() const;
void setWrappedKey(const QByteArray& key);
protected:
virtual QByteArray getUnwrappingKey(const QSharedPointer<AuthenticationFactorUserData>& userData) const;
QString m_name = "<Unnamed factor>";
AuthenticationFactorKeyType m_keyType = AuthenticationFactorKeyType::NONE;
QByteArray m_keySalt = QByteArray();
QByteArray m_wrappedKey = QByteArray();
QString m_factorType = FACTOR_TYPE_NULL;
QSharedPointer<FactorKeyDerivation> m_derivation;
};
#endif // KEEPASSXC_AUTHENTICATIONFACTOR_H

View File

@ -0,0 +1,98 @@
/*
* Copyright (C) 2024 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
* 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 "AuthenticationFactorGroup.h"
#include <QDebug>
void AuthenticationFactorGroup::setValidationIn(const QByteArray& validationIn)
{
m_validationIn = validationIn;
}
void AuthenticationFactorGroup::setValidationOut(const QByteArray& validationOut)
{
m_validationOut = validationOut;
}
void AuthenticationFactorGroup::setChallenge(const QByteArray& challenge)
{
m_challenge = challenge;
}
void AuthenticationFactorGroup::setValidationType(const AuthenticationFactorGroupValidationType validationType)
{
m_validationType = validationType;
}
void AuthenticationFactorGroup::addFactor(const QSharedPointer<AuthenticationFactor>& factor)
{
m_factors.append(factor);
factor->setParent(this);
}
const QList<QSharedPointer<AuthenticationFactor>>& AuthenticationFactorGroup::getFactors() const
{
return m_factors;
}
const QByteArray& AuthenticationFactorGroup::getValidationIn() const
{
return m_validationIn;
}
const QByteArray& AuthenticationFactorGroup::getValidationOut() const
{
return m_validationOut;
}
const QByteArray& AuthenticationFactorGroup::getChallenge() const
{
return m_challenge;
}
AuthenticationFactorGroupValidationType AuthenticationFactorGroup::getValidationType() const
{
return m_validationType;
}
QSharedPointer<QByteArray>
AuthenticationFactorGroup::getRawKey(const QSharedPointer<AuthenticationFactorUserData>& userData)
{
bool groupContributed = false;
for (const auto& factor : getFactors()) {
auto unwrappedKey = factor->unwrapKey(userData);
if (unwrappedKey == nullptr) {
qDebug() << QObject::tr("Factor '%1' did not contribute key material").arg(factor->getName());
continue;
}
qDebug() << QObject::tr("Got a key part from factor '%1'").arg(factor->getName());
m_key.insert(m_key.end(), unwrappedKey->begin(), unwrappedKey->end());
groupContributed = true;
break;
}
if (!groupContributed) {
return {nullptr};
}
return QSharedPointer<QByteArray>::create(m_key.data(), m_key.size());
}

View File

@ -0,0 +1,70 @@
/*
* Copyright (C) 2024 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
* 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 KEEPASSXC_AUTHENTICATIONFACTORGROUP_H
#define KEEPASSXC_AUTHENTICATIONFACTORGROUP_H
#include <QCoreApplication>
#include <QSharedPointer>
#include <botan/secmem.h>
#include "AuthenticationFactorInfo.h"
#include "core/AuthenticationFactorUserData.h"
#include "format/multifactor/AuthenticationFactor.h"
enum class AuthenticationFactorGroupValidationType
{
NONE,
HMAC_SHA512,
};
class AuthenticationFactor;
class AuthenticationFactorInfo;
class AuthenticationFactorGroup : public QObject
{
Q_OBJECT
public:
AuthenticationFactorGroup() = default;
~AuthenticationFactorGroup() override = default;
QSharedPointer<QByteArray> getRawKey(const QSharedPointer<AuthenticationFactorUserData>& userData);
void setValidationIn(const QByteArray& validationIn);
const QByteArray& getValidationIn() const;
void setValidationOut(const QByteArray& validationOut);
const QByteArray& getValidationOut() const;
void setChallenge(const QByteArray& challenge);
const QByteArray& getChallenge() const;
void setValidationType(AuthenticationFactorGroupValidationType validationType);
AuthenticationFactorGroupValidationType getValidationType() const;
void addFactor(const QSharedPointer<AuthenticationFactor>& factor);
const QList<QSharedPointer<AuthenticationFactor>>& getFactors() const;
protected:
QByteArray m_validationIn = QByteArray();
QByteArray m_validationOut = QByteArray();
QByteArray m_challenge = QByteArray();
AuthenticationFactorGroupValidationType m_validationType = AuthenticationFactorGroupValidationType::NONE;
QList<QSharedPointer<AuthenticationFactor>> m_factors = QList<QSharedPointer<AuthenticationFactor>>();
Botan::secure_vector<char> m_key;
};
#endif // KEEPASSXC_AUTHENTICATIONFACTORGROUP_H

View File

@ -0,0 +1,39 @@
/*
* Copyright (C) 2024 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
* 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 "AuthenticationFactorInfo.h"
void AuthenticationFactorInfo::setComprehensive(bool comprehensive)
{
m_comprehensive = comprehensive;
}
bool AuthenticationFactorInfo::isComprehensive() const
{
return m_comprehensive;
}
void AuthenticationFactorInfo::addGroup(const QSharedPointer<AuthenticationFactorGroup>& group)
{
m_groups.append(group);
group->setParent(this);
}
const QList<QSharedPointer<AuthenticationFactorGroup>>& AuthenticationFactorInfo::getGroups() const
{
return m_groups;
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (C) 2024 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
* 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 KEEPASSXC_AUTHENTICATIONFACTORINFO_H
#define KEEPASSXC_AUTHENTICATIONFACTORINFO_H
#include "format/multifactor/AuthenticationFactorGroup.h"
#include <QCoreApplication>
#include <QSharedPointer>
class AuthenticationFactorGroup;
class AuthenticationFactorInfo : public QObject
{
Q_OBJECT
public:
explicit AuthenticationFactorInfo() = default;
~AuthenticationFactorInfo() override = default;
bool isComprehensive() const;
void setComprehensive(bool comprehensive);
void addGroup(const QSharedPointer<AuthenticationFactorGroup>& group);
const QList<QSharedPointer<AuthenticationFactorGroup>>& getGroups() const;
protected:
bool m_comprehensive = false;
QList<QSharedPointer<AuthenticationFactorGroup>> m_groups = QList<QSharedPointer<AuthenticationFactorGroup>>();
};
#endif // KEEPASSXC_AUTHENTICATIONFACTORINFO_H

View File

@ -0,0 +1,23 @@
/*
* Copyright (C) 2024 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
* 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 "FactorKeyDerivation.h"
const QString& FactorKeyDerivation::getError() const
{
return m_error;
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (C) 2024 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
* 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 KEEPASSXC_FACTOR_KEY_DERIVATION_H
#define KEEPASSXC_FACTOR_KEY_DERIVATION_H
#include <QCoreApplication>
class FactorKeyDerivation : public QObject
{
Q_OBJECT
public:
explicit FactorKeyDerivation() = default;
~FactorKeyDerivation() override = default;
virtual bool derive(QByteArray& data, const QByteArray& key, const QByteArray& salt) = 0;
const QString& getError() const;
protected:
QString m_error;
};
#endif // KEEPASSXC_FACTOR_KEY_DERIVATION_H

View File

@ -0,0 +1,37 @@
/*
* Copyright (C) 2024 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
* 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 "FidoAuthenticationFactor.h"
FidoAuthenticationFactor::FidoAuthenticationFactor(const QSharedPointer<AuthenticationFactor>& factor)
{
m_name = factor->getName();
m_keyType = factor->getKeyType();
m_keySalt = factor->getKeySalt();
m_wrappedKey = factor->getWrappedKey();
m_factorType = FACTOR_TYPE_FIDO_ES256;
}
void FidoAuthenticationFactor::setCredentialID(const QByteArray& credentialID)
{
m_credentialID = credentialID;
}
const QByteArray& FidoAuthenticationFactor::getCredentialID() const
{
return m_credentialID;
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (C) 2024 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
* 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 KEEPASSXC_FIDOAUTHENTICATIONFACTOR_H
#define KEEPASSXC_FIDOAUTHENTICATIONFACTOR_H
#include "format/multifactor/AuthenticationFactor.h"
#include <QCoreApplication>
class FidoAuthenticationFactor : public AuthenticationFactor
{
Q_OBJECT
public:
explicit FidoAuthenticationFactor(const QSharedPointer<AuthenticationFactor>& factor);
~FidoAuthenticationFactor() override = default;
void setCredentialID(const QByteArray& credentialID);
const QByteArray& getCredentialID() const;
protected:
QByteArray m_credentialID;
};
#endif // KEEPASSXC_FIDOAUTHENTICATIONFACTOR_H

View File

@ -0,0 +1,50 @@
/*
* Copyright (C) 2024 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
* 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 "PasswordAuthenticationFactor.h"
#include "keys/PasswordKey.h"
#include <QCryptographicHash>
#include <QDebug>
PasswordAuthenticationFactor::PasswordAuthenticationFactor(const QSharedPointer<AuthenticationFactor>& factor)
{
m_name = factor->getName();
m_keyType = factor->getKeyType();
m_keySalt = factor->getKeySalt();
m_wrappedKey = factor->getWrappedKey();
m_factorType = FACTOR_TYPE_PASSWORD_SHA256;
}
QByteArray
PasswordAuthenticationFactor::getUnwrappingKey(const QSharedPointer<AuthenticationFactorUserData>& userData) const
{
auto ret = userData->getDataItem(getName());
QByteArray dataToUse;
if (ret.isNull()) {
// Default user password - already hashed...
qDebug() << tr("Falling back to default user password for factor '%1'").arg(getName());
dataToUse = *userData->getDataItem(PasswordKey::UUID.toString());
} else {
// Non-hashed password
dataToUse = QCryptographicHash::hash(*ret, QCryptographicHash::Sha256);
}
return dataToUse;
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (C) 2024 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
* 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 KEEPASSXC_PASSWORDAUTHENTICATIONFACTOR_H
#define KEEPASSXC_PASSWORDAUTHENTICATIONFACTOR_H
#include "format/multifactor/AuthenticationFactor.h"
#include <QCoreApplication>
class PasswordAuthenticationFactor : public AuthenticationFactor
{
Q_OBJECT
public:
explicit PasswordAuthenticationFactor(const QSharedPointer<AuthenticationFactor>& factor);
~PasswordAuthenticationFactor() override = default;
protected:
QByteArray getUnwrappingKey(const QSharedPointer<AuthenticationFactorUserData>& userData) const override;
};
#endif // KEEPASSXC_PASSWORDAUTHENTICATIONFACTOR_H

View File

@ -0,0 +1,93 @@
/*
* Copyright (C) 2024 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
* 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 "MultiAuthenticationHeaderKey.h"
#include "FileKey.h"
#include "PasswordKey.h"
#include "core/AsyncTask.h"
#include <QCoreApplication>
QUuid MultiAuthenticationHeaderKey::UUID("e31ab20b-ee50-45af-99bc-ab4c4d34f4cc");
MultiAuthenticationHeaderKey::MultiAuthenticationHeaderKey(
const QSharedPointer<const AuthenticationFactorInfo>& authenticationFactorInfo,
const QSharedPointer<const CompositeKey>& existingKey)
: Key(UUID)
{
m_authenticationFactorInfo = authenticationFactorInfo;
m_userData = QSharedPointer<AuthenticationFactorUserData>::create();
for (const auto& keyPart : existingKey->keys()) {
const auto& uuid = keyPart->uuid();
m_userData->addDataItem(uuid.toString(), QSharedPointer<QByteArray>::create(keyPart->rawKey()));
}
}
QByteArray MultiAuthenticationHeaderKey::rawKey() const
{
return {m_key.data(), int(m_key.size())};
}
bool MultiAuthenticationHeaderKey::process()
{
qDebug() << QObject::tr("Attempting to add key material from extra authentication factors");
auto userData = QSharedPointer<AuthenticationFactorUserData>::create();
if (m_key.empty()) {
// Construct key from parts
for (const auto& group : m_authenticationFactorInfo->getGroups()) {
auto groupPart = group->getRawKey(m_userData);
if (groupPart.isNull()) {
m_error = QObject::tr("Unable to get keying material from an authentication factor group");
return false;
}
m_key.insert(m_key.end(), groupPart->begin(), groupPart->end());
}
}
return true;
}
void MultiAuthenticationHeaderKey::setRawKey(const QByteArray& data)
{
m_key.assign(data.begin(), data.end());
m_error = "";
}
QString MultiAuthenticationHeaderKey::error() const
{
return m_error;
}
QByteArray MultiAuthenticationHeaderKey::serialize() const
{
QByteArray data;
QDataStream stream(&data, QIODevice::WriteOnly);
stream << uuid().toRfc4122();
return data;
}
void MultiAuthenticationHeaderKey::deserialize(const QByteArray& data)
{
QDataStream stream(data);
QByteArray uuidData;
stream >> uuidData;
}

View File

@ -0,0 +1,58 @@
/*
* Copyright (C) 2024 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
* 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 KPXC_MULTI_AUTHENTICATION_HEADER_KEY_H
#define KPXC_MULTI_AUTHENTICATION_HEADER_KEY_H
#include "CompositeKey.h"
#include "Key.h"
#include "format/multifactor/AuthenticationFactorInfo.h"
#include <QCoreApplication>
#include <botan/secmem.h>
class MultiAuthenticationHeaderKey : public Key
{
public:
explicit MultiAuthenticationHeaderKey(
const QSharedPointer<const AuthenticationFactorInfo>& authenticationFactorInfo,
const QSharedPointer<const CompositeKey>& existingKey);
~MultiAuthenticationHeaderKey() override = default;
bool process();
QByteArray rawKey() const override;
void setRawKey(const QByteArray&) override;
QString error() const;
QByteArray serialize() const override;
void deserialize(const QByteArray& data) override;
static QUuid UUID;
private:
Q_DISABLE_COPY(MultiAuthenticationHeaderKey);
QSharedPointer<const AuthenticationFactorInfo> m_authenticationFactorInfo;
QSharedPointer<AuthenticationFactorUserData> m_userData;
QString m_error;
Botan::secure_vector<char> m_key;
};
#endif // KPXC_MULTI_AUTHENTICATION_HEADER_KEY_H

View File

@ -100,6 +100,9 @@ set(testsupport_SOURCES
add_library(testsupport STATIC ${testsupport_SOURCES})
target_link_libraries(testsupport Qt5::Core Qt5::Concurrent Qt5::Widgets Qt5::Test)
add_unit_test(NAME testauthenticationfactorparsing SOURCES TestAuthenticationFactorParsing.cpp
LIBS testsupport ${TEST_LIBRARIES})
add_unit_test(NAME testgroup SOURCES TestGroup.cpp
LIBS testsupport ${TEST_LIBRARIES})

View File

@ -0,0 +1,142 @@
/*
* Copyright (C) 2024 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
* 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 "TestAuthenticationFactorParsing.h"
#include <QTest>
QTEST_GUILESS_MAIN(TestAuthenticationFactorParsing)
void TestAuthenticationFactorParsing::testNotXML()
{
m_reader.readAuthenticationFactors(nullptr, "blargh");
QVERIFY(m_reader.hasError());
}
void TestAuthenticationFactorParsing::testMalformedXML()
{
m_reader.readAuthenticationFactors(nullptr, "<FactorInfo><blah></FactorInfo>");
QVERIFY(m_reader.hasError());
}
void TestAuthenticationFactorParsing::testMissingFactorInfo()
{
m_reader.readAuthenticationFactors(nullptr, "<Something></Something>");
QVERIFY(m_reader.hasError());
}
void TestAuthenticationFactorParsing::testNoCompatVersion()
{
m_reader.readAuthenticationFactors(nullptr, "<FactorInfo></FactorInfo>");
QVERIFY(m_reader.hasError());
}
void TestAuthenticationFactorParsing::testUnsupportedCompatVersion()
{
m_reader.readAuthenticationFactors(nullptr, "<FactorInfo><CompatVersion>2</CompatVersion></FactorInfo>");
QVERIFY(m_reader.hasError());
}
void TestAuthenticationFactorParsing::testNoGroups()
{
auto res = m_reader.readAuthenticationFactors(nullptr, "<FactorInfo><CompatVersion>1</CompatVersion></FactorInfo>");
QVERIFY(!m_reader.hasError());
QCOMPARE(res->getGroups().size(), 0);
}
void TestAuthenticationFactorParsing::testGroupWithNoFactors()
{
m_reader.readAuthenticationFactors(nullptr,
"<FactorInfo><CompatVersion>1</CompatVersion><Group></Group></FactorInfo>");
QVERIFY(m_reader.hasError());
}
void TestAuthenticationFactorParsing::testUnsupportedFactorTypeAlone()
{
m_reader.readAuthenticationFactors(nullptr,
"<FactorInfo><CompatVersion>1</CompatVersion><Group>"
"<Factor><KeyType>AES-CBC</KeyType><TypeUUID>bogus</TypeUUID>"
"<WrappedKey>B4pHAoQomD8728UKeST2HOxglrjzwyq2M/IPEOV4xo8=</WrappedKey></Factor>"
"</Group></FactorInfo>");
QVERIFY(m_reader.hasError());
}
void TestAuthenticationFactorParsing::testUnsupportedFactorTypeAndSupportedTogether()
{
auto res = m_reader.readAuthenticationFactors(
nullptr,
"<FactorInfo><CompatVersion>1</CompatVersion><Group>"
"<Factor><KeyType>AES-CBC</KeyType><TypeUUID>bogus</TypeUUID>"
"<WrappedKey>B4pHAoQomD8728UKeST2HOxglrjzwyq2M/IPEOV4xo8=</WrappedKey></Factor>"
"<Factor><KeyType>AES-CBC</KeyType><TypeUUID>" FACTOR_TYPE_PASSWORD_SHA256 "</TypeUUID>"
"<WrappedKey>B4pHAoQomD8728UKeST2HOxglrjzwyq2M/IPEOV4xo8=</WrappedKey></Factor>"
"</Group></FactorInfo>");
QVERIFY(!m_reader.hasError());
QCOMPARE(res->getGroups().first()->getFactors().size(), 2);
}
void TestAuthenticationFactorParsing::testUnsupportedVerificationMethod()
{
auto res = m_reader.readAuthenticationFactors(
nullptr,
"<FactorInfo><CompatVersion>1</CompatVersion><Group>"
"<ValidationType>bogus</ValidationType>"
"<Factor><KeyType>AES-CBC</KeyType><TypeUUID>" FACTOR_TYPE_PASSWORD_SHA256 "</TypeUUID>"
"<WrappedKey>B4pHAoQomD8728UKeST2HOxglrjzwyq2M/IPEOV4xo8=</WrappedKey></Factor>"
"</Group></FactorInfo>");
QVERIFY(!m_reader.hasError());
QCOMPARE(res->getGroups().first()->getValidationType(), AuthenticationFactorGroupValidationType::NONE);
}
void TestAuthenticationFactorParsing::testOmittedVerification()
{
m_reader.readAuthenticationFactors(nullptr,
"<FactorInfo><CompatVersion>1</CompatVersion><Group>"
"<Factor><KeyType>AES-CBC</KeyType><TypeUUID>" FACTOR_TYPE_PASSWORD_SHA256
"</TypeUUID>"
"<WrappedKey>B4pHAoQomD8728UKeST2HOxglrjzwyq2M/IPEOV4xo8=</WrappedKey></Factor>"
"</Group></FactorInfo>");
QVERIFY(!m_reader.hasError());
}
void TestAuthenticationFactorParsing::testInvalidBase64()
{
m_reader.readAuthenticationFactors(nullptr,
"<FactorInfo><CompatVersion>1</CompatVersion><Group>"
"<Factor><KeyType>AES-CBC</KeyType><TypeUUID>" FACTOR_TYPE_PASSWORD_SHA256
"</TypeUUID>"
"<WrappedKey>_</WrappedKey></Factor>"
"</Group></FactorInfo>");
QVERIFY(m_reader.hasError());
}
void TestAuthenticationFactorParsing::testMissingRequiredFields()
{
m_reader.readAuthenticationFactors(nullptr,
"<FactorInfo><CompatVersion>1</CompatVersion><Group>"
"<Factor><TypeUUID>" FACTOR_TYPE_PASSWORD_SHA256 "</TypeUUID>"
"<WrappedKey>B4pHAoQomD8728UKeST2HOxglrjzwyq2M/IPEOV4xo8=</WrappedKey></Factor>"
"</Group></FactorInfo>");
QVERIFY(m_reader.hasError());
}

View File

@ -0,0 +1,51 @@
/*
* Copyright (C) 2024 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
* 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 KEEPASSXC_TEST_AUTHENTICATIONFACTORPARSING_H
#define KEEPASSXC_TEST_AUTHENTICATIONFACTORPARSING_H
#include <QObject>
#include "format/KdbxXmlAuthenticationFactorReader.h"
/**
* Tests of the parsing of different authentication-factor information headers.
*/
class TestAuthenticationFactorParsing : public QObject
{
Q_OBJECT
private slots:
void testNotXML();
void testMalformedXML();
void testMissingFactorInfo();
void testNoCompatVersion();
void testUnsupportedCompatVersion();
void testNoGroups();
void testGroupWithNoFactors();
void testUnsupportedFactorTypeAlone();
void testUnsupportedFactorTypeAndSupportedTogether();
void testUnsupportedVerificationMethod();
void testOmittedVerification();
void testInvalidBase64();
void testMissingRequiredFields();
private:
KdbxXmlAuthenticationFactorReader m_reader;
};
#endif // KEEPASSXC_TEST_AUTHENTICATIONFACTORPARSING_H

View File

@ -140,6 +140,7 @@ void TestKdbx4Format::testFormat400()
QCOMPARE(db->rootGroup()->name(), QString("Format400"));
QCOMPARE(db->metadata()->name(), QString("Format400"));
QCOMPARE(db->rootGroup()->entries().size(), 1);
QVERIFY(db->authenticationFactorInfo().isNull());
auto entry = db->rootGroup()->entries().at(0);
QCOMPARE(entry->title(), QString("Format400"));
@ -606,3 +607,30 @@ void TestKdbx4Format::testCustomData()
QCOMPARE(newEntry->customData()->value(customDataKey1), customData1);
QCOMPARE(newEntry->customData()->value(customDataKey2), customData2);
}
void TestKdbx4Format::testMultiFactorHeaderRead()
{
QString filename = QString(KEEPASSX_TEST_DATA_DIR).append("/MultiFactorPasswordOnly.kdbx");
auto key = QSharedPointer<CompositeKey>::create();
auto passwordKey = QSharedPointer<PasswordKey>::create();
passwordKey->setPassword(QByteArray::fromStdString("somepassword"));
key->addKey(QSharedPointer<PasswordKey>(passwordKey));
KeePass2Reader reader;
auto db = QSharedPointer<Database>::create();
reader.readDatabase(filename, key, db.data());
QVERIFY(!reader.hasError());
QVERIFY(db->authenticationFactorInfo() != nullptr);
QCOMPARE(db->authenticationFactorInfo()->isComprehensive(), true);
auto groups = db->authenticationFactorInfo()->getGroups();
QCOMPARE(groups.size(), 1);
auto group = groups.first();
QCOMPARE(group->getFactors().size(), 1);
auto factor = group->getFactors().first();
QCOMPARE(factor->getName(), QString("SomePassword"));
QCOMPARE(factor->getFactorType(), QString(FACTOR_TYPE_PASSWORD_SHA256));
}

View File

@ -66,6 +66,7 @@ private slots:
void testUpgradeMasterKeyIntegrity_data();
void testAttachmentIndexStability();
void testCustomData();
void testMultiFactorHeaderRead();
};
#endif // KEEPASSXC_TEST_KDBX4_H

Binary file not shown.