mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
201 Commits
4.1.6
...
feature/Pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01d778650c | ||
|
|
dd389dbab1 | ||
|
|
272ebd0c3f | ||
|
|
0aecc21f43 | ||
|
|
1e7e464e65 | ||
|
|
d5c378ac85 | ||
|
|
672f1ca37d | ||
|
|
2f9e1e4bf2 | ||
|
|
25d97e4f2e | ||
|
|
f49dcbd654 | ||
|
|
bf2d56b4fd | ||
|
|
5893541dd2 | ||
|
|
2230fe66ab | ||
|
|
84a62a32ff | ||
|
|
da8ef9340c | ||
|
|
af068349e4 | ||
|
|
56cb5953dd | ||
|
|
2fc2a9c7c1 | ||
|
|
69e7cdbc47 | ||
|
|
39d9a74a73 | ||
|
|
7212c73481 | ||
|
|
3ee4caa153 | ||
|
|
28e4d929bb | ||
|
|
803d637510 | ||
|
|
ccd5da0962 | ||
|
|
36e3b85400 | ||
|
|
cd73880e21 | ||
|
|
8337f98f3a | ||
|
|
47fbb562b7 | ||
|
|
a46251be7b | ||
|
|
ef98e8a2db | ||
|
|
e8ec27dc38 | ||
|
|
30dd7c567c | ||
|
|
e562694606 | ||
|
|
464bc1442d | ||
|
|
c1730353d0 | ||
|
|
55e32e4ac5 | ||
|
|
96ed9fc7a6 | ||
|
|
5fda628c9c | ||
|
|
17742e25a9 | ||
|
|
8086289e4b | ||
|
|
65157f661f | ||
|
|
5df637d01f | ||
|
|
8084920b9e | ||
|
|
b196145578 | ||
|
|
ac347db2d1 | ||
|
|
013c437cf7 | ||
|
|
1f600d60e3 | ||
|
|
a6af9976fc | ||
|
|
05c480b6d3 | ||
|
|
d5ecaeb331 | ||
|
|
db8b0100de | ||
|
|
5f41177a1f | ||
|
|
fb909dac52 | ||
|
|
a8130d67be | ||
|
|
754d195e26 | ||
|
|
074910ea19 | ||
|
|
988b18b515 | ||
|
|
8924254c25 | ||
|
|
0db2b7023e | ||
|
|
a2c2a21dde | ||
|
|
d7a3e7fedd | ||
|
|
2bedbf8a6c | ||
|
|
437a704bc8 | ||
|
|
a3bd5e1593 | ||
|
|
3feb177afc | ||
|
|
821f35fe05 | ||
|
|
d36f675da7 | ||
|
|
b7f9690a38 | ||
|
|
5e4ee167fc | ||
|
|
c911b7c511 | ||
|
|
c79d1f1b81 | ||
|
|
daf717becd | ||
|
|
48d4483484 | ||
|
|
c861fe790c | ||
|
|
1a717bda03 | ||
|
|
b80acd5a2d | ||
|
|
7e41527cfe | ||
|
|
200881278c | ||
|
|
0d133ffdb0 | ||
|
|
c6b0ee27df | ||
|
|
0053726d0b | ||
|
|
1395af88d1 | ||
|
|
2e3ade1b4a | ||
|
|
90c43acfbf | ||
|
|
90b68fd972 | ||
|
|
f8787ba03d | ||
|
|
4f10d13691 | ||
|
|
ef6aeceb20 | ||
|
|
ef8685f0e7 | ||
|
|
3021ed158b | ||
|
|
a57043f496 | ||
|
|
fdfd124fee | ||
|
|
71739de91a | ||
|
|
041b1fbf53 | ||
|
|
3a72b32b4a | ||
|
|
994f174300 | ||
|
|
c0f32254bb | ||
|
|
fd98dbeebe | ||
|
|
69ac6e6698 | ||
|
|
35e224d227 | ||
|
|
2da8552a53 | ||
|
|
a9a5047949 | ||
|
|
17c98f7fea | ||
|
|
c3bc890665 | ||
|
|
7a295c2541 | ||
|
|
01b1b74c6a | ||
|
|
fd25d21c72 | ||
|
|
6b1d8d24dd | ||
|
|
5d002f5128 | ||
|
|
98314c466f | ||
|
|
4f7afd7c97 | ||
|
|
a9e139ff7e | ||
|
|
4ff483a8d2 | ||
|
|
1916b79df1 | ||
|
|
98e15a7717 | ||
|
|
dfd18e3c7f | ||
|
|
8fbbaae05b | ||
|
|
98007c962d | ||
|
|
5f27f161a5 | ||
|
|
fcf723849b | ||
|
|
8a60056866 | ||
|
|
e9d20a51a5 | ||
|
|
a28d77ba32 | ||
|
|
5bd866e104 | ||
|
|
9985c6065d | ||
|
|
1f2e4a3719 | ||
|
|
fa2555a3f7 | ||
|
|
b4de7afe77 | ||
|
|
736cafbcc2 | ||
|
|
d143605a40 | ||
|
|
f2f4c1e63d | ||
|
|
bc86ee87a0 | ||
|
|
5cbd60c024 | ||
|
|
15972efb4f | ||
|
|
dae5f65c0d | ||
|
|
564b5f10ea | ||
|
|
e6e40f9bd4 | ||
|
|
bd15e36b52 | ||
|
|
43faca3061 | ||
|
|
82af9bada2 | ||
|
|
5817273872 | ||
|
|
32d6a11353 | ||
|
|
9477fba704 | ||
|
|
80b16bccf1 | ||
|
|
2befa68c93 | ||
|
|
6672085d84 | ||
|
|
05a39f6922 | ||
|
|
40e8dea485 | ||
|
|
7e09532d5d | ||
|
|
4034a2bfc4 | ||
|
|
0d93e867cf | ||
|
|
44e8f4f406 | ||
|
|
e3083c7773 | ||
|
|
d0c0c4a4d6 | ||
|
|
a9e8de26f8 | ||
|
|
c7a256ebf1 | ||
|
|
8cac4eb51c | ||
|
|
933d34ff1d | ||
|
|
d34f460b98 | ||
|
|
7632face63 | ||
|
|
d0ab5267cf | ||
|
|
88b701fd39 | ||
|
|
4a1cee619c | ||
|
|
c7741115ff | ||
|
|
19c987abc3 | ||
|
|
d03693341e | ||
|
|
bf496333eb | ||
|
|
c6b01947b3 | ||
|
|
91781f36ac | ||
|
|
3fbdf78ba1 | ||
|
|
d1f463d497 | ||
|
|
1f678fc975 | ||
|
|
082c839639 | ||
|
|
600d548fce | ||
|
|
3035f9b686 | ||
|
|
6eae0f02d3 | ||
|
|
87be2f4b9e | ||
|
|
3b054504a1 | ||
|
|
a88f6b968a | ||
|
|
1fc4f150bf | ||
|
|
1f4e59cbdc | ||
|
|
b5dc8d9adf | ||
|
|
43f7e08548 | ||
|
|
05fc6f87ec | ||
|
|
daae535fa1 | ||
|
|
90c8cb3455 | ||
|
|
daeee10de9 | ||
|
|
6c1c401a71 | ||
|
|
fd7f0fceb2 | ||
|
|
26b8a616be | ||
|
|
d88882f439 | ||
|
|
09dc1d6baa | ||
|
|
f4f5e86979 | ||
|
|
488fd60d5d | ||
|
|
41025f64c0 | ||
|
|
a2eac2ff76 | ||
|
|
34f2a2391a | ||
|
|
67b09014aa | ||
|
|
c907750446 | ||
|
|
69114c3cc0 |
@@ -1,7 +1,6 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: Report a bug.
|
description: Report a bug.
|
||||||
title: ""
|
labels: ["bug"]
|
||||||
labels: bug
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
name: Feature request
|
name: Feature request
|
||||||
description: Suggest an idea.
|
description: Suggest an idea.
|
||||||
title: ""
|
labels: ["feature"]
|
||||||
labels: feature
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -19,6 +19,9 @@ bin/
|
|||||||
gen/
|
gen/
|
||||||
out/
|
out/
|
||||||
|
|
||||||
|
# Kotlin folder
|
||||||
|
.kotlin/
|
||||||
|
|
||||||
# Gradle files
|
# Gradle files
|
||||||
.gradle/
|
.gradle/
|
||||||
build/
|
build/
|
||||||
|
|||||||
14
CHANGELOG
14
CHANGELOG
@@ -1,3 +1,17 @@
|
|||||||
|
KeePassDX(4.2.0)
|
||||||
|
* Passkeys management #1421 #2097 (Thx @cali-95)
|
||||||
|
|
||||||
|
KeePassDX(4.1.8)
|
||||||
|
* Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 #1687 (Thx @Dev-ClayP)
|
||||||
|
* Remember last read-only state #2099 #2100 (Thx @rmacklin)
|
||||||
|
* Fix merge deletion #1516
|
||||||
|
* Fix space in search #175
|
||||||
|
* Fix deletable recycle bin #2163
|
||||||
|
* Small fixes
|
||||||
|
|
||||||
|
KeePassDX(4.1.7)
|
||||||
|
* Fix CipherDatabase for biometric states #2119
|
||||||
|
|
||||||
KeePassDX(4.1.6)
|
KeePassDX(4.1.6)
|
||||||
* Auto open biometric prompt from database list #2113
|
* Auto open biometric prompt from database list #2113
|
||||||
* Fix Keystore errors #2114 #2115
|
* Fix Keystore errors #2114 #2115
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ apply plugin: 'kotlin-kapt'
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.kunzisoft.keepass'
|
namespace 'com.kunzisoft.keepass'
|
||||||
compileSdkVersion 34
|
compileSdkVersion 36
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.kunzisoft.keepass"
|
applicationId "com.kunzisoft.keepass"
|
||||||
minSdkVersion 15
|
minSdkVersion 19
|
||||||
targetSdkVersion 34
|
targetSdkVersion 35
|
||||||
versionCode = 138
|
versionCode = 142
|
||||||
versionName = "4.1.6"
|
versionName = "4.2.0beta02"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
|
|
||||||
testApplicationId = "com.kunzisoft.keepass.tests"
|
testApplicationId = "com.kunzisoft.keepass.tests"
|
||||||
@@ -35,6 +35,10 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig true
|
||||||
|
}
|
||||||
|
|
||||||
dependenciesInfo {
|
dependenciesInfo {
|
||||||
// Disables dependency metadata when building APKs.
|
// Disables dependency metadata when building APKs.
|
||||||
includeInApk = false
|
includeInApk = false
|
||||||
@@ -101,6 +105,11 @@ android {
|
|||||||
buildFeatures {
|
buildFeatures {
|
||||||
buildConfig true
|
buildConfig true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
packaging {
|
||||||
|
// Bouncy castle bug https://github.com/bcgit/bc-java/issues/1685
|
||||||
|
resources.pickFirsts.add('META-INF/versions/9/OSGI-INF/MANIFEST.MF')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def room_version = "2.5.1"
|
def room_version = "2.5.1"
|
||||||
@@ -141,6 +150,9 @@ dependencies {
|
|||||||
// Password generator
|
// Password generator
|
||||||
implementation 'me.gosimple:nbvcxz:1.5.0'
|
implementation 'me.gosimple:nbvcxz:1.5.0'
|
||||||
|
|
||||||
|
// Credentials Provider
|
||||||
|
implementation "androidx.credentials:credentials:1.2.2"
|
||||||
|
|
||||||
// Modules import
|
// Modules import
|
||||||
implementation project(path: ':database')
|
implementation project(path: ':database')
|
||||||
implementation project(path: ':icon-pack')
|
implementation project(path: ':icon-pack')
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 3,
|
||||||
|
"identityHash": "a20aec7cf09664b1102ec659fa51160a",
|
||||||
|
"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, `read_only` INTEGER, `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": "readOnly",
|
||||||
|
"columnName": "read_only",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "updated",
|
||||||
|
"columnName": "updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"database_uri"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"database_uri"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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, 'a20aec7cf09664b1102ec659fa51160a')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
64
app/src/free/assets/passkeys_privileged_apps_community.json
Normal file
64
app/src/free/assets/passkeys_privileged_apps_community.json
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"apps": [
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "io.github.forkmaintainers.iceraven",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "9C:0D:22:37:9F:48:7B:70:A4:F9:F8:BE:C0:17:3C:F9:1A:16:44:F0:8F:93:38:5B:5B:78:2C:E3:76:60:BA:81"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.chromium.chrome",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "A8:56:48:50:79:BC:B3:57:BF:BE:69:BA:19:A9:BA:43:CD:0A:D9:AB:22:67:52:C7:80:B6:88:8A:FD:48:21:6B"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.cromite.cromite",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "63:3F:A4:1D:82:11:D6:D0:91:6A:81:9B:89:66:8C:6D:E9:2E:64:23:2D:A6:7F:9D:16:FD:81:C3:B7:E9:23:FF"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.ironfoxoss.ironfox",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C5:E2:91:B5:A5:71:F9:C8:CD:9A:97:99:C2:C9:4E:02:EC:97:03:94:88:93:F2:CA:75:6D:67:B9:42:04:F9:04"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.fennec_fdroid",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "06:66:53:58:EF:D8:BA:05:BE:23:6A:47:A1:2C:B0:95:8D:7D:75:DD:93:9D:77:C2:B3:1F:53:98:53:7E:BD:C5"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
820
app/src/free/assets/passkeys_privileged_apps_google.json
Normal file
820
app/src/free/assets/passkeys_privileged_apps_google.json
Normal file
@@ -0,0 +1,820 @@
|
|||||||
|
{
|
||||||
|
"apps": [
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.android.chrome",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.chrome.beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "DA:63:3D:34:B6:9E:63:AE:21:03:B4:9D:53:CE:05:2F:C5:F7:F3:C5:3A:AB:94:FD:C2:A2:08:BD:FD:14:24:9C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "3D:7A:12:23:01:9A:A3:9D:9E:A0:E3:43:6A:B7:C0:89:6B:FB:4F:B6:79:F4:DE:5F:E7:C2:3F:32:6C:8F:99:4A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.chrome.dev",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "90:44:EE:5F:EE:4B:BC:5E:21:DD:44:66:54:31:C4:EB:1F:1F:71:A3:27:16:A0:BC:92:7B:CB:B3:92:33:CA:BF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "3D:7A:12:23:01:9A:A3:9D:9E:A0:E3:43:6A:B7:C0:89:6B:FB:4F:B6:79:F4:DE:5F:E7:C2:3F:32:6C:8F:99:4A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.chrome.canary",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "20:19:DF:A1:FB:23:EF:BF:70:C5:BC:D1:44:3C:5B:EA:B0:4F:3F:2F:F4:36:6E:9A:C1:E3:45:76:39:A2:4C:FC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.chromium.chrome",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C6:AD:B8:B8:3C:6D:4C:17:D2:92:AF:DE:56:FD:48:8A:51:D3:16:FF:8F:2C:11:C5:41:02:23:BF:F8:A7:DB:B3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.google.android.apps.chrome",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.fennec_webauthndebug",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "BD:AE:82:02:80:D2:AF:B7:74:94:EF:22:58:AA:78:A9:AE:A1:36:41:7E:8B:C2:3D:C9:87:75:2E:6F:48:E8:48"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.firefox",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "A7:8B:62:A5:16:5B:44:94:B2:FE:AD:9E:76:A2:80:D2:2D:93:7F:EE:62:51:AE:CE:59:94:46:B2:EA:31:9B:04"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.firefox_beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "A7:8B:62:A5:16:5B:44:94:B2:FE:AD:9E:76:A2:80:D2:2D:93:7F:EE:62:51:AE:CE:59:94:46:B2:EA:31:9B:04"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.focus",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.fennec_aurora",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "BC:04:88:83:8D:06:F4:CA:6B:F3:23:86:DA:AB:0D:D8:EB:CF:3E:77:30:78:74:59:F6:2F:B3:CD:14:A1:BA:AA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.rocket",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "86:3A:46:F0:97:39:32:B7:D0:19:9B:54:91:12:74:1C:2D:27:31:AC:72:EA:11:B7:52:3A:A9:0A:11:BF:56:91"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.fenix",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "50:04:77:90:88:E7:F9:88:D5:BC:5C:C5:F8:79:8F:EB:F4:F8:CD:08:4A:1B:2A:46:EF:D4:C8:EE:4A:EA:F2:11"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.fenix.debug",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "BD:AE:82:02:80:D2:AF:B7:74:94:EF:22:58:AA:78:A9:AE:A1:36:41:7E:8B:C2:3D:C9:87:75:2E:6F:48:E8:48"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.focus.beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.focus.nightly",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.klar",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.reference.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "B0:09:90:E3:0F:9D:81:5D:2E:BC:7B:9B:B2:21:CE:47:E5:C9:D5:17:AA:C7:0E:7F:D5:95:B1:E5:3E:9A:4B:14"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.microsoft.emmx.canary",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.microsoft.emmx.dev",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.microsoft.emmx.beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.microsoft.emmx",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.microsoft.emmx.rolling",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "32:A2:FC:74:D7:31:10:58:59:E5:A8:5D:F1:6D:95:F1:02:D8:5B:22:09:9B:80:64:C5:D8:91:5C:61:DA:D1:E0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.microsoft.emmx.local",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "32:A2:FC:74:D7:31:10:58:59:E5:A8:5D:F1:6D:95:F1:02:D8:5B:22:09:9B:80:64:C5:D8:91:5C:61:DA:D1:E0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.brave.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.brave.browser_beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.brave.browser_nightly",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "app.vanadium.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C6:AD:B8:B8:3C:6D:4C:17:D2:92:AF:DE:56:FD:48:8A:51:D3:16:FF:8F:2C:11:C5:41:02:23:BF:F8:A7:DB:B3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.vivaldi.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.vivaldi.browser.snapshot",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.vivaldi.browser.sopranos",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.citrix.Receiver",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "3D:D1:12:67:10:69:AB:36:4E:F9:BE:73:9A:B7:B5:EE:15:E1:CD:E9:D8:75:7B:1B:F0:64:F5:0C:55:68:9A:49"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "CE:B2:23:D7:77:09:F2:B6:BC:0B:3A:78:36:F5:A5:AF:4C:E1:D3:55:F4:A7:28:86:F7:9D:F8:0D:C9:D6:12:2E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "AA:D0:D4:57:E6:33:C3:78:25:77:30:5B:C1:B2:D9:E3:81:41:C7:21:DF:0D:AA:6E:29:07:2F:C4:1D:34:F0:AB"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.android.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C9:00:9D:01:EB:F9:F5:D0:30:2B:C7:1B:2F:E9:AA:9A:47:A4:32:BB:A1:73:08:A3:11:1B:75:D7:B2:14:90:25"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.sec.android.app.sbrowser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C8:A2:E9:BC:CF:59:7C:2F:B6:DC:66:BE:E2:93:FC:13:F2:FC:47:EC:77:BC:6B:2B:0D:52:C1:1F:51:19:2A:B8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "34:DF:0E:7A:9F:1C:F1:89:2E:45:C0:56:B4:97:3C:D8:1C:CF:14:8A:40:50:D1:1A:EA:4A:C5:A6:5F:90:0A:42"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.sec.android.app.sbrowser.beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C8:A2:E9:BC:CF:59:7C:2F:B6:DC:66:BE:E2:93:FC:13:F2:FC:47:EC:77:BC:6B:2B:0D:52:C1:1F:51:19:2A:B8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "34:DF:0E:7A:9F:1C:F1:89:2E:45:C0:56:B4:97:3C:D8:1C:CF:14:8A:40:50:D1:1A:EA:4A:C5:A6:5F:90:0A:42"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.google.android.gms",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "7C:E8:3C:1B:71:F3:D5:72:FE:D0:4C:8D:40:C5:CB:10:FF:75:E6:D8:7D:9D:F6:FB:D5:3F:04:68:C2:90:50:53"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "D2:2C:C5:00:29:9F:B2:28:73:A0:1A:01:0D:E1:C8:2F:BE:4D:06:11:19:B9:48:14:DD:30:1D:AB:50:CB:76:78"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.yandex.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.yandex.browser.beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.yandex.browser.alpha",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.yandex.browser.corp",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.yandex.browser.canary",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "1D:A9:CB:AE:2D:CC:C6:A5:8D:6C:94:7B:E9:4C:DB:B7:33:D6:5D:A4:D1:77:0F:A1:4A:53:64:CB:4A:28:EB:49"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.yandex.browser.broteam",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "1D:A9:CB:AE:2D:CC:C6:A5:8D:6C:94:7B:E9:4C:DB:B7:33:D6:5D:A4:D1:77:0F:A1:4A:53:64:CB:4A:28:EB:49"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.talonsec.talon",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "A3:66:03:44:A6:F6:AF:CA:81:8C:BF:43:96:A2:3C:CF:D5:ED:7A:78:1B:B4:A3:D1:85:03:01:E2:F4:6D:23:83"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "E2:A5:64:74:EA:23:7B:06:67:B6:F5:2C:DC:E9:04:5E:24:88:3B:AE:D0:82:59:9A:A2:DF:0B:60:3A:CF:6A:3B"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.talonsec.talon_beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "F5:86:62:7A:32:C8:9F:E6:7E:00:6D:B1:8C:34:31:9E:01:7F:B3:B2:BE:D6:9D:01:01:B7:F9:43:E7:7C:48:AE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "9A:A1:25:D5:E5:5E:3F:B0:DE:96:72:D9:A9:5D:04:65:3F:49:4A:1E:C3:EE:76:1E:94:C4:4E:5D:2F:65:8E:2F"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.duckduckgo.mobile.android.debug",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C4:F0:9E:2B:D7:25:AD:F5:AD:92:0B:A2:80:27:66:AC:16:4A:C1:53:B3:EA:9E:08:48:B0:57:98:37:F7:6A:29"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.duckduckgo.mobile.android",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "BB:7B:B3:1C:57:3C:46:A1:DA:7F:C5:C5:28:A6:AC:F4:32:10:84:56:FE:EC:50:81:0C:7F:33:69:4E:B3:D2:D4"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.naver.whale",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "0B:8B:85:23:BB:4A:EF:FA:34:6E:4B:DD:4F:BF:7D:19:34:50:56:9A:A1:4A:AA:D4:AD:FD:94:A3:F7:B2:27:BB"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.fido.fido2client",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "FC:98:DA:E6:3A:D3:96:26:C8:C6:7F:BE:83:F2:F0:6F:74:93:2A:9C:D1:46:B9:2C:EC:FC:6A:04:7A:90:43:86"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.heytap.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "AF:F8:A7:49:CF:0E:7D:75:44:65:D0:FB:FA:7B:8D:0C:64:5E:22:5C:10:C6:E2:32:AD:A0:D9:74:88:36:B8:E5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "A8:FE:A4:CA:FB:93:32:DA:26:B8:E6:81:08:17:C1:DA:90:A5:03:0E:35:A6:0A:79:E0:6C:90:97:AA:C6:A4:42"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "io.island.Island",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "D9:C3:39:AC:9C:3A:EE:E1:75:1D:85:8C:35:D9:BA:C5:CC:87:B3:CE:76:30:93:F0:F5:10:64:F5:A2:F6:9B:04"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "io.island.IslandCanary",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "90:17:13:23:45:6E:6F:39:CB:FD:CF:B2:56:BE:1D:CF:F3:BC:1C:59:8A:15:93:30:E4:97:73:D0:4C:B9:C9:05"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "io.island.IslandBeta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "35:31:83:1A:9E:2B:21:1D:E6:AA:C3:69:4B:45:83:6E:56:09:B9:D7:D0:04:C3:1B:21:87:40:FB:77:17:38:D1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "io.island.IslandDev",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "io.island.island.intune",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C2:38:24:15:41:20:A0:8F:C3:95:42:AC:D8:2A:E9:24:94:78:80:1E:47:FD:6C:66:2B:18:1C:28:CA:7E:59:4E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "io.island.island.canary.intune",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "1E:16:74:BB:79:EA:09:FB:37:CF:9F:1B:07:1B:1D:51:8D:46:03:0E:D3:EE:F2:C1:4E:AD:93:9E:C6:EE:3A:4C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "io.island.island.beta.intune",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "D2:5E:AD:F6:1C:E6:36:6C:A4:23:A4:7F:C4:DB:9B:8C:9C:8A:35:B4:B0:19:E8:D9:82:FB:D0:8A:D9:DB:49:5A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "io.island.island.dev.intune",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "6C:65:BD:B0:33:F5:CE:B1:74:09:EF:F9:99:48:D5:58:9F:55:63:9A:63:78:D5:A5:00:EB:95:FC:01:BC:6D:44"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "net.quetta.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "BE:FE:E7:31:12:6A:A5:6E:7E:FD:AE:AF:5E:F3:FA:EA:44:1C:19:CC:E0:CA:EC:42:6B:65:BB:F8:2C:59:46:80"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "F1:38:00:4F:38:04:51:D4:8A:05:2B:B3:A3:EF:17:24:23:D4:B0:D0:C8:A3:AA:DD:FB:DB:66:30:31:48:EC:A4"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "cz.seznam.sbrowser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "DB:95:40:66:10:78:83:6E:4E:B1:66:F6:9E:F4:07:30:9E:8D:AE:33:34:68:5E:C8:F6:FA:2F:13:81:B9:AC:F6"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.opera.mini.native",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "57:AC:BC:52:5F:1B:2E:BD:19:19:6C:D6:F0:14:39:7C:C9:10:FD:18:84:1E:0A:E8:50:FE:BC:3E:1E:59:3F:F2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.opera.mini.native.beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "57:AC:BC:52:5F:1B:2E:BD:19:19:6C:D6:F0:14:39:7C:C9:10:FD:18:84:1E:0A:E8:50:FE:BC:3E:1E:59:3F:F2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -45,7 +45,8 @@
|
|||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/KeepassDXStyle.Night"
|
android:theme="@style/KeepassDXStyle.Night"
|
||||||
tools:targetApi="s">
|
tools:targetApi="s"
|
||||||
|
tools:ignore="CredentialDependency">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.backup.api_key"
|
android:name="com.google.android.backup.api_key"
|
||||||
android:value="${googleAndroidBackupAPIKey}" />
|
android:value="${googleAndroidBackupAPIKey}" />
|
||||||
@@ -159,21 +160,33 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.AutofillLauncherActivity"
|
android:name="com.kunzisoft.keepass.settings.DeviceUnlockSettingsActivity" />
|
||||||
android:theme="@style/Theme.Transparent"
|
|
||||||
android:configChanges="keyboardHidden"
|
|
||||||
android:excludeFromRecents="true"/>
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.AdvancedUnlockSettingsActivity" />
|
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
||||||
|
android:label="@string/keyboard_setting_label"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
|
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity"
|
||||||
|
tools:targetApi="26" />
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.settings.PasskeysSettingsActivity"
|
||||||
|
tools:targetApi="34" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" />
|
android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity"
|
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity"
|
||||||
android:theme="@style/Theme.Transparent" />
|
android:theme="@style/Theme.Transparent" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
|
android:name="com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity"
|
||||||
|
android:theme="@style/Theme.Transparent"
|
||||||
|
android:configChanges="keyboardHidden"
|
||||||
|
android:excludeFromRecents="true"/>
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity"
|
||||||
android:theme="@style/Theme.Transparent"
|
android:theme="@style/Theme.Transparent"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
@@ -192,14 +205,12 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.MagikeyboardSettingsActivity"
|
android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity"
|
||||||
android:label="@string/keyboard_setting_label"
|
android:theme="@style/Theme.Transparent"
|
||||||
android:exported="true">
|
android:configChanges="keyboardHidden"
|
||||||
<intent-filter>
|
android:excludeFromRecents="true"
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
android:exported="false"
|
||||||
</intent-filter>
|
tools:targetApi="upside_down_cake" />
|
||||||
</activity>
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
||||||
android:foregroundServiceType="dataSync"
|
android:foregroundServiceType="dataSync"
|
||||||
@@ -221,14 +232,14 @@
|
|||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.services.AdvancedUnlockNotificationService"
|
android:name="com.kunzisoft.keepass.services.DeviceUnlockNotificationService"
|
||||||
android:foregroundServiceType="specialUse"
|
android:foregroundServiceType="specialUse"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<!-- Receiver for Autofill -->
|
<!-- Receiver for Autofill -->
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"
|
android:name="com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService"
|
||||||
android:label="@string/autofill_service_name"
|
android:label="@string/app_name"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
||||||
<meta-data
|
<meta-data
|
||||||
@@ -239,7 +250,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService"
|
android:name="com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService"
|
||||||
android:label="@string/keyboard_label"
|
android:label="@string/keyboard_label"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:permission="android.permission.BIND_INPUT_METHOD" >
|
android:permission="android.permission.BIND_INPUT_METHOD" >
|
||||||
@@ -249,6 +260,22 @@
|
|||||||
<action android:name="android.view.InputMethod" />
|
<action android:name="android.view.InputMethod" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
<service
|
||||||
|
android:name="com.kunzisoft.keepass.credentialprovider.passkey.PasskeyProviderService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/passkey_service_name"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"
|
||||||
|
tools:targetApi="upside_down_cake">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.credentials.CredentialProviderService" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.credentials.provider"
|
||||||
|
android:resource="@xml/provider" />
|
||||||
|
</service>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
|
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
|
|||||||
@@ -359,77 +359,40 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
|
|
||||||
isViewTranslateAnimationRunning = true
|
isViewTranslateAnimationRunning = true
|
||||||
|
|
||||||
|
imageView.run {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
val translationY = if (velY > 0) {
|
||||||
imageView.run {
|
originalViewBounds.top + height - top
|
||||||
val translationY = if (velY > 0) {
|
|
||||||
originalViewBounds.top + height - top
|
|
||||||
} else {
|
|
||||||
originalViewBounds.top - height - top
|
|
||||||
}
|
|
||||||
animate()
|
|
||||||
.setDuration(dismissAnimationDuration)
|
|
||||||
.setInterpolator(dismissAnimationInterpolator)
|
|
||||||
.translationY(translationY.toFloat())
|
|
||||||
.setUpdateListener {
|
|
||||||
val amount = calcTranslationAmount()
|
|
||||||
changeBackgroundAlpha(amount)
|
|
||||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
|
||||||
}
|
|
||||||
.setListener(object : Animator.AnimatorListener {
|
|
||||||
override fun onAnimationStart(p0: Animator) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationEnd(p0: Animator) {
|
|
||||||
isViewTranslateAnimationRunning = false
|
|
||||||
onViewTranslateListener?.onDismiss(imageView)
|
|
||||||
cleanup()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationCancel(p0: Animator) {
|
|
||||||
isViewTranslateAnimationRunning = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationRepeat(p0: Animator) {
|
|
||||||
// no op
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, if (velY > 0) {
|
|
||||||
originalViewBounds.top + imageView.height - imageView.top
|
|
||||||
} else {
|
} else {
|
||||||
originalViewBounds.top - imageView.height - imageView.top
|
originalViewBounds.top - height - top
|
||||||
}.toFloat()).apply {
|
|
||||||
duration = dismissAnimationDuration
|
|
||||||
interpolator = dismissAnimationInterpolator
|
|
||||||
addUpdateListener {
|
|
||||||
val amount = calcTranslationAmount()
|
|
||||||
changeBackgroundAlpha(amount)
|
|
||||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
|
||||||
}
|
|
||||||
addListener(object : Animator.AnimatorListener {
|
|
||||||
override fun onAnimationStart(p0: Animator) {
|
|
||||||
// no op
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationEnd(p0: Animator) {
|
|
||||||
isViewTranslateAnimationRunning = false
|
|
||||||
onViewTranslateListener?.onDismiss(imageView)
|
|
||||||
cleanup()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationCancel(p0: Animator) {
|
|
||||||
isViewTranslateAnimationRunning = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationRepeat(p0: Animator) {
|
|
||||||
// no op
|
|
||||||
}
|
|
||||||
})
|
|
||||||
start()
|
|
||||||
}
|
}
|
||||||
|
animate()
|
||||||
|
.setDuration(dismissAnimationDuration)
|
||||||
|
.setInterpolator(dismissAnimationInterpolator)
|
||||||
|
.translationY(translationY.toFloat())
|
||||||
|
.setUpdateListener {
|
||||||
|
val amount = calcTranslationAmount()
|
||||||
|
changeBackgroundAlpha(amount)
|
||||||
|
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
||||||
|
}
|
||||||
|
.setListener(object : Animator.AnimatorListener {
|
||||||
|
override fun onAnimationStart(p0: Animator) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(p0: Animator) {
|
||||||
|
isViewTranslateAnimationRunning = false
|
||||||
|
onViewTranslateListener?.onDismiss(imageView)
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationCancel(p0: Animator) {
|
||||||
|
isViewTranslateAnimationRunning = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationRepeat(p0: Animator) {
|
||||||
|
// no op
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -657,137 +620,76 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
|
|||||||
|
|
||||||
private fun restoreViewTransform() {
|
private fun restoreViewTransform() {
|
||||||
val imageView = imageViewRef.get() ?: return
|
val imageView = imageViewRef.get() ?: return
|
||||||
|
imageView.run {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
animate()
|
||||||
imageView.run {
|
.setDuration(restoreAnimationDuration)
|
||||||
animate()
|
.setInterpolator(restoreAnimationInterpolator)
|
||||||
.setDuration(restoreAnimationDuration)
|
.translationY((originalViewBounds.top - top).toFloat())
|
||||||
.setInterpolator(restoreAnimationInterpolator)
|
.setUpdateListener {
|
||||||
.translationY((originalViewBounds.top - top).toFloat())
|
val amount = calcTranslationAmount()
|
||||||
.setUpdateListener {
|
changeBackgroundAlpha(amount)
|
||||||
val amount = calcTranslationAmount()
|
onViewTranslateListener?.onViewTranslate(this, amount)
|
||||||
changeBackgroundAlpha(amount)
|
}
|
||||||
onViewTranslateListener?.onViewTranslate(this, amount)
|
.setListener(object : Animator.AnimatorListener {
|
||||||
|
override fun onAnimationStart(p0: Animator) {
|
||||||
|
// no op
|
||||||
}
|
}
|
||||||
.setListener(object : Animator.AnimatorListener {
|
|
||||||
override fun onAnimationStart(p0: Animator) {
|
|
||||||
// no op
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationEnd(p0: Animator) {
|
override fun onAnimationEnd(p0: Animator) {
|
||||||
onViewTranslateListener?.onRestore(imageView)
|
onViewTranslateListener?.onRestore(imageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationCancel(p0: Animator) {
|
override fun onAnimationCancel(p0: Animator) {
|
||||||
// no op
|
// no op
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationRepeat(p0: Animator) {
|
override fun onAnimationRepeat(p0: Animator) {
|
||||||
// no op
|
// no op
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, (originalViewBounds.top - imageView.top).toFloat()).apply {
|
|
||||||
duration = restoreAnimationDuration
|
|
||||||
interpolator = restoreAnimationInterpolator
|
|
||||||
addUpdateListener {
|
|
||||||
val amount = calcTranslationAmount()
|
|
||||||
changeBackgroundAlpha(amount)
|
|
||||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
|
||||||
}
|
|
||||||
addListener(object : Animator.AnimatorListener {
|
|
||||||
override fun onAnimationStart(p0: Animator) {
|
|
||||||
// no op
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationEnd(p0: Animator) {
|
|
||||||
onViewTranslateListener?.onRestore(imageView)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationCancel(p0: Animator) {
|
|
||||||
// no op
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationRepeat(p0: Animator) {
|
|
||||||
// no op
|
|
||||||
}
|
|
||||||
})
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startDragToDismissAnimation() {
|
private fun startDragToDismissAnimation() {
|
||||||
val imageView = imageViewRef.get() ?: return
|
val imageView = imageViewRef.get() ?: return
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
imageView.run {
|
||||||
imageView.run {
|
val translationY = if (y - initialY > 0) {
|
||||||
val translationY = if (y - initialY > 0) {
|
originalViewBounds.top + height - top
|
||||||
originalViewBounds.top + height - top
|
} else {
|
||||||
} else {
|
originalViewBounds.top - height - top
|
||||||
originalViewBounds.top - height - top
|
}
|
||||||
}
|
animate()
|
||||||
animate()
|
.setDuration(dismissAnimationDuration)
|
||||||
.setDuration(dismissAnimationDuration)
|
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
.translationY(translationY.toFloat())
|
||||||
.translationY(translationY.toFloat())
|
.setUpdateListener {
|
||||||
.setUpdateListener {
|
val amount = calcTranslationAmount()
|
||||||
val amount = calcTranslationAmount()
|
changeBackgroundAlpha(amount)
|
||||||
changeBackgroundAlpha(amount)
|
onViewTranslateListener?.onViewTranslate(this, amount)
|
||||||
onViewTranslateListener?.onViewTranslate(this, amount)
|
}
|
||||||
|
.setListener(object : Animator.AnimatorListener {
|
||||||
|
override fun onAnimationStart(p0: Animator) {
|
||||||
|
isViewTranslateAnimationRunning = true
|
||||||
}
|
}
|
||||||
.setListener(object : Animator.AnimatorListener {
|
|
||||||
override fun onAnimationStart(p0: Animator) {
|
|
||||||
isViewTranslateAnimationRunning = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationEnd(p0: Animator) {
|
override fun onAnimationEnd(p0: Animator) {
|
||||||
isViewTranslateAnimationRunning = false
|
isViewTranslateAnimationRunning = false
|
||||||
onViewTranslateListener?.onDismiss(imageView)
|
onViewTranslateListener?.onDismiss(imageView)
|
||||||
cleanup()
|
cleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationCancel(p0: Animator) {
|
override fun onAnimationCancel(p0: Animator) {
|
||||||
isViewTranslateAnimationRunning = false
|
isViewTranslateAnimationRunning = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationRepeat(p0: Animator) {
|
override fun onAnimationRepeat(p0: Animator) {
|
||||||
// no op
|
// no op
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, imageView.translationY).apply {
|
|
||||||
duration = dismissAnimationDuration
|
|
||||||
interpolator = AccelerateDecelerateInterpolator()
|
|
||||||
addUpdateListener {
|
|
||||||
val amount = calcTranslationAmount()
|
|
||||||
changeBackgroundAlpha(amount)
|
|
||||||
onViewTranslateListener?.onViewTranslate(imageView, amount)
|
|
||||||
}
|
|
||||||
addListener(object : Animator.AnimatorListener {
|
|
||||||
override fun onAnimationStart(p0: Animator) {
|
|
||||||
isViewTranslateAnimationRunning = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationEnd(p0: Animator) {
|
|
||||||
isViewTranslateAnimationRunning = false
|
|
||||||
onViewTranslateListener?.onDismiss(imageView)
|
|
||||||
cleanup()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationCancel(p0: Animator) {
|
|
||||||
isViewTranslateAnimationRunning = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationRepeat(p0: Animator) {
|
|
||||||
// no op
|
|
||||||
}
|
|
||||||
})
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processFlingToDismiss(velocityY: Float) {
|
private fun processFlingToDismiss(velocityY: Float) {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.activities
|
||||||
|
|
||||||
import android.content.pm.PackageManager.NameNotFoundException
|
import android.content.pm.PackageManager.NameNotFoundException
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -32,7 +31,7 @@ import androidx.core.text.HtmlCompat
|
|||||||
import com.kunzisoft.keepass.BuildConfig
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
|
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
|
||||||
import com.kunzisoft.keepass.utils.getPackageInfoCompat
|
import com.kunzisoft.keepass.utils.getPackageInfoCompat
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
@@ -58,7 +57,7 @@ class AboutActivity : StylishActivity() {
|
|||||||
var version: String
|
var version: String
|
||||||
var build: String
|
var build: String
|
||||||
try {
|
try {
|
||||||
version = packageManager.getPackageInfoCompat(packageName).versionName
|
version = packageManager.getPackageInfoCompat(packageName).versionName ?: ""
|
||||||
build = BuildConfig.BUILD_VERSION
|
build = BuildConfig.BUILD_VERSION
|
||||||
} catch (e: NameNotFoundException) {
|
} catch (e: NameNotFoundException) {
|
||||||
Log.w(javaClass.simpleName, "Unable to get the app or the build version", e)
|
Log.w(javaClass.simpleName, "Unable to get the app or the build version", e)
|
||||||
@@ -78,9 +77,8 @@ class AboutActivity : StylishActivity() {
|
|||||||
movementMethod = LinkMovementMethod.getInstance()
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
text = HtmlCompat.fromHtml(getString(R.string.html_about_licence, DateTime().year),
|
text = HtmlCompat.fromHtml(getString(R.string.html_about_licence, DateTime().year),
|
||||||
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
textDirection = View.TEXT_DIRECTION_ANY_RTL
|
||||||
textDirection = View.TEXT_DIRECTION_ANY_RTL
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
findViewById<TextView>(R.id.activity_about_privacy_text).apply {
|
findViewById<TextView>(R.id.activity_about_privacy_text).apply {
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import android.content.Intent
|
|||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
@@ -38,13 +37,10 @@ import androidx.activity.result.ActivityResultLauncher
|
|||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.res.ResourcesCompat
|
|
||||||
import androidx.core.graphics.BlendModeColorFilterCompat
|
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||||
import androidx.core.graphics.BlendModeCompat
|
import androidx.core.graphics.BlendModeCompat
|
||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowCompat
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
@@ -54,15 +50,15 @@ import com.google.android.material.tabs.TabLayout
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.fragments.EntryFragment
|
import com.kunzisoft.keepass.activities.fragments.EntryFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
import com.kunzisoft.keepass.adapters.TagsAdapter
|
import com.kunzisoft.keepass.adapters.TagsAdapter
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.education.EntryActivityEducation
|
import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.otp.OtpType
|
import com.kunzisoft.keepass.otp.OtpType
|
||||||
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
||||||
@@ -73,7 +69,7 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
|||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.UuidUtil
|
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
import com.kunzisoft.keepass.view.WindowInsetPosition
|
import com.kunzisoft.keepass.view.WindowInsetPosition
|
||||||
import com.kunzisoft.keepass.view.applyWindowInsets
|
import com.kunzisoft.keepass.view.applyWindowInsets
|
||||||
@@ -261,7 +257,7 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
mIcon = entryInfo.icon
|
mIcon = entryInfo.icon
|
||||||
// Assign title text
|
// Assign title text
|
||||||
val entryTitle =
|
val entryTitle =
|
||||||
if (entryInfo.title.isNotEmpty()) entryInfo.title else UuidUtil.toHexString(entryInfo.id)
|
entryInfo.title.ifEmpty { entryInfo.id.asHexString() }
|
||||||
collapsingToolbarLayout?.title = entryTitle
|
collapsingToolbarLayout?.title = entryTitle
|
||||||
toolbar?.title = entryTitle
|
toolbar?.title = entryTitle
|
||||||
// Assign tags
|
// Assign tags
|
||||||
|
|||||||
@@ -55,22 +55,23 @@ import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Compani
|
|||||||
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
|
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
|
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
import com.kunzisoft.keepass.database.element.Field
|
import com.kunzisoft.keepass.database.element.Field
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.template.Template
|
import com.kunzisoft.keepass.database.element.template.Template
|
||||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
|
||||||
import com.kunzisoft.keepass.model.AttachmentState
|
import com.kunzisoft.keepass.model.AttachmentState
|
||||||
import com.kunzisoft.keepass.model.DataTime
|
import com.kunzisoft.keepass.model.DataTime
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
@@ -79,9 +80,9 @@ import com.kunzisoft.keepass.model.SearchInfo
|
|||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
||||||
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
|
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getNewEntry
|
||||||
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
|
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
@@ -376,18 +377,25 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
// Don't wait for saving if it's to provide autofill
|
// Don't wait for saving if it's to provide autofill
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
EntrySelectionHelper.doSpecialAction(intent,
|
EntrySelectionHelper.doSpecialAction(
|
||||||
{},
|
intent = intent,
|
||||||
{},
|
defaultAction = {},
|
||||||
{},
|
searchAction = {},
|
||||||
{
|
saveAction = {},
|
||||||
|
keyboardSelectionAction = {
|
||||||
entryValidatedForKeyboardSelection(database, entrySave.newEntry)
|
entryValidatedForKeyboardSelection(database, entrySave.newEntry)
|
||||||
},
|
},
|
||||||
{ _, _ ->
|
autofillSelectionAction = { _, _ ->
|
||||||
entryValidatedForAutofillSelection(database, entrySave.newEntry)
|
entryValidatedForAutofillSelection(database, entrySave.newEntry)
|
||||||
},
|
},
|
||||||
{
|
autofillRegistrationAction = {
|
||||||
entryValidatedForAutofillRegistration(entrySave.newEntry)
|
entryValidatedForAutofillRegistration(entrySave.newEntry)
|
||||||
|
},
|
||||||
|
passkeySelectionAction = {
|
||||||
|
entryValidatedForPasskeySelection(database, entrySave.newEntry)
|
||||||
|
},
|
||||||
|
passkeyRegistrationAction = {
|
||||||
|
entryValidatedForPasskeyRegistration(database, entrySave.newEntry)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -424,34 +432,35 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||||
try {
|
try {
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
var newNodes: List<Node> = ArrayList()
|
result.data?.getNewEntry(database)?.let { entry ->
|
||||||
result.data?.getBundle(DatabaseTaskNotificationService.NEW_NODES_KEY)?.let { newNodesBundle ->
|
EntrySelectionHelper.doSpecialAction(
|
||||||
newNodes = DatabaseTaskNotificationService.getListNodesFromBundle(database, newNodesBundle)
|
intent = intent,
|
||||||
}
|
defaultAction = {
|
||||||
if (newNodes.size == 1) {
|
// Finish naturally
|
||||||
(newNodes[0] as? Entry?)?.let { entry ->
|
finishForEntryResult(entry)
|
||||||
EntrySelectionHelper.doSpecialAction(intent,
|
},
|
||||||
{
|
searchAction = {
|
||||||
// Finish naturally
|
// Nothing when search retrieved
|
||||||
finishForEntryResult(entry)
|
},
|
||||||
},
|
saveAction = {
|
||||||
{
|
entryValidatedForSave(entry)
|
||||||
// Nothing when search retrieved
|
},
|
||||||
},
|
keyboardSelectionAction = {
|
||||||
{
|
entryValidatedForKeyboardSelection(database, entry)
|
||||||
entryValidatedForSave(entry)
|
},
|
||||||
},
|
autofillSelectionAction = { _, _ ->
|
||||||
{
|
entryValidatedForAutofillSelection(database, entry)
|
||||||
entryValidatedForKeyboardSelection(database, entry)
|
},
|
||||||
},
|
autofillRegistrationAction = {
|
||||||
{ _, _ ->
|
entryValidatedForAutofillRegistration(entry)
|
||||||
entryValidatedForAutofillSelection(database, entry)
|
},
|
||||||
},
|
passkeySelectionAction = {
|
||||||
{
|
entryValidatedForPasskeySelection(database, entry)
|
||||||
entryValidatedForAutofillRegistration(entry)
|
},
|
||||||
}
|
passkeyRegistrationAction = {
|
||||||
)
|
entryValidatedForPasskeyRegistration(database, entry)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -488,9 +497,33 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
onValidateSpecialMode()
|
onValidateSpecialMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun entryValidatedForAutofillRegistration(entry: Entry) {
|
private fun entryValidatedForPasskeySelection(database: ContextualDatabase, entry: Entry) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
this.buildPasskeyResponseAndSetResult(
|
||||||
|
entryInfo = entry.getEntryInfo(database)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onValidateSpecialMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun entryValidatedForAutofillRegistration(entry: Entry) {
|
||||||
|
//if (isIntentSender()) {
|
||||||
|
// TODO Autofill Callback #765
|
||||||
|
//}
|
||||||
|
onValidateSpecialMode()
|
||||||
|
if (!isIntentSender()) {
|
||||||
|
finishForEntryResult(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun entryValidatedForPasskeyRegistration(database: ContextualDatabase, entry: Entry) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
this.buildPasskeyResponseAndSetResult(
|
||||||
|
entryInfo = entry.getEntryInfo(database),
|
||||||
|
extras = buildEntryResult(entry) // To update the previous screen
|
||||||
|
)
|
||||||
|
}
|
||||||
onValidateSpecialMode()
|
onValidateSpecialMode()
|
||||||
finishForEntryResult(entry)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -604,16 +637,12 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
isVisible = isEnabled
|
isVisible = isEnabled
|
||||||
}
|
}
|
||||||
menu?.findItem(R.id.menu_add_attachment)?.apply {
|
menu?.findItem(R.id.menu_add_attachment)?.apply {
|
||||||
// Attachment not compatible below KitKat
|
|
||||||
isEnabled = !mIsTemplate
|
isEnabled = !mIsTemplate
|
||||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
|
||||||
isVisible = isEnabled
|
isVisible = isEnabled
|
||||||
}
|
}
|
||||||
menu?.findItem(R.id.menu_add_otp)?.apply {
|
menu?.findItem(R.id.menu_add_otp)?.apply {
|
||||||
// OTP not compatible below KitKat
|
|
||||||
isEnabled = mAllowOTP
|
isEnabled = mAllowOTP
|
||||||
&& !mIsTemplate
|
&& !mIsTemplate
|
||||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
|
||||||
isVisible = isEnabled
|
isVisible = isEnabled
|
||||||
}
|
}
|
||||||
return super.onPrepareOptionsMenu(menu)
|
return super.onPrepareOptionsMenu(menu)
|
||||||
@@ -742,12 +771,17 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildEntryResult(entry: Entry): Bundle {
|
||||||
|
return Bundle().apply {
|
||||||
|
putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun finishForEntryResult(entry: Entry) {
|
private fun finishForEntryResult(entry: Entry) {
|
||||||
// Assign entry callback as a result
|
// Assign entry callback as a result
|
||||||
try {
|
try {
|
||||||
val bundle = Bundle()
|
val bundle = buildEntryResult(entry)
|
||||||
val intentEntry = Intent()
|
val intentEntry = Intent()
|
||||||
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
|
|
||||||
intentEntry.putExtras(bundle)
|
intentEntry.putExtras(bundle)
|
||||||
setResult(Activity.RESULT_OK, intentEntry)
|
setResult(Activity.RESULT_OK, intentEntry)
|
||||||
super.finish()
|
super.finish()
|
||||||
@@ -892,7 +926,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
val intent = Intent(activity, EntryEditActivity::class.java)
|
||||||
intent.putExtra(KEY_PARENT, groupId)
|
intent.putExtra(KEY_PARENT, groupId)
|
||||||
AutofillHelper.startActivityForAutofillResult(
|
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
activityResultLauncher,
|
activityResultLauncher,
|
||||||
@@ -903,21 +937,48 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch EntryEditActivity to add a new passkey entry
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
fun launchForPasskeySelectionResult(context: Context,
|
||||||
|
database: ContextualDatabase,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
groupId: NodeId<*>,
|
||||||
|
searchInfo: SearchInfo? = null) {
|
||||||
|
if (database.loaded && !database.isReadOnly) {
|
||||||
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||||
|
val intent = Intent(context, EntryEditActivity::class.java)
|
||||||
|
intent.putExtra(KEY_PARENT, groupId)
|
||||||
|
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
|
||||||
|
context,
|
||||||
|
intent,
|
||||||
|
activityResultLauncher,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launch EntryEditActivity to register an updated entry (from autofill)
|
* Launch EntryEditActivity to register an updated entry (from autofill)
|
||||||
*/
|
*/
|
||||||
fun launchToUpdateForRegistration(context: Context,
|
fun launchToUpdateForRegistration(context: Context,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
entryId: NodeId<UUID>,
|
entryId: NodeId<UUID>,
|
||||||
registerInfo: RegisterInfo? = null) {
|
registerInfo: RegisterInfo?,
|
||||||
|
typeMode: TypeMode) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||||
val intent = Intent(context, EntryEditActivity::class.java)
|
val intent = Intent(context, EntryEditActivity::class.java)
|
||||||
intent.putExtra(KEY_ENTRY, entryId)
|
intent.putExtra(KEY_ENTRY, entryId)
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
context,
|
context,
|
||||||
|
activityResultLauncher,
|
||||||
intent,
|
intent,
|
||||||
registerInfo
|
registerInfo,
|
||||||
|
typeMode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -928,16 +989,20 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
*/
|
*/
|
||||||
fun launchToCreateForRegistration(context: Context,
|
fun launchToCreateForRegistration(context: Context,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
groupId: NodeId<*>,
|
groupId: NodeId<*>,
|
||||||
registerInfo: RegisterInfo? = null) {
|
registerInfo: RegisterInfo? = null,
|
||||||
|
typeMode: TypeMode) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||||
val intent = Intent(context, EntryEditActivity::class.java)
|
val intent = Intent(context, EntryEditActivity::class.java)
|
||||||
intent.putExtra(KEY_PARENT, groupId)
|
intent.putExtra(KEY_PARENT, groupId)
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
context,
|
context,
|
||||||
|
activityResultLauncher,
|
||||||
intent,
|
intent,
|
||||||
registerInfo
|
registerInfo,
|
||||||
|
typeMode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,15 +44,16 @@ import androidx.recyclerview.widget.SimpleItemAnimator
|
|||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||||
@@ -65,12 +66,11 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
|
|||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
|
||||||
import com.kunzisoft.keepass.utils.DexUtil
|
import com.kunzisoft.keepass.utils.DexUtil
|
||||||
import com.kunzisoft.keepass.utils.MagikeyboardUtil
|
import com.kunzisoft.keepass.utils.MagikeyboardUtil
|
||||||
import com.kunzisoft.keepass.utils.MenuUtil
|
import com.kunzisoft.keepass.utils.MenuUtil
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
|
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
import com.kunzisoft.keepass.utils.allowCreateDocumentByStorageAccessFramework
|
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
import com.kunzisoft.keepass.view.asError
|
import com.kunzisoft.keepass.view.asError
|
||||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
@@ -99,10 +99,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
this.buildActivityResultLauncher()
|
||||||
AutofillHelper.buildActivityResultLauncher(this)
|
|
||||||
else null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -263,7 +261,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
GroupActivity.launch(
|
GroupActivity.launch(
|
||||||
this@FileDatabaseSelectActivity,
|
this@FileDatabaseSelectActivity,
|
||||||
database,
|
database,
|
||||||
PreferencesUtil.enableReadOnlyDatabase(this@FileDatabaseSelectActivity)
|
false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ACTION_DATABASE_LOAD_TASK -> {
|
ACTION_DATABASE_LOAD_TASK -> {
|
||||||
@@ -299,7 +297,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
},
|
},
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() },
|
{ onLaunchActivitySpecialMode() },
|
||||||
mAutofillActivityResultLauncher)
|
mCredentialActivityResultLauncher)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
||||||
@@ -309,7 +307,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
{ onValidateSpecialMode() },
|
{ onValidateSpecialMode() },
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() },
|
{ onLaunchActivitySpecialMode() },
|
||||||
mAutofillActivityResultLauncher)
|
mCredentialActivityResultLauncher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,13 +328,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
// Show open and create button or special mode
|
// Show open and create button or special mode
|
||||||
when (mSpecialMode) {
|
when (mSpecialMode) {
|
||||||
SpecialMode.DEFAULT -> {
|
SpecialMode.DEFAULT -> {
|
||||||
if (packageManager.allowCreateDocumentByStorageAccessFramework()) {
|
createDatabaseButtonView?.visibility = View.VISIBLE
|
||||||
// There is an activity which can handle this intent.
|
|
||||||
createDatabaseButtonView?.visibility = View.VISIBLE
|
|
||||||
} else{
|
|
||||||
// No Activity found that can handle this intent.
|
|
||||||
createDatabaseButtonView?.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// Disable create button if in selection mode or request for autofill
|
// Disable create button if in selection mode or request for autofill
|
||||||
@@ -494,23 +486,46 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
autofillComponent: AutofillComponent,
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo? = null) {
|
searchInfo: SearchInfo? = null) {
|
||||||
AutofillHelper.startActivityForAutofillResult(activity,
|
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(activity,
|
||||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||||
activityResultLauncher,
|
activityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Passkey Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
fun launchForPasskeySelectionResult(activity: Activity,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
searchInfo: SearchInfo? = null) {
|
||||||
|
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
|
||||||
|
activity,
|
||||||
|
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||||
|
activityResultLauncher,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Registration Launch
|
* Registration Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launchForRegistration(context: Context,
|
fun launchForRegistration(context: Context,
|
||||||
registerInfo: RegisterInfo? = null) {
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
|
registerInfo: RegisterInfo? = null,
|
||||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
typeMode: TypeMode) {
|
||||||
registerInfo)
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
|
context,
|
||||||
|
activityResultLauncher,
|
||||||
|
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||||
|
registerInfo,
|
||||||
|
typeMode
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ import android.view.ViewGroup
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
@@ -63,13 +62,17 @@ import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment
|
|||||||
import com.kunzisoft.keepass.activities.dialogs.MainCredentialDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.MainCredentialDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.fragments.GroupFragment
|
import com.kunzisoft.keepass.activities.fragments.GroupFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
import com.kunzisoft.keepass.adapters.BreadcrumbAdapter
|
import com.kunzisoft.keepass.adapters.BreadcrumbAdapter
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
@@ -80,17 +83,16 @@ import com.kunzisoft.keepass.database.element.node.Node
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
|
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
|
||||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||||
import com.kunzisoft.keepass.education.GroupActivityEducation
|
import com.kunzisoft.keepass.education.GroupActivityEducation
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
|
||||||
import com.kunzisoft.keepass.model.DataTime
|
import com.kunzisoft.keepass.model.DataTime
|
||||||
import com.kunzisoft.keepass.model.GroupInfo
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.NEW_NODES_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getNewEntry
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getListNodesFromBundle
|
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.settings.SettingsActivity
|
import com.kunzisoft.keepass.settings.SettingsActivity
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
@@ -113,6 +115,7 @@ import com.kunzisoft.keepass.view.applyWindowInsets
|
|||||||
import com.kunzisoft.keepass.view.hideByFading
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
import com.kunzisoft.keepass.view.setTransparentNavigationBar
|
||||||
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
|
||||||
|
import com.kunzisoft.keepass.view.toastError
|
||||||
import com.kunzisoft.keepass.view.updateLockPaddingStart
|
import com.kunzisoft.keepass.view.updateLockPaddingStart
|
||||||
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
|
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
|
||||||
import com.kunzisoft.keepass.viewmodels.GroupViewModel
|
import com.kunzisoft.keepass.viewmodels.GroupViewModel
|
||||||
@@ -264,10 +267,8 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
mGroupEditViewModel.selectIcon(icon)
|
mGroupEditViewModel.selectIcon(icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
this.buildActivityResultLauncher()
|
||||||
AutofillHelper.buildActivityResultLauncher(this)
|
|
||||||
else null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -484,59 +485,87 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
addNodeButtonView?.setAddEntryClickListener {
|
addNodeButtonView?.setAddEntryClickListener {
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
mMainGroup?.let { currentGroup ->
|
mMainGroup?.let { currentGroup ->
|
||||||
EntrySelectionHelper.doSpecialAction(intent,
|
EntrySelectionHelper.doSpecialAction(
|
||||||
{
|
intent = intent,
|
||||||
|
defaultAction = {
|
||||||
mMainGroup?.nodeId?.let { currentParentGroupId ->
|
mMainGroup?.nodeId?.let { currentParentGroupId ->
|
||||||
EntryEditActivity.launchToCreate(
|
EntryEditActivity.launchToCreate(
|
||||||
this@GroupActivity,
|
activity = this@GroupActivity,
|
||||||
database,
|
database = database,
|
||||||
currentParentGroupId,
|
groupId = currentParentGroupId,
|
||||||
mEntryActivityResultLauncher
|
activityResultLauncher = mEntryActivityResultLauncher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
searchAction = {
|
||||||
// Search not used
|
// Search not used
|
||||||
},
|
},
|
||||||
{ searchInfo ->
|
saveAction = { searchInfo ->
|
||||||
EntryEditActivity.launchToCreateForSave(
|
EntryEditActivity.launchToCreateForSave(
|
||||||
this@GroupActivity,
|
context = this@GroupActivity,
|
||||||
database,
|
database = database,
|
||||||
currentGroup.nodeId,
|
groupId = currentGroup.nodeId,
|
||||||
searchInfo
|
searchInfo = searchInfo
|
||||||
)
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
},
|
||||||
{ searchInfo ->
|
keyboardSelectionAction = { searchInfo ->
|
||||||
EntryEditActivity.launchForKeyboardSelectionResult(
|
EntryEditActivity.launchForKeyboardSelectionResult(
|
||||||
this@GroupActivity,
|
context = this@GroupActivity,
|
||||||
database,
|
database = database,
|
||||||
currentGroup.nodeId,
|
groupId = currentGroup.nodeId,
|
||||||
searchInfo
|
searchInfo = searchInfo
|
||||||
)
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
},
|
||||||
{ searchInfo, autofillComponent ->
|
autofillSelectionAction = { searchInfo, autofillComponent ->
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
EntryEditActivity.launchForAutofillResult(
|
EntryEditActivity.launchForAutofillResult(
|
||||||
this@GroupActivity,
|
activity = this@GroupActivity,
|
||||||
database,
|
database = database,
|
||||||
mAutofillActivityResultLauncher,
|
activityResultLauncher = mCredentialActivityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent = autofillComponent,
|
||||||
currentGroup.nodeId,
|
groupId = currentGroup.nodeId,
|
||||||
searchInfo
|
searchInfo = searchInfo
|
||||||
)
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
} else {
|
} else {
|
||||||
onCancelSpecialMode()
|
onCancelSpecialMode()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ searchInfo ->
|
autofillRegistrationAction = { registerInfo ->
|
||||||
EntryEditActivity.launchToCreateForRegistration(
|
EntryEditActivity.launchToCreateForRegistration(
|
||||||
this@GroupActivity,
|
context = this@GroupActivity,
|
||||||
database,
|
database = database,
|
||||||
currentGroup.nodeId,
|
activityResultLauncher = null,
|
||||||
searchInfo
|
groupId = currentGroup.nodeId,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
passkeySelectionAction = { searchInfo ->
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
EntryEditActivity.launchForPasskeySelectionResult(
|
||||||
|
context = this@GroupActivity,
|
||||||
|
database = database,
|
||||||
|
activityResultLauncher = mCredentialActivityResultLauncher,
|
||||||
|
groupId = currentGroup.nodeId,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
} else {
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
passkeyRegistrationAction = { registerInfo ->
|
||||||
|
EntryEditActivity.launchToCreateForRegistration(
|
||||||
|
context = this@GroupActivity,
|
||||||
|
database = database,
|
||||||
|
activityResultLauncher = mCredentialActivityResultLauncher,
|
||||||
|
groupId = currentGroup.nodeId,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.PASSKEY
|
||||||
)
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
}
|
}
|
||||||
@@ -669,9 +698,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
var entry: Entry? = null
|
var entry: Entry? = null
|
||||||
try {
|
try {
|
||||||
result.data?.getBundle(NEW_NODES_KEY)?.let { newNodesBundle ->
|
entry = result.data?.getNewEntry(database)
|
||||||
entry = getListNodesFromBundle(database, newNodesBundle)[0] as Entry
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to retrieve entry action for selection", e)
|
Log.e(TAG, "Unable to retrieve entry action for selection", e)
|
||||||
}
|
}
|
||||||
@@ -679,30 +706,40 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
EntrySelectionHelper.doSpecialAction(intent,
|
EntrySelectionHelper.doSpecialAction(
|
||||||
{
|
intent = intent,
|
||||||
|
defaultAction = {
|
||||||
// Standard not used after task
|
// Standard not used after task
|
||||||
},
|
},
|
||||||
{
|
searchAction = {
|
||||||
// Search not used
|
// Search not used
|
||||||
},
|
},
|
||||||
{
|
saveAction = {
|
||||||
// Save not used
|
// Save not used
|
||||||
},
|
},
|
||||||
{
|
keyboardSelectionAction = {
|
||||||
// Keyboard selection
|
// Keyboard selection
|
||||||
entry?.let {
|
entry?.let {
|
||||||
entrySelectedForKeyboardSelection(database, it)
|
entrySelectedForKeyboardSelection(database, it)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ _, _ ->
|
autofillSelectionAction = { _, _ ->
|
||||||
// Autofill selection
|
// Autofill selection
|
||||||
entry?.let {
|
entry?.let {
|
||||||
entrySelectedForAutofillSelection(database, it)
|
entrySelectedForAutofillSelection(database, it)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
autofillRegistrationAction = {
|
||||||
// Not use
|
// Not use
|
||||||
|
},
|
||||||
|
passkeySelectionAction = {
|
||||||
|
// Passkey selection
|
||||||
|
entry?.let {
|
||||||
|
entrySelectedForPasskeySelection(database, it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
passkeyRegistrationAction = {
|
||||||
|
// TODO Passkey Registration
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -846,27 +883,28 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
Type.ENTRY -> try {
|
Type.ENTRY -> try {
|
||||||
val entryVersioned = node as Entry
|
val entryVersioned = node as Entry
|
||||||
EntrySelectionHelper.doSpecialAction(intent,
|
EntrySelectionHelper.doSpecialAction(
|
||||||
{
|
intent = intent,
|
||||||
|
defaultAction = {
|
||||||
EntryActivity.launch(
|
EntryActivity.launch(
|
||||||
this@GroupActivity,
|
activity = this@GroupActivity,
|
||||||
database,
|
database = database,
|
||||||
entryVersioned.nodeId,
|
entryId = entryVersioned.nodeId,
|
||||||
mEntryActivityResultLauncher
|
activityResultLauncher = mEntryActivityResultLauncher
|
||||||
)
|
)
|
||||||
// Do not reload group here
|
// Do not reload group here
|
||||||
},
|
},
|
||||||
{
|
searchAction = {
|
||||||
// Nothing here, a search is simply performed
|
// Nothing here, a search is simply performed
|
||||||
},
|
},
|
||||||
{ searchInfo ->
|
saveAction = { searchInfo ->
|
||||||
if (!database.isReadOnly) {
|
if (!database.isReadOnly) {
|
||||||
entrySelectedForSave(database, entryVersioned, searchInfo)
|
entrySelectedForSave(database, entryVersioned, searchInfo)
|
||||||
loadGroup()
|
loadGroup()
|
||||||
} else
|
} else
|
||||||
finish()
|
finish()
|
||||||
},
|
},
|
||||||
{ searchInfo ->
|
keyboardSelectionAction = { searchInfo ->
|
||||||
if (!database.isReadOnly
|
if (!database.isReadOnly
|
||||||
&& searchInfo != null
|
&& searchInfo != null
|
||||||
&& PreferencesUtil.isKeyboardSaveSearchInfoEnable(this@GroupActivity)
|
&& PreferencesUtil.isKeyboardSaveSearchInfoEnable(this@GroupActivity)
|
||||||
@@ -876,7 +914,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
entrySelectedForKeyboardSelection(database, entryVersioned)
|
entrySelectedForKeyboardSelection(database, entryVersioned)
|
||||||
loadGroup()
|
loadGroup()
|
||||||
},
|
},
|
||||||
{ searchInfo, _ ->
|
autofillSelectionAction = { searchInfo, _ ->
|
||||||
if (!database.isReadOnly
|
if (!database.isReadOnly
|
||||||
&& searchInfo != null
|
&& searchInfo != null
|
||||||
&& PreferencesUtil.isAutofillSaveSearchInfoEnable(this@GroupActivity)
|
&& PreferencesUtil.isAutofillSaveSearchInfoEnable(this@GroupActivity)
|
||||||
@@ -886,9 +924,38 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
entrySelectedForAutofillSelection(database, entryVersioned)
|
entrySelectedForAutofillSelection(database, entryVersioned)
|
||||||
loadGroup()
|
loadGroup()
|
||||||
},
|
},
|
||||||
{ registerInfo ->
|
autofillRegistrationAction = { registerInfo ->
|
||||||
if (!database.isReadOnly) {
|
if (!database.isReadOnly) {
|
||||||
entrySelectedForRegistration(database, entryVersioned, registerInfo)
|
entrySelectedForRegistration(
|
||||||
|
database = database,
|
||||||
|
entry = entryVersioned,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL,
|
||||||
|
activityResultLauncher = null // TODO Result launcher autofill #765
|
||||||
|
)
|
||||||
|
loadGroup()
|
||||||
|
} else
|
||||||
|
finish()
|
||||||
|
},
|
||||||
|
passkeySelectionAction = { searchInfo ->
|
||||||
|
if (!database.isReadOnly
|
||||||
|
&& searchInfo != null
|
||||||
|
) {
|
||||||
|
updateEntryWithSearchInfo(database, entryVersioned, searchInfo)
|
||||||
|
}
|
||||||
|
entrySelectedForPasskeySelection(database, entryVersioned)
|
||||||
|
loadGroup()
|
||||||
|
},
|
||||||
|
passkeyRegistrationAction = { registerInfo ->
|
||||||
|
if (!database.isReadOnly) {
|
||||||
|
// TODO Passkey setting && PreferencesUtil.isAutofillOverwriteEnable(this@GroupActivity)
|
||||||
|
entrySelectedForRegistration(
|
||||||
|
database = database,
|
||||||
|
entry = entryVersioned,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.PASSKEY,
|
||||||
|
activityResultLauncher = mCredentialActivityResultLauncher
|
||||||
|
)
|
||||||
loadGroup()
|
loadGroup()
|
||||||
} else
|
} else
|
||||||
finish()
|
finish()
|
||||||
@@ -934,18 +1001,33 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
onValidateSpecialMode()
|
onValidateSpecialMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun entrySelectedForPasskeySelection(database: ContextualDatabase, entry: Entry) {
|
||||||
|
removeSearch()
|
||||||
|
// Build response with the entry selected
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
buildPasskeyResponseAndSetResult(
|
||||||
|
entryInfo = entry.getEntryInfo(database)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onValidateSpecialMode()
|
||||||
|
}
|
||||||
|
|
||||||
private fun entrySelectedForRegistration(
|
private fun entrySelectedForRegistration(
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
entry: Entry,
|
entry: Entry,
|
||||||
registerInfo: RegisterInfo?
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
registerInfo: RegisterInfo?,
|
||||||
|
typeMode: TypeMode
|
||||||
) {
|
) {
|
||||||
removeSearch()
|
removeSearch()
|
||||||
// Registration to update the entry
|
// Registration to update the entry
|
||||||
EntryEditActivity.launchToUpdateForRegistration(
|
EntryEditActivity.launchToUpdateForRegistration(
|
||||||
this@GroupActivity,
|
context = this@GroupActivity,
|
||||||
database,
|
database = database,
|
||||||
entry.nodeId,
|
activityResultLauncher = activityResultLauncher,
|
||||||
registerInfo
|
entryId = entry.nodeId,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = typeMode
|
||||||
)
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
}
|
}
|
||||||
@@ -961,11 +1043,10 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
raw = true,
|
raw = true,
|
||||||
removeTemplateConfiguration = false
|
removeTemplateConfiguration = false
|
||||||
)
|
)
|
||||||
val modification = entryInfo.saveSearchInfo(database, searchInfo)
|
// TODO Transform SearchInfo in RegisterInfo
|
||||||
|
entryInfo.saveSearchInfo(database, searchInfo)
|
||||||
newEntry.setEntryInfo(database, entryInfo)
|
newEntry.setEntryInfo(database, entryInfo)
|
||||||
if (modification) {
|
updateEntry(entry, newEntry)
|
||||||
updateEntry(entry, newEntry)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun finishNodeAction() {
|
private fun finishNodeAction() {
|
||||||
@@ -1373,7 +1454,8 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// Load the previous group
|
// Load the previous group
|
||||||
loadMainGroup(mPreviousGroupsIds.removeLast())
|
loadMainGroup(mPreviousGroupsIds
|
||||||
|
.removeAt(mPreviousGroupsIds.lastIndex))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1569,19 +1651,19 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
fun launchForAutofillSelectionResult(activity: AppCompatActivity,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
activityResultLaunch: ActivityResultLauncher<Intent>?,
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
autofillComponent: AutofillComponent,
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo? = null,
|
searchInfo: SearchInfo? = null,
|
||||||
autoSearch: Boolean = false) {
|
autoSearch: Boolean = false) {
|
||||||
if (database.loaded) {
|
if (database.loaded) {
|
||||||
checkTimeAndBuildIntent(activity, null) { intent ->
|
checkTimeAndBuildIntent(activity, null) { intent ->
|
||||||
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
||||||
AutofillHelper.startActivityForAutofillResult(
|
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
activityResultLaunch,
|
activityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo
|
searchInfo
|
||||||
)
|
)
|
||||||
@@ -1589,21 +1671,49 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Passkey Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
fun launchForPasskeySelectionResult(context: Context,
|
||||||
|
database: ContextualDatabase,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
searchInfo: SearchInfo? = null,
|
||||||
|
autoSearch: Boolean = false) {
|
||||||
|
if (database.loaded) {
|
||||||
|
checkTimeAndBuildIntent(context, null) { intent ->
|
||||||
|
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
||||||
|
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
|
||||||
|
context,
|
||||||
|
intent,
|
||||||
|
activityResultLauncher,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Registration Launch
|
* Registration Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launchForRegistration(context: Context,
|
fun launchForRegistration(context: Context,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
registerInfo: RegisterInfo? = null) {
|
registerInfo: RegisterInfo? = null,
|
||||||
|
typeMode: TypeMode) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
checkTimeAndBuildIntent(context, null) { intent ->
|
checkTimeAndBuildIntent(context, null) { intent ->
|
||||||
intent.putExtra(AUTO_SEARCH_KEY, false)
|
intent.putExtra(AUTO_SEARCH_KEY, false)
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
context,
|
context,
|
||||||
|
activityResultLauncher,
|
||||||
intent,
|
intent,
|
||||||
registerInfo
|
registerInfo,
|
||||||
|
typeMode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1619,153 +1729,221 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
onValidateSpecialMode: () -> Unit,
|
onValidateSpecialMode: () -> Unit,
|
||||||
onCancelSpecialMode: () -> Unit,
|
onCancelSpecialMode: () -> Unit,
|
||||||
onLaunchActivitySpecialMode: () -> Unit,
|
onLaunchActivitySpecialMode: () -> Unit,
|
||||||
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
|
activityResultLauncher: ActivityResultLauncher<Intent>?) {
|
||||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
EntrySelectionHelper.doSpecialAction(
|
||||||
{
|
intent = activity.intent,
|
||||||
// Default action
|
defaultAction = {
|
||||||
launch(
|
// Default action
|
||||||
activity,
|
launch(
|
||||||
|
activity,
|
||||||
|
database,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
},
|
||||||
|
searchAction = { searchInfo ->
|
||||||
|
// Search action
|
||||||
|
if (database.loaded) {
|
||||||
|
launchForSearchResult(activity,
|
||||||
database,
|
database,
|
||||||
true
|
searchInfo,
|
||||||
)
|
true)
|
||||||
},
|
onLaunchActivitySpecialMode()
|
||||||
{ searchInfo ->
|
} else {
|
||||||
// Search action
|
// Simply close if database not opened
|
||||||
if (database.loaded) {
|
onCancelSpecialMode()
|
||||||
launchForSearchResult(activity,
|
}
|
||||||
|
},
|
||||||
|
saveAction = { searchInfo ->
|
||||||
|
// Save info
|
||||||
|
if (database.loaded) {
|
||||||
|
if (!database.isReadOnly) {
|
||||||
|
launchForSaveResult(
|
||||||
|
activity,
|
||||||
database,
|
database,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
true)
|
false
|
||||||
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
} else {
|
} else {
|
||||||
// Simply close if database not opened
|
activity.toastError(RegisterInReadOnlyDatabaseException())
|
||||||
onCancelSpecialMode()
|
onCancelSpecialMode()
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
{ searchInfo ->
|
},
|
||||||
// Save info
|
keyboardSelectionAction = { searchInfo ->
|
||||||
if (database.loaded) {
|
// Keyboard selection
|
||||||
if (!database.isReadOnly) {
|
SearchHelper.checkAutoSearchInfo(
|
||||||
launchForSaveResult(
|
context = activity,
|
||||||
activity,
|
database = database,
|
||||||
database,
|
searchInfo = searchInfo,
|
||||||
searchInfo,
|
onItemsFound = { _, items ->
|
||||||
false
|
MagikeyboardService.performSelection(
|
||||||
)
|
items,
|
||||||
onLaunchActivitySpecialMode()
|
{ entryInfo ->
|
||||||
} else {
|
// Keyboard populated
|
||||||
Toast.makeText(
|
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||||
activity.applicationContext,
|
activity,
|
||||||
R.string.autofill_read_only_save,
|
entryInfo
|
||||||
Toast.LENGTH_LONG
|
|
||||||
)
|
|
||||||
.show()
|
|
||||||
onCancelSpecialMode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ searchInfo ->
|
|
||||||
// Keyboard selection
|
|
||||||
SearchHelper.checkAutoSearchInfo(activity,
|
|
||||||
database,
|
|
||||||
searchInfo,
|
|
||||||
{ _, items ->
|
|
||||||
MagikeyboardService.performSelection(
|
|
||||||
items,
|
|
||||||
{ entryInfo ->
|
|
||||||
// Keyboard populated
|
|
||||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
|
||||||
activity,
|
|
||||||
entryInfo
|
|
||||||
)
|
|
||||||
onValidateSpecialMode()
|
|
||||||
},
|
|
||||||
{ autoSearch ->
|
|
||||||
launchForKeyboardSelectionResult(activity,
|
|
||||||
database,
|
|
||||||
searchInfo,
|
|
||||||
autoSearch)
|
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
onValidateSpecialMode()
|
||||||
},
|
},
|
||||||
{
|
{ autoSearch ->
|
||||||
// Here no search info found, disable auto search
|
|
||||||
launchForKeyboardSelectionResult(activity,
|
launchForKeyboardSelectionResult(activity,
|
||||||
database,
|
database,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
false)
|
autoSearch)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
|
||||||
{
|
|
||||||
// Simply close if database not opened, normally not happened
|
|
||||||
onCancelSpecialMode()
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onItemNotFound = {
|
||||||
|
// Here no search info found, disable auto search
|
||||||
|
launchForKeyboardSelectionResult(activity,
|
||||||
|
database,
|
||||||
|
searchInfo,
|
||||||
|
false)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
// Simply close if database not opened, normally not happened
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
autofillSelectionAction = { searchInfo, autofillComponent ->
|
||||||
|
// Autofill selection
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
SearchHelper.checkAutoSearchInfo(
|
||||||
|
context = activity,
|
||||||
|
database = database,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { openedDatabase, items ->
|
||||||
|
// Response is build
|
||||||
|
AutofillHelper.buildResponseAndSetResult(activity, openedDatabase, items)
|
||||||
|
onValidateSpecialMode()
|
||||||
|
},
|
||||||
|
onItemNotFound = {
|
||||||
|
// Here no search info found, disable auto search
|
||||||
|
launchForAutofillSelectionResult(
|
||||||
|
activity = activity,
|
||||||
|
database = database,
|
||||||
|
autofillComponent = autofillComponent,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
autoSearch = false,
|
||||||
|
activityResultLauncher = activityResultLauncher)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
// Simply close if database not opened, normally not happened
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
},
|
} else {
|
||||||
{ searchInfo, autofillComponent ->
|
onCancelSpecialMode()
|
||||||
// Autofill selection
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
},
|
||||||
SearchHelper.checkAutoSearchInfo(activity,
|
autofillRegistrationAction = { registerInfo ->
|
||||||
database,
|
// Autofill registration
|
||||||
searchInfo,
|
if (!database.isReadOnly) {
|
||||||
{ openedDatabase, items ->
|
SearchHelper.checkAutoSearchInfo(
|
||||||
// Response is build
|
context = activity,
|
||||||
AutofillHelper.buildResponseAndSetResult(activity, openedDatabase, items)
|
database = database,
|
||||||
|
searchInfo = registerInfo?.searchInfo,
|
||||||
|
onItemsFound = { _, _ ->
|
||||||
|
// No auto search, it's a registration
|
||||||
|
launchForRegistration(
|
||||||
|
context = activity,
|
||||||
|
activityResultLauncher = null, // TODO Autofill result Launcher #765
|
||||||
|
database = database,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
onItemNotFound = {
|
||||||
|
// Here no search info found, disable auto search
|
||||||
|
launchForRegistration(
|
||||||
|
context = activity,
|
||||||
|
activityResultLauncher = null, // TODO Autofill result Launcher #765
|
||||||
|
database = database,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
// Simply close if database not opened, normally not happened
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
activity.toastError(RegisterInReadOnlyDatabaseException())
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
passkeySelectionAction = { searchInfo ->
|
||||||
|
// Passkey selection
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
SearchHelper.checkAutoSearchInfo(
|
||||||
|
context = activity,
|
||||||
|
database = database,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { _, items ->
|
||||||
|
// Response is build
|
||||||
|
EntrySelectionHelper.performSelection(
|
||||||
|
items = items,
|
||||||
|
actionPopulateCredentialProvider = { entryInfo ->
|
||||||
|
activity.buildPasskeyResponseAndSetResult(entryInfo)
|
||||||
onValidateSpecialMode()
|
onValidateSpecialMode()
|
||||||
},
|
},
|
||||||
{
|
actionEntrySelection = {
|
||||||
// Here no search info found, disable auto search
|
launchForPasskeySelectionResult(
|
||||||
launchForAutofillResult(activity,
|
context = activity,
|
||||||
database,
|
database = database,
|
||||||
autofillActivityResultLauncher,
|
searchInfo = searchInfo,
|
||||||
autofillComponent,
|
activityResultLauncher = activityResultLauncher,
|
||||||
searchInfo,
|
autoSearch = true
|
||||||
false)
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
|
||||||
{
|
|
||||||
// Simply close if database not opened, normally not happened
|
|
||||||
onCancelSpecialMode()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
},
|
||||||
onCancelSpecialMode()
|
onItemNotFound = {
|
||||||
}
|
// Here no search info found, disable auto search
|
||||||
},
|
launchForPasskeySelectionResult(
|
||||||
{ registerInfo ->
|
context = activity,
|
||||||
// Autofill registration
|
database = database,
|
||||||
if (!database.isReadOnly) {
|
searchInfo = searchInfo,
|
||||||
SearchHelper.checkAutoSearchInfo(activity,
|
activityResultLauncher = activityResultLauncher
|
||||||
database,
|
)
|
||||||
registerInfo?.searchInfo,
|
onLaunchActivitySpecialMode()
|
||||||
{ _, _ ->
|
},
|
||||||
// No auto search, it's a registration
|
onDatabaseClosed = {
|
||||||
launchForRegistration(activity,
|
// Simply close if database not opened, normally not happened
|
||||||
database,
|
onCancelSpecialMode()
|
||||||
registerInfo)
|
}
|
||||||
onLaunchActivitySpecialMode()
|
)
|
||||||
},
|
} else {
|
||||||
{
|
onCancelSpecialMode()
|
||||||
// Here no search info found, disable auto search
|
}
|
||||||
launchForRegistration(activity,
|
},
|
||||||
database,
|
passkeyRegistrationAction = { registerInfo ->
|
||||||
registerInfo)
|
// Passkey registration
|
||||||
onLaunchActivitySpecialMode()
|
if (!database.isReadOnly) {
|
||||||
},
|
launchForRegistration(
|
||||||
{
|
context = activity,
|
||||||
// Simply close if database not opened, normally not happened
|
activityResultLauncher = activityResultLauncher,
|
||||||
onCancelSpecialMode()
|
database = database,
|
||||||
}
|
registerInfo = registerInfo,
|
||||||
)
|
typeMode = TypeMode.PASSKEY
|
||||||
} else {
|
)
|
||||||
Toast.makeText(activity.applicationContext,
|
onLaunchActivitySpecialMode()
|
||||||
R.string.autofill_read_only_save,
|
} else {
|
||||||
Toast.LENGTH_LONG)
|
activity.toastError(RegisterInReadOnlyDatabaseException())
|
||||||
.show()
|
onCancelSpecialMode()
|
||||||
onCancelSpecialMode()
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,20 +43,23 @@ import androidx.biometric.BiometricManager
|
|||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
|
||||||
import com.kunzisoft.keepass.biometric.DeviceUnlockFragment
|
import com.kunzisoft.keepass.biometric.DeviceUnlockFragment
|
||||||
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
|
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
|
||||||
import com.kunzisoft.keepass.biometric.deviceUnlockError
|
import com.kunzisoft.keepass.biometric.deviceUnlockError
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
@@ -66,6 +69,7 @@ import com.kunzisoft.keepass.hardware.HardwareKey
|
|||||||
import com.kunzisoft.keepass.model.CipherDecryptDatabase
|
import com.kunzisoft.keepass.model.CipherDecryptDatabase
|
||||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||||
import com.kunzisoft.keepass.model.CredentialStorage
|
import com.kunzisoft.keepass.model.CredentialStorage
|
||||||
|
import com.kunzisoft.keepass.model.DatabaseFile
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
|
||||||
@@ -73,8 +77,8 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
|
|||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
||||||
import com.kunzisoft.keepass.settings.AdvancedUnlockSettingsActivity
|
|
||||||
import com.kunzisoft.keepass.settings.AppearanceSettingsActivity
|
import com.kunzisoft.keepass.settings.AppearanceSettingsActivity
|
||||||
|
import com.kunzisoft.keepass.settings.DeviceUnlockSettingsActivity
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
||||||
@@ -97,7 +101,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
private var toolbar: Toolbar? = null
|
private var toolbar: Toolbar? = null
|
||||||
private var filenameView: TextView? = null
|
private var filenameView: TextView? = null
|
||||||
private var logotypeButton: View? = null
|
private var logotypeButton: View? = null
|
||||||
private var advancedUnlockButton: View? = null
|
private var deviceUnlockButton: View? = null
|
||||||
private var mainCredentialView: MainCredentialView? = null
|
private var mainCredentialView: MainCredentialView? = null
|
||||||
private var confirmButtonView: Button? = null
|
private var confirmButtonView: Button? = null
|
||||||
private var infoContainerView: ViewGroup? = null
|
private var infoContainerView: ViewGroup? = null
|
||||||
@@ -105,7 +109,11 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
private var deviceUnlockFragment: DeviceUnlockFragment? = null
|
private var deviceUnlockFragment: DeviceUnlockFragment? = null
|
||||||
|
|
||||||
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
|
private val mDatabaseFileViewModel: DatabaseFileViewModel by viewModels()
|
||||||
private val mDeviceUnlockViewModel: DeviceUnlockViewModel by viewModels()
|
private val mDeviceUnlockViewModel: DeviceUnlockViewModel? by lazy {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
ViewModelProvider(this)[DeviceUnlockViewModel::class.java]
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
private val mPasswordActivityEducation = PasswordActivityEducation(this)
|
private val mPasswordActivityEducation = PasswordActivityEducation(this)
|
||||||
|
|
||||||
@@ -120,10 +128,8 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
private var mReadOnly: Boolean = false
|
private var mReadOnly: Boolean = false
|
||||||
private var mForceReadOnly: Boolean = false
|
private var mForceReadOnly: Boolean = false
|
||||||
|
|
||||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
this.buildActivityResultLauncher()
|
||||||
AutofillHelper.buildActivityResultLauncher(this)
|
|
||||||
else null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -138,7 +144,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
|
|
||||||
filenameView = findViewById(R.id.filename)
|
filenameView = findViewById(R.id.filename)
|
||||||
logotypeButton = findViewById(R.id.activity_password_logotype)
|
logotypeButton = findViewById(R.id.activity_password_logotype)
|
||||||
advancedUnlockButton = findViewById(R.id.fragment_advanced_unlock_container_view)
|
deviceUnlockButton = findViewById(R.id.fragment_device_unlock_container_view)
|
||||||
mainCredentialView = findViewById(R.id.activity_password_credentials)
|
mainCredentialView = findViewById(R.id.activity_password_credentials)
|
||||||
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
confirmButtonView = findViewById(R.id.activity_password_open_button)
|
||||||
infoContainerView = findViewById(R.id.activity_password_info_container)
|
infoContainerView = findViewById(R.id.activity_password_info_container)
|
||||||
@@ -147,7 +153,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) {
|
mReadOnly = if (savedInstanceState != null && savedInstanceState.containsKey(KEY_READ_ONLY)) {
|
||||||
savedInstanceState.getBoolean(KEY_READ_ONLY)
|
savedInstanceState.getBoolean(KEY_READ_ONLY)
|
||||||
} else {
|
} else {
|
||||||
PreferencesUtil.enableReadOnlyDatabase(this)
|
false
|
||||||
}
|
}
|
||||||
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
mRememberKeyFile = PreferencesUtil.rememberKeyFileLocations(this)
|
||||||
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this)
|
mRememberHardwareKey = PreferencesUtil.rememberHardwareKey(this)
|
||||||
@@ -175,7 +181,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
// Listen password checkbox to init advanced unlock and confirmation button
|
// Listen password checkbox to init advanced unlock and confirmation button
|
||||||
mainCredentialView?.onConditionToStoreCredentialChanged = { _, verified ->
|
mainCredentialView?.onConditionToStoreCredentialChanged = { _, verified ->
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
mDeviceUnlockViewModel.checkConditionToStoreCredential(
|
mDeviceUnlockViewModel?.checkConditionToStoreCredential(
|
||||||
condition = verified
|
condition = verified
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -203,6 +209,13 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
}
|
}
|
||||||
mForceReadOnly = databaseFileNotExists
|
mForceReadOnly = databaseFileNotExists
|
||||||
|
|
||||||
|
// Restore read-only state from database file if not forced
|
||||||
|
if (!mForceReadOnly) {
|
||||||
|
databaseFile?.readOnly?.let { savedReadOnlyState ->
|
||||||
|
mReadOnly = savedReadOnlyState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
|
|
||||||
// Post init uri with KeyFile only if needed
|
// Post init uri with KeyFile only if needed
|
||||||
@@ -233,29 +246,31 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
mDeviceUnlockViewModel.uiState.collect { uiState ->
|
mDeviceUnlockViewModel?.let { deviceUnlockViewModel ->
|
||||||
// New value received
|
deviceUnlockViewModel.uiState.collect { uiState ->
|
||||||
uiState.credentialRequiredCipher?.let { cipher ->
|
// New value received
|
||||||
mDeviceUnlockViewModel.encryptCredential(
|
uiState.credentialRequiredCipher?.let { cipher ->
|
||||||
credential = getCredentialForEncryption(),
|
deviceUnlockViewModel.encryptCredential(
|
||||||
cipher = cipher
|
credential = getCredentialForEncryption(),
|
||||||
)
|
cipher = cipher
|
||||||
}
|
)
|
||||||
uiState.cipherEncryptDatabase?.let { cipherEncryptDatabase ->
|
}
|
||||||
onCredentialEncrypted(cipherEncryptDatabase)
|
uiState.cipherEncryptDatabase?.let { cipherEncryptDatabase ->
|
||||||
mDeviceUnlockViewModel.consumeCredentialEncrypted()
|
onCredentialEncrypted(cipherEncryptDatabase)
|
||||||
}
|
deviceUnlockViewModel.consumeCredentialEncrypted()
|
||||||
uiState.cipherDecryptDatabase?.let { cipherDecryptDatabase ->
|
}
|
||||||
onCredentialDecrypted(cipherDecryptDatabase)
|
uiState.cipherDecryptDatabase?.let { cipherDecryptDatabase ->
|
||||||
mDeviceUnlockViewModel.consumeCredentialDecrypted()
|
onCredentialDecrypted(cipherDecryptDatabase)
|
||||||
}
|
deviceUnlockViewModel.consumeCredentialDecrypted()
|
||||||
uiState.exception?.let { error ->
|
}
|
||||||
Snackbar.make(
|
uiState.exception?.let { error ->
|
||||||
coordinatorLayout,
|
Snackbar.make(
|
||||||
deviceUnlockError(error, this@MainCredentialActivity),
|
coordinatorLayout,
|
||||||
Snackbar.LENGTH_LONG
|
deviceUnlockError(error, this@MainCredentialActivity),
|
||||||
).asError().show()
|
Snackbar.LENGTH_LONG
|
||||||
mDeviceUnlockViewModel.exceptionShown()
|
).asError().show()
|
||||||
|
deviceUnlockViewModel.exceptionShown()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -268,14 +283,14 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
|
|
||||||
// Init Biometric elements only if allowed
|
// Init Biometric elements only if allowed
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||||
&& PreferencesUtil.isAdvancedUnlockEnable(this)) {
|
&& PreferencesUtil.isDeviceUnlockEnable(this)) {
|
||||||
deviceUnlockFragment = supportFragmentManager
|
deviceUnlockFragment = supportFragmentManager
|
||||||
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? DeviceUnlockFragment?
|
.findFragmentByTag(UNLOCK_FRAGMENT_TAG) as? DeviceUnlockFragment?
|
||||||
if (deviceUnlockFragment == null) {
|
if (deviceUnlockFragment == null) {
|
||||||
deviceUnlockFragment = DeviceUnlockFragment().also {
|
deviceUnlockFragment = DeviceUnlockFragment().also {
|
||||||
supportFragmentManager.commit {
|
supportFragmentManager.commit {
|
||||||
replace(
|
replace(
|
||||||
R.id.fragment_advanced_unlock_container_view,
|
R.id.fragment_device_unlock_container_view,
|
||||||
it,
|
it,
|
||||||
UNLOCK_FRAGMENT_TAG
|
UNLOCK_FRAGMENT_TAG
|
||||||
)
|
)
|
||||||
@@ -421,7 +436,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
{ onValidateSpecialMode() },
|
{ onValidateSpecialMode() },
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() },
|
{ onLaunchActivitySpecialMode() },
|
||||||
mAutofillActivityResultLauncher
|
mCredentialActivityResultLauncher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -508,7 +523,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
} else {
|
} else {
|
||||||
// Init Biometric elements
|
// Init Biometric elements
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
mDeviceUnlockViewModel.connect(databaseFileUri)
|
mDeviceUnlockViewModel?.connect(databaseFileUri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,9 +576,9 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
mSpecialMode == SpecialMode.SAVE
|
mSpecialMode == SpecialMode.SAVE
|
||||||
|| mSpecialMode == SpecialMode.REGISTRATION)
|
|| mSpecialMode == SpecialMode.REGISTRATION)
|
||||||
) {
|
) {
|
||||||
Log.e(TAG, getString(R.string.autofill_read_only_save))
|
Log.e(TAG, getString(R.string.error_save_read_only))
|
||||||
Snackbar.make(coordinatorLayout,
|
Snackbar.make(coordinatorLayout,
|
||||||
R.string.autofill_read_only_save,
|
R.string.error_save_read_only,
|
||||||
Snackbar.LENGTH_LONG).asError().show()
|
Snackbar.LENGTH_LONG).asError().show()
|
||||||
} else {
|
} else {
|
||||||
databaseFileUri?.let { databaseUri ->
|
databaseFileUri?.let { databaseUri ->
|
||||||
@@ -652,7 +667,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
try {
|
try {
|
||||||
menu.findItem(R.id.menu_open_file_read_mode_key)
|
menu.findItem(R.id.menu_open_file_read_mode_key)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to find read mode menu")
|
Log.e(TAG, "Unable to find read mode menu", e)
|
||||||
}
|
}
|
||||||
performedNextEducation(menu)
|
performedNextEducation(menu)
|
||||||
},
|
},
|
||||||
@@ -665,14 +680,14 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
val biometricCanAuthenticate = DeviceUnlockManager.canAuthenticate(this)
|
val biometricCanAuthenticate = DeviceUnlockManager.canAuthenticate(this)
|
||||||
if ((biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
if ((biometricCanAuthenticate == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
|
||||||
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
|| biometricCanAuthenticate == BiometricManager.BIOMETRIC_SUCCESS)
|
||||||
&& advancedUnlockButton != null) {
|
&& deviceUnlockButton != null) {
|
||||||
mPasswordActivityEducation.checkAndPerformedBiometricEducation(
|
mPasswordActivityEducation.checkAndPerformedBiometricEducation(
|
||||||
advancedUnlockButton!!,
|
deviceUnlockButton!!,
|
||||||
{
|
{
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent(
|
Intent(
|
||||||
this,
|
this,
|
||||||
AdvancedUnlockSettingsActivity::class.java
|
DeviceUnlockSettingsActivity::class.java
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -681,7 +696,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ignored: Exception) {}
|
} catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -702,6 +717,12 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
R.id.menu_open_file_read_mode_key -> {
|
R.id.menu_open_file_read_mode_key -> {
|
||||||
mReadOnly = !mReadOnly
|
mReadOnly = !mReadOnly
|
||||||
changeOpenFileReadIcon(item)
|
changeOpenFileReadIcon(item)
|
||||||
|
// Save the read-only state to database
|
||||||
|
mDatabaseFileUri?.let { databaseUri ->
|
||||||
|
FileDatabaseHistoryAction.getInstance(applicationContext).addOrUpdateDatabaseFile(
|
||||||
|
DatabaseFile(databaseUri = databaseUri, readOnly = mReadOnly)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
else -> MenuUtil.onDefaultMenuOptionsItemSelected(this, item)
|
||||||
}
|
}
|
||||||
@@ -712,7 +733,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
mDeviceUnlockViewModel.disconnect()
|
mDeviceUnlockViewModel?.disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -830,14 +851,14 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
hardwareKey: HardwareKey?,
|
hardwareKey: HardwareKey?,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
autofillComponent: AutofillComponent,
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo?) {
|
searchInfo: SearchInfo?) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
AutofillHelper.startActivityForAutofillResult(
|
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
activityResultLauncher,
|
activityResultLauncher,
|
||||||
@@ -846,21 +867,51 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Passkey Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launchForPasskeyResult(activity: Activity,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
hardwareKey: HardwareKey?,
|
||||||
|
searchInfo: SearchInfo?) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
|
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
|
||||||
|
activity,
|
||||||
|
intent,
|
||||||
|
activityResultLauncher,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Registration Launch
|
* Registration Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launchForRegistration(activity: Activity,
|
fun launchForRegistration(
|
||||||
databaseFile: Uri,
|
activity: Activity,
|
||||||
keyFile: Uri?,
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
hardwareKey: HardwareKey?,
|
databaseFile: Uri,
|
||||||
registerInfo: RegisterInfo?) {
|
keyFile: Uri?,
|
||||||
|
hardwareKey: HardwareKey?,
|
||||||
|
typeMode: TypeMode,
|
||||||
|
registerInfo: RegisterInfo?
|
||||||
|
) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
activity,
|
context = activity,
|
||||||
intent,
|
activityResultLauncher = activityResultLauncher,
|
||||||
registerInfo)
|
intent = intent,
|
||||||
|
typeMode = typeMode,
|
||||||
|
registerInfo = registerInfo
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -876,74 +927,104 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
||||||
onCancelSpecialMode: () -> Unit,
|
onCancelSpecialMode: () -> Unit,
|
||||||
onLaunchActivitySpecialMode: () -> Unit,
|
onLaunchActivitySpecialMode: () -> Unit,
|
||||||
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
|
activityResultLauncher: ActivityResultLauncher<Intent>?) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
EntrySelectionHelper.doSpecialAction(
|
||||||
{
|
intent = activity.intent,
|
||||||
launch(
|
defaultAction = {
|
||||||
activity,
|
launch(
|
||||||
databaseUri,
|
activity = activity,
|
||||||
keyFile,
|
databaseFile = databaseUri,
|
||||||
hardwareKey
|
keyFile = keyFile,
|
||||||
)
|
hardwareKey = hardwareKey
|
||||||
},
|
)
|
||||||
{ searchInfo -> // Search Action
|
},
|
||||||
launchForSearchResult(
|
searchAction = { searchInfo ->
|
||||||
activity,
|
launchForSearchResult(
|
||||||
databaseUri,
|
activity = activity,
|
||||||
keyFile,
|
databaseFile = databaseUri,
|
||||||
hardwareKey,
|
keyFile = keyFile,
|
||||||
searchInfo
|
hardwareKey = hardwareKey,
|
||||||
)
|
searchInfo = searchInfo
|
||||||
onLaunchActivitySpecialMode()
|
)
|
||||||
},
|
onLaunchActivitySpecialMode()
|
||||||
{ searchInfo -> // Save Action
|
},
|
||||||
launchForSaveResult(
|
saveAction = { searchInfo ->
|
||||||
activity,
|
launchForSaveResult(
|
||||||
databaseUri,
|
activity = activity,
|
||||||
keyFile,
|
databaseFile = databaseUri,
|
||||||
hardwareKey,
|
keyFile = keyFile,
|
||||||
searchInfo
|
hardwareKey = hardwareKey,
|
||||||
)
|
searchInfo = searchInfo
|
||||||
onLaunchActivitySpecialMode()
|
)
|
||||||
},
|
onLaunchActivitySpecialMode()
|
||||||
{ searchInfo -> // Keyboard Selection Action
|
},
|
||||||
launchForKeyboardResult(
|
keyboardSelectionAction = { searchInfo ->
|
||||||
activity,
|
launchForKeyboardResult(
|
||||||
databaseUri,
|
activity = activity,
|
||||||
keyFile,
|
databaseFile = databaseUri,
|
||||||
hardwareKey,
|
keyFile = keyFile,
|
||||||
searchInfo
|
hardwareKey = hardwareKey,
|
||||||
)
|
searchInfo = searchInfo
|
||||||
onLaunchActivitySpecialMode()
|
)
|
||||||
},
|
onLaunchActivitySpecialMode()
|
||||||
{ searchInfo, autofillComponent -> // Autofill Selection Action
|
},
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
autofillSelectionAction = { searchInfo, autofillComponent ->
|
||||||
launchForAutofillResult(
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
activity,
|
launchForAutofillResult(
|
||||||
databaseUri,
|
activity = activity,
|
||||||
keyFile,
|
activityResultLauncher = activityResultLauncher,
|
||||||
hardwareKey,
|
databaseFile = databaseUri,
|
||||||
autofillActivityResultLauncher,
|
keyFile = keyFile,
|
||||||
autofillComponent,
|
hardwareKey = hardwareKey,
|
||||||
searchInfo
|
autofillComponent = autofillComponent,
|
||||||
)
|
searchInfo = searchInfo
|
||||||
onLaunchActivitySpecialMode()
|
|
||||||
} else {
|
|
||||||
onCancelSpecialMode()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ registerInfo -> // Registration Action
|
|
||||||
launchForRegistration(
|
|
||||||
activity,
|
|
||||||
databaseUri,
|
|
||||||
keyFile,
|
|
||||||
hardwareKey,
|
|
||||||
registerInfo
|
|
||||||
)
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
|
} else {
|
||||||
|
onCancelSpecialMode()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
autofillRegistrationAction = { registerInfo ->
|
||||||
|
launchForRegistration(
|
||||||
|
activity = activity,
|
||||||
|
activityResultLauncher = activityResultLauncher,
|
||||||
|
databaseFile = databaseUri,
|
||||||
|
keyFile = keyFile,
|
||||||
|
hardwareKey = hardwareKey,
|
||||||
|
typeMode = TypeMode.AUTOFILL,
|
||||||
|
registerInfo = registerInfo
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
passkeySelectionAction = { searchInfo ->
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
launchForPasskeyResult(
|
||||||
|
activity = activity,
|
||||||
|
activityResultLauncher = activityResultLauncher,
|
||||||
|
databaseFile = databaseUri,
|
||||||
|
keyFile = keyFile,
|
||||||
|
hardwareKey = hardwareKey,
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
} else {
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
passkeyRegistrationAction = { registerInfo ->
|
||||||
|
launchForRegistration(
|
||||||
|
activity = activity,
|
||||||
|
activityResultLauncher = activityResultLauncher,
|
||||||
|
databaseFile = databaseUri,
|
||||||
|
keyFile = keyFile,
|
||||||
|
hardwareKey = hardwareKey,
|
||||||
|
typeMode = TypeMode.PASSKEY,
|
||||||
|
registerInfo = registerInfo
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
} catch (e: FileNotFoundException) {
|
} catch (e: FileNotFoundException) {
|
||||||
fileNoFoundAction(e)
|
fileNoFoundAction(e)
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
|
@Deprecated(message = "")
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import com.kunzisoft.keepass.database.element.icon.IconImage
|
|||||||
import com.kunzisoft.keepass.model.GroupInfo
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
|
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
|
||||||
import com.kunzisoft.keepass.utils.UuidUtil
|
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
import com.kunzisoft.keepass.view.DateTimeFieldView
|
import com.kunzisoft.keepass.view.DateTimeFieldView
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ class GroupDialogFragment : DatabaseDialogFragment() {
|
|||||||
searchableView.text = stringFromInheritableBoolean(mGroupInfo.searchable)
|
searchableView.text = stringFromInheritableBoolean(mGroupInfo.searchable)
|
||||||
autoTypeView.text = stringFromInheritableBoolean(mGroupInfo.enableAutoType,
|
autoTypeView.text = stringFromInheritableBoolean(mGroupInfo.enableAutoType,
|
||||||
mGroupInfo.defaultAutoTypeSequence)
|
mGroupInfo.defaultAutoTypeSequence)
|
||||||
val uuid = UuidUtil.toHexString(mGroupInfo.id)
|
val uuid = mGroupInfo.id?.asHexString()
|
||||||
if (uuid == null || uuid.isEmpty()) {
|
if (uuid == null || uuid.isEmpty()) {
|
||||||
uuidContainerView.visibility = View.GONE
|
uuidContainerView.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -29,7 +29,11 @@ import android.view.MotionEvent
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.*
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.Spinner
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
@@ -40,15 +44,15 @@ import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_OTP_DIGITS
|
|||||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_TOTP_PERIOD
|
import com.kunzisoft.keepass.otp.OtpElement.Companion.MAX_TOTP_PERIOD
|
||||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_HOTP_COUNTER
|
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_HOTP_COUNTER
|
||||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_DIGITS
|
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_DIGITS
|
||||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
|
|
||||||
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_SECRET
|
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_OTP_SECRET
|
||||||
|
import com.kunzisoft.keepass.otp.OtpElement.Companion.MIN_TOTP_PERIOD
|
||||||
import com.kunzisoft.keepass.otp.OtpTokenType
|
import com.kunzisoft.keepass.otp.OtpTokenType
|
||||||
import com.kunzisoft.keepass.otp.OtpType
|
import com.kunzisoft.keepass.otp.OtpType
|
||||||
import com.kunzisoft.keepass.otp.TokenCalculator
|
import com.kunzisoft.keepass.otp.TokenCalculator
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
|
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
class SetOTPDialogFragment : DatabaseDialogFragment() {
|
class SetOTPDialogFragment : DatabaseDialogFragment() {
|
||||||
|
|
||||||
|
|||||||
@@ -176,21 +176,14 @@ class SortDialogFragment : DatabaseDialogFragment() {
|
|||||||
return bundle
|
return bundle
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getInstance(sortNodeEnum: SortNodeEnum,
|
|
||||||
ascending: Boolean,
|
|
||||||
groupsBefore: Boolean): SortDialogFragment {
|
|
||||||
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
|
|
||||||
val fragment = SortDialogFragment()
|
|
||||||
fragment.arguments = bundle
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getInstance(sortNodeEnum: SortNodeEnum,
|
fun getInstance(sortNodeEnum: SortNodeEnum,
|
||||||
ascending: Boolean,
|
ascending: Boolean,
|
||||||
groupsBefore: Boolean,
|
groupsBefore: Boolean,
|
||||||
recycleBinBottom: Boolean): SortDialogFragment {
|
recycleBinBottom: Boolean?): SortDialogFragment {
|
||||||
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
|
val bundle = buildBundle(sortNodeEnum, ascending, groupsBefore)
|
||||||
bundle.putBoolean(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY, recycleBinBottom)
|
recycleBinBottom?.let {
|
||||||
|
bundle.putBoolean(SORT_RECYCLE_BIN_BOTTOM_BUNDLE_KEY, recycleBinBottom)
|
||||||
|
}
|
||||||
val fragment = SortDialogFragment()
|
val fragment = SortDialogFragment()
|
||||||
fragment.arguments = bundle
|
fragment.arguments = bundle
|
||||||
return fragment
|
return fragment
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import com.kunzisoft.keepass.model.StreamDirection
|
|||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
|
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
|
||||||
import com.kunzisoft.keepass.utils.UuidUtil
|
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||||
import com.kunzisoft.keepass.view.TemplateView
|
import com.kunzisoft.keepass.view.TemplateView
|
||||||
import com.kunzisoft.keepass.view.hideByFading
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
import com.kunzisoft.keepass.view.showByFading
|
import com.kunzisoft.keepass.view.showByFading
|
||||||
@@ -184,7 +184,7 @@ class EntryFragment: DatabaseFragment() {
|
|||||||
// customDataView.text = entryInfo?.customData?.toString()
|
// customDataView.text = entryInfo?.customData?.toString()
|
||||||
|
|
||||||
// Assign special data
|
// Assign special data
|
||||||
uuidReferenceView.text = UuidUtil.toHexString(entryInfo?.id)
|
uuidReferenceView.text = entryInfo?.id?.asHexString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showClipboardDialog() {
|
private fun showClipboardDialog() {
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
|
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.adapters.NodesAdapter
|
import com.kunzisoft.keepass.adapters.NodesAdapter
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
@@ -76,9 +76,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
|
|
||||||
private var specialMode: SpecialMode = SpecialMode.DEFAULT
|
private var specialMode: SpecialMode = SpecialMode.DEFAULT
|
||||||
|
|
||||||
private var mRecycleBinEnable: Boolean = false
|
|
||||||
private var mRecycleBin: Group? = null
|
|
||||||
|
|
||||||
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
|
private var mRecycleViewScrollListener = object : RecyclerView.OnScrollListener() {
|
||||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||||
super.onScrollStateChanged(recyclerView, newState)
|
super.onScrollStateChanged(recyclerView, newState)
|
||||||
@@ -102,21 +99,14 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
R.id.menu_sort -> {
|
R.id.menu_sort -> {
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
val sortDialogFragment: SortDialogFragment =
|
val sortDialogFragment: SortDialogFragment =
|
||||||
if (mRecycleBinEnable) {
|
SortDialogFragment.getInstance(
|
||||||
SortDialogFragment.getInstance(
|
PreferencesUtil.getListSort(context),
|
||||||
PreferencesUtil.getListSort(context),
|
PreferencesUtil.getAscendingSort(context),
|
||||||
PreferencesUtil.getAscendingSort(context),
|
PreferencesUtil.getGroupsBeforeSort(context),
|
||||||
PreferencesUtil.getGroupsBeforeSort(context),
|
if (mDatabase?.isRecycleBinEnabled == true) {
|
||||||
PreferencesUtil.getRecycleBinBottomSort(context)
|
PreferencesUtil.getRecycleBinBottomSort(context)
|
||||||
)
|
} else null
|
||||||
} else {
|
)
|
||||||
SortDialogFragment.getInstance(
|
|
||||||
PreferencesUtil.getListSort(context),
|
|
||||||
PreferencesUtil.getAscendingSort(context),
|
|
||||||
PreferencesUtil.getGroupsBeforeSort(context)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
sortDialogFragment.show(childFragmentManager, "sortDialog")
|
sortDialogFragment.show(childFragmentManager, "sortDialog")
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
@@ -165,9 +155,6 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
mRecycleBinEnable = database?.isRecycleBinEnabled == true
|
|
||||||
mRecycleBin = database?.recycleBin
|
|
||||||
|
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
database?.let { database ->
|
database?.let { database ->
|
||||||
mAdapter = NodesAdapter(context, database).apply {
|
mAdapter = NodesAdapter(context, database).apply {
|
||||||
@@ -312,6 +299,11 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun containsRecycleBin(nodes: List<Node>): Boolean {
|
||||||
|
return mDatabase?.isRecycleBinEnabled == true
|
||||||
|
&& nodes.any { it == mDatabase?.recycleBin }
|
||||||
|
}
|
||||||
|
|
||||||
fun actionNodesCallback(database: ContextualDatabase,
|
fun actionNodesCallback(database: ContextualDatabase,
|
||||||
nodes: List<Node>,
|
nodes: List<Node>,
|
||||||
menuListener: NodesActionMenuListener?,
|
menuListener: NodesActionMenuListener?,
|
||||||
@@ -336,8 +328,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
// Open and Edit for a single item
|
// Open and Edit for a single item
|
||||||
if (nodes.size == 1) {
|
if (nodes.size == 1) {
|
||||||
// Edition
|
// Edition
|
||||||
if (database.isReadOnly
|
if (database.isReadOnly || containsRecycleBin(nodes)) {
|
||||||
|| (mRecycleBinEnable && nodes[0] == mRecycleBin)) {
|
|
||||||
menu?.removeItem(R.id.menu_edit)
|
menu?.removeItem(R.id.menu_edit)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -357,8 +348,7 @@ class GroupFragment : DatabaseFragment(), SortDialogFragment.SortSelectionListen
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Deletion
|
// Deletion
|
||||||
if (database.isReadOnly
|
if (database.isReadOnly || containsRecycleBin(nodes)) {
|
||||||
|| (mRecycleBinEnable && nodes.any { it == mRecycleBin })) {
|
|
||||||
menu?.removeItem(R.id.menu_delete)
|
menu?.removeItem(R.id.menu_delete)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.activities.helpers
|
|
||||||
|
|
||||||
enum class TypeMode {
|
|
||||||
DEFAULT, MAGIKEYBOARD, AUTOFILL
|
|
||||||
}
|
|
||||||
@@ -5,14 +5,14 @@ import android.os.Bundle
|
|||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
|
||||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||||
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.utils.getBinaryDir
|
import com.kunzisoft.keepass.utils.getBinaryDir
|
||||||
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
|
||||||
|
|
||||||
abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
|
||||||
|
|
||||||
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
|
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
|
||||||
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
||||||
@@ -41,6 +41,7 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
mDatabaseTaskProvider?.destroy()
|
mDatabaseTaskProvider?.destroy()
|
||||||
mDatabaseTaskProvider = null
|
mDatabaseTaskProvider = null
|
||||||
@@ -48,6 +49,7 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
mDatabase = database
|
mDatabase = database
|
||||||
mDatabaseViewModel.defineDatabase(database)
|
mDatabaseViewModel.defineDatabase(database)
|
||||||
@@ -77,7 +79,13 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
|||||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||||
fixDuplicateUuid: Boolean
|
fixDuplicateUuid: Boolean
|
||||||
) {
|
) {
|
||||||
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEncryptDatabase, fixDuplicateUuid)
|
mDatabaseTaskProvider?.startDatabaseLoad(
|
||||||
|
databaseUri,
|
||||||
|
mainCredential,
|
||||||
|
readOnly,
|
||||||
|
cipherEncryptDatabase,
|
||||||
|
fixDuplicateUuid
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun closeDatabase() {
|
protected fun closeDatabase() {
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ import androidx.appcompat.app.AlertDialog
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import android.view.View
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode
|
||||||
import com.kunzisoft.keepass.activities.helpers.TypeMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.ToolbarSpecial
|
import com.kunzisoft.keepass.view.ToolbarSpecial
|
||||||
@@ -42,14 +44,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
/**
|
/**
|
||||||
* Intent sender uses special retains data in callback
|
* Intent sender uses special retains data in callback
|
||||||
*/
|
*/
|
||||||
private fun isIntentSender(): Boolean {
|
protected fun isIntentSender(): Boolean {
|
||||||
return (mSpecialMode == SpecialMode.SELECTION
|
return isIntentSenderMode(mSpecialMode, mTypeMode)
|
||||||
&& mTypeMode == TypeMode.AUTOFILL)
|
|
||||||
/* TODO Registration callback #765
|
|
||||||
|| (mSpecialMode == SpecialMode.REGISTRATION
|
|
||||||
&& mTypeMode == TypeMode.AUTOFILL
|
|
||||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onLaunchActivitySpecialMode() {
|
fun onLaunchActivitySpecialMode() {
|
||||||
@@ -118,7 +114,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
|
|
||||||
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
||||||
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
||||||
val searchInfo: SearchInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)?.searchInfo
|
val registerInfo: RegisterInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)
|
||||||
|
val searchInfo: SearchInfo? = registerInfo?.searchInfo
|
||||||
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
||||||
|
|
||||||
// To show the selection mode
|
// To show the selection mode
|
||||||
@@ -136,12 +133,13 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
TypeMode.DEFAULT, // Not important because hidden
|
TypeMode.DEFAULT, // Not important because hidden
|
||||||
TypeMode.MAGIKEYBOARD -> R.string.magic_keyboard_title
|
TypeMode.MAGIKEYBOARD -> R.string.magic_keyboard_title
|
||||||
TypeMode.AUTOFILL -> R.string.autofill
|
TypeMode.AUTOFILL -> R.string.autofill
|
||||||
|
TypeMode.PASSKEY -> R.string.passkey
|
||||||
}
|
}
|
||||||
title = getString(selectionModeStringId)
|
title = getString(selectionModeStringId)
|
||||||
if (mTypeMode != TypeMode.DEFAULT)
|
if (mTypeMode != TypeMode.DEFAULT)
|
||||||
title = "$title (${getString(typeModeStringId)})"
|
title = "$title (${getString(typeModeStringId)})"
|
||||||
// Populate subtitle
|
// Populate subtitle
|
||||||
subtitle = searchInfo?.getName(resources)
|
subtitle = registerInfo?.getName(resources) ?: searchInfo?.getName(resources)
|
||||||
|
|
||||||
// Show the toolbar or not
|
// Show the toolbar or not
|
||||||
visible = when (mSpecialMode) {
|
visible = when (mSpecialMode) {
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ package com.kunzisoft.keepass.adapters
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@@ -418,6 +417,7 @@ class NodesAdapter (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OTP
|
||||||
val otpElement = entry.getOtpElement()
|
val otpElement = entry.getOtpElement()
|
||||||
holder.otpContainer?.removeCallbacks(holder.otpRunnable)
|
holder.otpContainer?.removeCallbacks(holder.otpRunnable)
|
||||||
if (otpElement != null
|
if (otpElement != null
|
||||||
@@ -438,7 +438,11 @@ class NodesAdapter (
|
|||||||
holder.otpContainer?.visibility = View.GONE
|
holder.otpContainer?.visibility = View.GONE
|
||||||
}
|
}
|
||||||
holder.attachmentIcon?.visibility =
|
holder.attachmentIcon?.visibility =
|
||||||
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
// Passkey
|
||||||
|
holder.passkeyIcon?.visibility =
|
||||||
|
if (entry.getPasskey() != null) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
// Assign colors
|
// Assign colors
|
||||||
assignBackgroundColor(holder.container, entry)
|
assignBackgroundColor(holder.container, entry)
|
||||||
@@ -451,6 +455,7 @@ class NodesAdapter (
|
|||||||
holder.otpToken?.setTextColor(foregroundColor)
|
holder.otpToken?.setTextColor(foregroundColor)
|
||||||
holder.otpProgress?.setIndicatorColor(foregroundColor)
|
holder.otpProgress?.setIndicatorColor(foregroundColor)
|
||||||
holder.attachmentIcon?.setColorFilter(foregroundColor)
|
holder.attachmentIcon?.setColorFilter(foregroundColor)
|
||||||
|
holder.passkeyIcon?.setColorFilter(foregroundColor)
|
||||||
holder.meta.setTextColor(foregroundColor)
|
holder.meta.setTextColor(foregroundColor)
|
||||||
iconColor = foregroundColor
|
iconColor = foregroundColor
|
||||||
} else {
|
} else {
|
||||||
@@ -459,6 +464,7 @@ class NodesAdapter (
|
|||||||
holder.otpToken?.setTextColor(mTextColorSecondary)
|
holder.otpToken?.setTextColor(mTextColorSecondary)
|
||||||
holder.otpProgress?.setIndicatorColor(mTextColorSecondary)
|
holder.otpProgress?.setIndicatorColor(mTextColorSecondary)
|
||||||
holder.attachmentIcon?.setColorFilter(mTextColorSecondary)
|
holder.attachmentIcon?.setColorFilter(mTextColorSecondary)
|
||||||
|
holder.passkeyIcon?.setColorFilter(mTextColorSecondary)
|
||||||
holder.meta.setTextColor(mTextColor)
|
holder.meta.setTextColor(mTextColor)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -467,6 +473,7 @@ class NodesAdapter (
|
|||||||
holder.otpToken?.setTextColor(mColorOnSecondary)
|
holder.otpToken?.setTextColor(mColorOnSecondary)
|
||||||
holder.otpProgress?.setIndicatorColor(mColorOnSecondary)
|
holder.otpProgress?.setIndicatorColor(mColorOnSecondary)
|
||||||
holder.attachmentIcon?.setColorFilter(mColorOnSecondary)
|
holder.attachmentIcon?.setColorFilter(mColorOnSecondary)
|
||||||
|
holder.passkeyIcon?.setColorFilter(mColorOnSecondary)
|
||||||
holder.meta.setTextColor(mColorOnSecondary)
|
holder.meta.setTextColor(mColorOnSecondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,9 +537,8 @@ class NodesAdapter (
|
|||||||
holder?.otpToken?.apply {
|
holder?.otpToken?.apply {
|
||||||
text = otpElement?.tokenString
|
text = otpElement?.tokenString
|
||||||
setTextSize(mTextSizeUnit, mOtpTokenTextDefaultDimension, mPrefSizeMultiplier)
|
setTextSize(mTextSizeUnit, mOtpTokenTextDefaultDimension, mPrefSizeMultiplier)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
textDirection = View.TEXT_DIRECTION_LTR
|
||||||
textDirection = View.TEXT_DIRECTION_LTR
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
holder?.otpContainer?.setOnClickListener {
|
holder?.otpContainer?.setOnClickListener {
|
||||||
otpElement?.token?.let { token ->
|
otpElement?.token?.let { token ->
|
||||||
@@ -612,6 +618,7 @@ class NodesAdapter (
|
|||||||
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
|
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
|
||||||
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
||||||
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
|
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
|
||||||
|
var passkeyIcon: ImageView? = itemView.findViewById(R.id.node_passkey_icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ class App : MultiDexApplication() {
|
|||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver)
|
ProcessLifecycleOwner.get().lifecycle.addObserver(AppLifecycleObserver)
|
||||||
|
|
||||||
Stylish.load(this)
|
Stylish.load(this)
|
||||||
PRNGFixes.apply()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,399 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.app;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This software is provided 'as-is', without any express or implied
|
|
||||||
* warranty. In no event will Google be held liable for any damages
|
|
||||||
* arising from the use of this software.
|
|
||||||
*
|
|
||||||
* Permission is granted to anyone to use this software for any purpose,
|
|
||||||
* including commercial applications, and to alter it and redistribute it
|
|
||||||
* freely, as long as the origin is not misrepresented.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Process;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.DataInputStream;
|
|
||||||
import java.io.DataOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.Provider;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.security.SecureRandomSpi;
|
|
||||||
import java.security.Security;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fixes for the output of the default PRNG having low entropy.
|
|
||||||
*
|
|
||||||
* The fixes need to be applied via {@link #apply()} before any use of Java
|
|
||||||
* Cryptography Architecture primitives. A good place to invoke them is in the
|
|
||||||
* application's {@code onCreate}.
|
|
||||||
*/
|
|
||||||
public final class PRNGFixes {
|
|
||||||
|
|
||||||
private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL =
|
|
||||||
getBuildFingerprintAndDeviceSerial();
|
|
||||||
|
|
||||||
/** Hidden constructor to prevent instantiation. */
|
|
||||||
private PRNGFixes() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies all fixes.
|
|
||||||
*
|
|
||||||
* @throws SecurityException if a fix is needed but could not be applied.
|
|
||||||
*/
|
|
||||||
public static void apply() {
|
|
||||||
try {
|
|
||||||
if (supportedOnThisDevice()) {
|
|
||||||
applyOpenSSLFix();
|
|
||||||
installLinuxPRNGSecureRandom();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Do nothing, do the best we can to implement the workaround
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean supportedOnThisDevice() {
|
|
||||||
// Blacklist on samsung devices
|
|
||||||
if (Build.MANUFACTURER.toLowerCase(Locale.ENGLISH).contains("samsung")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onSELinuxEnforce()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
File urandom = new File("/dev/urandom");
|
|
||||||
|
|
||||||
// Test permissions
|
|
||||||
if ( !(urandom.canRead() && urandom.canWrite()) ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Test actually writing to urandom
|
|
||||||
try {
|
|
||||||
FileOutputStream fos = new FileOutputStream(urandom);
|
|
||||||
fos.write(0);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean onSELinuxEnforce() {
|
|
||||||
try {
|
|
||||||
ProcessBuilder builder = new ProcessBuilder("getenforce");
|
|
||||||
builder.redirectErrorStream(true);
|
|
||||||
java.lang.Process process = builder.start();
|
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
|
||||||
process.waitFor();
|
|
||||||
|
|
||||||
String output = reader.readLine();
|
|
||||||
|
|
||||||
if (output == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return output.toLowerCase(Locale.US).startsWith("enforcing");
|
|
||||||
} catch (Exception e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the
|
|
||||||
* fix is not needed.
|
|
||||||
*
|
|
||||||
* @throws SecurityException if the fix is needed but could not be applied.
|
|
||||||
*/
|
|
||||||
private static void applyOpenSSLFix() throws SecurityException {
|
|
||||||
if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
|
|
||||||
|| (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2)) {
|
|
||||||
// No need to apply the fix
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Mix in the device- and invocation-specific seed.
|
|
||||||
Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
|
|
||||||
.getMethod("RAND_seed", byte[].class)
|
|
||||||
.invoke(null, generateSeed());
|
|
||||||
|
|
||||||
// Mix output of Linux PRNG into OpenSSL's PRNG
|
|
||||||
int bytesRead = (Integer) Class.forName(
|
|
||||||
"org.apache.harmony.xnet.provider.jsse.NativeCrypto")
|
|
||||||
.getMethod("RAND_load_file", String.class, long.class)
|
|
||||||
.invoke(null, "/dev/urandom", 1024);
|
|
||||||
if (bytesRead != 1024) {
|
|
||||||
throw new IOException(
|
|
||||||
"Unexpected number of bytes read from Linux PRNG: "
|
|
||||||
+ bytesRead);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new SecurityException("Failed to seed OpenSSL PRNG", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Installs a Linux PRNG-backed {@code SecureRandom} implementation as the
|
|
||||||
* default. Does nothing if the implementation is already the default or if
|
|
||||||
* there is not need to install the implementation.
|
|
||||||
*
|
|
||||||
* @throws SecurityException if the fix is needed but could not be applied.
|
|
||||||
*/
|
|
||||||
private static void installLinuxPRNGSecureRandom()
|
|
||||||
throws SecurityException {
|
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
|
||||||
// No need to apply the fix
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install a Linux PRNG-based SecureRandom implementation as the
|
|
||||||
// default, if not yet installed.
|
|
||||||
Provider[] secureRandomProviders =
|
|
||||||
Security.getProviders("SecureRandom.SHA1PRNG");
|
|
||||||
if ((secureRandomProviders == null)
|
|
||||||
|| (secureRandomProviders.length < 1)
|
|
||||||
|| (!LinuxPRNGSecureRandomProvider.class.equals(
|
|
||||||
secureRandomProviders[0].getClass()))) {
|
|
||||||
Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert that new SecureRandom() and
|
|
||||||
// SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed
|
|
||||||
// by the Linux PRNG-based SecureRandom implementation.
|
|
||||||
SecureRandom rng1 = new SecureRandom();
|
|
||||||
if (!LinuxPRNGSecureRandomProvider.class.equals(
|
|
||||||
rng1.getProvider().getClass())) {
|
|
||||||
throw new SecurityException(
|
|
||||||
"new SecureRandom() backed by wrong Provider: "
|
|
||||||
+ rng1.getProvider().getClass());
|
|
||||||
}
|
|
||||||
|
|
||||||
SecureRandom rng2;
|
|
||||||
try {
|
|
||||||
rng2 = SecureRandom.getInstance("SHA1PRNG");
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
throw new SecurityException("SHA1PRNG not available", e);
|
|
||||||
}
|
|
||||||
if (!LinuxPRNGSecureRandomProvider.class.equals(
|
|
||||||
rng2.getProvider().getClass())) {
|
|
||||||
throw new SecurityException(
|
|
||||||
"SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong"
|
|
||||||
+ " Provider: " + rng2.getProvider().getClass());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@code Provider} of {@code SecureRandom} engines which pass through
|
|
||||||
* all requests to the Linux PRNG.
|
|
||||||
*/
|
|
||||||
private static class LinuxPRNGSecureRandomProvider extends Provider {
|
|
||||||
|
|
||||||
public LinuxPRNGSecureRandomProvider() {
|
|
||||||
super("LinuxPRNG",
|
|
||||||
1.0,
|
|
||||||
"A Linux-specific random number provider that uses"
|
|
||||||
+ " /dev/urandom");
|
|
||||||
// Although /dev/urandom is not a SHA-1 PRNG, some apps
|
|
||||||
// explicitly request a SHA1PRNG SecureRandom and we thus need to
|
|
||||||
// prevent them from getting the default implementation whose output
|
|
||||||
// may have low entropy.
|
|
||||||
put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName());
|
|
||||||
put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link SecureRandomSpi} which passes all requests to the Linux PRNG
|
|
||||||
* ({@code /dev/urandom}).
|
|
||||||
*/
|
|
||||||
public static class LinuxPRNGSecureRandom extends SecureRandomSpi {
|
|
||||||
|
|
||||||
/*
|
|
||||||
* IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed
|
|
||||||
* are passed through to the Linux PRNG (/dev/urandom). Instances of
|
|
||||||
* this class seed themselves by mixing in the current time, PID, UID,
|
|
||||||
* build fingerprint, and hardware serial number (where available) into
|
|
||||||
* Linux PRNG.
|
|
||||||
*
|
|
||||||
* Concurrency: Read requests to the underlying Linux PRNG are
|
|
||||||
* serialized (on sLock) to ensure that multiple threads do not get
|
|
||||||
* duplicated PRNG output.
|
|
||||||
*/
|
|
||||||
|
|
||||||
private static final File URANDOM_FILE = new File("/dev/urandom");
|
|
||||||
|
|
||||||
|
|
||||||
private static final Object sLock = new Object();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Input stream for reading from Linux PRNG or {@code null} if not yet
|
|
||||||
* opened.
|
|
||||||
*
|
|
||||||
* @GuardedBy("sLock")
|
|
||||||
*/
|
|
||||||
private static DataInputStream sUrandomIn;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Output stream for writing to Linux PRNG or {@code null} if not yet
|
|
||||||
* opened.
|
|
||||||
*
|
|
||||||
* @GuardedBy("sLock")
|
|
||||||
*/
|
|
||||||
private static OutputStream sUrandomOut;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether this engine instance has been seeded. This is needed because
|
|
||||||
* each instance needs to seed itself if the client does not explicitly
|
|
||||||
* seed it.
|
|
||||||
*/
|
|
||||||
private boolean mSeeded;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void engineSetSeed(byte[] bytes) {
|
|
||||||
try {
|
|
||||||
OutputStream out;
|
|
||||||
synchronized (sLock) {
|
|
||||||
out = getUrandomOutputStream();
|
|
||||||
}
|
|
||||||
out.write(bytes);
|
|
||||||
out.flush();
|
|
||||||
mSeeded = true;
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new SecurityException(
|
|
||||||
"Failed to mix seed into " + URANDOM_FILE, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void engineNextBytes(byte[] bytes) {
|
|
||||||
if (!mSeeded) {
|
|
||||||
// Mix in the device- and invocation-specific seed.
|
|
||||||
engineSetSeed(generateSeed());
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
DataInputStream in;
|
|
||||||
synchronized (sLock) {
|
|
||||||
in = getUrandomInputStream();
|
|
||||||
}
|
|
||||||
synchronized (in) {
|
|
||||||
in.readFully(bytes);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new SecurityException(
|
|
||||||
"Failed to read from " + URANDOM_FILE, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected byte[] engineGenerateSeed(int size) {
|
|
||||||
byte[] seed = new byte[size];
|
|
||||||
engineNextBytes(seed);
|
|
||||||
return seed;
|
|
||||||
}
|
|
||||||
|
|
||||||
private DataInputStream getUrandomInputStream() {
|
|
||||||
synchronized (sLock) {
|
|
||||||
if (sUrandomIn == null) {
|
|
||||||
// NOTE: Consider inserting a BufferedInputStream between
|
|
||||||
// DataInputStream and FileInputStream if you need higher
|
|
||||||
// PRNG output performance and can live with future PRNG
|
|
||||||
// output being pulled into this process prematurely.
|
|
||||||
try {
|
|
||||||
sUrandomIn = new DataInputStream(
|
|
||||||
new FileInputStream(URANDOM_FILE));
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new SecurityException("Failed to open "
|
|
||||||
+ URANDOM_FILE + " for reading", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sUrandomIn;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private OutputStream getUrandomOutputStream() {
|
|
||||||
synchronized (sLock) {
|
|
||||||
if (sUrandomOut == null) {
|
|
||||||
try {
|
|
||||||
sUrandomOut = new FileOutputStream(URANDOM_FILE);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new SecurityException("Failed to open "
|
|
||||||
+ URANDOM_FILE + " for writing", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sUrandomOut;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a device- and invocation-specific seed to be mixed into the
|
|
||||||
* Linux PRNG.
|
|
||||||
*/
|
|
||||||
private static byte[] generateSeed() {
|
|
||||||
try {
|
|
||||||
ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
|
|
||||||
DataOutputStream seedBufferOut =
|
|
||||||
new DataOutputStream(seedBuffer);
|
|
||||||
seedBufferOut.writeLong(System.currentTimeMillis());
|
|
||||||
seedBufferOut.writeLong(System.nanoTime());
|
|
||||||
seedBufferOut.writeInt(Process.myPid());
|
|
||||||
seedBufferOut.writeInt(Process.myUid());
|
|
||||||
seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL);
|
|
||||||
seedBufferOut.close();
|
|
||||||
return seedBuffer.toByteArray();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new SecurityException("Failed to generate seed", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the hardware serial number of this device.
|
|
||||||
*
|
|
||||||
* @return serial number or {@code null} if not available.
|
|
||||||
*/
|
|
||||||
private static String getDeviceSerialNumber() {
|
|
||||||
// We're using the Reflection API because Build.SERIAL is only available
|
|
||||||
// since API Level 9 (Gingerbread, Android 2.3).
|
|
||||||
try {
|
|
||||||
return (String) Build.class.getField("SERIAL").get(null);
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] getBuildFingerprintAndDeviceSerial() {
|
|
||||||
StringBuilder result = new StringBuilder();
|
|
||||||
String fingerprint = Build.FINGERPRINT;
|
|
||||||
if (fingerprint != null) {
|
|
||||||
result.append(fingerprint);
|
|
||||||
}
|
|
||||||
String serial = getDeviceSerialNumber();
|
|
||||||
if (serial != null) {
|
|
||||||
result.append(serial);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return result.toString().getBytes("UTF-8");
|
|
||||||
} catch (UnsupportedEncodingException e) {
|
|
||||||
throw new RuntimeException("UTF-8 encoding not supported");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -26,10 +26,11 @@ import android.content.Context
|
|||||||
import androidx.room.AutoMigration
|
import androidx.room.AutoMigration
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
version = 2,
|
version = 3,
|
||||||
entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class],
|
entities = [FileDatabaseHistoryEntity::class, CipherDatabaseEntity::class],
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration (from = 1, to = 2)
|
AutoMigration (from = 1, to = 2),
|
||||||
|
AutoMigration (from = 2, to = 3)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|||||||
@@ -28,9 +28,10 @@ import android.os.IBinder
|
|||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.kunzisoft.keepass.database.element.binary.BinaryData.Companion.BASE64_FLAG
|
import com.kunzisoft.keepass.database.element.binary.BinaryData.Companion.BASE64_FLAG
|
||||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||||
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
|
import com.kunzisoft.keepass.services.DeviceUnlockNotificationService
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.IOActionTask
|
import com.kunzisoft.keepass.utils.IOActionTask
|
||||||
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
import com.kunzisoft.keepass.utils.SingletonHolderParameter
|
||||||
@@ -43,19 +44,19 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
AppDatabase.getDatabase(applicationContext).cipherDatabaseDao()
|
AppDatabase.getDatabase(applicationContext).cipherDatabaseDao()
|
||||||
|
|
||||||
// Temp DAO to easily remove content if object no longer in memory
|
// Temp DAO to easily remove content if object no longer in memory
|
||||||
private var useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
|
private var useTempDao = PreferencesUtil.isTempDeviceUnlockEnable(applicationContext)
|
||||||
|
|
||||||
private var mBinder: AdvancedUnlockNotificationService.AdvancedUnlockBinder? = null
|
private var mBinder: DeviceUnlockNotificationService.DeviceUnlockBinder? = null
|
||||||
private var mServiceConnection: ServiceConnection? = null
|
private var mServiceConnection: ServiceConnection? = null
|
||||||
|
|
||||||
private var mDatabaseListeners = LinkedList<CipherDatabaseListener>()
|
private var mDatabaseListeners = LinkedList<CipherDatabaseListener>()
|
||||||
private var mAdvancedUnlockBroadcastReceiver = AdvancedUnlockNotificationService.AdvancedUnlockReceiver {
|
private var mDeviceUnlockBroadcastReceiver = DeviceUnlockNotificationService.DeviceUnlockReceiver {
|
||||||
deleteAll()
|
deleteAll()
|
||||||
removeAllDataAndDetach()
|
removeAllDataAndDetach()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reloadPreferences() {
|
private fun reloadPreferences() {
|
||||||
useTempDao = PreferencesUtil.isTempAdvancedUnlockEnable(applicationContext)
|
useTempDao = PreferencesUtil.isTempDeviceUnlockEnable(applicationContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@@ -70,15 +71,15 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
private fun attachService(performedAction: () -> Unit) {
|
private fun attachService(performedAction: () -> Unit) {
|
||||||
ContextCompat.registerReceiver(applicationContext, mAdvancedUnlockBroadcastReceiver,
|
ContextCompat.registerReceiver(applicationContext, mDeviceUnlockBroadcastReceiver,
|
||||||
IntentFilter().apply {
|
IntentFilter().apply {
|
||||||
addAction(AdvancedUnlockNotificationService.REMOVE_ADVANCED_UNLOCK_KEY_ACTION)
|
addAction(DeviceUnlockNotificationService.REMOVE_DEVICE_UNLOCK_KEY_ACTION)
|
||||||
}, ContextCompat.RECEIVER_EXPORTED
|
}, ContextCompat.RECEIVER_EXPORTED
|
||||||
)
|
)
|
||||||
|
|
||||||
mServiceConnection = object : ServiceConnection {
|
mServiceConnection = object : ServiceConnection {
|
||||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||||
mBinder = (serviceBinder as AdvancedUnlockNotificationService.AdvancedUnlockBinder)
|
mBinder = (serviceBinder as DeviceUnlockNotificationService.DeviceUnlockBinder)
|
||||||
performedAction.invoke()
|
performedAction.invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +88,7 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
AdvancedUnlockNotificationService.bindService(applicationContext,
|
DeviceUnlockNotificationService.bindService(applicationContext,
|
||||||
mServiceConnection!!,
|
mServiceConnection!!,
|
||||||
Context.BIND_AUTO_CREATE)
|
Context.BIND_AUTO_CREATE)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -99,11 +100,11 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
@Synchronized
|
@Synchronized
|
||||||
private fun detachService() {
|
private fun detachService() {
|
||||||
try {
|
try {
|
||||||
applicationContext.unregisterReceiver(mAdvancedUnlockBroadcastReceiver)
|
applicationContext.unregisterReceiver(mDeviceUnlockBroadcastReceiver)
|
||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {}
|
||||||
|
|
||||||
mServiceConnection?.let {
|
mServiceConnection?.let {
|
||||||
AdvancedUnlockNotificationService.unbindService(applicationContext, it)
|
DeviceUnlockNotificationService.unbindService(applicationContext, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,23 +124,27 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
private fun onClear() {
|
private fun onClear() {
|
||||||
mBinder = null
|
mBinder = null
|
||||||
mServiceConnection = null
|
mServiceConnection = null
|
||||||
mDatabaseListeners.forEach {
|
mDatabaseListeners.forEach { listener ->
|
||||||
it.onCipherDatabaseCleared()
|
listener.onCipherDatabaseCleared()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CipherDatabaseListener {
|
interface CipherDatabaseListener {
|
||||||
|
fun onCipherDatabaseRetrieved(databaseUri: Uri, cipherDatabase: CipherEncryptDatabase?)
|
||||||
|
fun onCipherDatabaseAddedOrUpdated(cipherDatabase: CipherEncryptDatabase)
|
||||||
|
fun onCipherDatabaseDeleted(databaseUri: Uri)
|
||||||
|
fun onAllCipherDatabasesDeleted()
|
||||||
fun onCipherDatabaseCleared()
|
fun onCipherDatabaseCleared()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCipherDatabase(databaseUri: Uri,
|
fun getCipherDatabase(databaseUri: Uri,
|
||||||
cipherDatabaseResultListener: (CipherEncryptDatabase?) -> Unit) {
|
cipherDatabaseResultListener: ((CipherEncryptDatabase?) -> Unit)? = null) {
|
||||||
if (useTempDao) {
|
if (useTempDao) {
|
||||||
serviceActionTask {
|
serviceActionTask {
|
||||||
var cipherDatabase: CipherEncryptDatabase? = null
|
var cipherDatabase: CipherEncryptDatabase? = null
|
||||||
mBinder?.getCipherDatabase(databaseUri)?.let { cipherDatabaseEntity ->
|
mBinder?.getCipherDatabase(databaseUri)?.let { cipherDatabaseEntity ->
|
||||||
cipherDatabase = CipherEncryptDatabase().apply {
|
cipherDatabase = CipherEncryptDatabase().apply {
|
||||||
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
|
this.databaseUri = cipherDatabaseEntity.databaseUri.toUri()
|
||||||
this.encryptedValue = Base64.decode(
|
this.encryptedValue = Base64.decode(
|
||||||
cipherDatabaseEntity.encryptedValue,
|
cipherDatabaseEntity.encryptedValue,
|
||||||
BASE64_FLAG
|
BASE64_FLAG
|
||||||
@@ -150,7 +155,11 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cipherDatabaseResultListener.invoke(cipherDatabase)
|
cipherDatabaseResultListener?.invoke(cipherDatabase) ?: run {
|
||||||
|
mDatabaseListeners.forEach { listener ->
|
||||||
|
listener.onCipherDatabaseRetrieved(databaseUri, cipherDatabase)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
@@ -158,7 +167,7 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
|
cipherDatabaseDao.getByDatabaseUri(databaseUri.toString())
|
||||||
?.let { cipherDatabaseEntity ->
|
?.let { cipherDatabaseEntity ->
|
||||||
CipherEncryptDatabase().apply {
|
CipherEncryptDatabase().apply {
|
||||||
this.databaseUri = Uri.parse(cipherDatabaseEntity.databaseUri)
|
this.databaseUri = cipherDatabaseEntity.databaseUri.toUri()
|
||||||
this.encryptedValue = Base64.decode(
|
this.encryptedValue = Base64.decode(
|
||||||
cipherDatabaseEntity.encryptedValue,
|
cipherDatabaseEntity.encryptedValue,
|
||||||
Base64.NO_WRAP
|
Base64.NO_WRAP
|
||||||
@@ -170,14 +179,18 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{ cipherDatabase ->
|
||||||
cipherDatabaseResultListener.invoke(it)
|
cipherDatabaseResultListener?.invoke(cipherDatabase) ?: run {
|
||||||
|
mDatabaseListeners.forEach { listener ->
|
||||||
|
listener.onCipherDatabaseRetrieved(databaseUri, cipherDatabase)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun containsCipherDatabase(databaseUri: Uri?,
|
private fun containsCipherDatabase(databaseUri: Uri?,
|
||||||
contains: (Boolean) -> Unit) {
|
contains: (Boolean) -> Unit) {
|
||||||
if (databaseUri == null) {
|
if (databaseUri == null) {
|
||||||
contains.invoke(false)
|
contains.invoke(false)
|
||||||
@@ -210,7 +223,11 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
// The only case to create service (not needed to get an info)
|
// The only case to create service (not needed to get an info)
|
||||||
serviceActionTask(true) {
|
serviceActionTask(true) {
|
||||||
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
|
mBinder?.addOrUpdateCipherDatabase(cipherDatabaseEntity)
|
||||||
cipherDatabaseResultListener?.invoke()
|
cipherDatabaseResultListener?.invoke() ?: run {
|
||||||
|
mDatabaseListeners.forEach { listener ->
|
||||||
|
listener.onCipherDatabaseAddedOrUpdated(cipherEncryptDatabase)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
@@ -225,7 +242,11 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cipherDatabaseResultListener?.invoke()
|
cipherDatabaseResultListener?.invoke() ?: run {
|
||||||
|
mDatabaseListeners.forEach { listener ->
|
||||||
|
listener.onCipherDatabaseAddedOrUpdated(cipherEncryptDatabase)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
@@ -237,7 +258,11 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
if (useTempDao) {
|
if (useTempDao) {
|
||||||
serviceActionTask {
|
serviceActionTask {
|
||||||
mBinder?.deleteByDatabaseUri(databaseUri)
|
mBinder?.deleteByDatabaseUri(databaseUri)
|
||||||
cipherDatabaseResultListener?.invoke()
|
cipherDatabaseResultListener?.invoke() ?: run {
|
||||||
|
mDatabaseListeners.forEach { listener ->
|
||||||
|
listener.onCipherDatabaseDeleted(databaseUri)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
IOActionTask(
|
IOActionTask(
|
||||||
@@ -245,10 +270,15 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
|
cipherDatabaseDao.deleteByDatabaseUri(databaseUri.toString())
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
cipherDatabaseResultListener?.invoke()
|
cipherDatabaseResultListener?.invoke() ?: run {
|
||||||
|
mDatabaseListeners.forEach { listener ->
|
||||||
|
listener.onCipherDatabaseDeleted(databaseUri)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
}
|
}
|
||||||
|
reloadPreferences()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAll() {
|
fun deleteAll() {
|
||||||
@@ -263,8 +293,12 @@ class CipherDatabaseAction(context: Context) {
|
|||||||
cipherDatabaseDao.deleteAll()
|
cipherDatabaseDao.deleteAll()
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
|
mDatabaseListeners.forEach { listener ->
|
||||||
|
listener.onAllCipherDatabasesDeleted()
|
||||||
|
}
|
||||||
// Unbind
|
// Unbind
|
||||||
removeAllDataAndDetach()
|
removeAllDataAndDetach()
|
||||||
|
reloadPreferences()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction) {
|
companion object : SingletonHolderParameter<CipherDatabaseAction, Context>(::CipherDatabaseAction) {
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
|||||||
databaseUri,
|
databaseUri,
|
||||||
fileDatabaseHistoryEntity?.keyFileUri?.parseUri(),
|
fileDatabaseHistoryEntity?.keyFileUri?.parseUri(),
|
||||||
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity?.hardwareKey),
|
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity?.hardwareKey),
|
||||||
|
fileDatabaseHistoryEntity?.readOnly,
|
||||||
fileDatabaseHistoryEntity?.databaseUri?.decodeUri(),
|
fileDatabaseHistoryEntity?.databaseUri?.decodeUri(),
|
||||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias
|
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity?.databaseAlias
|
||||||
?: ""),
|
?: ""),
|
||||||
@@ -99,6 +100,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
|||||||
fileDatabaseHistoryEntity.databaseUri.parseUri(),
|
fileDatabaseHistoryEntity.databaseUri.parseUri(),
|
||||||
fileDatabaseHistoryEntity.keyFileUri?.parseUri(),
|
fileDatabaseHistoryEntity.keyFileUri?.parseUri(),
|
||||||
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity.hardwareKey),
|
HardwareKey.getHardwareKeyFromString(fileDatabaseHistoryEntity.hardwareKey),
|
||||||
|
fileDatabaseHistoryEntity.readOnly,
|
||||||
fileDatabaseHistoryEntity.databaseUri.decodeUri(),
|
fileDatabaseHistoryEntity.databaseUri.decodeUri(),
|
||||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
|
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistoryEntity.databaseAlias),
|
||||||
fileDatabaseInfo.exists,
|
fileDatabaseInfo.exists,
|
||||||
@@ -147,6 +149,8 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
|||||||
?: "",
|
?: "",
|
||||||
databaseFileToAddOrUpdate.keyFileUri?.toString(),
|
databaseFileToAddOrUpdate.keyFileUri?.toString(),
|
||||||
databaseFileToAddOrUpdate.hardwareKey?.value,
|
databaseFileToAddOrUpdate.hardwareKey?.value,
|
||||||
|
databaseFileToAddOrUpdate.readOnly
|
||||||
|
?: fileDatabaseHistoryRetrieve?.readOnly,
|
||||||
System.currentTimeMillis()
|
System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -168,6 +172,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
|||||||
fileDatabaseHistory.databaseUri.parseUri(),
|
fileDatabaseHistory.databaseUri.parseUri(),
|
||||||
fileDatabaseHistory.keyFileUri?.parseUri(),
|
fileDatabaseHistory.keyFileUri?.parseUri(),
|
||||||
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
|
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
|
||||||
|
fileDatabaseHistory.readOnly,
|
||||||
fileDatabaseHistory.databaseUri.decodeUri(),
|
fileDatabaseHistory.databaseUri.decodeUri(),
|
||||||
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
|
fileDatabaseInfo.retrieveDatabaseAlias(fileDatabaseHistory.databaseAlias),
|
||||||
fileDatabaseInfo.exists,
|
fileDatabaseInfo.exists,
|
||||||
@@ -195,6 +200,7 @@ class FileDatabaseHistoryAction(private val applicationContext: Context) {
|
|||||||
fileDatabaseHistory.databaseUri.parseUri(),
|
fileDatabaseHistory.databaseUri.parseUri(),
|
||||||
fileDatabaseHistory.keyFileUri?.parseUri(),
|
fileDatabaseHistory.keyFileUri?.parseUri(),
|
||||||
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
|
HardwareKey.getHardwareKeyFromString(fileDatabaseHistory.hardwareKey),
|
||||||
|
fileDatabaseHistory.readOnly,
|
||||||
fileDatabaseHistory.databaseUri.decodeUri(),
|
fileDatabaseHistory.databaseUri.decodeUri(),
|
||||||
databaseFileToDelete.databaseAlias
|
databaseFileToDelete.databaseAlias
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ data class FileDatabaseHistoryEntity(
|
|||||||
@ColumnInfo(name = "hardware_key")
|
@ColumnInfo(name = "hardware_key")
|
||||||
var hardwareKey: String?,
|
var hardwareKey: String?,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "read_only")
|
||||||
|
var readOnly: Boolean?,
|
||||||
|
|
||||||
@ColumnInfo(name = "updated")
|
@ColumnInfo(name = "updated")
|
||||||
val updated: Long
|
val updated: Long
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class DeviceUnlockFragment: Fragment() {
|
|||||||
private var mBiometricPrompt: BiometricPrompt? = null
|
private var mBiometricPrompt: BiometricPrompt? = null
|
||||||
|
|
||||||
// Only to fix multiple fingerprint menu #332
|
// Only to fix multiple fingerprint menu #332
|
||||||
private var mAllowAdvancedUnlockMenu = false
|
private var mAllowDeviceUnlockMenu = false
|
||||||
|
|
||||||
private var mDeviceCredentialResultLauncher: ActivityResultLauncher<Intent>? = registerForActivityResult(
|
private var mDeviceCredentialResultLauncher: ActivityResultLauncher<Intent>? = registerForActivityResult(
|
||||||
ActivityResultContracts.StartActivityForResult()
|
ActivityResultContracts.StartActivityForResult()
|
||||||
@@ -95,8 +95,8 @@ class DeviceUnlockFragment: Fragment() {
|
|||||||
private val menuProvider: MenuProvider = object: MenuProvider {
|
private val menuProvider: MenuProvider = object: MenuProvider {
|
||||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||||
// biometric menu
|
// biometric menu
|
||||||
if (mAllowAdvancedUnlockMenu)
|
if (mAllowDeviceUnlockMenu)
|
||||||
menuInflater.inflate(R.menu.advanced_unlock, menu)
|
menuInflater.inflate(R.menu.device_unlock, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||||
@@ -111,9 +111,9 @@ class DeviceUnlockFragment: Fragment() {
|
|||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
super.onCreateView(inflater, container, savedInstanceState)
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
val rootView = inflater.inflate(R.layout.fragment_advanced_unlock, container, false)
|
val rootView = inflater.inflate(R.layout.fragment_device_unlock, container, false)
|
||||||
|
|
||||||
mDeviceUnlockView = rootView.findViewById(R.id.advanced_unlock_view)
|
mDeviceUnlockView = rootView.findViewById(R.id.device_unlock_view)
|
||||||
|
|
||||||
return rootView
|
return rootView
|
||||||
}
|
}
|
||||||
@@ -138,35 +138,34 @@ class DeviceUnlockFragment: Fragment() {
|
|||||||
// Prompt
|
// Prompt
|
||||||
manageDeviceCredentialPrompt(uiState.cryptoPromptState)
|
manageDeviceCredentialPrompt(uiState.cryptoPromptState)
|
||||||
// Advanced menu
|
// Advanced menu
|
||||||
mAllowAdvancedUnlockMenu = uiState.allowAdvancedUnlockMenu
|
mAllowDeviceUnlockMenu = uiState.allowDeviceUnlockMenu
|
||||||
activity?.invalidateOptionsMenu()
|
activity?.invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
mDeviceUnlockViewModel.checkUnlockAvailability()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancelBiometricPrompt() {
|
fun cancelBiometricPrompt() {
|
||||||
mBiometricPrompt?.cancelAuthentication()
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
mBiometricPrompt?.cancelAuthentication()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleDeviceCredentialMode(deviceUnlockMode: DeviceUnlockMode) {
|
private fun toggleDeviceCredentialMode(deviceUnlockMode: DeviceUnlockMode) {
|
||||||
try {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
when (deviceUnlockMode) {
|
try {
|
||||||
DeviceUnlockMode.BIOMETRIC_UNAVAILABLE -> setNotAvailableMode()
|
when (deviceUnlockMode) {
|
||||||
DeviceUnlockMode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> setSecurityUpdateRequiredMode()
|
DeviceUnlockMode.BIOMETRIC_UNAVAILABLE -> setNotAvailableMode()
|
||||||
DeviceUnlockMode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> setNotConfiguredMode()
|
DeviceUnlockMode.BIOMETRIC_SECURITY_UPDATE_REQUIRED -> setSecurityUpdateRequiredMode()
|
||||||
DeviceUnlockMode.KEY_MANAGER_UNAVAILABLE -> setKeyManagerNotAvailableMode()
|
DeviceUnlockMode.DEVICE_CREDENTIAL_OR_BIOMETRIC_NOT_CONFIGURED -> setNotConfiguredMode()
|
||||||
DeviceUnlockMode.WAIT_CREDENTIAL -> setWaitCredentialMode()
|
DeviceUnlockMode.KEY_MANAGER_UNAVAILABLE -> setKeyManagerNotAvailableMode()
|
||||||
DeviceUnlockMode.STORE_CREDENTIAL -> setStoreCredentialMode()
|
DeviceUnlockMode.WAIT_CREDENTIAL -> setWaitCredentialMode()
|
||||||
DeviceUnlockMode.EXTRACT_CREDENTIAL -> setExtractCredentialMode()
|
DeviceUnlockMode.STORE_CREDENTIAL -> setStoreCredentialMode()
|
||||||
|
DeviceUnlockMode.EXTRACT_CREDENTIAL -> setExtractCredentialMode()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
mDeviceUnlockViewModel.setException(e)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
mDeviceUnlockViewModel.setException(e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,51 +188,52 @@ class DeviceUnlockFragment: Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun openPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) {
|
private fun openPrompt(cryptoPrompt: DeviceUnlockCryptoPrompt) {
|
||||||
try {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
val promptTitle = getString(cryptoPrompt.titleId)
|
try {
|
||||||
val promptDescription = cryptoPrompt.descriptionId?.let { descriptionId ->
|
val promptTitle = getString(cryptoPrompt.titleId)
|
||||||
getString(descriptionId)
|
val promptDescription = cryptoPrompt.descriptionId?.let { descriptionId ->
|
||||||
} ?: ""
|
getString(descriptionId)
|
||||||
|
} ?: ""
|
||||||
|
|
||||||
if (cryptoPrompt.isBiometricOperation) {
|
if (cryptoPrompt.isBiometricOperation) {
|
||||||
mBiometricPrompt?.authenticate(
|
mBiometricPrompt?.authenticate(
|
||||||
BiometricPrompt.PromptInfo.Builder().apply {
|
BiometricPrompt.PromptInfo.Builder().apply {
|
||||||
setTitle(promptTitle)
|
setTitle(promptTitle)
|
||||||
if (promptDescription.isNotEmpty())
|
if (promptDescription.isNotEmpty())
|
||||||
setDescription(promptDescription)
|
setDescription(promptDescription)
|
||||||
setConfirmationRequired(false)
|
setConfirmationRequired(false)
|
||||||
if (isDeviceCredentialBiometricOperation(context)) {
|
if (isDeviceCredentialBiometricOperation(context)) {
|
||||||
setAllowedAuthenticators(DEVICE_CREDENTIAL)
|
setAllowedAuthenticators(DEVICE_CREDENTIAL)
|
||||||
} else {
|
} else {
|
||||||
setNegativeButtonText(getString(android.R.string.cancel))
|
setNegativeButtonText(getString(android.R.string.cancel))
|
||||||
}
|
}
|
||||||
}.build(),
|
}.build(),
|
||||||
BiometricPrompt.CryptoObject(cryptoPrompt.cipher))
|
BiometricPrompt.CryptoObject(cryptoPrompt.cipher)
|
||||||
} else if (cryptoPrompt.isDeviceCredentialOperation) {
|
|
||||||
context?.let { context ->
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
mDeviceCredentialResultLauncher?.launch(
|
|
||||||
ContextCompat.getSystemService(
|
|
||||||
context,
|
|
||||||
KeyguardManager::class.java
|
|
||||||
)?.createConfirmDeviceCredentialIntent(
|
|
||||||
promptTitle,
|
|
||||||
promptDescription
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
} else if (cryptoPrompt.isDeviceCredentialOperation) {
|
||||||
|
context?.let { context ->
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
mDeviceCredentialResultLauncher?.launch(
|
||||||
|
ContextCompat.getSystemService(
|
||||||
|
context,
|
||||||
|
KeyguardManager::class.java
|
||||||
|
)?.createConfirmDeviceCredentialIntent(
|
||||||
|
promptTitle,
|
||||||
|
promptDescription
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to open prompt", e)
|
||||||
|
mDeviceUnlockViewModel.setException(e)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Unable to open prompt", e)
|
|
||||||
mDeviceUnlockViewModel.setException(e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setNotAvailableMode() {
|
private fun setNotAvailableMode() {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
showViews(false)
|
||||||
showViews(false)
|
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener(null)
|
||||||
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener(null)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openBiometricSetting() {
|
private fun openBiometricSetting() {
|
||||||
@@ -259,60 +259,48 @@ class DeviceUnlockFragment: Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setSecurityUpdateRequiredMode() {
|
private fun setSecurityUpdateRequiredMode() {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
showViews(true)
|
||||||
showViews(true)
|
setDeviceUnlockedTitleView(R.string.biometric_security_update_required)
|
||||||
setAdvancedUnlockedTitleView(R.string.biometric_security_update_required)
|
openBiometricSetting()
|
||||||
openBiometricSetting()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setNotConfiguredMode() {
|
private fun setNotConfiguredMode() {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
showViews(true)
|
||||||
showViews(true)
|
setDeviceUnlockedTitleView(R.string.configure_biometric)
|
||||||
setAdvancedUnlockedTitleView(R.string.configure_biometric)
|
openBiometricSetting()
|
||||||
openBiometricSetting()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setKeyManagerNotAvailableMode() {
|
private fun setKeyManagerNotAvailableMode() {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
showViews(true)
|
||||||
showViews(true)
|
setDeviceUnlockedTitleView(R.string.keystore_not_accessible)
|
||||||
setAdvancedUnlockedTitleView(R.string.keystore_not_accessible)
|
openBiometricSetting()
|
||||||
openBiometricSetting()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setWaitCredentialMode() {
|
private fun setWaitCredentialMode() {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
showViews(true)
|
||||||
showViews(true)
|
setDeviceUnlockedTitleView(R.string.unavailable)
|
||||||
setAdvancedUnlockedTitleView(R.string.unavailable)
|
context?.let { context ->
|
||||||
context?.let { context ->
|
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener {
|
||||||
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener {
|
mDeviceUnlockViewModel.setException(SecurityException(
|
||||||
mDeviceUnlockViewModel.setException(SecurityException(
|
context.getString(R.string.credential_before_click_device_unlock_button)
|
||||||
context.getString(R.string.credential_before_click_advanced_unlock_button)
|
))
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setStoreCredentialMode() {
|
private fun setStoreCredentialMode() {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
showViews(true)
|
||||||
showViews(true)
|
setDeviceUnlockedTitleView(R.string.unlock_and_link_biometric)
|
||||||
setAdvancedUnlockedTitleView(R.string.unlock_and_link_biometric)
|
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { _ ->
|
||||||
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { _ ->
|
mDeviceUnlockViewModel.showPrompt()
|
||||||
mDeviceUnlockViewModel.showPrompt()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setExtractCredentialMode() {
|
private fun setExtractCredentialMode() {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
showViews(true)
|
||||||
showViews(true)
|
setDeviceUnlockedTitleView(R.string.unlock)
|
||||||
setAdvancedUnlockedTitleView(R.string.unlock)
|
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { _ ->
|
||||||
mDeviceUnlockView?.setDeviceUnlockButtonViewClickListener { _ ->
|
mDeviceUnlockViewModel.showPrompt()
|
||||||
mDeviceUnlockViewModel.showPrompt()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,22 +309,18 @@ class DeviceUnlockFragment: Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun showViews(show: Boolean) {
|
private fun showViews(show: Boolean) {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
if (show) {
|
||||||
if (show) {
|
if (mDeviceUnlockView?.visibility != View.VISIBLE)
|
||||||
if (mDeviceUnlockView?.visibility != View.VISIBLE)
|
mDeviceUnlockView?.showByFading()
|
||||||
mDeviceUnlockView?.showByFading()
|
}
|
||||||
}
|
else {
|
||||||
else {
|
if (mDeviceUnlockView?.visibility == View.VISIBLE)
|
||||||
if (mDeviceUnlockView?.visibility == View.VISIBLE)
|
mDeviceUnlockView?.hideByFading()
|
||||||
mDeviceUnlockView?.hideByFading()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setAdvancedUnlockedTitleView(textId: Int) {
|
private fun setDeviceUnlockedTitleView(textId: Int) {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
mDeviceUnlockView?.setTitle(textId)
|
||||||
mDeviceUnlockView?.setTitle(textId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setAuthenticationError(errorCode: Int, errString: CharSequence) {
|
private fun setAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||||
@@ -358,7 +342,7 @@ class DeviceUnlockFragment: Fragment() {
|
|||||||
private fun setAuthenticationFailed() {
|
private fun setAuthenticationFailed() {
|
||||||
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
|
Log.e(TAG, "Biometric authentication failed, biometric not recognized")
|
||||||
mDeviceUnlockViewModel.setException(
|
mDeviceUnlockViewModel.setException(
|
||||||
SecurityException(getString(R.string.advanced_unlock_not_recognized))
|
SecurityException(getString(R.string.device_unlock_not_recognized))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,15 +58,15 @@ class DeviceUnlockManager(private var appContext: Context) {
|
|||||||
if (biometricUnlockEnable || deviceCredentialUnlockEnable) {
|
if (biometricUnlockEnable || deviceCredentialUnlockEnable) {
|
||||||
if (isDeviceSecure(appContext)) {
|
if (isDeviceSecure(appContext)) {
|
||||||
try {
|
try {
|
||||||
this.keyStore = KeyStore.getInstance(ADVANCED_UNLOCK_KEYSTORE)
|
this.keyStore = KeyStore.getInstance(DEVICE_UNLOCK_KEYSTORE)
|
||||||
this.keyGenerator = KeyGenerator.getInstance(
|
this.keyGenerator = KeyGenerator.getInstance(
|
||||||
ADVANCED_UNLOCK_KEY_ALGORITHM,
|
DEVICE_UNLOCK_KEY_ALGORITHM,
|
||||||
ADVANCED_UNLOCK_KEYSTORE
|
DEVICE_UNLOCK_KEYSTORE
|
||||||
)
|
)
|
||||||
this.cipher = Cipher.getInstance(
|
this.cipher = Cipher.getInstance(
|
||||||
ADVANCED_UNLOCK_KEY_ALGORITHM + "/"
|
DEVICE_UNLOCK_KEY_ALGORITHM + "/"
|
||||||
+ ADVANCED_UNLOCK_BLOCKS_MODES + "/"
|
+ DEVICE_UNLOCK_BLOCKS_MODES + "/"
|
||||||
+ ADVANCED_UNLOCK_ENCRYPTION_PADDING
|
+ DEVICE_UNLOCK_ENCRYPTION_PADDING
|
||||||
)
|
)
|
||||||
if (keyStore == null) {
|
if (keyStore == null) {
|
||||||
throw SecurityException("Unable to initialize the keystore")
|
throw SecurityException("Unable to initialize the keystore")
|
||||||
@@ -93,15 +93,15 @@ class DeviceUnlockManager(private var appContext: Context) {
|
|||||||
keyStore?.let { keyStore ->
|
keyStore?.let { keyStore ->
|
||||||
keyStore.load(null)
|
keyStore.load(null)
|
||||||
try {
|
try {
|
||||||
if (!keyStore.containsAlias(ADVANCED_UNLOCK_KEYSTORE_KEY)) {
|
if (!keyStore.containsAlias(DEVICE_UNLOCK_KEYSTORE_KEY)) {
|
||||||
// Set the alias of the entry in Android KeyStore where the key will appear
|
// Set the alias of the entry in Android KeyStore where the key will appear
|
||||||
// and the constrains (purposes) in the constructor of the Builder
|
// and the constrains (purposes) in the constructor of the Builder
|
||||||
keyGenerator?.init(
|
keyGenerator?.init(
|
||||||
KeyGenParameterSpec.Builder(
|
KeyGenParameterSpec.Builder(
|
||||||
ADVANCED_UNLOCK_KEYSTORE_KEY,
|
DEVICE_UNLOCK_KEYSTORE_KEY,
|
||||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||||
.setBlockModes(ADVANCED_UNLOCK_BLOCKS_MODES)
|
.setBlockModes(DEVICE_UNLOCK_BLOCKS_MODES)
|
||||||
.setEncryptionPaddings(ADVANCED_UNLOCK_ENCRYPTION_PADDING)
|
.setEncryptionPaddings(DEVICE_UNLOCK_ENCRYPTION_PADDING)
|
||||||
.apply {
|
.apply {
|
||||||
// Require the user to authenticate with a fingerprint to authorize every use
|
// Require the user to authenticate with a fingerprint to authorize every use
|
||||||
// of the key, don't use it for device credential because it's the user authentication
|
// of the key, don't use it for device credential because it's the user authentication
|
||||||
@@ -122,7 +122,7 @@ class DeviceUnlockManager(private var appContext: Context) {
|
|||||||
Log.e(TAG, "Unable to create a key in keystore", e)
|
Log.e(TAG, "Unable to create a key in keystore", e)
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
return keyStore.getKey(ADVANCED_UNLOCK_KEYSTORE_KEY, null) as SecretKey?
|
return keyStore.getKey(DEVICE_UNLOCK_KEYSTORE_KEY, null) as SecretKey?
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to retrieve the key in keystore", e)
|
Log.e(TAG, "Unable to retrieve the key in keystore", e)
|
||||||
@@ -149,8 +149,8 @@ class DeviceUnlockManager(private var appContext: Context) {
|
|||||||
DeviceUnlockCryptoPrompt(
|
DeviceUnlockCryptoPrompt(
|
||||||
type = DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION,
|
type = DeviceUnlockCryptoPromptType.CREDENTIAL_ENCRYPTION,
|
||||||
cipher = cipher,
|
cipher = cipher,
|
||||||
titleId = R.string.advanced_unlock_prompt_store_credential_title,
|
titleId = R.string.device_unlock_prompt_store_credential_title,
|
||||||
descriptionId = R.string.advanced_unlock_prompt_store_credential_message,
|
descriptionId = R.string.device_unlock_prompt_store_credential_message,
|
||||||
isDeviceCredentialOperation = isDeviceCredentialOperation(
|
isDeviceCredentialOperation = isDeviceCredentialOperation(
|
||||||
deviceCredentialUnlockEnable
|
deviceCredentialUnlockEnable
|
||||||
),
|
),
|
||||||
@@ -217,7 +217,7 @@ class DeviceUnlockManager(private var appContext: Context) {
|
|||||||
DeviceUnlockCryptoPrompt(
|
DeviceUnlockCryptoPrompt(
|
||||||
type = DeviceUnlockCryptoPromptType.CREDENTIAL_DECRYPTION,
|
type = DeviceUnlockCryptoPromptType.CREDENTIAL_DECRYPTION,
|
||||||
cipher = cipher,
|
cipher = cipher,
|
||||||
titleId = R.string.advanced_unlock_prompt_extract_credential_title,
|
titleId = R.string.device_unlock_prompt_extract_credential_title,
|
||||||
descriptionId = null,
|
descriptionId = null,
|
||||||
isDeviceCredentialOperation = isDeviceCredentialOperation(
|
isDeviceCredentialOperation = isDeviceCredentialOperation(
|
||||||
deviceCredentialUnlockEnable
|
deviceCredentialUnlockEnable
|
||||||
@@ -270,7 +270,7 @@ class DeviceUnlockManager(private var appContext: Context) {
|
|||||||
@Synchronized fun deleteKeystoreKey() {
|
@Synchronized fun deleteKeystoreKey() {
|
||||||
try {
|
try {
|
||||||
keyStore?.load(null)
|
keyStore?.load(null)
|
||||||
keyStore?.deleteEntry(ADVANCED_UNLOCK_KEYSTORE_KEY)
|
keyStore?.deleteEntry(DEVICE_UNLOCK_KEYSTORE_KEY)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to delete entry key in keystore", e)
|
Log.e(TAG, "Unable to delete entry key in keystore", e)
|
||||||
throw e
|
throw e
|
||||||
@@ -281,11 +281,11 @@ class DeviceUnlockManager(private var appContext: Context) {
|
|||||||
|
|
||||||
private val TAG = DeviceUnlockManager::class.java.name
|
private val TAG = DeviceUnlockManager::class.java.name
|
||||||
|
|
||||||
private const val ADVANCED_UNLOCK_KEYSTORE = "AndroidKeyStore"
|
private const val DEVICE_UNLOCK_KEYSTORE = "AndroidKeyStore"
|
||||||
private const val ADVANCED_UNLOCK_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key"
|
private const val DEVICE_UNLOCK_KEYSTORE_KEY = "com.kunzisoft.keepass.biometric.key"
|
||||||
private const val ADVANCED_UNLOCK_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
|
private const val DEVICE_UNLOCK_KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
|
||||||
private const val ADVANCED_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
|
private const val DEVICE_UNLOCK_BLOCKS_MODES = KeyProperties.BLOCK_MODE_CBC
|
||||||
private const val ADVANCED_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
private const val DEVICE_UNLOCK_ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||||
fun canAuthenticate(context: Context): Int {
|
fun canAuthenticate(context: Context): Int {
|
||||||
@@ -380,11 +380,11 @@ class DeviceUnlockManager(private var appContext: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deviceUnlockError(error: Exception, context: Context): String {
|
fun deviceUnlockError(error: Throwable, context: Context): String {
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||||
&& (error is UnrecoverableKeyException
|
&& (error is UnrecoverableKeyException
|
||||||
|| error is KeyPermanentlyInvalidatedException)) {
|
|| error is KeyPermanentlyInvalidatedException)) {
|
||||||
context.getString(R.string.advanced_unlock_invalid_key)
|
context.getString(R.string.device_unlock_invalid_key)
|
||||||
} else
|
} else
|
||||||
error.cause?.localizedMessage
|
error.cause?.localizedMessage
|
||||||
?: error.localizedMessage
|
?: error.localizedMessage
|
||||||
|
|||||||
@@ -17,17 +17,32 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.helpers
|
package com.kunzisoft.keepass.credentialprovider
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import android.widget.RemoteViews
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.addAutofillComponent
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||||
import com.kunzisoft.keepass.utils.getEnumExtra
|
import com.kunzisoft.keepass.utils.getEnumExtra
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
import com.kunzisoft.keepass.utils.putEnumExtra
|
import com.kunzisoft.keepass.utils.putEnumExtra
|
||||||
|
|
||||||
object EntrySelectionHelper {
|
object EntrySelectionHelper {
|
||||||
@@ -37,6 +52,47 @@ object EntrySelectionHelper {
|
|||||||
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
|
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
|
||||||
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
|
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finish the activity by passing the result code and by locking the database if necessary
|
||||||
|
*/
|
||||||
|
fun Activity.setActivityResult(
|
||||||
|
lockDatabase: Boolean = false,
|
||||||
|
resultCode: Int,
|
||||||
|
data: Intent? = null,
|
||||||
|
) {
|
||||||
|
when (resultCode) {
|
||||||
|
Activity.RESULT_OK ->
|
||||||
|
this.setResult(resultCode, data)
|
||||||
|
Activity.RESULT_CANCELED ->
|
||||||
|
this.setResult(resultCode)
|
||||||
|
}
|
||||||
|
this.finish()
|
||||||
|
|
||||||
|
if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(this)) {
|
||||||
|
// Close the database
|
||||||
|
this.sendBroadcast(Intent(LOCK_ACTION))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to build a registerForActivityResult,
|
||||||
|
* Used recursively, close each activity with return data
|
||||||
|
*/
|
||||||
|
fun AppCompatActivity.buildActivityResultLauncher(
|
||||||
|
lockDatabase: Boolean = false,
|
||||||
|
dataTransformation: (data: Intent?) -> Intent? = { it },
|
||||||
|
): ActivityResultLauncher<Intent> {
|
||||||
|
return this.registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) {
|
||||||
|
setActivityResult(
|
||||||
|
lockDatabase,
|
||||||
|
it.resultCode,
|
||||||
|
dataTransformation(it.data)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun startActivityForSearchModeResult(context: Context,
|
fun startActivityForSearchModeResult(context: Context,
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
searchInfo: SearchInfo) {
|
searchInfo: SearchInfo) {
|
||||||
@@ -66,15 +122,52 @@ object EntrySelectionHelper {
|
|||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startActivityForRegistrationModeResult(context: Context,
|
/**
|
||||||
intent: Intent,
|
* Utility method to start an activity with an Autofill for result
|
||||||
registerInfo: RegisterInfo?) {
|
*/
|
||||||
addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
// At the moment, only autofill for registration
|
fun startActivityForAutofillSelectionModeResult(
|
||||||
|
context: Context,
|
||||||
|
intent: Intent,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
autofillComponent: AutofillComponent,
|
||||||
|
searchInfo: SearchInfo?
|
||||||
|
) {
|
||||||
|
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||||
addTypeModeInIntent(intent, TypeMode.AUTOFILL)
|
addTypeModeInIntent(intent, TypeMode.AUTOFILL)
|
||||||
|
intent.addAutofillComponent(context, autofillComponent)
|
||||||
|
addSearchInfoInIntent(intent, searchInfo)
|
||||||
|
activityResultLauncher?.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
fun startActivityForPasskeySelectionModeResult(
|
||||||
|
context: Context,
|
||||||
|
intent: Intent,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
searchInfo: SearchInfo?
|
||||||
|
) {
|
||||||
|
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||||
|
addTypeModeInIntent(intent, TypeMode.PASSKEY)
|
||||||
|
addSearchInfoInIntent(intent, searchInfo)
|
||||||
|
activityResultLauncher?.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startActivityForRegistrationModeResult(
|
||||||
|
context: Context?,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
intent: Intent,
|
||||||
|
registerInfo: RegisterInfo?,
|
||||||
|
typeMode: TypeMode
|
||||||
|
) {
|
||||||
|
addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
|
||||||
|
addTypeModeInIntent(intent, typeMode)
|
||||||
addRegisterInfoInIntent(intent, registerInfo)
|
addRegisterInfoInIntent(intent, registerInfo)
|
||||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
if (activityResultLauncher == null) {
|
||||||
context.startActivity(intent)
|
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
}
|
||||||
|
activityResultLauncher?.launch(intent) ?: context?.startActivity(intent) ?:
|
||||||
|
throw IllegalStateException("At least Context or ActivityResultLauncher must not be null")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addSearchInfoInIntent(intent: Intent, searchInfo: SearchInfo?) {
|
fun addSearchInfoInIntent(intent: Intent, searchInfo: SearchInfo?) {
|
||||||
@@ -103,8 +196,13 @@ object EntrySelectionHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
|
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
|
||||||
|
// TODO Replace by Intent.addSpecialMode
|
||||||
intent.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
|
intent.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
|
||||||
}
|
}
|
||||||
|
fun Intent.addSpecialMode(specialMode: SpecialMode): Intent {
|
||||||
|
this.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
|
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
@@ -115,8 +213,13 @@ object EntrySelectionHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
|
private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
|
||||||
|
// TODO Replace by Intent.addTypeMode
|
||||||
intent.putEnumExtra(KEY_TYPE_MODE, typeMode)
|
intent.putEnumExtra(KEY_TYPE_MODE, typeMode)
|
||||||
}
|
}
|
||||||
|
fun Intent.addTypeMode(typeMode: TypeMode): Intent {
|
||||||
|
this.putEnumExtra(KEY_TYPE_MODE, typeMode)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
|
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
@@ -131,6 +234,17 @@ object EntrySelectionHelper {
|
|||||||
intent.removeExtra(KEY_TYPE_MODE)
|
intent.removeExtra(KEY_TYPE_MODE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intent sender uses special retains data in callback
|
||||||
|
*/
|
||||||
|
fun isIntentSenderMode(specialMode: SpecialMode, typeMode: TypeMode): Boolean {
|
||||||
|
return (specialMode == SpecialMode.SELECTION
|
||||||
|
&& (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
|
||||||
|
// TODO Autofill Registration callback #765 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||||
|
|| (specialMode == SpecialMode.REGISTRATION
|
||||||
|
&& typeMode == TypeMode.PASSKEY)
|
||||||
|
}
|
||||||
|
|
||||||
fun doSpecialAction(intent: Intent,
|
fun doSpecialAction(intent: Intent,
|
||||||
defaultAction: () -> Unit,
|
defaultAction: () -> Unit,
|
||||||
searchAction: (searchInfo: SearchInfo) -> Unit,
|
searchAction: (searchInfo: SearchInfo) -> Unit,
|
||||||
@@ -138,7 +252,9 @@ object EntrySelectionHelper {
|
|||||||
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
|
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
|
||||||
autofillSelectionAction: (searchInfo: SearchInfo?,
|
autofillSelectionAction: (searchInfo: SearchInfo?,
|
||||||
autofillComponent: AutofillComponent) -> Unit,
|
autofillComponent: AutofillComponent) -> Unit,
|
||||||
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
|
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit,
|
||||||
|
passkeySelectionAction: (searchInfo: SearchInfo?) -> Unit,
|
||||||
|
passkeyRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
|
||||||
|
|
||||||
when (retrieveSpecialModeFromIntent(intent)) {
|
when (retrieveSpecialModeFromIntent(intent)) {
|
||||||
SpecialMode.DEFAULT -> {
|
SpecialMode.DEFAULT -> {
|
||||||
@@ -186,6 +302,7 @@ object EntrySelectionHelper {
|
|||||||
defaultAction.invoke()
|
defaultAction.invoke()
|
||||||
}
|
}
|
||||||
TypeMode.MAGIKEYBOARD -> keyboardSelectionAction.invoke(searchInfo)
|
TypeMode.MAGIKEYBOARD -> keyboardSelectionAction.invoke(searchInfo)
|
||||||
|
TypeMode.PASSKEY -> passkeySelectionAction.invoke(searchInfo)
|
||||||
else -> {
|
else -> {
|
||||||
// In this case, error
|
// In this case, error
|
||||||
removeModesFromIntent(intent)
|
removeModesFromIntent(intent)
|
||||||
@@ -202,10 +319,59 @@ object EntrySelectionHelper {
|
|||||||
}
|
}
|
||||||
SpecialMode.REGISTRATION -> {
|
SpecialMode.REGISTRATION -> {
|
||||||
val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent)
|
val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent)
|
||||||
removeModesFromIntent(intent)
|
if (!isIntentSenderMode(
|
||||||
removeInfoFromIntent(intent)
|
specialMode = retrieveSpecialModeFromIntent(intent),
|
||||||
autofillRegistrationAction.invoke(registerInfo)
|
typeMode = retrieveTypeModeFromIntent(intent))
|
||||||
|
) {
|
||||||
|
removeModesFromIntent(intent)
|
||||||
|
removeInfoFromIntent(intent)
|
||||||
|
}
|
||||||
|
when (retrieveTypeModeFromIntent(intent)) {
|
||||||
|
TypeMode.AUTOFILL -> {
|
||||||
|
autofillRegistrationAction.invoke(registerInfo)
|
||||||
|
}
|
||||||
|
TypeMode.PASSKEY -> {
|
||||||
|
passkeyRegistrationAction.invoke(registerInfo)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Do other registration type
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun performSelection(items: List<EntryInfo>,
|
||||||
|
actionPopulateCredentialProvider: (entryInfo: EntryInfo) -> Unit,
|
||||||
|
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
|
||||||
|
if (items.size == 1) {
|
||||||
|
val itemFound = items[0]
|
||||||
|
actionPopulateCredentialProvider.invoke(itemFound)
|
||||||
|
} else if (items.size > 1) {
|
||||||
|
// Select the one we want in the selection
|
||||||
|
actionEntrySelection.invoke(true)
|
||||||
|
} else {
|
||||||
|
// Select an arbitrary one
|
||||||
|
actionEntrySelection.invoke(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to assign a drawable to a new icon from a database icon
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun EntryInfo.buildIcon(
|
||||||
|
context: Context,
|
||||||
|
database: ContextualDatabase
|
||||||
|
): Icon? {
|
||||||
|
try {
|
||||||
|
database.iconDrawableFactory.getBitmapFromIcon(context,
|
||||||
|
this.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
||||||
|
return Icon.createWithBitmap(bitmap)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.kunzisoft.keepass.activities.helpers
|
package com.kunzisoft.keepass.credentialprovider
|
||||||
|
|
||||||
enum class SpecialMode {
|
enum class SpecialMode {
|
||||||
DEFAULT,
|
DEFAULT,
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.kunzisoft.keepass.credentialprovider
|
||||||
|
|
||||||
|
enum class TypeMode {
|
||||||
|
DEFAULT, MAGIKEYBOARD, AUTOFILL, PASSKEY
|
||||||
|
}
|
||||||
@@ -17,9 +17,8 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.credentialprovider.activity
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -30,28 +29,32 @@ import android.widget.Toast
|
|||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
||||||
import com.kunzisoft.keepass.autofill.CompatInlineSuggestionsRequest
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.autofill.KeeAutofillService
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.CompatInlineSuggestionsRequest
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
|
||||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.utils.WebDomain
|
import com.kunzisoft.keepass.utils.AppUtil
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
|
import com.kunzisoft.keepass.view.toastError
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class AutofillLauncherActivity : DatabaseModeActivity() {
|
class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||||
|
|
||||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
this.buildActivityResultLauncher(lockDatabase = true)
|
||||||
AutofillHelper.buildActivityResultLauncher(this, true)
|
|
||||||
else null
|
|
||||||
|
|
||||||
override fun applyCustomStyle(): Boolean {
|
override fun applyCustomStyle(): Boolean {
|
||||||
return false
|
return false
|
||||||
@@ -72,11 +75,13 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
// To pass extra inline request
|
// To pass extra inline request
|
||||||
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
|
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
compatInlineSuggestionsRequest = bundle.getParcelableCompat(KEY_INLINE_SUGGESTION)
|
compatInlineSuggestionsRequest = bundle.getParcelableCompat(
|
||||||
|
KEY_INLINE_SUGGESTION
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// Build search param
|
// Build search param
|
||||||
bundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
|
bundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
|
||||||
WebDomain.getConcreteWebDomain(
|
AppUtil.getConcreteWebDomain(
|
||||||
this,
|
this,
|
||||||
searchInfo.webDomain
|
searchInfo.webDomain
|
||||||
) { concreteWebDomain ->
|
) { concreteWebDomain ->
|
||||||
@@ -102,16 +107,18 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
}
|
}
|
||||||
SpecialMode.REGISTRATION -> {
|
SpecialMode.REGISTRATION -> {
|
||||||
// To register info
|
// To register info
|
||||||
val registerInfo = intent.getParcelableExtraCompat<RegisterInfo>(KEY_REGISTER_INFO)
|
val registerInfo = intent.getParcelableExtraCompat<RegisterInfo>(
|
||||||
|
KEY_REGISTER_INFO
|
||||||
|
)
|
||||||
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
||||||
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
AppUtil.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||||
searchInfo.webDomain = concreteWebDomain
|
searchInfo.webDomain = concreteWebDomain
|
||||||
launchRegistration(database, searchInfo, registerInfo)
|
launchRegistration(database, searchInfo, registerInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// Not an autofill call
|
// Not an autofill call
|
||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(RESULT_CANCELED)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,7 +129,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
autofillComponent: AutofillComponent?,
|
autofillComponent: AutofillComponent?,
|
||||||
searchInfo: SearchInfo) {
|
searchInfo: SearchInfo) {
|
||||||
if (autofillComponent == null) {
|
if (autofillComponent == null) {
|
||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(RESULT_CANCELED)
|
||||||
finish()
|
finish()
|
||||||
} else if (KeeAutofillService.autofillAllowedFor(
|
} else if (KeeAutofillService.autofillAllowedFor(
|
||||||
applicationId = searchInfo.applicationId,
|
applicationId = searchInfo.applicationId,
|
||||||
@@ -130,34 +137,39 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
context = this
|
context = this
|
||||||
)) {
|
)) {
|
||||||
// If database is open
|
// If database is open
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(
|
||||||
database,
|
context = this,
|
||||||
searchInfo,
|
database = database,
|
||||||
{ openedDatabase, items ->
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { openedDatabase, items ->
|
||||||
// Items found
|
// Items found
|
||||||
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
|
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
|
||||||
finish()
|
finish()
|
||||||
},
|
},
|
||||||
{ openedDatabase ->
|
onItemNotFound = { openedDatabase ->
|
||||||
// Show the database UI to select the entry
|
// Show the database UI to select the entry
|
||||||
GroupActivity.launchForAutofillResult(this,
|
GroupActivity.launchForAutofillSelectionResult(
|
||||||
|
this,
|
||||||
openedDatabase,
|
openedDatabase,
|
||||||
mAutofillActivityResultLauncher,
|
mCredentialActivityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
false)
|
false
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
onDatabaseClosed = {
|
||||||
// If database not open
|
// If database not open
|
||||||
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
FileDatabaseSelectActivity.launchForAutofillResult(
|
||||||
mAutofillActivityResultLauncher,
|
this,
|
||||||
|
mCredentialActivityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo)
|
searchInfo
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
showBlockRestartMessage()
|
showBlockRestartMessage()
|
||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(RESULT_CANCELED)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,38 +183,51 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
context = this
|
context = this
|
||||||
)) {
|
)) {
|
||||||
val readOnly = database?.isReadOnly != false
|
val readOnly = database?.isReadOnly != false
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(
|
||||||
database,
|
context = this,
|
||||||
searchInfo,
|
database = database,
|
||||||
{ openedDatabase, _ ->
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { openedDatabase, _ ->
|
||||||
if (!readOnly) {
|
if (!readOnly) {
|
||||||
// Show the database UI to select the entry
|
// Show the database UI to select the entry
|
||||||
GroupActivity.launchForRegistration(this,
|
GroupActivity.launchForRegistration(
|
||||||
openedDatabase,
|
context = this,
|
||||||
registerInfo)
|
activityResultLauncher = null, // TODO Autofill result launcher #765
|
||||||
|
database = openedDatabase,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
showReadOnlySaveMessage()
|
showReadOnlySaveMessage()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ openedDatabase ->
|
onItemNotFound = { openedDatabase ->
|
||||||
if (!readOnly) {
|
if (!readOnly) {
|
||||||
// Show the database UI to select the entry
|
// Show the database UI to select the entry
|
||||||
GroupActivity.launchForRegistration(this,
|
GroupActivity.launchForRegistration(
|
||||||
openedDatabase,
|
context = this,
|
||||||
registerInfo)
|
activityResultLauncher = null, // TODO Autofill result launcher #765
|
||||||
|
database = openedDatabase,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
showReadOnlySaveMessage()
|
showReadOnlySaveMessage()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
onDatabaseClosed = {
|
||||||
// If database not open
|
// If database not open
|
||||||
FileDatabaseSelectActivity.launchForRegistration(this,
|
FileDatabaseSelectActivity.launchForRegistration(
|
||||||
registerInfo)
|
context = this,
|
||||||
|
activityResultLauncher = null, // TODO Autofill result launcher #765
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
showBlockRestartMessage()
|
showBlockRestartMessage()
|
||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(RESULT_CANCELED)
|
||||||
}
|
}
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
@@ -213,7 +238,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun showReadOnlySaveMessage() {
|
private fun showReadOnlySaveMessage() {
|
||||||
Toast.makeText(this.applicationContext, R.string.autofill_read_only_save, Toast.LENGTH_LONG).show()
|
toastError(RegisterInReadOnlyDatabaseException())
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -17,23 +17,27 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.credentialprovider.activity
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||||
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
|
||||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
|
import com.kunzisoft.keepass.utils.AppUtil
|
||||||
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
|
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
import com.kunzisoft.keepass.utils.WebDomain
|
import com.kunzisoft.keepass.view.toastError
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity to search or select entry in database,
|
* Activity to search or select entry in database,
|
||||||
@@ -73,7 +77,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
if (OtpEntryFields.isOTPUri(extra))
|
if (OtpEntryFields.isOTPUri(extra))
|
||||||
otpString = extra
|
otpString = extra
|
||||||
else
|
else
|
||||||
sharedWebDomain = Uri.parse(extra).host
|
sharedWebDomain = extra.toUri().host
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
launchSelection(database, sharedWebDomain, otpString)
|
launchSelection(database, sharedWebDomain, otpString)
|
||||||
@@ -107,7 +111,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
this.otpString = otpString
|
this.otpString = otpString
|
||||||
}
|
}
|
||||||
|
|
||||||
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
AppUtil.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||||
searchInfo.webDomain = concreteWebDomain
|
searchInfo.webDomain = concreteWebDomain
|
||||||
launch(database, searchInfo)
|
launch(database, searchInfo)
|
||||||
}
|
}
|
||||||
@@ -121,87 +125,99 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
|
|
||||||
// If database is open
|
// If database is open
|
||||||
val readOnly = database?.isReadOnly != false
|
val readOnly = database?.isReadOnly != false
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(
|
||||||
database,
|
context = this,
|
||||||
searchInfo,
|
database = database,
|
||||||
{ openedDatabase, items ->
|
searchInfo = searchInfo,
|
||||||
// Items found
|
onItemsFound = { openedDatabase, items ->
|
||||||
if (searchInfo.otpString != null) {
|
// Items found
|
||||||
if (!readOnly) {
|
if (searchInfo.otpString != null) {
|
||||||
GroupActivity.launchForSaveResult(
|
if (!readOnly) {
|
||||||
|
GroupActivity.launchForSaveResult(
|
||||||
|
this,
|
||||||
|
openedDatabase,
|
||||||
|
searchInfo,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
toastError(RegisterInReadOnlyDatabaseException())
|
||||||
|
}
|
||||||
|
} else if (searchShareForMagikeyboard) {
|
||||||
|
MagikeyboardService.performSelection(
|
||||||
|
items,
|
||||||
|
{ entryInfo ->
|
||||||
|
// Automatically populate keyboard
|
||||||
|
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
||||||
|
this,
|
||||||
|
entryInfo
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ autoSearch ->
|
||||||
|
GroupActivity.launchForKeyboardSelectionResult(
|
||||||
this,
|
this,
|
||||||
openedDatabase,
|
openedDatabase,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
false)
|
autoSearch
|
||||||
} else {
|
)
|
||||||
Toast.makeText(applicationContext,
|
|
||||||
R.string.autofill_read_only_save,
|
|
||||||
Toast.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
}
|
}
|
||||||
} else if (searchShareForMagikeyboard) {
|
)
|
||||||
MagikeyboardService.performSelection(
|
} else {
|
||||||
items,
|
GroupActivity.launchForSearchResult(
|
||||||
{ entryInfo ->
|
this,
|
||||||
// Automatically populate keyboard
|
openedDatabase,
|
||||||
MagikeyboardService.populateKeyboardAndMoveAppToBackground(
|
searchInfo,
|
||||||
this,
|
true
|
||||||
entryInfo
|
)
|
||||||
)
|
}
|
||||||
},
|
},
|
||||||
{ autoSearch ->
|
onItemNotFound = { openedDatabase ->
|
||||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
// Show the database UI to select the entry
|
||||||
openedDatabase,
|
if (searchInfo.otpString != null) {
|
||||||
searchInfo,
|
if (!readOnly) {
|
||||||
autoSearch)
|
GroupActivity.launchForSaveResult(
|
||||||
}
|
this,
|
||||||
|
openedDatabase,
|
||||||
|
searchInfo,
|
||||||
|
false
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
GroupActivity.launchForSearchResult(this,
|
toastError(RegisterInReadOnlyDatabaseException())
|
||||||
openedDatabase,
|
|
||||||
searchInfo,
|
|
||||||
true)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ openedDatabase ->
|
|
||||||
// Show the database UI to select the entry
|
|
||||||
if (searchInfo.otpString != null) {
|
|
||||||
if (!readOnly) {
|
|
||||||
GroupActivity.launchForSaveResult(this,
|
|
||||||
openedDatabase,
|
|
||||||
searchInfo,
|
|
||||||
false)
|
|
||||||
} else {
|
|
||||||
Toast.makeText(applicationContext,
|
|
||||||
R.string.autofill_read_only_save,
|
|
||||||
Toast.LENGTH_LONG)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
} else if (searchShareForMagikeyboard) {
|
|
||||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
|
||||||
openedDatabase,
|
|
||||||
searchInfo,
|
|
||||||
false)
|
|
||||||
} else {
|
|
||||||
GroupActivity.launchForSearchResult(this,
|
|
||||||
openedDatabase,
|
|
||||||
searchInfo,
|
|
||||||
false)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// If database not open
|
|
||||||
if (searchInfo.otpString != null) {
|
|
||||||
FileDatabaseSelectActivity.launchForSaveResult(this,
|
|
||||||
searchInfo)
|
|
||||||
} else if (searchShareForMagikeyboard) {
|
|
||||||
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this,
|
|
||||||
searchInfo)
|
|
||||||
} else {
|
|
||||||
FileDatabaseSelectActivity.launchForSearchResult(this,
|
|
||||||
searchInfo)
|
|
||||||
}
|
}
|
||||||
|
} else if (searchShareForMagikeyboard) {
|
||||||
|
GroupActivity.launchForKeyboardSelectionResult(
|
||||||
|
this,
|
||||||
|
openedDatabase,
|
||||||
|
searchInfo,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
GroupActivity.launchForSearchResult(
|
||||||
|
this,
|
||||||
|
openedDatabase,
|
||||||
|
searchInfo,
|
||||||
|
false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
// If database not open
|
||||||
|
if (searchInfo.otpString != null) {
|
||||||
|
FileDatabaseSelectActivity.launchForSaveResult(
|
||||||
|
this,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
} else if (searchShareForMagikeyboard) {
|
||||||
|
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(
|
||||||
|
this,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
FileDatabaseSelectActivity.launchForSearchResult(
|
||||||
|
this,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.activity
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||||
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivityResult
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addNodeId
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addSearchInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.model.AppOrigin
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import com.kunzisoft.keepass.view.toastError
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
class PasskeyLauncherActivity : DatabaseLockActivity() {
|
||||||
|
|
||||||
|
private val passkeyLauncherViewModel: PasskeyLauncherViewModel by viewModels()
|
||||||
|
|
||||||
|
private var mPasskeySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
|
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
passkeyLauncherViewModel.manageSelectionResult(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mPasskeyRegistrationActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
|
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
passkeyLauncherViewModel.manageRegistrationResult(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun applyCustomStyle(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finishActivityIfReloadRequested(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finishActivityIfDatabaseNotLoaded(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
lifecycleScope.launch {
|
||||||
|
// Initialize the parameters
|
||||||
|
passkeyLauncherViewModel.initialize()
|
||||||
|
// Retrieve the UI
|
||||||
|
passkeyLauncherViewModel.uiState.collect { uiState ->
|
||||||
|
when (uiState) {
|
||||||
|
is PasskeyLauncherViewModel.UIState.Loading -> {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
|
is PasskeyLauncherViewModel.UIState.ShowAppPrivilegedDialog -> {
|
||||||
|
showAppPrivilegedDialog(
|
||||||
|
temptingApp = uiState.temptingApp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is PasskeyLauncherViewModel.UIState.ShowAppSignatureDialog -> {
|
||||||
|
showAppSignatureDialog(
|
||||||
|
temptingApp = uiState.temptingApp,
|
||||||
|
nodeId = uiState.nodeId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is PasskeyLauncherViewModel.UIState.SetActivityResult -> {
|
||||||
|
setActivityResult(
|
||||||
|
lockDatabase = uiState.lockDatabase,
|
||||||
|
resultCode = uiState.resultCode,
|
||||||
|
data = uiState.data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is PasskeyLauncherViewModel.UIState.ShowError -> {
|
||||||
|
toastError(uiState.error)
|
||||||
|
passkeyLauncherViewModel.cancelResult()
|
||||||
|
}
|
||||||
|
is PasskeyLauncherViewModel.UIState.LaunchGroupActivityForSelection -> {
|
||||||
|
GroupActivity.launchForPasskeySelectionResult(
|
||||||
|
context = this@PasskeyLauncherActivity,
|
||||||
|
database = uiState.database,
|
||||||
|
activityResultLauncher = mPasskeySelectionActivityResultLauncher,
|
||||||
|
searchInfo = null,
|
||||||
|
autoSearch = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is PasskeyLauncherViewModel.UIState.LaunchGroupActivityForRegistration -> {
|
||||||
|
GroupActivity.launchForRegistration(
|
||||||
|
context = this@PasskeyLauncherActivity,
|
||||||
|
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
|
||||||
|
database = uiState.database,
|
||||||
|
registerInfo = uiState.registerInfo,
|
||||||
|
typeMode = uiState.typeMode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is PasskeyLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForSelection -> {
|
||||||
|
FileDatabaseSelectActivity.launchForPasskeySelectionResult(
|
||||||
|
activity = this@PasskeyLauncherActivity,
|
||||||
|
activityResultLauncher = mPasskeySelectionActivityResultLauncher,
|
||||||
|
searchInfo = uiState.searchInfo,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is PasskeyLauncherViewModel.UIState.LaunchFileDatabaseSelectActivityForRegistration -> {
|
||||||
|
FileDatabaseSelectActivity.launchForRegistration(
|
||||||
|
context = this@PasskeyLauncherActivity,
|
||||||
|
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
|
||||||
|
registerInfo = uiState.registerInfo,
|
||||||
|
typeMode = uiState.typeMode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is PasskeyLauncherViewModel.UIState.UpdateEntry -> {
|
||||||
|
updateEntry(uiState.oldEntry, uiState.newEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
passkeyLauncherViewModel.launchPasskeyActionIfNeeded(intent, mSpecialMode, database)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseActionFinished(
|
||||||
|
database: ContextualDatabase,
|
||||||
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) {
|
||||||
|
super.onDatabaseActionFinished(database, actionTask, result)
|
||||||
|
when (actionTask) {
|
||||||
|
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||||
|
passkeyLauncherViewModel.autoSelectPasskey(result, database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun viewToInvalidateTimeout(): View? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a dialog that asks the user to add an app to the list of privileged apps.
|
||||||
|
*/
|
||||||
|
private fun showAppPrivilegedDialog(
|
||||||
|
temptingApp: AndroidPrivilegedApp
|
||||||
|
) {
|
||||||
|
Log.w(javaClass.simpleName, "No privileged apps file found")
|
||||||
|
AlertDialog.Builder(this@PasskeyLauncherActivity).apply {
|
||||||
|
setTitle(getString(R.string.passkeys_privileged_apps_ask_title))
|
||||||
|
setMessage(StringBuilder()
|
||||||
|
.append(
|
||||||
|
getString(
|
||||||
|
R.string.passkeys_privileged_apps_ask_message,
|
||||||
|
temptingApp.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.append("\n\n")
|
||||||
|
.append(getString(R.string.passkeys_privileged_apps_explanation))
|
||||||
|
.toString()
|
||||||
|
)
|
||||||
|
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
passkeyLauncherViewModel.saveCustomPrivilegedApp(
|
||||||
|
intent = intent,
|
||||||
|
specialMode = mSpecialMode,
|
||||||
|
database = mDatabase,
|
||||||
|
temptingApp = temptingApp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
passkeyLauncherViewModel.cancelResult()
|
||||||
|
}
|
||||||
|
setOnCancelListener {
|
||||||
|
passkeyLauncherViewModel.cancelResult()
|
||||||
|
}
|
||||||
|
}.create().show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a dialog that asks the user to add an app signature in an existing passkey
|
||||||
|
*/
|
||||||
|
private fun showAppSignatureDialog(
|
||||||
|
temptingApp: AppOrigin,
|
||||||
|
nodeId: UUID
|
||||||
|
) {
|
||||||
|
AlertDialog.Builder(this@PasskeyLauncherActivity).apply {
|
||||||
|
setTitle(getString(R.string.passkeys_missing_signature_app_ask_title))
|
||||||
|
setMessage(StringBuilder()
|
||||||
|
.append(
|
||||||
|
getString(
|
||||||
|
R.string.passkeys_missing_signature_app_ask_message,
|
||||||
|
temptingApp.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.append("\n\n")
|
||||||
|
.append(getString(R.string.passkeys_missing_signature_app_ask_explanation))
|
||||||
|
.toString()
|
||||||
|
)
|
||||||
|
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
passkeyLauncherViewModel.saveAppSignature(
|
||||||
|
database = mDatabase,
|
||||||
|
temptingApp = temptingApp,
|
||||||
|
nodeId = nodeId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
passkeyLauncherViewModel.cancelResult()
|
||||||
|
}
|
||||||
|
setOnCancelListener {
|
||||||
|
passkeyLauncherViewModel.cancelResult()
|
||||||
|
}
|
||||||
|
}.create().show()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = PasskeyLauncherActivity::class.java.name
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a pending intent to launch the passkey launcher activity
|
||||||
|
* [nodeId] can be :
|
||||||
|
* - null if manual selection is requested
|
||||||
|
* - null if manual registration is requested
|
||||||
|
* - an entry node id if direct selection is requested
|
||||||
|
* - a group node id if direct registration is requested in a default group
|
||||||
|
* - an entry node id if overwriting is requested in an existing entry
|
||||||
|
*/
|
||||||
|
fun getPendingIntent(
|
||||||
|
context: Context,
|
||||||
|
specialMode: SpecialMode,
|
||||||
|
searchInfo: SearchInfo? = null,
|
||||||
|
appOrigin: AppOrigin? = null,
|
||||||
|
nodeId: UUID? = null
|
||||||
|
): PendingIntent? {
|
||||||
|
return PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
(Math.random() * Integer.MAX_VALUE).toInt(),
|
||||||
|
Intent(context, PasskeyLauncherActivity::class.java).apply {
|
||||||
|
addSpecialMode(specialMode)
|
||||||
|
addTypeMode(TypeMode.PASSKEY)
|
||||||
|
addSearchInfo(searchInfo)
|
||||||
|
addAppOrigin(appOrigin)
|
||||||
|
addNodeId(nodeId)
|
||||||
|
addAuthCode(nodeId)
|
||||||
|
},
|
||||||
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.credentialprovider.autofill
|
||||||
|
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.credentialprovider.autofill
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
@@ -40,17 +40,13 @@ import android.view.autofill.AutofillValue
|
|||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import android.widget.inline.InlinePresentationSpec
|
import android.widget.inline.InlinePresentationSpec
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.autofill.inline.UiVersions
|
import androidx.autofill.inline.UiVersions
|
||||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildIcon
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
@@ -58,7 +54,6 @@ import com.kunzisoft.keepass.model.EntryInfo
|
|||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@@ -263,7 +258,7 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (field in entryInfo.customFields) {
|
for (field in entryInfo.getCustomFieldsForFilling()) {
|
||||||
if (field.name == TemplateField.LABEL_HOLDER) {
|
if (field.name == TemplateField.LABEL_HOLDER) {
|
||||||
struct.creditCardHolderId?.let { ccNameId ->
|
struct.creditCardHolderId?.let { ccNameId ->
|
||||||
datasetBuilder.addValueToDatasetBuilder(
|
datasetBuilder.addValueToDatasetBuilder(
|
||||||
@@ -294,23 +289,6 @@ object AutofillHelper {
|
|||||||
return dataset
|
return dataset
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to assign a drawable to a new icon from a database icon
|
|
||||||
*/
|
|
||||||
private fun buildIconFromEntry(context: Context,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
entryInfo: EntryInfo): Icon? {
|
|
||||||
try {
|
|
||||||
database.iconDrawableFactory.getBitmapFromIcon(context,
|
|
||||||
entryInfo.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
|
||||||
return Icon.createWithBitmap(bitmap)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
private fun buildInlinePresentationForEntry(context: Context,
|
private fun buildInlinePresentationForEntry(context: Context,
|
||||||
@@ -338,11 +316,7 @@ object AutofillHelper {
|
|||||||
context,
|
context,
|
||||||
0,
|
0,
|
||||||
Intent(context, AutofillSettingsActivity::class.java),
|
Intent(context, AutofillSettingsActivity::class.java),
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
return InlinePresentation(
|
return InlinePresentation(
|
||||||
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
|
InlineSuggestionUi.newContentBuilder(pendingIntent).apply {
|
||||||
@@ -353,7 +327,7 @@ object AutofillHelper {
|
|||||||
Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
||||||
setTintBlendMode(BlendMode.DST)
|
setTintBlendMode(BlendMode.DST)
|
||||||
})
|
})
|
||||||
buildIconFromEntry(context, database, entryInfo)?.let { icon ->
|
entryInfo.buildIcon(context, database)?.let { icon ->
|
||||||
setEndIcon(icon.apply {
|
setEndIcon(icon.apply {
|
||||||
setTintBlendMode(BlendMode.DST)
|
setTintBlendMode(BlendMode.DST)
|
||||||
})
|
})
|
||||||
@@ -534,7 +508,9 @@ object AutofillHelper {
|
|||||||
StructureParser(structure).parse()?.let { result ->
|
StructureParser(structure).parse()?.let { result ->
|
||||||
// New Response
|
// New Response
|
||||||
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtraCompat<CompatInlineSuggestionsRequest>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtraCompat<CompatInlineSuggestionsRequest>(
|
||||||
|
EXTRA_INLINE_SUGGESTIONS_REQUEST
|
||||||
|
)
|
||||||
if (compatInlineSuggestionsRequest != null) {
|
if (compatInlineSuggestionsRequest != null) {
|
||||||
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
@@ -558,45 +534,14 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildActivityResultLauncher(activity: AppCompatActivity,
|
fun Intent.addAutofillComponent(context: Context, autofillComponent: AutofillComponent) {
|
||||||
lockDatabase: Boolean = false): ActivityResultLauncher<Intent> {
|
this.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
|
||||||
return activity.registerForActivityResult(
|
|
||||||
ActivityResultContracts.StartActivityForResult()
|
|
||||||
) {
|
|
||||||
// Utility method to loop and close each activity with return data
|
|
||||||
if (it.resultCode == Activity.RESULT_OK) {
|
|
||||||
activity.setResult(it.resultCode, it.data)
|
|
||||||
}
|
|
||||||
if (it.resultCode == Activity.RESULT_CANCELED) {
|
|
||||||
activity.setResult(Activity.RESULT_CANCELED)
|
|
||||||
}
|
|
||||||
activity.finish()
|
|
||||||
|
|
||||||
if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(activity)) {
|
|
||||||
// Close the database
|
|
||||||
activity.sendBroadcast(Intent(LOCK_ACTION))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility method to start an activity with an Autofill for result
|
|
||||||
*/
|
|
||||||
fun startActivityForAutofillResult(activity: AppCompatActivity,
|
|
||||||
intent: Intent,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
autofillComponent: AutofillComponent,
|
|
||||||
searchInfo: SearchInfo?) {
|
|
||||||
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
|
||||||
intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
|
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(context)) {
|
||||||
autofillComponent.compatInlineSuggestionsRequest?.let {
|
autofillComponent.compatInlineSuggestionsRequest?.let {
|
||||||
intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
this.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
|
|
||||||
activityResultLauncher?.launch(intent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val TAG = AutofillHelper::class.java.name
|
private val TAG = AutofillHelper::class.java.name
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.credentialprovider.autofill
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
import android.annotation.TargetApi
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.credentialprovider.autofill
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
@@ -43,8 +43,8 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.autofill.inline.UiVersions
|
import androidx.autofill.inline.UiVersions
|
||||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
import com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity
|
||||||
import com.kunzisoft.keepass.autofill.StructureParser.Companion.APPLICATION_ID_POPUP_WINDOW
|
import com.kunzisoft.keepass.credentialprovider.autofill.StructureParser.Companion.APPLICATION_ID_POPUP_WINDOW
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
@@ -53,7 +53,7 @@ import com.kunzisoft.keepass.model.RegisterInfo
|
|||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.WebDomain
|
import com.kunzisoft.keepass.utils.AppUtil
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ class KeeAutofillService : AutofillService() {
|
|||||||
webDomain = parseResult.webDomain
|
webDomain = parseResult.webDomain
|
||||||
webScheme = parseResult.webScheme
|
webScheme = parseResult.webScheme
|
||||||
}
|
}
|
||||||
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
|
AppUtil.getConcreteWebDomain(this, searchInfo.webDomain) { webDomainWithoutSubDomain ->
|
||||||
searchInfo.webDomain = webDomainWithoutSubDomain
|
searchInfo.webDomain = webDomainWithoutSubDomain
|
||||||
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
val inlineSuggestionsRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
&& autofillInlineSuggestionsEnabled) {
|
&& autofillInlineSuggestionsEnabled) {
|
||||||
@@ -143,25 +143,28 @@ class KeeAutofillService : AutofillService() {
|
|||||||
parseResult: StructureParser.Result,
|
parseResult: StructureParser.Result,
|
||||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
||||||
callback: FillCallback) {
|
callback: FillCallback) {
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(
|
||||||
database,
|
context = this,
|
||||||
searchInfo,
|
database = database,
|
||||||
{ openedDatabase, items ->
|
searchInfo = searchInfo,
|
||||||
callback.onSuccess(
|
onItemsFound = { openedDatabase, items ->
|
||||||
AutofillHelper.buildResponse(this, openedDatabase,
|
callback.onSuccess(
|
||||||
items, parseResult, inlineSuggestionsRequest)
|
AutofillHelper.buildResponse(
|
||||||
|
this, openedDatabase,
|
||||||
|
items, parseResult, inlineSuggestionsRequest
|
||||||
)
|
)
|
||||||
},
|
)
|
||||||
{ openedDatabase ->
|
},
|
||||||
// Show UI if no search result
|
onItemNotFound = { openedDatabase ->
|
||||||
showUIForEntrySelection(parseResult, openedDatabase,
|
// Show UI if no search result
|
||||||
searchInfo, inlineSuggestionsRequest, callback)
|
showUIForEntrySelection(parseResult, openedDatabase,
|
||||||
},
|
searchInfo, inlineSuggestionsRequest, callback)
|
||||||
{
|
},
|
||||||
// Show UI if database not open
|
onDatabaseClosed = {
|
||||||
showUIForEntrySelection(parseResult, null,
|
// Show UI if database not open
|
||||||
searchInfo, inlineSuggestionsRequest, callback)
|
showUIForEntrySelection(parseResult, null,
|
||||||
}
|
searchInfo, inlineSuggestionsRequest, callback)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,19 +388,21 @@ class KeeAutofillService : AutofillService() {
|
|||||||
|
|
||||||
// Show UI to save data
|
// Show UI to save data
|
||||||
val registerInfo = RegisterInfo(
|
val registerInfo = RegisterInfo(
|
||||||
SearchInfo().apply {
|
searchInfo = SearchInfo().apply {
|
||||||
applicationId = parseResult.applicationId
|
applicationId = parseResult.applicationId
|
||||||
webDomain = parseResult.webDomain
|
webDomain = parseResult.webDomain
|
||||||
webScheme = parseResult.webScheme
|
webScheme = parseResult.webScheme
|
||||||
},
|
},
|
||||||
parseResult.usernameValue?.textValue?.toString(),
|
username = parseResult.usernameValue?.textValue?.toString(),
|
||||||
parseResult.passwordValue?.textValue?.toString(),
|
password = parseResult.passwordValue?.textValue?.toString(),
|
||||||
|
creditCard =
|
||||||
CreditCard(
|
CreditCard(
|
||||||
parseResult.creditCardHolder,
|
parseResult.creditCardHolder,
|
||||||
parseResult.creditCardNumber,
|
parseResult.creditCardNumber,
|
||||||
expiration,
|
expiration,
|
||||||
parseResult.cardVerificationValue
|
parseResult.cardVerificationValue
|
||||||
))
|
)
|
||||||
|
)
|
||||||
|
|
||||||
// TODO Callback in each activity #765
|
// TODO Callback in each activity #765
|
||||||
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.credentialprovider.autofill
|
||||||
|
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
* the License.
|
* the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.kunzisoft.keepass.magikeyboard;
|
package com.kunzisoft.keepass.credentialprovider.magikeyboard;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
@@ -14,14 +14,14 @@
|
|||||||
* the License.
|
* the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.kunzisoft.keepass.magikeyboard;
|
package com.kunzisoft.keepass.credentialprovider.magikeyboard;
|
||||||
|
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_BACK_KEYBOARD;
|
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_BACK_KEYBOARD;
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_CHANGE_KEYBOARD;
|
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_CHANGE_KEYBOARD;
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_ENTRY;
|
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_ENTRY;
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_ENTRY_ALT;
|
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_ENTRY_ALT;
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP;
|
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_OTP;
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP_ALT;
|
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_OTP_ALT;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
@@ -52,7 +52,7 @@ import android.widget.TextView;
|
|||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R;
|
import com.kunzisoft.keepass.R;
|
||||||
import com.kunzisoft.keepass.magikeyboard.Keyboard.Key;
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.Keyboard.Key;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.kunzisoft.keepass.magikeyboard
|
package com.kunzisoft.keepass.credentialprovider.magikeyboard
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -41,9 +41,9 @@ import androidx.core.graphics.BlendModeCompat
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.adapters.FieldsAdapter
|
import com.kunzisoft.keepass.adapters.FieldsAdapter
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.element.Field
|
import com.kunzisoft.keepass.database.element.Field
|
||||||
@@ -324,9 +324,9 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
actionGoAutomatically()
|
actionGoAutomatically()
|
||||||
}
|
}
|
||||||
KEY_FIELDS -> {
|
KEY_FIELDS -> {
|
||||||
getEntryInfo()?.customFields?.let { customFields ->
|
getEntryInfo()?.getCustomFieldsForFilling()?.let { customFields ->
|
||||||
fieldsAdapter?.apply {
|
fieldsAdapter?.apply {
|
||||||
setFields(customFields.filter { it.name != OTP_TOKEN_FIELD})
|
setFields(customFields)
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -341,10 +341,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun actionKeyEntry(searchInfo: SearchInfo? = null) {
|
private fun actionKeyEntry(searchInfo: SearchInfo? = null) {
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(
|
||||||
mDatabase,
|
context = this,
|
||||||
searchInfo,
|
database = mDatabase,
|
||||||
{ _, items ->
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { _, items ->
|
||||||
performSelection(
|
performSelection(
|
||||||
items,
|
items,
|
||||||
{
|
{
|
||||||
@@ -361,11 +362,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
onItemNotFound = {
|
||||||
// Select if not found
|
// Select if not found
|
||||||
launchEntrySelection(searchInfo)
|
launchEntrySelection(searchInfo)
|
||||||
},
|
},
|
||||||
{
|
onDatabaseClosed = {
|
||||||
// Select if database not opened
|
// Select if database not opened
|
||||||
removeEntryInfo()
|
removeEntryInfo()
|
||||||
launchEntrySelection(searchInfo)
|
launchEntrySelection(searchInfo)
|
||||||
@@ -463,21 +464,18 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
fun performSelection(items: List<EntryInfo>,
|
fun performSelection(items: List<EntryInfo>,
|
||||||
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
|
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
|
||||||
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
|
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
|
||||||
if (items.size == 1) {
|
EntrySelectionHelper.performSelection(
|
||||||
val itemFound = items[0]
|
items = items,
|
||||||
if (entryUUID != itemFound.id) {
|
actionPopulateCredentialProvider = { itemFound ->
|
||||||
actionPopulateKeyboard.invoke(itemFound)
|
if (entryUUID != itemFound.id) {
|
||||||
} else {
|
actionPopulateKeyboard.invoke(itemFound)
|
||||||
// Force selection if magikeyboard already populated
|
} else {
|
||||||
actionEntrySelection.invoke(false)
|
// Force selection if magikeyboard already populated
|
||||||
}
|
actionEntrySelection.invoke(false)
|
||||||
} else if (items.size > 1) {
|
}
|
||||||
// Select the one we want in the selection
|
},
|
||||||
actionEntrySelection.invoke(true)
|
actionEntrySelection = actionEntrySelection
|
||||||
} else {
|
)
|
||||||
// Select an arbitrary one
|
|
||||||
actionEntrySelection.invoke(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
||||||
@@ -0,0 +1,354 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey
|
||||||
|
|
||||||
|
import android.graphics.BlendMode
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.CancellationSignal
|
||||||
|
import android.os.OutcomeReceiver
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.credentials.exceptions.ClearCredentialException
|
||||||
|
import androidx.credentials.exceptions.CreateCredentialException
|
||||||
|
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
||||||
|
import androidx.credentials.exceptions.GetCredentialException
|
||||||
|
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||||
|
import androidx.credentials.provider.BeginCreateCredentialRequest
|
||||||
|
import androidx.credentials.provider.BeginCreateCredentialResponse
|
||||||
|
import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
|
||||||
|
import androidx.credentials.provider.BeginGetCredentialRequest
|
||||||
|
import androidx.credentials.provider.BeginGetCredentialResponse
|
||||||
|
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||||
|
import androidx.credentials.provider.CreateEntry
|
||||||
|
import androidx.credentials.provider.CredentialEntry
|
||||||
|
import androidx.credentials.provider.CredentialProviderService
|
||||||
|
import androidx.credentials.provider.ProviderClearCredentialStateRequest
|
||||||
|
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildIcon
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||||
|
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil.isPasskeyAutoSelectEnable
|
||||||
|
import com.kunzisoft.keepass.view.toastError
|
||||||
|
import java.io.IOException
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
class PasskeyProviderService : CredentialProviderService() {
|
||||||
|
|
||||||
|
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
||||||
|
private var mDatabase: ContextualDatabase? = null
|
||||||
|
private lateinit var defaultIcon: Icon
|
||||||
|
private var isAutoSelectAllowed: Boolean = false
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
mDatabaseTaskProvider = DatabaseTaskProvider(this)
|
||||||
|
mDatabaseTaskProvider?.registerProgressTask()
|
||||||
|
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
||||||
|
this.mDatabase = database
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultIcon = Icon.createWithResource(
|
||||||
|
this@PasskeyProviderService,
|
||||||
|
R.mipmap.ic_launcher_round
|
||||||
|
).apply {
|
||||||
|
setTintBlendMode(BlendMode.DST)
|
||||||
|
}
|
||||||
|
|
||||||
|
isAutoSelectAllowed = isPasskeyAutoSelectEnable(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
mDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildPasskeySearchInfo(relyingParty: String): SearchInfo {
|
||||||
|
return SearchInfo().apply {
|
||||||
|
this.relyingParty = relyingParty
|
||||||
|
this.isAPasskeySearch = true
|
||||||
|
this.query = relyingParty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBeginGetCredentialRequest(
|
||||||
|
request: BeginGetCredentialRequest,
|
||||||
|
cancellationSignal: CancellationSignal,
|
||||||
|
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
|
||||||
|
) {
|
||||||
|
Log.d(javaClass.simpleName, "onBeginGetCredentialRequest called")
|
||||||
|
try {
|
||||||
|
processGetCredentialsRequest(request)?.let { response ->
|
||||||
|
callback.onResult(response)
|
||||||
|
} ?: run {
|
||||||
|
callback.onError(GetCredentialUnknownException())
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(javaClass.simpleName, "onBeginGetCredentialRequest error", e)
|
||||||
|
callback.onError(GetCredentialUnknownException())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processGetCredentialsRequest(request: BeginGetCredentialRequest): BeginGetCredentialResponse? {
|
||||||
|
val credentialEntries: MutableList<CredentialEntry> = mutableListOf()
|
||||||
|
|
||||||
|
for (option in request.beginGetCredentialOptions) {
|
||||||
|
when (option) {
|
||||||
|
is BeginGetPublicKeyCredentialOption -> {
|
||||||
|
credentialEntries.addAll(
|
||||||
|
populatePasskeyData(option)
|
||||||
|
)
|
||||||
|
return BeginGetCredentialResponse(credentialEntries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.w(javaClass.simpleName, "unknown beginGetCredentialOption")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populatePasskeyData(option: BeginGetPublicKeyCredentialOption): List<CredentialEntry> {
|
||||||
|
|
||||||
|
val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
|
||||||
|
|
||||||
|
val relyingPartyId = PublicKeyCredentialRequestOptions(option.requestJson).rpId
|
||||||
|
val searchInfo = buildPasskeySearchInfo(relyingPartyId)
|
||||||
|
Log.d(TAG, "Build passkey search for relying party $relyingPartyId")
|
||||||
|
SearchHelper.checkAutoSearchInfo(
|
||||||
|
context = this,
|
||||||
|
database = mDatabase,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { database, items ->
|
||||||
|
Log.d(TAG, "Add pending intent for passkey selection with found items")
|
||||||
|
for (passkeyEntry in items) {
|
||||||
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.SELECTION,
|
||||||
|
nodeId = passkeyEntry.id,
|
||||||
|
appOrigin = passkeyEntry.appOrigin
|
||||||
|
)?.let { usagePendingIntent ->
|
||||||
|
val passkey = passkeyEntry.passkey
|
||||||
|
passkeyEntries.add(
|
||||||
|
PublicKeyCredentialEntry(
|
||||||
|
context = applicationContext,
|
||||||
|
username = passkey?.username ?: "Unknown",
|
||||||
|
icon = passkeyEntry.buildIcon(this@PasskeyProviderService, database)?.apply {
|
||||||
|
setTintBlendMode(BlendMode.DST)
|
||||||
|
} ?: defaultIcon,
|
||||||
|
pendingIntent = usagePendingIntent,
|
||||||
|
beginGetPublicKeyCredentialOption = option,
|
||||||
|
displayName = passkeyEntry.getVisualTitle(),
|
||||||
|
isAutoSelectAllowed = isAutoSelectAllowed
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onItemNotFound = { _ ->
|
||||||
|
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId")
|
||||||
|
Log.d(TAG, "Add pending intent for passkey selection in opened database")
|
||||||
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.SELECTION,
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)?.let { pendingIntent ->
|
||||||
|
passkeyEntries.add(
|
||||||
|
PublicKeyCredentialEntry(
|
||||||
|
context = applicationContext,
|
||||||
|
username = getString(R.string.passkey_database_username),
|
||||||
|
displayName = getString(R.string.passkey_selection_description),
|
||||||
|
icon = defaultIcon,
|
||||||
|
pendingIntent = pendingIntent,
|
||||||
|
beginGetPublicKeyCredentialOption = option,
|
||||||
|
lastUsedTime = Instant.now(),
|
||||||
|
isAutoSelectAllowed = isAutoSelectAllowed
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
Log.d(TAG, "Add pending intent for passkey selection in closed database")
|
||||||
|
// Database is locked, a public key credential entry is shown to unlock it
|
||||||
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.SELECTION,
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)?.let { pendingIntent ->
|
||||||
|
passkeyEntries.add(
|
||||||
|
PublicKeyCredentialEntry(
|
||||||
|
context = applicationContext,
|
||||||
|
username = getString(R.string.passkey_database_username),
|
||||||
|
displayName = getString(R.string.passkey_locked_database_description),
|
||||||
|
icon = defaultIcon,
|
||||||
|
pendingIntent = pendingIntent,
|
||||||
|
beginGetPublicKeyCredentialOption = option,
|
||||||
|
lastUsedTime = Instant.now(),
|
||||||
|
isAutoSelectAllowed = isAutoSelectAllowed
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return passkeyEntries
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBeginCreateCredentialRequest(
|
||||||
|
request: BeginCreateCredentialRequest,
|
||||||
|
cancellationSignal: CancellationSignal,
|
||||||
|
callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>,
|
||||||
|
) {
|
||||||
|
Log.d(javaClass.simpleName, "onBeginCreateCredentialRequest called")
|
||||||
|
try {
|
||||||
|
callback.onResult(processCreateCredentialRequest(request))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(javaClass.simpleName, "onBeginCreateCredentialRequest error", e)
|
||||||
|
toastError(e)
|
||||||
|
callback.onError(CreateCredentialUnknownException(e.localizedMessage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processCreateCredentialRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse {
|
||||||
|
when (request) {
|
||||||
|
is BeginCreatePublicKeyCredentialRequest -> {
|
||||||
|
// Request is passkey type
|
||||||
|
return handleCreatePasskeyQuery(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// request type not supported
|
||||||
|
throw IOException("unknown type of BeginCreateCredentialRequest")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MutableList<CreateEntry>.addPendingIntentCreationNewEntry(
|
||||||
|
accountName: String,
|
||||||
|
searchInfo: SearchInfo?
|
||||||
|
) {
|
||||||
|
Log.d(TAG, "Add pending intent for registration in opened database to create new item")
|
||||||
|
// TODO add a setting to directly store in a specific group
|
||||||
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.REGISTRATION,
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)?.let { pendingIntent ->
|
||||||
|
this.add(
|
||||||
|
CreateEntry(
|
||||||
|
accountName = accountName,
|
||||||
|
icon = defaultIcon,
|
||||||
|
pendingIntent = pendingIntent,
|
||||||
|
description = getString(R.string.passkey_creation_description)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCreatePasskeyQuery(request: BeginCreatePublicKeyCredentialRequest): BeginCreateCredentialResponse {
|
||||||
|
|
||||||
|
val accountName = mDatabase?.name ?: getString(R.string.passkey_database_username)
|
||||||
|
val createEntries: MutableList<CreateEntry> = mutableListOf()
|
||||||
|
val relyingPartyId = PublicKeyCredentialCreationOptions(
|
||||||
|
requestJson = request.requestJson,
|
||||||
|
clientDataHash = request.clientDataHash
|
||||||
|
).relyingPartyEntity.id
|
||||||
|
val searchInfo = buildPasskeySearchInfo(relyingPartyId)
|
||||||
|
Log.d(TAG, "Build passkey search for relying party $relyingPartyId")
|
||||||
|
SearchHelper.checkAutoSearchInfo(
|
||||||
|
context = this,
|
||||||
|
database = mDatabase,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { database, items ->
|
||||||
|
if (database.isReadOnly) {
|
||||||
|
throw RegisterInReadOnlyDatabaseException()
|
||||||
|
} else {
|
||||||
|
// To create a new entry
|
||||||
|
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
|
||||||
|
/* TODO Overwrite
|
||||||
|
// To select an existing entry and permit an overwrite
|
||||||
|
Log.w(TAG, "Passkey already registered")
|
||||||
|
for (entryInfo in items) {
|
||||||
|
PasskeyHelper.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.REGISTRATION,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
passkeyEntryNodeId = entryInfo.id
|
||||||
|
)?.let { createPendingIntent ->
|
||||||
|
createEntries.add(
|
||||||
|
CreateEntry(
|
||||||
|
accountName = accountName,
|
||||||
|
pendingIntent = createPendingIntent,
|
||||||
|
description = getString(
|
||||||
|
R.string.passkey_update_description,
|
||||||
|
entryInfo.passkey?.displayName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onItemNotFound = { database ->
|
||||||
|
// To create a new entry
|
||||||
|
if (database.isReadOnly) {
|
||||||
|
throw RegisterInReadOnlyDatabaseException()
|
||||||
|
} else {
|
||||||
|
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
// Launch the passkey launcher activity to open the database
|
||||||
|
Log.d(TAG, "Add pending intent for passkey registration in closed database")
|
||||||
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.REGISTRATION
|
||||||
|
)?.let { pendingIntent ->
|
||||||
|
createEntries.add(
|
||||||
|
CreateEntry(
|
||||||
|
accountName = accountName,
|
||||||
|
icon = defaultIcon,
|
||||||
|
pendingIntent = pendingIntent,
|
||||||
|
description = getString(R.string.passkey_locked_database_description)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return BeginCreateCredentialResponse(createEntries)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClearCredentialStateRequest(
|
||||||
|
request: ProviderClearCredentialStateRequest,
|
||||||
|
cancellationSignal: CancellationSignal,
|
||||||
|
callback: OutcomeReceiver<Void?, ClearCredentialException>
|
||||||
|
) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = PasskeyProviderService::class.java.simpleName
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 AOSP modified by 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an Android privileged app, based on AOSP code
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class AndroidPrivilegedApp(
|
||||||
|
val packageName: String,
|
||||||
|
val fingerprints: Set<String>
|
||||||
|
): Parcelable {
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "$packageName ($fingerprints)"
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PACKAGE_NAME_KEY = "package_name"
|
||||||
|
private const val SIGNATURES_KEY = "signatures"
|
||||||
|
private const val FINGERPRINT_KEY = "cert_fingerprint_sha256"
|
||||||
|
private const val BUILD_KEY = "build"
|
||||||
|
private const val USER_DEBUG_KEY = "userdebug"
|
||||||
|
private const val TYPE_KEY = "type"
|
||||||
|
private const val APP_INFO_KEY = "info"
|
||||||
|
private const val ANDROID_TYPE_KEY = "android"
|
||||||
|
private const val USER_BUILD_TYPE = "userdebug"
|
||||||
|
private const val APPS_KEY = "apps"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts a list of AndroidPrivilegedApp objects from a JSONObject.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun extractPrivilegedApps(jsonObject: JSONObject): List<AndroidPrivilegedApp> {
|
||||||
|
val apps = mutableListOf<AndroidPrivilegedApp>()
|
||||||
|
if (!jsonObject.has(APPS_KEY)) {
|
||||||
|
return apps
|
||||||
|
}
|
||||||
|
val appsJsonArray = jsonObject.getJSONArray(APPS_KEY)
|
||||||
|
for (i in 0 until appsJsonArray.length()) {
|
||||||
|
try {
|
||||||
|
val appJsonObject = appsJsonArray.getJSONObject(i)
|
||||||
|
if (appJsonObject.getString(TYPE_KEY) != ANDROID_TYPE_KEY) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (!appJsonObject.has(APP_INFO_KEY)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
apps.add(
|
||||||
|
createFromJSONObject(
|
||||||
|
appJsonObject.getJSONObject(APP_INFO_KEY)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
Log.e(AndroidPrivilegedApp::class.simpleName, "Error parsing privileged app", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return apps
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an AndroidPrivilegedApp object from a JSONObject.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
private fun createFromJSONObject(
|
||||||
|
appInfoJsonObject: JSONObject,
|
||||||
|
filterUserDebug: Boolean = true
|
||||||
|
): AndroidPrivilegedApp {
|
||||||
|
val signaturesJson = appInfoJsonObject.getJSONArray(SIGNATURES_KEY)
|
||||||
|
val fingerprints = mutableSetOf<String>()
|
||||||
|
for (j in 0 until signaturesJson.length()) {
|
||||||
|
if (filterUserDebug) {
|
||||||
|
if (USER_DEBUG_KEY == signaturesJson.getJSONObject(j)
|
||||||
|
.optString(BUILD_KEY) && USER_BUILD_TYPE != Build.TYPE
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fingerprints.add(signaturesJson.getJSONObject(j).getString(FINGERPRINT_KEY))
|
||||||
|
}
|
||||||
|
return AndroidPrivilegedApp(
|
||||||
|
packageName = appInfoJsonObject.getString(PACKAGE_NAME_KEY),
|
||||||
|
fingerprints = fingerprints
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a JSONObject from a list of AndroidPrivilegedApp objects.
|
||||||
|
* The structure will be similar to what `extractPrivilegedApps` expects.
|
||||||
|
*
|
||||||
|
* @param privilegedApps The list of AndroidPrivilegedApp objects.
|
||||||
|
* @return A JSONObject representing the list.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun toJsonObject(privilegedApps: List<AndroidPrivilegedApp>): JSONObject {
|
||||||
|
val rootJsonObject = JSONObject()
|
||||||
|
val appsJsonArray = JSONArray()
|
||||||
|
|
||||||
|
for (app in privilegedApps) {
|
||||||
|
val appInfoObject = JSONObject()
|
||||||
|
appInfoObject.put(PACKAGE_NAME_KEY, app.packageName)
|
||||||
|
|
||||||
|
val signaturesArray = JSONArray()
|
||||||
|
for (fingerprint in app.fingerprints) {
|
||||||
|
val signatureObject = JSONObject()
|
||||||
|
signatureObject.put(FINGERPRINT_KEY, fingerprint)
|
||||||
|
// If needed: signatureObject.put(BUILD_KEY, "user")
|
||||||
|
signaturesArray.put(signatureObject)
|
||||||
|
}
|
||||||
|
appInfoObject.put(SIGNATURES_KEY, signaturesArray)
|
||||||
|
|
||||||
|
val appContainerObject = JSONObject()
|
||||||
|
appContainerObject.put(TYPE_KEY, ANDROID_TYPE_KEY)
|
||||||
|
appContainerObject.put(APP_INFO_KEY, appInfoObject)
|
||||||
|
|
||||||
|
appsJsonArray.put(appContainerObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootJsonObject.put(APPS_KEY, appsJsonArray)
|
||||||
|
return rootJsonObject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||||
|
import com.kunzisoft.encrypt.Signature
|
||||||
|
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class AuthenticatorAssertionResponse(
|
||||||
|
private val requestOptions: PublicKeyCredentialRequestOptions,
|
||||||
|
private val userPresent: Boolean,
|
||||||
|
private val userVerified: Boolean,
|
||||||
|
private val backupEligibility: Boolean,
|
||||||
|
private val backupState: Boolean,
|
||||||
|
private var userHandle: String,
|
||||||
|
privateKey: String,
|
||||||
|
private val clientDataResponse: ClientDataResponse,
|
||||||
|
) : AuthenticatorResponse {
|
||||||
|
|
||||||
|
override var clientJson = JSONObject()
|
||||||
|
private var authenticatorData: ByteArray = AuthenticatorData.buildAuthenticatorData(
|
||||||
|
relyingPartyId = requestOptions.rpId.toByteArray(),
|
||||||
|
userPresent = userPresent,
|
||||||
|
userVerified = userVerified,
|
||||||
|
backupEligibility = backupEligibility,
|
||||||
|
backupState = backupState
|
||||||
|
)
|
||||||
|
private var signature: ByteArray = byteArrayOf()
|
||||||
|
|
||||||
|
init {
|
||||||
|
try {
|
||||||
|
signature = Signature.sign(privateKey, dataToSign())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(this::class.java.simpleName, "Unable to sign: ${e.message}")
|
||||||
|
throw GetCredentialUnknownException("Signing failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dataToSign(): ByteArray {
|
||||||
|
return authenticatorData + clientDataResponse.hashData()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun json(): JSONObject {
|
||||||
|
// https://www.w3.org/TR/webauthn-3/#authdata-flags
|
||||||
|
return clientJson.apply {
|
||||||
|
put("clientDataJSON", clientDataResponse.buildResponse())
|
||||||
|
put("authenticatorData", b64Encode(authenticatorData))
|
||||||
|
put("signature", b64Encode(signature))
|
||||||
|
put("userHandle", userHandle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
|
||||||
|
import com.kunzisoft.keepass.utils.UUIDUtils.asBytes
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
class AuthenticatorAttestationResponse(
|
||||||
|
private val requestOptions: PublicKeyCredentialCreationOptions,
|
||||||
|
private val credentialId: ByteArray,
|
||||||
|
private val credentialPublicKey: ByteArray,
|
||||||
|
private val userPresent: Boolean,
|
||||||
|
private val userVerified: Boolean,
|
||||||
|
private val backupEligibility: Boolean,
|
||||||
|
private val backupState: Boolean,
|
||||||
|
private val publicKeyTypeId: Long,
|
||||||
|
private val publicKeyCbor: ByteArray,
|
||||||
|
private val clientDataResponse: ClientDataResponse,
|
||||||
|
) : AuthenticatorResponse {
|
||||||
|
|
||||||
|
override var clientJson = JSONObject()
|
||||||
|
var attestationObject: ByteArray
|
||||||
|
|
||||||
|
init {
|
||||||
|
attestationObject = defaultAttestationObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildAuthData(): ByteArray {
|
||||||
|
return AuthenticatorData.buildAuthenticatorData(
|
||||||
|
relyingPartyId = requestOptions.relyingPartyEntity.id.toByteArray(),
|
||||||
|
userPresent = userPresent,
|
||||||
|
userVerified = userVerified,
|
||||||
|
backupEligibility = backupEligibility,
|
||||||
|
backupState = backupState,
|
||||||
|
attestedCredentialData = true
|
||||||
|
) + AAGUID +
|
||||||
|
//credIdLen
|
||||||
|
byteArrayOf((credentialId.size shr 8).toByte(), credentialId.size.toByte()) +
|
||||||
|
credentialId +
|
||||||
|
credentialPublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun defaultAttestationObject(): ByteArray {
|
||||||
|
// https://www.w3.org/TR/webauthn-3/#attestation-object
|
||||||
|
val ao = mutableMapOf<String, Any>()
|
||||||
|
ao.put("fmt", "none")
|
||||||
|
ao.put("attStmt", emptyMap<Any, Any>())
|
||||||
|
ao.put("authData", buildAuthData())
|
||||||
|
return Cbor().encode(ao)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun json(): JSONObject {
|
||||||
|
// See AuthenticatorAttestationResponseJSON at
|
||||||
|
// https://w3c.github.io/webauthn/#ref-for-dom-publickeycredential-tojson
|
||||||
|
return clientJson.apply {
|
||||||
|
put("clientDataJSON", clientDataResponse.buildResponse())
|
||||||
|
put("authenticatorData", b64Encode(buildAuthData()))
|
||||||
|
put("transports", JSONArray(listOf("internal", "hybrid")))
|
||||||
|
put("publicKey", b64Encode(publicKeyCbor))
|
||||||
|
put("publicKeyAlgorithm", publicKeyTypeId)
|
||||||
|
put("attestationObject", b64Encode(attestationObject))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Authenticator Attestation Global Unique Identifier
|
||||||
|
private val AAGUID: ByteArray = UUID.fromString("eaecdef2-1c31-5634-8639-f1cbd9c00a08").asBytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import com.kunzisoft.encrypt.HashManager
|
||||||
|
|
||||||
|
class AuthenticatorData {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun buildAuthenticatorData(
|
||||||
|
relyingPartyId: ByteArray,
|
||||||
|
userPresent: Boolean,
|
||||||
|
userVerified: Boolean,
|
||||||
|
backupEligibility: Boolean,
|
||||||
|
backupState: Boolean,
|
||||||
|
attestedCredentialData: Boolean = false
|
||||||
|
): ByteArray {
|
||||||
|
// https://www.w3.org/TR/webauthn-3/#table-authData
|
||||||
|
var flags = 0
|
||||||
|
if (userPresent)
|
||||||
|
flags = flags or 0x01
|
||||||
|
// bit at index 1 is reserved
|
||||||
|
if (userVerified)
|
||||||
|
flags = flags or 0x04
|
||||||
|
if (backupEligibility)
|
||||||
|
flags = flags or 0x08
|
||||||
|
if (backupState)
|
||||||
|
flags = flags or 0x10
|
||||||
|
// bit at index 5 is reserved
|
||||||
|
if (attestedCredentialData) {
|
||||||
|
flags = flags or 0x40
|
||||||
|
}
|
||||||
|
// bit at index 7: Extension data included == false
|
||||||
|
|
||||||
|
return HashManager.hashSha256(relyingPartyId) +
|
||||||
|
byteArrayOf(flags.toByte()) +
|
||||||
|
byteArrayOf(0, 0, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
interface AuthenticatorResponse {
|
||||||
|
var clientJson: JSONObject
|
||||||
|
|
||||||
|
fun json(): JSONObject
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import androidx.annotation.RestrictTo
|
||||||
|
|
||||||
|
@RestrictTo(RestrictTo.Scope.LIBRARY)
|
||||||
|
class Cbor {
|
||||||
|
data class Item(val item: Any, val len: Int)
|
||||||
|
|
||||||
|
data class Arg(val arg: Long, val len: Int)
|
||||||
|
|
||||||
|
val TYPE_UNSIGNED_INT = 0x00
|
||||||
|
val TYPE_NEGATIVE_INT = 0x01
|
||||||
|
val TYPE_BYTE_STRING = 0x02
|
||||||
|
val TYPE_TEXT_STRING = 0x03
|
||||||
|
val TYPE_ARRAY = 0x04
|
||||||
|
val TYPE_MAP = 0x05
|
||||||
|
val TYPE_TAG = 0x06
|
||||||
|
val TYPE_FLOAT = 0x07
|
||||||
|
|
||||||
|
fun decode(data: ByteArray): Any {
|
||||||
|
val ret = parseItem(data, 0)
|
||||||
|
return ret.item
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encode(data: Any): ByteArray {
|
||||||
|
if (data is Number) {
|
||||||
|
if (data is Double) {
|
||||||
|
throw IllegalArgumentException("Don't support doubles yet")
|
||||||
|
} else {
|
||||||
|
val value = data.toLong()
|
||||||
|
if (value >= 0) {
|
||||||
|
return createArg(TYPE_UNSIGNED_INT, value)
|
||||||
|
} else {
|
||||||
|
return createArg(TYPE_NEGATIVE_INT, -1 - value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data is ByteArray) {
|
||||||
|
return createArg(TYPE_BYTE_STRING, data.size.toLong()) + data
|
||||||
|
}
|
||||||
|
if (data is String) {
|
||||||
|
return createArg(TYPE_TEXT_STRING, data.length.toLong()) + data.encodeToByteArray()
|
||||||
|
}
|
||||||
|
if (data is List<*>) {
|
||||||
|
var ret = createArg(TYPE_ARRAY, data.size.toLong())
|
||||||
|
for (i in data) {
|
||||||
|
ret += encode(i!!)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
if (data is Map<*, *>) {
|
||||||
|
// See:
|
||||||
|
// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#ctap2-canonical-cbor-encoding-form
|
||||||
|
var ret = createArg(TYPE_MAP, data.size.toLong())
|
||||||
|
var byteMap: MutableMap<ByteArray, ByteArray> = mutableMapOf()
|
||||||
|
for (i in data) {
|
||||||
|
// Convert to byte arrays so we can sort them.
|
||||||
|
byteMap.put(encode(i.key!!), encode(i.value!!))
|
||||||
|
}
|
||||||
|
|
||||||
|
var keysList = ArrayList<ByteArray>(byteMap.keys)
|
||||||
|
keysList.sortedWith(
|
||||||
|
Comparator<ByteArray> { a, b ->
|
||||||
|
// If two keys have different lengths, the shorter one sorts earlier;
|
||||||
|
// If two keys have the same length, the one with the lower value in (byte-wise)
|
||||||
|
// lexical order sorts earlier.
|
||||||
|
var aBytes = byteMap.get(a)!!
|
||||||
|
var bBytes = byteMap.get(b)!!
|
||||||
|
when {
|
||||||
|
a.size > b.size -> 1
|
||||||
|
a.size < b.size -> -1
|
||||||
|
aBytes.size > bBytes.size -> 1
|
||||||
|
aBytes.size < bBytes.size -> -1
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for (key in keysList) {
|
||||||
|
ret += key
|
||||||
|
ret += byteMap.get(key)!!
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException("Bad type")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getType(data: ByteArray, offset: Int): Int {
|
||||||
|
val d = data[offset].toInt()
|
||||||
|
return (d and 0xFF) shr 5
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getArg(data: ByteArray, offset: Int): Arg {
|
||||||
|
val arg = data[offset].toLong() and 0x1F
|
||||||
|
if (arg < 24) {
|
||||||
|
return Arg(arg, 1)
|
||||||
|
}
|
||||||
|
if (arg == 24L) {
|
||||||
|
return Arg(data[offset + 1].toLong() and 0xFF, 2)
|
||||||
|
}
|
||||||
|
if (arg == 25L) {
|
||||||
|
var ret = (data[offset + 1].toLong() and 0xFF) shl 8
|
||||||
|
ret = ret or (data[offset + 2].toLong() and 0xFF)
|
||||||
|
return Arg(ret, 3)
|
||||||
|
}
|
||||||
|
if (arg == 26L) {
|
||||||
|
var ret = (data[offset + 1].toLong() and 0xFF) shl 24
|
||||||
|
ret = ret or ((data[offset + 2].toLong() and 0xFF) shl 16)
|
||||||
|
ret = ret or ((data[offset + 3].toLong() and 0xFF) shl 8)
|
||||||
|
ret = ret or (data[offset + 4].toLong() and 0xFF)
|
||||||
|
return Arg(ret, 5)
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException("Bad arg")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseItem(data: ByteArray, offset: Int): Item {
|
||||||
|
val itemType = getType(data, offset)
|
||||||
|
val arg = getArg(data, offset)
|
||||||
|
println("Type $itemType ${arg.arg} ${arg.len}")
|
||||||
|
|
||||||
|
when (itemType) {
|
||||||
|
TYPE_UNSIGNED_INT -> {
|
||||||
|
return Item(arg.arg, arg.len)
|
||||||
|
}
|
||||||
|
TYPE_NEGATIVE_INT -> {
|
||||||
|
return Item(-1 - arg.arg, arg.len)
|
||||||
|
}
|
||||||
|
TYPE_BYTE_STRING -> {
|
||||||
|
val ret =
|
||||||
|
data.sliceArray(offset + arg.len.toInt() until offset + arg.len.toInt() + arg.arg.toInt())
|
||||||
|
return Item(ret, arg.len + arg.arg.toInt())
|
||||||
|
}
|
||||||
|
TYPE_TEXT_STRING -> {
|
||||||
|
val ret =
|
||||||
|
data.sliceArray(offset + arg.len.toInt() until offset + arg.len.toInt() + arg.arg.toInt())
|
||||||
|
return Item(ret.toString(Charsets.UTF_8), arg.len + arg.arg.toInt())
|
||||||
|
}
|
||||||
|
TYPE_ARRAY -> {
|
||||||
|
val ret = mutableListOf<Any>()
|
||||||
|
var consumed = arg.len
|
||||||
|
for (i in 0 until arg.arg.toInt()) {
|
||||||
|
val item = parseItem(data, offset + consumed)
|
||||||
|
ret.add(item.item)
|
||||||
|
consumed += item.len
|
||||||
|
}
|
||||||
|
return Item(ret.toList(), consumed)
|
||||||
|
}
|
||||||
|
TYPE_MAP -> {
|
||||||
|
val ret = mutableMapOf<Any, Any>()
|
||||||
|
var consumed = arg.len
|
||||||
|
for (i in 0 until arg.arg.toInt()) {
|
||||||
|
val key = parseItem(data, offset + consumed)
|
||||||
|
consumed += key.len
|
||||||
|
val value = parseItem(data, offset + consumed)
|
||||||
|
consumed += value.len
|
||||||
|
ret[key.item] = value.item
|
||||||
|
}
|
||||||
|
return Item(ret.toMap(), consumed)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw IllegalArgumentException("Bad type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createArg(type: Int, arg: Long): ByteArray {
|
||||||
|
val t = type shl 5
|
||||||
|
val a = arg.toInt()
|
||||||
|
if (arg < 24) {
|
||||||
|
return byteArrayOf(((t or a) and 0xFF).toByte())
|
||||||
|
}
|
||||||
|
if (arg <= 0xFF) {
|
||||||
|
return byteArrayOf(((t or 24) and 0xFF).toByte(), (a and 0xFF).toByte())
|
||||||
|
}
|
||||||
|
if (arg <= 0xFFFF) {
|
||||||
|
return byteArrayOf(
|
||||||
|
((t or 25) and 0xFF).toByte(),
|
||||||
|
((a shr 8) and 0xFF).toByte(),
|
||||||
|
(a and 0xFF).toByte()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (arg <= 0xFFFFFFFF) {
|
||||||
|
return byteArrayOf(
|
||||||
|
((t or 26) and 0xFF).toByte(),
|
||||||
|
((a shr 24) and 0xFF).toByte(),
|
||||||
|
((a shr 16) and 0xFF).toByte(),
|
||||||
|
((a shr 8) and 0xFF).toByte(),
|
||||||
|
(a and 0xFF).toByte()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException("bad Arg")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import com.kunzisoft.encrypt.HashManager
|
||||||
|
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
open class ClientDataBuildResponse(
|
||||||
|
type: Type,
|
||||||
|
challenge: ByteArray,
|
||||||
|
origin: String,
|
||||||
|
crossOrigin: Boolean? = false,
|
||||||
|
topOrigin: String? = null,
|
||||||
|
): AuthenticatorResponse, ClientDataResponse {
|
||||||
|
override var clientJson = JSONObject()
|
||||||
|
|
||||||
|
init {
|
||||||
|
// https://w3c.github.io/webauthn/#client-data
|
||||||
|
clientJson.put("type", type.value)
|
||||||
|
clientJson.put("challenge", b64Encode(challenge))
|
||||||
|
clientJson.put("origin", origin)
|
||||||
|
crossOrigin?.let {
|
||||||
|
clientJson.put("crossOrigin", it)
|
||||||
|
}
|
||||||
|
topOrigin?.let {
|
||||||
|
clientJson.put("topOrigin", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun json(): JSONObject {
|
||||||
|
return clientJson
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Type(val value: String) {
|
||||||
|
GET("webauthn.get"), CREATE("webauthn.create")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun buildResponse(): String {
|
||||||
|
return b64Encode(json().toString().toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashData(): ByteArray {
|
||||||
|
return HashManager.hashSha256(json().toString().toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
open class ClientDataDefinedResponse(
|
||||||
|
private val clientDataHash: ByteArray
|
||||||
|
): ClientDataResponse {
|
||||||
|
|
||||||
|
override fun hashData(): ByteArray {
|
||||||
|
return clientDataHash
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun buildResponse(): String {
|
||||||
|
return CLIENT_DATA_JSON_PRIVILEGED
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val CLIENT_DATA_JSON_PRIVILEGED = "<placeholder>"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
interface ClientDataResponse {
|
||||||
|
fun hashData(): ByteArray
|
||||||
|
fun buildResponse(): String
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
data class PublicKeyCredentialRpEntity(val name: String, val id: String)
|
||||||
|
|
||||||
|
data class PublicKeyCredentialUserEntity(
|
||||||
|
val name: String,
|
||||||
|
val id: ByteArray,
|
||||||
|
val displayName: String
|
||||||
|
) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as PublicKeyCredentialUserEntity
|
||||||
|
|
||||||
|
if (name != other.name) return false
|
||||||
|
if (!id.contentEquals(other.id)) return false
|
||||||
|
if (displayName != other.displayName) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = name.hashCode()
|
||||||
|
result = 31 * result + id.contentHashCode()
|
||||||
|
result = 31 * result + displayName.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PublicKeyCredentialParameters(val type: String, val alg: Long)
|
||||||
|
|
||||||
|
data class PublicKeyCredentialDescriptor(
|
||||||
|
val type: String,
|
||||||
|
val id: ByteArray,
|
||||||
|
val transports: List<String>
|
||||||
|
) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as PublicKeyCredentialDescriptor
|
||||||
|
|
||||||
|
if (type != other.type) return false
|
||||||
|
if (!id.contentEquals(other.id)) return false
|
||||||
|
if (transports != other.transports) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = type.hashCode()
|
||||||
|
result = 31 * result + id.contentHashCode()
|
||||||
|
result = 31 * result + transports.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class AuthenticatorSelectionCriteria(
|
||||||
|
val authenticatorAttachment: String,
|
||||||
|
val residentKey: String,
|
||||||
|
val requireResidentKey: Boolean = false,
|
||||||
|
val userVerification: String = "preferred"
|
||||||
|
)
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class FidoPublicKeyCredential(
|
||||||
|
val id: String,
|
||||||
|
val response: AuthenticatorResponse,
|
||||||
|
val authenticatorAttachment: String
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun json(): String {
|
||||||
|
// see at https://www.w3.org/TR/webauthn-3/#sctn-authenticator-credential-properties-extension
|
||||||
|
val discoverableCredential = true
|
||||||
|
val rk = JSONObject()
|
||||||
|
rk.put("rk", discoverableCredential)
|
||||||
|
val credProps = JSONObject()
|
||||||
|
credProps.put("credProps", rk)
|
||||||
|
|
||||||
|
// See RegistrationResponseJSON at
|
||||||
|
// https://w3c.github.io/webauthn/#ref-for-dom-publickeycredential-tojson
|
||||||
|
val ret = JSONObject()
|
||||||
|
ret.put("id", id)
|
||||||
|
ret.put("rawId", id)
|
||||||
|
ret.put("type", "public-key")
|
||||||
|
ret.put("authenticatorAttachment", authenticatorAttachment)
|
||||||
|
ret.put("response", response.json())
|
||||||
|
ret.put("clientExtensionResults", JSONObject()) // TODO credProps
|
||||||
|
|
||||||
|
return ret.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import com.kunzisoft.encrypt.Base64Helper
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class PublicKeyCredentialCreationOptions(
|
||||||
|
requestJson: String,
|
||||||
|
var clientDataHash: ByteArray?
|
||||||
|
) {
|
||||||
|
val json: JSONObject = JSONObject(requestJson)
|
||||||
|
|
||||||
|
val relyingPartyEntity: PublicKeyCredentialRpEntity
|
||||||
|
val userEntity: PublicKeyCredentialUserEntity
|
||||||
|
val challenge: ByteArray
|
||||||
|
val pubKeyCredParams: List<PublicKeyCredentialParameters>
|
||||||
|
|
||||||
|
var timeout: Long
|
||||||
|
var excludeCredentials: List<PublicKeyCredentialDescriptor>
|
||||||
|
var authenticatorSelection: AuthenticatorSelectionCriteria
|
||||||
|
var attestation: String
|
||||||
|
|
||||||
|
init {
|
||||||
|
val rpJson = json.getJSONObject("rp")
|
||||||
|
relyingPartyEntity = PublicKeyCredentialRpEntity(rpJson.getString("name"), rpJson.getString("id"))
|
||||||
|
val rpUser = json.getJSONObject("user")
|
||||||
|
val userId = Base64Helper.b64Decode(rpUser.getString("id"))
|
||||||
|
userEntity =
|
||||||
|
PublicKeyCredentialUserEntity(
|
||||||
|
rpUser.getString("name"),
|
||||||
|
userId,
|
||||||
|
rpUser.getString("displayName")
|
||||||
|
)
|
||||||
|
challenge = Base64Helper.b64Decode(json.getString("challenge"))
|
||||||
|
val pubKeyCredParamsJson = json.getJSONArray("pubKeyCredParams")
|
||||||
|
val pubKeyCredParamsTmp: MutableList<PublicKeyCredentialParameters> = mutableListOf()
|
||||||
|
for (i in 0 until pubKeyCredParamsJson.length()) {
|
||||||
|
val e = pubKeyCredParamsJson.getJSONObject(i)
|
||||||
|
pubKeyCredParamsTmp.add(
|
||||||
|
PublicKeyCredentialParameters(e.getString("type"), e.getLong("alg"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
pubKeyCredParams = pubKeyCredParamsTmp.toList()
|
||||||
|
|
||||||
|
timeout = json.optLong("timeout", 0)
|
||||||
|
// TODO: Fix excludeCredentials and authenticatorSelection
|
||||||
|
excludeCredentials = emptyList()
|
||||||
|
authenticatorSelection = AuthenticatorSelectionCriteria("platform", "required")
|
||||||
|
attestation = json.optString("attestation", "none")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = PublicKeyCredentialCreationOptions::class.simpleName
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import java.security.KeyPair
|
||||||
|
|
||||||
|
data class PublicKeyCredentialCreationParameters(
|
||||||
|
val publicKeyCredentialCreationOptions: PublicKeyCredentialCreationOptions,
|
||||||
|
val credentialId: ByteArray,
|
||||||
|
val signatureKey: Pair<KeyPair, Long>,
|
||||||
|
val clientDataResponse: ClientDataResponse
|
||||||
|
) {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as PublicKeyCredentialCreationParameters
|
||||||
|
|
||||||
|
if (publicKeyCredentialCreationOptions != other.publicKeyCredentialCreationOptions) return false
|
||||||
|
if (!credentialId.contentEquals(other.credentialId)) return false
|
||||||
|
if (signatureKey != other.signatureKey) return false
|
||||||
|
if (clientDataResponse != other.clientDataResponse) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = publicKeyCredentialCreationOptions.hashCode()
|
||||||
|
result = 31 * result + credentialId.contentHashCode()
|
||||||
|
result = 31 * result + signatureKey.hashCode()
|
||||||
|
result = 31 * result + clientDataResponse.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import com.kunzisoft.encrypt.Base64Helper
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class PublicKeyCredentialRequestOptions(requestJson: String) {
|
||||||
|
val json: JSONObject = JSONObject(requestJson)
|
||||||
|
val challenge: ByteArray = Base64Helper.b64Decode(json.getString("challenge"))
|
||||||
|
val timeout: Long = json.optLong("timeout", 0)
|
||||||
|
val rpId: String = json.optString("rpId", "")
|
||||||
|
val userVerification: String = json.optString("userVerification", "preferred")
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.model.AppOrigin
|
||||||
|
|
||||||
|
data class PublicKeyCredentialUsageParameters(
|
||||||
|
val publicKeyCredentialRequestOptions: PublicKeyCredentialRequestOptions,
|
||||||
|
val clientDataResponse: ClientDataResponse,
|
||||||
|
var appOrigin: AppOrigin
|
||||||
|
)
|
||||||
@@ -0,0 +1,634 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.util
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.ParcelUuid
|
||||||
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
|
import android.security.keystore.KeyProperties
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||||
|
import androidx.credentials.CreatePublicKeyCredentialResponse
|
||||||
|
import androidx.credentials.GetPublicKeyCredentialOption
|
||||||
|
import androidx.credentials.PublicKeyCredential
|
||||||
|
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
||||||
|
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||||
|
import androidx.credentials.provider.CallingAppInfo
|
||||||
|
import androidx.credentials.provider.PendingIntentHandler
|
||||||
|
import androidx.credentials.provider.ProviderCreateCredentialRequest
|
||||||
|
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||||
|
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
|
||||||
|
import com.kunzisoft.encrypt.Signature
|
||||||
|
import com.kunzisoft.encrypt.Signature.getApplicationFingerprints
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataBuildResponse
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataDefinedResponse
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataResponse
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.FidoPublicKeyCredential
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.getOriginFromPrivilegedAllowLists
|
||||||
|
import com.kunzisoft.keepass.model.AndroidOrigin
|
||||||
|
import com.kunzisoft.keepass.model.AppOrigin
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import com.kunzisoft.keepass.model.Passkey
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.utils.AppUtil
|
||||||
|
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.IOException
|
||||||
|
import java.security.KeyStore
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.UUID
|
||||||
|
import javax.crypto.KeyGenerator
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.SecretKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class to manage the passkey elements,
|
||||||
|
* allows to add and retrieve intent values with preconfigured keys,
|
||||||
|
* and makes it easy to create creation and usage requests
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
object PasskeyHelper {
|
||||||
|
|
||||||
|
private const val EXTRA_PASSKEY = "com.kunzisoft.keepass.passkey.extra.passkey"
|
||||||
|
|
||||||
|
private const val HMAC_TYPE = "HmacSHA256"
|
||||||
|
|
||||||
|
|
||||||
|
private const val EXTRA_SEARCH_INFO = "com.kunzisoft.keepass.extra.searchInfo"
|
||||||
|
private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin"
|
||||||
|
private const val EXTRA_NODE_ID = "com.kunzisoft.keepass.extra.nodeId"
|
||||||
|
private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp"
|
||||||
|
private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode"
|
||||||
|
|
||||||
|
private const val SEPARATOR = "_"
|
||||||
|
|
||||||
|
private const val NAME_OF_HMAC_KEY = "KeePassDXCredentialProviderHMACKey"
|
||||||
|
|
||||||
|
private const val KEYSTORE_TYPE = "AndroidKeyStore"
|
||||||
|
|
||||||
|
private val PLACEHOLDER_FOR_NEW_NODE_ID = "0".repeat(32)
|
||||||
|
|
||||||
|
private val REGEX_TIMESTAMP = "[0-9]{10}".toRegex()
|
||||||
|
private val REGEX_AUTHENTICATION_CODE = "[A-F0-9]{64}".toRegex() // 256 bits = 64 hex chars
|
||||||
|
|
||||||
|
private const val MAX_DIFF_IN_SECONDS = 60
|
||||||
|
|
||||||
|
private val internalSecureRandom: SecureRandom = SecureRandom()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the Passkey response for one entry
|
||||||
|
*/
|
||||||
|
fun Activity.buildPasskeyResponseAndSetResult(
|
||||||
|
entryInfo: EntryInfo,
|
||||||
|
extras: Bundle? = null
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
entryInfo.passkey?.let { passkey ->
|
||||||
|
val mReplyIntent = Intent()
|
||||||
|
Log.d(javaClass.name, "Success Passkey manual selection")
|
||||||
|
mReplyIntent.addPasskey(passkey)
|
||||||
|
mReplyIntent.addAppOrigin(entryInfo.appOrigin)
|
||||||
|
mReplyIntent.addNodeId(entryInfo.id)
|
||||||
|
extras?.let {
|
||||||
|
mReplyIntent.putExtras(it)
|
||||||
|
}
|
||||||
|
setResult(Activity.RESULT_OK, mReplyIntent)
|
||||||
|
} ?: run {
|
||||||
|
throw IOException("No passkey found")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(javaClass.name, "Unable to add the passkey as result", e)
|
||||||
|
Toast.makeText(
|
||||||
|
this,
|
||||||
|
getString(R.string.error_passkey_result),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an authentication code generated by an entry to the intent
|
||||||
|
*/
|
||||||
|
fun Intent.addAuthCode(passkeyEntryNodeId: UUID? = null) {
|
||||||
|
putExtras(Bundle().apply {
|
||||||
|
val timestamp = Instant.now().epochSecond
|
||||||
|
putString(EXTRA_TIMESTAMP, timestamp.toString())
|
||||||
|
putString(
|
||||||
|
EXTRA_AUTHENTICATION_CODE,
|
||||||
|
generatedAuthenticationCode(
|
||||||
|
passkeyEntryNodeId, timestamp
|
||||||
|
).toHexString()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the passkey to the intent
|
||||||
|
*/
|
||||||
|
fun Intent.addPasskey(passkey: Passkey?) {
|
||||||
|
passkey?.let {
|
||||||
|
putExtra(EXTRA_PASSKEY, passkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the passkey from the intent
|
||||||
|
*/
|
||||||
|
fun Intent.retrievePasskey(): Passkey? {
|
||||||
|
return this.getParcelableExtraCompat(EXTRA_PASSKEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the passkey from the intent
|
||||||
|
*/
|
||||||
|
fun Intent.removePasskey() {
|
||||||
|
return this.removeExtra(EXTRA_PASSKEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the search info to the intent
|
||||||
|
*/
|
||||||
|
fun Intent.addSearchInfo(searchInfo: SearchInfo?) {
|
||||||
|
searchInfo?.let {
|
||||||
|
putExtra(EXTRA_SEARCH_INFO, searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the search info from the intent
|
||||||
|
*/
|
||||||
|
fun Intent.retrieveSearchInfo(): SearchInfo? {
|
||||||
|
return this.getParcelableExtraCompat(EXTRA_SEARCH_INFO)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the app origin to the intent
|
||||||
|
*/
|
||||||
|
fun Intent.addAppOrigin(appOrigin: AppOrigin?) {
|
||||||
|
appOrigin?.let {
|
||||||
|
putExtra(EXTRA_APP_ORIGIN, appOrigin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the app origin from the intent
|
||||||
|
*/
|
||||||
|
fun Intent.retrieveAppOrigin(): AppOrigin? {
|
||||||
|
return this.getParcelableExtraCompat(EXTRA_APP_ORIGIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the app origin from the intent
|
||||||
|
*/
|
||||||
|
fun Intent.removeAppOrigin() {
|
||||||
|
return this.removeExtra(EXTRA_APP_ORIGIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the node id to the intent, useful for auto passkey selection
|
||||||
|
*/
|
||||||
|
fun Intent.addNodeId(nodeId: UUID?) {
|
||||||
|
nodeId?.let {
|
||||||
|
putExtra(EXTRA_NODE_ID, ParcelUuid(nodeId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the node id from the intent
|
||||||
|
*/
|
||||||
|
fun Intent.retrieveNodeId(): UUID? {
|
||||||
|
return getParcelableExtraCompat<ParcelUuid>(EXTRA_NODE_ID)?.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the timestamp and authentication code transmitted via PendingIntent
|
||||||
|
*/
|
||||||
|
fun checkSecurity(intent: Intent, nodeId: UUID?) {
|
||||||
|
val timestampString = intent.getStringExtra(EXTRA_TIMESTAMP)
|
||||||
|
if (timestampString.isNullOrEmpty())
|
||||||
|
throw CreateCredentialUnknownException("Timestamp null")
|
||||||
|
if (timestampString.matches(REGEX_TIMESTAMP).not()) {
|
||||||
|
throw CreateCredentialUnknownException("Timestamp not valid")
|
||||||
|
}
|
||||||
|
val timestamp = timestampString.toLong()
|
||||||
|
val diff = Instant.now().epochSecond - timestamp
|
||||||
|
if (diff < 0 || diff > MAX_DIFF_IN_SECONDS) {
|
||||||
|
throw CreateCredentialUnknownException("Out of time")
|
||||||
|
}
|
||||||
|
verifyAuthenticationCode(
|
||||||
|
intent.getStringExtra(EXTRA_AUTHENTICATION_CODE),
|
||||||
|
generatedAuthenticationCode(nodeId, timestamp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify the authentication code from the encrypted message received from the intent
|
||||||
|
*/
|
||||||
|
private fun verifyAuthenticationCode(
|
||||||
|
valueToCheck: String?,
|
||||||
|
authenticationCode: ByteArray
|
||||||
|
) {
|
||||||
|
if (valueToCheck.isNullOrEmpty())
|
||||||
|
throw CreateCredentialUnknownException("Authentication code empty")
|
||||||
|
if (valueToCheck.matches(REGEX_AUTHENTICATION_CODE).not())
|
||||||
|
throw CreateCredentialUnknownException("Authentication not valid")
|
||||||
|
if (MessageDigest.isEqual(authenticationCode, generateAuthenticationCode(valueToCheck)))
|
||||||
|
throw CreateCredentialUnknownException("Authentication code incorrect")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the authentication code base on the entry [nodeId] and [timestamp]
|
||||||
|
*/
|
||||||
|
private fun generatedAuthenticationCode(nodeId: UUID?, timestamp: Long): ByteArray {
|
||||||
|
return generateAuthenticationCode(
|
||||||
|
(nodeId?.toString() ?: PLACEHOLDER_FOR_NEW_NODE_ID) + SEPARATOR + timestamp.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the authentication code base on the entry [message]
|
||||||
|
*/
|
||||||
|
private fun generateAuthenticationCode(message: String): ByteArray {
|
||||||
|
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
|
||||||
|
keyStore.load(null)
|
||||||
|
val hmacKey = try {
|
||||||
|
keyStore.getKey(NAME_OF_HMAC_KEY, null) as SecretKey
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// key not found
|
||||||
|
generateKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
val mac = Mac.getInstance(HMAC_TYPE)
|
||||||
|
mac.init(hmacKey)
|
||||||
|
val authenticationCode = mac.doFinal(message.toByteArray())
|
||||||
|
return authenticationCode
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the HMAC key if cannot be found in the KeyStore
|
||||||
|
*/
|
||||||
|
private fun generateKey(): SecretKey? {
|
||||||
|
val keyGenerator = KeyGenerator.getInstance(
|
||||||
|
KeyProperties.KEY_ALGORITHM_HMAC_SHA256, KEYSTORE_TYPE
|
||||||
|
)
|
||||||
|
val keySizeInBits = 128
|
||||||
|
keyGenerator.init(
|
||||||
|
KeyGenParameterSpec.Builder(NAME_OF_HMAC_KEY, KeyProperties.PURPOSE_SIGN)
|
||||||
|
.setKeySize(keySizeInBits)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
val key = keyGenerator.generateKey()
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the [PublicKeyCredentialCreationOptions] from the intent
|
||||||
|
*/
|
||||||
|
fun ProviderCreateCredentialRequest.retrievePasskeyCreationComponent(): PublicKeyCredentialCreationOptions {
|
||||||
|
val request = this
|
||||||
|
if (request.callingRequest !is CreatePublicKeyCredentialRequest) {
|
||||||
|
throw CreateCredentialUnknownException("callingRequest is of wrong type: ${request.callingRequest.type}")
|
||||||
|
}
|
||||||
|
val createPublicKeyCredentialRequest = request.callingRequest as CreatePublicKeyCredentialRequest
|
||||||
|
return PublicKeyCredentialCreationOptions(
|
||||||
|
requestJson = createPublicKeyCredentialRequest.requestJson,
|
||||||
|
clientDataHash = createPublicKeyCredentialRequest.clientDataHash
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the [GetPublicKeyCredentialOption] from the intent
|
||||||
|
*/
|
||||||
|
fun ProviderGetCredentialRequest.retrievePasskeyUsageComponent(): GetPublicKeyCredentialOption {
|
||||||
|
val request = this
|
||||||
|
if (request.credentialOptions.size != 1) {
|
||||||
|
throw GetCredentialUnknownException("not exact one credentialOption")
|
||||||
|
}
|
||||||
|
if (request.credentialOptions[0] !is GetPublicKeyCredentialOption) {
|
||||||
|
throw CreateCredentialUnknownException("credentialOptions is of wrong type: ${request.credentialOptions[0]}")
|
||||||
|
}
|
||||||
|
return request.credentialOptions[0] as GetPublicKeyCredentialOption
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to retrieve the origin asynchronously,
|
||||||
|
* checks for the presence of the application in the privilege lists
|
||||||
|
*
|
||||||
|
* @param providedClientDataHash Client data hash precalculated by the system
|
||||||
|
* @param callingAppInfo CallingAppInfo to verify and retrieve the specific Origin
|
||||||
|
* @param context Context for file operations.
|
||||||
|
* call [onOriginRetrieved] if the origin is already calculated by the system and in the privileged list, return the clientDataHash
|
||||||
|
* call [onOriginNotRetrieved] if the origin is not retrieved from the system, return a new Android Origin
|
||||||
|
*/
|
||||||
|
suspend fun getOrigin(
|
||||||
|
providedClientDataHash: ByteArray?,
|
||||||
|
callingAppInfo: CallingAppInfo?,
|
||||||
|
context: Context,
|
||||||
|
onOriginRetrieved: suspend (appOrigin: AppOrigin, clientDataHash: ByteArray) -> Unit,
|
||||||
|
onOriginNotRetrieved: suspend (appOrigin: AppOrigin, androidOriginString: String) -> Unit
|
||||||
|
) {
|
||||||
|
if (callingAppInfo == null) {
|
||||||
|
throw SecurityException("Calling app info cannot be retrieved")
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
|
||||||
|
// For trusted browsers like Chrome and Firefox
|
||||||
|
val callOrigin = try {
|
||||||
|
getOriginFromPrivilegedAllowLists(callingAppInfo, context)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Throw the Privileged Exception only if it's a browser
|
||||||
|
if (e is PrivilegedAllowLists.PrivilegedException
|
||||||
|
&& AppUtil.getInstalledBrowsersWithSignatures(context).any {
|
||||||
|
it.packageName == e.temptingApp.packageName
|
||||||
|
}
|
||||||
|
) throw e
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the default Android origin
|
||||||
|
val androidOrigin = AndroidOrigin(
|
||||||
|
packageName = callingAppInfo.packageName,
|
||||||
|
fingerprint = callingAppInfo.signingInfo.getApplicationFingerprints()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check if the webDomain is validated by the system
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (callOrigin != null && providedClientDataHash != null) {
|
||||||
|
// Origin already defined by the system
|
||||||
|
Log.d(javaClass.simpleName, "Origin $callOrigin retrieved from callingAppInfo")
|
||||||
|
onOriginRetrieved(
|
||||||
|
AppOrigin.fromOrigin(callOrigin, androidOrigin, verified = true),
|
||||||
|
providedClientDataHash
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Add Android origin by default
|
||||||
|
onOriginNotRetrieved(
|
||||||
|
AppOrigin(verified = false).apply {
|
||||||
|
addAndroidOrigin(androidOrigin)
|
||||||
|
},
|
||||||
|
androidOrigin.toOriginValue()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a credential id randomly
|
||||||
|
*/
|
||||||
|
private fun generateCredentialId(): ByteArray {
|
||||||
|
// see https://w3c.github.io/webauthn/#credential-id
|
||||||
|
val size = 16
|
||||||
|
val credentialId = ByteArray(size)
|
||||||
|
internalSecureRandom.nextBytes(credentialId)
|
||||||
|
return credentialId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to create a passkey and the associated creation request parameters
|
||||||
|
* [intent] allows to retrieve the request
|
||||||
|
* [context] context to manage package verification files
|
||||||
|
* [passkeyCreated] is called asynchronously when the passkey has been created
|
||||||
|
*/
|
||||||
|
suspend fun retrievePasskeyCreationRequestParameters(
|
||||||
|
intent: Intent,
|
||||||
|
context: Context,
|
||||||
|
passkeyCreated: suspend (Passkey, AppOrigin?, PublicKeyCredentialCreationParameters) -> Unit
|
||||||
|
) {
|
||||||
|
val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
|
||||||
|
if (createCredentialRequest == null)
|
||||||
|
throw CreateCredentialUnknownException("could not retrieve request from intent")
|
||||||
|
val callingAppInfo = createCredentialRequest.callingAppInfo
|
||||||
|
val creationOptions = createCredentialRequest.retrievePasskeyCreationComponent()
|
||||||
|
|
||||||
|
val relyingParty = creationOptions.relyingPartyEntity.id
|
||||||
|
val username = creationOptions.userEntity.name
|
||||||
|
val userHandle = creationOptions.userEntity.id
|
||||||
|
val pubKeyCredParams = creationOptions.pubKeyCredParams
|
||||||
|
val clientDataHash = creationOptions.clientDataHash
|
||||||
|
|
||||||
|
val credentialId = generateCredentialId()
|
||||||
|
|
||||||
|
val (keyPair, keyTypeId) = Signature.generateKeyPair(
|
||||||
|
pubKeyCredParams.map { params -> params.alg }
|
||||||
|
) ?: throw CreateCredentialUnknownException("no known public key type found")
|
||||||
|
val privateKeyPem = Signature.convertPrivateKeyToPem(keyPair.private)
|
||||||
|
|
||||||
|
// Create the passkey element
|
||||||
|
val passkey = Passkey(
|
||||||
|
username = username,
|
||||||
|
privateKeyPem = privateKeyPem,
|
||||||
|
credentialId = b64Encode(credentialId),
|
||||||
|
userHandle = b64Encode(userHandle),
|
||||||
|
relyingParty = relyingParty
|
||||||
|
)
|
||||||
|
|
||||||
|
// create new entry in database
|
||||||
|
getOrigin(
|
||||||
|
providedClientDataHash = clientDataHash,
|
||||||
|
callingAppInfo = callingAppInfo,
|
||||||
|
context = context,
|
||||||
|
onOriginRetrieved = { appInfoToStore, clientDataHash ->
|
||||||
|
passkeyCreated.invoke(
|
||||||
|
passkey,
|
||||||
|
appInfoToStore,
|
||||||
|
PublicKeyCredentialCreationParameters(
|
||||||
|
publicKeyCredentialCreationOptions = creationOptions,
|
||||||
|
credentialId = credentialId,
|
||||||
|
signatureKey = Pair(keyPair, keyTypeId),
|
||||||
|
clientDataResponse = ClientDataDefinedResponse(clientDataHash)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onOriginNotRetrieved = { appInfoToStore, origin ->
|
||||||
|
passkeyCreated.invoke(
|
||||||
|
passkey,
|
||||||
|
appInfoToStore,
|
||||||
|
PublicKeyCredentialCreationParameters(
|
||||||
|
publicKeyCredentialCreationOptions = creationOptions,
|
||||||
|
credentialId = credentialId,
|
||||||
|
signatureKey = Pair(keyPair, keyTypeId),
|
||||||
|
clientDataResponse = ClientDataBuildResponse(
|
||||||
|
type = ClientDataBuildResponse.Type.CREATE,
|
||||||
|
challenge = creationOptions.challenge,
|
||||||
|
origin = origin
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the passkey public key credential response,
|
||||||
|
* by calling this method the user is always recognized as present and verified
|
||||||
|
*/
|
||||||
|
fun buildCreatePublicKeyCredentialResponse(
|
||||||
|
publicKeyCredentialCreationParameters: PublicKeyCredentialCreationParameters,
|
||||||
|
backupEligibility: Boolean,
|
||||||
|
backupState: Boolean
|
||||||
|
): CreatePublicKeyCredentialResponse {
|
||||||
|
|
||||||
|
val keyPair = publicKeyCredentialCreationParameters.signatureKey.first
|
||||||
|
val keyTypeId = publicKeyCredentialCreationParameters.signatureKey.second
|
||||||
|
val responseJson = FidoPublicKeyCredential(
|
||||||
|
id = b64Encode(publicKeyCredentialCreationParameters.credentialId),
|
||||||
|
response = AuthenticatorAttestationResponse(
|
||||||
|
requestOptions = publicKeyCredentialCreationParameters.publicKeyCredentialCreationOptions,
|
||||||
|
credentialId = publicKeyCredentialCreationParameters.credentialId,
|
||||||
|
credentialPublicKey = Cbor().encode(
|
||||||
|
Signature.convertPublicKeyToMap(
|
||||||
|
publicKeyIn = keyPair.public,
|
||||||
|
keyTypeId = keyTypeId
|
||||||
|
) ?: mapOf<Int, Any>()),
|
||||||
|
userPresent = true,
|
||||||
|
userVerified = true,
|
||||||
|
backupEligibility = backupEligibility,
|
||||||
|
backupState = backupState,
|
||||||
|
publicKeyTypeId = keyTypeId,
|
||||||
|
publicKeyCbor = Signature.convertPublicKey(keyPair.public, keyTypeId)!!,
|
||||||
|
clientDataResponse = publicKeyCredentialCreationParameters.clientDataResponse
|
||||||
|
),
|
||||||
|
authenticatorAttachment = "platform"
|
||||||
|
).json()
|
||||||
|
// log only the length to prevent logging sensitive information
|
||||||
|
Log.d(javaClass.simpleName, "Json response for key creation")
|
||||||
|
return CreatePublicKeyCredentialResponse(responseJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to use a passkey and create the associated usage request parameters
|
||||||
|
* [intent] allows to retrieve the request
|
||||||
|
* [context] context to manage package verification files
|
||||||
|
* [result] is called asynchronously after the creation of PublicKeyCredentialUsageParameters, the origin associated with it may or may not be verified
|
||||||
|
*/
|
||||||
|
suspend fun retrievePasskeyUsageRequestParameters(
|
||||||
|
intent: Intent,
|
||||||
|
context: Context,
|
||||||
|
result: suspend (PublicKeyCredentialUsageParameters) -> Unit
|
||||||
|
) {
|
||||||
|
val getCredentialRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
|
||||||
|
if (getCredentialRequest == null)
|
||||||
|
throw CreateCredentialUnknownException("could not retrieve request from intent")
|
||||||
|
val callingAppInfo = getCredentialRequest.callingAppInfo
|
||||||
|
val credentialOption = getCredentialRequest.retrievePasskeyUsageComponent()
|
||||||
|
val clientDataHash = credentialOption.clientDataHash
|
||||||
|
|
||||||
|
val requestOptions = PublicKeyCredentialRequestOptions(credentialOption.requestJson)
|
||||||
|
|
||||||
|
getOrigin(
|
||||||
|
providedClientDataHash = clientDataHash,
|
||||||
|
callingAppInfo = callingAppInfo,
|
||||||
|
context = context,
|
||||||
|
onOriginRetrieved = { appOrigin, clientDataHash ->
|
||||||
|
result.invoke(
|
||||||
|
PublicKeyCredentialUsageParameters(
|
||||||
|
publicKeyCredentialRequestOptions = requestOptions,
|
||||||
|
clientDataResponse = ClientDataDefinedResponse(clientDataHash),
|
||||||
|
appOrigin = appOrigin
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onOriginNotRetrieved = { appOrigin, androidOriginString ->
|
||||||
|
// By default we crate an usage parameter with Android origin
|
||||||
|
result.invoke(
|
||||||
|
PublicKeyCredentialUsageParameters(
|
||||||
|
publicKeyCredentialRequestOptions = requestOptions,
|
||||||
|
clientDataResponse = ClientDataBuildResponse(
|
||||||
|
type = ClientDataBuildResponse.Type.GET,
|
||||||
|
challenge = requestOptions.challenge,
|
||||||
|
origin = androidOriginString
|
||||||
|
),
|
||||||
|
appOrigin = appOrigin
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the passkey public key credential response,
|
||||||
|
* by calling this method the user is always recognized as present and verified
|
||||||
|
*/
|
||||||
|
fun buildPasskeyPublicKeyCredential(
|
||||||
|
requestOptions: PublicKeyCredentialRequestOptions,
|
||||||
|
clientDataResponse: ClientDataResponse,
|
||||||
|
passkey: Passkey,
|
||||||
|
backupEligibility: Boolean,
|
||||||
|
backupState: Boolean
|
||||||
|
): PublicKeyCredential {
|
||||||
|
val getCredentialResponse = FidoPublicKeyCredential(
|
||||||
|
id = passkey.credentialId,
|
||||||
|
response = AuthenticatorAssertionResponse(
|
||||||
|
requestOptions = requestOptions,
|
||||||
|
userPresent = true,
|
||||||
|
userVerified = true,
|
||||||
|
backupEligibility = backupEligibility,
|
||||||
|
backupState = backupState,
|
||||||
|
userHandle = passkey.userHandle,
|
||||||
|
privateKey = passkey.privateKeyPem,
|
||||||
|
clientDataResponse = clientDataResponse
|
||||||
|
),
|
||||||
|
authenticatorAttachment = "platform"
|
||||||
|
).json()
|
||||||
|
Log.d(javaClass.simpleName, "Json response for key usage")
|
||||||
|
return PublicKeyCredential(getCredentialResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that the application signature is contained in the [appOrigin]
|
||||||
|
*/
|
||||||
|
fun getVerifiedGETClientDataResponse(
|
||||||
|
usageParameters: PublicKeyCredentialUsageParameters,
|
||||||
|
appOrigin: AppOrigin
|
||||||
|
): ClientDataResponse {
|
||||||
|
val appToCheck = usageParameters.appOrigin
|
||||||
|
return if (appToCheck.verified) {
|
||||||
|
usageParameters.clientDataResponse
|
||||||
|
} else {
|
||||||
|
// Origin checked by Android app signature
|
||||||
|
ClientDataBuildResponse(
|
||||||
|
type = ClientDataBuildResponse.Type.GET,
|
||||||
|
challenge = usageParameters.publicKeyCredentialRequestOptions.challenge,
|
||||||
|
origin = appToCheck.checkAppOrigin(appOrigin)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.credentialprovider.passkey.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.credentials.provider.CallingAppInfo
|
||||||
|
import com.kunzisoft.encrypt.Signature.getAllFingerprints
|
||||||
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
object PrivilegedAllowLists {
|
||||||
|
|
||||||
|
private const val FILE_NAME_PRIVILEGED_APPS_CUSTOM = "passkeys_privileged_apps_custom.json"
|
||||||
|
private const val FILE_NAME_PRIVILEGED_APPS_COMMUNITY = "passkeys_privileged_apps_community.json"
|
||||||
|
private const val FILE_NAME_PRIVILEGED_APPS_GOOGLE = "passkeys_privileged_apps_google.json"
|
||||||
|
|
||||||
|
private fun retrieveContentFromStream(
|
||||||
|
inputStream: InputStream,
|
||||||
|
): String {
|
||||||
|
return inputStream.use { fileInputStream ->
|
||||||
|
fileInputStream.bufferedReader(Charsets.UTF_8).readText()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the origin from a predefined privileged allow list
|
||||||
|
*
|
||||||
|
* @param callingAppInfo CallingAppInfo to verify and retrieve the specific Origin
|
||||||
|
* @param inputStream File input stream containing the origin list as JSON
|
||||||
|
*/
|
||||||
|
private fun getOriginFromPrivilegedAllowListStream(
|
||||||
|
callingAppInfo: CallingAppInfo,
|
||||||
|
inputStream: InputStream
|
||||||
|
): String? {
|
||||||
|
val privilegedAllowList = retrieveContentFromStream(inputStream)
|
||||||
|
return callingAppInfo.getOrigin(privilegedAllowList)?.removeSuffix("/")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the origin from the predefined privileged allow lists
|
||||||
|
*
|
||||||
|
* @param callingAppInfo CallingAppInfo to verify and retrieve the specific Origin
|
||||||
|
* @param context Context for file operations.
|
||||||
|
*/
|
||||||
|
fun getOriginFromPrivilegedAllowLists(
|
||||||
|
callingAppInfo: CallingAppInfo,
|
||||||
|
context: Context
|
||||||
|
): String? {
|
||||||
|
return try {
|
||||||
|
// Check the custom apps first
|
||||||
|
getOriginFromPrivilegedAllowListStream(
|
||||||
|
callingAppInfo = callingAppInfo,
|
||||||
|
File(context.filesDir, FILE_NAME_PRIVILEGED_APPS_CUSTOM)
|
||||||
|
.inputStream()
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Then the Google list if allowed
|
||||||
|
if (BuildConfig.CLOSED_STORE) {
|
||||||
|
try {
|
||||||
|
// Check the Google list if allowed
|
||||||
|
// http://www.gstatic.com/gpm-passkeys-privileged-apps/apps.json
|
||||||
|
getOriginFromPrivilegedAllowListStream(
|
||||||
|
callingAppInfo = callingAppInfo,
|
||||||
|
inputStream = context.assets.open(FILE_NAME_PRIVILEGED_APPS_GOOGLE)
|
||||||
|
)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// Then the community apps list
|
||||||
|
getOriginFromPrivilegedAllowListStream(
|
||||||
|
callingAppInfo = callingAppInfo,
|
||||||
|
inputStream = context.assets.open(FILE_NAME_PRIVILEGED_APPS_COMMUNITY)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
when (e) {
|
||||||
|
is FileNotFoundException -> {
|
||||||
|
val attemptApp = AndroidPrivilegedApp(
|
||||||
|
packageName = callingAppInfo.packageName,
|
||||||
|
fingerprints = callingAppInfo.signingInfo
|
||||||
|
.getAllFingerprints() ?: emptySet()
|
||||||
|
)
|
||||||
|
throw PrivilegedException(
|
||||||
|
temptingApp = attemptApp,
|
||||||
|
message = "$attemptApp is not in the allow list"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a list of predefined AndroidPrivilegedApp objects from an asset JSON file.
|
||||||
|
*
|
||||||
|
* @param inputStream File input stream containing the origin list as JSON
|
||||||
|
*/
|
||||||
|
private fun retrievePrivilegedApps(
|
||||||
|
inputStream: InputStream
|
||||||
|
): List<AndroidPrivilegedApp> {
|
||||||
|
val jsonObject = JSONObject(retrieveContentFromStream(inputStream))
|
||||||
|
return AndroidPrivilegedApp.extractPrivilegedApps(jsonObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a list of predefined AndroidPrivilegedApp objects from a context
|
||||||
|
*
|
||||||
|
* @param context Context for file operations.
|
||||||
|
*/
|
||||||
|
fun retrievePredefinedPrivilegedApps(
|
||||||
|
context: Context
|
||||||
|
): List<AndroidPrivilegedApp> {
|
||||||
|
return try {
|
||||||
|
val predefinedApps = mutableListOf<AndroidPrivilegedApp>()
|
||||||
|
predefinedApps.addAll(retrievePrivilegedApps(context.assets.open(FILE_NAME_PRIVILEGED_APPS_COMMUNITY)))
|
||||||
|
if (BuildConfig.CLOSED_STORE) {
|
||||||
|
predefinedApps.addAll(retrievePrivilegedApps(context.assets.open(FILE_NAME_PRIVILEGED_APPS_GOOGLE)))
|
||||||
|
}
|
||||||
|
predefinedApps
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(PrivilegedAllowLists::class.simpleName, "Error retrieving privileged apps", e)
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a list of AndroidPrivilegedApp objects from the custom JSON file.
|
||||||
|
*
|
||||||
|
* @param context Context for file operations.
|
||||||
|
*/
|
||||||
|
fun retrieveCustomPrivilegedApps(
|
||||||
|
context: Context
|
||||||
|
): List<AndroidPrivilegedApp> {
|
||||||
|
return try {
|
||||||
|
retrievePrivilegedApps(File(context.filesDir, FILE_NAME_PRIVILEGED_APPS_CUSTOM).inputStream())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.i(PrivilegedAllowLists::class.simpleName, "No custom privileged apps", e)
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a list of all predefined and custom AndroidPrivilegedApp objects.
|
||||||
|
*/
|
||||||
|
fun retrieveAllPrivilegedApps(
|
||||||
|
context: Context
|
||||||
|
): List<AndroidPrivilegedApp> {
|
||||||
|
return retrievePredefinedPrivilegedApps(context) + retrieveCustomPrivilegedApps(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves a list of custom AndroidPrivilegedApp objects to a JSON file.
|
||||||
|
*
|
||||||
|
* @param context Context for file operations.
|
||||||
|
* @param privilegedApps The list of apps to save.
|
||||||
|
* @return True if saving was successful, false otherwise.
|
||||||
|
*/
|
||||||
|
fun saveCustomPrivilegedApps(context: Context, privilegedApps: List<AndroidPrivilegedApp>): Boolean {
|
||||||
|
return try {
|
||||||
|
val jsonToSave = AndroidPrivilegedApp.toJsonObject(privilegedApps)
|
||||||
|
val file = File(context.filesDir, FILE_NAME_PRIVILEGED_APPS_CUSTOM)
|
||||||
|
|
||||||
|
// Delete existing file before writing to ensure atomicity if needed
|
||||||
|
if (file.exists()) {
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
file.outputStream().use { fileOutputStream ->
|
||||||
|
fileOutputStream.write(
|
||||||
|
jsonToSave
|
||||||
|
.toString(4) // toString(4) for pretty print
|
||||||
|
.toByteArray(Charsets.UTF_8)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(PrivilegedAllowLists::class.simpleName, "Error saving privileged apps", e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the custom JSON file.
|
||||||
|
*
|
||||||
|
* @param context Context for file operations.
|
||||||
|
* @return True if deletion was successful or file didn't exist, false otherwise.
|
||||||
|
*/
|
||||||
|
fun deletePrivilegedAppsFile(context: Context): Boolean {
|
||||||
|
return try {
|
||||||
|
val file = File(context.filesDir, FILE_NAME_PRIVILEGED_APPS_CUSTOM)
|
||||||
|
if (file.exists()) {
|
||||||
|
file.delete()
|
||||||
|
} else {
|
||||||
|
true // File didn't exist, so considered "successfully deleted"
|
||||||
|
}
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Log.e(PrivilegedAllowLists::class.simpleName, "Error deleting privileged apps file", e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PrivilegedException(
|
||||||
|
val temptingApp: AndroidPrivilegedApp,
|
||||||
|
message: String
|
||||||
|
) : Exception(message)
|
||||||
|
}
|
||||||
@@ -0,0 +1,584 @@
|
|||||||
|
package com.kunzisoft.keepass.credentialprovider.viewmodel
|
||||||
|
|
||||||
|
import android.app.Activity.RESULT_CANCELED
|
||||||
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.credentials.GetCredentialResponse
|
||||||
|
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||||
|
import androidx.credentials.provider.PendingIntentHandler
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildCreatePublicKeyCredentialResponse
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyPublicKeyCredential
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.checkSecurity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getVerifiedGETClientDataResponse
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeAppOrigin
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveAppOrigin
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveNodeId
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskey
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationRequestParameters
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveSearchInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.saveCustomPrivilegedApps
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
|
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
|
import com.kunzisoft.keepass.model.AppOrigin
|
||||||
|
import com.kunzisoft.keepass.model.Passkey
|
||||||
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.model.SignatureNotFoundException
|
||||||
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.getNewEntry
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InvalidObjectException
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
class PasskeyLauncherViewModel(application: Application): AndroidViewModel(application) {
|
||||||
|
|
||||||
|
private var mUsageParameters: PublicKeyCredentialUsageParameters? = null
|
||||||
|
private var mCreationParameters: PublicKeyCredentialCreationParameters? = null
|
||||||
|
private var mPasskey: Passkey? = null
|
||||||
|
|
||||||
|
private var mBackupEligibility: Boolean = true
|
||||||
|
private var mBackupState: Boolean = false
|
||||||
|
private var mLockDatabase: Boolean = true
|
||||||
|
|
||||||
|
private var isResultLauncherRegistered: Boolean = false
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow<UIState>(UIState.Loading)
|
||||||
|
val uiState: StateFlow<UIState> = _uiState
|
||||||
|
|
||||||
|
fun initialize() {
|
||||||
|
mBackupEligibility = PreferencesUtil.isPasskeyBackupEligibilityEnable(getApplication())
|
||||||
|
mBackupState = PreferencesUtil.isPasskeyBackupStateEnable(getApplication())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showAppPrivilegedDialog(
|
||||||
|
temptingApp: AndroidPrivilegedApp
|
||||||
|
) {
|
||||||
|
_uiState.value = UIState.ShowAppPrivilegedDialog(temptingApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showAppSignatureDialog(
|
||||||
|
temptingApp: AppOrigin,
|
||||||
|
nodeId: UUID
|
||||||
|
) {
|
||||||
|
_uiState.value = UIState.ShowAppSignatureDialog(temptingApp, nodeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showError(error: Throwable) {
|
||||||
|
Log.e(TAG, "Error on passkey launch", error)
|
||||||
|
_uiState.value = UIState.ShowError(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveCustomPrivilegedApp(
|
||||||
|
intent: Intent,
|
||||||
|
specialMode: SpecialMode,
|
||||||
|
database: ContextualDatabase?,
|
||||||
|
temptingApp: AndroidPrivilegedApp
|
||||||
|
) {
|
||||||
|
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
||||||
|
showError(e)
|
||||||
|
}) {
|
||||||
|
saveCustomPrivilegedApps(
|
||||||
|
context = getApplication(),
|
||||||
|
privilegedApps = listOf(temptingApp)
|
||||||
|
)
|
||||||
|
launchPasskeyAction(
|
||||||
|
intent = intent,
|
||||||
|
specialMode = specialMode,
|
||||||
|
database = database
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveAppSignature(
|
||||||
|
database: ContextualDatabase?,
|
||||||
|
temptingApp: AppOrigin,
|
||||||
|
nodeId: UUID
|
||||||
|
) {
|
||||||
|
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
||||||
|
showError(e)
|
||||||
|
}) {
|
||||||
|
// Update the entry with app signature
|
||||||
|
val entry = database
|
||||||
|
?.getEntryById(NodeIdUUID(nodeId))
|
||||||
|
?: throw GetCredentialUnknownException(
|
||||||
|
"No passkey with nodeId $nodeId found"
|
||||||
|
)
|
||||||
|
if (database.isReadOnly)
|
||||||
|
throw RegisterInReadOnlyDatabaseException()
|
||||||
|
val newEntry = Entry(entry)
|
||||||
|
val entryInfo = newEntry.getEntryInfo(
|
||||||
|
database,
|
||||||
|
raw = true,
|
||||||
|
removeTemplateConfiguration = false
|
||||||
|
)
|
||||||
|
entryInfo.saveAppOrigin(database, temptingApp)
|
||||||
|
newEntry.setEntryInfo(database, entryInfo)
|
||||||
|
_uiState.value = UIState.UpdateEntry(
|
||||||
|
oldEntry = entry,
|
||||||
|
newEntry = newEntry
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setResult(intent: Intent) {
|
||||||
|
// Remove the launcher register
|
||||||
|
isResultLauncherRegistered = false
|
||||||
|
_uiState.value = UIState.SetActivityResult(
|
||||||
|
lockDatabase = mLockDatabase,
|
||||||
|
resultCode = RESULT_OK,
|
||||||
|
data = intent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelResult() {
|
||||||
|
isResultLauncherRegistered = false
|
||||||
|
_uiState.value = UIState.SetActivityResult(
|
||||||
|
lockDatabase = mLockDatabase,
|
||||||
|
resultCode = RESULT_CANCELED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun launchPasskeyActionIfNeeded(
|
||||||
|
intent: Intent,
|
||||||
|
specialMode: SpecialMode,
|
||||||
|
database: ContextualDatabase?
|
||||||
|
) {
|
||||||
|
if (isResultLauncherRegistered.not()) {
|
||||||
|
isResultLauncherRegistered = true
|
||||||
|
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
||||||
|
if (e is PrivilegedAllowLists.PrivilegedException) {
|
||||||
|
showAppPrivilegedDialog(e.temptingApp)
|
||||||
|
} else {
|
||||||
|
showError(e)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
launchPasskeyAction(intent, specialMode, database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch the main action to manage Passkey
|
||||||
|
*/
|
||||||
|
private suspend fun launchPasskeyAction(
|
||||||
|
intent: Intent,
|
||||||
|
specialMode: SpecialMode,
|
||||||
|
database: ContextualDatabase?
|
||||||
|
) {
|
||||||
|
val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo()
|
||||||
|
val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin(verified = false)
|
||||||
|
val nodeId = intent.retrieveNodeId()
|
||||||
|
checkSecurity(intent, nodeId)
|
||||||
|
when (specialMode) {
|
||||||
|
SpecialMode.SELECTION -> {
|
||||||
|
launchSelection(
|
||||||
|
intent = intent,
|
||||||
|
database = database,
|
||||||
|
nodeId = nodeId,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
appOrigin = appOrigin
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SpecialMode.REGISTRATION -> {
|
||||||
|
// TODO Registration in predefined group
|
||||||
|
// launchRegistration(database, nodeId, mSearchInfo)
|
||||||
|
launchRegistration(
|
||||||
|
intent = intent,
|
||||||
|
database = database,
|
||||||
|
nodeId = null,
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw InvalidObjectException("Passkey launch mode not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------
|
||||||
|
// Selection
|
||||||
|
// -------------
|
||||||
|
|
||||||
|
private suspend fun launchSelection(
|
||||||
|
intent: Intent,
|
||||||
|
database: ContextualDatabase?,
|
||||||
|
nodeId: UUID?,
|
||||||
|
searchInfo: SearchInfo,
|
||||||
|
appOrigin: AppOrigin
|
||||||
|
) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
Log.d(TAG, "Launch passkey selection")
|
||||||
|
retrievePasskeyUsageRequestParameters(
|
||||||
|
intent = intent,
|
||||||
|
context = getApplication()
|
||||||
|
) { usageParameters ->
|
||||||
|
// Save the requested parameters
|
||||||
|
mUsageParameters = usageParameters
|
||||||
|
// Manage the passkey to use
|
||||||
|
nodeId?.let { nodeId ->
|
||||||
|
autoSelectPasskeyAndSetResult(database, nodeId, appOrigin)
|
||||||
|
} ?: run {
|
||||||
|
SearchHelper.checkAutoSearchInfo(
|
||||||
|
context = getApplication(),
|
||||||
|
database = database,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { _, _ ->
|
||||||
|
Log.w(
|
||||||
|
TAG, "Passkey found for auto selection, should not append," +
|
||||||
|
"use PasskeyProviderService instead"
|
||||||
|
)
|
||||||
|
cancelResult()
|
||||||
|
},
|
||||||
|
onItemNotFound = { openedDatabase ->
|
||||||
|
Log.d(
|
||||||
|
TAG, "No Passkey found for selection," +
|
||||||
|
"launch manual selection in opened database"
|
||||||
|
)
|
||||||
|
_uiState.value = UIState.LaunchGroupActivityForSelection(
|
||||||
|
database = openedDatabase
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
Log.d(TAG, "Manual passkey selection in closed database")
|
||||||
|
_uiState.value =
|
||||||
|
UIState.LaunchFileDatabaseSelectActivityForSelection(
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun autoSelectPasskey(
|
||||||
|
result: ActionRunnable.Result,
|
||||||
|
database: ContextualDatabase
|
||||||
|
) {
|
||||||
|
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
||||||
|
showError(e)
|
||||||
|
}) {
|
||||||
|
if (result.isSuccess) {
|
||||||
|
val entry = result.data?.getNewEntry(database)
|
||||||
|
?: throw IOException("No passkey entry found")
|
||||||
|
autoSelectPasskeyAndSetResult(
|
||||||
|
database = database,
|
||||||
|
nodeId = entry.nodeId.id,
|
||||||
|
appOrigin = entry.getAppOrigin()
|
||||||
|
?: throw IOException("No App origin found")
|
||||||
|
)
|
||||||
|
} else throw result.exception
|
||||||
|
?: IOException("Unable to auto select passkey")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun autoSelectPasskeyAndSetResult(
|
||||||
|
database: ContextualDatabase?,
|
||||||
|
nodeId: UUID,
|
||||||
|
appOrigin: AppOrigin
|
||||||
|
) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
mUsageParameters?.let { usageParameters ->
|
||||||
|
// To get the passkey from the database
|
||||||
|
val passkey = database
|
||||||
|
?.getEntryById(NodeIdUUID(nodeId))
|
||||||
|
?.getEntryInfo(database)
|
||||||
|
?.passkey
|
||||||
|
?: throw IOException(
|
||||||
|
"No passkey with nodeId $nodeId found"
|
||||||
|
)
|
||||||
|
// Build the response
|
||||||
|
val result = Intent()
|
||||||
|
try {
|
||||||
|
PendingIntentHandler.setGetCredentialResponse(
|
||||||
|
result,
|
||||||
|
GetCredentialResponse(
|
||||||
|
buildPasskeyPublicKeyCredential(
|
||||||
|
requestOptions = usageParameters.publicKeyCredentialRequestOptions,
|
||||||
|
clientDataResponse = getVerifiedGETClientDataResponse(
|
||||||
|
usageParameters = usageParameters,
|
||||||
|
appOrigin = appOrigin
|
||||||
|
),
|
||||||
|
passkey = passkey,
|
||||||
|
backupEligibility = mBackupEligibility,
|
||||||
|
backupState = mBackupState
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setResult(result)
|
||||||
|
} catch (e: SignatureNotFoundException) {
|
||||||
|
// Request the dialog if signature exception
|
||||||
|
showAppSignatureDialog(e.temptingApp, nodeId)
|
||||||
|
}
|
||||||
|
} ?: throw IOException("Usage parameters is null")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun manageSelectionResult(
|
||||||
|
activityResult: ActivityResult
|
||||||
|
) {
|
||||||
|
val intent = activityResult.data
|
||||||
|
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
||||||
|
Log.e(TAG, "Unable to create selection response for passkey", e)
|
||||||
|
if (e is SignatureNotFoundException) {
|
||||||
|
intent?.retrieveNodeId()?.let { nodeId ->
|
||||||
|
showAppSignatureDialog(e.temptingApp, nodeId)
|
||||||
|
} ?: cancelResult()
|
||||||
|
} else {
|
||||||
|
showError(e)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
// Build a new formatted response from the selection response
|
||||||
|
val responseIntent = Intent()
|
||||||
|
when (activityResult.resultCode) {
|
||||||
|
RESULT_OK -> {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
Log.d(TAG, "Passkey selection result")
|
||||||
|
if (intent == null)
|
||||||
|
throw IOException("Intent is null")
|
||||||
|
val passkey = intent.retrievePasskey()
|
||||||
|
?: throw IOException("Passkey is null")
|
||||||
|
val appOrigin = intent.retrieveAppOrigin()
|
||||||
|
?: throw IOException("App origin is null")
|
||||||
|
intent.removePasskey()
|
||||||
|
intent.removeAppOrigin()
|
||||||
|
mUsageParameters?.let { usageParameters ->
|
||||||
|
// Check verified origin
|
||||||
|
PendingIntentHandler.setGetCredentialResponse(
|
||||||
|
responseIntent,
|
||||||
|
GetCredentialResponse(
|
||||||
|
buildPasskeyPublicKeyCredential(
|
||||||
|
requestOptions = usageParameters.publicKeyCredentialRequestOptions,
|
||||||
|
clientDataResponse = getVerifiedGETClientDataResponse(
|
||||||
|
usageParameters = usageParameters,
|
||||||
|
appOrigin = appOrigin
|
||||||
|
),
|
||||||
|
passkey = passkey,
|
||||||
|
backupEligibility = mBackupEligibility,
|
||||||
|
backupState = mBackupState
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} ?: run {
|
||||||
|
throw IOException("Usage parameters is null")
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
setResult(responseIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RESULT_CANCELED -> {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
cancelResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------
|
||||||
|
// Registration
|
||||||
|
// -------------
|
||||||
|
|
||||||
|
private suspend fun launchRegistration(
|
||||||
|
intent: Intent,
|
||||||
|
database: ContextualDatabase?,
|
||||||
|
nodeId: UUID?,
|
||||||
|
searchInfo: SearchInfo
|
||||||
|
) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
Log.d(TAG, "Launch passkey registration")
|
||||||
|
retrievePasskeyCreationRequestParameters(
|
||||||
|
intent = intent,
|
||||||
|
context = getApplication(),
|
||||||
|
passkeyCreated = { passkey, appInfoToStore, publicKeyCredentialParameters ->
|
||||||
|
// Save the requested parameters
|
||||||
|
mPasskey = passkey
|
||||||
|
mCreationParameters = publicKeyCredentialParameters
|
||||||
|
// Manage the passkey and create a register info
|
||||||
|
val registerInfo = RegisterInfo(
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
passkey = passkey,
|
||||||
|
appOrigin = appInfoToStore
|
||||||
|
)
|
||||||
|
// If nodeId already provided
|
||||||
|
nodeId?.let { nodeId ->
|
||||||
|
autoRegisterPasskeyAndSetResult(database, nodeId, passkey)
|
||||||
|
} ?: run {
|
||||||
|
SearchHelper.checkAutoSearchInfo(
|
||||||
|
context = getApplication(),
|
||||||
|
database = database,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { openedDatabase, _ ->
|
||||||
|
Log.w(
|
||||||
|
TAG, "Passkey found for registration, " +
|
||||||
|
"but launch manual registration for a new entry"
|
||||||
|
)
|
||||||
|
_uiState.value = UIState.LaunchGroupActivityForRegistration(
|
||||||
|
database = openedDatabase,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.PASSKEY
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onItemNotFound = { openedDatabase ->
|
||||||
|
Log.d(TAG, "Launch new manual registration in opened database")
|
||||||
|
_uiState.value = UIState.LaunchGroupActivityForRegistration(
|
||||||
|
database = openedDatabase,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.PASSKEY
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
Log.d(TAG, "Manual passkey registration in closed database")
|
||||||
|
_uiState.value =
|
||||||
|
UIState.LaunchFileDatabaseSelectActivityForRegistration(
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.PASSKEY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun autoRegisterPasskeyAndSetResult(
|
||||||
|
database: ContextualDatabase?,
|
||||||
|
nodeId: UUID,
|
||||||
|
passkey: Passkey
|
||||||
|
) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
mCreationParameters?.let { creationParameters ->
|
||||||
|
// To set the passkey to the database
|
||||||
|
// TODO Overwrite and Register in a predefined group
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
setResult(Intent())
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Log.e(TAG, "Unable to auto select passkey, usage parameters are empty")
|
||||||
|
cancelResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun manageRegistrationResult(activityResult: ActivityResult) {
|
||||||
|
val intent = activityResult.data
|
||||||
|
viewModelScope.launch(CoroutineExceptionHandler { _, e ->
|
||||||
|
Log.e(TAG, "Unable to create registration response for passkey", e)
|
||||||
|
if (e is SignatureNotFoundException) {
|
||||||
|
intent?.retrieveNodeId()?.let { nodeId ->
|
||||||
|
showAppSignatureDialog(e.temptingApp, nodeId)
|
||||||
|
} ?: cancelResult()
|
||||||
|
} else {
|
||||||
|
showError(e)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
// Build a new formatted response from the creation response
|
||||||
|
val responseIntent = Intent()
|
||||||
|
when (activityResult.resultCode) {
|
||||||
|
RESULT_OK -> {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
Log.d(TAG, "Passkey registration result")
|
||||||
|
val passkey = intent?.retrievePasskey()
|
||||||
|
intent?.removePasskey()
|
||||||
|
intent?.removeAppOrigin()
|
||||||
|
// If registered passkey is the same as the one we want to validate,
|
||||||
|
if (mPasskey == passkey) {
|
||||||
|
mCreationParameters?.let {
|
||||||
|
PendingIntentHandler.setCreateCredentialResponse(
|
||||||
|
intent = responseIntent,
|
||||||
|
response = buildCreatePublicKeyCredentialResponse(
|
||||||
|
publicKeyCredentialCreationParameters = it,
|
||||||
|
backupEligibility = mBackupEligibility,
|
||||||
|
backupState = mBackupState
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw SecurityException("Passkey was modified before registration")
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
setResult(responseIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RESULT_CANCELED -> {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
cancelResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class UIState {
|
||||||
|
object Loading : UIState()
|
||||||
|
data class ShowAppPrivilegedDialog(
|
||||||
|
val temptingApp: AndroidPrivilegedApp
|
||||||
|
): UIState()
|
||||||
|
data class ShowAppSignatureDialog(
|
||||||
|
val temptingApp: AppOrigin,
|
||||||
|
val nodeId: UUID
|
||||||
|
): UIState()
|
||||||
|
data class LaunchGroupActivityForSelection(
|
||||||
|
val database: ContextualDatabase
|
||||||
|
): UIState()
|
||||||
|
data class LaunchGroupActivityForRegistration(
|
||||||
|
val database: ContextualDatabase,
|
||||||
|
val registerInfo: RegisterInfo,
|
||||||
|
val typeMode: TypeMode
|
||||||
|
): UIState()
|
||||||
|
data class LaunchFileDatabaseSelectActivityForSelection(
|
||||||
|
val searchInfo: SearchInfo
|
||||||
|
): UIState()
|
||||||
|
data class LaunchFileDatabaseSelectActivityForRegistration(
|
||||||
|
val registerInfo: RegisterInfo,
|
||||||
|
val typeMode: TypeMode
|
||||||
|
): UIState()
|
||||||
|
data class SetActivityResult(
|
||||||
|
val lockDatabase: Boolean,
|
||||||
|
val resultCode: Int,
|
||||||
|
val data: Intent? = null
|
||||||
|
): UIState()
|
||||||
|
data class ShowError(
|
||||||
|
val error: Throwable
|
||||||
|
): UIState()
|
||||||
|
data class UpdateEntry(
|
||||||
|
val oldEntry: Entry,
|
||||||
|
val newEntry: Entry
|
||||||
|
): UIState()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = PasskeyLauncherViewModel::class.java.name
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -108,14 +108,19 @@ class DatabaseTaskProvider(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
// To show dialog only if context is an activity
|
// To show dialog only if context is an activity
|
||||||
private var activity: FragmentActivity? = try { context as? FragmentActivity? }
|
private var activity: FragmentActivity? = try {
|
||||||
catch (_: Exception) { null }
|
context as? FragmentActivity?
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
var onDatabaseRetrieved: ((database: ContextualDatabase?) -> Unit)? = null
|
var onDatabaseRetrieved: ((database: ContextualDatabase?) -> Unit)? = null
|
||||||
|
|
||||||
var onActionFinish: ((database: ContextualDatabase,
|
var onActionFinish: ((
|
||||||
actionTask: String,
|
database: ContextualDatabase,
|
||||||
result: ActionRunnable.Result) -> Unit)? = null
|
actionTask: String,
|
||||||
|
result: ActionRunnable.Result
|
||||||
|
) -> Unit)? = null
|
||||||
|
|
||||||
private var intentDatabaseTask: Intent = Intent(
|
private var intentDatabaseTask: Intent = Intent(
|
||||||
context.applicationContext,
|
context.applicationContext,
|
||||||
@@ -141,7 +146,7 @@ class DatabaseTaskProvider(
|
|||||||
this.databaseChangedDialogFragment = null
|
this.databaseChangedDialogFragment = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
|
private val actionTaskListener = object : DatabaseTaskNotificationService.ActionTaskListener {
|
||||||
override fun onActionStarted(
|
override fun onActionStarted(
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
progressMessage: ProgressMessage
|
progressMessage: ProgressMessage
|
||||||
@@ -175,13 +180,14 @@ class DatabaseTaskProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mActionDatabaseListener = object: DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
|
private val mActionDatabaseListener =
|
||||||
override fun validateDatabaseChanged() {
|
object : DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
|
||||||
mBinder?.getService()?.saveDatabaseInfo()
|
override fun validateDatabaseChanged() {
|
||||||
|
mBinder?.getService()?.saveDatabaseInfo()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private var databaseInfoListener = object:
|
private var databaseInfoListener = object :
|
||||||
DatabaseTaskNotificationService.DatabaseInfoListener {
|
DatabaseTaskNotificationService.DatabaseInfoListener {
|
||||||
override fun onDatabaseInfoChanged(
|
override fun onDatabaseInfoChanged(
|
||||||
previousDatabaseInfo: SnapFileDatabaseInfo,
|
previousDatabaseInfo: SnapFileDatabaseInfo,
|
||||||
@@ -214,7 +220,7 @@ class DatabaseTaskProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var databaseListener = object: DatabaseTaskNotificationService.DatabaseListener {
|
private var databaseListener = object : DatabaseTaskNotificationService.DatabaseListener {
|
||||||
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
onDatabaseRetrieved?.invoke(database)
|
onDatabaseRetrieved?.invoke(database)
|
||||||
}
|
}
|
||||||
@@ -265,12 +271,13 @@ class DatabaseTaskProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||||
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
|
mBinder =
|
||||||
addServiceListeners(this)
|
(serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
|
||||||
getService().checkDatabase()
|
addServiceListeners(this)
|
||||||
getService().checkDatabaseInfo()
|
getService().checkDatabase()
|
||||||
getService().checkAction()
|
getService().checkDatabaseInfo()
|
||||||
}
|
getService().checkAction()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceDisconnected(name: ComponentName?) {
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
@@ -296,7 +303,11 @@ class DatabaseTaskProvider(
|
|||||||
private fun bindService() {
|
private fun bindService() {
|
||||||
initServiceConnection()
|
initServiceConnection()
|
||||||
serviceConnection?.let {
|
serviceConnection?.let {
|
||||||
context.bindService(intentDatabaseTask, it, BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT)
|
context.bindService(
|
||||||
|
intentDatabaseTask,
|
||||||
|
it,
|
||||||
|
BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,6 +335,7 @@ class DatabaseTaskProvider(
|
|||||||
// Bind to the service when is starting
|
// Bind to the service when is starting
|
||||||
bindService()
|
bindService()
|
||||||
}
|
}
|
||||||
|
|
||||||
DATABASE_STOP_TASK_ACTION -> {
|
DATABASE_STOP_TASK_ACTION -> {
|
||||||
// Remove the progress task
|
// Remove the progress task
|
||||||
unBindService()
|
unBindService()
|
||||||
@@ -331,7 +343,8 @@ class DatabaseTaskProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ContextCompat.registerReceiver(context, databaseTaskBroadcastReceiver,
|
ContextCompat.registerReceiver(
|
||||||
|
context, databaseTaskBroadcastReceiver,
|
||||||
IntentFilter().apply {
|
IntentFilter().apply {
|
||||||
addAction(DATABASE_START_TASK_ACTION)
|
addAction(DATABASE_START_TASK_ACTION)
|
||||||
addAction(DATABASE_STOP_TASK_ACTION)
|
addAction(DATABASE_STOP_TASK_ACTION)
|
||||||
@@ -416,47 +429,51 @@ class DatabaseTaskProvider(
|
|||||||
----
|
----
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun startDatabaseCreate(databaseUri: Uri,
|
fun startDatabaseCreate(
|
||||||
mainCredential: MainCredential
|
databaseUri: Uri,
|
||||||
|
mainCredential: MainCredential
|
||||||
) {
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||||
}
|
}, ACTION_DATABASE_CREATE_TASK)
|
||||||
, ACTION_DATABASE_CREATE_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseLoad(databaseUri: Uri,
|
fun startDatabaseLoad(
|
||||||
mainCredential: MainCredential,
|
databaseUri: Uri,
|
||||||
readOnly: Boolean,
|
mainCredential: MainCredential,
|
||||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
readOnly: Boolean,
|
||||||
fixDuplicateUuid: Boolean) {
|
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||||
|
fixDuplicateUuid: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||||
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
|
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
|
||||||
putParcelable(DatabaseTaskNotificationService.CIPHER_DATABASE_KEY, cipherEncryptDatabase)
|
putParcelable(
|
||||||
|
DatabaseTaskNotificationService.CIPHER_DATABASE_KEY,
|
||||||
|
cipherEncryptDatabase
|
||||||
|
)
|
||||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||||
}
|
}, ACTION_DATABASE_LOAD_TASK)
|
||||||
, ACTION_DATABASE_LOAD_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseMerge(save: Boolean,
|
fun startDatabaseMerge(
|
||||||
fromDatabaseUri: Uri? = null,
|
save: Boolean,
|
||||||
mainCredential: MainCredential? = null) {
|
fromDatabaseUri: Uri? = null,
|
||||||
|
mainCredential: MainCredential? = null
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri)
|
||||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||||
}
|
}, ACTION_DATABASE_MERGE_TASK)
|
||||||
, ACTION_DATABASE_MERGE_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseReload(fixDuplicateUuid: Boolean) {
|
fun startDatabaseReload(fixDuplicateUuid: Boolean) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||||
}
|
}, ACTION_DATABASE_RELOAD_TASK)
|
||||||
, ACTION_DATABASE_RELOAD_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun askToStartDatabaseReload(conditionToAsk: Boolean, approved: () -> Unit) {
|
fun askToStartDatabaseReload(conditionToAsk: Boolean, approved: () -> Unit) {
|
||||||
@@ -472,15 +489,15 @@ class DatabaseTaskProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseAssignCredential(databaseUri: Uri,
|
fun startDatabaseAssignCredential(
|
||||||
mainCredential: MainCredential
|
databaseUri: Uri,
|
||||||
|
mainCredential: MainCredential
|
||||||
) {
|
) {
|
||||||
|
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||||
}
|
}, ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK)
|
||||||
, ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -489,54 +506,60 @@ class DatabaseTaskProvider(
|
|||||||
----
|
----
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun startDatabaseCreateGroup(newGroup: Group,
|
fun startDatabaseCreateGroup(
|
||||||
parent: Group,
|
newGroup: Group,
|
||||||
save: Boolean) {
|
parent: Group,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, newGroup)
|
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, newGroup)
|
||||||
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
|
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_CREATE_GROUP_TASK)
|
||||||
, ACTION_DATABASE_CREATE_GROUP_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseUpdateGroup(oldGroup: Group,
|
fun startDatabaseUpdateGroup(
|
||||||
groupToUpdate: Group,
|
oldGroup: Group,
|
||||||
save: Boolean) {
|
groupToUpdate: Group,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.GROUP_ID_KEY, oldGroup.nodeId)
|
putParcelable(DatabaseTaskNotificationService.GROUP_ID_KEY, oldGroup.nodeId)
|
||||||
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, groupToUpdate)
|
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, groupToUpdate)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_GROUP_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_GROUP_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseCreateEntry(newEntry: Entry,
|
fun startDatabaseCreateEntry(
|
||||||
parent: Group,
|
newEntry: Entry,
|
||||||
save: Boolean) {
|
parent: Group,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, newEntry)
|
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, newEntry)
|
||||||
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
|
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_CREATE_ENTRY_TASK)
|
||||||
, ACTION_DATABASE_CREATE_ENTRY_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseUpdateEntry(oldEntry: Entry,
|
fun startDatabaseUpdateEntry(
|
||||||
entryToUpdate: Entry,
|
oldEntry: Entry,
|
||||||
save: Boolean) {
|
entryToUpdate: Entry,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, oldEntry.nodeId)
|
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, oldEntry.nodeId)
|
||||||
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, entryToUpdate)
|
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, entryToUpdate)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_ENTRY_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_ENTRY_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startDatabaseActionListNodes(actionTask: String,
|
private fun startDatabaseActionListNodes(
|
||||||
nodesPaste: List<Node>,
|
actionTask: String,
|
||||||
newParent: Group?,
|
nodesPaste: List<Node>,
|
||||||
save: Boolean) {
|
newParent: Group?,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
val groupsIdToCopy = ArrayList<NodeId<*>>()
|
val groupsIdToCopy = ArrayList<NodeId<*>>()
|
||||||
val entriesIdToCopy = ArrayList<NodeId<UUID>>()
|
val entriesIdToCopy = ArrayList<NodeId<UUID>>()
|
||||||
nodesPaste.forEach { nodeVersioned ->
|
nodesPaste.forEach { nodeVersioned ->
|
||||||
@@ -544,6 +567,7 @@ class DatabaseTaskProvider(
|
|||||||
Type.GROUP -> {
|
Type.GROUP -> {
|
||||||
groupsIdToCopy.add((nodeVersioned as Group).nodeId)
|
groupsIdToCopy.add((nodeVersioned as Group).nodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
Type.ENTRY -> {
|
Type.ENTRY -> {
|
||||||
entriesIdToCopy.add((nodeVersioned as Entry).nodeId)
|
entriesIdToCopy.add((nodeVersioned as Entry).nodeId)
|
||||||
}
|
}
|
||||||
@@ -558,24 +582,29 @@ class DatabaseTaskProvider(
|
|||||||
if (newParentId != null)
|
if (newParentId != null)
|
||||||
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, newParentId)
|
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, newParentId)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, actionTask)
|
||||||
, actionTask)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseCopyNodes(nodesToCopy: List<Node>,
|
fun startDatabaseCopyNodes(
|
||||||
newParent: Group,
|
nodesToCopy: List<Node>,
|
||||||
save: Boolean) {
|
newParent: Group,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
startDatabaseActionListNodes(ACTION_DATABASE_COPY_NODES_TASK, nodesToCopy, newParent, save)
|
startDatabaseActionListNodes(ACTION_DATABASE_COPY_NODES_TASK, nodesToCopy, newParent, save)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseMoveNodes(nodesToMove: List<Node>,
|
fun startDatabaseMoveNodes(
|
||||||
newParent: Group,
|
nodesToMove: List<Node>,
|
||||||
save: Boolean) {
|
newParent: Group,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
startDatabaseActionListNodes(ACTION_DATABASE_MOVE_NODES_TASK, nodesToMove, newParent, save)
|
startDatabaseActionListNodes(ACTION_DATABASE_MOVE_NODES_TASK, nodesToMove, newParent, save)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseDeleteNodes(nodesToDelete: List<Node>,
|
fun startDatabaseDeleteNodes(
|
||||||
save: Boolean) {
|
nodesToDelete: List<Node>,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
startDatabaseActionListNodes(ACTION_DATABASE_DELETE_NODES_TASK, nodesToDelete, null, save)
|
startDatabaseActionListNodes(ACTION_DATABASE_DELETE_NODES_TASK, nodesToDelete, null, save)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,26 +614,28 @@ class DatabaseTaskProvider(
|
|||||||
-----------------
|
-----------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun startDatabaseRestoreEntryHistory(mainEntryId: NodeId<UUID>,
|
fun startDatabaseRestoreEntryHistory(
|
||||||
entryHistoryPosition: Int,
|
mainEntryId: NodeId<UUID>,
|
||||||
save: Boolean) {
|
entryHistoryPosition: Int,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
|
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
|
||||||
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
|
||||||
, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseDeleteEntryHistory(mainEntryId: NodeId<UUID>,
|
fun startDatabaseDeleteEntryHistory(
|
||||||
entryHistoryPosition: Int,
|
mainEntryId: NodeId<UUID>,
|
||||||
save: Boolean) {
|
entryHistoryPosition: Int,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
|
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
|
||||||
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_DELETE_ENTRY_HISTORY)
|
||||||
, ACTION_DATABASE_DELETE_ENTRY_HISTORY)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -613,110 +644,118 @@ class DatabaseTaskProvider(
|
|||||||
-----------------
|
-----------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun startDatabaseSaveName(oldName: String,
|
fun startDatabaseSaveName(
|
||||||
newName: String,
|
oldName: String,
|
||||||
save: Boolean) {
|
newName: String,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldName)
|
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldName)
|
||||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newName)
|
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newName)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_NAME_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_NAME_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveDescription(oldDescription: String,
|
fun startDatabaseSaveDescription(
|
||||||
newDescription: String,
|
oldDescription: String,
|
||||||
save: Boolean) {
|
newDescription: String,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDescription)
|
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDescription)
|
||||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDescription)
|
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDescription)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_DESCRIPTION_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_DESCRIPTION_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveDefaultUsername(oldDefaultUsername: String,
|
fun startDatabaseSaveDefaultUsername(
|
||||||
newDefaultUsername: String,
|
oldDefaultUsername: String,
|
||||||
save: Boolean) {
|
newDefaultUsername: String,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDefaultUsername)
|
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDefaultUsername)
|
||||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDefaultUsername)
|
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDefaultUsername)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveColor(oldColor: String,
|
fun startDatabaseSaveColor(
|
||||||
newColor: String,
|
oldColor: String,
|
||||||
save: Boolean) {
|
newColor: String,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldColor)
|
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldColor)
|
||||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newColor)
|
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newColor)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_COLOR_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_COLOR_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveCompression(oldCompression: CompressionAlgorithm,
|
fun startDatabaseSaveCompression(
|
||||||
newCompression: CompressionAlgorithm,
|
oldCompression: CompressionAlgorithm,
|
||||||
save: Boolean) {
|
newCompression: CompressionAlgorithm,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldCompression)
|
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldCompression)
|
||||||
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newCompression)
|
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newCompression)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseRemoveUnlinkedData(save: Boolean) {
|
fun startDatabaseRemoveUnlinkedData(save: Boolean) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
|
||||||
, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveRecycleBin(oldRecycleBin: Group?,
|
fun startDatabaseSaveRecycleBin(
|
||||||
newRecycleBin: Group?,
|
oldRecycleBin: Group?,
|
||||||
save: Boolean) {
|
newRecycleBin: Group?,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldRecycleBin)
|
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldRecycleBin)
|
||||||
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newRecycleBin)
|
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newRecycleBin)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveTemplatesGroup(oldTemplatesGroup: Group?,
|
fun startDatabaseSaveTemplatesGroup(
|
||||||
newTemplatesGroup: Group?,
|
oldTemplatesGroup: Group?,
|
||||||
save: Boolean) {
|
newTemplatesGroup: Group?,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldTemplatesGroup)
|
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldTemplatesGroup)
|
||||||
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newTemplatesGroup)
|
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newTemplatesGroup)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
|
fun startDatabaseSaveMaxHistoryItems(
|
||||||
newMaxHistoryItems: Int,
|
oldMaxHistoryItems: Int,
|
||||||
save: Boolean) {
|
newMaxHistoryItems: Int,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putInt(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistoryItems)
|
putInt(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistoryItems)
|
||||||
putInt(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistoryItems)
|
putInt(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistoryItems)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveMaxHistorySize(oldMaxHistorySize: Long,
|
fun startDatabaseSaveMaxHistorySize(
|
||||||
newMaxHistorySize: Long,
|
oldMaxHistorySize: Long,
|
||||||
save: Boolean) {
|
newMaxHistorySize: Long,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistorySize)
|
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistorySize)
|
||||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistorySize)
|
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistorySize)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -725,59 +764,64 @@ class DatabaseTaskProvider(
|
|||||||
-------------------
|
-------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun startDatabaseSaveEncryption(oldEncryption: EncryptionAlgorithm,
|
fun startDatabaseSaveEncryption(
|
||||||
newEncryption: EncryptionAlgorithm,
|
oldEncryption: EncryptionAlgorithm,
|
||||||
save: Boolean) {
|
newEncryption: EncryptionAlgorithm,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldEncryption)
|
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldEncryption)
|
||||||
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newEncryption)
|
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newEncryption)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_ENCRYPTION_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_ENCRYPTION_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveKeyDerivation(oldKeyDerivation: KdfEngine,
|
fun startDatabaseSaveKeyDerivation(
|
||||||
newKeyDerivation: KdfEngine,
|
oldKeyDerivation: KdfEngine,
|
||||||
save: Boolean) {
|
newKeyDerivation: KdfEngine,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldKeyDerivation)
|
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldKeyDerivation)
|
||||||
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newKeyDerivation)
|
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newKeyDerivation)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveIterations(oldIterations: Long,
|
fun startDatabaseSaveIterations(
|
||||||
newIterations: Long,
|
oldIterations: Long,
|
||||||
save: Boolean) {
|
newIterations: Long,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldIterations)
|
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldIterations)
|
||||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newIterations)
|
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newIterations)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_ITERATIONS_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_ITERATIONS_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveMemoryUsage(oldMemoryUsage: Long,
|
fun startDatabaseSaveMemoryUsage(
|
||||||
newMemoryUsage: Long,
|
oldMemoryUsage: Long,
|
||||||
save: Boolean) {
|
newMemoryUsage: Long,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMemoryUsage)
|
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMemoryUsage)
|
||||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMemoryUsage)
|
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMemoryUsage)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveParallelism(oldParallelism: Long,
|
fun startDatabaseSaveParallelism(
|
||||||
newParallelism: Long,
|
oldParallelism: Long,
|
||||||
save: Boolean) {
|
newParallelism: Long,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldParallelism)
|
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldParallelism)
|
||||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newParallelism)
|
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newParallelism)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_PARALLELISM_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_PARALLELISM_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -787,15 +831,13 @@ class DatabaseTaskProvider(
|
|||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, saveToUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, saveToUri)
|
||||||
}
|
}, ACTION_DATABASE_SAVE)
|
||||||
, ACTION_DATABASE_SAVE)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startChallengeResponded(response: ByteArray?) {
|
fun startChallengeResponded(response: ByteArray?) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putByteArray(DatabaseTaskNotificationService.DATA_BYTES, response)
|
putByteArray(DatabaseTaskNotificationService.DATA_BYTES, response)
|
||||||
}
|
}, ACTION_CHALLENGE_RESPONDED)
|
||||||
, ACTION_CHALLENGE_RESPONDED)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -24,14 +24,42 @@ import com.kunzisoft.keepass.R
|
|||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.template.TemplateEngine
|
import com.kunzisoft.keepass.database.element.template.TemplateEngine
|
||||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
import com.kunzisoft.keepass.database.exception.*
|
import com.kunzisoft.keepass.database.exception.CopyEntryDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.CopyGroupDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.CorruptedDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.DatabaseInputException
|
||||||
|
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||||
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.EmptyKeyDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.HardwareKeyDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.InvalidAlgorithmDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.InvalidCredentialsDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.KDFMemoryDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.LocalizedException
|
||||||
|
import com.kunzisoft.keepass.database.exception.MergeDatabaseKDBException
|
||||||
|
import com.kunzisoft.keepass.database.exception.MoveEntryDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.NoMemoryDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.SignatureDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
|
||||||
|
import com.kunzisoft.keepass.database.exception.VersionDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.XMLMalformedDatabaseException
|
||||||
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_CREDENTIAL_ID
|
||||||
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_PRIVATE_KEY
|
||||||
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_RELYING_PARTY
|
||||||
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_USERNAME
|
||||||
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_USER_HANDLE
|
||||||
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.PASSKEY_FIELD
|
||||||
|
|
||||||
fun DatabaseException.getLocalizedMessage(resources: Resources): String? =
|
fun LocalizedException.getLocalizedMessage(resources: Resources): String? =
|
||||||
when (this) {
|
when (this) {
|
||||||
is FileNotFoundDatabaseException -> resources.getString(R.string.file_not_found_content)
|
is FileNotFoundDatabaseException -> resources.getString(R.string.file_not_found_content)
|
||||||
is CorruptedDatabaseException -> resources.getString(R.string.corrupted_file)
|
is CorruptedDatabaseException -> resources.getString(R.string.corrupted_file)
|
||||||
is InvalidAlgorithmDatabaseException -> resources.getString(R.string.invalid_algorithm)
|
is InvalidAlgorithmDatabaseException -> resources.getString(R.string.invalid_algorithm)
|
||||||
is UnknownDatabaseLocationException -> resources.getString(R.string.error_location_unknown)
|
is UnknownDatabaseLocationException -> resources.getString(R.string.error_location_unknown)
|
||||||
|
is RegisterInReadOnlyDatabaseException -> resources.getString(R.string.error_save_read_only)
|
||||||
is HardwareKeyDatabaseException -> resources.getString(R.string.error_hardware_key_unsupported)
|
is HardwareKeyDatabaseException -> resources.getString(R.string.error_hardware_key_unsupported)
|
||||||
is EmptyKeyDatabaseException -> resources.getString(R.string.error_empty_key)
|
is EmptyKeyDatabaseException -> resources.getString(R.string.error_empty_key)
|
||||||
is SignatureDatabaseException -> resources.getString(R.string.invalid_db_sig)
|
is SignatureDatabaseException -> resources.getString(R.string.invalid_db_sig)
|
||||||
@@ -63,6 +91,11 @@ fun TemplateField.isStandardPasswordName(context: Context, name: String): Boolea
|
|||||||
|| name == getLocalizedName(context, LABEL_PASSWORD)
|
|| name == getLocalizedName(context, LABEL_PASSWORD)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun TemplateField.isPasskeyLabel(context: Context, name: String): Boolean {
|
||||||
|
return name.equals(PASSKEY_FIELD, true)
|
||||||
|
|| name == getLocalizedName(context, PASSKEY_FIELD)
|
||||||
|
}
|
||||||
|
|
||||||
fun TemplateField.getLocalizedName(context: Context?, name: String): String {
|
fun TemplateField.getLocalizedName(context: Context?, name: String): String {
|
||||||
if (context == null
|
if (context == null
|
||||||
|| TemplateEngine.containsTemplateDecorator(name)
|
|| TemplateEngine.containsTemplateDecorator(name)
|
||||||
@@ -107,6 +140,13 @@ fun TemplateField.getLocalizedName(context: Context?, name: String): String {
|
|||||||
LABEL_SECURE_NOTE.equals(name, true) -> context.getString(R.string.secure_note)
|
LABEL_SECURE_NOTE.equals(name, true) -> context.getString(R.string.secure_note)
|
||||||
LABEL_MEMBERSHIP.equals(name, true) -> context.getString(R.string.membership)
|
LABEL_MEMBERSHIP.equals(name, true) -> context.getString(R.string.membership)
|
||||||
|
|
||||||
|
PASSKEY_FIELD.equals(name, true) -> context.getString(R.string.passkey)
|
||||||
|
FIELD_USERNAME.equals(name, true) -> context.getString(R.string.passkey_username)
|
||||||
|
FIELD_PRIVATE_KEY.equals(name, true) -> context.getString(R.string.passkey_private_key)
|
||||||
|
FIELD_CREDENTIAL_ID.equals(name, true) -> context.getString(R.string.passkey_credential_id)
|
||||||
|
FIELD_USER_HANDLE.equals(name, true) -> context.getString(R.string.passkey_user_handle)
|
||||||
|
FIELD_RELYING_PARTY.equals(name, true) -> context.getString(R.string.passkey_relying_party)
|
||||||
|
|
||||||
else -> name
|
else -> name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,13 +43,15 @@ object SearchHelper {
|
|||||||
/**
|
/**
|
||||||
* Utility method to perform actions if item is found or not after an auto search in [database]
|
* Utility method to perform actions if item is found or not after an auto search in [database]
|
||||||
*/
|
*/
|
||||||
fun checkAutoSearchInfo(context: Context,
|
fun checkAutoSearchInfo(
|
||||||
database: ContextualDatabase?,
|
context: Context,
|
||||||
searchInfo: SearchInfo?,
|
database: ContextualDatabase?,
|
||||||
onItemsFound: (openedDatabase: ContextualDatabase,
|
searchInfo: SearchInfo?,
|
||||||
items: List<EntryInfo>) -> Unit,
|
onItemsFound: (openedDatabase: ContextualDatabase,
|
||||||
onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
|
items: List<EntryInfo>) -> Unit,
|
||||||
onDatabaseClosed: () -> Unit) {
|
onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
|
||||||
|
onDatabaseClosed: () -> Unit
|
||||||
|
) {
|
||||||
if (database == null || !database.loaded) {
|
if (database == null || !database.loaded) {
|
||||||
onDatabaseClosed.invoke()
|
onDatabaseClosed.invoke()
|
||||||
} else if (TimeoutHelper.checkTime(context)) {
|
} else if (TimeoutHelper.checkTime(context)) {
|
||||||
@@ -59,8 +61,7 @@ object SearchHelper {
|
|||||||
&& !searchInfo.containsOnlyNullValues()) {
|
&& !searchInfo.containsOnlyNullValues()) {
|
||||||
// If search provide results
|
// If search provide results
|
||||||
database.createVirtualGroupFromSearchInfo(
|
database.createVirtualGroupFromSearchInfo(
|
||||||
searchInfo.toString(),
|
searchInfo,
|
||||||
searchInfo.isASearchByDomain(),
|
|
||||||
MAX_SEARCH_ENTRY
|
MAX_SEARCH_ENTRY
|
||||||
)?.let { searchGroup ->
|
)?.let { searchGroup ->
|
||||||
if (searchGroup.numberOfChildEntries > 0) {
|
if (searchGroup.numberOfChildEntries > 0) {
|
||||||
|
|||||||
@@ -89,8 +89,8 @@ class PasswordActivityEducation(activity: Activity)
|
|||||||
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
|
onOuterViewClick: ((TapTargetView?) -> Unit)? = null): Boolean {
|
||||||
return checkAndPerformedEducation(isEducationBiometricPerformed(activity),
|
return checkAndPerformedEducation(isEducationBiometricPerformed(activity),
|
||||||
TapTarget.forView(educationView,
|
TapTarget.forView(educationView,
|
||||||
activity.getString(R.string.education_advanced_unlock_title),
|
activity.getString(R.string.education_device_unlock_title),
|
||||||
activity.getString(R.string.education_advanced_unlock_summary))
|
activity.getString(R.string.education_device_unlock_summary))
|
||||||
.outerCircleColorInt(getCircleColor())
|
.outerCircleColorInt(getCircleColor())
|
||||||
.outerCircleAlpha(getCircleAlpha())
|
.outerCircleAlpha(getCircleAlpha())
|
||||||
.icon(ContextCompat.getDrawable(activity, R.drawable.ic_fingerprint_24dp))
|
.icon(ContextCompat.getDrawable(activity, R.drawable.ic_fingerprint_24dp))
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import androidx.appcompat.app.AlertDialog
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.openExternalApp
|
import com.kunzisoft.keepass.utils.AppUtil.openExternalApp
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Special activity to deal with hardware key drivers,
|
* Special activity to deal with hardware key drivers,
|
||||||
|
|||||||
@@ -1,144 +0,0 @@
|
|||||||
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.utils.UriUtil.openExternalApp
|
|
||||||
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) { _, _ ->
|
|
||||||
activity.openExternalApp(activity.getString(R.string.key_driver_app_id))
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
|
||||||
builder.create().show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
package com.kunzisoft.keepass.receivers
|
package com.kunzisoft.keepass.receivers
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
|
||||||
import com.kunzisoft.keepass.utils.DexUtil
|
|
||||||
import com.kunzisoft.keepass.utils.MagikeyboardUtil
|
import com.kunzisoft.keepass.utils.MagikeyboardUtil
|
||||||
|
|
||||||
class DexModeReceiver : BroadcastReceiver() {
|
class DexModeReceiver : BroadcastReceiver() {
|
||||||
|
|||||||
@@ -274,10 +274,12 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
val containsPasswordToCopy = entry.password.isNotEmpty()
|
val containsPasswordToCopy = entry.password.isNotEmpty()
|
||||||
&& PreferencesUtil.allowCopyProtectedFields(context)
|
&& PreferencesUtil.allowCopyProtectedFields(context)
|
||||||
val containsOTPToCopy = entry.containsCustomField(OTP_TOKEN_FIELD)
|
val containsOTPToCopy = entry.containsCustomField(OTP_TOKEN_FIELD)
|
||||||
val containsExtraFieldToCopy = entry.customFields.isNotEmpty()
|
val customFields = entry.getCustomFieldsForFilling()
|
||||||
&& (entry.containsCustomFieldsNotProtected()
|
val containsExtraFieldToCopy = customFields.isNotEmpty()
|
||||||
|
&& (customFields.any { !it.protectedValue.isProtected }
|
||||||
||
|
||
|
||||||
(entry.containsCustomFieldsProtected() && PreferencesUtil.allowCopyProtectedFields(context))
|
(customFields.any { it.protectedValue.isProtected }
|
||||||
|
&& PreferencesUtil.allowCopyProtectedFields(context))
|
||||||
)
|
)
|
||||||
|
|
||||||
var startService = false
|
var startService = false
|
||||||
@@ -320,7 +322,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
if (containsExtraFieldToCopy) {
|
if (containsExtraFieldToCopy) {
|
||||||
try {
|
try {
|
||||||
var anonymousFieldNumber = 0
|
var anonymousFieldNumber = 0
|
||||||
entry.customFields.forEach { field ->
|
entry.getCustomFieldsForFilling().forEach { field ->
|
||||||
//If value is not protected or allowed
|
//If value is not protected or allowed
|
||||||
if ((!field.protectedValue.isProtected
|
if ((!field.protectedValue.isProtected
|
||||||
|| PreferencesUtil.allowCopyProtectedFields(context))
|
|| PreferencesUtil.allowCopyProtectedFields(context))
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
Log.i(TAG, "Database file modified " +
|
Log.i(TAG, "Database file modified " +
|
||||||
"$previousDatabaseInfo != $lastFileDatabaseInfo ")
|
"$previousDatabaseInfo != $lastFileDatabaseInfo ")
|
||||||
// Call listener to indicate a change in database info
|
// Call listener to indicate a change in database info
|
||||||
if (!mSaveState && previousDatabaseInfo != null) {
|
if (!mSaveState) {
|
||||||
mDatabaseInfoListeners.forEach { listener ->
|
mDatabaseInfoListeners.forEach { listener ->
|
||||||
listener.onDatabaseInfoChanged(
|
listener.onDatabaseInfoChanged(
|
||||||
previousDatabaseInfo,
|
previousDatabaseInfo,
|
||||||
@@ -1385,6 +1385,15 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
|
|||||||
return nodesAction
|
return nodesAction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Bundle.getNewEntry(database: ContextualDatabase): Entry? {
|
||||||
|
getBundle(NEW_NODES_KEY)
|
||||||
|
?.getParcelableList<NodeId<UUID>>(ENTRIES_ID_KEY)
|
||||||
|
?.get(0)?.let {
|
||||||
|
return database.getEntryById(it)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
fun getBundleFromListNodes(nodes: List<Node>): Bundle {
|
fun getBundleFromListNodes(nodes: List<Node>): Bundle {
|
||||||
val groupsId = mutableListOf<NodeId<*>>()
|
val groupsId = mutableListOf<NodeId<*>>()
|
||||||
val entriesId = mutableListOf<NodeId<UUID>>()
|
val entriesId = mutableListOf<NodeId<UUID>>()
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ import com.kunzisoft.keepass.app.database.CipherDatabaseEntity
|
|||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
|
||||||
class AdvancedUnlockNotificationService : NotificationService() {
|
class DeviceUnlockNotificationService : NotificationService() {
|
||||||
|
|
||||||
private lateinit var mTempCipherDao: ArrayList<CipherDatabaseEntity>
|
private lateinit var mTempCipherDao: ArrayList<CipherDatabaseEntity>
|
||||||
|
|
||||||
private var mActionTaskBinder = AdvancedUnlockBinder()
|
private var mActionTaskBinder = DeviceUnlockBinder()
|
||||||
|
|
||||||
inner class AdvancedUnlockBinder: Binder() {
|
inner class DeviceUnlockBinder: Binder() {
|
||||||
fun getCipherDatabase(databaseUri: Uri): CipherDatabaseEntity? {
|
fun getCipherDatabase(databaseUri: Uri): CipherDatabaseEntity? {
|
||||||
return mTempCipherDao.firstOrNull { it.databaseUri == databaseUri.toString()}
|
return mTempCipherDao.firstOrNull { it.databaseUri == databaseUri.toString()}
|
||||||
}
|
}
|
||||||
@@ -48,11 +48,11 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
|||||||
override val notificationId: Int = 593
|
override val notificationId: Int = 593
|
||||||
|
|
||||||
override fun retrieveChannelId(): String {
|
override fun retrieveChannelId(): String {
|
||||||
return CHANNEL_ADVANCED_UNLOCK_ID
|
return CHANNEL_DEVICE_UNLOCK_ID
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun retrieveChannelName(): String {
|
override fun retrieveChannelName(): String {
|
||||||
return getString(R.string.advanced_unlock)
|
return getString(R.string.device_unlock)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@@ -60,7 +60,7 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
|||||||
mTempCipherDao = ArrayList()
|
mTempCipherDao = ArrayList()
|
||||||
}
|
}
|
||||||
|
|
||||||
// It's simpler to use pendingIntent to perform REMOVE_ADVANCED_UNLOCK_KEY_ACTION
|
// It's simpler to use pendingIntent to perform REMOVE_DEVICE_UNLOCK_KEY_ACTION
|
||||||
// because can be directly broadcast to another module or app
|
// because can be directly broadcast to another module or app
|
||||||
@SuppressLint("LaunchActivityFromNotification")
|
@SuppressLint("LaunchActivityFromNotification")
|
||||||
override fun onBind(intent: Intent): IBinder {
|
override fun onBind(intent: Intent): IBinder {
|
||||||
@@ -68,7 +68,7 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
|||||||
|
|
||||||
val pendingDeleteIntent = PendingIntent.getBroadcast(this,
|
val pendingDeleteIntent = PendingIntent.getBroadcast(this,
|
||||||
4577,
|
4577,
|
||||||
Intent(REMOVE_ADVANCED_UNLOCK_KEY_ACTION),
|
Intent(REMOVE_DEVICE_UNLOCK_KEY_ACTION),
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
} else {
|
} else {
|
||||||
@@ -81,28 +81,28 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
|||||||
} else {
|
} else {
|
||||||
R.drawable.notification_ic_device_unlock_24dp
|
R.drawable.notification_ic_device_unlock_24dp
|
||||||
})
|
})
|
||||||
setContentTitle(getString(R.string.advanced_unlock))
|
setContentTitle(getString(R.string.device_unlock))
|
||||||
setContentText(getString(R.string.advanced_unlock_tap_delete))
|
setContentText(getString(R.string.device_unlock_tap_delete))
|
||||||
setContentIntent(pendingDeleteIntent)
|
setContentIntent(pendingDeleteIntent)
|
||||||
// Unfortunately swipe is disabled in lollipop+
|
// Unfortunately swipe is disabled in lollipop+
|
||||||
setDeleteIntent(pendingDeleteIntent)
|
setDeleteIntent(pendingDeleteIntent)
|
||||||
}
|
}
|
||||||
|
|
||||||
val notificationTimeoutMilliSecs = PreferencesUtil.getAdvancedUnlockTimeout(this)
|
val notificationTimeoutMilliSecs = PreferencesUtil.getDeviceUnlockTimeout(this)
|
||||||
// Not necessarily a foreground service
|
// Not necessarily a foreground service
|
||||||
if (mTimerJob == null && notificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
|
if (mTimerJob == null && notificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
|
||||||
defineTimerJob(
|
defineTimerJob(
|
||||||
notificationBuilder,
|
notificationBuilder,
|
||||||
NotificationServiceType.ADVANCED_UNLOCK,
|
NotificationServiceType.DEVICE_UNLOCK,
|
||||||
notificationTimeoutMilliSecs
|
notificationTimeoutMilliSecs
|
||||||
) {
|
) {
|
||||||
sendBroadcast(Intent(REMOVE_ADVANCED_UNLOCK_KEY_ACTION))
|
sendBroadcast(Intent(REMOVE_DEVICE_UNLOCK_KEY_ACTION))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
startForegroundCompat(
|
startForegroundCompat(
|
||||||
notificationId,
|
notificationId,
|
||||||
notificationBuilder,
|
notificationBuilder,
|
||||||
NotificationServiceType.ADVANCED_UNLOCK
|
NotificationServiceType.DEVICE_UNLOCK
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,11 +119,11 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
class AdvancedUnlockReceiver(var removeKeyAction: () -> Unit): BroadcastReceiver() {
|
class DeviceUnlockReceiver(var removeKeyAction: () -> Unit): BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
intent.action?.let {
|
intent.action?.let {
|
||||||
when (it) {
|
when (it) {
|
||||||
REMOVE_ADVANCED_UNLOCK_KEY_ACTION -> {
|
REMOVE_DEVICE_UNLOCK_KEY_ACTION -> {
|
||||||
removeKeyAction.invoke()
|
removeKeyAction.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,13 +132,13 @@ class AdvancedUnlockNotificationService : NotificationService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val CHANNEL_ADVANCED_UNLOCK_ID = "com.kunzisoft.keepass.notification.channel.unlock"
|
private const val CHANNEL_DEVICE_UNLOCK_ID = "com.kunzisoft.keepass.notification.channel.unlock"
|
||||||
const val REMOVE_ADVANCED_UNLOCK_KEY_ACTION = "com.kunzisoft.keepass.REMOVE_ADVANCED_UNLOCK_KEY"
|
const val REMOVE_DEVICE_UNLOCK_KEY_ACTION = "com.kunzisoft.keepass.REMOVE_DEVICE_UNLOCK_KEY"
|
||||||
|
|
||||||
// Only one service connection
|
// Only one service connection
|
||||||
fun bindService(context: Context, serviceConnection: ServiceConnection, flags: Int) {
|
fun bindService(context: Context, serviceConnection: ServiceConnection, flags: Int) {
|
||||||
context.bindService(Intent(context,
|
context.bindService(Intent(context,
|
||||||
AdvancedUnlockNotificationService::class.java),
|
DeviceUnlockNotificationService::class.java),
|
||||||
serviceConnection,
|
serviceConnection,
|
||||||
flags)
|
flags)
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ import android.util.Log
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE
|
|||||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import android.util.Log
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
@@ -105,7 +106,7 @@ abstract class NotificationService : Service() {
|
|||||||
NotificationServiceType.ATTACHMENT -> FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
NotificationServiceType.ATTACHMENT -> FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||||
NotificationServiceType.CLIPBOARD -> foregroundServiceTimer
|
NotificationServiceType.CLIPBOARD -> foregroundServiceTimer
|
||||||
NotificationServiceType.KEYBOARD -> foregroundServiceTimer
|
NotificationServiceType.KEYBOARD -> foregroundServiceTimer
|
||||||
NotificationServiceType.ADVANCED_UNLOCK -> foregroundServiceTimer
|
NotificationServiceType.DEVICE_UNLOCK -> foregroundServiceTimer
|
||||||
}
|
}
|
||||||
startForeground(notificationId, builder.build(), foregroundType)
|
startForeground(notificationId, builder.build(), foregroundType)
|
||||||
} else {
|
} else {
|
||||||
@@ -156,11 +157,21 @@ abstract class NotificationService : Service() {
|
|||||||
mReset = true
|
mReset = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onTimeout(startId: Int, fgsType: Int) {
|
||||||
|
super.onTimeout(startId, fgsType)
|
||||||
|
Log.e(javaClass::class.simpleName, "The service took too long to execute")
|
||||||
|
cancelNotification()
|
||||||
|
stopSelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun cancelNotification() {
|
||||||
mTimerJob?.cancel()
|
mTimerJob?.cancel()
|
||||||
mTimerJob = null
|
mTimerJob = null
|
||||||
notificationManager?.cancel(notificationId)
|
notificationManager?.cancel(notificationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
cancelNotification()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ enum class NotificationServiceType {
|
|||||||
ATTACHMENT,
|
ATTACHMENT,
|
||||||
CLIPBOARD,
|
CLIPBOARD,
|
||||||
KEYBOARD,
|
KEYBOARD,
|
||||||
ADVANCED_UNLOCK
|
DEVICE_UNLOCK
|
||||||
}
|
}
|
||||||
@@ -19,9 +19,12 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.settings
|
package com.kunzisoft.keepass.settings
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
class AutofillSettingsActivity : ExternalSettingsActivity() {
|
class AutofillSettingsActivity : ExternalSettingsActivity() {
|
||||||
|
|
||||||
override fun retrieveTitle(): Int {
|
override fun retrieveTitle(): Int {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package com.kunzisoft.keepass.settings
|
|||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
@@ -29,6 +30,7 @@ import com.kunzisoft.keepass.R
|
|||||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistAppIdPreferenceDialogFragmentCompat
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistAppIdPreferenceDialogFragmentCompat
|
||||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistWebDomainPreferenceDialogFragmentCompat
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.AutofillBlocklistWebDomainPreferenceDialogFragmentCompat
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
class AutofillSettingsFragment : PreferenceFragmentCompat() {
|
class AutofillSettingsFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
@@ -42,8 +44,6 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDisplayPreferenceDialog(preference: Preference) {
|
override fun onDisplayPreferenceDialog(preference: Preference) {
|
||||||
var otherDialogFragment = false
|
|
||||||
|
|
||||||
var dialogFragment: DialogFragment? = null
|
var dialogFragment: DialogFragment? = null
|
||||||
|
|
||||||
when (preference.key) {
|
when (preference.key) {
|
||||||
@@ -53,7 +53,7 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
|
|||||||
getString(R.string.autofill_web_domain_blocklist_key) -> {
|
getString(R.string.autofill_web_domain_blocklist_key) -> {
|
||||||
dialogFragment = AutofillBlocklistWebDomainPreferenceDialogFragmentCompat.newInstance(preference.key)
|
dialogFragment = AutofillBlocklistWebDomainPreferenceDialogFragmentCompat.newInstance(preference.key)
|
||||||
}
|
}
|
||||||
else -> otherDialogFragment = true
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dialogFragment != null) {
|
if (dialogFragment != null) {
|
||||||
@@ -62,7 +62,7 @@ class AutofillSettingsFragment : PreferenceFragmentCompat() {
|
|||||||
dialogFragment.show(parentFragmentManager, TAG_AUTOFILL_PREF_FRAGMENT)
|
dialogFragment.show(parentFragmentManager, TAG_AUTOFILL_PREF_FRAGMENT)
|
||||||
}
|
}
|
||||||
// Could not be handled here. Try with the super method.
|
// Could not be handled here. Try with the super method.
|
||||||
else if (otherDialogFragment) {
|
else {
|
||||||
super.onDisplayPreferenceDialog(preference)
|
super.onDisplayPreferenceDialog(preference)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,15 +23,15 @@ import android.os.Bundle
|
|||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
|
||||||
|
|
||||||
class AdvancedUnlockSettingsActivity : SettingsActivity() {
|
class DeviceUnlockSettingsActivity : SettingsActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
mTimeoutEnable = false
|
mTimeoutEnable = false
|
||||||
setTitle(NestedSettingsFragment.Screen.ADVANCED_UNLOCK)
|
setTitle(NestedSettingsFragment.Screen.DEVICE_UNLOCK)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun retrieveMainFragment(): Fragment {
|
override fun retrieveMainFragment(): Fragment {
|
||||||
return NestedSettingsFragment.newInstance(NestedSettingsFragment.Screen.ADVANCED_UNLOCK)
|
return NestedSettingsFragment.newInstance(NestedSettingsFragment.Screen.DEVICE_UNLOCK)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,9 +84,9 @@ class MainPreferenceFragment : PreferenceFragmentCompat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
findPreference<Preference>(getString(R.string.settings_advanced_unlock_key))?.apply {
|
findPreference<Preference>(getString(R.string.settings_device_unlock_key))?.apply {
|
||||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||||
mCallback?.onNestedPreferenceSelected(NestedSettingsFragment.Screen.ADVANCED_UNLOCK)
|
mCallback?.onNestedPreferenceSelected(NestedSettingsFragment.Screen.DEVICE_UNLOCK)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ package com.kunzisoft.keepass.settings
|
|||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
@@ -30,6 +29,7 @@ import android.view.autofill.AutofillManager
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
@@ -47,7 +47,7 @@ import com.kunzisoft.keepass.icons.IconPackChooser
|
|||||||
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.settings.preference.IconPackListPreference
|
import com.kunzisoft.keepass.settings.preference.IconPackListPreference
|
||||||
import com.kunzisoft.keepass.settings.preferencedialogfragment.DurationDialogFragmentCompat
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.DurationDialogFragmentCompat
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
|
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.releaseAllUnnecessaryPermissionUris
|
import com.kunzisoft.keepass.utils.UriUtil.releaseAllUnnecessaryPermissionUris
|
||||||
|
|
||||||
@@ -66,8 +66,8 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
Screen.FORM_FILLING -> {
|
Screen.FORM_FILLING -> {
|
||||||
onCreateFormFillingPreference(rootKey)
|
onCreateFormFillingPreference(rootKey)
|
||||||
}
|
}
|
||||||
Screen.ADVANCED_UNLOCK -> {
|
Screen.DEVICE_UNLOCK -> {
|
||||||
onCreateAdvancedUnlockPreferences(rootKey)
|
onCreateDeviceUnlockPreferences(rootKey)
|
||||||
}
|
}
|
||||||
Screen.APPEARANCE -> {
|
Screen.APPEARANCE -> {
|
||||||
onCreateAppearancePreferences(rootKey)
|
onCreateAppearancePreferences(rootKey)
|
||||||
@@ -119,7 +119,16 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
|
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val autoFillEnablePreference: TwoStatePreference? = findPreference(getString(R.string.settings_autofill_enable_key))
|
|
||||||
|
// Hide Passkeys settings if needed
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
findPreference<Preference>(getString(R.string.passkeys_explanation_key))
|
||||||
|
?.isVisible = false
|
||||||
|
findPreference<Preference>(getString(R.string.settings_passkeys_key))
|
||||||
|
?.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
val autoFillEnablePreference: TwoStatePreference? = findPreference(getString(R.string.settings_credential_provider_enable_key))
|
||||||
activity.getSystemService(AutofillManager::class.java)?.let { autofillManager ->
|
activity.getSystemService(AutofillManager::class.java)?.let { autofillManager ->
|
||||||
if (autofillManager.hasEnabledAutofillServices())
|
if (autofillManager.hasEnabledAutofillServices())
|
||||||
autoFillEnablePreference?.isChecked = autofillManager.hasEnabledAutofillServices()
|
autoFillEnablePreference?.isChecked = autofillManager.hasEnabledAutofillServices()
|
||||||
@@ -161,7 +170,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
val intent =
|
val intent =
|
||||||
Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE)
|
Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE)
|
||||||
intent.data =
|
intent.data =
|
||||||
Uri.parse("package:com.kunzisoft.keepass.autofill.KeeAutofillService")
|
"package:com.kunzisoft.keepass.autofill.KeeAutofillService".toUri()
|
||||||
Log.d(javaClass.name, "Autofill enable service: intent=$intent")
|
Log.d(javaClass.name, "Autofill enable service: intent=$intent")
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
} else {
|
} else {
|
||||||
@@ -171,7 +180,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
findPreference<Preference>(getString(R.string.autofill_key))?.isVisible = false
|
findPreference<Preference>(getString(R.string.credential_provider_key))?.isVisible = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,14 +201,28 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
findPreference<Preference>(getString(R.string.autofill_explanation_key))?.setOnPreferenceClickListener {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
context?.openUrl(R.string.autofill_explanation_url)
|
findPreference<Preference>(getString(R.string.passkeys_explanation_key))?.setOnPreferenceClickListener {
|
||||||
false
|
context?.openUrl(R.string.passkeys_explanation_url)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
findPreference<Preference>(getString(R.string.settings_passkeys_key))?.setOnPreferenceClickListener {
|
||||||
|
startActivity(Intent(context, PasskeysSettingsActivity::class.java))
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
findPreference<Preference>(getString(R.string.settings_autofill_key))?.setOnPreferenceClickListener {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
startActivity(Intent(context, AutofillSettingsActivity::class.java))
|
findPreference<Preference>(getString(R.string.autofill_explanation_key))?.setOnPreferenceClickListener {
|
||||||
false
|
context?.openUrl(R.string.autofill_explanation_url)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
findPreference<Preference>(getString(R.string.settings_autofill_key))?.setOnPreferenceClickListener {
|
||||||
|
startActivity(Intent(context, AutofillSettingsActivity::class.java))
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
findPreference<Preference>(getString(R.string.clipboard_notifications_key))?.setOnPreferenceChangeListener { _, newValue ->
|
findPreference<Preference>(getString(R.string.clipboard_notifications_key))?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
@@ -240,15 +263,15 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onCreateAdvancedUnlockPreferences(rootKey: String?) {
|
private fun onCreateDeviceUnlockPreferences(rootKey: String?) {
|
||||||
setPreferencesFromResource(R.xml.preferences_advanced_unlock, rootKey)
|
setPreferencesFromResource(R.xml.preferences_device_unlock, rootKey)
|
||||||
|
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
|
|
||||||
val biometricUnlockEnablePreference: TwoStatePreference? = findPreference(getString(R.string.biometric_unlock_enable_key))
|
val biometricUnlockEnablePreference: TwoStatePreference? = findPreference(getString(R.string.biometric_unlock_enable_key))
|
||||||
val deviceCredentialUnlockEnablePreference: TwoStatePreference? = findPreference(getString(R.string.device_credential_unlock_enable_key))
|
val deviceCredentialUnlockEnablePreference: TwoStatePreference? = findPreference(getString(R.string.device_credential_unlock_enable_key))
|
||||||
val autoOpenPromptPreference: TwoStatePreference? = findPreference(getString(R.string.biometric_auto_open_prompt_key))
|
val autoOpenPromptPreference: TwoStatePreference? = findPreference(getString(R.string.biometric_auto_open_prompt_key))
|
||||||
val tempAdvancedUnlockPreference: TwoStatePreference? = findPreference(getString(R.string.temp_advanced_unlock_enable_key))
|
val tempDeviceUnlockPreference: TwoStatePreference? = findPreference(getString(R.string.temp_device_unlock_enable_key))
|
||||||
|
|
||||||
val biometricUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
val biometricUnlockSupported = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
DeviceUnlockManager.biometricUnlockSupported(activity)
|
DeviceUnlockManager.biometricUnlockSupported(activity)
|
||||||
@@ -272,7 +295,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
warningMessage(activity, keystoreWarning = false, deleteKeys = true) {
|
warningMessage(activity, keystoreWarning = false, deleteKeys = true) {
|
||||||
biometricUnlockEnablePreference.isChecked = false
|
biometricUnlockEnablePreference.isChecked = false
|
||||||
autoOpenPromptPreference?.isEnabled = deviceCredentialChecked
|
autoOpenPromptPreference?.isEnabled = deviceCredentialChecked
|
||||||
tempAdvancedUnlockPreference?.isEnabled = deviceCredentialChecked
|
tempDeviceUnlockPreference?.isEnabled = deviceCredentialChecked
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (deviceCredentialChecked) {
|
if (deviceCredentialChecked) {
|
||||||
@@ -286,7 +309,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
warningMessage(activity, keystoreWarning = true, deleteKeys = false) {
|
warningMessage(activity, keystoreWarning = true, deleteKeys = false) {
|
||||||
biometricUnlockEnablePreference.isChecked = true
|
biometricUnlockEnablePreference.isChecked = true
|
||||||
autoOpenPromptPreference?.isEnabled = true
|
autoOpenPromptPreference?.isEnabled = true
|
||||||
tempAdvancedUnlockPreference?.isEnabled = true
|
tempDeviceUnlockPreference?.isEnabled = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -319,7 +342,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
warningMessage(activity, keystoreWarning = false, deleteKeys = true) {
|
warningMessage(activity, keystoreWarning = false, deleteKeys = true) {
|
||||||
deviceCredentialUnlockEnablePreference.isChecked = false
|
deviceCredentialUnlockEnablePreference.isChecked = false
|
||||||
autoOpenPromptPreference?.isEnabled = biometricChecked
|
autoOpenPromptPreference?.isEnabled = biometricChecked
|
||||||
tempAdvancedUnlockPreference?.isEnabled = biometricChecked
|
tempDeviceUnlockPreference?.isEnabled = biometricChecked
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (biometricChecked) {
|
if (biometricChecked) {
|
||||||
@@ -333,7 +356,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
warningMessage(activity, keystoreWarning = true, deleteKeys = false) {
|
warningMessage(activity, keystoreWarning = true, deleteKeys = false) {
|
||||||
deviceCredentialUnlockEnablePreference.isChecked = true
|
deviceCredentialUnlockEnablePreference.isChecked = true
|
||||||
autoOpenPromptPreference?.isEnabled = true
|
autoOpenPromptPreference?.isEnabled = true
|
||||||
tempAdvancedUnlockPreference?.isEnabled = true
|
tempDeviceUnlockPreference?.isEnabled = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,13 +367,13 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
|
|
||||||
autoOpenPromptPreference?.isEnabled = biometricUnlockEnablePreference?.isChecked == true
|
autoOpenPromptPreference?.isEnabled = biometricUnlockEnablePreference?.isChecked == true
|
||||||
|| deviceCredentialUnlockEnablePreference?.isChecked == true
|
|| deviceCredentialUnlockEnablePreference?.isChecked == true
|
||||||
tempAdvancedUnlockPreference?.isEnabled = biometricUnlockEnablePreference?.isChecked == true
|
tempDeviceUnlockPreference?.isEnabled = biometricUnlockEnablePreference?.isChecked == true
|
||||||
|| deviceCredentialUnlockEnablePreference?.isChecked == true
|
|| deviceCredentialUnlockEnablePreference?.isChecked == true
|
||||||
|
|
||||||
tempAdvancedUnlockPreference?.setOnPreferenceClickListener {
|
tempDeviceUnlockPreference?.setOnPreferenceClickListener {
|
||||||
tempAdvancedUnlockPreference.isChecked = !tempAdvancedUnlockPreference.isChecked
|
tempDeviceUnlockPreference.isChecked = !tempDeviceUnlockPreference.isChecked
|
||||||
warningMessage(activity, keystoreWarning = false, deleteKeys = true) {
|
warningMessage(activity, keystoreWarning = false, deleteKeys = true) {
|
||||||
tempAdvancedUnlockPreference.isChecked = !tempAdvancedUnlockPreference.isChecked
|
tempDeviceUnlockPreference.isChecked = !tempDeviceUnlockPreference.isChecked
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -366,8 +389,8 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
findPreference<Preference>(getString(R.string.advanced_unlock_explanation_key))?.setOnPreferenceClickListener {
|
findPreference<Preference>(getString(R.string.device_unlock_explanation_key))?.setOnPreferenceClickListener {
|
||||||
context?.openUrl(R.string.advanced_unlock_explanation_url)
|
context?.openUrl(R.string.device_unlock_explanation_url)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -378,14 +401,14 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
validate: (()->Unit)? = null) {
|
validate: (()->Unit)? = null) {
|
||||||
var message = ""
|
var message = ""
|
||||||
if (keystoreWarning) {
|
if (keystoreWarning) {
|
||||||
message += resources.getString(R.string.advanced_unlock_prompt_store_credential_message)
|
message += resources.getString(R.string.device_unlock_prompt_store_credential_message)
|
||||||
message += "\n\n" + resources.getString(R.string.advanced_unlock_keystore_warning)
|
message += "\n\n" + resources.getString(R.string.device_unlock_keystore_warning)
|
||||||
}
|
}
|
||||||
if (keystoreWarning && deleteKeys) {
|
if (keystoreWarning && deleteKeys) {
|
||||||
message += "\n\n"
|
message += "\n\n"
|
||||||
}
|
}
|
||||||
if (deleteKeys) {
|
if (deleteKeys) {
|
||||||
message += resources.getString(R.string.advanced_unlock_delete_all_key_warning)
|
message += resources.getString(R.string.device_unlock_delete_all_key_warning)
|
||||||
}
|
}
|
||||||
warningAlertDialog = AlertDialog.Builder(activity)
|
warningAlertDialog = AlertDialog.Builder(activity)
|
||||||
.setMessage(message)
|
.setMessage(message)
|
||||||
@@ -509,7 +532,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
when (preference.key) {
|
when (preference.key) {
|
||||||
getString(R.string.app_timeout_key),
|
getString(R.string.app_timeout_key),
|
||||||
getString(R.string.clipboard_timeout_key),
|
getString(R.string.clipboard_timeout_key),
|
||||||
getString(R.string.temp_advanced_unlock_timeout_key) -> {
|
getString(R.string.temp_device_unlock_timeout_key) -> {
|
||||||
dialogFragment = DurationDialogFragmentCompat.newInstance(preference.key)
|
dialogFragment = DurationDialogFragmentCompat.newInstance(preference.key)
|
||||||
}
|
}
|
||||||
else -> otherDialogFragment = true
|
else -> otherDialogFragment = true
|
||||||
@@ -530,7 +553,7 @@ class NestedAppSettingsFragment : NestedSettingsFragment() {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
findPreference<TwoStatePreference?>(getString(R.string.settings_autofill_enable_key))?.let { autoFillEnablePreference ->
|
findPreference<TwoStatePreference?>(getString(R.string.settings_credential_provider_enable_key))?.let { autoFillEnablePreference ->
|
||||||
val autofillManager = activity.getSystemService(AutofillManager::class.java)
|
val autofillManager = activity.getSystemService(AutofillManager::class.java)
|
||||||
autoFillEnablePreference.isChecked = autofillManager != null
|
autoFillEnablePreference.isChecked = autofillManager != null
|
||||||
&& autofillManager.hasEnabledAutofillServices()
|
&& autofillManager.hasEnabledAutofillServices()
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import com.kunzisoft.keepass.activities.dialogs.UnderDevelopmentFeatureDialogFra
|
|||||||
abstract class NestedSettingsFragment : PreferenceFragmentCompat() {
|
abstract class NestedSettingsFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
enum class Screen {
|
enum class Screen {
|
||||||
APPLICATION, FORM_FILLING, ADVANCED_UNLOCK, APPEARANCE, DATABASE, DATABASE_SECURITY, DATABASE_MASTER_KEY
|
APPLICATION, FORM_FILLING, DEVICE_UNLOCK, APPEARANCE, DATABASE, DATABASE_SECURITY, DATABASE_MASTER_KEY
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getScreen(): Screen {
|
fun getScreen(): Screen {
|
||||||
@@ -66,7 +66,7 @@ abstract class NestedSettingsFragment : PreferenceFragmentCompat() {
|
|||||||
val fragment: NestedSettingsFragment = when (key) {
|
val fragment: NestedSettingsFragment = when (key) {
|
||||||
Screen.APPLICATION,
|
Screen.APPLICATION,
|
||||||
Screen.FORM_FILLING,
|
Screen.FORM_FILLING,
|
||||||
Screen.ADVANCED_UNLOCK,
|
Screen.DEVICE_UNLOCK,
|
||||||
Screen.APPEARANCE -> NestedAppSettingsFragment()
|
Screen.APPEARANCE -> NestedAppSettingsFragment()
|
||||||
Screen.DATABASE,
|
Screen.DATABASE,
|
||||||
Screen.DATABASE_SECURITY,
|
Screen.DATABASE_SECURITY,
|
||||||
@@ -83,7 +83,7 @@ abstract class NestedSettingsFragment : PreferenceFragmentCompat() {
|
|||||||
return when (key) {
|
return when (key) {
|
||||||
Screen.APPLICATION -> resources.getString(R.string.menu_app_settings)
|
Screen.APPLICATION -> resources.getString(R.string.menu_app_settings)
|
||||||
Screen.FORM_FILLING -> resources.getString(R.string.menu_form_filling_settings)
|
Screen.FORM_FILLING -> resources.getString(R.string.menu_form_filling_settings)
|
||||||
Screen.ADVANCED_UNLOCK -> resources.getString(R.string.menu_advanced_unlock_settings)
|
Screen.DEVICE_UNLOCK -> resources.getString(R.string.menu_device_unlock_settings)
|
||||||
Screen.APPEARANCE -> resources.getString(R.string.menu_appearance_settings)
|
Screen.APPEARANCE -> resources.getString(R.string.menu_appearance_settings)
|
||||||
Screen.DATABASE -> resources.getString(R.string.menu_database_settings)
|
Screen.DATABASE -> resources.getString(R.string.menu_database_settings)
|
||||||
Screen.DATABASE_SECURITY -> resources.getString(R.string.menu_security_settings)
|
Screen.DATABASE_SECURITY -> resources.getString(R.string.menu_security_settings)
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.settings
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
class PasskeysSettingsActivity : ExternalSettingsActivity() {
|
||||||
|
|
||||||
|
override fun retrieveTitle(): Int {
|
||||||
|
return R.string.passkeys_preference_title
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun retrievePreferenceFragment(): PreferenceFragmentCompat {
|
||||||
|
return PasskeysSettingsFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.settings
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.preference.Preference
|
||||||
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.PasskeysPrivilegedAppsPreferenceDialogFragmentCompat
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
class PasskeysSettingsFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
// Load the preferences from an XML resource
|
||||||
|
setPreferencesFromResource(R.xml.preferences_passkeys, rootKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
override fun onDisplayPreferenceDialog(preference: Preference) {
|
||||||
|
var dialogFragment: DialogFragment? = null
|
||||||
|
|
||||||
|
when (preference.key) {
|
||||||
|
getString(R.string.passkeys_privileged_apps_key) -> {
|
||||||
|
dialogFragment = PasskeysPrivilegedAppsPreferenceDialogFragmentCompat.newInstance(preference.key)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dialogFragment != null) {
|
||||||
|
dialogFragment.setTargetFragment(this, 0)
|
||||||
|
dialogFragment.show(parentFragmentManager, TAG_PASSKEYS_PREF_FRAGMENT)
|
||||||
|
} else {
|
||||||
|
super.onDisplayPreferenceDialog(preference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val TAG_PASSKEYS_PREF_FRAGMENT = "TAG_PASSKEYS_PREF_FRAGMENT"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,8 +35,8 @@ import com.kunzisoft.keepass.database.search.SearchParameters
|
|||||||
import com.kunzisoft.keepass.education.Education
|
import com.kunzisoft.keepass.education.Education
|
||||||
import com.kunzisoft.keepass.password.PassphraseGenerator
|
import com.kunzisoft.keepass.password.PassphraseGenerator
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
|
||||||
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
|
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
|
|
||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
|
|
||||||
object PreferencesUtil {
|
object PreferencesUtil {
|
||||||
@@ -460,10 +460,10 @@ object PreferencesUtil {
|
|||||||
?: TimeoutHelper.DEFAULT_TIMEOUT
|
?: TimeoutHelper.DEFAULT_TIMEOUT
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAdvancedUnlockTimeout(context: Context): Long {
|
fun getDeviceUnlockTimeout(context: Context): Long {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getString(context.getString(R.string.temp_advanced_unlock_timeout_key),
|
return prefs.getString(context.getString(R.string.temp_device_unlock_timeout_key),
|
||||||
context.getString(R.string.temp_advanced_unlock_timeout_default))?.toLong()
|
context.getString(R.string.temp_device_unlock_timeout_default))?.toLong()
|
||||||
?: TimeoutHelper.DEFAULT_TIMEOUT
|
?: TimeoutHelper.DEFAULT_TIMEOUT
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,7 +503,7 @@ object PreferencesUtil {
|
|||||||
context.resources.getBoolean(R.bool.enable_screenshot_mode_key_default))
|
context.resources.getBoolean(R.bool.enable_screenshot_mode_key_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isAdvancedUnlockEnable(context: Context): Boolean {
|
fun isDeviceUnlockEnable(context: Context): Boolean {
|
||||||
return isBiometricUnlockEnable(context) || isDeviceCredentialUnlockEnable(context)
|
return isBiometricUnlockEnable(context) || isDeviceCredentialUnlockEnable(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,13 +526,13 @@ object PreferencesUtil {
|
|||||||
&& !isBiometricUnlockEnable(context)
|
&& !isBiometricUnlockEnable(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isTempAdvancedUnlockEnable(context: Context): Boolean {
|
fun isTempDeviceUnlockEnable(context: Context): Boolean {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getBoolean(context.getString(R.string.temp_advanced_unlock_enable_key),
|
return prefs.getBoolean(context.getString(R.string.temp_device_unlock_enable_key),
|
||||||
context.resources.getBoolean(R.bool.temp_advanced_unlock_enable_default))
|
context.resources.getBoolean(R.bool.temp_device_unlock_enable_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isAdvancedUnlockPromptAutoOpenEnable(context: Context): Boolean {
|
fun isDeviceUnlockPromptAutoOpenEnable(context: Context): Boolean {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getBoolean(context.getString(R.string.biometric_auto_open_prompt_key),
|
return prefs.getBoolean(context.getString(R.string.biometric_auto_open_prompt_key),
|
||||||
context.resources.getBoolean(R.bool.biometric_auto_open_prompt_default))
|
context.resources.getBoolean(R.bool.biometric_auto_open_prompt_default))
|
||||||
@@ -618,12 +618,6 @@ object PreferencesUtil {
|
|||||||
context.resources.getBoolean(R.bool.allow_no_password_default))
|
context.resources.getBoolean(R.bool.allow_no_password_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun enableReadOnlyDatabase(context: Context): Boolean {
|
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
return prefs.getBoolean(context.getString(R.string.enable_read_only_key),
|
|
||||||
context.resources.getBoolean(R.bool.enable_read_only_default))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deletePasswordAfterConnexionAttempt(context: Context): Boolean {
|
fun deletePasswordAfterConnexionAttempt(context: Context): Boolean {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getBoolean(context.getString(R.string.delete_entered_password_key),
|
return prefs.getBoolean(context.getString(R.string.delete_entered_password_key),
|
||||||
@@ -692,6 +686,26 @@ object PreferencesUtil {
|
|||||||
context.resources.getBoolean(R.bool.keyboard_previous_lock_default))
|
context.resources.getBoolean(R.bool.keyboard_previous_lock_default))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isPasskeyBackupEligibilityEnable(context: Context): Boolean {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
return prefs.getBoolean(context.getString(R.string.passkeys_backup_eligibility_key),
|
||||||
|
context.resources.getBoolean(R.bool.passkeys_backup_eligibility_default))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isPasskeyAutoSelectEnable(context: Context): Boolean {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
return prefs.getBoolean(context.getString(R.string.passkeys_auto_select_key),
|
||||||
|
context.resources.getBoolean(R.bool.passkeys_auto_select_default))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isPasskeyBackupStateEnable(context: Context): Boolean {
|
||||||
|
if (!isPasskeyBackupEligibilityEnable(context))
|
||||||
|
return false
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
return prefs.getBoolean(context.getString(R.string.passkeys_backup_state_key),
|
||||||
|
context.resources.getBoolean(R.bool.passkeys_backup_state_default))
|
||||||
|
}
|
||||||
|
|
||||||
fun isAutofillCloseDatabaseEnable(context: Context): Boolean {
|
fun isAutofillCloseDatabaseEnable(context: Context): Boolean {
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return prefs.getBoolean(context.getString(R.string.autofill_close_database_key),
|
return prefs.getBoolean(context.getString(R.string.autofill_close_database_key),
|
||||||
@@ -804,7 +818,6 @@ object PreferencesUtil {
|
|||||||
when (name) {
|
when (name) {
|
||||||
context.getString(R.string.allow_no_password_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.allow_no_password_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.delete_entered_password_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.delete_entered_password_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.enable_read_only_key) -> editor.putBoolean(name, value.toBoolean())
|
|
||||||
context.getString(R.string.enable_auto_save_database_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.enable_auto_save_database_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.enable_keep_screen_on_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.enable_keep_screen_on_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.auto_focus_search_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.auto_focus_search_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
@@ -821,14 +834,14 @@ object PreferencesUtil {
|
|||||||
context.getString(R.string.biometric_unlock_enable_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.biometric_unlock_enable_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.device_credential_unlock_enable_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.device_credential_unlock_enable_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.biometric_auto_open_prompt_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.biometric_auto_open_prompt_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.temp_advanced_unlock_enable_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.temp_device_unlock_enable_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.temp_advanced_unlock_timeout_key) -> editor.putString(name, value.toLong().toString())
|
context.getString(R.string.temp_device_unlock_timeout_key) -> editor.putString(name, value.toLong().toString())
|
||||||
|
|
||||||
context.getString(R.string.magic_keyboard_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.magic_keyboard_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.clipboard_notifications_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.clipboard_notifications_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.clear_clipboard_notification_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.clear_clipboard_notification_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.clipboard_timeout_key) -> editor.putString(name, value.toLong().toString())
|
context.getString(R.string.clipboard_timeout_key) -> editor.putString(name, value.toLong().toString())
|
||||||
context.getString(R.string.settings_autofill_enable_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.settings_credential_provider_enable_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.keyboard_notification_entry_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.keyboard_notification_entry_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.keyboard_notification_entry_clear_close_key) -> editor.putBoolean(name, value.toBoolean())
|
context.getString(R.string.keyboard_notification_entry_clear_close_key) -> editor.putBoolean(name, value.toBoolean())
|
||||||
context.getString(R.string.keyboard_entry_timeout_key) -> editor.putString(name, value.toLong().toString())
|
context.getString(R.string.keyboard_entry_timeout_key) -> editor.putString(name, value.toLong().toString())
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ class DurationDialogPreference @JvmOverloads constructor(context: Context,
|
|||||||
notifyChanged()
|
notifyChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated(message = "")
|
||||||
override fun onSetInitialValue(restorePersistedValue: Boolean, defaultValue: Any?) {
|
override fun onSetInitialValue(restorePersistedValue: Boolean, defaultValue: Any?) {
|
||||||
if (restorePersistedValue) {
|
if (restorePersistedValue) {
|
||||||
mDuration = getPersistedString(mDuration.toString()).toLongOrNull() ?: mDuration
|
mDuration = getPersistedString(mDuration.toString()).toLongOrNull() ?: mDuration
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.settings.preferencedialogfragment
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.adapter.ListSelectionItemAdapter
|
||||||
|
import com.kunzisoft.keepass.settings.preferencedialogfragment.viewmodel.PasskeysPrivilegedAppsViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
class PasskeysPrivilegedAppsPreferenceDialogFragmentCompat
|
||||||
|
: InputPreferenceDialogFragmentCompat() {
|
||||||
|
|
||||||
|
private var mAdapter = ListSelectionItemAdapter<AndroidPrivilegedApp>()
|
||||||
|
private val passkeysPrivilegedAppsViewModel : PasskeysPrivilegedAppsViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
passkeysPrivilegedAppsViewModel.retrievePrivilegedAppsToSelect()
|
||||||
|
|
||||||
|
passkeysPrivilegedAppsViewModel.uiState.collect { uiState ->
|
||||||
|
when(uiState) {
|
||||||
|
is PasskeysPrivilegedAppsViewModel.UiState.Loading -> {}
|
||||||
|
is PasskeysPrivilegedAppsViewModel.UiState.OnPrivilegedAppsToSelectRetrieved -> {
|
||||||
|
mAdapter.apply {
|
||||||
|
setItems(uiState.privilegedApps)
|
||||||
|
selectedItems = uiState.selected.toMutableList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindDialogView(view: View) {
|
||||||
|
super.onBindDialogView(view)
|
||||||
|
setExplanationText(R.string.passkeys_privileged_apps_explanation)
|
||||||
|
view.findViewById<RecyclerView>(R.id.pref_dialog_list).apply {
|
||||||
|
layoutManager = LinearLayoutManager(context)
|
||||||
|
adapter = mAdapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDialogClosed(positiveResult: Boolean) {
|
||||||
|
if (positiveResult) {
|
||||||
|
passkeysPrivilegedAppsViewModel.saveSelectedPrivilegedApp(mAdapter.selectedItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun newInstance(key: String): PasskeysPrivilegedAppsPreferenceDialogFragmentCompat {
|
||||||
|
val fragment = PasskeysPrivilegedAppsPreferenceDialogFragmentCompat()
|
||||||
|
val bundle = Bundle(1)
|
||||||
|
bundle.putString(ARG_KEY, key)
|
||||||
|
fragment.arguments = bundle
|
||||||
|
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 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.settings.preferencedialogfragment.adapter
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
|
||||||
|
class ListSelectionItemAdapter<T>()
|
||||||
|
: RecyclerView.Adapter<ListSelectionItemAdapter.SelectionViewHolder>() {
|
||||||
|
|
||||||
|
private val itemList: MutableList<T> = mutableListOf()
|
||||||
|
var selectedItems: MutableList<T> = mutableListOf()
|
||||||
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemSelectedCallback: ItemSelectedCallback<T>? = null
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SelectionViewHolder {
|
||||||
|
return SelectionViewHolder(LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.pref_dialog_list_item, parent, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n", "NotifyDataSetChanged")
|
||||||
|
override fun onBindViewHolder(holder: SelectionViewHolder, position: Int) {
|
||||||
|
val item = itemList[position]
|
||||||
|
|
||||||
|
holder.container.apply {
|
||||||
|
isSelected = selectedItems.contains(item)
|
||||||
|
}
|
||||||
|
holder.textView.apply {
|
||||||
|
text = item.toString()
|
||||||
|
setOnClickListener {
|
||||||
|
if (selectedItems.contains(item))
|
||||||
|
selectedItems.remove(item)
|
||||||
|
else
|
||||||
|
selectedItems.add(item)
|
||||||
|
itemSelectedCallback?.onItemSelected(item)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return itemList.size
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setItems(items: List<T>) {
|
||||||
|
this.itemList.clear()
|
||||||
|
this.itemList.addAll(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ItemSelectedCallback<T> {
|
||||||
|
fun onItemSelected(item: T)
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
var textView: TextView = itemView.findViewById(R.id.pref_dialog_list_text)
|
||||||
|
var container: ViewGroup = itemView.findViewById(R.id.pref_dialog_list_container)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.kunzisoft.keepass.settings.preferencedialogfragment.viewmodel
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.deletePrivilegedAppsFile
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.retrieveCustomPrivilegedApps
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.retrievePredefinedPrivilegedApps
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PrivilegedAllowLists.saveCustomPrivilegedApps
|
||||||
|
import com.kunzisoft.keepass.utils.AppUtil.getInstalledBrowsersWithSignatures
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
class PasskeysPrivilegedAppsViewModel(application: Application): AndroidViewModel(application) {
|
||||||
|
|
||||||
|
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
|
||||||
|
val uiState: StateFlow<UiState> = _uiState
|
||||||
|
|
||||||
|
fun retrievePrivilegedAppsToSelect() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val predefinedPrivilegedApps = retrievePredefinedPrivilegedApps(getApplication())
|
||||||
|
val customPrivilegedApps = retrieveCustomPrivilegedApps(getApplication())
|
||||||
|
// Only retrieve browser apps that are not already in the predefined list
|
||||||
|
val browserApps = getInstalledBrowsersWithSignatures(getApplication()).filter {
|
||||||
|
predefinedPrivilegedApps.none { privilegedApp ->
|
||||||
|
privilegedApp.packageName == it.packageName
|
||||||
|
&& privilegedApp.fingerprints.any {
|
||||||
|
fingerprint -> fingerprint in it.fingerprints
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_uiState.value = UiState.OnPrivilegedAppsToSelectRetrieved(
|
||||||
|
privilegedApps = browserApps,
|
||||||
|
selected = customPrivilegedApps
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveSelectedPrivilegedApp(privilegedApps: List<AndroidPrivilegedApp>) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
if (privilegedApps.isNotEmpty())
|
||||||
|
saveCustomPrivilegedApps(getApplication(), privilegedApps)
|
||||||
|
else
|
||||||
|
deletePrivilegedAppsFile(getApplication())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class UiState {
|
||||||
|
|
||||||
|
object Loading : UiState()
|
||||||
|
data class OnPrivilegedAppsToSelectRetrieved(
|
||||||
|
val privilegedApps: List<AndroidPrivilegedApp>,
|
||||||
|
val selected: List<AndroidPrivilegedApp>
|
||||||
|
) : UiState()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -65,27 +65,19 @@ object TimeoutHelper {
|
|||||||
(context.applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
|
(context.applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
|
||||||
val triggerTime = System.currentTimeMillis() + timeout
|
val triggerTime = System.currentTimeMillis() + timeout
|
||||||
Log.d(TAG, "TimeoutHelper start")
|
Log.d(TAG, "TimeoutHelper start")
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
&& !alarmManager.canScheduleExactAlarms()) {
|
||||||
&& !alarmManager.canScheduleExactAlarms()) {
|
|
||||||
alarmManager.set(
|
|
||||||
AlarmManager.RTC,
|
|
||||||
triggerTime,
|
|
||||||
getLockPendingIntent(context)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
alarmManager.setExact(
|
|
||||||
AlarmManager.RTC,
|
|
||||||
triggerTime,
|
|
||||||
getLockPendingIntent(context)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
alarmManager.set(
|
alarmManager.set(
|
||||||
AlarmManager.RTC,
|
AlarmManager.RTC,
|
||||||
triggerTime,
|
triggerTime,
|
||||||
getLockPendingIntent(context)
|
getLockPendingIntent(context)
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
alarmManager.setExact(
|
||||||
|
AlarmManager.RTC,
|
||||||
|
triggerTime,
|
||||||
|
getLockPendingIntent(context)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
147
app/src/main/java/com/kunzisoft/keepass/utils/AppUtil.kt
Normal file
147
app/src/main/java/com/kunzisoft/keepass/utils/AppUtil.kt
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
package com.kunzisoft.keepass.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.ResolveInfo
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.kunzisoft.encrypt.Signature.getAllFingerprints
|
||||||
|
import com.kunzisoft.keepass.BuildConfig
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||||
|
import com.kunzisoft.keepass.education.Education
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
||||||
|
|
||||||
|
object AppUtil {
|
||||||
|
|
||||||
|
fun Context.isExternalAppInstalled(packageName: String, showError: Boolean = true): Boolean {
|
||||||
|
try {
|
||||||
|
this.applicationContext.packageManager.getPackageInfoCompat(
|
||||||
|
packageName,
|
||||||
|
PackageManager.GET_ACTIVITIES
|
||||||
|
)
|
||||||
|
Education.setEducationScreenReclickedPerformed(this)
|
||||||
|
return true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (showError)
|
||||||
|
Log.e(AppUtil::class.simpleName, "App not accessible", e)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.openExternalApp(packageName: String, sourcesURL: String? = null) {
|
||||||
|
var launchIntent: Intent? = null
|
||||||
|
try {
|
||||||
|
launchIntent = this.packageManager.getLaunchIntentForPackage(packageName)?.apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
} catch (ignored: Exception) { }
|
||||||
|
try {
|
||||||
|
if (launchIntent == null) {
|
||||||
|
this.startActivity(
|
||||||
|
Intent(Intent.ACTION_VIEW)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
.setData(
|
||||||
|
if (sourcesURL != null
|
||||||
|
&& !BuildConfig.CLOSED_STORE
|
||||||
|
) {
|
||||||
|
sourcesURL
|
||||||
|
} else {
|
||||||
|
this.getString(
|
||||||
|
if (BuildConfig.CLOSED_STORE)
|
||||||
|
R.string.play_store_url
|
||||||
|
else
|
||||||
|
R.string.f_droid_url,
|
||||||
|
packageName
|
||||||
|
)
|
||||||
|
}.toUri()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.startActivity(launchIntent)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppUtil::class.simpleName, "App cannot be open", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.isContributingUser(): Boolean {
|
||||||
|
return (Education.isEducationScreenReclickedPerformed(this)
|
||||||
|
|| isExternalAppInstalled(this.getString(R.string.keepro_app_id), false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the concrete web domain AKA without sub domain if needed
|
||||||
|
*/
|
||||||
|
fun getConcreteWebDomain(context: Context,
|
||||||
|
webDomain: String?,
|
||||||
|
concreteWebDomain: (String?) -> Unit) {
|
||||||
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
if (webDomain != null) {
|
||||||
|
// Warning, web domain can contains IP, don't crop in this case
|
||||||
|
if (PreferencesUtil.searchSubdomains(context)
|
||||||
|
|| Regex(SearchInfo.WEB_IP_REGEX).matches(webDomain)) {
|
||||||
|
concreteWebDomain.invoke(webDomain)
|
||||||
|
} else {
|
||||||
|
val publicSuffixList = PublicSuffixList(context)
|
||||||
|
concreteWebDomain.invoke(publicSuffixList
|
||||||
|
.getPublicSuffixPlusOne(webDomain).await())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
concreteWebDomain.invoke(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.P)
|
||||||
|
fun getInstalledBrowsersWithSignatures(context: Context): List<AndroidPrivilegedApp> {
|
||||||
|
val packageManager = context.packageManager
|
||||||
|
val browserList = mutableListOf<AndroidPrivilegedApp>()
|
||||||
|
|
||||||
|
// Create a generic web intent
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW)
|
||||||
|
intent.data = context.getString(R.string.homepage_url).toUri()
|
||||||
|
|
||||||
|
// Query for apps that can handle this intent
|
||||||
|
val resolveInfoList: List<ResolveInfo> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
packageManager.queryIntentActivities(
|
||||||
|
intent,
|
||||||
|
PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_ALL.toLong())
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
packageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL)
|
||||||
|
}
|
||||||
|
|
||||||
|
val processedPackageNames = mutableSetOf<String>()
|
||||||
|
|
||||||
|
for (resolveInfo in resolveInfoList) {
|
||||||
|
val packageName = resolveInfo.activityInfo.packageName
|
||||||
|
if (packageName != null && !processedPackageNames.contains(packageName)) {
|
||||||
|
try {
|
||||||
|
val packageInfo = packageManager.getPackageInfo(
|
||||||
|
packageName,
|
||||||
|
PackageManager.GET_SIGNING_CERTIFICATES
|
||||||
|
)
|
||||||
|
val signatureFingerprints = packageInfo.signingInfo?.getAllFingerprints()
|
||||||
|
signatureFingerprints?.let {
|
||||||
|
browserList.add(AndroidPrivilegedApp(packageName, signatureFingerprints))
|
||||||
|
processedPackageNames.add(packageName)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppUtil::class.simpleName, "Error processing package: $packageName", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return browserList.distinctBy { it.packageName } // Ensure uniqueness just in case
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@ import androidx.core.content.ContextCompat
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.app.AppLifecycleObserver
|
import com.kunzisoft.keepass.app.AppLifecycleObserver
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
|
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
@@ -77,27 +77,19 @@ class LockReceiver(private var lockAction: () -> Unit) : BroadcastReceiver() {
|
|||||||
// Launch the effective action after a small time
|
// Launch the effective action after a small time
|
||||||
val first: Long = System.currentTimeMillis() + context.getString(R.string.timeout_screen_off).toLong()
|
val first: Long = System.currentTimeMillis() + context.getString(R.string.timeout_screen_off).toLong()
|
||||||
(context.getSystemService(ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
|
(context.getSystemService(ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
&& !alarmManager.canScheduleExactAlarms()) {
|
||||||
&& !alarmManager.canScheduleExactAlarms()) {
|
|
||||||
alarmManager.set(
|
|
||||||
AlarmManager.RTC_WAKEUP,
|
|
||||||
first,
|
|
||||||
lockPendingIntent
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
alarmManager.setExact(
|
|
||||||
AlarmManager.RTC_WAKEUP,
|
|
||||||
first,
|
|
||||||
lockPendingIntent
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
alarmManager.set(
|
alarmManager.set(
|
||||||
AlarmManager.RTC_WAKEUP,
|
AlarmManager.RTC_WAKEUP,
|
||||||
first,
|
first,
|
||||||
lockPendingIntent
|
lockPendingIntent
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
alarmManager.setExact(
|
||||||
|
AlarmManager.RTC_WAKEUP,
|
||||||
|
first,
|
||||||
|
lockPendingIntent
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import android.content.ComponentName
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
|
|
||||||
object MagikeyboardUtil {
|
object MagikeyboardUtil {
|
||||||
private val TAG = MagikeyboardUtil::class.java.name
|
private val TAG = MagikeyboardUtil::class.java.name
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import android.view.MenuItem
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.AboutActivity
|
import com.kunzisoft.keepass.activities.AboutActivity
|
||||||
import com.kunzisoft.keepass.settings.SettingsActivity
|
import com.kunzisoft.keepass.settings.SettingsActivity
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.isContributingUser
|
import com.kunzisoft.keepass.utils.AppUtil.isContributingUser
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
import com.kunzisoft.keepass.utils.UriUtil.openUrl
|
||||||
|
|
||||||
object MenuUtil {
|
object MenuUtil {
|
||||||
@@ -40,9 +40,6 @@ object MenuUtil {
|
|||||||
menu.findItem(R.id.menu_contribute)?.isVisible = false
|
menu.findItem(R.id.menu_contribute)?.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* @param checkLock Check the time lock before launch settings in LockingActivity
|
|
||||||
*/
|
|
||||||
fun onDefaultMenuOptionsItemSelected(activity: Activity,
|
fun onDefaultMenuOptionsItemSelected(activity: Activity,
|
||||||
item: MenuItem,
|
item: MenuItem,
|
||||||
timeoutEnable: Boolean = false) {
|
timeoutEnable: Boolean = false) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user