Compare commits
93 Commits
3.4.5
...
3.5.0beta0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecc4550261 | ||
|
|
8b046512e3 | ||
|
|
228a10c8e0 | ||
|
|
9c53bea190 | ||
|
|
11cf991498 | ||
|
|
a88c3721b2 | ||
|
|
0b4b6d4d91 | ||
|
|
941f9bcd48 | ||
|
|
f1bf9fb25c | ||
|
|
1751fa49c0 | ||
|
|
6b4fc9a4fa | ||
|
|
7c8d85e428 | ||
|
|
e335140f23 | ||
|
|
d85f398b5f | ||
|
|
a16082a59d | ||
|
|
456269a343 | ||
|
|
eb8e1e20eb | ||
|
|
ed3c84fec0 | ||
|
|
be40416a2d | ||
|
|
5b5476a513 | ||
|
|
dc64dd6400 | ||
|
|
eca02d3bde | ||
|
|
176b6c2936 | ||
|
|
5b22350bdf | ||
|
|
6e1e011234 | ||
|
|
ac65ef6a5c | ||
|
|
fc198dde74 | ||
|
|
15ac51b2fc | ||
|
|
34214432e1 | ||
|
|
361ca92493 | ||
|
|
e367051b80 | ||
|
|
a2a4a50c5e | ||
|
|
afc74b2f2a | ||
|
|
fc756d1eaf | ||
|
|
eb8a4b1e49 | ||
|
|
8d258b3538 | ||
|
|
a59cfa3477 | ||
|
|
f1e513006e | ||
|
|
9df5c8f439 | ||
|
|
3ae099accf | ||
|
|
bb3e9396f2 | ||
|
|
1628749bde | ||
|
|
f2006b5e42 | ||
|
|
80d387d9e7 | ||
|
|
4452b4d599 | ||
|
|
dfeaeb9888 | ||
|
|
7e45a20ee7 | ||
|
|
f3fe92e4de | ||
|
|
b606909c65 | ||
|
|
2882bb30d7 | ||
|
|
5b62227e3f | ||
|
|
8b6af6fd8a | ||
|
|
99e9a92953 | ||
|
|
9f626309c3 | ||
|
|
3fe7cf2bfd | ||
|
|
9b5c274b49 | ||
|
|
46b350e7ac | ||
|
|
22a4aeb108 | ||
|
|
332e116ba7 | ||
|
|
8b594a1a1f | ||
|
|
ab23ec6d4d | ||
|
|
647e3f9383 | ||
|
|
597f52799d | ||
|
|
a59e052ed8 | ||
|
|
1ff2f501ca | ||
|
|
cfcb49e233 | ||
|
|
467df2020e | ||
|
|
a961b41de0 | ||
|
|
40e8d5225e | ||
|
|
bc755ae1df | ||
|
|
b1cb0c3786 | ||
|
|
090d0fa2db | ||
|
|
27918a12b0 | ||
|
|
ba1498b0b2 | ||
|
|
cbde96dd82 | ||
|
|
344118a755 | ||
|
|
259c8a4bd9 | ||
|
|
f4d5bd1bea | ||
|
|
20b352cabe | ||
|
|
20e35f1a69 | ||
|
|
d963f56d0f | ||
|
|
5734df89f0 | ||
|
|
327c9de464 | ||
|
|
8b2f994769 | ||
|
|
a5e53d872b | ||
|
|
19bc2444bc | ||
|
|
b44c9cfc51 | ||
|
|
5b4338abae | ||
|
|
e8f79ae467 | ||
|
|
ecbee73eae | ||
|
|
1874a0056d | ||
|
|
279bd16b74 | ||
|
|
2e0081b66c |
@@ -1,3 +1,10 @@
|
||||
KeePassDX(3.5.0)
|
||||
* Support YubiKey challenge-response #8 #137
|
||||
* Better exception management during database save #1346
|
||||
* Better management of mime-types and extensions #1211
|
||||
* Add "Screenshot mode" setting #459 #1377 #1354 (Thx @GianpaMX)
|
||||
* Hide clipboard sensitive text when copy entry field #1386
|
||||
|
||||
KeePassDX(3.4.5)
|
||||
* Fix custom data in group (fix KeeShare) #1335
|
||||
* Fix device credential unlocking #1344
|
||||
|
||||
70
Gemfile.lock
@@ -3,25 +3,25 @@ GEM
|
||||
specs:
|
||||
CFPropertyList (3.0.5)
|
||||
rexml
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
addressable (2.8.1)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
artifactory (3.0.15)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.577.0)
|
||||
aws-sdk-core (3.130.1)
|
||||
aws-partitions (1.626.0)
|
||||
aws-sdk-core (3.140.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.525.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.55.0)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.58.0)
|
||||
aws-sdk-core (~> 3, >= 3.127.0)
|
||||
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-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.4.0)
|
||||
aws-sigv4 (1.5.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
claide (1.1.0)
|
||||
@@ -34,10 +34,10 @@ GEM
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.7.6)
|
||||
dotenv (2.8.1)
|
||||
emoji_regex (3.2.3)
|
||||
excon (0.92.2)
|
||||
faraday (1.10.0)
|
||||
excon (0.92.4)
|
||||
faraday (1.10.2)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
@@ -56,8 +56,8 @@ GEM
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.0.3)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-net_http (1.0.1)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
@@ -66,7 +66,7 @@ GEM
|
||||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.2.6)
|
||||
fastlane (2.205.1)
|
||||
fastlane (2.209.1)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
@@ -107,9 +107,9 @@ GEM
|
||||
xcpretty-travis-formatter (>= 0.0.3)
|
||||
fastlane-plugin-versioning_android (0.1.0)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.19.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-core (0.4.2)
|
||||
google-apis-androidpublisher_v3 (0.25.0)
|
||||
google-apis-core (>= 0.7, < 2.a)
|
||||
google-apis-core (0.7.0)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
@@ -118,27 +118,27 @@ GEM
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.10.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.7.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-storage_v1 (0.13.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-iamcredentials_v1 (0.13.0)
|
||||
google-apis-core (>= 0.7, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.10.0)
|
||||
google-apis-core (>= 0.7, < 2.a)
|
||||
google-apis-storage_v1 (0.17.0)
|
||||
google-apis-core (>= 0.7, < 2.a)
|
||||
google-cloud-core (1.6.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.2.0)
|
||||
google-cloud-storage (1.36.1)
|
||||
google-cloud-storage (1.39.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.17.0)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.1.2)
|
||||
googleauth (1.2.0)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
@@ -146,12 +146,12 @@ GEM
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.4)
|
||||
http-cookie (1.0.5)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
jmespath (1.6.1)
|
||||
json (2.6.1)
|
||||
jwt (2.3.0)
|
||||
json (2.6.2)
|
||||
jwt (2.5.0)
|
||||
memoist (0.16.2)
|
||||
mini_magick (4.11.0)
|
||||
mini_mime (1.1.2)
|
||||
@@ -162,9 +162,9 @@ GEM
|
||||
optparse (0.1.1)
|
||||
os (1.1.4)
|
||||
plist (3.6.0)
|
||||
public_suffix (4.0.7)
|
||||
public_suffix (5.0.0)
|
||||
rake (13.0.6)
|
||||
representable (3.1.1)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
@@ -174,9 +174,9 @@ GEM
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
security (0.1.3)
|
||||
signet (0.16.1)
|
||||
signet (0.17.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.0)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.8)
|
||||
@@ -193,11 +193,11 @@ GEM
|
||||
uber (0.1.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.8.1)
|
||||
unf_ext (0.0.8.2)
|
||||
unicode-display_width (1.8.0)
|
||||
webrick (1.7.0)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.21.0)
|
||||
xcodeproj (1.22.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
|
||||
@@ -12,8 +12,8 @@ android {
|
||||
applicationId "com.kunzisoft.keepass"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 32
|
||||
versionCode = 114
|
||||
versionName = "3.4.5"
|
||||
versionCode = 115
|
||||
versionName = "3.5.0 Beta01"
|
||||
multiDexEnabled true
|
||||
|
||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||
@@ -93,7 +93,7 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
def room_version = "2.4.2"
|
||||
def room_version = "2.4.3"
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
@@ -101,14 +101,14 @@ dependencies {
|
||||
implementation "androidx.appcompat:appcompat:$android_appcompat_version"
|
||||
implementation 'androidx.preference:preference-ktx:1.2.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.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.biometric:biometric:1.1.0'
|
||||
implementation 'androidx.media:media:1.6.0'
|
||||
// Lifecycle - LiveData - ViewModel - Coroutines
|
||||
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"
|
||||
// Token auto complete
|
||||
// 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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,6 @@
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data android:scheme="file" />
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="application/octet-stream" />
|
||||
<data android:mimeType="application/x-kdb" />
|
||||
<data android:mimeType="application/x-kdbx" />
|
||||
<data android:mimeType="application/x-keepass" />
|
||||
|
||||
@@ -77,6 +77,12 @@ class AboutActivity : StylishActivity() {
|
||||
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 {
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
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.database.element.Database
|
||||
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.SearchInfo
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
||||
@@ -155,8 +156,9 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
mAdapterDatabaseHistory?.setOnFileDatabaseHistoryOpenListener { fileDatabaseHistoryEntityToOpen ->
|
||||
fileDatabaseHistoryEntityToOpen.databaseUri?.let { databaseFileUri ->
|
||||
launchPasswordActivity(
|
||||
databaseFileUri,
|
||||
fileDatabaseHistoryEntityToOpen.keyFileUri
|
||||
databaseFileUri,
|
||||
fileDatabaseHistoryEntityToOpen.keyFileUri,
|
||||
fileDatabaseHistoryEntityToOpen.hardwareKey
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -250,7 +252,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
?: MainCredential()
|
||||
databaseFilesViewModel.addDatabaseFile(
|
||||
databaseUri,
|
||||
mainCredential.keyFileUri
|
||||
mainCredential.keyFileUri,
|
||||
mainCredential.hardwareKey
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -297,10 +300,11 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
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,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
{ exception ->
|
||||
fileNoFoundAction(exception)
|
||||
},
|
||||
@@ -321,7 +325,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
||||
}
|
||||
|
||||
private fun launchPasswordActivityWithPath(databaseUri: Uri) {
|
||||
launchPasswordActivity(databaseUri, null)
|
||||
launchPasswordActivity(databaseUri, null, null)
|
||||
// Delete flickering for kitkat <=
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
|
||||
overridePendingTransition(0, 0)
|
||||
|
||||
@@ -69,7 +69,7 @@ import com.kunzisoft.keepass.database.search.SearchParameters
|
||||
import com.kunzisoft.keepass.education.GroupActivityEducation
|
||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
||||
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.SearchInfo
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||
|
||||
@@ -56,9 +56,11 @@ import com.kunzisoft.keepass.autofill.AutofillHelper
|
||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockFragment
|
||||
import com.kunzisoft.keepass.biometric.AdvancedUnlockManager
|
||||
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.FileNotFoundDatabaseException
|
||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.model.*
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.CIPHER_DATABASE_KEY
|
||||
@@ -101,6 +103,8 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
private var mRememberKeyFile: Boolean = false
|
||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||
|
||||
private var mRememberHardwareKey: Boolean = false
|
||||
|
||||
private var mReadOnly: Boolean = false
|
||||
private var mForceReadOnly: Boolean = false
|
||||
|
||||
@@ -133,11 +137,13 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
PreferencesUtil.enableReadOnlyDatabase(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 ->
|
||||
if (uri != null) {
|
||||
mainCredentialView?.populateKeyFileTextView(uri)
|
||||
mainCredentialView?.populateKeyFileView(uri)
|
||||
}
|
||||
}
|
||||
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
|
||||
@@ -171,6 +177,16 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
mAdvancedUnlockViewModel.checkUnlockAvailability()
|
||||
enableConfirmationButton()
|
||||
}
|
||||
mainCredentialView?.onKeyFileChecked =
|
||||
CompoundButton.OnCheckedChangeListener { _, _ ->
|
||||
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
|
||||
enableConfirmationButton()
|
||||
}
|
||||
mainCredentialView?.onHardwareKeyChecked =
|
||||
CompoundButton.OnCheckedChangeListener { _, _ ->
|
||||
// TODO mAdvancedUnlockViewModel.checkUnlockAvailability()
|
||||
enableConfirmationButton()
|
||||
}
|
||||
|
||||
// Observe if default database
|
||||
mDatabaseFileViewModel.isDefaultDatabase.observe(this) { isDefaultDatabase ->
|
||||
@@ -204,10 +220,19 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
databaseKeyFileUri
|
||||
}
|
||||
|
||||
val databaseHardwareKey = mainCredentialView?.getMainCredential()?.hardwareKey
|
||||
val hardwareKey =
|
||||
if (mRememberHardwareKey
|
||||
&& databaseHardwareKey == null) {
|
||||
databaseFile?.hardwareKey
|
||||
} else {
|
||||
databaseHardwareKey
|
||||
}
|
||||
|
||||
// Define title
|
||||
filenameView?.text = databaseFile?.databaseAlias ?: ""
|
||||
|
||||
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri)
|
||||
onDatabaseFileLoaded(databaseFile?.databaseUri, keyFileUri, hardwareKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,6 +240,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
super.onResume()
|
||||
|
||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this@MainCredentialActivity)
|
||||
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this@MainCredentialActivity)
|
||||
|
||||
// Back to previous keyboard is setting activated
|
||||
if (PreferencesUtil.isKeyboardPreviousDatabaseCredentialsEnable(this@MainCredentialActivity)) {
|
||||
@@ -332,24 +358,36 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
private fun getUriFromIntent(intent: Intent?) {
|
||||
// If is a view intent
|
||||
val action = intent?.action
|
||||
if (action != null
|
||||
&& action == VIEW_INTENT) {
|
||||
mDatabaseFileUri = intent.data
|
||||
mainCredentialView?.populateKeyFileTextView(UriUtil.getUriFromIntent(intent, KEY_KEYFILE))
|
||||
if (action == VIEW_INTENT) {
|
||||
fillCredentials(
|
||||
intent.data,
|
||||
UriUtil.getUriFromIntent(intent, KEY_KEYFILE),
|
||||
HardwareKey.getHardwareKeyFromString(intent.getStringExtra(KEY_HARDWARE_KEY))
|
||||
)
|
||||
} else {
|
||||
mDatabaseFileUri = intent?.getParcelableExtra(KEY_FILENAME)
|
||||
intent?.getParcelableExtra<Uri?>(KEY_KEYFILE)?.let {
|
||||
mainCredentialView?.populateKeyFileTextView(it)
|
||||
}
|
||||
fillCredentials(
|
||||
intent?.getParcelableExtra(KEY_FILENAME),
|
||||
intent?.getParcelableExtra(KEY_KEYFILE),
|
||||
HardwareKey.getHardwareKeyFromString(intent?.getStringExtra(KEY_HARDWARE_KEY))
|
||||
)
|
||||
}
|
||||
try {
|
||||
intent?.removeExtra(KEY_KEYFILE)
|
||||
intent?.removeExtra(KEY_HARDWARE_KEY)
|
||||
} catch (e: Exception) {}
|
||||
mDatabaseFileUri?.let {
|
||||
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?) {
|
||||
super.onNewIntent(intent)
|
||||
getUriFromIntent(intent)
|
||||
@@ -358,7 +396,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
private fun launchGroupActivityIfLoaded(database: Database) {
|
||||
// Check if database really loaded
|
||||
if (database.loaded) {
|
||||
clearCredentialsViews(true)
|
||||
clearCredentialsViews(clearKeyFile = true, clearHardwareKey = true)
|
||||
GroupActivity.launch(this,
|
||||
database,
|
||||
{ onValidateSpecialMode() },
|
||||
@@ -408,7 +446,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
val mainCredential = mainCredentialView?.getMainCredential() ?: MainCredential()
|
||||
when (cipherDecryptDatabase.credentialStorage) {
|
||||
CredentialStorage.PASSWORD -> {
|
||||
mainCredential.masterPassword = String(cipherDecryptDatabase.decryptedValue)
|
||||
mainCredential.password = String(cipherDecryptDatabase.decryptedValue)
|
||||
}
|
||||
CredentialStorage.KEY_FILE -> {
|
||||
// TODO advanced unlock key file
|
||||
@@ -423,14 +461,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
|
||||
if (mRememberKeyFile) {
|
||||
mainCredentialView?.populateKeyFileTextView(keyFileUri)
|
||||
mainCredentialView?.populateKeyFileView(keyFileUri)
|
||||
}
|
||||
|
||||
// Define hardware key
|
||||
if (mRememberHardwareKey) {
|
||||
mainCredentialView?.populateHardwareKeyView(hardwareKey)
|
||||
}
|
||||
|
||||
// Define listener for validate button
|
||||
confirmButtonView?.setOnClickListener { loadDatabase() }
|
||||
confirmButtonView?.setOnClickListener {
|
||||
mainCredentialView?.validateCredential()
|
||||
}
|
||||
|
||||
// If Activity is launch with a password and want to open directly
|
||||
val intent = intent
|
||||
@@ -462,10 +509,14 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile) {
|
||||
private fun clearCredentialsViews(clearKeyFile: Boolean = !mRememberKeyFile,
|
||||
clearHardwareKey: Boolean = !mRememberHardwareKey) {
|
||||
mainCredentialView?.populatePasswordTextView(null)
|
||||
if (clearKeyFile) {
|
||||
mainCredentialView?.populateKeyFileTextView(null)
|
||||
mainCredentialView?.populateKeyFileView(null)
|
||||
}
|
||||
if (clearHardwareKey) {
|
||||
mainCredentialView?.populateHardwareKeyView(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -656,18 +707,24 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
|
||||
private const val KEY_FILENAME = "fileName"
|
||||
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 KEY_READ_ONLY = "KEY_READ_ONLY"
|
||||
private const val KEY_PASSWORD = "password"
|
||||
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) {
|
||||
val intent = Intent(activity, MainCredentialActivity::class.java)
|
||||
intent.putExtra(KEY_FILENAME, databaseFile)
|
||||
if (keyFile != null)
|
||||
intent.putExtra(KEY_KEYFILE, keyFile)
|
||||
if (hardwareKey != null)
|
||||
intent.putExtra(KEY_HARDWARE_KEY, hardwareKey.toString())
|
||||
intentBuildLauncher.invoke(intent)
|
||||
}
|
||||
|
||||
@@ -680,8 +737,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
@Throws(FileNotFoundException::class)
|
||||
fun launch(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
}
|
||||
@@ -696,8 +754,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
fun launchForSearchResult(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
searchInfo: SearchInfo) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForSearchModeResult(
|
||||
activity,
|
||||
intent,
|
||||
@@ -715,8 +774,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
fun launchForSaveResult(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
searchInfo: SearchInfo) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForSaveModeResult(
|
||||
activity,
|
||||
intent,
|
||||
@@ -734,8 +794,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
fun launchForKeyboardResult(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
searchInfo: SearchInfo?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForKeyboardSelectionModeResult(
|
||||
activity,
|
||||
intent,
|
||||
@@ -754,10 +815,11 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||
autofillComponent: AutofillComponent,
|
||||
searchInfo: SearchInfo?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
AutofillHelper.startActivityForAutofillResult(
|
||||
activity,
|
||||
intent,
|
||||
@@ -775,8 +837,9 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
fun launchForRegistration(activity: Activity,
|
||||
databaseFile: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
registerInfo: RegisterInfo?) {
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile) { intent ->
|
||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||
activity,
|
||||
intent,
|
||||
@@ -792,6 +855,7 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
fun launch(activity: AppCompatActivity,
|
||||
databaseUri: Uri,
|
||||
keyFile: Uri?,
|
||||
hardwareKey: HardwareKey?,
|
||||
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
||||
onCancelSpecialMode: () -> Unit,
|
||||
onLaunchActivitySpecialMode: () -> Unit,
|
||||
@@ -800,43 +864,67 @@ class MainCredentialActivity : DatabaseModeActivity(), AdvancedUnlockFragment.Bu
|
||||
try {
|
||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
||||
{
|
||||
MainCredentialActivity.launch(activity,
|
||||
databaseUri, keyFile)
|
||||
launch(
|
||||
activity,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey
|
||||
)
|
||||
},
|
||||
{ searchInfo -> // Search Action
|
||||
MainCredentialActivity.launchForSearchResult(activity,
|
||||
databaseUri, keyFile,
|
||||
searchInfo)
|
||||
launchForSearchResult(
|
||||
activity,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo -> // Save Action
|
||||
MainCredentialActivity.launchForSaveResult(activity,
|
||||
databaseUri, keyFile,
|
||||
searchInfo)
|
||||
launchForSaveResult(
|
||||
activity,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo -> // Keyboard Selection Action
|
||||
MainCredentialActivity.launchForKeyboardResult(activity,
|
||||
databaseUri, keyFile,
|
||||
searchInfo)
|
||||
launchForKeyboardResult(
|
||||
activity,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
},
|
||||
{ searchInfo, autofillComponent -> // Autofill Selection Action
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
MainCredentialActivity.launchForAutofillResult(activity,
|
||||
databaseUri, keyFile,
|
||||
autofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo)
|
||||
launchForAutofillResult(
|
||||
activity,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
autofillActivityResultLauncher,
|
||||
autofillComponent,
|
||||
searchInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
} else {
|
||||
onCancelSpecialMode()
|
||||
}
|
||||
},
|
||||
{ registerInfo -> // Registration Action
|
||||
MainCredentialActivity.launchForRegistration(activity,
|
||||
databaseUri, keyFile,
|
||||
registerInfo)
|
||||
launchForRegistration(
|
||||
activity,
|
||||
databaseUri,
|
||||
keyFile,
|
||||
hardwareKey,
|
||||
registerInfo
|
||||
)
|
||||
onLaunchActivitySpecialMode()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -27,7 +27,7 @@ import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.kunzisoft.keepass.R
|
||||
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.view.MainCredentialView
|
||||
|
||||
@@ -95,7 +95,7 @@ class MainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
mExternalFileHelper = ExternalFileHelper(this)
|
||||
mExternalFileHelper?.buildOpenDocument { uri ->
|
||||
if (uri != null) {
|
||||
mainCredentialView?.populateKeyFileTextView(uri)
|
||||
mainCredentialView?.populateKeyFileView(uri)
|
||||
}
|
||||
}
|
||||
mainCredentialView?.setOpenKeyfileClickListener(mExternalFileHelper)
|
||||
|
||||
@@ -26,7 +26,7 @@ import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.database.element.MainCredential
|
||||
|
||||
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_buy_pro), FROM_HTML_MODE_LEGACY))
|
||||
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 {
|
||||
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))
|
||||
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||
UriUtil.gotoUrl(requireContext(), R.string.contribution_url)
|
||||
UriUtil.gotoUrl(activity, R.string.contribution_url)
|
||||
}
|
||||
}
|
||||
builder.setMessage(stringBuilder)
|
||||
|
||||
@@ -35,9 +35,12 @@ import com.google.android.material.textfield.TextInputLayout
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.hardware.HardwareKeyResponseHelper
|
||||
import com.kunzisoft.keepass.database.element.MainCredential
|
||||
import com.kunzisoft.keepass.password.PasswordEntropy
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.HardwareKeySelectionView
|
||||
import com.kunzisoft.keepass.view.KeyFileSelectionView
|
||||
import com.kunzisoft.keepass.view.PassKeyView
|
||||
import com.kunzisoft.keepass.view.applyFontVisibility
|
||||
@@ -45,18 +48,21 @@ import com.kunzisoft.keepass.view.applyFontVisibility
|
||||
class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
|
||||
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 var passwordRepeatTextInputLayout: TextInputLayout? = null
|
||||
private var passwordRepeatView: TextView? = null
|
||||
private lateinit var keyFileCheckBox: CompoundButton
|
||||
private lateinit var keyFileSelectionView: KeyFileSelectionView
|
||||
|
||||
private var keyFileCheckBox: CompoundButton? = null
|
||||
private var keyFileSelectionView: KeyFileSelectionView? = null
|
||||
private lateinit var hardwareKeyCheckBox: CompoundButton
|
||||
private lateinit var hardwareKeySelectionView: HardwareKeySelectionView
|
||||
|
||||
private var mListener: AssignMainCredentialDialogListener? = null
|
||||
|
||||
@@ -67,13 +73,15 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
private var mNoKeyConfirmationDialog: AlertDialog? = null
|
||||
private var mEmptyKeyFileConfirmationDialog: AlertDialog? = null
|
||||
|
||||
private var mAllowNoMasterKey: Boolean = false
|
||||
|
||||
private val passwordTextWatcher = object : TextWatcher {
|
||||
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {}
|
||||
|
||||
override fun afterTextChanged(editable: Editable) {
|
||||
passwordCheckBox?.isChecked = true
|
||||
passwordCheckBox.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,10 +121,9 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
activity?.let { activity ->
|
||||
|
||||
var allowNoMasterKey = false
|
||||
arguments?.apply {
|
||||
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)
|
||||
@@ -128,63 +135,63 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.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)
|
||||
}
|
||||
|
||||
passwordCheckBox = rootView?.findViewById(R.id.password_checkbox)
|
||||
passKeyView = rootView?.findViewById(R.id.password_view)
|
||||
passwordRepeatTextInputLayout = rootView?.findViewById(R.id.password_repeat_input_layout)
|
||||
passwordRepeatView = rootView?.findViewById(R.id.password_confirmation)
|
||||
passwordRepeatView?.applyFontVisibility()
|
||||
passwordCheckBox = rootView.findViewById(R.id.password_checkbox)
|
||||
passwordView = rootView.findViewById(R.id.password_view)
|
||||
passwordRepeatTextInputLayout = rootView.findViewById(R.id.password_repeat_input_layout)
|
||||
passwordRepeatView = rootView.findViewById(R.id.password_confirmation)
|
||||
passwordRepeatView.applyFontVisibility()
|
||||
|
||||
keyFileCheckBox = rootView?.findViewById(R.id.keyfile_checkox)
|
||||
keyFileSelectionView = rootView?.findViewById(R.id.keyfile_selection)
|
||||
keyFileCheckBox = rootView.findViewById(R.id.keyfile_checkbox)
|
||||
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?.buildOpenDocument { uri ->
|
||||
uri?.let { pathUri ->
|
||||
UriUtil.getFileData(requireContext(), uri)?.length()?.let { lengthFile ->
|
||||
keyFileSelectionView?.error = null
|
||||
keyFileCheckBox?.isChecked = true
|
||||
keyFileSelectionView?.uri = pathUri
|
||||
keyFileSelectionView.error = null
|
||||
keyFileCheckBox.isChecked = true
|
||||
keyFileSelectionView.uri = pathUri
|
||||
if (lengthFile <= 0L) {
|
||||
showEmptyKeyFileConfirmationDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
keyFileSelectionView?.setOpenDocumentClickListener(mExternalFileHelper)
|
||||
keyFileSelectionView.setOpenDocumentClickListener(mExternalFileHelper)
|
||||
|
||||
hardwareKeySelectionView.selectionListener = { hardwareKey ->
|
||||
hardwareKeyCheckBox.isChecked = true
|
||||
hardwareKeySelectionView.error =
|
||||
if (!HardwareKeyResponseHelper.isHardwareKeyAvailable(requireActivity(), hardwareKey)) {
|
||||
// show hardware driver dialog if required
|
||||
getString(R.string.error_driver_required, hardwareKey.toString())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val dialog = builder.create()
|
||||
dialog.setOnShowListener { dialog1 ->
|
||||
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
positiveButton.setOnClickListener {
|
||||
|
||||
if (passwordCheckBox != null && keyFileCheckBox!= null) {
|
||||
dialog.setOnShowListener { dialog1 ->
|
||||
val positiveButton = (dialog1 as AlertDialog).getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
positiveButton.setOnClickListener {
|
||||
mMasterPassword = ""
|
||||
mKeyFileUri = null
|
||||
mHardwareKey = null
|
||||
|
||||
mMasterPassword = ""
|
||||
mKeyFile = null
|
||||
|
||||
var error = verifyPassword() || verifyKeyFile()
|
||||
if (!passwordCheckBox!!.isChecked && !keyFileCheckBox!!.isChecked) {
|
||||
error = true
|
||||
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()
|
||||
}
|
||||
approveMainCredential()
|
||||
}
|
||||
val negativeButton = dialog1.getButton(DialogInterface.BUTTON_NEGATIVE)
|
||||
negativeButton.setOnClickListener {
|
||||
mListener?.onAssignKeyDialogNegativeClick(retrieveMainCredential())
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,67 +201,113 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
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
|
||||
&& !HardwareKeyResponseHelper.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 {
|
||||
val masterPassword = if (passwordCheckBox!!.isChecked) mMasterPassword else null
|
||||
val keyFile = if (keyFileCheckBox!!.isChecked) mKeyFile else null
|
||||
return MainCredential(masterPassword, keyFile)
|
||||
val masterPassword = if (passwordCheckBox.isChecked) mMasterPassword else null
|
||||
val keyFileUri = if (keyFileCheckBox.isChecked) mKeyFileUri else null
|
||||
val hardwareKey = if (hardwareKeyCheckBox.isChecked) mHardwareKey else null
|
||||
return MainCredential(masterPassword, keyFileUri, hardwareKey)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// To check checkboxes if a text is present
|
||||
passKeyView?.addTextChangedListener(passwordTextWatcher)
|
||||
passwordView.addTextChangedListener(passwordTextWatcher)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
passKeyView?.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
|
||||
passwordView.removeTextChangedListener(passwordTextWatcher)
|
||||
}
|
||||
|
||||
private fun showEmptyPasswordConfirmationDialog() {
|
||||
@@ -262,10 +315,8 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
val builder = AlertDialog.Builder(it)
|
||||
builder.setMessage(R.string.warning_empty_password)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
if (!verifyKeyFile()) {
|
||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||
this@SetMainCredentialDialogFragment.dismiss()
|
||||
}
|
||||
mListener?.onAssignKeyDialogPositiveClick(retrieveMainCredential())
|
||||
this@SetMainCredentialDialogFragment.dismiss()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
mEmptyPasswordConfirmationDialog = builder.create()
|
||||
@@ -299,8 +350,8 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
|
||||
})
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
keyFileCheckBox?.isChecked = false
|
||||
keyFileSelectionView?.uri = null
|
||||
keyFileCheckBox.isChecked = false
|
||||
keyFileSelectionView.uri = null
|
||||
}
|
||||
mEmptyKeyFileConfirmationDialog = builder.create()
|
||||
mEmptyKeyFileConfirmationDialog?.show()
|
||||
|
||||
@@ -39,6 +39,7 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
|
||||
val stringBuilder = SpannableStringBuilder()
|
||||
/*
|
||||
if (UriUtil.contributingUser(activity)) {
|
||||
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")
|
||||
@@ -46,14 +47,14 @@ class UnderDevelopmentFeatureDialogFragment : DialogFragment() {
|
||||
.append(HtmlCompat.fromHtml(getString(R.string.html_text_dev_feature_upgrade), HtmlCompat.FROM_HTML_MODE_LEGACY)).append(" ")
|
||||
builder.setPositiveButton(android.R.string.ok) { _, _ -> dismiss() }
|
||||
} else {
|
||||
*/
|
||||
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_encourage), HtmlCompat.FROM_HTML_MODE_LEGACY))
|
||||
builder.setPositiveButton(R.string.contribute) { _, _ ->
|
||||
UriUtil.gotoUrl(requireContext(), R.string.contribution_url)
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel) { _, _ -> dismiss() }
|
||||
}
|
||||
//}
|
||||
builder.setMessage(stringBuilder)
|
||||
// Create the AlertDialog object and return it
|
||||
return builder.create()
|
||||
|
||||
@@ -56,7 +56,7 @@ class ExternalFileHelper {
|
||||
|
||||
fun buildOpenDocument(onFileSelected: ((uri: Uri?) -> Unit)?) {
|
||||
|
||||
val resultCallback = ActivityResultCallback<Uri> { result ->
|
||||
val resultCallback = ActivityResultCallback<Uri?> { result ->
|
||||
result?.let { uri ->
|
||||
UriUtil.takeUriPermission(activity?.contentResolver, uri)
|
||||
onFileSelected?.invoke(uri)
|
||||
@@ -91,7 +91,7 @@ class ExternalFileHelper {
|
||||
fun buildCreateDocument(typeString: String = "application/octet-stream",
|
||||
onFileCreated: (fileCreated: Uri?)->Unit) {
|
||||
|
||||
val resultCallback = ActivityResultCallback<Uri> { result ->
|
||||
val resultCallback = ActivityResultCallback<Uri?> { result ->
|
||||
onFileCreated.invoke(result)
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ class ExternalFileHelper {
|
||||
|
||||
class OpenDocument : ActivityResultContracts.OpenDocument() {
|
||||
@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 {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
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 {
|
||||
return super.createIntent(context, input).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = typeString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ import androidx.activity.viewModels
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||
import com.kunzisoft.keepass.database.action.DatabaseTaskProvider
|
||||
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.MainCredential
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.viewmodels.ChallengeResponseViewModel
|
||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||
|
||||
abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
||||
@@ -17,10 +18,12 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
||||
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
||||
protected var mDatabase: Database? = null
|
||||
|
||||
private val mChallengeResponseViewModel: ChallengeResponseViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
mDatabaseTaskProvider = DatabaseTaskProvider(this)
|
||||
mDatabaseTaskProvider = DatabaseTaskProvider(this, mChallengeResponseViewModel)
|
||||
|
||||
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
||||
val databaseWasReloaded = database?.wasReloaded == true
|
||||
@@ -36,6 +39,13 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
mDatabaseTaskProvider?.destroy()
|
||||
mDatabaseTaskProvider = null
|
||||
mDatabase = null
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onDatabaseRetrieved(database: Database?) {
|
||||
mDatabase = database
|
||||
mDatabaseViewModel.defineDatabase(database)
|
||||
|
||||
@@ -32,7 +32,6 @@ import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
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.PasswordEncodingDialogFragment
|
||||
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.icons.IconDrawableFactory
|
||||
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.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
|
||||
@@ -21,14 +21,21 @@ package com.kunzisoft.keepass.activities.stylish
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
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.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.PreferencesUtil
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
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
|
||||
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() {
|
||||
@@ -94,6 +117,7 @@ abstract class StylishActivity : AppCompatActivity() {
|
||||
Log.d(this.javaClass.name, "Theme change detected, restarting activity")
|
||||
recreateActivity()
|
||||
}
|
||||
setScreenshotMode(PreferencesUtil.isScreenshotModeEnabled(this))
|
||||
}
|
||||
|
||||
private fun recreateActivity() {
|
||||
|
||||
@@ -23,8 +23,15 @@ import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
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 fun fileDatabaseHistoryDao(): FileDatabaseHistoryDao
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.app.database
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.model.DatabaseFile
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
||||
@@ -44,6 +45,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||
DatabaseFile(
|
||||
databaseUri,
|
||||
UriUtil.parse(fileDatabaseHistoryEntity?.keyFileUri),
|
||||
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity?.hardwareKey),
|
||||
UriUtil.decode(fileDatabaseHistoryEntity?.databaseUri),
|
||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias ?: ""),
|
||||
fileDatabaseInfo.exists,
|
||||
@@ -85,13 +87,14 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||
|| !hideBrokenLocations) {
|
||||
databaseFileListLoaded.add(
|
||||
DatabaseFile(
|
||||
UriUtil.parse(fileDatabaseHistoryEntity.databaseUri),
|
||||
UriUtil.parse(fileDatabaseHistoryEntity.keyFileUri),
|
||||
UriUtil.decode(fileDatabaseHistoryEntity.databaseUri),
|
||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
|
||||
fileDatabaseInfo.exists,
|
||||
fileDatabaseInfo.getLastModificationString(),
|
||||
fileDatabaseInfo.getSizeString()
|
||||
UriUtil.parse(fileDatabaseHistoryEntity.databaseUri),
|
||||
UriUtil.parse(fileDatabaseHistoryEntity.keyFileUri),
|
||||
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity.hardwareKey),
|
||||
UriUtil.decode(fileDatabaseHistoryEntity.databaseUri),
|
||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
|
||||
fileDatabaseInfo.exists,
|
||||
fileDatabaseInfo.getLastModificationString(),
|
||||
fileDatabaseInfo.getSizeString()
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -107,11 +110,14 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||
).execute()
|
||||
}
|
||||
|
||||
fun addOrUpdateDatabaseUri(databaseUri: Uri, keyFileUri: Uri? = null,
|
||||
fun addOrUpdateDatabaseUri(databaseUri: Uri,
|
||||
keyFileUri: Uri? = null,
|
||||
hardwareKey: HardwareKey? = null,
|
||||
databaseFileAddedOrUpdatedResult: ((DatabaseFile?) -> Unit)? = null) {
|
||||
addOrUpdateDatabaseFile(DatabaseFile(
|
||||
databaseUri,
|
||||
keyFileUri
|
||||
databaseUri,
|
||||
keyFileUri,
|
||||
hardwareKey
|
||||
), databaseFileAddedOrUpdatedResult)
|
||||
}
|
||||
|
||||
@@ -130,6 +136,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||
?: fileDatabaseHistoryRetrieve?.databaseAlias
|
||||
?: "",
|
||||
databaseFileToAddOrUpdate.keyFileUri?.toString(),
|
||||
databaseFileToAddOrUpdate.hardwareKey?.value,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
|
||||
@@ -147,13 +154,14 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||
val fileDatabaseInfo = FileDatabaseInfo(applicationContext,
|
||||
fileDatabaseHistory.databaseUri)
|
||||
DatabaseFile(
|
||||
UriUtil.parse(fileDatabaseHistory.databaseUri),
|
||||
UriUtil.parse(fileDatabaseHistory.keyFileUri),
|
||||
UriUtil.decode(fileDatabaseHistory.databaseUri),
|
||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
|
||||
fileDatabaseInfo.exists,
|
||||
fileDatabaseInfo.getLastModificationString(),
|
||||
fileDatabaseInfo.getSizeString()
|
||||
UriUtil.parse(fileDatabaseHistory.databaseUri),
|
||||
UriUtil.parse(fileDatabaseHistory.keyFileUri),
|
||||
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
|
||||
UriUtil.decode(fileDatabaseHistory.databaseUri),
|
||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
|
||||
fileDatabaseInfo.exists,
|
||||
fileDatabaseInfo.getLastModificationString(),
|
||||
fileDatabaseInfo.getSizeString()
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -172,10 +180,11 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
||||
val returnValue = databaseFileHistoryDao.delete(fileDatabaseHistory)
|
||||
if (returnValue > 0) {
|
||||
DatabaseFile(
|
||||
UriUtil.parse(fileDatabaseHistory.databaseUri),
|
||||
UriUtil.parse(fileDatabaseHistory.keyFileUri),
|
||||
UriUtil.decode(fileDatabaseHistory.databaseUri),
|
||||
databaseFileToDelete.databaseAlias
|
||||
UriUtil.parse(fileDatabaseHistory.databaseUri),
|
||||
UriUtil.parse(fileDatabaseHistory.keyFileUri),
|
||||
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
|
||||
UriUtil.decode(fileDatabaseHistory.databaseUri),
|
||||
databaseFileToDelete.databaseAlias
|
||||
)
|
||||
} else {
|
||||
null
|
||||
|
||||
@@ -35,6 +35,9 @@ data class FileDatabaseHistoryEntity(
|
||||
@ColumnInfo(name = "keyfile_uri")
|
||||
var keyFileUri: String?,
|
||||
|
||||
@ColumnInfo(name = "hardware_key")
|
||||
var hardwareKey: String?,
|
||||
|
||||
@ColumnInfo(name = "updated")
|
||||
val updated: Long
|
||||
) {
|
||||
|
||||
@@ -34,12 +34,10 @@ import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.getkeepsafe.taptargetview.TapTargetView
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.stylish.StylishFragment
|
||||
import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.database.exception.IODatabaseException
|
||||
import com.kunzisoft.keepass.education.PasswordActivityEducation
|
||||
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
|
||||
import com.kunzisoft.keepass.model.CipherDecryptDatabase
|
||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||
import com.kunzisoft.keepass.model.CredentialStorage
|
||||
@@ -398,7 +396,7 @@ class AdvancedUnlockFragment: StylishFragment(), AdvancedUnlockManager.AdvancedU
|
||||
}
|
||||
} ?: deleteEncryptedDatabaseKey()
|
||||
}
|
||||
} ?: throw IODatabaseException()
|
||||
} ?: throw UnknownDatabaseLocationException()
|
||||
} ?: 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.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.database.element.MainCredential
|
||||
|
||||
open class AssignMainCredentialInDatabaseRunnable (
|
||||
context: Context,
|
||||
database: Database,
|
||||
protected val mDatabaseUri: Uri,
|
||||
protected val mMainCredential: MainCredential)
|
||||
: SaveDatabaseRunnable(context, database, true) {
|
||||
context: Context,
|
||||
database: Database,
|
||||
protected val mDatabaseUri: Uri,
|
||||
mainCredential: MainCredential,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: SaveDatabaseRunnable(context, database, true, mainCredential, challengeResponseRetriever) {
|
||||
|
||||
private var mBackupKey: ByteArray? = null
|
||||
|
||||
@@ -40,10 +41,7 @@ open class AssignMainCredentialInDatabaseRunnable (
|
||||
// Set key
|
||||
try {
|
||||
mBackupKey = ByteArray(database.masterKey.size)
|
||||
System.arraycopy(database.masterKey, 0, mBackupKey!!, 0, mBackupKey!!.size)
|
||||
|
||||
val uriInputStream = UriUtil.getUriInputStream(context.contentResolver, mMainCredential.keyFileUri)
|
||||
database.assignMasterKey(mMainCredential.masterPassword, uriInputStream)
|
||||
database.masterKey.copyInto(mBackupKey!!)
|
||||
} catch (e: Exception) {
|
||||
erase(mBackupKey)
|
||||
setError(e)
|
||||
|
||||
@@ -24,7 +24,8 @@ import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
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
|
||||
|
||||
class CreateDatabaseRunnable(context: Context,
|
||||
@@ -33,9 +34,10 @@ class CreateDatabaseRunnable(context: Context,
|
||||
private val databaseName: String,
|
||||
private val rootName: String,
|
||||
private val templateGroupName: String?,
|
||||
mainCredential: MainCredential,
|
||||
val mainCredential: MainCredential,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
|
||||
private val createDatabaseResult: ((Result) -> Unit)?)
|
||||
: AssignMainCredentialInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential) {
|
||||
: AssignMainCredentialInDatabaseRunnable(context, mDatabase, databaseUri, mainCredential, challengeResponseRetriever) {
|
||||
|
||||
override fun onStartRun() {
|
||||
try {
|
||||
@@ -58,8 +60,11 @@ class CreateDatabaseRunnable(context: Context,
|
||||
// Add database to recent files
|
||||
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
||||
FileDatabaseHistoryAction.getInstance(context.applicationContext)
|
||||
.addOrUpdateDatabaseUri(mDatabaseUri,
|
||||
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null)
|
||||
.addOrUpdateDatabaseUri(
|
||||
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
|
||||
|
||||
@@ -38,12 +38,16 @@ import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
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.MainCredential
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.database.exception.InvalidCredentialsDatabaseException
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.hardware.HardwareKeyResponseHelper
|
||||
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.services.DatabaseTaskNotificationService
|
||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
|
||||
@@ -82,6 +86,7 @@ import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
|
||||
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
|
||||
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
|
||||
import com.kunzisoft.keepass.viewmodels.ChallengeResponseViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
|
||||
@@ -92,7 +97,6 @@ import java.util.*
|
||||
class DatabaseTaskProvider {
|
||||
|
||||
private var activity: FragmentActivity? = null
|
||||
private var service: Service? = null
|
||||
private var context: Context
|
||||
|
||||
var onDatabaseRetrieved: ((database: Database?) -> Unit)? = null
|
||||
@@ -111,30 +115,80 @@ class DatabaseTaskProvider {
|
||||
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
|
||||
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
|
||||
|
||||
constructor(activity: FragmentActivity) {
|
||||
private var mChallengeResponseViewModel: ChallengeResponseViewModel? = null
|
||||
|
||||
constructor(activity: FragmentActivity,
|
||||
challengeResponseViewModel: ChallengeResponseViewModel) {
|
||||
this.activity = activity
|
||||
this.context = activity
|
||||
this.intentDatabaseTask = Intent(activity.applicationContext,
|
||||
DatabaseTaskNotificationService::class.java)
|
||||
|
||||
// ViewModel used to keep response if activity recreated
|
||||
this.mChallengeResponseViewModel = challengeResponseViewModel
|
||||
// To manage hardware key challenge response
|
||||
val hardwareKeyResponseHelper = HardwareKeyResponseHelper(activity)
|
||||
hardwareKeyResponseHelper.buildHardwareKeyResponse { responseData, _ ->
|
||||
// TODO Verify database
|
||||
// Send to view model in case activity is restarted and not yet connected to service
|
||||
challengeResponseViewModel.respond(responseData ?: ByteArray(0))
|
||||
}
|
||||
challengeResponseViewModel.dataResponded.observe(activity) { response ->
|
||||
// Consume the response
|
||||
if (response != null) {
|
||||
val binder = mBinder
|
||||
if (binder != null) {
|
||||
binder.getService().respondToChallenge(response)
|
||||
challengeResponseViewModel.consumeResponse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.requestChallengeListener = object: DatabaseTaskNotificationService.RequestChallengeListener {
|
||||
override fun onChallengeResponseRequested(hardwareKey: HardwareKey, seed: ByteArray?) {
|
||||
if (HardwareKeyResponseHelper.isHardwareKeyAvailable(activity, hardwareKey)) {
|
||||
hardwareKeyResponseHelper.launchChallengeForResponse(hardwareKey, seed)
|
||||
} else {
|
||||
throw InvalidCredentialsDatabaseException(
|
||||
context.getString(R.string.error_driver_required, hardwareKey.toString())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(service: Service) {
|
||||
this.service = service
|
||||
this.context = service
|
||||
this.intentDatabaseTask = Intent(service.applicationContext,
|
||||
DatabaseTaskNotificationService::class.java)
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
this.activity = null
|
||||
this.onDatabaseRetrieved = null
|
||||
this.onActionFinish = null
|
||||
this.databaseTaskBroadcastReceiver = null
|
||||
this.mBinder = null
|
||||
this.serviceConnection = null
|
||||
this.progressTaskDialogFragment = null
|
||||
this.databaseChangedDialogFragment = null
|
||||
this.mChallengeResponseViewModel = null
|
||||
}
|
||||
|
||||
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
|
||||
override fun onStartAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||
startDialog(titleId, messageId, warningId)
|
||||
override fun onStartAction(database: Database,
|
||||
progressMessage: ProgressMessage) {
|
||||
startDialog(progressMessage)
|
||||
}
|
||||
|
||||
override fun onUpdateAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?) {
|
||||
updateDialog(titleId, messageId, warningId)
|
||||
override fun onUpdateAction(database: Database,
|
||||
progressMessage: ProgressMessage) {
|
||||
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)
|
||||
// Remove the progress task
|
||||
stopDialog()
|
||||
@@ -181,9 +235,9 @@ class DatabaseTaskProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private fun startDialog(titleId: Int? = null,
|
||||
messageId: Int? = null,
|
||||
warningId: Int? = null) {
|
||||
private var requestChallengeListener: DatabaseTaskNotificationService.RequestChallengeListener? = null
|
||||
|
||||
private fun startDialog(progressMessage: ProgressMessage) {
|
||||
activity?.let { activity ->
|
||||
activity.lifecycleScope.launch {
|
||||
if (progressTaskDialogFragment == null) {
|
||||
@@ -197,22 +251,17 @@ class DatabaseTaskProvider {
|
||||
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 {
|
||||
titleId?.let {
|
||||
updateTitle(it)
|
||||
}
|
||||
messageId?.let {
|
||||
updateMessage(it)
|
||||
}
|
||||
warningId?.let {
|
||||
updateWarning(it)
|
||||
}
|
||||
updateTitle(progressMessage.titleId)
|
||||
updateMessage(progressMessage.messageId)
|
||||
updateWarning(progressMessage.warningId)
|
||||
setCancellable(progressMessage.cancelable)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,25 +275,38 @@ class DatabaseTaskProvider {
|
||||
serviceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
|
||||
addDatabaseListener(databaseListener)
|
||||
addDatabaseFileInfoListener(databaseInfoListener)
|
||||
addActionTaskListener(actionTaskListener)
|
||||
addServiceListeners(this)
|
||||
getService().checkDatabase()
|
||||
getService().checkDatabaseInfo()
|
||||
getService().checkAction()
|
||||
}
|
||||
mChallengeResponseViewModel?.resendResponse()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
mBinder?.removeActionTaskListener(actionTaskListener)
|
||||
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
|
||||
mBinder?.removeDatabaseListener(databaseListener)
|
||||
removeServiceListeners(mBinder)
|
||||
mBinder = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
|
||||
service?.addDatabaseListener(databaseListener)
|
||||
service?.addDatabaseFileInfoListener(databaseInfoListener)
|
||||
service?.addActionTaskListener(actionTaskListener)
|
||||
requestChallengeListener?.let {
|
||||
service?.addRequestChallengeListener(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
|
||||
service?.removeActionTaskListener(actionTaskListener)
|
||||
service?.removeDatabaseFileInfoListener(databaseInfoListener)
|
||||
service?.removeDatabaseListener(databaseListener)
|
||||
service?.removeRequestChallengeListener()
|
||||
}
|
||||
|
||||
private fun bindService() {
|
||||
initServiceConnection()
|
||||
serviceConnection?.let {
|
||||
@@ -262,10 +324,6 @@ class DatabaseTaskProvider {
|
||||
serviceConnection = null
|
||||
}
|
||||
|
||||
fun isBinded(): Boolean {
|
||||
return mBinder != null
|
||||
}
|
||||
|
||||
fun registerProgressTask() {
|
||||
stopDialog()
|
||||
|
||||
@@ -299,9 +357,7 @@ class DatabaseTaskProvider {
|
||||
fun unregisterProgressTask() {
|
||||
stopDialog()
|
||||
|
||||
mBinder?.removeActionTaskListener(actionTaskListener)
|
||||
mBinder?.removeDatabaseFileInfoListener(databaseInfoListener)
|
||||
mBinder?.removeDatabaseListener(databaseListener)
|
||||
removeServiceListeners(mBinder)
|
||||
mBinder = null
|
||||
|
||||
unBindService()
|
||||
@@ -321,7 +377,7 @@ class DatabaseTaskProvider {
|
||||
context.startService(intentDatabaseTask)
|
||||
} catch (e: Exception) {
|
||||
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 +388,8 @@ class DatabaseTaskProvider {
|
||||
*/
|
||||
|
||||
fun startDatabaseCreate(databaseUri: Uri,
|
||||
mainCredential: MainCredential) {
|
||||
mainCredential: MainCredential
|
||||
) {
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||
@@ -385,7 +442,8 @@ class DatabaseTaskProvider {
|
||||
}
|
||||
|
||||
fun startDatabaseAssignPassword(databaseUri: Uri,
|
||||
mainCredential: MainCredential) {
|
||||
mainCredential: MainCredential
|
||||
) {
|
||||
|
||||
start(Bundle().apply {
|
||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||
|
||||
@@ -25,9 +25,10 @@ import com.kunzisoft.keepass.app.database.CipherDatabaseAction
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
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.MainCredential
|
||||
import com.kunzisoft.keepass.database.element.MainCredential
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
@@ -35,8 +36,9 @@ import com.kunzisoft.keepass.utils.UriUtil
|
||||
|
||||
class LoadDatabaseRunnable(private val context: Context,
|
||||
private val mDatabase: Database,
|
||||
private val mUri: Uri,
|
||||
private val mDatabaseUri: Uri,
|
||||
private val mMainCredential: MainCredential,
|
||||
private val mChallengeResponseRetriever: (hardwareKey: HardwareKey, seed: ByteArray?) -> ByteArray,
|
||||
private val mReadonly: Boolean,
|
||||
private val mCipherEncryptDatabase: CipherEncryptDatabase?,
|
||||
private val mFixDuplicateUUID: Boolean,
|
||||
@@ -51,18 +53,21 @@ class LoadDatabaseRunnable(private val context: Context,
|
||||
|
||||
override fun onActionRun() {
|
||||
try {
|
||||
mDatabase.loadData(mUri,
|
||||
mMainCredential,
|
||||
mReadonly,
|
||||
context.contentResolver,
|
||||
UriUtil.getBinaryDir(context),
|
||||
{ memoryWanted ->
|
||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||
},
|
||||
mFixDuplicateUUID,
|
||||
progressTaskUpdater)
|
||||
mDatabase.loadData(
|
||||
context.contentResolver,
|
||||
mDatabaseUri,
|
||||
mMainCredential,
|
||||
mChallengeResponseRetriever,
|
||||
mReadonly,
|
||||
UriUtil.getBinaryDir(context),
|
||||
{ memoryWanted ->
|
||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||
},
|
||||
mFixDuplicateUUID,
|
||||
progressTaskUpdater
|
||||
)
|
||||
}
|
||||
catch (e: LoadDatabaseException) {
|
||||
catch (e: DatabaseInputException) {
|
||||
setError(e)
|
||||
}
|
||||
|
||||
@@ -70,8 +75,11 @@ class LoadDatabaseRunnable(private val context: Context,
|
||||
// Save keyFile in app database
|
||||
if (PreferencesUtil.rememberDatabaseLocations(context)) {
|
||||
FileDatabaseHistoryAction.getInstance(context)
|
||||
.addOrUpdateDatabaseUri(mUri,
|
||||
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null)
|
||||
.addOrUpdateDatabaseUri(
|
||||
mDatabaseUri,
|
||||
if (PreferencesUtil.rememberKeyFileLocations(context)) mMainCredential.keyFileUri else null,
|
||||
if (PreferencesUtil.rememberHardwareKey(context)) mMainCredential.hardwareKey else null,
|
||||
)
|
||||
}
|
||||
|
||||
// Register the biometric
|
||||
|
||||
@@ -22,9 +22,10 @@ package com.kunzisoft.keepass.database.action
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
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.exception.LoadDatabaseException
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.database.exception.DatabaseException
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
@@ -33,6 +34,7 @@ class MergeDatabaseRunnable(private val context: Context,
|
||||
private val mDatabase: Database,
|
||||
private val mDatabaseToMergeUri: Uri?,
|
||||
private val mDatabaseToMergeMainCredential: MainCredential?,
|
||||
private val mDatabaseToMergeChallengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
|
||||
private val progressTaskUpdater: ProgressTaskUpdater?,
|
||||
private val mLoadDatabaseResult: ((Result) -> Unit)?)
|
||||
: ActionRunnable() {
|
||||
@@ -43,15 +45,17 @@ class MergeDatabaseRunnable(private val context: Context,
|
||||
|
||||
override fun onActionRun() {
|
||||
try {
|
||||
mDatabase.mergeData(mDatabaseToMergeUri,
|
||||
mDatabaseToMergeMainCredential,
|
||||
mDatabase.mergeData(
|
||||
context.contentResolver,
|
||||
mDatabaseToMergeUri,
|
||||
mDatabaseToMergeMainCredential,
|
||||
mDatabaseToMergeChallengeResponseRetriever,
|
||||
{ memoryWanted ->
|
||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||
},
|
||||
progressTaskUpdater
|
||||
)
|
||||
} catch (e: LoadDatabaseException) {
|
||||
} catch (e: DatabaseException) {
|
||||
setError(e)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ package com.kunzisoft.keepass.database.action
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
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.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
@@ -47,7 +47,7 @@ class ReloadDatabaseRunnable(private val context: Context,
|
||||
BinaryData.canMemoryBeAllocatedInRAM(context, memoryWanted)
|
||||
},
|
||||
progressTaskUpdater)
|
||||
} catch (e: LoadDatabaseException) {
|
||||
} catch (e: DatabaseException) {
|
||||
setError(e)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,12 +21,14 @@ package com.kunzisoft.keepass.database.action
|
||||
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
class RemoveUnlinkedDataDatabaseRunnable (
|
||||
context: Context,
|
||||
database: Database,
|
||||
saveDatabase: Boolean)
|
||||
: SaveDatabaseRunnable(context, database, saveDatabase) {
|
||||
saveDatabase: Boolean,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: SaveDatabaseRunnable(context, database, saveDatabase, null, challengeResponseRetriever) {
|
||||
|
||||
override fun onActionRun() {
|
||||
try {
|
||||
|
||||
@@ -23,11 +23,15 @@ import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
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
|
||||
|
||||
open class SaveDatabaseRunnable(protected var context: Context,
|
||||
protected var database: Database,
|
||||
private var saveDatabase: Boolean,
|
||||
private var mainCredential: MainCredential?, // If null, uses composite Key
|
||||
private var challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
|
||||
private var databaseCopyUri: Uri? = null)
|
||||
: ActionRunnable() {
|
||||
|
||||
@@ -39,7 +43,12 @@ open class SaveDatabaseRunnable(protected var context: Context,
|
||||
database.checkVersion()
|
||||
if (saveDatabase && result.isSuccess) {
|
||||
try {
|
||||
database.saveData(databaseCopyUri, context.contentResolver)
|
||||
database.saveData(
|
||||
context.contentResolver,
|
||||
context.cacheDir,
|
||||
databaseCopyUri,
|
||||
mainCredential,
|
||||
challengeResponseRetriever)
|
||||
} catch (e: DatabaseException) {
|
||||
setError(e)
|
||||
}
|
||||
|
||||
@@ -22,14 +22,16 @@ package com.kunzisoft.keepass.database.action
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
class UpdateCompressionBinariesDatabaseRunnable (
|
||||
context: Context,
|
||||
database: Database,
|
||||
private val oldCompressionAlgorithm: CompressionAlgorithm,
|
||||
private val newCompressionAlgorithm: CompressionAlgorithm,
|
||||
saveDatabase: Boolean)
|
||||
: SaveDatabaseRunnable(context, database, saveDatabase) {
|
||||
saveDatabase: Boolean,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: SaveDatabaseRunnable(context, database, saveDatabase, null, challengeResponseRetriever) {
|
||||
|
||||
override fun onStartRun() {
|
||||
// Set new compression
|
||||
|
||||
@@ -23,14 +23,16 @@ import android.content.Context
|
||||
import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
class DeleteEntryHistoryDatabaseRunnable (
|
||||
context: Context,
|
||||
database: Database,
|
||||
private val mainEntry: Entry,
|
||||
private val entryHistoryPosition: Int,
|
||||
saveDatabase: Boolean)
|
||||
: SaveDatabaseRunnable(context, database, saveDatabase) {
|
||||
saveDatabase: Boolean,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: SaveDatabaseRunnable(context, database, saveDatabase, null, challengeResponseRetriever) {
|
||||
|
||||
override fun onStartRun() {
|
||||
try {
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.content.Context
|
||||
import com.kunzisoft.keepass.database.action.node.UpdateEntryRunnable
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.database.element.Entry
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||
|
||||
class RestoreEntryHistoryDatabaseRunnable (
|
||||
@@ -30,7 +31,8 @@ class RestoreEntryHistoryDatabaseRunnable (
|
||||
private val database: Database,
|
||||
private val mainEntry: Entry,
|
||||
private val entryHistoryPosition: Int,
|
||||
private val saveDatabase: Boolean)
|
||||
private val saveDatabase: Boolean,
|
||||
private val challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: ActionRunnable() {
|
||||
|
||||
private var updateEntryRunnable: UpdateEntryRunnable? = null
|
||||
@@ -43,12 +45,15 @@ class RestoreEntryHistoryDatabaseRunnable (
|
||||
historyToRestore.addEntryToHistory(it)
|
||||
}
|
||||
// Update the entry with the fresh formatted entry to restore
|
||||
updateEntryRunnable = UpdateEntryRunnable(context,
|
||||
database,
|
||||
mainEntry,
|
||||
historyToRestore,
|
||||
saveDatabase,
|
||||
null)
|
||||
updateEntryRunnable = UpdateEntryRunnable(
|
||||
context,
|
||||
database,
|
||||
mainEntry,
|
||||
historyToRestore,
|
||||
saveDatabase,
|
||||
null,
|
||||
challengeResponseRetriever
|
||||
)
|
||||
|
||||
updateEntryRunnable?.onStartRun()
|
||||
|
||||
|
||||
@@ -22,13 +22,15 @@ package com.kunzisoft.keepass.database.action.node
|
||||
import android.content.Context
|
||||
import com.kunzisoft.keepass.database.action.SaveDatabaseRunnable
|
||||
import com.kunzisoft.keepass.database.element.Database
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
abstract class ActionNodeDatabaseRunnable(
|
||||
context: Context,
|
||||
database: Database,
|
||||
private val afterActionNodesFinish: AfterActionNodesFinish?,
|
||||
save: Boolean)
|
||||
: SaveDatabaseRunnable(context, database, save) {
|
||||
save: Boolean,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: SaveDatabaseRunnable(context, database, save, null, challengeResponseRetriever) {
|
||||
|
||||
/**
|
||||
* 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.Group
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
class AddEntryRunnable constructor(
|
||||
context: Context,
|
||||
@@ -31,8 +32,9 @@ class AddEntryRunnable constructor(
|
||||
private val mNewEntry: Entry,
|
||||
private val mParent: Group,
|
||||
save: Boolean,
|
||||
afterActionNodesFinish: AfterActionNodesFinish?)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||
afterActionNodesFinish: AfterActionNodesFinish?,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
|
||||
|
||||
override fun nodeAction() {
|
||||
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.Group
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
class AddGroupRunnable constructor(
|
||||
context: Context,
|
||||
@@ -30,8 +31,9 @@ class AddGroupRunnable constructor(
|
||||
private val mNewGroup: Group,
|
||||
private val mParent: Group,
|
||||
save: Boolean,
|
||||
afterActionNodesFinish: AfterActionNodesFinish?)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||
afterActionNodesFinish: AfterActionNodesFinish?,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
|
||||
|
||||
override fun nodeAction() {
|
||||
mNewGroup.touch(modified = true, touchParents = true)
|
||||
|
||||
@@ -21,11 +21,14 @@ package com.kunzisoft.keepass.database.action.node
|
||||
|
||||
import android.content.Context
|
||||
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.Type
|
||||
import com.kunzisoft.keepass.database.exception.CopyEntryDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.CopyGroupDatabaseException
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
class CopyNodesRunnable constructor(
|
||||
context: Context,
|
||||
@@ -33,8 +36,9 @@ class CopyNodesRunnable constructor(
|
||||
private val mNodesToCopy: List<Node>,
|
||||
private val mNewParent: Group,
|
||||
save: Boolean,
|
||||
afterActionNodesFinish: AfterActionNodesFinish?)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||
afterActionNodesFinish: AfterActionNodesFinish?,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
|
||||
|
||||
private var mEntriesCopied = ArrayList<Entry>()
|
||||
|
||||
|
||||
@@ -20,16 +20,20 @@
|
||||
package com.kunzisoft.keepass.database.action.node
|
||||
|
||||
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.Type
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
class DeleteNodesRunnable(context: Context,
|
||||
database: Database,
|
||||
private val mNodesToDelete: List<Node>,
|
||||
save: Boolean,
|
||||
afterActionNodesFinish: AfterActionNodesFinish)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||
afterActionNodesFinish: AfterActionNodesFinish,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
|
||||
|
||||
private var mOldParent: Group? = null
|
||||
private var mCanRecycle: Boolean = false
|
||||
|
||||
@@ -21,11 +21,14 @@ package com.kunzisoft.keepass.database.action.node
|
||||
|
||||
import android.content.Context
|
||||
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.Type
|
||||
import com.kunzisoft.keepass.database.exception.MoveEntryDatabaseException
|
||||
import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
class MoveNodesRunnable constructor(
|
||||
context: Context,
|
||||
@@ -33,8 +36,9 @@ class MoveNodesRunnable constructor(
|
||||
private val mNodesToMove: List<Node>,
|
||||
private val mNewParent: Group,
|
||||
save: Boolean,
|
||||
afterActionNodesFinish: AfterActionNodesFinish?)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||
afterActionNodesFinish: AfterActionNodesFinish?,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
|
||||
|
||||
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.Entry
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
class UpdateEntryRunnable constructor(
|
||||
context: Context,
|
||||
@@ -31,8 +32,9 @@ class UpdateEntryRunnable constructor(
|
||||
private val mOldEntry: Entry,
|
||||
private val mNewEntry: Entry,
|
||||
save: Boolean,
|
||||
afterActionNodesFinish: AfterActionNodesFinish?)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||
afterActionNodesFinish: AfterActionNodesFinish?,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
|
||||
|
||||
override fun nodeAction() {
|
||||
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.Group
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
class UpdateGroupRunnable constructor(
|
||||
context: Context,
|
||||
@@ -30,8 +31,9 @@ class UpdateGroupRunnable constructor(
|
||||
private val mOldGroup: Group,
|
||||
private val mNewGroup: Group,
|
||||
save: Boolean,
|
||||
afterActionNodesFinish: AfterActionNodesFinish?)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save) {
|
||||
afterActionNodesFinish: AfterActionNodesFinish?,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray)
|
||||
: ActionNodeDatabaseRunnable(context, database, afterActionNodesFinish, save, challengeResponseRetriever) {
|
||||
|
||||
override fun nodeAction() {
|
||||
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.search.SearchHelper
|
||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.icons.IconDrawableFactory
|
||||
import com.kunzisoft.keepass.model.MainCredential
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
import com.kunzisoft.keepass.utils.SingletonHolder
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.utils.readBytes4ToUInt
|
||||
import com.kunzisoft.keepass.utils.*
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
|
||||
@@ -73,7 +71,7 @@ class Database {
|
||||
var fileUri: Uri? = null
|
||||
private set
|
||||
|
||||
private var mSearchHelper: SearchHelper? = null
|
||||
private var mSearchHelper: SearchHelper = SearchHelper()
|
||||
|
||||
var isReadOnly = false
|
||||
|
||||
@@ -384,10 +382,14 @@ class Database {
|
||||
set(masterKey) {
|
||||
mDatabaseKDB?.masterKey = masterKey
|
||||
mDatabaseKDBX?.masterKey = masterKey
|
||||
mDatabaseKDBX?.keyLastChanged = DateInstant()
|
||||
mDatabaseKDBX?.settingsChanged = DateInstant()
|
||||
dataModifiedSinceLastLoading = true
|
||||
}
|
||||
|
||||
val transformSeed: ByteArray?
|
||||
get() = mDatabaseKDB?.transformSeed ?: mDatabaseKDBX?.transformSeed
|
||||
|
||||
var rootGroup: Group?
|
||||
get() {
|
||||
mDatabaseKDB?.rootGroup?.let {
|
||||
@@ -557,79 +559,28 @@ class Database {
|
||||
this.dataModifiedSinceLastLoading = false
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
private fun readDatabaseStream(contentResolver: ContentResolver, uri: Uri,
|
||||
openDatabaseKDB: (InputStream) -> DatabaseKDB,
|
||||
openDatabaseKDBX: (InputStream) -> DatabaseKDBX) {
|
||||
var databaseInputStream: InputStream? = null
|
||||
try {
|
||||
// Load Data, pass Uris as InputStreams
|
||||
val databaseStream = UriUtil.getUriInputStream(contentResolver, uri)
|
||||
?: throw IOException("Database input stream cannot be retrieve")
|
||||
|
||||
databaseInputStream = BufferedInputStream(databaseStream)
|
||||
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?) {
|
||||
@Throws(DatabaseInputException::class)
|
||||
fun loadData(
|
||||
contentResolver: ContentResolver,
|
||||
databaseUri: Uri,
|
||||
mainCredential: MainCredential,
|
||||
challengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
|
||||
readOnly: Boolean,
|
||||
cacheDirectory: File,
|
||||
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||
fixDuplicateUUID: Boolean,
|
||||
progressTaskUpdater: ProgressTaskUpdater?
|
||||
) {
|
||||
|
||||
// Save database URI
|
||||
this.fileUri = uri
|
||||
this.fileUri = databaseUri
|
||||
|
||||
// Check if the file is writable
|
||||
this.isReadOnly = readOnly
|
||||
|
||||
// Pass KeyFile Uri as InputStreams
|
||||
var keyFileInputStream: InputStream? = null
|
||||
try {
|
||||
// Get keyFile inputStream
|
||||
mainCredential.keyFileUri?.let { keyFile ->
|
||||
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyFile)
|
||||
}
|
||||
|
||||
// Read database stream for the first time
|
||||
readDatabaseStream(contentResolver, uri,
|
||||
readDatabaseStream(contentResolver, databaseUri,
|
||||
{ databaseInputStream ->
|
||||
val databaseKDB = DatabaseKDB().apply {
|
||||
binaryCache.cacheDirectory = cacheDirectory
|
||||
@@ -639,12 +590,12 @@ class Database {
|
||||
.openDatabase(databaseInputStream,
|
||||
progressTaskUpdater
|
||||
) {
|
||||
databaseKDB.retrieveMasterKey(
|
||||
mainCredential.masterPassword,
|
||||
keyFileInputStream
|
||||
databaseKDB.deriveMasterKey(
|
||||
contentResolver,
|
||||
mainCredential
|
||||
)
|
||||
}
|
||||
databaseKDB
|
||||
setDatabaseKDB(databaseKDB)
|
||||
},
|
||||
{ databaseInputStream ->
|
||||
val databaseKDBX = DatabaseKDBX().apply {
|
||||
@@ -655,23 +606,23 @@ class Database {
|
||||
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
|
||||
openDatabase(databaseInputStream,
|
||||
progressTaskUpdater) {
|
||||
databaseKDBX.retrieveMasterKey(
|
||||
mainCredential.masterPassword,
|
||||
keyFileInputStream,
|
||||
databaseKDBX.deriveMasterKey(
|
||||
contentResolver,
|
||||
mainCredential,
|
||||
challengeResponseRetriever
|
||||
)
|
||||
}
|
||||
}
|
||||
databaseKDBX
|
||||
setDatabaseKDBX(databaseKDBX)
|
||||
}
|
||||
)
|
||||
} catch (e: FileNotFoundException) {
|
||||
throw FileNotFoundDatabaseException("Unable to load the keyfile")
|
||||
} catch (e: LoadDatabaseException) {
|
||||
throw e
|
||||
loaded = true
|
||||
} catch (e: Exception) {
|
||||
throw LoadDatabaseException(e)
|
||||
Log.e(TAG, "Unable to load the database")
|
||||
if (e is DatabaseInputException)
|
||||
throw e
|
||||
throw DatabaseInputException(e)
|
||||
} finally {
|
||||
keyFileInputStream?.close()
|
||||
dataModifiedSinceLastLoading = false
|
||||
}
|
||||
}
|
||||
@@ -680,48 +631,44 @@ class Database {
|
||||
return mDatabaseKDBX != null
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
fun mergeData(databaseToMergeUri: Uri?,
|
||||
databaseToMergeMainCredential: MainCredential?,
|
||||
contentResolver: ContentResolver,
|
||||
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||
@Throws(DatabaseInputException::class)
|
||||
fun mergeData(
|
||||
contentResolver: ContentResolver,
|
||||
databaseToMergeUri: Uri?,
|
||||
databaseToMergeMainCredential: MainCredential?,
|
||||
databaseToMergeChallengeResponseRetriever: (HardwareKey, ByteArray?) -> ByteArray,
|
||||
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||
progressTaskUpdater: ProgressTaskUpdater?
|
||||
) {
|
||||
|
||||
mDatabaseKDB?.let {
|
||||
throw IODatabaseException("Unable to merge from a database V1")
|
||||
throw MergeDatabaseKDBException()
|
||||
}
|
||||
|
||||
// New database instance to get new changes
|
||||
val databaseToMerge = Database()
|
||||
databaseToMerge.fileUri = databaseToMergeUri ?: this.fileUri
|
||||
|
||||
// Pass KeyFile Uri as InputStreams
|
||||
var keyFileInputStream: InputStream? = null
|
||||
try {
|
||||
val databaseUri = databaseToMerge.fileUri
|
||||
if (databaseUri != null) {
|
||||
if (databaseToMergeMainCredential != null) {
|
||||
// Get keyFile inputStream
|
||||
databaseToMergeMainCredential.keyFileUri?.let { keyFile ->
|
||||
keyFileInputStream = UriUtil.getUriInputStream(contentResolver, keyFile)
|
||||
}
|
||||
}
|
||||
|
||||
databaseToMerge.readDatabaseStream(contentResolver, databaseUri,
|
||||
readDatabaseStream(contentResolver, databaseUri,
|
||||
{ databaseInputStream ->
|
||||
val databaseToMergeKDB = DatabaseKDB()
|
||||
DatabaseInputKDB(databaseToMergeKDB)
|
||||
.openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||
if (databaseToMergeMainCredential != null) {
|
||||
databaseToMergeKDB.retrieveMasterKey(
|
||||
databaseToMergeMainCredential.masterPassword,
|
||||
keyFileInputStream,
|
||||
databaseToMergeKDB.deriveMasterKey(
|
||||
contentResolver,
|
||||
databaseToMergeMainCredential
|
||||
)
|
||||
} else {
|
||||
databaseToMergeKDB.masterKey = masterKey
|
||||
this@Database.mDatabaseKDB?.let { thisDatabaseKDB ->
|
||||
databaseToMergeKDB.copyMasterKeyFrom(thisDatabaseKDB)
|
||||
}
|
||||
}
|
||||
}
|
||||
databaseToMergeKDB
|
||||
databaseToMerge.setDatabaseKDB(databaseToMergeKDB)
|
||||
},
|
||||
{ databaseInputStream ->
|
||||
val databaseToMergeKDBX = DatabaseKDBX()
|
||||
@@ -729,18 +676,22 @@ class Database {
|
||||
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
|
||||
openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||
if (databaseToMergeMainCredential != null) {
|
||||
databaseToMergeKDBX.retrieveMasterKey(
|
||||
databaseToMergeMainCredential.masterPassword,
|
||||
keyFileInputStream,
|
||||
databaseToMergeKDBX.deriveMasterKey(
|
||||
contentResolver,
|
||||
databaseToMergeMainCredential,
|
||||
databaseToMergeChallengeResponseRetriever
|
||||
)
|
||||
} else {
|
||||
databaseToMergeKDBX.masterKey = masterKey
|
||||
this@Database.mDatabaseKDBX?.let { thisDatabaseKDBX ->
|
||||
databaseToMergeKDBX.copyMasterKeyFrom(thisDatabaseKDBX)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
databaseToMergeKDBX
|
||||
databaseToMerge.setDatabaseKDBX(databaseToMergeKDBX)
|
||||
}
|
||||
)
|
||||
loaded = true
|
||||
|
||||
mDatabaseKDBX?.let { currentDatabaseKDBX ->
|
||||
val databaseMerger = DatabaseKDBXMerger(currentDatabaseKDBX).apply {
|
||||
@@ -760,24 +711,24 @@ class Database {
|
||||
}
|
||||
}
|
||||
} 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) {
|
||||
throw LoadDatabaseException(e)
|
||||
Log.e(TAG, "Unable to merge the database")
|
||||
if (e is DatabaseException)
|
||||
throw e
|
||||
throw DatabaseInputException(e)
|
||||
} finally {
|
||||
keyFileInputStream?.close()
|
||||
databaseToMerge.clearAndClose()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
fun reloadData(contentResolver: ContentResolver,
|
||||
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||
progressTaskUpdater: ProgressTaskUpdater?) {
|
||||
@Throws(DatabaseInputException::class)
|
||||
fun reloadData(
|
||||
contentResolver: ContentResolver,
|
||||
isRAMSufficient: (memoryWanted: Long) -> Boolean,
|
||||
progressTaskUpdater: ProgressTaskUpdater?
|
||||
) {
|
||||
|
||||
// Retrieve the stream from the old database URI
|
||||
try {
|
||||
@@ -791,9 +742,11 @@ class Database {
|
||||
}
|
||||
DatabaseInputKDB(databaseKDB)
|
||||
.openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||
databaseKDB.masterKey = masterKey
|
||||
this@Database.mDatabaseKDB?.let { thisDatabaseKDB ->
|
||||
databaseKDB.copyMasterKeyFrom(thisDatabaseKDB)
|
||||
}
|
||||
}
|
||||
databaseKDB
|
||||
setDatabaseKDB(databaseKDB)
|
||||
},
|
||||
{ databaseInputStream ->
|
||||
val databaseKDBX = DatabaseKDBX()
|
||||
@@ -803,26 +756,144 @@ class Database {
|
||||
DatabaseInputKDBX(databaseKDBX).apply {
|
||||
setMethodToCheckIfRAMIsSufficient(isRAMSufficient)
|
||||
openDatabase(databaseInputStream, progressTaskUpdater) {
|
||||
databaseKDBX.masterKey = masterKey
|
||||
this@Database.mDatabaseKDBX?.let { thisDatabaseKDBX ->
|
||||
databaseKDBX.copyMasterKeyFrom(thisDatabaseKDBX)
|
||||
}
|
||||
}
|
||||
}
|
||||
databaseKDBX
|
||||
setDatabaseKDBX(databaseKDBX)
|
||||
}
|
||||
)
|
||||
loaded = true
|
||||
} 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) {
|
||||
throw LoadDatabaseException(e)
|
||||
Log.e(TAG, "Unable to reload the database")
|
||||
if (e is DatabaseException)
|
||||
throw e
|
||||
throw DatabaseInputException(e)
|
||||
} finally {
|
||||
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 {
|
||||
val groupKDB = group.groupKDB
|
||||
val groupKDBX = group.groupKDBX
|
||||
@@ -845,13 +916,13 @@ class Database {
|
||||
fun createVirtualGroupFromSearch(searchParameters: SearchParameters,
|
||||
fromGroup: NodeId<*>? = null,
|
||||
max: Int = Integer.MAX_VALUE): Group? {
|
||||
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
|
||||
return mSearchHelper.createVirtualGroupWithSearchResult(this,
|
||||
searchParameters, fromGroup, max)
|
||||
}
|
||||
|
||||
fun createVirtualGroupFromSearchInfo(searchInfoString: String,
|
||||
max: Int = Integer.MAX_VALUE): Group? {
|
||||
return mSearchHelper?.createVirtualGroupWithSearchResult(this,
|
||||
return mSearchHelper.createVirtualGroupWithSearchResult(this,
|
||||
SearchParameters().apply {
|
||||
searchQuery = searchInfoString
|
||||
searchInTitles = true
|
||||
@@ -908,40 +979,6 @@ class Database {
|
||||
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) {
|
||||
this.mDatabaseKDB?.clearIndexes()
|
||||
this.mDatabaseKDBX?.clearIndexes()
|
||||
@@ -987,20 +1024,13 @@ class Database {
|
||||
}
|
||||
|
||||
fun validatePasswordEncoding(mainCredential: MainCredential): Boolean {
|
||||
val password = mainCredential.masterPassword
|
||||
val password = mainCredential.password
|
||||
val containsKeyFile = mainCredential.keyFileUri != null
|
||||
return mDatabaseKDB?.validatePasswordEncoding(password, containsKeyFile)
|
||||
?: mDatabaseKDBX?.validatePasswordEncoding(password, containsKeyFile)
|
||||
?: false
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun assignMasterKey(key: String?, keyInputStream: InputStream?) {
|
||||
mDatabaseKDB?.retrieveMasterKey(key, keyInputStream)
|
||||
mDatabaseKDBX?.retrieveMasterKey(key, keyInputStream)
|
||||
mDatabaseKDBX?.keyLastChanged = DateInstant()
|
||||
}
|
||||
|
||||
fun rootCanContainsEntry(): Boolean {
|
||||
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
|
||||
|
||||
import android.content.ContentResolver
|
||||
import com.kunzisoft.encrypt.HashManager
|
||||
import com.kunzisoft.encrypt.aes.AESTransformer
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
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.entry.EntryKDB
|
||||
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.NodeIdUUID
|
||||
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.InputStream
|
||||
import java.nio.charset.Charset
|
||||
import java.util.*
|
||||
|
||||
class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
@@ -56,8 +60,8 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
KdfFactory.aesKdf
|
||||
)
|
||||
|
||||
override val passwordEncoding: String
|
||||
get() = "ISO-8859-1"
|
||||
override val passwordEncoding: Charset
|
||||
get() = Charsets.ISO_8859_1
|
||||
|
||||
override var numberKeyEncryptionRounds = 300L
|
||||
|
||||
@@ -116,20 +120,6 @@ class DatabaseKDB : DatabaseVersioned<Int, UUID, GroupKDB, EntryKDB>() {
|
||||
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)
|
||||
fun makeFinalKey(masterSeed: ByteArray, transformSeed: ByteArray, numRounds: Long) {
|
||||
// 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)
|
||||
}
|
||||
|
||||
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 {
|
||||
return GroupKDB()
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
*/
|
||||
package com.kunzisoft.keepass.database.element.database
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.res.Resources
|
||||
import android.util.Base64
|
||||
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.KdfFactory
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfParameters
|
||||
import com.kunzisoft.keepass.database.element.CustomData
|
||||
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.*
|
||||
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.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.template.Template
|
||||
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_40
|
||||
import com.kunzisoft.keepass.database.file.DatabaseHeaderKDBX.Companion.FILE_VERSION_41
|
||||
import com.kunzisoft.keepass.utils.StringUtil.removeSpaceChars
|
||||
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.utils.UnsignedInt
|
||||
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.InputStream
|
||||
import java.nio.charset.Charset
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.*
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
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 {
|
||||
val entryHandler = EntryOperationHandler()
|
||||
val groupHandler = GroupOperationHandler()
|
||||
@@ -364,8 +432,8 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
kdfEngine.setParallelism(kdfParameters!!, parallelism)
|
||||
}
|
||||
|
||||
override val passwordEncoding: String
|
||||
get() = "UTF-8"
|
||||
override val passwordEncoding: Charset
|
||||
get() = Charsets.UTF_8
|
||||
|
||||
private fun getGroupByUUID(groupUUID: UUID): GroupKDBX? {
|
||||
if (groupUUID == UUID_ZERO)
|
||||
@@ -528,22 +596,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
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)
|
||||
fun makeFinalKey(masterSeed: ByteArray) {
|
||||
|
||||
@@ -615,115 +667,6 @@ class DatabaseKDBX : DatabaseVersioned<UUID, UUID, GroupKDBX, EntryKDBX> {
|
||||
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 {
|
||||
var newId: NodeIdUUID
|
||||
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_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
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@
|
||||
package com.kunzisoft.keepass.database.element.database
|
||||
|
||||
import android.util.Log
|
||||
import com.kunzisoft.encrypt.HashManager
|
||||
import com.kunzisoft.keepass.database.crypto.EncryptionAlgorithm
|
||||
import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
|
||||
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.Type
|
||||
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.UnsupportedEncodingException
|
||||
import java.nio.charset.Charset
|
||||
import java.util.*
|
||||
|
||||
abstract class DatabaseVersioned<
|
||||
@@ -46,7 +43,6 @@ abstract class DatabaseVersioned<
|
||||
Entry : EntryVersioned<GroupId, EntryId, Group, Entry>
|
||||
> {
|
||||
|
||||
|
||||
// Algorithm used to encrypt the database
|
||||
abstract var encryptionAlgorithm: EncryptionAlgorithm
|
||||
abstract val availableEncryptionAlgorithms: List<EncryptionAlgorithm>
|
||||
@@ -55,11 +51,12 @@ abstract class DatabaseVersioned<
|
||||
abstract val kdfAvailableList: List<KdfEngine>
|
||||
abstract var numberKeyEncryptionRounds: Long
|
||||
|
||||
protected abstract val passwordEncoding: String
|
||||
abstract val passwordEncoding: Charset
|
||||
|
||||
var masterKey = ByteArray(32)
|
||||
var finalKey: ByteArray? = null
|
||||
protected set
|
||||
var transformSeed: ByteArray? = null
|
||||
|
||||
abstract val version: String
|
||||
abstract val defaultFileExtension: String
|
||||
@@ -91,58 +88,6 @@ abstract class DatabaseVersioned<
|
||||
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? {
|
||||
return null
|
||||
}
|
||||
@@ -158,20 +103,25 @@ abstract class DatabaseVersioned<
|
||||
|
||||
val bKey: ByteArray
|
||||
try {
|
||||
bKey = password.toByteArray(charset(encoding))
|
||||
bKey = password.toByteArray(encoding)
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
return false
|
||||
}
|
||||
|
||||
val reEncoded: String
|
||||
try {
|
||||
reEncoded = String(bKey, charset(encoding))
|
||||
reEncoded = String(bKey, encoding)
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
return false
|
||||
}
|
||||
return password == reEncoded
|
||||
}
|
||||
|
||||
fun copyMasterKeyFrom(databaseVersioned: DatabaseVersioned<GroupId, EntryId, Group, Entry>) {
|
||||
this.masterKey = databaseVersioned.masterKey
|
||||
this.transformSeed = databaseVersioned.transformSeed
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------------------
|
||||
* Node Creation
|
||||
|
||||
@@ -24,128 +24,172 @@ import androidx.annotation.StringRes
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import java.io.PrintStream
|
||||
import java.io.PrintWriter
|
||||
|
||||
abstract class DatabaseException : Exception {
|
||||
|
||||
var innerMessage: String? = null
|
||||
abstract var errorId: Int
|
||||
var parameters: (Array<out String>)? = null
|
||||
var mThrowable: Throwable? = null
|
||||
|
||||
constructor() : super()
|
||||
constructor(message: String) : super(message)
|
||||
constructor(message: String, throwable: Throwable) : super(message, throwable)
|
||||
constructor(throwable: Throwable) : super(throwable)
|
||||
constructor(message: String, throwable: 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 {
|
||||
parameters?.let {
|
||||
return resources.getString(errorId, *it)
|
||||
} ?: return resources.getString(errorId)
|
||||
val throwable = mThrowable
|
||||
if (throwable is DatabaseException)
|
||||
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 {
|
||||
@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 {
|
||||
class FileNotFoundDatabaseException : DatabaseInputException() {
|
||||
@StringRes
|
||||
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
|
||||
override var errorId: Int = R.string.invalid_algorithm
|
||||
constructor() : super()
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
}
|
||||
|
||||
class DuplicateUuidDatabaseException: LoadDatabaseException {
|
||||
class UnknownDatabaseLocationException : DatabaseException() {
|
||||
@StringRes
|
||||
override var errorId: Int = R.string.invalid_db_same_uuid
|
||||
constructor(type: Type, uuid: NodeId<*>) : super() {
|
||||
parameters = arrayOf(type.name, uuid.toString())
|
||||
}
|
||||
constructor(exception: Throwable) : super(exception)
|
||||
override var errorId: Int = R.string.error_location_unknown
|
||||
}
|
||||
|
||||
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
|
||||
override var errorId: Int = R.string.error_load_database
|
||||
constructor() : super()
|
||||
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 {
|
||||
@StringRes
|
||||
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 com.kunzisoft.keepass.R
|
||||
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 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.
|
||||
*
|
||||
* @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,
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
assignMasterKey: (() -> Unit)): D
|
||||
|
||||
@@ -47,7 +47,7 @@ import javax.crypto.CipherInputStream
|
||||
class DatabaseInputKDB(database: DatabaseKDB)
|
||||
: DatabaseInput<DatabaseKDB>(database) {
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
@Throws(DatabaseInputException::class)
|
||||
override fun openDatabase(databaseInputStream: InputStream,
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
assignMasterKey: (() -> Unit)): DatabaseKDB {
|
||||
@@ -76,6 +76,7 @@ class DatabaseInputKDB(database: DatabaseKDB)
|
||||
throw VersionDatabaseException()
|
||||
}
|
||||
|
||||
mDatabase.transformSeed = header.transformSeed
|
||||
assignMasterKey.invoke()
|
||||
|
||||
// Select algorithm
|
||||
@@ -310,18 +311,11 @@ class DatabaseInputKDB(database: DatabaseKDB)
|
||||
|
||||
stopContentTimer()
|
||||
|
||||
} catch (e: LoadDatabaseException) {
|
||||
} catch (e: Error) {
|
||||
mDatabase.clearAll()
|
||||
throw e
|
||||
} catch (e: IOException) {
|
||||
mDatabase.clearAll()
|
||||
throw IODatabaseException(e)
|
||||
} catch (e: OutOfMemoryError) {
|
||||
mDatabase.clearAll()
|
||||
throw NoMemoryDatabaseException(e)
|
||||
} catch (e: Exception) {
|
||||
mDatabase.clearAll()
|
||||
throw LoadDatabaseException(e)
|
||||
if (e is OutOfMemoryError)
|
||||
throw NoMemoryDatabaseException(e)
|
||||
throw DatabaseInputException(e)
|
||||
}
|
||||
|
||||
return mDatabase
|
||||
|
||||
@@ -99,7 +99,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
this.isRAMSufficient = method
|
||||
}
|
||||
|
||||
@Throws(LoadDatabaseException::class)
|
||||
@Throws(DatabaseInputException::class)
|
||||
override fun openDatabase(databaseInputStream: InputStream,
|
||||
progressTaskUpdater: ProgressTaskUpdater?,
|
||||
assignMasterKey: (() -> Unit)): DatabaseKDBX {
|
||||
@@ -114,6 +114,8 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
hashOfHeader = headerAndHash.hash
|
||||
val pbHeader = headerAndHash.header
|
||||
|
||||
val transformSeed = header.transformSeed
|
||||
mDatabase.transformSeed = transformSeed
|
||||
assignMasterKey.invoke()
|
||||
mDatabase.makeFinalKey(header.masterSeed)
|
||||
|
||||
@@ -155,7 +157,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
throw InvalidCredentialsDatabaseException()
|
||||
}
|
||||
|
||||
val hmacKey = mDatabase.hmacKey ?: throw LoadDatabaseException()
|
||||
val hmacKey = mDatabase.hmacKey ?: throw DatabaseInputException()
|
||||
|
||||
val blockKey = HmacBlock.getHmacKey64(hmacKey, UnsignedLong.MAX_BYTES)
|
||||
val hmac: Mac = HmacBlock.getHmacSha256(blockKey)
|
||||
@@ -187,7 +189,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
try {
|
||||
randomStream = CrsAlgorithm.getCipher(header.innerRandomStream, header.innerRandomStreamKey)
|
||||
} catch (e: Exception) {
|
||||
throw LoadDatabaseException(e)
|
||||
throw DatabaseInputException(e)
|
||||
}
|
||||
|
||||
val xmlPullParserFactory = XmlPullParserFactory.newInstance().apply {
|
||||
@@ -200,19 +202,12 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
|
||||
stopContentTimer()
|
||||
|
||||
} catch (e: LoadDatabaseException) {
|
||||
throw e
|
||||
} catch (e: XmlPullParserException) {
|
||||
throw IODatabaseException(e)
|
||||
} catch (e: IOException) {
|
||||
} catch (e: Error) {
|
||||
if (e is OutOfMemoryError)
|
||||
throw NoMemoryDatabaseException(e)
|
||||
if (e.message?.contains("Hash failed with code") == true)
|
||||
throw KDFMemoryDatabaseException(e)
|
||||
else
|
||||
throw IODatabaseException(e)
|
||||
} catch (e: OutOfMemoryError) {
|
||||
throw NoMemoryDatabaseException(e)
|
||||
} catch (e: Exception) {
|
||||
throw LoadDatabaseException(e)
|
||||
throw DatabaseInputException(e)
|
||||
}
|
||||
|
||||
return mDatabase
|
||||
@@ -227,7 +222,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
val fieldId = dataInputStream.read().toByte()
|
||||
|
||||
val size = dataInputStream.readBytes4ToUInt().toKotlinInt()
|
||||
if (size < 0) throw IOException("Corrupted file")
|
||||
if (size < 0) throw CorruptedDatabaseException()
|
||||
|
||||
var data = ByteArray(0)
|
||||
try {
|
||||
@@ -238,7 +233,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// OOM only if corrupted file
|
||||
throw IOException("Corrupted file")
|
||||
throw CorruptedDatabaseException()
|
||||
}
|
||||
|
||||
readStream = true
|
||||
@@ -297,7 +292,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
Binaries
|
||||
}
|
||||
|
||||
@Throws(XmlPullParserException::class, IOException::class, LoadDatabaseException::class)
|
||||
@Throws(XmlPullParserException::class, IOException::class, DatabaseInputException::class)
|
||||
private fun readDocumentStreamed(xpp: XmlPullParser) {
|
||||
|
||||
ctxGroups.clear()
|
||||
@@ -324,11 +319,11 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
}
|
||||
|
||||
// Error checks
|
||||
if (ctx != KdbContext.Null) throw IOException("Malformed")
|
||||
if (ctxGroups.size != 0) throw IOException("Malformed")
|
||||
if (ctx != KdbContext.Null) throw XMLMalformedDatabaseException()
|
||||
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 {
|
||||
val name = xpp.name
|
||||
when (ctx) {
|
||||
@@ -352,7 +347,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
if (encodedHash.isNotEmpty() && hashOfHeader != null) {
|
||||
val hash = Base64.decode(encodedHash, BASE_64_FLAG)
|
||||
if (!Arrays.equals(hash, hashOfHeader)) {
|
||||
throw LoadDatabaseException()
|
||||
throw DatabaseInputException()
|
||||
}
|
||||
}
|
||||
} else if (name.equals(DatabaseKDBXXML.ElemSettingsChanged, ignoreCase = true)) {
|
||||
@@ -824,7 +819,7 @@ class DatabaseInputKDBX(database: DatabaseKDBX)
|
||||
if (ctx != null) {
|
||||
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.SecureRandom
|
||||
|
||||
abstract class DatabaseOutput<Header : DatabaseHeader> protected constructor(protected var mOutputStream: OutputStream) {
|
||||
abstract class DatabaseOutput<Header : DatabaseHeader> {
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
protected open fun setIVs(header: Header): SecureRandom {
|
||||
@@ -44,9 +44,7 @@ abstract class DatabaseOutput<Header : DatabaseHeader> protected constructor(pro
|
||||
}
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
abstract fun output()
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
abstract fun outputHeader(outputStream: OutputStream): Header
|
||||
abstract fun writeDatabase(outputStream: OutputStream,
|
||||
assignMasterKey: () -> Unit)
|
||||
|
||||
}
|
||||
@@ -39,9 +39,8 @@ import java.security.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.CipherOutputStream
|
||||
|
||||
class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
outputStream: OutputStream)
|
||||
: DatabaseOutput<DatabaseHeaderKDB>(outputStream) {
|
||||
class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB)
|
||||
: DatabaseOutput<DatabaseHeaderKDB>() {
|
||||
|
||||
private var headerHashBlock: ByteArray? = null
|
||||
|
||||
@@ -60,15 +59,15 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
}
|
||||
|
||||
@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
|
||||
// and remove any orphaned nodes that are no longer part of the tree hierarchy
|
||||
// also remove the virtual root not present in kdb
|
||||
val rootGroup = mDatabaseKDB.rootGroup
|
||||
sortNodesForOutput()
|
||||
|
||||
val header = outputHeader(mOutputStream)
|
||||
|
||||
val header = outputHeader(outputStream, assignMasterKey)
|
||||
val finalKey = getFinalKey(header)
|
||||
|
||||
val cipher: Cipher = try {
|
||||
@@ -81,7 +80,7 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
}
|
||||
|
||||
try {
|
||||
val cos = CipherOutputStream(mOutputStream, cipher)
|
||||
val cos = CipherOutputStream(outputStream, cipher)
|
||||
val bos = BufferedOutputStream(cos)
|
||||
outputPlanGroupAndEntries(bos)
|
||||
bos.flush()
|
||||
@@ -107,7 +106,8 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
}
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
override fun outputHeader(outputStream: OutputStream): DatabaseHeaderKDB {
|
||||
private fun outputHeader(outputStream: OutputStream,
|
||||
assignMasterKey: () -> Unit): DatabaseHeaderKDB {
|
||||
// Build header
|
||||
val header = DatabaseHeaderKDB()
|
||||
header.signature1 = DatabaseHeaderKDB.DBSIG_1
|
||||
@@ -132,6 +132,9 @@ class DatabaseOutputKDB(private val mDatabaseKDB: DatabaseKDB,
|
||||
|
||||
setIVs(header)
|
||||
|
||||
mDatabaseKDB.transformSeed = header.transformSeed
|
||||
assignMasterKey()
|
||||
|
||||
// Header checksum
|
||||
val headerDigest: MessageDigest = HashManager.getHash256()
|
||||
|
||||
|
||||
@@ -56,9 +56,8 @@ import javax.crypto.CipherOutputStream
|
||||
import kotlin.experimental.or
|
||||
|
||||
|
||||
class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
outputStream: OutputStream)
|
||||
: DatabaseOutput<DatabaseHeaderKDBX>(outputStream) {
|
||||
class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX)
|
||||
: DatabaseOutput<DatabaseHeaderKDBX>() {
|
||||
|
||||
private var randomStream: StreamCipher? = null
|
||||
private lateinit var xml: XmlSerializer
|
||||
@@ -67,43 +66,34 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
private var headerHmac: ByteArray? = null
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
override fun output() {
|
||||
override fun writeDatabase(outputStream: OutputStream,
|
||||
assignMasterKey: () -> Unit) {
|
||||
|
||||
try {
|
||||
header = outputHeader(mOutputStream)
|
||||
header = outputHeader(outputStream, assignMasterKey)
|
||||
|
||||
val osPlain: OutputStream = if (header!!.version.isBefore(FILE_VERSION_40)) {
|
||||
val cos = attachStreamEncryptor(header!!, mOutputStream)
|
||||
val cos = attachStreamEncryptor(header!!, outputStream)
|
||||
cos.write(header!!.streamStartBytes)
|
||||
|
||||
HashedBlockOutputStream(cos)
|
||||
} else {
|
||||
mOutputStream.write(hashOfHeader!!)
|
||||
mOutputStream.write(headerHmac!!)
|
||||
outputStream.write(hashOfHeader!!)
|
||||
outputStream.write(headerHmac!!)
|
||||
|
||||
attachStreamEncryptor(header!!, HmacBlockOutputStream(mOutputStream, mDatabaseKDBX.hmacKey!!))
|
||||
attachStreamEncryptor(header!!, HmacBlockOutputStream(outputStream, mDatabaseKDBX.hmacKey!!))
|
||||
}
|
||||
|
||||
val xmlOutputStream: OutputStream
|
||||
try {
|
||||
xmlOutputStream = when(mDatabaseKDBX.compressionAlgorithm) {
|
||||
CompressionAlgorithm.GZip -> GZIPOutputStream(osPlain)
|
||||
else -> osPlain
|
||||
}
|
||||
|
||||
when(mDatabaseKDBX.compressionAlgorithm) {
|
||||
CompressionAlgorithm.GZip -> GZIPOutputStream(osPlain)
|
||||
else -> osPlain
|
||||
}.use { xmlOutputStream ->
|
||||
if (!header!!.version.isBefore(FILE_VERSION_40)) {
|
||||
outputInnerHeader(mDatabaseKDBX, header!!, xmlOutputStream)
|
||||
}
|
||||
|
||||
outputDatabase(xmlOutputStream)
|
||||
xmlOutputStream.close()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw DatabaseOutputException(e)
|
||||
} catch (e: IllegalStateException) {
|
||||
throw DatabaseOutputException(e)
|
||||
}
|
||||
|
||||
} catch (e: IOException) {
|
||||
} catch (e: Exception) {
|
||||
throw DatabaseOutputException(e)
|
||||
}
|
||||
}
|
||||
@@ -322,11 +312,15 @@ class DatabaseOutputKDBX(private val mDatabaseKDBX: DatabaseKDBX,
|
||||
}
|
||||
|
||||
@Throws(DatabaseOutputException::class)
|
||||
override fun outputHeader(outputStream: OutputStream): DatabaseHeaderKDBX {
|
||||
private fun outputHeader(outputStream: OutputStream,
|
||||
assignMasterKey: () -> Unit): DatabaseHeaderKDBX {
|
||||
try {
|
||||
val header = DatabaseHeaderKDBX(mDatabaseKDBX)
|
||||
setIVs(header)
|
||||
|
||||
mDatabaseKDBX.transformSeed = header.transformSeed
|
||||
assignMasterKey.invoke()
|
||||
|
||||
val pho = DatabaseHeaderOutputKDBX(mDatabaseKDBX, header, outputStream)
|
||||
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 = FIDO2_SECRET
|
||||
|
||||
fun getStringValues(): List<String> {
|
||||
return values().map { it.value }
|
||||
}
|
||||
|
||||
fun fromPosition(position: Int): HardwareKey {
|
||||
return when (position) {
|
||||
0 -> FIDO2_SECRET
|
||||
1 -> 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,141 @@
|
||||
package com.kunzisoft.keepass.hardware
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
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 androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.dialogs.UnderDevelopmentFeatureDialogFragment
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class HardwareKeyResponseHelper {
|
||||
|
||||
private var activity: FragmentActivity? = null
|
||||
private var fragment: Fragment? = null
|
||||
|
||||
private var getChallengeResponseResultLauncher: ActivityResultLauncher<Intent>? = null
|
||||
|
||||
constructor(context: FragmentActivity) {
|
||||
this.activity = context
|
||||
this.fragment = null
|
||||
}
|
||||
|
||||
constructor(context: Fragment) {
|
||||
this.activity = context.activity
|
||||
this.fragment = context
|
||||
}
|
||||
|
||||
fun buildHardwareKeyResponse(onChallengeResponded: (challengeResponse: ByteArray?,
|
||||
extra: Bundle?) -> Unit) {
|
||||
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")
|
||||
onChallengeResponded.invoke(challengeResponse,
|
||||
result.data?.getBundleExtra(EXTRA_BUNDLE_KEY))
|
||||
} else {
|
||||
Log.e(TAG, "Response from challenge error")
|
||||
onChallengeResponded.invoke(null,
|
||||
result.data?.getBundleExtra(EXTRA_BUNDLE_KEY))
|
||||
}
|
||||
}
|
||||
|
||||
getChallengeResponseResultLauncher = if (fragment != null) {
|
||||
fragment?.registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
resultCallback
|
||||
)
|
||||
} else {
|
||||
activity?.registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult(),
|
||||
resultCallback
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun launchChallengeForResponse(hardwareKey: HardwareKey, seed: ByteArray?) {
|
||||
when (hardwareKey) {
|
||||
HardwareKey.FIDO2_SECRET -> {
|
||||
// TODO FIDO2 under development
|
||||
throw Exception("FIDO2 not implemented")
|
||||
}
|
||||
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
|
||||
// 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
|
||||
getChallengeResponseResultLauncher!!.launch(
|
||||
Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT).apply {
|
||||
putExtra(HARDWARE_KEY_CHALLENGE_KEY, challenge)
|
||||
}
|
||||
)
|
||||
Log.d(TAG, "Challenge sent")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = HardwareKeyResponseHelper::class.java.simpleName
|
||||
|
||||
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"
|
||||
private const val EXTRA_BUNDLE_KEY = "EXTRA_BUNDLE_KEY"
|
||||
|
||||
fun isHardwareKeyAvailable(
|
||||
activity: FragmentActivity,
|
||||
hardwareKey: HardwareKey,
|
||||
showDialog: Boolean = true
|
||||
): Boolean {
|
||||
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(activity.packageManager) != null
|
||||
if (showDialog && !yubikeyDriverAvailable)
|
||||
showHardwareKeyDriverNeeded(activity, hardwareKey)
|
||||
yubikeyDriverAvailable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showHardwareKeyDriverNeeded(
|
||||
activity: FragmentActivity,
|
||||
hardwareKey: HardwareKey
|
||||
) {
|
||||
activity.lifecycleScope.launch {
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
builder
|
||||
.setMessage(
|
||||
activity.getString(R.string.error_driver_required, hardwareKey.toString())
|
||||
)
|
||||
.setPositiveButton(R.string.download) { _, _ ->
|
||||
UriUtil.gotoUrl(activity, activity.getString(R.string.key_driver_url))
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
builder.create().show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,11 +41,6 @@ class CipherEncryptDatabase(): Parcelable {
|
||||
parcel.readByteArray(specParameters)
|
||||
}
|
||||
|
||||
fun replaceContent(copy: CipherEncryptDatabase) {
|
||||
this.encryptedValue = copy.encryptedValue
|
||||
this.specParameters = copy.specParameters
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeParcelable(databaseUri, flags)
|
||||
parcel.writeEnum(credentialStorage)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.kunzisoft.keepass.model
|
||||
|
||||
import android.net.Uri
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
|
||||
data class DatabaseFile(var databaseUri: Uri? = null,
|
||||
var keyFileUri: Uri? = null,
|
||||
var hardwareKey: HardwareKey? = null,
|
||||
var databaseDecodedPath: String? = null,
|
||||
var databaseAlias: String? = null,
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
|
||||
@@ -27,6 +27,7 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.media.app.NotificationCompat
|
||||
import com.kunzisoft.keepass.R
|
||||
import com.kunzisoft.keepass.activities.GroupActivity
|
||||
@@ -37,12 +38,14 @@ import com.kunzisoft.keepass.database.action.node.*
|
||||
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.MainCredential
|
||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||
import com.kunzisoft.keepass.database.element.node.Node
|
||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||
import com.kunzisoft.keepass.database.element.node.Type
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
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.tasks.ActionRunnable
|
||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||
@@ -53,6 +56,7 @@ import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||
import com.kunzisoft.keepass.utils.closeDatabase
|
||||
import com.kunzisoft.keepass.viewmodels.FileDatabaseInfo
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import java.util.*
|
||||
|
||||
open class DatabaseTaskNotificationService : LockNotificationService(), ProgressTaskUpdater {
|
||||
@@ -61,20 +65,25 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
|
||||
private var mDatabase: Database? = null
|
||||
|
||||
// File description
|
||||
private var mSnapFileDatabaseInfo: SnapFileDatabaseInfo? = null
|
||||
private var mLastLocalSaveTime: Long = 0
|
||||
|
||||
private val mainScope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
private var mDatabaseListeners = LinkedList<DatabaseListener>()
|
||||
private var mDatabaseInfoListeners = LinkedList<DatabaseInfoListener>()
|
||||
private var mDatabaseListeners = mutableListOf<DatabaseListener>()
|
||||
private var mDatabaseInfoListeners = mutableListOf<DatabaseInfoListener>()
|
||||
private var mActionTaskBinder = ActionTaskBinder()
|
||||
private var mActionTaskListeners = LinkedList<ActionTaskListener>()
|
||||
private var mActionTaskListeners = mutableListOf<ActionTaskListener>()
|
||||
// Channel to connect asynchronously a listener or a response
|
||||
private var mRequestChallengeListenerChannel: Channel<RequestChallengeListener>? = null
|
||||
private var mResponseChallengeChannel: Channel<ByteArray?>? = null
|
||||
|
||||
private var mActionRunning = 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 mTitleId: Int = R.string.database_opened
|
||||
private var mMessageId: Int? = null
|
||||
private var mWarningId: Int? = null
|
||||
private var mProgressMessage: ProgressMessage = ProgressMessage(R.string.database_opened)
|
||||
|
||||
override fun retrieveChannelId(): String {
|
||||
return CHANNEL_DATABASE_ID
|
||||
@@ -114,6 +123,25 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
fun removeActionTaskListener(actionTaskListener: ActionTaskListener) {
|
||||
mActionTaskListeners.remove(actionTaskListener)
|
||||
}
|
||||
|
||||
fun addRequestChallengeListener(requestChallengeListener: RequestChallengeListener) {
|
||||
mainScope.launch {
|
||||
val requestChannel = mRequestChallengeListenerChannel
|
||||
if (requestChannel == null || requestChannel.isEmpty) {
|
||||
initializeChallengeResponse()
|
||||
mRequestChallengeListenerChannel?.send(requestChallengeListener)
|
||||
} else {
|
||||
cancelChallengeResponse(R.string.error_challenge_already_requested)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeRequestChallengeListener() {
|
||||
mainScope.launch {
|
||||
mRequestChallengeListenerChannel?.cancel()
|
||||
mRequestChallengeListenerChannel = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface DatabaseListener {
|
||||
@@ -126,9 +154,17 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
}
|
||||
|
||||
interface ActionTaskListener {
|
||||
fun onStartAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?)
|
||||
fun onUpdateAction(database: Database, titleId: Int?, messageId: Int?, warningId: Int?)
|
||||
fun onStopAction(database: Database, actionTask: String, result: ActionRunnable.Result)
|
||||
fun onStartAction(database: Database,
|
||||
progressMessage: ProgressMessage)
|
||||
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() {
|
||||
@@ -165,7 +201,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
Log.i(TAG, "Database file modified " +
|
||||
"$previousDatabaseInfo != $lastFileDatabaseInfo ")
|
||||
// Call listener to indicate a change in database info
|
||||
if (!mCreationState && previousDatabaseInfo != null) {
|
||||
if (!mSaveState && previousDatabaseInfo != null) {
|
||||
mDatabaseInfoListeners.forEach { listener ->
|
||||
listener.onDatabaseInfoChanged(previousDatabaseInfo, lastFileDatabaseInfo)
|
||||
}
|
||||
@@ -197,12 +233,53 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
mDatabase?.let { database ->
|
||||
if (mActionRunning) {
|
||||
mActionTaskListeners.forEach { actionTaskListener ->
|
||||
actionTaskListener.onStartAction(database, mTitleId, mMessageId, mWarningId)
|
||||
actionTaskListener.onStartAction(
|
||||
database, mProgressMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeChallengeResponse() {
|
||||
// Init the channels
|
||||
if (mRequestChallengeListenerChannel == null) {
|
||||
mRequestChallengeListenerChannel = Channel(0)
|
||||
}
|
||||
if (mResponseChallengeChannel == null) {
|
||||
mResponseChallengeChannel = Channel(0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun closeChallengeResponse() {
|
||||
mRequestChallengeListenerChannel?.close()
|
||||
mResponseChallengeChannel?.close()
|
||||
mRequestChallengeListenerChannel = null
|
||||
mResponseChallengeChannel = null
|
||||
}
|
||||
|
||||
private fun cancelChallengeResponse(@StringRes error: Int) {
|
||||
mRequestChallengeListenerChannel?.cancel(CancellationException(getString(error)))
|
||||
mRequestChallengeListenerChannel = null
|
||||
mResponseChallengeChannel?.cancel(CancellationException(getString(error)))
|
||||
mResponseChallengeChannel = null
|
||||
}
|
||||
|
||||
fun respondToChallenge(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
super.onBind(intent)
|
||||
return mActionTaskBinder
|
||||
@@ -219,8 +296,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
|
||||
buildMessage(intent, database.isReadOnly)
|
||||
buildNotification(intent)
|
||||
|
||||
val intentAction = intent?.action
|
||||
|
||||
@@ -272,13 +361,15 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
mActionRunning = true
|
||||
|
||||
sendBroadcast(Intent(DATABASE_START_TASK_ACTION).apply {
|
||||
putExtra(DATABASE_TASK_TITLE_KEY, mTitleId)
|
||||
putExtra(DATABASE_TASK_MESSAGE_KEY, mMessageId)
|
||||
putExtra(DATABASE_TASK_WARNING_KEY, mWarningId)
|
||||
putExtra(DATABASE_TASK_TITLE_KEY, mProgressMessage.titleId)
|
||||
putExtra(DATABASE_TASK_MESSAGE_KEY, mProgressMessage.messageId)
|
||||
putExtra(DATABASE_TASK_WARNING_KEY, mProgressMessage.warningId)
|
||||
})
|
||||
|
||||
mActionTaskListeners.forEach { actionTaskListener ->
|
||||
actionTaskListener.onStartAction(database, mTitleId, mMessageId, mWarningId)
|
||||
actionTaskListener.onStartAction(
|
||||
database, mProgressMessage
|
||||
)
|
||||
}
|
||||
|
||||
},
|
||||
@@ -353,61 +444,51 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildMessage(intent: Intent?, readOnly: Boolean) {
|
||||
private fun buildNotification(intent: Intent?) {
|
||||
// Assign elements for updates
|
||||
val intentAction = intent?.action
|
||||
|
||||
var saveAction = false
|
||||
if (intent != null && intent.hasExtra(SAVE_DATABASE_KEY)) {
|
||||
saveAction = !readOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, saveAction)
|
||||
}
|
||||
|
||||
mIconId = if (intentAction == null)
|
||||
// Get icon depending action state
|
||||
val iconId = if (intentAction == null)
|
||||
R.drawable.notification_ic_database_open
|
||||
else
|
||||
R.drawable.notification_ic_database_load
|
||||
R.drawable.notification_ic_database_action
|
||||
|
||||
mTitleId = when {
|
||||
saveAction -> {
|
||||
R.string.saving_database
|
||||
}
|
||||
intentAction == null -> {
|
||||
// Title depending on action
|
||||
mProgressMessage.titleId =
|
||||
if (intentAction == null) {
|
||||
R.string.database_opened
|
||||
}
|
||||
else -> {
|
||||
when (intentAction) {
|
||||
ACTION_DATABASE_CREATE_TASK -> R.string.creating_database
|
||||
ACTION_DATABASE_LOAD_TASK,
|
||||
ACTION_DATABASE_MERGE_TASK,
|
||||
ACTION_DATABASE_RELOAD_TASK -> R.string.loading_database
|
||||
ACTION_DATABASE_SAVE -> R.string.saving_database
|
||||
else -> {
|
||||
} else when (intentAction) {
|
||||
ACTION_DATABASE_CREATE_TASK -> R.string.creating_database
|
||||
ACTION_DATABASE_LOAD_TASK,
|
||||
ACTION_DATABASE_MERGE_TASK,
|
||||
ACTION_DATABASE_RELOAD_TASK -> R.string.loading_database
|
||||
ACTION_DATABASE_ASSIGN_PASSWORD_TASK,
|
||||
ACTION_DATABASE_SAVE -> R.string.saving_database
|
||||
else -> {
|
||||
if (mSaveState)
|
||||
R.string.saving_database
|
||||
else
|
||||
R.string.command_execution
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mMessageId = when (intentAction) {
|
||||
ACTION_DATABASE_LOAD_TASK,
|
||||
ACTION_DATABASE_MERGE_TASK,
|
||||
ACTION_DATABASE_RELOAD_TASK -> null
|
||||
else -> null
|
||||
}
|
||||
// Updated later
|
||||
mProgressMessage.messageId = null
|
||||
|
||||
mWarningId =
|
||||
if (!saveAction
|
||||
|| intentAction == ACTION_DATABASE_LOAD_TASK
|
||||
|| intentAction == ACTION_DATABASE_MERGE_TASK
|
||||
|| intentAction == ACTION_DATABASE_RELOAD_TASK)
|
||||
null
|
||||
else
|
||||
// Warning if data is saved
|
||||
mProgressMessage.warningId =
|
||||
if (mSaveState)
|
||||
R.string.do_not_kill_app
|
||||
else
|
||||
null
|
||||
|
||||
val notificationBuilder = buildNewNotification().apply {
|
||||
setSmallIcon(mIconId)
|
||||
setSmallIcon(iconId)
|
||||
intent?.let {
|
||||
setContentTitle(getString(intent.getIntExtra(DATABASE_TASK_TITLE_KEY, mTitleId)))
|
||||
setContentTitle(getString(
|
||||
intent.getIntExtra(DATABASE_TASK_TITLE_KEY, mProgressMessage.titleId))
|
||||
)
|
||||
}
|
||||
setAutoCancel(false)
|
||||
setContentIntent(null)
|
||||
@@ -513,15 +594,21 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateMessage(resId: Int) {
|
||||
mMessageId = resId
|
||||
private fun notifyProgressMessage() {
|
||||
mDatabase?.let { database ->
|
||||
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() {
|
||||
if (!TimeoutHelper.temporarilyDisableLock) {
|
||||
closeDatabase(mDatabase)
|
||||
@@ -539,6 +626,39 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
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()
|
||||
val challengeResponseRequestListener = mRequestChallengeListenerChannel?.receive()
|
||||
challengeResponseRequestListener?.onChallengeResponseRequested(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? {
|
||||
|
||||
if (intent.hasExtra(DATABASE_URI_KEY)
|
||||
@@ -550,15 +670,16 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
if (databaseUri == null)
|
||||
return null
|
||||
|
||||
mCreationState = true
|
||||
|
||||
return CreateDatabaseRunnable(this,
|
||||
database,
|
||||
databaseUri,
|
||||
getString(R.string.database_default_name),
|
||||
getString(R.string.database),
|
||||
getString(R.string.template_group_name),
|
||||
mainCredential
|
||||
mainCredential,
|
||||
{ hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}
|
||||
) { result ->
|
||||
result.data = Bundle().apply {
|
||||
putParcelable(DATABASE_URI_KEY, databaseUri)
|
||||
@@ -586,17 +707,18 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
if (databaseUri == null)
|
||||
return null
|
||||
|
||||
mCreationState = false
|
||||
|
||||
return LoadDatabaseRunnable(
|
||||
this,
|
||||
database,
|
||||
databaseUri,
|
||||
mainCredential,
|
||||
readOnly,
|
||||
cipherEncryptDatabase,
|
||||
intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false),
|
||||
this
|
||||
this,
|
||||
database,
|
||||
databaseUri,
|
||||
mainCredential,
|
||||
{ hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
},
|
||||
readOnly,
|
||||
cipherEncryptDatabase,
|
||||
intent.getBooleanExtra(FIX_DUPLICATE_UUID_KEY, false),
|
||||
this
|
||||
) { result ->
|
||||
// Add each info to reload database after thrown duplicate UUID exception
|
||||
result.data = Bundle().apply {
|
||||
@@ -626,6 +748,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
database,
|
||||
databaseToMergeUri,
|
||||
databaseToMergeMainCredential,
|
||||
{ hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
},
|
||||
this
|
||||
) { result ->
|
||||
// No need to add each info to reload database
|
||||
@@ -653,7 +778,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
database,
|
||||
databaseUri,
|
||||
intent.getParcelableExtra(MAIN_CREDENTIAL_KEY) ?: MainCredential()
|
||||
)
|
||||
) { hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -687,7 +814,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
newGroup,
|
||||
parent,
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||
AfterActionNodesRunnable())
|
||||
AfterActionNodesRunnable()
|
||||
) { hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
@@ -712,7 +842,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
oldGroup,
|
||||
newGroup,
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||
AfterActionNodesRunnable())
|
||||
AfterActionNodesRunnable()
|
||||
) { hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
@@ -737,7 +870,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
newEntry,
|
||||
parent,
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||
AfterActionNodesRunnable())
|
||||
AfterActionNodesRunnable()
|
||||
) { hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
@@ -762,7 +898,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
oldEntry,
|
||||
newEntry,
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||
AfterActionNodesRunnable())
|
||||
AfterActionNodesRunnable()
|
||||
) { hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
@@ -783,7 +922,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
getListNodesFromBundle(database, intent.extras!!),
|
||||
newParent,
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||
AfterActionNodesRunnable())
|
||||
AfterActionNodesRunnable()
|
||||
) { hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
@@ -804,7 +946,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
getListNodesFromBundle(database, intent.extras!!),
|
||||
newParent,
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||
AfterActionNodesRunnable())
|
||||
AfterActionNodesRunnable()
|
||||
) { hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
@@ -820,7 +965,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
database,
|
||||
getListNodesFromBundle(database, intent.extras!!),
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||
AfterActionNodesRunnable())
|
||||
AfterActionNodesRunnable()
|
||||
) { hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -838,7 +986,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
database,
|
||||
mainEntry,
|
||||
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 {
|
||||
null
|
||||
@@ -857,7 +1008,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
database,
|
||||
mainEntry,
|
||||
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 {
|
||||
null
|
||||
@@ -881,7 +1035,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
oldElement,
|
||||
newElement,
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||
).apply {
|
||||
) { hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}.apply {
|
||||
mAfterSaveDatabase = { result ->
|
||||
result.data = intent.extras
|
||||
}
|
||||
@@ -897,7 +1053,9 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
return RemoveUnlinkedDataDatabaseRunnable(this,
|
||||
database,
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||
).apply {
|
||||
) { hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}.apply {
|
||||
mAfterSaveDatabase = { result ->
|
||||
result.data = intent.extras
|
||||
}
|
||||
@@ -911,7 +1069,11 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
return if (intent.hasExtra(SAVE_DATABASE_KEY)) {
|
||||
return SaveDatabaseRunnable(this,
|
||||
database,
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false)
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||
null,
|
||||
{ hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
}
|
||||
).apply {
|
||||
mAfterSaveDatabase = { result ->
|
||||
result.data = intent.extras
|
||||
@@ -936,6 +1098,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
SaveDatabaseRunnable(this,
|
||||
database,
|
||||
!database.isReadOnly && intent.getBooleanExtra(SAVE_DATABASE_KEY, false),
|
||||
null,
|
||||
{ hardwareKey, seed ->
|
||||
retrieveResponseFromChallenge(hardwareKey, seed)
|
||||
},
|
||||
databaseCopyUri)
|
||||
} else {
|
||||
null
|
||||
@@ -1002,9 +1168,6 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
||||
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
|
||||
|
||||
private var mSnapFileDatabaseInfo: SnapFileDatabaseInfo? = null
|
||||
private var mLastLocalSaveTime: Long = 0
|
||||
|
||||
fun getListNodesFromBundle(database: Database, bundle: Bundle): List<Node> {
|
||||
val nodesAction = ArrayList<Node>()
|
||||
bundle.getParcelableArrayList<NodeId<*>>(GROUPS_ID_KEY)?.forEach {
|
||||
|
||||
@@ -96,6 +96,12 @@ object PreferencesUtil {
|
||||
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 {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
return prefs.getBoolean(context.getString(R.string.auto_focus_search_key),
|
||||
@@ -479,6 +485,12 @@ object PreferencesUtil {
|
||||
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 {
|
||||
return isBiometricUnlockEnable(context) || isDeviceCredentialUnlockEnable(context)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
|
||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||
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.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||
|
||||
@@ -24,15 +24,18 @@ import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.kunzisoft.keepass.R
|
||||
import java.lang.Exception
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
|
||||
open class ProgressTaskDialogFragment : DialogFragment() {
|
||||
|
||||
@StringRes
|
||||
private var title = UNDEFINED
|
||||
@@ -40,10 +43,12 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
|
||||
private var message = UNDEFINED
|
||||
@StringRes
|
||||
private var warning = UNDEFINED
|
||||
private var cancellable: (() -> Unit)? = null
|
||||
|
||||
private var titleView: TextView? = null
|
||||
private var messageView: TextView? = null
|
||||
private var warningView: TextView? = null
|
||||
private var cancelButton: Button? = null
|
||||
private var progressView: ProgressBar? = null
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
@@ -63,11 +68,13 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
|
||||
titleView = root.findViewById(R.id.progress_dialog_title)
|
||||
messageView = root.findViewById(R.id.progress_dialog_message)
|
||||
warningView = root.findViewById(R.id.progress_dialog_warning)
|
||||
cancelButton = root.findViewById(R.id.progress_dialog_cancel)
|
||||
progressView = root.findViewById(R.id.progress_dialog_bar)
|
||||
|
||||
updateTitle(title)
|
||||
updateMessage(message)
|
||||
updateWarning(warning)
|
||||
setCancellable(cancellable)
|
||||
|
||||
isCancelable = false
|
||||
|
||||
@@ -84,7 +91,7 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
|
||||
}
|
||||
|
||||
private fun updateView(textView: TextView?, @StringRes resId: Int) {
|
||||
activity?.runOnUiThread {
|
||||
activity?.lifecycleScope?.launch {
|
||||
if (resId == UNDEFINED) {
|
||||
textView?.visibility = View.GONE
|
||||
} else {
|
||||
@@ -94,21 +101,35 @@ open class ProgressTaskDialogFragment : DialogFragment(), ProgressTaskUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTitle(@StringRes resId: Int) {
|
||||
this.title = resId
|
||||
private fun updateCancelable() {
|
||||
activity?.lifecycleScope?.launch {
|
||||
cancelButton?.isVisible = cancellable != null
|
||||
cancelButton?.setOnClickListener {
|
||||
cancellable?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTitle(@StringRes resId: Int?) {
|
||||
this.title = resId ?: UNDEFINED
|
||||
updateView(titleView, title)
|
||||
}
|
||||
|
||||
override fun updateMessage(@StringRes resId: Int) {
|
||||
this.message = resId
|
||||
fun updateMessage(@StringRes resId: Int?) {
|
||||
this.message = resId ?: UNDEFINED
|
||||
updateView(messageView, message)
|
||||
}
|
||||
|
||||
fun updateWarning(@StringRes resId: Int) {
|
||||
this.warning = resId
|
||||
fun updateWarning(@StringRes resId: Int?) {
|
||||
this.warning = resId ?: UNDEFINED
|
||||
updateView(warningView, warning)
|
||||
}
|
||||
|
||||
fun setCancellable(cancellable: (() -> Unit)?) {
|
||||
this.cancellable = cancellable
|
||||
updateCancelable()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = ProgressTaskDialogFragment::class.java.simpleName
|
||||
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() =
|
||||
readString()?.let { enumValueOf<T>(it) }
|
||||
|
||||
|
||||
@@ -226,10 +226,10 @@ object UriUtil {
|
||||
}
|
||||
}
|
||||
|
||||
fun getUriFromIntent(intent: Intent, key: String): Uri? {
|
||||
fun getUriFromIntent(intent: Intent?, key: String): Uri? {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
val clipData = intent.clipData
|
||||
val clipData = intent?.clipData
|
||||
if (clipData != null) {
|
||||
if (clipData.description.label == key) {
|
||||
if (clipData.itemCount == 1) {
|
||||
@@ -242,7 +242,7 @@ object UriUtil {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return intent.getParcelableExtra(key)
|
||||
return intent?.getParcelableExtra(key)
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -269,11 +269,15 @@ object UriUtil {
|
||||
|
||||
fun contributingUser(context: Context): Boolean {
|
||||
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 {
|
||||
context.applicationContext.packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
|
||||
Education.setEducationScreenReclickedPerformed(context)
|
||||
@@ -295,10 +299,11 @@ object UriUtil {
|
||||
}
|
||||
try {
|
||||
if (launchIntent == null) {
|
||||
// TODO F-Droid
|
||||
context.startActivity(
|
||||
Intent(Intent.ACTION_VIEW)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.setData(Uri.parse("https://play.google.com/store/apps/details?id=$packageName"))
|
||||
.setData(Uri.parse(context.getString(R.string.play_store_url, packageName)))
|
||||
)
|
||||
} else {
|
||||
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.setOpenDocumentClickListener
|
||||
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,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyle: Int = 0)
|
||||
: FrameLayout(context, attrs, defStyle) {
|
||||
|
||||
private var passwordTextView: EditText
|
||||
private var keyFileSelectionView: KeyFileSelectionView
|
||||
private var checkboxPasswordView: CompoundButton
|
||||
private var passwordTextView: EditText
|
||||
private var checkboxKeyFileView: CompoundButton
|
||||
private var keyFileSelectionView: KeyFileSelectionView
|
||||
private var checkboxHardwareView: CompoundButton
|
||||
private var hardwareKeySelectionView: HardwareKeySelectionView
|
||||
|
||||
var onPasswordChecked: (CompoundButton.OnCheckedChangeListener)? = null
|
||||
var onKeyFileChecked: (CompoundButton.OnCheckedChangeListener)? = null
|
||||
var onHardwareKeyChecked: (CompoundButton.OnCheckedChangeListener)? = null
|
||||
var onValidateListener: (() -> Unit)? = null
|
||||
|
||||
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?
|
||||
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)
|
||||
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 {
|
||||
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
onValidateListener?.invoke()
|
||||
validateCredential()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -91,7 +98,7 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
||||
if (keyEvent.action == KeyEvent.ACTION_DOWN
|
||||
&& keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER
|
||||
) {
|
||||
onValidateListener?.invoke()
|
||||
validateCredential()
|
||||
handled = true
|
||||
}
|
||||
handled
|
||||
@@ -100,10 +107,30 @@ class MainCredentialView @JvmOverloads constructor(context: Context,
|
||||
checkboxPasswordView.setOnCheckedChangeListener { 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?) {
|
||||
keyFileSelectionView.setOpenDocumentClickListener(externalFileHelper)
|
||||
fun validateCredential() {
|
||||
onValidateListener?.invoke()
|
||||
}
|
||||
|
||||
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()) {
|
||||
keyFileSelectionView.uri = null
|
||||
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 {
|
||||
return checkboxPasswordView.isChecked || checkboxKeyFileView.isChecked
|
||||
return checkboxPasswordView.isChecked
|
||||
|| (checkboxKeyFileView.isChecked && keyFileSelectionView.uri != null)
|
||||
|| (checkboxHardwareView.isChecked && hardwareKeySelectionView.hardwareKey != null)
|
||||
}
|
||||
|
||||
fun getMainCredential(): MainCredential {
|
||||
return MainCredential().apply {
|
||||
this.masterPassword = if (checkboxPasswordView.isChecked)
|
||||
this.password = if (checkboxPasswordView.isChecked)
|
||||
passwordTextView.text?.toString() else null
|
||||
this.keyFileUri = if (checkboxKeyFileView.isChecked)
|
||||
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
|
||||
return when (mCredentialStorage) {
|
||||
CredentialStorage.PASSWORD -> checkboxPasswordView.isChecked
|
||||
CredentialStorage.KEY_FILE -> checkboxPasswordView.isChecked
|
||||
CredentialStorage.KEY_FILE -> false
|
||||
CredentialStorage.HARDWARE_KEY -> false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ class TemplateView @JvmOverloads constructor(context: Context,
|
||||
setCopyButtonState(TextFieldView.ButtonState.ACTIVATE)
|
||||
setCopyButtonClickListener { label, value ->
|
||||
mOnCopyActionClickListener
|
||||
?.invoke(Field(label, ProtectedString(false, value)))
|
||||
?.invoke(Field(label, ProtectedString(true, value)))
|
||||
}
|
||||
} else {
|
||||
setCopyButtonState(TextFieldView.ButtonState.GONE)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.kunzisoft.keepass.viewmodels
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class ChallengeResponseViewModel: ViewModel() {
|
||||
|
||||
val dataResponded : LiveData<ByteArray?> get() = _dataResponded
|
||||
private val _dataResponded = MutableLiveData<ByteArray?>()
|
||||
|
||||
fun respond(byteArray: ByteArray) {
|
||||
_dataResponded.value = byteArray
|
||||
}
|
||||
|
||||
fun resendResponse() {
|
||||
dataResponded.value?.let {
|
||||
_dataResponded.value = it
|
||||
}
|
||||
}
|
||||
|
||||
fun consumeResponse() {
|
||||
_dataResponded.value = null
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import androidx.lifecycle.MutableLiveData
|
||||
import com.kunzisoft.keepass.app.App
|
||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||
import com.kunzisoft.keepass.app.database.IOActionTask
|
||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||
import com.kunzisoft.keepass.model.DatabaseFile
|
||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
@@ -72,8 +73,12 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
|
||||
}
|
||||
}
|
||||
|
||||
fun addDatabaseFile(databaseUri: Uri, keyFileUri: Uri?) {
|
||||
mFileDatabaseHistoryAction?.addOrUpdateDatabaseUri(databaseUri, keyFileUri) { databaseFileAdded ->
|
||||
fun addDatabaseFile(databaseUri: Uri, keyFileUri: Uri?, hardwareKey: HardwareKey?) {
|
||||
mFileDatabaseHistoryAction?.addOrUpdateDatabaseUri(
|
||||
databaseUri,
|
||||
keyFileUri,
|
||||
hardwareKey
|
||||
) { databaseFileAdded ->
|
||||
databaseFileAdded?.let { _ ->
|
||||
databaseFilesLoaded.value = getDatabaseFilesLoadedValue().apply {
|
||||
this.databaseFileAction = DatabaseFileAction.ADD
|
||||
@@ -96,6 +101,7 @@ class DatabaseFilesViewModel(application: Application) : AndroidViewModel(applic
|
||||
.find { it.databaseUri == databaseFileUpdated.databaseUri }
|
||||
?.apply {
|
||||
keyFileUri = databaseFileUpdated.keyFileUri
|
||||
hardwareKey = databaseFileUpdated.hardwareKey
|
||||
databaseAlias = databaseFileUpdated.databaseAlias
|
||||
databaseFileExists = databaseFileUpdated.databaseFileExists
|
||||
databaseLastModified = databaseFileUpdated.databaseLastModified
|
||||
|
||||
|
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_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -118,6 +118,11 @@
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_width="match_parent"
|
||||
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
|
||||
android:id="@+id/activity_about_contribution_text"
|
||||
android:layout_marginTop="8dp"
|
||||
@@ -179,4 +184,5 @@
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<include layout="@layout/view_screenshot_mode_banner" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -17,165 +17,174 @@
|
||||
You should have received a copy of the GNU General Public License
|
||||
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:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/toolbar_coordinator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar"
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/toolbar_coordinator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/toolbar_parallax_height"
|
||||
android:background="?attr/colorPrimary">
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:id="@+id/toolbar_layout"
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:contentScrim="?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">
|
||||
android:layout_height="@dimen/toolbar_parallax_height"
|
||||
android:background="?attr/colorPrimary">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/title_block"
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
android:id="@+id/toolbar_layout"
|
||||
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"
|
||||
app:contentScrim="?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
|
||||
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_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>
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
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"/>
|
||||
|
||||
<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" />
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/entry_content"
|
||||
android:name="com.kunzisoft.keepass.activities.fragments.EntryFragment"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintWidth_percent="@dimen/content_percent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/entry_tags_list_view"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<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">
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/entry_content_tab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="120dp"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:background="?attr/cardBackgroundTransparentColor"
|
||||
app:tabIconTint="?android:attr/textColor"
|
||||
app:tabMode="fixed">
|
||||
|
||||
<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"
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:id="@+id/entry_content_tab_main"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
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"/>
|
||||
android:icon="@drawable/ic_view_list_white_24dp" />
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/entry_content"
|
||||
android:name="com.kunzisoft.keepass.activities.fragments.EntryFragment"
|
||||
android:layout_width="0dp"
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:id="@+id/entry_content_tab_advanced"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintWidth_percent="@dimen/content_percent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/entry_tags_list_view"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
android:icon="@drawable/ic_time_white_24dp" />
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</com.google.android.material.tabs.TabLayout>
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/entry_content_tab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="120dp"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:background="?attr/cardBackgroundTransparentColor"
|
||||
app:tabIconTint="?android:attr/textColor"
|
||||
app:tabMode="fixed">
|
||||
<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>
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:id="@+id/entry_content_tab_main"
|
||||
<include
|
||||
layout="@layout/view_button_lock"
|
||||
android:layout_width="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
|
||||
android:id="@+id/entry_content_tab_advanced"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:icon="@drawable/ic_time_white_24dp" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
</com.google.android.material.tabs.TabLayout>
|
||||
|
||||
<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>
|
||||
<include layout="@layout/view_screenshot_mode_banner" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:theme="?attr/toolbarActionAppearance"
|
||||
android:layout_gravity="bottom"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/entry_edit_validate"
|
||||
@@ -96,7 +96,7 @@
|
||||
android:tint="?attr/colorOnAccentColor"
|
||||
app:fabSize="mini"
|
||||
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_constraintStart_toStartOf="parent" />
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/loading"
|
||||
@@ -119,4 +119,6 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<include layout="@layout/view_screenshot_mode_banner" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
android:id="@+id/file_selection_buttons_container"
|
||||
android:layout_width="0dp"
|
||||
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_constraintStart_toStartOf="parent">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
@@ -194,4 +194,5 @@
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<include layout="@layout/view_screenshot_mode_banner" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<RelativeLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/activity_group_container_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
@@ -36,16 +36,17 @@
|
||||
android:id="@+id/special_mode_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="?attr/toolbarSpecialAppearance" />
|
||||
android:theme="?attr/toolbarSpecialAppearance"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:title="@string/app_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:layout_below="@+id/special_mode_view"
|
||||
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
|
||||
android:id="@+id/database_name_container"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -64,10 +65,12 @@
|
||||
<FrameLayout
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:layout_below="@+id/special_mode_view"
|
||||
android:layout_marginStart="50dp"
|
||||
android:layout_marginLeft="50dp">
|
||||
<ImageView
|
||||
android:layout_marginLeft="50dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/special_mode_view">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/database_color"
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="12dp"
|
||||
@@ -91,9 +94,9 @@
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/group_coordinator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@+id/toolbar"
|
||||
android:layout_above="@+id/toolbar_action">
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/toolbar_action"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar"
|
||||
@@ -159,7 +162,7 @@
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:visibility="gone"
|
||||
android:theme="?attr/toolbarActionAppearance"
|
||||
android:layout_alignParentBottom="true" />
|
||||
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -177,9 +180,10 @@
|
||||
layout="@layout/view_button_lock"
|
||||
android:layout_width="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
|
||||
android:id="@+id/database_nav_view"
|
||||
@@ -190,4 +194,4 @@
|
||||
app:subheaderColor="?attr/colorAccent"
|
||||
android:fitsSystemWindows="true" />
|
||||
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:layout_gravity="bottom"
|
||||
android:theme="?attr/toolbarActionAppearance"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/icon_picker_upload"
|
||||
@@ -53,15 +53,17 @@
|
||||
android:contentDescription="@string/validate"
|
||||
android:src="@drawable/ic_file_upload_white_24dp"
|
||||
app:fabSize="mini"
|
||||
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_constraintStart_toStartOf="parent"/>
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/toolbar" />
|
||||
|
||||
<include
|
||||
layout="@layout/view_button_lock"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<include layout="@layout/view_screenshot_mode_banner" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar">
|
||||
|
||||
<ProgressBar
|
||||
@@ -30,4 +30,6 @@
|
||||
android:layout_gravity="center"
|
||||
android:contentDescription="@string/entry_attachments" />
|
||||
</FrameLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<include layout="@layout/view_screenshot_mode_banner" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:layout_gravity="bottom"
|
||||
android:theme="?attr/toolbarActionAppearance"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/key_generator_validation"
|
||||
@@ -55,7 +55,7 @@
|
||||
android:tint="?attr/colorOnAccentColor"
|
||||
app:fabSize="mini"
|
||||
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_constraintStart_toStartOf="parent"/>
|
||||
|
||||
@@ -64,5 +64,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
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>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
You should have received a copy of the GNU General Public License
|
||||
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:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
@@ -28,6 +28,7 @@
|
||||
tools:targetApi="o">
|
||||
|
||||
<com.kunzisoft.keepass.view.SpecialModeView
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:id="@+id/special_mode_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -36,11 +37,11 @@
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/activity_password_coordinator_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/background_repeat"
|
||||
android:backgroundTint="?android:attr/textColor"
|
||||
android:layout_below="@+id/special_mode_view"
|
||||
android:layout_above="@+id/activity_password_footer">
|
||||
app:layout_constraintTop_toBottomOf="@+id/special_mode_view"
|
||||
app:layout_constraintBottom_toTopOf="@+id/activity_password_footer">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar"
|
||||
@@ -58,7 +59,7 @@
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:minHeight="144dp"
|
||||
android:minHeight="106dp"
|
||||
android:layout_marginTop="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary">
|
||||
<LinearLayout
|
||||
@@ -156,7 +157,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_alignParentBottom="true">
|
||||
app:layout_constraintBottom_toTopOf="@+id/screenshot_mode_banner">
|
||||
<LinearLayout
|
||||
android:id="@+id/activity_password_info_container"
|
||||
android:layout_width="match_parent"
|
||||
@@ -193,4 +194,5 @@
|
||||
android:text="@string/menu_open" />
|
||||
</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
|
||||
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/toolbar_coordinator"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:background="?android:attr/windowBackground"
|
||||
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
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
@@ -46,5 +53,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
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"
|
||||
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
|
||||
android:id="@+id/progress_dialog_bar"
|
||||
app:indicatorColor="?attr/colorAccent"
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<LinearLayout
|
||||
@@ -115,7 +116,7 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/keyfile_checkox"
|
||||
android:id="@+id/keyfile_checkbox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/entry_keyfile"/>
|
||||
@@ -126,9 +127,41 @@
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="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" />
|
||||
</LinearLayout>
|
||||
</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>
|
||||
</ScrollView>
|
||||
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">
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/keyfile_checkox"
|
||||
android:id="@+id/keyfile_checkbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignTop="@+id/keyfile_selection"
|
||||
@@ -76,9 +76,35 @@
|
||||
android:id="@+id/keyfile_selection"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"
|
||||
android:layout_toRightOf="@+id/keyfile_checkox"
|
||||
android:layout_toEndOf="@+id/keyfile_checkox"
|
||||
android:layout_toEndOf="@+id/keyfile_checkbox"
|
||||
android:layout_toRightOf="@+id/keyfile_checkbox"
|
||||
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:importantForAutofill="no" />
|
||||
</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>
|
||||
@@ -118,8 +118,8 @@
|
||||
<string name="select_to_copy">اختر لنسخ %1$s إلى الحافظة</string>
|
||||
<string name="retrieving_db_key">يجلب مفتاح قاعدة البيانات…</string>
|
||||
<string name="default_checkbox">استخدامها كقاعدة بيانات افتراضية</string>
|
||||
<string name="html_about_licence">KeePassDX © %1$d كونزيسوفت <strong>مفتوح المصدر</strong> و <strong>بدون اعلانات</strong>.
|
||||
\n يوزع كما هو، بدون ضمان, تحت ترخيص <strong>GPLv3</strong>.</string>
|
||||
<string name="html_about_licence">KeePassDX © %1$d كونزيسوفت <strong>مفتوح المصدر</strong> و <strong>بدون اعلانات</strong>.
|
||||
\n يوزع كما هو، بدون ضمان, تحت ترخيص <strong>GPLv3</strong>.</string>
|
||||
<string name="entry_accessed">نُفذ إليه</string>
|
||||
<string name="entry_expires">تنتهي صلاحيته في</string>
|
||||
<string name="entry_keyfile">ملف المفتاح</string>
|
||||
|
||||
@@ -300,15 +300,15 @@
|
||||
<string name="education_read_only_title">Protecció contra escriptura de la base de dades</string>
|
||||
<string name="education_donation_summary">Ajudeu a augmentar l’estabilitat i la seguretat i a crear més funcionalitats.</string>
|
||||
<string name="education_donation_title">Participació</string>
|
||||
<string name="html_text_dev_feature_buy_pro">En comprar la versió <strong>professional</strong>,</string>
|
||||
<string name="html_text_dev_feature_contibute">En <strong>col·laborar-hi</strong>,</string>
|
||||
<string name="html_text_dev_feature_buy_pro">En comprar la versió <strong>professional</strong>,</string>
|
||||
<string name="html_text_dev_feature_contibute">En <strong>col·laborar-hi</strong>,</string>
|
||||
<string name="content_description_keyfile_checkbox">Casella del fitxer de la clau</string>
|
||||
<string name="content_description_password_checkbox">Casella de la contrasenya</string>
|
||||
<string name="content_description_otp_information">Informació de la contrasenya d’un sol ús</string>
|
||||
<string name="content_description_credentials_information">Informació de les dades d’accés</string>
|
||||
<string name="content_description_add_item">Afegeix un element</string>
|
||||
<string name="education_lock_title">Bloca la base de dades</string>
|
||||
<string name="html_text_feature_generosity">Aquest <strong>estil visual</strong> és disponible gràcies a la vostra generositat.</string>
|
||||
<string name="html_text_feature_generosity">Aquest <strong>estil visual</strong> és disponible gràcies a la vostra generositat.</string>
|
||||
<string name="html_text_dev_feature_upgrade">No us oblideu de mantenir l’aplicació actualitzada instal·lant les versions noves.</string>
|
||||
<string name="icon_section_standard">Estàndard</string>
|
||||
<string name="show_uuid_title">Mostra l’UUID</string>
|
||||
|
||||
@@ -263,13 +263,13 @@
|
||||
<string name="education_sort_summary">Vyberte řazení položek a skupin.</string>
|
||||
<string name="education_donation_title">Zapojit se</string>
|
||||
<string name="education_donation_summary">Zapojte se a pomozte zvýšit stabilitu, zabezpečení a doplnění dalších funkcí.</string>
|
||||
<string name="html_text_ad_free">Na rozdíl od mnoha aplikací pro správu hesel je tato <strong>bez reklam</strong>, je <strong>svobodný software pod copyleft licencí</strong> a nesbírá žádné osobní údaje na svých serverech bez ohledu na to, jakou verzi používáte.</string>
|
||||
<string name="html_text_buy_pro">Zakoupením varianty „pro“ získáte přístup k tomuto <strong>vizuálnímu stylu</strong> a hlavně pomůžete <strong>uskutečnění komunitních projektů.</strong></string>
|
||||
<string name="html_text_ad_free">Na rozdíl od mnoha aplikací pro správu hesel je tato <strong>bez reklam</strong>, je <strong>svobodný software pod copyleft licencí</strong> a nesbírá žádné osobní údaje na svých serverech bez ohledu na to, jakou verzi používáte.</string>
|
||||
<string name="html_text_buy_pro">Zakoupením varianty „pro“ získáte přístup k tomuto <strong>vizuálnímu stylu</strong> a hlavně pomůžete <strong>uskutečnění komunitních projektů.</strong></string>
|
||||
<string name="html_text_feature_generosity">Tento <strong>vizuální styl</strong> je k dispozici díky vaší štědrosti.</string>
|
||||
<string name="html_text_donation">Pro zajištění svobody nás všech a pokračování aktivity počítáme s Vaším <strong>přispěním.</strong></string>
|
||||
<string name="html_text_dev_feature">Tato funkce je <strong>ve vývoji</strong> a potřebuje Váš <strong>příspěvek</strong>, aby byla brzy k dispozici.</string>
|
||||
<string name="html_text_dev_feature_buy_pro">Zakoupením <strong>pro</strong> varianty,</string>
|
||||
<string name="html_text_dev_feature_contibute"><strong>Podpořením vývoje</strong>,</string>
|
||||
<string name="html_text_dev_feature_contibute"><strong>Podpořením vývoje</strong>,</string>
|
||||
<string name="html_text_dev_feature_encourage">povzbudíte vývojáře k doplnění <strong>nových funkcí</strong> a <strong>opravám chyb</strong> dle vašich připomínek.</string>
|
||||
<string name="html_text_dev_feature_thanks">Mnohokrát děkujeme za Váš příspěvek.</string>
|
||||
<string name="html_text_dev_feature_work_hard">Tvrdě pracujeme na brzkém vydání této funkce.</string>
|
||||
@@ -419,9 +419,9 @@
|
||||
<string name="hide_broken_locations_title">Skrýt chybné odkazy na databáze</string>
|
||||
<string name="hide_broken_locations_summary">Skrýt chybné odkazy v seznamu nedávných databází</string>
|
||||
<string name="warning_database_read_only">Udělit právo zápisu pro uložení změn v databázi</string>
|
||||
<string name="html_about_licence">KeePassDX © %1$d Kunzisoft je <strong>open source</strong> a <strong>bez reklam</strong>.
|
||||
\nJe poskytován jak je, od licencí <strong>GPLv3</strong>, bez jakékoli záruky.</string>
|
||||
<string name="html_about_contribution">Abychom si <strong>udrželi svoji svobodu</strong>, <strong>mohli opravovat chyby</strong>, <strong>přidávat nové funkce</strong> a <strong>byli pořád aktivní</strong>, počítáme s Vaším <strong>přispěním</strong>.</string>
|
||||
<string name="html_about_licence">KeePassDX © %1$d Kunzisoft je <strong>open source</strong> a <strong>bez reklam</strong>.
|
||||
\nJe poskytován jak je, od licencí <strong>GPLv3</strong>, bez jakékoli záruky.</string>
|
||||
<string name="html_about_contribution">Abychom si <strong>udrželi svoji svobodu</strong>, <strong>mohli opravovat chyby</strong>, <strong>přidávat nové funkce</strong> a <strong>byli pořád aktivní</strong>, počítáme s Vaším <strong>přispěním</strong>.</string>
|
||||
<string name="error_create_database">Nepodařilo se vytvořit soubor databáze.</string>
|
||||
<string name="entry_add_attachment">Přidat přílohu</string>
|
||||
<string name="discard">Zahodit</string>
|
||||
|
||||
@@ -265,7 +265,7 @@
|
||||
<string name="html_text_ad_free">I modsætning til andre programmer til adgangskodeadministration er denne <strong>annoncefri</strong>, <strong>copyleft fri software</strong>, og indsamler ikke personlige data, uanset hvilken version der bruges.</string>
|
||||
<string name="html_text_buy_pro">Ved at købe pro-versionen, er der adgang til <strong>visuel stil</strong>, og det vil især hjælpe <strong>gennemførelsen af lokale projekter.</strong></string>
|
||||
<string name="html_text_feature_generosity">Denne <strong>visuelle stil</strong> er tilgængelige takket være bidrag.</string>
|
||||
<string name="html_text_donation">For at bevare uafhængighed og altid at være aktiv, håber vi på dit <strong>bidrag.</strong></string>
|
||||
<string name="html_text_donation">For at bevare uafhængighed og altid at være aktiv, håber vi på dit <strong>bidrag.</strong></string>
|
||||
<string name="html_text_dev_feature">Funktionen er <strong>under udvikling</strong>, og det kræver <strong>bidrag</strong>, for snart at være tilgængelig.</string>
|
||||
<string name="html_text_dev_feature_buy_pro">Ved at købe <strong>pro</strong> versionen,</string>
|
||||
<string name="html_text_dev_feature_contibute">Ved at <strong>bidrage</strong>,</string>
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
</string-array>
|
||||
<string name="warning_empty_password">Soll das Entsperren ohne Passwort wirklich möglich sein\?</string>
|
||||
<string name="warning_no_encryption_key">Soll wirklich kein Verschlüsselungsschlüssel verwendet werden\?</string>
|
||||
<string name="menu_appearance_settings">Aussehen</string>
|
||||
<string name="menu_appearance_settings">Erscheinungsbild</string>
|
||||
<string name="password_size_title">Generierte Passwortlänge</string>
|
||||
<string name="password_size_summary">Legt die Standardlänge des generierten Passworts fest</string>
|
||||
<string name="clipboard_notifications_title">Zwischenablage-Benachrichtigung</string>
|
||||
@@ -162,7 +162,7 @@
|
||||
<string name="path">Pfad</string>
|
||||
<string name="file_name">Dateiname</string>
|
||||
<string name="unavailable_feature_text">Dieses Feature konnte nicht gestartet werden.</string>
|
||||
<string name="biometric_unlock_enable_summary">Ermöglicht Ihre Biometrie zu scannen, um die Datenbank zu öffnen.</string>
|
||||
<string name="biometric_unlock_enable_summary">Ermöglicht das Scannen Ihrer biometrischen Daten, um die Datenbank zu öffnen</string>
|
||||
<string name="advanced_unlock">Moderne Entsperrung</string>
|
||||
<string name="biometric_unlock_enable_title">Biometrische Entsperrung</string>
|
||||
<string name="lock">Sperren</string>
|
||||
@@ -252,7 +252,7 @@
|
||||
<string name="education_donation_summary">Mithelfen, um Stabilität und Sicherheit zu verbessern sowie weitere Funktionen zu ermöglichen.</string>
|
||||
<string name="html_text_ad_free">Anders als viele andere Passwortmanager ist dieser <strong>werbefrei</strong>, <strong>quelloffen</strong> und unter einer <strong>Copyleft-Lizenz</strong>. Es werden keine persönlichen Daten gesammelt, in welcher Form auch immer, unabhängig von der verwendeten Version (kostenlos oder Pro).</string>
|
||||
<string name="html_text_buy_pro">Mit dem Kauf der Pro-Version erhalten Sie Zugriff auf diesen <strong>visuellen Stil</strong> und unterstützen insbesondere <strong>die Umsetzung gemeinschaftlicher Projekte.</strong></string>
|
||||
<string name="html_text_feature_generosity">Dieser <strong>visuelle Stil</strong> ist dank Ihrer Großzügigkeit verfügbar.</string>
|
||||
<string name="html_text_feature_generosity">Dieser <strong>visuelle Stil</strong> ist dank Ihrer Großzügigkeit verfügbar.</string>
|
||||
<string name="html_text_donation">Um unsere Freiheit zu bewahren und immer aktiv zu bleiben, zählen wir auf Ihren <strong>Beitrag.</strong></string>
|
||||
<string name="html_text_dev_feature">Diese Funktion ist <strong>in Entwicklung</strong> und erfordert <strong>Ihren Beitrag</strong>, um bald verfügbar zu sein.</string>
|
||||
<string name="html_text_dev_feature_buy_pro">Durch den Kauf der <strong>Pro-Version</strong>,</string>
|
||||
@@ -260,7 +260,7 @@
|
||||
<string name="html_text_dev_feature_encourage">Sie ermutigen die Entwickler:innen, <strong>neue Funktionen</strong> einzuführen und gemäß Ihren Anmerkungen <strong>Fehler zu beheben</strong>.</string>
|
||||
<string name="html_text_dev_feature_thanks">Vielen Dank für Ihre Unterstützung.</string>
|
||||
<string name="html_text_dev_feature_work_hard">Wir bemühen uns, diese Funktion bald zu veröffentlichen.</string>
|
||||
<string name="html_text_dev_feature_upgrade">Denken Sie daran, Ihre App auf dem neuesten Stand zu halten, indem Sie neue Versionen installieren.</string>
|
||||
<string name="html_text_dev_feature_upgrade">Denken Sie daran, Ihre App durch die Installation neuer Versionen auf dem aktuellsten Stand zu halten.</string>
|
||||
<string name="download">Download</string>
|
||||
<string name="contribute">Unterstützen</string>
|
||||
<string name="icon_pack_choose_title">Symbolpaket</string>
|
||||
@@ -285,7 +285,7 @@
|
||||
\n„Schreibgeschützt“ verhindert unbeabsichtigte Änderungen an der Datenbank.
|
||||
\nMit „Änderbar“ können Sie alle Elemente nach Belieben hinzufügen, löschen oder ändern.</string>
|
||||
<string name="edit_entry">Eintrag bearbeiten</string>
|
||||
<string name="error_load_database">Datenbank kann nicht geladen werden.</string>
|
||||
<string name="error_load_database">Die Datenbank konnte nicht geladen werden.</string>
|
||||
<string name="error_load_database_KDF_memory">Laden des Schlüssels fehlgeschlagen. Bitte versuchen, die „Speicherplatznutzung“ von KDF zu verringern.</string>
|
||||
<string name="list_entries_show_username_title">Benutzernamen anzeigen</string>
|
||||
<string name="list_entries_show_username_summary">Benutzernamen in Eintragslisten anzeigen</string>
|
||||
@@ -303,16 +303,16 @@
|
||||
<string name="keyboard_notification_entry_content_text">%1$s</string>
|
||||
<string name="keyboard_notification_entry_clear_close_title">Beim Schließen löschen</string>
|
||||
<string name="keyboard_notification_entry_clear_close_summary">Datenbank schließen, wenn die Benachrichtigung geschlossen wird</string>
|
||||
<string name="keyboard_appearance_category">Aussehen</string>
|
||||
<string name="keyboard_appearance_category">Erscheinungsbild</string>
|
||||
<string name="keyboard_theme_title">Tastaturdesign</string>
|
||||
<string name="keyboard_keys_category">Tasten</string>
|
||||
<string name="keyboard_key_vibrate_title">Vibrierende Tastendrücke</string>
|
||||
<string name="keyboard_key_sound_title">Hörbare Tastendrücke</string>
|
||||
<string name="selection_mode">Auswahlmodus</string>
|
||||
<string name="remember_database_locations_title">Datenbank-Speicherorte merken</string>
|
||||
<string name="remember_database_locations_summary">Verfolgt, wo Datenbanken gespeichert sind</string>
|
||||
<string name="remember_database_locations_summary">Verfolgt den Speicherort der Datenbanken</string>
|
||||
<string name="remember_keyfile_locations_title">Schlüsseldatei-Speicherorte merken</string>
|
||||
<string name="remember_keyfile_locations_summary">Verfolgt, wo Schlüsseldateien gespeichert sind</string>
|
||||
<string name="remember_keyfile_locations_summary">Verfolgt den Speicherort der Schlüsseldateien</string>
|
||||
<string name="show_recent_files_title">Zuletzt verwendete Dateien anzeigen</string>
|
||||
<string name="show_recent_files_summary">Speicherort zuletzt verwendeter Datenbanken anzeigen</string>
|
||||
<string name="hide_broken_locations_title">Defekte Datenbankverknüpfungen ausblenden</string>
|
||||
@@ -349,7 +349,7 @@
|
||||
<string name="content_description_background">Hintergrund</string>
|
||||
<string name="content_description_update_from_list">Aktualisieren</string>
|
||||
<string name="content_description_keyboard_close_fields">Felder schließen</string>
|
||||
<string name="error_create_database_file">Es ist nicht möglich, eine Datenbank mit diesem Passwort und dieser Schlüsseldatei zu erstellen.</string>
|
||||
<string name="error_create_database_file">Die Datenbank kann mit diesem Passwort und dieser Schlüsseldatei nicht erstellt werden.</string>
|
||||
<string name="menu_advanced_unlock_settings">Modernes Entsperren</string>
|
||||
<string name="biometric">Biometrisch</string>
|
||||
<string name="enable">Aktivieren</string>
|
||||
@@ -385,7 +385,7 @@
|
||||
<string name="contains_duplicate_uuid_procedure">Problem lösen, indem neue UUIDs für Duplikate generiert werden und danach fortfahren\?</string>
|
||||
<string name="database_opened">Datenbank geöffnet</string>
|
||||
<string name="clipboard_explanation_summary">Eintragsfelder mithilfe der Zwischenablage des Geräts kopieren</string>
|
||||
<string name="advanced_unlock_explanation_summary">Modernes Entsperren verwenden, um eine Datenbank einfacher zu öffnen.</string>
|
||||
<string name="advanced_unlock_explanation_summary">Modernes Entsperren verwenden, um eine Datenbank einfacher zu öffnen</string>
|
||||
<string name="database_data_compression_title">Datenkompression</string>
|
||||
<string name="database_data_compression_summary">Datenkompression reduziert die Datenbankgröße</string>
|
||||
<string name="max_history_items_title">Maximale Anzahl</string>
|
||||
@@ -416,8 +416,8 @@
|
||||
<string name="entry_attachments">Anhänge</string>
|
||||
<string name="menu_restore_entry_history">Historie wiederherstellen</string>
|
||||
<string name="menu_delete_entry_history">Historie löschen</string>
|
||||
<string name="keyboard_auto_go_action_title">Auto-Key-Aktion</string>
|
||||
<string name="keyboard_auto_go_action_summary">Aktion der Go-Taste, die automatisch nach dem Drücken einer Feldtaste ausgeführt wird</string>
|
||||
<string name="keyboard_auto_go_action_title">Automatische Tastenaktion</string>
|
||||
<string name="keyboard_auto_go_action_summary">Nach dem Drücken einer Feldtaste automatisch die Eingabetaste ausführen</string>
|
||||
<string name="download_attachment">%1$s herunterladen</string>
|
||||
<string name="download_initialization">Initialisieren …</string>
|
||||
<string name="download_progression">Fortschritt: %1$d%%</string>
|
||||
@@ -440,7 +440,7 @@
|
||||
<string name="warning_database_read_only">Datei Schreibrechte gewähren, um Datenbankänderungen zu speichern</string>
|
||||
<string name="education_setup_OTP_summary">Einrichten einer Einmal-Passwortverwaltung (HOTP / TOTP), um ein Token zu generieren, das für die Zwei-Faktor-Authentifizierung (2FA) angefordert wird.</string>
|
||||
<string name="education_setup_OTP_title">OTP einrichten</string>
|
||||
<string name="error_create_database">Es ist nicht möglich, eine Datenbankdatei zu erstellen.</string>
|
||||
<string name="error_create_database">Die Datenbankdatei kann nicht erstellt werden.</string>
|
||||
<string name="entry_add_attachment">Anhang hinzufügen</string>
|
||||
<string name="discard">Verwerfen</string>
|
||||
<string name="discard_changes">Änderungen verwerfen\?</string>
|
||||
@@ -464,7 +464,7 @@
|
||||
<string name="content_description_add_item">Element hinzufügen</string>
|
||||
<string name="filter">Filter</string>
|
||||
<string name="keyboard_change">Tastatur wechseln</string>
|
||||
<string name="keyboard_previous_fill_in_title">Auto-Key-Aktion</string>
|
||||
<string name="keyboard_previous_fill_in_title">Automatische Tastenaktion</string>
|
||||
<string name="keyboard_previous_database_credentials_title">Datenbank-Anmeldebildschirm</string>
|
||||
<string name="keyboard_previous_fill_in_summary">Nach dem Ausführen der automatischen Tastenaktion automatisch zur vorherigen Tastatur wechseln</string>
|
||||
<string name="keyboard_previous_database_credentials_summary">Auf dem Datenbank-Anmeldebildschirm automatisch zur vorherigen Tastatur wechseln</string>
|
||||
@@ -531,7 +531,7 @@
|
||||
<string name="device_credential_unlock_enable_title">Geräteanmeldedaten entsperren</string>
|
||||
<string name="device_credential">Geräteanmeldedaten</string>
|
||||
<string name="credential_before_click_advanced_unlock_button">Geben Sie das Passwort ein und klicken Sie dann auf diesen Knopf.</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">Dialog für modernes Entsperren konnte nicht gestartet werden.</string>
|
||||
<string name="advanced_unlock_prompt_not_initialized">Der Dialog für modernes Entsperren konnte nicht gestartet werden.</string>
|
||||
<string name="advanced_unlock_scanning_error">Fehler beim modernen Entsperren: %1$s</string>
|
||||
<string name="advanced_unlock_not_recognized">Abdruck zum modernen Entsperren nicht erkannt</string>
|
||||
<string name="advanced_unlock_invalid_key">Schlüssel zum modernen Entsperren nicht lesbar. Bitte löschen Sie ihn und wiederholen Sie die Prozedur zur Entsperrerkennung.</string>
|
||||
@@ -658,4 +658,25 @@
|
||||
<string name="character_count">Anzahl der Zeichen: %1$d</string>
|
||||
<string name="exclude_ambiguous_chars">Mehrdeutige Zeichen ausschließen</string>
|
||||
<string name="title_case">Groß-/Kleinschreibung des Titels</string>
|
||||
<string name="content_description_hardware_key_checkbox">Hardwareschlüssel-Kontrollkästchen</string>
|
||||
<string name="hardware_key">Hardwareschlüssel</string>
|
||||
<string name="error_no_hardware_key">Hardwareschlüssel auswählen.</string>
|
||||
<string name="error_XML_malformed">XML fehlerhaft.</string>
|
||||
<string name="waiting_challenge_response">Warte auf die Response-Antwort …</string>
|
||||
<string name="waiting_challenge_request">Warte auf die Challenge-Aufgabe …</string>
|
||||
<string name="error_cancel_by_user">Vom Benutzer abgebrochen.</string>
|
||||
<string name="error_driver_required">Treiber für %1$s ist erforderlich.</string>
|
||||
<string name="error_unable_merge_database_kdb">Die Zusammenführung aus einer Datenbank V1 ist nicht möglich.</string>
|
||||
<string name="error_location_unknown">Der Speicherort der Datenbank ist unbekannt, Datenbank-Aktion kann nicht ausgeführt werden.</string>
|
||||
<string name="error_hardware_key_unsupported">Der Hardwareschlüssel wird nicht unterstützt.</string>
|
||||
<string name="error_empty_key">Der Schlüssel darf nicht leer sein.</string>
|
||||
<string name="corrupted_file">Die Datei ist beschädigt.</string>
|
||||
<string name="error_no_response_from_challenge">Die Response-Antwort kann nicht abgerufen werden.</string>
|
||||
<string name="error_challenge_already_requested">Die Challenge-Aufgabe wurde bereits angefordert.</string>
|
||||
<string name="error_response_already_provided">Die Response-Antwort wurde bereits übertragen.</string>
|
||||
<string name="enable_screenshot_mode_title">Screenshot-Modus</string>
|
||||
<string name="enable_screenshot_mode_summary">Erlauben Sie Apps von Drittanbietern, Screenshots der Anwendung aufzuzeichnen oder zu erstellen</string>
|
||||
<string name="screenshot_mode_banner_text">Screenshot-Modus</string>
|
||||
<string name="remember_hardware_key_title">Hardwareschlüssel merken</string>
|
||||
<string name="remember_hardware_key_summary">Verfolgt die verwendeten Hardwareschlüssel</string>
|
||||
</resources>
|
||||
@@ -267,7 +267,7 @@
|
||||
\nΤο \"Προστατευμένο από εγγραφή\" αποτρέπει τυχόν μη επιθυμητές αλλαγές στη βάση δεδομένων.
|
||||
\nΤο \"Τροποποιητικό\" σάς επιτρέπει να προσθέσετε, να διαγράψετε ή να τροποποιήσετε όλα τα στοιχεία όπως επιθυμείτε.</string>
|
||||
<string name="edit_entry">Επεξεργασία καταχώρησης</string>
|
||||
<string name="error_load_database">Δεν ήταν δυνατή η φόρτωση της βάσης δεδομένων σας.</string>
|
||||
<string name="error_load_database">Δεν ήταν δυνατή η φόρτωση της βάσης δεδομένων.</string>
|
||||
<string name="error_load_database_KDF_memory">Δεν ήταν δυνατή η φόρτωση του κλειδιού. Προσπαθήστε να μειώσετε την KDF \"Χρήση μνήμης\".</string>
|
||||
<string name="list_entries_show_username_title">Εμφάνιση ονομάτων χρηστών</string>
|
||||
<string name="list_entries_show_username_summary">Εμφάνιση ονομάτων χρηστών σε λίστες καταχώρησης</string>
|
||||
@@ -645,4 +645,26 @@
|
||||
<string name="upper_case">ΚΕΦΑΛΑΙΑ ΓΡΑΜΜΑΤΑ</string>
|
||||
<string name="word_separator">Διαχωριστής</string>
|
||||
<string name="character_count">Αριθμός χαρακτήρων: %1$d</string>
|
||||
<string name="error_no_hardware_key">Επιλέξτε ένα κλειδί υλικού.</string>
|
||||
<string name="error_XML_malformed">Το XML είναι εσφαλμένο.</string>
|
||||
<string name="error_cancel_by_user">Ακυρώθηκε από τον χρήστη.</string>
|
||||
<string name="error_driver_required">Απαιτείται πρόγραμμα οδήγησης για %1$s.</string>
|
||||
<string name="error_hardware_key_unsupported">Το κλειδί υλικού δεν υποστηρίζεται.</string>
|
||||
<string name="remember_hardware_key_title">Να θυμάται τα κλειδιά υλικού</string>
|
||||
<string name="remember_hardware_key_summary">Παρακολουθεί τα κλειδιά υλικού που χρησιμοποιούνται</string>
|
||||
<string name="enable_screenshot_mode_summary">Επιτρέψτε σε εφαρμογές τρίτων να καταγράφουν ή να λαμβάνουν στιγμιότυπα οθόνης της εφαρμογής</string>
|
||||
<string name="waiting_challenge_request">Αναμονή για το αίτημα πρόκλησης…</string>
|
||||
<string name="waiting_challenge_response">Αναμονή για το αίτημα πρόκλησης…</string>
|
||||
<string name="content_description_hardware_key_checkbox">Πλαίσιο ελέγχου κλειδιού υλικού</string>
|
||||
<string name="hardware_key">Κλειδί υλικού</string>
|
||||
<string name="error_no_response_from_challenge">Δεν είναι δυνατή η λήψη της απάντησης από την πρόκληση.</string>
|
||||
<string name="enable_screenshot_mode_title">Λειτουργία στιγμιότυπου οθόνης</string>
|
||||
<string name="screenshot_mode_banner_text">Λειτουργία στιγμιότυπου οθόνης</string>
|
||||
<string name="error_challenge_already_requested">Η πρόκληση έχει ήδη ζητηθεί</string>
|
||||
<string name="error_response_already_provided">Η απάντηση έχει ήδη δοθεί.</string>
|
||||
<string name="error_location_unknown">Η θέση της βάσης δεδομένων είναι άγνωστη, η ενέργεια της βάσης δεδομένων δεν μπορεί να εκτελεστεί.</string>
|
||||
<string name="error_unable_merge_database_kdb">Δεν είναι δυνατή η συγχώνευση από μια βάση δεδομένων V1.</string>
|
||||
<string name="error_empty_key">Το κλειδί δεν μπορεί να είναι κενό.</string>
|
||||
<string name="corrupted_file">Κατεστραμμένο αρχείο.</string>
|
||||
<string name="html_about_privacy"><strong>Δεν ανακτώνται δεδομένα χρήστη</strong>, αυτή η εφαρμογή δεν συνδέεται με κανένα διακομιστή, λειτουργεί μόνο τοπικά και σέβεται πλήρως το απόρρητο των χρηστών.</string>
|
||||
</resources>
|
||||
@@ -20,5 +20,34 @@
|
||||
\n
|
||||
\nGroups (~folders) organise entries in your database.</string>
|
||||
<string name="error_otp_type">The existing OTP type is not recognised by this form, its validation may no longer correctly generate the token.</string>
|
||||
<string name="html_text_buy_pro">By buying the pro version, you will have access to this <strong>visual style</strong> and you will especially help <strong>the realisation of community projects.</strong></string>
|
||||
<string name="html_text_buy_pro">By buying the pro version, you will have access to this <strong>visual style</strong> and you will especially help <strong>the realisation of community projects.</strong></string>
|
||||
<string name="key_derivation_function">Key derivation function</string>
|
||||
<string name="feedback">Feedback</string>
|
||||
<string name="homepage">Homepage</string>
|
||||
<string name="accept">Accept</string>
|
||||
<string name="add_entry">Add entry</string>
|
||||
<string name="edit_entry">Edit entry</string>
|
||||
<string name="add_group">Add group</string>
|
||||
<string name="master_key">Master key</string>
|
||||
<string name="security">Security</string>
|
||||
<string name="encryption">Encryption</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="contribution">Contribution</string>
|
||||
<string name="about_description">password</string>
|
||||
<string name="encryption_algorithm">Encryption</string>
|
||||
<string name="app_timeout">Timeout</string>
|
||||
<string name="app_timeout_summary">database</string>
|
||||
<string name="application">App</string>
|
||||
<string name="brackets">Brackets</string>
|
||||
<string name="extended_ASCII">Extended ASCII</string>
|
||||
<string name="allow">Allow</string>
|
||||
<string name="content_description_background">Background</string>
|
||||
<string name="content_description_open_file">Open file</string>
|
||||
<string name="content_description_node_children">Node children</string>
|
||||
<string name="content_description_add_node">Add node</string>
|
||||
<string name="content_description_add_entry">Add entry</string>
|
||||
<string name="content_description_add_group">Add group</string>
|
||||
<string name="content_description_add_item">Add item</string>
|
||||
<string name="content_description_file_information">File info</string>
|
||||
<string name="content_description_credentials_information">Credentials info</string>
|
||||
</resources>
|
||||
@@ -243,9 +243,9 @@
|
||||
<string name="education_sort_summary">Ordenar registros y grupos de acuerdo a parámetros específicos.</string>
|
||||
<string name="education_donation_title">Participar</string>
|
||||
<string name="education_donation_summary">Participe para aumentar la estabilidad, la seguridad y agregar más funciones.</string>
|
||||
<string name="html_text_ad_free">A diferencia de muchas aplicaciones de gestión de contraseñas, esta <strong>no tiene publicidad</strong>, es <strong>libre, con licencia «copyleft»</strong> y no recopila datos personales en sus servidores, sin importar la versión que use.</string>
|
||||
<string name="html_text_ad_free">A diferencia de muchas aplicaciones de gestión de contraseñas, esta <strong>no tiene publicidad</strong>, es <strong>libre, con licencia «copyleft»</strong> y no recopila datos personales en sus servidores, sin importar la versión que use.</string>
|
||||
<string name="html_text_buy_pro">Al comprar la versión pro, tendrá acceso al <strong>estilo visual </strong>y ayudará especialmente a <strong>la realización de proyectos comunitarios.</strong></string>
|
||||
<string name="html_text_feature_generosity">Este <strong>estilo visual</strong> está disponible gracias a su generosidad.</string>
|
||||
<string name="html_text_feature_generosity">Este <strong>estilo visual</strong> está disponible gracias a su generosidad.</string>
|
||||
<string name="html_text_donation">Para mantener nuestra libertad y estar siempre vigente, contamos con tu <strong>contribución.</strong></string>
|
||||
<string name="html_text_dev_feature">Esta función está <strong>en desarrollo</strong> y requiere de tu <strong>contribución</strong> para estar disponible dentro de poco.</string>
|
||||
<string name="html_text_dev_feature_buy_pro">Al comprar la versión <strong>pro</strong>,</string>
|
||||
@@ -499,7 +499,7 @@
|
||||
<string name="autofill_application_id_blocklist_title">Lista de bloqueo de las aplicaciones</string>
|
||||
<string name="autofill_ask_to_save_data_summary">Solicitar datos de guardado al completar el llenado de un formulario</string>
|
||||
<string name="autofill_ask_to_save_data_title">Pedir que se guarden los datos</string>
|
||||
<string name="autofill_save_search_info_summary">Intente guardar la información de la búsqueda cuando haga una selección de entrada manual</string>
|
||||
<string name="autofill_save_search_info_summary">Trate de guardar la información de búsqueda al hacer una selección de entrada manual para facilitar los usos futuros</string>
|
||||
<string name="autofill_save_search_info_title">Guardar la información de la búsqueda</string>
|
||||
<string name="autofill_close_database_summary">Cerrar la base de datos después de una selección de autocompletado</string>
|
||||
<string name="autofill_close_database_title">Cerrar la base de datos</string>
|
||||
@@ -515,7 +515,7 @@
|
||||
<string name="keyboard_previous_database_credentials_summary">Cambiar automáticamente al teclado anterior en la pantalla de credenciales de la base de datos</string>
|
||||
<string name="keyboard_previous_database_credentials_title">Pantalla de credenciales de la base de datos</string>
|
||||
<string name="keyboard_auto_go_action_title">Acción de la tecla automática</string>
|
||||
<string name="keyboard_save_search_info_summary">Tras compartir informe a KeePassDX, cuando esté seleccionado un apunte, intente guardar el informe dentro del apunte para posibles futuros usos</string>
|
||||
<string name="keyboard_save_search_info_summary">Trate de guardar la información compartida al hacer una selección de entrada manual para facilitar los usos futuros</string>
|
||||
<string name="keyboard_save_search_info_title">Guardar información compartida</string>
|
||||
<string name="show_uuid_summary">Muestra el UUID vinculado a una entrada o a un grupo</string>
|
||||
<string name="show_uuid_title">Mostrar UUID</string>
|
||||
@@ -607,6 +607,8 @@
|
||||
<string name="warning_keyfile_integrity">El hash del archivo no está garantizado porque Android puede cambiar sus datos sobre la marcha. Cambia la extensión del archivo a .bin para una correcta integridad.</string>
|
||||
<string name="enable_keep_screen_on_title">Mantener la pantalla encendida</string>
|
||||
<string name="enable_keep_screen_on_summary">Mantenga la pantalla encendida cuando vea la entrada</string>
|
||||
<string name="enable_screenshot_mode_title">Modo captura de pantalla</string>
|
||||
<string name="enable_screenshot_mode_summary">Permitir que otras aplicaciones graben o tomen capturas de pantalla de la aplicación</string>
|
||||
<string name="show_entry_colors_summary">Muestra los colores de primer y segundo plano en una entrada</string>
|
||||
<string name="show_entry_colors_title">Colores de entrada</string>
|
||||
<string name="menu_merge_database">Fusionar datos</string>
|
||||
@@ -647,4 +649,5 @@
|
||||
<string name="upper_case">MAYÚSCULAS</string>
|
||||
<string name="title_case">Tipo Titular</string>
|
||||
<string name="character_count">Conteo de caracteres: %1$d</string>
|
||||
</resources>
|
||||
<string name="screenshot_mode_banner_text">Modo captura de pantalla</string>
|
||||
</resources>
|
||||
|
||||