mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-10-01 01:26:01 -04:00
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:
parent
a02bceabd2
commit
e700195f0a
@ -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})
|
||||
|
3
COPYING
3
COPYING
@ -143,11 +143,13 @@ License: MIT
|
||||
Files: share/icons/application/scalable/actions/application-exit.svg
|
||||
share/icons/application/scalable/actions/attributes-copy.svg
|
||||
share/icons/application/scalable/actions/auto-type.svg
|
||||
share/icons/application/scalable/actions/bitwarden.svg
|
||||
share/icons/application/scalable/actions/bugreport.svg
|
||||
share/icons/application/scalable/actions/chevron-double-down.svg
|
||||
share/icons/application/scalable/actions/chevron-double-right.svg
|
||||
share/icons/application/scalable/actions/clipboard-text.svg
|
||||
share/icons/application/scalable/actions/configure.svg
|
||||
share/icons/application/scalable/actions/csv.svg
|
||||
share/icons/application/scalable/actions/database-change-key.svg
|
||||
share/icons/application/scalable/actions/database-lock.svg
|
||||
share/icons/application/scalable/actions/database-lock-all.svg
|
||||
@ -192,6 +194,7 @@ Files: share/icons/application/scalable/actions/application-exit.svg
|
||||
share/icons/application/scalable/actions/move-up.svg
|
||||
share/icons/application/scalable/actions/object-locked.svg
|
||||
share/icons/application/scalable/actions/object-unlocked.svg
|
||||
share/icons/application/scalable/actions/onepassword.svg
|
||||
share/icons/application/scalable/actions/paperclip.svg
|
||||
share/icons/application/scalable/actions/password-copy.svg
|
||||
share/icons/application/scalable/actions/passkey.svg
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 100 KiB |
BIN
docs/images/import_wizard.png
Normal file
BIN
docs/images/import_wizard.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 115 KiB |
@ -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.
|
||||
|
1
share/icons/application/scalable/actions/bitwarden.svg
Normal file
1
share/icons/application/scalable/actions/bitwarden.svg
Normal 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 |
1
share/icons/application/scalable/actions/csv.svg
Normal file
1
share/icons/application/scalable/actions/csv.svg
Normal 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 |
1
share/icons/application/scalable/actions/onepassword.svg
Normal file
1
share/icons/application/scalable/actions/onepassword.svg
Normal 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 |
@ -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>
|
||||
|
@ -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>&Recent Databases</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>&Import</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>&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>
|
||||
|
@ -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}
|
||||
|
@ -21,6 +21,7 @@
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
#include <QPluginLoader>
|
||||
#include <QRegularExpression>
|
||||
#include <QUrl>
|
||||
|
||||
#include "config-keepassx.h"
|
||||
|
313
src/format/BitwardenReader.cpp
Normal file
313
src/format/BitwardenReader.cpp
Normal 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;
|
||||
}
|
@ -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
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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
288
src/format/OPUXReader.cpp
Normal 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;
|
||||
}
|
@ -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
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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_ */
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,8 @@
|
||||
#ifndef KEEPASSX_CLONEDIALOG_H
|
||||
#define KEEPASSX_CLONEDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
#include "core/Database.h"
|
||||
#include "gui/DatabaseWidget.h"
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -31,7 +31,7 @@
|
||||
#endif
|
||||
|
||||
#include <QScrollBar>
|
||||
|
||||
#include <QTabWidget>
|
||||
namespace
|
||||
{
|
||||
constexpr int GeneralTabIndex = 0;
|
||||
|
@ -26,6 +26,7 @@ namespace Ui
|
||||
class EntryPreviewWidget;
|
||||
}
|
||||
|
||||
class QTabWidget;
|
||||
class QTextEdit;
|
||||
|
||||
class EntryPreviewWidget : public QWidget
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -231,14 +231,6 @@
|
||||
<string>&Recent Databases</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuImport">
|
||||
<property name="title">
|
||||
<string>&Import</string>
|
||||
</property>
|
||||
<addaction name="actionImportCsv"/>
|
||||
<addaction name="actionImportOpVault"/>
|
||||
<addaction name="actionImportKeePass1"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuExport">
|
||||
<property name="title">
|
||||
<string>&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>
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -19,6 +19,8 @@
|
||||
#ifndef KEEPASSX_TOTPDIALOG_H
|
||||
#define KEEPASSX_TOTPDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
#include "core/Database.h"
|
||||
#include "gui/DatabaseWidget.h"
|
||||
|
||||
|
@ -25,6 +25,7 @@
|
||||
|
||||
#include <QBoxLayout>
|
||||
#include <QBuffer>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
|
@ -18,6 +18,8 @@
|
||||
#ifndef KEEPASSX_TotpExportSettingsDialog_H
|
||||
#define KEEPASSX_TotpExportSettingsDialog_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
#include "core/Database.h"
|
||||
#include "gui/DatabaseWidget.h"
|
||||
|
||||
|
@ -19,6 +19,8 @@
|
||||
#ifndef KEEPASSX_SETUPTOTPDIALOG_H
|
||||
#define KEEPASSX_SETUPTOTPDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
#include "core/Database.h"
|
||||
#include "gui/DatabaseWidget.h"
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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/>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
@ -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);
|
||||
}
|
@ -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
|
@ -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 {};
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -24,6 +24,7 @@
|
||||
|
||||
#include "TagsEdit.h"
|
||||
#include "gui/MainWindow.h"
|
||||
#include <QAbstractItemView>
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QCompleter>
|
||||
|
84
src/gui/wizard/ImportWizard.cpp
Normal file
84
src/gui/wizard/ImportWizard.cpp
Normal 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;
|
||||
}
|
60
src/gui/wizard/ImportWizard.h
Normal file
60
src/gui/wizard/ImportWizard.h
Normal 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
|
202
src/gui/wizard/ImportWizardPageReview.cpp
Normal file
202
src/gui/wizard/ImportWizardPageReview.cpp
Normal 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;
|
||||
}
|
60
src/gui/wizard/ImportWizardPageReview.h
Normal file
60
src/gui/wizard/ImportWizardPageReview.h
Normal 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
|
95
src/gui/wizard/ImportWizardPageReview.ui
Normal file
95
src/gui/wizard/ImportWizardPageReview.ui
Normal 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>
|
236
src/gui/wizard/ImportWizardPageSelect.cpp
Normal file
236
src/gui/wizard/ImportWizardPageSelect.cpp
Normal 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 {};
|
||||
}
|
||||
}
|
56
src/gui/wizard/ImportWizardPageSelect.h
Normal file
56
src/gui/wizard/ImportWizardPageSelect.h
Normal 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
|
276
src/gui/wizard/ImportWizardPageSelect.ui
Normal file
276
src/gui/wizard/ImportWizardPageSelect.ui
Normal 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>
|
@ -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})
|
||||
|
@ -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
267
tests/TestImports.cpp
Normal 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);
|
||||
}
|
@ -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 */
|
@ -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())));
|
||||
}
|
||||
}
|
BIN
tests/data/1PasswordExport.1pux
Normal file
BIN
tests/data/1PasswordExport.1pux
Normal file
Binary file not shown.
11
tests/data/bitwarden_encrypted_export.json
Normal file
11
tests/data/bitwarden_encrypted_export.json
Normal 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="
|
||||
}
|
180
tests/data/bitwarden_export.json
Normal file
180
tests/data/bitwarden_export.json
Normal 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
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -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);
|
||||
|
@ -61,7 +61,6 @@ private slots:
|
||||
void testSaveBackupPath();
|
||||
void testSaveBackupPath_data();
|
||||
void testDatabaseSettings();
|
||||
void testKeePass1Import();
|
||||
void testDatabaseLocking();
|
||||
void testDragAndDropKdbxFiles();
|
||||
void testSortGroups();
|
||||
|
Loading…
Reference in New Issue
Block a user