diff --git a/.gitignore b/.gitignore
index d02280737..3cd74fd62 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,9 @@ bin/
gen/
out/
+# Kotlin folder
+.kotlin/
+
# Gradle files
.gradle/
build/
diff --git a/app/build.gradle b/app/build.gradle
index 3d2414fe0..6ef748520 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -35,6 +35,10 @@ android {
}
}
+ buildFeatures {
+ buildConfig true
+ }
+
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
@@ -101,6 +105,10 @@ android {
buildFeatures {
buildConfig true
}
+
+ packaging {
+ resources.excludes.add("META-INF/versions/9/OSGI-INF/MANIFEST.MF") // necessary for bcpkix-jdk18on in crypto
+ }
}
def room_version = "2.5.1"
@@ -140,6 +148,9 @@ dependencies {
implementation 'commons-codec:commons-codec:1.15'
// Password generator
implementation 'me.gosimple:nbvcxz:1.5.0'
+
+ // Credentials Provider
+ implementation "androidx.credentials:credentials:1.2.2"
// Modules import
implementation project(path: ':database')
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 900abdea7..d9adc5a52 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -45,7 +45,8 @@
android:resizeableActivity="true"
android:supportsRtl="true"
android:theme="@style/KeepassDXStyle.Night"
- tools:targetApi="s">
+ tools:targetApi="s"
+ tools:ignore="CredentialDependency">
@@ -159,7 +160,7 @@
@@ -173,7 +174,7 @@
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity"
android:theme="@style/Theme.Transparent" />
@@ -199,7 +200,13 @@
-
+
@@ -239,7 +246,7 @@
@@ -249,6 +256,22 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/assets/trustedPackages.json b/app/src/main/assets/trustedPackages.json
new file mode 100644
index 000000000..8a24f37bb
--- /dev/null
+++ b/app/src/main/assets/trustedPackages.json
@@ -0,0 +1,548 @@
+{
+ "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.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"
+ }
+ ]
+ }
+ },
+ {
+ "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": "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"
+ }
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt
index 5da1544b8..5229e7995 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt
@@ -23,7 +23,6 @@ import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri
-import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
@@ -38,13 +37,10 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels
import androidx.appcompat.widget.Toolbar
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.BlendModeCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.ViewCompat
-import androidx.core.view.WindowCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
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.activities.fragments.EntryFragment
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.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.element.Attachment
import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.education.EntryActivityEducation
-import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.otp.OtpType
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.AttachmentFileBinderManager
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.view.WindowInsetPosition
import com.kunzisoft.keepass.view.applyWindowInsets
@@ -261,7 +257,7 @@ class EntryActivity : DatabaseLockActivity() {
mIcon = entryInfo.icon
// Assign title text
val entryTitle =
- if (entryInfo.title.isNotEmpty()) entryInfo.title else UuidUtil.toHexString(entryInfo.id)
+ entryInfo.title.ifEmpty { entryInfo.id.asHexString() }
collapsingToolbarLayout?.title = entryTitle
toolbar?.title = entryTitle
// Assign tags
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt
index 49f6544c6..d7f149202 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryEditActivity.kt
@@ -55,12 +55,15 @@ import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Compani
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
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.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
-import com.kunzisoft.keepass.autofill.AutofillComponent
-import com.kunzisoft.keepass.autofill.AutofillHelper
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
+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.element.Attachment
import com.kunzisoft.keepass.database.element.DateInstant
@@ -70,7 +73,6 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.template.Template
import com.kunzisoft.keepass.education.EntryEditActivityEducation
-import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.DataTime
import com.kunzisoft.keepass.model.EntryAttachmentState
@@ -376,18 +378,25 @@ class EntryEditActivity : DatabaseLockActivity(),
// Don't wait for saving if it's to provide autofill
mDatabase?.let { database ->
- EntrySelectionHelper.doSpecialAction(intent,
- {},
- {},
- {},
- {
+ EntrySelectionHelper.doSpecialAction(
+ intent = intent,
+ defaultAction = {},
+ searchAction = {},
+ saveAction = {},
+ keyboardSelectionAction = {
entryValidatedForKeyboardSelection(database, entrySave.newEntry)
},
- { _, _ ->
+ autofillSelectionAction = { _, _ ->
entryValidatedForAutofillSelection(database, entrySave.newEntry)
},
- {
+ autofillRegistrationAction = {
entryValidatedForAutofillRegistration(entrySave.newEntry)
+ },
+ passkeySelectionAction = {
+ entryValidatedForPasskeySelection(database, entrySave.newEntry)
+ },
+ passkeyRegistrationAction = {
+ entryValidatedForPasskeyRegistration(database, entrySave.newEntry)
}
)
}
@@ -430,25 +439,32 @@ class EntryEditActivity : DatabaseLockActivity(),
}
if (newNodes.size == 1) {
(newNodes[0] as? Entry?)?.let { entry ->
- EntrySelectionHelper.doSpecialAction(intent,
- {
+ EntrySelectionHelper.doSpecialAction(
+ intent = intent,
+ defaultAction = {
// Finish naturally
finishForEntryResult(entry)
},
- {
+ searchAction = {
// Nothing when search retrieved
},
- {
+ saveAction = {
entryValidatedForSave(entry)
},
- {
+ keyboardSelectionAction = {
entryValidatedForKeyboardSelection(database, entry)
},
- { _, _ ->
+ autofillSelectionAction = { _, _ ->
entryValidatedForAutofillSelection(database, entry)
},
- {
+ autofillRegistrationAction = {
entryValidatedForAutofillRegistration(entry)
+ },
+ passkeySelectionAction = {
+ entryValidatedForPasskeySelection(database, entry)
+ },
+ passkeyRegistrationAction = {
+ entryValidatedForPasskeyRegistration(database, entry)
}
)
}
@@ -488,9 +504,33 @@ class EntryEditActivity : DatabaseLockActivity(),
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()
- finishForEntryResult(entry)
}
override fun onResume() {
@@ -738,12 +778,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) {
// Assign entry callback as a result
try {
- val bundle = Bundle()
+ val bundle = buildEntryResult(entry)
val intentEntry = Intent()
- bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
intentEntry.putExtras(bundle)
setResult(Activity.RESULT_OK, intentEntry)
super.finish()
@@ -888,7 +933,7 @@ class EntryEditActivity : DatabaseLockActivity(),
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
val intent = Intent(activity, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId)
- AutofillHelper.startActivityForAutofillResult(
+ EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
activity,
intent,
activityResultLauncher,
@@ -899,21 +944,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?,
+ 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)
*/
fun launchToUpdateForRegistration(context: Context,
database: ContextualDatabase,
+ activityResultLauncher: ActivityResultLauncher?,
entryId: NodeId,
- registerInfo: RegisterInfo? = null) {
+ registerInfo: RegisterInfo?,
+ typeMode: TypeMode) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_ENTRY, entryId)
EntrySelectionHelper.startActivityForRegistrationModeResult(
context,
+ activityResultLauncher,
intent,
- registerInfo
+ registerInfo,
+ typeMode
)
}
}
@@ -924,16 +996,20 @@ class EntryEditActivity : DatabaseLockActivity(),
*/
fun launchToCreateForRegistration(context: Context,
database: ContextualDatabase,
+ activityResultLauncher: ActivityResultLauncher?,
groupId: NodeId<*>,
- registerInfo: RegisterInfo? = null) {
+ registerInfo: RegisterInfo? = null,
+ typeMode: TypeMode) {
if (database.loaded && !database.isReadOnly) {
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
val intent = Intent(context, EntryEditActivity::class.java)
intent.putExtra(KEY_PARENT, groupId)
EntrySelectionHelper.startActivityForRegistrationModeResult(
context,
+ activityResultLauncher,
intent,
- registerInfo
+ registerInfo,
+ typeMode
)
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt
index dffc3c586..081ca8d77 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/FileDatabaseSelectActivity.kt
@@ -44,15 +44,16 @@ import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
-import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
-import com.kunzisoft.keepass.activities.helpers.SpecialMode
+import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
-import com.kunzisoft.keepass.autofill.AutofillComponent
-import com.kunzisoft.keepass.autofill.AutofillHelper
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
+import com.kunzisoft.keepass.credentialprovider.TypeMode
+import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
@@ -98,10 +99,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
private var mExternalFileHelper: ExternalFileHelper? = null
- private var mAutofillActivityResultLauncher: ActivityResultLauncher? =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
- AutofillHelper.buildActivityResultLauncher(this)
- else null
+ private var mCredentialActivityResultLauncher: ActivityResultLauncher? =
+ this.buildActivityResultLauncher()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -298,7 +297,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
},
{ onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() },
- mAutofillActivityResultLauncher)
+ mCredentialActivityResultLauncher)
}
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
@@ -308,7 +307,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
{ onValidateSpecialMode() },
{ onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() },
- mAutofillActivityResultLauncher)
+ mCredentialActivityResultLauncher)
}
}
@@ -487,23 +486,46 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
activityResultLauncher: ActivityResultLauncher?,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo? = null) {
- AutofillHelper.startActivityForAutofillResult(activity,
+ EntrySelectionHelper.startActivityForAutofillSelectionModeResult(activity,
Intent(activity, FileDatabaseSelectActivity::class.java),
activityResultLauncher,
autofillComponent,
searchInfo)
}
+ /*
+ * -------------------------
+ * Passkey Launch
+ * -------------------------
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun launchForPasskeySelectionResult(activity: Activity,
+ activityResultLauncher: ActivityResultLauncher?,
+ searchInfo: SearchInfo? = null) {
+ EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
+ activity,
+ Intent(activity, FileDatabaseSelectActivity::class.java),
+ activityResultLauncher,
+ searchInfo
+ )
+ }
+
/*
* -------------------------
* Registration Launch
* -------------------------
*/
fun launchForRegistration(context: Context,
- registerInfo: RegisterInfo? = null) {
- EntrySelectionHelper.startActivityForRegistrationModeResult(context,
- Intent(context, FileDatabaseSelectActivity::class.java),
- registerInfo)
+ activityResultLauncher: ActivityResultLauncher?,
+ registerInfo: RegisterInfo? = null,
+ typeMode: TypeMode) {
+ EntrySelectionHelper.startActivityForRegistrationModeResult(
+ context,
+ activityResultLauncher,
+ Intent(context, FileDatabaseSelectActivity::class.java),
+ registerInfo,
+ typeMode
+ )
}
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt
index 03aacc6f2..eb6726cd8 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/GroupActivity.kt
@@ -63,13 +63,17 @@ import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment
import com.kunzisoft.keepass.activities.dialogs.MainCredentialDialogFragment
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
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.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
import com.kunzisoft.keepass.adapters.BreadcrumbAdapter
-import com.kunzisoft.keepass.autofill.AutofillComponent
-import com.kunzisoft.keepass.autofill.AutofillHelper
+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.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.MainCredential
import com.kunzisoft.keepass.database.element.DateInstant
@@ -83,7 +87,6 @@ import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.database.search.SearchParameters
import com.kunzisoft.keepass.education.GroupActivityEducation
-import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.DataTime
import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.model.RegisterInfo
@@ -264,10 +267,8 @@ class GroupActivity : DatabaseLockActivity(),
mGroupEditViewModel.selectIcon(icon)
}
- private var mAutofillActivityResultLauncher: ActivityResultLauncher? =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
- AutofillHelper.buildActivityResultLauncher(this)
- else null
+ private var mCredentialActivityResultLauncher: ActivityResultLauncher? =
+ this.buildActivityResultLauncher()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -484,59 +485,87 @@ class GroupActivity : DatabaseLockActivity(),
addNodeButtonView?.setAddEntryClickListener {
mDatabase?.let { database ->
mMainGroup?.let { currentGroup ->
- EntrySelectionHelper.doSpecialAction(intent,
- {
+ EntrySelectionHelper.doSpecialAction(
+ intent = intent,
+ defaultAction = {
mMainGroup?.nodeId?.let { currentParentGroupId ->
EntryEditActivity.launchToCreate(
- this@GroupActivity,
- database,
- currentParentGroupId,
- mEntryActivityResultLauncher
+ activity = this@GroupActivity,
+ database = database,
+ groupId = currentParentGroupId,
+ activityResultLauncher = mEntryActivityResultLauncher
)
}
},
- {
+ searchAction = {
// Search not used
},
- { searchInfo ->
+ saveAction = { searchInfo ->
EntryEditActivity.launchToCreateForSave(
- this@GroupActivity,
- database,
- currentGroup.nodeId,
- searchInfo
+ context = this@GroupActivity,
+ database = database,
+ groupId = currentGroup.nodeId,
+ searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
},
- { searchInfo ->
+ keyboardSelectionAction = { searchInfo ->
EntryEditActivity.launchForKeyboardSelectionResult(
- this@GroupActivity,
- database,
- currentGroup.nodeId,
- searchInfo
+ context = this@GroupActivity,
+ database = database,
+ groupId = currentGroup.nodeId,
+ searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
},
- { searchInfo, autofillComponent ->
+ autofillSelectionAction = { searchInfo, autofillComponent ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
EntryEditActivity.launchForAutofillResult(
- this@GroupActivity,
- database,
- mAutofillActivityResultLauncher,
- autofillComponent,
- currentGroup.nodeId,
- searchInfo
+ activity = this@GroupActivity,
+ database = database,
+ activityResultLauncher = mCredentialActivityResultLauncher,
+ autofillComponent = autofillComponent,
+ groupId = currentGroup.nodeId,
+ searchInfo = searchInfo
)
onLaunchActivitySpecialMode()
} else {
onCancelSpecialMode()
}
},
- { searchInfo ->
+ autofillRegistrationAction = { registerInfo ->
EntryEditActivity.launchToCreateForRegistration(
- this@GroupActivity,
- database,
- currentGroup.nodeId,
- searchInfo
+ context = this@GroupActivity,
+ database = database,
+ activityResultLauncher = null,
+ 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()
}
@@ -679,30 +708,40 @@ class GroupActivity : DatabaseLockActivity(),
when (actionTask) {
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
if (result.isSuccess) {
- EntrySelectionHelper.doSpecialAction(intent,
- {
+ EntrySelectionHelper.doSpecialAction(
+ intent = intent,
+ defaultAction = {
// Standard not used after task
},
- {
+ searchAction = {
// Search not used
},
- {
+ saveAction = {
// Save not used
},
- {
+ keyboardSelectionAction = {
// Keyboard selection
entry?.let {
entrySelectedForKeyboardSelection(database, it)
}
},
- { _, _ ->
+ autofillSelectionAction = { _, _ ->
// Autofill selection
entry?.let {
entrySelectedForAutofillSelection(database, it)
}
},
- {
+ autofillRegistrationAction = {
// Not use
+ },
+ passkeySelectionAction = {
+ // Passkey selection
+ entry?.let {
+ entrySelectedForPasskeySelection(database, it)
+ }
+ },
+ passkeyRegistrationAction = {
+ // TODO Passkey Registration
}
)
}
@@ -846,27 +885,28 @@ class GroupActivity : DatabaseLockActivity(),
Type.ENTRY -> try {
val entryVersioned = node as Entry
- EntrySelectionHelper.doSpecialAction(intent,
- {
+ EntrySelectionHelper.doSpecialAction(
+ intent = intent,
+ defaultAction = {
EntryActivity.launch(
- this@GroupActivity,
- database,
- entryVersioned.nodeId,
- mEntryActivityResultLauncher
+ activity = this@GroupActivity,
+ database = database,
+ entryId = entryVersioned.nodeId,
+ activityResultLauncher = mEntryActivityResultLauncher
)
// Do not reload group here
},
- {
+ searchAction = {
// Nothing here, a search is simply performed
},
- { searchInfo ->
+ saveAction = { searchInfo ->
if (!database.isReadOnly) {
entrySelectedForSave(database, entryVersioned, searchInfo)
loadGroup()
} else
finish()
},
- { searchInfo ->
+ keyboardSelectionAction = { searchInfo ->
if (!database.isReadOnly
&& searchInfo != null
&& PreferencesUtil.isKeyboardSaveSearchInfoEnable(this@GroupActivity)
@@ -876,7 +916,7 @@ class GroupActivity : DatabaseLockActivity(),
entrySelectedForKeyboardSelection(database, entryVersioned)
loadGroup()
},
- { searchInfo, _ ->
+ autofillSelectionAction = { searchInfo, _ ->
if (!database.isReadOnly
&& searchInfo != null
&& PreferencesUtil.isAutofillSaveSearchInfoEnable(this@GroupActivity)
@@ -886,9 +926,39 @@ class GroupActivity : DatabaseLockActivity(),
entrySelectedForAutofillSelection(database, entryVersioned)
loadGroup()
},
- { registerInfo ->
+ autofillRegistrationAction = { registerInfo ->
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
+ // TODO Passkey setting && PreferencesUtil.isAutofillSaveSearchInfoEnable(this@GroupActivity)
+ ) {
+ 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()
} else
finish()
@@ -934,18 +1004,33 @@ class GroupActivity : DatabaseLockActivity(),
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(
database: ContextualDatabase,
entry: Entry,
- registerInfo: RegisterInfo?
+ activityResultLauncher: ActivityResultLauncher?,
+ registerInfo: RegisterInfo?,
+ typeMode: TypeMode
) {
removeSearch()
// Registration to update the entry
EntryEditActivity.launchToUpdateForRegistration(
- this@GroupActivity,
- database,
- entry.nodeId,
- registerInfo
+ context = this@GroupActivity,
+ database = database,
+ activityResultLauncher = activityResultLauncher,
+ entryId = entry.nodeId,
+ registerInfo = registerInfo,
+ typeMode = typeMode
)
onLaunchActivitySpecialMode()
}
@@ -961,11 +1046,10 @@ class GroupActivity : DatabaseLockActivity(),
raw = true,
removeTemplateConfiguration = false
)
- val modification = entryInfo.saveSearchInfo(database, searchInfo)
+ // TODO Transform SearchInfo in RegisterInfo
+ entryInfo.saveSearchInfo(database, searchInfo)
newEntry.setEntryInfo(database, entryInfo)
- if (modification) {
- updateEntry(entry, newEntry)
- }
+ updateEntry(entry, newEntry)
}
private fun finishNodeAction() {
@@ -1569,19 +1653,19 @@ class GroupActivity : DatabaseLockActivity(),
* -------------------------
*/
@RequiresApi(api = Build.VERSION_CODES.O)
- fun launchForAutofillResult(activity: AppCompatActivity,
- database: ContextualDatabase,
- activityResultLaunch: ActivityResultLauncher?,
- autofillComponent: AutofillComponent,
- searchInfo: SearchInfo? = null,
- autoSearch: Boolean = false) {
+ fun launchForAutofillSelectionResult(activity: AppCompatActivity,
+ database: ContextualDatabase,
+ activityResultLauncher: ActivityResultLauncher?,
+ autofillComponent: AutofillComponent,
+ searchInfo: SearchInfo? = null,
+ autoSearch: Boolean = false) {
if (database.loaded) {
checkTimeAndBuildIntent(activity, null) { intent ->
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
- AutofillHelper.startActivityForAutofillResult(
+ EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
activity,
intent,
- activityResultLaunch,
+ activityResultLauncher,
autofillComponent,
searchInfo
)
@@ -1589,21 +1673,49 @@ class GroupActivity : DatabaseLockActivity(),
}
}
+ /*
+ * -------------------------
+ * Passkey Launch
+ * -------------------------
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ fun launchForPasskeySelectionResult(context: Context,
+ database: ContextualDatabase,
+ activityResultLauncher: ActivityResultLauncher?,
+ 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
* -------------------------
*/
fun launchForRegistration(context: Context,
+ activityResultLauncher: ActivityResultLauncher?,
database: ContextualDatabase,
- registerInfo: RegisterInfo? = null) {
+ registerInfo: RegisterInfo? = null,
+ typeMode: TypeMode) {
if (database.loaded && !database.isReadOnly) {
checkTimeAndBuildIntent(context, null) { intent ->
intent.putExtra(AUTO_SEARCH_KEY, false)
EntrySelectionHelper.startActivityForRegistrationModeResult(
context,
+ activityResultLauncher,
intent,
- registerInfo
+ registerInfo,
+ typeMode
)
}
}
@@ -1619,153 +1731,232 @@ class GroupActivity : DatabaseLockActivity(),
onValidateSpecialMode: () -> Unit,
onCancelSpecialMode: () -> Unit,
onLaunchActivitySpecialMode: () -> Unit,
- autofillActivityResultLauncher: ActivityResultLauncher?) {
- EntrySelectionHelper.doSpecialAction(activity.intent,
- {
- // Default action
- launch(
- activity,
+ activityResultLauncher: ActivityResultLauncher?) {
+ EntrySelectionHelper.doSpecialAction(
+ intent = activity.intent,
+ defaultAction = {
+ // Default action
+ launch(
+ activity,
+ database,
+ true
+ )
+ },
+ searchAction = { searchInfo ->
+ // Search action
+ if (database.loaded) {
+ launchForSearchResult(activity,
database,
- true
- )
- },
- { searchInfo ->
- // Search action
- if (database.loaded) {
- launchForSearchResult(activity,
+ searchInfo,
+ true)
+ onLaunchActivitySpecialMode()
+ } else {
+ // Simply close if database not opened
+ onCancelSpecialMode()
+ }
+ },
+ saveAction = { searchInfo ->
+ // Save info
+ if (database.loaded) {
+ if (!database.isReadOnly) {
+ launchForSaveResult(
+ activity,
database,
searchInfo,
- true)
+ false
+ )
onLaunchActivitySpecialMode()
} else {
- // Simply close if database not opened
+ Toast.makeText(
+ activity.applicationContext,
+ R.string.autofill_read_only_save,
+ Toast.LENGTH_LONG
+ )
+ .show()
onCancelSpecialMode()
}
- },
- { searchInfo ->
- // Save info
- if (database.loaded) {
- if (!database.isReadOnly) {
- launchForSaveResult(
- activity,
- database,
- searchInfo,
- false
- )
- onLaunchActivitySpecialMode()
- } else {
- Toast.makeText(
- activity.applicationContext,
- R.string.autofill_read_only_save,
- 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()
- }
+ }
+ },
+ keyboardSelectionAction = { searchInfo ->
+ // Keyboard selection
+ SearchHelper.checkAutoSearchInfo(
+ context = activity,
+ database = database,
+ searchInfo = searchInfo,
+ onItemsFound = { _, items ->
+ MagikeyboardService.performSelection(
+ items,
+ { entryInfo ->
+ // Keyboard populated
+ MagikeyboardService.populateKeyboardAndMoveAppToBackground(
+ activity,
+ entryInfo
)
+ onValidateSpecialMode()
},
- {
- // Here no search info found, disable auto search
+ { autoSearch ->
launchForKeyboardSelectionResult(activity,
database,
searchInfo,
- false)
+ autoSearch)
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()
+ }
)
- },
- { searchInfo, autofillComponent ->
- // Autofill selection
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- SearchHelper.checkAutoSearchInfo(activity,
- database,
- searchInfo,
- { openedDatabase, items ->
- // Response is build
- AutofillHelper.buildResponseAndSetResult(activity, openedDatabase, items)
+ } else {
+ onCancelSpecialMode()
+ }
+ },
+ autofillRegistrationAction = { registerInfo ->
+ // Autofill registration
+ if (!database.isReadOnly) {
+ SearchHelper.checkAutoSearchInfo(
+ context = activity,
+ 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 {
+ Toast.makeText(activity.applicationContext,
+ R.string.autofill_read_only_save,
+ Toast.LENGTH_LONG)
+ .show()
+ 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()
},
- {
- // Here no search info found, disable auto search
- launchForAutofillResult(activity,
- database,
- autofillActivityResultLauncher,
- autofillComponent,
- searchInfo,
- false)
+ actionEntrySelection = {
+ launchForPasskeySelectionResult(
+ context = activity,
+ database = database,
+ searchInfo = searchInfo,
+ activityResultLauncher = activityResultLauncher,
+ autoSearch = true
+ )
onLaunchActivitySpecialMode()
- },
- {
- // Simply close if database not opened, normally not happened
- onCancelSpecialMode()
}
- )
- } else {
- onCancelSpecialMode()
- }
- },
- { registerInfo ->
- // Autofill registration
- if (!database.isReadOnly) {
- SearchHelper.checkAutoSearchInfo(activity,
- database,
- registerInfo?.searchInfo,
- { _, _ ->
- // No auto search, it's a registration
- launchForRegistration(activity,
- database,
- registerInfo)
- onLaunchActivitySpecialMode()
- },
- {
- // Here no search info found, disable auto search
- launchForRegistration(activity,
- database,
- registerInfo)
- onLaunchActivitySpecialMode()
- },
- {
- // Simply close if database not opened, normally not happened
- onCancelSpecialMode()
- }
- )
- } else {
- Toast.makeText(activity.applicationContext,
- R.string.autofill_read_only_save,
- Toast.LENGTH_LONG)
- .show()
- onCancelSpecialMode()
- }
- })
+ )
+ },
+ onItemNotFound = {
+ // Here no search info found, disable auto search
+ launchForPasskeySelectionResult(
+ context = activity,
+ database = database,
+ searchInfo = searchInfo,
+ activityResultLauncher = activityResultLauncher
+ )
+ onLaunchActivitySpecialMode()
+ },
+ onDatabaseClosed = {
+ // Simply close if database not opened, normally not happened
+ onCancelSpecialMode()
+ }
+ )
+ } else {
+ onCancelSpecialMode()
+ }
+ },
+ passkeyRegistrationAction = { registerInfo ->
+ // Passkey registration
+ if (!database.isReadOnly) {
+ launchForRegistration(
+ context = activity,
+ activityResultLauncher = activityResultLauncher,
+ database = database,
+ registerInfo = registerInfo,
+ typeMode = TypeMode.PASSKEY
+ )
+ onLaunchActivitySpecialMode()
+ } else {
+ Toast.makeText(activity.applicationContext,
+ R.string.autofill_read_only_save,
+ Toast.LENGTH_LONG)
+ .show()
+ onCancelSpecialMode()
+ }
+ }
+ )
}
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt
index cd45b54a3..b40e6b33a 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/MainCredentialActivity.kt
@@ -48,16 +48,17 @@ import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.snackbar.Snackbar
import com.kunzisoft.keepass.R
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.SpecialMode
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
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.DeviceUnlockManager
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.MainCredential
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
@@ -75,8 +76,8 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
-import com.kunzisoft.keepass.settings.DeviceUnlockSettingsActivity
import com.kunzisoft.keepass.settings.AppearanceSettingsActivity
+import com.kunzisoft.keepass.settings.DeviceUnlockSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
@@ -122,10 +123,8 @@ class MainCredentialActivity : DatabaseModeActivity() {
private var mReadOnly: Boolean = false
private var mForceReadOnly: Boolean = false
- private var mAutofillActivityResultLauncher: ActivityResultLauncher? =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
- AutofillHelper.buildActivityResultLauncher(this)
- else null
+ private var mCredentialActivityResultLauncher: ActivityResultLauncher? =
+ this.buildActivityResultLauncher()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -430,7 +429,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
{ onValidateSpecialMode() },
{ onCancelSpecialMode() },
{ onLaunchActivitySpecialMode() },
- mAutofillActivityResultLauncher
+ mCredentialActivityResultLauncher
)
}
}
@@ -845,14 +844,14 @@ class MainCredentialActivity : DatabaseModeActivity() {
@RequiresApi(api = Build.VERSION_CODES.O)
@Throws(FileNotFoundException::class)
fun launchForAutofillResult(activity: AppCompatActivity,
+ activityResultLauncher: ActivityResultLauncher?,
databaseFile: Uri,
keyFile: Uri?,
hardwareKey: HardwareKey?,
- activityResultLauncher: ActivityResultLauncher?,
autofillComponent: AutofillComponent,
searchInfo: SearchInfo?) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
- AutofillHelper.startActivityForAutofillResult(
+ EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
activity,
intent,
activityResultLauncher,
@@ -861,21 +860,51 @@ class MainCredentialActivity : DatabaseModeActivity() {
}
}
+ /*
+ * -------------------------
+ * Passkey Launch
+ * -------------------------
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @Throws(FileNotFoundException::class)
+ fun launchForPasskeyResult(activity: Activity,
+ activityResultLauncher: ActivityResultLauncher?,
+ databaseFile: Uri,
+ keyFile: Uri?,
+ hardwareKey: HardwareKey?,
+ searchInfo: SearchInfo?) {
+ buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
+ EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
+ activity,
+ intent,
+ activityResultLauncher,
+ searchInfo
+ )
+ }
+ }
+
/*
* -------------------------
* Registration Launch
* -------------------------
*/
- fun launchForRegistration(activity: Activity,
- databaseFile: Uri,
- keyFile: Uri?,
- hardwareKey: HardwareKey?,
- registerInfo: RegisterInfo?) {
+ fun launchForRegistration(
+ activity: Activity,
+ activityResultLauncher: ActivityResultLauncher?,
+ databaseFile: Uri,
+ keyFile: Uri?,
+ hardwareKey: HardwareKey?,
+ typeMode: TypeMode,
+ registerInfo: RegisterInfo?
+ ) {
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
EntrySelectionHelper.startActivityForRegistrationModeResult(
- activity,
- intent,
- registerInfo)
+ context = activity,
+ activityResultLauncher = activityResultLauncher,
+ intent = intent,
+ typeMode = typeMode,
+ registerInfo = registerInfo
+ )
}
}
@@ -891,74 +920,104 @@ class MainCredentialActivity : DatabaseModeActivity() {
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
onCancelSpecialMode: () -> Unit,
onLaunchActivitySpecialMode: () -> Unit,
- autofillActivityResultLauncher: ActivityResultLauncher?) {
+ activityResultLauncher: ActivityResultLauncher?) {
try {
- EntrySelectionHelper.doSpecialAction(activity.intent,
- {
- launch(
- activity,
- databaseUri,
- keyFile,
- hardwareKey
- )
- },
- { searchInfo -> // Search Action
- launchForSearchResult(
- activity,
- databaseUri,
- keyFile,
- hardwareKey,
- searchInfo
- )
- onLaunchActivitySpecialMode()
- },
- { searchInfo -> // Save Action
- launchForSaveResult(
- activity,
- databaseUri,
- keyFile,
- hardwareKey,
- searchInfo
- )
- onLaunchActivitySpecialMode()
- },
- { searchInfo -> // Keyboard Selection Action
- launchForKeyboardResult(
- activity,
- databaseUri,
- keyFile,
- hardwareKey,
- searchInfo
- )
- onLaunchActivitySpecialMode()
- },
- { searchInfo, autofillComponent -> // Autofill Selection Action
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- launchForAutofillResult(
- activity,
- databaseUri,
- keyFile,
- hardwareKey,
- autofillActivityResultLauncher,
- autofillComponent,
- searchInfo
- )
- onLaunchActivitySpecialMode()
- } else {
- onCancelSpecialMode()
- }
- },
- { registerInfo -> // Registration Action
- launchForRegistration(
- activity,
- databaseUri,
- keyFile,
- hardwareKey,
- registerInfo
+ EntrySelectionHelper.doSpecialAction(
+ intent = activity.intent,
+ defaultAction = {
+ launch(
+ activity = activity,
+ databaseFile = databaseUri,
+ keyFile = keyFile,
+ hardwareKey = hardwareKey
+ )
+ },
+ searchAction = { searchInfo ->
+ launchForSearchResult(
+ activity = activity,
+ databaseFile = databaseUri,
+ keyFile = keyFile,
+ hardwareKey = hardwareKey,
+ searchInfo = searchInfo
+ )
+ onLaunchActivitySpecialMode()
+ },
+ saveAction = { searchInfo ->
+ launchForSaveResult(
+ activity = activity,
+ databaseFile = databaseUri,
+ keyFile = keyFile,
+ hardwareKey = hardwareKey,
+ searchInfo = searchInfo
+ )
+ onLaunchActivitySpecialMode()
+ },
+ keyboardSelectionAction = { searchInfo ->
+ launchForKeyboardResult(
+ activity = activity,
+ databaseFile = databaseUri,
+ keyFile = keyFile,
+ hardwareKey = hardwareKey,
+ searchInfo = searchInfo
+ )
+ onLaunchActivitySpecialMode()
+ },
+ autofillSelectionAction = { searchInfo, autofillComponent ->
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ launchForAutofillResult(
+ activity = activity,
+ activityResultLauncher = activityResultLauncher,
+ databaseFile = databaseUri,
+ keyFile = keyFile,
+ hardwareKey = hardwareKey,
+ autofillComponent = autofillComponent,
+ searchInfo = searchInfo
)
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) {
fileNoFoundAction(e)
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseDialogFragment.kt
index a8d53f9ee..2f6e0500c 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseDialogFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/DatabaseDialogFragment.kt
@@ -45,6 +45,7 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
}
@Suppress("DEPRECATION")
+ @Deprecated(message = "")
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupDialogFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupDialogFragment.kt
index 593a99d6f..f5c2e81e9 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupDialogFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/dialogs/GroupDialogFragment.kt
@@ -36,7 +36,7 @@ import com.kunzisoft.keepass.database.element.icon.IconImage
import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.settings.PreferencesUtil
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.view.DateTimeFieldView
@@ -155,7 +155,7 @@ class GroupDialogFragment : DatabaseDialogFragment() {
searchableView.text = stringFromInheritableBoolean(mGroupInfo.searchable)
autoTypeView.text = stringFromInheritableBoolean(mGroupInfo.enableAutoType,
mGroupInfo.defaultAutoTypeSequence)
- val uuid = UuidUtil.toHexString(mGroupInfo.id)
+ val uuid = mGroupInfo.id?.asHexString()
if (uuid == null || uuid.isEmpty()) {
uuidContainerView.visibility = View.GONE
} else {
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryFragment.kt
index a9de1526c..63b8c4601 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/EntryFragment.kt
@@ -24,7 +24,7 @@ import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.ClipboardHelper
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.hideByFading
import com.kunzisoft.keepass.view.showByFading
@@ -184,7 +184,7 @@ class EntryFragment: DatabaseFragment() {
// customDataView.text = entryInfo?.customData?.toString()
// Assign special data
- uuidReferenceView.text = UuidUtil.toHexString(entryInfo?.id)
+ uuidReferenceView.text = entryInfo?.id?.asHexString()
}
private fun showClipboardDialog() {
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt
index 7a7cf5acf..5dd676ea8 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/fragments/GroupFragment.kt
@@ -36,8 +36,8 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
-import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
-import com.kunzisoft.keepass.activities.helpers.SpecialMode
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
+import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.adapters.NodesAdapter
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.Group
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/TypeMode.kt b/app/src/main/java/com/kunzisoft/keepass/activities/helpers/TypeMode.kt
deleted file mode 100644
index 2269c0b01..000000000
--- a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/TypeMode.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.kunzisoft.keepass.activities.helpers
-
-enum class TypeMode {
- DEFAULT, MAGIKEYBOARD, AUTOFILL
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseActivity.kt
index a38b173ed..05a3a47e5 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseActivity.kt
@@ -5,14 +5,14 @@ import android.os.Bundle
import androidx.activity.viewModels
import com.kunzisoft.keepass.activities.stylish.StylishActivity
import com.kunzisoft.keepass.database.ContextualDatabase
-import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.DatabaseTaskProvider
+import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.getBinaryDir
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
-abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
+abstract class DatabaseActivity : StylishActivity(), DatabaseRetrieval {
protected val mDatabaseViewModel: DatabaseViewModel by viewModels()
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
@@ -77,7 +77,13 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
cipherEncryptDatabase: CipherEncryptDatabase?,
fixDuplicateUuid: Boolean
) {
- mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEncryptDatabase, fixDuplicateUuid)
+ mDatabaseTaskProvider?.startDatabaseLoad(
+ databaseUri,
+ mainCredential,
+ readOnly,
+ cipherEncryptDatabase,
+ fixDuplicateUuid
+ )
}
protected fun closeDatabase() {
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt
index 9f02b639b..ba30736a0 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseLockActivity.kt
@@ -34,8 +34,8 @@ import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
-import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
-import com.kunzisoft.keepass.activities.helpers.SpecialMode
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
+import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.element.Entry
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseModeActivity.kt b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseModeActivity.kt
index 640682278..ba1a200ee 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseModeActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/activities/legacy/DatabaseModeActivity.kt
@@ -5,9 +5,11 @@ import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import com.kunzisoft.keepass.R
-import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
-import com.kunzisoft.keepass.activities.helpers.SpecialMode
-import com.kunzisoft.keepass.activities.helpers.TypeMode
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode
+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.settings.PreferencesUtil
import com.kunzisoft.keepass.view.ToolbarSpecial
@@ -42,14 +44,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
/**
* Intent sender uses special retains data in callback
*/
- private fun isIntentSender(): Boolean {
- return (mSpecialMode == SpecialMode.SELECTION
- && mTypeMode == TypeMode.AUTOFILL)
- /* TODO Registration callback #765
- || (mSpecialMode == SpecialMode.REGISTRATION
- && mTypeMode == TypeMode.AUTOFILL
- && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
- */
+ protected fun isIntentSender(): Boolean {
+ return isIntentSenderMode(mSpecialMode, mTypeMode)
}
fun onLaunchActivitySpecialMode() {
@@ -118,7 +114,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(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)
// To show the selection mode
@@ -136,12 +133,13 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
TypeMode.DEFAULT, // Not important because hidden
TypeMode.MAGIKEYBOARD -> R.string.magic_keyboard_title
TypeMode.AUTOFILL -> R.string.autofill
+ TypeMode.PASSKEY -> R.string.passkey
}
title = getString(selectionModeStringId)
if (mTypeMode != TypeMode.DEFAULT)
title = "$title (${getString(typeModeStringId)})"
// Populate subtitle
- subtitle = searchInfo?.getName(resources)
+ subtitle = registerInfo?.getName(resources) ?: searchInfo?.getName(resources)
// Show the toolbar or not
visible = when (mSpecialMode) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/NodesAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/NodesAdapter.kt
index b58005093..67b2f7095 100644
--- a/app/src/main/java/com/kunzisoft/keepass/adapters/NodesAdapter.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/adapters/NodesAdapter.kt
@@ -418,6 +418,7 @@ class NodesAdapter (
}
}
+ // OTP
val otpElement = entry.getOtpElement()
holder.otpContainer?.removeCallbacks(holder.otpRunnable)
if (otpElement != null
@@ -438,7 +439,11 @@ class NodesAdapter (
holder.otpContainer?.visibility = View.GONE
}
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
assignBackgroundColor(holder.container, entry)
@@ -451,6 +456,7 @@ class NodesAdapter (
holder.otpToken?.setTextColor(foregroundColor)
holder.otpProgress?.setIndicatorColor(foregroundColor)
holder.attachmentIcon?.setColorFilter(foregroundColor)
+ holder.passkeyIcon?.setColorFilter(foregroundColor)
holder.meta.setTextColor(foregroundColor)
iconColor = foregroundColor
} else {
@@ -459,6 +465,7 @@ class NodesAdapter (
holder.otpToken?.setTextColor(mTextColorSecondary)
holder.otpProgress?.setIndicatorColor(mTextColorSecondary)
holder.attachmentIcon?.setColorFilter(mTextColorSecondary)
+ holder.passkeyIcon?.setColorFilter(mTextColorSecondary)
holder.meta.setTextColor(mTextColor)
}
} else {
@@ -467,6 +474,7 @@ class NodesAdapter (
holder.otpToken?.setTextColor(mColorOnSecondary)
holder.otpProgress?.setIndicatorColor(mColorOnSecondary)
holder.attachmentIcon?.setColorFilter(mColorOnSecondary)
+ holder.passkeyIcon?.setColorFilter(mColorOnSecondary)
holder.meta.setTextColor(mColorOnSecondary)
}
@@ -611,6 +619,7 @@ class NodesAdapter (
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
+ var passkeyIcon: ImageView? = itemView.findViewById(R.id.node_passkey_icon)
}
companion object {
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/EntrySelectionHelper.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/EntrySelectionHelper.kt
similarity index 54%
rename from app/src/main/java/com/kunzisoft/keepass/activities/helpers/EntrySelectionHelper.kt
rename to app/src/main/java/com/kunzisoft/keepass/credentialprovider/EntrySelectionHelper.kt
index 31a988772..6625420f4 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/EntrySelectionHelper.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/EntrySelectionHelper.kt
@@ -17,17 +17,32 @@
* along with KeePassDX. If not, see .
*
*/
-package com.kunzisoft.keepass.activities.helpers
+package com.kunzisoft.keepass.credentialprovider
+import android.app.Activity
import android.content.Context
import android.content.Intent
+import android.graphics.drawable.Icon
import android.os.Build
-import com.kunzisoft.keepass.autofill.AutofillComponent
-import com.kunzisoft.keepass.autofill.AutofillHelper
+import android.util.Log
+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.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.getParcelableExtraCompat
import com.kunzisoft.keepass.utils.putEnumExtra
object EntrySelectionHelper {
@@ -37,6 +52,33 @@ object EntrySelectionHelper {
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
+ /**
+ * 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 {
+ return this.registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) {
+ val resultCode = it.resultCode
+ if (resultCode == Activity.RESULT_OK) {
+ this.setResult(resultCode, dataTransformation(it.data))
+ }
+ if (resultCode == Activity.RESULT_CANCELED) {
+ this.setResult(Activity.RESULT_CANCELED)
+ }
+ this.finish()
+
+ if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(this)) {
+ // Close the database
+ this.sendBroadcast(Intent(LOCK_ACTION))
+ }
+ }
+ }
+
fun startActivityForSearchModeResult(context: Context,
intent: Intent,
searchInfo: SearchInfo) {
@@ -66,15 +108,52 @@ object EntrySelectionHelper {
context.startActivity(intent)
}
- fun startActivityForRegistrationModeResult(context: Context,
- intent: Intent,
- registerInfo: RegisterInfo?) {
- addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
- // At the moment, only autofill for registration
+ /**
+ * Utility method to start an activity with an Autofill for result
+ */
+ @RequiresApi(Build.VERSION_CODES.O)
+ fun startActivityForAutofillSelectionModeResult(
+ context: Context,
+ intent: Intent,
+ activityResultLauncher: ActivityResultLauncher?,
+ autofillComponent: AutofillComponent,
+ searchInfo: SearchInfo?
+ ) {
+ addSpecialModeInIntent(intent, SpecialMode.SELECTION)
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?,
+ searchInfo: SearchInfo?
+ ) {
+ addSpecialModeInIntent(intent, SpecialMode.SELECTION)
+ addTypeModeInIntent(intent, TypeMode.PASSKEY)
+ addSearchInfoInIntent(intent, searchInfo)
+ activityResultLauncher?.launch(intent)
+ }
+
+ fun startActivityForRegistrationModeResult(
+ context: Context?,
+ activityResultLauncher: ActivityResultLauncher?,
+ intent: Intent,
+ registerInfo: RegisterInfo?,
+ typeMode: TypeMode
+ ) {
+ addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
+ addTypeModeInIntent(intent, typeMode)
addRegisterInfoInIntent(intent, registerInfo)
- intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
- context.startActivity(intent)
+ if (activityResultLauncher == null) {
+ 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?) {
@@ -103,8 +182,13 @@ object EntrySelectionHelper {
}
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
+ // TODO Replace by Intent.addSpecialMode
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 {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -115,8 +199,13 @@ object EntrySelectionHelper {
}
private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
+ // TODO Replace by Intent.addTypeMode
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 {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -131,6 +220,17 @@ object EntrySelectionHelper {
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,
defaultAction: () -> Unit,
searchAction: (searchInfo: SearchInfo) -> Unit,
@@ -138,7 +238,9 @@ object EntrySelectionHelper {
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
autofillSelectionAction: (searchInfo: SearchInfo?,
autofillComponent: AutofillComponent) -> Unit,
- autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
+ autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit,
+ passkeySelectionAction: (searchInfo: SearchInfo?) -> Unit,
+ passkeyRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
when (retrieveSpecialModeFromIntent(intent)) {
SpecialMode.DEFAULT -> {
@@ -186,6 +288,7 @@ object EntrySelectionHelper {
defaultAction.invoke()
}
TypeMode.MAGIKEYBOARD -> keyboardSelectionAction.invoke(searchInfo)
+ TypeMode.PASSKEY -> passkeySelectionAction.invoke(searchInfo)
else -> {
// In this case, error
removeModesFromIntent(intent)
@@ -202,10 +305,59 @@ object EntrySelectionHelper {
}
SpecialMode.REGISTRATION -> {
val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent)
- removeModesFromIntent(intent)
- removeInfoFromIntent(intent)
- autofillRegistrationAction.invoke(registerInfo)
+ if (!isIntentSenderMode(
+ specialMode = retrieveSpecialModeFromIntent(intent),
+ 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,
+ 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
+ }
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/SpecialMode.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/SpecialMode.kt
similarity index 65%
rename from app/src/main/java/com/kunzisoft/keepass/activities/helpers/SpecialMode.kt
rename to app/src/main/java/com/kunzisoft/keepass/credentialprovider/SpecialMode.kt
index 7b1dbc2ef..e9b11771d 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/helpers/SpecialMode.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/SpecialMode.kt
@@ -1,4 +1,4 @@
-package com.kunzisoft.keepass.activities.helpers
+package com.kunzisoft.keepass.credentialprovider
enum class SpecialMode {
DEFAULT,
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/TypeMode.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/TypeMode.kt
new file mode 100644
index 000000000..6dc5a443c
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/TypeMode.kt
@@ -0,0 +1,5 @@
+package com.kunzisoft.keepass.credentialprovider
+
+enum class TypeMode {
+ DEFAULT, MAGIKEYBOARD, AUTOFILL, PASSKEY
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/AutofillLauncherActivity.kt
similarity index 76%
rename from app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt
rename to app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/AutofillLauncherActivity.kt
index f69d1475f..c14a21fa0 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/AutofillLauncherActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/AutofillLauncherActivity.kt
@@ -17,9 +17,8 @@
* along with KeePassDX. If not, see .
*
*/
-package com.kunzisoft.keepass.activities
+package com.kunzisoft.keepass.credentialprovider.activity
-import android.app.Activity
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
@@ -30,13 +29,17 @@ import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.RequiresApi
import com.kunzisoft.keepass.R
-import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
-import com.kunzisoft.keepass.activities.helpers.SpecialMode
+import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
+import com.kunzisoft.keepass.activities.GroupActivity
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
-import com.kunzisoft.keepass.autofill.AutofillComponent
-import com.kunzisoft.keepass.autofill.AutofillHelper
-import com.kunzisoft.keepass.autofill.CompatInlineSuggestionsRequest
-import com.kunzisoft.keepass.autofill.KeeAutofillService
+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.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.helper.SearchHelper
import com.kunzisoft.keepass.model.RegisterInfo
@@ -48,10 +51,8 @@ import com.kunzisoft.keepass.utils.getParcelableExtraCompat
@RequiresApi(api = Build.VERSION_CODES.O)
class AutofillLauncherActivity : DatabaseModeActivity() {
- private var mAutofillActivityResultLauncher: ActivityResultLauncher? =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
- AutofillHelper.buildActivityResultLauncher(this, true)
- else null
+ private var mCredentialActivityResultLauncher: ActivityResultLauncher? =
+ this.buildActivityResultLauncher(lockDatabase = true)
override fun applyCustomStyle(): Boolean {
return false
@@ -72,7 +73,9 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
// To pass extra inline request
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- compatInlineSuggestionsRequest = bundle.getParcelableCompat(KEY_INLINE_SUGGESTION)
+ compatInlineSuggestionsRequest = bundle.getParcelableCompat(
+ KEY_INLINE_SUGGESTION
+ )
}
// Build search param
bundle.getParcelableCompat(KEY_SEARCH_INFO)?.let { searchInfo ->
@@ -102,7 +105,9 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
}
SpecialMode.REGISTRATION -> {
// To register info
- val registerInfo = intent.getParcelableExtraCompat(KEY_REGISTER_INFO)
+ val registerInfo = intent.getParcelableExtraCompat(
+ KEY_REGISTER_INFO
+ )
val searchInfo = SearchInfo(registerInfo?.searchInfo)
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
searchInfo.webDomain = concreteWebDomain
@@ -111,7 +116,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
}
else -> {
// Not an autofill call
- setResult(Activity.RESULT_CANCELED)
+ setResult(RESULT_CANCELED)
finish()
}
}
@@ -122,7 +127,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
autofillComponent: AutofillComponent?,
searchInfo: SearchInfo) {
if (autofillComponent == null) {
- setResult(Activity.RESULT_CANCELED)
+ setResult(RESULT_CANCELED)
finish()
} else if (KeeAutofillService.autofillAllowedFor(
applicationId = searchInfo.applicationId,
@@ -130,34 +135,39 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
context = this
)) {
// If database is open
- SearchHelper.checkAutoSearchInfo(this,
- database,
- searchInfo,
- { openedDatabase, items ->
+ SearchHelper.checkAutoSearchInfo(
+ context = this,
+ database = database,
+ searchInfo = searchInfo,
+ onItemsFound = { openedDatabase, items ->
// Items found
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
finish()
},
- { openedDatabase ->
+ onItemNotFound = { openedDatabase ->
// Show the database UI to select the entry
- GroupActivity.launchForAutofillResult(this,
+ GroupActivity.launchForAutofillSelectionResult(
+ this,
openedDatabase,
- mAutofillActivityResultLauncher,
+ mCredentialActivityResultLauncher,
autofillComponent,
searchInfo,
- false)
+ false
+ )
},
- {
+ onDatabaseClosed = {
// If database not open
- FileDatabaseSelectActivity.launchForAutofillResult(this,
- mAutofillActivityResultLauncher,
+ FileDatabaseSelectActivity.launchForAutofillResult(
+ this,
+ mCredentialActivityResultLauncher,
autofillComponent,
- searchInfo)
+ searchInfo
+ )
}
)
} else {
showBlockRestartMessage()
- setResult(Activity.RESULT_CANCELED)
+ setResult(RESULT_CANCELED)
finish()
}
}
@@ -171,38 +181,51 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
context = this
)) {
val readOnly = database?.isReadOnly != false
- SearchHelper.checkAutoSearchInfo(this,
- database,
- searchInfo,
- { openedDatabase, _ ->
+ SearchHelper.checkAutoSearchInfo(
+ context = this,
+ database = database,
+ searchInfo = searchInfo,
+ onItemsFound = { openedDatabase, _ ->
if (!readOnly) {
// Show the database UI to select the entry
- GroupActivity.launchForRegistration(this,
- openedDatabase,
- registerInfo)
+ GroupActivity.launchForRegistration(
+ context = this,
+ activityResultLauncher = null, // TODO Autofill result launcher #765
+ database = openedDatabase,
+ registerInfo = registerInfo,
+ typeMode = TypeMode.AUTOFILL
+ )
} else {
showReadOnlySaveMessage()
}
},
- { openedDatabase ->
+ onItemNotFound = { openedDatabase ->
if (!readOnly) {
// Show the database UI to select the entry
- GroupActivity.launchForRegistration(this,
- openedDatabase,
- registerInfo)
+ GroupActivity.launchForRegistration(
+ context = this,
+ activityResultLauncher = null, // TODO Autofill result launcher #765
+ database = openedDatabase,
+ registerInfo = registerInfo,
+ typeMode = TypeMode.AUTOFILL
+ )
} else {
showReadOnlySaveMessage()
}
},
- {
+ onDatabaseClosed = {
// If database not open
- FileDatabaseSelectActivity.launchForRegistration(this,
- registerInfo)
+ FileDatabaseSelectActivity.launchForRegistration(
+ context = this,
+ activityResultLauncher = null, // TODO Autofill result launcher #765
+ registerInfo = registerInfo,
+ typeMode = TypeMode.AUTOFILL
+ )
}
)
} else {
showBlockRestartMessage()
- setResult(Activity.RESULT_CANCELED)
+ setResult(RESULT_CANCELED)
}
finish()
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/activities/EntrySelectionLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/EntrySelectionLauncherActivity.kt
similarity index 59%
rename from app/src/main/java/com/kunzisoft/keepass/activities/EntrySelectionLauncherActivity.kt
rename to app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/EntrySelectionLauncherActivity.kt
index 679ab39ba..2c1bdb4f5 100644
--- a/app/src/main/java/com/kunzisoft/keepass/activities/EntrySelectionLauncherActivity.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/EntrySelectionLauncherActivity.kt
@@ -17,23 +17,25 @@
* along with KeePassDX. If not, see .
*
*/
-package com.kunzisoft.keepass.activities
+package com.kunzisoft.keepass.credentialprovider.activity
import android.content.Context
import android.content.Intent
-import android.net.Uri
import android.os.Bundle
import android.widget.Toast
+import androidx.core.net.toUri
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.credentialprovider.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.helper.SearchHelper
-import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.otp.OtpEntryFields
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
-import com.kunzisoft.keepass.utils.getParcelableCompat
import com.kunzisoft.keepass.utils.WebDomain
+import com.kunzisoft.keepass.utils.getParcelableCompat
/**
* Activity to search or select entry in database,
@@ -73,7 +75,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
if (OtpEntryFields.isOTPUri(extra))
otpString = extra
else
- sharedWebDomain = Uri.parse(extra).host
+ sharedWebDomain = extra.toUri().host
}
}
launchSelection(database, sharedWebDomain, otpString)
@@ -121,87 +123,105 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
// If database is open
val readOnly = database?.isReadOnly != false
- SearchHelper.checkAutoSearchInfo(this,
- database,
- searchInfo,
- { openedDatabase, items ->
- // Items found
- if (searchInfo.otpString != null) {
- if (!readOnly) {
- GroupActivity.launchForSaveResult(
+ SearchHelper.checkAutoSearchInfo(
+ context = this,
+ database = database,
+ searchInfo = searchInfo,
+ onItemsFound = { openedDatabase, items ->
+ // Items found
+ 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) {
+ MagikeyboardService.performSelection(
+ items,
+ { entryInfo ->
+ // Automatically populate keyboard
+ MagikeyboardService.populateKeyboardAndMoveAppToBackground(
+ this,
+ entryInfo
+ )
+ },
+ { autoSearch ->
+ GroupActivity.launchForKeyboardSelectionResult(
this,
openedDatabase,
searchInfo,
- false)
- } else {
- Toast.makeText(applicationContext,
- R.string.autofill_read_only_save,
- Toast.LENGTH_LONG)
- .show()
+ autoSearch
+ )
}
- } else if (searchShareForMagikeyboard) {
- MagikeyboardService.performSelection(
- items,
- { entryInfo ->
- // Automatically populate keyboard
- MagikeyboardService.populateKeyboardAndMoveAppToBackground(
- this,
- entryInfo
- )
- },
- { autoSearch ->
- GroupActivity.launchForKeyboardSelectionResult(this,
- openedDatabase,
- searchInfo,
- autoSearch)
- }
+ )
+ } else {
+ GroupActivity.launchForSearchResult(
+ this,
+ openedDatabase,
+ searchInfo,
+ true
+ )
+ }
+ },
+ onItemNotFound = { openedDatabase ->
+ // Show the database UI to select the entry
+ if (searchInfo.otpString != null) {
+ if (!readOnly) {
+ GroupActivity.launchForSaveResult(
+ this,
+ openedDatabase,
+ searchInfo,
+ false
)
} else {
- GroupActivity.launchForSearchResult(this,
- 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)
+ 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
+ )
}
+ },
+ 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
+ )
+ }
+ }
)
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt
new file mode 100644
index 000000000..7239038dc
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt
@@ -0,0 +1,400 @@
+/*
+ * 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 .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.activity
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.util.Log
+import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
+import androidx.annotation.RequiresApi
+import androidx.credentials.GetCredentialResponse
+import androidx.credentials.exceptions.GetCredentialUnknownException
+import androidx.credentials.provider.PendingIntentHandler
+import androidx.lifecycle.lifecycleScope
+import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
+import com.kunzisoft.keepass.activities.GroupActivity
+import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
+import com.kunzisoft.keepass.credentialprovider.SpecialMode
+import com.kunzisoft.keepass.credentialprovider.TypeMode
+import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
+import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
+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.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.database.ContextualDatabase
+import com.kunzisoft.keepass.database.element.node.NodeIdUUID
+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 kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.launch
+import java.io.IOException
+import java.io.InvalidObjectException
+import java.util.UUID
+
+@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class PasskeyLauncherActivity : DatabaseModeActivity() {
+
+ private var mUsageParameters: PublicKeyCredentialUsageParameters? = null
+ private var mCreationParameters: PublicKeyCredentialCreationParameters? = null
+ private var mPasskey: Passkey? = null
+
+ private var mPasskeySelectionActivityResultLauncher: ActivityResultLauncher? =
+ this.buildActivityResultLauncher(
+ lockDatabase = true,
+ dataTransformation = { intent ->
+ // Build a new formatted response from the selection response
+ val responseIntent = Intent()
+ try {
+ 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
+ )
+ )
+ )
+ } ?: run {
+ throw IOException("Usage parameters is null")
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Unable to create selection response for passkey", e)
+ showError(e)
+ }
+ // Return the response
+ responseIntent
+ }
+ )
+
+ private var mPasskeyRegistrationActivityResultLauncher: ActivityResultLauncher? =
+ this.buildActivityResultLauncher(
+ lockDatabase = true,
+ dataTransformation = { intent ->
+ // Build a new formatted response from the creation response
+ val responseIntent = Intent()
+ try {
+ 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
+ )
+ )
+ }
+ } else {
+ throw SecurityException("Passkey was modified before registration")
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Unable to create registration response for passkey", e)
+ showError(e)
+ }
+ responseIntent
+ }
+ )
+
+ override fun applyCustomStyle(): Boolean {
+ return false
+ }
+
+ override fun finishActivityIfReloadRequested(): Boolean {
+ return false
+ }
+
+ override fun onDatabaseRetrieved(database: ContextualDatabase?) {
+ super.onDatabaseRetrieved(database)
+
+ lifecycleScope.launch(CoroutineExceptionHandler { _, e ->
+ Log.e(TAG, "Passkey launch error", e)
+ showError(e)
+ setResult(RESULT_CANCELED)
+ finish()
+ }) {
+ val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo()
+ val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin(verified = false)
+ val nodeId = intent.retrieveNodeId()
+ checkSecurity(intent, nodeId)
+ when (mSpecialMode) {
+ SpecialMode.SELECTION -> {
+ launchSelection(database, nodeId, searchInfo, appOrigin)
+ }
+ SpecialMode.REGISTRATION -> {
+ // TODO Registration in predefined group
+ // launchRegistration(database, nodeId, mSearchInfo)
+ launchRegistration(database, null, searchInfo)
+ }
+ else -> {
+ throw InvalidObjectException("Passkey launch mode not supported")
+ }
+ }
+ }
+ }
+
+ private fun autoSelectPasskeyAndSetResult(
+ database: ContextualDatabase?,
+ nodeId: UUID,
+ appOrigin: AppOrigin
+ ) {
+ mUsageParameters?.let { usageParameters ->
+ // To get the passkey from the database
+ val passkey = database
+ ?.getEntryById(NodeIdUUID(nodeId))
+ ?.getEntryInfo(database)
+ ?.passkey
+ ?: throw GetCredentialUnknownException("No passkey with nodeId $nodeId found")
+
+ val result = Intent()
+ PendingIntentHandler.setGetCredentialResponse(
+ result,
+ GetCredentialResponse(
+ buildPasskeyPublicKeyCredential(
+ requestOptions = usageParameters.publicKeyCredentialRequestOptions,
+ clientDataResponse = getVerifiedGETClientDataResponse(
+ usageParameters = usageParameters,
+ appOrigin = appOrigin
+ ),
+ passkey = passkey
+ )
+ )
+ )
+ setResult(RESULT_OK, result)
+ finish()
+ } ?: run {
+ Log.e(TAG, "Unable to auto select passkey, usage parameters are empty")
+ setResult(RESULT_CANCELED)
+ finish()
+ }
+ }
+
+ private suspend fun launchSelection(
+ database: ContextualDatabase?,
+ nodeId: UUID?,
+ searchInfo: SearchInfo,
+ appOrigin: AppOrigin
+ ) {
+ Log.d(TAG, "Launch passkey selection")
+ retrievePasskeyUsageRequestParameters(
+ intent = intent,
+ assetManager = assets
+ ) { usageParameters ->
+ // Save the requested parameters
+ mUsageParameters = usageParameters
+ // Manage the passkey to use
+ nodeId?.let { nodeId ->
+ autoSelectPasskeyAndSetResult(database, nodeId, appOrigin)
+ } ?: run {
+ SearchHelper.checkAutoSearchInfo(
+ context = this,
+ database = database,
+ searchInfo = searchInfo,
+ onItemsFound = { _, _ ->
+ Log.w(
+ TAG, "Passkey found for auto selection, should not append," +
+ "use PasskeyProviderService instead"
+ )
+ finish()
+ },
+ onItemNotFound = { openedDatabase ->
+ Log.d(
+ TAG, "No Passkey found for selection," +
+ "launch manual selection in opened database"
+ )
+ GroupActivity.launchForPasskeySelectionResult(
+ context = this,
+ database = openedDatabase,
+ activityResultLauncher = mPasskeySelectionActivityResultLauncher,
+ searchInfo = null,
+ autoSearch = false
+ )
+ },
+ onDatabaseClosed = {
+ Log.d(TAG, "Manual passkey selection in closed database")
+ FileDatabaseSelectActivity.launchForPasskeySelectionResult(
+ activity = this,
+ activityResultLauncher = mPasskeySelectionActivityResultLauncher,
+ searchInfo = searchInfo,
+ )
+ }
+ )
+ }
+ }
+ }
+
+ private fun autoRegisterPasskeyAndSetResult(
+ database: ContextualDatabase?,
+ nodeId: UUID,
+ passkey: Passkey
+ ) {
+ // TODO Overwrite and Register in a predefined group
+ mCreationParameters?.let { creationParameters ->
+ // To set the passkey to the database
+ setResult(RESULT_OK)
+ finish()
+ } ?: run {
+ Log.e(TAG, "Unable to auto select passkey, usage parameters are empty")
+ setResult(RESULT_CANCELED)
+ finish()
+ }
+ }
+
+ private suspend fun launchRegistration(
+ database: ContextualDatabase?,
+ nodeId: UUID?,
+ searchInfo: SearchInfo
+ ) {
+ Log.d(TAG, "Launch passkey registration")
+ retrievePasskeyCreationRequestParameters(
+ intent = intent,
+ assetManager = assets,
+ 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 = this,
+ database = database,
+ searchInfo = searchInfo,
+ onItemsFound = { openedDatabase, _ ->
+ Log.w(TAG, "Passkey found for registration, " +
+ "but launch manual registration for a new entry")
+ GroupActivity.launchForRegistration(
+ context = this,
+ activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
+ database = openedDatabase,
+ registerInfo = registerInfo,
+ typeMode = TypeMode.PASSKEY
+ )
+ },
+ onItemNotFound = { openedDatabase ->
+ Log.d(TAG, "Launch new manual registration in opened database")
+ GroupActivity.launchForRegistration(
+ context = this,
+ activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
+ database = openedDatabase,
+ registerInfo = registerInfo,
+ typeMode = TypeMode.PASSKEY
+ )
+ },
+ onDatabaseClosed = {
+ Log.d(TAG, "Manual passkey registration in closed database")
+ FileDatabaseSelectActivity.launchForRegistration(
+ context = this,
+ activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
+ registerInfo = registerInfo,
+ typeMode = TypeMode.PASSKEY
+ )
+ }
+ )
+ }
+ }
+ )
+ }
+
+ private fun showError(e: Throwable) {
+ Toast.makeText(this, e.localizedMessage, Toast.LENGTH_LONG).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
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillComponent.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/AutofillComponent.kt
similarity index 78%
rename from app/src/main/java/com/kunzisoft/keepass/autofill/AutofillComponent.kt
rename to app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/AutofillComponent.kt
index bf8a2996f..3a4097cee 100644
--- a/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillComponent.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/AutofillComponent.kt
@@ -1,4 +1,4 @@
-package com.kunzisoft.keepass.autofill
+package com.kunzisoft.keepass.credentialprovider.autofill
import android.app.assist.AssistStructure
diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/AutofillHelper.kt
similarity index 88%
rename from app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt
rename to app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/AutofillHelper.kt
index b0af5bf53..e93039556 100644
--- a/app/src/main/java/com/kunzisoft/keepass/autofill/AutofillHelper.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/AutofillHelper.kt
@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see .
*
*/
-package com.kunzisoft.keepass.autofill
+package com.kunzisoft.keepass.credentialprovider.autofill
import android.annotation.SuppressLint
import android.app.Activity
@@ -40,17 +40,13 @@ import android.view.autofill.AutofillValue
import android.widget.RemoteViews
import android.widget.Toast
import android.widget.inline.InlinePresentationSpec
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
-import androidx.appcompat.app.AppCompatActivity
import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi
import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R
-import com.kunzisoft.keepass.activities.AutofillLauncherActivity
-import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
-import com.kunzisoft.keepass.activities.helpers.SpecialMode
+import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildIcon
+import com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.icon.IconImage
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.settings.AutofillSettingsActivity
import com.kunzisoft.keepass.settings.PreferencesUtil
-import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
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) {
struct.creditCardHolderId?.let { ccNameId ->
datasetBuilder.addValueToDatasetBuilder(
@@ -294,23 +289,6 @@ object AutofillHelper {
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")
@RequiresApi(Build.VERSION_CODES.R)
private fun buildInlinePresentationForEntry(context: Context,
@@ -353,7 +331,7 @@ object AutofillHelper {
Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
setTintBlendMode(BlendMode.DST)
})
- buildIconFromEntry(context, database, entryInfo)?.let { icon ->
+ entryInfo.buildIcon(context, database)?.let { icon ->
setEndIcon(icon.apply {
setTintBlendMode(BlendMode.DST)
})
@@ -534,7 +512,9 @@ object AutofillHelper {
StructureParser(structure).parse()?.let { result ->
// New Response
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtraCompat(EXTRA_INLINE_SUGGESTIONS_REQUEST)
+ val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtraCompat(
+ EXTRA_INLINE_SUGGESTIONS_REQUEST
+ )
if (compatInlineSuggestionsRequest != null) {
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
}
@@ -558,45 +538,14 @@ object AutofillHelper {
}
}
- fun buildActivityResultLauncher(activity: AppCompatActivity,
- lockDatabase: Boolean = false): ActivityResultLauncher {
- 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?,
- autofillComponent: AutofillComponent,
- searchInfo: SearchInfo?) {
- EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
- intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
+ fun Intent.addAutofillComponent(context: Context, autofillComponent: AutofillComponent) {
+ this.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
- && PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
+ && PreferencesUtil.isAutofillInlineSuggestionsEnable(context)) {
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
diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/CompatInlineSuggestionsRequest.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/CompatInlineSuggestionsRequest.kt
similarity index 97%
rename from app/src/main/java/com/kunzisoft/keepass/autofill/CompatInlineSuggestionsRequest.kt
rename to app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/CompatInlineSuggestionsRequest.kt
index 5705682b5..8fba6845b 100644
--- a/app/src/main/java/com/kunzisoft/keepass/autofill/CompatInlineSuggestionsRequest.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/CompatInlineSuggestionsRequest.kt
@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see .
*
*/
-package com.kunzisoft.keepass.autofill
+package com.kunzisoft.keepass.credentialprovider.autofill
import android.annotation.TargetApi
import android.os.Build
diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/KeeAutofillService.kt
similarity index 92%
rename from app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt
rename to app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/KeeAutofillService.kt
index 98979a9d9..9d832e3e1 100644
--- a/app/src/main/java/com/kunzisoft/keepass/autofill/KeeAutofillService.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/KeeAutofillService.kt
@@ -17,7 +17,7 @@
* along with KeePassDX. If not, see .
*
*/
-package com.kunzisoft.keepass.autofill
+package com.kunzisoft.keepass.credentialprovider.autofill
import android.annotation.SuppressLint
import android.app.PendingIntent
@@ -43,8 +43,8 @@ import androidx.annotation.RequiresApi
import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.v1.InlineSuggestionUi
import com.kunzisoft.keepass.R
-import com.kunzisoft.keepass.activities.AutofillLauncherActivity
-import com.kunzisoft.keepass.autofill.StructureParser.Companion.APPLICATION_ID_POPUP_WINDOW
+import com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity
+import com.kunzisoft.keepass.credentialprovider.autofill.StructureParser.Companion.APPLICATION_ID_POPUP_WINDOW
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.helper.SearchHelper
@@ -143,25 +143,28 @@ class KeeAutofillService : AutofillService() {
parseResult: StructureParser.Result,
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
callback: FillCallback) {
- SearchHelper.checkAutoSearchInfo(this,
- database,
- searchInfo,
- { openedDatabase, items ->
- callback.onSuccess(
- AutofillHelper.buildResponse(this, openedDatabase,
- items, parseResult, inlineSuggestionsRequest)
+ SearchHelper.checkAutoSearchInfo(
+ context = this,
+ database = database,
+ searchInfo = searchInfo,
+ onItemsFound = { openedDatabase, items ->
+ callback.onSuccess(
+ AutofillHelper.buildResponse(
+ this, openedDatabase,
+ items, parseResult, inlineSuggestionsRequest
)
- },
- { openedDatabase ->
- // Show UI if no search result
- showUIForEntrySelection(parseResult, openedDatabase,
- searchInfo, inlineSuggestionsRequest, callback)
- },
- {
- // Show UI if database not open
- showUIForEntrySelection(parseResult, null,
- searchInfo, inlineSuggestionsRequest, callback)
- }
+ )
+ },
+ onItemNotFound = { openedDatabase ->
+ // Show UI if no search result
+ showUIForEntrySelection(parseResult, openedDatabase,
+ searchInfo, inlineSuggestionsRequest, callback)
+ },
+ onDatabaseClosed = {
+ // Show UI if database not open
+ showUIForEntrySelection(parseResult, null,
+ searchInfo, inlineSuggestionsRequest, callback)
+ }
)
}
@@ -385,19 +388,21 @@ class KeeAutofillService : AutofillService() {
// Show UI to save data
val registerInfo = RegisterInfo(
- SearchInfo().apply {
- applicationId = parseResult.applicationId
- webDomain = parseResult.webDomain
- webScheme = parseResult.webScheme
- },
- parseResult.usernameValue?.textValue?.toString(),
- parseResult.passwordValue?.textValue?.toString(),
+ searchInfo = SearchInfo().apply {
+ applicationId = parseResult.applicationId
+ webDomain = parseResult.webDomain
+ webScheme = parseResult.webScheme
+ },
+ username = parseResult.usernameValue?.textValue?.toString(),
+ password = parseResult.passwordValue?.textValue?.toString(),
+ creditCard =
CreditCard(
parseResult.creditCardHolder,
parseResult.creditCardNumber,
expiration,
parseResult.cardVerificationValue
- ))
+ )
+ )
// TODO Callback in each activity #765
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/StructureParser.kt
similarity index 99%
rename from app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt
rename to app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/StructureParser.kt
index c4b7ad0c8..cfbda6df3 100644
--- a/app/src/main/java/com/kunzisoft/keepass/autofill/StructureParser.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/autofill/StructureParser.kt
@@ -16,7 +16,7 @@
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see .
*/
-package com.kunzisoft.keepass.autofill
+package com.kunzisoft.keepass.credentialprovider.autofill
import android.app.assist.AssistStructure
import android.os.Build
diff --git a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/Keyboard.java b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/magikeyboard/Keyboard.java
similarity index 99%
rename from app/src/main/java/com/kunzisoft/keepass/magikeyboard/Keyboard.java
rename to app/src/main/java/com/kunzisoft/keepass/credentialprovider/magikeyboard/Keyboard.java
index 313fa465f..b385a897c 100644
--- a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/Keyboard.java
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/magikeyboard/Keyboard.java
@@ -14,7 +14,7 @@
* the License.
*/
-package com.kunzisoft.keepass.magikeyboard;
+package com.kunzisoft.keepass.credentialprovider.magikeyboard;
import android.content.Context;
import android.content.res.Resources;
diff --git a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/KeyboardView.java b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/magikeyboard/KeyboardView.java
similarity index 98%
rename from app/src/main/java/com/kunzisoft/keepass/magikeyboard/KeyboardView.java
rename to app/src/main/java/com/kunzisoft/keepass/credentialprovider/magikeyboard/KeyboardView.java
index 1ee04d2ea..fc992247e 100644
--- a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/KeyboardView.java
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/magikeyboard/KeyboardView.java
@@ -14,14 +14,14 @@
* 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.magikeyboard.MagikeyboardService.KEY_CHANGE_KEYBOARD;
-import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_ENTRY;
-import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_ENTRY_ALT;
-import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP;
-import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP_ALT;
+import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_BACK_KEYBOARD;
+import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_CHANGE_KEYBOARD;
+import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_ENTRY;
+import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_ENTRY_ALT;
+import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_OTP;
+import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_OTP_ALT;
import android.content.Context;
import android.content.res.TypedArray;
@@ -52,7 +52,7 @@ import android.widget.TextView;
import androidx.annotation.RequiresApi;
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.HashMap;
diff --git a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikeyboardService.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/magikeyboard/MagikeyboardService.kt
similarity index 93%
rename from app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikeyboardService.kt
rename to app/src/main/java/com/kunzisoft/keepass/credentialprovider/magikeyboard/MagikeyboardService.kt
index 34770e605..459d7a6a8 100644
--- a/app/src/main/java/com/kunzisoft/keepass/magikeyboard/MagikeyboardService.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/magikeyboard/MagikeyboardService.kt
@@ -18,7 +18,7 @@
*
*/
-package com.kunzisoft.keepass.magikeyboard
+package com.kunzisoft.keepass.credentialprovider.magikeyboard
import android.app.Activity
import android.content.Context
@@ -41,9 +41,9 @@ import androidx.core.graphics.BlendModeCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
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.credentialprovider.EntrySelectionHelper
+import com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.element.Field
@@ -324,9 +324,9 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
actionGoAutomatically()
}
KEY_FIELDS -> {
- getEntryInfo()?.customFields?.let { customFields ->
+ getEntryInfo()?.getCustomFieldsForFilling()?.let { customFields ->
fieldsAdapter?.apply {
- setFields(customFields.filter { it.name != OTP_TOKEN_FIELD})
+ setFields(customFields)
notifyDataSetChanged()
}
}
@@ -341,10 +341,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
}
private fun actionKeyEntry(searchInfo: SearchInfo? = null) {
- SearchHelper.checkAutoSearchInfo(this,
- mDatabase,
- searchInfo,
- { _, items ->
+ SearchHelper.checkAutoSearchInfo(
+ context = this,
+ database = mDatabase,
+ searchInfo = searchInfo,
+ onItemsFound = { _, items ->
performSelection(
items,
{
@@ -361,11 +362,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
}
)
},
- {
+ onItemNotFound = {
// Select if not found
launchEntrySelection(searchInfo)
},
- {
+ onDatabaseClosed = {
// Select if database not opened
removeEntryInfo()
launchEntrySelection(searchInfo)
@@ -463,21 +464,18 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
fun performSelection(items: List,
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
- if (items.size == 1) {
- val itemFound = items[0]
- if (entryUUID != itemFound.id) {
- actionPopulateKeyboard.invoke(itemFound)
- } else {
- // 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)
- } else {
- // Select an arbitrary one
- actionEntrySelection.invoke(false)
- }
+ EntrySelectionHelper.performSelection(
+ items = items,
+ actionPopulateCredentialProvider = { itemFound ->
+ if (entryUUID != itemFound.id) {
+ actionPopulateKeyboard.invoke(itemFound)
+ } else {
+ // Force selection if magikeyboard already populated
+ actionEntrySelection.invoke(false)
+ }
+ },
+ actionEntrySelection = actionEntrySelection
+ )
}
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt
new file mode 100644
index 000000000..6f6bcb652
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/PasskeyProviderService.kt
@@ -0,0 +1,355 @@
+/*
+ * 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 .
+ *
+ */
+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.helper.SearchHelper
+import com.kunzisoft.keepass.model.SearchInfo
+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
+
+ 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)
+ }
+ }
+
+ 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,
+ ) {
+ 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 = 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 {
+
+ val passkeyEntries: MutableList = 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 = true
+ )
+ )
+ }
+ }
+ },
+ 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_locked_database_username),
+ displayName = getString(R.string.passkey_selection_description),
+ icon = defaultIcon,
+ pendingIntent = pendingIntent,
+ beginGetPublicKeyCredentialOption = option,
+ lastUsedTime = Instant.now(),
+ isAutoSelectAllowed = false
+ )
+ )
+ }
+ },
+ 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_locked_database_username),
+ displayName = getString(R.string.passkey_locked_database_description),
+ icon = defaultIcon,
+ pendingIntent = pendingIntent,
+ beginGetPublicKeyCredentialOption = option,
+ lastUsedTime = Instant.now(),
+ isAutoSelectAllowed = true
+ )
+ )
+ }
+ }
+ )
+ return passkeyEntries
+ }
+
+ override fun onBeginCreateCredentialRequest(
+ request: BeginCreateCredentialRequest,
+ cancellationSignal: CancellationSignal,
+ callback: OutcomeReceiver,
+ ) {
+ Log.d(javaClass.simpleName, "onBeginCreateCredentialRequest called")
+ try {
+ processCreateCredentialRequest(request)?.let { response ->
+ callback.onResult(response)
+ } ?: let {
+ callback.onError(CreateCredentialUnknownException())
+ }
+ } catch (e: Exception) {
+ Log.e(javaClass.simpleName, "onBeginCreateCredentialRequest error", e)
+ callback.onError(CreateCredentialUnknownException())
+ }
+ }
+
+ private fun processCreateCredentialRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse? {
+ when (request) {
+ is BeginCreatePublicKeyCredentialRequest -> {
+ // Request is passkey type
+ return handleCreatePasskeyQuery(request)
+ }
+ }
+ // request type not supported
+ Log.w(javaClass.simpleName, "unknown type of BeginCreateCredentialRequest")
+ return null
+ }
+
+ private fun MutableList.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_locked_database_username)
+ val createEntries: MutableList = 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 CreateCredentialUnknownException(
+ "Unable to register or overwrite a passkey in a database that is read only"
+ )
+ } 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 CreateCredentialUnknownException(
+ "Unable to register a new passkey in a database that is read only"
+ )
+ } 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
+ ) {
+ // nothing to do
+ }
+
+ companion object {
+ private val TAG = PasskeyProviderService::class.java.simpleName
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAssertionResponse.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAssertionResponse.kt
new file mode 100644
index 000000000..3e22ee94b
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAssertionResponse.kt
@@ -0,0 +1,66 @@
+/*
+ * 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 .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.passkey.data
+
+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 {
+ signature = Signature.sign(privateKey, dataToSign())
+ ?: 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)
+ }
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAttestationResponse.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAttestationResponse.kt
new file mode 100644
index 000000000..07ac0ba45
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAttestationResponse.kt
@@ -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 .
+ *
+ */
+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()
+ ao.put("fmt", "none")
+ ao.put("attStmt", emptyMap())
+ 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()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorData.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorData.kt
new file mode 100644
index 000000000..6df2b10be
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorData.kt
@@ -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 .
+ *
+ */
+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)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorResponse.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorResponse.kt
new file mode 100644
index 000000000..57d9ab2dc
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorResponse.kt
@@ -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 .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.passkey.data
+
+import org.json.JSONObject
+
+interface AuthenticatorResponse {
+ var clientJson: JSONObject
+
+ fun json(): JSONObject
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/Cbor.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/Cbor.kt
new file mode 100644
index 000000000..8dbbb8657
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/Cbor.kt
@@ -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 = 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(byteMap.keys)
+ keysList.sortedWith(
+ Comparator { 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()
+ 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()
+ 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")
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataBuildResponse.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataBuildResponse.kt
new file mode 100644
index 000000000..0e9454d19
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataBuildResponse.kt
@@ -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 .
+ *
+ */
+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())
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataDefinedResponse.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataDefinedResponse.kt
new file mode 100644
index 000000000..ae323b904
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataDefinedResponse.kt
@@ -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 .
+ *
+ */
+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 = ""
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataResponse.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataResponse.kt
new file mode 100644
index 000000000..bdc5b3ec2
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataResponse.kt
@@ -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 .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.passkey.data
+
+interface ClientDataResponse {
+ fun hashData(): ByteArray
+ fun buildResponse(): String
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/FidoDataTypes.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/FidoDataTypes.kt
new file mode 100644
index 000000000..2f128d1d3
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/FidoDataTypes.kt
@@ -0,0 +1,40 @@
+/*
+ * 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
+)
+
+data class PublicKeyCredentialParameters(val type: String, val alg: Long)
+
+data class PublicKeyCredentialDescriptor(
+ val type: String,
+ val id: ByteArray,
+ val transports: List
+)
+
+data class AuthenticatorSelectionCriteria(
+ val authenticatorAttachment: String,
+ val residentKey: String,
+ val requireResidentKey: Boolean = false,
+ val userVerification: String = "preferred"
+)
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/FidoPublicKeyCredential.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/FidoPublicKeyCredential.kt
new file mode 100644
index 000000000..9fa66a8a8
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/FidoPublicKeyCredential.kt
@@ -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 .
+ *
+ */
+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()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationOptions.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationOptions.kt
new file mode 100644
index 000000000..b64634f63
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationOptions.kt
@@ -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 .
+ *
+ */
+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
+
+ var timeout: Long
+ var excludeCredentials: List
+ 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 = 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
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationParameters.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationParameters.kt
new file mode 100644
index 000000000..304fba835
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationParameters.kt
@@ -0,0 +1,29 @@
+/*
+ * 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 .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.passkey.data
+
+import java.security.KeyPair
+
+data class PublicKeyCredentialCreationParameters(
+ val publicKeyCredentialCreationOptions: PublicKeyCredentialCreationOptions,
+ val credentialId: ByteArray, // TODO Equals Hashcode
+ val signatureKey: Pair,
+ val clientDataResponse: ClientDataResponse
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialRequestOptions.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialRequestOptions.kt
new file mode 100644
index 000000000..42b0d75de
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialRequestOptions.kt
@@ -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 .
+ *
+ */
+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")
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialUsageParameters.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialUsageParameters.kt
new file mode 100644
index 000000000..c59541ad7
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialUsageParameters.kt
@@ -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 .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.passkey.data
+
+import com.kunzisoft.keepass.model.AppOrigin
+
+data class PublicKeyCredentialUsageParameters(
+ val publicKeyCredentialRequestOptions: PublicKeyCredentialRequestOptions,
+ val clientDataResponse: ClientDataResponse,
+ val appOrigin: AppOrigin
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt
new file mode 100644
index 000000000..d07412cad
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt
@@ -0,0 +1,606 @@
+/*
+ * 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 .
+ *
+ */
+package com.kunzisoft.keepass.credentialprovider.passkey.util
+
+import android.app.Activity
+import android.content.Intent
+import android.content.res.AssetManager
+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 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.HashManager.getApplicationFingerprints
+import com.kunzisoft.encrypt.Signature
+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.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.model.WebOrigin
+import com.kunzisoft.keepass.utils.StringUtil.toHexString
+import com.kunzisoft.keepass.utils.getParcelableExtraCompat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+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 {
+ val mReplyIntent = Intent()
+ Log.d(javaClass.name, "Success Passkey manual selection")
+ mReplyIntent.putExtra(EXTRA_PASSKEY, entryInfo.passkey)
+ mReplyIntent.putExtra(EXTRA_APP_ORIGIN, entryInfo.appOrigin)
+ extras?.let {
+ mReplyIntent.putExtras(it)
+ }
+ setResult(Activity.RESULT_OK, mReplyIntent)
+ } ?: run {
+ Log.w(javaClass.name, "Failed Passkey manual selection")
+ setResult(Activity.RESULT_CANCELED)
+ }
+ } catch (e: Exception) {
+ Log.e(javaClass.name, "Cant add passkey entry as result", e)
+ 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()
+ )
+ })
+ }
+
+ /**
+ * 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(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 (e: 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 list of the trustedPackages.json file,
+ * 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?,
+ assets: AssetManager,
+ relyingParty: String,
+ onOriginRetrieved: (appOrigin: AppOrigin, clientDataHash: ByteArray) -> Unit,
+ onOriginNotRetrieved: (appOrigin: AppOrigin, androidOriginString: String) -> Unit
+ ) {
+ if (callingAppInfo == null) {
+ throw SecurityException("Calling app info cannot be retrieved")
+ }
+ withContext(Dispatchers.IO) {
+ var callOrigin: String?
+ val privilegedAllowlist = assets.open("trustedPackages.json").bufferedReader().use {
+ it.readText()
+ }
+ // for trusted browsers like Chrome and Firefox
+ callOrigin = callingAppInfo.getOrigin(privilegedAllowlist)?.removeSuffix("/")
+ val androidOrigin = AndroidOrigin(
+ packageName = callingAppInfo.packageName,
+ fingerprint = callingAppInfo.signingInfo.getApplicationFingerprints()
+ )
+ val webOrigin = WebOrigin.fromRelyingParty(
+ relyingParty = relyingParty
+ )
+ // Check if the webDomain is validated for the
+ 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.toAndroidOrigin()
+ )
+ }
+ }
+ }
+ }
+
+ /**
+ * Generate a credential id randomly
+ */
+ 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
+ * [assetManager] has been transferred to the origin manager to manage package verification files
+ * [passkeyCreated] is called asynchronously when the passkey has been created
+ */
+ suspend fun retrievePasskeyCreationRequestParameters(
+ intent: Intent,
+ assetManager: AssetManager,
+ passkeyCreated: (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,
+ assets = assetManager,
+ relyingParty = relyingParty,
+ 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
+ ): 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()),
+ userPresent = true,
+ userVerified = true,
+ backupEligibility = BACKUP_ELIGIBILITY,
+ backupState = false, // TODO Setting to add a backup manually #2135
+ 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
+ * [assetManager] has been transferred to the origin manager 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,
+ assetManager: AssetManager,
+ result: (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,
+ assets = assetManager,
+ relyingParty = requestOptions.rpId,
+ 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
+ ): PublicKeyCredential {
+ val getCredentialResponse = FidoPublicKeyCredential(
+ id = passkey.credentialId,
+ response = AuthenticatorAssertionResponse(
+ requestOptions = requestOptions,
+ userPresent = true,
+ userVerified = true,
+ backupEligibility = BACKUP_ELIGIBILITY,
+ backupState = false, // TODO Setting to add a backup manually #2135
+ 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)
+ )
+ }
+ }
+
+ private const val BACKUP_ELIGIBILITY = true
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/DatabaseTaskProvider.kt b/app/src/main/java/com/kunzisoft/keepass/database/DatabaseTaskProvider.kt
index cd38f0046..f784056bc 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/DatabaseTaskProvider.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/DatabaseTaskProvider.kt
@@ -108,14 +108,19 @@ class DatabaseTaskProvider(
) {
// To show dialog only if context is an activity
- private var activity: FragmentActivity? = try { context as? FragmentActivity? }
- catch (_: Exception) { null }
+ private var activity: FragmentActivity? = try {
+ context as? FragmentActivity?
+ } catch (_: Exception) {
+ null
+ }
var onDatabaseRetrieved: ((database: ContextualDatabase?) -> Unit)? = null
- var onActionFinish: ((database: ContextualDatabase,
- actionTask: String,
- result: ActionRunnable.Result) -> Unit)? = null
+ var onActionFinish: ((
+ database: ContextualDatabase,
+ actionTask: String,
+ result: ActionRunnable.Result
+ ) -> Unit)? = null
private var intentDatabaseTask: Intent = Intent(
context.applicationContext,
@@ -141,7 +146,7 @@ class DatabaseTaskProvider(
this.databaseChangedDialogFragment = null
}
- private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
+ private val actionTaskListener = object : DatabaseTaskNotificationService.ActionTaskListener {
override fun onActionStarted(
database: ContextualDatabase,
progressMessage: ProgressMessage
@@ -175,13 +180,14 @@ class DatabaseTaskProvider(
}
}
- private val mActionDatabaseListener = object: DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
- override fun validateDatabaseChanged() {
- mBinder?.getService()?.saveDatabaseInfo()
+ private val mActionDatabaseListener =
+ object : DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
+ override fun validateDatabaseChanged() {
+ mBinder?.getService()?.saveDatabaseInfo()
+ }
}
- }
- private var databaseInfoListener = object:
+ private var databaseInfoListener = object :
DatabaseTaskNotificationService.DatabaseInfoListener {
override fun onDatabaseInfoChanged(
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?) {
onDatabaseRetrieved?.invoke(database)
}
@@ -265,12 +271,13 @@ class DatabaseTaskProvider(
}
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
- mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
- addServiceListeners(this)
- getService().checkDatabase()
- getService().checkDatabaseInfo()
- getService().checkAction()
- }
+ mBinder =
+ (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
+ addServiceListeners(this)
+ getService().checkDatabase()
+ getService().checkDatabaseInfo()
+ getService().checkAction()
+ }
}
override fun onServiceDisconnected(name: ComponentName?) {
@@ -296,7 +303,11 @@ class DatabaseTaskProvider(
private fun bindService() {
initServiceConnection()
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
+ )
}
}
@@ -331,7 +342,8 @@ class DatabaseTaskProvider(
}
}
}
- ContextCompat.registerReceiver(context, databaseTaskBroadcastReceiver,
+ ContextCompat.registerReceiver(
+ context, databaseTaskBroadcastReceiver,
IntentFilter().apply {
addAction(DATABASE_START_TASK_ACTION)
addAction(DATABASE_STOP_TASK_ACTION)
@@ -416,47 +428,51 @@ class DatabaseTaskProvider(
----
*/
- fun startDatabaseCreate(databaseUri: Uri,
- mainCredential: MainCredential
+ fun startDatabaseCreate(
+ databaseUri: Uri,
+ mainCredential: MainCredential
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
- }
- , ACTION_DATABASE_CREATE_TASK)
+ }, ACTION_DATABASE_CREATE_TASK)
}
- fun startDatabaseLoad(databaseUri: Uri,
- mainCredential: MainCredential,
- readOnly: Boolean,
- cipherEncryptDatabase: CipherEncryptDatabase?,
- fixDuplicateUuid: Boolean) {
+ fun startDatabaseLoad(
+ databaseUri: Uri,
+ mainCredential: MainCredential,
+ readOnly: Boolean,
+ cipherEncryptDatabase: CipherEncryptDatabase?,
+ fixDuplicateUuid: Boolean
+ ) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
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)
- }
- , ACTION_DATABASE_LOAD_TASK)
+ }, ACTION_DATABASE_LOAD_TASK)
}
- fun startDatabaseMerge(save: Boolean,
- fromDatabaseUri: Uri? = null,
- mainCredential: MainCredential? = null) {
+ fun startDatabaseMerge(
+ save: Boolean,
+ fromDatabaseUri: Uri? = null,
+ mainCredential: MainCredential? = null
+ ) {
start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
- }
- , ACTION_DATABASE_MERGE_TASK)
+ }, ACTION_DATABASE_MERGE_TASK)
}
fun startDatabaseReload(fixDuplicateUuid: Boolean) {
start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
- }
- , ACTION_DATABASE_RELOAD_TASK)
+ }, ACTION_DATABASE_RELOAD_TASK)
}
fun askToStartDatabaseReload(conditionToAsk: Boolean, approved: () -> Unit) {
@@ -472,15 +488,15 @@ class DatabaseTaskProvider(
}
}
- fun startDatabaseAssignCredential(databaseUri: Uri,
- mainCredential: MainCredential
+ fun startDatabaseAssignCredential(
+ databaseUri: Uri,
+ mainCredential: MainCredential
) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
- }
- , ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK)
+ }, ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK)
}
/*
@@ -489,54 +505,60 @@ class DatabaseTaskProvider(
----
*/
- fun startDatabaseCreateGroup(newGroup: Group,
- parent: Group,
- save: Boolean) {
+ fun startDatabaseCreateGroup(
+ newGroup: Group,
+ parent: Group,
+ save: Boolean
+ ) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, newGroup)
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_CREATE_GROUP_TASK)
+ }, ACTION_DATABASE_CREATE_GROUP_TASK)
}
- fun startDatabaseUpdateGroup(oldGroup: Group,
- groupToUpdate: Group,
- save: Boolean) {
+ fun startDatabaseUpdateGroup(
+ oldGroup: Group,
+ groupToUpdate: Group,
+ save: Boolean
+ ) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.GROUP_ID_KEY, oldGroup.nodeId)
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, groupToUpdate)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_GROUP_TASK)
+ }, ACTION_DATABASE_UPDATE_GROUP_TASK)
}
- fun startDatabaseCreateEntry(newEntry: Entry,
- parent: Group,
- save: Boolean) {
+ fun startDatabaseCreateEntry(
+ newEntry: Entry,
+ parent: Group,
+ save: Boolean
+ ) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, newEntry)
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_CREATE_ENTRY_TASK)
+ }, ACTION_DATABASE_CREATE_ENTRY_TASK)
}
- fun startDatabaseUpdateEntry(oldEntry: Entry,
- entryToUpdate: Entry,
- save: Boolean) {
+ fun startDatabaseUpdateEntry(
+ oldEntry: Entry,
+ entryToUpdate: Entry,
+ save: Boolean
+ ) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, oldEntry.nodeId)
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, entryToUpdate)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_ENTRY_TASK)
+ }, ACTION_DATABASE_UPDATE_ENTRY_TASK)
}
- private fun startDatabaseActionListNodes(actionTask: String,
- nodesPaste: List,
- newParent: Group?,
- save: Boolean) {
+ private fun startDatabaseActionListNodes(
+ actionTask: String,
+ nodesPaste: List,
+ newParent: Group?,
+ save: Boolean
+ ) {
val groupsIdToCopy = ArrayList>()
val entriesIdToCopy = ArrayList>()
nodesPaste.forEach { nodeVersioned ->
@@ -544,6 +566,7 @@ class DatabaseTaskProvider(
Type.GROUP -> {
groupsIdToCopy.add((nodeVersioned as Group).nodeId)
}
+
Type.ENTRY -> {
entriesIdToCopy.add((nodeVersioned as Entry).nodeId)
}
@@ -558,24 +581,29 @@ class DatabaseTaskProvider(
if (newParentId != null)
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, newParentId)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , actionTask)
+ }, actionTask)
}
- fun startDatabaseCopyNodes(nodesToCopy: List,
- newParent: Group,
- save: Boolean) {
+ fun startDatabaseCopyNodes(
+ nodesToCopy: List,
+ newParent: Group,
+ save: Boolean
+ ) {
startDatabaseActionListNodes(ACTION_DATABASE_COPY_NODES_TASK, nodesToCopy, newParent, save)
}
- fun startDatabaseMoveNodes(nodesToMove: List,
- newParent: Group,
- save: Boolean) {
+ fun startDatabaseMoveNodes(
+ nodesToMove: List,
+ newParent: Group,
+ save: Boolean
+ ) {
startDatabaseActionListNodes(ACTION_DATABASE_MOVE_NODES_TASK, nodesToMove, newParent, save)
}
- fun startDatabaseDeleteNodes(nodesToDelete: List,
- save: Boolean) {
+ fun startDatabaseDeleteNodes(
+ nodesToDelete: List,
+ save: Boolean
+ ) {
startDatabaseActionListNodes(ACTION_DATABASE_DELETE_NODES_TASK, nodesToDelete, null, save)
}
@@ -585,26 +613,28 @@ class DatabaseTaskProvider(
-----------------
*/
- fun startDatabaseRestoreEntryHistory(mainEntryId: NodeId,
- entryHistoryPosition: Int,
- save: Boolean) {
+ fun startDatabaseRestoreEntryHistory(
+ mainEntryId: NodeId,
+ entryHistoryPosition: Int,
+ save: Boolean
+ ) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
+ }, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
}
- fun startDatabaseDeleteEntryHistory(mainEntryId: NodeId,
- entryHistoryPosition: Int,
- save: Boolean) {
+ fun startDatabaseDeleteEntryHistory(
+ mainEntryId: NodeId,
+ entryHistoryPosition: Int,
+ save: Boolean
+ ) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_DELETE_ENTRY_HISTORY)
+ }, ACTION_DATABASE_DELETE_ENTRY_HISTORY)
}
/*
@@ -613,110 +643,118 @@ class DatabaseTaskProvider(
-----------------
*/
- fun startDatabaseSaveName(oldName: String,
- newName: String,
- save: Boolean) {
+ fun startDatabaseSaveName(
+ oldName: String,
+ newName: String,
+ save: Boolean
+ ) {
start(Bundle().apply {
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldName)
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newName)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_NAME_TASK)
+ }, ACTION_DATABASE_UPDATE_NAME_TASK)
}
- fun startDatabaseSaveDescription(oldDescription: String,
- newDescription: String,
- save: Boolean) {
+ fun startDatabaseSaveDescription(
+ oldDescription: String,
+ newDescription: String,
+ save: Boolean
+ ) {
start(Bundle().apply {
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDescription)
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDescription)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_DESCRIPTION_TASK)
+ }, ACTION_DATABASE_UPDATE_DESCRIPTION_TASK)
}
- fun startDatabaseSaveDefaultUsername(oldDefaultUsername: String,
- newDefaultUsername: String,
- save: Boolean) {
+ fun startDatabaseSaveDefaultUsername(
+ oldDefaultUsername: String,
+ newDefaultUsername: String,
+ save: Boolean
+ ) {
start(Bundle().apply {
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDefaultUsername)
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDefaultUsername)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK)
+ }, ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK)
}
- fun startDatabaseSaveColor(oldColor: String,
- newColor: String,
- save: Boolean) {
+ fun startDatabaseSaveColor(
+ oldColor: String,
+ newColor: String,
+ save: Boolean
+ ) {
start(Bundle().apply {
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldColor)
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newColor)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_COLOR_TASK)
+ }, ACTION_DATABASE_UPDATE_COLOR_TASK)
}
- fun startDatabaseSaveCompression(oldCompression: CompressionAlgorithm,
- newCompression: CompressionAlgorithm,
- save: Boolean) {
+ fun startDatabaseSaveCompression(
+ oldCompression: CompressionAlgorithm,
+ newCompression: CompressionAlgorithm,
+ save: Boolean
+ ) {
start(Bundle().apply {
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldCompression)
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newCompression)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
+ }, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
}
fun startDatabaseRemoveUnlinkedData(save: Boolean) {
start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
+ }, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
}
- fun startDatabaseSaveRecycleBin(oldRecycleBin: Group?,
- newRecycleBin: Group?,
- save: Boolean) {
+ fun startDatabaseSaveRecycleBin(
+ oldRecycleBin: Group?,
+ newRecycleBin: Group?,
+ save: Boolean
+ ) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldRecycleBin)
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newRecycleBin)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK)
+ }, ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK)
}
- fun startDatabaseSaveTemplatesGroup(oldTemplatesGroup: Group?,
- newTemplatesGroup: Group?,
- save: Boolean) {
+ fun startDatabaseSaveTemplatesGroup(
+ oldTemplatesGroup: Group?,
+ newTemplatesGroup: Group?,
+ save: Boolean
+ ) {
start(Bundle().apply {
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldTemplatesGroup)
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newTemplatesGroup)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK)
+ }, ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK)
}
- fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
- newMaxHistoryItems: Int,
- save: Boolean) {
+ fun startDatabaseSaveMaxHistoryItems(
+ oldMaxHistoryItems: Int,
+ newMaxHistoryItems: Int,
+ save: Boolean
+ ) {
start(Bundle().apply {
putInt(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistoryItems)
putInt(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistoryItems)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK)
+ }, ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK)
}
- fun startDatabaseSaveMaxHistorySize(oldMaxHistorySize: Long,
- newMaxHistorySize: Long,
- save: Boolean) {
+ fun startDatabaseSaveMaxHistorySize(
+ oldMaxHistorySize: Long,
+ newMaxHistorySize: Long,
+ save: Boolean
+ ) {
start(Bundle().apply {
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistorySize)
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistorySize)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK)
+ }, ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK)
}
/*
@@ -725,59 +763,64 @@ class DatabaseTaskProvider(
-------------------
*/
- fun startDatabaseSaveEncryption(oldEncryption: EncryptionAlgorithm,
- newEncryption: EncryptionAlgorithm,
- save: Boolean) {
+ fun startDatabaseSaveEncryption(
+ oldEncryption: EncryptionAlgorithm,
+ newEncryption: EncryptionAlgorithm,
+ save: Boolean
+ ) {
start(Bundle().apply {
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldEncryption)
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newEncryption)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_ENCRYPTION_TASK)
+ }, ACTION_DATABASE_UPDATE_ENCRYPTION_TASK)
}
- fun startDatabaseSaveKeyDerivation(oldKeyDerivation: KdfEngine,
- newKeyDerivation: KdfEngine,
- save: Boolean) {
+ fun startDatabaseSaveKeyDerivation(
+ oldKeyDerivation: KdfEngine,
+ newKeyDerivation: KdfEngine,
+ save: Boolean
+ ) {
start(Bundle().apply {
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldKeyDerivation)
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newKeyDerivation)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK)
+ }, ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK)
}
- fun startDatabaseSaveIterations(oldIterations: Long,
- newIterations: Long,
- save: Boolean) {
+ fun startDatabaseSaveIterations(
+ oldIterations: Long,
+ newIterations: Long,
+ save: Boolean
+ ) {
start(Bundle().apply {
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldIterations)
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newIterations)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_ITERATIONS_TASK)
+ }, ACTION_DATABASE_UPDATE_ITERATIONS_TASK)
}
- fun startDatabaseSaveMemoryUsage(oldMemoryUsage: Long,
- newMemoryUsage: Long,
- save: Boolean) {
+ fun startDatabaseSaveMemoryUsage(
+ oldMemoryUsage: Long,
+ newMemoryUsage: Long,
+ save: Boolean
+ ) {
start(Bundle().apply {
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMemoryUsage)
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMemoryUsage)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
+ }, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
}
- fun startDatabaseSaveParallelism(oldParallelism: Long,
- newParallelism: Long,
- save: Boolean) {
+ fun startDatabaseSaveParallelism(
+ oldParallelism: Long,
+ newParallelism: Long,
+ save: Boolean
+ ) {
start(Bundle().apply {
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldParallelism)
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newParallelism)
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
- }
- , ACTION_DATABASE_UPDATE_PARALLELISM_TASK)
+ }, ACTION_DATABASE_UPDATE_PARALLELISM_TASK)
}
/**
@@ -787,15 +830,13 @@ class DatabaseTaskProvider(
start(Bundle().apply {
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, saveToUri)
- }
- , ACTION_DATABASE_SAVE)
+ }, ACTION_DATABASE_SAVE)
}
fun startChallengeResponded(response: ByteArray?) {
start(Bundle().apply {
putByteArray(DatabaseTaskNotificationService.DATA_BYTES, response)
- }
- , ACTION_CHALLENGE_RESPONDED)
+ }, ACTION_CHALLENGE_RESPONDED)
}
companion object {
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/helper/LocalizedHelper.kt b/app/src/main/java/com/kunzisoft/keepass/database/helper/LocalizedHelper.kt
index 7ebe3aa0f..05bfa3c85 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/helper/LocalizedHelper.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/helper/LocalizedHelper.kt
@@ -24,7 +24,33 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
import com.kunzisoft.keepass.database.element.template.TemplateEngine
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.DatabaseException
+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.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.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? =
when (this) {
@@ -63,6 +89,11 @@ fun TemplateField.isStandardPasswordName(context: Context, name: String): Boolea
|| 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 {
if (context == null
|| TemplateEngine.containsTemplateDecorator(name)
@@ -107,6 +138,13 @@ fun TemplateField.getLocalizedName(context: Context?, name: String): String {
LABEL_SECURE_NOTE.equals(name, true) -> context.getString(R.string.secure_note)
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
}
}
diff --git a/app/src/main/java/com/kunzisoft/keepass/database/helper/SearchHelper.kt b/app/src/main/java/com/kunzisoft/keepass/database/helper/SearchHelper.kt
index c4a90cf3c..5a7f846a0 100644
--- a/app/src/main/java/com/kunzisoft/keepass/database/helper/SearchHelper.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/database/helper/SearchHelper.kt
@@ -43,13 +43,15 @@ object SearchHelper {
/**
* Utility method to perform actions if item is found or not after an auto search in [database]
*/
- fun checkAutoSearchInfo(context: Context,
- database: ContextualDatabase?,
- searchInfo: SearchInfo?,
- onItemsFound: (openedDatabase: ContextualDatabase,
- items: List) -> Unit,
- onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
- onDatabaseClosed: () -> Unit) {
+ fun checkAutoSearchInfo(
+ context: Context,
+ database: ContextualDatabase?,
+ searchInfo: SearchInfo?,
+ onItemsFound: (openedDatabase: ContextualDatabase,
+ items: List) -> Unit,
+ onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
+ onDatabaseClosed: () -> Unit
+ ) {
if (database == null || !database.loaded) {
onDatabaseClosed.invoke()
} else if (TimeoutHelper.checkTime(context)) {
@@ -59,8 +61,7 @@ object SearchHelper {
&& !searchInfo.containsOnlyNullValues()) {
// If search provide results
database.createVirtualGroupFromSearchInfo(
- searchInfo.toString(),
- searchInfo.isASearchByDomain(),
+ searchInfo,
MAX_SEARCH_ENTRY
)?.let { searchGroup ->
if (searchGroup.numberOfChildEntries > 0) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/receivers/DexModeReceiver.kt b/app/src/main/java/com/kunzisoft/keepass/receivers/DexModeReceiver.kt
index 7844c4251..c2c4c5334 100644
--- a/app/src/main/java/com/kunzisoft/keepass/receivers/DexModeReceiver.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/receivers/DexModeReceiver.kt
@@ -1,13 +1,9 @@
package com.kunzisoft.keepass.receivers
import android.content.BroadcastReceiver
-import android.content.ComponentName
import android.content.Context
import android.content.Intent
-import android.content.pm.PackageManager
import android.util.Log
-import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
-import com.kunzisoft.keepass.utils.DexUtil
import com.kunzisoft.keepass.utils.MagikeyboardUtil
class DexModeReceiver : BroadcastReceiver() {
diff --git a/app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationService.kt
index b33213891..98cf199c6 100644
--- a/app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationService.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/services/ClipboardEntryNotificationService.kt
@@ -274,10 +274,12 @@ class ClipboardEntryNotificationService : LockNotificationService() {
val containsPasswordToCopy = entry.password.isNotEmpty()
&& PreferencesUtil.allowCopyProtectedFields(context)
val containsOTPToCopy = entry.containsCustomField(OTP_TOKEN_FIELD)
- val containsExtraFieldToCopy = entry.customFields.isNotEmpty()
- && (entry.containsCustomFieldsNotProtected()
+ val customFields = entry.getCustomFieldsForFilling()
+ 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
@@ -320,7 +322,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
if (containsExtraFieldToCopy) {
try {
var anonymousFieldNumber = 0
- entry.customFields.forEach { field ->
+ entry.getCustomFieldsForFilling().forEach { field ->
//If value is not protected or allowed
if ((!field.protectedValue.isProtected
|| PreferencesUtil.allowCopyProtectedFields(context))
diff --git a/app/src/main/java/com/kunzisoft/keepass/services/KeyboardEntryNotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/KeyboardEntryNotificationService.kt
index 9986b304a..557f00cd7 100644
--- a/app/src/main/java/com/kunzisoft/keepass/services/KeyboardEntryNotificationService.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/services/KeyboardEntryNotificationService.kt
@@ -27,7 +27,7 @@ import android.util.Log
import android.widget.Toast
import androidx.preference.PreferenceManager
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.settings.PreferencesUtil
import com.kunzisoft.keepass.timeout.TimeoutHelper
diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/DurationDialogPreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/DurationDialogPreference.kt
index 46f7a4e92..dfaaeb2f0 100644
--- a/app/src/main/java/com/kunzisoft/keepass/settings/preference/DurationDialogPreference.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/DurationDialogPreference.kt
@@ -52,6 +52,7 @@ class DurationDialogPreference @JvmOverloads constructor(context: Context,
notifyChanged()
}
+ @Deprecated(message = "")
override fun onSetInitialValue(restorePersistedValue: Boolean, defaultValue: Any?) {
if (restorePersistedValue) {
mDuration = getPersistedString(mDuration.toString()).toLongOrNull() ?: mDuration
diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt b/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt
index 6244ecbda..8c4eb9e91 100644
--- a/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/utils/BroadcastAction.kt
@@ -33,7 +33,7 @@ import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.app.AppLifecycleObserver
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.KeyboardEntryNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
diff --git a/app/src/main/java/com/kunzisoft/keepass/utils/MagikeyboardUtil.kt b/app/src/main/java/com/kunzisoft/keepass/utils/MagikeyboardUtil.kt
index be095f648..e98c2831b 100644
--- a/app/src/main/java/com/kunzisoft/keepass/utils/MagikeyboardUtil.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/utils/MagikeyboardUtil.kt
@@ -4,7 +4,7 @@ import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.util.Log
-import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
+import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
object MagikeyboardUtil {
private val TAG = MagikeyboardUtil::class.java.name
diff --git a/app/src/main/java/com/kunzisoft/keepass/view/PasskeyTextFieldView.kt b/app/src/main/java/com/kunzisoft/keepass/view/PasskeyTextFieldView.kt
new file mode 100644
index 000000000..c099f92f9
--- /dev/null
+++ b/app/src/main/java/com/kunzisoft/keepass/view/PasskeyTextFieldView.kt
@@ -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 .
+ *
+ */
+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
+ }
+}
diff --git a/app/src/main/java/com/kunzisoft/keepass/view/PasswordTextFieldView.kt b/app/src/main/java/com/kunzisoft/keepass/view/PasswordTextFieldView.kt
index c56454d10..12942811e 100644
--- a/app/src/main/java/com/kunzisoft/keepass/view/PasswordTextFieldView.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/view/PasswordTextFieldView.kt
@@ -34,9 +34,9 @@ import com.kunzisoft.keepass.password.PasswordGenerator
import com.kunzisoft.keepass.settings.PreferencesUtil
-class PasswordTextFieldView @JvmOverloads constructor(context: Context,
- attrs: AttributeSet? = null,
- defStyle: Int = 0)
+open class PasswordTextFieldView @JvmOverloads constructor(context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = 0)
: TextFieldView(context, attrs, defStyle) {
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,
R.drawable.ic_shield_white_24dp
)?.apply {
@@ -98,7 +98,7 @@ class PasswordTextFieldView @JvmOverloads constructor(context: Context,
value = resources.getString(valueId)
}
- private fun getEntropyStrength(passwordText: String) {
+ protected open fun getEntropyStrength(passwordText: String) {
mPasswordEntropyCalculator.getEntropyStrength(passwordText) { entropyStrength ->
labelView.apply {
post {
diff --git a/app/src/main/java/com/kunzisoft/keepass/view/TemplateAbstractView.kt b/app/src/main/java/com/kunzisoft/keepass/view/TemplateAbstractView.kt
index 0d1823ffe..4d5eec7e7 100644
--- a/app/src/main/java/com/kunzisoft/keepass/view/TemplateAbstractView.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/view/TemplateAbstractView.kt
@@ -534,10 +534,15 @@ abstract class TemplateAbstractView<
}
protected fun getCustomField(fieldName: String): Field {
+ return getCustomFieldOrNull(fieldName)
+ ?: Field(fieldName, ProtectedString(false))
+ }
+
+ protected fun getCustomFieldOrNull(fieldName: String): Field? {
return getCustomField(fieldName,
templateFieldNotEmpty = false,
retrieveDefaultValues = false
- ) ?: Field(fieldName, ProtectedString(false))
+ )
}
private fun getCustomField(fieldName: String,
diff --git a/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt b/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt
index 8963534ba..ee07349b3 100644
--- a/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/view/TemplateEditView.kt
@@ -20,6 +20,8 @@ import com.kunzisoft.keepass.database.helper.getLocalizedName
import com.kunzisoft.keepass.database.helper.isStandardPasswordName
import com.kunzisoft.keepass.model.DataDate
import com.kunzisoft.keepass.model.DataTime
+import com.kunzisoft.keepass.model.AppOriginEntryField
+import com.kunzisoft.keepass.model.PasskeyEntryFields
import com.kunzisoft.keepass.otp.OtpEntryFields
@@ -256,9 +258,12 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
override fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean,
retrieveDefaultValues: Boolean) {
super.populateEntryInfoWithViews(templateFieldNotEmpty, retrieveDefaultValues)
- mEntryInfo?.otpModel = OtpEntryFields.parseFields { key ->
- getCustomField(key).protectedValue.toString()
- }?.otpModel
+ val getField: (id: String) -> String? = { key ->
+ getCustomFieldOrNull(key)?.protectedValue?.stringValue
+ }
+ mEntryInfo?.otpModel = OtpEntryFields.parseFields(getField)?.otpModel
+ mEntryInfo?.passkey = PasskeyEntryFields.parseFields(getField)
+ mEntryInfo?.appOrigin = AppOriginEntryField.parseFields(getField)
}
override fun onRestoreEntryInstanceState(state: SavedState) {
diff --git a/app/src/main/java/com/kunzisoft/keepass/view/TemplateView.kt b/app/src/main/java/com/kunzisoft/keepass/view/TemplateView.kt
index 92e4b0140..12b55ca34 100644
--- a/app/src/main/java/com/kunzisoft/keepass/view/TemplateView.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/view/TemplateView.kt
@@ -1,7 +1,6 @@
package com.kunzisoft.keepass.view
import android.content.Context
-import android.os.Build
import android.util.AttributeSet
import android.view.View
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.TemplateField
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.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.OtpEntryFields.OTP_TOKEN_FIELD
@@ -52,6 +54,8 @@ class TemplateView @JvmOverloads constructor(context: Context,
return context?.let {
(if (TemplateField.isStandardPasswordName(context, templateAttribute.label))
PasswordTextFieldView(it)
+ else if (TemplateField.isPasskeyLabel(context, templateAttribute.label))
+ PasskeyTextFieldView(it)
else TextFieldView(it)).apply {
applyFontVisibility(mFontInVisibility)
setProtection(field.protectedValue.isProtected, mHideProtectedValue)
@@ -123,20 +127,20 @@ class TemplateView @JvmOverloads constructor(context: Context,
override fun populateViewsWithEntryInfo(showEmptyFields: Boolean): List {
val emptyCustomFields = super.populateViewsWithEntryInfo(false)
-
// Hide empty custom fields
emptyCustomFields.forEach { customFieldId ->
customFieldId.view.isVisible = false
}
-
removeOtpRunnable()
mEntryInfo?.let { entryInfo ->
// Assign specific OTP dynamic view
entryInfo.otpModel?.let {
assignOtp(it)
}
+ entryInfo.passkey?.let {
+ assignPasskey(it)
+ }
}
-
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() {
mLastOtpTokenView?.removeCallbacks(mOtpRunnable)
mLastOtpTokenView = null
diff --git a/app/src/main/java/com/kunzisoft/keepass/view/TextFieldView.kt b/app/src/main/java/com/kunzisoft/keepass/view/TextFieldView.kt
index d9fbf49fa..fb53a360a 100644
--- a/app/src/main/java/com/kunzisoft/keepass/view/TextFieldView.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/view/TextFieldView.kt
@@ -27,7 +27,6 @@ import android.util.AttributeSet
import android.util.TypedValue
import android.view.ContextThemeWrapper
import android.view.View
-import android.view.View.OnClickListener
import android.widget.RelativeLayout
import androidx.annotation.StringRes
import androidx.appcompat.widget.AppCompatImageButton
@@ -37,7 +36,7 @@ import androidx.core.text.util.LinkifyCompat
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
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
@@ -46,7 +45,7 @@ open class TextFieldView @JvmOverloads constructor(context: Context,
defStyle: Int = 0)
: RelativeLayout(context, attrs, defStyle), GenericTextFieldView {
- private var labelViewId = ViewCompat.generateViewId()
+ protected var labelViewId = ViewCompat.generateViewId()
private var valueViewId = ViewCompat.generateViewId()
private var showButtonId = ViewCompat.generateViewId()
private var copyButtonId = ViewCompat.generateViewId()
diff --git a/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt b/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt
index b740d1fb0..e7e4f1b9b 100644
--- a/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt
+++ b/app/src/main/java/com/kunzisoft/keepass/viewmodels/EntryEditViewModel.kt
@@ -174,7 +174,8 @@ class EntryEditViewModel: NodeEditViewModel() {
// Load entry info
entry.getEntryInfo(database, true).let { tempEntryInfo ->
// Retrieve data from registration
- (registerInfo?.searchInfo ?: searchInfo)?.let { tempSearchInfo ->
+ // TODO only save registration
+ searchInfo?.let { tempSearchInfo ->
tempEntryInfo.saveSearchInfo(database, tempSearchInfo)
}
registerInfo?.let { regInfo ->
diff --git a/app/src/main/res/drawable/ic_passkey_white_24dp.xml b/app/src/main/res/drawable/ic_passkey_white_24dp.xml
new file mode 100644
index 000000000..4d0fabc83
--- /dev/null
+++ b/app/src/main/res/drawable/ic_passkey_white_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/layout/item_list_nodes_entry.xml b/app/src/main/res/layout/item_list_nodes_entry.xml
index 84bdf1020..9cee13e89 100644
--- a/app/src/main/res/layout/item_list_nodes_entry.xml
+++ b/app/src/main/res/layout/item_list_nodes_entry.xml
@@ -159,6 +159,14 @@
android:layout_gravity="center"
android:src="@drawable/ic_attach_file_white_24dp" />
+
+
- Expired entries are not shown
Hide templates
Templates are not shown
+ Passkey
+ KeePassDX Credential Provider
+ Save passkey in new entry
+ Update passkey in "%1$s"
+ No passkey found
+ Select an existing passkey
+ KeePassDX Database Locked
+ Select to unlock
+ Passkey Username
+ Passkey Private Key
+ Passkey Credential Id
+ Passkey User Handle
+ Passkey Relying Party
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 12cb2aacf..0f87e9f95 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -567,6 +567,13 @@
- 8dp
- 16sp
+
+