Compare commits

...

97 Commits

Author SHA1 Message Date
J-Jamet
01d778650c feat: Setting for auto select #2165 2025-09-18 12:26:56 +02:00
J-Jamet
dd389dbab1 fix: Passkey coroutine 2025-09-18 11:03:07 +02:00
J-Jamet
272ebd0c3f fix: Passkey auto save Signature 2025-09-17 23:23:41 +02:00
J-Jamet
0aecc21f43 fix: Passkey workflow 2025-09-17 20:02:55 +02:00
J-Jamet
1e7e464e65 feat: Add dialog 2025-09-17 13:58:12 +02:00
J-Jamet
d5c378ac85 fix: Private key format #2164 2025-09-14 23:48:27 +02:00
J-Jamet
672f1ca37d fix: Add toast error #2159 2025-09-14 13:17:49 +02:00
J-Jamet
2f9e1e4bf2 fix: Error message 2025-09-12 22:03:54 +02:00
J-Jamet
25d97e4f2e fix: Passkey Database Username 2025-09-12 21:20:12 +02:00
J-Jamet
f49dcbd654 fix: Unrecognized app that is not a browser #2157 2025-09-12 20:55:53 +02:00
J-Jamet
bf2d56b4fd feat: Add AAGUID Icons 2025-09-12 20:55:24 +02:00
J-Jamet
5893541dd2 Merge branch 'develop' into release/4.2.0 2025-09-12 16:14:19 +02:00
J-Jamet
2230fe66ab Merge tag '4.1.8' into develop
4.1.8
2025-09-12 16:04:08 +02:00
J-Jamet
803d637510 fix: Backup parameters init 2025-09-11 12:04:39 +02:00
J-Jamet
ccd5da0962 feat: Add backup as setting #2135 2025-09-11 00:00:22 +02:00
J-Jamet
36e3b85400 fix: Warnings 2025-09-09 20:55:30 +02:00
J-Jamet
cd73880e21 fix: Warnings 2025-09-09 14:06:52 +02:00
J-Jamet
8337f98f3a fix: Update to 4.2.0beta01 2025-09-09 13:57:58 +02:00
J-Jamet
47fbb562b7 Merge branch 'develop' into feature/Passkeys 2025-09-09 13:38:15 +02:00
J-Jamet
ef98e8a2db Merge branch 'develop' into feature/Passkeys 2025-09-09 11:51:41 +02:00
J-Jamet
e562694606 fix: Min Android version for Autofill 2025-09-08 15:07:19 +02:00
J-Jamet
464bc1442d fix: Min Android version 2025-09-08 15:01:19 +02:00
J-Jamet
c1730353d0 fix: Min Android version 2025-09-08 15:00:27 +02:00
J-Jamet
55e32e4ac5 fix: Web Origin 2025-09-08 14:19:30 +02:00
J-Jamet
96ed9fc7a6 fix: Dummy URL 2025-09-08 14:02:52 +02:00
J-Jamet
5fda628c9c fix: Remove unused dependency 2025-09-08 13:59:51 +02:00
J-Jamet
17742e25a9 fix: Move community json in free build 2025-09-08 13:58:04 +02:00
J-Jamet
8086289e4b fix: Small adjustments 2025-09-08 13:34:23 +02:00
J-Jamet
65157f661f fix: Add Passkey username field 2025-09-08 13:07:07 +02:00
J-Jamet
5df637d01f fix: Add Passkey username field 2025-09-08 13:06:42 +02:00
J-Jamet
8084920b9e Merge branch 'develop' into feature/Passkeys 2025-09-08 12:54:04 +02:00
J-Jamet
a6af9976fc fix: Change wording 2025-09-08 12:18:51 +02:00
J-Jamet
05c480b6d3 feat: Multiple custom list 2025-09-07 20:19:34 +02:00
J-Jamet
fb909dac52 feat: Dialog to ask privileged app 2025-09-05 14:27:04 +02:00
J-Jamet
a8130d67be fix: Wiki link 2025-09-03 19:29:36 +02:00
J-Jamet
754d195e26 fix: Selected privileged app 2025-09-03 17:35:48 +02:00
J-Jamet
074910ea19 feat: Add credential provider setting 2025-09-03 17:02:17 +02:00
J-Jamet
988b18b515 feat: JSON parser to manage Privileged apps 2025-09-03 14:25:53 +02:00
J-Jamet
8924254c25 feat: Custom settings and privileged 2025-09-02 19:57:02 +02:00
J-Jamet
2bedbf8a6c feat: Add priviled apps 2025-09-02 14:06:11 +02:00
J-Jamet
437a704bc8 fix: Small refactoring 2025-09-02 13:11:55 +02:00
J-Jamet
a3bd5e1593 fix: README 2025-09-02 12:47:50 +02:00
J-Jamet
3feb177afc fix: R attribute 2025-09-02 12:47:04 +02:00
J-Jamet
821f35fe05 Merge branch 'feature/passkeys_validation' into feature/Passkeys 2025-09-02 12:26:30 +02:00
J-Jamet
d36f675da7 fix: First validation pass 2025-09-02 11:49:40 +02:00
J-Jamet
b7f9690a38 Merge branch 'develop' into feature/Passkeys 2025-09-01 19:12:15 +02:00
J-Jamet
c861fe790c fix: Change backup eligibility 2025-09-01 16:05:47 +02:00
J-Jamet
1a717bda03 fix: Security exception 2025-09-01 15:53:45 +02:00
J-Jamet
b80acd5a2d fix: Add unit test for app signature and fix multiple fingerprint 2025-09-01 15:42:39 +02:00
J-Jamet
7e41527cfe fix: Refactoring verification 2025-09-01 15:29:19 +02:00
J-Jamet
200881278c fix: Change Android origin 2025-09-01 14:48:36 +02:00
J-Jamet
0d133ffdb0 feat: Change Android origin 2025-09-01 11:49:13 +02:00
J-Jamet
f8787ba03d fix: Add webOrigin, fix title and add verification state 2025-08-29 18:41:40 +02:00
J-Jamet
4f10d13691 fix: Small refactoring and add doc 2025-08-29 12:23:44 +02:00
J-Jamet
98007c962d fix: Browser selection and URL scheme 2025-08-27 23:27:48 +02:00
J-Jamet
5f27f161a5 fix: Allow to check multiple app signatures #1421 2025-08-27 23:10:16 +02:00
J-Jamet
fcf723849b fix: Move application signature function 2025-08-27 19:14:24 +02:00
J-Jamet
5bd866e104 feat: Add app Signature 2025-08-26 17:08:23 +02:00
J-Jamet
9985c6065d fix: Change Android scheme 2025-08-26 10:50:34 +02:00
J-Jamet
1f2e4a3719 fix: Change parameters 2025-08-25 20:30:28 +02:00
J-Jamet
fa2555a3f7 fix: Remove decodeHexToByteArray 2025-08-25 20:15:46 +02:00
cali-95
b4de7afe77 Fix constant intent request code
only one passkey was used, even if the users selected another one for the same relying party.
2025-08-25 20:10:10 +02:00
cali-95
736cafbcc2 Add support for ed25519 2025-08-25 20:09:42 +02:00
J-Jamet
f2f4c1e63d fix: Origin parameters and callingAppInfo 2025-08-25 15:38:34 +02:00
J-Jamet
bc86ee87a0 fix: package name 2025-08-24 23:44:13 +02:00
J-Jamet
5cbd60c024 Merge branch 'develop' into feature/Passkeys 2025-08-23 14:52:51 +02:00
J-Jamet
5817273872 fix: Passkey icon color 2025-08-22 11:57:54 +02:00
J-Jamet
32d6a11353 feat: Add passkey icon in entry list #1421 2025-08-22 11:45:59 +02:00
J-Jamet
9477fba704 fix: UUID string format #1421 2025-08-22 11:37:30 +02:00
J-Jamet
80b16bccf1 fix: UUIDUtils and fixed AAGUID #1421 2025-08-22 11:04:53 +02:00
J-Jamet
6672085d84 fix: Exclude field for form filling #2097 2025-08-21 21:25:26 +02:00
J-Jamet
05a39f6922 fix: Show dedicated Passkey view #2097 2025-08-21 20:47:12 +02:00
J-Jamet
40e8dea485 fix: Capture exception 2025-08-20 21:13:07 +02:00
J-Jamet
7e09532d5d fix: Add check security 2025-08-20 20:56:40 +02:00
J-Jamet
44e8f4f406 fix: AutoSearch 2025-08-20 12:29:24 +02:00
J-Jamet
e3083c7773 fix: search parameters 2025-08-20 10:11:55 +02:00
J-Jamet
d0c0c4a4d6 fix: gitignore .kotlin 2025-08-20 10:11:29 +02:00
J-Jamet
a9e8de26f8 fix: client data hash 2025-08-20 09:13:28 +02:00
J-Jamet
c7a256ebf1 Merge branch 'develop' into feature/Passkeys 2025-08-20 08:39:02 +02:00
J-Jamet
d0ab5267cf fix: Retrieve client data hash 2025-08-17 09:55:23 +02:00
J-Jamet
88b701fd39 fix: Remove Play store dependency 2025-08-16 20:23:03 +02:00
J-Jamet
4a1cee619c fix: Refactiring JSON objects 2025-08-16 19:57:50 +02:00
J-Jamet
c7741115ff Merge branch 'develop' into feature/Passkeys 2025-08-14 17:39:52 +02:00
J-Jamet
19c987abc3 Merge branch 'develop' into feature/Passkeys 2025-07-26 19:43:55 +02:00
J-Jamet
d03693341e Merge branch 'develop' into feature/Passkeys 2025-07-24 22:22:24 +02:00
J-Jamet
bf496333eb Merge branch 'develop' into feature/Passkeys 2025-07-24 20:12:05 +02:00
J-Jamet
c6b01947b3 fix: Multiple request 2025-07-22 13:45:44 +02:00
J-Jamet
91781f36ac fix: Registration callback 2025-07-16 16:15:55 +02:00
J-Jamet
3fbdf78ba1 fix: Search Info 2025-07-16 15:30:21 +02:00
J-Jamet
d1f463d497 fix: Refactoring Credential Provider 2025-07-16 14:03:48 +02:00
J-Jamet
488fd60d5d fix: Better handleCreatePasskeyQuery implementation 2025-07-08 17:23:20 +02:00
J-Jamet
41025f64c0 fix: Add fennec_fdroid according to https://github.com/Kunzisoft/KeePassDX/issues/1421#issuecomment-2872838246 2025-07-08 16:20:17 +02:00
J-Jamet
a2eac2ff76 fix: KeePassDX name 2025-07-08 15:44:57 +02:00
J-Jamet
34f2a2391a Merge branch 'master' into feature/Passkeys 2025-07-08 15:23:27 +02:00
J-Jamet
67b09014aa Merge branch 'develop' into feature/Passkeys 2024-11-18 16:40:46 +01:00
cali
c907750446 implement creation and update of passkeys 2024-10-13 20:37:46 +02:00
cali
69114c3cc0 first version credential provider 2024-09-08 18:49:27 +02:00
186 changed files with 8319 additions and 1612 deletions

