Add 1Password 1PUX and Bitwarden JSON Importers

* Closes #7545 - Support 1Password 1PUX import format based on https://support.1password.com/1pux-format/

* Closes #8367 - Support Bitwarden JSON import format (both unencrypted and encrypted) based on https://bitwarden.com/help/encrypted-export/

* Fixes #9577 - OPVault import when fields have the same name or type

* Introduce the import wizard to handle all import tasks (CSV, KDBX1, OPVault, 1PUX, JSON)

* Clean up CSV parser code to make it much more efficient and easier to read

* Combine all importer tests (except CSV) into one test file
This commit is contained in:
Jonathan White 2023-08-27 21:54:53 -04:00
parent a02bceabd2
commit e700195f0a
70 changed files with 3562 additions and 1876 deletions

View File

@ -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})

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View File

@ -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)
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.
.Import Wizard
image::import_wizard.png[]
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.
=== 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.
WARNING: A CSV file is unencrypted and you should securely delete this file after successfully importing it into KeePassXC.
To open the CSV file, perform the following steps:
1. Follow the steps above and click `Continue`. The CSV import wizard will appear.
1. Open KeePassXC.
2. Click Import from CSV button on the welcome screen or use the menu Database > Import > CSV File.
3. Navigate to the location of 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.
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.
1. Open the Import Wizard as shown above. Select the Bitwarden option.
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.
3. Click `Continue` to unlock and preview the import. Click `Done` to complete the import.
=== 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.
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.
To import a KeePass 1 database file in KeePassXC, perform the following steps:
1. Open KeePassXC.
1. Open the Import Wizard as shown above. Select the KeePass1 Database option.
2. Click Import from KeePass 1 button on the welcome screen or use the menu Database > Import > KeePass 1 Database.
2. Enter the password for your database and optionally provide a key file if it was configured for your KeePass1 database.
3. Navigate to the location of 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.
4. Enter the password for your old `.kdb` file and click *OK*. You are prompted to provide a name for the new database format that KeePassXC recognizes.
5. Provide a name for the new database format, select a folder on your computer to save the file, and click Save.
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.

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21,11C21,16.55 17.16,21.74 12,23C6.84,21.74 3,16.55 3,11V5L12,1L21,5V11M12,21C15.75,20 19,15.54 19,11.22V6.3L12,3.18V21Z" /></svg>

After

Width:  |  Height:  |  Size: 200 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14 2H6C4.9 2 4 2.9 4 4V20C4 21.1 4.9 22 6 22H18C19.1 22 20 21.1 20 20V8L14 2M18 20H6V4H13V9H18V20M10 19L12 15H9V10H15V15L13 19H10" /></svg>

After

Width:  |  Height:  |  Size: 209 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12,1C5.92,1 1,5.92 1,12C1,18.08 5.92,23 12,23C18.08,23 23,18.08 23,12C23,5.92 18.08,1 12,1M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20M13,13.5C13,14.13 13.4,14.7 14,14.91V18H10V11.91C10.78,11.64 11.19,10.8 10.93,10C10.78,9.58 10.44,9.24 10,9.09V6H14V12.09C13.4,12.3 13,12.87 13,13.5Z" /></svg>

After

Width:  |  Height:  |  Size: 387 B

View File

@ -8,11 +8,13 @@
<file>application/scalable/actions/application-exit.svg</file>
<file>application/scalable/actions/attributes-copy.svg</file>
<file>application/scalable/actions/auto-type.svg</file>
<file>application/scalable/actions/bitwarden.svg</file>
<file>application/scalable/actions/bugreport.svg</file>
<file>application/scalable/actions/chevron-double-down.svg</file>
<file>application/scalable/actions/chevron-double-right.svg</file>
<file>application/scalable/actions/clipboard-text.svg</file>
<file>application/scalable/actions/configure.svg</file>
<file>application/scalable/actions/csv.svg</file>
<file>application/scalable/actions/database-change-key.svg</file>
<file>application/scalable/actions/database-lock.svg</file>
<file>application/scalable/actions/database-lock-all.svg</file>
@ -58,6 +60,7 @@
<file>application/scalable/actions/move-up.svg</file>
<file>application/scalable/actions/object-locked.svg</file>
<file>application/scalable/actions/object-unlocked.svg</file>
<file>application/scalable/actions/onepassword.svg</file>
<file>application/scalable/actions/paperclip.svg</file>
<file>application/scalable/actions/passkey.svg</file>
<file>application/scalable/actions/password-copy.svg</file>

View File

