first version credential provider

This commit is contained in:
cali
2024-09-08 18:49:27 +02:00
parent 8177c9c34b
commit 69114c3cc0
42 changed files with 1270 additions and 39 deletions

View File

@@ -109,3 +109,24 @@ Other questions? You can read the [FAQ](https://github.com/Kunzisoft/KeePassDX/w
along with KeePassDX. If not, see <http://www.gnu.org/licenses/>. along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*This project is a fork of [KeePassDroid](https://github.com/bpellin/keepassdroid) by bpellin.* *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)

View File

@@ -9,7 +9,7 @@ android {
defaultConfig { defaultConfig {
applicationId "com.kunzisoft.keepass" applicationId "com.kunzisoft.keepass"
minSdkVersion 15 minSdkVersion 19
targetSdkVersion 34 targetSdkVersion 34
versionCode = 132 versionCode = 132
versionName = "4.1.0" versionName = "4.1.0"
@@ -35,6 +35,10 @@ android {
} }
} }
buildFeatures {
buildConfig true
}
dependenciesInfo { dependenciesInfo {
// Disables dependency metadata when building APKs. // Disables dependency metadata when building APKs.
includeInApk = false includeInApk = false
@@ -98,6 +102,10 @@ android {
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" 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" def room_version = "2.5.1"
@@ -122,6 +130,7 @@ dependencies {
implementation "com.splitwise:tokenautocomplete:4.0.0-beta05" implementation "com.splitwise:tokenautocomplete:4.0.0-beta05"
// Database // Database
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
implementation project(':crypto')
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
// Autofill // Autofill
implementation "androidx.autofill:autofill:1.1.0" implementation "androidx.autofill:autofill:1.1.0"
@@ -137,6 +146,10 @@ dependencies {
// Password generator // Password generator
implementation 'me.gosimple:nbvcxz:1.5.0' implementation 'me.gosimple:nbvcxz:1.5.0'
// Credentials Provider
implementation "androidx.credentials:credentials:1.2.2"
implementation "androidx.credentials:credentials-play-services-auth:1.2.2"
// Modules import // Modules import
implementation project(path: ':database') implementation project(path: ':database')
implementation project(path: ':icon-pack') implementation project(path: ':icon-pack')

View File

@@ -44,7 +44,9 @@
android:largeHeap="true" android:largeHeap="true"
android:resizeableActivity="true" android:resizeableActivity="true"
android:theme="@style/KeepassDXStyle.Night" android:theme="@style/KeepassDXStyle.Night"
tools:targetApi="s"> tools:targetApi="s"
android:enableOnBackInvokedCallback="true">
<meta-data <meta-data
android:name="com.google.android.backup.api_key" android:name="com.google.android.backup.api_key"
android:value="${googleAndroidBackupAPIKey}" /> android:value="${googleAndroidBackupAPIKey}" />
@@ -199,6 +201,14 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name="com.kunzisoft.keepass.credentialprovider.CredentialProviderActivity"
android:label="CredentialProviderActivity"
android:exported="true">
<intent-filter>
<action android:name="com.kunzisoft.keepass.credentialprovider.GET_PASSKEY"/>
</intent-filter>
</activity>
<service <service
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService" android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
android:foregroundServiceType="dataSync" android:foregroundServiceType="dataSync"
@@ -248,6 +258,21 @@
<action android:name="android.view.InputMethod" /> <action android:name="android.view.InputMethod" />
</intent-filter> </intent-filter>
</service> </service>
<service android:name="com.kunzisoft.keepass.credentialprovider.KeePassDXCredentialProviderService"
android:enabled="true"
android:exported="true"
android:label="KeyPassDX Credential Provider"
android:icon="@mipmap/ic_launcher"
android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE">
<intent-filter>
<action android:name="android.service.credentials.CredentialProviderService"/>
</intent-filter>
<meta-data
android:name="android.credentials.provider"
android:resource="@xml/provider"/>
</service>
<receiver <receiver
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver" android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
android:exported="true"> android:exported="true">

View File

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

View File

@@ -163,9 +163,9 @@ class EntryActivity : DatabaseLockActivity() {
toolbar?.title = " " toolbar?.title = " "
// Retrieve the textColor to tint the toolbar // Retrieve the textColor to tint the toolbar
val taColorSecondary = theme.obtainStyledAttributes(intArrayOf(R.attr.colorSecondary)) val taColorSecondary = theme.obtainStyledAttributes(intArrayOf(com.google.android.material.R.attr.colorSecondary))
val taColorSurface = theme.obtainStyledAttributes(intArrayOf(R.attr.colorSurface)) val taColorSurface = theme.obtainStyledAttributes(intArrayOf(com.google.android.material.R.attr.colorSurface))
val taColorOnSurface = theme.obtainStyledAttributes(intArrayOf(R.attr.colorOnSurface)) val taColorOnSurface = theme.obtainStyledAttributes(intArrayOf(com.google.android.material.R.attr.colorOnSurface))
val taColorBackground = theme.obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground)) val taColorBackground = theme.obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
mColorSecondary = taColorSecondary.getColor(0, Color.BLACK) mColorSecondary = taColorSecondary.getColor(0, Color.BLACK)
mColorSurface = taColorSurface.getColor(0, Color.BLACK) mColorSurface = taColorSurface.getColor(0, Color.BLACK)

View File

@@ -30,6 +30,7 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
} }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@Deprecated(message = "")
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)

View File

@@ -108,7 +108,7 @@ class GroupDialogFragment : DatabaseDialogFragment() {
uuidReferenceView = root.findViewById(R.id.group_UUID_reference) uuidReferenceView = root.findViewById(R.id.group_UUID_reference)
// Retrieve the textColor to tint the icon // 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) mIconColor = ta.getColor(0, Color.WHITE)
ta.recycle() ta.recycle()

View File

@@ -40,7 +40,7 @@ class BreadcrumbAdapter(val context: Context)
mShowUUID = PreferencesUtil.showUUID(context) mShowUUID = PreferencesUtil.showUUID(context)
// Retrieve the color to tint the icon // 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) mIconColor = taIconColor.getColor(0, Color.WHITE)
taIconColor.recycle() taIconColor.recycle()
} }

View File

@@ -118,7 +118,7 @@ class NodesAdapter (
this.mNodeSortedListCallback = NodeSortedListCallback() this.mNodeSortedListCallback = NodeSortedListCallback()
this.mNodeSortedList = SortedList(Node::class.java, mNodeSortedListCallback) 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) this.mColorSurfaceContainer = taColorSurfaceContainer.getColor(0, Color.BLACK)
taColorSurfaceContainer.recycle() taColorSurfaceContainer.recycle()
// Retrieve the color to tint the icon // Retrieve the color to tint the icon
@@ -134,11 +134,11 @@ class NodesAdapter (
this.mTextColorSecondary = taTextColorSecondary.getColor(0, Color.BLACK) this.mTextColorSecondary = taTextColorSecondary.getColor(0, Color.BLACK)
taTextColorSecondary.recycle() taTextColorSecondary.recycle()
// To get background color for selection // 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) this.mColorSecondary = taColorSecondary.getColor(0, Color.GRAY)
taColorSecondary.recycle() taColorSecondary.recycle()
// To get text color for selection // 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) this.mColorOnSecondary = taColorOnSecondary.getColor(0, Color.WHITE)
taColorOnSecondary.recycle() taColorOnSecondary.recycle()
} }

