diff --git a/README.md b/README.md index 745b444c5..364fd3b59 100644 --- a/README.md +++ b/README.md @@ -109,3 +109,24 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w along with KeePassDX. If not, see . *This project is a fork of [KeePassDroid](https://github.com/bpellin/keepassdroid) by bpellin.* + + +## Credential Provider + +Use this version only for testing at your own risk. + +### requirements +- Android 14 or up +- enable 3rd party passkeys in chrome. For detail see https://1password.community/discussion/comment/711037/#Comment_711037 +- set KeepassDX in the Android setting Passwords & Accounts > Your Provider > Enable + +### working +- sign in with ecdsa/rsa passkeys created by KeepassXC in Chrome. Tested with passkeys.io and webauthn.io. + +### maybe working +- sign in with passkeys apps natively (without browser) + +### not working +- create passkeys +- user credential provider with username/password +- open KeepassDX to unlock the database, if it is locked (currently a dummy entry with title unlock db is shown) \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 561308a84..b466e81e1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,7 +9,7 @@ android { defaultConfig { applicationId "com.kunzisoft.keepass" - minSdkVersion 15 + minSdkVersion 19 targetSdkVersion 34 versionCode = 132 versionName = "4.1.0" @@ -35,6 +35,10 @@ android { } } + buildFeatures { + buildConfig true + } + dependenciesInfo { // Disables dependency metadata when building APKs. includeInApk = false @@ -98,6 +102,10 @@ android { kotlinOptions { jvmTarget = "1.8" } + + packaging { + resources.excludes.add("META-INF/versions/9/OSGI-INF/MANIFEST.MF") // necessary for bcpkix-jdk18on in crypto + } } def room_version = "2.5.1" @@ -122,6 +130,7 @@ dependencies { implementation "com.splitwise:tokenautocomplete:4.0.0-beta05" // Database implementation "androidx.room:room-runtime:$room_version" + implementation project(':crypto') kapt "androidx.room:room-compiler:$room_version" // Autofill implementation "androidx.autofill:autofill:1.1.0" @@ -136,6 +145,10 @@ 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" + implementation "androidx.credentials:credentials-play-services-auth:1.2.2" // Modules import implementation project(path: ':database') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 070c5251c..5b881859b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -44,7 +44,9 @@ android:largeHeap="true" android:resizeableActivity="true" android:theme="@style/KeepassDXStyle.Night" - tools:targetApi="s"> + tools:targetApi="s" + android:enableOnBackInvokedCallback="true"> + @@ -199,6 +201,14 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/assets/trustedPackages.json b/app/src/main/assets/trustedPackages.json new file mode 100644 index 000000000..263805e82 --- /dev/null +++ b/app/src/main/assets/trustedPackages.json @@ -0,0 +1,536 @@ +{ + "apps": [ + { + "type": "android", + "info": { + "package_name": "com.android.chrome", + "signatures": [ + { + "build": "release", + "cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83" + }, + { + "build": "userdebug", + "cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00" + } + ] + } + }, + { + "type": "android", + "info": { + "package_name": "com.chrome.beta", + "signatures": [ + { + "build": "release", + "cert_fingerprint_sha256": "DA:63:3D:34:B6:9E:63:AE:21:03:B4:9D:53:CE:05:2F:C5:F7:F3:C5:3A:AB:94:FD:C2:A2:08:BD:FD:14:24:9C" + }, + { + "build": "release", + "cert_fingerprint_sha256": "3D:7A:12:23:01:9A:A3:9D:9E:A0:E3:43:6A:B7:C0:89:6B:FB:4F:B6:79:F4:DE:5F:E7:C2:3F:32:6C:8F:99:4A" + } + ] + } + }, + { + "type": "android", + "info": { + "package_name": "com.chrome.dev", + "signatures": [ + { + "build": "release", + "cert_fingerprint_sha256": "90:44:EE:5F:EE:4B:BC:5E:21:DD:44:66:54:31:C4:EB:1F:1F:71:A3:27:16:A0:BC:92:7B:CB:B3:92:33:CA:BF" + }, + { + "build": "release", + "cert_fingerprint_sha256": "3D:7A:12:23:01:9A:A3:9D:9E:A0:E3:43:6A:B7:C0:89:6B:FB:4F:B6:79:F4:DE:5F:E7:C2:3F:32:6C:8F:99:4A" + } + ] + } + }, + { + "type": "android", + "info": { + "package_name": "com.chrome.canary", + "signatures": [ + { + "build": "release", + "cert_fingerprint_sha256": "20:19:DF:A1:FB:23:EF:BF:70:C5:BC:D1:44:3C:5B:EA:B0:4F:3F:2F:F4:36:6E:9A:C1:E3:45:76:39:A2:4C:FC" + } + ] + } + }, + { + "type": "android", + "info": { + "package_name": "org.chromium.chrome", + "signatures": [ + { + "build": "release", + "cert_fingerprint_sha256": "C6:AD:B8:B8:3C:6D:4C:17:D2:92:AF:DE:56:FD:48:8A:51:D3:16:FF:8F:2C:11:C5:41:02:23:BF:F8:A7:DB:B3" + }, + { + "build": "userdebug", + "cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00" + } + ] + } + }, + { + "type": "android", + "info": { + "package_name": "com.google.android.apps.chrome", + "signatures": [ + { + "build": "userdebug", + "cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00" + } + ] + } + }, + { + "type": "android", + "info": { + "package_name": "org.mozilla.fennec_webauthndebug", + "signatures": [ + { + "build": "userdebug", + "cert_fingerprint_sha256": "BD:AE:82:02:80:D2:AF:B7:74:94:EF:22:58:AA:78:A9:AE:A1:36:41:7E:8B:C2:3D:C9:87:75:2E:6F:48:E8:48" + } + ] + } + }, + { + "type": "android", + "info": { + "package_name": "org.mozilla.firefox", + "signatures": [ + { + "build": "release", + "cert_fingerprint_sha256": "A7:8B:62:A5:16:5B:44:94:B2:FE:AD:9E:76:A2:80:D2:2D:93:7F:EE:62:51:AE:CE:59:94:46:B2:EA:31:9B:04" + } + ] + } + }, + { + "type": "android", + "info": { + "package_name": "org.mozilla.firefox_beta", + "signatures": [ + { + "build": "release", + "cert_fingerprint_sha256": "A7:8B:62:A5:16:5B:44:94:B2:FE:AD:9E:76:A2:80:D2:2D:93:7F:EE:62:51:AE:CE:59:94:46:B2:EA:31:9B:04" + } + ] + } + }, + { + "type": "android", + "info": { + "package_name": "org.mozilla.focus", + "signatures": [ + { + "build": "release", + "cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC" + } + ] + } + }, + { + "type": "android", + "info": { + "package_name": "org.mozilla.fennec_aurora", + "signatures": [ + { + "build": "release", + "cert_fingerprint_sha256": "BC:04:88:83:8D:06:F4:CA:6B:F3:23:86:DA:AB:0D:D8:EB:CF:3E:77:30:78:74:59:F6:2F:B3:CD:14:A1:BA:AA" + } + ] + } + }, + { + "type": "android", + "info": { + "package_name": "org.mozilla.rocket", + "signatures": [ + { + "build": "release", + "cert_fingerprint_sha256": "86:3A:46:F0:97:39:32:B7:D0:19:9B:54:91:12:74:1C:2D:27:31:AC:72:EA:11:B7:52:3A:A9:0A:11:BF:56:91" + } + ] + } + }, + { + "type": "android", + "info": { + "package_name": "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..75bc8c5f0 100644 --- a/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/activities/EntryActivity.kt @@ -163,9 +163,9 @@ class EntryActivity : DatabaseLockActivity() { toolbar?.title = " " // Retrieve the textColor to tint the toolbar - val taColorSecondary = theme.obtainStyledAttributes(intArrayOf(R.attr.colorSecondary)) - val taColorSurface = theme.obtainStyledAttributes(intArrayOf(R.attr.colorSurface)) - val taColorOnSurface = theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnSurface)) + val taColorSecondary = theme.obtainStyledAttributes(intArrayOf(com.google.android.material.R.attr.colorSecondary)) + val taColorSurface = theme.obtainStyledAttributes(intArrayOf(com.google.android.material.R.attr.colorSurface)) + val taColorOnSurface = theme.obtainStyledAttributes(intArrayOf(com.google.android.material.R.attr.colorOnSurface)) val taColorBackground = theme.obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground)) mColorSecondary = taColorSecondary.getColor(0, Color.BLACK) mColorSurface = taColorSurface.getColor(0, Color.BLACK) 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 3c83c7750..1050ba7b8 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 @@ -30,6 +30,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..6a41fb3bb 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 @@ -108,7 +108,7 @@ class GroupDialogFragment : DatabaseDialogFragment() { uuidReferenceView = root.findViewById(R.id.group_UUID_reference) // Retrieve the textColor to tint the icon - val ta = activity.theme.obtainStyledAttributes(intArrayOf(R.attr.colorSecondary)) + val ta = activity.theme.obtainStyledAttributes(intArrayOf(com.google.android.material.R.attr.colorSecondary)) mIconColor = ta.getColor(0, Color.WHITE) ta.recycle() diff --git a/app/src/main/java/com/kunzisoft/keepass/adapters/BreadcrumbAdapter.kt b/app/src/main/java/com/kunzisoft/keepass/adapters/BreadcrumbAdapter.kt index 6b0dafb2b..1e073e8ae 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/BreadcrumbAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/BreadcrumbAdapter.kt @@ -40,7 +40,7 @@ class BreadcrumbAdapter(val context: Context) mShowUUID = PreferencesUtil.showUUID(context) // Retrieve the color to tint the icon - val taIconColor = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnSurface)) + val taIconColor = context.theme.obtainStyledAttributes(intArrayOf(com.google.android.material.R.attr.colorOnSurface)) mIconColor = taIconColor.getColor(0, Color.WHITE) taIconColor.recycle() } 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 fc47ca14f..9961a8675 100644 --- a/app/src/main/java/com/kunzisoft/keepass/adapters/NodesAdapter.kt +++ b/app/src/main/java/com/kunzisoft/keepass/adapters/NodesAdapter.kt @@ -118,7 +118,7 @@ class NodesAdapter ( this.mNodeSortedListCallback = NodeSortedListCallback() this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback) - val taColorSurfaceContainer = context.obtainStyledAttributes(intArrayOf(R.attr.colorSurfaceContainer)) + val taColorSurfaceContainer = context.obtainStyledAttributes(intArrayOf(com.google.android.material.R.attr.colorSurfaceContainer)) this.mColorSurfaceContainer = taColorSurfaceContainer.getColor(0, Color.BLACK) taColorSurfaceContainer.recycle() // Retrieve the color to tint the icon @@ -134,11 +134,11 @@ class NodesAdapter ( this.mTextColorSecondary = taTextColorSecondary.getColor(0, Color.BLACK) taTextColorSecondary.recycle() // To get background color for selection - val taColorSecondary = context.obtainStyledAttributes(intArrayOf(R.attr.colorSecondary)) + val taColorSecondary = context.obtainStyledAttributes(intArrayOf(com.google.android.material.R.attr.colorSecondary)) this.mColorSecondary = taColorSecondary.getColor(0, Color.GRAY) taColorSecondary.recycle() // To get text color for selection - val taColorOnSecondary = context.obtainStyledAttributes(intArrayOf(R.attr.colorOnSecondary)) + val taColorOnSecondary = context.obtainStyledAttributes(intArrayOf(com.google.android.material.R.attr.colorOnSecondary)) this.mColorOnSecondary = taColorOnSecondary.getColor(0, Color.WHITE) taColorOnSecondary.recycle() } diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/CredentialProviderActivity.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/CredentialProviderActivity.kt new file mode 100644 index 000000000..62d20ea7e --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/CredentialProviderActivity.kt @@ -0,0 +1,185 @@ +package com.kunzisoft.keepass.credentialprovider + +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.biometric.BiometricManager +import androidx.credentials.GetCredentialResponse +import androidx.credentials.GetPasswordOption +import androidx.credentials.GetPublicKeyCredentialOption +import androidx.credentials.PublicKeyCredential +import androidx.credentials.provider.CallingAppInfo +import androidx.credentials.provider.PendingIntentHandler +import androidx.credentials.webauthn.MyAuthenticatorAssertionResponse +import androidx.credentials.webauthn.FidoPublicKeyCredential +import androidx.credentials.webauthn.PublicKeyCredentialRequestOptions +import com.kunzisoft.signature.Signature +import com.kunzisoft.keepass.activities.legacy.DatabaseActivity +import com.kunzisoft.keepass.database.ContextualDatabase +import org.apache.commons.codec.binary.Base64 +import androidx.biometric.BiometricPrompt; +import com.kunzisoft.keepass.R + +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +class CredentialProviderActivity : DatabaseActivity() { + + @SuppressLint("RestrictedApi") + private fun validatePasskey(requestJson: String, origin: String, packageName: String, uid: ByteArray, username: Any, credId: ByteArray, privateKey: String) { + + val request = PublicKeyCredentialRequestOptions(requestJson) + + // https://www.w3.org/TR/webauthn-3/#authdata-flags + val userPresent = true + val userVerified = true + val backupEligibility = true + val backupState = true + val response = MyAuthenticatorAssertionResponse( + requestOptions = request, + credentialId = credId, + origin = origin, + up = userPresent, + uv = userVerified, + be = backupEligibility, + bs = backupState, + userHandle = uid + ) + + val messageToSign = response.dataToSign() + + val sig = Signature.sign(privateKey, messageToSign) + + response.signature = sig + + val credential = FidoPublicKeyCredential( + rawId = credId, response = response, authenticatorAttachment = "platform" + ) + val result = Intent() + + val cJson = credential.json() + Log.w("", cJson) + val passkeyCredential = PublicKeyCredential(cJson) + PendingIntentHandler.setGetCredentialResponse( + result, GetCredentialResponse(passkeyCredential) + ) + setResult(RESULT_OK, result) + finish() + } + + private fun b64Decode(encodedString: String?): ByteArray { + return Base64.decodeBase64(encodedString) + } + + private fun cleanUp() { + setResult(RESULT_CANCELED) + finish() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + override fun onDatabaseRetrieved(database: ContextualDatabase?) { + Log.d(javaClass.simpleName,"onDatabaseRetrieved called: database = $database") + super.onDatabaseRetrieved(database) + + val getRequest = + PendingIntentHandler.retrieveProviderGetCredentialRequest(intent) + + if (getRequest?.credentialOptions?.size != 1) { + throw Exception("not exact 1 credentialOption") + } + + when (val credOption = getRequest.credentialOptions[0]) { + is GetPublicKeyCredentialOption -> handlePublicKeyCredOption(credOption, getRequest.callingAppInfo) + is GetPasswordOption -> handlePasswordOption(credOption) + else -> throw Exception("unknown type of credentialOption") + } + + Log.d(javaClass.simpleName, "onDatabaseRetrieved finished") + + } + private fun handlePublicKeyCredOption(publicKeyRequest: GetPublicKeyCredentialOption, callingAppInfo: CallingAppInfo) { + + val requestInfo = intent.getBundleExtra(KeePassDXCredentialProviderService.INTENT_EXTRA_KEY) + val nodeId = requestInfo?.getString(KeePassDXCredentialProviderService.NODE_ID_KEY) + + Log.d(javaClass.simpleName, "nodeId = $nodeId") + + if (mDatabase == null || nodeId == null) { + cleanUp() + return + } + val passkey = PasskeyUtil.searchPassKeyByNodeId(mDatabase!!, nodeId) + + + if (passkey == null) { + cleanUp() + return + } + Log.d(javaClass.simpleName, "passkey found") + + val credId = b64Decode(passkey.credId) + val privateKey = passkey.privateKeyPem + val uid = b64Decode(passkey.userHandle) + + val origin = appInfoToOrigin(callingAppInfo) + + Log.d(javaClass.simpleName, "origin = $origin") + val packageName = callingAppInfo.packageName + + val biometricPrompt = BiometricPrompt( + this, + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + cleanUp() + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + cleanUp() + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + validatePasskey( + publicKeyRequest.requestJson, + origin!!, + packageName, + uid, + passkey.username, + credId, + privateKey + ) + } + } + ) + + val title = getString(R.string.passkey_biometric_prompt_title) + val subtitle = getString(R.string.passkey_biometric_prompt_subtitle, origin) + val negativeButtonText = getString(R.string.passkey_biometric_prompt_negative_button_text) + val promptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle(title) + .setSubtitle(subtitle) + .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG) + .setNegativeButtonText(negativeButtonText) + .build() + biometricPrompt.authenticate(promptInfo) + } + + private fun handlePasswordOption(passwordOption: GetPasswordOption) { + // TODO + } + + private fun appInfoToOrigin(callingAppInfo: CallingAppInfo): String? { + val privilegedAllowlist = assets.open("trustedPackages.json").bufferedReader().use { + it.readText() + } + return callingAppInfo.getOrigin(privilegedAllowlist)?.removeSuffix("/") + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/KeePassDXCredentialProviderService.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/KeePassDXCredentialProviderService.kt new file mode 100644 index 000000000..eaaba0c35 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/KeePassDXCredentialProviderService.kt @@ -0,0 +1,179 @@ +package com.kunzisoft.keepass.credentialprovider; + +import android.app.PendingIntent +import android.content.Intent +import android.os.Bundle +import android.os.CancellationSignal; +import android.os.OutcomeReceiver; +import android.provider.ContactsContract.Directory.PACKAGE_NAME + +import androidx.annotation.RequiresApi +import androidx.credentials.exceptions.ClearCredentialException +import androidx.credentials.exceptions.CreateCredentialException +import androidx.credentials.exceptions.GetCredentialException; +import androidx.credentials.exceptions.GetCredentialUnknownException +import androidx.credentials.provider.BeginCreateCredentialRequest +import androidx.credentials.provider.BeginCreateCredentialResponse +import androidx.credentials.provider.BeginGetCredentialRequest +import androidx.credentials.provider.BeginGetCredentialResponse; +import androidx.credentials.provider.BeginGetPasswordOption +import androidx.credentials.provider.BeginGetPublicKeyCredentialOption +import androidx.credentials.provider.CallingAppInfo +import androidx.credentials.provider.CredentialEntry +import androidx.credentials.provider.CredentialProviderService +import androidx.credentials.provider.ProviderClearCredentialStateRequest +import androidx.credentials.provider.PublicKeyCredentialEntry +import org.json.JSONObject + +import android.util.Log +import com.kunzisoft.keepass.database.DatabaseTaskProvider +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.search.SearchHelper +import com.kunzisoft.keepass.database.search.SearchParameters + +@RequiresApi(value = 34) +class KeePassDXCredentialProviderService : CredentialProviderService() { + + private var mDatabaseTaskProvider: DatabaseTaskProvider? = null + private var mDatabase: Database? = null + + override fun onCreate() { + super.onCreate() + + mDatabaseTaskProvider = DatabaseTaskProvider(this) + mDatabaseTaskProvider?.registerProgressTask() + mDatabaseTaskProvider?.onDatabaseRetrieved = { database -> + this.mDatabase = database + } + } + + override fun onDestroy() { + mDatabaseTaskProvider?.unregisterProgressTask() + super.onDestroy() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + return super.onStartCommand(intent, flags, startId) + } + + override fun onBeginCreateCredentialRequest(request: BeginCreateCredentialRequest, cancellationSignal: CancellationSignal, callback: OutcomeReceiver) { + TODO("Not yet implemented") + + } + + override fun onBeginGetCredentialRequest( + request: BeginGetCredentialRequest, + cancellationSignal: CancellationSignal, + callback: OutcomeReceiver, + ) { + /* + val unlockEntryTitle = "Authenticate to continue" + if (isAppLocked()) { + callback.onResult(BeginGetCredentialResponse( + authenticationActions = mutableListOf(AuthenticationAction( + unlockEntryTitle, createUnlockPendingIntent()) + ) + ) + ) + return + } + */ + try { + val response = processGetCredentialsRequest(request) + callback.onResult(response) + } catch (e: GetCredentialException) { + callback.onError(GetCredentialUnknownException()) + } + } + + companion object { + private const val GET_PASSKEY_INTENT_ACTION = "com.kunzisoft.keepass.credentialprovider.GET_PASSKEY" + private const val GET_PASSWORD_INTENT_ACTION = "com.kunzisoft.keepass.credentialprovider.GET_PASSWORD" + + const val NODE_ID_KEY = "nodeId" + const val INTENT_EXTRA_KEY = "CREDENTIAL_DATA" + } + + private fun processGetCredentialsRequest( + request: BeginGetCredentialRequest + ): BeginGetCredentialResponse { + + val callingAppInfo = request.callingAppInfo ?: throw Exception("callingAppInfo is null") + val credentialEntries: MutableList = mutableListOf() + + for (option in request.beginGetCredentialOptions) { + when (option) { + is BeginGetPasswordOption -> { + // TODO + } + is BeginGetPublicKeyCredentialOption -> { + credentialEntries.addAll( + populatePasskeyData(callingAppInfo, option) + ) + } else -> { + Log.d(javaClass.simpleName,"Request not supported") + } + } + } + return BeginGetCredentialResponse(credentialEntries) + } + + private fun populatePasskeyData(callingAppInfo: CallingAppInfo, option: BeginGetPublicKeyCredentialOption): List { + + val json = JSONObject(option.requestJson) + + val relyingPartyId = json.optString("rpId", "") + + val passkeys = getCredentialsFromDb(relyingPartyId) + + val passkeyEntries: MutableList = mutableListOf() + for (passkey in passkeys) { + val data = Bundle() + data.putString(NODE_ID_KEY, passkey.nodeId) + passkeyEntries.add( + PublicKeyCredentialEntry( + context = applicationContext, + username = passkey.username, + pendingIntent = createNewPendingIntent( + GET_PASSKEY_INTENT_ACTION, + data + ), + beginGetPublicKeyCredentialOption = option, + displayName = passkey.displayName, + lastUsedTime = passkey.lastUsedTime, + isAutoSelectAllowed = false + ) + ) + } + return passkeyEntries + } + + private fun getCredentialsFromDb(relyingPartyId: String) : List { + if (mDatabase == null) { + // TODO make sure that the database is open + val dummyPassKey = PasskeyUtil.Passkey("", "unknown", "unlock db", "", "", "", "", null) + return listOf(dummyPassKey) + } + val passkeys = PasskeyUtil.searchPasskeys(mDatabase!!) + val passkeysMatching = passkeys.filter { p -> p.relyingParty == relyingPartyId } + return passkeysMatching + } + + + private fun createNewPendingIntent(action: String, extra: Bundle? = null): PendingIntent { + val intent = Intent(action).setPackage(PACKAGE_NAME).setClass(applicationContext, CredentialProviderActivity::class.java) + if (extra != null) { + intent.putExtra(INTENT_EXTRA_KEY, extra) + } + val requestCode = 42 // not used + return PendingIntent.getActivity( + applicationContext, requestCode, intent, + (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) + ) + } + + override fun onClearCredentialStateRequest(request: ProviderClearCredentialStateRequest, cancellationSignal: CancellationSignal, callback: OutcomeReceiver) { + // nothing to do + } + +} diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/MyAuthenticatorAssertionResponse.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/MyAuthenticatorAssertionResponse.kt new file mode 100644 index 000000000..d8536a26f --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/MyAuthenticatorAssertionResponse.kt @@ -0,0 +1,114 @@ +/* +* 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 androidx.credentials.webauthn + +import android.annotation.SuppressLint +import android.util.Log +import java.security.MessageDigest +import org.json.JSONObject +import org.apache.commons.codec.binary.Base64 + +@SuppressLint("RestrictedApi") +class MyAuthenticatorAssertionResponse( + private val requestOptions: PublicKeyCredentialRequestOptions, + private val credentialId: ByteArray, + private val origin: String, + private val up: Boolean, + private val uv: Boolean, + private val be: Boolean, + private val bs: Boolean, + private var userHandle: ByteArray, + private val packageName: String? = null, + private val clientDataHash: ByteArray? = null, +) : AuthenticatorResponse { + override var clientJson = JSONObject() + var authenticatorData: ByteArray + var signature: ByteArray = byteArrayOf() + + init { + clientJson.put("type", "webauthn.get") + clientJson.put("challenge", b64Encode(requestOptions.challenge)) + clientJson.put("origin", origin) + clientJson.put("crossOrigin", false) + if (packageName != null) { + clientJson.put("androidPackageName", packageName) + } + + authenticatorData = defaultAuthenticatorData() + } + + fun defaultAuthenticatorData(): ByteArray { + val md = MessageDigest.getInstance("SHA-256") + val rpHash = md.digest(requestOptions.rpId.toByteArray()) + var flags: Int = 0 + if (up) { + flags = flags or 0x01 + } + if (uv) { + flags = flags or 0x04 + } + if (be) { + flags = flags or 0x08 + } + if (bs) { + flags = flags or 0x10 + } + val ret = rpHash + byteArrayOf(flags.toByte()) + byteArrayOf(0, 0, 0, 0) + return ret + } + + fun dataToSign(): ByteArray { + val md = MessageDigest.getInstance("SHA-256") + var hash: ByteArray + if (clientDataHash != null) { + hash = clientDataHash + } else { + hash = md.digest(temp().toByteArray()) + } + + return authenticatorData + hash + } + + override fun json(): JSONObject { + + val clientJsonTemp = temp() + Log.w("", clientJsonTemp) + val clientData = clientJsonTemp.toByteArray() + val response = JSONObject() + if (clientDataHash == null) { + response.put("clientDataJSON", b64Encode(clientData)) + } + response.put("authenticatorData", b64Encode(authenticatorData)) + response.put("signature", b64Encode(signature)) + response.put("userHandle", b64Encode(userHandle)) + return response + } + + fun temp(): String { + val clientJsonTemp = clientJson.toString() + val clientJsonGood = clientJsonTemp.replace("\\/", "/") + return clientJsonGood + } + + private fun b64Decode(encodedString: String?): ByteArray { + return Base64.decodeBase64(encodedString) + } + + private fun b64Encode(binData: ByteArray?): String { + return Base64.encodeBase64URLSafeString(binData) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/PasskeyUtil.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/PasskeyUtil.kt new file mode 100644 index 000000000..c1b578c01 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/PasskeyUtil.kt @@ -0,0 +1,93 @@ +package com.kunzisoft.keepass.credentialprovider + +import android.os.Build +import com.kunzisoft.keepass.database.element.Database +import com.kunzisoft.keepass.database.element.Entry +import com.kunzisoft.keepass.database.element.node.NodeIdUUID +import com.kunzisoft.keepass.database.search.SearchHelper +import com.kunzisoft.keepass.database.search.SearchParameters +import com.kunzisoft.keepass.utils.UuidUtil +import java.time.Instant + +class PasskeyUtil { + + data class Passkey(val nodeId: String, val username: String, val displayName: String, val privateKeyPem: String, val credId: String, val userHandle: String, val relyingParty: String, val lastUsedTime: Instant?) + + companion object { + + const val PASSKEY_TAG = "Passkey" + fun convertEntryToPasskey(entry: Entry): Passkey? { + if (!entry.tags.toList().contains(PASSKEY_TAG)) { + return null + } + + val nodeId = UuidUtil.toHexString(entry.nodeId.id)!! + + val displayName = entry.getVisualTitle() + val lastUsedTime = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + entry.lastAccessTime.date.toInstant() + } else { + null + } + var username = "" + var privateKeyPem = "" + var credId = "" + var userHandle = "" + var relyingParty = "" + + for (field in entry.getExtraFields()) { + val fieldName = field.name + + // field names from KeypassXC are used + if (fieldName == "KPEX_PASSKEY_USERNAME") { + username = field.protectedValue.stringValue + } else if (field.name == "KPEX_PASSKEY_PRIVATE_KEY_PEM") { + privateKeyPem = field.protectedValue.stringValue + } else if (field.name == "KPEX_PASSKEY_CREDENTIAL_ID") { + credId = field.protectedValue.stringValue + } else if (field.name == "KPEX_PASSKEY_USER_HANDLE") { + userHandle = field.protectedValue.stringValue + } else if (field.name == "KPEX_PASSKEY_RELYING_PARTY") { + relyingParty = field.protectedValue.stringValue + } + // KPEX_PASSKEY_RELYING_PARTY + } + return Passkey(nodeId, username, displayName, privateKeyPem, credId, userHandle, relyingParty, lastUsedTime) + } + + fun convertEntriesListToPasskeys(entries: List): List { + return entries.mapNotNull { e -> convertEntryToPasskey(e) } + } + + fun searchPasskeys(database: Database): List { + val searchHelper = SearchHelper() + val searchParameters = SearchParameters().apply { + searchQuery = PASSKEY_TAG + searchInTitles = false + searchInUsernames = false + searchInPasswords = false + searchInUrls = false + searchInNotes = false + searchInOTP = false + searchInOther = false + searchInUUIDs = false + searchInTags = true + searchInCurrentGroup = false + searchInSearchableGroup = false + searchInRecycleBin = false + searchInTemplates = false + } + val searchResult = searchHelper.createVirtualGroupWithSearchResult(database, searchParameters, null, Int.MAX_VALUE) + ?: return emptyList() + + return convertEntriesListToPasskeys(searchResult.getChildEntries()) + } + + fun searchPassKeyByNodeId(database: Database, nodeId: String): Passkey? { + val uuidToSearch = UuidUtil.fromHexString(nodeId)!! + val nodeIdUUIDToSearch = NodeIdUUID(uuidToSearch) + val entry = database.getEntryById(nodeIdUUIDToSearch)!! + return convertEntryToPasskey(entry) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/education/Education.kt b/app/src/main/java/com/kunzisoft/keepass/education/Education.kt index 1bd0f9a14..4cfaad746 100644 --- a/app/src/main/java/com/kunzisoft/keepass/education/Education.kt +++ b/app/src/main/java/com/kunzisoft/keepass/education/Education.kt @@ -98,7 +98,7 @@ open class Education(val activity: Activity) { } protected fun getCircleColor(): Int { - val typedArray = activity.obtainStyledAttributes(intArrayOf(R.attr.colorPrimaryContainer)) + val typedArray = activity.obtainStyledAttributes(intArrayOf(com.google.android.material.R.attr.colorPrimaryContainer)) val colorControl = typedArray.getColor(0, Color.GREEN) typedArray.recycle() return colorControl @@ -109,7 +109,7 @@ open class Education(val activity: Activity) { } protected fun getTextColor(): Int { - val typedArray = activity.obtainStyledAttributes(intArrayOf(R.attr.colorOnPrimaryContainer)) + val typedArray = activity.obtainStyledAttributes(intArrayOf(com.google.android.material.R.attr.colorOnPrimaryContainer)) val colorControl = typedArray.getColor(0, Color.WHITE) typedArray.recycle() return colorControl diff --git a/app/src/main/java/com/kunzisoft/keepass/services/NotificationService.kt b/app/src/main/java/com/kunzisoft/keepass/services/NotificationService.kt index a3eb2bf9b..774272583 100644 --- a/app/src/main/java/com/kunzisoft/keepass/services/NotificationService.kt +++ b/app/src/main/java/com/kunzisoft/keepass/services/NotificationService.kt @@ -70,7 +70,7 @@ abstract class NotificationService : Service() { setTheme(Stylish.getThemeId(this)) val typedValue = TypedValue() val theme = theme - theme.resolveAttribute(R.attr.colorPrimary, typedValue, true) + theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue, true) colorNotificationAccent = typedValue.data } diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/DialogColorPreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/DialogColorPreference.kt index 68ff04335..134848ef4 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preference/DialogColorPreference.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/DialogColorPreference.kt @@ -29,7 +29,7 @@ import com.kunzisoft.keepass.R class DialogColorPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = R.attr.dialogPreferenceStyle, + defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE, defStyleRes: Int = defStyleAttr) : ChromaPreferenceCompat(context, attrs, defStyleAttr, defStyleRes) { diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/DialogListExplanationPreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/DialogListExplanationPreference.kt index c0a028bbe..83d2531b5 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preference/DialogListExplanationPreference.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/DialogListExplanationPreference.kt @@ -27,7 +27,7 @@ import com.kunzisoft.keepass.R class DialogListExplanationPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = R.attr.dialogPreferenceStyle, + defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE, defStyleRes: Int = defStyleAttr) : DialogPreference(context, attrs, defStyleAttr, defStyleRes) { 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..6d1730e27 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 @@ -27,7 +27,7 @@ import com.kunzisoft.keepass.R class DurationDialogPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = R.attr.dialogPreferenceStyle, + defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE, defStyleRes: Int = defStyleAttr) : DialogPreference(context, attrs, defStyleAttr, defStyleRes) { @@ -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/settings/preference/IconPackListPreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/IconPackListPreference.kt index 3ed4a43ec..6d99c99f3 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preference/IconPackListPreference.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/IconPackListPreference.kt @@ -28,7 +28,7 @@ import java.util.* class IconPackListPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = R.attr.dialogPreferenceStyle, + defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE, defStyleRes: Int = defStyleAttr) : ListPreference(context, attrs, defStyleAttr, defStyleRes) { diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfNumberPreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfNumberPreference.kt index 367f73489..0be703810 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfNumberPreference.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfNumberPreference.kt @@ -27,7 +27,7 @@ import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine open class InputKdfNumberPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = R.attr.dialogPreferenceStyle, + defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE, defStyleRes: Int = defStyleAttr) : DialogPreference(context, attrs, defStyleAttr, defStyleRes) { diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfSizePreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfSizePreference.kt index ff07d973d..cb691ae88 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfSizePreference.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputKdfSizePreference.kt @@ -26,7 +26,7 @@ import com.kunzisoft.keepass.utils.DataByte class InputKdfSizePreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = R.attr.dialogPreferenceStyle, + defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE, defStyleRes: Int = defStyleAttr) : InputKdfNumberPreference(context, attrs, defStyleAttr, defStyleRes) { diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputListPreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputListPreference.kt index 708ba3fd1..eaaee1223 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputListPreference.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputListPreference.kt @@ -26,7 +26,7 @@ import com.kunzisoft.keepass.R open class InputListPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = R.attr.dialogPreferenceStyle, + defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE, defStyleRes: Int = defStyleAttr) : DialogPreference(context, attrs, defStyleAttr, defStyleRes) { diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputNumberPreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputNumberPreference.kt index 116eaedbe..8a1f69879 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputNumberPreference.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputNumberPreference.kt @@ -26,7 +26,7 @@ import com.kunzisoft.keepass.R open class InputNumberPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = R.attr.dialogPreferenceStyle, + defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE, defStyleRes: Int = defStyleAttr) : DialogPreference(context, attrs, defStyleAttr, defStyleRes) { diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputSizePreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputSizePreference.kt index 39dee92e6..2030fc7c2 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputSizePreference.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputSizePreference.kt @@ -26,7 +26,7 @@ import com.kunzisoft.keepass.utils.DataByte open class InputSizePreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = R.attr.dialogPreferenceStyle, + defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE, defStyleRes: Int = defStyleAttr) : InputNumberPreference(context, attrs, defStyleAttr, defStyleRes) { diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputTextPreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputTextPreference.kt index a8e18a750..b2bc8e3ce 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputTextPreference.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/InputTextPreference.kt @@ -27,7 +27,7 @@ import com.kunzisoft.keepass.R open class InputTextPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = R.attr.dialogPreferenceStyle, + defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE, defStyleRes: Int = defStyleAttr) : DialogPreference(context, attrs, defStyleAttr, defStyleRes) { diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/PreferenceConstant.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/PreferenceConstant.kt new file mode 100644 index 000000000..0ac354129 --- /dev/null +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/PreferenceConstant.kt @@ -0,0 +1,8 @@ +package com.kunzisoft.keepass.settings.preference + +class PreferenceConstant { + companion object { + const val R_ATTR_DIALOG_PREFERENCE_STYLE: Int = 16842897; + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/settings/preference/TextPreference.kt b/app/src/main/java/com/kunzisoft/keepass/settings/preference/TextPreference.kt index 8b204e0c0..b60e48d0a 100644 --- a/app/src/main/java/com/kunzisoft/keepass/settings/preference/TextPreference.kt +++ b/app/src/main/java/com/kunzisoft/keepass/settings/preference/TextPreference.kt @@ -27,7 +27,7 @@ import com.kunzisoft.keepass.R open class TextPreference @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = R.attr.dialogPreferenceStyle, + defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE, defStyleRes: Int = defStyleAttr) : DialogPreference(context, attrs, defStyleAttr, defStyleRes) { diff --git a/app/src/main/java/com/kunzisoft/keepass/view/SectionView.kt b/app/src/main/java/com/kunzisoft/keepass/view/SectionView.kt index 396b66835..26666ce6c 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/SectionView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/SectionView.kt @@ -30,7 +30,7 @@ import com.kunzisoft.keepass.R class SectionView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, - defStyle: Int = R.attr.cardViewStyle) + defStyle: Int = com.google.android.material.R.attr.cardViewStyle) : CardView(context, attrs, defStyle) { private var containerSectionView = LinearLayout(context).apply { 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 dd336c843..d6c7fb1f6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/TemplateAbstractView.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/TemplateAbstractView.kt @@ -148,7 +148,7 @@ abstract class TemplateAbstractView< // Build each section template.sections.forEach { templateSection -> - val sectionView = SectionView(context, null, R.attr.cardViewStyle) + val sectionView = SectionView(context, null, com.google.android.material.R.attr.cardViewStyle) // Build each attribute templateSection.attributes.forEach { templateAttribute -> diff --git a/app/src/main/java/com/kunzisoft/keepass/view/ToolbarAction.kt b/app/src/main/java/com/kunzisoft/keepass/view/ToolbarAction.kt index 82f0fbfa1..975da93d3 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/ToolbarAction.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/ToolbarAction.kt @@ -47,7 +47,7 @@ class ToolbarAction @JvmOverloads constructor(context: Context, init { ContextCompat.getDrawable(context, R.drawable.ic_close_white_24dp)?.let { closeDrawable -> val typedValue = TypedValue() - context.theme.resolveAttribute(R.attr.colorOnSurface, typedValue, true) + context.theme.resolveAttribute(com.google.android.material.R.attr.colorOnSurface, typedValue, true) @ColorInt val colorControl = typedValue.data closeDrawable.colorFilter = PorterDuffColorFilter(colorControl, PorterDuff.Mode.SRC_ATOP) navigationIcon = closeDrawable diff --git a/app/src/main/java/com/kunzisoft/keepass/view/ToolbarSpecial.kt b/app/src/main/java/com/kunzisoft/keepass/view/ToolbarSpecial.kt index 8367fdea0..141490c47 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/ToolbarSpecial.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/ToolbarSpecial.kt @@ -38,7 +38,7 @@ class ToolbarSpecial @JvmOverloads constructor(context: Context, init { ContextCompat.getDrawable(context, R.drawable.ic_arrow_back_white_24dp)?.let { closeDrawable -> val typedValue = TypedValue() - context.theme.resolveAttribute(R.attr.colorOnSurface, typedValue, true) + context.theme.resolveAttribute(com.google.android.material.R.attr.colorOnSurface, typedValue, true) @ColorInt val colorOnSurface = typedValue.data closeDrawable.colorFilter = PorterDuffColorFilter(colorOnSurface, PorterDuff.Mode.SRC_ATOP) navigationIcon = closeDrawable diff --git a/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt b/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt index 90e9ff5b2..a329902f2 100644 --- a/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt +++ b/app/src/main/java/com/kunzisoft/keepass/view/ViewUtil.kt @@ -117,7 +117,7 @@ fun TextView.customLink(listener: (View) -> Unit) { fun Snackbar.asError(): Snackbar { this.view.apply { setBackgroundColor(Color.RED) - findViewById(R.id.snackbar_text).setTextColor(Color.WHITE) + findViewById(com.google.android.material.R.id.snackbar_text).setTextColor(Color.WHITE) } return this } @@ -308,7 +308,7 @@ fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, appl WindowCompat.setDecorFitsSystemWindows(window, false) window.navigationBarColor = ContextCompat.getColor(this, R.color.surface_selector) if (applyToStatusBar) { - obtainStyledAttributes(intArrayOf(R.attr.colorSurface)).apply { + obtainStyledAttributes(intArrayOf(com.google.android.material.R.attr.colorSurface)).apply { window.statusBarColor = getColor(0, Color.GRAY) recycle() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f4befb9e5..01bff8c47 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -733,4 +733,8 @@ Displays foreground and background colors for an entry Hide expired entries Expired entries are not shown + + Confirm passkey usage + for %1$s + Cancel \ No newline at end of file diff --git a/app/src/main/res/xml/provider.xml b/app/src/main/res/xml/provider.xml new file mode 100644 index 000000000..5dc307a8c --- /dev/null +++ b/app/src/main/res/xml/provider.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/build.gradle b/build.gradle index 80b4e9176..b736f33cb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.8.20' + ext.kotlin_version = '2.0.0' ext.android_core_version = '1.10.1' ext.android_appcompat_version = '1.6.1' ext.android_material_version = '1.9.0' @@ -10,7 +10,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:8.4.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/crypto/build.gradle b/crypto/build.gradle index 20634e48f..ddab8d18a 100644 --- a/crypto/build.gradle +++ b/crypto/build.gradle @@ -9,7 +9,7 @@ android { ndkVersion "21.4.7075529" defaultConfig { - minSdkVersion 15 + minSdkVersion 19 targetSdkVersion 34 multiDexEnabled true @@ -40,7 +40,7 @@ android { dependencies { // Crypto - implementation 'org.bouncycastle:bcprov-jdk15on:1.70' + implementation 'org.bouncycastle:bcpkix-jdk18on:1.78.1' testImplementation "androidx.test:runner:$android_test_version" } diff --git a/crypto/src/main/java/com/kunzisoft/signature/Signature.kt b/crypto/src/main/java/com/kunzisoft/signature/Signature.kt new file mode 100644 index 000000000..ee94f44ca --- /dev/null +++ b/crypto/src/main/java/com/kunzisoft/signature/Signature.kt @@ -0,0 +1,43 @@ +package com.kunzisoft.signature + +import android.util.Log +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo +import org.bouncycastle.jce.provider.BouncyCastleProvider +import java.io.StringReader +import java.security.PrivateKey +import java.security.Security +import java.security.Signature + +import org.bouncycastle.openssl.PEMParser +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter + +object Signature { + + init { + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) + Security.addProvider(BouncyCastleProvider()) + } + fun sign(privateKeyPem: String, message: ByteArray): ByteArray { + val privateKey = createPrivateKey(privateKeyPem) + val algorithmKey = privateKey.algorithm + val algorithmSignature = when (algorithmKey) { + "EC" -> "SHA256withECDSA" + "ECDSA" -> "SHA256withECDSA" + "RSA" -> "SHA256withRSA" + else -> "no signature algorithms known" + } + val sig = Signature.getInstance(algorithmSignature, BouncyCastleProvider.PROVIDER_NAME) + sig.initSign(privateKey) + sig.update(message) + return sig.sign() + } + + private fun createPrivateKey(privateKeyPem: String): PrivateKey { + val targetReader = StringReader(privateKeyPem); + val a = PEMParser(targetReader) + val privateKeyInfo = a.readObject() as PrivateKeyInfo + val privateKey = JcaPEMKeyConverter().getPrivateKey(privateKeyInfo) + return privateKey + } + +} \ No newline at end of file diff --git a/database/build.gradle b/database/build.gradle index b11df6359..5fd603a8f 100644 --- a/database/build.gradle +++ b/database/build.gradle @@ -6,7 +6,7 @@ android { compileSdkVersion 34 defaultConfig { - minSdkVersion 15 + minSdkVersion 19 targetSdk 34 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ae04661ee..00def7772 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Sun Sep 08 17:39:21 CEST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/icon-pack/build.gradle b/icon-pack/build.gradle index bceaec7f5..fe907a19f 100644 --- a/icon-pack/build.gradle +++ b/icon-pack/build.gradle @@ -6,7 +6,7 @@ android { compileSdkVersion 34 defaultConfig { - minSdkVersion 14 + minSdkVersion 19 targetSdkVersion 34 } diff --git a/icon-pack/classic/build.gradle b/icon-pack/classic/build.gradle index c5262974d..6fcd807bc 100644 --- a/icon-pack/classic/build.gradle +++ b/icon-pack/classic/build.gradle @@ -5,7 +5,7 @@ android { compileSdkVersion 34 defaultConfig { - minSdkVersion 14 + minSdkVersion 19 targetSdkVersion 34 } diff --git a/icon-pack/material/build.gradle b/icon-pack/material/build.gradle index b4dfe5d79..b17b7ebee 100644 --- a/icon-pack/material/build.gradle +++ b/icon-pack/material/build.gradle @@ -5,7 +5,7 @@ android { compileSdkVersion 34 defaultConfig { - minSdkVersion 14 + minSdkVersion 19 targetSdkVersion 34 }