From 8a3985959de9eb07feeaf8090656cf8f898d4378 Mon Sep 17 00:00:00 2001 From: Bryan Jacobs Date: Thu, 15 Feb 2024 16:55:00 +1100 Subject: [PATCH] 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. --- share/translations/keepassxc_en.ts | 212 +++++++++-- src/CMakeLists.txt | 10 + src/core/AuthenticationFactorUserData.cpp | 34 ++ src/core/AuthenticationFactorUserData.h | 40 +++ src/core/Database.cpp | 16 + src/core/Database.h | 11 + src/crypto/SymmetricCipher.cpp | 2 + src/crypto/SymmetricCipher.h | 1 + src/format/Kdbx4Reader.cpp | 20 +- src/format/Kdbx4Reader.h | 2 + src/format/KdbxReader.cpp | 24 ++ .../KdbxXmlAuthenticationFactorReader.cpp | 334 ++++++++++++++++++ .../KdbxXmlAuthenticationFactorReader.h | 59 ++++ .../multifactor/AESCBCFactorKeyDerivation.cpp | 38 ++ .../multifactor/AESCBCFactorKeyDerivation.h | 38 ++ .../multifactor/AuthenticationFactor.cpp | 98 +++++ src/format/multifactor/AuthenticationFactor.h | 74 ++++ .../multifactor/AuthenticationFactorGroup.cpp | 98 +++++ .../multifactor/AuthenticationFactorGroup.h | 70 ++++ .../multifactor/AuthenticationFactorInfo.cpp | 39 ++ .../multifactor/AuthenticationFactorInfo.h | 46 +++ .../multifactor/FactorKeyDerivation.cpp | 23 ++ src/format/multifactor/FactorKeyDerivation.h | 39 ++ .../multifactor/FidoAuthenticationFactor.cpp | 37 ++ .../multifactor/FidoAuthenticationFactor.h | 40 +++ .../PasswordAuthenticationFactor.cpp | 50 +++ .../PasswordAuthenticationFactor.h | 37 ++ src/keys/MultiAuthenticationHeaderKey.cpp | 93 +++++ src/keys/MultiAuthenticationHeaderKey.h | 58 +++ tests/CMakeLists.txt | 3 + tests/TestAuthenticationFactorParsing.cpp | 142 ++++++++ tests/TestAuthenticationFactorParsing.h | 51 +++ tests/TestKdbx4.cpp | 28 ++ tests/TestKdbx4.h | 1 + tests/data/MultiFactorPasswordOnly.kdbx | Bin 0 -> 2112 bytes 35 files changed, 1835 insertions(+), 33 deletions(-) create mode 100644 src/core/AuthenticationFactorUserData.cpp create mode 100644 src/core/AuthenticationFactorUserData.h create mode 100644 src/format/KdbxXmlAuthenticationFactorReader.cpp create mode 100644 src/format/KdbxXmlAuthenticationFactorReader.h create mode 100644 src/format/multifactor/AESCBCFactorKeyDerivation.cpp create mode 100644 src/format/multifactor/AESCBCFactorKeyDerivation.h create mode 100644 src/format/multifactor/AuthenticationFactor.cpp create mode 100644 src/format/multifactor/AuthenticationFactor.h create mode 100644 src/format/multifactor/AuthenticationFactorGroup.cpp create mode 100644 src/format/multifactor/AuthenticationFactorGroup.h create mode 100644 src/format/multifactor/AuthenticationFactorInfo.cpp create mode 100644 src/format/multifactor/AuthenticationFactorInfo.h create mode 100644 src/format/multifactor/FactorKeyDerivation.cpp create mode 100644 src/format/multifactor/FactorKeyDerivation.h create mode 100644 src/format/multifactor/FidoAuthenticationFactor.cpp create mode 100644 src/format/multifactor/FidoAuthenticationFactor.h create mode 100644 src/format/multifactor/PasswordAuthenticationFactor.cpp create mode 100644 src/format/multifactor/PasswordAuthenticationFactor.h create mode 100644 src/keys/MultiAuthenticationHeaderKey.cpp create mode 100644 src/keys/MultiAuthenticationHeaderKey.h create mode 100644 tests/TestAuthenticationFactorParsing.cpp create mode 100644 tests/TestAuthenticationFactorParsing.h create mode 100644 tests/data/MultiFactorPasswordOnly.kdbx 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 0000000000000000000000000000000000000000..4dc45a173ef7d3e86393fa8aec22e8fcc2ecd219 GIT binary patch literal 2112 zcmZ8i3pA8#7aq3~;Y3OzN;+wbG1C~gF`1h&#$_0{Q7$v%!!S29V{RdGIw)O4If_3; z;#j5Jigc`8QVL0ulAMyH<8Ll@4(E|BX% zAWe|^4y91$8tb0TWg4D2DO20iQh=f;>J}@~i zb8PVBTt#J>#%rCmTB;f(ZKcy!nrf{eJ}N}HRJQ&9+RI8}GED#gDj^{RQ%r{d_R(D-ZTBUg06DxHziKx<=Eay63;N>5J`#Q|Iqi%A!; zc-+VsI#a|G2=Ph+%420u;>PgMC>)Q&r;CCCfl$dp!%)zF0xNL=pv)v>i2<}okOX|C z{0~DB1-v9a8Wl`uv!Yju1Tc<0bS zij){9B>RK}cuH_UfJZDj7$(7ab1_7Fs)WaL`#zOY`duQwBoP{*#RAMkD%cOh@$q8B zlK>2W!O+8y4sZ{^&mJBI7qBED9BDKj;~f~p^A5rZL&b@~2qsBJhK5lPTn{*zBF6^! z5CY;UNHEulCk`ZgN!W5@uR9E>ALq%QbG=@Nh;IO!r$O?c85zzU3AX-U4d&uMo31UxsLUgPEPY@Hx zPzSg-$ulH|DW&*>`2xXiE=}gibd>O%l)1kXqM%>L1dW1zd*E-t%G`XNTRj~;9Rcb3 z8xurXA~6$9Ik(4URS{BQv)*)WVzQ=W=`*9Ziy*t@YLQ^v>(%?ztvsd6!iGBLSN5@X z#-Y-5>3VZfg|%=)?QBbS+*oYM2+7uC^X!S|X%p=9tNw-8P85__u2y{&gM*@(^N(nT zn|zuUiV78d{*~C)3==2Yr^~yxB_y$iviHVWc2&mBc4S^T`VJ_MvT!hK*Y3+x&vBOS zdo=@MfoH~DvqaUU$5Q;KUrn3CWC?|wCb#^yOpZmWMP#-Ax({Mv`sL2lpNH@Ui)ABk zBDgfrdr0ZEt{~zD0Rn7aWo;ibf-QQ+ef_LwOLd#}A2vOpZrD~v+m1`!IXQ+dIrq;I zZ_mH3l2_*`z?~UWHniWR?RE18ufOc%Yl43o>>e?N-Mt*j#uNH=GPeJ1NJ|P^rna6u zwXc6XR3xjiI(+7jO92thY4=LpmO0kYX{~T(&#n@uVwXbACoV3E$oJ(F-Bm-S3+wDG zN$Z5u@0~0%vQA#});YZWK(aZ149qm4;T%tEwk##LC$yePy;)gd^6*kh;RU@f(+%1? zI(yslg~W!v4^GjKbj=`AI$Ks&mHo%7UP5^@38Gfn2XUuyt)0I^FB-Vo7bdKMu%xX$ogZxx=%; zgE2ccl^>ObpfoB%BOgyKvb-Ks`~SXv=5)As8eYM@d0*R=s6F@b^9x!)fm-rHpeyy5 z^mi;VY7<`K_Gh_57X{qtFQIlByDvAjT-@zAyp7s@qD0cI?Pq|&LGl%l!oI8+i<8)f_!o1{2vk)4I^lgnwS)aFRO-}ZY z^~aC4Sl!s%ktEkZJTfEB-10oIP5$Fc_63-;jCTmJyUpl>WJ~*&teQ!Ei+asSte_~+ zd@hQ6@xEF}@o3aGd6_$9bz9}nUFKDjkc2dco;w+^qgSKguRn*s&1}T&d?e>@i>f&E z)Q;{ud(hH%RG@$IU+`CA3^i*M{7i4m+rMe}sjpqZ5HZom2x0jg=yVbB(wf!6=i26U z-$d;Vzj>$jaPGlq9Ypnu7glQ-SLRO)Beg2xC$056AS1ut(6&Y78cgOMah==aBQI>Q z^vk|$e?W6NXYb_(o?DcffHelFKZ1@T8 z5rl_w@nCUhpS^BG^3`0jZS`zj;CKY%b3zB@^bM22w@W@Ur&X!_OStB>`*S z?%jA^3(n~IMPX}p+dX$F-D76|`k&sxgBwqdK*FO)$lvmtF_#(>NmOUNxI;nPA> zsY{~bMG-y&;v4+d&h8ktvA@~>O8SegVo9FCQmRJka-pERR%>!9f8(C+;;t-BgGIe~ z)9f{dW+Z1yzTFh`SIO+y`R7CV9SZbuVdH$T-96*sj7uZKe^y`c$qVYe!CtNJ@{+?%zN{T>^e&pamncJFH?Rr)xY%L9