mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
first version credential provider
This commit is contained in:
21
README.md
21
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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
*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)
|
||||
@@ -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')
|
||||
|
||||
@@ -44,7 +44,9 @@
|
||||
android:largeHeap="true"
|
||||
android:resizeableActivity="true"
|
||||
android:theme="@style/KeepassDXStyle.Night"
|
||||
tools:targetApi="s">
|
||||
tools:targetApi="s"
|
||||
android:enableOnBackInvokedCallback="true">
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.backup.api_key"
|
||||
android:value="${googleAndroidBackupAPIKey}" />
|
||||
@@ -199,6 +201,14 @@
|
||||
</intent-filter>
|
||||
</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
|
||||
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
@@ -248,6 +258,21 @@
|
||||
<action android:name="android.view.InputMethod" />
|
||||
</intent-filter>
|
||||
</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
|
||||
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
|
||||
android:exported="true">
|
||||
|
||||
536
app/src/main/assets/trustedPackages.json
Normal file
536
app/src/main/assets/trustedPackages.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -30,6 +30,7 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated(message = "")
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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("/")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.kunzisoft.keepass.settings.preference
|
||||
|
||||
class PreferenceConstant {
|
||||
companion object {
|
||||
const val R_ATTR_DIALOG_PREFERENCE_STYLE: Int = 16842897;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -117,7 +117,7 @@ fun TextView.customLink(listener: (View) -> Unit) {
|
||||
fun Snackbar.asError(): Snackbar {
|
||||
this.view.apply {
|
||||
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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -733,4 +733,8 @@
|
||||
<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_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>
|
||||
7
app/src/main/res/xml/provider.xml
Normal file
7
app/src/main/res/xml/provider.xml
Normal 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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
43
crypto/src/main/java/com/kunzisoft/signature/Signature.kt
Normal file
43
crypto/src/main/java/com/kunzisoft/signature/Signature.kt
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,7 +6,7 @@ android {
|
||||
compileSdkVersion 34
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 15
|
||||
minSdkVersion 19
|
||||
targetSdk 34
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
|
||||
|
||||
@@ -6,7 +6,7 @@ android {
|
||||
compileSdkVersion 34
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 14
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 34
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ android {
|
||||
compileSdkVersion 34
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 14
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 34
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ android {
|
||||
compileSdkVersion 34
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 14
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 34
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user