Merge branch 'release/3.5.0'
10
CHANGELOG
@@ -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
|
||||||
|
|||||||
72
Gemfile.lock
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
15
app/src/free/res/drawable-v24/ic_launcher_monochrome.xml
Normal 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>
|
||||||
15
app/src/libre/res/drawable-v24/ic_launcher_monochrome.xml
Normal 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>
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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>()
|
||||||
|
|||||||
@@ -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 -> {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 897 B After Width: | Height: | Size: 897 B |
|
Before Width: | Height: | Size: 657 B After Width: | Height: | Size: 657 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
@@ -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>
|
||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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"
|
||||||
|
|||||||
26
app/src/main/res/layout/view_hardware_key_selection.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
18
app/src/main/res/layout/view_screenshot_mode_banner.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||