diff --git a/CMakeLists.txt b/CMakeLists.txt index 672ac4c0ab..776d9bfa2c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -567,6 +567,9 @@ if(ZLIB_VERSION_STRING VERSION_LESS "1.2.0") endif() include_directories(SYSTEM ${ZLIB_INCLUDE_DIR}) +# Find Minizip +find_package(Minizip REQUIRED) + if(WITH_XC_YUBIKEY) find_package(PCSC REQUIRED) include_directories(SYSTEM ${PCSC_INCLUDE_DIRS}) diff --git a/COPYING b/COPYING index 480b01f07d..6baf4c27e2 100644 --- a/COPYING +++ b/COPYING @@ -143,11 +143,13 @@ License: MIT Files: share/icons/application/scalable/actions/application-exit.svg share/icons/application/scalable/actions/attributes-copy.svg share/icons/application/scalable/actions/auto-type.svg + share/icons/application/scalable/actions/bitwarden.svg share/icons/application/scalable/actions/bugreport.svg share/icons/application/scalable/actions/chevron-double-down.svg share/icons/application/scalable/actions/chevron-double-right.svg share/icons/application/scalable/actions/clipboard-text.svg share/icons/application/scalable/actions/configure.svg + share/icons/application/scalable/actions/csv.svg share/icons/application/scalable/actions/database-change-key.svg share/icons/application/scalable/actions/database-lock.svg share/icons/application/scalable/actions/database-lock-all.svg @@ -192,6 +194,7 @@ Files: share/icons/application/scalable/actions/application-exit.svg share/icons/application/scalable/actions/move-up.svg share/icons/application/scalable/actions/object-locked.svg share/icons/application/scalable/actions/object-unlocked.svg + share/icons/application/scalable/actions/onepassword.svg share/icons/application/scalable/actions/paperclip.svg share/icons/application/scalable/actions/password-copy.svg share/icons/application/scalable/actions/passkey.svg diff --git a/docs/images/csv_import.png b/docs/images/csv_import.png index 8fce05e57f..cddd2c4e04 100644 Binary files a/docs/images/csv_import.png and b/docs/images/csv_import.png differ diff --git a/docs/images/import_wizard.png b/docs/images/import_wizard.png new file mode 100644 index 0000000000..bba4cdd94d Binary files /dev/null and b/docs/images/import_wizard.png differ diff --git a/docs/topics/ImportExport.adoc b/docs/topics/ImportExport.adoc index dad5d98bb0..bcae4ce579 100644 --- a/docs/topics/ImportExport.adoc +++ b/docs/topics/ImportExport.adoc @@ -6,53 +6,66 @@ include::.sharedheader[] == Importing External Databases KeePassXC allows you to import external databases from the following options: -* Comma-Separated Values (CSV) file -* 1Password OPVault -* KeePass 1 Database +* Comma Separated Values (.csv) +* 1Password Export (.1pux) +* 1Password Vault (.opvault) +* Bitwarden (.json) +* KeePass 1 Database (.kdb) -=== Importing CSV File -If you have been saving your URLs, usernames, passwords, and so on in a CSV file, you can migrate all that information from the CSV file to KeePassXC and start using KeePassXC to maintain your data. +To import any of these files, start KeePassXC and either click the `Import File` button on the welcome screen or use the menu Database > Import... to launch the Import Wizard. -To open the CSV file, perform the following steps: +.Import Wizard +image::import_wizard.png[] -1. Open KeePassXC. +For each of the import options, you will be prompted to select the file to import and then provide credentials to unlock the file, if necessary. You can then choose to import the file into a new database or into an existing database that is already unlocked in KeePassXC. -2. Click Import from CSV button on the welcome screen or use the menu Database > Import > CSV File. +=== Importing CSV File +WARNING: A CSV file is unencrypted and you should securely delete this file after successfully importing it into KeePassXC. -3. Navigate to the location of the your CSV file on your computer and open the file. The new database wizard will appear. Follow the steps of creating a new database in Chapter 1. +1. Follow the steps above and click `Continue`. The CSV import wizard will appear. -4. After saving your new database file, the CSV import wizard will appear. On this dialog you can choose the various options for properly importing the data. You may need to select the _First line has field names_ checkbox before starting. Analyze the output in the preview at the bottom to determine the correct import settings. +2. On this dialog you can choose the various options for properly importing the data. Analyze the output in the preview at the bottom to determine the correct import settings. You may need to re-map the column associations to match the data in your CSV file. + .CSV Import Wizard image::csv_import.png[] -Your CSV file gets imported to KeePassXC and the data is converted to the KeePassXC format for further usage and maintenance. The new database file is saved on to your computer with the default `.kdbx` extension. +3. Click `Done` to complete the import. If you chose to create a new database, the New Database dialog will appear. Otherwise your entries will be nested under the group you chose for the existing database. + +=== Importing 1Password Export +WARNING: A 1Password Export file is unencrypted and you should securely delete this file after successfully importing it into KeePassXC. + +1. Open the Import Wizard as shown above. Select the 1Password Export option. + +2. Click `Continue` to unlock and preview the import. Click `Done` to complete the import. === Importing 1Password OPVault +NOTE: You must have 1Password version 7 or 8 to export your data to an OPVault. If you are using a newer version of 1Password, you should use the 1Password Export (1PUX) format instead. + Save your 1Password Vault locally to create an OPVault directory. Please see 1Password instructions on how to do this. Once an OPVault is created, perform the following steps: -1. Open KeePassXC. +1. Open the Import Wizard as shown above. Select the 1Password Vault option. -2. Use the menu Database > Import > 1Password Vault. Select the OPVault to import. +2. Enter the password for your vault and click `Continue` to unlock and preview the import. Click `Done` to complete the import. -3. Enter the password for your OPVault to unlock and import. +=== Importing Bitwarden +WARNING: A Bitwarden Export file may be unencrypted and you should securely delete this file after successfully importing it into KeePassXC. -=== Importing KeePass 1 Database -KeePass 1 database is an older format of the database created using legacy version of KeePass. KeePassXC lets your import this older format of the database and you can seamlessly start using this database in your new KeePassXC application. +1. Open the Import Wizard as shown above. Select the Bitwarden option. -To import a KeePass 1 database file in KeePassXC, perform the following steps: +2. Optionally provide a password to decrypt the Bitwarden export file. You should only need to do this if you have chosen the encrypted json export option within Bitwarden. -1. Open KeePassXC. +3. Click `Continue` to unlock and preview the import. Click `Done` to complete the import. -2. Click Import from KeePass 1 button on the welcome screen or use the menu Database > Import > KeePass 1 Database. +=== Importing KeePass 1 Database +KeePass 1 database is an older format of the database created using a legacy version of KeePass. KeePassXC lets your import this older format of the database and you can seamlessly start using this database in your new KeePassXC application. -3. Navigate to the location of the your legacy KeePass 1 database file (`.kdb`) on your computer and open the file. You are prompted for the password and the Key file for your `.kdb` file. +To import a KeePass 1 database file in KeePassXC, perform the following steps: -4. Enter the password for your old `.kdb` file and click *OK*. You are prompted for provide a name for the new database format that KeePassXC recognizes. +1. Open the Import Wizard as shown above. Select the KeePass1 Database option. -5. Provide a name for the new database format, select a folder on your computer to save the file, and click Save. +2. Enter the password for your database and optionally provide a key file if it was configured for your KeePass1 database. -6. The data from the `.kdb` file gets imported and converted to the new format, which is compatible with KeePassXC. You can now start using the new database file (`.kdbx`) in KeePassXC. +3. Click `Continue` to unlock and preview the import. Click `Done` to complete the import. == Exporting Databases KeePassXC supports multiple ways to export your database for transfer to another program or to print out and archive. diff --git a/share/icons/application/scalable/actions/bitwarden.svg b/share/icons/application/scalable/actions/bitwarden.svg new file mode 100644 index 0000000000..9d22e9adc1 --- /dev/null +++ b/share/icons/application/scalable/actions/bitwarden.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/csv.svg b/share/icons/application/scalable/actions/csv.svg new file mode 100644 index 0000000000..e76afb0a81 --- /dev/null +++ b/share/icons/application/scalable/actions/csv.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/onepassword.svg b/share/icons/application/scalable/actions/onepassword.svg new file mode 100644 index 0000000000..b43237abc2 --- /dev/null +++ b/share/icons/application/scalable/actions/onepassword.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/icons.qrc b/share/icons/icons.qrc index 78980d971b..a846bf7bc4 100644 --- a/share/icons/icons.qrc +++ b/share/icons/icons.qrc @@ -8,11 +8,13 @@ application/scalable/actions/application-exit.svg application/scalable/actions/attributes-copy.svg application/scalable/actions/auto-type.svg + application/scalable/actions/bitwarden.svg application/scalable/actions/bugreport.svg application/scalable/actions/chevron-double-down.svg application/scalable/actions/chevron-double-right.svg application/scalable/actions/clipboard-text.svg application/scalable/actions/configure.svg + application/scalable/actions/csv.svg application/scalable/actions/database-change-key.svg application/scalable/actions/database-lock.svg application/scalable/actions/database-lock-all.svg @@ -58,6 +60,7 @@ application/scalable/actions/move-up.svg application/scalable/actions/object-locked.svg application/scalable/actions/object-unlocked.svg + application/scalable/actions/onepassword.svg application/scalable/actions/paperclip.svg application/scalable/actions/passkey.svg application/scalable/actions/password-copy.svg diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index 494d42f3f3..4f84ea6e83 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -1286,14 +1286,6 @@ Would you like to migrate your existing settings now? CsvImportWidget - - Import CSV fields - - - - filename - - size, rows, columns @@ -1402,18 +1394,6 @@ Would you like to migrate your existing settings now? Column %1 - - Imported from CSV file - - - - Original data: - - - - Error(s) detected in CSV file! - - [%n more message(s) skipped] @@ -1422,31 +1402,19 @@ Would you like to migrate your existing settings now? - Error + Failed to parse CSV file: %1 - CSV import: writer has errors: -%1 + Imported from CSV file: %1 CsvParserModel - - %1, %2, %3 - file info: bytes, rows, columns - - - - %n byte(s) - - - - - %n row(s) + CSV row count @@ -1454,6 +1422,7 @@ Would you like to migrate your existing settings now? %n column(s) + CSV column count @@ -2315,26 +2284,10 @@ This is definitely a bug, please report it to the developers. CSV file - - Select CSV file - - Merge database - - KeePass 1 database - - - - Open KeePass 1 database - - - - Open OPVault - - Export database to CSV file @@ -2355,15 +2308,6 @@ This is definitely a bug, please report it to the developers. You are about to export your database to an unencrypted file. This will leave your passwords and sensitive information vulnerable! Are you sure you want to continue? - - New Database - - - - %1 [New Database] - Database tab name modifier - - %1 [Locked] Database tab name modifier @@ -2541,6 +2485,19 @@ Disable safe saves and try again? Could not find database file: %1 + + New Database + + + + %1 [New Database] + Database tab name modifier + + + + Searches and Tags + + Entries expiring within %1 day(s) @@ -2549,17 +2506,13 @@ Disable safe saves and try again? - Searches and Tags + Save Enter a unique name or overwrite an existing search from the list: - - Save - - Save Search @@ -4311,6 +4264,147 @@ You can enable the DuckDuckGo website icon service in the security section of th + + ImportWizard + + Import Wizard + + + + + ImportWizardPageReview + + WizardPage + + + + Entry count: %1 + + + + Group + + + + Title + + + + Username + + + + Password + + + + Url + + + + + ImportWizardPageSelect + + Form + + + + Import File Selection + + + + Password: + + + + Key File: + + + + Browse… + + + + Import Into: + + + + New Database + + + + No unlocked databases available + + + + Existing Database: + + + + Import File: + + + + Comma Separated Values (.csv) + + + + 1Password Export (.1pux) + + + + 1Password Vault (.opvault) + + + + Bitwarden (.json) + + + + KeePass 1 Database (.kdb) + + + + Open OPVault + + + + Select import file + + + + All files + + + + Key files + + + + Select key file + + + + Comma Separated Values + + + + 1Password Export + + + + Bitwarden JSON Export + + + + 1Password Vault + + + + KeePass1 Database + + + KMessageWidget @@ -4738,17 +4832,6 @@ Line %2, column %3 - - KeePass1OpenWidget - - Import KeePass1 Database - - - - Unable to open the database. - - - KeePass1Reader @@ -5102,10 +5185,6 @@ Are you sure you want to continue with this file? &Recent Databases - - &Import - - &Export @@ -5558,7 +5637,19 @@ We recommend you use the AppImage available on our downloads page. - No Tags + 1Password 1PUX... + + + + Import a 1Password 1PUX file + + + + Import… + + + + Passkeys… @@ -5589,15 +5680,15 @@ We recommend you use the AppImage available on our downloads page. - Passkeys… + Passkeys - Passkeys + Import Passkey - Import Passkey + No Tags @@ -5824,14 +5915,6 @@ We recommend you use the AppImage available on our downloads page. - - OpVaultOpenWidget - - Read Database did not produce an instance -%1 - - - OpVaultReader @@ -8275,7 +8358,73 @@ This options is deprecated, use --set-key-file instead. - Credential is excluded + Favorite + Tag for favorite entries + + + + File does not exist. + + + + Cannot open file: %1 + + + + Cannot parse file: %1 at position %2 + + + + Failed to decrypt json file: %1 + + + + Invalid encKeyValidation field + + + + Invalid cipher list within encKeyValidation field + + + + Wrong password + + + + Invalid encrypted data field + + + + Invalid cipher list within encrypted data field + + + + Cannot initialize cipher + + + + Cannot decrypt data + + + + Bitwarden Import + + + + Archived + Tag for archived entries + + + + Invalid 1PUX file format: Not a valid ZIP file. + + + + Invalid 1PUX file format: Missing export.data + + + + 1Password Import @@ -8302,6 +8451,10 @@ This options is deprecated, use --set-key-file instead. Cannot remove file key: The database does not have a file key. + + Credential is excluded + + QtIOCompressor @@ -9340,35 +9493,27 @@ Example: JBSWY3DPEHPK3PXP - Create new database - - - - Open existing database - - - - Import from KeePass 1 + Recent databases - Import from 1Password + Open a recent database - Import from CSV + Welcome to KeePassXC %1 - Recent databases + Create Database - Open a recent database + Open Database - Welcome to KeePassXC %1 + Import File diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f3886d960e..40dae93714 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -71,6 +71,7 @@ set(keepassx_SOURCES crypto/kdf/Kdf.cpp crypto/kdf/AesKdf.cpp crypto/kdf/Argon2Kdf.cpp + format/BitwardenReader.cpp format/CsvExporter.cpp format/CsvParser.cpp format/KeePass1Reader.cpp @@ -87,6 +88,7 @@ set(keepassx_SOURCES format/Kdbx4Writer.cpp format/KdbxXmlWriter.cpp format/OpData01.cpp + format/OPUXReader.cpp format/OpVaultReader.cpp format/OpVaultReaderAttachments.cpp format/OpVaultReaderBandEntry.cpp @@ -117,12 +119,10 @@ set(keepassx_SOURCES gui/GuiTools.cpp gui/HtmlExporter.cpp gui/IconModels.cpp - gui/KeePass1OpenWidget.cpp gui/KMessageWidget.cpp gui/MainWindow.cpp gui/MessageBox.cpp gui/MessageWidget.cpp - gui/OpVaultOpenWidget.cpp gui/PasswordWidget.cpp gui/PasswordGeneratorWidget.cpp gui/ApplicationSettingsWidget.cpp @@ -137,7 +137,6 @@ set(keepassx_SOURCES gui/URLEdit.cpp gui/WelcomeWidget.cpp gui/csvImport/CsvImportWidget.cpp - gui/csvImport/CsvImportWizard.cpp gui/csvImport/CsvParserModel.cpp gui/entry/AutoTypeAssociationsModel.cpp gui/entry/EditEntryWidget.cpp @@ -180,6 +179,9 @@ set(keepassx_SOURCES gui/widgets/ElidedLabel.cpp gui/widgets/KPToolBar.cpp gui/widgets/PopupHelpWidget.cpp + gui/wizard/ImportWizard.cpp + gui/wizard/ImportWizardPageReview.cpp + gui/wizard/ImportWizardPageSelect.cpp gui/wizard/NewDatabaseWizard.cpp gui/wizard/NewDatabaseWizardPage.cpp gui/wizard/NewDatabaseWizardPageMetaData.cpp @@ -366,6 +368,7 @@ target_link_libraries(keepassx_core ${PCSC_LIBRARIES} ${ZXCVBN_LIBRARIES} ${ZLIB_LIBRARIES} + ${MINIZIP_LIBRARIES} ${ARGON2_LIBRARIES} ${thirdparty_LIBRARIES} ) diff --git a/src/autotype/AutoType.cpp b/src/autotype/AutoType.cpp index 7500539557..d530f09ed8 100644 --- a/src/autotype/AutoType.cpp +++ b/src/autotype/AutoType.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include "config-keepassx.h" diff --git a/src/format/BitwardenReader.cpp b/src/format/BitwardenReader.cpp new file mode 100644 index 0000000000..8f7a926fa1 --- /dev/null +++ b/src/format/BitwardenReader.cpp @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2023 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 "BitwardenReader.h" + +#include "core/Database.h" +#include "core/Entry.h" +#include "core/Group.h" +#include "core/Metadata.h" +#include "core/Tools.h" +#include "core/Totp.h" +#include "crypto/CryptoHash.h" +#include "crypto/SymmetricCipher.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + Entry* readItem(const QJsonObject& item, QString& folderId) + { + // Create the item map and extract the folder id + const auto itemMap = item.toVariantMap(); + folderId = itemMap.value("folderId").toString(); + + // Create entry and assign basic values + QScopedPointer entry(new Entry()); + entry->setUuid(QUuid::createUuid()); + entry->setTitle(itemMap.value("name").toString()); + entry->setNotes(itemMap.value("notes").toString()); + + if (itemMap.value("favorite").toBool()) { + entry->addTag(QObject::tr("Favorite", "Tag for favorite entries")); + } + + // Parse login details if present + if (itemMap.contains("login")) { + const auto loginMap = itemMap.value("login").toMap(); + entry->setUsername(loginMap.value("username").toString()); + entry->setPassword(loginMap.value("password").toString()); + if (loginMap.contains("totp")) { + // Bitwarden stores TOTP as otpauth string + entry->setTotp(Totp::parseSettings(loginMap.value("totp").toString())); + } + + // Set the entry url(s) + int i = 1; + for (const auto& urlObj : loginMap.value("uris").toList()) { + const auto url = urlObj.toMap().value("uri").toString(); + if (entry->url().isEmpty()) { + // First url encountered is set as the primary url + entry->setUrl(url); + } else { + // Subsequent urls + entry->attributes()->set( + QString("%1_%2").arg(EntryAttributes::AdditionalUrlAttribute, QString::number(i)), url); + ++i; + } + } + } + + // Parse identity details if present + if (itemMap.contains("identity")) { + const auto idMap = itemMap.value("identity").toMap(); + + // Combine name attributes + auto attrs = QStringList({idMap.value("title").toString(), + idMap.value("firstName").toString(), + idMap.value("middleName").toString(), + idMap.value("lastName").toString()}); + attrs.removeAll(""); + entry->attributes()->set("identity_name", attrs.join(" ")); + + // Combine all the address attributes + attrs = QStringList({idMap.value("address1").toString(), + idMap.value("address2").toString(), + idMap.value("address3").toString()}); + attrs.removeAll(""); + auto address = attrs.join("\n") + "\n" + idMap.value("city").toString() + ", " + + idMap.value("state").toString() + " " + idMap.value("postalCode").toString() + "\n" + + idMap.value("country").toString(); + entry->attributes()->set("identity_address", address); + + // Add the remaining attributes + attrs = QStringList({"company", "email", "phone", "ssn", "passportNumber", "licenseNumber"}); + const QStringList sensitive({"ssn", "passportNumber", "licenseNumber"}); + for (const auto& attr : attrs) { + const auto value = idMap.value(attr).toString(); + if (!value.isEmpty()) { + entry->attributes()->set("identity_" + attr, value, sensitive.contains(attr)); + } + } + + // Set the username or push it into attributes if already set + const auto username = idMap.value("username").toString(); + if (!username.isEmpty()) { + if (entry->username().isEmpty()) { + entry->setUsername(username); + } else { + entry->attributes()->set("identity_username", username); + } + } + } + + // Parse card details if present + if (itemMap.contains("card")) { + const auto cardMap = itemMap.value("card").toMap(); + const QStringList attrs({"cardholderName", "brand", "number", "expMonth", "expYear", "code"}); + const QStringList sensitive({"code"}); + for (const auto& attr : attrs) { + auto value = cardMap.value(attr).toString(); + if (!value.isEmpty()) { + entry->attributes()->set("card_" + attr, value, sensitive.contains(attr)); + } + } + } + + // Parse remaining fields + for (const auto& field : itemMap.value("fields").toList()) { + // Derive a prefix for attribute names using the title or uuid if missing + const auto fieldMap = field.toMap(); + auto name = fieldMap.value("name").toString(); + if (entry->attributes()->hasKey(name)) { + name = QString("%1_%2").arg(name, QUuid::createUuid().toString().mid(1, 5)); + } + + const auto value = fieldMap.value("value").toString(); + const auto type = fieldMap.value("type").toInt(); + + entry->attributes()->set(name, value, type == 1); + } + + // Collapse any accumulated history + entry->removeHistoryItems(entry->historyItems()); + + return entry.take(); + } + + void writeVaultToDatabase(const QJsonObject& vault, QSharedPointer db) + { + if (!vault.contains("folders") || !vault.contains("items")) { + // Early out if the vault is missing critical items + return; + } + + // Create groups from folders and store a temporary map of id -> uuid + QMap folderMap; + for (const auto& folder : vault.value("folders").toArray()) { + auto group = new Group(); + group->setUuid(QUuid::createUuid()); + group->setName(folder.toObject().value("name").toString()); + group->setParent(db->rootGroup()); + + folderMap.insert(folder.toObject().value("id").toString(), group); + } + + QString folderId; + const auto items = vault.value("items").toArray(); + for (const auto& item : items) { + auto entry = readItem(item.toObject(), folderId); + if (entry) { + entry->setGroup(folderMap.value(folderId, db->rootGroup()), false); + } + } + } +} // namespace + +bool BitwardenReader::hasError() +{ + return !m_error.isEmpty(); +} + +QString BitwardenReader::errorString() +{ + return m_error; +} + +QSharedPointer BitwardenReader::convert(const QString& path, const QString& password) +{ + m_error.clear(); + + QFileInfo fileinfo(path); + if (!fileinfo.exists()) { + m_error = QObject::tr("File does not exist.").arg(path); + return {}; + } + + // Bitwarden uses a json file format + QFile file(fileinfo.absoluteFilePath()); + if (!file.open(QFile::ReadOnly)) { + m_error = QObject::tr("Cannot open file: %1").arg(file.errorString()); + return {}; + } + + QJsonParseError error; + auto json = QJsonDocument::fromJson(file.readAll(), &error).object(); + if (error.error != QJsonParseError::NoError) { + m_error = + QObject::tr("Cannot parse file: %1 at position %2").arg(error.errorString(), QString::number(error.offset)); + return {}; + } + + file.close(); + + // Check if this is an encrypted json + if (json.contains("encrypted") && json.value("encrypted").toBool()) { + auto buildError = [](const QString& errorString) { + return QObject::tr("Failed to decrypt json file: %1").arg(errorString); + }; + + QByteArray key(32, '\0'); + auto salt = json.value("salt").toString().toUtf8(); + + auto pwd_fam = Botan::PasswordHashFamily::create_or_throw("PBKDF2(SHA-256)"); + auto kdf = Botan::KDF::create_or_throw("HKDF-Expand(SHA-256)"); + + // Derive the Master Key + auto pwd_hash = pwd_fam->from_params(json.value("kdfIterations").toInt()); + pwd_hash->derive_key(reinterpret_cast(key.data()), + key.size(), + password.toUtf8().data(), + password.toUtf8().size(), + reinterpret_cast(salt.data()), + salt.size()); + // Derive the MAC Key + auto stretched_mac = kdf->derive_key(32, reinterpret_cast(key.data()), key.size(), "", "mac"); + auto mac = QByteArray(reinterpret_cast(stretched_mac.data()), stretched_mac.size()); + + // Stretch the Master Key + auto stretched_key = kdf->derive_key(32, reinterpret_cast(key.data()), key.size(), "", "enc"); + key = QByteArray(reinterpret_cast(stretched_key.data()), stretched_key.size()); + + // Validate the encryption key + auto keyList = json.value("encKeyValidation_DO_NOT_EDIT").toString().split("."); + if (keyList.size() < 2) { + m_error = buildError(QObject::tr("Invalid encKeyValidation field")); + return {}; + } + auto cipherList = keyList[1].split("|"); + if (cipherList.size() < 3) { + m_error = buildError(QObject::tr("Invalid cipher list within encKeyValidation field")); + return {}; + } + CryptoHash hash(CryptoHash::Sha256, true); + hash.setKey(mac); + hash.addData(QByteArray::fromBase64(cipherList[0].toUtf8())); // iv + hash.addData(QByteArray::fromBase64(cipherList[1].toUtf8())); // ciphertext + if (hash.result().toBase64() != cipherList[2].toUtf8()) { + // Calculated MAC doesn't equal the Validation + m_error = buildError(QObject::tr("Wrong password")); + return {}; + } + + // Decrypt data field using AES-256-CBC + keyList = json.value("data").toString().split("."); + if (keyList.size() < 2) { + m_error = buildError(QObject::tr("Invalid encrypted data field")); + return {}; + } + cipherList = keyList[1].split("|"); + if (cipherList.size() < 2) { + m_error = buildError(QObject::tr("Invalid cipher list within encrypted data field")); + return {}; + } + auto iv = QByteArray::fromBase64(cipherList[0].toUtf8()); + auto data = QByteArray::fromBase64(cipherList[1].toUtf8()); + + SymmetricCipher cipher; + if (!cipher.init(SymmetricCipher::Aes256_CBC, SymmetricCipher::Decrypt, key, iv)) { + m_error = buildError(QObject::tr("Cannot initialize cipher")); + return {}; + } + if (!cipher.finish(data)) { + m_error = buildError(QObject::tr("Cannot decrypt data")); + return {}; + } + + json = QJsonDocument::fromJson(data, &error).object(); + if (error.error != QJsonParseError::NoError) { + m_error = buildError(error.errorString()); + return {}; + } + } + + auto db = QSharedPointer::create(); + db->rootGroup()->setName(QObject::tr("Bitwarden Import")); + + writeVaultToDatabase(json, db); + + return db; +} diff --git a/tests/TestOpVaultReader.h b/src/format/BitwardenReader.h similarity index 51% rename from tests/TestOpVaultReader.h rename to src/format/BitwardenReader.h index feb904c6d8..52e1e6f70c 100644 --- a/tests/TestOpVaultReader.h +++ b/src/format/BitwardenReader.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 KeePassXC Team + * Copyright (C) 2023 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 @@ -15,31 +15,29 @@ * along with this program. If not, see . */ -#ifndef TEST_OPVAULT_READER_H_ -#define TEST_OPVAULT_READER_H_ +#ifndef BITWARDEN_READER_H +#define BITWARDEN_READER_H -#include -#include +#include -class TestOpVaultReader : public QObject +class Database; + +/*! + * Imports a Bitwarden vault in JSON format: https://bitwarden.com/help/encrypted-export/ + */ +class BitwardenReader { - Q_OBJECT +public: + explicit BitwardenReader() = default; + ~BitwardenReader() = default; + + QSharedPointer convert(const QString& path, const QString& password = {}); -private slots: - void initTestCase(); - void testReadIntoDatabase(); + bool hasError(); + QString errorString(); private: - // absolute path to the .opvault directory - QString m_opVaultPath; - - /* - * Points to the file made by using the 1Password GUI to "Export all" - * to its text file format, which are almost key=value pairs - * except for multi-line strings. - */ - QString m_opVaultTextExportPath; - QStringList m_categories; + QString m_error; }; -#endif /* TEST_OPVAULT_READER_H_ */ +#endif // BITWARDEN_READER_H diff --git a/src/format/CsvParser.cpp b/src/format/CsvParser.cpp index a376e3662b..0e174fe350 100644 --- a/src/format/CsvParser.cpp +++ b/src/format/CsvParser.cpp @@ -24,20 +24,13 @@ #include "core/Tools.h" CsvParser::CsvParser() - : m_ch(0) - , m_comment('#') - , m_currCol(1) - , m_currRow(1) + : m_comment('#') , m_isBackslashSyntax(false) - , m_isEof(false) , m_isFileLoaded(false) - , m_isGood(true) - , m_lastPos(-1) - , m_maxCols(0) , m_qualifier('"') , m_separator(',') - , m_statusMsg("") { + reset(); m_csv.setBuffer(&m_array); m_ts.setDevice(&m_csv); m_csv.open(QIODevice::ReadOnly); @@ -105,10 +98,10 @@ void CsvParser::reset() m_isGood = true; m_lastPos = -1; m_maxCols = 0; - m_statusMsg = ""; + m_statusMsg.clear(); m_ts.seek(0); m_table.clear(); - // the following are users' concern :) + // the following can be overridden by the user // m_comment = '#'; // m_backslashSyntax = false; // m_comment = '#'; @@ -148,7 +141,7 @@ void CsvParser::parseRecord() do { parseField(row); getChar(m_ch); - } while (isSeparator(m_ch) && !m_isEof); + } while (m_ch == m_separator && !m_isEof); if (!m_isEof) { ungetChar(); @@ -168,7 +161,7 @@ void CsvParser::parseField(CsvRow& row) { QString field; peek(m_ch); - if (!isTerminator(m_ch)) { + if (m_ch != m_separator && m_ch != '\n' && m_ch != '\r') { if (isQualifier(m_ch)) { parseQuoted(field); } else { @@ -182,7 +175,7 @@ void CsvParser::parseSimple(QString& s) { QChar c; getChar(c); - while ((isText(c)) && (!m_isEof)) { + while (c != '\n' && c != m_separator && !m_isEof) { s.append(c); getChar(c); } @@ -215,7 +208,7 @@ void CsvParser::parseEscaped(QString& s) void CsvParser::parseEscapedText(QString& s) { getChar(m_ch); - while ((!isQualifier(m_ch)) && !m_isEof) { + while (!isQualifier(m_ch) && !m_isEof) { s.append(m_ch); getChar(m_ch); } @@ -223,10 +216,9 @@ void CsvParser::parseEscapedText(QString& s) bool CsvParser::processEscapeMark(QString& s, QChar c) { - QChar buf; - peek(buf); QChar c2; - if (true == m_isBackslashSyntax) { + peek(c2); + if (m_isBackslashSyntax) { // escape-character syntax, e.g. \" if (c != '\\') { return false; @@ -237,25 +229,24 @@ bool CsvParser::processEscapeMark(QString& s, QChar c) c2 = '\\'; s.append('\\'); return false; - } else { + } + s.append(c2); + return true; + } + + // double quote syntax, e.g. "" + if (!isQualifier(c)) { + return false; + } + peek(c2); + if (!m_isEof) { // not EOF, can read one char + if (isQualifier(c2)) { s.append(c2); + getChar(c2); return true; } - } else { - // double quote syntax, e.g. "" - if (!isQualifier(c)) { - return false; - } - peek(c2); - if (!m_isEof) { // not EOF, can read one char - if (isQualifier(c2)) { - s.append(c2); - getChar(c2); - return true; - } - } - return false; } + return false; } void CsvParser::fillColumns() @@ -282,7 +273,7 @@ void CsvParser::skipLine() bool CsvParser::skipEndline() { getChar(m_ch); - return (m_ch == '\n'); + return m_ch == '\n'; } void CsvParser::getChar(QChar& c) @@ -312,11 +303,10 @@ void CsvParser::peek(QChar& c) bool CsvParser::isQualifier(const QChar& c) const { - if (true == m_isBackslashSyntax && (c != m_qualifier)) { - return (c == '\\'); - } else { - return (c == m_qualifier); + if (m_isBackslashSyntax && c != m_qualifier) { + return c == '\\'; } + return c == m_qualifier; } bool CsvParser::isComment() @@ -327,7 +317,7 @@ bool CsvParser::isComment() do { getChar(c2); - } while ((isSpace(c2) || isTab(c2)) && (!m_isEof)); + } while ((c2 == ' ' || c2 == '\t') && !m_isEof); if (c2 == m_comment) { result = true; @@ -336,47 +326,16 @@ bool CsvParser::isComment() return result; } -bool CsvParser::isText(QChar c) const -{ - return !((isCRLF(c)) || (isSeparator(c))); -} - bool CsvParser::isEmptyRow(const CsvRow& row) const { - CsvRow::const_iterator it = row.constBegin(); - for (; it != row.constEnd(); ++it) { - if (((*it) != "\n") && ((*it) != "")) { + for (auto it = row.constBegin(); it != row.constEnd(); ++it) { + if (*it != "\n" && *it != "") { return false; } } return true; } -bool CsvParser::isCRLF(const QChar& c) const -{ - return (c == '\n'); -} - -bool CsvParser::isSpace(const QChar& c) const -{ - return (c == ' '); -} - -bool CsvParser::isTab(const QChar& c) const -{ - return (c == '\t'); -} - -bool CsvParser::isSeparator(const QChar& c) const -{ - return (c == m_separator); -} - -bool CsvParser::isTerminator(const QChar& c) const -{ - return (isSeparator(c) || (c == '\n') || (c == '\r')); -} - void CsvParser::setBackslashSyntax(bool set) { m_isBackslashSyntax = set; @@ -407,7 +366,7 @@ int CsvParser::getFileSize() const return m_csv.size(); } -const CsvTable CsvParser::getCsvTable() const +CsvTable CsvParser::getCsvTable() const { return m_table; } @@ -421,9 +380,8 @@ int CsvParser::getCsvCols() const { if (!m_table.isEmpty() && !m_table.at(0).isEmpty()) { return m_table.at(0).size(); - } else { - return 0; } + return 0; } int CsvParser::getCsvRows() const diff --git a/src/format/CsvParser.h b/src/format/CsvParser.h index 73fb13d24b..afba9688dd 100644 --- a/src/format/CsvParser.h +++ b/src/format/CsvParser.h @@ -47,7 +47,7 @@ class CsvParser int getCsvRows() const; int getCsvCols() const; QString getStatus() const; - const CsvTable getCsvTable() const; + CsvTable getCsvTable() const; protected: CsvTable m_table; @@ -74,15 +74,9 @@ class CsvParser void ungetChar(); void peek(QChar& c); void fillColumns(); - bool isTerminator(const QChar& c) const; - bool isSeparator(const QChar& c) const; bool isQualifier(const QChar& c) const; bool processEscapeMark(QString& s, QChar c); - bool isText(QChar c) const; bool isComment(); - bool isCRLF(const QChar& c) const; - bool isSpace(const QChar& c) const; - bool isTab(const QChar& c) const; bool isEmptyRow(const CsvRow& row) const; bool parseFile(); void parseRecord(); diff --git a/src/format/KeePass1Reader.cpp b/src/format/KeePass1Reader.cpp index 0b7f168a4c..1461b22c50 100644 --- a/src/format/KeePass1Reader.cpp +++ b/src/format/KeePass1Reader.cpp @@ -18,6 +18,7 @@ #include "KeePass1Reader.h" #include +#include #include #include "core/Endian.h" @@ -275,6 +276,10 @@ KeePass1Reader::readDatabase(const QString& filename, const QString& password, c return {}; } + if (db) { + db->metadata()->setName(QFileInfo(filename).completeBaseName()); + } + return db; } diff --git a/src/format/OPUXReader.cpp b/src/format/OPUXReader.cpp new file mode 100644 index 0000000000..d52aa4640c --- /dev/null +++ b/src/format/OPUXReader.cpp @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2023 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 "OPUXReader.h" + +#include "core/Database.h" +#include "core/Entry.h" +#include "core/Group.h" +#include "core/Metadata.h" +#include "core/Totp.h" + +#include +#include +#include +#include +#include +#include + +#include + +namespace +{ + QByteArray extractFile(unzFile uf, QString filename) + { + if (unzLocateFile(uf, filename.toLatin1(), 2) != UNZ_OK) { + qWarning("Failed to extract 1PUX document: %s", qPrintable(filename)); + return {}; + } + + // Read export.data into memory + int bytes, bytesRead = 0; + QByteArray data; + unzOpenCurrentFile(uf); + do { + data.resize(data.size() + 8192); + bytes = unzReadCurrentFile(uf, data.data() + bytesRead, 8192); + if (bytes > 0) { + bytesRead += bytes; + } + } while (bytes > 0); + unzCloseCurrentFile(uf); + data.truncate(bytesRead); + + return data; + } + + Entry* readItem(const QJsonObject& item, unzFile uf = nullptr) + { + const auto itemMap = item.toVariantMap(); + const auto overviewMap = itemMap.value("overview").toMap(); + const auto detailsMap = itemMap.value("details").toMap(); + + // Create entry and assign basic values + QScopedPointer entry(new Entry()); + entry->setUuid(QUuid::createUuid()); + entry->setTitle(overviewMap.value("title").toString()); + entry->setUrl(overviewMap.value("url").toString()); + if (overviewMap.contains("urls")) { + int i = 1; + for (const auto& urlRaw : overviewMap.value("urls").toList()) { + const auto urlMap = urlRaw.toMap(); + const auto url = urlMap.value("url").toString(); + if (entry->url() != url) { + entry->attributes()->set( + QString("%1_%2").arg(EntryAttributes::AdditionalUrlAttribute, QString::number(i)), url); + ++i; + } + } + } + if (overviewMap.contains("tags")) { + entry->setTags(overviewMap.value("tags").toStringList().join(",")); + } + if (itemMap.value("favIndex").toString() == "1") { + entry->addTag(QObject::tr("Favorite", "Tag for favorite entries")); + } + if (itemMap.value("state").toString() == "archived") { + entry->addTag(QObject::tr("Archived", "Tag for archived entries")); + } + + // Parse the details map by setting the username, password, and notes first + const auto loginFields = detailsMap.value("loginFields").toList(); + for (const auto& field : loginFields) { + const auto fieldMap = field.toMap(); + const auto designation = fieldMap.value("designation").toString(); + if (designation.compare("username", Qt::CaseInsensitive) == 0) { + entry->setUsername(fieldMap.value("value").toString()); + } else if (designation.compare("password", Qt::CaseInsensitive) == 0) { + entry->setPassword(fieldMap.value("value").toString()); + } + } + entry->setNotes(detailsMap.value("notesPlain").toString()); + + // Dive into the item sections to pull out advanced attributes + const auto sections = detailsMap.value("sections").toList(); + for (const auto& section : sections) { + // Derive a prefix for attribute names using the title or uuid if missing + const auto sectionMap = section.toMap(); + auto prefix = sectionMap.value("title").toString(); + if (prefix.isEmpty()) { + prefix = QUuid::createUuid().toString().mid(1, 5); + } + + for (const auto& field : sectionMap.value("fields").toList()) { + // Form the name of the attribute using the prefix and title or id + const auto fieldMap = field.toMap(); + auto name = fieldMap.value("title").toString(); + if (name.isEmpty()) { + name = fieldMap.value("id").toString(); + } + name = QString("%1_%2").arg(prefix, name); + + const auto valueMap = fieldMap.value("value").toMap(); + const auto key = valueMap.firstKey(); + if (key == "totp") { + // Build otpauth url + QUrl otpurl(QString("otpauth://totp/%1:%2?secret=%3") + .arg(entry->title(), entry->username(), valueMap.value(key).toString())); + + if (entry->hasTotp()) { + // Store multiple TOTP definitions as additional otp attributes + int i = 0; + name = "otp"; + const auto attributes = entry->attributes()->keys(); + while (attributes.contains(name)) { + name = QString("otp_%1").arg(++i); + } + entry->attributes()->set(name, otpurl.toEncoded(), true); + } else { + // First otp value encountered gets formal storage + entry->setTotp(Totp::parseSettings(otpurl.toEncoded())); + } + } else if (key == "file") { + // Add a file to the entry attachments + const auto fileMap = valueMap.value(key).toMap(); + const auto fileName = fileMap.value("fileName").toString(); + const auto docId = fileMap.value("documentId").toString(); + const auto data = extractFile(uf, QString("files/%1__%2").arg(docId, fileName)); + if (!data.isNull()) { + entry->attachments()->set(fileName, data); + } + } else { + auto value = valueMap.value(key).toString(); + if (key == "date") { + // Convert date fields from Unix time + value = QDateTime::fromSecsSinceEpoch(valueMap.value(key).toULongLong(), Qt::UTC).toString(); + } else if (key == "email") { + // Email address is buried in a sub-value + value = valueMap.value(key).toMap().value("email_address").toString(); + } else if (key == "address") { + // Combine all the address attributes into a fully formed structure + const auto address = valueMap.value(key).toMap(); + value = address.value("street").toString() + "\n" + address.value("city").toString() + ", " + + address.value("state").toString() + " " + address.value("zip").toString() + "\n" + + address.value("country").toString(); + } + + if (!value.isEmpty()) { + entry->attributes()->set(name, value, key == "concealed"); + } + } + } + } + + // Add a document attachment if defined + if (detailsMap.contains("documentAttributes")) { + const auto document = detailsMap.value("documentAttributes").toMap(); + const auto fileName = document.value("fileName").toString(); + const auto docId = document.value("documentId").toString(); + const auto data = extractFile(uf, QString("files/%1__%2").arg(docId, fileName)); + if (!data.isNull()) { + entry->attachments()->set(fileName, data); + } + } + + // Collapse any accumulated history + entry->removeHistoryItems(entry->historyItems()); + + // Adjust the created and modified times + auto timeInfo = entry->timeInfo(); + const auto createdTime = QDateTime::fromSecsSinceEpoch(itemMap.value("createdAt").toULongLong(), Qt::UTC); + const auto modifiedTime = QDateTime::fromSecsSinceEpoch(itemMap.value("updatedAt").toULongLong(), Qt::UTC); + timeInfo.setCreationTime(createdTime); + timeInfo.setLastModificationTime(modifiedTime); + timeInfo.setLastAccessTime(modifiedTime); + entry->setTimeInfo(timeInfo); + + return entry.take(); + } + + void writeVaultToDatabase(const QJsonObject& vault, QSharedPointer db, unzFile uf = nullptr) + { + if (!vault.contains("attrs") || !vault.contains("items")) { + // Early out if the vault is missing critical items + return; + } + + const auto attr = vault.value("attrs").toObject().toVariantMap(); + + // Create group and assign basic values + auto group = new Group(); + group->setUuid(QUuid::createUuid()); + group->setName(attr.value("name").toString()); + group->setParent(db->rootGroup()); + + const auto items = vault.value("items").toArray(); + for (const auto& item : items) { + auto entry = readItem(item.toObject(), uf); + if (entry) { + entry->setGroup(group, false); + } + } + + // Add the group icon if present + const auto icon = attr.value("avatar").toString(); + if (!icon.isEmpty()) { + auto data = extractFile(uf, QString("files/%1").arg(icon)); + if (!data.isNull()) { + const auto uuid = QUuid::createUuid(); + db->metadata()->addCustomIcon(uuid, data); + group->setIcon(uuid); + } + } + } +} // namespace + +bool OPUXReader::hasError() +{ + return !m_error.isEmpty(); +} + +QString OPUXReader::errorString() +{ + return m_error; +} + +QSharedPointer OPUXReader::convert(const QString& path) +{ + m_error.clear(); + + QFileInfo fileinfo(path); + if (!fileinfo.exists()) { + m_error = QObject::tr("File does not exist.").arg(path); + return {}; + } + + // 1PUX is a zip file format, open it and process the contents in memory + auto uf = unzOpen64(fileinfo.absoluteFilePath().toLatin1().constData()); + if (!uf) { + m_error = QObject::tr("Invalid 1PUX file format: Not a valid ZIP file."); + return {}; + } + + // Find the export.data file, if not found this isn't a 1PUX file + auto data = extractFile(uf, "export.data"); + if (data.isNull()) { + m_error = QObject::tr("Invalid 1PUX file format: Missing export.data"); + unzClose(uf); + return {}; + } + + auto db = QSharedPointer::create(); + db->rootGroup()->setName(QObject::tr("1Password Import")); + const auto json = QJsonDocument::fromJson(data); + + const auto account = json.object().value("accounts").toArray().first().toObject(); + const auto vaults = account.value("vaults").toArray(); + + for (const auto& vault : vaults) { + writeVaultToDatabase(vault.toObject(), db, uf); + } + + unzClose(uf); + return db; +} diff --git a/src/gui/OpVaultOpenWidget.h b/src/format/OPUXReader.h similarity index 58% rename from src/gui/OpVaultOpenWidget.h rename to src/format/OPUXReader.h index aed96caba6..e2779c8143 100644 --- a/src/gui/OpVaultOpenWidget.h +++ b/src/format/OPUXReader.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 KeePassXC Team + * Copyright (C) 2023 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 @@ -15,20 +15,29 @@ * along with this program. If not, see . */ -#ifndef KEEPASSXC_OPVAULTOPENWIDGET_H -#define KEEPASSXC_OPVAULTOPENWIDGET_H +#ifndef OPUX_READER_H +#define OPUX_READER_H -#include "gui/DatabaseOpenWidget.h" +#include -class OpVaultOpenWidget : public DatabaseOpenWidget -{ - Q_OBJECT +class Database; +/*! + * Imports a 1Password vault in 1PUX format: https://support.1password.com/1pux-format/ + */ +class OPUXReader +{ public: - explicit OpVaultOpenWidget(QWidget* parent = nullptr); + explicit OPUXReader() = default; + ~OPUXReader() = default; + + QSharedPointer convert(const QString& path); + + bool hasError(); + QString errorString(); -protected: - void openDatabase() override; +private: + QString m_error; }; -#endif // KEEPASSXC_OPVAULTOPENWIDGET_H +#endif // OPUX_READER_H diff --git a/src/format/OpVaultReader.cpp b/src/format/OpVaultReader.cpp index 201354bced..fa4034a005 100644 --- a/src/format/OpVaultReader.cpp +++ b/src/format/OpVaultReader.cpp @@ -31,7 +31,6 @@ OpVaultReader::OpVaultReader(QObject* parent) : QObject(parent) - , m_error(false) { } @@ -39,62 +38,44 @@ OpVaultReader::~OpVaultReader() { } -Database* OpVaultReader::readDatabase(QDir& opdataDir, const QString& password) +QSharedPointer OpVaultReader::convert(QDir& opdataDir, const QString& password) { if (!opdataDir.exists()) { - m_error = true; - m_errorStr = tr("Directory .opvault must exist"); - return nullptr; + m_error = tr("Directory .opvault must exist"); + return {}; } if (!opdataDir.isReadable()) { - m_error = true; - m_errorStr = tr("Directory .opvault must be readable"); - return nullptr; + m_error = tr("Directory .opvault must be readable"); + return {}; } // https://support.1password.com/opvault-design/#directory-layout QDir defaultDir = QDir(opdataDir); if (!defaultDir.cd("default")) { - m_error = true; - m_errorStr = tr("Directory .opvault/default must exist"); - return nullptr; + m_error = tr("Directory .opvault/default must exist"); + return {}; } if (!defaultDir.isReadable()) { - m_error = true; - m_errorStr = tr("Directory .opvault/default must be readable"); - return nullptr; + m_error = tr("Directory .opvault/default must be readable"); + return {}; } auto vaultName = opdataDir.dirName(); - auto key = QSharedPointer::create(); - key->addKey(QSharedPointer::create(password)); - - QScopedPointer db(new Database()); - db->setKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2D)); - db->setCipher(KeePass2::CIPHER_AES256); - db->setKey(key, true, false); - db->metadata()->setName(vaultName); - + auto db = QSharedPointer::create(); auto rootGroup = db->rootGroup(); - rootGroup->setTimeInfo({}); - rootGroup->setUpdateTimeinfo(false); rootGroup->setName(vaultName.remove(".opvault")); - rootGroup->setUuid(QUuid::createUuid()); populateCategoryGroups(rootGroup); QFile profileJsFile(defaultDir.absoluteFilePath("profile.js")); QJsonObject profileJson = readAndAssertJsonFile(profileJsFile, "var profile=", ";"); if (profileJson.isEmpty()) { - return nullptr; + return {}; } if (!processProfileJson(profileJson, password, rootGroup)) { zeroKeys(); - return nullptr; - } - if (profileJson.contains("uuid") and profileJson["uuid"].isString()) { - rootGroup->setUuid(Tools::hexToUuid(profileJson["uuid"].toString())); + return {}; } QFile foldersJsFile(defaultDir.filePath("folders.js")); @@ -102,7 +83,7 @@ Database* OpVaultReader::readDatabase(QDir& opdataDir, const QString& password) QJsonObject foldersJs = readAndAssertJsonFile(foldersJsFile, "loadFolders(", ");"); if (!processFolderJson(foldersJs, rootGroup)) { zeroKeys(); - return nullptr; + return {}; } } @@ -152,17 +133,17 @@ Database* OpVaultReader::readDatabase(QDir& opdataDir, const QString& password) } zeroKeys(); - return db.take(); + return db; } bool OpVaultReader::hasError() { - return m_error; + return !m_error.isEmpty(); } QString OpVaultReader::errorString() { - return m_errorStr; + return m_error; } bool OpVaultReader::processProfileJson(QJsonObject& profileJson, const QString& password, Group* rootGroup) @@ -184,38 +165,29 @@ bool OpVaultReader::processProfileJson(QJsonObject& profileJson, const QString& rootGroupTime.setLastModificationTime(QDateTime::fromTime_t(updatedAt, Qt::UTC)); rootGroup->setUuid(Tools::hexToUuid(profileJson["uuid"].toString())); - const auto derivedKeys = deriveKeysFromPassPhrase(salt, password, iterations); - if (derivedKeys->error) { - m_error = true; - m_errorStr = derivedKeys->errorStr; - delete derivedKeys; + QScopedPointer derivedKeys(deriveKeysFromPassPhrase(salt, password, iterations)); + if (!derivedKeys->error.isEmpty()) { + m_error = derivedKeys->error; return false; } QByteArray encKey = derivedKeys->encrypt; QByteArray hmacKey = derivedKeys->hmac; - delete derivedKeys; - auto masterKeys = decodeB64CompositeKeys(masterKeyB64, encKey, hmacKey); - if (masterKeys->error) { - m_error = true; - m_errorStr = masterKeys->errorStr; - delete masterKeys; + QScopedPointer masterKeys(decodeB64CompositeKeys(masterKeyB64, encKey, hmacKey)); + if (!masterKeys->error.isEmpty()) { + m_error = masterKeys->error; return false; } m_masterKey = masterKeys->encrypt; m_masterHmacKey = masterKeys->hmac; - delete masterKeys; - auto overviewKeys = decodeB64CompositeKeys(overviewKeyB64, encKey, hmacKey); - if (overviewKeys->error) { - m_error = true; - m_errorStr = overviewKeys->errorStr; - delete overviewKeys; + QScopedPointer overviewKeys(decodeB64CompositeKeys(overviewKeyB64, encKey, hmacKey)); + if (!overviewKeys->error.isEmpty()) { + m_error = overviewKeys->error; return false; } m_overviewKey = overviewKeys->encrypt; m_overviewHmacKey = overviewKeys->hmac; - delete overviewKeys; return true; } @@ -340,15 +312,13 @@ QJsonObject OpVaultReader::readAndAssertJsonFile(QFile& file, const QString& str OpVaultReader::DerivedKeyHMAC* OpVaultReader::decodeB64CompositeKeys(const QString& b64, const QByteArray& encKey, const QByteArray& hmacKey) { - auto result = new DerivedKeyHMAC(); OpData01 keyKey01; if (!keyKey01.decodeBase64(b64, encKey, hmacKey)) { - result->error = true; - result->errorStr = tr("Unable to decode masterKey: %1").arg(keyKey01.errorString()); + auto result = new DerivedKeyHMAC(); + result->error = tr("Unable to decode masterKey: %1").arg(keyKey01.errorString()); return result; } - delete result; const QByteArray keyKey = keyKey01.getClearText(); @@ -366,7 +336,6 @@ OpVaultReader::decodeB64CompositeKeys(const QString& b64, const QByteArray& encK OpVaultReader::DerivedKeyHMAC* OpVaultReader::decodeCompositeKeys(const QByteArray& keyKey) { auto result = new DerivedKeyHMAC; - result->error = false; auto digest = CryptoHash::hash(keyKey, CryptoHash::Sha512); result->encrypt = digest.left(32); @@ -385,7 +354,6 @@ OpVaultReader::DerivedKeyHMAC* OpVaultReader::deriveKeysFromPassPhrase(QByteArray& salt, const QString& password, unsigned long iterations) { auto result = new DerivedKeyHMAC; - result->error = false; QByteArray out(64, '\0'); try { @@ -397,8 +365,7 @@ OpVaultReader::deriveKeysFromPassPhrase(QByteArray& salt, const QString& passwor reinterpret_cast(salt.constData()), salt.size()); } catch (std::exception& e) { - result->error = true; - result->errorStr = tr("Unable to derive master key: %1").arg(e.what()); + result->error = tr("Unable to derive master key: %1").arg(e.what()); return result; } diff --git a/src/format/OpVaultReader.h b/src/format/OpVaultReader.h index 0dff45c2dd..50ccf0b96a 100644 --- a/src/format/OpVaultReader.h +++ b/src/format/OpVaultReader.h @@ -39,7 +39,7 @@ class OpVaultReader : public QObject explicit OpVaultReader(QObject* parent = nullptr); ~OpVaultReader() override; - Database* readDatabase(QDir& opdataDir, const QString& password); + QSharedPointer convert(QDir& opdataDir, const QString& password); bool hasError(); QString errorString(); @@ -49,8 +49,7 @@ class OpVaultReader : public QObject { QByteArray encrypt; QByteArray hmac; - bool error; - QString errorStr; + QString error; }; QJsonObject readAndAssertJsonFile(QFile& file, const QString& stripLeading, const QString& stripTrailing); @@ -106,15 +105,14 @@ class OpVaultReader : public QObject /*! Used to blank the memory after the keys have been used. */ void zeroKeys(); - bool m_error; - QString m_errorStr; + QString m_error; QByteArray m_masterKey; QByteArray m_masterHmacKey; /*! Used to decrypt overview text, such as folder names. */ QByteArray m_overviewKey; QByteArray m_overviewHmacKey; - friend class TestOpVaultReader; + friend class TestImports; }; #endif /* OPVAULT_READER_H_ */ diff --git a/src/format/OpVaultReaderAttachments.cpp b/src/format/OpVaultReaderAttachments.cpp index 7c65b7f5a0..c9dc6c76ee 100644 --- a/src/format/OpVaultReaderAttachments.cpp +++ b/src/format/OpVaultReaderAttachments.cpp @@ -229,6 +229,10 @@ void OpVaultReader::fillAttachment(Entry* entry, qWarning() << QString("Unexpected type of attachment \"filename\": %1").arg(attFilename.type()); } } + if (entry->attachments()->hasKey(attachKey)) { + // Prepend a random string to the attachment name to avoid collisions + attachKey.prepend(QString("%1_").arg(QUuid::createUuid().toString().mid(1, 5))); + } entry->attachments()->set(attachKey, attachPayload); } diff --git a/src/format/OpVaultReaderSections.cpp b/src/format/OpVaultReaderSections.cpp index d05f8fca12..42da14bb62 100644 --- a/src/format/OpVaultReaderSections.cpp +++ b/src/format/OpVaultReaderSections.cpp @@ -92,7 +92,7 @@ void OpVaultReader::fillFromSectionField(Entry* entry, const QString& sectionNam while (attributes.contains(name)) { name = QString("otp_%1").arg(++i); } - entry->attributes()->set(name, attrValue); + entry->attributes()->set(name, attrValue, true); } else if (attrValue.startsWith("otpauth://")) { QUrlQuery query(attrValue); // at least as of 1Password 7, they don't append the digits= and period= which totp.cpp requires @@ -128,10 +128,14 @@ void OpVaultReader::fillFromSectionField(Entry* entry, const QString& sectionNam } else if (kind == "address") { // Expand address into multiple attributes auto addrFields = field.value("v").toObject().toVariantMap(); - for (auto part : addrFields.keys()) { + for (auto& part : addrFields.keys()) { entry->attributes()->set(attrName + QString("_%1").arg(part), addrFields.value(part).toString()); } } else { + if (entry->attributes()->hasKey(attrName)) { + // Append a random string to the attribute name to avoid collisions + attrName += QString("_%1").arg(QUuid::createUuid().toString().mid(1, 5)); + } entry->attributes()->set(attrName, attrValue, (kind == "password" || kind == "concealed")); } } diff --git a/src/gui/CloneDialog.h b/src/gui/CloneDialog.h index 4f72e9011d..649ab1d5f0 100644 --- a/src/gui/CloneDialog.h +++ b/src/gui/CloneDialog.h @@ -18,6 +18,8 @@ #ifndef KEEPASSX_CLONEDIALOG_H #define KEEPASSX_CLONEDIALOG_H +#include + #include "core/Database.h" #include "gui/DatabaseWidget.h" diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index 1b9802699a..949c30513d 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -21,6 +21,7 @@ #include #include "autotype/AutoType.h" +#include "core/Merger.h" #include "core/Tools.h" #include "format/CsvExporter.h" #include "gui/Clipboard.h" @@ -28,13 +29,13 @@ #include "gui/DatabaseWidget.h" #include "gui/DatabaseWidgetStateSync.h" #include "gui/FileDialog.h" -#include "gui/HtmlExporter.h" #include "gui/MessageBox.h" #include "gui/export/ExportDialog.h" #ifdef Q_OS_MACOS #include "gui/osutils/macutils/MacUtils.h" #endif #include "gui/wizard/NewDatabaseWizard.h" +#include "wizard/ImportWizard.h" DatabaseTabWidget::DatabaseTabWidget(QWidget* parent) : QTabWidget(parent) @@ -252,24 +253,52 @@ void DatabaseTabWidget::addDatabaseTab(DatabaseWidget* dbWidget, bool inBackgrou connect(dbWidget, SIGNAL(databaseLocked()), SLOT(emitDatabaseLockChanged())); } -void DatabaseTabWidget::importCsv() +DatabaseWidget* DatabaseTabWidget::importFile() { - auto filter = QString("%1 (*.csv);;%2 (*)").arg(tr("CSV file"), tr("All files")); - auto fileName = fileDialog()->getOpenFileName(this, tr("Select CSV file"), FileDialog::getLastDir("csv"), filter); - if (fileName.isEmpty()) { - return; + // Show the import wizard + QScopedPointer wizard(new ImportWizard(this)); + if (!wizard->exec()) { + return nullptr; } - FileDialog::saveLastDir("csv", fileName, true); - - auto db = execNewDatabaseWizard(); + auto db = wizard->database(); if (!db) { - return; + // Import wizard was cancelled + return nullptr; } - auto* dbWidget = new DatabaseWidget(db, this); - addDatabaseTab(dbWidget); - dbWidget->switchToCsvImport(fileName); + auto importInto = wizard->importInto(); + if (importInto.first.isNull()) { + // Start the new database wizard with the imported database + auto newDb = execNewDatabaseWizard(); + if (newDb) { + // Merge the imported db into the new one + Merger merger(db.data(), newDb.data()); + merger.merge(); + // Show the new database + auto dbWidget = new DatabaseWidget(newDb, this); + addDatabaseTab(dbWidget); + newDb->markAsModified(); + return dbWidget; + } + } else { + for (int i = 0, c = count(); i < c; ++i) { + // Find the database and group to import into based on import wizard choice + auto dbWidget = databaseWidgetFromIndex(i); + if (!dbWidget->isLocked() && dbWidget->database()->uuid() == importInto.first) { + auto group = dbWidget->database()->rootGroup()->findGroupByUuid(importInto.second); + if (group) { + // Extract the root group from the import database + auto importGroup = db->setRootGroup(new Group()); + importGroup->setParent(group); + setCurrentIndex(i); + return dbWidget; + } + } + } + } + + return nullptr; } void DatabaseTabWidget::mergeDatabase() @@ -291,44 +320,6 @@ void DatabaseTabWidget::mergeDatabase(const QString& filePath) unlockDatabaseInDialog(currentDatabaseWidget(), DatabaseOpenDialog::Intent::Merge, filePath); } -void DatabaseTabWidget::importKeePass1Database() -{ - auto filter = QString("%1 (*.kdb);;%2 (*)").arg(tr("KeePass 1 database"), tr("All files")); - auto fileName = - fileDialog()->getOpenFileName(this, tr("Open KeePass 1 database"), FileDialog::getLastDir("kp1"), filter); - if (fileName.isEmpty()) { - return; - } - - FileDialog::saveLastDir("kp1", fileName, true); - - auto db = QSharedPointer::create(); - auto* dbWidget = new DatabaseWidget(db, this); - addDatabaseTab(dbWidget); - dbWidget->switchToImportKeepass1(fileName); -} - -void DatabaseTabWidget::importOpVaultDatabase() -{ - auto defaultDir = FileDialog::getLastDir("opvault"); -#ifdef Q_OS_MACOS - QString fileName = fileDialog()->getOpenFileName(this, tr("Open OPVault"), defaultDir, "OPVault (*.opvault)"); -#else - QString fileName = fileDialog()->getExistingDirectory(this, tr("Open OPVault"), defaultDir); -#endif - - if (fileName.isEmpty()) { - return; - } - - FileDialog::saveLastDir("opvault", fileName); - - auto db = QSharedPointer::create(); - auto* dbWidget = new DatabaseWidget(db, this); - addDatabaseTab(dbWidget); - dbWidget->switchToImportOpVault(fileName); -} - /** * Attempt to close the current database and remove its tab afterwards. * @@ -613,43 +604,18 @@ bool DatabaseTabWidget::hasLockableDatabases() const */ QString DatabaseTabWidget::tabName(int index) { - if (index == -1 || index > count()) { - return ""; - } - - auto* dbWidget = databaseWidgetFromIndex(index); - - auto db = dbWidget->database(); - Q_ASSERT(db); - if (!db) { - return ""; + auto dbWidget = databaseWidgetFromIndex(index); + if (!dbWidget) { + return {}; } - QString tabName; - - if (!db->filePath().isEmpty()) { - QFileInfo fileInfo(db->filePath()); - - if (db->metadata()->name().isEmpty()) { - tabName = fileInfo.fileName(); - } else { - tabName = db->metadata()->name(); - } - - setTabToolTip(index, fileInfo.absoluteFilePath()); - } else { - if (db->metadata()->name().isEmpty()) { - tabName = tr("New Database"); - } else { - tabName = tr("%1 [New Database]", "Database tab name modifier").arg(db->metadata()->name()); - } - } + auto tabName = dbWidget->displayName(); if (dbWidget->isLocked()) { tabName = tr("%1 [Locked]", "Database tab name modifier").arg(tabName); } - if (db->isModified()) { + if (dbWidget->database()->isModified()) { tabName.append("*"); } @@ -672,6 +638,7 @@ void DatabaseTabWidget::updateTabName(int index) } index = indexOf(dbWidget); setTabText(index, tabName(index)); + setTabToolTip(index, dbWidget->displayFilePath()); emit tabNameChanged(); } diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h index 8f27038786..5c995b536a 100644 --- a/src/gui/DatabaseTabWidget.h +++ b/src/gui/DatabaseTabWidget.h @@ -64,9 +64,7 @@ public slots: DatabaseWidget* newDatabase(); void openDatabase(); void mergeDatabase(); - void importCsv(); - void importKeePass1Database(); - void importOpVaultDatabase(); + DatabaseWidget* importFile(); bool saveDatabase(int index = -1); bool saveDatabaseAs(int index = -1); bool saveDatabaseBackup(int index = -1); diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index deeec3662f..d51e9483d9 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -30,20 +30,20 @@ #include #include #include -#include #include "autotype/AutoType.h" #include "core/EntrySearcher.h" #include "core/Merger.h" +#include "core/Tools.h" #include "gui/Clipboard.h" #include "gui/CloneDialog.h" +#include "gui/DatabaseOpenDialog.h" +#include "gui/DatabaseOpenWidget.h" #include "gui/EntryPreviewWidget.h" #include "gui/FileDialog.h" #include "gui/GuiTools.h" -#include "gui/KeePass1OpenWidget.h" #include "gui/MainWindow.h" #include "gui/MessageBox.h" -#include "gui/OpVaultOpenWidget.h" #include "gui/TotpDialog.h" #include "gui/TotpExportSettingsDialog.h" #include "gui/TotpSetupDialog.h" @@ -79,15 +79,12 @@ DatabaseWidget::DatabaseWidget(QSharedPointer db, QWidget* parent) , m_previewSplitter(new QSplitter(m_mainWidget)) , m_searchingLabel(new QLabel(this)) , m_shareLabel(new ElidedLabel(this)) - , m_csvImportWizard(new CsvImportWizard(this)) , m_editEntryWidget(new EditEntryWidget(this)) , m_editGroupWidget(new EditGroupWidget(this)) , m_historyEditEntryWidget(new EditEntryWidget(this)) , m_reportsDialog(new ReportsDialog(this)) , m_databaseSettingDialog(new DatabaseSettingsDialog(this)) , m_databaseOpenWidget(new DatabaseOpenWidget(this)) - , m_keepass1OpenWidget(new KeePass1OpenWidget(this)) - , m_opVaultOpenWidget(new OpVaultOpenWidget(this)) , m_groupView(new GroupView(m_db.data(), this)) , m_tagView(new TagView(this)) , m_saveAttempts(0) @@ -179,12 +176,9 @@ DatabaseWidget::DatabaseWidget(QSharedPointer db, QWidget* parent) m_editEntryWidget->setObjectName("editEntryWidget"); m_editGroupWidget->setObjectName("editGroupWidget"); - m_csvImportWizard->setObjectName("csvImportWizard"); m_reportsDialog->setObjectName("reportsDialog"); m_databaseSettingDialog->setObjectName("databaseSettingsDialog"); m_databaseOpenWidget->setObjectName("databaseOpenWidget"); - m_keepass1OpenWidget->setObjectName("keepass1OpenWidget"); - m_opVaultOpenWidget->setObjectName("opVaultOpenWidget"); addChildWidget(m_mainWidget); addChildWidget(m_editEntryWidget); @@ -193,9 +187,6 @@ DatabaseWidget::DatabaseWidget(QSharedPointer db, QWidget* parent) addChildWidget(m_databaseSettingDialog); addChildWidget(m_historyEditEntryWidget); addChildWidget(m_databaseOpenWidget); - addChildWidget(m_csvImportWizard); - addChildWidget(m_keepass1OpenWidget); - addChildWidget(m_opVaultOpenWidget); // clang-format off connect(m_mainSplitter, SIGNAL(splitterMoved(int,int)), SIGNAL(splitterSizesChanged())); @@ -216,9 +207,6 @@ DatabaseWidget::DatabaseWidget(QSharedPointer db, QWidget* parent) connect(m_reportsDialog, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool))); connect(m_databaseSettingDialog, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool))); connect(m_databaseOpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool))); - connect(m_keepass1OpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool))); - connect(m_opVaultOpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool))); - connect(m_csvImportWizard, SIGNAL(importFinished(bool)), SLOT(csvImportFinished(bool))); connect(this, SIGNAL(currentChanged(int)), SLOT(emitCurrentModeChanged())); connect(this, SIGNAL(requestGlobalAutoType(const QString&)), parent, SLOT(performGlobalAutoType(const QString&))); // clang-format on @@ -269,10 +257,8 @@ DatabaseWidget::Mode DatabaseWidget::currentMode() const return Mode::None; } else if (currentWidget() == m_mainWidget) { return Mode::ViewMode; - } else if (currentWidget() == m_databaseOpenWidget || currentWidget() == m_keepass1OpenWidget) { + } else if (currentWidget() == m_databaseOpenWidget) { return Mode::LockedMode; - } else if (currentWidget() == m_csvImportWizard) { - return Mode::ImportMode; } else { return Mode::EditMode; } @@ -323,6 +309,45 @@ bool DatabaseWidget::isEditWidgetModified() const return false; } +QString DatabaseWidget::displayName() const +{ + if (!m_db) { + return {}; + } + + auto displayName = m_db->metadata()->name(); + if (!m_db->filePath().isEmpty()) { + if (displayName.isEmpty()) { + displayName = displayFileName(); + } + } else { + if (displayName.isEmpty()) { + displayName = tr("New Database"); + } else { + displayName = tr("%1 [New Database]", "Database tab name modifier").arg(displayName); + } + } + + return displayName; +} + +QString DatabaseWidget::displayFileName() const +{ + if (m_db) { + QFileInfo fileinfo(m_db->filePath()); + return fileinfo.fileName(); + } + return {}; +} + +QString DatabaseWidget::displayFilePath() const +{ + if (m_db) { + return m_db->canonicalFilePath(); + } + return {}; +} + QHash> DatabaseWidget::splitterSizes() const { return {{Config::GUI_SplitterState, m_mainSplitter->sizes()}, @@ -1337,33 +1362,6 @@ void DatabaseWidget::switchToOpenDatabase(const QString& filePath, const QString m_databaseOpenWidget->enterKey(password, keyFile); } -void DatabaseWidget::switchToCsvImport(const QString& filePath) -{ - setCurrentWidget(m_csvImportWizard); - m_csvImportWizard->load(filePath, m_db.data()); -} - -void DatabaseWidget::csvImportFinished(bool accepted) -{ - if (!accepted) { - emit closeRequest(); - } else { - switchToMainView(); - } -} - -void DatabaseWidget::switchToImportKeepass1(const QString& filePath) -{ - m_keepass1OpenWidget->load(filePath); - setCurrentWidget(m_keepass1OpenWidget); -} - -void DatabaseWidget::switchToImportOpVault(const QString& fileName) -{ - m_opVaultOpenWidget->load(fileName); - setCurrentWidget(m_opVaultOpenWidget); -} - void DatabaseWidget::switchToEntryEdit() { auto entry = m_entryView->currentEntry(); diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index 77fa3d7f4c..571db44a46 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -19,35 +19,29 @@ #ifndef KEEPASSX_DATABASEWIDGET_H #define KEEPASSX_DATABASEWIDGET_H -#include -#include +#include #include -#include "DatabaseOpenDialog.h" -#include "config-keepassx.h" +#include "core/Database.h" +#include "core/Group.h" +#include "core/Metadata.h" #include "gui/MessageWidget.h" -#include "gui/csvImport/CsvImportWizard.h" #include "gui/entry/EntryModel.h" +class DatabaseOpenDialog; class DatabaseOpenWidget; -class KeePass1OpenWidget; -class OpVaultOpenWidget; class DatabaseSettingsDialog; class ReportsDialog; -class Database; class FileWatcher; class EditEntryWidget; class EditGroupWidget; -class Entry; class EntryView; class EntrySearcher; -class Group; class GroupView; class QFile; class QMenu; class QSplitter; class QLabel; -class MessageWidget; class EntryPreviewWidget; class TagView; class ElidedLabel; @@ -67,7 +61,6 @@ class DatabaseWidget : public QStackedWidget enum class Mode { None, - ImportMode, ViewMode, EditMode, LockedMode @@ -104,6 +97,10 @@ class DatabaseWidget : public QStackedWidget int numberOfSelectedEntries() const; int currentEntryIndex() const; + QString displayName() const; + QString displayFileName() const; + QString displayFilePath() const; + QStringList customEntryAttributes() const; bool isEditWidgetModified() const; void clearAllWidgets(); @@ -219,11 +216,7 @@ public slots: void switchToOpenDatabase(); void switchToOpenDatabase(const QString& filePath); void switchToOpenDatabase(const QString& filePath, const QString& password, const QString& keyFile); - void switchToCsvImport(const QString& filePath); void performUnlockDatabase(const QString& password, const QString& keyfile = {}); - void csvImportFinished(bool accepted); - void switchToImportKeepass1(const QString& filePath); - void switchToImportOpVault(const QString& fileName); void emptyRecycleBin(); // Search related slots @@ -286,15 +279,12 @@ private slots: QPointer m_previewSplitter; QPointer m_searchingLabel; QPointer m_shareLabel; - QPointer m_csvImportWizard; QPointer m_editEntryWidget; QPointer m_editGroupWidget; QPointer m_historyEditEntryWidget; QPointer m_reportsDialog; QPointer m_databaseSettingDialog; QPointer m_databaseOpenWidget; - QPointer m_keepass1OpenWidget; - QPointer m_opVaultOpenWidget; QPointer m_groupView; QPointer m_tagView; QPointer m_entryView; diff --git a/src/gui/EntryPreviewWidget.cpp b/src/gui/EntryPreviewWidget.cpp index b7c8ca1bcc..3ae0dc08b1 100644 --- a/src/gui/EntryPreviewWidget.cpp +++ b/src/gui/EntryPreviewWidget.cpp @@ -31,7 +31,7 @@ #endif #include - +#include namespace { constexpr int GeneralTabIndex = 0; diff --git a/src/gui/EntryPreviewWidget.h b/src/gui/EntryPreviewWidget.h index f4ea8be665..83af07ee97 100644 --- a/src/gui/EntryPreviewWidget.h +++ b/src/gui/EntryPreviewWidget.h @@ -26,6 +26,7 @@ namespace Ui class EntryPreviewWidget; } +class QTabWidget; class QTextEdit; class EntryPreviewWidget : public QWidget diff --git a/src/gui/Icons.cpp b/src/gui/Icons.cpp index 50b9c1e7d7..194c6625d9 100644 --- a/src/gui/Icons.cpp +++ b/src/gui/Icons.cpp @@ -18,6 +18,7 @@ #include "Icons.h" +#include #include #include #include @@ -25,6 +26,7 @@ #include "config-keepassx.h" #include "core/Config.h" +#include "core/Database.h" #include "gui/DatabaseIcons.h" #include "gui/MainWindow.h" #include "gui/osutils/OSUtils.h" diff --git a/src/gui/KeePass1OpenWidget.cpp b/src/gui/KeePass1OpenWidget.cpp deleted file mode 100644 index 9a485a7f1f..0000000000 --- a/src/gui/KeePass1OpenWidget.cpp +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2012 Felix Geyer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 2 or (at your option) - * version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "KeePass1OpenWidget.h" -#include "ui_DatabaseOpenWidget.h" - -#include - -#include "core/Database.h" -#include "core/Metadata.h" -#include "format/KeePass1Reader.h" - -KeePass1OpenWidget::KeePass1OpenWidget(QWidget* parent) - : DatabaseOpenWidget(parent) -{ - m_ui->labelHeadline->setText(tr("Import KeePass1 Database")); -} - -void KeePass1OpenWidget::openDatabase() -{ - KeePass1Reader reader; - - QString password; - QString keyFileName = m_ui->keyFileLineEdit->text(); - - if (!m_ui->editPassword->text().isEmpty() || m_retryUnlockWithEmptyPassword) { - password = m_ui->editPassword->text(); - } - - QFile file(m_filename); - if (!file.open(QIODevice::ReadOnly)) { - m_ui->messageWidget->showMessage(tr("Unable to open the database.").append("\n").append(file.errorString()), - MessageWidget::Error); - return; - } - - QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); - m_db = reader.readDatabase(&file, password, keyFileName); - QApplication::restoreOverrideCursor(); - - if (m_db) { - m_db->metadata()->setName(QFileInfo(m_filename).completeBaseName()); - emit dialogFinished(true); - clearForms(); - } else { - m_ui->messageWidget->showMessage(tr("Unable to open the database.").append("\n").append(reader.errorString()), - MessageWidget::Error); - } -} diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index d08c709d61..845072ccb7 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -388,7 +388,7 @@ MainWindow::MainWindow() m_ui->actionLockAllDatabases->setIcon(icons()->icon("database-lock-all")); m_ui->actionQuit->setIcon(icons()->icon("application-exit")); m_ui->actionDatabaseMerge->setIcon(icons()->icon("database-merge")); - m_ui->menuImport->setIcon(icons()->icon("document-import")); + m_ui->actionImport->setIcon(icons()->icon("document-import")); m_ui->menuExport->setIcon(icons()->icon("document-export")); m_ui->actionEntryNew->setIcon(icons()->icon("entry-new")); @@ -498,9 +498,7 @@ MainWindow::MainWindow() connect(m_ui->actionImportPasskey, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importPasskey())); connect(m_ui->actionEntryImportPasskey, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importPasskeyToEntry())); #endif - connect(m_ui->actionImportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importCsv())); - connect(m_ui->actionImportKeePass1, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importKeePass1Database())); - connect(m_ui->actionImportOpVault, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importOpVaultDatabase())); + connect(m_ui->actionImport, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importFile())); connect(m_ui->actionExportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(exportToCsv())); connect(m_ui->actionExportHtml, SIGNAL(triggered()), m_ui->tabWidget, SLOT(exportToHtml())); connect(m_ui->actionExportXML, SIGNAL(triggered()), m_ui->tabWidget, SLOT(exportToXML())); @@ -566,9 +564,7 @@ MainWindow::MainWindow() connect(m_ui->welcomeWidget, SIGNAL(newDatabase()), SLOT(switchToNewDatabase())); connect(m_ui->welcomeWidget, SIGNAL(openDatabase()), SLOT(switchToOpenDatabase())); connect(m_ui->welcomeWidget, SIGNAL(openDatabaseFile(QString)), SLOT(switchToDatabaseFile(QString))); - connect(m_ui->welcomeWidget, SIGNAL(importKeePass1Database()), SLOT(switchToKeePass1Database())); - connect(m_ui->welcomeWidget, SIGNAL(importOpVaultDatabase()), SLOT(switchToOpVaultDatabase())); - connect(m_ui->welcomeWidget, SIGNAL(importCsv()), SLOT(switchToCsvImport())); + connect(m_ui->welcomeWidget, SIGNAL(importFile()), m_ui->tabWidget, SLOT(importFile())); connect(m_ui->actionAbout, SIGNAL(triggered()), SLOT(showAboutDialog())); connect(m_ui->actionDonate, SIGNAL(triggered()), SLOT(openDonateUrl())); @@ -897,7 +893,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) m_ui->actionDatabaseNew->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); m_ui->actionDatabaseOpen->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); m_ui->menuRecentDatabases->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); - m_ui->menuImport->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); + m_ui->actionImport->setEnabled(inDatabaseTabWidgetOrWelcomeWidget); m_ui->actionLockDatabase->setEnabled(m_ui->tabWidget->hasLockableDatabases()); m_ui->actionLockDatabaseToolbar->setEnabled(m_ui->tabWidget->hasLockableDatabases()); m_ui->actionLockAllDatabases->setEnabled(m_ui->tabWidget->hasLockableDatabases()); @@ -1011,7 +1007,6 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) break; } case DatabaseWidget::Mode::EditMode: - case DatabaseWidget::Mode::ImportMode: case DatabaseWidget::Mode::LockedMode: { // Enable select actions when editing an entry bool editEntryActive = dbWidget->isEntryEditActive(); @@ -1325,24 +1320,6 @@ void MainWindow::switchToDatabaseFile(const QString& file) switchToDatabases(); } -void MainWindow::switchToKeePass1Database() -{ - m_ui->tabWidget->importKeePass1Database(); - switchToDatabases(); -} - -void MainWindow::switchToOpVaultDatabase() -{ - m_ui->tabWidget->importOpVaultDatabase(); - switchToDatabases(); -} - -void MainWindow::switchToCsvImport() -{ - m_ui->tabWidget->importCsv(); - switchToDatabases(); -} - void MainWindow::databaseStatusChanged(DatabaseWidget* dbWidget) { Q_UNUSED(dbWidget); diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 2e548abc2e..1f96a6ec07 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -24,6 +24,7 @@ #include #include #include +#include #include "core/SignalMultiplexer.h" #include "gui/DatabaseWidget.h" @@ -124,9 +125,6 @@ private slots: void switchToNewDatabase(); void switchToOpenDatabase(); void switchToDatabaseFile(const QString& file); - void switchToKeePass1Database(); - void switchToOpVaultDatabase(); - void switchToCsvImport(); void databaseStatusChanged(DatabaseWidget* dbWidget); void databaseTabChanged(int tabIndex); void openRecentDatabase(QAction* action); diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index a0b8d6e687..48574049eb 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -231,14 +231,6 @@ &Recent Databases - - - &Import - - - - - &Export @@ -266,7 +258,7 @@ - + @@ -1163,6 +1155,19 @@ Allow Screen Capture + + + 1Password 1PUX... + + + Import a 1Password 1PUX file + + + + + Import… + + diff --git a/src/gui/OpVaultOpenWidget.cpp b/src/gui/OpVaultOpenWidget.cpp deleted file mode 100644 index cae569cf20..0000000000 --- a/src/gui/OpVaultOpenWidget.cpp +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2019 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 "OpVaultOpenWidget.h" - -#include "core/Database.h" -#include "format/OpVaultReader.h" -#include "ui_DatabaseOpenWidget.h" - -OpVaultOpenWidget::OpVaultOpenWidget(QWidget* parent) - : DatabaseOpenWidget(parent) -{ - m_ui->labelHeadline->setText("Import 1Password Database"); -} - -void OpVaultOpenWidget::openDatabase() -{ - OpVaultReader reader; - - QString password; - password = m_ui->editPassword->text(); - - QDir opVaultDir(m_filename); - - QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); - m_db.reset(reader.readDatabase(opVaultDir, password)); - QApplication::restoreOverrideCursor(); - - if (m_db) { - emit dialogFinished(true); - } else { - m_ui->messageWidget->showMessage(tr("Read Database did not produce an instance\n%1").arg(reader.errorString()), - MessageWidget::Error); - m_ui->editPassword->clear(); - } -} diff --git a/src/gui/PasswordWidget.h b/src/gui/PasswordWidget.h index 0014b696f8..d803b1f814 100644 --- a/src/gui/PasswordWidget.h +++ b/src/gui/PasswordWidget.h @@ -33,6 +33,7 @@ class PasswordWidget : public QWidget { Q_OBJECT + Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged USER true) public: explicit PasswordWidget(QWidget* parent = nullptr); ~PasswordWidget() override; diff --git a/src/gui/TotpDialog.h b/src/gui/TotpDialog.h index 1782c2de8d..d645497596 100644 --- a/src/gui/TotpDialog.h +++ b/src/gui/TotpDialog.h @@ -19,6 +19,8 @@ #ifndef KEEPASSX_TOTPDIALOG_H #define KEEPASSX_TOTPDIALOG_H +#include + #include "core/Database.h" #include "gui/DatabaseWidget.h" diff --git a/src/gui/TotpExportSettingsDialog.cpp b/src/gui/TotpExportSettingsDialog.cpp index 8e56d5d2ec..0568996b12 100644 --- a/src/gui/TotpExportSettingsDialog.cpp +++ b/src/gui/TotpExportSettingsDialog.cpp @@ -25,6 +25,7 @@ #include #include +#include #include #include #include diff --git a/src/gui/TotpExportSettingsDialog.h b/src/gui/TotpExportSettingsDialog.h index 8c80c9be5e..ff8308d824 100644 --- a/src/gui/TotpExportSettingsDialog.h +++ b/src/gui/TotpExportSettingsDialog.h @@ -18,6 +18,8 @@ #ifndef KEEPASSX_TotpExportSettingsDialog_H #define KEEPASSX_TotpExportSettingsDialog_H +#include + #include "core/Database.h" #include "gui/DatabaseWidget.h" diff --git a/src/gui/TotpSetupDialog.h b/src/gui/TotpSetupDialog.h index 9781b3954e..3822f119f5 100644 --- a/src/gui/TotpSetupDialog.h +++ b/src/gui/TotpSetupDialog.h @@ -19,6 +19,8 @@ #ifndef KEEPASSX_SETUPTOTPDIALOG_H #define KEEPASSX_SETUPTOTPDIALOG_H +#include + #include "core/Database.h" #include "gui/DatabaseWidget.h" diff --git a/src/gui/WelcomeWidget.cpp b/src/gui/WelcomeWidget.cpp index 49563ff73d..03f3925a66 100644 --- a/src/gui/WelcomeWidget.cpp +++ b/src/gui/WelcomeWidget.cpp @@ -37,14 +37,15 @@ WelcomeWidget::WelcomeWidget(QWidget* parent) m_ui->welcomeLabel->setFont(welcomeLabelFont); m_ui->iconLabel->setPixmap(icons()->applicationIcon().pixmap(64)); + m_ui->buttonNewDatabase->setIcon(icons()->icon("document-new")); + m_ui->buttonOpenDatabase->setIcon(icons()->icon("document-open")); + m_ui->buttonImport->setIcon(icons()->icon("document-import")); refreshLastDatabases(); connect(m_ui->buttonNewDatabase, SIGNAL(clicked()), SIGNAL(newDatabase())); connect(m_ui->buttonOpenDatabase, SIGNAL(clicked()), SIGNAL(openDatabase())); - connect(m_ui->buttonImportKeePass1, SIGNAL(clicked()), SIGNAL(importKeePass1Database())); - connect(m_ui->buttonImportOpVault, SIGNAL(clicked()), SIGNAL(importOpVaultDatabase())); - connect(m_ui->buttonImportCSV, SIGNAL(clicked()), SIGNAL(importCsv())); + connect(m_ui->buttonImport, SIGNAL(clicked()), SIGNAL(importFile())); connect(m_ui->recentListWidget, SIGNAL(itemActivated(QListWidgetItem*)), this, diff --git a/src/gui/WelcomeWidget.h b/src/gui/WelcomeWidget.h index 15f93466cb..15c3da7ff2 100644 --- a/src/gui/WelcomeWidget.h +++ b/src/gui/WelcomeWidget.h @@ -39,9 +39,7 @@ class WelcomeWidget : public QWidget void newDatabase(); void openDatabase(); void openDatabaseFile(QString); - void importKeePass1Database(); - void importOpVaultDatabase(); - void importCsv(); + void importFile(); protected: void keyPressEvent(QKeyEvent* event) override; diff --git a/src/gui/WelcomeWidget.ui b/src/gui/WelcomeWidget.ui index 8b72df8403..7a753e0104 100644 --- a/src/gui/WelcomeWidget.ui +++ b/src/gui/WelcomeWidget.ui @@ -70,6 +70,22 @@ + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + @@ -103,40 +119,26 @@ - - - - Create new database - - - - - - - Open existing database - - - - + - Import from KeePass 1 + Create Database - + - Import from 1Password + Open Database - + - Import from CSV + Import File @@ -148,12 +150,12 @@ Qt::Vertical - QSizePolicy::Minimum + QSizePolicy::Fixed 0 - 5 + 20 @@ -193,11 +195,7 @@ - buttonNewDatabase - buttonOpenDatabase - buttonImportKeePass1 - buttonImportOpVault - buttonImportCSV + buttonImport recentListWidget diff --git a/src/gui/csvImport/CsvImportWidget.cpp b/src/gui/csvImport/CsvImportWidget.cpp index 08f6d6589c..cbf2ae0a21 100644 --- a/src/gui/csvImport/CsvImportWidget.cpp +++ b/src/gui/csvImport/CsvImportWidget.cpp @@ -1,4 +1,4 @@ -/* +/* * Copyright (C) 2016 Enrico Mariotti * Copyright (C) 2017 KeePassXC Team * @@ -19,38 +19,69 @@ #include "CsvImportWidget.h" #include "ui_CsvImportWidget.h" -#include - #include "core/Clock.h" +#include "core/Database.h" +#include "core/Group.h" #include "core/Totp.h" +#include "format/CsvParser.h" #include "format/KeePass2Writer.h" -#include "gui/MessageBox.h" +#include "gui/csvImport/CsvParserModel.h" + +#include + +namespace +{ + // Extract group names from nested path and return the last group created + Group* createGroupStructure(Database* db, const QString& groupPath) + { + auto group = db->rootGroup(); + if (!group || groupPath.isEmpty()) { + return group; + } -// I wanted to make the CSV import GUI future-proof, so if one day you need a new field, -// all you have to do is add a field to m_columnHeader, and the GUI will follow: -// dynamic generation of comboBoxes, labels, placement and so on. Try it for immense fun! + auto nameList = groupPath.split("/", QString::SkipEmptyParts); + // Skip over first group name if root + if (nameList.first().compare("root", Qt::CaseInsensitive)) { + nameList.removeFirst(); + } + + for (const auto& name : qAsConst(nameList)) { + auto child = group->findChildByName(name); + if (!child) { + auto newGroup = new Group(); + newGroup->setUuid(QUuid::createUuid()); + newGroup->setName(name); + newGroup->setParent(group); + group = newGroup; + } else { + group = child; + } + } + return group; + } +} // namespace CsvImportWidget::CsvImportWidget(QWidget* parent) : QWidget(parent) , m_ui(new Ui::CsvImportWidget()) , m_parserModel(new CsvParserModel(this)) , m_comboModel(new QStringListModel(this)) - , m_columnHeader(QStringList() << QObject::tr("Group") << QObject::tr("Title") << QObject::tr("Username") - << QObject::tr("Password") << QObject::tr("URL") << QObject::tr("Notes") - << QObject::tr("TOTP") << QObject::tr("Icon") << QObject::tr("Last Modified") - << QObject::tr("Created")) - , m_fieldSeparatorList(QStringList() << "," - << ";" - << "-" - << ":" - << "." - << "\t") { m_ui->setupUi(this); m_ui->tableViewFields->setSelectionMode(QAbstractItemView::NoSelection); m_ui->tableViewFields->setFocusPolicy(Qt::NoFocus); - m_ui->messageWidget->setHidden(true); + + m_columnHeader << QObject::tr("Group") << QObject::tr("Title") << QObject::tr("Username") << QObject::tr("Password") + << QObject::tr("URL") << QObject::tr("Notes") << QObject::tr("TOTP") << QObject::tr("Icon") + << QObject::tr("Last Modified") << QObject::tr("Created"); + + m_fieldSeparatorList << "," + << ";" + << "-" + << ":" + << "." + << "\t"; m_combos << m_ui->groupCombo << m_ui->titleCombo << m_ui->usernameCombo << m_ui->passwordCombo << m_ui->urlCombo << m_ui->notesCombo << m_ui->totpCombo << m_ui->iconCombo << m_ui->lastModifiedCombo << m_ui->createdCombo; @@ -70,15 +101,12 @@ CsvImportWidget::CsvImportWidget(QWidget* parent) connect(m_ui->comboBoxFieldSeparator, SIGNAL(currentIndexChanged(int)), SLOT(parse())); connect(m_ui->checkBoxBackslash, SIGNAL(toggled(bool)), SLOT(parse())); connect(m_ui->checkBoxFieldNames, SIGNAL(toggled(bool)), SLOT(updatePreview())); - - connect(m_ui->buttonBox, SIGNAL(accepted()), this, SLOT(writeDatabase())); - connect(m_ui->buttonBox, SIGNAL(rejected()), this, SLOT(reject())); } void CsvImportWidget::comboChanged(int index) { // this line is the one that actually updates GUI table - m_parserModel->mapColumns(index, m_combos.indexOf(qobject_cast(sender()))); + m_parserModel->mapColumns(index - 1, m_combos.indexOf(qobject_cast(sender()))); updateTableview(); } @@ -94,68 +122,81 @@ CsvImportWidget::~CsvImportWidget() void CsvImportWidget::configParser() { - m_parserModel->setBackslashSyntax(m_ui->checkBoxBackslash->isChecked()); - m_parserModel->setComment(m_ui->comboBoxComment->currentText().at(0)); - m_parserModel->setTextQualifier(m_ui->comboBoxTextQualifier->currentText().at(0)); - m_parserModel->setCodec(m_ui->comboBoxCodec->currentText()); - m_parserModel->setFieldSeparator(m_fieldSeparatorList.at(m_ui->comboBoxFieldSeparator->currentIndex()).at(0)); + auto parser = m_parserModel->parser(); + parser->setBackslashSyntax(m_ui->checkBoxBackslash->isChecked()); + parser->setComment(m_ui->comboBoxComment->currentText().at(0)); + parser->setTextQualifier(m_ui->comboBoxTextQualifier->currentText().at(0)); + parser->setCodec(m_ui->comboBoxCodec->currentText()); + parser->setFieldSeparator(m_fieldSeparatorList.at(m_ui->comboBoxFieldSeparator->currentIndex()).at(0)); } void CsvImportWidget::updateTableview() { - m_ui->tableViewFields->resizeRowsToContents(); - m_ui->tableViewFields->resizeColumnsToContents(); + if (!m_buildingPreview) { + m_ui->tableViewFields->resizeRowsToContents(); + m_ui->tableViewFields->resizeColumnsToContents(); - for (int c = 0; c < m_ui->tableViewFields->horizontalHeader()->count(); ++c) { - m_ui->tableViewFields->horizontalHeader()->setSectionResizeMode(c, QHeaderView::Stretch); + for (int c = 0; c < m_ui->tableViewFields->horizontalHeader()->count(); ++c) { + m_ui->tableViewFields->horizontalHeader()->setSectionResizeMode(c, QHeaderView::Stretch); + } } } void CsvImportWidget::updatePreview() { - int minSkip = 0; - if (m_ui->checkBoxFieldNames->isChecked()) { - minSkip = 1; - } + m_buildingPreview = true; + + int minSkip = m_ui->checkBoxFieldNames->isChecked() ? 1 : 0; m_ui->labelSizeRowsCols->setText(m_parserModel->getFileInfo()); m_ui->spinBoxSkip->setRange(minSkip, qMax(minSkip, m_parserModel->rowCount() - 1)); m_ui->spinBoxSkip->setValue(minSkip); - QStringList list(tr("Not Present")); - for (int i = 1; i < m_parserModel->getCsvCols(); ++i) { + QStringList csvColumns(tr("Not Present")); + auto parser = m_parserModel->parser(); + for (int i = 0; i < parser->getCsvCols(); ++i) { if (m_ui->checkBoxFieldNames->isChecked()) { - auto columnName = m_parserModel->getCsvTable().at(0).at(i); + auto columnName = parser->getCsvTable().at(0).at(i); if (columnName.isEmpty()) { - list << QString(tr("Column %1").arg(i)); + csvColumns << QString(tr("Column %1").arg(i)); } else { - list << columnName; + csvColumns << columnName; } } else { - list << QString(tr("Column %1").arg(i)); + csvColumns << QString(tr("Column %1").arg(i)); } } - m_comboModel->setStringList(list); + m_comboModel->setStringList(csvColumns); + + // Try to match named columns to the combo boxes + for (int i = 0; i < m_columnHeader.size(); ++i) { + if (i >= m_combos.size()) { + // This should not happen, it is a programming error otherwise + Q_ASSERT(false); + break; + } - int j = 1; - for (QComboBox* b : m_combos) { - if (j < m_parserModel->getCsvCols()) { - b->setCurrentIndex(j); - } else { - b->setCurrentIndex(0); + bool found = false; + for (int j = 0; j < csvColumns.size(); ++j) { + if (m_columnHeader.at(i).compare(csvColumns.at(j), Qt::CaseInsensitive) == 0) { + m_combos.at(i)->setCurrentIndex(j); + found = true; + break; + } + } + // Named column not found, default to "Not Present" + if (!found) { + m_combos.at(i)->setCurrentIndex(0); } - ++j; } + + m_buildingPreview = false; + updateTableview(); } -void CsvImportWidget::load(const QString& filename, Database* const db) +void CsvImportWidget::load(const QString& filename) { - // QApplication::processEvents(); - m_db = db; + m_filename = filename; m_parserModel->setFilename(filename); - m_ui->labelFilename->setText(filename); - Group* group = m_db->rootGroup(); - group->setUuid(QUuid::createUuid()); - group->setNotes(tr("Imported from CSV file").append("\n").append(tr("Original data: ")) + filename); parse(); } @@ -163,42 +204,33 @@ void CsvImportWidget::parse() { configParser(); QApplication::setOverrideCursor(Qt::WaitCursor); - // QApplication::processEvents(); + QApplication::processEvents(); bool good = m_parserModel->parse(); updatePreview(); QApplication::restoreOverrideCursor(); if (!good) { - m_ui->messageWidget->showMessage(tr("Error(s) detected in CSV file!").append("\n").append(formatStatusText()), - MessageWidget::Warning); - } else { - m_ui->messageWidget->setHidden(true); + emit message(tr("Failed to parse CSV file: %1").arg(formatStatusText())); } } -QString CsvImportWidget::formatStatusText() const +QSharedPointer CsvImportWidget::buildDatabase() { - QString text = m_parserModel->getStatus(); - int items = text.count('\n'); - if (items > 2) { - return text.section('\n', 0, 1).append("\n").append(tr("[%n more message(s) skipped]", "", items - 2)); - } - if (items == 1) { - text.append(QString("\n")); - } - return text; -} + auto db = QSharedPointer::create(); + db->rootGroup()->setNotes(tr("Imported from CSV file: %1").arg(m_filename)); -void CsvImportWidget::writeDatabase() -{ - setRootGroup(); for (int r = 0; r < m_parserModel->rowCount(); ++r) { // use validity of second column as a GO/NOGO for all others fields - if (not m_parserModel->data(m_parserModel->index(r, 1)).isValid()) { + if (!m_parserModel->data(m_parserModel->index(r, 1)).isValid()) { continue; } - Entry* entry = new Entry(); + auto group = createGroupStructure(db.data(), m_parserModel->data(m_parserModel->index(r, 0)).toString()); + if (!group) { + continue; + } + + auto entry = new Entry(); entry->setUuid(QUuid::createUuid()); - entry->setGroup(splitGroups(m_parserModel->data(m_parserModel->index(r, 0)).toString())); + entry->setGroup(group); entry->setTitle(m_parserModel->data(m_parserModel->index(r, 1)).toString()); entry->setUsername(m_parserModel->data(m_parserModel->index(r, 2)).toString()); entry->setPassword(m_parserModel->data(m_parserModel->index(r, 3)).toString()); @@ -257,99 +289,19 @@ void CsvImportWidget::writeDatabase() } entry->setTimeInfo(timeInfo); } - QBuffer buffer; - buffer.open(QBuffer::ReadWrite); - - KeePass2Writer writer; - writer.writeDatabase(&buffer, m_db); - if (writer.hasError()) { - MessageBox::warning(this, - tr("Error"), - tr("CSV import: writer has errors:\n%1").arg(writer.errorString()), - MessageBox::Ok, - MessageBox::Ok); - } - emit editFinished(true); -} - -void CsvImportWidget::setRootGroup() -{ - QString groupLabel; - QStringList groupList; - bool is_root = false; - bool is_empty = false; - bool is_label = false; - for (int r = 0; r < m_parserModel->rowCount(); ++r) { - // use validity of second column as a GO/NOGO for all others fields - if (not m_parserModel->data(m_parserModel->index(r, 1)).isValid()) { - continue; - } - groupLabel = m_parserModel->data(m_parserModel->index(r, 0)).toString(); - // check if group name is either "root", "" (empty) or some other label - groupList = groupLabel.split("/", QString::SkipEmptyParts); - if (groupList.isEmpty()) { - is_empty = true; - } else if (not groupList.first().compare("Root", Qt::CaseSensitive)) { - is_root = true; - } else if (not groupLabel.compare("")) { - is_empty = true; - } else { - is_label = true; - } - - groupList.clear(); - } - - if ((is_empty and is_root) or (is_label and not is_empty and is_root)) { - m_db->rootGroup()->setName("CSV IMPORTED"); - } else { - m_db->rootGroup()->setName("Root"); - } + return db; } -Group* CsvImportWidget::splitGroups(const QString& label) +QString CsvImportWidget::formatStatusText() const { - // extract group names from nested path provided in "label" - Group* current = m_db->rootGroup(); - if (label.isEmpty()) { - return current; - } - - QStringList groupList = label.split("/", QString::SkipEmptyParts); - // avoid the creation of a subgroup with the same name as Root - if (m_db->rootGroup()->name() == "Root" && groupList.first() == "Root") { - groupList.removeFirst(); - } - - for (const QString& groupName : groupList) { - Group* children = hasChildren(current, groupName); - if (children == nullptr) { - Group* brandNew = new Group(); - brandNew->setParent(current); - brandNew->setName(groupName); - brandNew->setUuid(QUuid::createUuid()); - current = brandNew; - } else { - Q_ASSERT(children != nullptr); - current = children; - } + QString text = m_parserModel->parser()->getStatus(); + int items = text.count('\n'); + if (items > 2) { + return text.section('\n', 0, 1).append("\n").append(tr("[%n more message(s) skipped]", "", items - 2)); } - return current; -} - -Group* CsvImportWidget::hasChildren(Group* current, const QString& groupName) -{ - // returns the group whose name is "groupName" and is child of "current" group - for (Group* group : current->children()) { - if (group->name() == groupName) { - return group; - } + if (items == 1) { + text.append(QString("\n")); } - return nullptr; -} - -void CsvImportWidget::reject() -{ - emit editFinished(false); + return text; } diff --git a/src/gui/csvImport/CsvImportWidget.h b/src/gui/csvImport/CsvImportWidget.h index 1711e32a66..d5f29c43ac 100644 --- a/src/gui/csvImport/CsvImportWidget.h +++ b/src/gui/csvImport/CsvImportWidget.h @@ -1,4 +1,4 @@ -/* +/* * Copyright (C) 2016 Enrico Mariotti * Copyright (C) 2017 KeePassXC Team * @@ -19,12 +19,13 @@ #ifndef KEEPASSX_CSVIMPORTWIDGET_H #define KEEPASSX_CSVIMPORTWIDGET_H -#include +#include +#include -#include "core/Metadata.h" -#include "gui/csvImport/CsvParserModel.h" - -class QStringListModel; +class CsvParserModel; +class Database; +class Group; +class QComboBox; namespace Ui { @@ -37,36 +38,36 @@ class CsvImportWidget : public QWidget public: explicit CsvImportWidget(QWidget* parent = nullptr); - ~CsvImportWidget(); - void load(const QString& filename, Database* const db); + ~CsvImportWidget() override; + + void load(const QString& filename); + QSharedPointer buildDatabase(); signals: - void editFinished(bool accepted); + void message(QString msg); private slots: void parse(); void comboChanged(int index); void skippedChanged(int rows); - void writeDatabase(); void updatePreview(); - void setRootGroup(); - void reject(); private: - Q_DISABLE_COPY(CsvImportWidget) - const QScopedPointer m_ui; - CsvParserModel* const m_parserModel; - QStringListModel* const m_comboModel; - QList m_combos; - Database* m_db; - - const QStringList m_columnHeader; - QStringList m_fieldSeparatorList; void configParser(); void updateTableview(); - Group* splitGroups(const QString& label); - Group* hasChildren(Group* current, const QString& groupName); QString formatStatusText() const; + + QScopedPointer m_ui; + + CsvParserModel* m_parserModel; + QStringListModel* m_comboModel; + QList m_combos; + QStringList m_columnHeader; + QStringList m_fieldSeparatorList; + QString m_filename; + bool m_buildingPreview = false; + + Q_DISABLE_COPY(CsvImportWidget) }; #endif // KEEPASSX_CSVIMPORTWIDGET_H diff --git a/src/gui/csvImport/CsvImportWidget.ui b/src/gui/csvImport/CsvImportWidget.ui index 7a5627d980..cd7af98167 100644 --- a/src/gui/csvImport/CsvImportWidget.ui +++ b/src/gui/csvImport/CsvImportWidget.ui @@ -6,763 +6,677 @@ 0 0 - 788 - 530 + 820 + 523 + + 0 + + + 0 + + + 0 + + + 0 + - - - - - - Qt::ScrollBarAlwaysOff - - - true - - - - - 0 - 0 - 753 - 615 - - - - - - - - 11 - 75 - true - - - - Import CSV fields - - - - - - - + + + + + + 75 + true + + + + Column Association + + + + + + + 50 + false + + + + Icon + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 2 + + + + + + + + 50 + false + + + + TOTP + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 2 + + + + + + + + + + + 50 + false + + + + Notes + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 2 + + + + + + + + 50 + false + + + + Title + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 2 + + + + + + + + + + + + + + + + + + + + 50 + false + + + + First line has field names + + + true + + + + + + + + + + + + + + 50 + false + + + + Password + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 2 + + + + + + + + 50 + false + + + + Group + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 2 + + + + + + + + + + + 50 + false + + + + Username + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 2 + + + + + + + + 50 + false + + + + URL + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 2 + + + + + + + + + + + 50 + false + + + + Created + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 2 + + + + + + + + + + + 50 + false + + + + Last Modified + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 2 + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 6 + 20 + + + + + + + + + + + + 75 + true + + + + Encoding + + + + + + + 50 + false + + + + Codec + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 50 + false + + + + Codec + + + false + + + UTF-8 + + - filename + UTF-8 - - - - - - Qt::Horizontal + + + + Windows-1252 - - - 40 - 20 - + + + + UTF-16 - - - - + + - size, rows, columns + UTF-16LE - - - - - - - - - - - 75 - true - + + + + + + + + 50 + false + + + + Text is qualified by + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 50 + false + + + + Text qualification + + + false + + + " + + + + " - - Column Association + + + + ' - - - - - - - - - - - 50 - false - - - - Password - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 2 - - - - - - - - 50 - false - - - - Username - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 2 - - - - - - - - - - - - - - 50 - false - - - - Title - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 2 - - - - - - - - - - - 50 - false - - - - Group - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 2 - - - - - - - - 50 - false - - - - URL - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 2 - - - - - - - - - - - 50 - false - - - - Notes - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 2 - - - - - - - - - - - 50 - false - - - - TOTP - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 2 - - - - - - - - - - - - - - 50 - false - - - - Created - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 2 - - - - - - - - 50 - false - - - - Last Modified - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 2 - - - - - - - - - - - 50 - false - - - - Icon - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 2 - - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - 75 - true - + + + + : - - Encoding + + + + . - - - - - - 50 - false - - - - Codec - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 50 - false - - - - Codec - - - false - - - UTF-8 - - - - UTF-8 - - - - - Windows-1252 - - - - - UTF-16 - - - - - UTF-16LE - - - - - - - - - 50 - false - - - - Text is qualified by - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 50 - false - - - - Text qualification - - - false - - - " - - - - " - - - - - ' - - - - - : - - - - - . - - - - - | - - - - - - - - - 50 - false - - - - Fields are separated by - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 50 - false - - - - Field separation - - - false - - - , - - - - , - - - - - ; - - - - - - - - - - - : - - - - - . - - - - - TAB (\t) - - - - - - - - - 50 - false - - - - Comments start with - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 50 - false - - - - Comments start with - - - false - - - # - - - - # - - - - - ; - - - - - : - - - - - @ - - - - - - - - - 50 - false - - - - Header lines skipped - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - 50 - false - - - - Number of header lines to discard - - - - - - - Qt::Horizontal - - - - 122 - 20 - - - - - - - - - - - 50 - false - - - - First line has field names - - - - - - - - 50 - false - - - - Consider '\' an escape character - - - - - - - - - - Qt::Horizontal + + + + | - - - 40 - 20 - + + + + + + + + 50 + false + + + + Fields are separated by + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 50 + false + + + + Field separation + + + false + + + , + + + + , - - - - - - - - - 75 - true - - - - Preview - - - false - - + - - - - 0 - 0 - - - - - 0 - 300 - - - - - 50 - false - - - - CSV import preview - - - false - - - true - - + + ; + - - - - - - + + + - + + + + + : + + + + + . + + + + + TAB (\t) + + + + + + + + + 50 + false + + + + Comments start with + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 50 + false + + + + Comments start with + + + false + + + # + + + + # + + + + + ; + + + + + : + + + + + @ + + + + + + + + + 50 + false + + + + Header lines skipped + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 50 + false + + + + Number of header lines to discard + + + + + + + + 50 + false + + + + Consider '\' an escape character + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + 75 + true + + + + Preview - + false + + + 3 + + + 0 + + + + + + 50 + false + + + + size, rows, columns + + + + + + + + 0 + 0 + + + + + 800 + 300 + + + + + 50 + false + + + + CSV import preview + + + false + + + true + + + + - - - MessageWidget - QWidget -
gui/MessageWidget.h
- 1 -
-
- scrollArea - groupCombo - titleCombo - usernameCombo - passwordCombo - urlCombo - notesCombo - totpCombo - iconCombo - lastModifiedCombo - createdCombo comboBoxCodec comboBoxTextQualifier comboBoxFieldSeparator comboBoxComment - spinBoxSkip - checkBoxFieldNames - checkBoxBackslash tableViewFields diff --git a/src/gui/csvImport/CsvImportWizard.cpp b/src/gui/csvImport/CsvImportWizard.cpp deleted file mode 100644 index 4595c06d92..0000000000 --- a/src/gui/csvImport/CsvImportWizard.cpp +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2016 Enrico Mariotti - * Copyright (C) 2017 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 "CsvImportWizard.h" - -#include - -CsvImportWizard::CsvImportWizard(QWidget* parent) - : DialogyWidget(parent) -{ - m_layout = new QGridLayout(this); - m_layout->addWidget(m_parse = new CsvImportWidget(this), 0, 0); - - connect(m_parse, SIGNAL(editFinished(bool)), this, SLOT(parseFinished(bool))); -} - -CsvImportWizard::~CsvImportWizard() -{ -} - -void CsvImportWizard::load(const QString& filename, Database* database) -{ - m_db = database; - m_parse->load(filename, database); -} - -void CsvImportWizard::parseFinished(bool accepted) -{ - emit importFinished(accepted); -} diff --git a/src/gui/csvImport/CsvImportWizard.h b/src/gui/csvImport/CsvImportWizard.h deleted file mode 100644 index 084d8794d8..0000000000 --- a/src/gui/csvImport/CsvImportWizard.h +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2016 Enrico Mariotti - * Copyright (C) 2017 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 KEEPASSX_CSVIMPORTWIZARD_H -#define KEEPASSX_CSVIMPORTWIZARD_H - -#include "CsvImportWidget.h" - -#include "gui/DialogyWidget.h" - -class QGridLayout; - -class CsvImportWidget; - -class CsvImportWizard : public DialogyWidget -{ - Q_OBJECT - -public: - explicit CsvImportWizard(QWidget* parent = nullptr); - ~CsvImportWizard() override; - void load(const QString& filename, Database* database); - -signals: - void importFinished(bool accepted); - -private slots: - void parseFinished(bool accepted); - -private: - QPointer m_db; - CsvImportWidget* m_parse; - QGridLayout* m_layout; -}; - -#endif // KEEPASSX_CSVIMPORTWIZARD_H diff --git a/src/gui/csvImport/CsvParserModel.cpp b/src/gui/csvImport/CsvParserModel.cpp index 77abb59317..0ae350817a 100644 --- a/src/gui/csvImport/CsvParserModel.cpp +++ b/src/gui/csvImport/CsvParserModel.cpp @@ -18,10 +18,14 @@ #include "CsvParserModel.h" +#include "core/Tools.h" +#include "format/CsvParser.h" + #include CsvParserModel::CsvParserModel(QObject* parent) : QAbstractTableModel(parent) + , m_parser(new CsvParser()) , m_skipped(0) { } @@ -30,6 +34,11 @@ CsvParserModel::~CsvParserModel() { } +CsvParser* CsvParserModel::parser() +{ + return m_parser; +} + void CsvParserModel::setFilename(const QString& filename) { m_filename = filename; @@ -37,11 +46,10 @@ void CsvParserModel::setFilename(const QString& filename) QString CsvParserModel::getFileInfo() { - QString a(tr("%1, %2, %3", "file info: bytes, rows, columns") - .arg(tr("%n byte(s)", nullptr, getFileSize()), - tr("%n row(s)", nullptr, getCsvRows()), - tr("%n column(s)", nullptr, qMax(0, getCsvCols() - 1)))); - return a; + return QString("%1, %2, %3") + .arg(Tools::humanReadableFileSize(m_parser->getFileSize()), + tr("%n row(s)", "CSV row count", m_parser->getCsvRows()), + tr("%n column(s)", "CSV column count", qMax(0, m_parser->getCsvCols() - 1))); } bool CsvParserModel::parse() @@ -49,37 +57,28 @@ bool CsvParserModel::parse() bool r; beginResetModel(); m_columnMap.clear(); - if (CsvParser::isFileLoaded()) { - r = CsvParser::reparse(); + if (m_parser->isFileLoaded()) { + r = m_parser->reparse(); } else { QFile csv(m_filename); - r = CsvParser::parse(&csv); + r = m_parser->parse(&csv); } for (int i = 0; i < columnCount(); ++i) { m_columnMap.insert(i, 0); } - addEmptyColumn(); endResetModel(); return r; } -void CsvParserModel::addEmptyColumn() -{ - for (int i = 0; i < m_table.size(); ++i) { - CsvRow r = m_table.at(i); - r.prepend(QString("")); - m_table.replace(i, r); - } -} - void CsvParserModel::mapColumns(int csvColumn, int dbColumn) { - if ((csvColumn < 0) || (dbColumn < 0)) { + if (dbColumn < 0 || dbColumn >= m_columnMap.size()) { return; } beginResetModel(); - if (csvColumn >= getCsvCols()) { - m_columnMap[dbColumn] = 0; // map to the empty column + if (csvColumn < 0 || csvColumn >= m_parser->getCsvCols()) { + // This indicates a blank cell + m_columnMap[dbColumn] = -1; } else { m_columnMap[dbColumn] = csvColumn; } @@ -105,7 +104,7 @@ int CsvParserModel::rowCount(const QModelIndex& parent) const if (parent.isValid()) { return 0; } - return getCsvRows(); + return m_parser->getCsvRows(); } int CsvParserModel::columnCount(const QModelIndex& parent) const @@ -118,11 +117,14 @@ int CsvParserModel::columnCount(const QModelIndex& parent) const QVariant CsvParserModel::data(const QModelIndex& index, int role) const { - if ((index.column() >= m_columnHeader.size()) || (index.row() + m_skipped >= rowCount()) || !index.isValid()) { - return QVariant(); + if (index.column() >= m_columnHeader.size() || index.row() + m_skipped >= rowCount() || !index.isValid()) { + return {}; } if (role == Qt::DisplayRole) { - return m_table.at(index.row() + m_skipped).at(m_columnMap[index.column()]); + auto column = m_columnMap[index.column()]; + if (column >= 0) { + return m_parser->getCsvTable().at(index.row() + m_skipped).at(column); + } } return QVariant(); } @@ -131,15 +133,13 @@ QVariant CsvParserModel::headerData(int section, Qt::Orientation orientation, in { if (role == Qt::DisplayRole) { if (orientation == Qt::Horizontal) { - if ((section < 0) || (section >= m_columnHeader.size())) { - return QVariant(); + if (section >= 0 && section < m_columnHeader.size()) { + return m_columnHeader.at(section); } - return m_columnHeader.at(section); } else if (orientation == Qt::Vertical) { - if (section + m_skipped >= rowCount()) { - return QVariant(); + if (section + m_skipped < rowCount()) { + return QString::number(section + 1); } - return QString::number(section + 1); } } return QVariant(); diff --git a/src/gui/csvImport/CsvParserModel.h b/src/gui/csvImport/CsvParserModel.h index 5e979421cc..2717b826dc 100644 --- a/src/gui/csvImport/CsvParserModel.h +++ b/src/gui/csvImport/CsvParserModel.h @@ -21,20 +21,22 @@ #include -#include "core/Group.h" -#include "format/CsvParser.h" +class CsvParser; -class CsvParserModel : public QAbstractTableModel, public CsvParser +class CsvParserModel : public QAbstractTableModel { Q_OBJECT public: explicit CsvParserModel(QObject* parent = nullptr); - ~CsvParserModel(); + ~CsvParserModel() override; + void setFilename(const QString& filename); QString getFileInfo(); bool parse(); + CsvParser* parser(); + void setHeaderLabels(const QStringList& labels); void mapColumns(int csvColumn, int dbColumn); @@ -47,12 +49,12 @@ public slots: void setSkippedRows(int skipped); private: + CsvParser* m_parser; int m_skipped; QString m_filename; QStringList m_columnHeader; // first column of model must be empty (aka combobox row "Not present in CSV file") void addEmptyColumn(); - // mapping CSV columns to keepassx columns QMap m_columnMap; }; diff --git a/src/gui/tag/TagsEdit.cpp b/src/gui/tag/TagsEdit.cpp index 7d5b657ce7..4388f4790e 100644 --- a/src/gui/tag/TagsEdit.cpp +++ b/src/gui/tag/TagsEdit.cpp @@ -24,6 +24,7 @@ #include "TagsEdit.h" #include "gui/MainWindow.h" +#include #include #include #include diff --git a/src/gui/wizard/ImportWizard.cpp b/src/gui/wizard/ImportWizard.cpp new file mode 100644 index 0000000000..05fdbfe38f --- /dev/null +++ b/src/gui/wizard/ImportWizard.cpp @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2018 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 "ImportWizard.h" +#include "ImportWizardPageReview.h" +#include "ImportWizardPageSelect.h" + +#include "core/Global.h" +#include "core/Group.h" + +#include +#include + +ImportWizard::ImportWizard(QWidget* parent) + : QWizard(parent) + , m_pageSelect(new ImportWizardPageSelect) + , m_pageReview(new ImportWizardPageReview) +{ + setWizardStyle(MacStyle); + setOption(HaveHelpButton, false); + setOption(NoDefaultButton, false); // Needed for macOS + + addPage(m_pageSelect.data()); + addPage(m_pageReview.data()); + + setWindowTitle(tr("Import Wizard")); + + Q_INIT_RESOURCE(wizard); + setPixmap(BackgroundPixmap, QPixmap(":/wizard/background-pixmap.png")); + + // Fix MacStyle QWizard page frame too bright in dark mode (QTBUG-70346, QTBUG-71696) + QPalette defaultPalette; + auto windowColor = defaultPalette.color(QPalette::Window); + windowColor.setAlpha(153); + auto baseColor = defaultPalette.color(QPalette::Base); + baseColor.setAlpha(153); + + auto* pageFrame = findChildren()[0]; + auto framePalette = pageFrame->palette(); + framePalette.setBrush(QPalette::Window, windowColor.lighter(120)); + framePalette.setBrush(QPalette::Base, baseColor.lighter(120)); + pageFrame->setPalette(framePalette); +} + +ImportWizard::~ImportWizard() +{ +} + +bool ImportWizard::validateCurrentPage() +{ + bool ret = QWizard::validateCurrentPage(); + if (ret && currentPage() == m_pageReview) { + m_db = m_pageReview->database(); + } + return ret; +} + +QPair ImportWizard::importInto() +{ + auto list = field("ImportInto").toList(); + if (list.size() >= 2) { + return qMakePair(QUuid(list[0].toString()), QUuid(list[1].toString())); + } + return {}; +} + +QSharedPointer ImportWizard::database() +{ + return m_db; +} diff --git a/src/gui/wizard/ImportWizard.h b/src/gui/wizard/ImportWizard.h new file mode 100644 index 0000000000..b7e9de68d3 --- /dev/null +++ b/src/gui/wizard/ImportWizard.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2018 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_IMPORTWIZARD_H +#define KEEPASSXC_IMPORTWIZARD_H + +#include +#include + +class Database; +class ImportWizardPageSelect; +class ImportWizardPageReview; + +/** + * Setup wizard for importing a file into a database. + */ +class ImportWizard : public QWizard +{ + Q_OBJECT + +public: + explicit ImportWizard(QWidget* parent = nullptr); + ~ImportWizard() override; + + bool validateCurrentPage() override; + + QSharedPointer database(); + QPair importInto(); + + enum ImportType + { + IMPORT_NONE = 0, + IMPORT_CSV, + IMPORT_OPVAULT, + IMPORT_OPUX, + IMPORT_BITWARDEN, + IMPORT_KEEPASS1 + }; + +private: + QSharedPointer m_db; + QPointer m_pageSelect; + QPointer m_pageReview; +}; + +#endif // KEEPASSXC_IMPORTWIZARD_H diff --git a/src/gui/wizard/ImportWizardPageReview.cpp b/src/gui/wizard/ImportWizardPageReview.cpp new file mode 100644 index 0000000000..2cb56791b6 --- /dev/null +++ b/src/gui/wizard/ImportWizardPageReview.cpp @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2023 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 "ImportWizardPageReview.h" +#include "ui_ImportWizardPageReview.h" + +#include "core/Database.h" +#include "core/Group.h" +#include "format/BitwardenReader.h" +#include "format/KeePass1Reader.h" +#include "format/OPUXReader.h" +#include "format/OpVaultReader.h" +#include "gui/csvImport/CsvImportWidget.h" +#include "gui/wizard/ImportWizard.h" + +#include +#include +#include +#include + +ImportWizardPageReview::ImportWizardPageReview(QWidget* parent) + : QWizardPage(parent) + , m_ui(new Ui::ImportWizardPageReview) +{ +} + +ImportWizardPageReview::~ImportWizardPageReview() +{ +} + +void ImportWizardPageReview::initializePage() +{ + m_db.reset(); + + // Reset the widget in case we changed the import type + for (auto child : children()) { + delete child; + } + m_ui->setupUi(this); + + auto filename = field("ImportFile").toString(); + m_ui->filenameLabel->setText(filename); + + m_ui->messageWidget->hideMessage(); + m_ui->messageWidget->setAnimate(false); + m_ui->messageWidget->setCloseButtonVisible(false); + + auto importType = field("ImportType").toInt(); + switch (importType) { + case ImportWizard::IMPORT_CSV: + setupCsvImport(filename); + break; + case ImportWizard::IMPORT_OPVAULT: + m_db = importOPVault(filename, field("ImportPassword").toString()); + setupDatabasePreview(); + break; + case ImportWizard::IMPORT_OPUX: + m_db = importOPUX(filename); + setupDatabasePreview(); + break; + case ImportWizard::IMPORT_KEEPASS1: + m_db = importKeePass1(filename, field("ImportPassword").toString(), field("ImportKeyFile").toString()); + setupDatabasePreview(); + break; + case ImportWizard::IMPORT_BITWARDEN: + m_db = importBitwarden(filename, field("ImportPassword").toString()); + setupDatabasePreview(); + break; + default: + break; + } +} + +bool ImportWizardPageReview::validatePage() +{ + if (m_csvWidget && field("ImportType").toInt() == ImportWizard::IMPORT_CSV) { + m_db = m_csvWidget->buildDatabase(); + } + return !m_db.isNull(); +} + +QSharedPointer ImportWizardPageReview::database() +{ + return m_db; +} + +void ImportWizardPageReview::setupCsvImport(const QString& filename) +{ + // No need for this label with CSV + m_ui->previewLabel->hide(); + + m_csvWidget = new CsvImportWidget(); + connect(m_csvWidget, &CsvImportWidget::message, m_ui->messageWidget, [this](QString message) { + m_ui->messageWidget->showMessage(message, KMessageWidget::Error, -1); + }); + + m_csvWidget->load(filename); + + // Qt does not automatically resize a QScrollWidget in a QWizard... + m_ui->scrollAreaContents->layout()->addWidget(m_csvWidget); + m_ui->scrollArea->setMinimumSize(m_csvWidget->width() + 50, m_csvWidget->height() + 100); +} + +void ImportWizardPageReview::setupDatabasePreview() +{ + if (!m_db) { + m_ui->scrollArea->setVisible(false); + return; + } + + auto entryList = m_db->rootGroup()->entriesRecursive(); + m_ui->previewLabel->setText(tr("Entry count: %1").arg(entryList.count())); + + QStringList headerLabels({tr("Group"), tr("Title"), tr("Username"), tr("Password"), tr("Url")}); + + auto tableWidget = new QTableWidget(entryList.count(), headerLabels.count()); + tableWidget->setHorizontalHeaderLabels(headerLabels); + + int row = 0; + for (auto entry : entryList) { + QList items({new QTableWidgetItem(entry->group()->name()), + new QTableWidgetItem(entry->title()), + new QTableWidgetItem(entry->username()), + new QTableWidgetItem(entry->password()), + new QTableWidgetItem(entry->url())}); + int column = 0; + for (auto item : items) { + tableWidget->setItem(row, column++, item); + } + ++row; + } + + tableWidget->setSortingEnabled(true); + tableWidget->setSelectionMode(QTableWidget::NoSelection); + tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers); + tableWidget->setWordWrap(true); + tableWidget->horizontalHeader()->setMaximumSectionSize(200); + tableWidget->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + tableWidget->horizontalHeader()->setStretchLastSection(true); + + m_ui->scrollAreaContents->layout()->addWidget(tableWidget); +} + +QSharedPointer ImportWizardPageReview::importOPUX(const QString& filename) +{ + OPUXReader reader; + auto db = reader.convert(filename); + if (reader.hasError()) { + m_ui->messageWidget->showMessage(reader.errorString(), KMessageWidget::Error, -1); + } + return db; +} + +QSharedPointer ImportWizardPageReview::importBitwarden(const QString& filename, const QString& password) +{ + BitwardenReader reader; + auto db = reader.convert(filename, password); + if (reader.hasError()) { + m_ui->messageWidget->showMessage(reader.errorString(), KMessageWidget::Error, -1); + } + return db; +} + +QSharedPointer ImportWizardPageReview::importOPVault(const QString& filename, const QString& password) +{ + OpVaultReader reader; + QDir opVault(filename); + auto db = reader.convert(opVault, password); + if (reader.hasError()) { + m_ui->messageWidget->showMessage(reader.errorString(), KMessageWidget::Error, -1); + } + return db; +} + +QSharedPointer +ImportWizardPageReview::importKeePass1(const QString& filename, const QString& password, const QString& keyfile) +{ + KeePass1Reader reader; + + // TODO: Handle case of empty password? + + auto db = reader.readDatabase(filename, password, keyfile); + if (reader.hasError()) { + m_ui->messageWidget->showMessage(reader.errorString(), KMessageWidget::Error, -1); + } + + return db; +} diff --git a/src/gui/wizard/ImportWizardPageReview.h b/src/gui/wizard/ImportWizardPageReview.h new file mode 100644 index 0000000000..87f501c85c --- /dev/null +++ b/src/gui/wizard/ImportWizardPageReview.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2023 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_IMPORTWIZARDPAGEREVIEW_H +#define KEEPASSXC_IMPORTWIZARDPAGEREVIEW_H + +#include +#include + +class CsvImportWidget; +class Database; +namespace Ui +{ + class ImportWizardPageReview; +}; + +class ImportWizardPageReview : public QWizardPage +{ + Q_OBJECT + +public: + explicit ImportWizardPageReview(QWidget* parent = nullptr); + Q_DISABLE_COPY(ImportWizardPageReview) + ~ImportWizardPageReview() override; + + void initializePage() override; + bool validatePage() override; + + QSharedPointer database(); + +private: + void setupCsvImport(const QString& filename); + QSharedPointer importOPUX(const QString& filename); + QSharedPointer importBitwarden(const QString& filename, const QString& password); + QSharedPointer importOPVault(const QString& filename, const QString& password); + QSharedPointer importKeePass1(const QString& filename, const QString& password, const QString& keyfile); + + void setupDatabasePreview(); + + QScopedPointer m_ui; + + QSharedPointer m_db; + QPointer m_csvWidget; +}; + +#endif diff --git a/src/gui/wizard/ImportWizardPageReview.ui b/src/gui/wizard/ImportWizardPageReview.ui new file mode 100644 index 0000000000..6872ec4eac --- /dev/null +++ b/src/gui/wizard/ImportWizardPageReview.ui @@ -0,0 +1,95 @@ + + + ImportWizardPageReview + + + + 0 + 0 + 518 + 334 + + + + WizardPage + + + + + + + + + + 500 + 300 + + + + Qt::ScrollBarAlwaysOff + + + QAbstractScrollArea::AdjustToContentsOnFirstShow + + + true + + + + + 0 + 0 + 498 + 298 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 75 + true + + + + filename + + + + + + + Entry count: %1 + + + + + + + + + + + + MessageWidget + QWidget +
gui/MessageWidget.h
+ 1 +
+
+ + +
diff --git a/src/gui/wizard/ImportWizardPageSelect.cpp b/src/gui/wizard/ImportWizardPageSelect.cpp new file mode 100644 index 0000000000..7bca1a39a0 --- /dev/null +++ b/src/gui/wizard/ImportWizardPageSelect.cpp @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2023 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 "ImportWizardPageSelect.h" +#include "ui_ImportWizardPageSelect.h" + +#include "ImportWizard.h" + +#include "gui/DatabaseWidget.h" +#include "gui/FileDialog.h" +#include "gui/Icons.h" +#include "gui/MainWindow.h" + +ImportWizardPageSelect::ImportWizardPageSelect(QWidget* parent) + : QWizardPage(parent) + , m_ui(new Ui::ImportWizardPageSelect()) +{ + m_ui->setupUi(this); + + new QListWidgetItem(icons()->icon("csv"), tr("Comma Separated Values (.csv)"), m_ui->importTypeList); + new QListWidgetItem(icons()->icon("onepassword"), tr("1Password Export (.1pux)"), m_ui->importTypeList); + new QListWidgetItem(icons()->icon("onepassword"), tr("1Password Vault (.opvault)"), m_ui->importTypeList); + new QListWidgetItem(icons()->icon("bitwarden"), tr("Bitwarden (.json)"), m_ui->importTypeList); + new QListWidgetItem(icons()->icon("object-locked"), tr("KeePass 1 Database (.kdb)"), m_ui->importTypeList); + + m_ui->importTypeList->item(0)->setData(Qt::UserRole, ImportWizard::IMPORT_CSV); + m_ui->importTypeList->item(1)->setData(Qt::UserRole, ImportWizard::IMPORT_OPUX); + m_ui->importTypeList->item(2)->setData(Qt::UserRole, ImportWizard::IMPORT_OPVAULT); + m_ui->importTypeList->item(3)->setData(Qt::UserRole, ImportWizard::IMPORT_BITWARDEN); + m_ui->importTypeList->item(4)->setData(Qt::UserRole, ImportWizard::IMPORT_KEEPASS1); + + connect(m_ui->importTypeList, &QListWidget::currentItemChanged, this, &ImportWizardPageSelect::itemSelected); + m_ui->importTypeList->setCurrentRow(0); + + connect(m_ui->importFileButton, &QAbstractButton::clicked, this, &ImportWizardPageSelect::chooseImportFile); + connect(m_ui->keyFileButton, &QAbstractButton::clicked, this, &ImportWizardPageSelect::chooseKeyFile); + connect(m_ui->existingDatabaseRadio, &QRadioButton::toggled, this, [this](bool state) { + m_ui->existingDatabaseChoice->setEnabled(state); + }); + + updateDatabaseChoices(); + + registerField("ImportType", this); + registerField("ImportFile*", m_ui->importFileEdit); + registerField("ImportInto", m_ui->importIntoLabel); + registerField("ImportPassword", m_ui->passwordEdit, "text", "textChanged"); + registerField("ImportKeyFile", m_ui->keyFileEdit); +} + +ImportWizardPageSelect::~ImportWizardPageSelect() +{ +} + +void ImportWizardPageSelect::initializePage() +{ + setField("ImportType", m_ui->importTypeList->currentItem()->data(Qt::UserRole).toInt()); + adjustSize(); +} + +bool ImportWizardPageSelect::validatePage() +{ + if (m_ui->existingDatabaseRadio->isChecked()) { + if (m_ui->existingDatabaseChoice->currentIndex() == -1) { + return false; + } + setField("ImportInto", m_ui->existingDatabaseChoice->currentData()); + } else { + setField("ImportInto", {}); + } + + return true; +} + +void ImportWizardPageSelect::itemSelected(QListWidgetItem* current, QListWidgetItem* previous) +{ + Q_UNUSED(previous) + + if (!current) { + setCredentialState(false); + return; + } + + m_ui->importFileEdit->clear(); + m_ui->passwordEdit->clear(); + m_ui->keyFileEdit->clear(); + + auto type = current->data(Qt::UserRole).toInt(); + setField("ImportType", type); + switch (type) { + // Unencrypted types + case ImportWizard::IMPORT_CSV: + case ImportWizard::IMPORT_OPUX: + setCredentialState(false); + break; + // Password may be required + case ImportWizard::IMPORT_BITWARDEN: + case ImportWizard::IMPORT_OPVAULT: + setCredentialState(true); + break; + // Password and/or Key File may be required + case ImportWizard::IMPORT_KEEPASS1: + setCredentialState(true, true); + break; + default: + Q_ASSERT(false); + } +} + +void ImportWizardPageSelect::updateDatabaseChoices() const +{ + m_ui->existingDatabaseChoice->clear(); + auto mainWindow = getMainWindow(); + if (mainWindow) { + for (auto dbWidget : mainWindow->getOpenDatabases()) { + // Skip over locked databases + if (dbWidget->isLocked()) { + continue; + } + + // Enable the selection of an existing database + m_ui->existingDatabaseRadio->setEnabled(true); + m_ui->existingDatabaseRadio->setToolTip(""); + + // Add a separator between databases + if (m_ui->existingDatabaseChoice->count() > 0) { + m_ui->existingDatabaseChoice->insertSeparator(m_ui->existingDatabaseChoice->count()); + } + + // Add the root group as a special line item + auto db = dbWidget->database(); + m_ui->existingDatabaseChoice->addItem( + QString("%1 (%2)").arg(dbWidget->displayName(), db->rootGroup()->name()), + QList() << db->uuid() << db->rootGroup()->uuid()); + + if (dbWidget->isVisible()) { + m_ui->existingDatabaseChoice->setCurrentIndex(m_ui->existingDatabaseChoice->count() - 1); + } + + // Add remaining groups + for (const auto& group : db->rootGroup()->groupsRecursive(false)) { + if (!group->isRecycled()) { + auto path = group->hierarchy(); + path.removeFirst(); + m_ui->existingDatabaseChoice->addItem(QString(" / %1").arg(path.join(" / ")), + QList() << db->uuid() << group->uuid()); + } + } + } + } +} + +void ImportWizardPageSelect::chooseImportFile() +{ + QString file; +#ifndef Q_OS_MACOS + // OPVault is a folder except on macOS + if (field("ImportType").toInt() == ImportWizard::IMPORT_OPVAULT) { + file = fileDialog()->getExistingDirectory(this, tr("Open OPVault"), QDir::homePath()); + } else { +#endif + file = fileDialog()->getOpenFileName(this, tr("Select import file"), QDir::homePath(), importFileFilter()); +#ifndef Q_OS_MACOS + } +#endif + + if (!file.isEmpty()) { + m_ui->importFileEdit->setText(file); + } +} + +void ImportWizardPageSelect::chooseKeyFile() +{ + auto filter = QString("%1 (*);;%2 (*.keyx; *.key)").arg(tr("All files"), tr("Key files")); + auto file = fileDialog()->getOpenFileName(this, tr("Select key file"), QDir::homePath(), filter); + if (!file.isEmpty()) { + m_ui->keyFileEdit->setText(file); + } +} + +void ImportWizardPageSelect::setCredentialState(bool passwordEnabled, bool keyFileEnable) +{ + bool passwordStateChanged = m_ui->passwordLabel->isVisible() != passwordEnabled; + m_ui->passwordLabel->setVisible(passwordEnabled); + m_ui->passwordEdit->setVisible(passwordEnabled); + + bool keyFileStateChanged = m_ui->keyFileLabel->isVisible() != keyFileEnable; + m_ui->keyFileLabel->setVisible(keyFileEnable); + m_ui->keyFileEdit->setVisible(keyFileEnable); + m_ui->keyFileButton->setVisible(keyFileEnable); + + // Workaround Qt bug where the wizard window is not updated when the internal layout changes + if (window()) { + int height = window()->height(); + if (passwordStateChanged) { + auto diff = m_ui->passwordEdit->height() + m_ui->inputFields->layout()->spacing(); + height += passwordEnabled ? diff : -diff; + } + if (keyFileStateChanged) { + auto diff = m_ui->keyFileEdit->height() + m_ui->inputFields->layout()->spacing(); + height += keyFileEnable ? diff : -diff; + } + window()->resize(window()->width(), height); + } +} + +QString ImportWizardPageSelect::importFileFilter() +{ + switch (field("ImportType").toInt()) { + case ImportWizard::IMPORT_CSV: + return QString("%1 (*.csv);;%2 (*)").arg(tr("Comma Separated Values"), tr("All files")); + case ImportWizard::IMPORT_OPUX: + return QString("%1 (*.1pux)").arg(tr("1Password Export")); + case ImportWizard::IMPORT_BITWARDEN: + return QString("%1 (*.json)").arg(tr("Bitwarden JSON Export")); + case ImportWizard::IMPORT_OPVAULT: + return QString("%1 (*.opvault)").arg(tr("1Password Vault")); + case ImportWizard::IMPORT_KEEPASS1: + return QString("%1 (*.kdb)").arg(tr("KeePass1 Database")); + default: + return {}; + } +} diff --git a/src/gui/wizard/ImportWizardPageSelect.h b/src/gui/wizard/ImportWizardPageSelect.h new file mode 100644 index 0000000000..029865a56a --- /dev/null +++ b/src/gui/wizard/ImportWizardPageSelect.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2023 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_IMPORTWIZARDPAGESELECT_H +#define KEEPASSXC_IMPORTWIZARDPAGESELECT_H + +#include +#include + +class QListWidgetItem; + +namespace Ui +{ + class ImportWizardPageSelect; +} + +class ImportWizardPageSelect : public QWizardPage +{ + Q_OBJECT + +public: + explicit ImportWizardPageSelect(QWidget* parent = nullptr); + Q_DISABLE_COPY(ImportWizardPageSelect) + ~ImportWizardPageSelect() override; + + void initializePage() override; + bool validatePage() override; + +private slots: + void itemSelected(QListWidgetItem* current, QListWidgetItem* previous); + void chooseImportFile(); + void chooseKeyFile(); + void updateDatabaseChoices() const; + +private: + QString importFileFilter(); + void setCredentialState(bool passwordEnabled, bool keyFileEnable = false); + + QScopedPointer m_ui; +}; + +#endif diff --git a/src/gui/wizard/ImportWizardPageSelect.ui b/src/gui/wizard/ImportWizardPageSelect.ui new file mode 100644 index 0000000000..6a7c8adad0 --- /dev/null +++ b/src/gui/wizard/ImportWizardPageSelect.ui @@ -0,0 +1,276 @@ + + + ImportWizardPageSelect + + + + 0 + 0 + 500 + 388 + + + + Form + + + Import File Selection + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 500 + 125 + + + + QAbstractScrollArea::AdjustToContents + + + QAbstractItemView::NoEditTriggers + + + true + + + QAbstractItemView::SelectRows + + + Qt::ElideNone + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 10 + + + + + + + + + 0 + 0 + + + + + 400 + 0 + + + + + QLayout::SetMinimumSize + + + + + Import File: + + + + + + + + + + + + Browse… + + + + + + + + + Password: + + + + + + + + + + Key File: + + + + + + + + + + + + Browse… + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 15 + + + + + + + + Import Into: + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + + 0 + 0 + + + + + 0 + 60 + + + + + + + + QLayout::SetMinimumSize + + + 6 + + + 6 + + + 6 + + + 6 + + + + + New Database + + + true + + + + + + + + + false + + + No unlocked databases available + + + Existing Database: + + + + + + + false + + + + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::MinimumExpanding + + + + 20 + 40 + + + + + + + + + PasswordWidget + QWidget +
gui/PasswordWidget.h
+ 1 +
+
+ + +
diff --git a/src/keeshare/CMakeLists.txt b/src/keeshare/CMakeLists.txt index 3adf272add..5f7a396585 100644 --- a/src/keeshare/CMakeLists.txt +++ b/src/keeshare/CMakeLists.txt @@ -13,8 +13,6 @@ if(WITH_XC_KEESHARE) ShareObserver.cpp ) - find_package(Minizip REQUIRED) - add_library(keeshare STATIC ${keeshare_SOURCES}) target_link_libraries(keeshare PUBLIC Qt5::Core Qt5::Widgets ${BOTAN_LIBRARIES} ${ZLIB_LIBRARIES} PRIVATE ${MINIZIP_LIBRARIES}) include_directories(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1181921c28..2285995b5c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -140,7 +140,7 @@ add_unit_test(NAME testdeletedobjects SOURCES TestDeletedObjects.cpp add_unit_test(NAME testkeepass1reader SOURCES TestKeePass1Reader.cpp LIBS ${TEST_LIBRARIES}) -add_unit_test(NAME testopvaultreader SOURCES TestOpVaultReader.cpp +add_unit_test(NAME testimports SOURCES TestImports.cpp LIBS ${TEST_LIBRARIES}) if(WITH_XC_NETWORKING) diff --git a/tests/TestImports.cpp b/tests/TestImports.cpp new file mode 100644 index 0000000000..a809cc3edc --- /dev/null +++ b/tests/TestImports.cpp @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2022 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 "TestImports.h" + +#include "config-keepassx-tests.h" +#include "core/Group.h" +#include "core/Metadata.h" +#include "core/Totp.h" +#include "crypto/Crypto.h" +#include "format/BitwardenReader.h" +#include "format/OPUXReader.h" +#include "format/OpVaultReader.h" + +#include +#include +#include + +QTEST_GUILESS_MAIN(TestImports) + +void TestImports::initTestCase() +{ + QVERIFY(Crypto::init()); +} + +void TestImports::testOPUX() +{ + auto opuxPath = QStringLiteral("%1/%2").arg(KEEPASSX_TEST_DATA_DIR, QStringLiteral("/1PasswordExport.1pux")); + + OPUXReader reader; + auto db = reader.convert(opuxPath); + QVERIFY2(!reader.hasError(), qPrintable(reader.errorString())); + QVERIFY(db); + + // Confirm specific entry details are valid + auto entry = db->rootGroup()->findEntryByPath("/Personal/Login"); + QVERIFY(entry); + QCOMPARE(entry->title(), QStringLiteral("Login")); + QCOMPARE(entry->username(), QStringLiteral("team@keepassxc.org")); + QCOMPARE(entry->password(), QStringLiteral("password")); + QCOMPARE(entry->url(), QStringLiteral("https://keepassxc.org")); + QCOMPARE(entry->notes(), QStringLiteral("Note to self")); + // Check extra URL's + QCOMPARE(entry->attribute("KP2A_URL_1"), QStringLiteral("https://twitter.com")); + // Check TOTP + QVERIFY(entry->hasTotp()); + QVERIFY(!entry->attribute("otp_1").isEmpty()); + // Check tags + QVERIFY(entry->tagList().contains("Favorite")); + QVERIFY(entry->tagList().contains("website")); + + // Check attachments + entry = db->rootGroup()->findEntryByPath("/Personal/KeePassXC Logo"); + auto attachments = entry->attachments(); + QCOMPARE(attachments->keys().count(), 1); + QCOMPARE(attachments->keys()[0], QString("keepassxc.png")); + + // Confirm advanced attributes + // NOTE: 1PUX does not support an explicit expiration field + entry = db->rootGroup()->findEntryByPath("/Personal/Credit Card"); + QVERIFY(entry); + auto tmpl = QString("Credit Card Fields_%1"); + auto attr = entry->attributes(); + QCOMPARE(attr->value(tmpl.arg("cardholder name")), QStringLiteral("KeePassXC")); + QCOMPARE(attr->value(tmpl.arg("expiry date")), QStringLiteral("202206")); + QCOMPARE(attr->value(tmpl.arg("verification number")), QStringLiteral("123")); + QVERIFY(attr->isProtected(tmpl.arg("verification number"))); + + // Confirm address fields + entry = db->rootGroup()->findEntryByPath("/Personal/Identity"); + QVERIFY(entry); + attr = entry->attributes(); + QCOMPARE(attr->value("Address_address"), QStringLiteral("123 Avenue Rd\nBoston, MA 12345\nus")); + + // Check archived entries + entry = db->rootGroup()->findEntryByPath("/Personal/Login Archived"); + QVERIFY(entry); + QVERIFY(entry->tagList().contains("Archived")); + + // Check vault to group structure + entry = db->rootGroup()->findEntryByPath("/Shared/Bank Account"); + QVERIFY(entry); + // Check custom group icon + QVERIFY(!entry->group()->iconUuid().isNull()); +} + +void TestImports::testOPVault() +{ + auto opVaultPath = QStringLiteral("%1/%2").arg(KEEPASSX_TEST_DATA_DIR, QStringLiteral("/keepassxc.opvault")); + + auto categories = QStringList({QStringLiteral("Login"), + QStringLiteral("Credit Card"), + QStringLiteral("Secure Note"), + QStringLiteral("Identity"), + QStringLiteral("Password"), + QStringLiteral("Tombstone"), + QStringLiteral("Software License"), + QStringLiteral("Bank Account"), + QStringLiteral("Database"), + QStringLiteral("Driver License"), + QStringLiteral("Outdoor License"), + QStringLiteral("Membership"), + QStringLiteral("Passport"), + QStringLiteral("Rewards"), + QStringLiteral("SSN"), + QStringLiteral("Router"), + QStringLiteral("Server"), + QStringLiteral("Email")}); + + QDir opVaultDir(opVaultPath); + + OpVaultReader reader; + auto db = reader.convert(opVaultDir, "a"); + QVERIFY2(!reader.hasError(), qPrintable(reader.errorString())); + QVERIFY(db); + + // Confirm specific entry details are valid + auto entry = db->rootGroup()->findEntryByPath("/Login/KeePassXC"); + QVERIFY(entry); + QCOMPARE(entry->title(), QStringLiteral("KeePassXC")); + QCOMPARE(entry->username(), QStringLiteral("keepassxc")); + QCOMPARE(entry->password(), QStringLiteral("opvault")); + QCOMPARE(entry->url(), QStringLiteral("https://www.keepassxc.org")); + QCOMPARE(entry->notes(), QStringLiteral("KeePassXC Account")); + // Check extra URL's + QCOMPARE(entry->attribute("KP2A_URL_1"), QStringLiteral("https://snapshot.keepassxc.org")); + // Check TOTP + QVERIFY(entry->hasTotp()); + // Check attachments + auto attachments = entry->attachments(); + QCOMPARE(attachments->keys().count(), 1); + QCOMPARE(*attachments->values().begin(), QByteArray("attachment")); + + // Confirm expired entries + entry = db->rootGroup()->findEntryByPath("/Login/Expired Login"); + QVERIFY(entry->isExpired()); + + // Confirm advanced attributes + entry = db->rootGroup()->findEntryByPath("/Credit Card/My Credit Card"); + QVERIFY(entry); + auto attr = entry->attributes(); + QCOMPARE(attr->value("cardholder name"), QStringLiteral("Team KeePassXC")); + QVERIFY(!attr->value("valid from").isEmpty()); + QCOMPARE(attr->value("Additional Details_PIN"), QStringLiteral("1234")); + QVERIFY(attr->isProtected("Additional Details_PIN")); + + // Confirm address fields + entry = db->rootGroup()->findEntryByPath("/Identity/Team KeePassXC"); + QVERIFY(entry); + attr = entry->attributes(); + QCOMPARE(attr->value("address_street"), QStringLiteral("123 Password Lane")); + + // Confirm complex passwords + entry = db->rootGroup()->findEntryByPath("/Password/Complex Password"); + QVERIFY(entry); + QCOMPARE(entry->password(), QStringLiteral("HfgcHjEL}iO}^3N!?*cv~O:9GJZQ0>oC")); + QVERIFY(entry->hasTotp()); + auto totpSettings = entry->totpSettings(); + QCOMPARE(totpSettings->digits, static_cast(8)); + QCOMPARE(totpSettings->step, static_cast(45)); + + // Add another OTP to this entry to confirm it doesn't overwrite the existing one + auto field = QJsonObject::fromVariantMap({{"n", "TOTP_SETTINGS"}, {"v", "otpauth://test.url?digits=6"}}); + reader.fillFromSectionField(entry, "", field); + QVERIFY(entry->hasTotp()); + totpSettings = entry->totpSettings(); + QCOMPARE(totpSettings->digits, static_cast(8)); + QCOMPARE(totpSettings->step, static_cast(45)); + QVERIFY(entry->attributes()->contains("otp_1")); + + // Confirm trashed entries are sent to the recycle bin + auto recycleBin = db->metadata()->recycleBin(); + QVERIFY(recycleBin); + QVERIFY(!recycleBin->isEmpty()); + QVERIFY(recycleBin->findEntryByPath("Trashed Password")); + + // Confirm created groups align with category names + for (const auto group : db->rootGroup()->children()) { + if (group == recycleBin) { + continue; + } + QVERIFY2(categories.contains(group->name()), + qPrintable(QStringLiteral("Invalid group name: %1").arg(group->name()))); + // Confirm each group is not empty + QVERIFY2(!group->isEmpty(), qPrintable(QStringLiteral("Group %1 is empty").arg(group->name()))); + } +} + +void TestImports::testBitwarden() +{ + auto bitwardenPath = QStringLiteral("%1/%2").arg(KEEPASSX_TEST_DATA_DIR, QStringLiteral("/bitwarden_export.json")); + + BitwardenReader reader; + auto db = reader.convert(bitwardenPath); + QVERIFY2(!reader.hasError(), qPrintable(reader.errorString())); + QVERIFY(db); + + // Confirm Login fields + auto entry = db->rootGroup()->findEntryByPath("/My Folder/Login Name"); + QVERIFY(entry); + QCOMPARE(entry->title(), QStringLiteral("Login Name")); + QCOMPARE(entry->username(), QStringLiteral("myusername@gmail.com")); + QCOMPARE(entry->password(), QStringLiteral("mypassword")); + QCOMPARE(entry->url(), QStringLiteral("https://mail.google.com")); + QCOMPARE(entry->notes(), QStringLiteral("1st line of note text\n2nd Line of note text")); + // Check extra URL's + QCOMPARE(entry->attribute("KP2A_URL_1"), QStringLiteral("https://google.com")); + QCOMPARE(entry->attribute("KP2A_URL_2"), QStringLiteral("https://gmail.com")); + // Check TOTP + QVERIFY(entry->hasTotp()); + // NOTE: Bitwarden does not export attachments + // NOTE: Bitwarden does not export expiration dates + + // Confirm Identity fields + entry = db->rootGroup()->findEntryByPath("/My Folder/My Identity"); + QVERIFY(entry); + auto attr = entry->attributes(); + // NOTE: The extra spaces are deliberate to test unmodified ingest of data + QCOMPARE(attr->value("identity_address"), + QStringLiteral(" 1 North Calle Cesar Chavez \nSanta Barbara, CA 93103\nUnited States ")); + QCOMPARE(attr->value("identity_name"), QStringLiteral("Mrs Jane A Doe")); + QCOMPARE(attr->value("identity_ssn"), QStringLiteral("123-12-1234")); + QVERIFY(attr->isProtected("identity_ssn")); + + // Confirm Secure Note + entry = db->rootGroup()->findEntryByPath("/My Folder/My Secure Note"); + QVERIFY(entry); + QCOMPARE(entry->notes(), + QStringLiteral("1st line of secure note\n2nd line of secure note\n3rd line of secure note")); + + // Confirm Credit Card + entry = db->rootGroup()->findEntryByPath("/Second Folder/Card Name"); + QVERIFY(entry); + attr = entry->attributes(); + QCOMPARE(attr->value("card_cardholderName"), QStringLiteral("Jane Doe")); + QCOMPARE(attr->value("card_number"), QStringLiteral("1234567891011121")); + QCOMPARE(attr->value("card_code"), QStringLiteral("123")); + QVERIFY(attr->isProtected("card_code")); +} + +void TestImports::testBitwardenEncrypted() +{ + // We already tested the parser so just test that decryption works properly + auto bitwardenPath = + QStringLiteral("%1/%2").arg(KEEPASSX_TEST_DATA_DIR, QStringLiteral("/bitwarden_encrypted_export.json")); + + BitwardenReader reader; + auto db = reader.convert(bitwardenPath, "a"); + if (reader.hasError()) { + QFAIL(qPrintable(reader.errorString())); + } + QVERIFY(db); +} diff --git a/src/gui/KeePass1OpenWidget.h b/tests/TestImports.h similarity index 64% rename from src/gui/KeePass1OpenWidget.h rename to tests/TestImports.h index 203b78632f..2e00de9a6a 100644 --- a/src/gui/KeePass1OpenWidget.h +++ b/tests/TestImports.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012 Felix Geyer + * Copyright (C) 2022 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 @@ -15,20 +15,21 @@ * along with this program. If not, see . */ -#ifndef KEEPASSX_KEEPASS1OPENWIDGET_H -#define KEEPASSX_KEEPASS1OPENWIDGET_H +#ifndef TEST_IMPORTS_H +#define TEST_IMPORTS_H -#include "gui/DatabaseOpenWidget.h" +#include -class KeePass1OpenWidget : public DatabaseOpenWidget +class TestImports : public QObject { Q_OBJECT -public: - explicit KeePass1OpenWidget(QWidget* parent = nullptr); - -protected: - void openDatabase() override; +private slots: + void initTestCase(); + void testOPUX(); + void testOPVault(); + void testBitwarden(); + void testBitwardenEncrypted(); }; -#endif // KEEPASSX_KEEPASS1OPENWIDGET_H +#endif /* TEST_IMPORTS_H */ diff --git a/tests/TestOpVaultReader.cpp b/tests/TestOpVaultReader.cpp deleted file mode 100644 index 4899d335fb..0000000000 --- a/tests/TestOpVaultReader.cpp +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (C) 2019 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 "TestOpVaultReader.h" - -#include "config-keepassx-tests.h" -#include "core/Group.h" -#include "core/Metadata.h" -#include "core/Totp.h" -#include "crypto/Crypto.h" -#include "format/OpVaultReader.h" - -#include -#include -#include -#include - -QTEST_GUILESS_MAIN(TestOpVaultReader) - -void TestOpVaultReader::initTestCase() -{ - QVERIFY(Crypto::init()); - - m_opVaultPath = QStringLiteral("%1/%2").arg(KEEPASSX_TEST_DATA_DIR, QStringLiteral("/keepassxc.opvault")); - - m_categories = QStringList({QStringLiteral("Login"), - QStringLiteral("Credit Card"), - QStringLiteral("Secure Note"), - QStringLiteral("Identity"), - QStringLiteral("Password"), - QStringLiteral("Tombstone"), - QStringLiteral("Software License"), - QStringLiteral("Bank Account"), - QStringLiteral("Database"), - QStringLiteral("Driver License"), - QStringLiteral("Outdoor License"), - QStringLiteral("Membership"), - QStringLiteral("Passport"), - QStringLiteral("Rewards"), - QStringLiteral("SSN"), - QStringLiteral("Router"), - QStringLiteral("Server"), - QStringLiteral("Email")}); -} - -void TestOpVaultReader::testReadIntoDatabase() -{ - QDir opVaultDir(m_opVaultPath); - - OpVaultReader reader; - QScopedPointer db(reader.readDatabase(opVaultDir, "a")); - QVERIFY(db); - QVERIFY2(!reader.hasError(), qPrintable(reader.errorString())); - - // Confirm specific entry details are valid - auto entry = db->rootGroup()->findEntryByPath("/Login/KeePassXC"); - QVERIFY(entry); - QCOMPARE(entry->title(), QStringLiteral("KeePassXC")); - QCOMPARE(entry->username(), QStringLiteral("keepassxc")); - QCOMPARE(entry->password(), QStringLiteral("opvault")); - QCOMPARE(entry->url(), QStringLiteral("https://www.keepassxc.org")); - QCOMPARE(entry->notes(), QStringLiteral("KeePassXC Account")); - // Check extra URL's - QCOMPARE(entry->attribute("KP2A_URL_1"), QStringLiteral("https://snapshot.keepassxc.org")); - // Check TOTP - QVERIFY(entry->hasTotp()); - // Check attachments - auto attachments = entry->attachments(); - QCOMPARE(attachments->keys().count(), 1); - QCOMPARE(*attachments->values().begin(), QByteArray("attachment")); - - // Confirm expired entries - entry = db->rootGroup()->findEntryByPath("/Login/Expired Login"); - QVERIFY(entry->isExpired()); - - // Confirm advanced attributes - entry = db->rootGroup()->findEntryByPath("/Credit Card/My Credit Card"); - QVERIFY(entry); - auto attr = entry->attributes(); - QCOMPARE(attr->value("cardholder name"), QStringLiteral("Team KeePassXC")); - QVERIFY(!attr->value("valid from").isEmpty()); - QCOMPARE(attr->value("Additional Details_PIN"), QStringLiteral("1234")); - QVERIFY(attr->isProtected("Additional Details_PIN")); - - // Confirm address fields - entry = db->rootGroup()->findEntryByPath("/Identity/Team KeePassXC"); - QVERIFY(entry); - attr = entry->attributes(); - QCOMPARE(attr->value("address_street"), QStringLiteral("123 Password Lane")); - - // Confirm complex passwords - entry = db->rootGroup()->findEntryByPath("/Password/Complex Password"); - QVERIFY(entry); - QCOMPARE(entry->password(), QStringLiteral("HfgcHjEL}iO}^3N!?*cv~O:9GJZQ0>oC")); - QVERIFY(entry->hasTotp()); - auto totpSettings = entry->totpSettings(); - QCOMPARE(totpSettings->digits, static_cast(8)); - QCOMPARE(totpSettings->step, static_cast(45)); - - // Add another OTP to this entry to confirm it doesn't overwrite the existing one - auto field = QJsonObject::fromVariantMap({{"n", "TOTP_SETTINGS"}, {"v", "otpauth://test.url?digits=6"}}); - reader.fillFromSectionField(entry, "", field); - QVERIFY(entry->hasTotp()); - totpSettings = entry->totpSettings(); - QCOMPARE(totpSettings->digits, static_cast(8)); - QCOMPARE(totpSettings->step, static_cast(45)); - QVERIFY(entry->attributes()->contains("otp_1")); - - // Confirm trashed entries are sent to the recycle bin - auto recycleBin = db->metadata()->recycleBin(); - QVERIFY(recycleBin); - QVERIFY(!recycleBin->isEmpty()); - QVERIFY(recycleBin->findEntryByPath("Trashed Password")); - - // Confirm created groups align with category names - for (const auto group : db->rootGroup()->children()) { - if (group == recycleBin) { - continue; - } - QVERIFY2(m_categories.contains(group->name()), - qPrintable(QStringLiteral("Invalid group name: %1").arg(group->name()))); - // Confirm each group is not empty - QVERIFY2(!group->isEmpty(), qPrintable(QStringLiteral("Group %1 is empty").arg(group->name()))); - } -} diff --git a/tests/data/1PasswordExport.1pux b/tests/data/1PasswordExport.1pux new file mode 100644 index 0000000000..509d7d07b7 Binary files /dev/null and b/tests/data/1PasswordExport.1pux differ diff --git a/tests/data/bitwarden_encrypted_export.json b/tests/data/bitwarden_encrypted_export.json new file mode 100644 index 0000000000..0f759686ce --- /dev/null +++ b/tests/data/bitwarden_encrypted_export.json @@ -0,0 +1,11 @@ +{ + "encrypted": true, + "passwordProtected": true, + "salt": "jxJdzv853aLmu0y/mCgSxg==", + "kdfType": 0, + "kdfIterations": 100000, + "kdfMemory": null, + "kdfParallelism": null, + "encKeyValidation_DO_NOT_EDIT": "2.6O5+RkqPRTCxGIjEIyqukQ==|J7Ks4QhjfOyt7qMU82XEuJoGw0GpQabv0vGerKC+YjSQWmaqbbyMDgba78vyRvSU|f0nwbY+JRc2KfkU6mY0dmiKNiDb00A0CngpF4TEEM0c=", + "data": "2.OA/bDI14kq+642rwmWYWxg==|216xw4kCZbqhVifzikzvzqLY2Er35tiYo+gl+hgf9dmLrPMf5vYcFgMe8IdTHXZCdUEuyEdpQeoAwGT8zG8d9GcwdKKOktccl04lE39Cb6XqKEr1PA3d4R8iPYTpeeFSm/cFLQlod5iymUkW4wxHTSIVn9KO/y0F8LWyKX7XxAdCEJykatSoUcC9SmFTPxEtR7BBgfkLTCgSZ9AUE0suKoYIUR6sJSlDq3IHP/09T8w0bbahBkRzevj5+JXawxn1DvBld00bVzo6GgrGojHz+VNa/crpLSaPqyR/IlD66+bS1DdIZ4UBODpZVZTxNKbWd7mPhkCcHF+NchCb47MR442dVQD9QjXk8q7E3SoK76JkYdOZsd3FIH8fZNdYTSOZsvLOYans74RyX1qCD5w3QVaR1cwRYD+kwCe0eFlHmzCLCx3IAuTfH1QdXvIvqaevYKikuKE8tfaAhrPJ2N4MHoKyxdF801jqslZdWrUrZvWsovdBZhp2siQZiWpd/fteJTSpy19sJ+J49+4SYEAfYe3lbz2K7ypKia8duffnV4+eh5tsK24MkExHO3ZQzQVkQdsX6eeFJmdqK4wONunSgnXIDti5rw/bWNtjVvAYiDMX+DNULML/opp9TPZUvrTgFhMsFFwbVzIjTxwE/PS9w+lD3etP195QkD717F87dClpkIrvm+UfQrQwCgDxOQ9PTPcUBVugq9MEflsiSn4NjCXdjWL1siSgxm2eJQ3k0OKJoN5bUInwG9i9njLh5fjxc4aiuvOOGAkqgX/mr3MxuuV8luKWRy884Reu8DdnZq6Vdq+yHgs7o9Ipxrtr8t85yBeU90yqyJrtmwDgEJiLK0TpJ1bY9ZwnqhYrtJGRrzxWrfvAGcJzEsWJ98aq9T+r/CnnsInyRTyptOgmsjmPlw0rTZ8//KI2afwRQRL+yIZ4817T+DekyF18QnYD607njiJb5igEy9MzYw04osr9yyQikheyuPDvD2UnGgGYdy04sHKVNgt0/xtzsJvxhsmy0mXknRcajExsKe6wH1jzTq6IxWo+08+5lnMmr05gtE8Y6HN2nsOAVdGU6x26MQVI|ASjBD0/F6Z61WSWc++RXhA6iQv8AziJO+/EXrj7GSVY=" +} \ No newline at end of file diff --git a/tests/data/bitwarden_export.json b/tests/data/bitwarden_export.json new file mode 100644 index 0000000000..7d6a5de219 --- /dev/null +++ b/tests/data/bitwarden_export.json @@ -0,0 +1,180 @@ +{ + "folders": [ + { + "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "name": "My Folder" + }, + { + "id": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy", + "name": "Second Folder" + } + ], + "items": [ + { + "id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa", + "organizationId": null, + "folderId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "type": 2, + "name": "My Secure Note", + "notes": "1st line of secure note\n2nd line of secure note\n3rd line of secure note", + "favorite": false, + "fields": [ + { + "name": "Text Field", + "value": "text-field-value", + "type": 0 + }, + { + "name": "Hidden Field", + "value": "hidden-field-value", + "type": 1 + }, + { + "name": "Boolean Field", + "value": "false", + "type": 2 + } + ], + "secureNote": { + "type": 0 + }, + "collectionIds": [ + null + ] + }, + { + "id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "organizationId": null, + "folderId": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy", + "type": 3, + "name": "Card Name", + "notes": "1st line of note text\n2nd line of note text", + "favorite": false, + "fields": [ + { + "name": "Text Field", + "value": "text-field-value", + "type": 0 + }, + { + "name": "Hidden Field", + "value": "hidden-field-value", + "type": 1 + }, + { + "name": "Boolean Field", + "value": "false", + "type": 2 + } + ], + "card": { + "cardholderName": "Jane Doe", + "brand": "Visa", + "number": "1234567891011121", + "expMonth": "10", + "expYear": "2021", + "code": "123" + }, + "collectionIds": [ + null + ] + }, + { + "id": "cccccccc-cccc-cccc-cccc-cccccccccccc", + "organizationId": null, + "folderId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "type": 4, + "name": "My Identity", + "notes": "1st line of a note\n2nd line of a note", + "favorite": false, + "fields": [ + { + "name": "Text Field", + "value": "text-field-value", + "type": 0 + }, + { + "name": "Hidden Field", + "value": "hidden-field-value", + "type": 1 + }, + { + "name": "Boolean Field", + "value": "true", + "type": 2 + } + ], + "identity": { + "title": "Mrs", + "firstName": "Jane", + "middleName": "A", + "lastName": "Doe", + "address1": " 1 North Calle Cesar Chavez ", + "address2": null, + "address3": null, + "city": "Santa Barbara", + "state": "CA", + "postalCode": "93103", + "country": "United States ", + "company": "My Employer", + "email": "myemail@gmail.com", + "phone": "123-123-1234", + "ssn": "123-12-1234", + "username": "myusername", + "passportNumber": "123456789", + "licenseNumber": "123456789" + }, + "collectionIds": [ + null + ] + }, + { + "id": "dddddddd-dddd-dddd-dddd-dddddddddddd", + "organizationId": null, + "folderId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "type": 1, + "name": "Login Name", + "notes": "1st line of note text\n2nd Line of note text", + "favorite": true, + "fields": [ + { + "name": "Text Field", + "value": "text-field-value", + "type": 0 + }, + { + "name": "Hidden Field", + "value": "hidden-field-value", + "type": 1 + }, + { + "name": "Boolean Field", + "value": "true", + "type": 2 + } + ], + "login": { + "uris": [ + { + "match": null, + "uri": "https://mail.google.com" + }, + { + "match": null, + "uri": "https://google.com" + }, + { + "match": null, + "uri": "https://gmail.com" + } + ], + "username": "myusername@gmail.com", + "password": "mypassword", + "totp": "otpauth://totp/Google:myusername%40gmail.com?secret=DFDFDEF%3D&period=30&digits=6&issuer=Google" + }, + "collectionIds": [ + null + ] + } + ] +} diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 3e6c7c8002..e411f36937 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -1489,28 +1489,6 @@ void TestGui::testDatabaseSettings() checkSaveDatabase(); } -void TestGui::testKeePass1Import() -{ - fileDialog()->setNextFileName(QString(KEEPASSX_TEST_DATA_DIR).append("/basic.kdb")); - triggerAction("actionImportKeePass1"); - - auto* keepass1OpenWidget = m_tabWidget->currentDatabaseWidget()->findChild("keepass1OpenWidget"); - auto* editPassword = - keepass1OpenWidget->findChild("editPassword")->findChild("passwordEdit"); - QVERIFY(editPassword); - - QTest::keyClicks(editPassword, "masterpw"); - QTest::keyClick(editPassword, Qt::Key_Enter); - - QTRY_COMPARE(m_tabWidget->count(), 2); - QTRY_COMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), QString("basic [New Database]*")); - - // Close the KeePass1 Database - MessageBox::setNextAnswer(MessageBox::No); - triggerAction("actionDatabaseClose"); - QApplication::processEvents(); -} - void TestGui::testDatabaseLocking() { QString origDbName = m_tabWidget->tabText(0); diff --git a/tests/gui/TestGui.h b/tests/gui/TestGui.h index c631e5bdc8..fa3d613f9b 100644 --- a/tests/gui/TestGui.h +++ b/tests/gui/TestGui.h @@ -61,7 +61,6 @@ private slots: void testSaveBackupPath(); void testSaveBackupPath_data(); void testDatabaseSettings(); - void testKeePass1Import(); void testDatabaseLocking(); void testDragAndDropKdbxFiles(); void testSortGroups();