Merge branch 'release/3.5.0'

This commit is contained in:
J-Jamet
2023-01-26 23:25:40 +01:00
155 changed files with 6123 additions and 2097 deletions

View File

@@ -1,3 +1,13 @@
KeePassDX(3.5.0)
* Support YubiKey challenge-response #8 #137
* Better exception management during database save #1346
* Add "Screenshot mode" setting #459 #1377 #1354 (Thx @GianpaMX)
* Hide clipboard sensitive text when copy entry field #1386
* Fix attachment download button #1401
* Add monochrome icon #1403 #1404 (Thx @Sandelinos)
* Fix lock with back button #1412 #1414 (Thx @ryg-git)
* Vanadium compatibility #1447 (Thx @flawedworld)
KeePassDX(3.4.5) KeePassDX(3.4.5)
* Fix custom data in group (fix KeeShare) #1335 * Fix custom data in group (fix KeeShare) #1335
* Fix device credential unlocking #1344 * Fix device credential unlocking #1344

View File

@@ -3,25 +3,25 @@ GEM
specs: specs:
CFPropertyList (3.0.5) CFPropertyList (3.0.5)
rexml rexml
addressable (2.8.0) addressable (2.8.1)
public_suffix (>= 2.0.2, < 5.0) public_suffix (>= 2.0.2, < 6.0)
artifactory (3.0.15) artifactory (3.0.15)
atomos (0.1.3) atomos (0.1.3)
aws-eventstream (1.2.0) aws-eventstream (1.2.0)
aws-partitions (1.577.0) aws-partitions (1.646.0)
aws-sdk-core (3.130.1) aws-sdk-core (3.160.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0) aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
jmespath (~> 1.0) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.55.0) aws-sdk-kms (1.58.0)
aws-sdk-core (~> 3, >= 3.127.0) aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.113.0) aws-sdk-s3 (1.114.0)
aws-sdk-core (~> 3, >= 3.127.0) aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4) aws-sigv4 (~> 1.4)
aws-sigv4 (1.4.0) aws-sigv4 (1.5.2)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4) babosa (1.0.4)
claide (1.1.0) claide (1.1.0)
@@ -34,10 +34,10 @@ GEM
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701) domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6) dotenv (2.8.1)
emoji_regex (3.2.3) emoji_regex (3.2.3)
excon (0.92.2) excon (0.93.0)
faraday (1.10.0) faraday (1.10.2)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0) faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1) faraday-excon (~> 1.1)
@@ -56,8 +56,8 @@ GEM
faraday-em_synchrony (1.0.0) faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0) faraday-excon (1.1.0)
faraday-httpclient (1.0.1) faraday-httpclient (1.0.1)
faraday-multipart (1.0.3) faraday-multipart (1.0.4)
multipart-post (>= 1.2, < 3) multipart-post (~> 2)
faraday-net_http (1.0.1) faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0) faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0) faraday-patron (1.0.0)
@@ -66,7 +66,7 @@ GEM
faraday_middleware (1.2.0) faraday_middleware (1.2.0)
faraday (~> 1.0) faraday (~> 1.0)
fastimage (2.2.6) fastimage (2.2.6)
fastlane (2.205.1) fastlane (2.210.1)
CFPropertyList (>= 2.3, < 4.0.0) CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0) addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0) artifactory (~> 3.0)
@@ -107,9 +107,9 @@ GEM
xcpretty-travis-formatter (>= 0.0.3) xcpretty-travis-formatter (>= 0.0.3)
fastlane-plugin-versioning_android (0.1.0) fastlane-plugin-versioning_android (0.1.0)
gh_inspector (1.1.3) gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.19.0) google-apis-androidpublisher_v3 (0.29.0)
google-apis-core (>= 0.4, < 2.a) google-apis-core (>= 0.9.0, < 2.a)
google-apis-core (0.4.2) google-apis-core (0.9.0)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a) httpclient (>= 2.8.1, < 3.a)
@@ -118,27 +118,27 @@ GEM
retriable (>= 2.0, < 4.a) retriable (>= 2.0, < 4.a)
rexml rexml
webrick webrick
google-apis-iamcredentials_v1 (0.10.0) google-apis-iamcredentials_v1 (0.15.0)
google-apis-core (>= 0.4, < 2.a) google-apis-core (>= 0.9.0, < 2.a)
google-apis-playcustomapp_v1 (0.7.0) google-apis-playcustomapp_v1 (0.11.0)
google-apis-core (>= 0.4, < 2.a) google-apis-core (>= 0.9.0, < 2.a)
google-apis-storage_v1 (0.13.0) google-apis-storage_v1 (0.19.0)
google-apis-core (>= 0.4, < 2.a) google-apis-core (>= 0.9.0, < 2.a)
google-cloud-core (1.6.0) google-cloud-core (1.6.0)
google-cloud-env (~> 1.0) google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0) google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0) faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.2.0) google-cloud-errors (1.3.0)
google-cloud-storage (1.36.1) google-cloud-storage (1.43.0)
addressable (~> 2.8) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1) google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.1) google-apis-storage_v1 (~> 0.19.0)
google-cloud-core (~> 1.6) google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0) mini_mime (~> 1.0)
googleauth (1.1.2) googleauth (1.2.0)
faraday (>= 0.17.3, < 3.a) faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
memoist (~> 0.16) memoist (~> 0.16)
@@ -146,12 +146,12 @@ GEM
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
highline (2.0.3) highline (2.0.3)
http-cookie (1.0.4) http-cookie (1.0.5)
domain_name (~> 0.5) domain_name (~> 0.5)
httpclient (2.8.3) httpclient (2.8.3)
jmespath (1.6.1) jmespath (1.6.1)
json (2.6.1) json (2.6.2)
jwt (2.3.0) jwt (2.5.0)
memoist (0.16.2) memoist (0.16.2)
mini_magick (4.11.0) mini_magick (4.11.0)
mini_mime (1.1.2) mini_mime (1.1.2)
@@ -162,9 +162,9 @@ GEM
optparse (0.1.1) optparse (0.1.1)
os (1.1.4) os (1.1.4)
plist (3.6.0) plist (3.6.0)
public_suffix (4.0.7) public_suffix (5.0.0)
rake (13.0.6) rake (13.0.6)
representable (3.1.1) representable (3.2.0)
declarative (< 0.1.0) declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0) trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
@@ -174,9 +174,9 @@ GEM
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyzip (2.3.2) rubyzip (2.3.2)
security (0.1.3) security (0.1.3)
signet (0.16.1) signet (0.17.0)
addressable (~> 2.8) addressable (~> 2.8)
faraday (>= 0.17.5, < 3.0) faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0) jwt (>= 1.5, < 3.0)
multi_json (~> 1.10) multi_json (~> 1.10)
simctl (1.6.8) simctl (1.6.8)
@@ -193,11 +193,11 @@ GEM
uber (0.1.0) uber (0.1.0)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.8.1) unf_ext (0.0.8.2)
unicode-display_width (1.8.0) unicode-display_width (1.8.0)
webrick (1.7.0) webrick (1.7.0)
word_wrap (1.0.0) word_wrap (1.0.0)
xcodeproj (1.21.0) xcodeproj (1.22.0)
CFPropertyList (>= 2.3.3, < 4.0) CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3) atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0) claide (>= 1.0.2, < 2.0)

View File

@@ -8,7 +8,7 @@
- Create database files / entries and groups. - Create database files / entries and groups.
- Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm. - Support for **.kdb** and **.kdbx** files (version 1 to 4) with AES - Twofish - ChaCha20 - Argon2 algorithm.
- **Compatible** with the majority of alternative programs (KeePass, KeePassX, KeePassXC, …). - **Compatible** with the majority of alternative programs (KeePass, KeePassXC, KeeWeb, …).
- Allows opening and **copying URI / URL fields quickly**. - Allows opening and **copying URI / URL fields quickly**.
- **Biometric recognition** for fast unlocking *(fingerprint / face unlock / …)*. - **Biometric recognition** for fast unlocking *(fingerprint / face unlock / …)*.
- **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA). - **One-Time Password** management *(HOTP / TOTP)* for Two-factor authentication (2FA).
@@ -74,7 +74,7 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
## License ## License
Copyright © 2022 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com). Copyright © 2023 Jeremy Jamet / [Kunzisoft](https://www.kunzisoft.com).
This file is part of KeePassDX. This file is part of KeePassDX.

View File

@@ -12,8 +12,8 @@ android {
applicationId "com.kunzisoft.keepass" applicationId "com.kunzisoft.keepass"
minSdkVersion 15 minSdkVersion 15
targetSdkVersion 32 targetSdkVersion 32
versionCode = 114 versionCode = 118
versionName = "3.4.5" versionName = "3.5.0"
multiDexEnabled true multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests" testApplicationId = "com.kunzisoft.keepass.tests"
@@ -93,7 +93,7 @@ android {
} }
} }
def room_version = "2.4.2" def room_version = "2.4.3"
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
@@ -101,14 +101,14 @@ dependencies {
implementation "androidx.appcompat:appcompat:$android_appcompat_version" implementation "androidx.appcompat:appcompat:$android_appcompat_version"
implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.biometric:biometric:1.1.0' implementation 'androidx.biometric:biometric:1.1.0'
implementation 'androidx.media:media:1.6.0' implementation 'androidx.media:media:1.6.0'
// Lifecycle - LiveData - ViewModel - Coroutines // Lifecycle - LiveData - ViewModel - Coroutines
implementation "androidx.core:core-ktx:$android_core_version" implementation "androidx.core:core-ktx:$android_core_version"
implementation 'androidx.fragment:fragment-ktx:1.4.1' implementation 'androidx.fragment:fragment-ktx:1.5.2'
implementation "com.google.android.material:material:$android_material_version" implementation "com.google.android.material:material:$android_material_version"
// Token auto complete // Token auto complete
// From sources until https://github.com/splitwise/TokenAutoComplete/pull/422 fixed // From sources until https://github.com/splitwise/TokenAutoComplete/pull/422 fixed

View File

@@ -0,0 +1,90 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "f8fb4aed546de19ae7ca0797f49b26a4",
"entities": [
{
"tableName": "file_database_history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `database_alias` TEXT NOT NULL, `keyfile_uri` TEXT, `hardware_key` TEXT, `updated` INTEGER NOT NULL, PRIMARY KEY(`database_uri`))",
"fields": [
{
"fieldPath": "databaseUri",
"columnName": "database_uri",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "databaseAlias",
"columnName": "database_alias",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "keyFileUri",
"columnName": "keyfile_uri",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "hardwareKey",
"columnName": "hardware_key",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "updated",
"columnName": "updated",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"database_uri"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "cipher_database",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`database_uri` TEXT NOT NULL, `encrypted_value` TEXT NOT NULL, `specs_parameters` TEXT NOT NULL, PRIMARY KEY(`database_uri`))",
"fields": [
{
"fieldPath": "databaseUri",
"columnName": "database_uri",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "encryptedValue",
"columnName": "encrypted_value",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "specParameters",
"columnName": "specs_parameters",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"database_uri"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f8fb4aed546de19ae7ca0797f49b26a4')"
]
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="120"
android:viewportHeight="120">
<group
android:translateX="6"
android:translateY="6">
<path
android:fillColor="#ffffff"
android:strokeWidth="1.99999297"
android:pathData="M63.9961,34.0059 C61.5643,34.096,59.2564,35.102,57.5352,36.8223 C53.7682,40.589,53.7682,46.6982,57.5352,50.4649 C61.3017,54.232,67.4073,54.232,71.1739,50.4649 C74.9409,46.6982,74.9409,40.589,71.1739,36.8223 C69.2766,34.9258,66.6768,33.9054,63.9962,34.0059 Z M68.1992,40.6954 C69.8278,40.6958,71.148,42.016,71.1484,43.6446 C71.148,45.2732,69.8278,46.5934,68.1992,46.5938 C66.5706,46.5934,65.2504,45.2732,65.25,43.6446 C65.2504,42.016,66.5706,40.6958,68.1992,40.6954 Z M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
</group>
</vector>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="120"
android:viewportHeight="120">
<group
android:translateX="6"
android:translateY="6">
<path
android:fillColor="#ffffff"
android:strokeWidth="1.99999297"
android:pathData="M64.501,35.0576 C63.7095,35.0576,62.918,35.3613,62.3115,35.9678 L55.0127,43.2666 C53.7998,44.4795,53.7998,46.4306,55.0127,47.6436 L62.3115,54.9424 C63.5244,56.1553,65.4775,56.1553,66.6904,54.9424 L73.9873,47.6436 C75.2002,46.4307,75.2002,44.4796,73.9873,43.2666 L66.6904,35.9678 C66.0839,35.3613,65.2924,35.0576,64.5009,35.0576 Z M67.6729,42.6006 C69.3298,42.6006,70.6729,43.9437,70.6729,45.6006 C70.6729,47.2575,69.3298,48.6006,67.6729,48.6006 C66.016,48.6006,64.6729,47.2575,64.6729,45.6006 C64.6729,43.9437,66.016,42.6006,67.6729,42.6006 Z M36,36 L36,40.2422 L67.7578,72 L72,72 L72,67.7578 L40.2422,36 Z M48.3438,55.4141 L36,67.7578 L36,72 L40.2422,72 L44.7578,67.4844 L44.7578,67.5 L49,67.5 L49,63.2578 L48.9844,63.2578 L49,63.2422 L49,63.2578 L53.2578,63.2578 L53.2578,60.3281 Z" />
</group>
</vector>

View File

@@ -156,6 +156,9 @@
android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" /> android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
<activity <activity
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" /> android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
<activity
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity"
android:theme="@style/Theme.Transparent" />
<activity <activity
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity" android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
android:theme="@style/Theme.Transparent" android:theme="@style/Theme.Transparent"

View File

@@ -77,6 +77,12 @@ class AboutActivity : StylishActivity() {
HtmlCompat.FROM_HTML_MODE_LEGACY) HtmlCompat.FROM_HTML_MODE_LEGACY)
} }
findViewById<TextView>(R.id.activity_about_privacy_text).apply {
movementMethod = LinkMovementMethod.getInstance()
text = HtmlCompat.fromHtml(getString(R.string.html_about_privacy),
HtmlCompat.FROM_HTML_MODE_LEGACY)
}
findViewById<TextView>(R.id.activity_about_contribution_text).apply { findViewById<TextView>(R.id.activity_about_contribution_text).apply {
movementMethod = LinkMovementMethod.getInstance() movementMethod = LinkMovementMethod.getInstance()
text = HtmlCompat.fromHtml(getString(R.string.html_about_contribution), text = HtmlCompat.fromHtml(getString(R.string.html_about_contribution),

View File

@@ -55,7 +55,8 @@ import com.kunzisoft.keepass.autofill.AutofillComponent
import com.kunzisoft.keepass.autofill.AutofillHelper import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
@@ -66,6 +67,7 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.* import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel import com.kunzisoft.keepass.viewmodels.DatabaseFilesViewModel
import java.io.FileNotFoundException import java.io.FileNotFoundException
@@ -155,8 +157,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen -> mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri -> fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
launchPasswordActivity( launchPasswordActivity(
databaseFileUri, databaseFileUri,
fileDatabaseHistoryEntityToOpen.keyFileUri fileDatabaseHistoryEntityToOpen.keyFileUri,
fileDatabaseHistoryEntityToOpen.hardwareKey
) )
} }
} }
@@ -250,7 +253,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
?: MainCredential() ?: MainCredential()
databaseFilesViewModel.addDatabaseFile( databaseFilesViewModel.addDatabaseFile(
databaseUri, databaseUri,
mainCredential.keyFileUri mainCredential.keyFileUri,
mainCredential.hardwareKey
) )
} }
} }
@@ -268,18 +272,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
launchGroupActivityIfLoaded(database) launchGroupActivityIfLoaded(database)
} }
} }
} else {
var resultError = ""
val resultMessage = result.message
// Show error message
if (resultMessage != null && resultMessage.isNotEmpty()) {
resultError = "$resultError $resultMessage"
}
Log.e(TAG, resultError)
Snackbar.make(coordinatorLayout,
resultError,
Snackbar.LENGTH_LONG).asError().show()
} }
coordinatorLayout.showActionErrorIfNeeded(result)
} }
/** /**
@@ -297,10 +291,11 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show() Snackbar.make(coordinatorLayout, error, Snackbar.LENGTH_LONG).asError().show()
} }
private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?) { private fun launchPasswordActivity(databaseUri: Uri, keyFile: Uri?, hardwareKey: HardwareKey?) {
MainCredentialActivity.launch(this, MainCredentialActivity.launch(this,
databaseUri, databaseUri,
keyFile, keyFile,
hardwareKey,
{ exception -> { exception ->
fileNoFoundAction(exception) fileNoFoundAction(exception)
}, },
@@ -321,7 +316,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
} }
private fun launchPasswordActivityWithPath(databaseUri: Uri) { private fun launchPasswordActivityWithPath(databaseUri: Uri) {
launchPasswordActivity(databaseUri, null) launchPasswordActivity(databaseUri, null, null)
// Delete flickering for kitkat <= // Delete flickering for kitkat <=
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
overridePendingTransition(0, 0) overridePendingTransition(0, 0)

View File

@@ -69,7 +69,7 @@ import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.education.GroupActivityEducation import com.kunzisoft.keepass.education.GroupActivityEducation
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.GroupInfo import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
@@ -1368,7 +1368,6 @@ class GroupActivity : DatabaseLockActivity(),
EntrySelectionHelper.removeInfoFromIntent(intent) EntrySelectionHelper.removeInfoFromIntent(intent)
if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) { if (PreferencesUtil.isLockDatabaseWhenBackButtonOnRootClicked(this)) {
lockAndExit() lockAndExit()
super.onRegularBackPressed()
} else { } else {
backToTheAppCaller() backToTheAppCaller()
} }

View File

@@ -56,9 +56,11 @@ import com.kunzisoft.keepass.autofill.AutofillHelper
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
import com.kunzisoft.keepass.education.PasswordActivityEducation import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.* import com.kunzisoft.keepass.model.*
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY
@@ -73,6 +75,7 @@ import com.kunzisoft.keepass.utils.MenuUtil
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.MainCredentialView import com.kunzisoft.keepass.view.MainCredentialView
import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.asError
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel import com.kunzisoft.keepass.viewmodels.AdvancedUnlockViewModel
import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel import com.kunzisoft.keepass.viewmodels.DatabaseFileViewModel
import java.io.FileNotFoundException import java.io.FileNotFoundException
@@ -101,6 +104,8 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
private var mRememberKeyFile: Boolean = false private var mRememberKeyFile: Boolean = false
private var mExternalFileHelper: ExternalFileHelper? = null private var mExternalFileHelper: ExternalFileHelper? = null
private var mRememberHardwareKey: Boolean = false
private var mReadOnly: Boolean = false private var mReadOnly: Boolean = false
private var mForceReadOnly: Boolean = false private var mForceReadOnly: Boolean = false
@@ -133,11 +138,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
PreferencesUtil.enableReadOnlyDatabase(this) PreferencesUtil.enableReadOnlyDatabase(this)
} }
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this) mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this)
mExternalFileHelper = ExternalFileHelper(this@MainCredentialActivity) // Build elements to manage keyfile selection
mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri -> mExternalFileHelper?.buildOpenDocument { uri ->
if (uri != null) { if (uri != null) {
mainCredentialView?.populateKeyFileTextView(uri) mainCredentialView?.populateKeyFileView(uri)
} }
} }
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper) mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
@@ -171,6 +178,16 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
mAdvancedUnlockViewModel.checkUnlockAvailability() mAdvancedUnlockViewModel.checkUnlockAvailability()
enableConfirmationButton() enableConfirmationButton()
} }
mainCredentialView?.onKeyFileChecked =
CompoundButton.OnCheckedChangeListener { _, _ ->
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
enableConfirmationButton()
}
mainCredentialView?.onHardwareKeyChecked =
CompoundButton.OnCheckedChangeListener { _, _ ->
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
enableConfirmationButton()
}
// Observe if default database // Observe if default database
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase -> mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
@@ -204,10 +221,19 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
databaseKeyFileUri databaseKeyFileUri
} }
val databaseHardwareKey = mainCredentialView?.getMainCredential()?.hardwareKey
val hardwareKey =
if (mRememberHardwareKey
&& databaseHardwareKey == null) {
databaseFile?.hardwareKey
} else {
databaseHardwareKey
}
// Define title // Define title
filenameView?.text = databaseFile?.databaseAlias ?: "" filenameView?.text = databaseFile?.databaseAlias ?: ""
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri) onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri, hardwareKey)
} }
} }
@@ -215,6 +241,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
super.onResume() super.onResume()
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity) mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity)
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this@MainCredentialActivity)
// Back to previous keyboard is setting activated // Back to previous keyboard is setting activated
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@MainCredentialActivity)) { if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@MainCredentialActivity)) {
@@ -266,90 +293,84 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
launchGroupActivityIfLoaded(database) launchGroupActivityIfLoaded(database)
} else { } else {
mainCredentialView?.requestPasswordFocus() mainCredentialView?.requestPasswordFocus()
// Manage special exceptions
when (result.exception) {
is DuplicateUuidDatabaseException -> {
// Relaunch loading if we need to fix UUID
showLoadDatabaseDuplicateUuidMessage {
var resultError = "" var databaseUri: Uri? = null
val resultException = result.exception var mainCredential = MainCredential()
val resultMessage = result.message var readOnly = true
var cipherEncryptDatabase: CipherEncryptDatabase? = null
if (resultException != null) { result.data?.let { resultData ->
resultError = resultException.getLocalizedMessage(resources) databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
mainCredential =
when (resultException) { resultData.getParcelable(MAIN_CREDENTIAL_KEY)
is DuplicateUuidDatabaseException -> { ?: mainCredential
// Relaunch loading if we need to fix UUID readOnly = resultData.getBoolean(READ_ONLY_KEY)
showLoadDatabaseDuplicateUuidMessage { cipherEncryptDatabase =
resultData.getParcelable(CIPHER_DATABASE_KEY)
var databaseUri: Uri? = null
var mainCredential = MainCredential()
var readOnly = true
var cipherEncryptDatabase: CipherEncryptDatabase? = null
result.data?.let { resultData ->
databaseUri = resultData.getParcelable(DATABASE_URI_KEY)
mainCredential =
resultData.getParcelable(MAIN_CREDENTIAL_KEY)
?: mainCredential
readOnly = resultData.getBoolean(READ_ONLY_KEY)
cipherEncryptDatabase =
resultData.getParcelable(CIPHER_DATABASE_KEY)
}
databaseUri?.let { databaseFileUri ->
showProgressDialogAndLoadDatabase(
databaseFileUri,
mainCredential,
readOnly,
cipherEncryptDatabase,
true
)
}
} }
}
is FileNotFoundDatabaseException -> { databaseUri?.let { databaseFileUri ->
// Remove this default database inaccessible showProgressDialogAndLoadDatabase(
if (mDefaultDatabase) { databaseFileUri,
mDatabaseFileViewModel.removeDefaultDatabase() mainCredential,
readOnly,
cipherEncryptDatabase,
true
)
} }
} }
} }
is FileNotFoundDatabaseException -> {
// Remove this default database inaccessible
if (mDefaultDatabase) {
mDatabaseFileViewModel.removeDefaultDatabase()
}
}
} }
// Show error message
if (resultMessage != null && resultMessage.isNotEmpty()) {
resultError = "$resultError $resultMessage"
}
Log.e(TAG, resultError)
Snackbar.make(
coordinatorLayout,
resultError,
Snackbar.LENGTH_LONG
).asError().show()
} }
} }
} }
coordinatorLayout.showActionErrorIfNeeded(result)
} }
private fun getUriFromIntent(intent: Intent?) { private fun getUriFromIntent(intent: Intent?) {
// If is a view intent // If is a view intent
val action = intent?.action val action = intent?.action
if (action != null if (action == VIEW_INTENT) {
&& action == VIEW_INTENT) { fillCredentials(
mDatabaseFileUri = intent.data intent.data,
mainCredentialView?.populateKeyFileTextView(UriUtil.getUriFromIntent(intent, KEY_KEYFILE)) UriUtil.getUriFromIntent(intent, KEY_KEYFILE),
HardwareKey.getHardwareKeyFromString(intent.getStringExtra(KEY_HARDWARE_KEY))
)
} else { } else {
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME) fillCredentials(
intent?.getParcelableExtra<Uri?>(KEY_KEYFILE)?.let { intent?.getParcelableExtra(KEY_FILENAME),
mainCredentialView?.populateKeyFileTextView(it) intent?.getParcelableExtra(KEY_KEYFILE),
} HardwareKey.getHardwareKeyFromString(intent?.getStringExtra(KEY_HARDWARE_KEY))
)
} }
try { try {
intent?.removeExtra(KEY_KEYFILE) intent?.removeExtra(KEY_KEYFILE)
intent?.removeExtra(KEY_HARDWARE_KEY)
} catch (e: Exception) {} } catch (e: Exception) {}
mDatabaseFileUri?.let { mDatabaseFileUri?.let {
mDatabaseFileViewModel.checkIfIsDefaultDatabase(it) mDatabaseFileViewModel.checkIfIsDefaultDatabase(it)
} }
} }
private fun fillCredentials(databaseUri: Uri?,
keyFileUri: Uri?,
hardwareKey: HardwareKey?) {
mDatabaseFileUri = databaseUri
mainCredentialView?.populateKeyFileView(keyFileUri)
mainCredentialView?.populateHardwareKeyView(hardwareKey)
}
override fun onNewIntent(intent: Intent?) { override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent) super.onNewIntent(intent)
getUriFromIntent(intent) getUriFromIntent(intent)
@@ -358,7 +379,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
private fun launchGroupActivityIfLoaded(database: Database) { private fun launchGroupActivityIfLoaded(database: Database) {
// Check if database really loaded // Check if database really loaded
if (database.loaded) { if (database.loaded) {
clearCredentialsViews(true) clearCredentialsViews(clearKeyFile = true, clearHardwareKey = true)
GroupActivity.launch(this, GroupActivity.launch(this,
database, database,
{ onValidateSpecialMode() }, { onValidateSpecialMode() },
@@ -408,7 +429,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential() val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential()
when (cipherDecryptDatabase.credentialStorage) { when (cipherDecryptDatabase.credentialStorage) {
CredentialStorage.PASSWORD -> { CredentialStorage.PASSWORD -> {
mainCredential.masterPassword = String(cipherDecryptDatabase.decryptedValue) mainCredential.password = String(cipherDecryptDatabase.decryptedValue)
} }
CredentialStorage.KEY_FILE -> { CredentialStorage.KEY_FILE -> {
// TODO advanced unlock key file // TODO advanced unlock key file
@@ -423,14 +444,23 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
) )
} }
private fun onDatabaseFileLoaded(databaseFileUri: Uri?, keyFileUri: Uri?) { private fun onDatabaseFileLoaded(databaseFileUri: Uri?,
keyFileUri: Uri?,
hardwareKey: HardwareKey?) {
// Define Key File text // Define Key File text
if (mRememberKeyFile) { if (mRememberKeyFile) {
mainCredentialView?.populateKeyFileTextView(keyFileUri) mainCredentialView?.populateKeyFileView(keyFileUri)
}
// Define hardware key
if (mRememberHardwareKey) {
mainCredentialView?.populateHardwareKeyView(hardwareKey)
} }
// Define listener for validate button // Define listener for validate button
confirmButtonView?.setOnClickListener { loadDatabase() } confirmButtonView?.setOnClickListener {
mainCredentialView?.validateCredential()
}
// If Activity is launch with a password and want to open directly // If Activity is launch with a password and want to open directly
val intent = intent val intent = intent
@@ -462,10 +492,14 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
} }
} }
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) { private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile,
clearHardwareKey: Boolean = !mRememberHardwareKey) {
mainCredentialView?.populatePasswordTextView(null) mainCredentialView?.populatePasswordTextView(null)
if (clearKeyFile) { if (clearKeyFile) {
mainCredentialView?.populateKeyFileTextView(null) mainCredentialView?.populateKeyFileView(null)
}
if (clearHardwareKey) {
mainCredentialView?.populateHardwareKeyView(null)
} }
} }
@@ -656,18 +690,24 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
private const val KEY_FILENAME = "fileName" private const val KEY_FILENAME = "fileName"
private const val KEY_KEYFILE = "keyFile" private const val KEY_KEYFILE = "keyFile"
private const val KEY_HARDWARE_KEY = "hardwareKey"
private const val VIEW_INTENT = "android.intent.action.VIEW" private const val VIEW_INTENT = "android.intent.action.VIEW"
private const val KEY_READ_ONLY = "KEY_READ_ONLY" private const val KEY_READ_ONLY = "KEY_READ_ONLY"
private const val KEY_PASSWORD = "password" private const val KEY_PASSWORD = "password"
private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately" private const val KEY_LAUNCH_IMMEDIATELY = "launchImmediately"
private fun buildAndLaunchIntent(activity: Activity, databaseFile: Uri, keyFile: Uri?, private fun buildAndLaunchIntent(activity: Activity,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
intentBuildLauncher: (Intent) -> Unit) { intentBuildLauncher: (Intent) -> Unit) {
val intent = Intent(activity, MainCredentialActivity::class.java) val intent = Intent(activity, MainCredentialActivity::class.java)
intent.putExtra(KEY_FILENAME, databaseFile) intent.putExtra(KEY_FILENAME, databaseFile)
if (keyFile != null) if (keyFile != null)
intent.putExtra(KEY_KEYFILE, keyFile) intent.putExtra(KEY_KEYFILE, keyFile)
if (hardwareKey != null)
intent.putExtra(KEY_HARDWARE_KEY, hardwareKey.toString())
intentBuildLauncher.invoke(intent) intentBuildLauncher.invoke(intent)
} }
@@ -680,8 +720,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
@Throws(FileNotFoundException::class) @Throws(FileNotFoundException::class)
fun launch(activity: Activity, fun launch(activity: Activity,
databaseFile: Uri, databaseFile: Uri,
keyFile: Uri?) { keyFile: Uri?,
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> hardwareKey: HardwareKey?) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
activity.startActivity(intent) activity.startActivity(intent)
} }
} }
@@ -696,8 +737,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForSearchResult(activity: Activity, fun launchForSearchResult(activity: Activity,
databaseFile: Uri, databaseFile: Uri,
keyFile: Uri?, keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo) { searchInfo: SearchInfo) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForSearchModeResult( EntrySelectionHelper.startActivityForSearchModeResult(
activity, activity,
intent, intent,
@@ -715,8 +757,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForSaveResult(activity: Activity, fun launchForSaveResult(activity: Activity,
databaseFile: Uri, databaseFile: Uri,
keyFile: Uri?, keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo) { searchInfo: SearchInfo) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForSaveModeResult( EntrySelectionHelper.startActivityForSaveModeResult(
activity, activity,
intent, intent,
@@ -734,8 +777,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForKeyboardResult(activity: Activity, fun launchForKeyboardResult(activity: Activity,
databaseFile: Uri, databaseFile: Uri,
keyFile: Uri?, keyFile: Uri?,
hardwareKey: HardwareKey?,
searchInfo: SearchInfo?) { searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult( EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
activity, activity,
intent, intent,
@@ -754,10 +798,11 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForAutofillResult(activity: AppCompatActivity, fun launchForAutofillResult(activity: AppCompatActivity,
databaseFile: Uri, databaseFile: Uri,
keyFile: Uri?, keyFile: Uri?,
hardwareKey: HardwareKey?,
activityResultLauncher: ActivityResultLauncher<Intent>?, activityResultLauncher: ActivityResultLauncher<Intent>?,
autofillComponent: AutofillComponent, autofillComponent: AutofillComponent,
searchInfo: SearchInfo?) { searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
AutofillHelper.startActivityForAutofillResult( AutofillHelper.startActivityForAutofillResult(
activity, activity,
intent, intent,
@@ -775,8 +820,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launchForRegistration(activity: Activity, fun launchForRegistration(activity: Activity,
databaseFile: Uri, databaseFile: Uri,
keyFile: Uri?, keyFile: Uri?,
hardwareKey: HardwareKey?,
registerInfo: RegisterInfo?) { registerInfo: RegisterInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent -> buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForRegistrationModeResult( EntrySelectionHelper.startActivityForRegistrationModeResult(
activity, activity,
intent, intent,
@@ -792,6 +838,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
fun launch(activity: AppCompatActivity, fun launch(activity: AppCompatActivity,
databaseUri: Uri, databaseUri: Uri,
keyFile: Uri?, keyFile: Uri?,
hardwareKey: HardwareKey?,
fileNoFoundAction: (exception: FileNotFoundException) -> Unit, fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
onCancelSpecialMode: () -> Unit, onCancelSpecialMode: () -> Unit,
onLaunchActivitySpecialMode: () -> Unit, onLaunchActivitySpecialMode: () -> Unit,
@@ -800,43 +847,67 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
try { try {
EntrySelectionHelper.doSpecialAction(activity.intent, EntrySelectionHelper.doSpecialAction(activity.intent,
{ {
MainCredentialActivity.launch(activity, launch(
databaseUri, keyFile) activity,
databaseUri,
keyFile,
hardwareKey
)
}, },
{ searchInfo -> // Search Action { searchInfo -> // Search Action
MainCredentialActivity.launchForSearchResult(activity, launchForSearchResult(
databaseUri, keyFile, activity,
searchInfo) databaseUri,
keyFile,
hardwareKey,
searchInfo
)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()
}, },
{ searchInfo -> // Save Action { searchInfo -> // Save Action
MainCredentialActivity.launchForSaveResult(activity, launchForSaveResult(
databaseUri, keyFile, activity,
searchInfo) databaseUri,
keyFile,
hardwareKey,
searchInfo
)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()
}, },
{ searchInfo -> // Keyboard Selection Action { searchInfo -> // Keyboard Selection Action
MainCredentialActivity.launchForKeyboardResult(activity, launchForKeyboardResult(
databaseUri, keyFile, activity,
searchInfo) databaseUri,
keyFile,
hardwareKey,
searchInfo
)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()
}, },
{ searchInfo, autofillComponent -> // Autofill Selection Action { searchInfo, autofillComponent -> // Autofill Selection Action
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
MainCredentialActivity.launchForAutofillResult(activity, launchForAutofillResult(
databaseUri, keyFile, activity,
autofillActivityResultLauncher, databaseUri,
autofillComponent, keyFile,
searchInfo) hardwareKey,
autofillActivityResultLauncher,
autofillComponent,
searchInfo
)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()
} else { } else {
onCancelSpecialMode() onCancelSpecialMode()
} }
}, },
{ registerInfo -> // Registration Action { registerInfo -> // Registration Action
MainCredentialActivity.launchForRegistration(activity, launchForRegistration(
databaseUri, keyFile, activity,
registerInfo) databaseUri,
keyFile,
hardwareKey,
registerInfo
)
onLaunchActivitySpecialMode() onLaunchActivitySpecialMode()
} }
) )

View File

@@ -27,7 +27,7 @@ import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.MainCredentialView import com.kunzisoft.keepass.view.MainCredentialView
@@ -95,7 +95,7 @@ class MainCredentialDialogFragment : DatabaseDialogFragment() {
mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri -> mExternalFileHelper?.buildOpenDocument { uri ->
if (uri != null) { if (uri != null) {
mainCredentialView?.populateKeyFileTextView(uri) mainCredentialView?.populateKeyFileView(uri)
} }
} }
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper) mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)

View File

@@ -26,7 +26,7 @@ import android.os.Bundle
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.database.element.MainCredential
class PasswordEncodingDialogFragment : DialogFragment() { class PasswordEncodingDialogFragment : DialogFragment() {

View File

@@ -45,13 +45,16 @@ class ProFeatureDialogFragment : DialogFragment() {
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_ad_free), FROM_HTML_MODE_LEGACY)).append("\n\n") stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_ad_free), FROM_HTML_MODE_LEGACY)).append("\n\n")
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_buy_pro), FROM_HTML_MODE_LEGACY)) stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_buy_pro), FROM_HTML_MODE_LEGACY))
builder.setPositiveButton(R.string.download) { _, _ -> builder.setPositiveButton(R.string.download) { _, _ ->
UriUtil.gotoUrl(requireContext(), R.string.app_pro_url) UriUtil.gotoUrl(activity,
activity.getString(R.string.play_store_url,
activity.getString(R.string.keepro_app_id))
)
} }
} else { } else {
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_feature_generosity), FROM_HTML_MODE_LEGACY)).append("\n\n") stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_feature_generosity), FROM_HTML_MODE_LEGACY)).append("\n\n")
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_donation), FROM_HTML_MODE_LEGACY)) stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_donation), FROM_HTML_MODE_LEGACY))
builder.setPositiveButton(R.string.contribute) { _, _ -> builder.setPositiveButton(R.string.contribute) { _, _ ->
UriUtil.gotoUrl(requireContext(), R.string.contribution_url) UriUtil.gotoUrl(activity, R.string.contribution_url)
} }
} }
builder.setMessage(stringBuilder) builder.setMessage(stringBuilder)

View File

@@ -35,9 +35,12 @@ import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.password.PasswordEntropy import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.view.HardwareKeySelectionView
import com.kunzisoft.keepass.view.KeyFileSelectionView import com.kunzisoft.keepass.view.KeyFileSelectionView
import com.kunzisoft.keepass.view.PassKeyView import com.kunzisoft.keepass.view.PassKeyView
import com.kunzisoft.keepass.view.applyFontVisibility import com.kunzisoft.keepass.view.applyFontVisibility
@@ -45,18 +48,21 @@ import com.kunzisoft.keepass.view.applyFontVisibility
class SetMainCredentialDialogFragment : DatabaseDialogFragment() { class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
private var mMasterPassword: String? = null private var mMasterPassword: String? = null
private var mKeyFile: Uri? = null private var mKeyFileUri: Uri? = null
private var mHardwareKey: HardwareKey? = null
private var rootView: View? = null private lateinit var rootView: View
private var passwordCheckBox: CompoundButton? = null private lateinit var passwordCheckBox: CompoundButton
private lateinit var passwordView: PassKeyView
private lateinit var passwordRepeatTextInputLayout: TextInputLayout
private lateinit var passwordRepeatView: TextView
private var passKeyView: PassKeyView? = null private lateinit var keyFileCheckBox: CompoundButton
private var passwordRepeatTextInputLayout: TextInputLayout? = null private lateinit var keyFileSelectionView: KeyFileSelectionView
private var passwordRepeatView: TextView? = null
private var keyFileCheckBox: CompoundButton? = null private lateinit var hardwareKeyCheckBox: CompoundButton
private var keyFileSelectionView: KeyFileSelectionView? = null private lateinit var hardwareKeySelectionView: HardwareKeySelectionView
private var mListener: AssignMainCredentialDialogListener? = null private var mListener: AssignMainCredentialDialogListener? = null
@@ -67,13 +73,15 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
private var mNoKeyConfirmationDialog: AlertDialog? = null private var mNoKeyConfirmationDialog: AlertDialog? = null
private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null
private var mAllowNoMasterKey: Boolean = false
private val passwordTextWatcher = object : TextWatcher { private val passwordTextWatcher = object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
override fun afterTextChanged(editable: Editable) { override fun afterTextChanged(editable: Editable) {
passwordCheckBox?.isChecked = true passwordCheckBox.isChecked = true
} }
} }
@@ -113,10 +121,9 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
activity?.let { activity -> activity?.let { activity ->
var allowNoMasterKey = false
arguments?.apply { arguments?.apply {
if (containsKey(ALLOW_NO_MASTER_KEY_ARG)) if (containsKey(ALLOW_NO_MASTER_KEY_ARG))
allowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false) mAllowNoMasterKey = getBoolean(ALLOW_NO_MASTER_KEY_ARG, false)
} }
val builder = AlertDialog.Builder(activity) val builder = AlertDialog.Builder(activity)
@@ -128,63 +135,63 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
.setPositiveButton(android.R.string.ok) { _, _ -> } .setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> }
rootView?.findViewById<View>(R.id.credentials_information)?.setOnClickListener { rootView.findViewById<View>(R.id.credentials_information)?.setOnClickListener {
UriUtil.gotoUrl(activity, R.string.credentials_explanation_url) UriUtil.gotoUrl(activity, R.string.credentials_explanation_url)
} }
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox) passwordCheckBox = rootView.findViewById(R.id.password_checkbox)
passKeyView = rootView?.findViewById(R.id.password_view) passwordView = rootView.findViewById(R.id.password_view)
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout) passwordRepeatTextInputLayout = rootView.findViewById(R.id.password_repeat_input_layout)
passwordRepeatView = rootView?.findViewById(R.id.password_confirmation) passwordRepeatView = rootView.findViewById(R.id.password_confirmation)
passwordRepeatView?.applyFontVisibility() passwordRepeatView.applyFontVisibility()
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox) keyFileCheckBox = rootView.findViewById(R.id.keyfile_checkbox)
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection) keyFileSelectionView = rootView.findViewById(R.id.keyfile_selection)
hardwareKeyCheckBox = rootView.findViewById(R.id.hardware_key_checkbox)
hardwareKeySelectionView = rootView.findViewById(R.id.hardware_key_selection)
mExternalFileHelper = ExternalFileHelper(this) mExternalFileHelper = ExternalFileHelper(this)
mExternalFileHelper?.buildOpenDocument { uri -> mExternalFileHelper?.buildOpenDocument { uri ->
uri?.let { pathUri -> uri?.let { pathUri ->
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile -> UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
keyFileSelectionView?.error = null keyFileSelectionView.error = null
keyFileCheckBox?.isChecked = true keyFileCheckBox.isChecked = true
keyFileSelectionView?.uri = pathUri keyFileSelectionView.uri = pathUri
if (lengthFile <= 0L) { if (lengthFile <= 0L) {
showEmptyKeyFileConfirmationDialog() showEmptyKeyFileConfirmationDialog()
} }
} }
} }
} }
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper) keyFileSelectionView.setOpenDocumentClickListener(mExternalFileHelper)
hardwareKeySelectionView.selectionListener = { hardwareKey ->
hardwareKeyCheckBox.isChecked = true
hardwareKeySelectionView.error =
if (!HardwareKeyActivity.isHardwareKeyAvailable(requireActivity(), hardwareKey)) {
// show hardware driver dialog if required
getString(R.string.error_driver_required, hardwareKey.toString())
} else {
null
}
}
val dialog = builder.create() val dialog = builder.create()
dialog.setOnShowListener { dialog1 ->
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
positiveButton.setOnClickListener {
if (passwordCheckBox != null && keyFileCheckBox!= null) { mMasterPassword = ""
dialog.setOnShowListener { dialog1 -> mKeyFileUri = null
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE) mHardwareKey = null
positiveButton.setOnClickListener {
mMasterPassword = "" approveMainCredential()
mKeyFile = null }
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
var error = verifyPassword() || verifyKeyFile() negativeButton.setOnClickListener {
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) { mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
error = true dismiss()
if (allowNoMasterKey)
showNoKeyConfirmationDialog()
else {
passwordRepeatTextInputLayout?.error = getString(R.string.error_disallow_no_credentials)
}
}
if (!error) {
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
dismiss()
}
}
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
negativeButton.setOnClickListener {
mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
dismiss()
}
} }
} }
@@ -194,67 +201,113 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
return super.onCreateDialog(savedInstanceState) return super.onCreateDialog(savedInstanceState)
} }
private fun approveMainCredential() {
val errorPassword = verifyPassword()
val errorKeyFile = verifyKeyFile()
val errorHardwareKey = verifyHardwareKey()
// Check all to fill error
var error = errorPassword || errorKeyFile || errorHardwareKey
val hardwareKey = hardwareKeySelectionView.hardwareKey
if (!error
&& (!passwordCheckBox.isChecked)
&& (!keyFileCheckBox.isChecked)
&& (!hardwareKeyCheckBox.isChecked)
) {
error = true
if (mAllowNoMasterKey) {
// show no key dialog if required
showNoKeyConfirmationDialog()
} else {
passwordRepeatTextInputLayout.error =
getString(R.string.error_disallow_no_credentials)
}
} else if (!error
&& mMasterPassword.isNullOrEmpty()
&& !keyFileCheckBox.isChecked
&& !hardwareKeyCheckBox.isChecked
) {
// show empty password dialog if required
error = true
showEmptyPasswordConfirmationDialog()
} else if (!error
&& hardwareKey != null
&& !HardwareKeyActivity.isHardwareKeyAvailable(
requireActivity(), hardwareKey, false)
) {
// show hardware driver dialog if required
error = true
hardwareKeySelectionView.error =
getString(R.string.error_driver_required, hardwareKey.toString())
}
if (!error) {
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
dismiss()
}
}
private fun verifyPassword(): Boolean {
var error = false
passwordRepeatTextInputLayout.error = null
if (passwordCheckBox.isChecked) {
mMasterPassword = passwordView.passwordString
val confPassword = passwordRepeatView.text.toString()
// Verify that passwords match
if (mMasterPassword != confPassword) {
error = true
// Passwords do not match
passwordRepeatTextInputLayout.error = getString(R.string.error_pass_match)
}
}
return error
}
private fun verifyKeyFile(): Boolean {
var error = false
keyFileSelectionView.error = null
if (keyFileCheckBox.isChecked) {
keyFileSelectionView.uri?.let { uri ->
mKeyFileUri = uri
} ?: run {
error = true
keyFileSelectionView.error = getString(R.string.error_nokeyfile)
}
}
return error
}
private fun verifyHardwareKey(): Boolean {
var error = false
hardwareKeySelectionView.error = null
if (hardwareKeyCheckBox.isChecked) {
hardwareKeySelectionView.hardwareKey?.let { hardwareKey ->
mHardwareKey = hardwareKey
} ?: run {
error = true
hardwareKeySelectionView.error = getString(R.string.error_no_hardware_key)
}
}
return error
}
private fun retrieveMainCredential(): MainCredential { private fun retrieveMainCredential(): MainCredential {
val masterPassword = if (passwordCheckBox!!.isChecked) mMasterPassword else null val masterPassword = if (passwordCheckBox.isChecked) mMasterPassword else null
val keyFile = if (keyFileCheckBox!!.isChecked) mKeyFile else null val keyFileUri = if (keyFileCheckBox.isChecked) mKeyFileUri else null
return MainCredential(masterPassword, keyFile) val hardwareKey = if (hardwareKeyCheckBox.isChecked) mHardwareKey else null
return MainCredential(masterPassword, keyFileUri, hardwareKey)
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// To check checkboxes if a text is present // To check checkboxes if a text is present
passKeyView?.addTextChangedListener(passwordTextWatcher) passwordView.addTextChangedListener(passwordTextWatcher)
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
passKeyView?.removeTextChangedListener(passwordTextWatcher) passwordView.removeTextChangedListener(passwordTextWatcher)
}
private fun verifyPassword(): Boolean {
var error = false
if (passwordCheckBox != null
&& passwordCheckBox!!.isChecked
&& passKeyView != null
&& passwordRepeatView != null) {
mMasterPassword = passKeyView!!.passwordString
val confPassword = passwordRepeatView!!.text.toString()
// Verify that passwords match
if (mMasterPassword != confPassword) {
error = true
// Passwords do not match
passwordRepeatTextInputLayout?.error = getString(R.string.error_pass_match)
}
if ((mMasterPassword == null
|| mMasterPassword!!.isEmpty())
&& (keyFileCheckBox == null
|| !keyFileCheckBox!!.isChecked
|| keyFileSelectionView?.uri == null)) {
error = true
showEmptyPasswordConfirmationDialog()
}
}
return error
}
private fun verifyKeyFile(): Boolean {
var error = false
if (keyFileCheckBox != null
&& keyFileCheckBox!!.isChecked) {
keyFileSelectionView?.uri?.let { uri ->
mKeyFile = uri
} ?: run {
error = true
keyFileSelectionView?.error = getString(R.string.error_nokeyfile)
}
}
return error
} }
private fun showEmptyPasswordConfirmationDialog() { private fun showEmptyPasswordConfirmationDialog() {
@@ -262,10 +315,8 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
val builder = AlertDialog.Builder(it) val builder = AlertDialog.Builder(it)
builder.setMessage(R.string.warning_empty_password) builder.setMessage(R.string.warning_empty_password)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
if (!verifyKeyFile()) { mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential()) this@SetMainCredentialDialogFragment.dismiss()
this@SetMainCredentialDialogFragment.dismiss()
}
} }
.setNegativeButton(android.R.string.cancel) { _, _ -> } .setNegativeButton(android.R.string.cancel) { _, _ -> }
mEmptyPasswordConfirmationDialog = builder.create() mEmptyPasswordConfirmationDialog = builder.create()
@@ -299,8 +350,8 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
}) })
.setPositiveButton(android.R.string.ok) { _, _ -> } .setPositiveButton(android.R.string.ok) { _, _ -> }
.setNegativeButton(android.R.string.cancel) { _, _ -> .setNegativeButton(android.R.string.cancel) { _, _ ->
keyFileCheckBox?.isChecked = false keyFileCheckBox.isChecked = false
keyFileSelectionView?.uri = null keyFileSelectionView.uri = null
} }
mEmptyKeyFileConfirmationDialog = builder.create() mEmptyKeyFileConfirmationDialog = builder.create()
mEmptyKeyFileConfirmationDialog?.show() mEmptyKeyFileConfirmationDialog?.show()

View File

@@ -39,6 +39,7 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
val builder = AlertDialog.Builder(activity) val builder = AlertDialog.Builder(activity)
val stringBuilder = SpannableStringBuilder() val stringBuilder = SpannableStringBuilder()
/*
if (UriUtil.contributingUser(activity)) { if (UriUtil.contributingUser(activity)) {
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_thanks), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n") stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_thanks), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_rose), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n") .append(HtmlCompat.fromHtml(getString(R.string.html_rose), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
@@ -46,14 +47,14 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_upgrade), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ") .append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_upgrade), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() } builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() }
} else { } else {
*/
stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n") stringBuilder.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature), HtmlCompat.FROM_HTML_MODE_LEGACY)).append("\n\n")
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_contibute), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ") .append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_contibute), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY)) .append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
builder.setPositiveButton(R.string.contribute) { _, _ -> builder.setPositiveButton(R.string.contribute) { _, _ ->
UriUtil.gotoUrl(requireContext(), R.string.contribution_url) UriUtil.gotoUrl(requireContext(), R.string.contribution_url)
} }
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() } //}
}
builder.setMessage(stringBuilder) builder.setMessage(stringBuilder)
// Create the AlertDialog object and return it // Create the AlertDialog object and return it
return builder.create() return builder.create()