View File

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

View File

@@ -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<BeginCreateCredentialResponse, CreateCredentialException>) {
TODO("Not yet implemented")
}
override fun onBeginGetCredentialRequest(
request: BeginGetCredentialRequest,
cancellationSignal: CancellationSignal,
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
) {
/*
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<CredentialEntry> = 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<CredentialEntry> {
val json = JSONObject(option.requestJson)
val relyingPartyId = json.optString("rpId", "")
val passkeys = getCredentialsFromDb(relyingPartyId)
val passkeyEntries: MutableList<CredentialEntry> = 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<PasskeyUtil.Passkey> {
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<Void?, ClearCredentialException>) {
// nothing to do
}
}

View File

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

View File

@@ -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<Entry>): List<Passkey> {
return entries.mapNotNull { e -> convertEntryToPasskey(e) }
}
fun searchPasskeys(database: Database): List<Passkey> {
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)
}
}
}

View File

@@ -98,7 +98,7 @@ open class Education(val activity: Activity) {
} }
protected fun getCircleColor(): Int { 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) val colorControl = typedArray.getColor(0, Color.GREEN)
typedArray.recycle() typedArray.recycle()
return colorControl return colorControl
@@ -109,7 +109,7 @@ open class Education(val activity: Activity) {
} }
protected fun getTextColor(): Int { 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) val colorControl = typedArray.getColor(0, Color.WHITE)
typedArray.recycle() typedArray.recycle()
return colorControl return colorControl

View File

@@ -70,7 +70,7 @@ abstract class NotificationService : Service() {
setTheme(Stylish.getThemeId(this)) setTheme(Stylish.getThemeId(this))
val typedValue = TypedValue() val typedValue = TypedValue()
val theme = theme val theme = theme
theme.resolveAttribute(R.attr.colorPrimary, typedValue, true) theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue, true)
colorNotificationAccent = typedValue.data colorNotificationAccent = typedValue.data
} }

View File

@@ -29,7 +29,7 @@ import com.kunzisoft.keepass.R
class DialogColorPreference @JvmOverloads constructor(context: Context, class DialogColorPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle, defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: ChromaPreferenceCompat(context, attrs, defStyleAttr, defStyleRes) { : ChromaPreferenceCompat(context, attrs, defStyleAttr, defStyleRes) {

View File

@@ -27,7 +27,7 @@ import com.kunzisoft.keepass.R
class DialogListExplanationPreference @JvmOverloads constructor(context: Context, class DialogListExplanationPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle, defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: DialogPreference(context, attrs, defStyleAttr, defStyleRes) { : DialogPreference(context, attrs, defStyleAttr, defStyleRes) {

View File

@@ -27,7 +27,7 @@ import com.kunzisoft.keepass.R
class DurationDialogPreference @JvmOverloads constructor(context: Context, class DurationDialogPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle, defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: DialogPreference(context, attrs, defStyleAttr, defStyleRes) { : DialogPreference(context, attrs, defStyleAttr, defStyleRes) {
@@ -52,6 +52,7 @@ class DurationDialogPreference @JvmOverloads constructor(context: Context,
notifyChanged() notifyChanged()
} }
@Deprecated(message = "")
override fun onSetInitialValue(restorePersistedValue: Boolean, defaultValue: Any?) { override fun onSetInitialValue(restorePersistedValue: Boolean, defaultValue: Any?) {
if (restorePersistedValue) { if (restorePersistedValue) {
mDuration = getPersistedString(mDuration.toString()).toLongOrNull() ?: mDuration mDuration = getPersistedString(mDuration.toString()).toLongOrNull() ?: mDuration

View File

@@ -28,7 +28,7 @@ import java.util.*
class IconPackListPreference @JvmOverloads constructor(context: Context, class IconPackListPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle, defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: ListPreference(context, attrs, defStyleAttr, defStyleRes) { : ListPreference(context, attrs, defStyleAttr, defStyleRes) {

View File

@@ -27,7 +27,7 @@ import com.kunzisoft.keepass.database.crypto.kdf.KdfEngine
open class InputKdfNumberPreference @JvmOverloads constructor(context: Context, open class InputKdfNumberPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle, defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: DialogPreference(context, attrs, defStyleAttr, defStyleRes) { : DialogPreference(context, attrs, defStyleAttr, defStyleRes) {

View File

@@ -26,7 +26,7 @@ import com.kunzisoft.keepass.utils.DataByte
class InputKdfSizePreference @JvmOverloads constructor(context: Context, class InputKdfSizePreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle, defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: InputKdfNumberPreference(context, attrs, defStyleAttr, defStyleRes) { : InputKdfNumberPreference(context, attrs, defStyleAttr, defStyleRes) {

View File

@@ -26,7 +26,7 @@ import com.kunzisoft.keepass.R
open class InputListPreference @JvmOverloads constructor(context: Context, open class InputListPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle, defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: DialogPreference(context, attrs, defStyleAttr, defStyleRes) { : DialogPreference(context, attrs, defStyleAttr, defStyleRes) {

View File

@@ -26,7 +26,7 @@ import com.kunzisoft.keepass.R
open class InputNumberPreference @JvmOverloads constructor(context: Context, open class InputNumberPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle, defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: DialogPreference(context, attrs, defStyleAttr, defStyleRes) { : DialogPreference(context, attrs, defStyleAttr, defStyleRes) {

View File

@@ -26,7 +26,7 @@ import com.kunzisoft.keepass.utils.DataByte
open class InputSizePreference @JvmOverloads constructor(context: Context, open class InputSizePreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle, defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: InputNumberPreference(context, attrs, defStyleAttr, defStyleRes) { : InputNumberPreference(context, attrs, defStyleAttr, defStyleRes) {

View File

@@ -27,7 +27,7 @@ import com.kunzisoft.keepass.R
open class InputTextPreference @JvmOverloads constructor(context: Context, open class InputTextPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle, defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: DialogPreference(context, attrs, defStyleAttr, defStyleRes) { : DialogPreference(context, attrs, defStyleAttr, defStyleRes) {

View File

@@ -0,0 +1,8 @@
package com.kunzisoft.keepass.settings.preference
class PreferenceConstant {
companion object {
const val R_ATTR_DIALOG_PREFERENCE_STYLE: Int = 16842897;
}
}

View File

@@ -27,7 +27,7 @@ import com.kunzisoft.keepass.R
open class TextPreference @JvmOverloads constructor(context: Context, open class TextPreference @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dialogPreferenceStyle, defStyleAttr: Int = PreferenceConstant.R_ATTR_DIALOG_PREFERENCE_STYLE,
defStyleRes: Int = defStyleAttr) defStyleRes: Int = defStyleAttr)
: DialogPreference(context, attrs, defStyleAttr, defStyleRes) { : DialogPreference(context, attrs, defStyleAttr, defStyleRes) {

View File

@@ -30,7 +30,7 @@ import com.kunzisoft.keepass.R
class SectionView @JvmOverloads constructor(context: Context, class SectionView @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyle: Int = R.attr.cardViewStyle) defStyle: Int = com.google.android.material.R.attr.cardViewStyle)
: CardView(context, attrs, defStyle) { : CardView(context, attrs, defStyle) {
private var containerSectionView = LinearLayout(context).apply { private var containerSectionView = LinearLayout(context).apply {

View File

@@ -148,7 +148,7 @@ abstract class TemplateAbstractView<
// Build each section // Build each section
template.sections.forEach { templateSection -> 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 // Build each attribute
templateSection.attributes.forEach { templateAttribute -> templateSection.attributes.forEach { templateAttribute ->

View File

@@ -47,7 +47,7 @@ class ToolbarAction @JvmOverloads constructor(context: Context,
init { init {
ContextCompat.getDrawable(context, R.drawable.ic_close_white_24dp)?.let { closeDrawable -> ContextCompat.getDrawable(context, R.drawable.ic_close_white_24dp)?.let { closeDrawable ->
val typedValue = TypedValue() 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 @ColorInt val colorControl = typedValue.data
closeDrawable.colorFilter = PorterDuffColorFilter(colorControl, PorterDuff.Mode.SRC_ATOP) closeDrawable.colorFilter = PorterDuffColorFilter(colorControl, PorterDuff.Mode.SRC_ATOP)
navigationIcon = closeDrawable navigationIcon = closeDrawable

View File

@@ -38,7 +38,7 @@ class ToolbarSpecial @JvmOverloads constructor(context: Context,
init { init {
ContextCompat.getDrawable(context, R.drawable.ic_arrow_back_white_24dp)?.let { closeDrawable -> ContextCompat.getDrawable(context, R.drawable.ic_arrow_back_white_24dp)?.let { closeDrawable ->
val typedValue = TypedValue() 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 @ColorInt val colorOnSurface = typedValue.data
closeDrawable.colorFilter = PorterDuffColorFilter(colorOnSurface, PorterDuff.Mode.SRC_ATOP) closeDrawable.colorFilter = PorterDuffColorFilter(colorOnSurface, PorterDuff.Mode.SRC_ATOP)
navigationIcon = closeDrawable navigationIcon = closeDrawable

View File

@@ -117,7 +117,7 @@ fun TextView.customLink(listener: (View) -> Unit) {
fun Snackbar.asError(): Snackbar { fun Snackbar.asError(): Snackbar {
this.view.apply { this.view.apply {
setBackgroundColor(Color.RED) setBackgroundColor(Color.RED)
findViewById<TextView>(R.id.snackbar_text).setTextColor(Color.WHITE) findViewById<TextView>(com.google.android.material.R.id.snackbar_text).setTextColor(Color.WHITE)
} }
return this return this
} }
@@ -308,7 +308,7 @@ fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, appl
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
window.navigationBarColor = ContextCompat.getColor(this, R.color.surface_selector) window.navigationBarColor = ContextCompat.getColor(this, R.color.surface_selector)
if (applyToStatusBar) { if (applyToStatusBar) {
obtainStyledAttributes(intArrayOf(R.attr.colorSurface)).apply { obtainStyledAttributes(intArrayOf(com.google.android.material.R.attr.colorSurface)).apply {
window.statusBarColor = getColor(0, Color.GRAY) window.statusBarColor = getColor(0, Color.GRAY)
recycle() recycle()
} }

View File

@@ -733,4 +733,8 @@
<string name="show_entry_colors_summary">Displays foreground and background colors for an entry</string> <string name="show_entry_colors_summary">Displays foreground and background colors for an entry</string>
<string name="hide_expired_entries_title">Hide expired entries</string> <string name="hide_expired_entries_title">Hide expired entries</string>
<string name="hide_expired_entries_summary">Expired entries are not shown</string> <string name="hide_expired_entries_summary">Expired entries are not shown</string>
<string name="passkey_biometric_prompt_title">Confirm passkey usage</string>
<string name="passkey_biometric_prompt_subtitle">for %1$s</string>
<string name="passkey_biometric_prompt_negative_button_text">Cancel</string>
</resources> </resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<credential-provider xmlns:android="http://schemas.android.com/apk/res/android">
<capabilities>
<capability name="android.credentials.TYPE_PASSWORD_CREDENTIAL" />
<capability name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
</capabilities>
</credential-provider>

View File

@@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.8.20' ext.kotlin_version = '2.0.0'
ext.android_core_version = '1.10.1' ext.android_core_version = '1.10.1'
ext.android_appcompat_version = '1.6.1' ext.android_appcompat_version = '1.6.1'
ext.android_material_version = '1.9.0' ext.android_material_version = '1.9.0'
@@ -10,7 +10,7 @@ buildscript {
google() google()
} }
dependencies { 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" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }

View File

@@ -9,7 +9,7 @@ android {
ndkVersion "21.4.7075529" ndkVersion "21.4.7075529"
defaultConfig { defaultConfig {
minSdkVersion 15 minSdkVersion 19
targetSdkVersion 34 targetSdkVersion 34
multiDexEnabled true multiDexEnabled true
@@ -40,7 +40,7 @@ android {
dependencies { dependencies {
// Crypto // Crypto
implementation 'org.bouncycastle:bcprov-jdk15on:1.70' implementation 'org.bouncycastle:bcpkix-jdk18on:1.78.1'
testImplementation "androidx.test:runner:$android_test_version" testImplementation "androidx.test:runner:$android_test_version"
} }

View File

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

View File

@@ -6,7 +6,7 @@ android {
compileSdkVersion 34 compileSdkVersion 34
defaultConfig { defaultConfig {
minSdkVersion 15 minSdkVersion 19
targetSdk 34 targetSdk 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -1,5 +1,6 @@
#Sun Sep 08 17:39:21 CEST 2024
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@@ -6,7 +6,7 @@ android {
compileSdkVersion 34 compileSdkVersion 34
defaultConfig { defaultConfig {
minSdkVersion 14 minSdkVersion 19
targetSdkVersion 34 targetSdkVersion 34
} }

View File

@@ -5,7 +5,7 @@ android {
compileSdkVersion 34 compileSdkVersion 34
defaultConfig { defaultConfig {
minSdkVersion 14 minSdkVersion 19
targetSdkVersion 34 targetSdkVersion 34
} }

View File

@@ -5,7 +5,7 @@ android {
compileSdkVersion 34 compileSdkVersion 34
defaultConfig { defaultConfig {
minSdkVersion 14 minSdkVersion 19
targetSdkVersion 34 targetSdkVersion 34
} }