@ -1256,14 +1256,6 @@ Do you want to overwrite the Passkey in %1 - %2?</source>
</context>
<context>
<name>CsvImportWidget</name>
<message>
<source>Import CSV fields</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>filename</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>size, rows, columns</source>
<translation type="unfinished"></translation>
@ -1372,18 +1364,6 @@ Do you want to overwrite the Passkey in %1 - %2?</source>
<source>Column %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Imported from CSV file</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Original data: </source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Error(s) detected in CSV file!</source>
<translation type="unfinished"></translation>
</message>
<message numerus="yes">
<source>[%n more message(s) skipped]</source>
<translation type="unfinished">
@ -1392,31 +1372,19 @@ Do you want to overwrite the Passkey in %1 - %2?</source>
</translation>
</message>
<message>
<source>Error</source>
<source>Failed to parse CSV file: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>CSV import: writer has errors:
%1</source>
<source>Imported from CSV file: %1</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>CsvParserModel</name>
<message>
<source>%1, %2, %3</source>
<comment>file info: bytes, rows, columns</comment>
<translation type="unfinished"></translation>
</message>
<message numerus="yes">
<source>%n byte(s)</source>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
</translation>
</message>
<message numerus="yes">
<source>%n row(s)</source>
<comment>CSV row count</comment>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
@ -1424,6 +1392,7 @@ Do you want to overwrite the Passkey in %1 - %2?</source>
</message>
<message numerus="yes">
<source>%n column(s)</source>
<comment>CSV column count</comment>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
@ -1800,6 +1769,10 @@ Permissions to access entries will be revoked.</source>
This is only necessary if your database is a copy of another and the browser extension cannot connect.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Convert legacy KeePassHTTP attributes to KeePassXC-Browser compatible custom data</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No keys found</source>
<translation type="unfinished"></translation>
@ -2287,26 +2260,10 @@ This is definitely a bug, please report it to the developers.</source>
<source>CSV file</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Select CSV file</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Merge database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>KeePass 1 database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Open KeePass 1 database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Open OPVault</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Export database to CSV file</source>
<translation type="unfinished"></translation>
@ -2339,15 +2296,6 @@ This is definitely a bug, please report it to the developers.</source>
<source>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?</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>New Database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>%1 [New Database]</source>
<comment>Database tab name modifier</comment>
<translation type="unfinished"></translation>
</message>
<message>
<source>%1 [Locked]</source>
<comment>Database tab name modifier</comment>
@ -2536,6 +2484,15 @@ Disable safe saves and try again?</source>
<source>Could not find database file: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>New Database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>%1 [New Database]</source>
<comment>Database tab name modifier</comment>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>EditEntryWidget</name>
@ -4299,6 +4256,147 @@ You can enable the DuckDuckGo website icon service in the security section of th
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ImportWizard</name>
<message>
<source>Import Wizard</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ImportWizardPageReview</name>
<message>
<source>WizardPage</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Entry count: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Group</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Title</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Username</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Password</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Url</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ImportWizardPageSelect</name>
<message>
<source>Form</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Import File Selection</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Password:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Key File:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Browse</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Import Into:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>New Database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No unlocked databases available</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Existing Database:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Import File:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Comma Separated Values (.csv)</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>1Password Export (.1pux)</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>1Password Vault (.opvault)</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bitwarden (.json)</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>KeePass 1 Database (.kdb)</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Open OPVault</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Select import file</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>All files</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Key files</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Select key file</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Comma Separated Values</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>1Password Export</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bitwarden JSON Export</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>1Password Vault</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>KeePass1 Database</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>KMessageWidget</name>
<message>
@ -4726,17 +4824,6 @@ Line %2, column %3</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>KeePass1OpenWidget</name>
<message>
<source>Import KeePass1 Database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unable to open the database.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>KeePass1Reader</name>
<message>
@ -5090,10 +5177,6 @@ Are you sure you want to continue with this file?</source>
<source>&amp;Recent Databases</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&amp;Import</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&amp;Export</source>
<translation type="unfinished"></translation>
@ -5500,6 +5583,18 @@ We recommend you use the AppImage available on our downloads page.</source>
<source>Allow Screen Capture</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>1Password 1PUX...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Import a 1Password 1PUX file</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Import</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Passkeys</source>
<translation type="unfinished"></translation>
@ -5948,14 +6043,6 @@ We recommend you use the AppImage available on our downloads page.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>OpVaultOpenWidget</name>
<message>
<source>Read Database did not produce an instance
%1</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>OpVaultReader</name>
<message>
@ -8485,6 +8572,76 @@ Kernel: %3 %4</source>
<source>Failed to decrypt key data.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Favorite</source>
<comment>Tag for favorite entries</comment>
<translation type="unfinished"></translation>
</message>
<message>
<source>File does not exist.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Cannot open file: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Cannot parse file: %1 at position %2</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to decrypt json file: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Invalid encKeyValidation field</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Invalid cipher list within encKeyValidation field</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Wrong password</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Invalid encrypted data field</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Invalid cipher list within encrypted data field</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Cannot initialize cipher</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Cannot decrypt data</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bitwarden Import</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Archived</source>
<comment>Tag for archived entries</comment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Invalid 1PUX file format: Not a valid ZIP file.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Invalid 1PUX file format: Missing export.data</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>1Password Import</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Origin is empty or not allowed</source>
<translation type="unfinished"></translation>
@ -9577,26 +9734,6 @@ Example: JBSWY3DPEHPK3PXP</source>
<source>Start storing your passwords securely in a KeePassXC database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Create new database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Open existing database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Import from KeePass 1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Import from 1Password</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Import from CSV</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Recent databases</source>
<translation type="unfinished"></translation>
@ -9609,6 +9746,18 @@ Example: JBSWY3DPEHPK3PXP</source>
<source>Welcome to KeePassXC %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Create Database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Open Database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Import File</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>WinUtils</name>

