diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index c83fefc75..7788b914b 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -1,6 +1,13 @@ + + AESCBCFactorKeyDerivation + + Performing AES-CBC decryption on wrapped key + + + AboutDialog @@ -635,6 +642,13 @@ + + AuthenticationFactor + + Validation failed when unwrapping factor '%1': %2 + + + AutoType @@ -4908,6 +4922,14 @@ If this reoccurs, then your database file may be corrupt. Translation: variant map = data structure for storing meta data + + Parsing authentication factors + + + + Parsed authentication factors, got %1 group + + Kdbx4Writer @@ -4996,6 +5018,109 @@ This is a one-way migration. You won't be able to open the imported databas + + KdbxXmlAuthenticationFactorReader + + Read authentication factor XML: %1 + + + + XML parsing failure on authentication factors: %1 + + + + Failed to parse authentication factor info + + + + Read authentication factor compat version: %1 + + + + Incompatible authentication factor version + + + + Secondary authentication factors are comprehensive + + + + Comprehensive set to unknown value %1 + + + + Unknown element type while processing authentication factor info: %1 + + + + Unable to decode validation input for authentication factor + + + + Unable to decode validation output for authentication factor + + + + Unknown authentication validation type %1 + + + + Unable to decode challenge for authentication factor + + + + Unknown element type while processing authentication factor group: %1 + + + + Authentication factor group is empty! + + + + An authentication factor group contains only unsupported factors + + + + Factor is a SHA256-hashed password + + + + Factor is a FIDO credential with type ES256 + + + + Unrecognized factor UUID %1 + + + + Unrecognized factor key type %1 + + + + Unable to decode key salt for authentication factor + + + + Unable to decode wrapped key for authentication factor + + + + Encountered a CredentialID element on factor of non-FIDO type %1 + + + + Unable to decode FIDO credential ID for authentication factor + + + + Unknown element type while processing generic authentication factor: %1 + + + + Factor %1 is missing required fields + + + KdbxXmlReader @@ -6700,6 +6825,13 @@ The following data is missing: + + PasswordAuthenticationFactor + + Falling back to default user password for factor '%1' + + + PasswordEditWidget @@ -8908,6 +9040,22 @@ This option is deprecated, use --set-key-file instead. Failed to decrypt key data. + + Factor '%1' did not contribute key material + + + + Got a key part from factor '%1' + + + + Attempting to add key material from extra authentication factors + + + + Unable to get keying material from an authentication factor group + + Origin is empty or not allowed @@ -8928,6 +9076,10 @@ This option is deprecated, use --set-key-file instead. Wait for timer to expire + + Unknown passkeys error + + Challenge is shorter than required minimum length @@ -8936,6 +9088,10 @@ This option is deprecated, use --set-key-file instead. user.id does not match the required length + + Cannot generate valid passphrases because the wordlist is too short + + Favorite Tag for favorite entries @@ -8957,6 +9113,18 @@ This option is deprecated, use --set-key-file instead. Failed to decrypt json file: %1 + + Unsupported format, ensure your Bitwarden export is password-protected + + + + Invalid KDF iterations, cannot decrypt json file + + + + Only PBKDF and Argon2 are supported, cannot decrypt json file + + Invalid encKeyValidation field @@ -9006,6 +9174,17 @@ This option is deprecated, use --set-key-file instead. 1Password Import + + Delete plugin data? + + + + Delete plugin data from Entry(s)? + + + + + Enter Shortcut @@ -9019,19 +9198,7 @@ This option is deprecated, use --set-key-file instead. - Unknown passkeys error - - - - Invalid KDF iterations, cannot decrypt json file - - - - Unsupported format, ensure your Bitwarden export is password-protected - - - - Only PBKDF and Argon2 are supported, cannot decrypt json file + Passkey @@ -9054,25 +9221,6 @@ This option is deprecated, use --set-key-file instead. Shortcut %1 conflicts with '%2'. Overwrite shortcut? - - Cannot generate valid passphrases because the wordlist is too short - - - - Delete plugin data? - - - - Delete plugin data from Entry(s)? - - - - - - - Passkey - - QtIOCompressor diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ee83fac32..b644bfa53 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 diff --git a/src/core/AuthenticationFactorUserData.cpp b/src/core/AuthenticationFactorUserData.cpp new file mode 100644 index 000000000..2b58e4718 --- /dev/null +++ b/src/core/AuthenticationFactorUserData.cpp @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 "AuthenticationFactorUserData.h" + +void AuthenticationFactorUserData::addDataItem(const QString& key, const QSharedPointer& value) +{ + m_data.insert(key, value); +} + +QSharedPointer AuthenticationFactorUserData::getDataItem(const QString& key) const +{ + const auto& v = m_data.find(key); + + if (v == m_data.end()) { + return {nullptr}; + } + + return *v; +} diff --git a/src/core/AuthenticationFactorUserData.h b/src/core/AuthenticationFactorUserData.h new file mode 100644 index 000000000..27fbb622e --- /dev/null +++ b/src/core/AuthenticationFactorUserData.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 . + */ + +#ifndef KEEPASSXC_AUTHENTICATION_FACTOR_USER_DATA_H +#define KEEPASSXC_AUTHENTICATION_FACTOR_USER_DATA_H + +#include +#include +#include + +class AuthenticationFactorUserData : public QObject +{ + Q_OBJECT + +public: + explicit AuthenticationFactorUserData() = default; + ~AuthenticationFactorUserData() override = default; + + void addDataItem(const QString& key, const QSharedPointer& value); + QSharedPointer getDataItem(const QString& key) const; + +protected: + QHash> m_data; +}; + +#endif // KEEPASSXC_AUTHENTICATION_FACTOR_USER_DATA_H diff --git a/src/core/Database.cpp b/src/core/Database.cpp index 5734f9521..9a499ffba 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -1108,3 +1108,19 @@ bool Database::isTemporaryDatabase() { return m_isTemporaryDatabase; } + +QSharedPointer Database::authenticationFactorInfo() +{ + return m_data.authenticationFactorInfo; +} + +const QSharedPointer& Database::authenticationFactorInfo() const +{ + return m_data.authenticationFactorInfo; +} + +void Database::setAuthenticationFactorInfo(const QSharedPointer& authenticationFactorInfo) +{ + m_data.authenticationFactorInfo = authenticationFactorInfo; + authenticationFactorInfo->setParent(this); +} diff --git a/src/core/Database.h b/src/core/Database.h index 29314650e..107b47746 100644 --- a/src/core/Database.h +++ b/src/core/Database.h @@ -20,6 +20,7 @@ #define KEEPASSX_DATABASE_H #include +#include #include #include #include @@ -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); + QSharedPointer authenticationFactorInfo(); + const QSharedPointer& authenticationFactorInfo() const; + static Database* databaseByUuid(const QUuid& uuid); public slots: @@ -202,6 +208,8 @@ private: QVariantMap publicCustomData; + QSharedPointer 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(); diff --git a/src/crypto/SymmetricCipher.cpp b/src/crypto/SymmetricCipher.cpp index 33e61aa4f..24711c7dd 100644 --- a/src/crypto/SymmetricCipher.cpp +++ b/src/crypto/SymmetricCipher.cpp @@ -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: diff --git a/src/crypto/SymmetricCipher.h b/src/crypto/SymmetricCipher.h index 224e8baa9..fc111cd43 100644 --- a/src/crypto/SymmetricCipher.h +++ b/src/crypto/SymmetricCipher.h @@ -40,6 +40,7 @@ public: ChaCha20, Salsa20, Aes256_GCM, + Aes256_CBC_UNPADDED, InvalidMode = -1, }; diff --git a/src/format/Kdbx4Reader.cpp b/src/format/Kdbx4Reader.cpp index 0698a0b2b..2b7a44c86 100644 --- a/src/format/Kdbx4Reader.cpp +++ b/src/format/Kdbx4Reader.cpp @@ -16,9 +16,9 @@ */ #include "Kdbx4Reader.h" +#include "KdbxXmlAuthenticationFactorReader.h" #include -#include #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(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; } diff --git a/src/format/Kdbx4Reader.h b/src/format/Kdbx4Reader.h index 301d4ff6c..8c7310f72 100644 --- a/src/format/Kdbx4Reader.h +++ b/src/format/Kdbx4Reader.h @@ -18,6 +18,8 @@ #ifndef KEEPASSX_KDBX4READER_H #define KEEPASSX_KDBX4READER_H +#define AUTHENTICATION_FACTORS_HEADER_KEY "authentication_factors" + #include "format/KdbxReader.h" /** diff --git a/src/format/KdbxReader.cpp b/src/format/KdbxReader.cpp index b552bd1cb..81adff928 100644 --- a/src/format/KdbxReader.cpp +++ b/src/format/KdbxReader.cpp @@ -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, QSharedPointerauthenticationFactorInfo(); + if (!authenticationFactorInfo.isNull()) { + // Augment (or replace) the given composite key with factors from the header + auto newCompositeKey = QSharedPointer::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::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; diff --git a/src/format/KdbxXmlAuthenticationFactorReader.cpp b/src/format/KdbxXmlAuthenticationFactorReader.cpp new file mode 100644 index 000000000..3f8f07bae --- /dev/null +++ b/src/format/KdbxXmlAuthenticationFactorReader.cpp @@ -0,0 +1,334 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 "KdbxXmlAuthenticationFactorReader.h" +#include "format/multifactor/FidoAuthenticationFactor.h" +#include "format/multifactor/PasswordAuthenticationFactor.h" +#include + +/** + * 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 +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::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& 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& info) +{ + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == "Group"); + + auto group = QSharedPointer::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 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::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::create(factor); + } else if (text == FACTOR_TYPE_FIDO_ES256) { + qDebug() << tr("Factor is a FIDO credential with type ES256"); + + factor = QSharedPointer::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()->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; +} diff --git a/src/format/KdbxXmlAuthenticationFactorReader.h b/src/format/KdbxXmlAuthenticationFactorReader.h new file mode 100644 index 000000000..28c13d7e8 --- /dev/null +++ b/src/format/KdbxXmlAuthenticationFactorReader.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 . + */ + +#ifndef KEEPASSXC_KDBXXMLAUTHENTICATIONFACTORREADER_H +#define KEEPASSXC_KDBXXMLAUTHENTICATIONFACTORREADER_H + +#include +#include +#include + +#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 readAuthenticationFactors(Database* db, + const QString& authenticationFactorXml); + + [[nodiscard]] bool hasError() const; + [[nodiscard]] QString errorString() const; + +protected: + void raiseError(const QString& errorMessage); + + bool parseFactorInfo(const QSharedPointer& info); + bool parseFactorGroup(const QSharedPointer& info); + bool parseFactor(AuthenticationFactorGroup* group); + + bool m_error = false; + QString m_errorStr = ""; + QXmlStreamReader m_xml; +}; + +#endif // KEEPASSXC_KDBXXMLAUTHENTICATIONFACTORREADER_H diff --git a/src/format/multifactor/AESCBCFactorKeyDerivation.cpp b/src/format/multifactor/AESCBCFactorKeyDerivation.cpp new file mode 100644 index 000000000..b08ee7c1a --- /dev/null +++ b/src/format/multifactor/AESCBCFactorKeyDerivation.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 "AESCBCFactorKeyDerivation.h" +#include "crypto/SymmetricCipher.h" + +#include + +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; +} diff --git a/src/format/multifactor/AESCBCFactorKeyDerivation.h b/src/format/multifactor/AESCBCFactorKeyDerivation.h new file mode 100644 index 000000000..84aa68a22 --- /dev/null +++ b/src/format/multifactor/AESCBCFactorKeyDerivation.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 . + */ + +#ifndef KEEPASSXC_AESCBC_DERIVATION_H +#define KEEPASSXC_AESCBC_DERIVATION_H + +#include "FactorKeyDerivation.h" + +#include + +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 diff --git a/src/format/multifactor/AuthenticationFactor.cpp b/src/format/multifactor/AuthenticationFactor.cpp new file mode 100644 index 000000000..15937b54b --- /dev/null +++ b/src/format/multifactor/AuthenticationFactor.cpp @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 "AuthenticationFactor.h" +#include "AESCBCFactorKeyDerivation.h" + +#include +#include + +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::create(); + } else { + m_derivation = QSharedPointer(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 +AuthenticationFactor::unwrapKey(const QSharedPointer& userData) const +{ + auto unwrappingKey = getUnwrappingKey(userData); + + auto wrappedKey = getWrappedKey(); + + if (m_derivation->derive(wrappedKey, unwrappingKey, getKeySalt())) { + // "wrappedKey" is now unwrapped! + return QSharedPointer::create(wrappedKey); + } else { + qWarning() << tr("Validation failed when unwrapping factor '%1': %2").arg(getName(), m_derivation->getError()); + } + + return {nullptr}; +} + +QByteArray AuthenticationFactor::getUnwrappingKey(const QSharedPointer& 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(); +} diff --git a/src/format/multifactor/AuthenticationFactor.h b/src/format/multifactor/AuthenticationFactor.h new file mode 100644 index 000000000..77420dc4f --- /dev/null +++ b/src/format/multifactor/AuthenticationFactor.h @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 . + */ + +#ifndef KEEPASSXC_AUTHENTICATIONFACTOR_H +#define KEEPASSXC_AUTHENTICATIONFACTOR_H + +#include "AuthenticationFactorGroup.h" +#include "FactorKeyDerivation.h" +#include "core/AuthenticationFactorUserData.h" + +#include + +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 unwrapKey(const QSharedPointer& 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& userData) const; + + QString m_name = ""; + AuthenticationFactorKeyType m_keyType = AuthenticationFactorKeyType::NONE; + QByteArray m_keySalt = QByteArray(); + QByteArray m_wrappedKey = QByteArray(); + + QString m_factorType = FACTOR_TYPE_NULL; + QSharedPointer m_derivation; +}; + +#endif // KEEPASSXC_AUTHENTICATIONFACTOR_H diff --git a/src/format/multifactor/AuthenticationFactorGroup.cpp b/src/format/multifactor/AuthenticationFactorGroup.cpp new file mode 100644 index 000000000..201482bcf --- /dev/null +++ b/src/format/multifactor/AuthenticationFactorGroup.cpp @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 "AuthenticationFactorGroup.h" + +#include + +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& factor) +{ + m_factors.append(factor); + factor->setParent(this); +} + +const QList>& 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 +AuthenticationFactorGroup::getRawKey(const QSharedPointer& 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::create(m_key.data(), m_key.size()); +} diff --git a/src/format/multifactor/AuthenticationFactorGroup.h b/src/format/multifactor/AuthenticationFactorGroup.h new file mode 100644 index 000000000..b48b45dfe --- /dev/null +++ b/src/format/multifactor/AuthenticationFactorGroup.h @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 . + */ + +#ifndef KEEPASSXC_AUTHENTICATIONFACTORGROUP_H +#define KEEPASSXC_AUTHENTICATIONFACTORGROUP_H + +#include +#include +#include + +#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 getRawKey(const QSharedPointer& 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& factor); + const QList>& getFactors() const; + +protected: + QByteArray m_validationIn = QByteArray(); + QByteArray m_validationOut = QByteArray(); + QByteArray m_challenge = QByteArray(); + AuthenticationFactorGroupValidationType m_validationType = AuthenticationFactorGroupValidationType::NONE; + + QList> m_factors = QList>(); + + Botan::secure_vector m_key; +}; + +#endif // KEEPASSXC_AUTHENTICATIONFACTORGROUP_H diff --git a/src/format/multifactor/AuthenticationFactorInfo.cpp b/src/format/multifactor/AuthenticationFactorInfo.cpp new file mode 100644 index 000000000..b045165f2 --- /dev/null +++ b/src/format/multifactor/AuthenticationFactorInfo.cpp @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 "AuthenticationFactorInfo.h" + +void AuthenticationFactorInfo::setComprehensive(bool comprehensive) +{ + m_comprehensive = comprehensive; +} + +bool AuthenticationFactorInfo::isComprehensive() const +{ + return m_comprehensive; +} + +void AuthenticationFactorInfo::addGroup(const QSharedPointer& group) +{ + m_groups.append(group); + group->setParent(this); +} + +const QList>& AuthenticationFactorInfo::getGroups() const +{ + return m_groups; +} diff --git a/src/format/multifactor/AuthenticationFactorInfo.h b/src/format/multifactor/AuthenticationFactorInfo.h new file mode 100644 index 000000000..adc85c621 --- /dev/null +++ b/src/format/multifactor/AuthenticationFactorInfo.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 . + */ + +#ifndef KEEPASSXC_AUTHENTICATIONFACTORINFO_H +#define KEEPASSXC_AUTHENTICATIONFACTORINFO_H + +#include "format/multifactor/AuthenticationFactorGroup.h" +#include +#include + +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& group); + const QList>& getGroups() const; + +protected: + bool m_comprehensive = false; + QList> m_groups = QList>(); +}; + +#endif // KEEPASSXC_AUTHENTICATIONFACTORINFO_H diff --git a/src/format/multifactor/FactorKeyDerivation.cpp b/src/format/multifactor/FactorKeyDerivation.cpp new file mode 100644 index 000000000..dc2320e57 --- /dev/null +++ b/src/format/multifactor/FactorKeyDerivation.cpp @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 "FactorKeyDerivation.h" + +const QString& FactorKeyDerivation::getError() const +{ + return m_error; +} diff --git a/src/format/multifactor/FactorKeyDerivation.h b/src/format/multifactor/FactorKeyDerivation.h new file mode 100644 index 000000000..ec734587f --- /dev/null +++ b/src/format/multifactor/FactorKeyDerivation.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 . + */ + +#ifndef KEEPASSXC_FACTOR_KEY_DERIVATION_H +#define KEEPASSXC_FACTOR_KEY_DERIVATION_H + +#include + +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 diff --git a/src/format/multifactor/FidoAuthenticationFactor.cpp b/src/format/multifactor/FidoAuthenticationFactor.cpp new file mode 100644 index 000000000..87cdf764e --- /dev/null +++ b/src/format/multifactor/FidoAuthenticationFactor.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 "FidoAuthenticationFactor.h" + +FidoAuthenticationFactor::FidoAuthenticationFactor(const QSharedPointer& 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; +} \ No newline at end of file diff --git a/src/format/multifactor/FidoAuthenticationFactor.h b/src/format/multifactor/FidoAuthenticationFactor.h new file mode 100644 index 000000000..d17b13131 --- /dev/null +++ b/src/format/multifactor/FidoAuthenticationFactor.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 . + */ + +#ifndef KEEPASSXC_FIDOAUTHENTICATIONFACTOR_H +#define KEEPASSXC_FIDOAUTHENTICATIONFACTOR_H + +#include "format/multifactor/AuthenticationFactor.h" + +#include + +class FidoAuthenticationFactor : public AuthenticationFactor +{ + Q_OBJECT + +public: + explicit FidoAuthenticationFactor(const QSharedPointer& factor); + ~FidoAuthenticationFactor() override = default; + + void setCredentialID(const QByteArray& credentialID); + const QByteArray& getCredentialID() const; + +protected: + QByteArray m_credentialID; +}; + +#endif // KEEPASSXC_FIDOAUTHENTICATIONFACTOR_H diff --git a/src/format/multifactor/PasswordAuthenticationFactor.cpp b/src/format/multifactor/PasswordAuthenticationFactor.cpp new file mode 100644 index 000000000..fad9e5ffb --- /dev/null +++ b/src/format/multifactor/PasswordAuthenticationFactor.cpp @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 "PasswordAuthenticationFactor.h" +#include "keys/PasswordKey.h" + +#include +#include + +PasswordAuthenticationFactor::PasswordAuthenticationFactor(const QSharedPointer& 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& 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; +} diff --git a/src/format/multifactor/PasswordAuthenticationFactor.h b/src/format/multifactor/PasswordAuthenticationFactor.h new file mode 100644 index 000000000..b04671888 --- /dev/null +++ b/src/format/multifactor/PasswordAuthenticationFactor.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 . + */ + +#ifndef KEEPASSXC_PASSWORDAUTHENTICATIONFACTOR_H +#define KEEPASSXC_PASSWORDAUTHENTICATIONFACTOR_H + +#include "format/multifactor/AuthenticationFactor.h" + +#include + +class PasswordAuthenticationFactor : public AuthenticationFactor +{ + Q_OBJECT + +public: + explicit PasswordAuthenticationFactor(const QSharedPointer& factor); + ~PasswordAuthenticationFactor() override = default; + +protected: + QByteArray getUnwrappingKey(const QSharedPointer& userData) const override; +}; + +#endif // KEEPASSXC_PASSWORDAUTHENTICATIONFACTOR_H diff --git a/src/keys/MultiAuthenticationHeaderKey.cpp b/src/keys/MultiAuthenticationHeaderKey.cpp new file mode 100644 index 000000000..d23faa27c --- /dev/null +++ b/src/keys/MultiAuthenticationHeaderKey.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 "MultiAuthenticationHeaderKey.h" + +#include "FileKey.h" +#include "PasswordKey.h" +#include "core/AsyncTask.h" + +#include + +QUuid MultiAuthenticationHeaderKey::UUID("e31ab20b-ee50-45af-99bc-ab4c4d34f4cc"); + +MultiAuthenticationHeaderKey::MultiAuthenticationHeaderKey( + const QSharedPointer& authenticationFactorInfo, + const QSharedPointer& existingKey) + : Key(UUID) +{ + m_authenticationFactorInfo = authenticationFactorInfo; + m_userData = QSharedPointer::create(); + + for (const auto& keyPart : existingKey->keys()) { + const auto& uuid = keyPart->uuid(); + + m_userData->addDataItem(uuid.toString(), QSharedPointer::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::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; +} diff --git a/src/keys/MultiAuthenticationHeaderKey.h b/src/keys/MultiAuthenticationHeaderKey.h new file mode 100644 index 000000000..da6c1cb99 --- /dev/null +++ b/src/keys/MultiAuthenticationHeaderKey.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 . + */ + +#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 +#include + +class MultiAuthenticationHeaderKey : public Key +{ +public: + explicit MultiAuthenticationHeaderKey( + const QSharedPointer& authenticationFactorInfo, + const QSharedPointer& 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 m_authenticationFactorInfo; + QSharedPointer m_userData; + + QString m_error; + Botan::secure_vector m_key; +}; + +#endif // KPXC_MULTI_AUTHENTICATION_HEADER_KEY_H diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8ed6868df..7e9e10fc3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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}) diff --git a/tests/TestAuthenticationFactorParsing.cpp b/tests/TestAuthenticationFactorParsing.cpp new file mode 100644 index 000000000..d4cd6204a --- /dev/null +++ b/tests/TestAuthenticationFactorParsing.cpp @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 "TestAuthenticationFactorParsing.h" + +#include + +QTEST_GUILESS_MAIN(TestAuthenticationFactorParsing) + +void TestAuthenticationFactorParsing::testNotXML() +{ + m_reader.readAuthenticationFactors(nullptr, "blargh"); + + QVERIFY(m_reader.hasError()); +} + +void TestAuthenticationFactorParsing::testMalformedXML() +{ + m_reader.readAuthenticationFactors(nullptr, ""); + + QVERIFY(m_reader.hasError()); +} + +void TestAuthenticationFactorParsing::testMissingFactorInfo() +{ + m_reader.readAuthenticationFactors(nullptr, ""); + + QVERIFY(m_reader.hasError()); +} + +void TestAuthenticationFactorParsing::testNoCompatVersion() +{ + m_reader.readAuthenticationFactors(nullptr, ""); + + QVERIFY(m_reader.hasError()); +} + +void TestAuthenticationFactorParsing::testUnsupportedCompatVersion() +{ + m_reader.readAuthenticationFactors(nullptr, "2"); + + QVERIFY(m_reader.hasError()); +} + +void TestAuthenticationFactorParsing::testNoGroups() +{ + auto res = m_reader.readAuthenticationFactors(nullptr, "1"); + + QVERIFY(!m_reader.hasError()); + QCOMPARE(res->getGroups().size(), 0); +} + +void TestAuthenticationFactorParsing::testGroupWithNoFactors() +{ + m_reader.readAuthenticationFactors(nullptr, + "1"); + + QVERIFY(m_reader.hasError()); +} + +void TestAuthenticationFactorParsing::testUnsupportedFactorTypeAlone() +{ + m_reader.readAuthenticationFactors(nullptr, + "1" + "AES-CBCbogus" + "B4pHAoQomD8728UKeST2HOxglrjzwyq2M/IPEOV4xo8=" + ""); + QVERIFY(m_reader.hasError()); +} + +void TestAuthenticationFactorParsing::testUnsupportedFactorTypeAndSupportedTogether() +{ + auto res = m_reader.readAuthenticationFactors( + nullptr, + "1" + "AES-CBCbogus" + "B4pHAoQomD8728UKeST2HOxglrjzwyq2M/IPEOV4xo8=" + "AES-CBC" FACTOR_TYPE_PASSWORD_SHA256 "" + "B4pHAoQomD8728UKeST2HOxglrjzwyq2M/IPEOV4xo8=" + ""); + QVERIFY(!m_reader.hasError()); + QCOMPARE(res->getGroups().first()->getFactors().size(), 2); +} + +void TestAuthenticationFactorParsing::testUnsupportedVerificationMethod() +{ + auto res = m_reader.readAuthenticationFactors( + nullptr, + "1" + "bogus" + "AES-CBC" FACTOR_TYPE_PASSWORD_SHA256 "" + "B4pHAoQomD8728UKeST2HOxglrjzwyq2M/IPEOV4xo8=" + ""); + QVERIFY(!m_reader.hasError()); + QCOMPARE(res->getGroups().first()->getValidationType(), AuthenticationFactorGroupValidationType::NONE); +} + +void TestAuthenticationFactorParsing::testOmittedVerification() +{ + m_reader.readAuthenticationFactors(nullptr, + "1" + "AES-CBC" FACTOR_TYPE_PASSWORD_SHA256 + "" + "B4pHAoQomD8728UKeST2HOxglrjzwyq2M/IPEOV4xo8=" + ""); + QVERIFY(!m_reader.hasError()); +} + +void TestAuthenticationFactorParsing::testInvalidBase64() +{ + m_reader.readAuthenticationFactors(nullptr, + "1" + "AES-CBC" FACTOR_TYPE_PASSWORD_SHA256 + "" + "_" + ""); + QVERIFY(m_reader.hasError()); +} + +void TestAuthenticationFactorParsing::testMissingRequiredFields() +{ + m_reader.readAuthenticationFactors(nullptr, + "1" + "" FACTOR_TYPE_PASSWORD_SHA256 "" + "B4pHAoQomD8728UKeST2HOxglrjzwyq2M/IPEOV4xo8=" + ""); + QVERIFY(m_reader.hasError()); +} diff --git a/tests/TestAuthenticationFactorParsing.h b/tests/TestAuthenticationFactorParsing.h new file mode 100644 index 000000000..50fa0c2ee --- /dev/null +++ b/tests/TestAuthenticationFactorParsing.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 . + */ + +#ifndef KEEPASSXC_TEST_AUTHENTICATIONFACTORPARSING_H +#define KEEPASSXC_TEST_AUTHENTICATIONFACTORPARSING_H + +#include + +#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 diff --git a/tests/TestKdbx4.cpp b/tests/TestKdbx4.cpp index 3233d570a..ec665cb24 100644 --- a/tests/TestKdbx4.cpp +++ b/tests/TestKdbx4.cpp @@ -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::create(); + auto passwordKey = QSharedPointer::create(); + passwordKey->setPassword(QByteArray::fromStdString("somepassword")); + key->addKey(QSharedPointer(passwordKey)); + + KeePass2Reader reader; + auto db = QSharedPointer::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)); +} diff --git a/tests/TestKdbx4.h b/tests/TestKdbx4.h index cd5bc4788..ec17d6a31 100644 --- a/tests/TestKdbx4.h +++ b/tests/TestKdbx4.h @@ -66,6 +66,7 @@ private slots: void testUpgradeMasterKeyIntegrity_data(); void testAttachmentIndexStability(); void testCustomData(); + void testMultiFactorHeaderRead(); }; #endif // KEEPASSXC_TEST_KDBX4_H diff --git a/tests/data/MultiFactorPasswordOnly.kdbx b/tests/data/MultiFactorPasswordOnly.kdbx new file mode 100644 index 000000000..4dc45a173 Binary files /dev/null and b/tests/data/MultiFactorPasswordOnly.kdbx differ