3
.gitignore vendored
View File

@@ -19,6 +19,9 @@ bin/
gen/ gen/
out/ out/
# Kotlin folder
.kotlin/
# Gradle files # Gradle files
.gradle/ .gradle/
build/ build/

View File

@@ -1,3 +1,6 @@
KeePassDX(4.2.0)
* Passkeys management #1421 #2097 (Thx @cali-95)
KeePassDX(4.1.8) KeePassDX(4.1.8)
* Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 #1687 (Thx @Dev-ClayP) * Updated to API 35 minimum SDK 19 #2073 #2138 #2067 #2133 #1687 (Thx @Dev-ClayP)
* Remember last read-only state #2099 #2100 (Thx @rmacklin) * Remember last read-only state #2099 #2100 (Thx @rmacklin)

View File

@@ -11,8 +11,8 @@ android {
applicationId "com.kunzisoft.keepass" applicationId "com.kunzisoft.keepass"
minSdkVersion 19 minSdkVersion 19
targetSdkVersion 35 targetSdkVersion 35
versionCode = 141 versionCode = 142
versionName = "4.1.8" 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')

View 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"
}
]
}
}
]
}

View 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"
}
]
}
}
]
}

View File

@@ -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}" />
@@ -158,22 +159,34 @@
android:label="@string/about" /> android:label="@string/about" />
<activity <activity
android:name="com.kunzisoft.keepass.settings.SettingsActivity" /> android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
<activity
android:name="com.kunzisoft.keepass.activities.AutofillLauncherActivity"
android:theme="@style/Theme.Transparent"
android:configChanges="keyboardHidden"
android:excludeFromRecents="true"/>
<activity <activity
android:name="com.kunzisoft.keepass.settings.DeviceUnlockSettingsActivity" /> android:name="com.kunzisoft.keepass.settings.DeviceUnlockSettingsActivity" />
<activity <activity
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" /> 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
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"
@@ -227,8 +238,8 @@
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">