View File

@ -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
@ -118,12 +120,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
@ -139,7 +139,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
@ -183,6 +182,9 @@ set(keepassx_SOURCES
gui/widgets/KPToolBar.cpp
gui/widgets/PopupHelpWidget.cpp
gui/widgets/ShortcutWidget.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
@ -390,6 +392,7 @@ target_link_libraries(keepassx_core
${PCSC_LIBRARIES}
${ZXCVBN_LIBRARIES}
${ZLIB_LIBRARIES}
${MINIZIP_LIBRARIES}
${ARGON2_LIBRARIES}
${KEYUTILS_LIBRARIES}
${thirdparty_LIBRARIES}

View File

@ -21,6 +21,7 @@
#include <QApplication>
#include <QDebug>
#include <QPluginLoader>
#include <QRegularExpression>
#include <QUrl>
#include "config-keepassx.h"

View File

@ -0,0 +1,313 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "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 <botan/kdf.h>
#include <botan/pwdhash.h>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QMap>
#include <QScopedPointer>
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> 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<Database> 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<QString, Group*> 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<Database> 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<uint8_t*>(key.data()),
key.size(),
password.toUtf8().data(),
password.toUtf8().size(),
reinterpret_cast<uint8_t*>(salt.data()),
salt.size());
// Derive the MAC Key
auto stretched_mac = kdf->derive_key(32, reinterpret_cast<const uint8_t*>(key.data()), key.size(), "", "mac");
auto mac = QByteArray(reinterpret_cast<const char*>(stretched_mac.data()), stretched_mac.size());
// Stretch the Master Key
auto stretched_key = kdf->derive_key(32, reinterpret_cast<const uint8_t*>(key.data()), key.size(), "", "enc");
key = QByteArray(reinterpret_cast<const char*>(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<Database>::create();
db->rootGroup()->setName(QObject::tr("Bitwarden Import"));
writeVaultToDatabase(json, db);
return db;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -15,31 +15,29 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef TEST_OPVAULT_READER_H_
#define TEST_OPVAULT_READER_H_
#ifndef BITWARDEN_READER_H
#define BITWARDEN_READER_H
#include <QMap>
#include <QObject>
#include <QSharedPointer>
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;
private slots:
void initTestCase();
void testReadIntoDatabase();
QSharedPointer<Database> convert(const QString& path, const QString& password = {});
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

View File

@ -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;
}
} 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;
}
}
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;
}
}
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

View File

@ -47,7 +47,7 @@ public:
int getCsvRows() const;
int getCsvCols() const;
QString getStatus() const;
const CsvTable getCsvTable() const;
CsvTable getCsvTable() const;
protected:
CsvTable m_table;
@ -74,15 +74,9 @@ private:
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();

View File

@ -18,6 +18,7 @@
#include "KeePass1Reader.h"
#include <QFile>
#include <QFileInfo>
#include <QTextCodec>
#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;
}

288
src/format/OPUXReader.cpp Normal file
View File

@ -0,0 +1,288 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "OPUXReader.h"
#include "core/Database.h"
#include "core/Entry.h"
#include "core/Group.h"
#include "core/Metadata.h"
#include "core/Totp.h"
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QScopedPointer>
#include <QUrl>
#include <minizip/unzip.h>
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> 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<Database> 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<Database> 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<Database>::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;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -15,20 +15,29 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_OPVAULTOPENWIDGET_H
#define KEEPASSXC_OPVAULTOPENWIDGET_H
#ifndef OPUX_READER_H
#define OPUX_READER_H
#include "gui/DatabaseOpenWidget.h"
#include <QSharedPointer>
class OpVaultOpenWidget : public DatabaseOpenWidget
class Database;
/*!
* Imports a 1Password vault in 1PUX format: https://support.1password.com/1pux-format/
*/
class OPUXReader
{
Q_OBJECT
public:
explicit OpVaultOpenWidget(QWidget* parent = nullptr);
explicit OPUXReader() = default;
~OPUXReader() = default;
protected:
void openDatabase() override;
QSharedPointer<Database> convert(const QString& path);
bool hasError();
QString errorString();
private:
QString m_error;
};
#endif // KEEPASSXC_OPVAULTOPENWIDGET_H
#endif // OPUX_READER_H