View File

@@ -56,7 +56,7 @@ class ExternalFileHelper {
fun buildOpenDocument(onFileSelected: ((uri: Uri?) -> Unit)?) { fun buildOpenDocument(onFileSelected: ((uri: Uri?) -> Unit)?) {
val resultCallback = ActivityResultCallback<Uri> { result -> val resultCallback = ActivityResultCallback<Uri?> { result ->
result?.let { uri -> result?.let { uri ->
UriUtil.takeUriPermission(activity?.contentResolver, uri) UriUtil.takeUriPermission(activity?.contentResolver, uri)
onFileSelected?.invoke(uri) onFileSelected?.invoke(uri)
@@ -91,7 +91,7 @@ class ExternalFileHelper {
fun buildCreateDocument(typeString: String = "application/octet-stream", fun buildCreateDocument(typeString: String = "application/octet-stream",
onFileCreated: (fileCreated: Uri?)->Unit) { onFileCreated: (fileCreated: Uri?)->Unit) {
val resultCallback = ActivityResultCallback<Uri> { result -> val resultCallback = ActivityResultCallback<Uri?> { result ->
onFileCreated.invoke(result) onFileCreated.invoke(result)
} }
@@ -150,7 +150,7 @@ class ExternalFileHelper {
class OpenDocument : ActivityResultContracts.OpenDocument() { class OpenDocument : ActivityResultContracts.OpenDocument() {
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
override fun createIntent(context: Context, input: Array<out String>): Intent { override fun createIntent(context: Context, input: Array<String>): Intent {
return super.createIntent(context, input).apply { return super.createIntent(context, input).apply {
addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
@@ -178,11 +178,10 @@ class ExternalFileHelper {
} }
} }
class CreateDocument(private val typeString: String) : ActivityResultContracts.CreateDocument() { class CreateDocument(typeString: String) : ActivityResultContracts.CreateDocument(typeString) {
override fun createIntent(context: Context, input: String): Intent { override fun createIntent(context: Context, input: String): Intent {
return super.createIntent(context, input).apply { return super.createIntent(context, input).apply {
addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
type = typeString
} }
} }
} }

View File

@@ -6,8 +6,8 @@ import androidx.activity.viewModels
import com.kunzisoft.keepass.activities.stylish.StylishActivity import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
@@ -20,7 +20,7 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
mDatabaseTaskProvider = DatabaseTaskProvider(this) mDatabaseTaskProvider = DatabaseTaskProvider(this, showDatabaseDialog())
mDatabaseTaskProvider?.onDatabaseRetrieved = { database -> mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
val databaseWasReloaded = database?.wasReloaded == true val databaseWasReloaded = database?.wasReloaded == true
@@ -36,6 +36,17 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
} }
} }
protected open fun showDatabaseDialog(): Boolean {
return true
}
override fun onDestroy() {
mDatabaseTaskProvider?.destroy()
mDatabaseTaskProvider = null
mDatabase = null
super.onDestroy()
}
override fun onDatabaseRetrieved(database: Database?) { override fun onDatabaseRetrieved(database: Database?) {
mDatabase = database mDatabase = database
mDatabaseViewModel.defineDatabase(database) mDatabaseViewModel.defineDatabase(database)

View File

@@ -32,7 +32,6 @@ import android.widget.Toast
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DatabaseDialogFragment
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
@@ -44,7 +43,7 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.icons.IconDrawableFactory import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.model.GroupInfo import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -91,8 +90,8 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
mDatabaseTaskProvider?.startDatabaseSave(save) mDatabaseTaskProvider?.startDatabaseSave(save)
} }
mDatabaseViewModel.mergeDatabase.observe(this) { mDatabaseViewModel.mergeDatabase.observe(this) { save ->
mDatabaseTaskProvider?.startDatabaseMerge() mDatabaseTaskProvider?.startDatabaseMerge(save)
} }
mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid -> mDatabaseViewModel.reloadDatabase.observe(this) { fixDuplicateUuid ->
@@ -228,6 +227,9 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
// Reload the current activity // Reload the current activity
if (result.isSuccess) { if (result.isSuccess) {
reloadActivity() reloadActivity()
if (actionTask == DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK) {
Toast.makeText(this, R.string.merge_success, Toast.LENGTH_LONG).show()
}
} else { } else {
this.showActionErrorIfNeeded(result) this.showActionErrorIfNeeded(result)
finish() finish()
@@ -271,11 +273,11 @@ abstract class DatabaseLockActivity : DatabaseModeActivity(),
} }
fun mergeDatabase() { fun mergeDatabase() {
mDatabaseTaskProvider?.startDatabaseMerge() mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable)
} }
fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) { fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
mDatabaseTaskProvider?.startDatabaseMerge(uri, mainCredential) mDatabaseTaskProvider?.startDatabaseMerge(mAutoSaveEnable, uri, mainCredential)
} }
fun reloadDatabase() { fun reloadDatabase() {

View File

@@ -21,14 +21,21 @@ package com.kunzisoft.keepass.activities.stylish
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import android.view.WindowManager import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.WindowManager.LayoutParams.FLAG_SECURE
import androidx.annotation.StyleRes import androidx.annotation.StyleRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_PREFERENCE_CHANGED import com.kunzisoft.keepass.settings.NestedAppSettingsFragment.Companion.DATABASE_PREFERENCE_CHANGED
import com.kunzisoft.keepass.settings.PreferencesUtil
/** /**
* Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from * Stylish Hide Activity that apply a dynamic style and sets FLAG_SECURE to prevent screenshots / from
@@ -81,8 +88,24 @@ abstract class StylishActivity : AppCompatActivity() {
setTheme(themeId) setTheme(themeId)
} }
PreferenceManager.getDefaultSharedPreferences(this)
.registerOnSharedPreferenceChangeListener(onScreenshotModePrefListener)
}
private val onScreenshotModePrefListener = OnSharedPreferenceChangeListener { _, key ->
if (key != getString(R.string.enable_screenshot_mode_key)) return@OnSharedPreferenceChangeListener
setScreenshotMode(PreferencesUtil.isScreenshotModeEnabled(this))
}
private fun setScreenshotMode(isEnabled: Boolean) {
findViewById<View>(R.id.screenshot_mode_banner)?.visibility = if (isEnabled) VISIBLE else GONE
// Several gingerbread devices have problems with FLAG_SECURE // Several gingerbread devices have problems with FLAG_SECURE
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE) if (isEnabled) {
window.clearFlags(FLAG_SECURE)
} else {
window.setFlags(FLAG_SECURE, FLAG_SECURE)
}
} }
override fun onResume() { override fun onResume() {
@@ -94,6 +117,7 @@ abstract class StylishActivity : AppCompatActivity() {
Log.d(this.javaClass.name, "Theme change detected, restarting activity") Log.d(this.javaClass.name, "Theme change detected, restarting activity")
recreateActivity() recreateActivity()
} }
setScreenshotMode(PreferencesUtil.isScreenshotModeEnabled(this))
} }
private fun recreateActivity() { private fun recreateActivity() {

View File

@@ -23,8 +23,15 @@ import androidx.room.Database
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import android.content.Context import android.content.Context
import androidx.room.AutoMigration
@Database(version = 1, entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class]) @Database(
version = 2,
entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class],
autoMigrations = [
AutoMigration (from = 1, to = 2)
]
)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun fileDatabaseHistoryDao(): FileDatabaseHistoryDao abstract fun fileDatabaseHistoryDao(): FileDatabaseHistoryDao

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.app.database
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.DatabaseFile import com.kunzisoft.keepass.model.DatabaseFile
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.SingletonHolderParameter import com.kunzisoft.keepass.utils.SingletonHolderParameter
@@ -44,6 +45,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
DatabaseFile( DatabaseFile(
databaseUri, databaseUri,
UriUtil.parse(fileDatabaseHistoryEntity?.keyFileUri), UriUtil.parse(fileDatabaseHistoryEntity?.keyFileUri),
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity?.hardwareKey),
UriUtil.decode(fileDatabaseHistoryEntity?.databaseUri), UriUtil.decode(fileDatabaseHistoryEntity?.databaseUri),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias ?: ""), fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias ?: ""),
fileDatabaseInfo.exists, fileDatabaseInfo.exists,
@@ -85,13 +87,14 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|| !hideBrokenLocations) { || !hideBrokenLocations) {
databaseFileListLoaded.add( databaseFileListLoaded.add(
DatabaseFile( DatabaseFile(
UriUtil.parse(fileDatabaseHistoryEntity.databaseUri), UriUtil.parse(fileDatabaseHistoryEntity.databaseUri),
UriUtil.parse(fileDatabaseHistoryEntity.keyFileUri), UriUtil.parse(fileDatabaseHistoryEntity.keyFileUri),
UriUtil.decode(fileDatabaseHistoryEntity.databaseUri), HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity.hardwareKey),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias), UriUtil.decode(fileDatabaseHistoryEntity.databaseUri),
fileDatabaseInfo.exists, fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
fileDatabaseInfo.getLastModificationString(), fileDatabaseInfo.exists,
fileDatabaseInfo.getSizeString() fileDatabaseInfo.getLastModificationString(),
fileDatabaseInfo.getSizeString()
) )
) )
} }
@@ -107,11 +110,14 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
).execute() ).execute()
} }
fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null, fun addOrUpdateDatabaseUri(databaseUri: Uri,
keyFileUri: Uri? = null,
hardwareKey: HardwareKey? = null,
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) { databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
addOrUpdateDatabaseFile(DatabaseFile( addOrUpdateDatabaseFile(DatabaseFile(
databaseUri, databaseUri,
keyFileUri keyFileUri,
hardwareKey
), databaseFileAddedOrUpdatedResult) ), databaseFileAddedOrUpdatedResult)
} }
@@ -130,6 +136,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
?: fileDatabaseHistoryRetrieve?.databaseAlias ?: fileDatabaseHistoryRetrieve?.databaseAlias
?: "", ?: "",
databaseFileToAddOrUpdate.keyFileUri?.toString(), databaseFileToAddOrUpdate.keyFileUri?.toString(),
databaseFileToAddOrUpdate.hardwareKey?.value,
System.currentTimeMillis() System.currentTimeMillis()
) )
@@ -147,13 +154,14 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
val fileDatabaseInfo = FileDatabaseInfo(applicationContext, val fileDatabaseInfo = FileDatabaseInfo(applicationContext,
fileDatabaseHistory.databaseUri) fileDatabaseHistory.databaseUri)
DatabaseFile( DatabaseFile(
UriUtil.parse(fileDatabaseHistory.databaseUri), UriUtil.parse(fileDatabaseHistory.databaseUri),
UriUtil.parse(fileDatabaseHistory.keyFileUri), UriUtil.parse(fileDatabaseHistory.keyFileUri),
UriUtil.decode(fileDatabaseHistory.databaseUri), HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias), UriUtil.decode(fileDatabaseHistory.databaseUri),
fileDatabaseInfo.exists, fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
fileDatabaseInfo.getLastModificationString(), fileDatabaseInfo.exists,
fileDatabaseInfo.getSizeString() fileDatabaseInfo.getLastModificationString(),
fileDatabaseInfo.getSizeString()
) )
} }
}, },
@@ -172,10 +180,11 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
val returnValue = databaseFileHistoryDao.delete(fileDatabaseHistory) val returnValue = databaseFileHistoryDao.delete(fileDatabaseHistory)
if (returnValue > 0) { if (returnValue > 0) {
DatabaseFile( DatabaseFile(
UriUtil.parse(fileDatabaseHistory.databaseUri), UriUtil.parse(fileDatabaseHistory.databaseUri),
UriUtil.parse(fileDatabaseHistory.keyFileUri), UriUtil.parse(fileDatabaseHistory.keyFileUri),
UriUtil.decode(fileDatabaseHistory.databaseUri), HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
databaseFileToDelete.databaseAlias UriUtil.decode(fileDatabaseHistory.databaseUri),
databaseFileToDelete.databaseAlias
) )
} else { } else {
null null

View File

@@ -35,6 +35,9 @@ data class FileDatabaseHistoryEntity(
@ColumnInfo(name = "keyfile_uri") @ColumnInfo(name = "keyfile_uri")
var keyFileUri: String?, var keyFileUri: String?,
@ColumnInfo(name = "hardware_key")
var hardwareKey: String?,
@ColumnInfo(name = "updated") @ColumnInfo(name = "updated")
val updated: Long val updated: Long
) { ) {

View File

@@ -34,12 +34,10 @@ import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.getkeepsafe.taptargetview.TapTargetView
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.StylishFragment import com.kunzisoft.keepass.activities.stylish.StylishFragment
import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.database.exception.IODatabaseException import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
import com.kunzisoft.keepass.education.PasswordActivityEducation
import com.kunzisoft.keepass.model.CipherDecryptDatabase import com.kunzisoft.keepass.model.CipherDecryptDatabase
import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.CredentialStorage import com.kunzisoft.keepass.model.CredentialStorage
@@ -398,7 +396,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
} }
} ?: deleteEncryptedDatabaseKey() } ?: deleteEncryptedDatabaseKey()
} }
} ?: throw IODatabaseException() } ?: throw UnknownDatabaseLocationException()
} ?: throw Exception("AdvancedUnlockManager not initialized") } ?: throw Exception("AdvancedUnlockManager not initialized")
} }

View File

@@ -24,15 +24,16 @@ import android.net.Uri
import com.kunzisoft.keepass.app.database.CipherDatabaseAction import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.database.element.MainCredential
open class AssignMainCredentialInDatabaseRunnable ( open class AssignMainCredentialInDatabaseRunnable (
context: Context, context: Context,
database: Database, database: Database,
protected val mDatabaseUri: Uri, protected val mDatabaseUri: Uri,
protected val mMainCredential: MainCredential) mainCredential: MainCredential,
: SaveDatabaseRunnable(context, database, true) { challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: SaveDatabaseRunnable(context, database, true, mainCredential, challengeResponseRetriever) {
private var mBackupKey: ByteArray? = null private var mBackupKey: ByteArray? = null
@@ -40,10 +41,7 @@ open class AssignMainCredentialInDatabaseRunnable (
// Set key // Set key
try { try {
mBackupKey = ByteArray(database.masterKey.size) mBackupKey = ByteArray(database.masterKey.size)
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size) database.masterKey.copyInto(mBackupKey!!)
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri)
database.assignMasterKey(mMainCredential.masterPassword, uriInputStream)
} catch (e: Exception) { } catch (e: Exception) {
erase(mBackupKey) erase(mBackupKey)
setError(e) setError(e)

View File

@@ -24,7 +24,8 @@ import android.net.Uri
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
class CreateDatabaseRunnable(context: Context, class CreateDatabaseRunnable(context: Context,
@@ -33,9 +34,10 @@ class CreateDatabaseRunnable(context: Context,
private val databaseName: String, private val databaseName: String,
private val rootName: String, private val rootName: String,
private val templateGroupName: String?, private val templateGroupName: String?,
mainCredential: MainCredential, val mainCredential: MainCredential,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
private val createDatabaseResult: ((Result) -> Unit)?) private val createDatabaseResult: ((Result) -> Unit)?)
: AssignMainCredentialInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) { : AssignMainCredentialInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential, challengeResponseRetriever) {
override fun onStartRun() { override fun onStartRun() {
try { try {
@@ -58,8 +60,11 @@ class CreateDatabaseRunnable(context: Context,
// Add database to recent files // Add database to recent files
if (PreferencesUtil.rememberDatabaseLocations(context)) { if (PreferencesUtil.rememberDatabaseLocations(context)) {
FileDatabaseHistoryAction.getInstance(context.applicationContext) FileDatabaseHistoryAction.getInstance(context.applicationContext)
.addOrUpdateDatabaseUri(mDatabaseUri, .addOrUpdateDatabaseUri(
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null) mDatabaseUri,
if (PreferencesUtil.rememberKeyFileLocations(context)) mainCredential.keyFileUri else null,
if (PreferencesUtil.rememberHardwareKey(context)) mainCredential.hardwareKey else null,
)
} }
// Register the current time to init the lock timer // Register the current time to init the lock timer
@@ -72,6 +77,9 @@ class CreateDatabaseRunnable(context: Context,
override fun onFinishRun() { override fun onFinishRun() {
super.onFinishRun() super.onFinishRun()
if (result.isSuccess) {
mDatabase.loaded = true
}
createDatabaseResult?.invoke(result) createDatabaseResult?.invoke(result)
} }
} }

View File

@@ -19,7 +19,6 @@
*/ */
package com.kunzisoft.keepass.database.action package com.kunzisoft.keepass.database.action
import android.app.Service
import android.content.* import android.content.*
import android.content.Context.* import android.content.Context.*
import android.net.Uri import android.net.Uri
@@ -38,14 +37,16 @@ import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.ProgressMessage
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_CHALLENGE_RESPONDED
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
@@ -89,11 +90,12 @@ import java.util.*
* Utility class to connect an activity or a service to the DatabaseTaskNotificationService, * Utility class to connect an activity or a service to the DatabaseTaskNotificationService,
* Useful to retrieve a database instance and sending tasks commands * Useful to retrieve a database instance and sending tasks commands
*/ */
class DatabaseTaskProvider { class DatabaseTaskProvider(private var context: Context,
private var showDialog: Boolean = true) {
private var activity: FragmentActivity? = null // To show dialog only if context is an activity
private var service: Service? = null private var activity: FragmentActivity? = try { context as? FragmentActivity? }
private var context: Context catch (_: Exception) { null }
var onDatabaseRetrieved: ((database: Database?) -> Unit)? = null var onDatabaseRetrieved: ((database: Database?) -> Unit)? = null
@@ -101,7 +103,10 @@ class DatabaseTaskProvider {
actionTask: String, actionTask: String,
result: ActionRunnable.Result) -> Unit)? = null result: ActionRunnable.Result) -> Unit)? = null
private var intentDatabaseTask: Intent private var intentDatabaseTask: Intent = Intent(
context.applicationContext,
DatabaseTaskNotificationService::class.java
)
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
@@ -111,30 +116,33 @@ class DatabaseTaskProvider {
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
constructor(activity: FragmentActivity) { fun destroy() {
this.activity = activity this.activity = null
this.context = activity this.onDatabaseRetrieved = null
this.intentDatabaseTask = Intent(activity.applicationContext, this.onActionFinish = null
DatabaseTaskNotificationService::class.java) this.databaseTaskBroadcastReceiver = null
} this.mBinder = null
this.serviceConnection = null
constructor(service: Service) { this.progressTaskDialogFragment = null
this.service = service this.databaseChangedDialogFragment = null
this.context = service
this.intentDatabaseTask = Intent(service.applicationContext,
DatabaseTaskNotificationService::class.java)
} }
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener { private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
override fun onStartAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) { override fun onStartAction(database: Database,
startDialog(titleId, messageId, warningId) progressMessage: ProgressMessage) {
if (showDialog)
startDialog(progressMessage)
} }
override fun onUpdateAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) { override fun onUpdateAction(database: Database,
updateDialog(titleId, messageId, warningId) progressMessage: ProgressMessage) {
if (showDialog)
updateDialog(progressMessage)
} }
override fun onStopAction(database: Database, actionTask: String, result: ActionRunnable.Result) { override fun onStopAction(database: Database,
actionTask: String,
result: ActionRunnable.Result) {
onActionFinish?.invoke(database, actionTask, result) onActionFinish?.invoke(database, actionTask, result)
// Remove the progress task // Remove the progress task
stopDialog() stopDialog()
@@ -181,9 +189,7 @@ class DatabaseTaskProvider {
} }
} }
private fun startDialog(titleId: Int? = null, private fun startDialog(progressMessage: ProgressMessage) {
messageId: Int? = null,
warningId: Int? = null) {
activity?.let { activity -> activity?.let { activity ->
activity.lifecycleScope.launch { activity.lifecycleScope.launch {
if (progressTaskDialogFragment == null) { if (progressTaskDialogFragment == null) {
@@ -197,22 +203,17 @@ class DatabaseTaskProvider {
PROGRESS_TASK_DIALOG_TAG PROGRESS_TASK_DIALOG_TAG
) )
} }
updateDialog(titleId, messageId, warningId) updateDialog(progressMessage)
} }
} }
} }
private fun updateDialog(titleId: Int?, messageId: Int?, warningId: Int?) { private fun updateDialog(progressMessage: ProgressMessage) {
progressTaskDialogFragment?.apply { progressTaskDialogFragment?.apply {
titleId?.let { updateTitle(progressMessage.titleId)
updateTitle(it) updateMessage(progressMessage.messageId)
} updateWarning(progressMessage.warningId)
messageId?.let { setCancellable(progressMessage.cancelable)
updateMessage(it)
}
warningId?.let {
updateWarning(it)
}
} }
} }
@@ -226,9 +227,7 @@ class DatabaseTaskProvider {
serviceConnection = object : ServiceConnection { serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) { override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply { mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
addDatabaseListener(databaseListener) addServiceListeners(this)
addDatabaseFileInfoListener(databaseInfoListener)
addActionTaskListener(actionTaskListener)
getService().checkDatabase() getService().checkDatabase()
getService().checkDatabaseInfo() getService().checkDatabaseInfo()
getService().checkAction() getService().checkAction()
@@ -236,15 +235,25 @@ class DatabaseTaskProvider {
} }
override fun onServiceDisconnected(name: ComponentName?) { override fun onServiceDisconnected(name: ComponentName?) {
mBinder?.removeActionTaskListener(actionTaskListener) removeServiceListeners(mBinder)
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
mBinder?.removeDatabaseListener(databaseListener)
mBinder = null mBinder = null
} }
} }
} }
} }
private fun addServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
service?.addDatabaseListener(databaseListener)
service?.addDatabaseFileInfoListener(databaseInfoListener)
service?.addActionTaskListener(actionTaskListener)
}
private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
service?.removeActionTaskListener(actionTaskListener)
service?.removeDatabaseFileInfoListener(databaseInfoListener)
service?.removeDatabaseListener(databaseListener)
}
private fun bindService() { private fun bindService() {
initServiceConnection() initServiceConnection()
serviceConnection?.let { serviceConnection?.let {
@@ -262,10 +271,6 @@ class DatabaseTaskProvider {
serviceConnection = null serviceConnection = null
} }
fun isBinded(): Boolean {
return mBinder != null
}
fun registerProgressTask() { fun registerProgressTask() {
stopDialog() stopDialog()
@@ -299,9 +304,7 @@ class DatabaseTaskProvider {
fun unregisterProgressTask() { fun unregisterProgressTask() {
stopDialog() stopDialog()
mBinder?.removeActionTaskListener(actionTaskListener) removeServiceListeners(mBinder)
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
mBinder?.removeDatabaseListener(databaseListener)
mBinder = null mBinder = null
unBindService() unBindService()
@@ -321,7 +324,7 @@ class DatabaseTaskProvider {
context.startService(intentDatabaseTask) context.startService(intentDatabaseTask)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Unable to perform database action", e) Log.e(TAG, "Unable to perform database action", e)
Toast.makeText(activity, R.string.error_start_database_action, Toast.LENGTH_LONG).show() Toast.makeText(context, R.string.error_start_database_action, Toast.LENGTH_LONG).show()
} }
} }
@@ -332,7 +335,8 @@ class DatabaseTaskProvider {
*/ */
fun startDatabaseCreate(databaseUri: Uri, fun startDatabaseCreate(databaseUri: Uri,
mainCredential: MainCredential) { mainCredential: MainCredential
) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential) putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
@@ -355,9 +359,11 @@ class DatabaseTaskProvider {
, ACTION_DATABASE_LOAD_TASK) , ACTION_DATABASE_LOAD_TASK)
} }
fun startDatabaseMerge(fromDatabaseUri: Uri? = null, fun startDatabaseMerge(save: Boolean,
fromDatabaseUri: Uri? = null,
mainCredential: MainCredential? = null) { mainCredential: MainCredential? = null) {
start(Bundle().apply { start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri) putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential) putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
} }
@@ -385,7 +391,8 @@ class DatabaseTaskProvider {
} }
fun startDatabaseAssignPassword(databaseUri: Uri, fun startDatabaseAssignPassword(databaseUri: Uri,
mainCredential: MainCredential) { mainCredential: MainCredential
) {
start(Bundle().apply { start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri) putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
@@ -702,6 +709,13 @@ class DatabaseTaskProvider {
, ACTION_DATABASE_SAVE) , ACTION_DATABASE_SAVE)
} }
fun startChallengeResponded(response: ByteArray?) {
start(Bundle().apply {
putByteArray(DatabaseTaskNotificationService.DATA_BYTES, response)
}
, ACTION_CHALLENGE_RESPONDED)
}
companion object { companion object {
private val TAG = DatabaseTaskProvider::class.java.name private val TAG = DatabaseTaskProvider::class.java.name
} }

View File

@@ -25,9 +25,10 @@ import com.kunzisoft.keepass.app.database.CipherDatabaseAction
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.database.exception.DatabaseInputException
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
@@ -35,8 +36,9 @@ import com.kunzisoft.keepass.utils.UriUtil
class LoadDatabaseRunnable(private val context: Context, class LoadDatabaseRunnable(private val context: Context,
private val mDatabase: Database, private val mDatabase: Database,
private val mUri: Uri, private val mDatabaseUri: Uri,
private val mMainCredential: MainCredential, private val mMainCredential: MainCredential,
private val mChallengeResponseRetriever: (hardwareKey: HardwareKey, seed: ByteArray?) -> ByteArray,
private val mReadonly: Boolean, private val mReadonly: Boolean,
private val mCipherEncryptDatabase: CipherEncryptDatabase?, private val mCipherEncryptDatabase: CipherEncryptDatabase?,
private val mFixDuplicateUUID: Boolean, private val mFixDuplicateUUID: Boolean,
@@ -51,18 +53,21 @@ class LoadDatabaseRunnable(private val context: Context,
override fun onActionRun() { override fun onActionRun() {
try { try {
mDatabase.loadData(mUri, mDatabase.loadData(
mMainCredential, context.contentResolver,
mReadonly, mDatabaseUri,
context.contentResolver, mMainCredential,
UriUtil.getBinaryDir(context), mChallengeResponseRetriever,
{ memoryWanted -> mReadonly,
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted) UriUtil.getBinaryDir(context),
}, { memoryWanted ->
mFixDuplicateUUID, BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
progressTaskUpdater) },
mFixDuplicateUUID,
progressTaskUpdater
)
} }
catch (e: LoadDatabaseException) { catch (e: DatabaseInputException) {
setError(e) setError(e)
} }
@@ -70,8 +75,11 @@ class LoadDatabaseRunnable(private val context: Context,
// Save keyFile in app database // Save keyFile in app database
if (PreferencesUtil.rememberDatabaseLocations(context)) { if (PreferencesUtil.rememberDatabaseLocations(context)) {
FileDatabaseHistoryAction.getInstance(context) FileDatabaseHistoryAction.getInstance(context)
.addOrUpdateDatabaseUri(mUri, .addOrUpdateDatabaseUri(
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null) mDatabaseUri,
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null,
if (PreferencesUtil.rememberHardwareKey(context)) mMainCredential.hardwareKey else null,
)
} }
// Register the biometric // Register the biometric

View File

@@ -22,36 +22,43 @@ package com.kunzisoft.keepass.database.action
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.database.exception.DatabaseException
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
class MergeDatabaseRunnable(private val context: Context, class MergeDatabaseRunnable(
private val mDatabase: Database, context: Context,
private val mDatabaseToMergeUri: Uri?, private val mDatabaseToMergeUri: Uri?,
private val mDatabaseToMergeMainCredential: MainCredential?, private val mDatabaseToMergeMainCredential: MainCredential?,
private val progressTaskUpdater: ProgressTaskUpdater?, private val mDatabaseToMergeChallengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
private val mLoadDatabaseResult: ((Result) -> Unit)?) database: Database,
: ActionRunnable() { saveDatabase: Boolean,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
private val progressTaskUpdater: ProgressTaskUpdater?,
private val mLoadDatabaseResult: ((Result) -> Unit)?)
: SaveDatabaseRunnable(context, database, saveDatabase, null, challengeResponseRetriever) {
override fun onStartRun() { override fun onStartRun() {
mDatabase.wasReloaded = true database.wasReloaded = true
super.onStartRun()
} }
override fun onActionRun() { override fun onActionRun() {
try { try {
mDatabase.mergeData(mDatabaseToMergeUri, database.mergeData(
mDatabaseToMergeMainCredential,
context.contentResolver, context.contentResolver,
mDatabaseToMergeUri,
mDatabaseToMergeMainCredential,
mDatabaseToMergeChallengeResponseRetriever,
{ memoryWanted -> { memoryWanted ->
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted) BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
}, },
progressTaskUpdater progressTaskUpdater
) )
} catch (e: LoadDatabaseException) { } catch (e: DatabaseException) {
setError(e) setError(e)
} }
@@ -59,9 +66,11 @@ class MergeDatabaseRunnable(private val context: Context,
// Register the current time to init the lock timer // Register the current time to init the lock timer
PreferencesUtil.saveCurrentTime(context) PreferencesUtil.saveCurrentTime(context)
} }
super.onActionRun()
} }
override fun onFinishRun() { override fun onFinishRun() {
super.onFinishRun()
mLoadDatabaseResult?.invoke(result) mLoadDatabaseResult?.invoke(result)
} }
} }

View File

@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.database.action
import android.content.Context import android.content.Context
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.database.exception.DatabaseException
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
@@ -47,7 +47,7 @@ class ReloadDatabaseRunnable(private val context: Context,
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted) BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
}, },
progressTaskUpdater) progressTaskUpdater)
} catch (e: LoadDatabaseException) { } catch (e: DatabaseException) {
setError(e) setError(e)
} }

View File

@@ -21,12 +21,14 @@ package com.kunzisoft.keepass.database.action
import android.content.Context import android.content.Context
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.hardware.HardwareKey
class RemoveUnlinkedDataDatabaseRunnable ( class RemoveUnlinkedDataDatabaseRunnable (
context: Context, context: Context,
database: Database, database: Database,
saveDatabase: Boolean) saveDatabase: Boolean,
: SaveDatabaseRunnable(context, database, saveDatabase) { challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: SaveDatabaseRunnable(context, database, saveDatabase, null, challengeResponseRetriever) {
override fun onActionRun() { override fun onActionRun() {
try { try {

View File

@@ -23,11 +23,15 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.exception.DatabaseException import com.kunzisoft.keepass.database.exception.DatabaseException
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
open class SaveDatabaseRunnable(protected var context: Context, open class SaveDatabaseRunnable(protected var context: Context,
protected var database: Database, protected var database: Database,
private var saveDatabase: Boolean, private var saveDatabase: Boolean,
private var mainCredential: MainCredential?, // If null, uses composite Key
private var challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
private var databaseCopyUri: Uri? = null) private var databaseCopyUri: Uri? = null)
: ActionRunnable() { : ActionRunnable() {
@@ -39,7 +43,12 @@ open class SaveDatabaseRunnable(protected var context: Context,
database.checkVersion() database.checkVersion()
if (saveDatabase && result.isSuccess) { if (saveDatabase && result.isSuccess) {
try { try {
database.saveData(databaseCopyUri, context.contentResolver) database.saveData(
context.contentResolver,
context.cacheDir,
databaseCopyUri,
mainCredential,
challengeResponseRetriever)
} catch (e: DatabaseException) { } catch (e: DatabaseException) {
setError(e) setError(e)
} }

View File

@@ -22,14 +22,16 @@ package com.kunzisoft.keepass.database.action
import android.content.Context import android.content.Context
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.hardware.HardwareKey
class UpdateCompressionBinariesDatabaseRunnable ( class UpdateCompressionBinariesDatabaseRunnable (
context: Context, context: Context,
database: Database, database: Database,
private val oldCompressionAlgorithm: CompressionAlgorithm, private val oldCompressionAlgorithm: CompressionAlgorithm,
private val newCompressionAlgorithm: CompressionAlgorithm, private val newCompressionAlgorithm: CompressionAlgorithm,
saveDatabase: Boolean) saveDatabase: Boolean,
: SaveDatabaseRunnable(context, database, saveDatabase) { challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: SaveDatabaseRunnable(context, database, saveDatabase, null, challengeResponseRetriever) {
override fun onStartRun() { override fun onStartRun() {
// Set new compression // Set new compression

View File

@@ -23,14 +23,16 @@ import android.content.Context
import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.hardware.HardwareKey
class DeleteEntryHistoryDatabaseRunnable ( class DeleteEntryHistoryDatabaseRunnable (
context: Context, context: Context,
database: Database, database: Database,
private val mainEntry: Entry, private val mainEntry: Entry,
private val entryHistoryPosition: Int, private val entryHistoryPosition: Int,
saveDatabase: Boolean) saveDatabase: Boolean,
: SaveDatabaseRunnable(context, database, saveDatabase) { challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: SaveDatabaseRunnable(context, database, saveDatabase, null, challengeResponseRetriever) {
override fun onStartRun() { override fun onStartRun() {
try { try {

View File

@@ -23,6 +23,7 @@ import android.content.Context
import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
class RestoreEntryHistoryDatabaseRunnable ( class RestoreEntryHistoryDatabaseRunnable (
@@ -30,7 +31,8 @@ class RestoreEntryHistoryDatabaseRunnable (
private val database: Database, private val database: Database,
private val mainEntry: Entry, private val mainEntry: Entry,
private val entryHistoryPosition: Int, private val entryHistoryPosition: Int,
private val saveDatabase: Boolean) private val saveDatabase: Boolean,
private val challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionRunnable() { : ActionRunnable() {
private var updateEntryRunnable: UpdateEntryRunnable? = null private var updateEntryRunnable: UpdateEntryRunnable? = null
@@ -43,12 +45,15 @@ class RestoreEntryHistoryDatabaseRunnable (
historyToRestore.addEntryToHistory(it) historyToRestore.addEntryToHistory(it)
} }
// Update the entry with the fresh formatted entry to restore // Update the entry with the fresh formatted entry to restore
updateEntryRunnable = UpdateEntryRunnable(context, updateEntryRunnable = UpdateEntryRunnable(
database, context,
mainEntry, database,
historyToRestore, mainEntry,
saveDatabase, historyToRestore,
null) saveDatabase,
null,
challengeResponseRetriever
)
updateEntryRunnable?.onStartRun() updateEntryRunnable?.onStartRun()

View File

@@ -22,13 +22,15 @@ package com.kunzisoft.keepass.database.action.node
import android.content.Context import android.content.Context
import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.hardware.HardwareKey
abstract class ActionNodeDatabaseRunnable( abstract class ActionNodeDatabaseRunnable(
context: Context, context: Context,
database: Database, database: Database,
private val afterActionNodesFinish: AfterActionNodesFinish?, private val afterActionNodesFinish: AfterActionNodesFinish?,
save: Boolean) save: Boolean,
: SaveDatabaseRunnable(context, database, save) { challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: SaveDatabaseRunnable(context, database, save, null, challengeResponseRetriever) {
/** /**
* Function do to a node action * Function do to a node action

View File

@@ -24,6 +24,7 @@ import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.hardware.HardwareKey
class AddEntryRunnable constructor( class AddEntryRunnable constructor(
context: Context, context: Context,
@@ -31,8 +32,9 @@ class AddEntryRunnable constructor(
private val mNewEntry: Entry, private val mNewEntry: Entry,
private val mParent: Group, private val mParent: Group,
save: Boolean, save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?) afterActionNodesFinish: AfterActionNodesFinish?,
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
override fun nodeAction() { override fun nodeAction() {
mNewEntry.touch(modified = true, touchParents = true) mNewEntry.touch(modified = true, touchParents = true)

View File

@@ -23,6 +23,7 @@ import android.content.Context
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.hardware.HardwareKey
class AddGroupRunnable constructor( class AddGroupRunnable constructor(
context: Context, context: Context,
@@ -30,8 +31,9 @@ class AddGroupRunnable constructor(
private val mNewGroup: Group, private val mNewGroup: Group,
private val mParent: Group, private val mParent: Group,
save: Boolean, save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?) afterActionNodesFinish: AfterActionNodesFinish?,
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
override fun nodeAction() { override fun nodeAction() {
mNewGroup.touch(modified = true, touchParents = true) mNewGroup.touch(modified = true, touchParents = true)

View File

@@ -21,11 +21,14 @@ package com.kunzisoft.keepass.database.action.node
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.exception.CopyEntryDatabaseException import com.kunzisoft.keepass.database.exception.CopyEntryDatabaseException
import com.kunzisoft.keepass.database.exception.CopyGroupDatabaseException import com.kunzisoft.keepass.database.exception.CopyGroupDatabaseException
import com.kunzisoft.keepass.hardware.HardwareKey
class CopyNodesRunnable constructor( class CopyNodesRunnable constructor(
context: Context, context: Context,
@@ -33,8 +36,9 @@ class CopyNodesRunnable constructor(
private val mNodesToCopy: List<Node>, private val mNodesToCopy: List<Node>,
private val mNewParent: Group, private val mNewParent: Group,
save: Boolean, save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?) afterActionNodesFinish: AfterActionNodesFinish?,
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
private var mEntriesCopied = ArrayList<Entry>() private var mEntriesCopied = ArrayList<Entry>()

View File

@@ -20,16 +20,20 @@
package com.kunzisoft.keepass.database.action.node package com.kunzisoft.keepass.database.action.node
import android.content.Context import android.content.Context
import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.hardware.HardwareKey
class DeleteNodesRunnable(context: Context, class DeleteNodesRunnable(context: Context,
database: Database, database: Database,
private val mNodesToDelete: List<Node>, private val mNodesToDelete: List<Node>,
save: Boolean, save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish) afterActionNodesFinish: AfterActionNodesFinish,
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
private var mOldParent: Group? = null private var mOldParent: Group? = null
private var mCanRecycle: Boolean = false private var mCanRecycle: Boolean = false

View File

@@ -21,11 +21,14 @@ package com.kunzisoft.keepass.database.action.node
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.database.element.* import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.exception.MoveEntryDatabaseException import com.kunzisoft.keepass.database.exception.MoveEntryDatabaseException
import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException
import com.kunzisoft.keepass.hardware.HardwareKey
class MoveNodesRunnable constructor( class MoveNodesRunnable constructor(
context: Context, context: Context,
@@ -33,8 +36,9 @@ class MoveNodesRunnable constructor(
private val mNodesToMove: List<Node>, private val mNodesToMove: List<Node>,
private val mNewParent: Group, private val mNewParent: Group,
save: Boolean, save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?) afterActionNodesFinish: AfterActionNodesFinish?,
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
private var mOldParent: Group? = null private var mOldParent: Group? = null

View File

@@ -24,6 +24,7 @@ import com.kunzisoft.keepass.database.element.Attachment
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.hardware.HardwareKey
class UpdateEntryRunnable constructor( class UpdateEntryRunnable constructor(
context: Context, context: Context,
@@ -31,8 +32,9 @@ class UpdateEntryRunnable constructor(
private val mOldEntry: Entry, private val mOldEntry: Entry,
private val mNewEntry: Entry, private val mNewEntry: Entry,
save: Boolean, save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?) afterActionNodesFinish: AfterActionNodesFinish?,
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
override fun nodeAction() { override fun nodeAction() {
if (mOldEntry.nodeId == mNewEntry.nodeId) { if (mOldEntry.nodeId == mNewEntry.nodeId) {

View File

@@ -23,6 +23,7 @@ import android.content.Context
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.hardware.HardwareKey
class UpdateGroupRunnable constructor( class UpdateGroupRunnable constructor(
context: Context, context: Context,
@@ -30,8 +31,9 @@ class UpdateGroupRunnable constructor(
private val mOldGroup: Group, private val mOldGroup: Group,
private val mNewGroup: Group, private val mNewGroup: Group,
save: Boolean, save: Boolean,
afterActionNodesFinish: AfterActionNodesFinish?) afterActionNodesFinish: AfterActionNodesFinish?,
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) { challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
override fun nodeAction() { override fun nodeAction() {
if (mOldGroup.nodeId == mNewGroup.nodeId) { if (mOldGroup.nodeId == mNewGroup.nodeId) {

View File

@@ -0,0 +1,34 @@
package com.kunzisoft.keepass.database.element
import com.kunzisoft.keepass.hardware.HardwareKey
data class CompositeKey(var passwordData: ByteArray? = null,
var keyFileData: ByteArray? = null,
var hardwareKey: HardwareKey? = null) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CompositeKey
if (passwordData != null) {
if (other.passwordData == null) return false
if (!passwordData.contentEquals(other.passwordData)) return false
} else if (other.passwordData != null) return false
if (keyFileData != null) {
if (other.keyFileData == null) return false
if (!keyFileData.contentEquals(other.keyFileData)) return false
} else if (other.keyFileData != null) return false
if (hardwareKey != other.hardwareKey) return false
return true
}
override fun hashCode(): Int {
var result = passwordData?.contentHashCode() ?: 0
result = 31 * result + (keyFileData?.contentHashCode() ?: 0)
result = 31 * result + (hardwareKey?.hashCode() ?: 0)
return result
}
}

View File

@@ -54,12 +54,10 @@ import com.kunzisoft.keepass.database.file.output.DatabaseOutputKDBX
import com.kunzisoft.keepass.database.merge.DatabaseKDBXMerger import com.kunzisoft.keepass.database.merge.DatabaseKDBXMerger
import com.kunzisoft.keepass.database.search.SearchHelper import com.kunzisoft.keepass.database.search.SearchHelper
import com.kunzisoft.keepass.database.search.SearchParameters import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.icons.IconDrawableFactory import com.kunzisoft.keepass.icons.IconDrawableFactory
import com.kunzisoft.keepass.model.MainCredential
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import com.kunzisoft.keepass.utils.SingletonHolder import com.kunzisoft.keepass.utils.*
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.readBytes4ToUInt
import java.io.* import java.io.*
import java.util.* import java.util.*
@@ -73,7 +71,7 @@ class Database {
var fileUri: Uri? = null var fileUri: Uri? = null
private set private set
private var mSearchHelper: SearchHelper? = null private var mSearchHelper: SearchHelper = SearchHelper()
var isReadOnly = false var isReadOnly = false
@@ -384,10 +382,14 @@ class Database {
set(masterKey) { set(masterKey) {
mDatabaseKDB?.masterKey = masterKey mDatabaseKDB?.masterKey = masterKey
mDatabaseKDBX?.masterKey = masterKey mDatabaseKDBX?.masterKey = masterKey
mDatabaseKDBX?.keyLastChanged = DateInstant()
mDatabaseKDBX?.settingsChanged = DateInstant() mDatabaseKDBX?.settingsChanged = DateInstant()
dataModifiedSinceLastLoading = true dataModifiedSinceLastLoading = true
} }
val transformSeed: ByteArray?
get() = mDatabaseKDB?.transformSeed ?: mDatabaseKDBX?.transformSeed
var rootGroup: Group? var rootGroup: Group?
get() { get() {
mDatabaseKDB?.rootGroup?.let { mDatabaseKDB?.rootGroup?.let {
@@ -553,83 +555,31 @@ class Database {
setDatabaseKDBX(newDatabase) setDatabaseKDBX(newDatabase)
this.fileUri = databaseUri this.fileUri = databaseUri
// Set Database state // Set Database state
this.loaded = true
this.dataModifiedSinceLastLoading = false this.dataModifiedSinceLastLoading = false
} }
@Throws(LoadDatabaseException::class) @Throws(DatabaseInputException::class)
private fun readDatabaseStream(contentResolver: ContentResolver, uri: Uri, fun loadData(
openDatabaseKDB: (InputStream) -> DatabaseKDB, contentResolver: ContentResolver,
openDatabaseKDBX: (InputStream) -> DatabaseKDBX) { databaseUri: Uri,
var databaseInputStream: InputStream? = null mainCredential: MainCredential,
try { challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
// Load Data, pass Uris as InputStreams readOnly: Boolean,
val databaseStream = UriUtil.getUriInputStream(contentResolver, uri) cacheDirectory: File,
?: throw IOException("Database input stream cannot be retrieve") isRAMSufficient: (memoryWanted: Long) -> Boolean,
fixDuplicateUUID: Boolean,
databaseInputStream = BufferedInputStream(databaseStream) progressTaskUpdater: ProgressTaskUpdater?
if (!databaseInputStream.markSupported()) { ) {
throw IOException("Input stream does not support mark.")
}
// We'll end up reading 8 bytes to identify the header. Might as well use two extra.
databaseInputStream.mark(10)
// Get the file directory to save the attachments
val sig1 = databaseInputStream.readBytes4ToUInt()
val sig2 = databaseInputStream.readBytes4ToUInt()
// Return to the start
databaseInputStream.reset()
when {
// Header of database KDB
DatabaseHeaderKDB.matchesHeader(sig1, sig2) -> setDatabaseKDB(openDatabaseKDB(databaseInputStream))
// Header of database KDBX
DatabaseHeaderKDBX.matchesHeader(sig1, sig2) -> setDatabaseKDBX(openDatabaseKDBX(databaseInputStream))
// Header not recognized
else -> throw SignatureDatabaseException()
}
this.mSearchHelper = SearchHelper()
loaded = true
} catch (e: LoadDatabaseException) {
throw e
} catch (e: Exception) {
throw LoadDatabaseException(e)
} finally {
databaseInputStream?.close()
}
}
@Throws(LoadDatabaseException::class)
fun loadData(uri: Uri,
mainCredential: MainCredential,
readOnly: Boolean,
contentResolver: ContentResolver,
cacheDirectory: File,
isRAMSufficient: (memoryWanted: Long) -> Boolean,
fixDuplicateUUID: Boolean,
progressTaskUpdater: ProgressTaskUpdater?) {
// Save database URI // Save database URI
this.fileUri = uri this.fileUri = databaseUri
// Check if the file is writable // Check if the file is writable
this.isReadOnly = readOnly this.isReadOnly = readOnly
// Pass KeyFile Uri as InputStreams
var keyFileInputStream: InputStream? = null
try { try {
// Get keyFile inputStream
mainCredential.keyFileUri?.let { keyFile ->
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyFile)
}
// Read database stream for the first time // Read database stream for the first time
readDatabaseStream(contentResolver, uri, readDatabaseStream(contentResolver, databaseUri,
{ databaseInputStream -> { databaseInputStream ->
val databaseKDB = DatabaseKDB().apply { val databaseKDB = DatabaseKDB().apply {
binaryCache.cacheDirectory = cacheDirectory binaryCache.cacheDirectory = cacheDirectory
@@ -639,12 +589,12 @@ class Database {
.openDatabase(databaseInputStream, .openDatabase(databaseInputStream,
progressTaskUpdater progressTaskUpdater
) { ) {
databaseKDB.retrieveMasterKey( databaseKDB.deriveMasterKey(
mainCredential.masterPassword, contentResolver,
keyFileInputStream mainCredential
) )
} }
databaseKDB setDatabaseKDB(databaseKDB)
}, },
{ databaseInputStream -> { databaseInputStream ->
val databaseKDBX = DatabaseKDBX().apply { val databaseKDBX = DatabaseKDBX().apply {
@@ -655,23 +605,23 @@ class Database {
setMethodToCheckIfRAMIsSufficient(isRAMSufficient) setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
openDatabase(databaseInputStream, openDatabase(databaseInputStream,
progressTaskUpdater) { progressTaskUpdater) {
databaseKDBX.retrieveMasterKey( databaseKDBX.deriveMasterKey(
mainCredential.masterPassword, contentResolver,
keyFileInputStream, mainCredential,
challengeResponseRetriever
) )
} }
} }
databaseKDBX setDatabaseKDBX(databaseKDBX)
} }
) )
} catch (e: FileNotFoundException) { loaded = true
throw FileNotFoundDatabaseException("Unable to load the keyfile")
} catch (e: LoadDatabaseException) {
throw e
} catch (e: Exception) { } catch (e: Exception) {
throw LoadDatabaseException(e) Log.e(TAG, "Unable to load the database")
if (e is DatabaseInputException)
throw e
throw DatabaseInputException(e)
} finally { } finally {
keyFileInputStream?.close()
dataModifiedSinceLastLoading = false dataModifiedSinceLastLoading = false
} }
} }
@@ -680,48 +630,44 @@ class Database {
return mDatabaseKDBX != null return mDatabaseKDBX != null
} }
@Throws(LoadDatabaseException::class) @Throws(DatabaseInputException::class)
fun mergeData(databaseToMergeUri: Uri?, fun mergeData(
databaseToMergeMainCredential: MainCredential?, contentResolver: ContentResolver,
contentResolver: ContentResolver, databaseToMergeUri: Uri?,
isRAMSufficient: (memoryWanted: Long) -> Boolean, databaseToMergeMainCredential: MainCredential?,
progressTaskUpdater: ProgressTaskUpdater?) { databaseToMergeChallengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
isRAMSufficient: (memoryWanted: Long) -> Boolean,
progressTaskUpdater: ProgressTaskUpdater?
) {
mDatabaseKDB?.let { mDatabaseKDB?.let {
throw IODatabaseException("Unable to merge from a database V1") throw MergeDatabaseKDBException()
} }
// New database instance to get new changes // New database instance to get new changes
val databaseToMerge = Database() val databaseToMerge = Database()
databaseToMerge.fileUri = databaseToMergeUri ?: this.fileUri databaseToMerge.fileUri = databaseToMergeUri ?: this.fileUri
// Pass KeyFile Uri as InputStreams
var keyFileInputStream: InputStream? = null
try { try {
val databaseUri = databaseToMerge.fileUri val databaseUri = databaseToMerge.fileUri
if (databaseUri != null) { if (databaseUri != null) {
if (databaseToMergeMainCredential != null) { readDatabaseStream(contentResolver, databaseUri,
// Get keyFile inputStream
databaseToMergeMainCredential.keyFileUri?.let { keyFile ->
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyFile)
}
}
databaseToMerge.readDatabaseStream(contentResolver, databaseUri,
{ databaseInputStream -> { databaseInputStream ->
val databaseToMergeKDB = DatabaseKDB() val databaseToMergeKDB = DatabaseKDB()
DatabaseInputKDB(databaseToMergeKDB) DatabaseInputKDB(databaseToMergeKDB)
.openDatabase(databaseInputStream, progressTaskUpdater) { .openDatabase(databaseInputStream, progressTaskUpdater) {
if (databaseToMergeMainCredential != null) { if (databaseToMergeMainCredential != null) {
databaseToMergeKDB.retrieveMasterKey( databaseToMergeKDB.deriveMasterKey(
databaseToMergeMainCredential.masterPassword, contentResolver,
keyFileInputStream, databaseToMergeMainCredential
) )
} else { } else {
databaseToMergeKDB.masterKey = masterKey this@Database.mDatabaseKDB?.let { thisDatabaseKDB ->
databaseToMergeKDB.copyMasterKeyFrom(thisDatabaseKDB)
}
} }
} }
databaseToMergeKDB databaseToMerge.setDatabaseKDB(databaseToMergeKDB)
}, },
{ databaseInputStream -> { databaseInputStream ->
val databaseToMergeKDBX = DatabaseKDBX() val databaseToMergeKDBX = DatabaseKDBX()
@@ -729,18 +675,22 @@ class Database {
setMethodToCheckIfRAMIsSufficient(isRAMSufficient) setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
openDatabase(databaseInputStream, progressTaskUpdater) { openDatabase(databaseInputStream, progressTaskUpdater) {
if (databaseToMergeMainCredential != null) { if (databaseToMergeMainCredential != null) {
databaseToMergeKDBX.retrieveMasterKey( databaseToMergeKDBX.deriveMasterKey(
databaseToMergeMainCredential.masterPassword, contentResolver,
keyFileInputStream, databaseToMergeMainCredential,
databaseToMergeChallengeResponseRetriever
) )
} else { } else {
databaseToMergeKDBX.masterKey = masterKey this@Database.mDatabaseKDBX?.let { thisDatabaseKDBX ->
databaseToMergeKDBX.copyMasterKeyFrom(thisDatabaseKDBX)
}
} }
} }
} }
databaseToMergeKDBX databaseToMerge.setDatabaseKDBX(databaseToMergeKDBX)
} }
) )
loaded = true
mDatabaseKDBX?.let { currentDatabaseKDBX -> mDatabaseKDBX?.let { currentDatabaseKDBX ->
val databaseMerger = DatabaseKDBXMerger(currentDatabaseKDBX).apply { val databaseMerger = DatabaseKDBXMerger(currentDatabaseKDBX).apply {
@@ -760,24 +710,24 @@ class Database {
} }
} }
} else { } else {
throw IODatabaseException("Database URI is null, database cannot be merged") throw UnknownDatabaseLocationException()
} }
} catch (e: FileNotFoundException) {
throw FileNotFoundDatabaseException("Unable to load the keyfile")
} catch (e: LoadDatabaseException) {
throw e
} catch (e: Exception) { } catch (e: Exception) {
throw LoadDatabaseException(e) Log.e(TAG, "Unable to merge the database")
if (e is DatabaseException)
throw e
throw DatabaseInputException(e)
} finally { } finally {
keyFileInputStream?.close()
databaseToMerge.clearAndClose() databaseToMerge.clearAndClose()
} }
} }
@Throws(LoadDatabaseException::class) @Throws(DatabaseInputException::class)
fun reloadData(contentResolver: ContentResolver, fun reloadData(
isRAMSufficient: (memoryWanted: Long) -> Boolean, contentResolver: ContentResolver,
progressTaskUpdater: ProgressTaskUpdater?) { isRAMSufficient: (memoryWanted: Long) -> Boolean,
progressTaskUpdater: ProgressTaskUpdater?
) {
// Retrieve the stream from the old database URI // Retrieve the stream from the old database URI
try { try {
@@ -791,9 +741,11 @@ class Database {
} }
DatabaseInputKDB(databaseKDB) DatabaseInputKDB(databaseKDB)
.openDatabase(databaseInputStream, progressTaskUpdater) { .openDatabase(databaseInputStream, progressTaskUpdater) {
databaseKDB.masterKey = masterKey this@Database.mDatabaseKDB?.let { thisDatabaseKDB ->
databaseKDB.copyMasterKeyFrom(thisDatabaseKDB)
}
} }
databaseKDB setDatabaseKDB(databaseKDB)
}, },
{ databaseInputStream -> { databaseInputStream ->
val databaseKDBX = DatabaseKDBX() val databaseKDBX = DatabaseKDBX()
@@ -803,26 +755,144 @@ class Database {
DatabaseInputKDBX(databaseKDBX).apply { DatabaseInputKDBX(databaseKDBX).apply {
setMethodToCheckIfRAMIsSufficient(isRAMSufficient) setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
openDatabase(databaseInputStream, progressTaskUpdater) { openDatabase(databaseInputStream, progressTaskUpdater) {
databaseKDBX.masterKey = masterKey this@Database.mDatabaseKDBX?.let { thisDatabaseKDBX ->
databaseKDBX.copyMasterKeyFrom(thisDatabaseKDBX)
}
} }
} }
databaseKDBX setDatabaseKDBX(databaseKDBX)
} }
) )
loaded = true
} else { } else {
throw IODatabaseException("Database URI is null, database cannot be reloaded") throw UnknownDatabaseLocationException()
} }
} catch (e: FileNotFoundException) {
throw FileNotFoundDatabaseException("Unable to load the keyfile")
} catch (e: LoadDatabaseException) {
throw e
} catch (e: Exception) { } catch (e: Exception) {
throw LoadDatabaseException(e) Log.e(TAG, "Unable to reload the database")
if (e is DatabaseException)
throw e
throw DatabaseInputException(e)
} finally { } finally {
dataModifiedSinceLastLoading = false dataModifiedSinceLastLoading = false
} }
} }
@Throws(Exception::class)
private fun readDatabaseStream(contentResolver: ContentResolver,
databaseUri: Uri,
openDatabaseKDB: (InputStream) -> Unit,
openDatabaseKDBX: (InputStream) -> Unit) {
try {
// Load Data, pass Uris as InputStreams
val databaseStream = UriUtil.getUriInputStream(contentResolver, databaseUri)
?: throw UnknownDatabaseLocationException()
BufferedInputStream(databaseStream).use { databaseInputStream ->
// We'll end up reading 8 bytes to identify the header. Might as well use two extra.
databaseInputStream.mark(10)
// Get the file directory to save the attachments
val sig1 = databaseInputStream.readBytes4ToUInt()
val sig2 = databaseInputStream.readBytes4ToUInt()
// Return to the start
databaseInputStream.reset()
when {
// Header of database KDB
DatabaseHeaderKDB.matchesHeader(sig1, sig2) -> openDatabaseKDB(
databaseInputStream
)
// Header of database KDBX
DatabaseHeaderKDBX.matchesHeader(sig1, sig2) -> openDatabaseKDBX(
databaseInputStream
)
// Header not recognized
else -> throw SignatureDatabaseException()
}
}
} catch (fileNotFoundException : FileNotFoundException) {
throw FileNotFoundDatabaseException()
}
}
@Throws(DatabaseOutputException::class)
fun saveData(contentResolver: ContentResolver,
cacheDir: File,
databaseCopyUri: Uri?,
mainCredential: MainCredential?,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray) {
val saveUri = databaseCopyUri ?: this.fileUri
// Build temp database file to avoid file corruption if error
val cacheFile = File(cacheDir, saveUri.hashCode().toString())
try {
if (saveUri != null) {
// Save in a temp memory to avoid exception
cacheFile.outputStream().use { outputStream ->
mDatabaseKDB?.let { databaseKDB ->
DatabaseOutputKDB(databaseKDB).apply {
writeDatabase(outputStream) {
if (mainCredential != null) {
databaseKDB.deriveMasterKey(
contentResolver,
mainCredential
)
} else {
// No master key change
}
}
}
}
?: mDatabaseKDBX?.let { databaseKDBX ->
DatabaseOutputKDBX(databaseKDBX).apply {
writeDatabase(outputStream) {
if (mainCredential != null) {
// Build new master key from MainCredential
databaseKDBX.deriveMasterKey(
contentResolver,
mainCredential,
challengeResponseRetriever
)
} else {
// Reuse composite key parts
databaseKDBX.deriveCompositeKey(
challengeResponseRetriever
)
}
}
}
}
}
// Copy from the cache to the final stream
UriUtil.getUriOutputStream(contentResolver, saveUri)?.use { outputStream ->
cacheFile.inputStream().use { inputStream ->
inputStream.readAllBytes { buffer ->
outputStream.write(buffer)
}
}
}
} else {
throw UnknownDatabaseLocationException()
}
} catch (e: Exception) {
Log.e(TAG, "Unable to save database", e)
if (e is DatabaseException)
throw e
throw DatabaseOutputException(e)
} finally {
try {
Log.d(TAG, "Delete database cache file $cacheFile")
cacheFile.delete()
} catch (e: Exception) {
Log.e(TAG, "Cache file $cacheFile cannot be deleted", e)
}
if (databaseCopyUri == null) {
this.dataModifiedSinceLastLoading = false
}
}
}
fun groupIsInRecycleBin(group: Group): Boolean { fun groupIsInRecycleBin(group: Group): Boolean {
val groupKDB = group.groupKDB val groupKDB = group.groupKDB
val groupKDBX = group.groupKDBX val groupKDBX = group.groupKDBX
@@ -845,13 +915,13 @@ class Database {
fun createVirtualGroupFromSearch(searchParameters: SearchParameters, fun createVirtualGroupFromSearch(searchParameters: SearchParameters,
fromGroup: NodeId<*>? = null, fromGroup: NodeId<*>? = null,
max: Int = Integer.MAX_VALUE): Group? { max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.createVirtualGroupWithSearchResult(this, return mSearchHelper.createVirtualGroupWithSearchResult(this,
searchParameters, fromGroup, max) searchParameters, fromGroup, max)
} }
fun createVirtualGroupFromSearchInfo(searchInfoString: String, fun createVirtualGroupFromSearchInfo(searchInfoString: String,
max: Int = Integer.MAX_VALUE): Group? { max: Int = Integer.MAX_VALUE): Group? {
return mSearchHelper?.createVirtualGroupWithSearchResult(this, return mSearchHelper.createVirtualGroupWithSearchResult(this,
SearchParameters().apply { SearchParameters().apply {
searchQuery = searchInfoString searchQuery = searchInfoString
searchInTitles = true searchInTitles = true
@@ -908,40 +978,6 @@ class Database {
dataModifiedSinceLastLoading = true dataModifiedSinceLastLoading = true
} }
@Throws(DatabaseOutputException::class)
fun saveData(databaseCopyUri: Uri?, contentResolver: ContentResolver) {
try {
val saveUri = databaseCopyUri ?: this.fileUri
if (saveUri != null) {
var outputStream: OutputStream? = null
try {
outputStream = UriUtil.getUriOutputStream(contentResolver, saveUri)
outputStream?.let { definedOutputStream ->
val databaseOutput =
mDatabaseKDB?.let { DatabaseOutputKDB(it, definedOutputStream) }
?: mDatabaseKDBX?.let {
DatabaseOutputKDBX(
it,
definedOutputStream
)
}
databaseOutput?.output()
}
} catch (e: Exception) {
throw IOException(e)
} finally {
outputStream?.close()
}
if (databaseCopyUri == null) {
this.dataModifiedSinceLastLoading = false
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to save database", e)
throw DatabaseOutputException(e)
}
}
fun clearIndexesAndBinaries(filesDirectory: File? = null) { fun clearIndexesAndBinaries(filesDirectory: File? = null) {
this.mDatabaseKDB?.clearIndexes() this.mDatabaseKDB?.clearIndexes()
this.mDatabaseKDBX?.clearIndexes() this.mDatabaseKDBX?.clearIndexes()
@@ -987,20 +1023,13 @@ class Database {
} }
fun validatePasswordEncoding(mainCredential: MainCredential): Boolean { fun validatePasswordEncoding(mainCredential: MainCredential): Boolean {
val password = mainCredential.masterPassword val password = mainCredential.password
val containsKeyFile = mainCredential.keyFileUri != null val containsKeyFile = mainCredential.keyFileUri != null
return mDatabaseKDB?.validatePasswordEncoding(password, containsKeyFile) return mDatabaseKDB?.validatePasswordEncoding(password, containsKeyFile)
?: mDatabaseKDBX?.validatePasswordEncoding(password, containsKeyFile) ?: mDatabaseKDBX?.validatePasswordEncoding(password, containsKeyFile)
?: false ?: false
} }
@Throws(IOException::class)
fun assignMasterKey(key: String?, keyInputStream: InputStream?) {
mDatabaseKDB?.retrieveMasterKey(key, keyInputStream)
mDatabaseKDBX?.retrieveMasterKey(key, keyInputStream)
mDatabaseKDBX?.keyLastChanged = DateInstant()
}
fun rootCanContainsEntry(): Boolean { fun rootCanContainsEntry(): Boolean {
return mDatabaseKDB?.rootCanContainsEntry() ?: mDatabaseKDBX?.rootCanContainsEntry() ?: false return mDatabaseKDB?.rootCanContainsEntry() ?: mDatabaseKDBX?.rootCanContainsEntry() ?: false
} }

View File

@@ -0,0 +1,276 @@
/*
* Copyright 2022 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX 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 3 of the License, or
* (at your option) any later version.
*
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*/
package com.kunzisoft.keepass.database.element
import android.content.ContentResolver
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import android.util.Base64
import android.util.Log
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.utils.UriUtil
import com.kunzisoft.keepass.utils.readEnum
import com.kunzisoft.keepass.utils.writeEnum
import org.apache.commons.codec.binary.Hex
import org.w3c.dom.Node
import java.io.ByteArrayInputStream
import java.io.IOException
import java.io.InputStream
import java.io.UnsupportedEncodingException
import java.nio.charset.Charset
import javax.xml.XMLConstants
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.ParserConfigurationException
data class MainCredential(var password: String? = null,
var keyFileUri: Uri? = null,
var hardwareKey: HardwareKey? = null): Parcelable {
constructor(parcel: Parcel) : this() {
password = parcel.readString()
keyFileUri = parcel.readParcelable(Uri::class.java.classLoader)
hardwareKey = parcel.readEnum<HardwareKey>()
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(password)
parcel.writeParcelable(keyFileUri, flags)
parcel.writeEnum(hardwareKey)
}
override fun describeContents(): Int {
return 0
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MainCredential
if (password != other.password) return false
if (keyFileUri != other.keyFileUri) return false
if (hardwareKey != other.hardwareKey) return false
return true
}
override fun hashCode(): Int {
var result = password?.hashCode() ?: 0
result = 31 * result + (keyFileUri?.hashCode() ?: 0)
result = 31 * result + (hardwareKey?.hashCode() ?: 0)
return result
}
companion object CREATOR : Parcelable.Creator<MainCredential> {
override fun createFromParcel(parcel: Parcel): MainCredential {
return MainCredential(parcel)
}
override fun newArray(size: Int): Array<MainCredential?> {
return arrayOfNulls(size)
}
private val TAG = MainCredential::class.java.simpleName
@Throws(IOException::class)
fun retrievePasswordKey(key: String,
encoding: Charset
): ByteArray {
val bKey: ByteArray = try {
key.toByteArray(encoding)
} catch (e: UnsupportedEncodingException) {
key.toByteArray()
}
return HashManager.hashSha256(bKey)
}
@Throws(IOException::class)
fun retrieveFileKey(contentResolver: ContentResolver,
keyFileUri: Uri?,
allowXML: Boolean): ByteArray {
if (keyFileUri == null)
throw IOException("Keyfile URI is null")
val keyData = getKeyFileData(contentResolver, keyFileUri)
?: throw IOException("No data retrieved")
try {
// Check XML key file
val xmlKeyByteArray = if (allowXML)
loadXmlKeyFile(ByteArrayInputStream(keyData))
else
null
if (xmlKeyByteArray != null) {
return xmlKeyByteArray
}
// Check 32 bytes key file
when (keyData.size) {
32 -> return keyData
64 -> try {
return Hex.decodeHex(String(keyData).toCharArray())
} catch (ignoredException: Exception) {
// Key is not base 64, treat it as binary data
}
}
// Hash file as binary data
return HashManager.hashSha256(keyData)
} catch (e: Exception) {
throw IOException("Unable to load the keyfile.", e)
}
}
@Throws(IOException::class)
fun retrieveHardwareKey(keyData: ByteArray): ByteArray {
return HashManager.hashSha256(keyData)
}
@Throws(Exception::class)
private fun getKeyFileData(contentResolver: ContentResolver,
keyFileUri: Uri): ByteArray? {
UriUtil.getUriInputStream(contentResolver, keyFileUri)?.use { keyFileInputStream ->
return keyFileInputStream.readBytes()
}
return null
}
private fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
try {
val documentBuilderFactory = DocumentBuilderFactory.newInstance()
// Disable certain unsecure XML-Parsing DocumentBuilderFactory features
try {
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
} catch (e : ParserConfigurationException) {
Log.w(TAG, "Unable to add FEATURE_SECURE_PROCESSING to prevent XML eXternal Entity injection (XXE)")
}
val documentBuilder = documentBuilderFactory.newDocumentBuilder()
val doc = documentBuilder.parse(keyInputStream)
var xmlKeyFileVersion = 1F
val docElement = doc.documentElement
val keyFileChildNodes = docElement.childNodes
// <KeyFile> Root node
if (docElement == null
|| !docElement.nodeName.equals(XML_NODE_ROOT_NAME, ignoreCase = true)) {
return null
}
if (keyFileChildNodes.length < 2)
return null
for (keyFileChildPosition in 0 until keyFileChildNodes.length) {
val keyFileChildNode = keyFileChildNodes.item(keyFileChildPosition)
// <Meta>
if (keyFileChildNode.nodeName.equals(XML_NODE_META_NAME, ignoreCase = true)) {
val metaChildNodes = keyFileChildNode.childNodes
for (metaChildPosition in 0 until metaChildNodes.length) {
val metaChildNode = metaChildNodes.item(metaChildPosition)
// <Version>
if (metaChildNode.nodeName.equals(XML_NODE_VERSION_NAME, ignoreCase = true)) {
val versionChildNodes = metaChildNode.childNodes
for (versionChildPosition in 0 until versionChildNodes.length) {
val versionChildNode = versionChildNodes.item(versionChildPosition)
if (versionChildNode.nodeType == Node.TEXT_NODE) {
val versionText = versionChildNode.textContent.removeSpaceChars()
try {
xmlKeyFileVersion = versionText.toFloat()
Log.i(TAG, "Reading XML KeyFile version : $xmlKeyFileVersion")
} catch (e: Exception) {
Log.e(TAG, "XML Keyfile version cannot be read : $versionText")
}
}
}
}
}
}
// <Key>
if (keyFileChildNode.nodeName.equals(XML_NODE_KEY_NAME, ignoreCase = true)) {
val keyChildNodes = keyFileChildNode.childNodes
for (keyChildPosition in 0 until keyChildNodes.length) {
val keyChildNode = keyChildNodes.item(keyChildPosition)
// <Data>
if (keyChildNode.nodeName.equals(XML_NODE_DATA_NAME, ignoreCase = true)) {
var hashString : String? = null
if (keyChildNode.hasAttributes()) {
val dataNodeAttributes = keyChildNode.attributes
hashString = dataNodeAttributes
.getNamedItem(XML_ATTRIBUTE_DATA_HASH).nodeValue
}
val dataChildNodes = keyChildNode.childNodes
for (dataChildPosition in 0 until dataChildNodes.length) {
val dataChildNode = dataChildNodes.item(dataChildPosition)
if (dataChildNode.nodeType == Node.TEXT_NODE) {
val dataString = dataChildNode.textContent.removeSpaceChars()
when (xmlKeyFileVersion) {
1F -> {
// No hash in KeyFile XML version 1
return Base64.decode(dataString,
DatabaseKDBX.BASE_64_FLAG
)
}
2F -> {
return if (hashString != null
&& checkKeyFileHash(dataString, hashString)
) {
Log.i(TAG, "Successful key file hash check.")
Hex.decodeHex(dataString.toCharArray())
} else {
Log.e(TAG, "Unable to check the hash of the key file.")
null
}
}
}
}
}
}
}
}
}
} catch (e: Exception) {
return null
}
return null
}
private fun checkKeyFileHash(data: String, hash: String): Boolean {
var success = false
try {
// hexadecimal encoding of the first 4 bytes of the SHA-256 hash of the key.
val dataDigest = HashManager.hashSha256(Hex.decodeHex(data.toCharArray()))
.copyOfRange(0, 4).toHexString()
success = dataDigest == hash
} catch (e: Exception) {
e.printStackTrace()
}
return success
}
private const val XML_NODE_ROOT_NAME = "KeyFile"
private const val XML_NODE_META_NAME = "Meta"
private const val XML_NODE_VERSION_NAME = "Version"
private const val XML_NODE_KEY_NAME = "Key"
private const val XML_NODE_DATA_NAME = "Data"
private const val XML_ATTRIBUTE_DATA_HASH = "Hash"
}
}

View File

@@ -19,11 +19,13 @@
package com.kunzisoft.keepass.database.element.database package com.kunzisoft.keepass.database.element.database
import android.content.ContentResolver
import com.kunzisoft.encrypt.HashManager import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.encrypt.aes.AESTransformer import com.kunzisoft.encrypt.aes.AESTransformer
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.entry.EntryKDB import com.kunzisoft.keepass.database.element.entry.EntryKDB
import com.kunzisoft.keepass.database.element.group.GroupKDB import com.kunzisoft.keepass.database.element.group.GroupKDB
@@ -31,8 +33,10 @@ import com.kunzisoft.keepass.database.element.icon.IconImageStandard
import com.kunzisoft.keepass.database.element.node.NodeIdInt import com.kunzisoft.keepass.database.element.node.NodeIdInt
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.NodeVersioned import com.kunzisoft.keepass.database.element.node.NodeVersioned
import com.kunzisoft.keepass.database.exception.EmptyKeyDatabaseException
import com.kunzisoft.keepass.database.exception.HardwareKeyDatabaseException
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.nio.charset.Charset
import java.util.* import java.util.*
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() { class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
@@ -56,8 +60,8 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
KdfFactory.aesKdf KdfFactory.aesKdf
) )
override val passwordEncoding: String override val passwordEncoding: Charset
get() = "ISO-8859-1" get() = Charsets.ISO_8859_1
override var numberKeyEncryptionRounds = 300L override var numberKeyEncryptionRounds = 300L
@@ -116,20 +120,6 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
return newId return newId
} }
@Throws(IOException::class)
override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray {
return if (key != null && keyInputStream != null) {
getCompositeKey(key, keyInputStream)
} else if (key != null) { // key.length() >= 0
getPasswordKey(key)
} else if (keyInputStream != null) { // key == null
getFileKey(keyInputStream)
} else {
throw IllegalArgumentException("Key cannot be empty.")
}
}
@Throws(IOException::class) @Throws(IOException::class)
fun makeFinalKey(masterSeed: ByteArray, transformSeed: ByteArray, numRounds: Long) { fun makeFinalKey(masterSeed: ByteArray, transformSeed: ByteArray, numRounds: Long) {
// Encrypt the master key a few times to make brute-force key-search harder // Encrypt the master key a few times to make brute-force key-search harder
@@ -138,6 +128,41 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
finalKey = HashManager.hashSha256(masterSeed, transformedKey) finalKey = HashManager.hashSha256(masterSeed, transformedKey)
} }
fun deriveMasterKey(
contentResolver: ContentResolver,
mainCredential: MainCredential
) {
// Exception when no password
if (mainCredential.hardwareKey != null)
throw HardwareKeyDatabaseException()
if (mainCredential.password == null && mainCredential.keyFileUri == null)
throw EmptyKeyDatabaseException()
// Retrieve plain data
val password = mainCredential.password
val keyFileUri = mainCredential.keyFileUri
val passwordBytes = if (password != null) MainCredential.retrievePasswordKey(
password,
passwordEncoding
) else null
val keyFileBytes = if (keyFileUri != null) MainCredential.retrieveFileKey(
contentResolver,
keyFileUri,
false
) else null
// Build master key
if (passwordBytes != null
&& keyFileBytes != null) {
this.masterKey = HashManager.hashSha256(
passwordBytes,
keyFileBytes
)
} else {
this.masterKey = passwordBytes ?: keyFileBytes ?: byteArrayOf(0)
}
}
override fun createGroup(): GroupKDB { override fun createGroup(): GroupKDB {
return GroupKDB() return GroupKDB()
} }

View File

@@ -19,6 +19,7 @@
*/ */
package com.kunzisoft.keepass.database.element.database package com.kunzisoft.keepass.database.element.database
import android.content.ContentResolver
import android.content.res.Resources import android.content.res.Resources
import android.util.Base64 import android.util.Base64
import android.util.Log import android.util.Log
@@ -31,10 +32,7 @@ import com.kunzisoft.keepass.database.crypto.kdf.AesKdf
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory import com.kunzisoft.keepass.database.crypto.kdf.KdfFactory
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
import com.kunzisoft.keepass.database.element.CustomData import com.kunzisoft.keepass.database.element.*
import com.kunzisoft.keepass.database.element.DateInstant
import com.kunzisoft.keepass.database.element.DeletedObject
import com.kunzisoft.keepass.database.element.Tags
import com.kunzisoft.keepass.database.element.binary.BinaryData import com.kunzisoft.keepass.database.element.binary.BinaryData
import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE import com.kunzisoft.keepass.database.element.database.DatabaseKDB.Companion.BACKUP_FOLDER_TITLE
import com.kunzisoft.keepass.database.element.entry.EntryKDBX import com.kunzisoft.keepass.database.element.entry.EntryKDBX
@@ -49,30 +47,27 @@ import com.kunzisoft.keepass.database.element.node.NodeVersioned
import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig import com.kunzisoft.keepass.database.element.security.MemoryProtectionConfig
import com.kunzisoft.keepass.database.element.template.Template import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.database.element.template.TemplateEngineCompatible import com.kunzisoft.keepass.database.element.template.TemplateEngineCompatible
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_31 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_31
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_40
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41 import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.utils.UnsignedInt import com.kunzisoft.keepass.utils.UnsignedInt
import com.kunzisoft.keepass.utils.longTo8Bytes import com.kunzisoft.keepass.utils.longTo8Bytes
import org.apache.commons.codec.binary.Hex
import org.w3c.dom.Node
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.nio.charset.Charset
import java.security.MessageDigest import java.security.MessageDigest
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import java.util.* import java.util.*
import javax.crypto.Mac import javax.crypto.Mac
import javax.xml.XMLConstants
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.parsers.ParserConfigurationException
import kotlin.collections.HashSet
import kotlin.math.min import kotlin.math.min
class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> { class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
// To resave the database with same credential when already loaded
private var mCompositeKey = CompositeKey()
var hmacKey: ByteArray? = null var hmacKey: ByteArray? = null
private set private set
@@ -233,6 +228,79 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
} }
} }
fun deriveMasterKey(
contentResolver: ContentResolver,
mainCredential: MainCredential,
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray
) {
// Retrieve each plain credential
val password = mainCredential.password
val keyFileUri = mainCredential.keyFileUri
val hardwareKey = mainCredential.hardwareKey
val passwordBytes = if (password != null) MainCredential.retrievePasswordKey(
password,
passwordEncoding
) else null
val keyFileBytes = if (keyFileUri != null) MainCredential.retrieveFileKey(
contentResolver,
keyFileUri,
true
) else null
val hardwareKeyBytes = if (hardwareKey != null) MainCredential.retrieveHardwareKey(
challengeResponseRetriever.invoke(hardwareKey, transformSeed)
) else null
// Save to rebuild master password with new seed later
mCompositeKey = CompositeKey(passwordBytes, keyFileBytes, hardwareKey)
// Build the master key
this.masterKey = composedKeyToMasterKey(
passwordBytes,
keyFileBytes,
hardwareKeyBytes
)
}
@Throws(DatabaseOutputException::class)
fun deriveCompositeKey(
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray
) {
val passwordBytes = mCompositeKey.passwordData
val keyFileBytes = mCompositeKey.keyFileData
val hardwareKey = mCompositeKey.hardwareKey
if (hardwareKey == null) {
// If no hardware key, simply rebuild from composed keys
this.masterKey = composedKeyToMasterKey(
passwordBytes,
keyFileBytes
)
} else {
val hardwareKeyBytes = MainCredential.retrieveHardwareKey(
challengeResponseRetriever.invoke(hardwareKey, transformSeed)
)
this.masterKey = composedKeyToMasterKey(
passwordBytes,
keyFileBytes,
hardwareKeyBytes
)
}
}
private fun composedKeyToMasterKey(passwordData: ByteArray?,
keyFileData: ByteArray?,
hardwareKeyData: ByteArray? = null): ByteArray {
return HashManager.hashSha256(
passwordData,
keyFileData,
hardwareKeyData
)
}
fun copyMasterKeyFrom(databaseVersioned: DatabaseKDBX) {
super.copyMasterKeyFrom(databaseVersioned)
this.mCompositeKey = databaseVersioned.mCompositeKey
}
fun getMinKdbxVersion(): UnsignedInt { fun getMinKdbxVersion(): UnsignedInt {
val entryHandler = EntryOperationHandler() val entryHandler = EntryOperationHandler()
val groupHandler = GroupOperationHandler() val groupHandler = GroupOperationHandler()
@@ -364,8 +432,8 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
kdfEngine.setParallelism(kdfParameters!!, parallelism) kdfEngine.setParallelism(kdfParameters!!, parallelism)
} }
override val passwordEncoding: String override val passwordEncoding: Charset
get() = "UTF-8" get() = Charsets.UTF_8
private fun getGroupByUUID(groupUUID: UUID): GroupKDBX? { private fun getGroupByUUID(groupUUID: UUID): GroupKDBX? {
if (groupUUID == UUID_ZERO) if (groupUUID == UUID_ZERO)
@@ -528,22 +596,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return mFieldReferenceEngine.compile(textReference, recursionLevel) return mFieldReferenceEngine.compile(textReference, recursionLevel)
} }
@Throws(IOException::class)
public override fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray {
var masterKey = byteArrayOf()
if (key != null && keyInputStream != null) {
return getCompositeKey(key, keyInputStream)
} else if (key != null) { // key.length() >= 0
masterKey = getPasswordKey(key)
} else if (keyInputStream != null) { // key == null
masterKey = getFileKey(keyInputStream)
}
return HashManager.hashSha256(masterKey)
}
@Throws(IOException::class) @Throws(IOException::class)
fun makeFinalKey(masterSeed: ByteArray) { fun makeFinalKey(masterSeed: ByteArray) {
@@ -615,115 +667,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
return ret return ret
} }
override fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
try {
val documentBuilderFactory = DocumentBuilderFactory.newInstance()
// Disable certain unsecure XML-Parsing DocumentBuilderFactory features
try {
documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
} catch (e : ParserConfigurationException) {
Log.w(TAG, "Unable to add FEATURE_SECURE_PROCESSING to prevent XML eXternal Entity injection (XXE)")
}
val documentBuilder = documentBuilderFactory.newDocumentBuilder()
val doc = documentBuilder.parse(keyInputStream)
var xmlKeyFileVersion = 1F
val docElement = doc.documentElement
val keyFileChildNodes = docElement.childNodes
// <KeyFile> Root node
if (docElement == null
|| !docElement.nodeName.equals(XML_NODE_ROOT_NAME, ignoreCase = true)) {
return null
}
if (keyFileChildNodes.length < 2)
return null
for (keyFileChildPosition in 0 until keyFileChildNodes.length) {
val keyFileChildNode = keyFileChildNodes.item(keyFileChildPosition)
// <Meta>
if (keyFileChildNode.nodeName.equals(XML_NODE_META_NAME, ignoreCase = true)) {
val metaChildNodes = keyFileChildNode.childNodes
for (metaChildPosition in 0 until metaChildNodes.length) {
val metaChildNode = metaChildNodes.item(metaChildPosition)
// <Version>
if (metaChildNode.nodeName.equals(XML_NODE_VERSION_NAME, ignoreCase = true)) {
val versionChildNodes = metaChildNode.childNodes
for (versionChildPosition in 0 until versionChildNodes.length) {
val versionChildNode = versionChildNodes.item(versionChildPosition)
if (versionChildNode.nodeType == Node.TEXT_NODE) {
val versionText = versionChildNode.textContent.removeSpaceChars()
try {
xmlKeyFileVersion = versionText.toFloat()
Log.i(TAG, "Reading XML KeyFile version : $xmlKeyFileVersion")
} catch (e: Exception) {
Log.e(TAG, "XML Keyfile version cannot be read : $versionText")
}
}
}
}
}
}
// <Key>
if (keyFileChildNode.nodeName.equals(XML_NODE_KEY_NAME, ignoreCase = true)) {
val keyChildNodes = keyFileChildNode.childNodes
for (keyChildPosition in 0 until keyChildNodes.length) {
val keyChildNode = keyChildNodes.item(keyChildPosition)
// <Data>
if (keyChildNode.nodeName.equals(XML_NODE_DATA_NAME, ignoreCase = true)) {
var hashString : String? = null
if (keyChildNode.hasAttributes()) {
val dataNodeAttributes = keyChildNode.attributes
hashString = dataNodeAttributes
.getNamedItem(XML_ATTRIBUTE_DATA_HASH).nodeValue
}
val dataChildNodes = keyChildNode.childNodes
for (dataChildPosition in 0 until dataChildNodes.length) {
val dataChildNode = dataChildNodes.item(dataChildPosition)
if (dataChildNode.nodeType == Node.TEXT_NODE) {
val dataString = dataChildNode.textContent.removeSpaceChars()
when (xmlKeyFileVersion) {
1F -> {
// No hash in KeyFile XML version 1
return Base64.decode(dataString, BASE_64_FLAG)
}
2F -> {
return if (hashString != null
&& checkKeyFileHash(dataString, hashString)) {
Log.i(TAG, "Successful key file hash check.")
Hex.decodeHex(dataString.toCharArray())
} else {
Log.e(TAG, "Unable to check the hash of the key file.")
null
}
}
}
}
}
}
}
}
}
} catch (e: Exception) {
return null
}
return null
}
private fun checkKeyFileHash(data: String, hash: String): Boolean {
var success = false
try {
// hexadecimal encoding of the first 4 bytes of the SHA-256 hash of the key.
val dataDigest = HashManager.hashSha256(Hex.decodeHex(data.toCharArray()))
.copyOfRange(0, 4).toHexString()
success = dataDigest == hash
} catch (e: Exception) {
e.printStackTrace()
}
return success
}
override fun newGroupId(): NodeIdUUID { override fun newGroupId(): NodeIdUUID {
var newId: NodeIdUUID var newId: NodeIdUUID
do { do {
@@ -928,13 +871,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
private const val DEFAULT_HISTORY_MAX_ITEMS = 10 // -1 unlimited private const val DEFAULT_HISTORY_MAX_ITEMS = 10 // -1 unlimited
private const val DEFAULT_HISTORY_MAX_SIZE = (6 * 1024 * 1024).toLong() // -1 unlimited private const val DEFAULT_HISTORY_MAX_SIZE = (6 * 1024 * 1024).toLong() // -1 unlimited
private const val XML_NODE_ROOT_NAME = "KeyFile"
private const val XML_NODE_META_NAME = "Meta"
private const val XML_NODE_VERSION_NAME = "Version"
private const val XML_NODE_KEY_NAME = "Key"
private const val XML_NODE_DATA_NAME = "Data"
private const val XML_ATTRIBUTE_DATA_HASH = "Hash"
const val BASE_64_FLAG = Base64.NO_WRAP const val BASE_64_FLAG = Base64.NO_WRAP
} }
} }

View File

@@ -20,7 +20,6 @@
package com.kunzisoft.keepass.database.element.database package com.kunzisoft.keepass.database.element.database
import android.util.Log import android.util.Log
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
import com.kunzisoft.keepass.database.element.binary.AttachmentPool import com.kunzisoft.keepass.database.element.binary.AttachmentPool
@@ -32,11 +31,9 @@ import com.kunzisoft.keepass.database.element.icon.IconsManager
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
import org.apache.commons.codec.binary.Hex
import java.io.ByteArrayInputStream
import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.UnsupportedEncodingException import java.io.UnsupportedEncodingException
import java.nio.charset.Charset
import java.util.* import java.util.*
abstract class DatabaseVersioned< abstract class DatabaseVersioned<
@@ -46,7 +43,6 @@ abstract class DatabaseVersioned<
Entry : EntryVersioned<GroupId, EntryId, Group, Entry> Entry : EntryVersioned<GroupId, EntryId, Group, Entry>
> { > {
// Algorithm used to encrypt the database // Algorithm used to encrypt the database
abstract var encryptionAlgorithm: EncryptionAlgorithm abstract var encryptionAlgorithm: EncryptionAlgorithm
abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm> abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
@@ -55,11 +51,12 @@ abstract class DatabaseVersioned<
abstract val kdfAvailableList: List<KdfEngine> abstract val kdfAvailableList: List<KdfEngine>
abstract var numberKeyEncryptionRounds: Long abstract var numberKeyEncryptionRounds: Long
protected abstract val passwordEncoding: String abstract val passwordEncoding: Charset
var masterKey = ByteArray(32) var masterKey = ByteArray(32)
var finalKey: ByteArray? = null var finalKey: ByteArray? = null
protected set protected set
var transformSeed: ByteArray? = null
abstract val version: String abstract val version: String
abstract val defaultFileExtension: String abstract val defaultFileExtension: String
@@ -91,58 +88,6 @@ abstract class DatabaseVersioned<
return getGroupIndexes().filter { it != rootGroup } return getGroupIndexes().filter { it != rootGroup }
} }
@Throws(IOException::class)
protected abstract fun getMasterKey(key: String?, keyInputStream: InputStream?): ByteArray
@Throws(IOException::class)
fun retrieveMasterKey(key: String?, keyfileInputStream: InputStream?) {
masterKey = getMasterKey(key, keyfileInputStream)
}
@Throws(IOException::class)
protected fun getCompositeKey(key: String, keyfileInputStream: InputStream): ByteArray {
val fileKey = getFileKey(keyfileInputStream)
val passwordKey = getPasswordKey(key)
return HashManager.hashSha256(passwordKey, fileKey)
}
@Throws(IOException::class)
protected fun getPasswordKey(key: String): ByteArray {
val bKey: ByteArray = try {
key.toByteArray(charset(passwordEncoding))
} catch (e: UnsupportedEncodingException) {
key.toByteArray()
}
return HashManager.hashSha256(bKey)
}
@Throws(IOException::class)
protected fun getFileKey(keyInputStream: InputStream): ByteArray {
try {
val keyData = keyInputStream.readBytes()
// Check XML key file
val xmlKeyByteArray = loadXmlKeyFile(ByteArrayInputStream(keyData))
if (xmlKeyByteArray != null) {
return xmlKeyByteArray
}
// Check 32 bytes key file
when (keyData.size) {
32 -> return keyData
64 -> try {
return Hex.decodeHex(String(keyData).toCharArray())
} catch (ignoredException: Exception) {
// Key is not base 64, treat it as binary data
}
}
// Hash file as binary data
return HashManager.hashSha256(keyData)
} catch (outOfMemoryError: OutOfMemoryError) {
throw IOException("Keyfile data is too large", outOfMemoryError)
}
}
protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? { protected open fun loadXmlKeyFile(keyInputStream: InputStream): ByteArray? {
return null return null
} }
@@ -158,20 +103,25 @@ abstract class DatabaseVersioned<
val bKey: ByteArray val bKey: ByteArray
try { try {
bKey = password.toByteArray(charset(encoding)) bKey = password.toByteArray(encoding)
} catch (e: UnsupportedEncodingException) { } catch (e: UnsupportedEncodingException) {
return false return false
} }
val reEncoded: String val reEncoded: String
try { try {
reEncoded = String(bKey, charset(encoding)) reEncoded = String(bKey, encoding)
} catch (e: UnsupportedEncodingException) { } catch (e: UnsupportedEncodingException) {
return false return false
} }
return password == reEncoded return password == reEncoded
} }
fun copyMasterKeyFrom(databaseVersioned: DatabaseVersioned<GroupId, EntryId, Group, Entry>) {
this.masterKey = databaseVersioned.masterKey
this.transformSeed = databaseVersioned.transformSeed
}
/* /*
* ------------------------------------- * -------------------------------------
* Node Creation * Node Creation

View File

@@ -24,128 +24,172 @@ import androidx.annotation.StringRes
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import java.io.PrintStream
import java.io.PrintWriter
abstract class DatabaseException : Exception { abstract class DatabaseException : Exception {
var innerMessage: String? = null
abstract var errorId: Int abstract var errorId: Int
var parameters: (Array<out String>)? = null var parameters: (Array<out String>)? = null
var mThrowable: Throwable? = null
constructor() : super() constructor() : super()
constructor(message: String) : super(message) constructor(message: String) : super(message)
constructor(message: String, throwable: Throwable) : super(message, throwable) constructor(message: String, throwable: Throwable) {
constructor(throwable: Throwable) : super(throwable) mThrowable = throwable
innerMessage = StringBuilder().apply {
append(message)
if (throwable.localizedMessage != null) {
append(" ")
append(throwable.localizedMessage)
}
}.toString()
}
constructor(throwable: Throwable) {
mThrowable = throwable
innerMessage = throwable.localizedMessage
}
fun getLocalizedMessage(resources: Resources): String { fun getLocalizedMessage(resources: Resources): String {
parameters?.let { val throwable = mThrowable
return resources.getString(errorId, *it) if (throwable is DatabaseException)
} ?: return resources.getString(errorId) errorId = throwable.errorId
val localMessage = parameters?.let {
resources.getString(errorId, *it)
} ?: resources.getString(errorId)
return StringBuilder().apply {
append(localMessage)
if (innerMessage != null) {
append(" ")
append(innerMessage)
}
}.toString()
}
override fun printStackTrace() {
mThrowable?.printStackTrace()
super.printStackTrace()
}
override fun printStackTrace(s: PrintStream) {
mThrowable?.printStackTrace(s)
super.printStackTrace(s)
}
override fun printStackTrace(s: PrintWriter) {
mThrowable?.printStackTrace(s)
super.printStackTrace(s)
} }
} }
open class LoadDatabaseException : DatabaseException { class FileNotFoundDatabaseException : DatabaseInputException() {
@StringRes
override var errorId: Int = R.string.error_load_database
constructor() : super()
constructor(string: String) : super(string)
constructor(throwable: Throwable) : super(throwable)
}
class FileNotFoundDatabaseException : LoadDatabaseException {
@StringRes @StringRes
override var errorId: Int = R.string.file_not_found_content override var errorId: Int = R.string.file_not_found_content
constructor() : super()
constructor(string: String) : super(string)
constructor(exception: Throwable) : super(exception)
} }
class InvalidAlgorithmDatabaseException : LoadDatabaseException { class CorruptedDatabaseException : DatabaseInputException() {
@StringRes
override var errorId: Int = R.string.corrupted_file
}
class InvalidAlgorithmDatabaseException : DatabaseInputException {
@StringRes @StringRes
override var errorId: Int = R.string.invalid_algorithm override var errorId: Int = R.string.invalid_algorithm
constructor() : super() constructor() : super()
constructor(exception: Throwable) : super(exception) constructor(exception: Throwable) : super(exception)
} }
class DuplicateUuidDatabaseException: LoadDatabaseException { class UnknownDatabaseLocationException : DatabaseException() {
@StringRes @StringRes
override var errorId: Int = R.string.invalid_db_same_uuid override var errorId: Int = R.string.error_location_unknown
constructor(type: Type, uuid: NodeId<*>) : super() {
parameters = arrayOf(type.name, uuid.toString())
}
constructor(exception: Throwable) : super(exception)
} }
class IODatabaseException : LoadDatabaseException { class HardwareKeyDatabaseException : DatabaseException() {
@StringRes
override var errorId: Int = R.string.error_hardware_key_unsupported
}
class EmptyKeyDatabaseException : DatabaseException() {
@StringRes
override var errorId: Int = R.string.error_empty_key
}
class SignatureDatabaseException : DatabaseInputException() {
@StringRes
override var errorId: Int = R.string.invalid_db_sig
}
class VersionDatabaseException : DatabaseInputException() {
@StringRes
override var errorId: Int = R.string.unsupported_db_version
}
class InvalidCredentialsDatabaseException : DatabaseInputException {
@StringRes
override var errorId: Int = R.string.invalid_credentials
constructor() : super()
constructor(string: String) : super(string)
}
class KDFMemoryDatabaseException(exception: Throwable) : DatabaseInputException(exception) {
@StringRes
override var errorId: Int = R.string.error_load_database_KDF_memory
}
class NoMemoryDatabaseException(exception: Throwable) : DatabaseInputException(exception) {
@StringRes
override var errorId: Int = R.string.error_out_of_memory
}
class DuplicateUuidDatabaseException(type: Type, uuid: NodeId<*>) : DatabaseInputException() {
@StringRes
override var errorId: Int = R.string.invalid_db_same_uuid
init {
parameters = arrayOf(type.name, uuid.toString())
}
}
class XMLMalformedDatabaseException : DatabaseInputException {
@StringRes
override var errorId: Int = R.string.error_XML_malformed
constructor() : super()
constructor(string: String) : super(string)
}
class MergeDatabaseKDBException : DatabaseInputException() {
@StringRes
override var errorId: Int = R.string.error_unable_merge_database_kdb
}
class MoveEntryDatabaseException : DatabaseException() {
@StringRes
override var errorId: Int = R.string.error_move_entry_here
}
class MoveGroupDatabaseException : DatabaseException() {
@StringRes
override var errorId: Int = R.string.error_move_group_here
}
class CopyEntryDatabaseException : DatabaseException() {
@StringRes
override var errorId: Int = R.string.error_copy_entry_here
}
class CopyGroupDatabaseException : DatabaseException() {
@StringRes
override var errorId: Int = R.string.error_copy_group_here
}
open class DatabaseInputException : DatabaseException {
@StringRes @StringRes
override var errorId: Int = R.string.error_load_database override var errorId: Int = R.string.error_load_database
constructor() : super() constructor() : super()
constructor(string: String) : super(string) constructor(string: String) : super(string)
constructor(exception: Throwable) : super(exception) constructor(throwable: Throwable) : super(throwable)
} }
class KDFMemoryDatabaseException : LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_load_database_KDF_memory
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class SignatureDatabaseException : LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.invalid_db_sig
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class VersionDatabaseException : LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.unsupported_db_version
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class InvalidCredentialsDatabaseException : LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.invalid_credentials
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class NoMemoryDatabaseException: LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_out_of_memory
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class MoveEntryDatabaseException: LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_move_entry_here
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class MoveGroupDatabaseException: LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_move_group_here
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class CopyEntryDatabaseException: LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_copy_entry_here
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
class CopyGroupDatabaseException: LoadDatabaseException {
@StringRes
override var errorId: Int = R.string.error_copy_group_here
constructor() : super()
constructor(exception: Throwable) : super(exception)
}
// TODO Output Exception
open class DatabaseOutputException : DatabaseException { open class DatabaseOutputException : DatabaseException {
@StringRes @StringRes
override var errorId: Int = R.string.error_save_database override var errorId: Int = R.string.error_save_database

View File

@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.database.file.input
import android.util.Log import android.util.Log
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.database.DatabaseVersioned import com.kunzisoft.keepass.database.element.database.DatabaseVersioned
import com.kunzisoft.keepass.database.exception.LoadDatabaseException import com.kunzisoft.keepass.database.exception.DatabaseInputException
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
import java.io.InputStream import java.io.InputStream
@@ -33,15 +33,9 @@ abstract class DatabaseInput<D : DatabaseVersioned<*, *, *, *>> (protected var m
/** /**
* Load a versioned database file, return contents in a new DatabaseVersioned. * Load a versioned database file, return contents in a new DatabaseVersioned.
*
* @param databaseInputStream Existing file to load.
* @param password Pass phrase for infile.
* @return new DatabaseVersioned container.
*
* @throws LoadDatabaseException on database error (contains IO exceptions)
*/ */
@Throws(LoadDatabaseException::class) @Throws(DatabaseInputException::class)
abstract fun openDatabase(databaseInputStream: InputStream, abstract fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?, progressTaskUpdater: ProgressTaskUpdater?,
assignMasterKey: (() -> Unit)): D assignMasterKey: (() -> Unit)): D

View File

@@ -47,7 +47,7 @@ import javax.crypto.CipherInputStream
class DatabaseInputKDB(database: DatabaseKDB) class DatabaseInputKDB(database: DatabaseKDB)
: DatabaseInput<DatabaseKDB>(database) { : DatabaseInput<DatabaseKDB>(database) {
@Throws(LoadDatabaseException::class) @Throws(DatabaseInputException::class)
override fun openDatabase(databaseInputStream: InputStream, override fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?, progressTaskUpdater: ProgressTaskUpdater?,
assignMasterKey: (() -> Unit)): DatabaseKDB { assignMasterKey: (() -> Unit)): DatabaseKDB {
@@ -76,6 +76,7 @@ class DatabaseInputKDB(database: DatabaseKDB)
throw VersionDatabaseException() throw VersionDatabaseException()
} }
mDatabase.transformSeed = header.transformSeed
assignMasterKey.invoke() assignMasterKey.invoke()
// Select algorithm // Select algorithm
@@ -310,18 +311,11 @@ class DatabaseInputKDB(database: DatabaseKDB)
stopContentTimer() stopContentTimer()
} catch (e: LoadDatabaseException) { } catch (e: Error) {
mDatabase.clearAll() mDatabase.clearAll()
throw e if (e is OutOfMemoryError)
} catch (e: IOException) { throw NoMemoryDatabaseException(e)
mDatabase.clearAll() throw DatabaseInputException(e)
throw IODatabaseException(e)
} catch (e: OutOfMemoryError) {
mDatabase.clearAll()
throw NoMemoryDatabaseException(e)
} catch (e: Exception) {
mDatabase.clearAll()
throw LoadDatabaseException(e)
} }
return mDatabase return mDatabase

View File

@@ -99,7 +99,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
this.isRAMSufficient = method this.isRAMSufficient = method
} }
@Throws(LoadDatabaseException::class) @Throws(DatabaseInputException::class)
override fun openDatabase(databaseInputStream: InputStream, override fun openDatabase(databaseInputStream: InputStream,
progressTaskUpdater: ProgressTaskUpdater?, progressTaskUpdater: ProgressTaskUpdater?,
assignMasterKey: (() -> Unit)): DatabaseKDBX { assignMasterKey: (() -> Unit)): DatabaseKDBX {
@@ -114,6 +114,8 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
hashOfHeader = headerAndHash.hash hashOfHeader = headerAndHash.hash
val pbHeader = headerAndHash.header val pbHeader = headerAndHash.header
val transformSeed = header.transformSeed
mDatabase.transformSeed = transformSeed
assignMasterKey.invoke() assignMasterKey.invoke()
mDatabase.makeFinalKey(header.masterSeed) mDatabase.makeFinalKey(header.masterSeed)
@@ -155,7 +157,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
throw InvalidCredentialsDatabaseException() throw InvalidCredentialsDatabaseException()
} }
val hmacKey = mDatabase.hmacKey ?: throw LoadDatabaseException() val hmacKey = mDatabase.hmacKey ?: throw DatabaseInputException()
val blockKey = HmacBlock.getHmacKey64(hmacKey, UnsignedLong.MAX_BYTES) val blockKey = HmacBlock.getHmacKey64(hmacKey, UnsignedLong.MAX_BYTES)
val hmac: Mac = HmacBlock.getHmacSha256(blockKey) val hmac: Mac = HmacBlock.getHmacSha256(blockKey)
@@ -187,7 +189,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
try { try {
randomStream = CrsAlgorithm.getCipher(header.innerRandomStream, header.innerRandomStreamKey) randomStream = CrsAlgorithm.getCipher(header.innerRandomStream, header.innerRandomStreamKey)
} catch (e: Exception) { } catch (e: Exception) {
throw LoadDatabaseException(e) throw DatabaseInputException(e)
} }
val xmlPullParserFactory = XmlPullParserFactory.newInstance().apply { val xmlPullParserFactory = XmlPullParserFactory.newInstance().apply {
@@ -200,19 +202,12 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
stopContentTimer() stopContentTimer()
} catch (e: LoadDatabaseException) { } catch (e: Error) {
throw e if (e is OutOfMemoryError)
} catch (e: XmlPullParserException) { throw NoMemoryDatabaseException(e)
throw IODatabaseException(e)
} catch (e: IOException) {
if (e.message?.contains("Hash failed with code") == true) if (e.message?.contains("Hash failed with code") == true)
throw KDFMemoryDatabaseException(e) throw KDFMemoryDatabaseException(e)
else throw DatabaseInputException(e)
throw IODatabaseException(e)
} catch (e: OutOfMemoryError) {
throw NoMemoryDatabaseException(e)
} catch (e: Exception) {
throw LoadDatabaseException(e)
} }
return mDatabase return mDatabase
@@ -227,7 +222,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
val fieldId = dataInputStream.read().toByte() val fieldId = dataInputStream.read().toByte()
val size = dataInputStream.readBytes4ToUInt().toKotlinInt() val size = dataInputStream.readBytes4ToUInt().toKotlinInt()
if (size < 0) throw IOException("Corrupted file") if (size < 0) throw CorruptedDatabaseException()
var data = ByteArray(0) var data = ByteArray(0)
try { try {
@@ -238,7 +233,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
} }
} catch (e: Exception) { } catch (e: Exception) {
// OOM only if corrupted file // OOM only if corrupted file
throw IOException("Corrupted file") throw CorruptedDatabaseException()
} }
readStream = true readStream = true
@@ -297,7 +292,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
Binaries Binaries
} }
@Throws(XmlPullParserException::class, IOException::class, LoadDatabaseException::class) @Throws(XmlPullParserException::class, IOException::class, DatabaseInputException::class)
private fun readDocumentStreamed(xpp: XmlPullParser) { private fun readDocumentStreamed(xpp: XmlPullParser) {
ctxGroups.clear() ctxGroups.clear()
@@ -324,11 +319,11 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
} }
// Error checks // Error checks
if (ctx != KdbContext.Null) throw IOException("Malformed") if (ctx != KdbContext.Null) throw XMLMalformedDatabaseException()
if (ctxGroups.size != 0) throw IOException("Malformed") if (ctxGroups.size != 0) throw XMLMalformedDatabaseException()
} }
@Throws(XmlPullParserException::class, IOException::class, LoadDatabaseException::class) @Throws(XmlPullParserException::class, IOException::class, DatabaseInputException::class)
private fun readXmlElement(ctx: KdbContext, xpp: XmlPullParser): KdbContext { private fun readXmlElement(ctx: KdbContext, xpp: XmlPullParser): KdbContext {
val name = xpp.name val name = xpp.name
when (ctx) { when (ctx) {
@@ -352,7 +347,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
if (encodedHash.isNotEmpty() && hashOfHeader != null) { if (encodedHash.isNotEmpty() && hashOfHeader != null) {
val hash = Base64.decode(encodedHash, BASE_64_FLAG) val hash = Base64.decode(encodedHash, BASE_64_FLAG)
if (!Arrays.equals(hash, hashOfHeader)) { if (!Arrays.equals(hash, hashOfHeader)) {
throw LoadDatabaseException() throw DatabaseInputException()
} }
} }
} else if (name.equals(DatabaseKDBXXML.ElemSettingsChanged, ignoreCase = true)) { } else if (name.equals(DatabaseKDBXXML.ElemSettingsChanged, ignoreCase = true)) {
@@ -824,7 +819,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
if (ctx != null) { if (ctx != null) {
contextName = ctx.name contextName = ctx.name
} }
throw RuntimeException("Invalid end element: Context " + contextName + "End element: " + name) throw XMLMalformedDatabaseException("Invalid end element: Context " + contextName + "End element: " + name)
} }
} }

View File

@@ -26,7 +26,7 @@ import java.io.OutputStream
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import java.security.SecureRandom import java.security.SecureRandom
abstract class DatabaseOutput<Header : DatabaseHeader> protected constructor(protected var mOutputStream: OutputStream) { abstract class DatabaseOutput<Header : DatabaseHeader> {
@Throws(DatabaseOutputException::class) @Throws(DatabaseOutputException::class)
protected open fun setIVs(header: Header): SecureRandom { protected open fun setIVs(header: Header): SecureRandom {
@@ -44,9 +44,7 @@ abstract class DatabaseOutput<Header : DatabaseHeader> protected constructor(pro
} }
@Throws(DatabaseOutputException::class) @Throws(DatabaseOutputException::class)
abstract fun output() abstract fun writeDatabase(outputStream: OutputStream,
assignMasterKey: () -> Unit)
@Throws(DatabaseOutputException::class)
abstract fun outputHeader(outputStream: OutputStream): Header
} }

View File

@@ -39,9 +39,8 @@ import java.security.*
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.CipherOutputStream import javax.crypto.CipherOutputStream
class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB, class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB)
outputStream: OutputStream) : DatabaseOutput<DatabaseHeaderKDB>() {
: DatabaseOutput<DatabaseHeaderKDB>(outputStream) {
private var headerHashBlock: ByteArray? = null private var headerHashBlock: ByteArray? = null
@@ -60,15 +59,15 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
} }
@Throws(DatabaseOutputException::class) @Throws(DatabaseOutputException::class)
override fun output() { override fun writeDatabase(outputStream: OutputStream,
assignMasterKey: () -> Unit) {
// Before we output the header, we should sort our list of groups // Before we output the header, we should sort our list of groups
// and remove any orphaned nodes that are no longer part of the tree hierarchy // and remove any orphaned nodes that are no longer part of the tree hierarchy
// also remove the virtual root not present in kdb // also remove the virtual root not present in kdb
val rootGroup = mDatabaseKDB.rootGroup val rootGroup = mDatabaseKDB.rootGroup
sortNodesForOutput() sortNodesForOutput()
val header = outputHeader(mOutputStream) val header = outputHeader(outputStream, assignMasterKey)
val finalKey = getFinalKey(header) val finalKey = getFinalKey(header)
val cipher: Cipher = try { val cipher: Cipher = try {
@@ -81,7 +80,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
} }
try { try {
val cos = CipherOutputStream(mOutputStream, cipher) val cos = CipherOutputStream(outputStream, cipher)
val bos = BufferedOutputStream(cos) val bos = BufferedOutputStream(cos)
outputPlanGroupAndEntries(bos) outputPlanGroupAndEntries(bos)
bos.flush() bos.flush()
@@ -107,7 +106,8 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
} }
@Throws(DatabaseOutputException::class) @Throws(DatabaseOutputException::class)
override fun outputHeader(outputStream: OutputStream): DatabaseHeaderKDB { private fun outputHeader(outputStream: OutputStream,
assignMasterKey: () -> Unit): DatabaseHeaderKDB {
// Build header // Build header
val header = DatabaseHeaderKDB() val header = DatabaseHeaderKDB()
header.signature1 = DatabaseHeaderKDB.DBSIG_1 header.signature1 = DatabaseHeaderKDB.DBSIG_1
@@ -132,6 +132,9 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
setIVs(header) setIVs(header)
mDatabaseKDB.transformSeed = header.transformSeed
assignMasterKey()
// Header checksum // Header checksum
val headerDigest: MessageDigest = HashManager.getHash256() val headerDigest: MessageDigest = HashManager.getHash256()

View File

@@ -56,9 +56,8 @@ import javax.crypto.CipherOutputStream
import kotlin.experimental.or import kotlin.experimental.or
class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX, class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX)
outputStream: OutputStream) : DatabaseOutput<DatabaseHeaderKDBX>() {
: DatabaseOutput<DatabaseHeaderKDBX>(outputStream) {
private var randomStream: StreamCipher? = null private var randomStream: StreamCipher? = null
private lateinit var xml: XmlSerializer private lateinit var xml: XmlSerializer
@@ -67,43 +66,34 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
private var headerHmac: ByteArray? = null private var headerHmac: ByteArray? = null
@Throws(DatabaseOutputException::class) @Throws(DatabaseOutputException::class)
override fun output() { override fun writeDatabase(outputStream: OutputStream,
assignMasterKey: () -> Unit) {
try { try {
header = outputHeader(mOutputStream) header = outputHeader(outputStream, assignMasterKey)
val osPlain: OutputStream = if (header!!.version.isBefore(FILE_VERSION_40)) { val osPlain: OutputStream = if (header!!.version.isBefore(FILE_VERSION_40)) {
val cos = attachStreamEncryptor(header!!, mOutputStream) val cos = attachStreamEncryptor(header!!, outputStream)
cos.write(header!!.streamStartBytes) cos.write(header!!.streamStartBytes)
HashedBlockOutputStream(cos) HashedBlockOutputStream(cos)
} else { } else {
mOutputStream.write(hashOfHeader!!) outputStream.write(hashOfHeader!!)
mOutputStream.write(headerHmac!!) outputStream.write(headerHmac!!)
attachStreamEncryptor(header!!, HmacBlockOutputStream(mOutputStream, mDatabaseKDBX.hmacKey!!)) attachStreamEncryptor(header!!, HmacBlockOutputStream(outputStream, mDatabaseKDBX.hmacKey!!))
} }
val xmlOutputStream: OutputStream when(mDatabaseKDBX.compressionAlgorithm) {
try { CompressionAlgorithm.GZip -> GZIPOutputStream(osPlain)
xmlOutputStream = when(mDatabaseKDBX.compressionAlgorithm) { else -> osPlain
CompressionAlgorithm.GZip -> GZIPOutputStream(osPlain) }.use { xmlOutputStream ->
else -> osPlain
}
if (!header!!.version.isBefore(FILE_VERSION_40)) { if (!header!!.version.isBefore(FILE_VERSION_40)) {
outputInnerHeader(mDatabaseKDBX, header!!, xmlOutputStream) outputInnerHeader(mDatabaseKDBX, header!!, xmlOutputStream)
} }
outputDatabase(xmlOutputStream) outputDatabase(xmlOutputStream)
xmlOutputStream.close()
} catch (e: IllegalArgumentException) {
throw DatabaseOutputException(e)
} catch (e: IllegalStateException) {
throw DatabaseOutputException(e)
} }
} catch (e: Exception) {
} catch (e: IOException) {
throw DatabaseOutputException(e) throw DatabaseOutputException(e)
} }
} }
@@ -322,11 +312,15 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
} }
@Throws(DatabaseOutputException::class) @Throws(DatabaseOutputException::class)
override fun outputHeader(outputStream: OutputStream): DatabaseHeaderKDBX { private fun outputHeader(outputStream: OutputStream,
assignMasterKey: () -> Unit): DatabaseHeaderKDBX {
try { try {
val header = DatabaseHeaderKDBX(mDatabaseKDBX) val header = DatabaseHeaderKDBX(mDatabaseKDBX)
setIVs(header) setIVs(header)
mDatabaseKDBX.transformSeed = header.transformSeed
assignMasterKey.invoke()
val pho = DatabaseHeaderOutputKDBX(mDatabaseKDBX, header, outputStream) val pho = DatabaseHeaderOutputKDBX(mDatabaseKDBX, header, outputStream)
pho.output() pho.output()

View File

@@ -0,0 +1,35 @@
package com.kunzisoft.keepass.hardware
enum class HardwareKey(val value: String) {
// FIDO2_SECRET("FIDO2 secret"),
CHALLENGE_RESPONSE_YUBIKEY("Yubikey challenge-response");
override fun toString(): String {
return value
}
companion object {
val DEFAULT = CHALLENGE_RESPONSE_YUBIKEY
fun getStringValues(): List<String> {
return values().map { it.value }
}
fun fromPosition(position: Int): HardwareKey {
return when (position) {
// 0 -> FIDO2_SECRET
0 -> CHALLENGE_RESPONSE_YUBIKEY
else -> DEFAULT
}
}
fun getHardwareKeyFromString(text: String?): HardwareKey? {
if (text == null)
return null
values().find { it.value == text }?.let {
return it
}
return DEFAULT
}
}
}

View File

@@ -0,0 +1,172 @@
package com.kunzisoft.keepass.hardware
import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.util.Log
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.utils.UriUtil
/**
* Special activity to deal with hardware key drivers,
* return the response to the database service once finished
*/
class HardwareKeyActivity: DatabaseModeActivity(){
// To manage hardware key challenge response
private val resultCallback = ActivityResultCallback<ActivityResult> { result ->
if (result.resultCode == Activity.RESULT_OK) {
val challengeResponse: ByteArray? = result.data?.getByteArrayExtra(HARDWARE_KEY_RESPONSE_KEY)
Log.d(TAG, "Response form challenge")
mDatabaseTaskProvider?.startChallengeResponded(challengeResponse ?: ByteArray(0))
} else {
Log.e(TAG, "Response from challenge error")
mDatabaseTaskProvider?.startChallengeResponded(ByteArray(0))
}
finish()
}
private var activityResultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
resultCallback
)
override fun applyCustomStyle(): Boolean {
return false
}
override fun showDatabaseDialog(): Boolean {
return false
}
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
val hardwareKey = HardwareKey.getHardwareKeyFromString(
intent.getStringExtra(DATA_HARDWARE_KEY)
)
if (isHardwareKeyAvailable(this, hardwareKey, true) {
mDatabaseTaskProvider?.startChallengeResponded(ByteArray(0))
}) {
when (hardwareKey) {
/*
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2 under development
throw Exception("FIDO2 not implemented")
}
*/
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
launchYubikeyChallengeForResponse(intent.getByteArrayExtra(DATA_SEED))
}
else -> {
finish()
}
}
}
}
private fun launchYubikeyChallengeForResponse(seed: ByteArray?) {
// Transform the seed before sending
var challenge: ByteArray? = null
if (seed != null) {
challenge = ByteArray(64)
seed.copyInto(challenge, 0, 0, 32)
challenge.fill(32, 32, 64)
}
// Send to the driver
activityResultLauncher.launch(
Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT).apply {
putExtra(HARDWARE_KEY_CHALLENGE_KEY, challenge)
}
)
Log.d(TAG, "Challenge sent")
}
companion object {
private val TAG = HardwareKeyActivity::class.java.simpleName
private const val DATA_HARDWARE_KEY = "DATA_HARDWARE_KEY"
private const val DATA_SEED = "DATA_SEED"
private const val YUBIKEY_CHALLENGE_RESPONSE_INTENT = "android.yubikey.intent.action.CHALLENGE_RESPONSE"
private const val HARDWARE_KEY_CHALLENGE_KEY = "challenge"
private const val HARDWARE_KEY_RESPONSE_KEY = "response"
fun launchHardwareKeyActivity(
context: Context,
hardwareKey: HardwareKey,
seed: ByteArray?
) {
context.startActivity(Intent(context, HardwareKeyActivity::class.java).apply {
flags = FLAG_ACTIVITY_NEW_TASK
putExtra(DATA_HARDWARE_KEY, hardwareKey.value)
putExtra(DATA_SEED, seed)
})
}
fun isHardwareKeyAvailable(
context: Context,
hardwareKey: HardwareKey?,
showDialog: Boolean = true,
onDialogDismissed: DialogInterface.OnDismissListener? = null
): Boolean {
if (hardwareKey == null)
return false
return when (hardwareKey) {
/*
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2 under development
if (showDialog)
UnderDevelopmentFeatureDialogFragment()
.show(activity.supportFragmentManager, "underDevFeatureDialog")
false
}
*/
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
// Check available intent
val yubikeyDriverAvailable =
Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT)
.resolveActivity(context.packageManager) != null
if (showDialog && !yubikeyDriverAvailable
&& context is Activity)
showHardwareKeyDriverNeeded(context, hardwareKey) {
onDialogDismissed?.onDismiss(it)
context.finish()
}
yubikeyDriverAvailable
}
}
}
private fun showHardwareKeyDriverNeeded(
context: Context,
hardwareKey: HardwareKey,
onDialogDismissed: DialogInterface.OnDismissListener
) {
val builder = AlertDialog.Builder(context)
builder
.setMessage(
context.getString(R.string.error_driver_required, hardwareKey.toString())
)
.setPositiveButton(R.string.download) { _, _ ->
UriUtil.openExternalApp(
context,
context.getString(R.string.key_driver_app_id),
context.getString(R.string.key_driver_url)
)
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.setOnDismissListener(onDialogDismissed)
builder.create().show()
}
}
}

View File

@@ -41,11 +41,6 @@ class CipherEncryptDatabase(): Parcelable {
parcel.readByteArray(specParameters) parcel.readByteArray(specParameters)
} }
fun replaceContent(copy: CipherEncryptDatabase) {
this.encryptedValue = copy.encryptedValue
this.specParameters = copy.specParameters
}
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(databaseUri, flags) parcel.writeParcelable(databaseUri, flags)
parcel.writeEnum(credentialStorage) parcel.writeEnum(credentialStorage)

View File

@@ -1,9 +1,11 @@
package com.kunzisoft.keepass.model package com.kunzisoft.keepass.model
import android.net.Uri import android.net.Uri
import com.kunzisoft.keepass.hardware.HardwareKey
data class DatabaseFile(var databaseUri: Uri? = null, data class DatabaseFile(var databaseUri: Uri? = null,
var keyFileUri: Uri? = null, var keyFileUri: Uri? = null,
var hardwareKey: HardwareKey? = null,
var databaseDecodedPath: String? = null, var databaseDecodedPath: String? = null,
var databaseAlias: String? = null, var databaseAlias: String? = null,
var databaseFileExists: Boolean = false, var databaseFileExists: Boolean = false,

View File

@@ -1,32 +0,0 @@
package com.kunzisoft.keepass.model
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
data class MainCredential(var masterPassword: String? = null, var keyFileUri: Uri? = null): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString(),
parcel.readParcelable(Uri::class.java.classLoader)) {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(masterPassword)
parcel.writeParcelable(keyFileUri, flags)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<MainCredential> {
override fun createFromParcel(parcel: Parcel): MainCredential {
return MainCredential(parcel)
}
override fun newArray(size: Int): Array<MainCredential?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -0,0 +1,13 @@
package com.kunzisoft.keepass.model
import androidx.annotation.StringRes
data class ProgressMessage(
@StringRes
var titleId: Int,
@StringRes
var messageId: Int? = null,
@StringRes
var warningId: Int? = null,
var cancelable: (() -> Unit)? = null
)

View File

@@ -1,3 +1,22 @@
/*
* Copyright 2021 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX 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 3 of the License, or
* (at your option) any later version.
*
* KeePassDX 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 KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.model package com.kunzisoft.keepass.model
import android.content.Context import android.content.Context

View File

@@ -27,6 +27,7 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.util.Log import android.util.Log
import androidx.annotation.StringRes
import androidx.media.app.NotificationCompat import androidx.media.app.NotificationCompat
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.GroupActivity import com.kunzisoft.keepass.activities.GroupActivity
@@ -37,12 +38,15 @@ import com.kunzisoft.keepass.database.action.node.*
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Entry import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.node.Node import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
import com.kunzisoft.keepass.model.CipherEncryptDatabase import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.model.ProgressMessage
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
@@ -53,6 +57,7 @@ import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.closeDatabase import com.kunzisoft.keepass.utils.closeDatabase
import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import java.util.* import java.util.*
open class DatabaseTaskNotificationService : LockNotificationService(), ProgressTaskUpdater { open class DatabaseTaskNotificationService : LockNotificationService(), ProgressTaskUpdater {
@@ -61,20 +66,24 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
private var mDatabase: Database? = null private var mDatabase: Database? = null
// File description
private var mSnapFileDatabaseInfo: SnapFileDatabaseInfo? = null
private var mLastLocalSaveTime: Long = 0
private val mainScope = CoroutineScope(Dispatchers.Main) private val mainScope = CoroutineScope(Dispatchers.Main)
private var mDatabaseListeners = LinkedList<DatabaseListener>() private var mDatabaseListeners = mutableListOf<DatabaseListener>()
private var mDatabaseInfoListeners = LinkedList<DatabaseInfoListener>() private var mDatabaseInfoListeners = mutableListOf<DatabaseInfoListener>()
private var mActionTaskBinder = ActionTaskBinder() private var mActionTaskBinder = ActionTaskBinder()
private var mActionTaskListeners = LinkedList<ActionTaskListener>() private var mActionTaskListeners = mutableListOf<ActionTaskListener>()
// Channel to connect asynchronously a response
private var mResponseChallengeChannel: Channel<ByteArray?>? = null
private var mActionRunning = false private var mActionRunning = false
private var mTaskRemovedRequested = false private var mTaskRemovedRequested = false
private var mCreationState = false private var mSaveState = false
private var mIconId: Int = R.drawable.notification_ic_database_load private var mProgressMessage: ProgressMessage = ProgressMessage(R.string.database_opened)
private var mTitleId: Int = R.string.database_opened
private var mMessageId: Int? = null
private var mWarningId: Int? = null
override fun retrieveChannelId(): String { override fun retrieveChannelId(): String {
return CHANNEL_DATABASE_ID return CHANNEL_DATABASE_ID
@@ -126,9 +135,20 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
} }
interface ActionTaskListener { interface ActionTaskListener {
fun onStartAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) fun onStartAction(database: Database,
fun onUpdateAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) progressMessage: ProgressMessage)
fun onStopAction(database: Database, actionTask: String, result: ActionRunnable.Result) fun onUpdateAction(database: Database,
progressMessage: ProgressMessage)
fun onStopAction(database: Database,
actionTask: String,
result: ActionRunnable.Result)
}
interface RequestChallengeListener {
fun onChallengeResponseRequested(
hardwareKey: HardwareKey,
seed: ByteArray?
)
} }
fun checkDatabase() { fun checkDatabase() {
@@ -165,7 +185,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
Log.i(TAG, "Database file modified " + Log.i(TAG, "Database file modified " +
"$previousDatabaseInfo != $lastFileDatabaseInfo ") "$previousDatabaseInfo != $lastFileDatabaseInfo ")
// Call listener to indicate a change in database info // Call listener to indicate a change in database info
if (!mCreationState && previousDatabaseInfo != null) { if (!mSaveState && previousDatabaseInfo != null) {
mDatabaseInfoListeners.forEach { listener -> mDatabaseInfoListeners.forEach { listener ->
listener.onDatabaseInfoChanged(previousDatabaseInfo, lastFileDatabaseInfo) listener.onDatabaseInfoChanged(previousDatabaseInfo, lastFileDatabaseInfo)
} }
@@ -197,12 +217,47 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
mDatabase?.let { database -> mDatabase?.let { database ->
if (mActionRunning) { if (mActionRunning) {
mActionTaskListeners.forEach { actionTaskListener -> mActionTaskListeners.forEach { actionTaskListener ->
actionTaskListener.onStartAction(database, mTitleId, mMessageId, mWarningId) actionTaskListener.onStartAction(
database, mProgressMessage
)
} }
} }
} }
} }
@OptIn(ExperimentalCoroutinesApi::class)
private fun sendResponseToChallenge(response: ByteArray) {
mainScope.launch {
val responseChannel = mResponseChallengeChannel
if (responseChannel == null || responseChannel.isEmpty) {
if (response.isEmpty()) {
cancelChallengeResponse(R.string.error_no_response_from_challenge)
} else {
mResponseChallengeChannel?.send(response)
}
} else {
cancelChallengeResponse(R.string.error_response_already_provided)
}
}
}
private fun initializeChallengeResponse() {
// Init the channels
if (mResponseChallengeChannel == null) {
mResponseChallengeChannel = Channel(0)
}
}
private fun closeChallengeResponse() {
mResponseChallengeChannel?.close()
mResponseChallengeChannel = null
}
private fun cancelChallengeResponse(@StringRes error: Int) {
mResponseChallengeChannel?.cancel(CancellationException(getString(error)))
mResponseChallengeChannel = null
}
override fun onBind(intent: Intent): IBinder? { override fun onBind(intent: Intent): IBinder? {
super.onBind(intent) super.onBind(intent)
return mActionTaskBinder return mActionTaskBinder
@@ -219,8 +274,20 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
} }
} }
// Get save state
mSaveState = if (intent != null) {
if (intent.hasExtra(SAVE_DATABASE_KEY)) {
!database.isReadOnly && intent.getBooleanExtra(
SAVE_DATABASE_KEY,
mSaveState
)
} else (intent.action == ACTION_DATABASE_CREATE_TASK
|| intent.action == ACTION_DATABASE_ASSIGN_PASSWORD_TASK
|| intent.action == ACTION_DATABASE_SAVE)
} else false
// Create the notification // Create the notification
buildMessage(intent, database.isReadOnly) buildNotification(intent)
val intentAction = intent?.action val intentAction = intent?.action
@@ -258,7 +325,8 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK,
ACTION_DATABASE_UPDATE_PARALLELISM_TASK, ACTION_DATABASE_UPDATE_PARALLELISM_TASK,
ACTION_DATABASE_UPDATE_ITERATIONS_TASK -> buildDatabaseUpdateElementActionTask(intent, database) ACTION_DATABASE_UPDATE_ITERATIONS_TASK -> buildDatabaseUpdateElementActionTask(intent, database)
ACTION_DATABASE_SAVE -> buildDatabaseSave(intent, database) ACTION_DATABASE_SAVE -> buildDatabaseSaveActionTask(intent, database)
ACTION_CHALLENGE_RESPONDED -> buildChallengeRespondedActionTask(intent)
else -> null else -> null
} }
@@ -272,15 +340,16 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
mActionRunning = true mActionRunning = true
sendBroadcast(Intent(DATABASE_START_TASK_ACTION).apply { sendBroadcast(Intent(DATABASE_START_TASK_ACTION).apply {
putExtra(DATABASE_TASK_TITLE_KEY, mTitleId) putExtra(DATABASE_TASK_TITLE_KEY, mProgressMessage.titleId)
putExtra(DATABASE_TASK_MESSAGE_KEY, mMessageId) putExtra(DATABASE_TASK_MESSAGE_KEY, mProgressMessage.messageId)
putExtra(DATABASE_TASK_WARNING_KEY, mWarningId) putExtra(DATABASE_TASK_WARNING_KEY, mProgressMessage.warningId)
}) })
mActionTaskListeners.forEach { actionTaskListener -> mActionTaskListeners.forEach { actionTaskListener ->
actionTaskListener.onStartAction(database, mTitleId, mMessageId, mWarningId) actionTaskListener.onStartAction(
database, mProgressMessage
)
} }
}, },
{ {
actionRunnable actionRunnable
@@ -325,7 +394,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
try { try {
startService(Intent(applicationContext, startService(Intent(applicationContext,
DatabaseTaskNotificationService::class.java)) DatabaseTaskNotificationService::class.java))
} catch (e: IllegalStateException) {} } catch (e: IllegalStateException) {
Log.w(TAG, "Cannot restart the database task service", e)
}
} }
} }
mTaskRemovedRequested = false mTaskRemovedRequested = false
@@ -353,61 +424,51 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
} }
} }
private fun buildMessage(intent: Intent?, readOnly: Boolean) { private fun buildNotification(intent: Intent?) {
// Assign elements for updates // Assign elements for updates
val intentAction = intent?.action val intentAction = intent?.action
var saveAction = false // Get icon depending action state
if (intent != null && intent.hasExtra(SAVE_DATABASE_KEY)) { val iconId = if (intentAction == null)
saveAction = !readOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, saveAction)
}
mIconId = if (intentAction == null)
R.drawable.notification_ic_database_open R.drawable.notification_ic_database_open
else else
R.drawable.notification_ic_database_load R.drawable.notification_ic_database_action
mTitleId = when { // Title depending on action
saveAction -> { mProgressMessage.titleId =
R.string.saving_database if (intentAction == null) {
}
intentAction == null -> {
R.string.database_opened R.string.database_opened
} } else when (intentAction) {
else -> { ACTION_DATABASE_CREATE_TASK -> R.string.creating_database
when (intentAction) { ACTION_DATABASE_LOAD_TASK,
ACTION_DATABASE_CREATE_TASK -> R.string.creating_database ACTION_DATABASE_MERGE_TASK,
ACTION_DATABASE_LOAD_TASK, ACTION_DATABASE_RELOAD_TASK -> R.string.loading_database
ACTION_DATABASE_MERGE_TASK, ACTION_DATABASE_ASSIGN_PASSWORD_TASK,
ACTION_DATABASE_RELOAD_TASK -> R.string.loading_database ACTION_DATABASE_SAVE -> R.string.saving_database
ACTION_DATABASE_SAVE -> R.string.saving_database else -> {
else -> { if (mSaveState)
R.string.saving_database
else
R.string.command_execution R.string.command_execution
}
} }
} }
}
mMessageId = when (intentAction) { // Updated later
ACTION_DATABASE_LOAD_TASK, mProgressMessage.messageId = null
ACTION_DATABASE_MERGE_TASK,
ACTION_DATABASE_RELOAD_TASK -> null
else -> null
}
mWarningId = // Warning if data is saved
if (!saveAction mProgressMessage.warningId =
|| intentAction == ACTION_DATABASE_LOAD_TASK if (mSaveState)
|| intentAction == ACTION_DATABASE_MERGE_TASK
|| intentAction == ACTION_DATABASE_RELOAD_TASK)
null
else
R.string.do_not_kill_app R.string.do_not_kill_app
else
null
val notificationBuilder = buildNewNotification().apply { val notificationBuilder = buildNewNotification().apply {
setSmallIcon(mIconId) setSmallIcon(iconId)
intent?.let { intent?.let {
setContentTitle(getString(intent.getIntExtra(DATABASE_TASK_TITLE_KEY, mTitleId))) setContentTitle(getString(
intent.getIntExtra(DATABASE_TASK_TITLE_KEY, mProgressMessage.titleId))
)
} }
setAutoCancel(false) setAutoCancel(false)
setContentIntent(null) setContentIntent(null)
@@ -513,15 +574,21 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
} }
} }
override fun updateMessage(resId: Int) { private fun notifyProgressMessage() {
mMessageId = resId
mDatabase?.let { database -> mDatabase?.let { database ->
mActionTaskListeners.forEach { actionTaskListener -> mActionTaskListeners.forEach { actionTaskListener ->
actionTaskListener.onUpdateAction(database, mTitleId, mMessageId, mWarningId) actionTaskListener.onUpdateAction(
database, mProgressMessage
)
} }
} }
} }
override fun updateMessage(resId: Int) {
mProgressMessage.messageId = resId
notifyProgressMessage()
}
override fun actionOnLock() { override fun actionOnLock() {
if (!TimeoutHelper.temporarilyDisableLock) { if (!TimeoutHelper.temporarilyDisableLock) {
closeDatabase(mDatabase) closeDatabase(mDatabase)
@@ -539,6 +606,43 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
super.onTaskRemoved(rootIntent) super.onTaskRemoved(rootIntent)
} }
private fun retrieveResponseFromChallenge(hardwareKey: HardwareKey,
seed: ByteArray?): ByteArray {
// Request a challenge - response
var response: ByteArray
runBlocking {
// Initialize the channels
initializeChallengeResponse()
val previousMessage = mProgressMessage.copy()
mProgressMessage.apply {
messageId = R.string.waiting_challenge_request
cancelable = {
cancelChallengeResponse(R.string.error_cancel_by_user)
}
}
// Send the request
notifyProgressMessage()
HardwareKeyActivity
.launchHardwareKeyActivity(
this@DatabaseTaskNotificationService,
hardwareKey,
seed
)
// Wait the response
mProgressMessage.apply {
messageId = R.string.waiting_challenge_response
}
notifyProgressMessage()
response = mResponseChallengeChannel?.receive() ?: byteArrayOf()
// Close channels
closeChallengeResponse()
// Restore previous message
mProgressMessage = previousMessage
notifyProgressMessage()
}
return response
}
private fun buildDatabaseCreateActionTask(intent: Intent, database: Database): ActionRunnable? { private fun buildDatabaseCreateActionTask(intent: Intent, database: Database): ActionRunnable? {
if (intent.hasExtra(DATABASE_URI_KEY) if (intent.hasExtra(DATABASE_URI_KEY)
@@ -550,15 +654,16 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
if (databaseUri == null) if (databaseUri == null)
return null return null
mCreationState = true
return CreateDatabaseRunnable(this, return CreateDatabaseRunnable(this,
database, database,
databaseUri, databaseUri,
getString(R.string.database_default_name), getString(R.string.database_default_name),
getString(R.string.database), getString(R.string.database),
getString(R.string.template_group_name), getString(R.string.template_group_name),
mainCredential mainCredential,
{ hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
) { result -> ) { result ->
result.data = Bundle().apply { result.data = Bundle().apply {
putParcelable(DATABASE_URI_KEY, databaseUri) putParcelable(DATABASE_URI_KEY, databaseUri)
@@ -586,17 +691,18 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
if (databaseUri == null) if (databaseUri == null)
return null return null
mCreationState = false
return LoadDatabaseRunnable( return LoadDatabaseRunnable(
this, this,
database, database,
databaseUri, databaseUri,
mainCredential, mainCredential,
readOnly, { hardwareKey, seed ->
cipherEncryptDatabase, retrieveResponseFromChallenge(hardwareKey, seed)
intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false), },
this readOnly,
cipherEncryptDatabase,
intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false),
this
) { result -> ) { result ->
// Add each info to reload database after thrown duplicate UUID exception // Add each info to reload database after thrown duplicate UUID exception
result.data = Bundle().apply { result.data = Bundle().apply {
@@ -623,9 +729,16 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
return MergeDatabaseRunnable( return MergeDatabaseRunnable(
this, this,
database,
databaseToMergeUri, databaseToMergeUri,
databaseToMergeMainCredential, databaseToMergeMainCredential,
{ hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
},
database,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
{ hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
},
this this
) { result -> ) { result ->
// No need to add each info to reload database // No need to add each info to reload database
@@ -653,7 +766,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
database, database,
databaseUri, databaseUri,
intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential() intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential()
) ) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
} else { } else {
null null
} }
@@ -687,7 +802,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
newGroup, newGroup,
parent, parent,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false), !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable()) AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
} }
} else { } else {
null null
@@ -712,7 +830,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
oldGroup, oldGroup,
newGroup, newGroup,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false), !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable()) AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
} }
} else { } else {
null null
@@ -737,7 +858,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
newEntry, newEntry,
parent, parent,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false), !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable()) AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
} }
} else { } else {
null null
@@ -762,7 +886,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
oldEntry, oldEntry,
newEntry, newEntry,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false), !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable()) AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
} }
} else { } else {
null null
@@ -783,7 +910,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
getListNodesFromBundle(database, intent.extras!!), getListNodesFromBundle(database, intent.extras!!),
newParent, newParent,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false), !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable()) AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
} }
} else { } else {
null null
@@ -804,7 +934,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
getListNodesFromBundle(database, intent.extras!!), getListNodesFromBundle(database, intent.extras!!),
newParent, newParent,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false), !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable()) AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
} }
} else { } else {
null null
@@ -820,7 +953,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
database, database,
getListNodesFromBundle(database, intent.extras!!), getListNodesFromBundle(database, intent.extras!!),
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false), !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
AfterActionNodesRunnable()) AfterActionNodesRunnable()
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
} else { } else {
null null
} }
@@ -838,7 +974,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
database, database,
mainEntry, mainEntry,
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1), intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)) !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
} }
} else { } else {
null null
@@ -857,7 +996,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
database, database,
mainEntry, mainEntry,
intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1), intent.getIntExtra(ENTRY_HISTORY_POSITION_KEY, -1),
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)) !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
} }
} else { } else {
null null
@@ -881,7 +1023,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
oldElement, oldElement,
newElement, newElement,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false) !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
).apply { ) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}.apply {
mAfterSaveDatabase = { result -> mAfterSaveDatabase = { result ->
result.data = intent.extras result.data = intent.extras
} }
@@ -897,7 +1041,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
return RemoveUnlinkedDataDatabaseRunnable(this, return RemoveUnlinkedDataDatabaseRunnable(this,
database, database,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false) !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
).apply { ) { hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}.apply {
mAfterSaveDatabase = { result -> mAfterSaveDatabase = { result ->
result.data = intent.extras result.data = intent.extras
} }
@@ -911,7 +1057,11 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
return if (intent.hasExtra(SAVE_DATABASE_KEY)) { return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
return SaveDatabaseRunnable(this, return SaveDatabaseRunnable(this,
database, database,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false) !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
null,
{ hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
}
).apply { ).apply {
mAfterSaveDatabase = { result -> mAfterSaveDatabase = { result ->
result.data = intent.extras result.data = intent.extras
@@ -925,7 +1075,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
/** /**
* Save database without parameter * Save database without parameter
*/ */
private fun buildDatabaseSave(intent: Intent, database: Database): ActionRunnable? { private fun buildDatabaseSaveActionTask(intent: Intent, database: Database): ActionRunnable? {
return if (intent.hasExtra(SAVE_DATABASE_KEY)) { return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
var databaseCopyUri: Uri? = null var databaseCopyUri: Uri? = null
@@ -936,12 +1086,34 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
SaveDatabaseRunnable(this, SaveDatabaseRunnable(this,
database, database,
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false), !database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
null,
{ hardwareKey, seed ->
retrieveResponseFromChallenge(hardwareKey, seed)
},
databaseCopyUri) databaseCopyUri)
} else { } else {
null null
} }
} }
private fun buildChallengeRespondedActionTask(intent: Intent): ActionRunnable? {
return if (intent.hasExtra(DATA_BYTES)) {
object : ActionRunnable() {
override fun onStartRun() {}
override fun onActionRun() {
mainScope.launch {
intent.getByteArrayExtra(DATA_BYTES)?.let { response ->
sendResponseToChallenge(response)
}
}
}
override fun onFinishRun() {}
}
} else {
null
}
}
companion object { companion object {
private val TAG = DatabaseTaskNotificationService::class.java.name private val TAG = DatabaseTaskNotificationService::class.java.name
@@ -978,6 +1150,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
const val ACTION_DATABASE_UPDATE_PARALLELISM_TASK = "ACTION_DATABASE_UPDATE_PARALLELISM_TASK" const val ACTION_DATABASE_UPDATE_PARALLELISM_TASK = "ACTION_DATABASE_UPDATE_PARALLELISM_TASK"
const val ACTION_DATABASE_UPDATE_ITERATIONS_TASK = "ACTION_DATABASE_UPDATE_ITERATIONS_TASK" const val ACTION_DATABASE_UPDATE_ITERATIONS_TASK = "ACTION_DATABASE_UPDATE_ITERATIONS_TASK"
const val ACTION_DATABASE_SAVE = "ACTION_DATABASE_SAVE" const val ACTION_DATABASE_SAVE = "ACTION_DATABASE_SAVE"
const val ACTION_CHALLENGE_RESPONDED = "ACTION_CHALLENGE_RESPONDED"
const val DATABASE_TASK_TITLE_KEY = "DATABASE_TASK_TITLE_KEY" const val DATABASE_TASK_TITLE_KEY = "DATABASE_TASK_TITLE_KEY"
const val DATABASE_TASK_MESSAGE_KEY = "DATABASE_TASK_MESSAGE_KEY" const val DATABASE_TASK_MESSAGE_KEY = "DATABASE_TASK_MESSAGE_KEY"
@@ -1001,9 +1174,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
const val NEW_NODES_KEY = "NEW_NODES_KEY" const val NEW_NODES_KEY = "NEW_NODES_KEY"
const val OLD_ELEMENT_KEY = "OLD_ELEMENT_KEY" // Warning type of this thing change every time const val OLD_ELEMENT_KEY = "OLD_ELEMENT_KEY" // Warning type of this thing change every time
const val NEW_ELEMENT_KEY = "NEW_ELEMENT_KEY" // Warning type of this thing change every time const val NEW_ELEMENT_KEY = "NEW_ELEMENT_KEY" // Warning type of this thing change every time
const val DATA_BYTES = "DATA_BYTES"
private var mSnapFileDatabaseInfo: SnapFileDatabaseInfo? = null
private var mLastLocalSaveTime: Long = 0
fun getListNodesFromBundle(database: Database, bundle: Bundle): List<Node> { fun getListNodesFromBundle(database: Database, bundle: Bundle): List<Node> {
val nodesAction = ArrayList<Node>() val nodesAction = ArrayList<Node>()

View File

@@ -115,8 +115,8 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
mDatabaseViewModel.saveDatabase(save) mDatabaseViewModel.saveDatabase(save)
} }
private fun mergeDatabase() { private fun mergeDatabase(save: Boolean) {
mDatabaseViewModel.mergeDatabase(false) mDatabaseViewModel.mergeDatabase(save)
} }
private fun reloadDatabase() { private fun reloadDatabase() {
@@ -671,7 +671,7 @@ class NestedDatabaseSettingsFragment : NestedSettingsFragment(), DatabaseRetriev
true true
} }
R.id.menu_merge_database -> { R.id.menu_merge_database -> {
mergeDatabase() mergeDatabase(!mDatabaseReadOnly)
true true
} }
R.id.menu_reload_database -> { R.id.menu_reload_database -> {

View File

@@ -96,6 +96,12 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.remember_keyfile_locations_default)) context.resources.getBoolean(R.bool.remember_keyfile_locations_default))
} }
fun rememberHardwareKey(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.remember_hardware_key_key),
context.resources.getBoolean(R.bool.remember_hardware_key_default))
}
fun automaticallyFocusSearch(context: Context): Boolean { fun automaticallyFocusSearch(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.auto_focus_search_key), return prefs.getBoolean(context.getString(R.string.auto_focus_search_key),
@@ -479,29 +485,33 @@ object PreferencesUtil {
context.resources.getBoolean(R.bool.enable_keep_screen_on_default)) context.resources.getBoolean(R.bool.enable_keep_screen_on_default))
} }
fun isScreenshotModeEnabled(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.enable_screenshot_mode_key),
context.resources.getBoolean(R.bool.enable_screenshot_mode_key_default))
}
fun isAdvancedUnlockEnable(context: Context): Boolean { fun isAdvancedUnlockEnable(context: Context): Boolean {
return isBiometricUnlockEnable(context) || isDeviceCredentialUnlockEnable(context) return isBiometricUnlockEnable(context) || isDeviceCredentialUnlockEnable(context)
} }
fun isBiometricUnlockEnable(context: Context): Boolean { fun isBiometricUnlockEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val biometricSupported = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
AdvancedUnlockManager.biometricUnlockSupported(context)
} else {
false
}
return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key), return prefs.getBoolean(context.getString(R.string.biometric_unlock_enable_key),
context.resources.getBoolean(R.bool.biometric_unlock_enable_default)) context.resources.getBoolean(R.bool.biometric_unlock_enable_default))
&& biometricSupported && (if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
AdvancedUnlockManager.biometricUnlockSupported(context)
} else {
false
})
} }
fun isDeviceCredentialUnlockEnable(context: Context): Boolean { fun isDeviceCredentialUnlockEnable(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
// Priority to biometric unlock // Priority to biometric unlock
val biometricAlreadySupported = isBiometricUnlockEnable(context)
return prefs.getBoolean(context.getString(R.string.device_credential_unlock_enable_key), return prefs.getBoolean(context.getString(R.string.device_credential_unlock_enable_key),
context.resources.getBoolean(R.bool.device_credential_unlock_enable_default)) context.resources.getBoolean(R.bool.device_credential_unlock_enable_default))
&& !biometricAlreadySupported && !isBiometricUnlockEnable(context)
} }
fun isTempAdvancedUnlockEnable(context: Context): Boolean { fun isTempAdvancedUnlockEnable(context: Context): Boolean {

View File

@@ -36,7 +36,7 @@ import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.database.element.Database import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.view.showActionErrorIfNeeded import com.kunzisoft.keepass.view.showActionErrorIfNeeded

View File

@@ -24,15 +24,18 @@ import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.widget.Button
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import java.lang.Exception import kotlinx.coroutines.launch
open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater { open class ProgressTaskDialogFragment : DialogFragment() {
@StringRes @StringRes
private var title = UNDEFINED private var title = UNDEFINED
@@ -40,10 +43,12 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
private var message = UNDEFINED private var message = UNDEFINED
@StringRes @StringRes
private var warning = UNDEFINED private var warning = UNDEFINED
private var cancellable: (() -> Unit)? = null
private var titleView: TextView? = null private var titleView: TextView? = null
private var messageView: TextView? = null private var messageView: TextView? = null
private var warningView: TextView? = null private var warningView: TextView? = null
private var cancelButton: Button? = null
private var progressView: ProgressBar? = null private var progressView: ProgressBar? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@@ -63,11 +68,13 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
titleView = root.findViewById(R.id.progress_dialog_title) titleView = root.findViewById(R.id.progress_dialog_title)
messageView = root.findViewById(R.id.progress_dialog_message) messageView = root.findViewById(R.id.progress_dialog_message)
warningView = root.findViewById(R.id.progress_dialog_warning) warningView = root.findViewById(R.id.progress_dialog_warning)
cancelButton = root.findViewById(R.id.progress_dialog_cancel)
progressView = root.findViewById(R.id.progress_dialog_bar) progressView = root.findViewById(R.id.progress_dialog_bar)
updateTitle(title) updateTitle(title)
updateMessage(message) updateMessage(message)
updateWarning(warning) updateWarning(warning)
setCancellable(cancellable)
isCancelable = false isCancelable = false
@@ -84,7 +91,7 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
} }
private fun updateView(textView: TextView?, @StringRes resId: Int) { private fun updateView(textView: TextView?, @StringRes resId: Int) {
activity?.runOnUiThread { activity?.lifecycleScope?.launch {
if (resId == UNDEFINED) { if (resId == UNDEFINED) {
textView?.visibility = View.GONE textView?.visibility = View.GONE
} else { } else {
@@ -94,21 +101,35 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
} }
} }
fun updateTitle(@StringRes resId: Int) { private fun updateCancelable() {
this.title = resId activity?.lifecycleScope?.launch {
cancelButton?.isVisible = cancellable != null
cancelButton?.setOnClickListener {
cancellable?.invoke()
}
}
}
fun updateTitle(@StringRes resId: Int?) {
this.title = resId ?: UNDEFINED
updateView(titleView, title) updateView(titleView, title)
} }
override fun updateMessage(@StringRes resId: Int) { fun updateMessage(@StringRes resId: Int?) {
this.message = resId this.message = resId ?: UNDEFINED
updateView(messageView, message) updateView(messageView, message)
} }
fun updateWarning(@StringRes resId: Int) { fun updateWarning(@StringRes resId: Int?) {
this.warning = resId this.warning = resId ?: UNDEFINED
updateView(warningView, warning) updateView(warningView, warning)
} }
fun setCancellable(cancellable: (() -> Unit)?) {
this.cancellable = cancellable
updateCancelable()
}
companion object { companion object {
private val TAG = ProgressTaskDialogFragment::class.java.simpleName private val TAG = ProgressTaskDialogFragment::class.java.simpleName
const val PROGRESS_TASK_DIALOG_TAG = "progressDialogFragment" const val PROGRESS_TASK_DIALOG_TAG = "progressDialogFragment"

View File

@@ -126,6 +126,26 @@ object ParcelableUtil {
} }
} }
fun Parcel.readByteArrayCompat(): ByteArray? {
val dataLength = readInt()
return if (dataLength >= 0) {
val data = ByteArray(dataLength)
readByteArray(data)
data
} else {
null
}
}
fun Parcel.writeByteArrayCompat(data: ByteArray?) {
if (data != null) {
writeInt(data.size)
writeByteArray(data)
} else {
writeInt(-1)
}
}
inline fun <reified T : Enum<T>> Parcel.readEnum() = inline fun <reified T : Enum<T>> Parcel.readEnum() =
readString()?.let { enumValueOf<T>(it) } readString()?.let { enumValueOf<T>(it) }

View File

@@ -28,6 +28,7 @@ import android.os.Build
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.kunzisoft.keepass.BuildConfig
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.education.Education import com.kunzisoft.keepass.education.Education
@@ -226,10 +227,10 @@ object UriUtil {
} }
} }
fun getUriFromIntent(intent: Intent, key: String): Uri? { fun getUriFromIntent(intent: Intent?, key: String): Uri? {
try { try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
val clipData = intent.clipData val clipData = intent?.clipData
if (clipData != null) { if (clipData != null) {
if (clipData.description.label == key) { if (clipData.description.label == key) {
if (clipData.itemCount == 1) { if (clipData.itemCount == 1) {
@@ -242,7 +243,7 @@ object UriUtil {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
return intent.getParcelableExtra(key) return intent?.getParcelableExtra(key)
} }
return null return null
} }
@@ -269,11 +270,15 @@ object UriUtil {
fun contributingUser(context: Context): Boolean { fun contributingUser(context: Context): Boolean {
return (Education.isEducationScreenReclickedPerformed(context) return (Education.isEducationScreenReclickedPerformed(context)
|| isExternalAppInstalled(context, "com.kunzisoft.keepass.pro", false) || isExternalAppInstalled(
context,
context.getString(R.string.keepro_app_id),
false
)
) )
} }
private fun isExternalAppInstalled(context: Context, packageName: String, showError: Boolean = true): Boolean { fun isExternalAppInstalled(context: Context, packageName: String, showError: Boolean = true): Boolean {
try { try {
context.applicationContext.packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES) context.applicationContext.packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
Education.setEducationScreenReclickedPerformed(context) Education.setEducationScreenReclickedPerformed(context)
@@ -285,20 +290,35 @@ object UriUtil {
return false return false
} }
fun openExternalApp(context: Context, packageName: String) { fun openExternalApp(context: Context, packageName: String, sourcesURL: String? = null) {
var launchIntent: Intent? = null var launchIntent: Intent? = null
try { try {
launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)?.apply { launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)?.apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
} }
} catch (ignored: Exception) { } catch (ignored: Exception) { }
}
try { try {
if (launchIntent == null) { if (launchIntent == null) {
context.startActivity( context.startActivity(
Intent(Intent.ACTION_VIEW) Intent(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setData(Uri.parse("https://play.google.com/store/apps/details?id=$packageName")) .setData(
Uri.parse(
if (sourcesURL != null
&& !BuildConfig.CLOSED_STORE
) {
sourcesURL
} else {
context.getString(
if (BuildConfig.CLOSED_STORE)
R.string.play_store_url
else
R.string.f_droid_url,
packageName
)
}
)
)
) )
} else { } else {
context.startActivity(launchIntent) context.startActivity(launchIntent)

View File

@@ -0,0 +1,149 @@
package com.kunzisoft.keepass.view
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import android.os.Parcelable.Creator
import android.text.InputType
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Filter
import androidx.appcompat.widget.AppCompatAutoCompleteTextView
import androidx.constraintlayout.widget.ConstraintLayout
import com.google.android.material.textfield.TextInputLayout
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.utils.readEnum
import com.kunzisoft.keepass.utils.writeEnum
class HardwareKeySelectionView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: ConstraintLayout(context, attrs, defStyle) {
private var mHardwareKey: HardwareKey? = null
private val hardwareKeyLayout: TextInputLayout
private val hardwareKeyCompletion: AppCompatAutoCompleteTextView
var selectionListener: ((HardwareKey)-> Unit)? = null
private val mHardwareKeyAdapter = ArrayAdapterNoFilter(context)
private class ArrayAdapterNoFilter(context: Context)
: ArrayAdapter<String>(context, android.R.layout.simple_list_item_1) {
val hardwareKeys = HardwareKey.values()
override fun getCount(): Int {
return hardwareKeys.size
}
override fun getItem(position: Int): String {
return hardwareKeys[position].value
}
override fun getItemId(position: Int): Long {
// Or just return p0
return hardwareKeys[position].hashCode().toLong()
}
override fun getFilter(): Filter {
return object : Filter() {
override fun performFiltering(p0: CharSequence?): FilterResults {
return FilterResults().apply {
values = hardwareKeys
}
}
override fun publishResults(p0: CharSequence?, p1: FilterResults?) {
notifyDataSetChanged()
}
}
}
}
init {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_hardware_key_selection, this)
hardwareKeyLayout = findViewById(R.id.input_entry_hardware_key_layout)
hardwareKeyCompletion = findViewById(R.id.input_entry_hardware_key_completion)
hardwareKeyCompletion.isFocusable = false
hardwareKeyCompletion.isFocusableInTouchMode = false
//hardwareKeyCompletion.isEnabled = false
hardwareKeyCompletion.isCursorVisible = false
hardwareKeyCompletion.setTextIsSelectable(false)
hardwareKeyCompletion.inputType = InputType.TYPE_NULL
hardwareKeyCompletion.setAdapter(mHardwareKeyAdapter)
hardwareKeyCompletion.setOnClickListener {
hardwareKeyCompletion.showDropDown()
}
hardwareKeyCompletion.onItemClickListener =
AdapterView.OnItemClickListener { _, _, position, _ ->
mHardwareKey = HardwareKey.fromPosition(position)
mHardwareKey?.let { hardwareKey ->
selectionListener?.invoke(hardwareKey)
}
}
}
var hardwareKey: HardwareKey?
get() {
return mHardwareKey
}
set(value) {
mHardwareKey = value
hardwareKeyCompletion.setText(value?.toString() ?: "")
}
var error: CharSequence?
get() = hardwareKeyLayout.error
set(value) {
hardwareKeyLayout.error = value
}
override fun onSaveInstanceState(): Parcelable {
val superState = super.onSaveInstanceState()
val saveState = SavedState(superState)
saveState.mHardwareKey = this.mHardwareKey
return saveState
}
override fun onRestoreInstanceState(state: Parcelable?) {
if (state !is SavedState) {
super.onRestoreInstanceState(state)
return
}
super.onRestoreInstanceState(state.superState)
this.mHardwareKey = state.mHardwareKey
}
internal class SavedState : BaseSavedState {
var mHardwareKey: HardwareKey? = null
constructor(superState: Parcelable?) : super(superState)
private constructor(parcel: Parcel) : super(parcel) {
mHardwareKey = parcel.readEnum<HardwareKey>()
}
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeEnum(mHardwareKey)
}
companion object CREATOR : Creator<SavedState> {
override fun createFromParcel(parcel: Parcel): SavedState {
return SavedState(parcel)
}
override fun newArray(size: Int): Array<SavedState?> {
return arrayOfNulls(size)
}
}
}
}

View File

@@ -39,19 +39,24 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.model.CredentialStorage import com.kunzisoft.keepass.model.CredentialStorage
import com.kunzisoft.keepass.model.MainCredential import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.hardware.HardwareKey
class MainCredentialView @JvmOverloads constructor(context: Context, class MainCredentialView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyle: Int = 0) defStyle: Int = 0)
: FrameLayout(context, attrs, defStyle) { : FrameLayout(context, attrs, defStyle) {
private var passwordTextView: EditText
private var keyFileSelectionView: KeyFileSelectionView
private var checkboxPasswordView: CompoundButton private var checkboxPasswordView: CompoundButton
private var passwordTextView: EditText
private var checkboxKeyFileView: CompoundButton private var checkboxKeyFileView: CompoundButton
private var keyFileSelectionView: KeyFileSelectionView
private var checkboxHardwareView: CompoundButton
private var hardwareKeySelectionView: HardwareKeySelectionView
var onPasswordChecked: (CompoundButton.OnCheckedChangeListener)? = null var onPasswordChecked: (CompoundButton.OnCheckedChangeListener)? = null
var onKeyFileChecked: (CompoundButton.OnCheckedChangeListener)? = null
var onHardwareKeyChecked: (CompoundButton.OnCheckedChangeListener)? = null
var onValidateListener: (() -> Unit)? = null var onValidateListener: (() -> Unit)? = null
private var mCredentialStorage: CredentialStorage = CredentialStorage.PASSWORD private var mCredentialStorage: CredentialStorage = CredentialStorage.PASSWORD
@@ -60,15 +65,17 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater? val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater?
inflater?.inflate(R.layout.view_main_credentials, this) inflater?.inflate(R.layout.view_main_credentials, this)
passwordTextView = findViewById(R.id.password_text_view)
keyFileSelectionView = findViewById(R.id.keyfile_selection)
checkboxPasswordView = findViewById(R.id.password_checkbox) checkboxPasswordView = findViewById(R.id.password_checkbox)
checkboxKeyFileView = findViewById(R.id.keyfile_checkox) passwordTextView = findViewById(R.id.password_text_view)
checkboxKeyFileView = findViewById(R.id.keyfile_checkbox)
keyFileSelectionView = findViewById(R.id.keyfile_selection)
checkboxHardwareView = findViewById(R.id.hardware_key_checkbox)
hardwareKeySelectionView = findViewById(R.id.hardware_key_selection)
val onEditorActionListener = object : TextView.OnEditorActionListener { val onEditorActionListener = object : TextView.OnEditorActionListener {
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean { override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
if (actionId == EditorInfo.IME_ACTION_DONE) { if (actionId == EditorInfo.IME_ACTION_DONE) {
onValidateListener?.invoke() validateCredential()
return true return true
} }
return false return false
@@ -91,7 +98,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
if (keyEvent.action == KeyEvent.ACTION_DOWN if (keyEvent.action == KeyEvent.ACTION_DOWN
&& keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER && keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER
) { ) {
onValidateListener?.invoke() validateCredential()
handled = true handled = true
} }
handled handled
@@ -100,10 +107,30 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
checkboxPasswordView.setOnCheckedChangeListener { view, checked -> checkboxPasswordView.setOnCheckedChangeListener { view, checked ->
onPasswordChecked?.onCheckedChanged(view, checked) onPasswordChecked?.onCheckedChanged(view, checked)
} }
checkboxKeyFileView.setOnCheckedChangeListener { view, checked ->
if (checked) {
if (keyFileSelectionView.uri == null) {
checkboxKeyFileView.isChecked = false
}
}
onKeyFileChecked?.onCheckedChanged(view, checked)
}
checkboxHardwareView.setOnCheckedChangeListener { view, checked ->
if (checked) {
if (hardwareKeySelectionView.hardwareKey == null) {
checkboxHardwareView.isChecked = false
}
}
onHardwareKeyChecked?.onCheckedChanged(view, checked)
}
hardwareKeySelectionView.selectionListener = { _ ->
checkboxHardwareView.isChecked = true
}
} }
fun setOpenKeyfileClickListener(externalFileHelper: ExternalFileHelper?) { fun validateCredential() {
keyFileSelectionView.setOpenDocumentClickListener(externalFileHelper) onValidateListener?.invoke()
} }
fun populatePasswordTextView(text: String?) { fun populatePasswordTextView(text: String?) {
@@ -118,7 +145,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
} }
} }
fun populateKeyFileTextView(uri: Uri?) { fun populateKeyFileView(uri: Uri?) {
if (uri == null || uri.toString().isEmpty()) { if (uri == null || uri.toString().isEmpty()) {
keyFileSelectionView.uri = null keyFileSelectionView.uri = null
if (checkboxKeyFileView.isChecked) if (checkboxKeyFileView.isChecked)
@@ -130,16 +157,36 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
} }
} }
fun populateHardwareKeyView(hardwareKey: HardwareKey?) {
if (hardwareKey == null) {
hardwareKeySelectionView.hardwareKey = null
if (checkboxHardwareView.isChecked)
checkboxHardwareView.isChecked = false
} else {
hardwareKeySelectionView.hardwareKey = hardwareKey
if (!checkboxHardwareView.isChecked)
checkboxHardwareView.isChecked = true
}
}
fun setOpenKeyfileClickListener(externalFileHelper: ExternalFileHelper?) {
keyFileSelectionView.setOpenDocumentClickListener(externalFileHelper)
}
fun isFill(): Boolean { fun isFill(): Boolean {
return checkboxPasswordView.isChecked || checkboxKeyFileView.isChecked return checkboxPasswordView.isChecked
|| (checkboxKeyFileView.isChecked && keyFileSelectionView.uri != null)
|| (checkboxHardwareView.isChecked && hardwareKeySelectionView.hardwareKey != null)
} }
fun getMainCredential(): MainCredential { fun getMainCredential(): MainCredential {
return MainCredential().apply { return MainCredential().apply {
this.masterPassword = if (checkboxPasswordView.isChecked) this.password = if (checkboxPasswordView.isChecked)
passwordTextView.text?.toString() else null passwordTextView.text?.toString() else null
this.keyFileUri = if (checkboxKeyFileView.isChecked) this.keyFileUri = if (checkboxKeyFileView.isChecked)
keyFileSelectionView.uri else null keyFileSelectionView.uri else null
this.hardwareKey = if (checkboxHardwareView.isChecked)
hardwareKeySelectionView.hardwareKey else null
} }
} }
@@ -151,7 +198,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
// TODO HARDWARE_KEY // TODO HARDWARE_KEY
return when (mCredentialStorage) { return when (mCredentialStorage) {
CredentialStorage.PASSWORD -> checkboxPasswordView.isChecked CredentialStorage.PASSWORD -> checkboxPasswordView.isChecked
CredentialStorage.KEY_FILE -> checkboxPasswordView.isChecked CredentialStorage.KEY_FILE -> false
CredentialStorage.HARDWARE_KEY -> false CredentialStorage.HARDWARE_KEY -> false
} }
} }

View File

@@ -68,7 +68,7 @@ class TemplateView @JvmOverloads constructor(context: Context,
setCopyButtonState(TextFieldView.ButtonState.ACTIVATE) setCopyButtonState(TextFieldView.ButtonState.ACTIVATE)
setCopyButtonClickListener { label, value -> setCopyButtonClickListener { label, value ->
mOnCopyActionClickListener mOnCopyActionClickListener
?.invoke(Field(label, ProtectedString(false, value))) ?.invoke(Field(label, ProtectedString(true, value)))
} }
} else { } else {
setCopyButtonState(TextFieldView.ButtonState.GONE) setCopyButtonState(TextFieldView.ButtonState.GONE)

View File

@@ -226,8 +226,8 @@ fun View.updateLockPaddingLeft() {
fun Context.showActionErrorIfNeeded(result: ActionRunnable.Result) { fun Context.showActionErrorIfNeeded(result: ActionRunnable.Result) {
if (!result.isSuccess) { if (!result.isSuccess) {
result.exception?.errorId?.let { errorId -> result.exception?.getLocalizedMessage(resources)?.let { errorMessage ->
Toast.makeText(this, errorId, Toast.LENGTH_LONG).show() Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show()
} ?: result.message?.let { message -> } ?: result.message?.let { message ->
Toast.makeText(this, message, Toast.LENGTH_LONG).show() Toast.makeText(this, message, Toast.LENGTH_LONG).show()
} }
@@ -236,8 +236,8 @@ fun Context.showActionErrorIfNeeded(result: ActionRunnable.Result) {
fun CoordinatorLayout.showActionErrorIfNeeded(result: ActionRunnable.Result) { fun CoordinatorLayout.showActionErrorIfNeeded(result: ActionRunnable.Result) {
if (!result.isSuccess) { if (!result.isSuccess) {
result.exception?.errorId?.let { errorId -> result.exception?.getLocalizedMessage(resources)?.let { errorMessage ->
Snackbar.make(this, errorId, Snackbar.LENGTH_LONG).asError().show() Snackbar.make(this, errorMessage, Snackbar.LENGTH_LONG).asError().show()
} ?: result.message?.let { message -> } ?: result.message?.let { message ->
Snackbar.make(this, message, Snackbar.LENGTH_LONG).asError().show() Snackbar.make(this, message, Snackbar.LENGTH_LONG).asError().show()
} }

View File

@@ -7,6 +7,7 @@ import androidx.lifecycle.MutableLiveData
import com.kunzisoft.keepass.app.App import com.kunzisoft.keepass.app.App
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.app.database.IOActionTask import com.kunzisoft.keepass.app.database.IOActionTask
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.model.DatabaseFile import com.kunzisoft.keepass.model.DatabaseFile
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.utils.UriUtil import com.kunzisoft.keepass.utils.UriUtil
@@ -72,8 +73,12 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
} }
} }
fun addDatabaseFile(databaseUri: Uri, keyFileUri: Uri?) { fun addDatabaseFile(databaseUri: Uri, keyFileUri: Uri?, hardwareKey: HardwareKey?) {
mFileDatabaseHistoryAction?.addOrUpdateDatabaseUri(databaseUri, keyFileUri) { databaseFileAdded -> mFileDatabaseHistoryAction?.addOrUpdateDatabaseUri(
databaseUri,
keyFileUri,
hardwareKey
) { databaseFileAdded ->
databaseFileAdded?.let { _ -> databaseFileAdded?.let { _ ->
databaseFilesLoaded.value = getDatabaseFilesLoadedValue().apply { databaseFilesLoaded.value = getDatabaseFilesLoadedValue().apply {
this.databaseFileAction = DatabaseFileAction.ADD this.databaseFileAction = DatabaseFileAction.ADD
@@ -96,6 +101,7 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
.find { it.databaseUri == databaseFileUpdated.databaseUri } .find { it.databaseUri == databaseFileUpdated.databaseUri }
?.apply { ?.apply {
keyFileUri = databaseFileUpdated.keyFileUri keyFileUri = databaseFileUpdated.keyFileUri
hardwareKey = databaseFileUpdated.hardwareKey
databaseAlias = databaseFileUpdated.databaseAlias databaseAlias = databaseFileUpdated.databaseAlias
databaseFileExists = databaseFileUpdated.databaseFileExists databaseFileExists = databaseFileUpdated.databaseFileExists
databaseLastModified = databaseFileUpdated.databaseLastModified databaseLastModified = databaseFileUpdated.databaseLastModified

View File

@@ -87,8 +87,8 @@ class DatabaseViewModel: ViewModel() {
_saveDatabase.value = save _saveDatabase.value = save
} }
fun mergeDatabase(fixDuplicateUuid: Boolean) { fun mergeDatabase(save: Boolean) {
_mergeDatabase.value = fixDuplicateUuid _mergeDatabase.value = save
} }
fun reloadDatabase(fixDuplicateUuid: Boolean) { fun reloadDatabase(fixDuplicateUuid: Boolean) {
@@ -196,6 +196,8 @@ class DatabaseViewModel: ViewModel() {
data class SuperLong(val oldValue: Long, data class SuperLong(val oldValue: Long,
val newValue: Long, val newValue: Long,
val save: Boolean) val save: Boolean)
data class SuperMerge(val fixDuplicateUuid: Boolean,
val save: Boolean)
data class SuperCompression(val oldValue: CompressionAlgorithm, data class SuperCompression(val oldValue: CompressionAlgorithm,
val newValue: CompressionAlgorithm, val newValue: CompressionAlgorithm,
val save: Boolean) val save: Boolean)

View File

Before

Width:  |  Height:  |  Size: 897 B

After

Width:  |  Height:  |  Size: 897 B

View File

Before

Width:  |  Height:  |  Size: 657 B

After

Width:  |  Height:  |  Size: 657 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -34,7 +34,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/toolbar" app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintBottom_toBottomOf="parent"> app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -118,6 +118,11 @@
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView
android:id="@+id/activity_about_privacy_text"
android:layout_marginTop="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView <TextView
android:id="@+id/activity_about_contribution_text" android:id="@+id/activity_about_contribution_text"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
@@ -179,4 +184,5 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
<include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -17,165 +17,174 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
--> -->
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/toolbar_coordinator"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:filterTouchesWhenObscured="true" android:filterTouchesWhenObscured="true"
android:fitsSystemWindows="true"> android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/app_bar" android:id="@+id/toolbar_coordinator"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/toolbar_parallax_height" android:layout_height="0dp"
android:background="?attr/colorPrimary"> app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.CollapsingToolbarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/toolbar_layout" android:id="@+id/app_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="@dimen/toolbar_parallax_height"
app:contentScrim="?attr/colorPrimary" android:background="?attr/colorPrimary">
app:expandedTitleGravity="center_horizontal|bottom"
app:expandedTitleMarginStart="@dimen/default_margin"
app:expandedTitleMarginEnd="@dimen/default_margin"
app:expandedTitleMarginBottom="24dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<FrameLayout <com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/title_block" android:id="@+id/toolbar_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_collapseMode="parallax" app:contentScrim="?attr/colorPrimary"
android:orientation="vertical" app:expandedTitleGravity="center_horizontal|bottom"
android:background="@drawable/background_repeat" app:expandedTitleMarginStart="@dimen/default_margin"
android:gravity="center" app:expandedTitleMarginEnd="@dimen/default_margin"
android:paddingBottom="12dp" app:expandedTitleMarginBottom="24dp"
style="@style/KeepassDXStyle.TextAppearance.Default"> app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/entry_icon" <FrameLayout
android:id="@+id/title_block"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_collapseMode="parallax"
android:orientation="vertical"
android:background="@drawable/background_repeat"
android:gravity="center"
android:paddingBottom="12dp"
style="@style/KeepassDXStyle.TextAppearance.Default">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/entry_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:src="@drawable/ic_blank_32dp"
style="@style/KeepassDXStyle.Icon"
android:layout_gravity="center"/>
</FrameLayout>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:theme="?attr/toolbarAppearance"
app:layout_collapseMode="pin"
tools:targetApi="lollipop">
</androidx.appcompat.widget.Toolbar>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/entry_progress"
android:visibility="gone"
android:indeterminate="false"
app:indicatorColor="?attr/colorAccent"
android:progress="10"
android:max="30"
android:layout_gravity="bottom"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/entry_scroll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbarStyle="insideOverlay"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/history_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
android:visibility="gone"
android:background="?attr/colorAccent"
android:padding="12dp"
android:textColor="?attr/colorOnAccentColor"
android:text="@string/entry_history"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/entry_tags_list_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="4dp" app:layout_constraintStart_toStartOf="parent"
android:src="@drawable/ic_blank_32dp" app:layout_constraintEnd_toEndOf="parent"
style="@style/KeepassDXStyle.Icon" android:paddingTop="12dp"
android:layout_gravity="center"/> android:paddingStart="5dp"
</FrameLayout> android:paddingLeft="5dp"
<androidx.appcompat.widget.Toolbar android:paddingEnd="5dp"
android:id="@+id/toolbar" android:paddingRight="5dp"
android:layout_width="match_parent" android:layout_gravity="center"
android:layout_height="?attr/actionBarSize" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:theme="?attr/toolbarAppearance" android:orientation="horizontal"
app:layout_collapseMode="pin" app:layout_constraintTop_toBottomOf="@+id/history_container"/>
tools:targetApi="lollipop">
</androidx.appcompat.widget.Toolbar>
<com.google.android.material.progressindicator.LinearProgressIndicator <androidx.fragment.app.FragmentContainerView
android:id="@+id/entry_progress" android:id="@+id/entry_content"
android:visibility="gone" android:name="com.kunzisoft.keepass.activities.fragments.EntryFragment"
android:indeterminate="false" android:layout_width="0dp"
app:indicatorColor="?attr/colorAccent" android:layout_height="wrap_content"
android:progress="10" app:layout_constraintWidth_percent="@dimen/content_percent"
android:max="30" app:layout_constraintTop_toBottomOf="@+id/entry_tags_list_view"
android:layout_gravity="bottom" app:layout_constraintStart_toStartOf="parent"
android:layout_width="match_parent" app:layout_constraintEnd_toEndOf="parent"/>
android:layout_height="wrap_content" /> </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout> </androidx.core.widget.NestedScrollView>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView <com.google.android.material.tabs.TabLayout
android:id="@+id/entry_scroll" android:id="@+id/entry_content_tab"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:scrollbarStyle="insideOverlay" android:minWidth="120dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"> android:layout_gravity="bottom|center_horizontal"
android:background="?attr/cardBackgroundTransparentColor"
app:tabIconTint="?android:attr/textColor"
app:tabMode="fixed">
<androidx.constraintlayout.widget.ConstraintLayout <com.google.android.material.tabs.TabItem
android:layout_width="match_parent" android:id="@+id/entry_content_tab_main"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/history_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
android:visibility="gone"
android:background="?attr/colorAccent"
android:padding="12dp"
android:textColor="?attr/colorOnAccentColor"
android:text="@string/entry_history"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/entry_tags_list_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent" android:icon="@drawable/ic_view_list_white_24dp" />
app:layout_constraintEnd_toEndOf="parent"
android:paddingTop="12dp"
android:paddingStart="5dp"
android:paddingLeft="5dp"
android:paddingEnd="5dp"
android:paddingRight="5dp"
android:layout_gravity="center"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="@+id/history_container"/>
<androidx.fragment.app.FragmentContainerView <com.google.android.material.tabs.TabItem
android:id="@+id/entry_content" android:id="@+id/entry_content_tab_advanced"
android:name="com.kunzisoft.keepass.activities.fragments.EntryFragment" android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintWidth_percent="@dimen/content_percent" android:icon="@drawable/ic_time_white_24dp" />
app:layout_constraintTop_toBottomOf="@+id/entry_tags_list_view"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView> </com.google.android.material.tabs.TabLayout>
<com.google.android.material.tabs.TabLayout <FrameLayout
android:id="@+id/entry_content_tab" android:layout_width="match_parent"
android:layout_width="wrap_content" android:layout_height="match_parent">
android:layout_height="wrap_content" <ProgressBar
android:minWidth="120dp" android:id="@+id/loading"
android:layout_gravity="bottom|center_horizontal" android:layout_width="wrap_content"
android:background="?attr/cardBackgroundTransparentColor" android:layout_height="wrap_content"
app:tabIconTint="?android:attr/textColor" android:layout_gravity="center"
app:tabMode="fixed"> android:indeterminate="true" />
</FrameLayout>
<com.google.android.material.tabs.TabItem <include
android:id="@+id/entry_content_tab_main" layout="@layout/view_button_lock"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:icon="@drawable/ic_view_list_white_24dp" /> android:layout_gravity="start|bottom" />
<com.google.android.material.tabs.TabItem </androidx.coordinatorlayout.widget.CoordinatorLayout>
android:id="@+id/entry_content_tab_advanced"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/ic_time_white_24dp" />
</com.google.android.material.tabs.TabLayout> <include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true" />
</FrameLayout>
<include
layout="@layout/view_button_lock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|bottom" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -84,7 +84,7 @@
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:theme="?attr/toolbarActionAppearance" android:theme="?attr/toolbarActionAppearance"
android:layout_gravity="bottom" android:layout_gravity="bottom"
app:layout_constraintBottom_toBottomOf="parent" /> app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" />
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/entry_edit_validate" android:id="@+id/entry_edit_validate"
@@ -96,7 +96,7 @@
android:tint="?attr/colorOnAccentColor" android:tint="?attr/colorOnAccentColor"
app:fabSize="mini" app:fabSize="mini"
app:layout_constraintTop_toTopOf="@+id/entry_edit_bottom_bar" app:layout_constraintTop_toTopOf="@+id/entry_edit_bottom_bar"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
@@ -105,7 +105,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent" /> app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"/>
<ProgressBar <ProgressBar
android:id="@+id/loading" android:id="@+id/loading"
@@ -119,4 +119,6 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -146,7 +146,7 @@
android:id="@+id/file_selection_buttons_container" android:id="@+id/file_selection_buttons_container"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"> app:layout_constraintStart_toStartOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
@@ -194,4 +194,5 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout> </FrameLayout>
<include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -27,7 +27,7 @@
android:filterTouchesWhenObscured="true" android:filterTouchesWhenObscured="true"
android:fitsSystemWindows="true"> android:fitsSystemWindows="true">
<RelativeLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/activity_group_container_view" android:id="@+id/activity_group_container_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@@ -36,16 +36,17 @@
android:id="@+id/special_mode_view" android:id="@+id/special_mode_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:theme="?attr/toolbarSpecialAppearance" /> android:theme="?attr/toolbarSpecialAppearance"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:title="@string/app_name"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:layout_below="@+id/special_mode_view"
android:background="?attr/colorPrimary" android:background="?attr/colorPrimary"
android:theme="?attr/toolbarAppearance" > android:theme="?attr/toolbarAppearance"
android:title="@string/app_name"
app:layout_constraintTop_toBottomOf="@+id/special_mode_view">
<FrameLayout <FrameLayout
android:id="@+id/database_name_container" android:id="@+id/database_name_container"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -64,10 +65,12 @@
<FrameLayout <FrameLayout
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:layout_below="@+id/special_mode_view"
android:layout_marginStart="50dp" android:layout_marginStart="50dp"
android:layout_marginLeft="50dp"> android:layout_marginLeft="50dp"
<ImageView app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/special_mode_view">
<ImageView
android:id="@+id/database_color" android:id="@+id/database_color"
android:layout_width="12dp" android:layout_width="12dp"
android:layout_height="12dp" android:layout_height="12dp"
@@ -91,9 +94,9 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/group_coordinator" android:id="@+id/group_coordinator"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_below="@+id/toolbar" app:layout_constraintBottom_toTopOf="@+id/toolbar_action"
android:layout_above="@+id/toolbar_action"> app:layout_constraintTop_toBottomOf="@+id/toolbar">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar" android:id="@+id/app_bar"
@@ -159,7 +162,7 @@
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:visibility="gone" android:visibility="gone"
android:theme="?attr/toolbarActionAppearance" android:theme="?attr/toolbarActionAppearance"
android:layout_alignParentBottom="true" /> app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" />
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -177,9 +180,10 @@
layout="@layout/view_button_lock" layout="@layout/view_button_lock"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentBottom="true"/> app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" />
</RelativeLayout> <include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.kunzisoft.keepass.view.NavigationDatabaseView <com.kunzisoft.keepass.view.NavigationDatabaseView
android:id="@+id/database_nav_view" android:id="@+id/database_nav_view"

View File

@@ -43,7 +43,7 @@
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:layout_gravity="bottom" android:layout_gravity="bottom"
android:theme="?attr/toolbarActionAppearance" android:theme="?attr/toolbarActionAppearance"
app:layout_constraintBottom_toBottomOf="parent" /> app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" />
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/icon_picker_upload" android:id="@+id/icon_picker_upload"
@@ -53,15 +53,17 @@
android:contentDescription="@string/validate" android:contentDescription="@string/validate"
android:src="@drawable/ic_file_upload_white_24dp" android:src="@drawable/ic_file_upload_white_24dp"
app:fabSize="mini" app:fabSize="mini"
app:layout_constraintTop_toTopOf="@+id/toolbar" app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/> app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/toolbar" />
<include <include
layout="@layout/view_button_lock" layout="@layout/view_button_lock"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"
app:layout_constraintBottom_toBottomOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
<include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -14,7 +14,7 @@
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"
app:layout_constraintTop_toBottomOf="@+id/toolbar"> app:layout_constraintTop_toBottomOf="@+id/toolbar">
<ProgressBar <ProgressBar
@@ -30,4 +30,6 @@
android:layout_gravity="center" android:layout_gravity="center"
android:contentDescription="@string/entry_attachments" /> android:contentDescription="@string/entry_attachments" />
</FrameLayout> </FrameLayout>
<include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -43,7 +43,7 @@
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:layout_gravity="bottom" android:layout_gravity="bottom"
android:theme="?attr/toolbarActionAppearance" android:theme="?attr/toolbarActionAppearance"
app:layout_constraintBottom_toBottomOf="parent" /> app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" />
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/key_generator_validation" android:id="@+id/key_generator_validation"
@@ -55,7 +55,7 @@
android:tint="?attr/colorOnAccentColor" android:tint="?attr/colorOnAccentColor"
app:fabSize="mini" app:fabSize="mini"
app:layout_constraintTop_toTopOf="@+id/toolbar" app:layout_constraintTop_toTopOf="@+id/toolbar"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/> app:layout_constraintStart_toStartOf="parent"/>
@@ -64,5 +64,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent" /> app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" />
<include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -17,7 +17,7 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
--> -->
<RelativeLayout <androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
@@ -28,6 +28,7 @@
tools:targetApi="o"> tools:targetApi="o">
<com.kunzisoft.keepass.view.SpecialModeView <com.kunzisoft.keepass.view.SpecialModeView
app:layout_constraintTop_toTopOf="parent"
android:id="@+id/special_mode_view" android:id="@+id/special_mode_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -36,11 +37,11 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/activity_password_coordinator_layout" android:id="@+id/activity_password_coordinator_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="0dp"
android:background="@drawable/background_repeat" android:background="@drawable/background_repeat"
android:backgroundTint="?android:attr/textColor" android:backgroundTint="?android:attr/textColor"
android:layout_below="@+id/special_mode_view" app:layout_constraintTop_toBottomOf="@+id/special_mode_view"
android:layout_above="@+id/activity_password_footer"> app:layout_constraintBottom_toTopOf="@+id/activity_password_footer">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar" android:id="@+id/app_bar"
@@ -156,7 +157,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:layout_alignParentBottom="true"> app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner">
<LinearLayout <LinearLayout
android:id="@+id/activity_password_info_container" android:id="@+id/activity_password_info_container"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -193,4 +194,5 @@
android:text="@string/menu_open" /> android:text="@string/menu_open" />
</LinearLayout> </LinearLayout>
</RelativeLayout> <include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -17,15 +17,22 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
--> -->
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/toolbar_coordinator"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:filterTouchesWhenObscured="true" android:filterTouchesWhenObscured="true"
android:background="?android:attr/windowBackground" android:background="?android:attr/windowBackground"
android:fitsSystemWindows="true"> android:fitsSystemWindows="true">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/toolbar_coordinator"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner">
<LinearLayout <LinearLayout
android:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -46,5 +53,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="start|bottom" /> android:layout_gravity="start|bottom" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> <include layout="@layout/view_screenshot_mode_banner" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -58,6 +58,16 @@
android:layout_marginEnd="20dp" android:layout_marginEnd="20dp"
style="@style/KeepassDXStyle.TextAppearance.Warning"/> style="@style/KeepassDXStyle.TextAppearance.Warning"/>
<Button
android:id="@+id/progress_dialog_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="20dp"
android:layout_marginRight="20dp"
android:layout_marginEnd="20dp"
android:text="@string/entry_cancel" />
<com.google.android.material.progressindicator.LinearProgressIndicator <com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_dialog_bar" android:id="@+id/progress_dialog_bar"
app:indicatorColor="?attr/colorAccent" app:indicatorColor="?attr/colorAccent"

View File

@@ -20,6 +20,7 @@
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:filterTouchesWhenObscured="true"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<LinearLayout <LinearLayout
@@ -115,7 +116,7 @@
android:orientation="vertical"> android:orientation="vertical">
<androidx.appcompat.widget.SwitchCompat <androidx.appcompat.widget.SwitchCompat
android:id="@+id/keyfile_checkox" android:id="@+id/keyfile_checkbox"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/entry_keyfile"/> android:text="@string/entry_keyfile"/>
@@ -126,9 +127,41 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/keyfile_checkox" app:layout_constraintStart_toEndOf="@+id/keyfile_checkbox"
app:layout_constraintEnd_toEndOf="parent" /> app:layout_constraintEnd_toEndOf="parent" />
</LinearLayout> </LinearLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/card_view_hardware_key"
android:layout_gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
app:cardCornerRadius="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/default_margin"
android:orientation="vertical">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/hardware_key_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/hardware_key"/>
<com.kunzisoft.keepass.view.HardwareKeySelectionView
android:id="@+id/hardware_key_selection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/hardware_key_checkbox"
app:layout_constraintEnd_toEndOf="parent"
android:importantForAccessibility="no"
android:importantForAutofill="no" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

View File

@@ -129,6 +129,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:clickable="false"
android:contentDescription="@string/download" android:contentDescription="@string/download"
android:src="@drawable/ic_file_stream_white_24dp" android:src="@drawable/ic_file_stream_white_24dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container_hardware_key"
android:layout_marginBottom="@dimen/default_margin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:importantForAutofill="noExcludeDescendants"
android:importantForAccessibility="no"
tools:ignore="UnusedAttribute">
<com.google.android.material.textfield.TextInputLayout
style="@style/KeepassDXStyle.TextInputLayout.ExposedMenu"
android:id="@+id/input_entry_hardware_key_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hardware_key">
<androidx.appcompat.widget.AppCompatAutoCompleteTextView
android:id="@+id/input_entry_hardware_key_completion"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
</FrameLayout>

View File

@@ -63,7 +63,7 @@
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<androidx.appcompat.widget.SwitchCompat <androidx.appcompat.widget.SwitchCompat
android:id="@+id/keyfile_checkox" android:id="@+id/keyfile_checkbox"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignTop="@+id/keyfile_selection" android:layout_alignTop="@+id/keyfile_selection"
@@ -76,9 +76,35 @@
android:id="@+id/keyfile_selection" android:id="@+id/keyfile_selection"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="48dp" android:layout_toEndOf="@+id/keyfile_checkbox"
android:layout_toRightOf="@+id/keyfile_checkox" android:layout_toRightOf="@+id/keyfile_checkbox"
android:layout_toEndOf="@+id/keyfile_checkox" android:importantForAccessibility="no"
android:importantForAutofill="no"
android:minHeight="48dp" />
</RelativeLayout>
<!-- Hardware key -->
<RelativeLayout
android:id="@+id/container_hardware_key"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/hardware_key_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/hardware_key_selection"
android:layout_marginTop="22dp"
android:contentDescription="@string/content_description_hardware_key_checkbox"
android:focusable="false"
android:gravity="center_vertical" />
<com.kunzisoft.keepass.view.HardwareKeySelectionView
android:id="@+id/hardware_key_selection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/hardware_key_checkbox"
android:layout_toRightOf="@+id/hardware_key_checkbox"
android:importantForAccessibility="no" android:importantForAccessibility="no"
android:importantForAutofill="no" /> android:importantForAutofill="no" />
</RelativeLayout> </RelativeLayout>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:id="@+id/screenshot_mode_banner"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@color/grey"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp"
android:text="@string/screenshot_mode_banner_text"
android:textColor="@color/white"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</merge>

View File

@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" /> <background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon> </adaptive-icon>

View File

@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" /> <background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon> </adaptive-icon>

Some files were not shown because too many files have changed in this diff Show More