View File

@@ -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

View File

@@ -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

View File

@@ -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() {
@@ -738,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()
@@ -888,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,
@@ -899,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
) )
} }
} }
@@ -924,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
) )
} }
} }

View File

@@ -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,10 +66,10 @@ 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.getParcelableCompat import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.view.asError import com.kunzisoft.keepass.view.asError
@@ -98,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)
@@ -298,7 +297,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
}, },
{ onCancelSpecialMode() }, { onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() }, { onLaunchActivitySpecialMode() },
mAutofillActivityResultLauncher) mCredentialActivityResultLauncher)
} }
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) { private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
@@ -308,7 +307,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
{ onValidateSpecialMode() }, { onValidateSpecialMode() },
{ onCancelSpecialMode() }, { onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() }, { onLaunchActivitySpecialMode() },
mAutofillActivityResultLauncher) mCredentialActivityResultLauncher)
} }
} }
@@ -487,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
)
} }
} }
} }

View File

@@ -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() {
@@ -1570,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
) )
@@ -1590,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
) )
} }
} }
@@ -1620,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() }
} }
}) )
} }
} }
} }

View File

@@ -49,16 +49,17 @@ 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.app.database.FileDatabaseHistoryAction import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
import com.kunzisoft.keepass.autofill.AutofillComponent
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
@@ -127,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)
@@ -437,7 +436,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
{ onValidateSpecialMode() }, { onValidateSpecialMode() },
{ onCancelSpecialMode() }, { onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() }, { onLaunchActivitySpecialMode() },
mAutofillActivityResultLauncher mCredentialActivityResultLauncher
) )
} }
} }
@@ -577,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 ->
@@ -852,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,
@@ -868,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
)
} }
} }
@@ -898,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)

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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() {

View File

@@ -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() {

View File

@@ -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

View File

@@ -1,5 +0,0 @@
package com.kunzisoft.keepass.activities.helpers
enum class TypeMode {
DEFAULT, MAGIKEYBOARD, AUTOFILL
}

View File

@@ -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() {

View File

@@ -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

View File

@@ -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) {

View File

@@ -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)
} }
@@ -611,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 {

View File

@@ -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
}
} }

View File

@@ -1,4 +1,4 @@
package com.kunzisoft.keepass.activities.helpers package com.kunzisoft.keepass.credentialprovider
enum class SpecialMode { enum class SpecialMode {
DEFAULT, DEFAULT,

View File

@@ -0,0 +1,5 @@
package com.kunzisoft.keepass.credentialprovider
enum class TypeMode {
DEFAULT, MAGIKEYBOARD, AUTOFILL, PASSKEY
}

View File

@@ -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 {

View File

@@ -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
)
}
}
) )
} }

View File

@@ -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
)
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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) {

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,

View File

@@ -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
}
}

View File

@@ -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
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
}

View File

@@ -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")
}
}

View File

@@ -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())
}
}

View File

@@ -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>"
}
}

View File

@@ -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
}

View File

@@ -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"
)

View File

@@ -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()
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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")
}

View File

@@ -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
)

View File

@@ -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)
)
}
}
}

View File

@@ -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)
}

View File

@@ -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
}
}

View File

@@ -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 {

View File

@@ -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
} }
} }

View File

@@ -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,9 +61,8 @@ object SearchHelper {
&& !searchInfo.containsOnlyNullValues()) { && !searchInfo.containsOnlyNullValues()) {
// If search provide results // If search provide results
database.createVirtualGroupFromSearchInfo( database.createVirtualGroupFromSearchInfo(
searchInfoString = searchInfo.toString(), searchInfo,
searchInfoByDomain = searchInfo.isASearchByDomain(), MAX_SEARCH_ENTRY
max = MAX_SEARCH_ENTRY
)?.let { searchGroup -> )?.let { searchGroup ->
if (searchGroup.numberOfChildEntries > 0) { if (searchGroup.numberOfChildEntries > 0) {
searchWithoutUI = true searchWithoutUI = true

View File

@@ -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,

View File

@@ -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()
}
}
}
}

View File

@@ -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() {

View File

@@ -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))

View File

@@ -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>>()

View File

@@ -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

View File

@@ -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 {

View File

@@ -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)
} }
} }

View File

@@ -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
@@ -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 ->
@@ -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()

View File

@@ -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()
}
}

View File

@@ -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"
}
}

View File

@@ -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 {
@@ -686,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),
@@ -821,7 +841,7 @@ object PreferencesUtil {
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())

View File

@@ -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

View File