View File

@ -31,68 +31,49 @@
OpVaultReader::OpVaultReader(QObject* parent)
: QObject(parent)
, m_error(false)
{
}
OpVaultReader::~OpVaultReader() = default;
Database* OpVaultReader::readDatabase(QDir& opdataDir, const QString& password)
QSharedPointer<Database> 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<CompositeKey>::create();
key->addKey(QSharedPointer<PasswordKey>::create(password));
QScopedPointer<Database> 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<Database>::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"));
@ -100,7 +81,7 @@ Database* OpVaultReader::readDatabase(QDir& opdataDir, const QString& password)
QJsonObject foldersJs = readAndAssertJsonFile(foldersJsFile, "loadFolders(", ");");
if (!processFolderJson(foldersJs, rootGroup)) {
zeroKeys();
return nullptr;
return {};
}
}
@ -150,17 +131,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)
@ -182,38 +163,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;
}
@ -338,15 +310,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();
@ -364,7 +334,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);
@ -383,7 +352,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 {
@ -395,8 +363,7 @@ OpVaultReader::deriveKeysFromPassPhrase(QByteArray& salt, const QString& passwor
reinterpret_cast<const uint8_t*>(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;
}

View File

@ -39,7 +39,7 @@ public:
explicit OpVaultReader(QObject* parent = nullptr);
~OpVaultReader() override;
Database* readDatabase(QDir& opdataDir, const QString& password);
QSharedPointer<Database> convert(QDir& opdataDir, const QString& password);
bool hasError();
QString errorString();
@ -49,8 +49,7 @@ private:
{
QByteArray encrypt;
QByteArray hmac;
bool error;
QString errorStr;
QString error;
};
QJsonObject readAndAssertJsonFile(QFile& file, const QString& stripLeading, const QString& stripTrailing);
@ -106,15 +105,14 @@ private:
/*! 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_ */

View File

@ -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);
}

View File

@ -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"));
}
}

View File

@ -18,6 +18,8 @@
#ifndef KEEPASSX_CLONEDIALOG_H
#define KEEPASSX_CLONEDIALOG_H
#include <QDialog>
#include "core/Database.h"
#include "gui/DatabaseWidget.h"

View File

@ -21,6 +21,7 @@
#include <QTabBar>
#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)
@ -250,24 +251,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()
@ -289,44 +318,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<Database>::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<Database>::create();
auto* dbWidget = new DatabaseWidget(db, this);
addDatabaseTab(dbWidget);
dbWidget->switchToImportOpVault(fileName);
}
/**
* Attempt to close the current database and remove its tab afterwards.
*
@ -611,43 +602,18 @@ bool DatabaseTabWidget::hasLockableDatabases() const
*/
QString DatabaseTabWidget::tabName(int index)
{
if (index == -1 || index > count()) {
return "";
auto dbWidget = databaseWidgetFromIndex(index);
if (!dbWidget) {
return {};
}
auto* dbWidget = databaseWidgetFromIndex(index);
auto db = dbWidget->database();
Q_ASSERT(db);
if (!db) {
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("*");
}
@ -670,6 +636,7 @@ void DatabaseTabWidget::updateTabName(int index)
}
index = indexOf(dbWidget);
setTabText(index, tabName(index));
setTabToolTip(index, dbWidget->displayFilePath());
emit tabNameChanged();
}

View File

@ -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);

View File

@ -30,20 +30,20 @@
#include <QSplitter>
#include <QTextDocumentFragment>
#include <QTextEdit>
#include <core/Tools.h>
#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<Database> 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<Database> 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<Database> 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<Database> 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
@ -273,10 +261,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;
}
@ -327,6 +313,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<Config::ConfigKey, QList<int>> DatabaseWidget::splitterSizes() const
{
return {{Config::GUI_SplitterState, m_mainSplitter->sizes()},
@ -1341,33 +1366,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();

View File

@ -19,35 +19,29 @@
#ifndef KEEPASSX_DATABASEWIDGET_H
#define KEEPASSX_DATABASEWIDGET_H
#include <QFileSystemWatcher>
#include <QListView>
#include <QBuffer>
#include <QStackedWidget>
#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 @@ public:
enum class Mode
{
None,
ImportMode,
ViewMode,
EditMode,
LockedMode
@ -104,6 +97,10 @@ public:
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
@ -288,15 +281,12 @@ private:
QPointer<QSplitter> m_previewSplitter;
QPointer<QLabel> m_searchingLabel;
QPointer<ElidedLabel> m_shareLabel;
QPointer<CsvImportWizard> m_csvImportWizard;
QPointer<EditEntryWidget> m_editEntryWidget;
QPointer<EditGroupWidget> m_editGroupWidget;
QPointer<EditEntryWidget> m_historyEditEntryWidget;
QPointer<ReportsDialog> m_reportsDialog;
QPointer<DatabaseSettingsDialog> m_databaseSettingDialog;
QPointer<DatabaseOpenWidget> m_databaseOpenWidget;
QPointer<KeePass1OpenWidget> m_keepass1OpenWidget;
QPointer<OpVaultOpenWidget> m_opVaultOpenWidget;
QPointer<GroupView> m_groupView;
QPointer<TagView> m_tagView;
QPointer<EntryView> m_entryView;

View File

@ -31,7 +31,7 @@
#endif
#include <QScrollBar>
#include <QTabWidget>
namespace
{
constexpr int GeneralTabIndex = 0;

View File

@ -26,6 +26,7 @@ namespace Ui
class EntryPreviewWidget;
}
class QTabWidget;
class QTextEdit;
class EntryPreviewWidget : public QWidget

View File

@ -18,6 +18,7 @@
#include "Icons.h"
#include <QBuffer>
#include <QIconEngine>
#include <QImageReader>
#include <QPaintDevice>
@ -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"

View File

@ -1,63 +0,0 @@
/*
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "KeePass1OpenWidget.h"
#include "ui_DatabaseOpenWidget.h"
#include <QFileInfo>
#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);
}
}

View File

@ -354,7 +354,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"));
@ -464,9 +464,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()));
@ -532,9 +530,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()));
@ -863,7 +859,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());
@ -977,7 +973,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();
@ -1291,24 +1286,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);

View File

@ -24,6 +24,7 @@
#include <QMainWindow>
#include <QProgressBar>
#include <QSystemTrayIcon>
#include <QTimer>
#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);

View File

@ -231,14 +231,6 @@
<string>&amp;Recent Databases</string>
</property>
</widget>
<widget class="QMenu" name="menuImport">
<property name="title">
<string>&amp;Import</string>
</property>
<addaction name="actionImportCsv"/>
<addaction name="actionImportOpVault"/>
<addaction name="actionImportKeePass1"/>
</widget>
<widget class="QMenu" name="menuExport">
<property name="title">
<string>&amp;Export</string>
@ -266,7 +258,7 @@
<addaction name="actionImportPasskey"/>
<addaction name="separator"/>
<addaction name="actionDatabaseMerge"/>
<addaction name="menuImport"/>
<addaction name="actionImport"/>
<addaction name="menuExport"/>
<addaction name="separator"/>
<addaction name="actionQuit"/>
@ -1271,6 +1263,19 @@
<string>Toggle Allow Screen Capture</string>
</property>
</action>
<action name="actionImport1PUX">
<property name="text">
<string>1Password 1PUX...</string>
</property>
<property name="toolTip">
<string>Import a 1Password 1PUX file</string>
</property>
</action>
<action name="actionImport">
<property name="text">
<string>Import…</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>

View File

@ -1,50 +0,0 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "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();
}
}

View File

@ -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;

View File

@ -19,6 +19,8 @@
#ifndef KEEPASSX_TOTPDIALOG_H
#define KEEPASSX_TOTPDIALOG_H
#include <QDialog>
#include "core/Database.h"
#include "gui/DatabaseWidget.h"

View File

@ -25,6 +25,7 @@
#include <QBoxLayout>
#include <QBuffer>
#include <QDialogButtonBox>
#include <QLabel>
#include <QMessageBox>
#include <QPushButton>

View File

@ -18,6 +18,8 @@
#ifndef KEEPASSX_TotpExportSettingsDialog_H
#define KEEPASSX_TotpExportSettingsDialog_H
#include <QDialog>
#include "core/Database.h"
#include "gui/DatabaseWidget.h"

View File

@ -19,6 +19,8 @@
#ifndef KEEPASSX_SETUPTOTPDIALOG_H
#define KEEPASSX_SETUPTOTPDIALOG_H
#include <QDialog>
#include "core/Database.h"
#include "gui/DatabaseWidget.h"

View File

@ -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,

View File

@ -39,9 +39,7 @@ signals:
void newDatabase();
void openDatabase();
void openDatabaseFile(QString);
void importKeePass1Database();
void importOpVaultDatabase();
void importCsv();
void importFile();
protected:
void keyPressEvent(QKeyEvent* event) override;

View File

@ -70,6 +70,22 @@
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="welcomeLabel">
<property name="alignment">
@ -103,40 +119,26 @@
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="buttonNewDatabase">
<property name="text">
<string>Create new database</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonOpenDatabase">
<property name="text">
<string>Open existing database</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="buttonImportKeePass1">
<widget class="QPushButton" name="buttonNewDatabase">
<property name="text">
<string>Import from KeePass 1</string>
<string>Create Database</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonImportOpVault">
<widget class="QPushButton" name="buttonOpenDatabase">
<property name="text">
<string>Import from 1Password</string>
<string>Open Database</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonImportCSV">
<widget class="QPushButton" name="buttonImport">
<property name="text">
<string>Import from CSV</string>
<string>Import File</string>
</property>
</widget>
</item>
@ -148,12 +150,12 @@
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>5</height>
<height>20</height>
</size>
</property>
</spacer>
@ -193,11 +195,7 @@
</layout>
</widget>
<tabstops>
<tabstop>buttonNewDatabase</tabstop>
<tabstop>buttonOpenDatabase</tabstop>
<tabstop>buttonImportKeePass1</tabstop>
<tabstop>buttonImportOpVault</tabstop>
<tabstop>buttonImportCSV</tabstop>
<tabstop>buttonImport</tabstop>
<tabstop>recentListWidget</tabstop>
</tabstops>
<resources/>

View File

@ -1,4 +1,4 @@
/*
/*
* Copyright (C) 2016 Enrico Mariotti <enricomariotti@yahoo.it>
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
*
@ -19,38 +19,69 @@
#include "CsvImportWidget.h"
#include "ui_CsvImportWidget.h"
#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/csvImport/CsvParserModel.h"
#include <QStringListModel>
#include "core/Clock.h"
#include "core/Totp.h"
#include "format/KeePass2Writer.h"
#include "gui/MessageBox.h"
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<QComboBox*>(sender())));
m_parserModel->mapColumns(index - 1, m_combos.indexOf(qobject_cast<QComboBox*>(sender())));
updateTableview();
}
@ -92,68 +120,81 @@ CsvImportWidget::~CsvImportWidget() = default;
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);
int j = 1;
for (QComboBox* b : m_combos) {
if (j < m_parserModel->getCsvCols()) {
b->setCurrentIndex(j);
} else {
b->setCurrentIndex(0);
// 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;
}
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();
}
@ -161,42 +202,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<Database> 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<Database>::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;
}
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());
@ -255,99 +287,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);
return db;
}
void CsvImportWidget::setRootGroup()
QString CsvImportWidget::formatStatusText() const
{
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();
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));
}
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");
if (items == 1) {
text.append(QString("\n"));
}
}
Group* CsvImportWidget::splitGroups(const QString& label)
{
// 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) {
auto brandNew = new Group();
brandNew->setParent(current);
brandNew->setName(groupName);
brandNew->setUuid(QUuid::createUuid());
current = brandNew;
} else {
Q_ASSERT(children != nullptr);
current = children;
}
}
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;
}
}
return nullptr;
}
void CsvImportWidget::reject()
{
emit editFinished(false);
return text;
}

View File

@ -19,12 +19,13 @@
#ifndef KEEPASSX_CSVIMPORTWIDGET_H
#define KEEPASSX_CSVIMPORTWIDGET_H
#include <QComboBox>
#include <QStringListModel>
#include <QWidget>
#include "core/Metadata.h"
#include "gui/csvImport/CsvParserModel.h"
class QStringListModel;
class CsvParserModel;
class Database;
class Group;
class QComboBox;
namespace Ui
{
@ -38,35 +39,35 @@ class CsvImportWidget : public QWidget
public:
explicit CsvImportWidget(QWidget* parent = nullptr);
~CsvImportWidget() override;
void load(const QString& filename, Database* const db);
void load(const QString& filename);
QSharedPointer<Database> 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<Ui::CsvImportWidget> m_ui;
CsvParserModel* const m_parserModel;
QStringListModel* const m_comboModel;
QList<QComboBox*> 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<Ui::CsvImportWidget> m_ui;
CsvParserModel* m_parserModel;
QStringListModel* m_comboModel;
QList<QComboBox*> m_combos;
QStringList m_columnHeader;
QStringList m_fieldSeparatorList;
QString m_filename;
bool m_buildingPreview = false;
Q_DISABLE_COPY(CsvImportWidget)
};
#endif // KEEPASSX_CSVIMPORTWIDGET_H

File diff suppressed because it is too large Load Diff

View File

@ -1,43 +0,0 @@
/*
* Copyright (C) 2016 Enrico Mariotti <enricomariotti@yahoo.it>
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "CsvImportWizard.h"
#include <QGridLayout>
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() = default;
void CsvImportWizard::load(const QString& filename, Database* database)
{
m_db = database;
m_parse->load(filename, database);
}
void CsvImportWizard::parseFinished(bool accepted)
{
emit importFinished(accepted);
}

View File

@ -1,51 +0,0 @@
/*
* Copyright (C) 2016 Enrico Mariotti <enricomariotti@yahoo.it>
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef 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<Database> m_db;
CsvImportWidget* m_parse;
QGridLayout* m_layout;
};
#endif // KEEPASSX_CSVIMPORTWIZARD_H

View File

@ -18,16 +18,25 @@
#include "CsvParserModel.h"
#include "core/Tools.h"
#include "format/CsvParser.h"
#include <QFile>
CsvParserModel::CsvParserModel(QObject* parent)
: QAbstractTableModel(parent)
, m_parser(new CsvParser())
, m_skipped(0)
{
}
CsvParserModel::~CsvParserModel() = default;
CsvParser* CsvParserModel::parser()
{
return m_parser;
}
void CsvParserModel::setFilename(const QString& filename)
{
m_filename = filename;
@ -35,11 +44,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()
@ -47,37 +55,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;
}
@ -103,7 +102,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
@ -116,11 +115,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()) {
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 {};
}
@ -129,15 +131,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 {};
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 {};
if (section + m_skipped < rowCount()) {
return QString::number(section + 1);
}
return QString::number(section + 1);
}
}
return {};

View File

@ -21,20 +21,22 @@
#include <QAbstractTableModel>
#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() 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<int, int> m_columnMap;
};

View File

@ -24,6 +24,7 @@
#include "TagsEdit.h"
#include "gui/MainWindow.h"
#include <QAbstractItemView>
#include <QApplication>
#include <QClipboard>
#include <QCompleter>

View File

@ -0,0 +1,84 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ImportWizard.h"
#include "ImportWizardPageReview.h"
#include "ImportWizardPageSelect.h"
#include "core/Global.h"
#include "core/Group.h"
#include <QFrame>
#include <QPalette>
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<QFrame*>()[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<QUuid, QUuid> ImportWizard::importInto()
{
auto list = field("ImportInto").toList();
if (list.size() >= 2) {
return qMakePair(QUuid(list[0].toString()), QUuid(list[1].toString()));
}
return {};
}
QSharedPointer<Database> ImportWizard::database()
{
return m_db;
}

View File

@ -0,0 +1,60 @@
/*
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_IMPORTWIZARD_H
#define KEEPASSXC_IMPORTWIZARD_H
#include <QPointer>
#include <QWizard>
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> database();
QPair<QUuid, QUuid> importInto();
enum ImportType
{
IMPORT_NONE = 0,
IMPORT_CSV,
IMPORT_OPVAULT,
IMPORT_OPUX,
IMPORT_BITWARDEN,
IMPORT_KEEPASS1
};
private:
QSharedPointer<Database> m_db;
QPointer<ImportWizardPageSelect> m_pageSelect;
QPointer<ImportWizardPageReview> m_pageReview;
};
#endif // KEEPASSXC_IMPORTWIZARD_H

View File

@ -0,0 +1,202 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "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 <QBoxLayout>
#include <QDir>
#include <QHeaderView>
#include <QTableWidget>
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<Database> 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<Database> 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<Database> 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<Database> 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<Database>
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;
}

View File

@ -0,0 +1,60 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_IMPORTWIZARDPAGEREVIEW_H
#define KEEPASSXC_IMPORTWIZARDPAGEREVIEW_H
#include <QPointer>
#include <QWizardPage>
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> database();
private:
void setupCsvImport(const QString& filename);
QSharedPointer<Database> importOPUX(const QString& filename);
QSharedPointer<Database> importBitwarden(const QString& filename, const QString& password);
QSharedPointer<Database> importOPVault(const QString& filename, const QString& password);
QSharedPointer<Database> importKeePass1(const QString& filename, const QString& password, const QString& keyfile);
void setupDatabasePreview();
QScopedPointer<Ui::ImportWizardPageReview> m_ui;
QSharedPointer<Database> m_db;
QPointer<CsvImportWidget> m_csvWidget;
};
#endif

View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ImportWizardPageReview</class>
<widget class="QWizardPage" name="ImportWizardPageReview">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>518</width>
<height>334</height>
</rect>
</property>
<property name="windowTitle">
<string>WizardPage</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="MessageWidget" name="messageWidget" native="true"/>
</item>
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="minimumSize">
<size>
<width>500</width>
<height>300</height>
</size>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustToContentsOnFirstShow</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>498</width>
<height>298</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="filenameLabel">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string notr="true">filename</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="previewLabel">
<property name="text">
<string notr="true">Entry count: %1</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>MessageWidget</class>
<extends>QWidget</extends>
<header>gui/MessageWidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,236 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "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<QVariant>() << 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<QVariant>() << 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 {};
}
}

View File

@ -0,0 +1,56 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_IMPORTWIZARDPAGESELECT_H
#define KEEPASSXC_IMPORTWIZARDPAGESELECT_H
#include <QPointer>
#include <QWizardPage>
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<Ui::ImportWizardPageSelect> m_ui;
};
#endif

View File

@ -0,0 +1,276 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ImportWizardPageSelect</class>
<widget class="QWizardPage" name="ImportWizardPageSelect">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>500</width>
<height>388</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<property name="title">
<string>Import File Selection</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QListWidget" name="importTypeList">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>500</width>
<height>125</height>
</size>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustToContents</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="tabKeyNavigation">
<bool>true</bool>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="textElideMode">
<enum>Qt::ElideNone</enum>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QWidget" name="inputFields" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>400</width>
<height>0</height>
</size>
</property>
<layout class="QFormLayout" name="formLayout_2">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="importFileLabel">
<property name="text">
<string>Import File:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="importFileLayout">
<item>
<widget class="QLineEdit" name="importFileEdit"/>
</item>
<item>
<widget class="QPushButton" name="importFileButton">
<property name="text">
<string>Browse…</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="passwordLabel">
<property name="text">
<string>Password:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="PasswordWidget" name="passwordEdit" native="true"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="keyFileLabel">
<property name="text">
<string>Key File:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="keyFileLayout">
<item>
<widget class="QLineEdit" name="keyFileEdit"/>
</item>
<item>
<widget class="QPushButton" name="keyFileButton">
<property name="text">
<string>Browse…</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="1">
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>15</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="0">
<widget class="QLabel" name="importIntoLabel">
<property name="text">
<string>Import Into:</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QGroupBox" name="importIntoGroupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>60</height>
</size>
</property>
<property name="title">
<string/>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item>
<widget class="QRadioButton" name="newDatabaseRadio">
<property name="text">
<string>New Database</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="existingDatabaseLayout" stretch="0,1">
<item>
<widget class="QRadioButton" name="existingDatabaseRadio">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>No unlocked databases available</string>
</property>
<property name="text">
<string>Existing Database:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="existingDatabaseChoice">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::MinimumExpanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>PasswordWidget</class>
<extends>QWidget</extends>
<header>gui/PasswordWidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -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})

View File

@ -142,7 +142,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)

267
tests/TestImports.cpp Normal file
View File

@ -0,0 +1,267 @@
/*
* Copyright (C) 2022 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "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 <QJsonObject>
#include <QList>
#include <QTest>
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<unsigned int>(8));
QCOMPARE(totpSettings->step, static_cast<unsigned int>(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<unsigned int>(8));
QCOMPARE(totpSettings->step, static_cast<unsigned int>(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);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
* Copyright (C) 2022 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -15,20 +15,21 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSX_KEEPASS1OPENWIDGET_H
#define KEEPASSX_KEEPASS1OPENWIDGET_H
#ifndef TEST_IMPORTS_H
#define TEST_IMPORTS_H
#include "gui/DatabaseOpenWidget.h"
#include <QObject>
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 */

View File

@ -1,139 +0,0 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "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 <QJsonObject>
#include <QList>
#include <QStringList>
#include <QTest>
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<Database> 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<unsigned int>(8));
QCOMPARE(totpSettings->step, static_cast<unsigned int>(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<unsigned int>(8));
QCOMPARE(totpSettings->step, static_cast<unsigned int>(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())));
}
}

Binary file not shown.

View File

@ -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="
}

View File

@ -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
]
}
]
}

View File

@ -1640,28 +1640,6 @@ void TestGui::testDatabaseSettings()
config()->set(Config::AutoSaveAfterEveryChange, false);
}
void TestGui::testKeePass1Import()
{
fileDialog()->setNextFileName(QString(KEEPASSX_TEST_DATA_DIR).append("/basic.kdb"));
triggerAction("actionImportKeePass1");
auto* keepass1OpenWidget = m_tabWidget->currentDatabaseWidget()->findChild<QWidget*>("keepass1OpenWidget");
auto* editPassword =
keepass1OpenWidget->findChild<PasswordWidget*>("editPassword")->findChild<QLineEdit*>("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);

View File

@ -61,7 +61,6 @@ private slots:
void testSaveBackupPath();
void testSaveBackupPath_data();
void testDatabaseSettings();
void testKeePass1Import();
void testDatabaseLocking();
void testDragAndDropKdbxFiles();
void testSortGroups();