@@ -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
}
}
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View 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
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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) {

View File

@@ -200,64 +200,5 @@ object UriUtil {
this.openUrl(this.getString(resId)) this.openUrl(this.getString(resId))
} }
fun Context.isContributingUser(): Boolean {
return (Education.isEducationScreenReclickedPerformed(this)
|| isExternalAppInstalled(this.getString(R.string.keepro_app_id), false)
)
}
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(TAG, "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(
Uri.parse(
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
)
}
)
)
)
} else {
this.startActivity(launchIntent)
}
} catch (e: Exception) {
Log.e(TAG, "App cannot be open", e)
}
}
private const val TAG = "UriUtil" private const val TAG = "UriUtil"
} }

View File

@@ -1,35 +0,0 @@
package com.kunzisoft.keepass.utils
import android.content.Context
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 WebDomain {
/**
* 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)
}
}
}
}

View File

@@ -0,0 +1,169 @@
/*
* 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.view
import android.content.Context
import android.util.AttributeSet
import android.util.TypedValue
import android.view.ContextThemeWrapper
import androidx.appcompat.widget.AppCompatImageView
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.ViewCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.password.PasswordEntropy
class PasskeyTextFieldView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0)
: PasswordTextFieldView(context, attrs, defStyle) {
private var relyingPartyViewId = ViewCompat.generateViewId()
private var usernameViewId = ViewCompat.generateViewId()
private var passkeyImageId = ViewCompat.generateViewId()
private var passkeyImage = AppCompatImageView(
ContextThemeWrapper(context, R.style.KeepassDXStyle_ImageButton_Simple), null, 0).apply {
layoutParams = LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT)
setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_passkey_white_24dp))
contentDescription = context.getString(R.string.passkey)
}
private val relyingPartyView = AppCompatTextView(context).apply {
setTextAppearance(context,
R.style.KeepassDXStyle_TextAppearance_TextNodePrimary)
layoutParams = LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT
).also {
it.topMargin = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
8f,
resources.displayMetrics
).toInt()
it.leftMargin = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
8f,
resources.displayMetrics
).toInt()
it.marginStart = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
8f,
resources.displayMetrics
).toInt()
}
setTextIsSelectable(true)
}
private val usernameView = AppCompatTextView(context).apply {
setTextAppearance(context,
R.style.KeepassDXStyle_TextAppearance_TextNodeSecondary)
layoutParams = LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT).also {
it.topMargin = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
4f,
resources.displayMetrics
).toInt()
it.leftMargin = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
8f,
resources.displayMetrics
).toInt()
it.marginStart = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
8f,
resources.displayMetrics
).toInt()
}
setTextIsSelectable(true)
}
private fun buildViews() {
indicatorDrawable?.let {
DrawableCompat.setTint(it, PasswordEntropy.Strength.VERY_UNGUESSABLE.color)
}
passkeyImage.apply {
id = passkeyImageId
layoutParams = (layoutParams as LayoutParams?)?.also {
it.addRule(ALIGN_PARENT_RIGHT)
it.addRule(ALIGN_PARENT_END)
}
}
labelView.apply {
layoutParams = (layoutParams as LayoutParams?)?.also {
it.addRule(LEFT_OF, passkeyImageId)
it.addRule(START_OF, passkeyImageId)
}
}
relyingPartyView.apply {
id = relyingPartyViewId
layoutParams = (layoutParams as LayoutParams?)?.also {
it.addRule(LEFT_OF, passkeyImageId)
it.addRule(START_OF, passkeyImageId)
it.addRule(BELOW, labelViewId)
}
}
usernameView.apply {
id = usernameViewId
layoutParams = (layoutParams as LayoutParams?)?.also {
it.addRule(LEFT_OF, passkeyImageId)
it.addRule(START_OF, passkeyImageId)
it.addRule(BELOW, relyingPartyViewId)
}
}
}
init {
removeAllViews()
buildViews()
addView(passkeyImage)
addView(labelView)
addView(relyingPartyView)
addView(usernameView)
}
override var default: String = ""
override var isFieldVisible: Boolean = true
var relyingParty: String
get() {
return relyingPartyView.text.toString()
}
set(value) {
relyingPartyView.text = value
}
var username: String
get() {
return usernameView.text.toString()
}
set(value) {
usernameView.text = value
}
override fun getEntropyStrength(passwordText: String) {
// Do nothing
}
}

View File

@@ -34,9 +34,9 @@ import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
class PasswordTextFieldView @JvmOverloads constructor(context: Context, open class PasswordTextFieldView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyle: Int = 0) defStyle: Int = 0)
: TextFieldView(context, attrs, defStyle) { : TextFieldView(context, attrs, defStyle) {
private var mPasswordEntropyCalculator: PasswordEntropy = PasswordEntropy { private var mPasswordEntropyCalculator: PasswordEntropy = PasswordEntropy {
@@ -45,7 +45,7 @@ class PasswordTextFieldView @JvmOverloads constructor(context: Context,
} }
} }
private var indicatorDrawable = ContextCompat.getDrawable( protected var indicatorDrawable = ContextCompat.getDrawable(
context, context,
R.drawable.ic_shield_white_24dp R.drawable.ic_shield_white_24dp
)?.apply { )?.apply {
@@ -98,7 +98,7 @@ class PasswordTextFieldView @JvmOverloads constructor(context: Context,
value = resources.getString(valueId) value = resources.getString(valueId)
} }
private fun getEntropyStrength(passwordText: String) { protected open fun getEntropyStrength(passwordText: String) {
mPasswordEntropyCalculator.getEntropyStrength(passwordText) { entropyStrength -> mPasswordEntropyCalculator.getEntropyStrength(passwordText) { entropyStrength ->
labelView.apply { labelView.apply {
post { post {

View File

@@ -534,10 +534,15 @@ abstract class TemplateAbstractView<
} }
protected fun getCustomField(fieldName: String): Field { protected fun getCustomField(fieldName: String): Field {
return getCustomFieldOrNull(fieldName)
?: Field(fieldName, ProtectedString(false))
}
protected fun getCustomFieldOrNull(fieldName: String): Field? {
return getCustomField(fieldName, return getCustomField(fieldName,
templateFieldNotEmpty = false, templateFieldNotEmpty = false,
retrieveDefaultValues = false retrieveDefaultValues = false
) ?: Field(fieldName, ProtectedString(false)) )
} }
private fun getCustomField(fieldName: String, private fun getCustomField(fieldName: String,

View File

@@ -20,6 +20,8 @@ import com.kunzisoft.keepass.database.helper.getLocalizedName
import com.kunzisoft.keepass.database.helper.isStandardPasswordName import com.kunzisoft.keepass.database.helper.isStandardPasswordName
import com.kunzisoft.keepass.model.DataDate import com.kunzisoft.keepass.model.DataDate
import com.kunzisoft.keepass.model.DataTime import com.kunzisoft.keepass.model.DataTime
import com.kunzisoft.keepass.model.AppOriginEntryField
import com.kunzisoft.keepass.model.PasskeyEntryFields
import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.otp.OtpEntryFields
@@ -256,9 +258,12 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
override fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean, override fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean,
retrieveDefaultValues: Boolean) { retrieveDefaultValues: Boolean) {
super.populateEntryInfoWithViews(templateFieldNotEmpty, retrieveDefaultValues) super.populateEntryInfoWithViews(templateFieldNotEmpty, retrieveDefaultValues)
mEntryInfo?.otpModel = OtpEntryFields.parseFields { key -> val getField: (id: String) -> String? = { key ->
getCustomField(key).protectedValue.toString() getCustomFieldOrNull(key)?.protectedValue?.stringValue
}?.otpModel }
mEntryInfo?.otpModel = OtpEntryFields.parseFields(getField)?.otpModel
mEntryInfo?.passkey = PasskeyEntryFields.parseFields(getField)
mEntryInfo?.appOrigin = AppOriginEntryField.parseFields(getField)
} }
override fun onRestoreEntryInstanceState(state: SavedState) { override fun onRestoreEntryInstanceState(state: SavedState) {

View File

@@ -1,7 +1,6 @@
package com.kunzisoft.keepass.view package com.kunzisoft.keepass.view
import android.content.Context import android.content.Context
import android.os.Build
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
@@ -11,8 +10,11 @@ import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.database.element.template.TemplateAttribute import com.kunzisoft.keepass.database.element.template.TemplateAttribute
import com.kunzisoft.keepass.database.element.template.TemplateField import com.kunzisoft.keepass.database.element.template.TemplateField
import com.kunzisoft.keepass.database.helper.getLocalizedName import com.kunzisoft.keepass.database.helper.getLocalizedName
import com.kunzisoft.keepass.database.helper.isPasskeyLabel
import com.kunzisoft.keepass.database.helper.isStandardPasswordName import com.kunzisoft.keepass.database.helper.isStandardPasswordName
import com.kunzisoft.keepass.model.OtpModel import com.kunzisoft.keepass.model.OtpModel
import com.kunzisoft.keepass.model.Passkey
import com.kunzisoft.keepass.model.PasskeyEntryFields.PASSKEY_FIELD
import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpElement
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
@@ -52,6 +54,8 @@ class TemplateView @JvmOverloads constructor(context: Context,
return context?.let { return context?.let {
(if (TemplateField.isStandardPasswordName(context, templateAttribute.label)) (if (TemplateField.isStandardPasswordName(context, templateAttribute.label))
PasswordTextFieldView(it) PasswordTextFieldView(it)
else if (TemplateField.isPasskeyLabel(context, templateAttribute.label))
PasskeyTextFieldView(it)
else TextFieldView(it)).apply { else TextFieldView(it)).apply {
applyFontVisibility(mFontInVisibility) applyFontVisibility(mFontInVisibility)
setProtection(field.protectedValue.isProtected, mHideProtectedValue) setProtection(field.protectedValue.isProtected, mHideProtectedValue)
@@ -123,20 +127,20 @@ class TemplateView @JvmOverloads constructor(context: Context,
override fun populateViewsWithEntryInfo(showEmptyFields: Boolean): List<ViewField> { override fun populateViewsWithEntryInfo(showEmptyFields: Boolean): List<ViewField> {
val emptyCustomFields = super.populateViewsWithEntryInfo(false) val emptyCustomFields = super.populateViewsWithEntryInfo(false)
// Hide empty custom fields // Hide empty custom fields
emptyCustomFields.forEach { customFieldId -> emptyCustomFields.forEach { customFieldId ->
customFieldId.view.isVisible = false customFieldId.view.isVisible = false
} }
removeOtpRunnable() removeOtpRunnable()
mEntryInfo?.let { entryInfo -> mEntryInfo?.let { entryInfo ->
// Assign specific OTP dynamic view // Assign specific OTP dynamic view
entryInfo.otpModel?.let { entryInfo.otpModel?.let {
assignOtp(it) assignOtp(it)
} }
entryInfo.passkey?.let {
assignPasskey(it)
}
} }
return emptyCustomFields return emptyCustomFields
} }
@@ -196,6 +200,22 @@ class TemplateView @JvmOverloads constructor(context: Context,
} }
} }
private fun getPasskeyView(): PasskeyTextFieldView? {
getViewFieldByName(PASSKEY_FIELD)?.let { viewField ->
val view = viewField.view
if (view is PasskeyTextFieldView)
return view
}
return null
}
private fun assignPasskey(passkey: Passkey) {
getPasskeyView()?.apply {
relyingParty = passkey.relyingParty
username = passkey.username
}
}
private fun removeOtpRunnable() { private fun removeOtpRunnable() {
mLastOtpTokenView?.removeCallbacks(mOtpRunnable) mLastOtpTokenView?.removeCallbacks(mOtpRunnable)
mLastOtpTokenView = null mLastOtpTokenView = null

View File

@@ -20,14 +20,12 @@
package com.kunzisoft.keepass.view package com.kunzisoft.keepass.view
import android.content.Context import android.content.Context
import android.os.Build
import android.text.InputFilter import android.text.InputFilter
import android.text.util.Linkify import android.text.util.Linkify
import android.util.AttributeSet import android.util.AttributeSet
import android.util.TypedValue import android.util.TypedValue
import android.view.ContextThemeWrapper import android.view.ContextThemeWrapper
import android.view.View import android.view.View
import android.view.View.OnClickListener
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.widget.AppCompatImageButton import androidx.appcompat.widget.AppCompatImageButton
@@ -37,8 +35,8 @@ import androidx.core.text.util.LinkifyCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.EntryInfo.Companion.APPLICATION_ID_FIELD_NAME import com.kunzisoft.keepass.model.AppOriginEntryField.APPLICATION_ID_FIELD_NAME
import com.kunzisoft.keepass.utils.UriUtil.openExternalApp import com.kunzisoft.keepass.utils.AppUtil.openExternalApp
open class TextFieldView @JvmOverloads constructor(context: Context, open class TextFieldView @JvmOverloads constructor(context: Context,
@@ -46,7 +44,7 @@ open class TextFieldView @JvmOverloads constructor(context: Context,
defStyle: Int = 0) defStyle: Int = 0)
: RelativeLayout(context, attrs, defStyle), GenericTextFieldView { : RelativeLayout(context, attrs, defStyle), GenericTextFieldView {
private var labelViewId = ViewCompat.generateViewId() protected var labelViewId = ViewCompat.generateViewId()
private var valueViewId = ViewCompat.generateViewId() private var valueViewId = ViewCompat.generateViewId()
private var showButtonId = ViewCompat.generateViewId() private var showButtonId = ViewCompat.generateViewId()
private var copyButtonId = ViewCompat.generateViewId() private var copyButtonId = ViewCompat.generateViewId()

View File

@@ -58,11 +58,11 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.forEach import androidx.core.view.forEach
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.core.view.updatePaddingRelative import androidx.core.view.updatePaddingRelative
import com.google.android.material.appbar.CollapsingToolbarLayout import com.google.android.material.appbar.CollapsingToolbarLayout
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.database.exception.LocalizedException
import com.kunzisoft.keepass.database.helper.getLocalizedMessage import com.kunzisoft.keepass.database.helper.getLocalizedMessage
import com.kunzisoft.keepass.settings.PreferencesUtil import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable import com.kunzisoft.keepass.tasks.ActionRunnable
@@ -238,6 +238,17 @@ fun View.updateLockPaddingStart() {
} }
} }
fun Context.toastError(e: Throwable) {
Toast.makeText(
applicationContext,
if (e is LocalizedException)
e.getLocalizedMessage(resources)
else
e.localizedMessage,
Toast.LENGTH_LONG
).show()
}
fun Context.showActionErrorIfNeeded(result: ActionRunnable.Result) { fun Context.showActionErrorIfNeeded(result: ActionRunnable.Result) {
if (!result.isSuccess) { if (!result.isSuccess) {
result.exception?.getLocalizedMessage(resources)?.let { errorMessage -> result.exception?.getLocalizedMessage(resources)?.let { errorMessage ->

View File

@@ -174,7 +174,8 @@ class EntryEditViewModel: NodeEditViewModel() {
// Load entry info // Load entry info
entry.getEntryInfo(database, true).let { tempEntryInfo -> entry.getEntryInfo(database, true).let { tempEntryInfo ->
// Retrieve data from registration // Retrieve data from registration
(registerInfo?.searchInfo ?: searchInfo)?.let { tempSearchInfo -> // TODO only save registration
searchInfo?.let { tempSearchInfo ->
tempEntryInfo.saveSearchInfo(database, tempSearchInfo) tempEntryInfo.saveSearchInfo(database, tempSearchInfo)
} }
registerInfo?.let { regInfo -> registerInfo?.let { regInfo ->

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M3,20v-2.35c0,-0.633 0.158,-1.175 0.475,-1.625 0.317,-0.45 0.725,-0.792 1.225,-1.025 1.117,-0.5 2.188,-0.875 3.213,-1.125S9.967,13.5 11,13.5c0.433,0 0.854,0.021 1.263,0.063s0.829,0.104 1.263,0.188c-0.083,0.967 0.096,1.879 0.538,2.737C14.504,17.346 15.15,18.017 16,18.5v1.5L3,20ZM19,23.675 L17.5,22.175v-4.65c-0.733,-0.217 -1.333,-0.629 -1.8,-1.237 -0.467,-0.608 -0.7,-1.313 -0.7,-2.112 0,-0.967 0.342,-1.792 1.025,-2.475 0.683,-0.683 1.508,-1.025 2.475,-1.025s1.792,0.342 2.475,1.025c0.683,0.683 1.025,1.508 1.025,2.475 0,0.75 -0.213,1.417 -0.637,2 -0.425,0.583 -0.962,1 -1.612,1.25l1.25,1.25 -1.5,1.5 1.5,1.5 -2,2ZM11,11.5c-1.05,0 -1.938,-0.363 -2.662,-1.087 -0.725,-0.725 -1.087,-1.612 -1.087,-2.662s0.363,-1.938 1.087,-2.662C9.063,4.363 9.95,4 11,4s1.938,0.363 2.662,1.087c0.725,0.725 1.087,1.612 1.087,2.662s-0.363,1.938 -1.087,2.662C12.938,11.137 12.05,11.5 11,11.5ZM18.5,14.675c0.283,0 0.521,-0.096 0.712,-0.287S19.5,13.958 19.5,13.675c0,-0.283 -0.096,-0.521 -0.287,-0.712s-0.429,-0.287 -0.712,-0.287c-0.283,0 -0.521,0.096 -0.712,0.287S17.5,13.392 17.5,13.675c0,0.283 0.096,0.521 0.287,0.712s0.429,0.287 0.712,0.287Z"
android:strokeWidth="0.5"
android:fillColor="#ffffff"/>
</vector>

View File

@@ -159,6 +159,14 @@
android:layout_gravity="center" android:layout_gravity="center"
android:src="@drawable/ic_attach_file_white_24dp" /> android:src="@drawable/ic_attach_file_white_24dp" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/node_passkey_icon"
style="@style/KeepassDXStyle.Icon.Entry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/ic_passkey_white_24dp" />
</LinearLayout> </LinearLayout>
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView

View File

@@ -95,7 +95,7 @@
android:maxLines="1" android:maxLines="1"
android:ellipsize="end" android:ellipsize="end"
android:textColor="@color/grey_blue_slighter"/> android:textColor="@color/grey_blue_slighter"/>
<com.kunzisoft.keepass.magikeyboard.KeyboardView <com.kunzisoft.keepass.credentialprovider.magikeyboard.KeyboardView
android:id="@+id/magikeyboard_view" android:id="@+id/magikeyboard_view"
style="@style/KeepassDXStyle.Keyboard" style="@style/KeepassDXStyle.Keyboard"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -44,7 +44,7 @@
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
style="@style/KeepassDXStyle.SubTitle"/> style="@style/KeepassDXStyle.Text.Preferences.Explanation"/>
<Button <Button
android:id="@+id/explanation_button" android:id="@+id/explanation_button"

View File

@@ -33,7 +33,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:gravity="center" android:gravity="center"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
style="@style/KeepassDXStyle.SubTitle"/> style="@style/KeepassDXStyle.Text.Preferences.Explanation"/>
<com.google.android.material.materialswitch.MaterialSwitch <com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_element" android:id="@+id/switch_element"

View File

@@ -36,7 +36,7 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
style="@style/KeepassDXStyle.SubTitle"/> style="@style/KeepassDXStyle.Text.Preferences.Explanation"/>
<com.google.android.material.materialswitch.MaterialSwitch <com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_element" android:id="@+id/switch_element"
android:layout_marginTop="12dp" android:layout_marginTop="12dp"

View File

@@ -36,7 +36,7 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
style="@style/KeepassDXStyle.SubTitle"/> style="@style/KeepassDXStyle.Text.Preferences.Explanation"/>
<com.google.android.material.materialswitch.MaterialSwitch <com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_element" android:id="@+id/switch_element"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@@ -32,7 +32,7 @@
android:gravity="center" android:gravity="center"
android:layout_marginBottom="12dp" android:layout_marginBottom="12dp"
android:visibility="gone" android:visibility="gone"
style="@style/KeepassDXStyle.SubTitle"/> style="@style/KeepassDXStyle.Text.Preferences.Explanation"/>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/pref_dialog_list" android:id="@+id/pref_dialog_list"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2019 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/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/pref_dialog_list_container"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:minHeight="@dimen/selectable_min_height"
style="@style/KeepassDXStyle.Selectable.Item"
android:layout_margin="8dp">
<TextView
android:id="@+id/pref_dialog_list_text"
android:layout_margin="8dp"
style="@style/KeepassDXStyle.SubTitle.Entry"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@@ -35,7 +35,7 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
style="@style/KeepassDXStyle.SubTitle"/> style="@style/KeepassDXStyle.Text.Preferences.Explanation"/>
<com.google.android.material.materialswitch.MaterialSwitch <com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_element" android:id="@+id/switch_element"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@@ -167,7 +167,7 @@
<string name="general">عام</string> <string name="general">عام</string>
<string name="autofill">الملء التلقائي</string> <string name="autofill">الملء التلقائي</string>
<string name="autofill_sign_in_prompt">سجل باستخدام KeePassDX</string> <string name="autofill_sign_in_prompt">سجل باستخدام KeePassDX</string>
<string name="set_autofill_service_title">تعيين خدمة الملأ التلقائي الافتراضية</string> <string name="set_credential_provider_service_title">تعيين خدمة الملأ التلقائي الافتراضية</string>
<string name="password_size_title">حجم كلمة السر المولدة</string> <string name="password_size_title">حجم كلمة السر المولدة</string>
<string name="password_size_summary">تعيين الحجم الافتراضي لكلمات السر المولدة</string> <string name="password_size_summary">تعيين الحجم الافتراضي لكلمات السر المولدة</string>
<string name="list_password_generator_options_title">محارف كلمة السر</string> <string name="list_password_generator_options_title">محارف كلمة السر</string>
@@ -493,7 +493,7 @@
<string name="regex">تعابير نمطية</string> <string name="regex">تعابير نمطية</string>
<string name="enable_keep_screen_on_title">أبقِ الشاشة شغّالة</string> <string name="enable_keep_screen_on_title">أبقِ الشاشة شغّالة</string>
<string name="enable_education_screens_summary">أبرز العناصر لتعلم طريقة عمل التطبيق</string> <string name="enable_education_screens_summary">أبرز العناصر لتعلم طريقة عمل التطبيق</string>
<string name="autofill_read_only_save">غير مسموح حفظ البيانات في قاعدة بيانات مفتوحة للقراءة فقط.</string> <string name="error_save_read_only">غير مسموح حفظ البيانات في قاعدة بيانات مفتوحة للقراءة فقط.</string>
<string name="autofill_inline_suggestions_keyboard">أُضيف اقتراح ملء تلقائي.</string> <string name="autofill_inline_suggestions_keyboard">أُضيف اقتراح ملء تلقائي.</string>
<string name="keyboard_previous_database_credentials_summary">الرجوع للوحة المفاتيح السابقة تلقائيًا في شاشة بيانات اعتماد قاعدة البيانات</string> <string name="keyboard_previous_database_credentials_summary">الرجوع للوحة المفاتيح السابقة تلقائيًا في شاشة بيانات اعتماد قاعدة البيانات</string>
<string name="autofill_manual_selection_summary">اعرض خيارًا يسمح للمستخدم باختيار مدخلة من قاعدة البيانات</string> <string name="autofill_manual_selection_summary">اعرض خيارًا يسمح للمستخدم باختيار مدخلة من قاعدة البيانات</string>
@@ -646,7 +646,6 @@
<string name="menu_appearance_settings_summary">المظاهر والألوان والسمات</string> <string name="menu_appearance_settings_summary">المظاهر والألوان والسمات</string>
<string name="autofill_explanation_summary">تمكين الملء التلقائي لملء النماذج بسرعة في التطبيقات الأخرى</string> <string name="autofill_explanation_summary">تمكين الملء التلقائي لملء النماذج بسرعة في التطبيقات الأخرى</string>
<string name="device_credential_unlock_enable_summary">يتيح لك استخدام بيانات اعتماد جهازك لفتح قاعدة البيانات</string> <string name="device_credential_unlock_enable_summary">يتيح لك استخدام بيانات اعتماد جهازك لفتح قاعدة البيانات</string>
<string name="autofill_service_name">KeePassDX نموذج الملء التلقائي</string>
<string name="unlock">فتح</string> <string name="unlock">فتح</string>
<string name="menu_app_settings_summary">البحث، القفل، التاريخ، الخصائص</string> <string name="menu_app_settings_summary">البحث، القفل، التاريخ، الخصائص</string>
<string name="menu_form_filling_settings_summary">لوحة المفاتيح، الملء التلقائي، الحافظة</string> <string name="menu_form_filling_settings_summary">لوحة المفاتيح، الملء التلقائي، الحافظة</string>

View File

@@ -221,7 +221,6 @@
<string name="device_credential">Cihazın şəxsiyyətini təsdiq edən məlumatları</string> <string name="device_credential">Cihazın şəxsiyyətini təsdiq edən məlumatları</string>
<string name="general">Ümumi</string> <string name="general">Ümumi</string>
<string name="autofill">Avtomatik doldurma</string> <string name="autofill">Avtomatik doldurma</string>
<string name="autofill_service_name">KeePassDX avtomatik doldurma formu</string>
<string name="autofill_sign_in_prompt">KeePassDX ile giriş edin</string> <string name="autofill_sign_in_prompt">KeePassDX ile giriş edin</string>
<string name="education_entry_new_field_title">Xüsusi bölmələr əlavə edin</string> <string name="education_entry_new_field_title">Xüsusi bölmələr əlavə edin</string>
<string name="education_add_attachment_title">Qoşma əlavə edin</string> <string name="education_add_attachment_title">Qoşma əlavə edin</string>
@@ -518,7 +517,7 @@
<string name="device_unlock_prompt_not_initialized">Cihaz kilid açma istəyini başlatmaq mümkün deyil.</string> <string name="device_unlock_prompt_not_initialized">Cihaz kilid açma istəyini başlatmaq mümkün deyil.</string>
<string name="autofill_explanation_summary">Digər tətbiqlərdə formları (anket) daha sürətli doldurmaq üçün avtomatik doldurma funksiyasını aktiv edin</string> <string name="autofill_explanation_summary">Digər tətbiqlərdə formları (anket) daha sürətli doldurmaq üçün avtomatik doldurma funksiyasını aktiv edin</string>
<string name="autofill_select_entry">Şifrə seç .…</string> <string name="autofill_select_entry">Şifrə seç .…</string>
<string name="set_autofill_service_title">Standart avtomatik doldurma xidmətini təyin edin</string> <string name="set_credential_provider_service_title">Standart avtomatik doldurma xidmətini təyin edin</string>
<string name="autofill_preference_title">Avtomatik doldurmanın parametrləri</string> <string name="autofill_preference_title">Avtomatik doldurmanın parametrləri</string>
<string name="password_size_title">Yaradılan şifrə həcmi</string> <string name="password_size_title">Yaradılan şifrə həcmi</string>
<string name="password_size_summary">Yaradılan şifrələrin standart həcmini təyin edər</string> <string name="password_size_summary">Yaradılan şifrələrin standart həcmini təyin edər</string>
@@ -567,7 +566,7 @@
<string name="autofill_web_domain_blocklist_summary">Veb domenlərin avtomatik olaraq doldurulmasını əngəlləyən bloklama siyahısı</string> <string name="autofill_web_domain_blocklist_summary">Veb domenlərin avtomatik olaraq doldurulmasını əngəlləyən bloklama siyahısı</string>
<string name="autofill_block">Avtomatik doldurmanı blokla</string> <string name="autofill_block">Avtomatik doldurmanı blokla</string>
<string name="autofill_block_restart">Bloklamanı aktiv etmək üçün anketin daxil olduğu tətbiqi yenidən başladın.</string> <string name="autofill_block_restart">Bloklamanı aktiv etmək üçün anketin daxil olduğu tətbiqi yenidən başladın.</string>
<string name="autofill_read_only_save">Yazma-qorumalı (dəyişməz) olaraq açılan məlumat bazasında yeni məlumatları yadda saxlamağa icazə verilmir.</string> <string name="error_save_read_only">Yazma-qorumalı (dəyişməz) olaraq açılan məlumat bazasında yeni məlumatları yadda saxlamağa icazə verilmir.</string>
<string name="allow_no_password_summary">Əgər şəxsiyyəti təsdiq edən məlumatlar seçilməyibsə, \"Aç\" düyməsinin sıxılmasına icazə ver</string> <string name="allow_no_password_summary">Əgər şəxsiyyəti təsdiq edən məlumatlar seçilməyibsə, \"Aç\" düyməsinin sıxılmasına icazə ver</string>
<string name="delete_entered_password_title">Şifrəni sil</string> <string name="delete_entered_password_title">Şifrəni sil</string>
<string name="delete_entered_password_summary">Məlumat bazasına bağlantı cəhdindən sonra daxil edilmiş şifrəni sil</string> <string name="delete_entered_password_summary">Məlumat bazasına bağlantı cəhdindən sonra daxil edilmiş şifrəni sil</string>

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