mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
fix: First validation pass
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -19,6 +19,9 @@ bin/
|
|||||||
gen/
|
gen/
|
||||||
out/
|
out/
|
||||||
|
|
||||||
|
# Kotlin folder
|
||||||
|
.kotlin/
|
||||||
|
|
||||||
# Gradle files
|
# Gradle files
|
||||||
.gradle/
|
.gradle/
|
||||||
build/
|
build/
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig true
|
||||||
|
}
|
||||||
|
|
||||||
dependenciesInfo {
|
dependenciesInfo {
|
||||||
// Disables dependency metadata when building APKs.
|
// Disables dependency metadata when building APKs.
|
||||||
includeInApk = false
|
includeInApk = false
|
||||||
@@ -101,6 +105,10 @@ android {
|
|||||||
buildFeatures {
|
buildFeatures {
|
||||||
buildConfig true
|
buildConfig true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
packaging {
|
||||||
|
resources.excludes.add("META-INF/versions/9/OSGI-INF/MANIFEST.MF") // necessary for bcpkix-jdk18on in crypto
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def room_version = "2.5.1"
|
def room_version = "2.5.1"
|
||||||
@@ -141,6 +149,9 @@ dependencies {
|
|||||||
// Password generator
|
// Password generator
|
||||||
implementation 'me.gosimple:nbvcxz:1.5.0'
|
implementation 'me.gosimple:nbvcxz:1.5.0'
|
||||||
|
|
||||||
|
// Credentials Provider
|
||||||
|
implementation "androidx.credentials:credentials:1.2.2"
|
||||||
|
|
||||||
// Modules import
|
// Modules import
|
||||||
implementation project(path: ':database')
|
implementation project(path: ':database')
|
||||||
implementation project(path: ':icon-pack')
|
implementation project(path: ':icon-pack')
|
||||||
|
|||||||
@@ -45,7 +45,8 @@
|
|||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/KeepassDXStyle.Night"
|
android:theme="@style/KeepassDXStyle.Night"
|
||||||
tools:targetApi="s">
|
tools:targetApi="s"
|
||||||
|
tools:ignore="CredentialDependency">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.backup.api_key"
|
android:name="com.google.android.backup.api_key"
|
||||||
android:value="${googleAndroidBackupAPIKey}" />
|
android:value="${googleAndroidBackupAPIKey}" />
|
||||||
@@ -159,7 +160,7 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
android:name="com.kunzisoft.keepass.settings.SettingsActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.AutofillLauncherActivity"
|
android:name="com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity"
|
||||||
android:theme="@style/Theme.Transparent"
|
android:theme="@style/Theme.Transparent"
|
||||||
android:configChanges="keyboardHidden"
|
android:configChanges="keyboardHidden"
|
||||||
android:excludeFromRecents="true"/>
|
android:excludeFromRecents="true"/>
|
||||||
@@ -173,7 +174,7 @@
|
|||||||
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity"
|
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity"
|
||||||
android:theme="@style/Theme.Transparent" />
|
android:theme="@style/Theme.Transparent" />
|
||||||
<activity
|
<activity
|
||||||
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
|
android:name="com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity"
|
||||||
android:theme="@style/Theme.Transparent"
|
android:theme="@style/Theme.Transparent"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
@@ -199,7 +200,13 @@
|
|||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity"
|
||||||
|
android:theme="@style/Theme.Transparent"
|
||||||
|
android:configChanges="keyboardHidden"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="false"
|
||||||
|
tools:targetApi="upside_down_cake" />
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
|
||||||
android:foregroundServiceType="dataSync"
|
android:foregroundServiceType="dataSync"
|
||||||
@@ -227,7 +234,7 @@
|
|||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<!-- Receiver for Autofill -->
|
<!-- Receiver for Autofill -->
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.autofill.KeeAutofillService"
|
android:name="com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService"
|
||||||
android:label="@string/autofill_service_name"
|
android:label="@string/autofill_service_name"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
|
||||||
@@ -239,7 +246,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service
|
<service
|
||||||
android:name="com.kunzisoft.keepass.magikeyboard.MagikeyboardService"
|
android:name="com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService"
|
||||||
android:label="@string/keyboard_label"
|
android:label="@string/keyboard_label"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:permission="android.permission.BIND_INPUT_METHOD" >
|
android:permission="android.permission.BIND_INPUT_METHOD" >
|
||||||
@@ -249,6 +256,22 @@
|
|||||||
<action android:name="android.view.InputMethod" />
|
<action android:name="android.view.InputMethod" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
<service
|
||||||
|
android:name="com.kunzisoft.keepass.credentialprovider.passkey.PasskeyProviderService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/passkey_service_name"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"
|
||||||
|
tools:targetApi="upside_down_cake">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.credentials.CredentialProviderService" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.credentials.provider"
|
||||||
|
android:resource="@xml/provider" />
|
||||||
|
</service>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
|
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
|
|||||||
548
app/src/main/assets/trustedPackages.json
Normal file
548
app/src/main/assets/trustedPackages.json
Normal file
@@ -0,0 +1,548 @@
|
|||||||
|
{
|
||||||
|
"apps": [
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.android.chrome",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.chrome.beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "DA:63:3D:34:B6:9E:63:AE:21:03:B4:9D:53:CE:05:2F:C5:F7:F3:C5:3A:AB:94:FD:C2:A2:08:BD:FD:14:24:9C"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "3D:7A:12:23:01:9A:A3:9D:9E:A0:E3:43:6A:B7:C0:89:6B:FB:4F:B6:79:F4:DE:5F:E7:C2:3F:32:6C:8F:99:4A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.chrome.dev",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "90:44:EE:5F:EE:4B:BC:5E:21:DD:44:66:54:31:C4:EB:1F:1F:71:A3:27:16:A0:BC:92:7B:CB:B3:92:33:CA:BF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "3D:7A:12:23:01:9A:A3:9D:9E:A0:E3:43:6A:B7:C0:89:6B:FB:4F:B6:79:F4:DE:5F:E7:C2:3F:32:6C:8F:99:4A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.chrome.canary",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "20:19:DF:A1:FB:23:EF:BF:70:C5:BC:D1:44:3C:5B:EA:B0:4F:3F:2F:F4:36:6E:9A:C1:E3:45:76:39:A2:4C:FC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.chromium.chrome",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C6:AD:B8:B8:3C:6D:4C:17:D2:92:AF:DE:56:FD:48:8A:51:D3:16:FF:8F:2C:11:C5:41:02:23:BF:F8:A7:DB:B3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.google.android.apps.chrome",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.fennec_webauthndebug",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "BD:AE:82:02:80:D2:AF:B7:74:94:EF:22:58:AA:78:A9:AE:A1:36:41:7E:8B:C2:3D:C9:87:75:2E:6F:48:E8:48"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.firefox",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "A7:8B:62:A5:16:5B:44:94:B2:FE:AD:9E:76:A2:80:D2:2D:93:7F:EE:62:51:AE:CE:59:94:46:B2:EA:31:9B:04"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.firefox_beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "A7:8B:62:A5:16:5B:44:94:B2:FE:AD:9E:76:A2:80:D2:2D:93:7F:EE:62:51:AE:CE:59:94:46:B2:EA:31:9B:04"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.focus",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.fennec_aurora",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "BC:04:88:83:8D:06:F4:CA:6B:F3:23:86:DA:AB:0D:D8:EB:CF:3E:77:30:78:74:59:F6:2F:B3:CD:14:A1:BA:AA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.fennec_fdroid",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "06:66:53:58:EF:D8:BA:05:BE:23:6A:47:A1:2C:B0:95:8D:7D:75:DD:93:9D:77:C2:B3:1F:53:98:53:7E:BD:C5"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "org.mozilla.rocket",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "86:3A:46:F0:97:39:32:B7:D0:19:9B:54:91:12:74:1C:2D:27:31:AC:72:EA:11:B7:52:3A:A9:0A:11:BF:56:91"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.microsoft.emmx.canary",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.microsoft.emmx.dev",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.microsoft.emmx.beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.microsoft.emmx",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.microsoft.emmx.rolling",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "32:A2:FC:74:D7:31:10:58:59:E5:A8:5D:F1:6D:95:F1:02:D8:5B:22:09:9B:80:64:C5:D8:91:5C:61:DA:D1:E0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.microsoft.emmx.local",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "userdebug",
|
||||||
|
"cert_fingerprint_sha256": "32:A2:FC:74:D7:31:10:58:59:E5:A8:5D:F1:6D:95:F1:02:D8:5B:22:09:9B:80:64:C5:D8:91:5C:61:DA:D1:E0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.brave.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.brave.browser_beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.brave.browser_nightly",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "app.vanadium.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C6:AD:B8:B8:3C:6D:4C:17:D2:92:AF:DE:56:FD:48:8A:51:D3:16:FF:8F:2C:11:C5:41:02:23:BF:F8:A7:DB:B3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.vivaldi.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.vivaldi.browser.snapshot",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.vivaldi.browser.sopranos",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.citrix.Receiver",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "3D:D1:12:67:10:69:AB:36:4E:F9:BE:73:9A:B7:B5:EE:15:E1:CD:E9:D8:75:7B:1B:F0:64:F5:0C:55:68:9A:49"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "CE:B2:23:D7:77:09:F2:B6:BC:0B:3A:78:36:F5:A5:AF:4C:E1:D3:55:F4:A7:28:86:F7:9D:F8:0D:C9:D6:12:2E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "AA:D0:D4:57:E6:33:C3:78:25:77:30:5B:C1:B2:D9:E3:81:41:C7:21:DF:0D:AA:6E:29:07:2F:C4:1D:34:F0:AB"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.android.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C9:00:9D:01:EB:F9:F5:D0:30:2B:C7:1B:2F:E9:AA:9A:47:A4:32:BB:A1:73:08:A3:11:1B:75:D7:B2:14:90:25"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.sec.android.app.sbrowser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C8:A2:E9:BC:CF:59:7C:2F:B6:DC:66:BE:E2:93:FC:13:F2:FC:47:EC:77:BC:6B:2B:0D:52:C1:1F:51:19:2A:B8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "34:DF:0E:7A:9F:1C:F1:89:2E:45:C0:56:B4:97:3C:D8:1C:CF:14:8A:40:50:D1:1A:EA:4A:C5:A6:5F:90:0A:42"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.sec.android.app.sbrowser.beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C8:A2:E9:BC:CF:59:7C:2F:B6:DC:66:BE:E2:93:FC:13:F2:FC:47:EC:77:BC:6B:2B:0D:52:C1:1F:51:19:2A:B8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "34:DF:0E:7A:9F:1C:F1:89:2E:45:C0:56:B4:97:3C:D8:1C:CF:14:8A:40:50:D1:1A:EA:4A:C5:A6:5F:90:0A:42"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.google.android.gms",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "7C:E8:3C:1B:71:F3:D5:72:FE:D0:4C:8D:40:C5:CB:10:FF:75:E6:D8:7D:9D:F6:FB:D5:3F:04:68:C2:90:50:53"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "D2:2C:C5:00:29:9F:B2:28:73:A0:1A:01:0D:E1:C8:2F:BE:4D:06:11:19:B9:48:14:DD:30:1D:AB:50:CB:76:78"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.yandex.browser",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.yandex.browser.beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.yandex.browser.alpha",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.yandex.browser.corp",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.yandex.browser.canary",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "1D:A9:CB:AE:2D:CC:C6:A5:8D:6C:94:7B:E9:4C:DB:B7:33:D6:5D:A4:D1:77:0F:A1:4A:53:64:CB:4A:28:EB:49"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.yandex.browser.broteam",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "1D:A9:CB:AE:2D:CC:C6:A5:8D:6C:94:7B:E9:4C:DB:B7:33:D6:5D:A4:D1:77:0F:A1:4A:53:64:CB:4A:28:EB:49"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.talonsec.talon",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "A3:66:03:44:A6:F6:AF:CA:81:8C:BF:43:96:A2:3C:CF:D5:ED:7A:78:1B:B4:A3:D1:85:03:01:E2:F4:6D:23:83"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "E2:A5:64:74:EA:23:7B:06:67:B6:F5:2C:DC:E9:04:5E:24:88:3B:AE:D0:82:59:9A:A2:DF:0B:60:3A:CF:6A:3B"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.talonsec.talon_beta",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "F5:86:62:7A:32:C8:9F:E6:7E:00:6D:B1:8C:34:31:9E:01:7F:B3:B2:BE:D6:9D:01:01:B7:F9:43:E7:7C:48:AE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "9A:A1:25:D5:E5:5E:3F:B0:DE:96:72:D9:A9:5D:04:65:3F:49:4A:1E:C3:EE:76:1E:94:C4:4E:5D:2F:65:8E:2F"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.duckduckgo.mobile.android.debug",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "C4:F0:9E:2B:D7:25:AD:F5:AD:92:0B:A2:80:27:66:AC:16:4A:C1:53:B3:EA:9E:08:48:B0:57:98:37:F7:6A:29"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "android",
|
||||||
|
"info": {
|
||||||
|
"package_name": "com.duckduckgo.mobile.android",
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"build": "release",
|
||||||
|
"cert_fingerprint_sha256": "BB:7B:B3:1C:57:3C:46:A1:DA:7F:C5:C5:28:A6:AC:F4:32:10:84:56:FE:EC:50:81:0C:7F:33:69:4E:B3:D2:D4"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -23,7 +23,6 @@ import android.content.Intent
|
|||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
@@ -38,13 +37,10 @@ import androidx.activity.result.ActivityResultLauncher
|
|||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.res.ResourcesCompat
|
|
||||||
import androidx.core.graphics.BlendModeColorFilterCompat
|
import androidx.core.graphics.BlendModeColorFilterCompat
|
||||||
import androidx.core.graphics.BlendModeCompat
|
import androidx.core.graphics.BlendModeCompat
|
||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowCompat
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
@@ -54,15 +50,15 @@ import com.google.android.material.tabs.TabLayout
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.fragments.EntryFragment
|
import com.kunzisoft.keepass.activities.fragments.EntryFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
import com.kunzisoft.keepass.adapters.TagsAdapter
|
import com.kunzisoft.keepass.adapters.TagsAdapter
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.education.EntryActivityEducation
|
import com.kunzisoft.keepass.education.EntryActivityEducation
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
import com.kunzisoft.keepass.otp.OtpType
|
import com.kunzisoft.keepass.otp.OtpType
|
||||||
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
import com.kunzisoft.keepass.services.AttachmentFileNotificationService
|
||||||
@@ -73,7 +69,7 @@ import com.kunzisoft.keepass.settings.PreferencesUtil
|
|||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
import com.kunzisoft.keepass.tasks.AttachmentFileBinderManager
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
import com.kunzisoft.keepass.utils.UuidUtil
|
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
import com.kunzisoft.keepass.view.WindowInsetPosition
|
import com.kunzisoft.keepass.view.WindowInsetPosition
|
||||||
import com.kunzisoft.keepass.view.applyWindowInsets
|
import com.kunzisoft.keepass.view.applyWindowInsets
|
||||||
@@ -261,7 +257,7 @@ class EntryActivity : DatabaseLockActivity() {
|
|||||||
mIcon = entryInfo.icon
|
mIcon = entryInfo.icon
|
||||||
// Assign title text
|
// Assign title text
|
||||||
val entryTitle =
|
val entryTitle =
|
||||||
if (entryInfo.title.isNotEmpty()) entryInfo.title else UuidUtil.toHexString(entryInfo.id)
|
entryInfo.title.ifEmpty { entryInfo.id.asHexString() }
|
||||||
collapsingToolbarLayout?.title = entryTitle
|
collapsingToolbarLayout?.title = entryTitle
|
||||||
toolbar?.title = entryTitle
|
toolbar?.title = entryTitle
|
||||||
// Assign tags
|
// Assign tags
|
||||||
|
|||||||
@@ -55,12 +55,15 @@ import com.kunzisoft.keepass.activities.dialogs.FileTooBigDialogFragment.Compani
|
|||||||
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.ReplaceFileDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SetOTPDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
|
import com.kunzisoft.keepass.activities.fragments.EntryEditFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
|
import com.kunzisoft.keepass.adapters.TemplatesSelectorAdapter
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.Attachment
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
@@ -70,7 +73,6 @@ import com.kunzisoft.keepass.database.element.node.Node
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.template.Template
|
import com.kunzisoft.keepass.database.element.template.Template
|
||||||
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
import com.kunzisoft.keepass.education.EntryEditActivityEducation
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
|
||||||
import com.kunzisoft.keepass.model.AttachmentState
|
import com.kunzisoft.keepass.model.AttachmentState
|
||||||
import com.kunzisoft.keepass.model.DataTime
|
import com.kunzisoft.keepass.model.DataTime
|
||||||
import com.kunzisoft.keepass.model.EntryAttachmentState
|
import com.kunzisoft.keepass.model.EntryAttachmentState
|
||||||
@@ -376,18 +378,25 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
// Don't wait for saving if it's to provide autofill
|
// Don't wait for saving if it's to provide autofill
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
EntrySelectionHelper.doSpecialAction(intent,
|
EntrySelectionHelper.doSpecialAction(
|
||||||
{},
|
intent = intent,
|
||||||
{},
|
defaultAction = {},
|
||||||
{},
|
searchAction = {},
|
||||||
{
|
saveAction = {},
|
||||||
|
keyboardSelectionAction = {
|
||||||
entryValidatedForKeyboardSelection(database, entrySave.newEntry)
|
entryValidatedForKeyboardSelection(database, entrySave.newEntry)
|
||||||
},
|
},
|
||||||
{ _, _ ->
|
autofillSelectionAction = { _, _ ->
|
||||||
entryValidatedForAutofillSelection(database, entrySave.newEntry)
|
entryValidatedForAutofillSelection(database, entrySave.newEntry)
|
||||||
},
|
},
|
||||||
{
|
autofillRegistrationAction = {
|
||||||
entryValidatedForAutofillRegistration(entrySave.newEntry)
|
entryValidatedForAutofillRegistration(entrySave.newEntry)
|
||||||
|
},
|
||||||
|
passkeySelectionAction = {
|
||||||
|
entryValidatedForPasskeySelection(database, entrySave.newEntry)
|
||||||
|
},
|
||||||
|
passkeyRegistrationAction = {
|
||||||
|
entryValidatedForPasskeyRegistration(database, entrySave.newEntry)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -430,25 +439,32 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
if (newNodes.size == 1) {
|
if (newNodes.size == 1) {
|
||||||
(newNodes[0] as? Entry?)?.let { entry ->
|
(newNodes[0] as? Entry?)?.let { entry ->
|
||||||
EntrySelectionHelper.doSpecialAction(intent,
|
EntrySelectionHelper.doSpecialAction(
|
||||||
{
|
intent = intent,
|
||||||
|
defaultAction = {
|
||||||
// Finish naturally
|
// Finish naturally
|
||||||
finishForEntryResult(entry)
|
finishForEntryResult(entry)
|
||||||
},
|
},
|
||||||
{
|
searchAction = {
|
||||||
// Nothing when search retrieved
|
// Nothing when search retrieved
|
||||||
},
|
},
|
||||||
{
|
saveAction = {
|
||||||
entryValidatedForSave(entry)
|
entryValidatedForSave(entry)
|
||||||
},
|
},
|
||||||
{
|
keyboardSelectionAction = {
|
||||||
entryValidatedForKeyboardSelection(database, entry)
|
entryValidatedForKeyboardSelection(database, entry)
|
||||||
},
|
},
|
||||||
{ _, _ ->
|
autofillSelectionAction = { _, _ ->
|
||||||
entryValidatedForAutofillSelection(database, entry)
|
entryValidatedForAutofillSelection(database, entry)
|
||||||
},
|
},
|
||||||
{
|
autofillRegistrationAction = {
|
||||||
entryValidatedForAutofillRegistration(entry)
|
entryValidatedForAutofillRegistration(entry)
|
||||||
|
},
|
||||||
|
passkeySelectionAction = {
|
||||||
|
entryValidatedForPasskeySelection(database, entry)
|
||||||
|
},
|
||||||
|
passkeyRegistrationAction = {
|
||||||
|
entryValidatedForPasskeyRegistration(database, entry)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -488,10 +504,34 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
onValidateSpecialMode()
|
onValidateSpecialMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun entryValidatedForAutofillRegistration(entry: Entry) {
|
private fun entryValidatedForPasskeySelection(database: ContextualDatabase, entry: Entry) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
this.buildPasskeyResponseAndSetResult(
|
||||||
|
entryInfo = entry.getEntryInfo(database)
|
||||||
|
)
|
||||||
|
}
|
||||||
onValidateSpecialMode()
|
onValidateSpecialMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun entryValidatedForAutofillRegistration(entry: Entry) {
|
||||||
|
//if (isIntentSender()) {
|
||||||
|
// TODO Autofill Callback #765
|
||||||
|
//}
|
||||||
|
onValidateSpecialMode()
|
||||||
|
if (!isIntentSender()) {
|
||||||
finishForEntryResult(entry)
|
finishForEntryResult(entry)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun entryValidatedForPasskeyRegistration(database: ContextualDatabase, entry: Entry) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
this.buildPasskeyResponseAndSetResult(
|
||||||
|
entryInfo = entry.getEntryInfo(database),
|
||||||
|
extras = buildEntryResult(entry) // To update the previous screen
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onValidateSpecialMode()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
@@ -738,12 +778,17 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildEntryResult(entry: Entry): Bundle {
|
||||||
|
return Bundle().apply {
|
||||||
|
putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun finishForEntryResult(entry: Entry) {
|
private fun finishForEntryResult(entry: Entry) {
|
||||||
// Assign entry callback as a result
|
// Assign entry callback as a result
|
||||||
try {
|
try {
|
||||||
val bundle = Bundle()
|
val bundle = buildEntryResult(entry)
|
||||||
val intentEntry = Intent()
|
val intentEntry = Intent()
|
||||||
bundle.putParcelable(ADD_OR_UPDATE_ENTRY_KEY, entry.nodeId)
|
|
||||||
intentEntry.putExtras(bundle)
|
intentEntry.putExtras(bundle)
|
||||||
setResult(Activity.RESULT_OK, intentEntry)
|
setResult(Activity.RESULT_OK, intentEntry)
|
||||||
super.finish()
|
super.finish()
|
||||||
@@ -888,7 +933,7 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(activity)) {
|
||||||
val intent = Intent(activity, EntryEditActivity::class.java)
|
val intent = Intent(activity, EntryEditActivity::class.java)
|
||||||
intent.putExtra(KEY_PARENT, groupId)
|
intent.putExtra(KEY_PARENT, groupId)
|
||||||
AutofillHelper.startActivityForAutofillResult(
|
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
activityResultLauncher,
|
activityResultLauncher,
|
||||||
@@ -899,21 +944,48 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch EntryEditActivity to add a new passkey entry
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
fun launchForPasskeySelectionResult(context: Context,
|
||||||
|
database: ContextualDatabase,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
groupId: NodeId<*>,
|
||||||
|
searchInfo: SearchInfo? = null) {
|
||||||
|
if (database.loaded && !database.isReadOnly) {
|
||||||
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||||
|
val intent = Intent(context, EntryEditActivity::class.java)
|
||||||
|
intent.putExtra(KEY_PARENT, groupId)
|
||||||
|
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
|
||||||
|
context,
|
||||||
|
intent,
|
||||||
|
activityResultLauncher,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launch EntryEditActivity to register an updated entry (from autofill)
|
* Launch EntryEditActivity to register an updated entry (from autofill)
|
||||||
*/
|
*/
|
||||||
fun launchToUpdateForRegistration(context: Context,
|
fun launchToUpdateForRegistration(context: Context,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
entryId: NodeId<UUID>,
|
entryId: NodeId<UUID>,
|
||||||
registerInfo: RegisterInfo? = null) {
|
registerInfo: RegisterInfo?,
|
||||||
|
typeMode: TypeMode) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||||
val intent = Intent(context, EntryEditActivity::class.java)
|
val intent = Intent(context, EntryEditActivity::class.java)
|
||||||
intent.putExtra(KEY_ENTRY, entryId)
|
intent.putExtra(KEY_ENTRY, entryId)
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
context,
|
context,
|
||||||
|
activityResultLauncher,
|
||||||
intent,
|
intent,
|
||||||
registerInfo
|
registerInfo,
|
||||||
|
typeMode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -924,16 +996,20 @@ class EntryEditActivity : DatabaseLockActivity(),
|
|||||||
*/
|
*/
|
||||||
fun launchToCreateForRegistration(context: Context,
|
fun launchToCreateForRegistration(context: Context,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
groupId: NodeId<*>,
|
groupId: NodeId<*>,
|
||||||
registerInfo: RegisterInfo? = null) {
|
registerInfo: RegisterInfo? = null,
|
||||||
|
typeMode: TypeMode) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
if (TimeoutHelper.checkTimeAndLockIfTimeout(context)) {
|
||||||
val intent = Intent(context, EntryEditActivity::class.java)
|
val intent = Intent(context, EntryEditActivity::class.java)
|
||||||
intent.putExtra(KEY_PARENT, groupId)
|
intent.putExtra(KEY_PARENT, groupId)
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
context,
|
context,
|
||||||
|
activityResultLauncher,
|
||||||
intent,
|
intent,
|
||||||
registerInfo
|
registerInfo,
|
||||||
|
typeMode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,15 +44,16 @@ import androidx.recyclerview.widget.SimpleItemAnimator
|
|||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SetMainCredentialDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
import com.kunzisoft.keepass.adapters.FileDatabaseHistoryAdapter
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
import com.kunzisoft.keepass.education.FileDatabaseSelectActivityEducation
|
||||||
@@ -98,10 +99,8 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
|
|
||||||
private var mExternalFileHelper: ExternalFileHelper? = null
|
private var mExternalFileHelper: ExternalFileHelper? = null
|
||||||
|
|
||||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
this.buildActivityResultLauncher()
|
||||||
AutofillHelper.buildActivityResultLauncher(this)
|
|
||||||
else null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -298,7 +297,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
},
|
},
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() },
|
{ onLaunchActivitySpecialMode() },
|
||||||
mAutofillActivityResultLauncher)
|
mCredentialActivityResultLauncher)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
private fun launchGroupActivityIfLoaded(database: ContextualDatabase) {
|
||||||
@@ -308,7 +307,7 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
{ onValidateSpecialMode() },
|
{ onValidateSpecialMode() },
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() },
|
{ onLaunchActivitySpecialMode() },
|
||||||
mAutofillActivityResultLauncher)
|
mCredentialActivityResultLauncher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,23 +486,46 @@ class FileDatabaseSelectActivity : DatabaseModeActivity(),
|
|||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
autofillComponent: AutofillComponent,
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo? = null) {
|
searchInfo: SearchInfo? = null) {
|
||||||
AutofillHelper.startActivityForAutofillResult(activity,
|
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(activity,
|
||||||
Intent(activity, FileDatabaseSelectActivity::class.java),
|
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||||
activityResultLauncher,
|
activityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo)
|
searchInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Passkey Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
fun launchForPasskeySelectionResult(activity: Activity,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
searchInfo: SearchInfo? = null) {
|
||||||
|
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
|
||||||
|
activity,
|
||||||
|
Intent(activity, FileDatabaseSelectActivity::class.java),
|
||||||
|
activityResultLauncher,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Registration Launch
|
* Registration Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launchForRegistration(context: Context,
|
fun launchForRegistration(context: Context,
|
||||||
registerInfo: RegisterInfo? = null) {
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(context,
|
registerInfo: RegisterInfo? = null,
|
||||||
|
typeMode: TypeMode) {
|
||||||
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
|
context,
|
||||||
|
activityResultLauncher,
|
||||||
Intent(context, FileDatabaseSelectActivity::class.java),
|
Intent(context, FileDatabaseSelectActivity::class.java),
|
||||||
registerInfo)
|
registerInfo,
|
||||||
|
typeMode
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,13 +63,17 @@ import com.kunzisoft.keepass.activities.dialogs.GroupEditDialogFragment
|
|||||||
import com.kunzisoft.keepass.activities.dialogs.MainCredentialDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.MainCredentialDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.fragments.GroupFragment
|
import com.kunzisoft.keepass.activities.fragments.GroupFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseLockActivity
|
||||||
import com.kunzisoft.keepass.adapters.BreadcrumbAdapter
|
import com.kunzisoft.keepass.adapters.BreadcrumbAdapter
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyResponseAndSetResult
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.database.element.DateInstant
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
@@ -83,7 +87,6 @@ import com.kunzisoft.keepass.database.element.node.Type
|
|||||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||||
import com.kunzisoft.keepass.education.GroupActivityEducation
|
import com.kunzisoft.keepass.education.GroupActivityEducation
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
|
||||||
import com.kunzisoft.keepass.model.DataTime
|
import com.kunzisoft.keepass.model.DataTime
|
||||||
import com.kunzisoft.keepass.model.GroupInfo
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
@@ -264,10 +267,8 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
mGroupEditViewModel.selectIcon(icon)
|
mGroupEditViewModel.selectIcon(icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
this.buildActivityResultLauncher()
|
||||||
AutofillHelper.buildActivityResultLauncher(this)
|
|
||||||
else null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -484,59 +485,87 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
addNodeButtonView?.setAddEntryClickListener {
|
addNodeButtonView?.setAddEntryClickListener {
|
||||||
mDatabase?.let { database ->
|
mDatabase?.let { database ->
|
||||||
mMainGroup?.let { currentGroup ->
|
mMainGroup?.let { currentGroup ->
|
||||||
EntrySelectionHelper.doSpecialAction(intent,
|
EntrySelectionHelper.doSpecialAction(
|
||||||
{
|
intent = intent,
|
||||||
|
defaultAction = {
|
||||||
mMainGroup?.nodeId?.let { currentParentGroupId ->
|
mMainGroup?.nodeId?.let { currentParentGroupId ->
|
||||||
EntryEditActivity.launchToCreate(
|
EntryEditActivity.launchToCreate(
|
||||||
this@GroupActivity,
|
activity = this@GroupActivity,
|
||||||
database,
|
database = database,
|
||||||
currentParentGroupId,
|
groupId = currentParentGroupId,
|
||||||
mEntryActivityResultLauncher
|
activityResultLauncher = mEntryActivityResultLauncher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
searchAction = {
|
||||||
// Search not used
|
// Search not used
|
||||||
},
|
},
|
||||||
{ searchInfo ->
|
saveAction = { searchInfo ->
|
||||||
EntryEditActivity.launchToCreateForSave(
|
EntryEditActivity.launchToCreateForSave(
|
||||||
this@GroupActivity,
|
context = this@GroupActivity,
|
||||||
database,
|
database = database,
|
||||||
currentGroup.nodeId,
|
groupId = currentGroup.nodeId,
|
||||||
searchInfo
|
searchInfo = searchInfo
|
||||||
)
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
},
|
||||||
{ searchInfo ->
|
keyboardSelectionAction = { searchInfo ->
|
||||||
EntryEditActivity.launchForKeyboardSelectionResult(
|
EntryEditActivity.launchForKeyboardSelectionResult(
|
||||||
this@GroupActivity,
|
context = this@GroupActivity,
|
||||||
database,
|
database = database,
|
||||||
currentGroup.nodeId,
|
groupId = currentGroup.nodeId,
|
||||||
searchInfo
|
searchInfo = searchInfo
|
||||||
)
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
},
|
||||||
{ searchInfo, autofillComponent ->
|
autofillSelectionAction = { searchInfo, autofillComponent ->
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
EntryEditActivity.launchForAutofillResult(
|
EntryEditActivity.launchForAutofillResult(
|
||||||
this@GroupActivity,
|
activity = this@GroupActivity,
|
||||||
database,
|
database = database,
|
||||||
mAutofillActivityResultLauncher,
|
activityResultLauncher = mCredentialActivityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent = autofillComponent,
|
||||||
currentGroup.nodeId,
|
groupId = currentGroup.nodeId,
|
||||||
searchInfo
|
searchInfo = searchInfo
|
||||||
)
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
} else {
|
} else {
|
||||||
onCancelSpecialMode()
|
onCancelSpecialMode()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ searchInfo ->
|
autofillRegistrationAction = { registerInfo ->
|
||||||
EntryEditActivity.launchToCreateForRegistration(
|
EntryEditActivity.launchToCreateForRegistration(
|
||||||
this@GroupActivity,
|
context = this@GroupActivity,
|
||||||
database,
|
database = database,
|
||||||
currentGroup.nodeId,
|
activityResultLauncher = null,
|
||||||
searchInfo
|
groupId = currentGroup.nodeId,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
passkeySelectionAction = { searchInfo ->
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
EntryEditActivity.launchForPasskeySelectionResult(
|
||||||
|
context = this@GroupActivity,
|
||||||
|
database = database,
|
||||||
|
activityResultLauncher = mCredentialActivityResultLauncher,
|
||||||
|
groupId = currentGroup.nodeId,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
} else {
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
passkeyRegistrationAction = { registerInfo ->
|
||||||
|
EntryEditActivity.launchToCreateForRegistration(
|
||||||
|
context = this@GroupActivity,
|
||||||
|
database = database,
|
||||||
|
activityResultLauncher = mCredentialActivityResultLauncher,
|
||||||
|
groupId = currentGroup.nodeId,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.PASSKEY
|
||||||
)
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
}
|
}
|
||||||
@@ -679,30 +708,40 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
when (actionTask) {
|
when (actionTask) {
|
||||||
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
ACTION_DATABASE_UPDATE_ENTRY_TASK -> {
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
EntrySelectionHelper.doSpecialAction(intent,
|
EntrySelectionHelper.doSpecialAction(
|
||||||
{
|
intent = intent,
|
||||||
|
defaultAction = {
|
||||||
// Standard not used after task
|
// Standard not used after task
|
||||||
},
|
},
|
||||||
{
|
searchAction = {
|
||||||
// Search not used
|
// Search not used
|
||||||
},
|
},
|
||||||
{
|
saveAction = {
|
||||||
// Save not used
|
// Save not used
|
||||||
},
|
},
|
||||||
{
|
keyboardSelectionAction = {
|
||||||
// Keyboard selection
|
// Keyboard selection
|
||||||
entry?.let {
|
entry?.let {
|
||||||
entrySelectedForKeyboardSelection(database, it)
|
entrySelectedForKeyboardSelection(database, it)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ _, _ ->
|
autofillSelectionAction = { _, _ ->
|
||||||
// Autofill selection
|
// Autofill selection
|
||||||
entry?.let {
|
entry?.let {
|
||||||
entrySelectedForAutofillSelection(database, it)
|
entrySelectedForAutofillSelection(database, it)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
autofillRegistrationAction = {
|
||||||
// Not use
|
// Not use
|
||||||
|
},
|
||||||
|
passkeySelectionAction = {
|
||||||
|
// Passkey selection
|
||||||
|
entry?.let {
|
||||||
|
entrySelectedForPasskeySelection(database, it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
passkeyRegistrationAction = {
|
||||||
|
// TODO Passkey Registration
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -846,27 +885,28 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
|
|
||||||
Type.ENTRY -> try {
|
Type.ENTRY -> try {
|
||||||
val entryVersioned = node as Entry
|
val entryVersioned = node as Entry
|
||||||
EntrySelectionHelper.doSpecialAction(intent,
|
EntrySelectionHelper.doSpecialAction(
|
||||||
{
|
intent = intent,
|
||||||
|
defaultAction = {
|
||||||
EntryActivity.launch(
|
EntryActivity.launch(
|
||||||
this@GroupActivity,
|
activity = this@GroupActivity,
|
||||||
database,
|
database = database,
|
||||||
entryVersioned.nodeId,
|
entryId = entryVersioned.nodeId,
|
||||||
mEntryActivityResultLauncher
|
activityResultLauncher = mEntryActivityResultLauncher
|
||||||
)
|
)
|
||||||
// Do not reload group here
|
// Do not reload group here
|
||||||
},
|
},
|
||||||
{
|
searchAction = {
|
||||||
// Nothing here, a search is simply performed
|
// Nothing here, a search is simply performed
|
||||||
},
|
},
|
||||||
{ searchInfo ->
|
saveAction = { searchInfo ->
|
||||||
if (!database.isReadOnly) {
|
if (!database.isReadOnly) {
|
||||||
entrySelectedForSave(database, entryVersioned, searchInfo)
|
entrySelectedForSave(database, entryVersioned, searchInfo)
|
||||||
loadGroup()
|
loadGroup()
|
||||||
} else
|
} else
|
||||||
finish()
|
finish()
|
||||||
},
|
},
|
||||||
{ searchInfo ->
|
keyboardSelectionAction = { searchInfo ->
|
||||||
if (!database.isReadOnly
|
if (!database.isReadOnly
|
||||||
&& searchInfo != null
|
&& searchInfo != null
|
||||||
&& PreferencesUtil.isKeyboardSaveSearchInfoEnable(this@GroupActivity)
|
&& PreferencesUtil.isKeyboardSaveSearchInfoEnable(this@GroupActivity)
|
||||||
@@ -876,7 +916,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
entrySelectedForKeyboardSelection(database, entryVersioned)
|
entrySelectedForKeyboardSelection(database, entryVersioned)
|
||||||
loadGroup()
|
loadGroup()
|
||||||
},
|
},
|
||||||
{ searchInfo, _ ->
|
autofillSelectionAction = { searchInfo, _ ->
|
||||||
if (!database.isReadOnly
|
if (!database.isReadOnly
|
||||||
&& searchInfo != null
|
&& searchInfo != null
|
||||||
&& PreferencesUtil.isAutofillSaveSearchInfoEnable(this@GroupActivity)
|
&& PreferencesUtil.isAutofillSaveSearchInfoEnable(this@GroupActivity)
|
||||||
@@ -886,9 +926,39 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
entrySelectedForAutofillSelection(database, entryVersioned)
|
entrySelectedForAutofillSelection(database, entryVersioned)
|
||||||
loadGroup()
|
loadGroup()
|
||||||
},
|
},
|
||||||
{ registerInfo ->
|
autofillRegistrationAction = { registerInfo ->
|
||||||
if (!database.isReadOnly) {
|
if (!database.isReadOnly) {
|
||||||
entrySelectedForRegistration(database, entryVersioned, registerInfo)
|
entrySelectedForRegistration(
|
||||||
|
database = database,
|
||||||
|
entry = entryVersioned,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL,
|
||||||
|
activityResultLauncher = null // TODO Result launcher autofill #765
|
||||||
|
)
|
||||||
|
loadGroup()
|
||||||
|
} else
|
||||||
|
finish()
|
||||||
|
},
|
||||||
|
passkeySelectionAction = { searchInfo ->
|
||||||
|
if (!database.isReadOnly
|
||||||
|
&& searchInfo != null
|
||||||
|
// TODO Passkey setting && PreferencesUtil.isAutofillSaveSearchInfoEnable(this@GroupActivity)
|
||||||
|
) {
|
||||||
|
updateEntryWithSearchInfo(database, entryVersioned, searchInfo)
|
||||||
|
}
|
||||||
|
entrySelectedForPasskeySelection(database, entryVersioned)
|
||||||
|
loadGroup()
|
||||||
|
},
|
||||||
|
passkeyRegistrationAction = { registerInfo ->
|
||||||
|
if (!database.isReadOnly) {
|
||||||
|
// TODO Passkey setting && PreferencesUtil.isAutofillOverwriteEnable(this@GroupActivity)
|
||||||
|
entrySelectedForRegistration(
|
||||||
|
database = database,
|
||||||
|
entry = entryVersioned,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.PASSKEY,
|
||||||
|
activityResultLauncher = mCredentialActivityResultLauncher
|
||||||
|
)
|
||||||
loadGroup()
|
loadGroup()
|
||||||
} else
|
} else
|
||||||
finish()
|
finish()
|
||||||
@@ -934,18 +1004,33 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
onValidateSpecialMode()
|
onValidateSpecialMode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun entrySelectedForPasskeySelection(database: ContextualDatabase, entry: Entry) {
|
||||||
|
removeSearch()
|
||||||
|
// Build response with the entry selected
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
buildPasskeyResponseAndSetResult(
|
||||||
|
entryInfo = entry.getEntryInfo(database)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onValidateSpecialMode()
|
||||||
|
}
|
||||||
|
|
||||||
private fun entrySelectedForRegistration(
|
private fun entrySelectedForRegistration(
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
entry: Entry,
|
entry: Entry,
|
||||||
registerInfo: RegisterInfo?
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
registerInfo: RegisterInfo?,
|
||||||
|
typeMode: TypeMode
|
||||||
) {
|
) {
|
||||||
removeSearch()
|
removeSearch()
|
||||||
// Registration to update the entry
|
// Registration to update the entry
|
||||||
EntryEditActivity.launchToUpdateForRegistration(
|
EntryEditActivity.launchToUpdateForRegistration(
|
||||||
this@GroupActivity,
|
context = this@GroupActivity,
|
||||||
database,
|
database = database,
|
||||||
entry.nodeId,
|
activityResultLauncher = activityResultLauncher,
|
||||||
registerInfo
|
entryId = entry.nodeId,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = typeMode
|
||||||
)
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
}
|
}
|
||||||
@@ -961,12 +1046,11 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
raw = true,
|
raw = true,
|
||||||
removeTemplateConfiguration = false
|
removeTemplateConfiguration = false
|
||||||
)
|
)
|
||||||
val modification = entryInfo.saveSearchInfo(database, searchInfo)
|
// TODO Transform SearchInfo in RegisterInfo
|
||||||
|
entryInfo.saveSearchInfo(database, searchInfo)
|
||||||
newEntry.setEntryInfo(database, entryInfo)
|
newEntry.setEntryInfo(database, entryInfo)
|
||||||
if (modification) {
|
|
||||||
updateEntry(entry, newEntry)
|
updateEntry(entry, newEntry)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun finishNodeAction() {
|
private fun finishNodeAction() {
|
||||||
actionNodeMode?.finish()
|
actionNodeMode?.finish()
|
||||||
@@ -1569,19 +1653,19 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
fun launchForAutofillSelectionResult(activity: AppCompatActivity,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
activityResultLaunch: ActivityResultLauncher<Intent>?,
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
autofillComponent: AutofillComponent,
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo? = null,
|
searchInfo: SearchInfo? = null,
|
||||||
autoSearch: Boolean = false) {
|
autoSearch: Boolean = false) {
|
||||||
if (database.loaded) {
|
if (database.loaded) {
|
||||||
checkTimeAndBuildIntent(activity, null) { intent ->
|
checkTimeAndBuildIntent(activity, null) { intent ->
|
||||||
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
||||||
AutofillHelper.startActivityForAutofillResult(
|
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
activityResultLaunch,
|
activityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo
|
searchInfo
|
||||||
)
|
)
|
||||||
@@ -1589,21 +1673,49 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Passkey Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
fun launchForPasskeySelectionResult(context: Context,
|
||||||
|
database: ContextualDatabase,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
searchInfo: SearchInfo? = null,
|
||||||
|
autoSearch: Boolean = false) {
|
||||||
|
if (database.loaded) {
|
||||||
|
checkTimeAndBuildIntent(context, null) { intent ->
|
||||||
|
intent.putExtra(AUTO_SEARCH_KEY, autoSearch)
|
||||||
|
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
|
||||||
|
context,
|
||||||
|
intent,
|
||||||
|
activityResultLauncher,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Registration Launch
|
* Registration Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launchForRegistration(context: Context,
|
fun launchForRegistration(context: Context,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
database: ContextualDatabase,
|
database: ContextualDatabase,
|
||||||
registerInfo: RegisterInfo? = null) {
|
registerInfo: RegisterInfo? = null,
|
||||||
|
typeMode: TypeMode) {
|
||||||
if (database.loaded && !database.isReadOnly) {
|
if (database.loaded && !database.isReadOnly) {
|
||||||
checkTimeAndBuildIntent(context, null) { intent ->
|
checkTimeAndBuildIntent(context, null) { intent ->
|
||||||
intent.putExtra(AUTO_SEARCH_KEY, false)
|
intent.putExtra(AUTO_SEARCH_KEY, false)
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
context,
|
context,
|
||||||
|
activityResultLauncher,
|
||||||
intent,
|
intent,
|
||||||
registerInfo
|
registerInfo,
|
||||||
|
typeMode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1619,9 +1731,10 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
onValidateSpecialMode: () -> Unit,
|
onValidateSpecialMode: () -> Unit,
|
||||||
onCancelSpecialMode: () -> Unit,
|
onCancelSpecialMode: () -> Unit,
|
||||||
onLaunchActivitySpecialMode: () -> Unit,
|
onLaunchActivitySpecialMode: () -> Unit,
|
||||||
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
|
activityResultLauncher: ActivityResultLauncher<Intent>?) {
|
||||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
EntrySelectionHelper.doSpecialAction(
|
||||||
{
|
intent = activity.intent,
|
||||||
|
defaultAction = {
|
||||||
// Default action
|
// Default action
|
||||||
launch(
|
launch(
|
||||||
activity,
|
activity,
|
||||||
@@ -1629,7 +1742,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
true
|
true
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{ searchInfo ->
|
searchAction = { searchInfo ->
|
||||||
// Search action
|
// Search action
|
||||||
if (database.loaded) {
|
if (database.loaded) {
|
||||||
launchForSearchResult(activity,
|
launchForSearchResult(activity,
|
||||||
@@ -1642,7 +1755,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
onCancelSpecialMode()
|
onCancelSpecialMode()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ searchInfo ->
|
saveAction = { searchInfo ->
|
||||||
// Save info
|
// Save info
|
||||||
if (database.loaded) {
|
if (database.loaded) {
|
||||||
if (!database.isReadOnly) {
|
if (!database.isReadOnly) {
|
||||||
@@ -1664,12 +1777,13 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ searchInfo ->
|
keyboardSelectionAction = { searchInfo ->
|
||||||
// Keyboard selection
|
// Keyboard selection
|
||||||
SearchHelper.checkAutoSearchInfo(activity,
|
SearchHelper.checkAutoSearchInfo(
|
||||||
database,
|
context = activity,
|
||||||
searchInfo,
|
database = database,
|
||||||
{ _, items ->
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { _, items ->
|
||||||
MagikeyboardService.performSelection(
|
MagikeyboardService.performSelection(
|
||||||
items,
|
items,
|
||||||
{ entryInfo ->
|
{ entryInfo ->
|
||||||
@@ -1689,7 +1803,7 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
onItemNotFound = {
|
||||||
// Here no search info found, disable auto search
|
// Here no search info found, disable auto search
|
||||||
launchForKeyboardSelectionResult(activity,
|
launchForKeyboardSelectionResult(activity,
|
||||||
database,
|
database,
|
||||||
@@ -1697,34 +1811,36 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
false)
|
false)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
},
|
||||||
{
|
onDatabaseClosed = {
|
||||||
// Simply close if database not opened, normally not happened
|
// Simply close if database not opened, normally not happened
|
||||||
onCancelSpecialMode()
|
onCancelSpecialMode()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{ searchInfo, autofillComponent ->
|
autofillSelectionAction = { searchInfo, autofillComponent ->
|
||||||
// Autofill selection
|
// Autofill selection
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
SearchHelper.checkAutoSearchInfo(activity,
|
SearchHelper.checkAutoSearchInfo(
|
||||||
database,
|
context = activity,
|
||||||
searchInfo,
|
database = database,
|
||||||
{ openedDatabase, items ->
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { openedDatabase, items ->
|
||||||
// Response is build
|
// Response is build
|
||||||
AutofillHelper.buildResponseAndSetResult(activity, openedDatabase, items)
|
AutofillHelper.buildResponseAndSetResult(activity, openedDatabase, items)
|
||||||
onValidateSpecialMode()
|
onValidateSpecialMode()
|
||||||
},
|
},
|
||||||
{
|
onItemNotFound = {
|
||||||
// Here no search info found, disable auto search
|
// Here no search info found, disable auto search
|
||||||
launchForAutofillResult(activity,
|
launchForAutofillSelectionResult(
|
||||||
database,
|
activity = activity,
|
||||||
autofillActivityResultLauncher,
|
database = database,
|
||||||
autofillComponent,
|
autofillComponent = autofillComponent,
|
||||||
searchInfo,
|
searchInfo = searchInfo,
|
||||||
false)
|
autoSearch = false,
|
||||||
|
activityResultLauncher = activityResultLauncher)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
},
|
||||||
{
|
onDatabaseClosed = {
|
||||||
// Simply close if database not opened, normally not happened
|
// Simply close if database not opened, normally not happened
|
||||||
onCancelSpecialMode()
|
onCancelSpecialMode()
|
||||||
}
|
}
|
||||||
@@ -1733,27 +1849,36 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
onCancelSpecialMode()
|
onCancelSpecialMode()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ registerInfo ->
|
autofillRegistrationAction = { registerInfo ->
|
||||||
// Autofill registration
|
// Autofill registration
|
||||||
if (!database.isReadOnly) {
|
if (!database.isReadOnly) {
|
||||||
SearchHelper.checkAutoSearchInfo(activity,
|
SearchHelper.checkAutoSearchInfo(
|
||||||
database,
|
context = activity,
|
||||||
registerInfo?.searchInfo,
|
database = database,
|
||||||
{ _, _ ->
|
searchInfo = registerInfo?.searchInfo,
|
||||||
|
onItemsFound = { _, _ ->
|
||||||
// No auto search, it's a registration
|
// No auto search, it's a registration
|
||||||
launchForRegistration(activity,
|
launchForRegistration(
|
||||||
database,
|
context = activity,
|
||||||
registerInfo)
|
activityResultLauncher = null, // TODO Autofill result Launcher #765
|
||||||
|
database = database,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL
|
||||||
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
},
|
||||||
{
|
onItemNotFound = {
|
||||||
// Here no search info found, disable auto search
|
// Here no search info found, disable auto search
|
||||||
launchForRegistration(activity,
|
launchForRegistration(
|
||||||
database,
|
context = activity,
|
||||||
registerInfo)
|
activityResultLauncher = null, // TODO Autofill result Launcher #765
|
||||||
|
database = database,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL
|
||||||
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
},
|
||||||
{
|
onDatabaseClosed = {
|
||||||
// Simply close if database not opened, normally not happened
|
// Simply close if database not opened, normally not happened
|
||||||
onCancelSpecialMode()
|
onCancelSpecialMode()
|
||||||
}
|
}
|
||||||
@@ -1765,7 +1890,73 @@ class GroupActivity : DatabaseLockActivity(),
|
|||||||
.show()
|
.show()
|
||||||
onCancelSpecialMode()
|
onCancelSpecialMode()
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
passkeySelectionAction = { searchInfo ->
|
||||||
|
// Passkey selection
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
SearchHelper.checkAutoSearchInfo(
|
||||||
|
context = activity,
|
||||||
|
database = database,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { _, items ->
|
||||||
|
// Response is build
|
||||||
|
EntrySelectionHelper.performSelection(
|
||||||
|
items = items,
|
||||||
|
actionPopulateCredentialProvider = { entryInfo ->
|
||||||
|
activity.buildPasskeyResponseAndSetResult(entryInfo)
|
||||||
|
onValidateSpecialMode()
|
||||||
|
},
|
||||||
|
actionEntrySelection = {
|
||||||
|
launchForPasskeySelectionResult(
|
||||||
|
context = activity,
|
||||||
|
database = database,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
activityResultLauncher = activityResultLauncher,
|
||||||
|
autoSearch = true
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onItemNotFound = {
|
||||||
|
// Here no search info found, disable auto search
|
||||||
|
launchForPasskeySelectionResult(
|
||||||
|
context = activity,
|
||||||
|
database = database,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
activityResultLauncher = activityResultLauncher
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
// Simply close if database not opened, normally not happened
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
passkeyRegistrationAction = { registerInfo ->
|
||||||
|
// Passkey registration
|
||||||
|
if (!database.isReadOnly) {
|
||||||
|
launchForRegistration(
|
||||||
|
context = activity,
|
||||||
|
activityResultLauncher = activityResultLauncher,
|
||||||
|
database = database,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.PASSKEY
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(activity.applicationContext,
|
||||||
|
R.string.autofill_read_only_save,
|
||||||
|
Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,16 +48,17 @@ import androidx.lifecycle.repeatOnLifecycle
|
|||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
import com.kunzisoft.keepass.activities.dialogs.DuplicateUuidDialog
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
import com.kunzisoft.keepass.app.database.FileDatabaseHistoryAction
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
|
||||||
import com.kunzisoft.keepass.biometric.DeviceUnlockFragment
|
import com.kunzisoft.keepass.biometric.DeviceUnlockFragment
|
||||||
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
|
import com.kunzisoft.keepass.biometric.DeviceUnlockManager
|
||||||
import com.kunzisoft.keepass.biometric.deviceUnlockError
|
import com.kunzisoft.keepass.biometric.deviceUnlockError
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
@@ -75,8 +76,8 @@ import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.
|
|||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.DATABASE_URI_KEY
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.MAIN_CREDENTIAL_KEY
|
||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.READ_ONLY_KEY
|
||||||
import com.kunzisoft.keepass.settings.DeviceUnlockSettingsActivity
|
|
||||||
import com.kunzisoft.keepass.settings.AppearanceSettingsActivity
|
import com.kunzisoft.keepass.settings.AppearanceSettingsActivity
|
||||||
|
import com.kunzisoft.keepass.settings.DeviceUnlockSettingsActivity
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
import com.kunzisoft.keepass.utils.BACK_PREVIOUS_KEYBOARD_ACTION
|
||||||
@@ -122,10 +123,8 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
private var mReadOnly: Boolean = false
|
private var mReadOnly: Boolean = false
|
||||||
private var mForceReadOnly: Boolean = false
|
private var mForceReadOnly: Boolean = false
|
||||||
|
|
||||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
this.buildActivityResultLauncher()
|
||||||
AutofillHelper.buildActivityResultLauncher(this)
|
|
||||||
else null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -430,7 +429,7 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
{ onValidateSpecialMode() },
|
{ onValidateSpecialMode() },
|
||||||
{ onCancelSpecialMode() },
|
{ onCancelSpecialMode() },
|
||||||
{ onLaunchActivitySpecialMode() },
|
{ onLaunchActivitySpecialMode() },
|
||||||
mAutofillActivityResultLauncher
|
mCredentialActivityResultLauncher
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -845,14 +844,14 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
fun launchForAutofillResult(activity: AppCompatActivity,
|
fun launchForAutofillResult(activity: AppCompatActivity,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
hardwareKey: HardwareKey?,
|
hardwareKey: HardwareKey?,
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
autofillComponent: AutofillComponent,
|
autofillComponent: AutofillComponent,
|
||||||
searchInfo: SearchInfo?) {
|
searchInfo: SearchInfo?) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
AutofillHelper.startActivityForAutofillResult(
|
EntrySelectionHelper.startActivityForAutofillSelectionModeResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
activityResultLauncher,
|
activityResultLauncher,
|
||||||
@@ -863,19 +862,49 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* -------------------------
|
* -------------------------
|
||||||
* Registration Launch
|
* Passkey Launch
|
||||||
* -------------------------
|
* -------------------------
|
||||||
*/
|
*/
|
||||||
fun launchForRegistration(activity: Activity,
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
@Throws(FileNotFoundException::class)
|
||||||
|
fun launchForPasskeyResult(activity: Activity,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
databaseFile: Uri,
|
databaseFile: Uri,
|
||||||
keyFile: Uri?,
|
keyFile: Uri?,
|
||||||
hardwareKey: HardwareKey?,
|
hardwareKey: HardwareKey?,
|
||||||
registerInfo: RegisterInfo?) {
|
searchInfo: SearchInfo?) {
|
||||||
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
EntrySelectionHelper.startActivityForPasskeySelectionModeResult(
|
||||||
activity,
|
activity,
|
||||||
intent,
|
intent,
|
||||||
registerInfo)
|
activityResultLauncher,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* -------------------------
|
||||||
|
* Registration Launch
|
||||||
|
* -------------------------
|
||||||
|
*/
|
||||||
|
fun launchForRegistration(
|
||||||
|
activity: Activity,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
databaseFile: Uri,
|
||||||
|
keyFile: Uri?,
|
||||||
|
hardwareKey: HardwareKey?,
|
||||||
|
typeMode: TypeMode,
|
||||||
|
registerInfo: RegisterInfo?
|
||||||
|
) {
|
||||||
|
buildAndLaunchIntent(activity, databaseFile, keyFile, hardwareKey) { intent ->
|
||||||
|
EntrySelectionHelper.startActivityForRegistrationModeResult(
|
||||||
|
context = activity,
|
||||||
|
activityResultLauncher = activityResultLauncher,
|
||||||
|
intent = intent,
|
||||||
|
typeMode = typeMode,
|
||||||
|
registerInfo = registerInfo
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -891,71 +920,101 @@ class MainCredentialActivity : DatabaseModeActivity() {
|
|||||||
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
fileNoFoundAction: (exception: FileNotFoundException) -> Unit,
|
||||||
onCancelSpecialMode: () -> Unit,
|
onCancelSpecialMode: () -> Unit,
|
||||||
onLaunchActivitySpecialMode: () -> Unit,
|
onLaunchActivitySpecialMode: () -> Unit,
|
||||||
autofillActivityResultLauncher: ActivityResultLauncher<Intent>?) {
|
activityResultLauncher: ActivityResultLauncher<Intent>?) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
EntrySelectionHelper.doSpecialAction(activity.intent,
|
EntrySelectionHelper.doSpecialAction(
|
||||||
{
|
intent = activity.intent,
|
||||||
|
defaultAction = {
|
||||||
launch(
|
launch(
|
||||||
activity,
|
activity = activity,
|
||||||
databaseUri,
|
databaseFile = databaseUri,
|
||||||
keyFile,
|
keyFile = keyFile,
|
||||||
hardwareKey
|
hardwareKey = hardwareKey
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{ searchInfo -> // Search Action
|
searchAction = { searchInfo ->
|
||||||
launchForSearchResult(
|
launchForSearchResult(
|
||||||
activity,
|
activity = activity,
|
||||||
databaseUri,
|
databaseFile = databaseUri,
|
||||||
keyFile,
|
keyFile = keyFile,
|
||||||
hardwareKey,
|
hardwareKey = hardwareKey,
|
||||||
searchInfo
|
searchInfo = searchInfo
|
||||||
)
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
},
|
||||||
{ searchInfo -> // Save Action
|
saveAction = { searchInfo ->
|
||||||
launchForSaveResult(
|
launchForSaveResult(
|
||||||
activity,
|
activity = activity,
|
||||||
databaseUri,
|
databaseFile = databaseUri,
|
||||||
keyFile,
|
keyFile = keyFile,
|
||||||
hardwareKey,
|
hardwareKey = hardwareKey,
|
||||||
searchInfo
|
searchInfo = searchInfo
|
||||||
)
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
},
|
||||||
{ searchInfo -> // Keyboard Selection Action
|
keyboardSelectionAction = { searchInfo ->
|
||||||
launchForKeyboardResult(
|
launchForKeyboardResult(
|
||||||
activity,
|
activity = activity,
|
||||||
databaseUri,
|
databaseFile = databaseUri,
|
||||||
keyFile,
|
keyFile = keyFile,
|
||||||
hardwareKey,
|
hardwareKey = hardwareKey,
|
||||||
searchInfo
|
searchInfo = searchInfo
|
||||||
)
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
},
|
},
|
||||||
{ searchInfo, autofillComponent -> // Autofill Selection Action
|
autofillSelectionAction = { searchInfo, autofillComponent ->
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
launchForAutofillResult(
|
launchForAutofillResult(
|
||||||
activity,
|
activity = activity,
|
||||||
databaseUri,
|
activityResultLauncher = activityResultLauncher,
|
||||||
keyFile,
|
databaseFile = databaseUri,
|
||||||
hardwareKey,
|
keyFile = keyFile,
|
||||||
autofillActivityResultLauncher,
|
hardwareKey = hardwareKey,
|
||||||
autofillComponent,
|
autofillComponent = autofillComponent,
|
||||||
searchInfo
|
searchInfo = searchInfo
|
||||||
)
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
} else {
|
} else {
|
||||||
onCancelSpecialMode()
|
onCancelSpecialMode()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ registerInfo -> // Registration Action
|
autofillRegistrationAction = { registerInfo ->
|
||||||
launchForRegistration(
|
launchForRegistration(
|
||||||
activity,
|
activity = activity,
|
||||||
databaseUri,
|
activityResultLauncher = activityResultLauncher,
|
||||||
keyFile,
|
databaseFile = databaseUri,
|
||||||
hardwareKey,
|
keyFile = keyFile,
|
||||||
registerInfo
|
hardwareKey = hardwareKey,
|
||||||
|
typeMode = TypeMode.AUTOFILL,
|
||||||
|
registerInfo = registerInfo
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
},
|
||||||
|
passkeySelectionAction = { searchInfo ->
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
launchForPasskeyResult(
|
||||||
|
activity = activity,
|
||||||
|
activityResultLauncher = activityResultLauncher,
|
||||||
|
databaseFile = databaseUri,
|
||||||
|
keyFile = keyFile,
|
||||||
|
hardwareKey = hardwareKey,
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)
|
||||||
|
onLaunchActivitySpecialMode()
|
||||||
|
} else {
|
||||||
|
onCancelSpecialMode()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
passkeyRegistrationAction = { registerInfo ->
|
||||||
|
launchForRegistration(
|
||||||
|
activity = activity,
|
||||||
|
activityResultLauncher = activityResultLauncher,
|
||||||
|
databaseFile = databaseUri,
|
||||||
|
keyFile = keyFile,
|
||||||
|
hardwareKey = hardwareKey,
|
||||||
|
typeMode = TypeMode.PASSKEY,
|
||||||
|
registerInfo = registerInfo
|
||||||
)
|
)
|
||||||
onLaunchActivitySpecialMode()
|
onLaunchActivitySpecialMode()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ abstract class DatabaseDialogFragment : DialogFragment(), DatabaseRetrieval {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
|
@Deprecated(message = "")
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import com.kunzisoft.keepass.database.element.icon.IconImage
|
|||||||
import com.kunzisoft.keepass.model.GroupInfo
|
import com.kunzisoft.keepass.model.GroupInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
|
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
|
||||||
import com.kunzisoft.keepass.utils.UuidUtil
|
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
import com.kunzisoft.keepass.view.DateTimeFieldView
|
import com.kunzisoft.keepass.view.DateTimeFieldView
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ class GroupDialogFragment : DatabaseDialogFragment() {
|
|||||||
searchableView.text = stringFromInheritableBoolean(mGroupInfo.searchable)
|
searchableView.text = stringFromInheritableBoolean(mGroupInfo.searchable)
|
||||||
autoTypeView.text = stringFromInheritableBoolean(mGroupInfo.enableAutoType,
|
autoTypeView.text = stringFromInheritableBoolean(mGroupInfo.enableAutoType,
|
||||||
mGroupInfo.defaultAutoTypeSequence)
|
mGroupInfo.defaultAutoTypeSequence)
|
||||||
val uuid = UuidUtil.toHexString(mGroupInfo.id)
|
val uuid = mGroupInfo.id?.asHexString()
|
||||||
if (uuid == null || uuid.isEmpty()) {
|
if (uuid == null || uuid.isEmpty()) {
|
||||||
uuidContainerView.visibility = View.GONE
|
uuidContainerView.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import com.kunzisoft.keepass.model.StreamDirection
|
|||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
import com.kunzisoft.keepass.timeout.ClipboardHelper
|
||||||
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
|
import com.kunzisoft.keepass.utils.TimeUtil.getDateTimeString
|
||||||
import com.kunzisoft.keepass.utils.UuidUtil
|
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||||
import com.kunzisoft.keepass.view.TemplateView
|
import com.kunzisoft.keepass.view.TemplateView
|
||||||
import com.kunzisoft.keepass.view.hideByFading
|
import com.kunzisoft.keepass.view.hideByFading
|
||||||
import com.kunzisoft.keepass.view.showByFading
|
import com.kunzisoft.keepass.view.showByFading
|
||||||
@@ -184,7 +184,7 @@ class EntryFragment: DatabaseFragment() {
|
|||||||
// customDataView.text = entryInfo?.customData?.toString()
|
// customDataView.text = entryInfo?.customData?.toString()
|
||||||
|
|
||||||
// Assign special data
|
// Assign special data
|
||||||
uuidReferenceView.text = UuidUtil.toHexString(entryInfo?.id)
|
uuidReferenceView.text = entryInfo?.id?.asHexString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showClipboardDialog() {
|
private fun showClipboardDialog() {
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
|
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.SortDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.adapters.NodesAdapter
|
import com.kunzisoft.keepass.adapters.NodesAdapter
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
package com.kunzisoft.keepass.activities.helpers
|
|
||||||
|
|
||||||
enum class TypeMode {
|
|
||||||
DEFAULT, MAGIKEYBOARD, AUTOFILL
|
|
||||||
}
|
|
||||||
@@ -5,8 +5,8 @@ import android.os.Bundle
|
|||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
import com.kunzisoft.keepass.activities.stylish.StylishActivity
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
|
||||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||||
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
import com.kunzisoft.keepass.model.CipherEncryptDatabase
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.utils.getBinaryDir
|
import com.kunzisoft.keepass.utils.getBinaryDir
|
||||||
@@ -77,7 +77,13 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
|
|||||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||||
fixDuplicateUuid: Boolean
|
fixDuplicateUuid: Boolean
|
||||||
) {
|
) {
|
||||||
mDatabaseTaskProvider?.startDatabaseLoad(databaseUri, mainCredential, readOnly, cipherEncryptDatabase, fixDuplicateUuid)
|
mDatabaseTaskProvider?.startDatabaseLoad(
|
||||||
|
databaseUri,
|
||||||
|
mainCredential,
|
||||||
|
readOnly,
|
||||||
|
cipherEncryptDatabase,
|
||||||
|
fixDuplicateUuid
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun closeDatabase() {
|
protected fun closeDatabase() {
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ import androidx.appcompat.app.AlertDialog
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.MainCredential
|
import com.kunzisoft.keepass.database.MainCredential
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import android.view.View
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode
|
||||||
import com.kunzisoft.keepass.activities.helpers.TypeMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.ToolbarSpecial
|
import com.kunzisoft.keepass.view.ToolbarSpecial
|
||||||
@@ -42,14 +44,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
/**
|
/**
|
||||||
* Intent sender uses special retains data in callback
|
* Intent sender uses special retains data in callback
|
||||||
*/
|
*/
|
||||||
private fun isIntentSender(): Boolean {
|
protected fun isIntentSender(): Boolean {
|
||||||
return (mSpecialMode == SpecialMode.SELECTION
|
return isIntentSenderMode(mSpecialMode, mTypeMode)
|
||||||
&& mTypeMode == TypeMode.AUTOFILL)
|
|
||||||
/* TODO Registration callback #765
|
|
||||||
|| (mSpecialMode == SpecialMode.REGISTRATION
|
|
||||||
&& mTypeMode == TypeMode.AUTOFILL
|
|
||||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onLaunchActivitySpecialMode() {
|
fun onLaunchActivitySpecialMode() {
|
||||||
@@ -118,7 +114,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
|
|
||||||
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
||||||
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
||||||
val searchInfo: SearchInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)?.searchInfo
|
val registerInfo: RegisterInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)
|
||||||
|
val searchInfo: SearchInfo? = registerInfo?.searchInfo
|
||||||
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
||||||
|
|
||||||
// To show the selection mode
|
// To show the selection mode
|
||||||
@@ -136,12 +133,13 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
TypeMode.DEFAULT, // Not important because hidden
|
TypeMode.DEFAULT, // Not important because hidden
|
||||||
TypeMode.MAGIKEYBOARD -> R.string.magic_keyboard_title
|
TypeMode.MAGIKEYBOARD -> R.string.magic_keyboard_title
|
||||||
TypeMode.AUTOFILL -> R.string.autofill
|
TypeMode.AUTOFILL -> R.string.autofill
|
||||||
|
TypeMode.PASSKEY -> R.string.passkey
|
||||||
}
|
}
|
||||||
title = getString(selectionModeStringId)
|
title = getString(selectionModeStringId)
|
||||||
if (mTypeMode != TypeMode.DEFAULT)
|
if (mTypeMode != TypeMode.DEFAULT)
|
||||||
title = "$title (${getString(typeModeStringId)})"
|
title = "$title (${getString(typeModeStringId)})"
|
||||||
// Populate subtitle
|
// Populate subtitle
|
||||||
subtitle = searchInfo?.getName(resources)
|
subtitle = registerInfo?.getName(resources) ?: searchInfo?.getName(resources)
|
||||||
|
|
||||||
// Show the toolbar or not
|
// Show the toolbar or not
|
||||||
visible = when (mSpecialMode) {
|
visible = when (mSpecialMode) {
|
||||||
|
|||||||
@@ -418,6 +418,7 @@ class NodesAdapter (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OTP
|
||||||
val otpElement = entry.getOtpElement()
|
val otpElement = entry.getOtpElement()
|
||||||
holder.otpContainer?.removeCallbacks(holder.otpRunnable)
|
holder.otpContainer?.removeCallbacks(holder.otpRunnable)
|
||||||
if (otpElement != null
|
if (otpElement != null
|
||||||
@@ -440,6 +441,10 @@ class NodesAdapter (
|
|||||||
holder.attachmentIcon?.visibility =
|
holder.attachmentIcon?.visibility =
|
||||||
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
if (entry.containsAttachment()) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
// Passkey
|
||||||
|
holder.passkeyIcon?.visibility =
|
||||||
|
if (entry.getPasskey() != null) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
// Assign colors
|
// Assign colors
|
||||||
assignBackgroundColor(holder.container, entry)
|
assignBackgroundColor(holder.container, entry)
|
||||||
assignBackgroundColor(holder.otpContainer, entry)
|
assignBackgroundColor(holder.otpContainer, entry)
|
||||||
@@ -451,6 +456,7 @@ class NodesAdapter (
|
|||||||
holder.otpToken?.setTextColor(foregroundColor)
|
holder.otpToken?.setTextColor(foregroundColor)
|
||||||
holder.otpProgress?.setIndicatorColor(foregroundColor)
|
holder.otpProgress?.setIndicatorColor(foregroundColor)
|
||||||
holder.attachmentIcon?.setColorFilter(foregroundColor)
|
holder.attachmentIcon?.setColorFilter(foregroundColor)
|
||||||
|
holder.passkeyIcon?.setColorFilter(foregroundColor)
|
||||||
holder.meta.setTextColor(foregroundColor)
|
holder.meta.setTextColor(foregroundColor)
|
||||||
iconColor = foregroundColor
|
iconColor = foregroundColor
|
||||||
} else {
|
} else {
|
||||||
@@ -459,6 +465,7 @@ class NodesAdapter (
|
|||||||
holder.otpToken?.setTextColor(mTextColorSecondary)
|
holder.otpToken?.setTextColor(mTextColorSecondary)
|
||||||
holder.otpProgress?.setIndicatorColor(mTextColorSecondary)
|
holder.otpProgress?.setIndicatorColor(mTextColorSecondary)
|
||||||
holder.attachmentIcon?.setColorFilter(mTextColorSecondary)
|
holder.attachmentIcon?.setColorFilter(mTextColorSecondary)
|
||||||
|
holder.passkeyIcon?.setColorFilter(mTextColorSecondary)
|
||||||
holder.meta.setTextColor(mTextColor)
|
holder.meta.setTextColor(mTextColor)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -467,6 +474,7 @@ class NodesAdapter (
|
|||||||
holder.otpToken?.setTextColor(mColorOnSecondary)
|
holder.otpToken?.setTextColor(mColorOnSecondary)
|
||||||
holder.otpProgress?.setIndicatorColor(mColorOnSecondary)
|
holder.otpProgress?.setIndicatorColor(mColorOnSecondary)
|
||||||
holder.attachmentIcon?.setColorFilter(mColorOnSecondary)
|
holder.attachmentIcon?.setColorFilter(mColorOnSecondary)
|
||||||
|
holder.passkeyIcon?.setColorFilter(mColorOnSecondary)
|
||||||
holder.meta.setTextColor(mColorOnSecondary)
|
holder.meta.setTextColor(mColorOnSecondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,6 +619,7 @@ class NodesAdapter (
|
|||||||
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
|
var otpRunnable: OtpRunnable = OtpRunnable(otpContainer)
|
||||||
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
var numberChildren: TextView? = itemView.findViewById(R.id.node_child_numbers)
|
||||||
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
|
var attachmentIcon: ImageView? = itemView.findViewById(R.id.node_attachment_icon)
|
||||||
|
var passkeyIcon: ImageView? = itemView.findViewById(R.id.node_passkey_icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -17,17 +17,32 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities.helpers
|
package com.kunzisoft.keepass.credentialprovider
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import android.widget.RemoteViews
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper.addAutofillComponent
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||||
import com.kunzisoft.keepass.utils.getEnumExtra
|
import com.kunzisoft.keepass.utils.getEnumExtra
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
import com.kunzisoft.keepass.utils.putEnumExtra
|
import com.kunzisoft.keepass.utils.putEnumExtra
|
||||||
|
|
||||||
object EntrySelectionHelper {
|
object EntrySelectionHelper {
|
||||||
@@ -37,6 +52,33 @@ object EntrySelectionHelper {
|
|||||||
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
|
private const val KEY_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO"
|
||||||
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
|
private const val KEY_REGISTER_INFO = "com.kunzisoft.keepass.extra.REGISTER_INFO"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to build a registerForActivityResult,
|
||||||
|
* Used recursively, close each activity with return data
|
||||||
|
*/
|
||||||
|
fun AppCompatActivity.buildActivityResultLauncher(
|
||||||
|
lockDatabase: Boolean = false,
|
||||||
|
dataTransformation: (data: Intent?) -> Intent? = { it },
|
||||||
|
): ActivityResultLauncher<Intent> {
|
||||||
|
return this.registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) {
|
||||||
|
val resultCode = it.resultCode
|
||||||
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
|
this.setResult(resultCode, dataTransformation(it.data))
|
||||||
|
}
|
||||||
|
if (resultCode == Activity.RESULT_CANCELED) {
|
||||||
|
this.setResult(Activity.RESULT_CANCELED)
|
||||||
|
}
|
||||||
|
this.finish()
|
||||||
|
|
||||||
|
if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(this)) {
|
||||||
|
// Close the database
|
||||||
|
this.sendBroadcast(Intent(LOCK_ACTION))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun startActivityForSearchModeResult(context: Context,
|
fun startActivityForSearchModeResult(context: Context,
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
searchInfo: SearchInfo) {
|
searchInfo: SearchInfo) {
|
||||||
@@ -66,15 +108,52 @@ object EntrySelectionHelper {
|
|||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startActivityForRegistrationModeResult(context: Context,
|
/**
|
||||||
|
* Utility method to start an activity with an Autofill for result
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
fun startActivityForAutofillSelectionModeResult(
|
||||||
|
context: Context,
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
registerInfo: RegisterInfo?) {
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
|
autofillComponent: AutofillComponent,
|
||||||
// At the moment, only autofill for registration
|
searchInfo: SearchInfo?
|
||||||
|
) {
|
||||||
|
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||||
addTypeModeInIntent(intent, TypeMode.AUTOFILL)
|
addTypeModeInIntent(intent, TypeMode.AUTOFILL)
|
||||||
|
intent.addAutofillComponent(context, autofillComponent)
|
||||||
|
addSearchInfoInIntent(intent, searchInfo)
|
||||||
|
activityResultLauncher?.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
fun startActivityForPasskeySelectionModeResult(
|
||||||
|
context: Context,
|
||||||
|
intent: Intent,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
searchInfo: SearchInfo?
|
||||||
|
) {
|
||||||
|
addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
||||||
|
addTypeModeInIntent(intent, TypeMode.PASSKEY)
|
||||||
|
addSearchInfoInIntent(intent, searchInfo)
|
||||||
|
activityResultLauncher?.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startActivityForRegistrationModeResult(
|
||||||
|
context: Context?,
|
||||||
|
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
||||||
|
intent: Intent,
|
||||||
|
registerInfo: RegisterInfo?,
|
||||||
|
typeMode: TypeMode
|
||||||
|
) {
|
||||||
|
addSpecialModeInIntent(intent, SpecialMode.REGISTRATION)
|
||||||
|
addTypeModeInIntent(intent, typeMode)
|
||||||
addRegisterInfoInIntent(intent, registerInfo)
|
addRegisterInfoInIntent(intent, registerInfo)
|
||||||
|
if (activityResultLauncher == null) {
|
||||||
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
intent.flags = intent.flags or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
context.startActivity(intent)
|
}
|
||||||
|
activityResultLauncher?.launch(intent) ?: context?.startActivity(intent) ?:
|
||||||
|
throw IllegalStateException("At least Context or ActivityResultLauncher must not be null")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addSearchInfoInIntent(intent: Intent, searchInfo: SearchInfo?) {
|
fun addSearchInfoInIntent(intent: Intent, searchInfo: SearchInfo?) {
|
||||||
@@ -103,8 +182,13 @@ object EntrySelectionHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
|
fun addSpecialModeInIntent(intent: Intent, specialMode: SpecialMode) {
|
||||||
|
// TODO Replace by Intent.addSpecialMode
|
||||||
intent.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
|
intent.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
|
||||||
}
|
}
|
||||||
|
fun Intent.addSpecialMode(specialMode: SpecialMode): Intent {
|
||||||
|
this.putEnumExtra(KEY_SPECIAL_MODE, specialMode)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
|
fun retrieveSpecialModeFromIntent(intent: Intent): SpecialMode {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
@@ -115,8 +199,13 @@ object EntrySelectionHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
|
private fun addTypeModeInIntent(intent: Intent, typeMode: TypeMode) {
|
||||||
|
// TODO Replace by Intent.addTypeMode
|
||||||
intent.putEnumExtra(KEY_TYPE_MODE, typeMode)
|
intent.putEnumExtra(KEY_TYPE_MODE, typeMode)
|
||||||
}
|
}
|
||||||
|
fun Intent.addTypeMode(typeMode: TypeMode): Intent {
|
||||||
|
this.putEnumExtra(KEY_TYPE_MODE, typeMode)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
|
fun retrieveTypeModeFromIntent(intent: Intent): TypeMode {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
@@ -131,6 +220,17 @@ object EntrySelectionHelper {
|
|||||||
intent.removeExtra(KEY_TYPE_MODE)
|
intent.removeExtra(KEY_TYPE_MODE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intent sender uses special retains data in callback
|
||||||
|
*/
|
||||||
|
fun isIntentSenderMode(specialMode: SpecialMode, typeMode: TypeMode): Boolean {
|
||||||
|
return (specialMode == SpecialMode.SELECTION
|
||||||
|
&& (typeMode == TypeMode.AUTOFILL || typeMode == TypeMode.PASSKEY))
|
||||||
|
// TODO Autofill Registration callback #765 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||||
|
|| (specialMode == SpecialMode.REGISTRATION
|
||||||
|
&& typeMode == TypeMode.PASSKEY)
|
||||||
|
}
|
||||||
|
|
||||||
fun doSpecialAction(intent: Intent,
|
fun doSpecialAction(intent: Intent,
|
||||||
defaultAction: () -> Unit,
|
defaultAction: () -> Unit,
|
||||||
searchAction: (searchInfo: SearchInfo) -> Unit,
|
searchAction: (searchInfo: SearchInfo) -> Unit,
|
||||||
@@ -138,7 +238,9 @@ object EntrySelectionHelper {
|
|||||||
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
|
keyboardSelectionAction: (searchInfo: SearchInfo?) -> Unit,
|
||||||
autofillSelectionAction: (searchInfo: SearchInfo?,
|
autofillSelectionAction: (searchInfo: SearchInfo?,
|
||||||
autofillComponent: AutofillComponent) -> Unit,
|
autofillComponent: AutofillComponent) -> Unit,
|
||||||
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
|
autofillRegistrationAction: (registerInfo: RegisterInfo?) -> Unit,
|
||||||
|
passkeySelectionAction: (searchInfo: SearchInfo?) -> Unit,
|
||||||
|
passkeyRegistrationAction: (registerInfo: RegisterInfo?) -> Unit) {
|
||||||
|
|
||||||
when (retrieveSpecialModeFromIntent(intent)) {
|
when (retrieveSpecialModeFromIntent(intent)) {
|
||||||
SpecialMode.DEFAULT -> {
|
SpecialMode.DEFAULT -> {
|
||||||
@@ -186,6 +288,7 @@ object EntrySelectionHelper {
|
|||||||
defaultAction.invoke()
|
defaultAction.invoke()
|
||||||
}
|
}
|
||||||
TypeMode.MAGIKEYBOARD -> keyboardSelectionAction.invoke(searchInfo)
|
TypeMode.MAGIKEYBOARD -> keyboardSelectionAction.invoke(searchInfo)
|
||||||
|
TypeMode.PASSKEY -> passkeySelectionAction.invoke(searchInfo)
|
||||||
else -> {
|
else -> {
|
||||||
// In this case, error
|
// In this case, error
|
||||||
removeModesFromIntent(intent)
|
removeModesFromIntent(intent)
|
||||||
@@ -202,10 +305,59 @@ object EntrySelectionHelper {
|
|||||||
}
|
}
|
||||||
SpecialMode.REGISTRATION -> {
|
SpecialMode.REGISTRATION -> {
|
||||||
val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent)
|
val registerInfo: RegisterInfo? = retrieveRegisterInfoFromIntent(intent)
|
||||||
|
if (!isIntentSenderMode(
|
||||||
|
specialMode = retrieveSpecialModeFromIntent(intent),
|
||||||
|
typeMode = retrieveTypeModeFromIntent(intent))
|
||||||
|
) {
|
||||||
removeModesFromIntent(intent)
|
removeModesFromIntent(intent)
|
||||||
removeInfoFromIntent(intent)
|
removeInfoFromIntent(intent)
|
||||||
|
}
|
||||||
|
when (retrieveTypeModeFromIntent(intent)) {
|
||||||
|
TypeMode.AUTOFILL -> {
|
||||||
autofillRegistrationAction.invoke(registerInfo)
|
autofillRegistrationAction.invoke(registerInfo)
|
||||||
}
|
}
|
||||||
|
TypeMode.PASSKEY -> {
|
||||||
|
passkeyRegistrationAction.invoke(registerInfo)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Do other registration type
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun performSelection(items: List<EntryInfo>,
|
||||||
|
actionPopulateCredentialProvider: (entryInfo: EntryInfo) -> Unit,
|
||||||
|
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
|
||||||
|
if (items.size == 1) {
|
||||||
|
val itemFound = items[0]
|
||||||
|
actionPopulateCredentialProvider.invoke(itemFound)
|
||||||
|
} else if (items.size > 1) {
|
||||||
|
// Select the one we want in the selection
|
||||||
|
actionEntrySelection.invoke(true)
|
||||||
|
} else {
|
||||||
|
// Select an arbitrary one
|
||||||
|
actionEntrySelection.invoke(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to assign a drawable to a new icon from a database icon
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun EntryInfo.buildIcon(
|
||||||
|
context: Context,
|
||||||
|
database: ContextualDatabase
|
||||||
|
): Icon? {
|
||||||
|
try {
|
||||||
|
database.iconDrawableFactory.getBitmapFromIcon(context,
|
||||||
|
this.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
||||||
|
return Icon.createWithBitmap(bitmap)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.kunzisoft.keepass.activities.helpers
|
package com.kunzisoft.keepass.credentialprovider
|
||||||
|
|
||||||
enum class SpecialMode {
|
enum class SpecialMode {
|
||||||
DEFAULT,
|
DEFAULT,
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.kunzisoft.keepass.credentialprovider
|
||||||
|
|
||||||
|
enum class TypeMode {
|
||||||
|
DEFAULT, MAGIKEYBOARD, AUTOFILL, PASSKEY
|
||||||
|
}
|
||||||
@@ -17,9 +17,8 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.credentialprovider.activity
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -30,13 +29,17 @@ import android.widget.Toast
|
|||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
import com.kunzisoft.keepass.autofill.AutofillComponent
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
import com.kunzisoft.keepass.autofill.AutofillHelper
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
||||||
import com.kunzisoft.keepass.autofill.CompatInlineSuggestionsRequest
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.autofill.KeeAutofillService
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillComponent
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.AutofillHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.CompatInlineSuggestionsRequest
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.autofill.KeeAutofillService
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
import com.kunzisoft.keepass.model.RegisterInfo
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
@@ -48,10 +51,8 @@ import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
|||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
class AutofillLauncherActivity : DatabaseModeActivity() {
|
class AutofillLauncherActivity : DatabaseModeActivity() {
|
||||||
|
|
||||||
private var mAutofillActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
private var mCredentialActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
this.buildActivityResultLauncher(lockDatabase = true)
|
||||||
AutofillHelper.buildActivityResultLauncher(this, true)
|
|
||||||
else null
|
|
||||||
|
|
||||||
override fun applyCustomStyle(): Boolean {
|
override fun applyCustomStyle(): Boolean {
|
||||||
return false
|
return false
|
||||||
@@ -72,7 +73,9 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
// To pass extra inline request
|
// To pass extra inline request
|
||||||
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
|
var compatInlineSuggestionsRequest: CompatInlineSuggestionsRequest? = null
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
compatInlineSuggestionsRequest = bundle.getParcelableCompat(KEY_INLINE_SUGGESTION)
|
compatInlineSuggestionsRequest = bundle.getParcelableCompat(
|
||||||
|
KEY_INLINE_SUGGESTION
|
||||||
|
)
|
||||||
}
|
}
|
||||||
// Build search param
|
// Build search param
|
||||||
bundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
|
bundle.getParcelableCompat<SearchInfo>(KEY_SEARCH_INFO)?.let { searchInfo ->
|
||||||
@@ -102,7 +105,9 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
}
|
}
|
||||||
SpecialMode.REGISTRATION -> {
|
SpecialMode.REGISTRATION -> {
|
||||||
// To register info
|
// To register info
|
||||||
val registerInfo = intent.getParcelableExtraCompat<RegisterInfo>(KEY_REGISTER_INFO)
|
val registerInfo = intent.getParcelableExtraCompat<RegisterInfo>(
|
||||||
|
KEY_REGISTER_INFO
|
||||||
|
)
|
||||||
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
val searchInfo = SearchInfo(registerInfo?.searchInfo)
|
||||||
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
WebDomain.getConcreteWebDomain(this, searchInfo.webDomain) { concreteWebDomain ->
|
||||||
searchInfo.webDomain = concreteWebDomain
|
searchInfo.webDomain = concreteWebDomain
|
||||||
@@ -111,7 +116,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// Not an autofill call
|
// Not an autofill call
|
||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(RESULT_CANCELED)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,7 +127,7 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
autofillComponent: AutofillComponent?,
|
autofillComponent: AutofillComponent?,
|
||||||
searchInfo: SearchInfo) {
|
searchInfo: SearchInfo) {
|
||||||
if (autofillComponent == null) {
|
if (autofillComponent == null) {
|
||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(RESULT_CANCELED)
|
||||||
finish()
|
finish()
|
||||||
} else if (KeeAutofillService.autofillAllowedFor(
|
} else if (KeeAutofillService.autofillAllowedFor(
|
||||||
applicationId = searchInfo.applicationId,
|
applicationId = searchInfo.applicationId,
|
||||||
@@ -130,34 +135,39 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
context = this
|
context = this
|
||||||
)) {
|
)) {
|
||||||
// If database is open
|
// If database is open
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(
|
||||||
database,
|
context = this,
|
||||||
searchInfo,
|
database = database,
|
||||||
{ openedDatabase, items ->
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { openedDatabase, items ->
|
||||||
// Items found
|
// Items found
|
||||||
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
|
AutofillHelper.buildResponseAndSetResult(this, openedDatabase, items)
|
||||||
finish()
|
finish()
|
||||||
},
|
},
|
||||||
{ openedDatabase ->
|
onItemNotFound = { openedDatabase ->
|
||||||
// Show the database UI to select the entry
|
// Show the database UI to select the entry
|
||||||
GroupActivity.launchForAutofillResult(this,
|
GroupActivity.launchForAutofillSelectionResult(
|
||||||
|
this,
|
||||||
openedDatabase,
|
openedDatabase,
|
||||||
mAutofillActivityResultLauncher,
|
mCredentialActivityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
false)
|
false
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
onDatabaseClosed = {
|
||||||
// If database not open
|
// If database not open
|
||||||
FileDatabaseSelectActivity.launchForAutofillResult(this,
|
FileDatabaseSelectActivity.launchForAutofillResult(
|
||||||
mAutofillActivityResultLauncher,
|
this,
|
||||||
|
mCredentialActivityResultLauncher,
|
||||||
autofillComponent,
|
autofillComponent,
|
||||||
searchInfo)
|
searchInfo
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
showBlockRestartMessage()
|
showBlockRestartMessage()
|
||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(RESULT_CANCELED)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,38 +181,51 @@ class AutofillLauncherActivity : DatabaseModeActivity() {
|
|||||||
context = this
|
context = this
|
||||||
)) {
|
)) {
|
||||||
val readOnly = database?.isReadOnly != false
|
val readOnly = database?.isReadOnly != false
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(
|
||||||
database,
|
context = this,
|
||||||
searchInfo,
|
database = database,
|
||||||
{ openedDatabase, _ ->
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { openedDatabase, _ ->
|
||||||
if (!readOnly) {
|
if (!readOnly) {
|
||||||
// Show the database UI to select the entry
|
// Show the database UI to select the entry
|
||||||
GroupActivity.launchForRegistration(this,
|
GroupActivity.launchForRegistration(
|
||||||
openedDatabase,
|
context = this,
|
||||||
registerInfo)
|
activityResultLauncher = null, // TODO Autofill result launcher #765
|
||||||
|
database = openedDatabase,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
showReadOnlySaveMessage()
|
showReadOnlySaveMessage()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ openedDatabase ->
|
onItemNotFound = { openedDatabase ->
|
||||||
if (!readOnly) {
|
if (!readOnly) {
|
||||||
// Show the database UI to select the entry
|
// Show the database UI to select the entry
|
||||||
GroupActivity.launchForRegistration(this,
|
GroupActivity.launchForRegistration(
|
||||||
openedDatabase,
|
context = this,
|
||||||
registerInfo)
|
activityResultLauncher = null, // TODO Autofill result launcher #765
|
||||||
|
database = openedDatabase,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
showReadOnlySaveMessage()
|
showReadOnlySaveMessage()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
onDatabaseClosed = {
|
||||||
// If database not open
|
// If database not open
|
||||||
FileDatabaseSelectActivity.launchForRegistration(this,
|
FileDatabaseSelectActivity.launchForRegistration(
|
||||||
registerInfo)
|
context = this,
|
||||||
|
activityResultLauncher = null, // TODO Autofill result launcher #765
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.AUTOFILL
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
showBlockRestartMessage()
|
showBlockRestartMessage()
|
||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(RESULT_CANCELED)
|
||||||
}
|
}
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
@@ -17,23 +17,25 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.activities
|
package com.kunzisoft.keepass.credentialprovider.activity
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||||
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
|
import com.kunzisoft.keepass.utils.KeyboardUtil.isKeyboardActivatedInSettings
|
||||||
import com.kunzisoft.keepass.utils.getParcelableCompat
|
|
||||||
import com.kunzisoft.keepass.utils.WebDomain
|
import com.kunzisoft.keepass.utils.WebDomain
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableCompat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity to search or select entry in database,
|
* Activity to search or select entry in database,
|
||||||
@@ -73,7 +75,7 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
if (OtpEntryFields.isOTPUri(extra))
|
if (OtpEntryFields.isOTPUri(extra))
|
||||||
otpString = extra
|
otpString = extra
|
||||||
else
|
else
|
||||||
sharedWebDomain = Uri.parse(extra).host
|
sharedWebDomain = extra.toUri().host
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
launchSelection(database, sharedWebDomain, otpString)
|
launchSelection(database, sharedWebDomain, otpString)
|
||||||
@@ -121,10 +123,11 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
|
|
||||||
// If database is open
|
// If database is open
|
||||||
val readOnly = database?.isReadOnly != false
|
val readOnly = database?.isReadOnly != false
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(
|
||||||
database,
|
context = this,
|
||||||
searchInfo,
|
database = database,
|
||||||
{ openedDatabase, items ->
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { openedDatabase, items ->
|
||||||
// Items found
|
// Items found
|
||||||
if (searchInfo.otpString != null) {
|
if (searchInfo.otpString != null) {
|
||||||
if (!readOnly) {
|
if (!readOnly) {
|
||||||
@@ -132,7 +135,8 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
this,
|
this,
|
||||||
openedDatabase,
|
openedDatabase,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
false)
|
false
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(applicationContext,
|
Toast.makeText(applicationContext,
|
||||||
R.string.autofill_read_only_save,
|
R.string.autofill_read_only_save,
|
||||||
@@ -150,27 +154,33 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
{ autoSearch ->
|
{ autoSearch ->
|
||||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
GroupActivity.launchForKeyboardSelectionResult(
|
||||||
|
this,
|
||||||
openedDatabase,
|
openedDatabase,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
autoSearch)
|
autoSearch
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
GroupActivity.launchForSearchResult(this,
|
GroupActivity.launchForSearchResult(
|
||||||
|
this,
|
||||||
openedDatabase,
|
openedDatabase,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
true)
|
true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ openedDatabase ->
|
onItemNotFound = { openedDatabase ->
|
||||||
// Show the database UI to select the entry
|
// Show the database UI to select the entry
|
||||||
if (searchInfo.otpString != null) {
|
if (searchInfo.otpString != null) {
|
||||||
if (!readOnly) {
|
if (!readOnly) {
|
||||||
GroupActivity.launchForSaveResult(this,
|
GroupActivity.launchForSaveResult(
|
||||||
|
this,
|
||||||
openedDatabase,
|
openedDatabase,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
false)
|
false
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(applicationContext,
|
Toast.makeText(applicationContext,
|
||||||
R.string.autofill_read_only_save,
|
R.string.autofill_read_only_save,
|
||||||
@@ -178,28 +188,38 @@ class EntrySelectionLauncherActivity : DatabaseModeActivity() {
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
} else if (searchShareForMagikeyboard) {
|
} else if (searchShareForMagikeyboard) {
|
||||||
GroupActivity.launchForKeyboardSelectionResult(this,
|
GroupActivity.launchForKeyboardSelectionResult(
|
||||||
|
this,
|
||||||
openedDatabase,
|
openedDatabase,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
false)
|
false
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
GroupActivity.launchForSearchResult(this,
|
GroupActivity.launchForSearchResult(
|
||||||
|
this,
|
||||||
openedDatabase,
|
openedDatabase,
|
||||||
searchInfo,
|
searchInfo,
|
||||||
false)
|
false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
onDatabaseClosed = {
|
||||||
// If database not open
|
// If database not open
|
||||||
if (searchInfo.otpString != null) {
|
if (searchInfo.otpString != null) {
|
||||||
FileDatabaseSelectActivity.launchForSaveResult(this,
|
FileDatabaseSelectActivity.launchForSaveResult(
|
||||||
searchInfo)
|
this,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
} else if (searchShareForMagikeyboard) {
|
} else if (searchShareForMagikeyboard) {
|
||||||
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(this,
|
FileDatabaseSelectActivity.launchForKeyboardSelectionResult(
|
||||||
searchInfo)
|
this,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
FileDatabaseSelectActivity.launchForSearchResult(this,
|
FileDatabaseSelectActivity.launchForSearchResult(
|
||||||
searchInfo)
|
this,
|
||||||
|
searchInfo
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,400 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.activity
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.credentials.GetCredentialResponse
|
||||||
|
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||||
|
import androidx.credentials.provider.PendingIntentHandler
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||||
|
import com.kunzisoft.keepass.activities.GroupActivity
|
||||||
|
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addSpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.addTypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildActivityResultLauncher
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addNodeId
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addSearchInfo
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildCreatePublicKeyCredentialResponse
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyPublicKeyCredential
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.checkSecurity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getVerifiedGETClientDataResponse
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeAppOrigin
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveAppOrigin
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveNodeId
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskey
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationRequestParameters
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveSearchInfo
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
|
import com.kunzisoft.keepass.model.AppOrigin
|
||||||
|
import com.kunzisoft.keepass.model.Passkey
|
||||||
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InvalidObjectException
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
class PasskeyLauncherActivity : DatabaseModeActivity() {
|
||||||
|
|
||||||
|
private var mUsageParameters: PublicKeyCredentialUsageParameters? = null
|
||||||
|
private var mCreationParameters: PublicKeyCredentialCreationParameters? = null
|
||||||
|
private var mPasskey: Passkey? = null
|
||||||
|
|
||||||
|
private var mPasskeySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
|
this.buildActivityResultLauncher(
|
||||||
|
lockDatabase = true,
|
||||||
|
dataTransformation = { intent ->
|
||||||
|
// Build a new formatted response from the selection response
|
||||||
|
val responseIntent = Intent()
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "Passkey selection result")
|
||||||
|
if (intent == null)
|
||||||
|
throw IOException("Intent is null")
|
||||||
|
val passkey = intent.retrievePasskey()
|
||||||
|
?: throw IOException("Passkey is null")
|
||||||
|
val appOrigin = intent.retrieveAppOrigin()
|
||||||
|
?: throw IOException("App origin is null")
|
||||||
|
intent.removePasskey()
|
||||||
|
intent.removeAppOrigin()
|
||||||
|
mUsageParameters?.let { usageParameters ->
|
||||||
|
// Check verified origin
|
||||||
|
PendingIntentHandler.setGetCredentialResponse(
|
||||||
|
responseIntent,
|
||||||
|
GetCredentialResponse(
|
||||||
|
buildPasskeyPublicKeyCredential(
|
||||||
|
requestOptions = usageParameters.publicKeyCredentialRequestOptions,
|
||||||
|
clientDataResponse = getVerifiedGETClientDataResponse(
|
||||||
|
usageParameters = usageParameters,
|
||||||
|
appOrigin = appOrigin
|
||||||
|
),
|
||||||
|
passkey = passkey
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} ?: run {
|
||||||
|
throw IOException("Usage parameters is null")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to create selection response for passkey", e)
|
||||||
|
showError(e)
|
||||||
|
}
|
||||||
|
// Return the response
|
||||||
|
responseIntent
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
private var mPasskeyRegistrationActivityResultLauncher: ActivityResultLauncher<Intent>? =
|
||||||
|
this.buildActivityResultLauncher(
|
||||||
|
lockDatabase = true,
|
||||||
|
dataTransformation = { intent ->
|
||||||
|
// Build a new formatted response from the creation response
|
||||||
|
val responseIntent = Intent()
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "Passkey registration result")
|
||||||
|
val passkey = intent?.retrievePasskey()
|
||||||
|
intent?.removePasskey()
|
||||||
|
intent?.removeAppOrigin()
|
||||||
|
// If registered passkey is the same as the one we want to validate,
|
||||||
|
if (mPasskey == passkey) {
|
||||||
|
mCreationParameters?.let {
|
||||||
|
PendingIntentHandler.setCreateCredentialResponse(
|
||||||
|
intent = responseIntent,
|
||||||
|
response = buildCreatePublicKeyCredentialResponse(
|
||||||
|
publicKeyCredentialCreationParameters = it
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw SecurityException("Passkey was modified before registration")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Unable to create registration response for passkey", e)
|
||||||
|
showError(e)
|
||||||
|
}
|
||||||
|
responseIntent
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun applyCustomStyle(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun finishActivityIfReloadRequested(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDatabaseRetrieved(database: ContextualDatabase?) {
|
||||||
|
super.onDatabaseRetrieved(database)
|
||||||
|
|
||||||
|
lifecycleScope.launch(CoroutineExceptionHandler { _, e ->
|
||||||
|
Log.e(TAG, "Passkey launch error", e)
|
||||||
|
showError(e)
|
||||||
|
setResult(RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
}) {
|
||||||
|
val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo()
|
||||||
|
val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin(verified = false)
|
||||||
|
val nodeId = intent.retrieveNodeId()
|
||||||
|
checkSecurity(intent, nodeId)
|
||||||
|
when (mSpecialMode) {
|
||||||
|
SpecialMode.SELECTION -> {
|
||||||
|
launchSelection(database, nodeId, searchInfo, appOrigin)
|
||||||
|
}
|
||||||
|
SpecialMode.REGISTRATION -> {
|
||||||
|
// TODO Registration in predefined group
|
||||||
|
// launchRegistration(database, nodeId, mSearchInfo)
|
||||||
|
launchRegistration(database, null, searchInfo)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw InvalidObjectException("Passkey launch mode not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun autoSelectPasskeyAndSetResult(
|
||||||
|
database: ContextualDatabase?,
|
||||||
|
nodeId: UUID,
|
||||||
|
appOrigin: AppOrigin
|
||||||
|
) {
|
||||||
|
mUsageParameters?.let { usageParameters ->
|
||||||
|
// To get the passkey from the database
|
||||||
|
val passkey = database
|
||||||
|
?.getEntryById(NodeIdUUID(nodeId))
|
||||||
|
?.getEntryInfo(database)
|
||||||
|
?.passkey
|
||||||
|
?: throw GetCredentialUnknownException("No passkey with nodeId $nodeId found")
|
||||||
|
|
||||||
|
val result = Intent()
|
||||||
|
PendingIntentHandler.setGetCredentialResponse(
|
||||||
|
result,
|
||||||
|
GetCredentialResponse(
|
||||||
|
buildPasskeyPublicKeyCredential(
|
||||||
|
requestOptions = usageParameters.publicKeyCredentialRequestOptions,
|
||||||
|
clientDataResponse = getVerifiedGETClientDataResponse(
|
||||||
|
usageParameters = usageParameters,
|
||||||
|
appOrigin = appOrigin
|
||||||
|
),
|
||||||
|
passkey = passkey
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setResult(RESULT_OK, result)
|
||||||
|
finish()
|
||||||
|
} ?: run {
|
||||||
|
Log.e(TAG, "Unable to auto select passkey, usage parameters are empty")
|
||||||
|
setResult(RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun launchSelection(
|
||||||
|
database: ContextualDatabase?,
|
||||||
|
nodeId: UUID?,
|
||||||
|
searchInfo: SearchInfo,
|
||||||
|
appOrigin: AppOrigin
|
||||||
|
) {
|
||||||
|
Log.d(TAG, "Launch passkey selection")
|
||||||
|
retrievePasskeyUsageRequestParameters(
|
||||||
|
intent = intent,
|
||||||
|
assetManager = assets
|
||||||
|
) { usageParameters ->
|
||||||
|
// Save the requested parameters
|
||||||
|
mUsageParameters = usageParameters
|
||||||
|
// Manage the passkey to use
|
||||||
|
nodeId?.let { nodeId ->
|
||||||
|
autoSelectPasskeyAndSetResult(database, nodeId, appOrigin)
|
||||||
|
} ?: run {
|
||||||
|
SearchHelper.checkAutoSearchInfo(
|
||||||
|
context = this,
|
||||||
|
database = database,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { _, _ ->
|
||||||
|
Log.w(
|
||||||
|
TAG, "Passkey found for auto selection, should not append," +
|
||||||
|
"use PasskeyProviderService instead"
|
||||||
|
)
|
||||||
|
finish()
|
||||||
|
},
|
||||||
|
onItemNotFound = { openedDatabase ->
|
||||||
|
Log.d(
|
||||||
|
TAG, "No Passkey found for selection," +
|
||||||
|
"launch manual selection in opened database"
|
||||||
|
)
|
||||||
|
GroupActivity.launchForPasskeySelectionResult(
|
||||||
|
context = this,
|
||||||
|
database = openedDatabase,
|
||||||
|
activityResultLauncher = mPasskeySelectionActivityResultLauncher,
|
||||||
|
searchInfo = null,
|
||||||
|
autoSearch = false
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
Log.d(TAG, "Manual passkey selection in closed database")
|
||||||
|
FileDatabaseSelectActivity.launchForPasskeySelectionResult(
|
||||||
|
activity = this,
|
||||||
|
activityResultLauncher = mPasskeySelectionActivityResultLauncher,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun autoRegisterPasskeyAndSetResult(
|
||||||
|
database: ContextualDatabase?,
|
||||||
|
nodeId: UUID,
|
||||||
|
passkey: Passkey
|
||||||
|
) {
|
||||||
|
// TODO Overwrite and Register in a predefined group
|
||||||
|
mCreationParameters?.let { creationParameters ->
|
||||||
|
// To set the passkey to the database
|
||||||
|
setResult(RESULT_OK)
|
||||||
|
finish()
|
||||||
|
} ?: run {
|
||||||
|
Log.e(TAG, "Unable to auto select passkey, usage parameters are empty")
|
||||||
|
setResult(RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun launchRegistration(
|
||||||
|
database: ContextualDatabase?,
|
||||||
|
nodeId: UUID?,
|
||||||
|
searchInfo: SearchInfo
|
||||||
|
) {
|
||||||
|
Log.d(TAG, "Launch passkey registration")
|
||||||
|
retrievePasskeyCreationRequestParameters(
|
||||||
|
intent = intent,
|
||||||
|
assetManager = assets,
|
||||||
|
passkeyCreated = { passkey, appInfoToStore, publicKeyCredentialParameters ->
|
||||||
|
// Save the requested parameters
|
||||||
|
mPasskey = passkey
|
||||||
|
mCreationParameters = publicKeyCredentialParameters
|
||||||
|
// Manage the passkey and create a register info
|
||||||
|
val registerInfo = RegisterInfo(
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
passkey = passkey,
|
||||||
|
appOrigin = appInfoToStore
|
||||||
|
)
|
||||||
|
// If nodeId already provided
|
||||||
|
nodeId?.let { nodeId ->
|
||||||
|
autoRegisterPasskeyAndSetResult(database, nodeId, passkey)
|
||||||
|
} ?: run {
|
||||||
|
SearchHelper.checkAutoSearchInfo(
|
||||||
|
context = this,
|
||||||
|
database = database,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { openedDatabase, _ ->
|
||||||
|
Log.w(TAG, "Passkey found for registration, " +
|
||||||
|
"but launch manual registration for a new entry")
|
||||||
|
GroupActivity.launchForRegistration(
|
||||||
|
context = this,
|
||||||
|
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
|
||||||
|
database = openedDatabase,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.PASSKEY
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onItemNotFound = { openedDatabase ->
|
||||||
|
Log.d(TAG, "Launch new manual registration in opened database")
|
||||||
|
GroupActivity.launchForRegistration(
|
||||||
|
context = this,
|
||||||
|
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
|
||||||
|
database = openedDatabase,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.PASSKEY
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
Log.d(TAG, "Manual passkey registration in closed database")
|
||||||
|
FileDatabaseSelectActivity.launchForRegistration(
|
||||||
|
context = this,
|
||||||
|
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.PASSKEY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showError(e: Throwable) {
|
||||||
|
Toast.makeText(this, e.localizedMessage, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = PasskeyLauncherActivity::class.java.name
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a pending intent to launch the passkey launcher activity
|
||||||
|
* [nodeId] can be :
|
||||||
|
* - null if manual selection is requested
|
||||||
|
* - null if manual registration is requested
|
||||||
|
* - an entry node id if direct selection is requested
|
||||||
|
* - a group node id if direct registration is requested in a default group
|
||||||
|
* - an entry node id if overwriting is requested in an existing entry
|
||||||
|
*/
|
||||||
|
fun getPendingIntent(
|
||||||
|
context: Context,
|
||||||
|
specialMode: SpecialMode,
|
||||||
|
searchInfo: SearchInfo? = null,
|
||||||
|
appOrigin: AppOrigin? = null,
|
||||||
|
nodeId: UUID? = null
|
||||||
|
): PendingIntent? {
|
||||||
|
return PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
(Math.random() * Integer.MAX_VALUE).toInt(),
|
||||||
|
Intent(context, PasskeyLauncherActivity::class.java).apply {
|
||||||
|
addSpecialMode(specialMode)
|
||||||
|
addTypeMode(TypeMode.PASSKEY)
|
||||||
|
addSearchInfo(searchInfo)
|
||||||
|
addAppOrigin(appOrigin)
|
||||||
|
addNodeId(nodeId)
|
||||||
|
addAuthCode(nodeId)
|
||||||
|
},
|
||||||
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.credentialprovider.autofill
|
||||||
|
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.credentialprovider.autofill
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
@@ -40,17 +40,13 @@ import android.view.autofill.AutofillValue
|
|||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import android.widget.inline.InlinePresentationSpec
|
import android.widget.inline.InlinePresentationSpec
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.autofill.inline.UiVersions
|
import androidx.autofill.inline.UiVersions
|
||||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildIcon
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
import com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity
|
||||||
import com.kunzisoft.keepass.activities.helpers.SpecialMode
|
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.element.icon.IconImage
|
import com.kunzisoft.keepass.database.element.icon.IconImage
|
||||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
@@ -58,7 +54,6 @@ import com.kunzisoft.keepass.model.EntryInfo
|
|||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
import com.kunzisoft.keepass.settings.AutofillSettingsActivity
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@@ -263,7 +258,7 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (field in entryInfo.customFields) {
|
for (field in entryInfo.getCustomFieldsForFilling()) {
|
||||||
if (field.name == TemplateField.LABEL_HOLDER) {
|
if (field.name == TemplateField.LABEL_HOLDER) {
|
||||||
struct.creditCardHolderId?.let { ccNameId ->
|
struct.creditCardHolderId?.let { ccNameId ->
|
||||||
datasetBuilder.addValueToDatasetBuilder(
|
datasetBuilder.addValueToDatasetBuilder(
|
||||||
@@ -294,23 +289,6 @@ object AutofillHelper {
|
|||||||
return dataset
|
return dataset
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to assign a drawable to a new icon from a database icon
|
|
||||||
*/
|
|
||||||
private fun buildIconFromEntry(context: Context,
|
|
||||||
database: ContextualDatabase,
|
|
||||||
entryInfo: EntryInfo): Icon? {
|
|
||||||
try {
|
|
||||||
database.iconDrawableFactory.getBitmapFromIcon(context,
|
|
||||||
entryInfo.icon, ContextCompat.getColor(context, R.color.green))?.let { bitmap ->
|
|
||||||
return Icon.createWithBitmap(bitmap)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(RemoteViews::class.java.name, "Unable to assign icon in remote view", e)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
private fun buildInlinePresentationForEntry(context: Context,
|
private fun buildInlinePresentationForEntry(context: Context,
|
||||||
@@ -353,7 +331,7 @@ object AutofillHelper {
|
|||||||
Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
Icon.createWithResource(context, R.mipmap.ic_launcher_round).apply {
|
||||||
setTintBlendMode(BlendMode.DST)
|
setTintBlendMode(BlendMode.DST)
|
||||||
})
|
})
|
||||||
buildIconFromEntry(context, database, entryInfo)?.let { icon ->
|
entryInfo.buildIcon(context, database)?.let { icon ->
|
||||||
setEndIcon(icon.apply {
|
setEndIcon(icon.apply {
|
||||||
setTintBlendMode(BlendMode.DST)
|
setTintBlendMode(BlendMode.DST)
|
||||||
})
|
})
|
||||||
@@ -534,7 +512,9 @@ object AutofillHelper {
|
|||||||
StructureParser(structure).parse()?.let { result ->
|
StructureParser(structure).parse()?.let { result ->
|
||||||
// New Response
|
// New Response
|
||||||
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
val response = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtraCompat<CompatInlineSuggestionsRequest>(EXTRA_INLINE_SUGGESTIONS_REQUEST)
|
val compatInlineSuggestionsRequest = activity.intent?.getParcelableExtraCompat<CompatInlineSuggestionsRequest>(
|
||||||
|
EXTRA_INLINE_SUGGESTIONS_REQUEST
|
||||||
|
)
|
||||||
if (compatInlineSuggestionsRequest != null) {
|
if (compatInlineSuggestionsRequest != null) {
|
||||||
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
Toast.makeText(activity.applicationContext, R.string.autofill_inline_suggestions_keyboard, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
@@ -558,45 +538,14 @@ object AutofillHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildActivityResultLauncher(activity: AppCompatActivity,
|
fun Intent.addAutofillComponent(context: Context, autofillComponent: AutofillComponent) {
|
||||||
lockDatabase: Boolean = false): ActivityResultLauncher<Intent> {
|
this.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
|
||||||
return activity.registerForActivityResult(
|
|
||||||
ActivityResultContracts.StartActivityForResult()
|
|
||||||
) {
|
|
||||||
// Utility method to loop and close each activity with return data
|
|
||||||
if (it.resultCode == Activity.RESULT_OK) {
|
|
||||||
activity.setResult(it.resultCode, it.data)
|
|
||||||
}
|
|
||||||
if (it.resultCode == Activity.RESULT_CANCELED) {
|
|
||||||
activity.setResult(Activity.RESULT_CANCELED)
|
|
||||||
}
|
|
||||||
activity.finish()
|
|
||||||
|
|
||||||
if (lockDatabase && PreferencesUtil.isAutofillCloseDatabaseEnable(activity)) {
|
|
||||||
// Close the database
|
|
||||||
activity.sendBroadcast(Intent(LOCK_ACTION))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility method to start an activity with an Autofill for result
|
|
||||||
*/
|
|
||||||
fun startActivityForAutofillResult(activity: AppCompatActivity,
|
|
||||||
intent: Intent,
|
|
||||||
activityResultLauncher: ActivityResultLauncher<Intent>?,
|
|
||||||
autofillComponent: AutofillComponent,
|
|
||||||
searchInfo: SearchInfo?) {
|
|
||||||
EntrySelectionHelper.addSpecialModeInIntent(intent, SpecialMode.SELECTION)
|
|
||||||
intent.putExtra(EXTRA_ASSIST_STRUCTURE, autofillComponent.assistStructure)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(activity)) {
|
&& PreferencesUtil.isAutofillInlineSuggestionsEnable(context)) {
|
||||||
autofillComponent.compatInlineSuggestionsRequest?.let {
|
autofillComponent.compatInlineSuggestionsRequest?.let {
|
||||||
intent.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
this.putExtra(EXTRA_INLINE_SUGGESTIONS_REQUEST, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EntrySelectionHelper.addSearchInfoInIntent(intent, searchInfo)
|
|
||||||
activityResultLauncher?.launch(intent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val TAG = AutofillHelper::class.java.name
|
private val TAG = AutofillHelper::class.java.name
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.credentialprovider.autofill
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
import android.annotation.TargetApi
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.credentialprovider.autofill
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
@@ -43,8 +43,8 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.autofill.inline.UiVersions
|
import androidx.autofill.inline.UiVersions
|
||||||
import androidx.autofill.inline.v1.InlineSuggestionUi
|
import androidx.autofill.inline.v1.InlineSuggestionUi
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.AutofillLauncherActivity
|
import com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity
|
||||||
import com.kunzisoft.keepass.autofill.StructureParser.Companion.APPLICATION_ID_POPUP_WINDOW
|
import com.kunzisoft.keepass.credentialprovider.autofill.StructureParser.Companion.APPLICATION_ID_POPUP_WINDOW
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
@@ -143,21 +143,24 @@ class KeeAutofillService : AutofillService() {
|
|||||||
parseResult: StructureParser.Result,
|
parseResult: StructureParser.Result,
|
||||||
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
inlineSuggestionsRequest: CompatInlineSuggestionsRequest?,
|
||||||
callback: FillCallback) {
|
callback: FillCallback) {
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(
|
||||||
database,
|
context = this,
|
||||||
searchInfo,
|
database = database,
|
||||||
{ openedDatabase, items ->
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { openedDatabase, items ->
|
||||||
callback.onSuccess(
|
callback.onSuccess(
|
||||||
AutofillHelper.buildResponse(this, openedDatabase,
|
AutofillHelper.buildResponse(
|
||||||
items, parseResult, inlineSuggestionsRequest)
|
this, openedDatabase,
|
||||||
|
items, parseResult, inlineSuggestionsRequest
|
||||||
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{ openedDatabase ->
|
onItemNotFound = { openedDatabase ->
|
||||||
// Show UI if no search result
|
// Show UI if no search result
|
||||||
showUIForEntrySelection(parseResult, openedDatabase,
|
showUIForEntrySelection(parseResult, openedDatabase,
|
||||||
searchInfo, inlineSuggestionsRequest, callback)
|
searchInfo, inlineSuggestionsRequest, callback)
|
||||||
},
|
},
|
||||||
{
|
onDatabaseClosed = {
|
||||||
// Show UI if database not open
|
// Show UI if database not open
|
||||||
showUIForEntrySelection(parseResult, null,
|
showUIForEntrySelection(parseResult, null,
|
||||||
searchInfo, inlineSuggestionsRequest, callback)
|
searchInfo, inlineSuggestionsRequest, callback)
|
||||||
@@ -385,19 +388,21 @@ class KeeAutofillService : AutofillService() {
|
|||||||
|
|
||||||
// Show UI to save data
|
// Show UI to save data
|
||||||
val registerInfo = RegisterInfo(
|
val registerInfo = RegisterInfo(
|
||||||
SearchInfo().apply {
|
searchInfo = SearchInfo().apply {
|
||||||
applicationId = parseResult.applicationId
|
applicationId = parseResult.applicationId
|
||||||
webDomain = parseResult.webDomain
|
webDomain = parseResult.webDomain
|
||||||
webScheme = parseResult.webScheme
|
webScheme = parseResult.webScheme
|
||||||
},
|
},
|
||||||
parseResult.usernameValue?.textValue?.toString(),
|
username = parseResult.usernameValue?.textValue?.toString(),
|
||||||
parseResult.passwordValue?.textValue?.toString(),
|
password = parseResult.passwordValue?.textValue?.toString(),
|
||||||
|
creditCard =
|
||||||
CreditCard(
|
CreditCard(
|
||||||
parseResult.creditCardHolder,
|
parseResult.creditCardHolder,
|
||||||
parseResult.creditCardNumber,
|
parseResult.creditCardNumber,
|
||||||
expiration,
|
expiration,
|
||||||
parseResult.cardVerificationValue
|
parseResult.cardVerificationValue
|
||||||
))
|
)
|
||||||
|
)
|
||||||
|
|
||||||
// TODO Callback in each activity #765
|
// TODO Callback in each activity #765
|
||||||
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.autofill
|
package com.kunzisoft.keepass.credentialprovider.autofill
|
||||||
|
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
* the License.
|
* the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.kunzisoft.keepass.magikeyboard;
|
package com.kunzisoft.keepass.credentialprovider.magikeyboard;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
@@ -14,14 +14,14 @@
|
|||||||
* the License.
|
* the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.kunzisoft.keepass.magikeyboard;
|
package com.kunzisoft.keepass.credentialprovider.magikeyboard;
|
||||||
|
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_BACK_KEYBOARD;
|
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_BACK_KEYBOARD;
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_CHANGE_KEYBOARD;
|
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_CHANGE_KEYBOARD;
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_ENTRY;
|
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_ENTRY;
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_ENTRY_ALT;
|
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_ENTRY_ALT;
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP;
|
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_OTP;
|
||||||
import static com.kunzisoft.keepass.magikeyboard.MagikeyboardService.KEY_OTP_ALT;
|
import static com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService.KEY_OTP_ALT;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
@@ -52,7 +52,7 @@ import android.widget.TextView;
|
|||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
import com.kunzisoft.keepass.R;
|
import com.kunzisoft.keepass.R;
|
||||||
import com.kunzisoft.keepass.magikeyboard.Keyboard.Key;
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.Keyboard.Key;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.kunzisoft.keepass.magikeyboard
|
package com.kunzisoft.keepass.credentialprovider.magikeyboard
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -41,9 +41,9 @@ import androidx.core.graphics.BlendModeCompat
|
|||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity
|
|
||||||
import com.kunzisoft.keepass.activities.helpers.EntrySelectionHelper
|
|
||||||
import com.kunzisoft.keepass.adapters.FieldsAdapter
|
import com.kunzisoft.keepass.adapters.FieldsAdapter
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.element.Field
|
import com.kunzisoft.keepass.database.element.Field
|
||||||
@@ -324,9 +324,9 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
actionGoAutomatically()
|
actionGoAutomatically()
|
||||||
}
|
}
|
||||||
KEY_FIELDS -> {
|
KEY_FIELDS -> {
|
||||||
getEntryInfo()?.customFields?.let { customFields ->
|
getEntryInfo()?.getCustomFieldsForFilling()?.let { customFields ->
|
||||||
fieldsAdapter?.apply {
|
fieldsAdapter?.apply {
|
||||||
setFields(customFields.filter { it.name != OTP_TOKEN_FIELD})
|
setFields(customFields)
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -341,10 +341,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun actionKeyEntry(searchInfo: SearchInfo? = null) {
|
private fun actionKeyEntry(searchInfo: SearchInfo? = null) {
|
||||||
SearchHelper.checkAutoSearchInfo(this,
|
SearchHelper.checkAutoSearchInfo(
|
||||||
mDatabase,
|
context = this,
|
||||||
searchInfo,
|
database = mDatabase,
|
||||||
{ _, items ->
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { _, items ->
|
||||||
performSelection(
|
performSelection(
|
||||||
items,
|
items,
|
||||||
{
|
{
|
||||||
@@ -361,11 +362,11 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
onItemNotFound = {
|
||||||
// Select if not found
|
// Select if not found
|
||||||
launchEntrySelection(searchInfo)
|
launchEntrySelection(searchInfo)
|
||||||
},
|
},
|
||||||
{
|
onDatabaseClosed = {
|
||||||
// Select if database not opened
|
// Select if database not opened
|
||||||
removeEntryInfo()
|
removeEntryInfo()
|
||||||
launchEntrySelection(searchInfo)
|
launchEntrySelection(searchInfo)
|
||||||
@@ -463,21 +464,18 @@ class MagikeyboardService : InputMethodService(), KeyboardView.OnKeyboardActionL
|
|||||||
fun performSelection(items: List<EntryInfo>,
|
fun performSelection(items: List<EntryInfo>,
|
||||||
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
|
actionPopulateKeyboard: (entryInfo: EntryInfo) -> Unit,
|
||||||
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
|
actionEntrySelection: (autoSearch: Boolean) -> Unit) {
|
||||||
if (items.size == 1) {
|
EntrySelectionHelper.performSelection(
|
||||||
val itemFound = items[0]
|
items = items,
|
||||||
|
actionPopulateCredentialProvider = { itemFound ->
|
||||||
if (entryUUID != itemFound.id) {
|
if (entryUUID != itemFound.id) {
|
||||||
actionPopulateKeyboard.invoke(itemFound)
|
actionPopulateKeyboard.invoke(itemFound)
|
||||||
} else {
|
} else {
|
||||||
// Force selection if magikeyboard already populated
|
// Force selection if magikeyboard already populated
|
||||||
actionEntrySelection.invoke(false)
|
actionEntrySelection.invoke(false)
|
||||||
}
|
}
|
||||||
} else if (items.size > 1) {
|
},
|
||||||
// Select the one we want in the selection
|
actionEntrySelection = actionEntrySelection
|
||||||
actionEntrySelection.invoke(true)
|
)
|
||||||
} else {
|
|
||||||
// Select an arbitrary one
|
|
||||||
actionEntrySelection.invoke(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
fun populateKeyboardAndMoveAppToBackground(activity: Activity,
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey
|
||||||
|
|
||||||
|
import android.graphics.BlendMode
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.CancellationSignal
|
||||||
|
import android.os.OutcomeReceiver
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.credentials.exceptions.ClearCredentialException
|
||||||
|
import androidx.credentials.exceptions.CreateCredentialException
|
||||||
|
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
||||||
|
import androidx.credentials.exceptions.GetCredentialException
|
||||||
|
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||||
|
import androidx.credentials.provider.BeginCreateCredentialRequest
|
||||||
|
import androidx.credentials.provider.BeginCreateCredentialResponse
|
||||||
|
import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
|
||||||
|
import androidx.credentials.provider.BeginGetCredentialRequest
|
||||||
|
import androidx.credentials.provider.BeginGetCredentialResponse
|
||||||
|
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||||
|
import androidx.credentials.provider.CreateEntry
|
||||||
|
import androidx.credentials.provider.CredentialEntry
|
||||||
|
import androidx.credentials.provider.CredentialProviderService
|
||||||
|
import androidx.credentials.provider.ProviderClearCredentialStateRequest
|
||||||
|
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildIcon
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
|
||||||
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||||
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
class PasskeyProviderService : CredentialProviderService() {
|
||||||
|
|
||||||
|
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
||||||
|
private var mDatabase: ContextualDatabase? = null
|
||||||
|
private lateinit var defaultIcon: Icon
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
mDatabaseTaskProvider = DatabaseTaskProvider(this)
|
||||||
|
mDatabaseTaskProvider?.registerProgressTask()
|
||||||
|
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
|
||||||
|
this.mDatabase = database
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultIcon = Icon.createWithResource(
|
||||||
|
this@PasskeyProviderService,
|
||||||
|
R.mipmap.ic_launcher_round
|
||||||
|
).apply {
|
||||||
|
setTintBlendMode(BlendMode.DST)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
mDatabaseTaskProvider?.unregisterProgressTask()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildPasskeySearchInfo(relyingParty: String): SearchInfo {
|
||||||
|
return SearchInfo().apply {
|
||||||
|
this.relyingParty = relyingParty
|
||||||
|
this.isAPasskeySearch = true
|
||||||
|
this.query = relyingParty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBeginGetCredentialRequest(
|
||||||
|
request: BeginGetCredentialRequest,
|
||||||
|
cancellationSignal: CancellationSignal,
|
||||||
|
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
|
||||||
|
) {
|
||||||
|
Log.d(javaClass.simpleName, "onBeginGetCredentialRequest called")
|
||||||
|
try {
|
||||||
|
processGetCredentialsRequest(request)?.let { response ->
|
||||||
|
callback.onResult(response)
|
||||||
|
} ?: run {
|
||||||
|
callback.onError(GetCredentialUnknownException())
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(javaClass.simpleName, "onBeginGetCredentialRequest error", e)
|
||||||
|
callback.onError(GetCredentialUnknownException())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processGetCredentialsRequest(request: BeginGetCredentialRequest): BeginGetCredentialResponse? {
|
||||||
|
val credentialEntries: MutableList<CredentialEntry> = mutableListOf()
|
||||||
|
|
||||||
|
for (option in request.beginGetCredentialOptions) {
|
||||||
|
when (option) {
|
||||||
|
is BeginGetPublicKeyCredentialOption -> {
|
||||||
|
credentialEntries.addAll(
|
||||||
|
populatePasskeyData(option)
|
||||||
|
)
|
||||||
|
return BeginGetCredentialResponse(credentialEntries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.w(javaClass.simpleName, "unknown beginGetCredentialOption")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun populatePasskeyData(option: BeginGetPublicKeyCredentialOption): List<CredentialEntry> {
|
||||||
|
|
||||||
|
val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
|
||||||
|
|
||||||
|
val relyingPartyId = PublicKeyCredentialRequestOptions(option.requestJson).rpId
|
||||||
|
val searchInfo = buildPasskeySearchInfo(relyingPartyId)
|
||||||
|
Log.d(TAG, "Build passkey search for relying party $relyingPartyId")
|
||||||
|
SearchHelper.checkAutoSearchInfo(
|
||||||
|
context = this,
|
||||||
|
database = mDatabase,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { database, items ->
|
||||||
|
Log.d(TAG, "Add pending intent for passkey selection with found items")
|
||||||
|
for (passkeyEntry in items) {
|
||||||
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.SELECTION,
|
||||||
|
nodeId = passkeyEntry.id,
|
||||||
|
appOrigin = passkeyEntry.appOrigin
|
||||||
|
)?.let { usagePendingIntent ->
|
||||||
|
val passkey = passkeyEntry.passkey
|
||||||
|
passkeyEntries.add(
|
||||||
|
PublicKeyCredentialEntry(
|
||||||
|
context = applicationContext,
|
||||||
|
username = passkey?.username ?: "Unknown",
|
||||||
|
icon = passkeyEntry.buildIcon(this@PasskeyProviderService, database)?.apply {
|
||||||
|
setTintBlendMode(BlendMode.DST)
|
||||||
|
} ?: defaultIcon,
|
||||||
|
pendingIntent = usagePendingIntent,
|
||||||
|
beginGetPublicKeyCredentialOption = option,
|
||||||
|
displayName = passkeyEntry.getVisualTitle(),
|
||||||
|
isAutoSelectAllowed = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onItemNotFound = { _ ->
|
||||||
|
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId")
|
||||||
|
Log.d(TAG, "Add pending intent for passkey selection in opened database")
|
||||||
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.SELECTION,
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)?.let { pendingIntent ->
|
||||||
|
passkeyEntries.add(
|
||||||
|
PublicKeyCredentialEntry(
|
||||||
|
context = applicationContext,
|
||||||
|
username = getString(R.string.passkey_locked_database_username),
|
||||||
|
displayName = getString(R.string.passkey_selection_description),
|
||||||
|
icon = defaultIcon,
|
||||||
|
pendingIntent = pendingIntent,
|
||||||
|
beginGetPublicKeyCredentialOption = option,
|
||||||
|
lastUsedTime = Instant.now(),
|
||||||
|
isAutoSelectAllowed = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
Log.d(TAG, "Add pending intent for passkey selection in closed database")
|
||||||
|
// Database is locked, a public key credential entry is shown to unlock it
|
||||||
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.SELECTION,
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)?.let { pendingIntent ->
|
||||||
|
passkeyEntries.add(
|
||||||
|
PublicKeyCredentialEntry(
|
||||||
|
context = applicationContext,
|
||||||
|
username = getString(R.string.passkey_locked_database_username),
|
||||||
|
displayName = getString(R.string.passkey_locked_database_description),
|
||||||
|
icon = defaultIcon,
|
||||||
|
pendingIntent = pendingIntent,
|
||||||
|
beginGetPublicKeyCredentialOption = option,
|
||||||
|
lastUsedTime = Instant.now(),
|
||||||
|
isAutoSelectAllowed = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return passkeyEntries
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBeginCreateCredentialRequest(
|
||||||
|
request: BeginCreateCredentialRequest,
|
||||||
|
cancellationSignal: CancellationSignal,
|
||||||
|
callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>,
|
||||||
|
) {
|
||||||
|
Log.d(javaClass.simpleName, "onBeginCreateCredentialRequest called")
|
||||||
|
try {
|
||||||
|
processCreateCredentialRequest(request)?.let { response ->
|
||||||
|
callback.onResult(response)
|
||||||
|
} ?: let {
|
||||||
|
callback.onError(CreateCredentialUnknownException())
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(javaClass.simpleName, "onBeginCreateCredentialRequest error", e)
|
||||||
|
callback.onError(CreateCredentialUnknownException())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processCreateCredentialRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse? {
|
||||||
|
when (request) {
|
||||||
|
is BeginCreatePublicKeyCredentialRequest -> {
|
||||||
|
// Request is passkey type
|
||||||
|
return handleCreatePasskeyQuery(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// request type not supported
|
||||||
|
Log.w(javaClass.simpleName, "unknown type of BeginCreateCredentialRequest")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun MutableList<CreateEntry>.addPendingIntentCreationNewEntry(
|
||||||
|
accountName: String,
|
||||||
|
searchInfo: SearchInfo?
|
||||||
|
) {
|
||||||
|
Log.d(TAG, "Add pending intent for registration in opened database to create new item")
|
||||||
|
// TODO add a setting to directly store in a specific group
|
||||||
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.REGISTRATION,
|
||||||
|
searchInfo = searchInfo
|
||||||
|
)?.let { pendingIntent ->
|
||||||
|
this.add(
|
||||||
|
CreateEntry(
|
||||||
|
accountName = accountName,
|
||||||
|
icon = defaultIcon,
|
||||||
|
pendingIntent = pendingIntent,
|
||||||
|
description = getString(R.string.passkey_creation_description)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCreatePasskeyQuery(request: BeginCreatePublicKeyCredentialRequest): BeginCreateCredentialResponse {
|
||||||
|
|
||||||
|
val accountName = mDatabase?.name ?: getString(R.string.passkey_locked_database_username)
|
||||||
|
val createEntries: MutableList<CreateEntry> = mutableListOf()
|
||||||
|
val relyingPartyId = PublicKeyCredentialCreationOptions(
|
||||||
|
requestJson = request.requestJson,
|
||||||
|
clientDataHash = request.clientDataHash
|
||||||
|
).relyingPartyEntity.id
|
||||||
|
val searchInfo = buildPasskeySearchInfo(relyingPartyId)
|
||||||
|
Log.d(TAG, "Build passkey search for relying party $relyingPartyId")
|
||||||
|
SearchHelper.checkAutoSearchInfo(
|
||||||
|
context = this,
|
||||||
|
database = mDatabase,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
onItemsFound = { database, items ->
|
||||||
|
if (database.isReadOnly) {
|
||||||
|
throw CreateCredentialUnknownException(
|
||||||
|
"Unable to register or overwrite a passkey in a database that is read only"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// To create a new entry
|
||||||
|
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
|
||||||
|
/* TODO Overwrite
|
||||||
|
// To select an existing entry and permit an overwrite
|
||||||
|
Log.w(TAG, "Passkey already registered")
|
||||||
|
for (entryInfo in items) {
|
||||||
|
PasskeyHelper.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.REGISTRATION,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
passkeyEntryNodeId = entryInfo.id
|
||||||
|
)?.let { createPendingIntent ->
|
||||||
|
createEntries.add(
|
||||||
|
CreateEntry(
|
||||||
|
accountName = accountName,
|
||||||
|
pendingIntent = createPendingIntent,
|
||||||
|
description = getString(
|
||||||
|
R.string.passkey_update_description,
|
||||||
|
entryInfo.passkey?.displayName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onItemNotFound = { database ->
|
||||||
|
// To create a new entry
|
||||||
|
if (database.isReadOnly) {
|
||||||
|
throw CreateCredentialUnknownException(
|
||||||
|
"Unable to register a new passkey in a database that is read only"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
createEntries.addPendingIntentCreationNewEntry(accountName, searchInfo)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
// Launch the passkey launcher activity to open the database
|
||||||
|
Log.d(TAG, "Add pending intent for passkey registration in closed database")
|
||||||
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.REGISTRATION
|
||||||
|
)?.let { pendingIntent ->
|
||||||
|
createEntries.add(
|
||||||
|
CreateEntry(
|
||||||
|
accountName = accountName,
|
||||||
|
icon = defaultIcon,
|
||||||
|
pendingIntent = pendingIntent,
|
||||||
|
description = getString(R.string.passkey_locked_database_description)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return BeginCreateCredentialResponse(createEntries)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClearCredentialStateRequest(
|
||||||
|
request: ProviderClearCredentialStateRequest,
|
||||||
|
cancellationSignal: CancellationSignal,
|
||||||
|
callback: OutcomeReceiver<Void?, ClearCredentialException>
|
||||||
|
) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = PasskeyProviderService::class.java.simpleName
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||||
|
import com.kunzisoft.encrypt.Signature
|
||||||
|
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class AuthenticatorAssertionResponse(
|
||||||
|
private val requestOptions: PublicKeyCredentialRequestOptions,
|
||||||
|
private val userPresent: Boolean,
|
||||||
|
private val userVerified: Boolean,
|
||||||
|
private val backupEligibility: Boolean,
|
||||||
|
private val backupState: Boolean,
|
||||||
|
private var userHandle: String,
|
||||||
|
privateKey: String,
|
||||||
|
private val clientDataResponse: ClientDataResponse,
|
||||||
|
) : AuthenticatorResponse {
|
||||||
|
|
||||||
|
override var clientJson = JSONObject()
|
||||||
|
private var authenticatorData: ByteArray = AuthenticatorData.buildAuthenticatorData(
|
||||||
|
relyingPartyId = requestOptions.rpId.toByteArray(),
|
||||||
|
userPresent = userPresent,
|
||||||
|
userVerified = userVerified,
|
||||||
|
backupEligibility = backupEligibility,
|
||||||
|
backupState = backupState
|
||||||
|
)
|
||||||
|
private var signature: ByteArray = byteArrayOf()
|
||||||
|
|
||||||
|
init {
|
||||||
|
signature = Signature.sign(privateKey, dataToSign())
|
||||||
|
?: throw GetCredentialUnknownException("signing failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dataToSign(): ByteArray {
|
||||||
|
return authenticatorData + clientDataResponse.hashData()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun json(): JSONObject {
|
||||||
|
// https://www.w3.org/TR/webauthn-3/#authdata-flags
|
||||||
|
return clientJson.apply {
|
||||||
|
put("clientDataJSON", clientDataResponse.buildResponse())
|
||||||
|
put("authenticatorData", b64Encode(authenticatorData))
|
||||||
|
put("signature", b64Encode(signature))
|
||||||
|
put("userHandle", userHandle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
|
||||||
|
import com.kunzisoft.keepass.utils.UUIDUtils.asBytes
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
class AuthenticatorAttestationResponse(
|
||||||
|
private val requestOptions: PublicKeyCredentialCreationOptions,
|
||||||
|
private val credentialId: ByteArray,
|
||||||
|
private val credentialPublicKey: ByteArray,
|
||||||
|
private val userPresent: Boolean,
|
||||||
|
private val userVerified: Boolean,
|
||||||
|
private val backupEligibility: Boolean,
|
||||||
|
private val backupState: Boolean,
|
||||||
|
private val publicKeyTypeId: Long,
|
||||||
|
private val publicKeyCbor: ByteArray,
|
||||||
|
private val clientDataResponse: ClientDataResponse,
|
||||||
|
) : AuthenticatorResponse {
|
||||||
|
|
||||||
|
override var clientJson = JSONObject()
|
||||||
|
var attestationObject: ByteArray
|
||||||
|
|
||||||
|
init {
|
||||||
|
attestationObject = defaultAttestationObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildAuthData(): ByteArray {
|
||||||
|
return AuthenticatorData.buildAuthenticatorData(
|
||||||
|
relyingPartyId = requestOptions.relyingPartyEntity.id.toByteArray(),
|
||||||
|
userPresent = userPresent,
|
||||||
|
userVerified = userVerified,
|
||||||
|
backupEligibility = backupEligibility,
|
||||||
|
backupState = backupState,
|
||||||
|
attestedCredentialData = true
|
||||||
|
) + AAGUID +
|
||||||
|
//credIdLen
|
||||||
|
byteArrayOf((credentialId.size shr 8).toByte(), credentialId.size.toByte()) +
|
||||||
|
credentialId +
|
||||||
|
credentialPublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun defaultAttestationObject(): ByteArray {
|
||||||
|
// https://www.w3.org/TR/webauthn-3/#attestation-object
|
||||||
|
val ao = mutableMapOf<String, Any>()
|
||||||
|
ao.put("fmt", "none")
|
||||||
|
ao.put("attStmt", emptyMap<Any, Any>())
|
||||||
|
ao.put("authData", buildAuthData())
|
||||||
|
return Cbor().encode(ao)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun json(): JSONObject {
|
||||||
|
// See AuthenticatorAttestationResponseJSON at
|
||||||
|
// https://w3c.github.io/webauthn/#ref-for-dom-publickeycredential-tojson
|
||||||
|
return clientJson.apply {
|
||||||
|
put("clientDataJSON", clientDataResponse.buildResponse())
|
||||||
|
put("authenticatorData", b64Encode(buildAuthData()))
|
||||||
|
put("transports", JSONArray(listOf("internal", "hybrid")))
|
||||||
|
put("publicKey", b64Encode(publicKeyCbor))
|
||||||
|
put("publicKeyAlgorithm", publicKeyTypeId)
|
||||||
|
put("attestationObject", b64Encode(attestationObject))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Authenticator Attestation Global Unique Identifier
|
||||||
|
private val AAGUID: ByteArray = UUID.fromString("eaecdef2-1c31-5634-8639-f1cbd9c00a08").asBytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import com.kunzisoft.encrypt.HashManager
|
||||||
|
|
||||||
|
class AuthenticatorData {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun buildAuthenticatorData(
|
||||||
|
relyingPartyId: ByteArray,
|
||||||
|
userPresent: Boolean,
|
||||||
|
userVerified: Boolean,
|
||||||
|
backupEligibility: Boolean,
|
||||||
|
backupState: Boolean,
|
||||||
|
attestedCredentialData: Boolean = false
|
||||||
|
): ByteArray {
|
||||||
|
// https://www.w3.org/TR/webauthn-3/#table-authData
|
||||||
|
var flags = 0
|
||||||
|
if (userPresent)
|
||||||
|
flags = flags or 0x01
|
||||||
|
// bit at index 1 is reserved
|
||||||
|
if (userVerified)
|
||||||
|
flags = flags or 0x04
|
||||||
|
if (backupEligibility)
|
||||||
|
flags = flags or 0x08
|
||||||
|
if (backupState)
|
||||||
|
flags = flags or 0x10
|
||||||
|
// bit at index 5 is reserved
|
||||||
|
if (attestedCredentialData) {
|
||||||
|
flags = flags or 0x40
|
||||||
|
}
|
||||||
|
// bit at index 7: Extension data included == false
|
||||||
|
|
||||||
|
return HashManager.hashSha256(relyingPartyId) +
|
||||||
|
byteArrayOf(flags.toByte()) +
|
||||||
|
byteArrayOf(0, 0, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
interface AuthenticatorResponse {
|
||||||
|
var clientJson: JSONObject
|
||||||
|
|
||||||
|
fun json(): JSONObject
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import androidx.annotation.RestrictTo
|
||||||
|
|
||||||
|
@RestrictTo(RestrictTo.Scope.LIBRARY)
|
||||||
|
class Cbor {
|
||||||
|
data class Item(val item: Any, val len: Int)
|
||||||
|
|
||||||
|
data class Arg(val arg: Long, val len: Int)
|
||||||
|
|
||||||
|
val TYPE_UNSIGNED_INT = 0x00
|
||||||
|
val TYPE_NEGATIVE_INT = 0x01
|
||||||
|
val TYPE_BYTE_STRING = 0x02
|
||||||
|
val TYPE_TEXT_STRING = 0x03
|
||||||
|
val TYPE_ARRAY = 0x04
|
||||||
|
val TYPE_MAP = 0x05
|
||||||
|
val TYPE_TAG = 0x06
|
||||||
|
val TYPE_FLOAT = 0x07
|
||||||
|
|
||||||
|
fun decode(data: ByteArray): Any {
|
||||||
|
val ret = parseItem(data, 0)
|
||||||
|
return ret.item
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encode(data: Any): ByteArray {
|
||||||
|
if (data is Number) {
|
||||||
|
if (data is Double) {
|
||||||
|
throw IllegalArgumentException("Don't support doubles yet")
|
||||||
|
} else {
|
||||||
|
val value = data.toLong()
|
||||||
|
if (value >= 0) {
|
||||||
|
return createArg(TYPE_UNSIGNED_INT, value)
|
||||||
|
} else {
|
||||||
|
return createArg(TYPE_NEGATIVE_INT, -1 - value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (data is ByteArray) {
|
||||||
|
return createArg(TYPE_BYTE_STRING, data.size.toLong()) + data
|
||||||
|
}
|
||||||
|
if (data is String) {
|
||||||
|
return createArg(TYPE_TEXT_STRING, data.length.toLong()) + data.encodeToByteArray()
|
||||||
|
}
|
||||||
|
if (data is List<*>) {
|
||||||
|
var ret = createArg(TYPE_ARRAY, data.size.toLong())
|
||||||
|
for (i in data) {
|
||||||
|
ret += encode(i!!)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
if (data is Map<*, *>) {
|
||||||
|
// See:
|
||||||
|
// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#ctap2-canonical-cbor-encoding-form
|
||||||
|
var ret = createArg(TYPE_MAP, data.size.toLong())
|
||||||
|
var byteMap: MutableMap<ByteArray, ByteArray> = mutableMapOf()
|
||||||
|
for (i in data) {
|
||||||
|
// Convert to byte arrays so we can sort them.
|
||||||
|
byteMap.put(encode(i.key!!), encode(i.value!!))
|
||||||
|
}
|
||||||
|
|
||||||
|
var keysList = ArrayList<ByteArray>(byteMap.keys)
|
||||||
|
keysList.sortedWith(
|
||||||
|
Comparator<ByteArray> { a, b ->
|
||||||
|
// If two keys have different lengths, the shorter one sorts earlier;
|
||||||
|
// If two keys have the same length, the one with the lower value in (byte-wise)
|
||||||
|
// lexical order sorts earlier.
|
||||||
|
var aBytes = byteMap.get(a)!!
|
||||||
|
var bBytes = byteMap.get(b)!!
|
||||||
|
when {
|
||||||
|
a.size > b.size -> 1
|
||||||
|
a.size < b.size -> -1
|
||||||
|
aBytes.size > bBytes.size -> 1
|
||||||
|
aBytes.size < bBytes.size -> -1
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for (key in keysList) {
|
||||||
|
ret += key
|
||||||
|
ret += byteMap.get(key)!!
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException("Bad type")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getType(data: ByteArray, offset: Int): Int {
|
||||||
|
val d = data[offset].toInt()
|
||||||
|
return (d and 0xFF) shr 5
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getArg(data: ByteArray, offset: Int): Arg {
|
||||||
|
val arg = data[offset].toLong() and 0x1F
|
||||||
|
if (arg < 24) {
|
||||||
|
return Arg(arg, 1)
|
||||||
|
}
|
||||||
|
if (arg == 24L) {
|
||||||
|
return Arg(data[offset + 1].toLong() and 0xFF, 2)
|
||||||
|
}
|
||||||
|
if (arg == 25L) {
|
||||||
|
var ret = (data[offset + 1].toLong() and 0xFF) shl 8
|
||||||
|
ret = ret or (data[offset + 2].toLong() and 0xFF)
|
||||||
|
return Arg(ret, 3)
|
||||||
|
}
|
||||||
|
if (arg == 26L) {
|
||||||
|
var ret = (data[offset + 1].toLong() and 0xFF) shl 24
|
||||||
|
ret = ret or ((data[offset + 2].toLong() and 0xFF) shl 16)
|
||||||
|
ret = ret or ((data[offset + 3].toLong() and 0xFF) shl 8)
|
||||||
|
ret = ret or (data[offset + 4].toLong() and 0xFF)
|
||||||
|
return Arg(ret, 5)
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException("Bad arg")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseItem(data: ByteArray, offset: Int): Item {
|
||||||
|
val itemType = getType(data, offset)
|
||||||
|
val arg = getArg(data, offset)
|
||||||
|
println("Type $itemType ${arg.arg} ${arg.len}")
|
||||||
|
|
||||||
|
when (itemType) {
|
||||||
|
TYPE_UNSIGNED_INT -> {
|
||||||
|
return Item(arg.arg, arg.len)
|
||||||
|
}
|
||||||
|
TYPE_NEGATIVE_INT -> {
|
||||||
|
return Item(-1 - arg.arg, arg.len)
|
||||||
|
}
|
||||||
|
TYPE_BYTE_STRING -> {
|
||||||
|
val ret =
|
||||||
|
data.sliceArray(offset + arg.len.toInt() until offset + arg.len.toInt() + arg.arg.toInt())
|
||||||
|
return Item(ret, arg.len + arg.arg.toInt())
|
||||||
|
}
|
||||||
|
TYPE_TEXT_STRING -> {
|
||||||
|
val ret =
|
||||||
|
data.sliceArray(offset + arg.len.toInt() until offset + arg.len.toInt() + arg.arg.toInt())
|
||||||
|
return Item(ret.toString(Charsets.UTF_8), arg.len + arg.arg.toInt())
|
||||||
|
}
|
||||||
|
TYPE_ARRAY -> {
|
||||||
|
val ret = mutableListOf<Any>()
|
||||||
|
var consumed = arg.len
|
||||||
|
for (i in 0 until arg.arg.toInt()) {
|
||||||
|
val item = parseItem(data, offset + consumed)
|
||||||
|
ret.add(item.item)
|
||||||
|
consumed += item.len
|
||||||
|
}
|
||||||
|
return Item(ret.toList(), consumed)
|
||||||
|
}
|
||||||
|
TYPE_MAP -> {
|
||||||
|
val ret = mutableMapOf<Any, Any>()
|
||||||
|
var consumed = arg.len
|
||||||
|
for (i in 0 until arg.arg.toInt()) {
|
||||||
|
val key = parseItem(data, offset + consumed)
|
||||||
|
consumed += key.len
|
||||||
|
val value = parseItem(data, offset + consumed)
|
||||||
|
consumed += value.len
|
||||||
|
ret[key.item] = value.item
|
||||||
|
}
|
||||||
|
return Item(ret.toMap(), consumed)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw IllegalArgumentException("Bad type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createArg(type: Int, arg: Long): ByteArray {
|
||||||
|
val t = type shl 5
|
||||||
|
val a = arg.toInt()
|
||||||
|
if (arg < 24) {
|
||||||
|
return byteArrayOf(((t or a) and 0xFF).toByte())
|
||||||
|
}
|
||||||
|
if (arg <= 0xFF) {
|
||||||
|
return byteArrayOf(((t or 24) and 0xFF).toByte(), (a and 0xFF).toByte())
|
||||||
|
}
|
||||||
|
if (arg <= 0xFFFF) {
|
||||||
|
return byteArrayOf(
|
||||||
|
((t or 25) and 0xFF).toByte(),
|
||||||
|
((a shr 8) and 0xFF).toByte(),
|
||||||
|
(a and 0xFF).toByte()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (arg <= 0xFFFFFFFF) {
|
||||||
|
return byteArrayOf(
|
||||||
|
((t or 26) and 0xFF).toByte(),
|
||||||
|
((a shr 24) and 0xFF).toByte(),
|
||||||
|
((a shr 16) and 0xFF).toByte(),
|
||||||
|
((a shr 8) and 0xFF).toByte(),
|
||||||
|
(a and 0xFF).toByte()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException("bad Arg")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import com.kunzisoft.encrypt.HashManager
|
||||||
|
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
open class ClientDataBuildResponse(
|
||||||
|
type: Type,
|
||||||
|
challenge: ByteArray,
|
||||||
|
origin: String,
|
||||||
|
crossOrigin: Boolean? = false,
|
||||||
|
topOrigin: String? = null,
|
||||||
|
): AuthenticatorResponse, ClientDataResponse {
|
||||||
|
override var clientJson = JSONObject()
|
||||||
|
|
||||||
|
init {
|
||||||
|
// https://w3c.github.io/webauthn/#client-data
|
||||||
|
clientJson.put("type", type.value)
|
||||||
|
clientJson.put("challenge", b64Encode(challenge))
|
||||||
|
clientJson.put("origin", origin)
|
||||||
|
crossOrigin?.let {
|
||||||
|
clientJson.put("crossOrigin", it)
|
||||||
|
}
|
||||||
|
topOrigin?.let {
|
||||||
|
clientJson.put("topOrigin", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun json(): JSONObject {
|
||||||
|
return clientJson
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Type(val value: String) {
|
||||||
|
GET("webauthn.get"), CREATE("webauthn.create")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun buildResponse(): String {
|
||||||
|
return b64Encode(json().toString().toByteArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashData(): ByteArray {
|
||||||
|
return HashManager.hashSha256(json().toString().toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
open class ClientDataDefinedResponse(
|
||||||
|
private val clientDataHash: ByteArray
|
||||||
|
): ClientDataResponse {
|
||||||
|
|
||||||
|
override fun hashData(): ByteArray {
|
||||||
|
return clientDataHash
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun buildResponse(): String {
|
||||||
|
return CLIENT_DATA_JSON_PRIVILEGED
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val CLIENT_DATA_JSON_PRIVILEGED = "<placeholder>"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
interface ClientDataResponse {
|
||||||
|
fun hashData(): ByteArray
|
||||||
|
fun buildResponse(): String
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
data class PublicKeyCredentialRpEntity(val name: String, val id: String)
|
||||||
|
|
||||||
|
data class PublicKeyCredentialUserEntity(
|
||||||
|
val name: String,
|
||||||
|
val id: ByteArray,
|
||||||
|
val displayName: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PublicKeyCredentialParameters(val type: String, val alg: Long)
|
||||||
|
|
||||||
|
data class PublicKeyCredentialDescriptor(
|
||||||
|
val type: String,
|
||||||
|
val id: ByteArray,
|
||||||
|
val transports: List<String>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AuthenticatorSelectionCriteria(
|
||||||
|
val authenticatorAttachment: String,
|
||||||
|
val residentKey: String,
|
||||||
|
val requireResidentKey: Boolean = false,
|
||||||
|
val userVerification: String = "preferred"
|
||||||
|
)
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class FidoPublicKeyCredential(
|
||||||
|
val id: String,
|
||||||
|
val response: AuthenticatorResponse,
|
||||||
|
val authenticatorAttachment: String
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun json(): String {
|
||||||
|
// see at https://www.w3.org/TR/webauthn-3/#sctn-authenticator-credential-properties-extension
|
||||||
|
val discoverableCredential = true
|
||||||
|
val rk = JSONObject()
|
||||||
|
rk.put("rk", discoverableCredential)
|
||||||
|
val credProps = JSONObject()
|
||||||
|
credProps.put("credProps", rk)
|
||||||
|
|
||||||
|
// See RegistrationResponseJSON at
|
||||||
|
// https://w3c.github.io/webauthn/#ref-for-dom-publickeycredential-tojson
|
||||||
|
val ret = JSONObject()
|
||||||
|
ret.put("id", id)
|
||||||
|
ret.put("rawId", id)
|
||||||
|
ret.put("type", "public-key")
|
||||||
|
ret.put("authenticatorAttachment", authenticatorAttachment)
|
||||||
|
ret.put("response", response.json())
|
||||||
|
ret.put("clientExtensionResults", JSONObject()) // TODO credProps
|
||||||
|
|
||||||
|
return ret.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import com.kunzisoft.encrypt.Base64Helper
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class PublicKeyCredentialCreationOptions(
|
||||||
|
requestJson: String,
|
||||||
|
var clientDataHash: ByteArray?
|
||||||
|
) {
|
||||||
|
val json: JSONObject = JSONObject(requestJson)
|
||||||
|
|
||||||
|
val relyingPartyEntity: PublicKeyCredentialRpEntity
|
||||||
|
val userEntity: PublicKeyCredentialUserEntity
|
||||||
|
val challenge: ByteArray
|
||||||
|
val pubKeyCredParams: List<PublicKeyCredentialParameters>
|
||||||
|
|
||||||
|
var timeout: Long
|
||||||
|
var excludeCredentials: List<PublicKeyCredentialDescriptor>
|
||||||
|
var authenticatorSelection: AuthenticatorSelectionCriteria
|
||||||
|
var attestation: String
|
||||||
|
|
||||||
|
init {
|
||||||
|
val rpJson = json.getJSONObject("rp")
|
||||||
|
relyingPartyEntity = PublicKeyCredentialRpEntity(rpJson.getString("name"), rpJson.getString("id"))
|
||||||
|
val rpUser = json.getJSONObject("user")
|
||||||
|
val userId = Base64Helper.b64Decode(rpUser.getString("id"))
|
||||||
|
userEntity =
|
||||||
|
PublicKeyCredentialUserEntity(
|
||||||
|
rpUser.getString("name"),
|
||||||
|
userId,
|
||||||
|
rpUser.getString("displayName")
|
||||||
|
)
|
||||||
|
challenge = Base64Helper.b64Decode(json.getString("challenge"))
|
||||||
|
val pubKeyCredParamsJson = json.getJSONArray("pubKeyCredParams")
|
||||||
|
val pubKeyCredParamsTmp: MutableList<PublicKeyCredentialParameters> = mutableListOf()
|
||||||
|
for (i in 0 until pubKeyCredParamsJson.length()) {
|
||||||
|
val e = pubKeyCredParamsJson.getJSONObject(i)
|
||||||
|
pubKeyCredParamsTmp.add(
|
||||||
|
PublicKeyCredentialParameters(e.getString("type"), e.getLong("alg"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
pubKeyCredParams = pubKeyCredParamsTmp.toList()
|
||||||
|
|
||||||
|
timeout = json.optLong("timeout", 0)
|
||||||
|
// TODO: Fix excludeCredentials and authenticatorSelection
|
||||||
|
excludeCredentials = emptyList()
|
||||||
|
authenticatorSelection = AuthenticatorSelectionCriteria("platform", "required")
|
||||||
|
attestation = json.optString("attestation", "none")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = PublicKeyCredentialCreationOptions::class.simpleName
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import java.security.KeyPair
|
||||||
|
|
||||||
|
data class PublicKeyCredentialCreationParameters(
|
||||||
|
val publicKeyCredentialCreationOptions: PublicKeyCredentialCreationOptions,
|
||||||
|
val credentialId: ByteArray, // TODO Equals Hashcode
|
||||||
|
val signatureKey: Pair<KeyPair, Long>,
|
||||||
|
val clientDataResponse: ClientDataResponse
|
||||||
|
)
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import com.kunzisoft.encrypt.Base64Helper
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class PublicKeyCredentialRequestOptions(requestJson: String) {
|
||||||
|
val json: JSONObject = JSONObject(requestJson)
|
||||||
|
val challenge: ByteArray = Base64Helper.b64Decode(json.getString("challenge"))
|
||||||
|
val timeout: Long = json.optLong("timeout", 0)
|
||||||
|
val rpId: String = json.optString("rpId", "")
|
||||||
|
val userVerification: String = json.optString("userVerification", "preferred")
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.model.AppOrigin
|
||||||
|
|
||||||
|
data class PublicKeyCredentialUsageParameters(
|
||||||
|
val publicKeyCredentialRequestOptions: PublicKeyCredentialRequestOptions,
|
||||||
|
val clientDataResponse: ClientDataResponse,
|
||||||
|
val appOrigin: AppOrigin
|
||||||
|
)
|
||||||
@@ -0,0 +1,606 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.credentialprovider.passkey.util
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.res.AssetManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.ParcelUuid
|
||||||
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
|
import android.security.keystore.KeyProperties
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||||
|
import androidx.credentials.CreatePublicKeyCredentialResponse
|
||||||
|
import androidx.credentials.GetPublicKeyCredentialOption
|
||||||
|
import androidx.credentials.PublicKeyCredential
|
||||||
|
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
||||||
|
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||||
|
import androidx.credentials.provider.CallingAppInfo
|
||||||
|
import androidx.credentials.provider.PendingIntentHandler
|
||||||
|
import androidx.credentials.provider.ProviderCreateCredentialRequest
|
||||||
|
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||||
|
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
|
||||||
|
import com.kunzisoft.encrypt.HashManager.getApplicationFingerprints
|
||||||
|
import com.kunzisoft.encrypt.Signature
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataBuildResponse
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataDefinedResponse
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataResponse
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.FidoPublicKeyCredential
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
|
||||||
|
import com.kunzisoft.keepass.model.AndroidOrigin
|
||||||
|
import com.kunzisoft.keepass.model.AppOrigin
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import com.kunzisoft.keepass.model.Passkey
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
|
import com.kunzisoft.keepass.model.WebOrigin
|
||||||
|
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
||||||
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.security.KeyStore
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.UUID
|
||||||
|
import javax.crypto.KeyGenerator
|
||||||
|
import javax.crypto.Mac
|
||||||
|
import javax.crypto.SecretKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class to manage the passkey elements,
|
||||||
|
* allows to add and retrieve intent values with preconfigured keys,
|
||||||
|
* and makes it easy to create creation and usage requests
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
object PasskeyHelper {
|
||||||
|
|
||||||
|
private const val EXTRA_PASSKEY = "com.kunzisoft.keepass.passkey.extra.passkey"
|
||||||
|
|
||||||
|
private const val HMAC_TYPE = "HmacSHA256"
|
||||||
|
|
||||||
|
|
||||||
|
private const val EXTRA_SEARCH_INFO = "com.kunzisoft.keepass.extra.searchInfo"
|
||||||
|
private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin"
|
||||||
|
private const val EXTRA_NODE_ID = "com.kunzisoft.keepass.extra.nodeId"
|
||||||
|
private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp"
|
||||||
|
private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode"
|
||||||
|
|
||||||
|
private const val SEPARATOR = "_"
|
||||||
|
|
||||||
|
private const val NAME_OF_HMAC_KEY = "KeePassDXCredentialProviderHMACKey"
|
||||||
|
|
||||||
|
private const val KEYSTORE_TYPE = "AndroidKeyStore"
|
||||||
|
|
||||||
|
private val PLACEHOLDER_FOR_NEW_NODE_ID = "0".repeat(32)
|
||||||
|
|
||||||
|
private val REGEX_TIMESTAMP = "[0-9]{10}".toRegex()
|
||||||
|
private val REGEX_AUTHENTICATION_CODE = "[A-F0-9]{64}".toRegex() // 256 bits = 64 hex chars
|
||||||
|
|
||||||
|
private const val MAX_DIFF_IN_SECONDS = 60
|
||||||
|
|
||||||
|
private val internalSecureRandom: SecureRandom = SecureRandom()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the Passkey response for one entry
|
||||||
|
*/
|
||||||
|
fun Activity.buildPasskeyResponseAndSetResult(
|
||||||
|
entryInfo: EntryInfo,
|
||||||
|
extras: Bundle? = null
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
entryInfo.passkey?.let {
|
||||||
|
val mReplyIntent = Intent()
|
||||||
|
Log.d(javaClass.name, "Success Passkey manual selection")
|
||||||
|
mReplyIntent.putExtra(EXTRA_PASSKEY, entryInfo.passkey)
|
||||||
|
mReplyIntent.putExtra(EXTRA_APP_ORIGIN, entryInfo.appOrigin)
|
||||||
|
extras?.let {
|
||||||
|
mReplyIntent.putExtras(it)
|
||||||
|
}
|
||||||
|
setResult(Activity.RESULT_OK, mReplyIntent)
|
||||||
|
} ?: run {
|
||||||
|
Log.w(javaClass.name, "Failed Passkey manual selection")
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(javaClass.name, "Cant add passkey entry as result", e)
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an authentication code generated by an entry to the intent
|
||||||
|
*/
|
||||||
|
fun Intent.addAuthCode(passkeyEntryNodeId: UUID? = null) {
|
||||||
|
putExtras(Bundle().apply {
|
||||||
|
val timestamp = Instant.now().epochSecond
|
||||||
|
putString(EXTRA_TIMESTAMP, timestamp.toString())
|
||||||
|
putString(
|
||||||
|
EXTRA_AUTHENTICATION_CODE,
|
||||||
|
generatedAuthenticationCode(
|
||||||
|
passkeyEntryNodeId, timestamp
|
||||||
|
).toHexString()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the passkey from the intent
|
||||||
|
*/
|
||||||
|
fun Intent.retrievePasskey(): Passkey? {
|
||||||
|
return this.getParcelableExtraCompat(EXTRA_PASSKEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the passkey from the intent
|
||||||
|
*/
|
||||||
|
fun Intent.removePasskey() {
|
||||||
|
return this.removeExtra(EXTRA_PASSKEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the search info to the intent
|
||||||
|
*/
|
||||||
|
fun Intent.addSearchInfo(searchInfo: SearchInfo?) {
|
||||||
|
searchInfo?.let {
|
||||||
|
putExtra(EXTRA_SEARCH_INFO, searchInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the search info from the intent
|
||||||
|
*/
|
||||||
|
fun Intent.retrieveSearchInfo(): SearchInfo? {
|
||||||
|
return this.getParcelableExtraCompat(EXTRA_SEARCH_INFO)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the app origin to the intent
|
||||||
|
*/
|
||||||
|
fun Intent.addAppOrigin(appOrigin: AppOrigin?) {
|
||||||
|
appOrigin?.let {
|
||||||
|
putExtra(EXTRA_APP_ORIGIN, appOrigin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the app origin from the intent
|
||||||
|
*/
|
||||||
|
fun Intent.retrieveAppOrigin(): AppOrigin? {
|
||||||
|
return this.getParcelableExtraCompat(EXTRA_APP_ORIGIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the app origin from the intent
|
||||||
|
*/
|
||||||
|
fun Intent.removeAppOrigin() {
|
||||||
|
return this.removeExtra(EXTRA_APP_ORIGIN)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the node id to the intent, useful for auto passkey selection
|
||||||
|
*/
|
||||||
|
fun Intent.addNodeId(nodeId: UUID?) {
|
||||||
|
nodeId?.let {
|
||||||
|
putExtra(EXTRA_NODE_ID, ParcelUuid(nodeId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the node id from the intent
|
||||||
|
*/
|
||||||
|
fun Intent.retrieveNodeId(): UUID? {
|
||||||
|
return getParcelableExtraCompat<ParcelUuid>(EXTRA_NODE_ID)?.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the timestamp and authentication code transmitted via PendingIntent
|
||||||
|
*/
|
||||||
|
fun checkSecurity(intent: Intent, nodeId: UUID?) {
|
||||||
|
val timestampString = intent.getStringExtra(EXTRA_TIMESTAMP)
|
||||||
|
if (timestampString.isNullOrEmpty())
|
||||||
|
throw CreateCredentialUnknownException("Timestamp null")
|
||||||
|
if (timestampString.matches(REGEX_TIMESTAMP).not()) {
|
||||||
|
throw CreateCredentialUnknownException("Timestamp not valid")
|
||||||
|
}
|
||||||
|
val timestamp = timestampString.toLong()
|
||||||
|
val diff = Instant.now().epochSecond - timestamp
|
||||||
|
if (diff < 0 || diff > MAX_DIFF_IN_SECONDS) {
|
||||||
|
throw CreateCredentialUnknownException("Out of time")
|
||||||
|
}
|
||||||
|
verifyAuthenticationCode(
|
||||||
|
intent.getStringExtra(EXTRA_AUTHENTICATION_CODE),
|
||||||
|
generatedAuthenticationCode(nodeId, timestamp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify the authentication code from the encrypted message received from the intent
|
||||||
|
*/
|
||||||
|
private fun verifyAuthenticationCode(
|
||||||
|
valueToCheck: String?,
|
||||||
|
authenticationCode: ByteArray
|
||||||
|
) {
|
||||||
|
if (valueToCheck.isNullOrEmpty())
|
||||||
|
throw CreateCredentialUnknownException("Authentication code empty")
|
||||||
|
if (valueToCheck.matches(REGEX_AUTHENTICATION_CODE).not())
|
||||||
|
throw CreateCredentialUnknownException("Authentication not valid")
|
||||||
|
if (MessageDigest.isEqual(authenticationCode, generateAuthenticationCode(valueToCheck)))
|
||||||
|
throw CreateCredentialUnknownException("Authentication code incorrect")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the authentication code base on the entry [nodeId] and [timestamp]
|
||||||
|
*/
|
||||||
|
private fun generatedAuthenticationCode(nodeId: UUID?, timestamp: Long): ByteArray {
|
||||||
|
return generateAuthenticationCode(
|
||||||
|
(nodeId?.toString() ?: PLACEHOLDER_FOR_NEW_NODE_ID) + SEPARATOR + timestamp.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the authentication code base on the entry [message]
|
||||||
|
*/
|
||||||
|
private fun generateAuthenticationCode(message: String): ByteArray {
|
||||||
|
val keyStore = KeyStore.getInstance(KEYSTORE_TYPE)
|
||||||
|
keyStore.load(null)
|
||||||
|
val hmacKey = try {
|
||||||
|
keyStore.getKey(NAME_OF_HMAC_KEY, null) as SecretKey
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// key not found
|
||||||
|
generateKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
val mac = Mac.getInstance(HMAC_TYPE)
|
||||||
|
mac.init(hmacKey)
|
||||||
|
val authenticationCode = mac.doFinal(message.toByteArray())
|
||||||
|
return authenticationCode
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the HMAC key if cannot be found in the KeyStore
|
||||||
|
*/
|
||||||
|
private fun generateKey(): SecretKey? {
|
||||||
|
val keyGenerator = KeyGenerator.getInstance(
|
||||||
|
KeyProperties.KEY_ALGORITHM_HMAC_SHA256, KEYSTORE_TYPE
|
||||||
|
)
|
||||||
|
val keySizeInBits = 128
|
||||||
|
keyGenerator.init(
|
||||||
|
KeyGenParameterSpec.Builder(NAME_OF_HMAC_KEY, KeyProperties.PURPOSE_SIGN)
|
||||||
|
.setKeySize(keySizeInBits)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
val key = keyGenerator.generateKey()
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the [PublicKeyCredentialCreationOptions] from the intent
|
||||||
|
*/
|
||||||
|
fun ProviderCreateCredentialRequest.retrievePasskeyCreationComponent(): PublicKeyCredentialCreationOptions {
|
||||||
|
val request = this
|
||||||
|
if (request.callingRequest !is CreatePublicKeyCredentialRequest) {
|
||||||
|
throw CreateCredentialUnknownException("callingRequest is of wrong type: ${request.callingRequest.type}")
|
||||||
|
}
|
||||||
|
val createPublicKeyCredentialRequest = request.callingRequest as CreatePublicKeyCredentialRequest
|
||||||
|
return PublicKeyCredentialCreationOptions(
|
||||||
|
requestJson = createPublicKeyCredentialRequest.requestJson,
|
||||||
|
clientDataHash = createPublicKeyCredentialRequest.clientDataHash
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the [GetPublicKeyCredentialOption] from the intent
|
||||||
|
*/
|
||||||
|
fun ProviderGetCredentialRequest.retrievePasskeyUsageComponent(): GetPublicKeyCredentialOption {
|
||||||
|
val request = this
|
||||||
|
if (request.credentialOptions.size != 1) {
|
||||||
|
throw GetCredentialUnknownException("not exact one credentialOption")
|
||||||
|
}
|
||||||
|
if (request.credentialOptions[0] !is GetPublicKeyCredentialOption) {
|
||||||
|
throw CreateCredentialUnknownException("credentialOptions is of wrong type: ${request.credentialOptions[0]}")
|
||||||
|
}
|
||||||
|
return request.credentialOptions[0] as GetPublicKeyCredentialOption
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to retrieve the origin asynchronously,
|
||||||
|
* checks for the presence of the application in the privilege list of the trustedPackages.json file,
|
||||||
|
* call [onOriginRetrieved] if the origin is already calculated by the system and in the privileged list, return the clientDataHash
|
||||||
|
* call [onOriginNotRetrieved] if the origin is not retrieved from the system, return a new Android Origin
|
||||||
|
*/
|
||||||
|
suspend fun getOrigin(
|
||||||
|
providedClientDataHash: ByteArray?,
|
||||||
|
callingAppInfo: CallingAppInfo?,
|
||||||
|
assets: AssetManager,
|
||||||
|
relyingParty: String,
|
||||||
|
onOriginRetrieved: (appOrigin: AppOrigin, clientDataHash: ByteArray) -> Unit,
|
||||||
|
onOriginNotRetrieved: (appOrigin: AppOrigin, androidOriginString: String) -> Unit
|
||||||
|
) {
|
||||||
|
if (callingAppInfo == null) {
|
||||||
|
throw SecurityException("Calling app info cannot be retrieved")
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
var callOrigin: String?
|
||||||
|
val privilegedAllowlist = assets.open("trustedPackages.json").bufferedReader().use {
|
||||||
|
it.readText()
|
||||||
|
}
|
||||||
|
// for trusted browsers like Chrome and Firefox
|
||||||
|
callOrigin = callingAppInfo.getOrigin(privilegedAllowlist)?.removeSuffix("/")
|
||||||
|
val androidOrigin = AndroidOrigin(
|
||||||
|
packageName = callingAppInfo.packageName,
|
||||||
|
fingerprint = callingAppInfo.signingInfo.getApplicationFingerprints()
|
||||||
|
)
|
||||||
|
val webOrigin = WebOrigin.fromRelyingParty(
|
||||||
|
relyingParty = relyingParty
|
||||||
|
)
|
||||||
|
// Check if the webDomain is validated for the
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (callOrigin != null && providedClientDataHash != null) {
|
||||||
|
// Origin already defined by the system
|
||||||
|
Log.d(javaClass.simpleName, "Origin $callOrigin retrieved from callingAppInfo")
|
||||||
|
onOriginRetrieved(
|
||||||
|
AppOrigin.fromOrigin(callOrigin, androidOrigin, verified = true),
|
||||||
|
providedClientDataHash
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Add Android origin by default
|
||||||
|
onOriginNotRetrieved(
|
||||||
|
AppOrigin(verified = false).apply {
|
||||||
|
addAndroidOrigin(androidOrigin)
|
||||||
|
},
|
||||||
|
androidOrigin.toAndroidOrigin()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a credential id randomly
|
||||||
|
*/
|
||||||
|
fun generateCredentialId(): ByteArray {
|
||||||
|
// see https://w3c.github.io/webauthn/#credential-id
|
||||||
|
val size = 16
|
||||||
|
val credentialId = ByteArray(size)
|
||||||
|
internalSecureRandom.nextBytes(credentialId)
|
||||||
|
return credentialId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to create a passkey and the associated creation request parameters
|
||||||
|
* [intent] allows to retrieve the request
|
||||||
|
* [assetManager] has been transferred to the origin manager to manage package verification files
|
||||||
|
* [passkeyCreated] is called asynchronously when the passkey has been created
|
||||||
|
*/
|
||||||
|
suspend fun retrievePasskeyCreationRequestParameters(
|
||||||
|
intent: Intent,
|
||||||
|
assetManager: AssetManager,
|
||||||
|
passkeyCreated: (Passkey, AppOrigin?, PublicKeyCredentialCreationParameters) -> Unit
|
||||||
|
) {
|
||||||
|
val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
|
||||||
|
if (createCredentialRequest == null)
|
||||||
|
throw CreateCredentialUnknownException("could not retrieve request from intent")
|
||||||
|
val callingAppInfo = createCredentialRequest.callingAppInfo
|
||||||
|
val creationOptions = createCredentialRequest.retrievePasskeyCreationComponent()
|
||||||
|
|
||||||
|
val relyingParty = creationOptions.relyingPartyEntity.id
|
||||||
|
val username = creationOptions.userEntity.name
|
||||||
|
val userHandle = creationOptions.userEntity.id
|
||||||
|
val pubKeyCredParams = creationOptions.pubKeyCredParams
|
||||||
|
val clientDataHash = creationOptions.clientDataHash
|
||||||
|
|
||||||
|
val credentialId = generateCredentialId()
|
||||||
|
|
||||||
|
val (keyPair, keyTypeId) = Signature.generateKeyPair(
|
||||||
|
pubKeyCredParams.map { params -> params.alg }
|
||||||
|
) ?: throw CreateCredentialUnknownException("no known public key type found")
|
||||||
|
val privateKeyPem = Signature.convertPrivateKeyToPem(keyPair.private)
|
||||||
|
|
||||||
|
// Create the passkey element
|
||||||
|
val passkey = Passkey(
|
||||||
|
username = username,
|
||||||
|
privateKeyPem = privateKeyPem,
|
||||||
|
credentialId = b64Encode(credentialId),
|
||||||
|
userHandle = b64Encode(userHandle),
|
||||||
|
relyingParty = relyingParty
|
||||||
|
)
|
||||||
|
|
||||||
|
// create new entry in database
|
||||||
|
getOrigin(
|
||||||
|
providedClientDataHash = clientDataHash,
|
||||||
|
callingAppInfo = callingAppInfo,
|
||||||
|
assets = assetManager,
|
||||||
|
relyingParty = relyingParty,
|
||||||
|
onOriginRetrieved = { appInfoToStore, clientDataHash ->
|
||||||
|
passkeyCreated.invoke(
|
||||||
|
passkey,
|
||||||
|
appInfoToStore,
|
||||||
|
PublicKeyCredentialCreationParameters(
|
||||||
|
publicKeyCredentialCreationOptions = creationOptions,
|
||||||
|
credentialId = credentialId,
|
||||||
|
signatureKey = Pair(keyPair, keyTypeId),
|
||||||
|
clientDataResponse = ClientDataDefinedResponse(clientDataHash)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onOriginNotRetrieved = { appInfoToStore, origin ->
|
||||||
|
passkeyCreated.invoke(
|
||||||
|
passkey,
|
||||||
|
appInfoToStore,
|
||||||
|
PublicKeyCredentialCreationParameters(
|
||||||
|
publicKeyCredentialCreationOptions = creationOptions,
|
||||||
|
credentialId = credentialId,
|
||||||
|
signatureKey = Pair(keyPair, keyTypeId),
|
||||||
|
clientDataResponse = ClientDataBuildResponse(
|
||||||
|
type = ClientDataBuildResponse.Type.CREATE,
|
||||||
|
challenge = creationOptions.challenge,
|
||||||
|
origin = origin
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the passkey public key credential response,
|
||||||
|
* by calling this method the user is always recognized as present and verified
|
||||||
|
*/
|
||||||
|
fun buildCreatePublicKeyCredentialResponse(
|
||||||
|
publicKeyCredentialCreationParameters: PublicKeyCredentialCreationParameters
|
||||||
|
): CreatePublicKeyCredentialResponse {
|
||||||
|
|
||||||
|
val keyPair = publicKeyCredentialCreationParameters.signatureKey.first
|
||||||
|
val keyTypeId = publicKeyCredentialCreationParameters.signatureKey.second
|
||||||
|
val responseJson = FidoPublicKeyCredential(
|
||||||
|
id = b64Encode(publicKeyCredentialCreationParameters.credentialId),
|
||||||
|
response = AuthenticatorAttestationResponse(
|
||||||
|
requestOptions = publicKeyCredentialCreationParameters.publicKeyCredentialCreationOptions,
|
||||||
|
credentialId = publicKeyCredentialCreationParameters.credentialId,
|
||||||
|
credentialPublicKey = Cbor().encode(
|
||||||
|
Signature.convertPublicKeyToMap(
|
||||||
|
publicKeyIn = keyPair.public,
|
||||||
|
keyTypeId = keyTypeId
|
||||||
|
) ?: mapOf<Int, Any>()),
|
||||||
|
userPresent = true,
|
||||||
|
userVerified = true,
|
||||||
|
backupEligibility = BACKUP_ELIGIBILITY,
|
||||||
|
backupState = false, // TODO Setting to add a backup manually #2135
|
||||||
|
publicKeyTypeId = keyTypeId,
|
||||||
|
publicKeyCbor = Signature.convertPublicKey(keyPair.public, keyTypeId)!!,
|
||||||
|
clientDataResponse = publicKeyCredentialCreationParameters.clientDataResponse
|
||||||
|
),
|
||||||
|
authenticatorAttachment = "platform"
|
||||||
|
).json()
|
||||||
|
// log only the length to prevent logging sensitive information
|
||||||
|
Log.d(javaClass.simpleName, "Json response for key creation")
|
||||||
|
return CreatePublicKeyCredentialResponse(responseJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to use a passkey and create the associated usage request parameters
|
||||||
|
* [intent] allows to retrieve the request
|
||||||
|
* [assetManager] has been transferred to the origin manager to manage package verification files
|
||||||
|
* [result] is called asynchronously after the creation of PublicKeyCredentialUsageParameters, the origin associated with it may or may not be verified
|
||||||
|
*/
|
||||||
|
suspend fun retrievePasskeyUsageRequestParameters(
|
||||||
|
intent: Intent,
|
||||||
|
assetManager: AssetManager,
|
||||||
|
result: (PublicKeyCredentialUsageParameters) -> Unit
|
||||||
|
) {
|
||||||
|
val getCredentialRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
|
||||||
|
if (getCredentialRequest == null)
|
||||||
|
throw CreateCredentialUnknownException("could not retrieve request from intent")
|
||||||
|
val callingAppInfo = getCredentialRequest.callingAppInfo
|
||||||
|
val credentialOption = getCredentialRequest.retrievePasskeyUsageComponent()
|
||||||
|
val clientDataHash = credentialOption.clientDataHash
|
||||||
|
|
||||||
|
val requestOptions = PublicKeyCredentialRequestOptions(credentialOption.requestJson)
|
||||||
|
|
||||||
|
getOrigin(
|
||||||
|
providedClientDataHash = clientDataHash,
|
||||||
|
callingAppInfo = callingAppInfo,
|
||||||
|
assets = assetManager,
|
||||||
|
relyingParty = requestOptions.rpId,
|
||||||
|
onOriginRetrieved = { appOrigin, clientDataHash ->
|
||||||
|
result.invoke(
|
||||||
|
PublicKeyCredentialUsageParameters(
|
||||||
|
publicKeyCredentialRequestOptions = requestOptions,
|
||||||
|
clientDataResponse = ClientDataDefinedResponse(clientDataHash),
|
||||||
|
appOrigin = appOrigin
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onOriginNotRetrieved = { appOrigin, androidOriginString ->
|
||||||
|
// By default we crate an usage parameter with Android origin
|
||||||
|
result.invoke(
|
||||||
|
PublicKeyCredentialUsageParameters(
|
||||||
|
publicKeyCredentialRequestOptions = requestOptions,
|
||||||
|
clientDataResponse = ClientDataBuildResponse(
|
||||||
|
type = ClientDataBuildResponse.Type.GET,
|
||||||
|
challenge = requestOptions.challenge,
|
||||||
|
origin = androidOriginString
|
||||||
|
),
|
||||||
|
appOrigin = appOrigin
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the passkey public key credential response,
|
||||||
|
* by calling this method the user is always recognized as present and verified
|
||||||
|
*/
|
||||||
|
fun buildPasskeyPublicKeyCredential(
|
||||||
|
requestOptions: PublicKeyCredentialRequestOptions,
|
||||||
|
clientDataResponse: ClientDataResponse,
|
||||||
|
passkey: Passkey
|
||||||
|
): PublicKeyCredential {
|
||||||
|
val getCredentialResponse = FidoPublicKeyCredential(
|
||||||
|
id = passkey.credentialId,
|
||||||
|
response = AuthenticatorAssertionResponse(
|
||||||
|
requestOptions = requestOptions,
|
||||||
|
userPresent = true,
|
||||||
|
userVerified = true,
|
||||||
|
backupEligibility = BACKUP_ELIGIBILITY,
|
||||||
|
backupState = false, // TODO Setting to add a backup manually #2135
|
||||||
|
userHandle = passkey.userHandle,
|
||||||
|
privateKey = passkey.privateKeyPem,
|
||||||
|
clientDataResponse = clientDataResponse
|
||||||
|
),
|
||||||
|
authenticatorAttachment = "platform"
|
||||||
|
).json()
|
||||||
|
Log.d(javaClass.simpleName, "Json response for key usage")
|
||||||
|
return PublicKeyCredential(getCredentialResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that the application signature is contained in the [appOrigin]
|
||||||
|
*/
|
||||||
|
fun getVerifiedGETClientDataResponse(
|
||||||
|
usageParameters: PublicKeyCredentialUsageParameters,
|
||||||
|
appOrigin: AppOrigin
|
||||||
|
): ClientDataResponse {
|
||||||
|
val appToCheck = usageParameters.appOrigin
|
||||||
|
return if (appToCheck.verified) {
|
||||||
|
usageParameters.clientDataResponse
|
||||||
|
} else {
|
||||||
|
// Origin checked by Android app signature
|
||||||
|
ClientDataBuildResponse(
|
||||||
|
type = ClientDataBuildResponse.Type.GET,
|
||||||
|
challenge = usageParameters.publicKeyCredentialRequestOptions.challenge,
|
||||||
|
origin = appToCheck.checkAppOrigin(appOrigin)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val BACKUP_ELIGIBILITY = true
|
||||||
|
}
|
||||||
@@ -108,14 +108,19 @@ class DatabaseTaskProvider(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
// To show dialog only if context is an activity
|
// To show dialog only if context is an activity
|
||||||
private var activity: FragmentActivity? = try { context as? FragmentActivity? }
|
private var activity: FragmentActivity? = try {
|
||||||
catch (_: Exception) { null }
|
context as? FragmentActivity?
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
var onDatabaseRetrieved: ((database: ContextualDatabase?) -> Unit)? = null
|
var onDatabaseRetrieved: ((database: ContextualDatabase?) -> Unit)? = null
|
||||||
|
|
||||||
var onActionFinish: ((database: ContextualDatabase,
|
var onActionFinish: ((
|
||||||
|
database: ContextualDatabase,
|
||||||
actionTask: String,
|
actionTask: String,
|
||||||
result: ActionRunnable.Result) -> Unit)? = null
|
result: ActionRunnable.Result
|
||||||
|
) -> Unit)? = null
|
||||||
|
|
||||||
private var intentDatabaseTask: Intent = Intent(
|
private var intentDatabaseTask: Intent = Intent(
|
||||||
context.applicationContext,
|
context.applicationContext,
|
||||||
@@ -175,7 +180,8 @@ class DatabaseTaskProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mActionDatabaseListener = object: DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
|
private val mActionDatabaseListener =
|
||||||
|
object : DatabaseChangedDialogFragment.ActionDatabaseChangedListener {
|
||||||
override fun validateDatabaseChanged() {
|
override fun validateDatabaseChanged() {
|
||||||
mBinder?.getService()?.saveDatabaseInfo()
|
mBinder?.getService()?.saveDatabaseInfo()
|
||||||
}
|
}
|
||||||
@@ -265,7 +271,8 @@ class DatabaseTaskProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
|
||||||
mBinder = (serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
|
mBinder =
|
||||||
|
(serviceBinder as DatabaseTaskNotificationService.ActionTaskBinder?)?.apply {
|
||||||
addServiceListeners(this)
|
addServiceListeners(this)
|
||||||
getService().checkDatabase()
|
getService().checkDatabase()
|
||||||
getService().checkDatabaseInfo()
|
getService().checkDatabaseInfo()
|
||||||
@@ -296,7 +303,11 @@ class DatabaseTaskProvider(
|
|||||||
private fun bindService() {
|
private fun bindService() {
|
||||||
initServiceConnection()
|
initServiceConnection()
|
||||||
serviceConnection?.let {
|
serviceConnection?.let {
|
||||||
context.bindService(intentDatabaseTask, it, BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT)
|
context.bindService(
|
||||||
|
intentDatabaseTask,
|
||||||
|
it,
|
||||||
|
BIND_AUTO_CREATE or BIND_IMPORTANT or BIND_ABOVE_CLIENT
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,7 +342,8 @@ class DatabaseTaskProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ContextCompat.registerReceiver(context, databaseTaskBroadcastReceiver,
|
ContextCompat.registerReceiver(
|
||||||
|
context, databaseTaskBroadcastReceiver,
|
||||||
IntentFilter().apply {
|
IntentFilter().apply {
|
||||||
addAction(DATABASE_START_TASK_ACTION)
|
addAction(DATABASE_START_TASK_ACTION)
|
||||||
addAction(DATABASE_STOP_TASK_ACTION)
|
addAction(DATABASE_STOP_TASK_ACTION)
|
||||||
@@ -416,47 +428,51 @@ class DatabaseTaskProvider(
|
|||||||
----
|
----
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun startDatabaseCreate(databaseUri: Uri,
|
fun startDatabaseCreate(
|
||||||
|
databaseUri: Uri,
|
||||||
mainCredential: MainCredential
|
mainCredential: MainCredential
|
||||||
) {
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||||
}
|
}, ACTION_DATABASE_CREATE_TASK)
|
||||||
, ACTION_DATABASE_CREATE_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseLoad(databaseUri: Uri,
|
fun startDatabaseLoad(
|
||||||
|
databaseUri: Uri,
|
||||||
mainCredential: MainCredential,
|
mainCredential: MainCredential,
|
||||||
readOnly: Boolean,
|
readOnly: Boolean,
|
||||||
cipherEncryptDatabase: CipherEncryptDatabase?,
|
cipherEncryptDatabase: CipherEncryptDatabase?,
|
||||||
fixDuplicateUuid: Boolean) {
|
fixDuplicateUuid: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||||
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
|
putBoolean(DatabaseTaskNotificationService.READ_ONLY_KEY, readOnly)
|
||||||
putParcelable(DatabaseTaskNotificationService.CIPHER_DATABASE_KEY, cipherEncryptDatabase)
|
putParcelable(
|
||||||
|
DatabaseTaskNotificationService.CIPHER_DATABASE_KEY,
|
||||||
|
cipherEncryptDatabase
|
||||||
|
)
|
||||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||||
}
|
}, ACTION_DATABASE_LOAD_TASK)
|
||||||
, ACTION_DATABASE_LOAD_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseMerge(save: Boolean,
|
fun startDatabaseMerge(
|
||||||
|
save: Boolean,
|
||||||
fromDatabaseUri: Uri? = null,
|
fromDatabaseUri: Uri? = null,
|
||||||
mainCredential: MainCredential? = null) {
|
mainCredential: MainCredential? = null
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, fromDatabaseUri)
|
||||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||||
}
|
}, ACTION_DATABASE_MERGE_TASK)
|
||||||
, ACTION_DATABASE_MERGE_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseReload(fixDuplicateUuid: Boolean) {
|
fun startDatabaseReload(fixDuplicateUuid: Boolean) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
putBoolean(DatabaseTaskNotificationService.FIX_DUPLICATE_UUID_KEY, fixDuplicateUuid)
|
||||||
}
|
}, ACTION_DATABASE_RELOAD_TASK)
|
||||||
, ACTION_DATABASE_RELOAD_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun askToStartDatabaseReload(conditionToAsk: Boolean, approved: () -> Unit) {
|
fun askToStartDatabaseReload(conditionToAsk: Boolean, approved: () -> Unit) {
|
||||||
@@ -472,15 +488,15 @@ class DatabaseTaskProvider(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseAssignCredential(databaseUri: Uri,
|
fun startDatabaseAssignCredential(
|
||||||
|
databaseUri: Uri,
|
||||||
mainCredential: MainCredential
|
mainCredential: MainCredential
|
||||||
) {
|
) {
|
||||||
|
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, databaseUri)
|
||||||
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
putParcelable(DatabaseTaskNotificationService.MAIN_CREDENTIAL_KEY, mainCredential)
|
||||||
}
|
}, ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK)
|
||||||
, ACTION_DATABASE_ASSIGN_CREDENTIAL_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -489,54 +505,60 @@ class DatabaseTaskProvider(
|
|||||||
----
|
----
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun startDatabaseCreateGroup(newGroup: Group,
|
fun startDatabaseCreateGroup(
|
||||||
|
newGroup: Group,
|
||||||
parent: Group,
|
parent: Group,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, newGroup)
|
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, newGroup)
|
||||||
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
|
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_CREATE_GROUP_TASK)
|
||||||
, ACTION_DATABASE_CREATE_GROUP_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseUpdateGroup(oldGroup: Group,
|
fun startDatabaseUpdateGroup(
|
||||||
|
oldGroup: Group,
|
||||||
groupToUpdate: Group,
|
groupToUpdate: Group,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.GROUP_ID_KEY, oldGroup.nodeId)
|
putParcelable(DatabaseTaskNotificationService.GROUP_ID_KEY, oldGroup.nodeId)
|
||||||
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, groupToUpdate)
|
putParcelable(DatabaseTaskNotificationService.GROUP_KEY, groupToUpdate)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_GROUP_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_GROUP_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseCreateEntry(newEntry: Entry,
|
fun startDatabaseCreateEntry(
|
||||||
|
newEntry: Entry,
|
||||||
parent: Group,
|
parent: Group,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, newEntry)
|
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, newEntry)
|
||||||
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
|
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, parent.nodeId)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_CREATE_ENTRY_TASK)
|
||||||
, ACTION_DATABASE_CREATE_ENTRY_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseUpdateEntry(oldEntry: Entry,
|
fun startDatabaseUpdateEntry(
|
||||||
|
oldEntry: Entry,
|
||||||
entryToUpdate: Entry,
|
entryToUpdate: Entry,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, oldEntry.nodeId)
|
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, oldEntry.nodeId)
|
||||||
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, entryToUpdate)
|
putParcelable(DatabaseTaskNotificationService.ENTRY_KEY, entryToUpdate)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_ENTRY_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_ENTRY_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startDatabaseActionListNodes(actionTask: String,
|
private fun startDatabaseActionListNodes(
|
||||||
|
actionTask: String,
|
||||||
nodesPaste: List<Node>,
|
nodesPaste: List<Node>,
|
||||||
newParent: Group?,
|
newParent: Group?,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
|
) {
|
||||||
val groupsIdToCopy = ArrayList<NodeId<*>>()
|
val groupsIdToCopy = ArrayList<NodeId<*>>()
|
||||||
val entriesIdToCopy = ArrayList<NodeId<UUID>>()
|
val entriesIdToCopy = ArrayList<NodeId<UUID>>()
|
||||||
nodesPaste.forEach { nodeVersioned ->
|
nodesPaste.forEach { nodeVersioned ->
|
||||||
@@ -544,6 +566,7 @@ class DatabaseTaskProvider(
|
|||||||
Type.GROUP -> {
|
Type.GROUP -> {
|
||||||
groupsIdToCopy.add((nodeVersioned as Group).nodeId)
|
groupsIdToCopy.add((nodeVersioned as Group).nodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
Type.ENTRY -> {
|
Type.ENTRY -> {
|
||||||
entriesIdToCopy.add((nodeVersioned as Entry).nodeId)
|
entriesIdToCopy.add((nodeVersioned as Entry).nodeId)
|
||||||
}
|
}
|
||||||
@@ -558,24 +581,29 @@ class DatabaseTaskProvider(
|
|||||||
if (newParentId != null)
|
if (newParentId != null)
|
||||||
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, newParentId)
|
putParcelable(DatabaseTaskNotificationService.PARENT_ID_KEY, newParentId)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, actionTask)
|
||||||
, actionTask)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseCopyNodes(nodesToCopy: List<Node>,
|
fun startDatabaseCopyNodes(
|
||||||
|
nodesToCopy: List<Node>,
|
||||||
newParent: Group,
|
newParent: Group,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
|
) {
|
||||||
startDatabaseActionListNodes(ACTION_DATABASE_COPY_NODES_TASK, nodesToCopy, newParent, save)
|
startDatabaseActionListNodes(ACTION_DATABASE_COPY_NODES_TASK, nodesToCopy, newParent, save)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseMoveNodes(nodesToMove: List<Node>,
|
fun startDatabaseMoveNodes(
|
||||||
|
nodesToMove: List<Node>,
|
||||||
newParent: Group,
|
newParent: Group,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
|
) {
|
||||||
startDatabaseActionListNodes(ACTION_DATABASE_MOVE_NODES_TASK, nodesToMove, newParent, save)
|
startDatabaseActionListNodes(ACTION_DATABASE_MOVE_NODES_TASK, nodesToMove, newParent, save)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseDeleteNodes(nodesToDelete: List<Node>,
|
fun startDatabaseDeleteNodes(
|
||||||
save: Boolean) {
|
nodesToDelete: List<Node>,
|
||||||
|
save: Boolean
|
||||||
|
) {
|
||||||
startDatabaseActionListNodes(ACTION_DATABASE_DELETE_NODES_TASK, nodesToDelete, null, save)
|
startDatabaseActionListNodes(ACTION_DATABASE_DELETE_NODES_TASK, nodesToDelete, null, save)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,26 +613,28 @@ class DatabaseTaskProvider(
|
|||||||
-----------------
|
-----------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun startDatabaseRestoreEntryHistory(mainEntryId: NodeId<UUID>,
|
fun startDatabaseRestoreEntryHistory(
|
||||||
|
mainEntryId: NodeId<UUID>,
|
||||||
entryHistoryPosition: Int,
|
entryHistoryPosition: Int,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
|
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
|
||||||
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
|
||||||
, ACTION_DATABASE_RESTORE_ENTRY_HISTORY)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseDeleteEntryHistory(mainEntryId: NodeId<UUID>,
|
fun startDatabaseDeleteEntryHistory(
|
||||||
|
mainEntryId: NodeId<UUID>,
|
||||||
entryHistoryPosition: Int,
|
entryHistoryPosition: Int,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
|
putParcelable(DatabaseTaskNotificationService.ENTRY_ID_KEY, mainEntryId)
|
||||||
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
putInt(DatabaseTaskNotificationService.ENTRY_HISTORY_POSITION_KEY, entryHistoryPosition)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_DELETE_ENTRY_HISTORY)
|
||||||
, ACTION_DATABASE_DELETE_ENTRY_HISTORY)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -613,110 +643,118 @@ class DatabaseTaskProvider(
|
|||||||
-----------------
|
-----------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun startDatabaseSaveName(oldName: String,
|
fun startDatabaseSaveName(
|
||||||
|
oldName: String,
|
||||||
newName: String,
|
newName: String,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldName)
|
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldName)
|
||||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newName)
|
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newName)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_NAME_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_NAME_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveDescription(oldDescription: String,
|
fun startDatabaseSaveDescription(
|
||||||
|
oldDescription: String,
|
||||||
newDescription: String,
|
newDescription: String,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDescription)
|
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDescription)
|
||||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDescription)
|
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDescription)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_DESCRIPTION_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_DESCRIPTION_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveDefaultUsername(oldDefaultUsername: String,
|
fun startDatabaseSaveDefaultUsername(
|
||||||
|
oldDefaultUsername: String,
|
||||||
newDefaultUsername: String,
|
newDefaultUsername: String,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDefaultUsername)
|
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldDefaultUsername)
|
||||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDefaultUsername)
|
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newDefaultUsername)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_DEFAULT_USERNAME_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveColor(oldColor: String,
|
fun startDatabaseSaveColor(
|
||||||
|
oldColor: String,
|
||||||
newColor: String,
|
newColor: String,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldColor)
|
putString(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldColor)
|
||||||
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newColor)
|
putString(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newColor)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_COLOR_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_COLOR_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveCompression(oldCompression: CompressionAlgorithm,
|
fun startDatabaseSaveCompression(
|
||||||
|
oldCompression: CompressionAlgorithm,
|
||||||
newCompression: CompressionAlgorithm,
|
newCompression: CompressionAlgorithm,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldCompression)
|
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldCompression)
|
||||||
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newCompression)
|
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newCompression)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_COMPRESSION_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseRemoveUnlinkedData(save: Boolean) {
|
fun startDatabaseRemoveUnlinkedData(save: Boolean) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
|
||||||
, ACTION_DATABASE_REMOVE_UNLINKED_DATA_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveRecycleBin(oldRecycleBin: Group?,
|
fun startDatabaseSaveRecycleBin(
|
||||||
|
oldRecycleBin: Group?,
|
||||||
newRecycleBin: Group?,
|
newRecycleBin: Group?,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldRecycleBin)
|
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldRecycleBin)
|
||||||
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newRecycleBin)
|
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newRecycleBin)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_RECYCLE_BIN_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveTemplatesGroup(oldTemplatesGroup: Group?,
|
fun startDatabaseSaveTemplatesGroup(
|
||||||
|
oldTemplatesGroup: Group?,
|
||||||
newTemplatesGroup: Group?,
|
newTemplatesGroup: Group?,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldTemplatesGroup)
|
putParcelable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldTemplatesGroup)
|
||||||
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newTemplatesGroup)
|
putParcelable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newTemplatesGroup)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_TEMPLATES_GROUP_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveMaxHistoryItems(oldMaxHistoryItems: Int,
|
fun startDatabaseSaveMaxHistoryItems(
|
||||||
|
oldMaxHistoryItems: Int,
|
||||||
newMaxHistoryItems: Int,
|
newMaxHistoryItems: Int,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putInt(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistoryItems)
|
putInt(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistoryItems)
|
||||||
putInt(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistoryItems)
|
putInt(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistoryItems)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_MAX_HISTORY_ITEMS_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveMaxHistorySize(oldMaxHistorySize: Long,
|
fun startDatabaseSaveMaxHistorySize(
|
||||||
|
oldMaxHistorySize: Long,
|
||||||
newMaxHistorySize: Long,
|
newMaxHistorySize: Long,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistorySize)
|
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMaxHistorySize)
|
||||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistorySize)
|
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMaxHistorySize)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_MAX_HISTORY_SIZE_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -725,59 +763,64 @@ class DatabaseTaskProvider(
|
|||||||
-------------------
|
-------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
fun startDatabaseSaveEncryption(oldEncryption: EncryptionAlgorithm,
|
fun startDatabaseSaveEncryption(
|
||||||
|
oldEncryption: EncryptionAlgorithm,
|
||||||
newEncryption: EncryptionAlgorithm,
|
newEncryption: EncryptionAlgorithm,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldEncryption)
|
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldEncryption)
|
||||||
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newEncryption)
|
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newEncryption)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_ENCRYPTION_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_ENCRYPTION_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveKeyDerivation(oldKeyDerivation: KdfEngine,
|
fun startDatabaseSaveKeyDerivation(
|
||||||
|
oldKeyDerivation: KdfEngine,
|
||||||
newKeyDerivation: KdfEngine,
|
newKeyDerivation: KdfEngine,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldKeyDerivation)
|
putSerializable(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldKeyDerivation)
|
||||||
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newKeyDerivation)
|
putSerializable(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newKeyDerivation)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_KEY_DERIVATION_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveIterations(oldIterations: Long,
|
fun startDatabaseSaveIterations(
|
||||||
|
oldIterations: Long,
|
||||||
newIterations: Long,
|
newIterations: Long,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldIterations)
|
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldIterations)
|
||||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newIterations)
|
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newIterations)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_ITERATIONS_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_ITERATIONS_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveMemoryUsage(oldMemoryUsage: Long,
|
fun startDatabaseSaveMemoryUsage(
|
||||||
|
oldMemoryUsage: Long,
|
||||||
newMemoryUsage: Long,
|
newMemoryUsage: Long,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMemoryUsage)
|
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldMemoryUsage)
|
||||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMemoryUsage)
|
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newMemoryUsage)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_MEMORY_USAGE_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startDatabaseSaveParallelism(oldParallelism: Long,
|
fun startDatabaseSaveParallelism(
|
||||||
|
oldParallelism: Long,
|
||||||
newParallelism: Long,
|
newParallelism: Long,
|
||||||
save: Boolean) {
|
save: Boolean
|
||||||
|
) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldParallelism)
|
putLong(DatabaseTaskNotificationService.OLD_ELEMENT_KEY, oldParallelism)
|
||||||
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newParallelism)
|
putLong(DatabaseTaskNotificationService.NEW_ELEMENT_KEY, newParallelism)
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
}
|
}, ACTION_DATABASE_UPDATE_PARALLELISM_TASK)
|
||||||
, ACTION_DATABASE_UPDATE_PARALLELISM_TASK)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -787,15 +830,13 @@ class DatabaseTaskProvider(
|
|||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
putBoolean(DatabaseTaskNotificationService.SAVE_DATABASE_KEY, save)
|
||||||
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, saveToUri)
|
putParcelable(DatabaseTaskNotificationService.DATABASE_URI_KEY, saveToUri)
|
||||||
}
|
}, ACTION_DATABASE_SAVE)
|
||||||
, ACTION_DATABASE_SAVE)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startChallengeResponded(response: ByteArray?) {
|
fun startChallengeResponded(response: ByteArray?) {
|
||||||
start(Bundle().apply {
|
start(Bundle().apply {
|
||||||
putByteArray(DatabaseTaskNotificationService.DATA_BYTES, response)
|
putByteArray(DatabaseTaskNotificationService.DATA_BYTES, response)
|
||||||
}
|
}, ACTION_CHALLENGE_RESPONDED)
|
||||||
, ACTION_CHALLENGE_RESPONDED)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -24,7 +24,33 @@ import com.kunzisoft.keepass.R
|
|||||||
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
|
||||||
import com.kunzisoft.keepass.database.element.template.TemplateEngine
|
import com.kunzisoft.keepass.database.element.template.TemplateEngine
|
||||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
import com.kunzisoft.keepass.database.exception.*
|
import com.kunzisoft.keepass.database.exception.CopyEntryDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.CopyGroupDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.CorruptedDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.DatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.DatabaseInputException
|
||||||
|
import com.kunzisoft.keepass.database.exception.DatabaseOutputException
|
||||||
|
import com.kunzisoft.keepass.database.exception.DuplicateUuidDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.EmptyKeyDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.FileNotFoundDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.HardwareKeyDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.InvalidAlgorithmDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.InvalidCredentialsDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.KDFMemoryDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.MergeDatabaseKDBException
|
||||||
|
import com.kunzisoft.keepass.database.exception.MoveEntryDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.MoveGroupDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.NoMemoryDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.SignatureDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.UnknownDatabaseLocationException
|
||||||
|
import com.kunzisoft.keepass.database.exception.VersionDatabaseException
|
||||||
|
import com.kunzisoft.keepass.database.exception.XMLMalformedDatabaseException
|
||||||
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_CREDENTIAL_ID
|
||||||
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_PRIVATE_KEY
|
||||||
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_RELYING_PARTY
|
||||||
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_USERNAME
|
||||||
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_USER_HANDLE
|
||||||
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.PASSKEY_FIELD
|
||||||
|
|
||||||
fun DatabaseException.getLocalizedMessage(resources: Resources): String? =
|
fun DatabaseException.getLocalizedMessage(resources: Resources): String? =
|
||||||
when (this) {
|
when (this) {
|
||||||
@@ -63,6 +89,11 @@ fun TemplateField.isStandardPasswordName(context: Context, name: String): Boolea
|
|||||||
|| name == getLocalizedName(context, LABEL_PASSWORD)
|
|| name == getLocalizedName(context, LABEL_PASSWORD)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun TemplateField.isPasskeyLabel(context: Context, name: String): Boolean {
|
||||||
|
return name.equals(PASSKEY_FIELD, true)
|
||||||
|
|| name == getLocalizedName(context, PASSKEY_FIELD)
|
||||||
|
}
|
||||||
|
|
||||||
fun TemplateField.getLocalizedName(context: Context?, name: String): String {
|
fun TemplateField.getLocalizedName(context: Context?, name: String): String {
|
||||||
if (context == null
|
if (context == null
|
||||||
|| TemplateEngine.containsTemplateDecorator(name)
|
|| TemplateEngine.containsTemplateDecorator(name)
|
||||||
@@ -107,6 +138,13 @@ fun TemplateField.getLocalizedName(context: Context?, name: String): String {
|
|||||||
LABEL_SECURE_NOTE.equals(name, true) -> context.getString(R.string.secure_note)
|
LABEL_SECURE_NOTE.equals(name, true) -> context.getString(R.string.secure_note)
|
||||||
LABEL_MEMBERSHIP.equals(name, true) -> context.getString(R.string.membership)
|
LABEL_MEMBERSHIP.equals(name, true) -> context.getString(R.string.membership)
|
||||||
|
|
||||||
|
PASSKEY_FIELD.equals(name, true) -> context.getString(R.string.passkey)
|
||||||
|
FIELD_USERNAME.equals(name, true) -> context.getString(R.string.passkey_username)
|
||||||
|
FIELD_PRIVATE_KEY.equals(name, true) -> context.getString(R.string.passkey_private_key)
|
||||||
|
FIELD_CREDENTIAL_ID.equals(name, true) -> context.getString(R.string.passkey_credential_id)
|
||||||
|
FIELD_USER_HANDLE.equals(name, true) -> context.getString(R.string.passkey_user_handle)
|
||||||
|
FIELD_RELYING_PARTY.equals(name, true) -> context.getString(R.string.passkey_relying_party)
|
||||||
|
|
||||||
else -> name
|
else -> name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,13 +43,15 @@ object SearchHelper {
|
|||||||
/**
|
/**
|
||||||
* Utility method to perform actions if item is found or not after an auto search in [database]
|
* Utility method to perform actions if item is found or not after an auto search in [database]
|
||||||
*/
|
*/
|
||||||
fun checkAutoSearchInfo(context: Context,
|
fun checkAutoSearchInfo(
|
||||||
|
context: Context,
|
||||||
database: ContextualDatabase?,
|
database: ContextualDatabase?,
|
||||||
searchInfo: SearchInfo?,
|
searchInfo: SearchInfo?,
|
||||||
onItemsFound: (openedDatabase: ContextualDatabase,
|
onItemsFound: (openedDatabase: ContextualDatabase,
|
||||||
items: List<EntryInfo>) -> Unit,
|
items: List<EntryInfo>) -> Unit,
|
||||||
onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
|
onItemNotFound: (openedDatabase: ContextualDatabase) -> Unit,
|
||||||
onDatabaseClosed: () -> Unit) {
|
onDatabaseClosed: () -> Unit
|
||||||
|
) {
|
||||||
if (database == null || !database.loaded) {
|
if (database == null || !database.loaded) {
|
||||||
onDatabaseClosed.invoke()
|
onDatabaseClosed.invoke()
|
||||||
} else if (TimeoutHelper.checkTime(context)) {
|
} else if (TimeoutHelper.checkTime(context)) {
|
||||||
@@ -59,8 +61,7 @@ object SearchHelper {
|
|||||||
&& !searchInfo.containsOnlyNullValues()) {
|
&& !searchInfo.containsOnlyNullValues()) {
|
||||||
// If search provide results
|
// If search provide results
|
||||||
database.createVirtualGroupFromSearchInfo(
|
database.createVirtualGroupFromSearchInfo(
|
||||||
searchInfo.toString(),
|
searchInfo,
|
||||||
searchInfo.isASearchByDomain(),
|
|
||||||
MAX_SEARCH_ENTRY
|
MAX_SEARCH_ENTRY
|
||||||
)?.let { searchGroup ->
|
)?.let { searchGroup ->
|
||||||
if (searchGroup.numberOfChildEntries > 0) {
|
if (searchGroup.numberOfChildEntries > 0) {
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
package com.kunzisoft.keepass.receivers
|
package com.kunzisoft.keepass.receivers
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
|
||||||
import com.kunzisoft.keepass.utils.DexUtil
|
|
||||||
import com.kunzisoft.keepass.utils.MagikeyboardUtil
|
import com.kunzisoft.keepass.utils.MagikeyboardUtil
|
||||||
|
|
||||||
class DexModeReceiver : BroadcastReceiver() {
|
class DexModeReceiver : BroadcastReceiver() {
|
||||||
|
|||||||
@@ -274,10 +274,12 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
val containsPasswordToCopy = entry.password.isNotEmpty()
|
val containsPasswordToCopy = entry.password.isNotEmpty()
|
||||||
&& PreferencesUtil.allowCopyProtectedFields(context)
|
&& PreferencesUtil.allowCopyProtectedFields(context)
|
||||||
val containsOTPToCopy = entry.containsCustomField(OTP_TOKEN_FIELD)
|
val containsOTPToCopy = entry.containsCustomField(OTP_TOKEN_FIELD)
|
||||||
val containsExtraFieldToCopy = entry.customFields.isNotEmpty()
|
val customFields = entry.getCustomFieldsForFilling()
|
||||||
&& (entry.containsCustomFieldsNotProtected()
|
val containsExtraFieldToCopy = customFields.isNotEmpty()
|
||||||
|
&& (customFields.any { !it.protectedValue.isProtected }
|
||||||
||
|
||
|
||||||
(entry.containsCustomFieldsProtected() && PreferencesUtil.allowCopyProtectedFields(context))
|
(customFields.any { it.protectedValue.isProtected }
|
||||||
|
&& PreferencesUtil.allowCopyProtectedFields(context))
|
||||||
)
|
)
|
||||||
|
|
||||||
var startService = false
|
var startService = false
|
||||||
@@ -320,7 +322,7 @@ class ClipboardEntryNotificationService : LockNotificationService() {
|
|||||||
if (containsExtraFieldToCopy) {
|
if (containsExtraFieldToCopy) {
|
||||||
try {
|
try {
|
||||||
var anonymousFieldNumber = 0
|
var anonymousFieldNumber = 0
|
||||||
entry.customFields.forEach { field ->
|
entry.getCustomFieldsForFilling().forEach { field ->
|
||||||
//If value is not protected or allowed
|
//If value is not protected or allowed
|
||||||
if ((!field.protectedValue.isProtected
|
if ((!field.protectedValue.isProtected
|
||||||
|| PreferencesUtil.allowCopyProtectedFields(context))
|
|| PreferencesUtil.allowCopyProtectedFields(context))
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import android.util.Log
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import androidx.core.content.ContextCompat
|
|||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.app.AppLifecycleObserver
|
import com.kunzisoft.keepass.app.AppLifecycleObserver
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
import com.kunzisoft.keepass.services.ClipboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
|
import com.kunzisoft.keepass.services.KeyboardEntryNotificationService
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import android.content.ComponentName
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
|
import com.kunzisoft.keepass.credentialprovider.magikeyboard.MagikeyboardService
|
||||||
|
|
||||||
object MagikeyboardUtil {
|
object MagikeyboardUtil {
|
||||||
private val TAG = MagikeyboardUtil::class.java.name
|
private val TAG = MagikeyboardUtil::class.java.name
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.view
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.ContextThemeWrapper
|
||||||
|
import androidx.appcompat.widget.AppCompatImageView
|
||||||
|
import androidx.appcompat.widget.AppCompatTextView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import com.kunzisoft.keepass.R
|
||||||
|
import com.kunzisoft.keepass.password.PasswordEntropy
|
||||||
|
|
||||||
|
|
||||||
|
class PasskeyTextFieldView @JvmOverloads constructor(context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyle: Int = 0)
|
||||||
|
: PasswordTextFieldView(context, attrs, defStyle) {
|
||||||
|
|
||||||
|
private var relyingPartyViewId = ViewCompat.generateViewId()
|
||||||
|
private var usernameViewId = ViewCompat.generateViewId()
|
||||||
|
private var passkeyImageId = ViewCompat.generateViewId()
|
||||||
|
|
||||||
|
private var passkeyImage = AppCompatImageView(
|
||||||
|
ContextThemeWrapper(context, R.style.KeepassDXStyle_ImageButton_Simple), null, 0).apply {
|
||||||
|
layoutParams = LayoutParams(
|
||||||
|
LayoutParams.WRAP_CONTENT,
|
||||||
|
LayoutParams.WRAP_CONTENT)
|
||||||
|
setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_passkey_white_24dp))
|
||||||
|
contentDescription = context.getString(R.string.passkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val relyingPartyView = AppCompatTextView(context).apply {
|
||||||
|
setTextAppearance(context,
|
||||||
|
R.style.KeepassDXStyle_TextAppearance_TextNodePrimary)
|
||||||
|
layoutParams = LayoutParams(
|
||||||
|
LayoutParams.MATCH_PARENT,
|
||||||
|
LayoutParams.WRAP_CONTENT
|
||||||
|
).also {
|
||||||
|
it.topMargin = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
8f,
|
||||||
|
resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
it.leftMargin = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
8f,
|
||||||
|
resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
it.marginStart = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
8f,
|
||||||
|
resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
}
|
||||||
|
setTextIsSelectable(true)
|
||||||
|
}
|
||||||
|
private val usernameView = AppCompatTextView(context).apply {
|
||||||
|
setTextAppearance(context,
|
||||||
|
R.style.KeepassDXStyle_TextAppearance_TextNodeSecondary)
|
||||||
|
layoutParams = LayoutParams(
|
||||||
|
LayoutParams.MATCH_PARENT,
|
||||||
|
LayoutParams.WRAP_CONTENT).also {
|
||||||
|
it.topMargin = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
4f,
|
||||||
|
resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
it.leftMargin = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
8f,
|
||||||
|
resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
it.marginStart = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
8f,
|
||||||
|
resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
}
|
||||||
|
setTextIsSelectable(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildViews() {
|
||||||
|
indicatorDrawable?.let {
|
||||||
|
DrawableCompat.setTint(it, PasswordEntropy.Strength.VERY_UNGUESSABLE.color)
|
||||||
|
}
|
||||||
|
passkeyImage.apply {
|
||||||
|
id = passkeyImageId
|
||||||
|
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||||
|
it.addRule(ALIGN_PARENT_RIGHT)
|
||||||
|
it.addRule(ALIGN_PARENT_END)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
labelView.apply {
|
||||||
|
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||||
|
it.addRule(LEFT_OF, passkeyImageId)
|
||||||
|
it.addRule(START_OF, passkeyImageId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
relyingPartyView.apply {
|
||||||
|
id = relyingPartyViewId
|
||||||
|
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||||
|
it.addRule(LEFT_OF, passkeyImageId)
|
||||||
|
it.addRule(START_OF, passkeyImageId)
|
||||||
|
it.addRule(BELOW, labelViewId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
usernameView.apply {
|
||||||
|
id = usernameViewId
|
||||||
|
layoutParams = (layoutParams as LayoutParams?)?.also {
|
||||||
|
it.addRule(LEFT_OF, passkeyImageId)
|
||||||
|
it.addRule(START_OF, passkeyImageId)
|
||||||
|
it.addRule(BELOW, relyingPartyViewId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
removeAllViews()
|
||||||
|
buildViews()
|
||||||
|
addView(passkeyImage)
|
||||||
|
addView(labelView)
|
||||||
|
addView(relyingPartyView)
|
||||||
|
addView(usernameView)
|
||||||
|
}
|
||||||
|
|
||||||
|
override var default: String = ""
|
||||||
|
override var isFieldVisible: Boolean = true
|
||||||
|
|
||||||
|
var relyingParty: String
|
||||||
|
get() {
|
||||||
|
return relyingPartyView.text.toString()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
relyingPartyView.text = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var username: String
|
||||||
|
get() {
|
||||||
|
return usernameView.text.toString()
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
usernameView.text = value
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getEntropyStrength(passwordText: String) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ import com.kunzisoft.keepass.password.PasswordGenerator
|
|||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
|
|
||||||
|
|
||||||
class PasswordTextFieldView @JvmOverloads constructor(context: Context,
|
open class PasswordTextFieldView @JvmOverloads constructor(context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
defStyle: Int = 0)
|
defStyle: Int = 0)
|
||||||
: TextFieldView(context, attrs, defStyle) {
|
: TextFieldView(context, attrs, defStyle) {
|
||||||
@@ -45,7 +45,7 @@ class PasswordTextFieldView @JvmOverloads constructor(context: Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var indicatorDrawable = ContextCompat.getDrawable(
|
protected var indicatorDrawable = ContextCompat.getDrawable(
|
||||||
context,
|
context,
|
||||||
R.drawable.ic_shield_white_24dp
|
R.drawable.ic_shield_white_24dp
|
||||||
)?.apply {
|
)?.apply {
|
||||||
@@ -98,7 +98,7 @@ class PasswordTextFieldView @JvmOverloads constructor(context: Context,
|
|||||||
value = resources.getString(valueId)
|
value = resources.getString(valueId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getEntropyStrength(passwordText: String) {
|
protected open fun getEntropyStrength(passwordText: String) {
|
||||||
mPasswordEntropyCalculator.getEntropyStrength(passwordText) { entropyStrength ->
|
mPasswordEntropyCalculator.getEntropyStrength(passwordText) { entropyStrength ->
|
||||||
labelView.apply {
|
labelView.apply {
|
||||||
post {
|
post {
|
||||||
|
|||||||
@@ -534,10 +534,15 @@ abstract class TemplateAbstractView<
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected fun getCustomField(fieldName: String): Field {
|
protected fun getCustomField(fieldName: String): Field {
|
||||||
|
return getCustomFieldOrNull(fieldName)
|
||||||
|
?: Field(fieldName, ProtectedString(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun getCustomFieldOrNull(fieldName: String): Field? {
|
||||||
return getCustomField(fieldName,
|
return getCustomField(fieldName,
|
||||||
templateFieldNotEmpty = false,
|
templateFieldNotEmpty = false,
|
||||||
retrieveDefaultValues = false
|
retrieveDefaultValues = false
|
||||||
) ?: Field(fieldName, ProtectedString(false))
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCustomField(fieldName: String,
|
private fun getCustomField(fieldName: String,
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import com.kunzisoft.keepass.database.helper.getLocalizedName
|
|||||||
import com.kunzisoft.keepass.database.helper.isStandardPasswordName
|
import com.kunzisoft.keepass.database.helper.isStandardPasswordName
|
||||||
import com.kunzisoft.keepass.model.DataDate
|
import com.kunzisoft.keepass.model.DataDate
|
||||||
import com.kunzisoft.keepass.model.DataTime
|
import com.kunzisoft.keepass.model.DataTime
|
||||||
|
import com.kunzisoft.keepass.model.AppOriginEntryField
|
||||||
|
import com.kunzisoft.keepass.model.PasskeyEntryFields
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
|
|
||||||
|
|
||||||
@@ -256,9 +258,12 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
|
|||||||
override fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean,
|
override fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean,
|
||||||
retrieveDefaultValues: Boolean) {
|
retrieveDefaultValues: Boolean) {
|
||||||
super.populateEntryInfoWithViews(templateFieldNotEmpty, retrieveDefaultValues)
|
super.populateEntryInfoWithViews(templateFieldNotEmpty, retrieveDefaultValues)
|
||||||
mEntryInfo?.otpModel = OtpEntryFields.parseFields { key ->
|
val getField: (id: String) -> String? = { key ->
|
||||||
getCustomField(key).protectedValue.toString()
|
getCustomFieldOrNull(key)?.protectedValue?.stringValue
|
||||||
}?.otpModel
|
}
|
||||||
|
mEntryInfo?.otpModel = OtpEntryFields.parseFields(getField)?.otpModel
|
||||||
|
mEntryInfo?.passkey = PasskeyEntryFields.parseFields(getField)
|
||||||
|
mEntryInfo?.appOrigin = AppOriginEntryField.parseFields(getField)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestoreEntryInstanceState(state: SavedState) {
|
override fun onRestoreEntryInstanceState(state: SavedState) {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.kunzisoft.keepass.view
|
package com.kunzisoft.keepass.view
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
@@ -11,8 +10,11 @@ import com.kunzisoft.keepass.database.element.security.ProtectedString
|
|||||||
import com.kunzisoft.keepass.database.element.template.TemplateAttribute
|
import com.kunzisoft.keepass.database.element.template.TemplateAttribute
|
||||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
import com.kunzisoft.keepass.database.element.template.TemplateField
|
||||||
import com.kunzisoft.keepass.database.helper.getLocalizedName
|
import com.kunzisoft.keepass.database.helper.getLocalizedName
|
||||||
|
import com.kunzisoft.keepass.database.helper.isPasskeyLabel
|
||||||
import com.kunzisoft.keepass.database.helper.isStandardPasswordName
|
import com.kunzisoft.keepass.database.helper.isStandardPasswordName
|
||||||
import com.kunzisoft.keepass.model.OtpModel
|
import com.kunzisoft.keepass.model.OtpModel
|
||||||
|
import com.kunzisoft.keepass.model.Passkey
|
||||||
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.PASSKEY_FIELD
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
||||||
|
|
||||||
@@ -52,6 +54,8 @@ class TemplateView @JvmOverloads constructor(context: Context,
|
|||||||
return context?.let {
|
return context?.let {
|
||||||
(if (TemplateField.isStandardPasswordName(context, templateAttribute.label))
|
(if (TemplateField.isStandardPasswordName(context, templateAttribute.label))
|
||||||
PasswordTextFieldView(it)
|
PasswordTextFieldView(it)
|
||||||
|
else if (TemplateField.isPasskeyLabel(context, templateAttribute.label))
|
||||||
|
PasskeyTextFieldView(it)
|
||||||
else TextFieldView(it)).apply {
|
else TextFieldView(it)).apply {
|
||||||
applyFontVisibility(mFontInVisibility)
|
applyFontVisibility(mFontInVisibility)
|
||||||
setProtection(field.protectedValue.isProtected, mHideProtectedValue)
|
setProtection(field.protectedValue.isProtected, mHideProtectedValue)
|
||||||
@@ -123,20 +127,20 @@ class TemplateView @JvmOverloads constructor(context: Context,
|
|||||||
|
|
||||||
override fun populateViewsWithEntryInfo(showEmptyFields: Boolean): List<ViewField> {
|
override fun populateViewsWithEntryInfo(showEmptyFields: Boolean): List<ViewField> {
|
||||||
val emptyCustomFields = super.populateViewsWithEntryInfo(false)
|
val emptyCustomFields = super.populateViewsWithEntryInfo(false)
|
||||||
|
|
||||||
// Hide empty custom fields
|
// Hide empty custom fields
|
||||||
emptyCustomFields.forEach { customFieldId ->
|
emptyCustomFields.forEach { customFieldId ->
|
||||||
customFieldId.view.isVisible = false
|
customFieldId.view.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
removeOtpRunnable()
|
removeOtpRunnable()
|
||||||
mEntryInfo?.let { entryInfo ->
|
mEntryInfo?.let { entryInfo ->
|
||||||
// Assign specific OTP dynamic view
|
// Assign specific OTP dynamic view
|
||||||
entryInfo.otpModel?.let {
|
entryInfo.otpModel?.let {
|
||||||
assignOtp(it)
|
assignOtp(it)
|
||||||
}
|
}
|
||||||
|
entryInfo.passkey?.let {
|
||||||
|
assignPasskey(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return emptyCustomFields
|
return emptyCustomFields
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,6 +200,22 @@ class TemplateView @JvmOverloads constructor(context: Context,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getPasskeyView(): PasskeyTextFieldView? {
|
||||||
|
getViewFieldByName(PASSKEY_FIELD)?.let { viewField ->
|
||||||
|
val view = viewField.view
|
||||||
|
if (view is PasskeyTextFieldView)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assignPasskey(passkey: Passkey) {
|
||||||
|
getPasskeyView()?.apply {
|
||||||
|
relyingParty = passkey.relyingParty
|
||||||
|
username = passkey.username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun removeOtpRunnable() {
|
private fun removeOtpRunnable() {
|
||||||
mLastOtpTokenView?.removeCallbacks(mOtpRunnable)
|
mLastOtpTokenView?.removeCallbacks(mOtpRunnable)
|
||||||
mLastOtpTokenView = null
|
mLastOtpTokenView = null
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import android.util.AttributeSet
|
|||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.ContextThemeWrapper
|
import android.view.ContextThemeWrapper
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.View.OnClickListener
|
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.widget.AppCompatImageButton
|
import androidx.appcompat.widget.AppCompatImageButton
|
||||||
@@ -37,7 +36,7 @@ import androidx.core.text.util.LinkifyCompat
|
|||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.model.EntryInfo.Companion.APPLICATION_ID_FIELD_NAME
|
import com.kunzisoft.keepass.model.AppOriginEntryField.APPLICATION_ID_FIELD_NAME
|
||||||
import com.kunzisoft.keepass.utils.UriUtil.openExternalApp
|
import com.kunzisoft.keepass.utils.UriUtil.openExternalApp
|
||||||
|
|
||||||
|
|
||||||
@@ -46,7 +45,7 @@ open class TextFieldView @JvmOverloads constructor(context: Context,
|
|||||||
defStyle: Int = 0)
|
defStyle: Int = 0)
|
||||||
: RelativeLayout(context, attrs, defStyle), GenericTextFieldView {
|
: RelativeLayout(context, attrs, defStyle), GenericTextFieldView {
|
||||||
|
|
||||||
private var labelViewId = ViewCompat.generateViewId()
|
protected var labelViewId = ViewCompat.generateViewId()
|
||||||
private var valueViewId = ViewCompat.generateViewId()
|
private var valueViewId = ViewCompat.generateViewId()
|
||||||
private var showButtonId = ViewCompat.generateViewId()
|
private var showButtonId = ViewCompat.generateViewId()
|
||||||
private var copyButtonId = ViewCompat.generateViewId()
|
private var copyButtonId = ViewCompat.generateViewId()
|
||||||
|
|||||||
@@ -174,7 +174,8 @@ class EntryEditViewModel: NodeEditViewModel() {
|
|||||||
// Load entry info
|
// Load entry info
|
||||||
entry.getEntryInfo(database, true).let { tempEntryInfo ->
|
entry.getEntryInfo(database, true).let { tempEntryInfo ->
|
||||||
// Retrieve data from registration
|
// Retrieve data from registration
|
||||||
(registerInfo?.searchInfo ?: searchInfo)?.let { tempSearchInfo ->
|
// TODO only save registration
|
||||||
|
searchInfo?.let { tempSearchInfo ->
|
||||||
tempEntryInfo.saveSearchInfo(database, tempSearchInfo)
|
tempEntryInfo.saveSearchInfo(database, tempSearchInfo)
|
||||||
}
|
}
|
||||||
registerInfo?.let { regInfo ->
|
registerInfo?.let { regInfo ->
|
||||||
|
|||||||
10
app/src/main/res/drawable/ic_passkey_white_24dp.xml
Normal file
10
app/src/main/res/drawable/ic_passkey_white_24dp.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M3,20v-2.35c0,-0.633 0.158,-1.175 0.475,-1.625 0.317,-0.45 0.725,-0.792 1.225,-1.025 1.117,-0.5 2.188,-0.875 3.213,-1.125S9.967,13.5 11,13.5c0.433,0 0.854,0.021 1.263,0.063s0.829,0.104 1.263,0.188c-0.083,0.967 0.096,1.879 0.538,2.737C14.504,17.346 15.15,18.017 16,18.5v1.5L3,20ZM19,23.675 L17.5,22.175v-4.65c-0.733,-0.217 -1.333,-0.629 -1.8,-1.237 -0.467,-0.608 -0.7,-1.313 -0.7,-2.112 0,-0.967 0.342,-1.792 1.025,-2.475 0.683,-0.683 1.508,-1.025 2.475,-1.025s1.792,0.342 2.475,1.025c0.683,0.683 1.025,1.508 1.025,2.475 0,0.75 -0.213,1.417 -0.637,2 -0.425,0.583 -0.962,1 -1.612,1.25l1.25,1.25 -1.5,1.5 1.5,1.5 -2,2ZM11,11.5c-1.05,0 -1.938,-0.363 -2.662,-1.087 -0.725,-0.725 -1.087,-1.612 -1.087,-2.662s0.363,-1.938 1.087,-2.662C9.063,4.363 9.95,4 11,4s1.938,0.363 2.662,1.087c0.725,0.725 1.087,1.612 1.087,2.662s-0.363,1.938 -1.087,2.662C12.938,11.137 12.05,11.5 11,11.5ZM18.5,14.675c0.283,0 0.521,-0.096 0.712,-0.287S19.5,13.958 19.5,13.675c0,-0.283 -0.096,-0.521 -0.287,-0.712s-0.429,-0.287 -0.712,-0.287c-0.283,0 -0.521,0.096 -0.712,0.287S17.5,13.392 17.5,13.675c0,0.283 0.096,0.521 0.287,0.712s0.429,0.287 0.712,0.287Z"
|
||||||
|
android:strokeWidth="0.5"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
</vector>
|
||||||
@@ -159,6 +159,14 @@
|
|||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:src="@drawable/ic_attach_file_white_24dp" />
|
android:src="@drawable/ic_attach_file_white_24dp" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:id="@+id/node_passkey_icon"
|
||||||
|
style="@style/KeepassDXStyle.Icon.Entry"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:src="@drawable/ic_passkey_white_24dp" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatTextView
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:textColor="@color/grey_blue_slighter"/>
|
android:textColor="@color/grey_blue_slighter"/>
|
||||||
<com.kunzisoft.keepass.magikeyboard.KeyboardView
|
<com.kunzisoft.keepass.credentialprovider.magikeyboard.KeyboardView
|
||||||
android:id="@+id/magikeyboard_view"
|
android:id="@+id/magikeyboard_view"
|
||||||
style="@style/KeepassDXStyle.Keyboard"
|
style="@style/KeepassDXStyle.Keyboard"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -736,4 +736,17 @@
|
|||||||
<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="hide_templates_title">Hide templates</string>
|
<string name="hide_templates_title">Hide templates</string>
|
||||||
<string name="hide_templates_summary">Templates are not shown</string>
|
<string name="hide_templates_summary">Templates are not shown</string>
|
||||||
|
<string name="passkey">Passkey</string>
|
||||||
|
<string name="passkey_service_name">KeePassDX Credential Provider</string>
|
||||||
|
<string name="passkey_creation_description">Save passkey in new entry</string>
|
||||||
|
<string name="passkey_update_description">Update passkey in "%1$s"</string>
|
||||||
|
<string name="passkey_selection_username">No passkey found</string>
|
||||||
|
<string name="passkey_selection_description">Select an existing passkey</string>
|
||||||
|
<string name="passkey_locked_database_username">KeePassDX Database Locked</string>
|
||||||
|
<string name="passkey_locked_database_description">Select to unlock</string>
|
||||||
|
<string name="passkey_username">Passkey Username</string>
|
||||||
|
<string name="passkey_private_key">Passkey Private Key</string>
|
||||||
|
<string name="passkey_credential_id">Passkey Credential Id</string>
|
||||||
|
<string name="passkey_user_handle">Passkey User Handle</string>
|
||||||
|
<string name="passkey_relying_party">Passkey Relying Party</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -567,6 +567,13 @@
|
|||||||
<item name="android:paddingEnd">8dp</item>
|
<item name="android:paddingEnd">8dp</item>
|
||||||
<item name="android:textSize">16sp</item>
|
<item name="android:textSize">16sp</item>
|
||||||
</style>
|
</style>
|
||||||
|
<style name="KeepassDXStyle.TextAppearance.TextNodePrimary" parent="KeepassDXStyle.TextAppearance.TextNode">
|
||||||
|
<item name="android:textSize">14sp</item>
|
||||||
|
<item name="android:textStyle">bold</item>
|
||||||
|
</style>
|
||||||
|
<style name="KeepassDXStyle.TextAppearance.TextNodeSecondary" parent="KeepassDXStyle.TextAppearance.TextNode">
|
||||||
|
<item name="android:textSize">14sp</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<!-- Snackbar -->
|
<!-- Snackbar -->
|
||||||
<style name="KeepassDXStyle.SnackBar" parent="Widget.Material3.Snackbar">
|
<style name="KeepassDXStyle.SnackBar" parent="Widget.Material3.Snackbar">
|
||||||
|
|||||||
6
app/src/main/res/xml/provider.xml
Normal file
6
app/src/main/res/xml/provider.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<credential-provider>
|
||||||
|
<capabilities>
|
||||||
|
<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.
|
// 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'
|
||||||
|
|||||||
@@ -36,12 +36,17 @@ android {
|
|||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "17"
|
jvmTarget = "17"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
packaging {
|
||||||
|
resources.excludes.add("META-INF/versions/9/OSGI-INF/MANIFEST.MF") // bouncycastle need this
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Crypto
|
// Crypto
|
||||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
|
implementation 'org.bouncycastle:bcpkix-jdk18on:1.81'
|
||||||
|
|
||||||
|
//androidTestImplementation 'org.testng:testng:6.9.6'
|
||||||
androidTestImplementation "androidx.test:runner:$android_test_version"
|
androidTestImplementation "androidx.test:runner:$android_test_version"
|
||||||
testImplementation "androidx.test:runner:$android_test_version"
|
testImplementation "androidx.test:runner:$android_test_version"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
package com.kunzisoft.encrypt
|
||||||
|
|
||||||
|
import com.kunzisoft.encrypt.HashManager.fingerprintToUrlSafeBase64
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class SignatureTest {
|
||||||
|
|
||||||
|
// All private keys are for testing only.
|
||||||
|
// DO NOT USE THEM
|
||||||
|
|
||||||
|
// region ES256
|
||||||
|
private val es256PemInKeePassXC =
|
||||||
|
"""
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgaIrmuL+0IpvMpZ4O
|
||||||
|
8+CpXEzVNoyNkhquyRqD8CtVWDmhRANCAARyucecj8E9YvcAZHEYgElcLjwLMWmM
|
||||||
|
vQ2BDZPVL4pLG1oBZer1mPEEQV7LzwGYvTzV/eb9GlXPwj/4la/bpVp1
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
""".trimIndent().trim()
|
||||||
|
|
||||||
|
private val es256PemInKeePassDX = """
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgaIrmuL+0IpvMpZ4O
|
||||||
|
8+CpXEzVNoyNkhquyRqD8CtVWDmgCgYIKoZIzj0DAQehRANCAARyucecj8E9YvcA
|
||||||
|
ZHEYgElcLjwLMWmMvQ2BDZPVL4pLG1oBZer1mPEEQV7LzwGYvTzV/eb9GlXPwj/4
|
||||||
|
la/bpVp1
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
""".trimIndent().trim()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testEC256KeyConversionKeypassXCIn() {
|
||||||
|
val privateKey = Signature.createPrivateKey(es256PemInKeePassXC)
|
||||||
|
val pemOut = Signature.convertPrivateKeyToPem(privateKey)
|
||||||
|
|
||||||
|
assert(pemOut == es256PemInKeePassDX)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testEC256KeyConversionKeePassDXIn() {
|
||||||
|
val privateKey = Signature.createPrivateKey(es256PemInKeePassDX)
|
||||||
|
val pemOut = Signature.convertPrivateKeyToPem(privateKey)
|
||||||
|
|
||||||
|
assert(pemOut == es256PemInKeePassDX)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testEC256KeyGenAndConversion() {
|
||||||
|
val (keyPair, keyTypeId) = Signature.generateKeyPair(listOf(Signature.ES256_ALGORITHM))!!
|
||||||
|
val privateKeyPem = Signature.convertPrivateKeyToPem(keyPair.private)
|
||||||
|
|
||||||
|
assert(keyTypeId == Signature.ES256_ALGORITHM)
|
||||||
|
assert(privateKeyPem.contains("-----BEGIN PRIVATE KEY-----", true))
|
||||||
|
assert( privateKeyPem.contains("-----BEGIN EC PRIVATE KEY-----", true).not())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region RSA
|
||||||
|
|
||||||
|
private val rsa256PemIn = """
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCaunVJEhLHl7/f
|
||||||
|
NZufOmj4MY/1J/YHgMAZYFBORQVm58psUjCU7jIww+BK5aRGShdumRzbxr1Yqyh6
|
||||||
|
yvWbv2u9l6cOdtQKFtXDsWtP9tMBqqhODhG30gE3rEt5l2k1CSzSO9sGghUxlb2i
|
||||||
|
Q9fiSQ4HmiEc+cXbsSbeYsWwGYNYNhPdJ7vwsZsXzmD0RPpcxy0uJatASjWx3lXF
|
||||||
|
+eprpcZUr0NLlGIob6VfCt0q2ZfoeEHcEcp4qQ9nI+hOLPFzp/x1TFLX8wKvmwRh
|
||||||
|
ifwyAauVIaDZaQvAeoBuV2hSBl596ujiJt2Kd3pbQ4SjD1MXudbVvGkofgSZNR0f
|
||||||
|
ai6f6POFAgMBAAECggEAFtSIVb3K85RahU7dpYLy1hxKB3xb+wNuVNA3STU59NMi
|
||||||
|
tRTzgiYbVcKxJ5v2v0BTcMg6z9rlOV4X3PZxgwedmB32UlYKN2rjI7rcALKEs+xA
|
||||||
|
ZTQCPUNJVrOfd1N1/JNb/7FBQhaTlftoPbcQ9Zyd61U8qY/ZN+9NsuaUEMXS8YLe
|
||||||
|
cqlwJjRcWh3PuTQ+qeVw5l6lgK4XEyDbh/Aj9DGgwVsAkwGdXpuQRBQr8UClO/he
|
||||||
|
2iOwkn4LJ5nnXwByMpEct03+eUj0kxlijunYbBnKJfRv6tz+ZcpZoc1EqeWbrTB0
|
||||||
|
eKf+R6N8MHgJSemVVGZvgsUYbfMqkJA/LNOyIpQ/wQKBgQDJhowHVDtze+FQrunR
|
||||||
|
NchOXgZNWTFf3ZITxxnWnTgumtKdg3MkxeKEBCzAqefb6n6zi2rQzP/PAZAKT/we
|
||||||
|
YP24hwUVeFePH9/Llf5QuCOWGtkZbNRFSCHWcbfRQAL4vfPJk79bhwCoC5wQ5uk1
|
||||||
|
atCA+dln6b3wDXq2bvBs6Rj7bQKBgQDEjZIMMgYoEq6yKCFK+11BFo3sj3rmbCcE
|
||||||
|
tu29mXBfromgzfL0NLoqUAB5OsYKO1nl7eQp2QVIgdLLs8BwTKkel4gosK9B5T4C
|
||||||
|
umFG0yGIOJz7twA5joLuZAFsoPazj6yHUXaFJaye2P5KwVCL9ws5V2WKgnsF/hKe
|
||||||
|
QWwSIjtxeQKBgAbyR1NdWOtDIuIYFWErvGrPHOJ/p48JYSajX0Whh7U7ivT4+fgT
|
||||||
|
hhpM1ooRkTdoXtOrg5QM7OhiwmdImIUnjLdWmBtEWahKTfmDgw+fOULMTB1vPeXh
|
||||||
|
daEhrFdfIHsYeRXCrP7nqWMhe1Ct1O4Nb4BynEbTrMNgg5FUQ59NbZoFAoGAXNNb
|
||||||
|
YSUS4UQJexwWtRnHbeDgABO3ADGdr81QtBVOC/IbD3WUQx7PuQH1Z0uJkfV7vGpA
|
||||||
|
Mj9LDnY5fniS7rZVvJvl8wmWi3FfetxY6qD1mibahMplcclLLpjOT2YpfJ3i5jlj
|
||||||
|
1vf28UIbvmRTzPZMN7V9wA9lWGwokNLm3h2Ko0kCgYBq0NEd+VMkuIXuGz6j2IXC
|
||||||
|
qjKf187RZAn2B7otXoumCze+uxm4N0PyOYb1fNGHeE8/RjNQO7VmzZg/dMrk9ZJh
|
||||||
|
ueHJgOLTbDdlQCUacSipHGmWMN9E+EjgBRiqmPZzV6dq/kGc2FUSGB22wY8gckEX
|
||||||
|
AmqgkPgYHZ/VzFPTrp97IQ==
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
""".trimIndent().trim()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRS256KeyConversion() {
|
||||||
|
val privateKey = Signature.createPrivateKey(rsa256PemIn)
|
||||||
|
val pemOut = Signature.convertPrivateKeyToPem(privateKey)
|
||||||
|
|
||||||
|
assert(pemOut == rsa256PemIn)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRS256KeyGenAndConversion() {
|
||||||
|
val (keyPair, keyTypeId) = Signature.generateKeyPair(listOf(Signature.RS256_ALGORITHM))!!
|
||||||
|
val privateKeyPem = Signature.convertPrivateKeyToPem(keyPair.private)
|
||||||
|
|
||||||
|
assert(keyTypeId == Signature.RS256_ALGORITHM)
|
||||||
|
assert(privateKeyPem.contains("-----BEGIN PRIVATE KEY-----", true))
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region ED25519
|
||||||
|
|
||||||
|
private val ed25519PemInShort = """
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MC4CAQAwBQYDK2VwBCIEILBoCo4+IXxIuwN36/oaEsPgbe6WYJcV9YW+xnprDF4H
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
private val ed25519PemInLong = """
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MFECAQEwBQYDK2VwBCIEIESP8edVGbqoR/pKNmy7j7FV8Y68zrIi/5VEuAJ281K6
|
||||||
|
gSEAyJU1wQNaJUeyxPcWjN7xZKZUhCRoIFS/MQvbdd4QE7Q=
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
private val ed25519PemOut = """
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MC4CAQAwBQYDK2VwBCIEIESP8edVGbqoR/pKNmy7j7FV8Y68zrIi/5VEuAJ281K6
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testEd25519KeyConverionShortIn() {
|
||||||
|
val privateKey = Signature.createPrivateKey(ed25519PemInShort)
|
||||||
|
val pemOut = Signature.convertPrivateKeyToPem(privateKey)
|
||||||
|
|
||||||
|
assert(pemOut == ed25519PemInShort)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testEd25519KeyConverionLongIn() {
|
||||||
|
val privateKey = Signature.createPrivateKey(ed25519PemInLong)
|
||||||
|
val pemOut = Signature.convertPrivateKeyToPem(privateKey)
|
||||||
|
|
||||||
|
assert(pemOut == ed25519PemOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testEd25519KeyGenAndConversion() {
|
||||||
|
val (keyPair, keyTypeId) = Signature.generateKeyPair(listOf(Signature.ED_DSA_ALGORITHM))!!
|
||||||
|
val privateKeyPem = Signature.convertPrivateKeyToPem(keyPair.private)
|
||||||
|
|
||||||
|
assert(keyTypeId == Signature.ED_DSA_ALGORITHM)
|
||||||
|
assert(privateKeyPem.contains("-----BEGIN PRIVATE KEY-----", true))
|
||||||
|
assert(privateKeyPem.contains("-----BEGIN EC PRIVATE KEY-----", true).not())
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSingleSignature() {
|
||||||
|
// Generate random input
|
||||||
|
val fingerprint = "A7:5C:63:72:A0:B6:7D:B0:16:86:B4:7D:F6:8C:91:51:6E:E1:62:29:EE:C4:C0:C6:7D:35:5E:32:20:7C:66:17"
|
||||||
|
val expected = "p1xjcqC2fbAWhrR99oyRUW7hYinuxMDGfTVeMiB8Zhc"
|
||||||
|
|
||||||
|
Assert.assertEquals("Check fingerprint app", expected, fingerprintToUrlSafeBase64(fingerprint))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMultipleSignature() {
|
||||||
|
// Generate random input
|
||||||
|
val fingerprint = "A7:5C:63:72:A0:B6:7D:B0:16:86:B4:7D:F6:8C:91:51:6E:E1:62:29:EE:C4:C0:C6:7D:35:5E:32:20:7C:66:17##SIG##DB:25:8A:A6:19:08:9B:D1:3D:BA:71:9E:5A:DA:EC:FF:7F:12:C8:8F:67:AD:68:3C:1F:BC:F2:28:B3:88:BD:91"
|
||||||
|
val expected = "p1xjcqC2fbAWhrR99oyRUW7hYinuxMDGfTVeMiB8Zhc"
|
||||||
|
|
||||||
|
Assert.assertEquals("Check fingerprint app", expected, fingerprintToUrlSafeBase64(fingerprint))
|
||||||
|
}
|
||||||
|
}
|
||||||
23
crypto/src/main/java/com/kunzisoft/encrypt/Base64Helper.kt
Normal file
23
crypto/src/main/java/com/kunzisoft/encrypt/Base64Helper.kt
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package com.kunzisoft.encrypt
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
|
||||||
|
class Base64Helper {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun b64Decode(encodedString: String): ByteArray {
|
||||||
|
return Base64.decode(
|
||||||
|
encodedString,
|
||||||
|
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun b64Encode(data: ByteArray): String {
|
||||||
|
return Base64.encodeToString(
|
||||||
|
data,
|
||||||
|
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,11 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.encrypt
|
package com.kunzisoft.encrypt
|
||||||
|
|
||||||
|
import android.content.pm.Signature
|
||||||
|
import android.content.pm.SigningInfo
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.AndroidException
|
||||||
|
import android.util.Log
|
||||||
import org.bouncycastle.crypto.engines.ChaCha7539Engine
|
import org.bouncycastle.crypto.engines.ChaCha7539Engine
|
||||||
import org.bouncycastle.crypto.engines.Salsa20Engine
|
import org.bouncycastle.crypto.engines.Salsa20Engine
|
||||||
import org.bouncycastle.crypto.params.KeyParameter
|
import org.bouncycastle.crypto.params.KeyParameter
|
||||||
@@ -26,9 +31,14 @@ import org.bouncycastle.crypto.params.ParametersWithIV
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import java.security.cert.CertificateFactory
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
object HashManager {
|
object HashManager {
|
||||||
|
|
||||||
|
private val TAG = HashManager::class.simpleName
|
||||||
|
|
||||||
fun getHash256(): MessageDigest {
|
fun getHash256(): MessageDigest {
|
||||||
val messageDigest: MessageDigest
|
val messageDigest: MessageDigest
|
||||||
try {
|
try {
|
||||||
@@ -107,4 +117,115 @@ object HashManager {
|
|||||||
|
|
||||||
return StreamCipher(cipher)
|
return StreamCipher(cipher)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const val SIGNATURE_DELIMITER = "##SIG##"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Signature object into its SHA-256 fingerprint string.
|
||||||
|
* The fingerprint is typically represented as uppercase hex characters separated by colons.
|
||||||
|
*/
|
||||||
|
private fun signatureToSha256Fingerprint(signature: Signature): String? {
|
||||||
|
return try {
|
||||||
|
val certificateFactory = CertificateFactory.getInstance("X.509")
|
||||||
|
val x509Certificate = certificateFactory.generateCertificate(
|
||||||
|
signature.toByteArray().inputStream()
|
||||||
|
) as X509Certificate
|
||||||
|
|
||||||
|
val messageDigest = MessageDigest.getInstance("SHA-256")
|
||||||
|
val digest = messageDigest.digest(x509Certificate.encoded)
|
||||||
|
|
||||||
|
// Format as colon-separated HEX uppercase string
|
||||||
|
digest.joinToString(separator = ":") { byte -> "%02X".format(byte) }
|
||||||
|
.uppercase(Locale.US)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("SigningInfoUtil", "Error converting signature to SHA-256 fingerprint", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all relevant SHA-256 signature fingerprints for a given package.
|
||||||
|
*
|
||||||
|
* @param signingInfo The SigningInfo object to retrieve the strings signatures
|
||||||
|
* @return A List of SHA-256 fingerprint strings, or null if an error occurs or no signatures are found.
|
||||||
|
*/
|
||||||
|
fun getAllFingerprints(signingInfo: SigningInfo?): List<String>? {
|
||||||
|
try {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
|
||||||
|
throw AndroidException("API level ${Build.VERSION.SDK_INT} not supported")
|
||||||
|
val signatures = mutableSetOf<String>()
|
||||||
|
if (signingInfo != null) {
|
||||||
|
// Includes past and current keys if rotation occurred. This is generally preferred.
|
||||||
|
signingInfo.signingCertificateHistory?.forEach { signature ->
|
||||||
|
signatureToSha256Fingerprint(signature)?.let { signatures.add(it) }
|
||||||
|
}
|
||||||
|
// If only one signer and history is empty (e.g. new app), this might be needed.
|
||||||
|
// Or if multiple signers are explicitly used for the APK content.
|
||||||
|
if (signingInfo.hasMultipleSigners()) {
|
||||||
|
signingInfo.apkContentsSigners?.forEach { signature ->
|
||||||
|
signatureToSha256Fingerprint(signature)?.let { signatures.add(it) }
|
||||||
|
}
|
||||||
|
} else { // Fallback for single signer if history was somehow null/empty
|
||||||
|
signingInfo.signingCertificateHistory?.firstOrNull()?.let {
|
||||||
|
signatureToSha256Fingerprint(it)?.let { fp -> signatures.add(fp) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return if (signatures.isEmpty()) null else signatures.toList()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error getting signatures", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines a list of signature into a single string for database storage.
|
||||||
|
*
|
||||||
|
* @return A single string with fingerprints joined by a ##SIG## delimiter,
|
||||||
|
* or null if the input list is null or empty.
|
||||||
|
*/
|
||||||
|
fun SigningInfo.getApplicationFingerprints(): String? {
|
||||||
|
val fingerprints = getAllFingerprints(this)
|
||||||
|
if (fingerprints.isNullOrEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return fingerprints.joinToString(SIGNATURE_DELIMITER)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms a colon-separated hex fingerprint string into a URL-safe,
|
||||||
|
* padding-removed Base64 string, mimicking the Python behavior:
|
||||||
|
* base64.urlsafe_b64encode(binascii.a2b_hex(fingerprint.replace(':', ''))).decode('utf8').replace('=', '')
|
||||||
|
*
|
||||||
|
* Only check the first footprint if there are several delimited by ##SIG##.
|
||||||
|
*
|
||||||
|
* @param fingerprint The colon-separated hex fingerprint string (e.g., "91:F7:CB:...").
|
||||||
|
* @return The Android App Origin string.
|
||||||
|
* @throws IllegalArgumentException if the hex string (after removing colons) has an odd length
|
||||||
|
* or contains non-hex characters.
|
||||||
|
*/
|
||||||
|
fun fingerprintToUrlSafeBase64(fingerprint: String): String {
|
||||||
|
val firstFingerprint = fingerprint.split(SIGNATURE_DELIMITER).firstOrNull()?.trim()
|
||||||
|
if (firstFingerprint.isNullOrEmpty()) {
|
||||||
|
throw IllegalArgumentException("Invalid fingerprint $fingerprint")
|
||||||
|
}
|
||||||
|
val hexStringNoColons = firstFingerprint.replace(":", "")
|
||||||
|
if (hexStringNoColons.length % 2 != 0) {
|
||||||
|
throw IllegalArgumentException("Hex string must have an even number of characters: $hexStringNoColons")
|
||||||
|
}
|
||||||
|
if (hexStringNoColons.length != 64) {
|
||||||
|
throw IllegalArgumentException("Expected a 64-character hex string for a SHA-256 hash, but got ${hexStringNoColons.length} characters.")
|
||||||
|
}
|
||||||
|
val hashBytes = ByteArray(hexStringNoColons.length / 2)
|
||||||
|
for (i in hashBytes.indices) {
|
||||||
|
try {
|
||||||
|
val index = i * 2
|
||||||
|
val byteValue = hexStringNoColons.substring(index, index + 2).toInt(16)
|
||||||
|
hashBytes[i] = byteValue.toByte()
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
throw IllegalArgumentException("Invalid hex character in fingerprint: $hexStringNoColons", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Base64Helper.b64Encode(hashBytes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
237
crypto/src/main/java/com/kunzisoft/encrypt/Signature.kt
Normal file
237
crypto/src/main/java/com/kunzisoft/encrypt/Signature.kt
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
package com.kunzisoft.encrypt
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
|
||||||
|
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters
|
||||||
|
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey
|
||||||
|
import org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPrivateKey
|
||||||
|
import org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPublicKey
|
||||||
|
import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPublicKey
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||||
|
import org.bouncycastle.openssl.PEMParser
|
||||||
|
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter
|
||||||
|
import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator
|
||||||
|
import org.bouncycastle.util.BigIntegers
|
||||||
|
import org.bouncycastle.util.io.pem.PemWriter
|
||||||
|
import java.io.StringReader
|
||||||
|
import java.io.StringWriter
|
||||||
|
import java.security.KeyPair
|
||||||
|
import java.security.KeyPairGenerator
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.security.Security
|
||||||
|
import java.security.Signature
|
||||||
|
import java.security.spec.ECGenParameterSpec
|
||||||
|
|
||||||
|
class Signature {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
// see at https://www.iana.org/assignments/cose/cose.xhtml
|
||||||
|
const val ES256_ALGORITHM: Long = -7
|
||||||
|
const val RS256_ALGORITHM: Long = -257
|
||||||
|
private const val RS256_KEY_SIZE_IN_BITS = 2048
|
||||||
|
|
||||||
|
const val ED_DSA_ALGORITHM: Long = -8
|
||||||
|
|
||||||
|
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"
|
||||||
|
"Ed25519" -> "Ed25519"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (algorithmSignature == null) {
|
||||||
|
Log.e(this::class.java.simpleName, "sign: the algorithm $algorithmKey is unknown")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val sig = Signature.getInstance(algorithmSignature, BouncyCastleProvider.PROVIDER_NAME)
|
||||||
|
sig.initSign(privateKey)
|
||||||
|
sig.update(message)
|
||||||
|
return sig.sign()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createPrivateKey(privateKeyPem: String): PrivateKey {
|
||||||
|
val targetReader = StringReader(privateKeyPem)
|
||||||
|
val pemParser = PEMParser(targetReader)
|
||||||
|
val privateKeyInfo = pemParser.readObject() as PrivateKeyInfo
|
||||||
|
val privateKey = JcaPEMKeyConverter().getPrivateKey(privateKeyInfo)
|
||||||
|
pemParser.close()
|
||||||
|
targetReader.close()
|
||||||
|
return privateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
fun convertPrivateKeyToPem(privateKey: PrivateKey): String {
|
||||||
|
var useV1Info = false
|
||||||
|
if (privateKey is BCEdDSAPrivateKey) {
|
||||||
|
// to generate PEM, which are compatible to KeepassXC
|
||||||
|
useV1Info = true
|
||||||
|
}
|
||||||
|
System.setProperty(
|
||||||
|
"org.bouncycastle.pkcs8.v1_info_only",
|
||||||
|
useV1Info.toString().lowercase()
|
||||||
|
)
|
||||||
|
|
||||||
|
val noOutputEncryption = null
|
||||||
|
val pemObjectGenerator = JcaPKCS8Generator(privateKey, noOutputEncryption)
|
||||||
|
|
||||||
|
val writer = StringWriter()
|
||||||
|
val pemWriter = PemWriter(writer)
|
||||||
|
pemWriter.writeObject(pemObjectGenerator)
|
||||||
|
pemWriter.close()
|
||||||
|
|
||||||
|
val privateKeyInPem = writer.toString().trim()
|
||||||
|
writer.close()
|
||||||
|
return privateKeyInPem
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateKeyPair(keyTypeIdList: List<Long>): Pair<KeyPair, Long>? {
|
||||||
|
|
||||||
|
for (typeId in keyTypeIdList) {
|
||||||
|
if (typeId == ES256_ALGORITHM) {
|
||||||
|
val es256CurveNameBC = "secp256r1"
|
||||||
|
val spec = ECGenParameterSpec(es256CurveNameBC)
|
||||||
|
val keyPairGen =
|
||||||
|
KeyPairGenerator.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME)
|
||||||
|
keyPairGen.initialize(spec)
|
||||||
|
val keyPair = keyPairGen.genKeyPair()
|
||||||
|
return Pair(keyPair, ES256_ALGORITHM)
|
||||||
|
|
||||||
|
} else if (typeId == RS256_ALGORITHM) {
|
||||||
|
val keyPairGen =
|
||||||
|
KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME)
|
||||||
|
keyPairGen.initialize(RS256_KEY_SIZE_IN_BITS)
|
||||||
|
val keyPair = keyPairGen.genKeyPair()
|
||||||
|
return Pair(keyPair, RS256_ALGORITHM)
|
||||||
|
|
||||||
|
} else if (typeId == ED_DSA_ALGORITHM) {
|
||||||
|
val keyPairGen =
|
||||||
|
KeyPairGenerator.getInstance("Ed25519", BouncyCastleProvider.PROVIDER_NAME)
|
||||||
|
val keyPair = keyPairGen.genKeyPair()
|
||||||
|
return Pair(keyPair, ED_DSA_ALGORITHM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.e(this::class.java.simpleName, "generateKeyPair: no known key type id found")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun convertPublicKey(publicKeyIn: PublicKey, keyTypeId: Long): ByteArray? {
|
||||||
|
if (keyTypeId == ES256_ALGORITHM) {
|
||||||
|
if (publicKeyIn is BCECPublicKey) {
|
||||||
|
publicKeyIn.setPointFormat("UNCOMPRESSED")
|
||||||
|
return publicKeyIn.encoded
|
||||||
|
}
|
||||||
|
} else if (keyTypeId == RS256_ALGORITHM) {
|
||||||
|
return publicKeyIn.encoded
|
||||||
|
} else if (keyTypeId == ED_DSA_ALGORITHM) {
|
||||||
|
return publicKeyIn.encoded
|
||||||
|
}
|
||||||
|
Log.e(this::class.java.simpleName, "convertPublicKey: unknown key type id found")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun convertPublicKeyToMap(publicKeyIn: PublicKey, keyTypeId: Long): Map<Int, Any>? {
|
||||||
|
|
||||||
|
// https://www.iana.org/assignments/cose/cose.xhtml#key-common-parameters
|
||||||
|
val keyTypeLabel = 1
|
||||||
|
val algorithmLabel = 3
|
||||||
|
|
||||||
|
if (keyTypeId == ES256_ALGORITHM) {
|
||||||
|
if (publicKeyIn !is BCECPublicKey) {
|
||||||
|
Log.e(
|
||||||
|
this::class.java.simpleName,
|
||||||
|
"publicKey object has wrong type for keyTypeId $ES256_ALGORITHM: ${publicKeyIn.javaClass.canonicalName}"
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// constants see at https://w3c.github.io/webauthn/#example-bdbd14cc
|
||||||
|
val publicKeyMap = mutableMapOf<Int, Any>()
|
||||||
|
|
||||||
|
val es256KeyTypeId = 2
|
||||||
|
val es256EllipticCurveP256Id = 1
|
||||||
|
|
||||||
|
publicKeyMap[keyTypeLabel] = es256KeyTypeId
|
||||||
|
publicKeyMap[algorithmLabel] = ES256_ALGORITHM
|
||||||
|
|
||||||
|
publicKeyMap[-1] = es256EllipticCurveP256Id
|
||||||
|
|
||||||
|
val ecPoint = publicKeyIn.q
|
||||||
|
publicKeyMap[-2] = ecPoint.xCoord.encoded
|
||||||
|
publicKeyMap[-3] = ecPoint.yCoord.encoded
|
||||||
|
|
||||||
|
return publicKeyMap
|
||||||
|
|
||||||
|
} else if (keyTypeId == RS256_ALGORITHM) {
|
||||||
|
if (publicKeyIn !is BCRSAPublicKey) {
|
||||||
|
Log.e(
|
||||||
|
this::class.java.simpleName,
|
||||||
|
"publicKey object has wrong type for keyTypeId $RS256_ALGORITHM: ${publicKeyIn.javaClass.canonicalName}"
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// constants see at https://w3c.github.io/webauthn/#example-8dfabc00
|
||||||
|
|
||||||
|
val rs256KeySizeInBytes = RS256_KEY_SIZE_IN_BITS / 8
|
||||||
|
val rs256KeyTypeId = 3
|
||||||
|
val rs256ExponentSizeInBytes = 3
|
||||||
|
|
||||||
|
val publicKeyMap = mutableMapOf<Int, Any>()
|
||||||
|
publicKeyMap[keyTypeLabel] = rs256KeyTypeId
|
||||||
|
publicKeyMap[algorithmLabel] = RS256_ALGORITHM
|
||||||
|
publicKeyMap[-1] =
|
||||||
|
BigIntegers.asUnsignedByteArray(rs256KeySizeInBytes, publicKeyIn.modulus)
|
||||||
|
publicKeyMap[-2] =
|
||||||
|
BigIntegers.asUnsignedByteArray(
|
||||||
|
rs256ExponentSizeInBytes,
|
||||||
|
publicKeyIn.publicExponent
|
||||||
|
)
|
||||||
|
return publicKeyMap
|
||||||
|
} else if (keyTypeId == ED_DSA_ALGORITHM) {
|
||||||
|
if (publicKeyIn !is BCEdDSAPublicKey) {
|
||||||
|
Log.e(
|
||||||
|
this::class.java.simpleName,
|
||||||
|
"publicKey object has wrong type for keyTypeId $ED_DSA_ALGORITHM: ${publicKeyIn.javaClass.canonicalName}"
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val publicKeyMap = mutableMapOf<Int, Any>()
|
||||||
|
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc9053#name-key-object-parameters
|
||||||
|
val octetKeyPairId = 1
|
||||||
|
|
||||||
|
val curveLabel = -1
|
||||||
|
val ed25519CurveId = 6
|
||||||
|
|
||||||
|
val publicKeyLabel = -2
|
||||||
|
|
||||||
|
publicKeyMap[keyTypeLabel] = octetKeyPairId
|
||||||
|
publicKeyMap[algorithmLabel] = ED_DSA_ALGORITHM
|
||||||
|
|
||||||
|
publicKeyMap[curveLabel] = ed25519CurveId
|
||||||
|
|
||||||
|
val length = Ed25519PublicKeyParameters.KEY_SIZE
|
||||||
|
|
||||||
|
publicKeyMap[publicKeyLabel] = BigIntegers.asUnsignedByteArray(
|
||||||
|
length,
|
||||||
|
BigIntegers.fromUnsignedByteArray(publicKeyIn.pointEncoding)
|
||||||
|
)
|
||||||
|
|
||||||
|
return publicKeyMap
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.e(this::class.java.simpleName, "convertPublicKeyToMap: no known key type id found")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
|
apply plugin: 'kotlin-parcelize'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.kunzisoft.keepass.database'
|
namespace 'com.kunzisoft.keepass.database'
|
||||||
@@ -41,7 +42,7 @@ dependencies {
|
|||||||
implementation 'commons-io:commons-io:2.8.0'
|
implementation 'commons-io:commons-io:2.8.0'
|
||||||
implementation 'commons-codec:commons-codec:1.15'
|
implementation 'commons-codec:commons-codec:1.15'
|
||||||
|
|
||||||
implementation project(path: ':crypto')
|
api project(path: ':crypto')
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
androidTestImplementation "androidx.test:runner:$android_test_version"
|
androidTestImplementation "androidx.test:runner:$android_test_version"
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import com.kunzisoft.keepass.database.merge.DatabaseKDBXMerger
|
|||||||
import com.kunzisoft.keepass.database.search.SearchHelper
|
import com.kunzisoft.keepass.database.search.SearchHelper
|
||||||
import com.kunzisoft.keepass.database.search.SearchParameters
|
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
import com.kunzisoft.keepass.tasks.ProgressTaskUpdater
|
||||||
import com.kunzisoft.keepass.utils.SingletonHolder
|
import com.kunzisoft.keepass.utils.SingletonHolder
|
||||||
import com.kunzisoft.keepass.utils.StringUtil.toFormattedColorInt
|
import com.kunzisoft.keepass.utils.StringUtil.toFormattedColorInt
|
||||||
@@ -885,29 +886,15 @@ open class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun createVirtualGroupFromSearchInfo(
|
fun createVirtualGroupFromSearchInfo(
|
||||||
searchInfoString: String,
|
searchInfo: SearchInfo,
|
||||||
searchInfoByDomain: Boolean,
|
|
||||||
max: Int = Integer.MAX_VALUE
|
max: Int = Integer.MAX_VALUE
|
||||||
): Group? {
|
): Group? {
|
||||||
return mSearchHelper.createVirtualGroupWithSearchResult(this,
|
return mSearchHelper.createVirtualGroupWithSearchResult(
|
||||||
SearchParameters().apply {
|
database = this,
|
||||||
searchQuery = searchInfoString
|
searchParameters = searchInfo.buildSearchParameters(),
|
||||||
allowEmptyQuery = false
|
fromGroup = null,
|
||||||
searchInTitles = true
|
max = max
|
||||||
searchInUsernames = false
|
)
|
||||||
searchInPasswords = false
|
|
||||||
searchInUrls = true
|
|
||||||
searchByDomain = searchInfoByDomain
|
|
||||||
searchInNotes = true
|
|
||||||
searchInOTP = false
|
|
||||||
searchInOther = true
|
|
||||||
searchInUUIDs = false
|
|
||||||
searchInTags = false
|
|
||||||
searchInCurrentGroup = false
|
|
||||||
searchInSearchableGroup = true
|
|
||||||
searchInRecycleBin = false
|
|
||||||
searchInTemplates = false
|
|
||||||
}, null, max)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val tagPool: Tags
|
val tagPool: Tags
|
||||||
|
|||||||
@@ -33,12 +33,16 @@ import com.kunzisoft.keepass.database.element.node.Node
|
|||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.database.element.node.Type
|
import com.kunzisoft.keepass.database.element.node.Type
|
||||||
|
import com.kunzisoft.keepass.model.AppOrigin
|
||||||
|
import com.kunzisoft.keepass.model.AppOriginEntryField
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
|
import com.kunzisoft.keepass.model.Passkey
|
||||||
|
import com.kunzisoft.keepass.model.PasskeyEntryFields
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
import com.kunzisoft.keepass.utils.readParcelableCompat
|
|
||||||
import com.kunzisoft.keepass.utils.StringUtil.toFormattedColorInt
|
import com.kunzisoft.keepass.utils.StringUtil.toFormattedColorInt
|
||||||
import com.kunzisoft.keepass.utils.StringUtil.toFormattedColorString
|
import com.kunzisoft.keepass.utils.StringUtil.toFormattedColorString
|
||||||
|
import com.kunzisoft.keepass.utils.readParcelableCompat
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
class Entry : Node, EntryVersionedInterface<Group> {
|
class Entry : Node, EntryVersionedInterface<Group> {
|
||||||
@@ -354,6 +358,24 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getPasskey(): Passkey? {
|
||||||
|
entryKDBX?.let {
|
||||||
|
return PasskeyEntryFields.parseFields { key ->
|
||||||
|
it.getFieldValue(key)?.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAppOrigin(): AppOrigin? {
|
||||||
|
entryKDBX?.let {
|
||||||
|
return AppOriginEntryField.parseFields { key ->
|
||||||
|
it.getFieldValue(key)?.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
fun startToManageFieldReferences(database: DatabaseKDBX) {
|
fun startToManageFieldReferences(database: DatabaseKDBX) {
|
||||||
entryKDBX?.startToManageFieldReferences(database)
|
entryKDBX?.startToManageFieldReferences(database)
|
||||||
}
|
}
|
||||||
@@ -470,9 +492,13 @@ class Entry : Node, EntryVersionedInterface<Group> {
|
|||||||
entryInfo.customFields = getExtraFields().toMutableList()
|
entryInfo.customFields = getExtraFields().toMutableList()
|
||||||
// Add otpElement to generate token
|
// Add otpElement to generate token
|
||||||
entryInfo.otpModel = getOtpElement()?.otpModel
|
entryInfo.otpModel = getOtpElement()?.otpModel
|
||||||
|
// Add Passkey
|
||||||
|
entryInfo.passkey = getPasskey()
|
||||||
|
entryInfo.appOrigin = getAppOrigin()
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
// Replace parameter fields by generated OTP fields
|
// Replace parameter fields by generated OTP fields
|
||||||
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
|
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)
|
||||||
|
entryInfo.customFields = PasskeyEntryFields.generateAutoFields(entryInfo.customFields)
|
||||||
}
|
}
|
||||||
database?.attachmentPool?.let { binaryPool ->
|
database?.attachmentPool?.let { binaryPool ->
|
||||||
entryInfo.attachments = getAttachments(binaryPool).toMutableList()
|
entryInfo.attachments = getAttachments(binaryPool).toMutableList()
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ class Tags: Parcelable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun contains(tag: String): Boolean {
|
||||||
|
return mTags.contains(tag)
|
||||||
|
}
|
||||||
|
|
||||||
fun isEmpty(): Boolean {
|
fun isEmpty(): Boolean {
|
||||||
return mTags.isEmpty()
|
return mTags.isEmpty()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ package com.kunzisoft.keepass.database.element.entry
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
import com.kunzisoft.keepass.utils.UuidUtil
|
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||||
|
import com.kunzisoft.keepass.utils.UUIDUtils.asUUID
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
class FieldReferencesEngine(private val mDatabase: DatabaseKDBX) {
|
class FieldReferencesEngine(private val mDatabase: DatabaseKDBX) {
|
||||||
@@ -79,7 +80,7 @@ class FieldReferencesEngine(private val mDatabase: DatabaseKDBX) {
|
|||||||
'A' -> entryFound?.decodeUrlKey(newRecursionLevel)
|
'A' -> entryFound?.decodeUrlKey(newRecursionLevel)
|
||||||
'P' -> entryFound?.decodePasswordKey(newRecursionLevel)
|
'P' -> entryFound?.decodePasswordKey(newRecursionLevel)
|
||||||
'N' -> entryFound?.decodeNotesKey(newRecursionLevel)
|
'N' -> entryFound?.decodeNotesKey(newRecursionLevel)
|
||||||
'I' -> UuidUtil.toHexString(entryFound?.nodeId?.id)
|
'I' -> entryFound?.nodeId?.id?.asHexString()
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
refsCache[fullReference] = data
|
refsCache[fullReference] = data
|
||||||
@@ -127,7 +128,7 @@ class FieldReferencesEngine(private val mDatabase: DatabaseKDBX) {
|
|||||||
'P' -> mDatabase.getEntryByPassword(searchQuery, recursionLevel)
|
'P' -> mDatabase.getEntryByPassword(searchQuery, recursionLevel)
|
||||||
'N' -> mDatabase.getEntryByNotes(searchQuery, recursionLevel)
|
'N' -> mDatabase.getEntryByNotes(searchQuery, recursionLevel)
|
||||||
'I' -> {
|
'I' -> {
|
||||||
UuidUtil.fromHexString(searchQuery)?.let { uuid ->
|
searchQuery.asUUID()?.let { uuid ->
|
||||||
mDatabase.getEntryById(NodeIdUUID(uuid))
|
mDatabase.getEntryById(NodeIdUUID(uuid))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ package com.kunzisoft.keepass.database.element.node
|
|||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.ParcelUuid
|
import android.os.ParcelUuid
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||||
import com.kunzisoft.keepass.utils.readParcelableCompat
|
import com.kunzisoft.keepass.utils.readParcelableCompat
|
||||||
import com.kunzisoft.keepass.utils.UuidUtil
|
import java.util.UUID
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class NodeIdUUID : NodeId<UUID> {
|
class NodeIdUUID : NodeId<UUID> {
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ class NodeIdUUID : NodeId<UUID> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return UuidUtil.toHexString(id) ?: id.toString()
|
return id.asHexString() ?: id.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toVisualString(): String {
|
override fun toVisualString(): String {
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ import com.kunzisoft.keepass.database.element.Field
|
|||||||
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
import com.kunzisoft.keepass.database.element.database.DatabaseKDBX
|
||||||
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
import com.kunzisoft.keepass.database.element.entry.EntryKDBX
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
import com.kunzisoft.keepass.utils.UuidUtil
|
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||||
|
import com.kunzisoft.keepass.utils.UUIDUtils.asUUID
|
||||||
|
|
||||||
class TemplateEngineCompatible(database: DatabaseKDBX): TemplateEngine(database) {
|
class TemplateEngineCompatible(database: DatabaseKDBX): TemplateEngine(database) {
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ class TemplateEngineCompatible(database: DatabaseKDBX): TemplateEngine(database)
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getTemplate(entryKDBX: EntryKDBX): Template? {
|
override fun getTemplate(entryKDBX: EntryKDBX): Template? {
|
||||||
UuidUtil.fromHexString(entryKDBX.getCustomFieldValue(TEMPLATE_ENTRY_UUID))?.let { templateUUID ->
|
entryKDBX.getCustomFieldValue(TEMPLATE_ENTRY_UUID).asUUID()?.let { templateUUID ->
|
||||||
return getTemplateByCache(templateUUID)
|
return getTemplateByCache(templateUUID)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
@@ -48,7 +49,7 @@ class TemplateEngineCompatible(database: DatabaseKDBX): TemplateEngine(database)
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getTemplateUUIDField(template: Template): Field? {
|
private fun getTemplateUUIDField(template: Template): Field? {
|
||||||
UuidUtil.toHexString(template.uuid)?.let { uuidString ->
|
template.uuid.asHexString()?.let { uuidString ->
|
||||||
return Field(TEMPLATE_ENTRY_UUID,
|
return Field(TEMPLATE_ENTRY_UUID,
|
||||||
ProtectedString(false, uuidString))
|
ProtectedString(false, uuidString))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,11 @@ import com.kunzisoft.keepass.database.element.Entry
|
|||||||
import com.kunzisoft.keepass.database.element.Group
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeHandler
|
import com.kunzisoft.keepass.database.element.node.NodeHandler
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeId
|
import com.kunzisoft.keepass.database.element.node.NodeId
|
||||||
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.FIELD_RELYING_PARTY
|
||||||
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.isPasskeyExclusion
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_FIELD
|
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_FIELD
|
||||||
import com.kunzisoft.keepass.utils.UuidUtil
|
import com.kunzisoft.keepass.otp.OtpEntryFields.isOtpExclusion
|
||||||
|
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||||
import com.kunzisoft.keepass.utils.inTheSameDomainAs
|
import com.kunzisoft.keepass.utils.inTheSameDomainAs
|
||||||
|
|
||||||
class SearchHelper {
|
class SearchHelper {
|
||||||
@@ -163,18 +166,31 @@ class SearchHelper {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (searchParameters.searchInUUIDs) {
|
if (searchParameters.searchInUUIDs) {
|
||||||
val hexString = UuidUtil.toHexString(entry.nodeId.id) ?: ""
|
val hexString = entry.nodeId.id.asHexString() ?: ""
|
||||||
if (checkSearchQuery(hexString, searchParameters))
|
if (checkSearchQuery(hexString, searchParameters))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (searchParameters.searchInOther) {
|
if (searchParameters.searchInOTP) {
|
||||||
entry.getExtraFields().forEach { field ->
|
if(entry.getExtraFields().any { field ->
|
||||||
if (field.name != OTP_FIELD
|
field.name == OTP_FIELD
|
||||||
|| (field.name == OTP_FIELD && searchParameters.searchInOTP)) {
|
&& checkSearchQuery(field.protectedValue.stringValue, searchParameters)
|
||||||
if (checkSearchQuery(field.protectedValue.toString(), searchParameters))
|
})
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if (searchParameters.searchInRelyingParty) {
|
||||||
|
if(entry.getExtraFields().any { field ->
|
||||||
|
field.name == FIELD_RELYING_PARTY
|
||||||
|
&& checkSearchQuery(field.protectedValue.stringValue, searchParameters)
|
||||||
|
})
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
if (searchParameters.searchInOther) {
|
||||||
|
if(entry.getExtraFields().any { field ->
|
||||||
|
field.isOtpExclusion()
|
||||||
|
&& field.isPasskeyExclusion()
|
||||||
|
&& checkSearchQuery(field.protectedValue.toString(), searchParameters)
|
||||||
|
})
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
if (searchParameters.searchInTags) {
|
if (searchParameters.searchInTags) {
|
||||||
if (checkSearchQuery(entry.tags.toString(), searchParameters))
|
if (checkSearchQuery(entry.tags.toString(), searchParameters))
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class SearchParameters() : Parcelable{
|
|||||||
var searchInExpired = false
|
var searchInExpired = false
|
||||||
var searchInNotes = true
|
var searchInNotes = true
|
||||||
var searchInOTP = false
|
var searchInOTP = false
|
||||||
|
var searchInRelyingParty = false
|
||||||
var searchInOther = true
|
var searchInOther = true
|
||||||
var searchInUUIDs = false
|
var searchInUUIDs = false
|
||||||
var searchInTags = false
|
var searchInTags = false
|
||||||
@@ -60,6 +61,7 @@ class SearchParameters() : Parcelable{
|
|||||||
searchInExpired = parcel.readByte() != 0.toByte()
|
searchInExpired = parcel.readByte() != 0.toByte()
|
||||||
searchInNotes = parcel.readByte() != 0.toByte()
|
searchInNotes = parcel.readByte() != 0.toByte()
|
||||||
searchInOTP = parcel.readByte() != 0.toByte()
|
searchInOTP = parcel.readByte() != 0.toByte()
|
||||||
|
searchInRelyingParty = parcel.readByte() != 0.toByte()
|
||||||
searchInOther = parcel.readByte() != 0.toByte()
|
searchInOther = parcel.readByte() != 0.toByte()
|
||||||
searchInUUIDs = parcel.readByte() != 0.toByte()
|
searchInUUIDs = parcel.readByte() != 0.toByte()
|
||||||
searchInTags = parcel.readByte() != 0.toByte()
|
searchInTags = parcel.readByte() != 0.toByte()
|
||||||
@@ -81,6 +83,7 @@ class SearchParameters() : Parcelable{
|
|||||||
parcel.writeByte(if (searchInExpired) 1 else 0)
|
parcel.writeByte(if (searchInExpired) 1 else 0)
|
||||||
parcel.writeByte(if (searchInNotes) 1 else 0)
|
parcel.writeByte(if (searchInNotes) 1 else 0)
|
||||||
parcel.writeByte(if (searchInOTP) 1 else 0)
|
parcel.writeByte(if (searchInOTP) 1 else 0)
|
||||||
|
parcel.writeByte(if (searchInRelyingParty) 1 else 0)
|
||||||
parcel.writeByte(if (searchInOther) 1 else 0)
|
parcel.writeByte(if (searchInOther) 1 else 0)
|
||||||
parcel.writeByte(if (searchInUUIDs) 1 else 0)
|
parcel.writeByte(if (searchInUUIDs) 1 else 0)
|
||||||
parcel.writeByte(if (searchInTags) 1 else 0)
|
parcel.writeByte(if (searchInTags) 1 else 0)
|
||||||
|
|||||||
143
database/src/main/java/com/kunzisoft/keepass/model/AppOrigin.kt
Normal file
143
database/src/main/java/com/kunzisoft/keepass/model/AppOrigin.kt
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.Log
|
||||||
|
import com.kunzisoft.encrypt.HashManager.fingerprintToUrlSafeBase64
|
||||||
|
import com.kunzisoft.keepass.model.WebOrigin.Companion.RELYING_PARTY_DEFAULT_PROTOCOL
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class AppOrigin(
|
||||||
|
val verified: Boolean,
|
||||||
|
val androidOrigins: MutableList<AndroidOrigin> = mutableListOf(),
|
||||||
|
val webOrigins: MutableList<WebOrigin> = mutableListOf(),
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
fun addAndroidOrigin(androidOrigin: AndroidOrigin) {
|
||||||
|
androidOrigins.add(androidOrigin)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addWebOrigin(webOrigin: WebOrigin) {
|
||||||
|
this.webOrigins.add(webOrigin)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify the app origin by comparing it to the list of android origins,
|
||||||
|
* return the first verified origin or throw an exception if none is found
|
||||||
|
*/
|
||||||
|
fun checkAppOrigin(compare: AppOrigin): String {
|
||||||
|
return androidOrigins.firstOrNull { androidOrigin ->
|
||||||
|
compare.androidOrigins.any {
|
||||||
|
it.packageName == androidOrigin.packageName
|
||||||
|
&& it.fingerprint == androidOrigin.fingerprint
|
||||||
|
}
|
||||||
|
}?.let {
|
||||||
|
AndroidOrigin(
|
||||||
|
packageName = it.packageName,
|
||||||
|
fingerprint = it.fingerprint
|
||||||
|
).toAndroidOrigin()
|
||||||
|
} ?: throw SecurityException("Wrong signature for ${toName()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
androidOrigins.clear()
|
||||||
|
webOrigins.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isEmpty(): Boolean {
|
||||||
|
return androidOrigins.isEmpty() && webOrigins.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toName(): String? {
|
||||||
|
return if (androidOrigins.isNotEmpty()) {
|
||||||
|
androidOrigins.first().packageName
|
||||||
|
} else if (webOrigins.isNotEmpty()){
|
||||||
|
webOrigins.first().origin
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val TAG = AppOrigin::class.java.simpleName
|
||||||
|
|
||||||
|
fun fromOrigin(origin: String, androidOrigin: AndroidOrigin, verified: Boolean): AppOrigin {
|
||||||
|
val appOrigin = AppOrigin(verified)
|
||||||
|
if (origin.startsWith(RELYING_PARTY_DEFAULT_PROTOCOL)) {
|
||||||
|
appOrigin.apply {
|
||||||
|
addWebOrigin(WebOrigin(origin))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Unknown verified origin $origin")
|
||||||
|
appOrigin.apply {
|
||||||
|
addAndroidOrigin(androidOrigin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return appOrigin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class AndroidOrigin(
|
||||||
|
val packageName: String,
|
||||||
|
val fingerprint: String?
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an Android App Origin string of the form "android:apk-key-hash:<base64_urlsafe_hash>"
|
||||||
|
* from a colon-separated hex fingerprint string.
|
||||||
|
*
|
||||||
|
* The input fingerprint is assumed to be the SHA-256 hash of the app's signing certificate.
|
||||||
|
*
|
||||||
|
* @param fingerprint The colon-separated hex fingerprint string (e.g., "91:F7:CB:...").
|
||||||
|
* @return The Android App Origin string.
|
||||||
|
* @throws IllegalArgumentException if the hex string (after removing colons) has an odd length
|
||||||
|
* or contains non-hex characters.
|
||||||
|
*/
|
||||||
|
fun toAndroidOrigin(): String {
|
||||||
|
if (fingerprint == null) {
|
||||||
|
throw IllegalArgumentException("Fingerprint $fingerprint cannot be null")
|
||||||
|
}
|
||||||
|
return "android:apk-key-hash:${fingerprintToUrlSafeBase64(fingerprint)}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class WebOrigin(
|
||||||
|
val origin: String
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
fun toWebOrigin(): String {
|
||||||
|
return origin
|
||||||
|
}
|
||||||
|
|
||||||
|
fun defaultAssetLinks(): String {
|
||||||
|
return "${origin}/.well-known/assetlinks.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val RELYING_PARTY_DEFAULT_PROTOCOL = "https"
|
||||||
|
fun fromRelyingParty(relyingParty: String): WebOrigin = WebOrigin(
|
||||||
|
origin ="$RELYING_PARTY_DEFAULT_PROTOCOL://$relyingParty"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.model
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.database.element.Field
|
||||||
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfo.Companion.suffixFieldNamePosition
|
||||||
|
|
||||||
|
object AppOriginEntryField {
|
||||||
|
|
||||||
|
const val WEB_DOMAIN_FIELD_NAME = "URL"
|
||||||
|
const val APPLICATION_ID_FIELD_NAME = "AndroidApp"
|
||||||
|
const val APPLICATION_SIGNATURE_FIELD_NAME = "AndroidApp Signature"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse fields of an entry to retrieve a an AppOrigin
|
||||||
|
*/
|
||||||
|
fun parseFields(getField: (id: String) -> String?): AppOrigin {
|
||||||
|
val appOrigin = AppOrigin(verified = true)
|
||||||
|
// Get Application identifiers
|
||||||
|
generateSequence(0) { it + 1 }
|
||||||
|
.map { position ->
|
||||||
|
val appId = getField(APPLICATION_ID_FIELD_NAME + suffixFieldNamePosition(position))
|
||||||
|
val appSignature = getField(APPLICATION_SIGNATURE_FIELD_NAME + suffixFieldNamePosition(position))
|
||||||
|
// Pair them up, if appId is null, we stop
|
||||||
|
if (appId != null) {
|
||||||
|
appId to (appSignature ?: "")
|
||||||
|
} else {
|
||||||
|
// Stop
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.takeWhile { it != null }
|
||||||
|
.forEach { pair ->
|
||||||
|
appOrigin.addAndroidOrigin(
|
||||||
|
AndroidOrigin(pair!!.first, pair.second)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Get Domains
|
||||||
|
var domainFieldPosition = 0
|
||||||
|
while (true) {
|
||||||
|
val domainKey = WEB_DOMAIN_FIELD_NAME + suffixFieldNamePosition(domainFieldPosition)
|
||||||
|
val domainValue = getField(domainKey)
|
||||||
|
if (domainValue != null) {
|
||||||
|
appOrigin.addWebOrigin(WebOrigin(origin = domainValue))
|
||||||
|
domainFieldPosition++
|
||||||
|
} else {
|
||||||
|
break // No more domain found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return appOrigin
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Useful to detect if an other KeePass compatibility app already add a web domain or an app id
|
||||||
|
*/
|
||||||
|
private fun EntryInfo.containsDomainOrApplicationId(search: String): Boolean {
|
||||||
|
if (url.contains(search))
|
||||||
|
return true
|
||||||
|
return customFields.find {
|
||||||
|
it.protectedValue.stringValue.contains(search)
|
||||||
|
} != null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun EntryInfo.setWebDomain(webDomain: String?, scheme: String?, customFieldsAllowed: Boolean) {
|
||||||
|
// If unable to save web domain in custom field or URL not populated, save in URL
|
||||||
|
webDomain?.let {
|
||||||
|
val webScheme = if (scheme.isNullOrEmpty()) "https" else scheme
|
||||||
|
val webDomainToStore = if (webDomain.contains("://")) {
|
||||||
|
webDomain
|
||||||
|
} else {
|
||||||
|
"$webScheme://$webDomain"
|
||||||
|
}
|
||||||
|
if (!containsDomainOrApplicationId(webDomain)) {
|
||||||
|
if (!customFieldsAllowed || url.isEmpty()) {
|
||||||
|
url = webDomainToStore
|
||||||
|
} else {
|
||||||
|
// Save web domain in custom field
|
||||||
|
addUniqueField(
|
||||||
|
Field(
|
||||||
|
WEB_DOMAIN_FIELD_NAME,
|
||||||
|
ProtectedString(false, webDomainToStore)
|
||||||
|
),
|
||||||
|
1 // Start to one because URL is a standard field name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save application id in custom field and the application signature if provided
|
||||||
|
*/
|
||||||
|
fun EntryInfo.setApplicationId(applicationId: String?, signature: String? = null) {
|
||||||
|
// Save application id in custom field
|
||||||
|
applicationId?.let {
|
||||||
|
// Check compatibility with other KeePass client unless a signature need to be saved
|
||||||
|
if (!containsDomainOrApplicationId(applicationId) || signature != null) {
|
||||||
|
val position = addUniqueField(
|
||||||
|
Field(
|
||||||
|
APPLICATION_ID_FIELD_NAME,
|
||||||
|
ProtectedString(false, applicationId)
|
||||||
|
)
|
||||||
|
).first
|
||||||
|
signature?.let {
|
||||||
|
addOrReplaceFieldWithSuffix(
|
||||||
|
Field(
|
||||||
|
APPLICATION_SIGNATURE_FIELD_NAME,
|
||||||
|
ProtectedString(true, signature)
|
||||||
|
),
|
||||||
|
position
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign an AppOrigin to an EntryInfo,
|
||||||
|
* Only if [customFieldsAllowed] is true
|
||||||
|
*/
|
||||||
|
fun EntryInfo.setAppOrigin(appOrigin: AppOrigin?, customFieldsAllowed: Boolean) {
|
||||||
|
appOrigin?.androidOrigins?.forEach { appIdentifier ->
|
||||||
|
setApplicationId(appIdentifier.packageName, appIdentifier.fingerprint)
|
||||||
|
}
|
||||||
|
appOrigin?.webOrigins?.forEach { webOrigin ->
|
||||||
|
setWebDomain(webOrigin.origin, null, customFieldsAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.model
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.database.element.DateInstant
|
||||||
|
import com.kunzisoft.keepass.database.element.Field
|
||||||
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
|
import com.kunzisoft.keepass.database.element.template.TemplateField.LABEL_CVV
|
||||||
|
import com.kunzisoft.keepass.database.element.template.TemplateField.LABEL_HOLDER
|
||||||
|
import com.kunzisoft.keepass.database.element.template.TemplateField.LABEL_NUMBER
|
||||||
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
|
object CreditCardEntryFields {
|
||||||
|
|
||||||
|
const val CREDIT_CARD_TAG = "Credit Card"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse fields of an entry to retrieve a Passkey
|
||||||
|
*/
|
||||||
|
fun parseFields(getField: (id: String) -> String?): CreditCard? {
|
||||||
|
val cardHolderField = getField(LABEL_HOLDER)
|
||||||
|
val cardNumberField = getField(LABEL_NUMBER)
|
||||||
|
val cardExpiration = DateTime() // TODO Expiration
|
||||||
|
val cardCVVField = getField(LABEL_CVV)
|
||||||
|
if (cardHolderField == null
|
||||||
|
|| cardNumberField == null)
|
||||||
|
return null
|
||||||
|
return CreditCard(
|
||||||
|
cardholder = cardHolderField,
|
||||||
|
number = cardNumberField,
|
||||||
|
expiration = cardExpiration,
|
||||||
|
cvv = cardCVVField
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun EntryInfo.setCreditCard(creditCard: CreditCard?) {
|
||||||
|
if (creditCard != null) {
|
||||||
|
tags.put(CREDIT_CARD_TAG)
|
||||||
|
creditCard.cardholder?.let {
|
||||||
|
addOrReplaceField(
|
||||||
|
Field(
|
||||||
|
LABEL_HOLDER,
|
||||||
|
ProtectedString(enableProtection = false, it)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
creditCard.number?.let {
|
||||||
|
addOrReplaceField(
|
||||||
|
Field(
|
||||||
|
LABEL_NUMBER,
|
||||||
|
ProtectedString(enableProtection = false, it)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
creditCard.expiration?.let {
|
||||||
|
expires = true
|
||||||
|
expiryTime = DateInstant(creditCard.expiration.toInstant())
|
||||||
|
}
|
||||||
|
creditCard.cvv?.let {
|
||||||
|
addOrReplaceField(
|
||||||
|
Field(
|
||||||
|
LABEL_CVV,
|
||||||
|
ProtectedString(enableProtection = true, it)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,18 +22,27 @@ package com.kunzisoft.keepass.model
|
|||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.ParcelUuid
|
import android.os.ParcelUuid
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.kunzisoft.keepass.database.element.*
|
import com.kunzisoft.keepass.database.element.Attachment
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.Field
|
||||||
|
import com.kunzisoft.keepass.database.element.Tags
|
||||||
import com.kunzisoft.keepass.database.element.entry.AutoType
|
import com.kunzisoft.keepass.database.element.entry.AutoType
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.model.AppOriginEntryField.setAppOrigin
|
||||||
import com.kunzisoft.keepass.database.element.template.TemplateField
|
import com.kunzisoft.keepass.model.AppOriginEntryField.setApplicationId
|
||||||
|
import com.kunzisoft.keepass.model.AppOriginEntryField.setWebDomain
|
||||||
|
import com.kunzisoft.keepass.model.CreditCardEntryFields.setCreditCard
|
||||||
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.isPasskeyExclusion
|
||||||
|
import com.kunzisoft.keepass.model.PasskeyEntryFields.setPasskey
|
||||||
import com.kunzisoft.keepass.otp.OtpElement
|
import com.kunzisoft.keepass.otp.OtpElement
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
|
||||||
|
import com.kunzisoft.keepass.otp.OtpEntryFields.isOtpExclusion
|
||||||
|
import com.kunzisoft.keepass.otp.OtpEntryFields.setOtp
|
||||||
import com.kunzisoft.keepass.utils.readBooleanCompat
|
import com.kunzisoft.keepass.utils.readBooleanCompat
|
||||||
import com.kunzisoft.keepass.utils.readListCompat
|
import com.kunzisoft.keepass.utils.readListCompat
|
||||||
import com.kunzisoft.keepass.utils.readParcelableCompat
|
import com.kunzisoft.keepass.utils.readParcelableCompat
|
||||||
import com.kunzisoft.keepass.utils.writeBooleanCompat
|
import com.kunzisoft.keepass.utils.writeBooleanCompat
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class EntryInfo : NodeInfo {
|
class EntryInfo : NodeInfo {
|
||||||
|
|
||||||
@@ -49,6 +58,8 @@ class EntryInfo : NodeInfo {
|
|||||||
var attachments: MutableList<Attachment> = mutableListOf()
|
var attachments: MutableList<Attachment> = mutableListOf()
|
||||||
var autoType: AutoType = AutoType()
|
var autoType: AutoType = AutoType()
|
||||||
var otpModel: OtpModel? = null
|
var otpModel: OtpModel? = null
|
||||||
|
var passkey: Passkey? = null
|
||||||
|
var appOrigin: AppOrigin? = null
|
||||||
var isTemplate: Boolean = false
|
var isTemplate: Boolean = false
|
||||||
|
|
||||||
constructor() : super()
|
constructor() : super()
|
||||||
@@ -68,6 +79,8 @@ class EntryInfo : NodeInfo {
|
|||||||
parcel.readListCompat(attachments)
|
parcel.readListCompat(attachments)
|
||||||
autoType = parcel.readParcelableCompat() ?: autoType
|
autoType = parcel.readParcelableCompat() ?: autoType
|
||||||
otpModel = parcel.readParcelableCompat() ?: otpModel
|
otpModel = parcel.readParcelableCompat() ?: otpModel
|
||||||
|
passkey = parcel.readParcelableCompat() ?: passkey
|
||||||
|
appOrigin = parcel.readParcelableCompat() ?: appOrigin
|
||||||
isTemplate = parcel.readBooleanCompat()
|
isTemplate = parcel.readBooleanCompat()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,15 +102,16 @@ class EntryInfo : NodeInfo {
|
|||||||
parcel.writeList(attachments)
|
parcel.writeList(attachments)
|
||||||
parcel.writeParcelable(autoType, flags)
|
parcel.writeParcelable(autoType, flags)
|
||||||
parcel.writeParcelable(otpModel, flags)
|
parcel.writeParcelable(otpModel, flags)
|
||||||
|
parcel.writeParcelable(passkey, flags)
|
||||||
|
parcel.writeParcelable(appOrigin, flags)
|
||||||
parcel.writeBooleanCompat(isTemplate)
|
parcel.writeBooleanCompat(isTemplate)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun containsCustomFieldsProtected(): Boolean {
|
fun getCustomFieldsForFilling(): List<Field> {
|
||||||
return customFields.any { it.protectedValue.isProtected }
|
return customFields.filter {
|
||||||
|
!it.isOtpExclusion()
|
||||||
|
&& !it.isPasskeyExclusion()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun containsCustomFieldsNotProtected(): Boolean {
|
|
||||||
return customFields.any { !it.protectedValue.isProtected }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun containsCustomField(label: String): Boolean {
|
fun containsCustomField(label: String): Boolean {
|
||||||
@@ -113,133 +127,98 @@ class EntryInfo : NodeInfo {
|
|||||||
return customFields.lastOrNull { it.name == label }?.protectedValue?.toString() ?: ""
|
return customFields.lastOrNull { it.name == label }?.protectedValue?.toString() ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return true if modified
|
/**
|
||||||
private fun addUniqueField(field: Field, number: Int = 0) {
|
* Add a field to the custom fields list, replace if name already exists
|
||||||
var sameName = false
|
*/
|
||||||
var sameValue = false
|
fun addOrReplaceField(field: Field) {
|
||||||
val suffix = if (number > 0) "_$number" else ""
|
customFields.lastOrNull { it.name == field.name }?.let {
|
||||||
customFields.forEach { currentField ->
|
it.apply {
|
||||||
// Not write the same data again
|
protectedValue = field.protectedValue
|
||||||
if (currentField.protectedValue.stringValue == field.protectedValue.stringValue) {
|
|
||||||
sameValue = true
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
// Same name but new value, create a new suffix
|
} ?: customFields.add(field)
|
||||||
if (currentField.name == field.name + suffix) {
|
|
||||||
sameName = true
|
|
||||||
addUniqueField(field, number + 1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!sameName && !sameValue)
|
|
||||||
(customFields as ArrayList<Field>).add(Field(field.name + suffix, field.protectedValue))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun containsDomainOrApplicationId(search: String): Boolean {
|
|
||||||
if (url.contains(search))
|
|
||||||
return true
|
|
||||||
return customFields.find {
|
|
||||||
it.protectedValue.stringValue.contains(search)
|
|
||||||
} != null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add searchInfo to current EntryInfo, return true if new data, false if no modification
|
* Add a field to the custom fields list with a suffix position,
|
||||||
|
* replace if name already exists
|
||||||
*/
|
*/
|
||||||
fun saveSearchInfo(database: Database?, searchInfo: SearchInfo): Boolean {
|
fun addOrReplaceFieldWithSuffix(field: Field, position: Int) {
|
||||||
var modification = false
|
addOrReplaceField(Field(
|
||||||
searchInfo.otpString?.let { otpString ->
|
field.name + suffixFieldNamePosition(position),
|
||||||
// Replace the OTP field
|
field.protectedValue)
|
||||||
OtpEntryFields.parseOTPUri(otpString)?.let { otpElement ->
|
)
|
||||||
if (title.isEmpty())
|
|
||||||
title = otpElement.issuer
|
|
||||||
if (username.isEmpty())
|
|
||||||
username = otpElement.name
|
|
||||||
// Add OTP field
|
|
||||||
val mutableCustomFields = customFields as ArrayList<Field>
|
|
||||||
val otpField = OtpEntryFields.buildOtpField(otpElement, null, null)
|
|
||||||
if (mutableCustomFields.contains(otpField)) {
|
|
||||||
mutableCustomFields.remove(otpField)
|
|
||||||
}
|
}
|
||||||
mutableCustomFields.add(otpField)
|
|
||||||
modification = true
|
/**
|
||||||
|
* Add an unique field to the custom fields list with a suffix
|
||||||
|
* if name already exists and value not the same
|
||||||
|
* @param field the field to add
|
||||||
|
* @param position the number to add to the suffix
|
||||||
|
* @return the increment number and the custom field created
|
||||||
|
*/
|
||||||
|
fun addUniqueField(field: Field, position: Int = 0): Pair<Int, Field> {
|
||||||
|
val suffix = suffixFieldNamePosition(position)
|
||||||
|
if (customFields.any { currentField -> currentField.name == field.name + suffix }) {
|
||||||
|
val fieldFound = customFields.find {
|
||||||
|
it.name == field.name + suffix
|
||||||
|
&& it.protectedValue.stringValue == field.protectedValue.stringValue
|
||||||
}
|
}
|
||||||
} ?: searchInfo.webDomain?.let { webDomain ->
|
return if (fieldFound != null) {
|
||||||
// If unable to save web domain in custom field or URL not populated, save in URL
|
Pair(position, fieldFound)
|
||||||
val scheme = searchInfo.webScheme
|
|
||||||
val webScheme = if (scheme.isNullOrEmpty()) "https" else scheme
|
|
||||||
val webDomainToStore = "$webScheme://$webDomain"
|
|
||||||
if (!containsDomainOrApplicationId(webDomain)) {
|
|
||||||
if (database?.allowEntryCustomFields() != true || url.isEmpty()) {
|
|
||||||
url = webDomainToStore
|
|
||||||
} else {
|
} else {
|
||||||
// Save web domain in custom field
|
addUniqueField(field, position + 1)
|
||||||
addUniqueField(
|
}
|
||||||
Field(
|
} else {
|
||||||
WEB_DOMAIN_FIELD_NAME,
|
val field = Field(field.name + suffix, field.protectedValue)
|
||||||
ProtectedString(false, webDomainToStore)
|
customFields.add(field)
|
||||||
),
|
return Pair(position, field)
|
||||||
1 // Start to one because URL is a standard field name
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capitalize and remove suffix of a title
|
||||||
|
*/
|
||||||
|
fun String.toTitle(): String {
|
||||||
|
return this.replaceFirstChar {
|
||||||
|
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add searchInfo to current EntryInfo
|
||||||
|
*/
|
||||||
|
fun saveSearchInfo(database: Database?, searchInfo: SearchInfo) {
|
||||||
|
searchInfo.otpString?.let { otpString ->
|
||||||
|
setOtp(otpString)
|
||||||
|
} ?: searchInfo.webDomain?.let { webDomain ->
|
||||||
|
setWebDomain(
|
||||||
|
webDomain,
|
||||||
|
searchInfo.webScheme,
|
||||||
|
database?.allowEntryCustomFields() == true
|
||||||
)
|
)
|
||||||
}
|
|
||||||
modification = true
|
|
||||||
}
|
|
||||||
} ?: searchInfo.applicationId?.let { applicationId ->
|
} ?: searchInfo.applicationId?.let { applicationId ->
|
||||||
// Save application id in custom field
|
setApplicationId(applicationId)
|
||||||
if (database?.allowEntryCustomFields() == true) {
|
|
||||||
if (!containsDomainOrApplicationId(applicationId)) {
|
|
||||||
addUniqueField(
|
|
||||||
Field(
|
|
||||||
APPLICATION_ID_FIELD_NAME,
|
|
||||||
ProtectedString(false, applicationId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
modification = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (title.isEmpty()) {
|
if (title.isEmpty()) {
|
||||||
title = searchInfoToTitle(searchInfo)
|
title = searchInfo.toString().toTitle()
|
||||||
}
|
}
|
||||||
return modification
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Capitalize and remove suffix of web domain to create a title
|
* Add registerInfo to current EntryInfo
|
||||||
*/
|
*/
|
||||||
private fun searchInfoToTitle(searchInfo: SearchInfo): String {
|
|
||||||
val webDomain = searchInfo.webDomain
|
|
||||||
return webDomain?.substring(0, webDomain.lastIndexOf('.'))?.replaceFirstChar {
|
|
||||||
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
|
|
||||||
} ?: searchInfo.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveRegisterInfo(database: Database?, registerInfo: RegisterInfo) {
|
fun saveRegisterInfo(database: Database?, registerInfo: RegisterInfo) {
|
||||||
registerInfo.searchInfo.let {
|
saveSearchInfo(database, registerInfo.searchInfo)
|
||||||
title = searchInfoToTitle(it)
|
registerInfo.username?.let { username = it }
|
||||||
}
|
registerInfo.password?.let { password = it }
|
||||||
registerInfo.username?.let {
|
setCreditCard(registerInfo.creditCard)
|
||||||
username = it
|
setPasskey(registerInfo.passkey)
|
||||||
}
|
setAppOrigin(
|
||||||
registerInfo.password?.let {
|
registerInfo.appOrigin,
|
||||||
password = it
|
database?.allowEntryCustomFields() == true
|
||||||
}
|
)
|
||||||
|
if (title.isEmpty()) {
|
||||||
if (database?.allowEntryCustomFields() == true) {
|
title = registerInfo.toString().toTitle()
|
||||||
val creditCard: CreditCard? = registerInfo.creditCard
|
|
||||||
creditCard?.cardholder?.let {
|
|
||||||
addUniqueField(Field(TemplateField.LABEL_HOLDER, ProtectedString(false, it)))
|
|
||||||
}
|
|
||||||
creditCard?.expiration?.let {
|
|
||||||
expires = true
|
|
||||||
expiryTime = DateInstant(creditCard.expiration.toInstant())
|
|
||||||
}
|
|
||||||
creditCard?.number?.let {
|
|
||||||
addUniqueField(Field(TemplateField.LABEL_NUMBER, ProtectedString(false, it)))
|
|
||||||
}
|
|
||||||
creditCard?.cvv?.let {
|
|
||||||
addUniqueField(Field(TemplateField.LABEL_CVV, ProtectedString(true, it)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,6 +247,8 @@ class EntryInfo : NodeInfo {
|
|||||||
if (attachments != other.attachments) return false
|
if (attachments != other.attachments) return false
|
||||||
if (autoType != other.autoType) return false
|
if (autoType != other.autoType) return false
|
||||||
if (otpModel != other.otpModel) return false
|
if (otpModel != other.otpModel) return false
|
||||||
|
if (passkey != other.passkey) return false
|
||||||
|
if (appOrigin != other.appOrigin) return false
|
||||||
if (isTemplate != other.isTemplate) return false
|
if (isTemplate != other.isTemplate) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -287,6 +268,8 @@ class EntryInfo : NodeInfo {
|
|||||||
result = 31 * result + attachments.hashCode()
|
result = 31 * result + attachments.hashCode()
|
||||||
result = 31 * result + autoType.hashCode()
|
result = 31 * result + autoType.hashCode()
|
||||||
result = 31 * result + (otpModel?.hashCode() ?: 0)
|
result = 31 * result + (otpModel?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (passkey?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (appOrigin?.hashCode() ?: 0)
|
||||||
result = 31 * result + isTemplate.hashCode()
|
result = 31 * result + isTemplate.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -294,8 +277,12 @@ class EntryInfo : NodeInfo {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val WEB_DOMAIN_FIELD_NAME = "URL"
|
/**
|
||||||
const val APPLICATION_ID_FIELD_NAME = "AndroidApp"
|
* Create a field name suffix depending on the field position
|
||||||
|
*/
|
||||||
|
fun suffixFieldNamePosition(position: Int): String {
|
||||||
|
return if (position > 0) "_$position" else ""
|
||||||
|
}
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
val CREATOR: Parcelable.Creator<EntryInfo> = object : Parcelable.Creator<EntryInfo> {
|
val CREATOR: Parcelable.Creator<EntryInfo> = object : Parcelable.Creator<EntryInfo> {
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class Passkey(
|
||||||
|
val username: String,
|
||||||
|
val privateKeyPem: String,
|
||||||
|
val credentialId: String,
|
||||||
|
val userHandle: String,
|
||||||
|
val relyingParty: String
|
||||||
|
): Parcelable
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package com.kunzisoft.keepass.model
|
||||||
|
|
||||||
|
import com.kunzisoft.keepass.database.element.Field
|
||||||
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
|
|
||||||
|
object PasskeyEntryFields {
|
||||||
|
|
||||||
|
// field names from KeypassXC are used
|
||||||
|
const val FIELD_USERNAME = "KPEX_PASSKEY_USERNAME"
|
||||||
|
|
||||||
|
const val FIELD_PRIVATE_KEY = "KPEX_PASSKEY_PRIVATE_KEY_PEM"
|
||||||
|
const val FIELD_CREDENTIAL_ID = "KPEX_PASSKEY_CREDENTIAL_ID"
|
||||||
|
const val FIELD_USER_HANDLE = "KPEX_PASSKEY_USER_HANDLE"
|
||||||
|
const val FIELD_RELYING_PARTY = "KPEX_PASSKEY_RELYING_PARTY"
|
||||||
|
|
||||||
|
const val PASSKEY_FIELD = "Passkey"
|
||||||
|
const val PASSKEY_TAG = "Passkey"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse fields of an entry to retrieve a Passkey
|
||||||
|
*/
|
||||||
|
fun parseFields(getField: (id: String) -> String?): Passkey? {
|
||||||
|
val usernameField = getField(FIELD_USERNAME)
|
||||||
|
val privateKeyField = getField(FIELD_PRIVATE_KEY)
|
||||||
|
val credentialIdField = getField(FIELD_CREDENTIAL_ID)
|
||||||
|
val userHandleField = getField(FIELD_USER_HANDLE)
|
||||||
|
val relyingPartyField = getField(FIELD_RELYING_PARTY)
|
||||||
|
if (usernameField == null
|
||||||
|
|| privateKeyField == null
|
||||||
|
|| credentialIdField == null
|
||||||
|
|| userHandleField == null
|
||||||
|
|| relyingPartyField == null)
|
||||||
|
return null
|
||||||
|
return Passkey(
|
||||||
|
username = usernameField,
|
||||||
|
privateKeyPem = privateKeyField,
|
||||||
|
credentialId = credentialIdField,
|
||||||
|
userHandle = userHandleField,
|
||||||
|
relyingParty = relyingPartyField
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun EntryInfo.setPasskey(passkey: Passkey?) {
|
||||||
|
if (passkey != null) {
|
||||||
|
tags.put(PASSKEY_TAG)
|
||||||
|
addOrReplaceField(
|
||||||
|
Field(
|
||||||
|
FIELD_USERNAME,
|
||||||
|
ProtectedString(enableProtection = false, passkey.username)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
addOrReplaceField(
|
||||||
|
Field(
|
||||||
|
FIELD_PRIVATE_KEY,
|
||||||
|
ProtectedString(enableProtection = true, passkey.privateKeyPem)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
addOrReplaceField(
|
||||||
|
Field(
|
||||||
|
FIELD_CREDENTIAL_ID,
|
||||||
|
ProtectedString(enableProtection = true, passkey.credentialId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
addOrReplaceField(
|
||||||
|
Field(
|
||||||
|
FIELD_USER_HANDLE,
|
||||||
|
ProtectedString(enableProtection = true, passkey.userHandle)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
addOrReplaceField(
|
||||||
|
Field(
|
||||||
|
FIELD_RELYING_PARTY,
|
||||||
|
ProtectedString(enableProtection = false, passkey.relyingParty)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build Passkey field from a Passkey
|
||||||
|
*/
|
||||||
|
fun buildPasskeyField(passkey: Passkey): Field {
|
||||||
|
return Field(
|
||||||
|
name = PASSKEY_FIELD,
|
||||||
|
value = ProtectedString(
|
||||||
|
enableProtection = false,
|
||||||
|
string = passkey.relyingParty
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build new generated fields in a new list from [fieldsToParse] in parameter,
|
||||||
|
* Remove parameters fields use to generate auto fields
|
||||||
|
*/
|
||||||
|
fun generateAutoFields(fieldsToParse: List<Field>): MutableList<Field> {
|
||||||
|
val newCustomFields: MutableList<Field> = ArrayList(fieldsToParse)
|
||||||
|
// Remove parameter fields
|
||||||
|
val usernameField = Field(FIELD_USERNAME)
|
||||||
|
val privateKeyField = Field(FIELD_PRIVATE_KEY)
|
||||||
|
val credentialIdField = Field(FIELD_CREDENTIAL_ID)
|
||||||
|
val userHandleField = Field(FIELD_USER_HANDLE)
|
||||||
|
val relyingPartyField = Field(FIELD_RELYING_PARTY)
|
||||||
|
newCustomFields.remove(usernameField)
|
||||||
|
newCustomFields.remove(privateKeyField)
|
||||||
|
newCustomFields.remove(credentialIdField)
|
||||||
|
newCustomFields.remove(userHandleField)
|
||||||
|
newCustomFields.remove(relyingPartyField)
|
||||||
|
// Empty auto generated OTP Token field
|
||||||
|
if (fieldsToParse.contains(usernameField)
|
||||||
|
|| fieldsToParse.contains(privateKeyField)
|
||||||
|
|| fieldsToParse.contains(credentialIdField)
|
||||||
|
|| fieldsToParse.contains(userHandleField)
|
||||||
|
|| fieldsToParse.contains(relyingPartyField)
|
||||||
|
)
|
||||||
|
newCustomFields.add(
|
||||||
|
Field(
|
||||||
|
name = PASSKEY_FIELD,
|
||||||
|
value = ProtectedString(enableProtection = false)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return newCustomFields
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field ignored for a search or a form filling
|
||||||
|
*/
|
||||||
|
fun Field.isPasskeyExclusion(): Boolean {
|
||||||
|
return when(name) {
|
||||||
|
PASSKEY_FIELD -> true
|
||||||
|
FIELD_USERNAME -> true
|
||||||
|
FIELD_PRIVATE_KEY -> true
|
||||||
|
FIELD_CREDENTIAL_ID -> true
|
||||||
|
FIELD_USER_HANDLE -> true
|
||||||
|
FIELD_RELYING_PARTY -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,32 +1,56 @@
|
|||||||
package com.kunzisoft.keepass.model
|
package com.kunzisoft.keepass.model
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.keepass.utils.ObjectNameResource
|
||||||
import com.kunzisoft.keepass.utils.readParcelableCompat
|
import com.kunzisoft.keepass.utils.readParcelableCompat
|
||||||
|
|
||||||
data class RegisterInfo(val searchInfo: SearchInfo,
|
data class RegisterInfo(
|
||||||
val username: String?,
|
val searchInfo: SearchInfo,
|
||||||
val password: String?,
|
val username: String? = null,
|
||||||
val creditCard: CreditCard?): Parcelable {
|
val password: String? = null,
|
||||||
|
val creditCard: CreditCard? = null,
|
||||||
|
val passkey: Passkey? = null,
|
||||||
|
val appOrigin: AppOrigin? = null
|
||||||
|
) : ObjectNameResource, Parcelable {
|
||||||
|
|
||||||
constructor(parcel: Parcel) : this(
|
constructor(parcel: Parcel) : this(
|
||||||
parcel.readParcelableCompat() ?: SearchInfo(),
|
searchInfo = parcel.readParcelableCompat() ?: SearchInfo(),
|
||||||
parcel.readString() ?: "",
|
username = parcel.readString(),
|
||||||
parcel.readString() ?: "",
|
password = parcel.readString(),
|
||||||
parcel.readParcelableCompat()) {
|
creditCard = parcel.readParcelableCompat(),
|
||||||
}
|
passkey = parcel.readParcelableCompat(),
|
||||||
|
appOrigin = parcel.readParcelableCompat()
|
||||||
|
)
|
||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
parcel.writeParcelable(searchInfo, flags)
|
parcel.writeParcelable(searchInfo, flags)
|
||||||
parcel.writeString(username)
|
parcel.writeString(username)
|
||||||
parcel.writeString(password)
|
parcel.writeString(password)
|
||||||
parcel.writeParcelable(creditCard, flags)
|
parcel.writeParcelable(creditCard, flags)
|
||||||
|
parcel.writeParcelable(passkey, flags)
|
||||||
|
parcel.writeParcelable(appOrigin, flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun describeContents(): Int {
|
override fun describeContents(): Int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getName(resources: Resources): String {
|
||||||
|
return username
|
||||||
|
?: passkey?.relyingParty
|
||||||
|
?: appOrigin?.toName()
|
||||||
|
?: searchInfo.getName(resources)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return username
|
||||||
|
?: passkey?.relyingParty
|
||||||
|
?: appOrigin?.toName()
|
||||||
|
?: searchInfo.toString()
|
||||||
|
}
|
||||||
|
|
||||||
companion object CREATOR : Parcelable.Creator<RegisterInfo> {
|
companion object CREATOR : Parcelable.Creator<RegisterInfo> {
|
||||||
override fun createFromParcel(parcel: Parcel): RegisterInfo {
|
override fun createFromParcel(parcel: Parcel): RegisterInfo {
|
||||||
return RegisterInfo(parcel)
|
return RegisterInfo(parcel)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.res.Resources
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.keepass.database.search.SearchParameters
|
||||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||||
import com.kunzisoft.keepass.utils.ObjectNameResource
|
import com.kunzisoft.keepass.utils.ObjectNameResource
|
||||||
import com.kunzisoft.keepass.utils.readBooleanCompat
|
import com.kunzisoft.keepass.utils.readBooleanCompat
|
||||||
@@ -11,6 +12,7 @@ import com.kunzisoft.keepass.utils.writeBooleanCompat
|
|||||||
|
|
||||||
class SearchInfo : ObjectNameResource, Parcelable {
|
class SearchInfo : ObjectNameResource, Parcelable {
|
||||||
var manualSelection: Boolean = false
|
var manualSelection: Boolean = false
|
||||||
|
var tag: String? = null
|
||||||
var applicationId: String? = null
|
var applicationId: String? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = when {
|
field = when {
|
||||||
@@ -33,26 +35,33 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
get() {
|
get() {
|
||||||
return if (webDomain == null) null else field
|
return if (webDomain == null) null else field
|
||||||
}
|
}
|
||||||
|
var relyingParty: String? = null
|
||||||
var otpString: String? = null
|
var otpString: String? = null
|
||||||
|
|
||||||
constructor()
|
constructor()
|
||||||
|
|
||||||
constructor(toCopy: SearchInfo?) {
|
constructor(toCopy: SearchInfo?) {
|
||||||
manualSelection = toCopy?.manualSelection ?: manualSelection
|
manualSelection = toCopy?.manualSelection ?: manualSelection
|
||||||
|
tag = toCopy?.tag
|
||||||
applicationId = toCopy?.applicationId
|
applicationId = toCopy?.applicationId
|
||||||
webDomain = toCopy?.webDomain
|
webDomain = toCopy?.webDomain
|
||||||
webScheme = toCopy?.webScheme
|
webScheme = toCopy?.webScheme
|
||||||
|
relyingParty = toCopy?.relyingParty
|
||||||
otpString = toCopy?.otpString
|
otpString = toCopy?.otpString
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor(parcel: Parcel) {
|
private constructor(parcel: Parcel) {
|
||||||
manualSelection = parcel.readBooleanCompat()
|
manualSelection = parcel.readBooleanCompat()
|
||||||
|
val readTag = parcel.readString()
|
||||||
|
tag = if (readTag.isNullOrEmpty()) null else readTag
|
||||||
val readAppId = parcel.readString()
|
val readAppId = parcel.readString()
|
||||||
applicationId = if (readAppId.isNullOrEmpty()) null else readAppId
|
applicationId = if (readAppId.isNullOrEmpty()) null else readAppId
|
||||||
val readDomain = parcel.readString()
|
val readDomain = parcel.readString()
|
||||||
webDomain = if (readDomain.isNullOrEmpty()) null else readDomain
|
webDomain = if (readDomain.isNullOrEmpty()) null else readDomain
|
||||||
val readScheme = parcel.readString()
|
val readScheme = parcel.readString()
|
||||||
webScheme = if (readScheme.isNullOrEmpty()) null else readScheme
|
webScheme = if (readScheme.isNullOrEmpty()) null else readScheme
|
||||||
|
val readRelyingParty = parcel.readString()
|
||||||
|
relyingParty = if (readRelyingParty.isNullOrEmpty()) null else readRelyingParty
|
||||||
val readOtp = parcel.readString()
|
val readOtp = parcel.readString()
|
||||||
otpString = if (readOtp.isNullOrEmpty()) null else readOtp
|
otpString = if (readOtp.isNullOrEmpty()) null else readOtp
|
||||||
}
|
}
|
||||||
@@ -63,9 +72,11 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
|
|
||||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||||
parcel.writeBooleanCompat(manualSelection)
|
parcel.writeBooleanCompat(manualSelection)
|
||||||
|
parcel.writeString(tag ?: "")
|
||||||
parcel.writeString(applicationId ?: "")
|
parcel.writeString(applicationId ?: "")
|
||||||
parcel.writeString(webDomain ?: "")
|
parcel.writeString(webDomain ?: "")
|
||||||
parcel.writeString(webScheme ?: "")
|
parcel.writeString(webScheme ?: "")
|
||||||
|
parcel.writeString(relyingParty ?: "")
|
||||||
parcel.writeString(otpString ?: "")
|
parcel.writeString(otpString ?: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,24 +90,54 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun containsOnlyNullValues(): Boolean {
|
fun containsOnlyNullValues(): Boolean {
|
||||||
return applicationId == null
|
return tag == null
|
||||||
|
&& applicationId == null
|
||||||
&& webDomain == null
|
&& webDomain == null
|
||||||
&& webScheme == null
|
&& webScheme == null
|
||||||
|
&& relyingParty == null
|
||||||
&& otpString == null
|
&& otpString == null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isASearchByDomain(): Boolean {
|
private fun isADomainSearch(): Boolean {
|
||||||
return toString() == webDomain && webDomain != null
|
return toString() == webDomain && webDomain != null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isAPasskeySearch: Boolean = false
|
||||||
|
|
||||||
|
var query: String? = null
|
||||||
|
|
||||||
|
fun buildSearchParameters(): SearchParameters {
|
||||||
|
return SearchParameters().apply {
|
||||||
|
searchQuery = query ?: this@SearchInfo.toString()
|
||||||
|
allowEmptyQuery = false
|
||||||
|
searchInTitles = !isAPasskeySearch
|
||||||
|
searchInUsernames = false
|
||||||
|
searchInPasswords = false
|
||||||
|
searchInUrls = !isAPasskeySearch
|
||||||
|
searchByDomain = isADomainSearch()
|
||||||
|
searchInNotes = false
|
||||||
|
searchInOTP = false
|
||||||
|
searchInOther = true
|
||||||
|
searchInUUIDs = false
|
||||||
|
searchInTags = false
|
||||||
|
searchInRelyingParty = isAPasskeySearch
|
||||||
|
searchInCurrentGroup = false
|
||||||
|
searchInSearchableGroup = true
|
||||||
|
searchInRecycleBin = false
|
||||||
|
searchInTemplates = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other !is SearchInfo) return false
|
if (other !is SearchInfo) return false
|
||||||
|
|
||||||
if (manualSelection != other.manualSelection) return false
|
if (manualSelection != other.manualSelection) return false
|
||||||
|
if (tag != other.tag) return false
|
||||||
if (applicationId != other.applicationId) return false
|
if (applicationId != other.applicationId) return false
|
||||||
if (webDomain != other.webDomain) return false
|
if (webDomain != other.webDomain) return false
|
||||||
if (webScheme != other.webScheme) return false
|
if (webScheme != other.webScheme) return false
|
||||||
|
if (relyingParty != other.relyingParty) return false
|
||||||
if (otpString != other.otpString) return false
|
if (otpString != other.otpString) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -104,15 +145,17 @@ class SearchInfo : ObjectNameResource, Parcelable {
|
|||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = manualSelection.hashCode()
|
var result = manualSelection.hashCode()
|
||||||
|
result = 31 * result + (tag?.hashCode() ?: 0)
|
||||||
result = 31 * result + (applicationId?.hashCode() ?: 0)
|
result = 31 * result + (applicationId?.hashCode() ?: 0)
|
||||||
result = 31 * result + (webDomain?.hashCode() ?: 0)
|
result = 31 * result + (webDomain?.hashCode() ?: 0)
|
||||||
result = 31 * result + (webScheme?.hashCode() ?: 0)
|
result = 31 * result + (webScheme?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (relyingParty?.hashCode() ?: 0)
|
||||||
result = 31 * result + (otpString?.hashCode() ?: 0)
|
result = 31 * result + (otpString?.hashCode() ?: 0)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return otpString ?: webDomain ?: applicationId ?: ""
|
return otpString ?: webDomain ?: applicationId ?: relyingParty ?: tag ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import android.net.Uri
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.kunzisoft.keepass.database.element.Field
|
import com.kunzisoft.keepass.database.element.Field
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
import com.kunzisoft.keepass.otp.TokenCalculator.HOTP_INITIAL_COUNTER
|
import com.kunzisoft.keepass.otp.TokenCalculator.HOTP_INITIAL_COUNTER
|
||||||
import com.kunzisoft.keepass.otp.TokenCalculator.HashAlgorithm
|
import com.kunzisoft.keepass.otp.TokenCalculator.HashAlgorithm
|
||||||
import com.kunzisoft.keepass.otp.TokenCalculator.OTP_DEFAULT_DIGITS
|
import com.kunzisoft.keepass.otp.TokenCalculator.OTP_DEFAULT_DIGITS
|
||||||
@@ -434,6 +435,25 @@ object OtpEntryFields {
|
|||||||
buildOtpUri(otpElement, title, username).toString()))
|
buildOtpUri(otpElement, title, username).toString()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun EntryInfo.setOtp(otpString: String): Boolean {
|
||||||
|
// Replace the OTP field
|
||||||
|
parseOTPUri(otpString)?.let { otpElement ->
|
||||||
|
if (title.isEmpty())
|
||||||
|
title = otpElement.issuer
|
||||||
|
if (username.isEmpty())
|
||||||
|
username = otpElement.name
|
||||||
|
// Add OTP field
|
||||||
|
val mutableCustomFields = customFields as ArrayList<Field>
|
||||||
|
val otpField = OtpEntryFields.buildOtpField(otpElement, null, null)
|
||||||
|
if (mutableCustomFields.contains(otpField)) {
|
||||||
|
mutableCustomFields.remove(otpField)
|
||||||
|
}
|
||||||
|
mutableCustomFields.add(otpField)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build new generated fields in a new list from [fieldsToParse] in parameter,
|
* Build new generated fields in a new list from [fieldsToParse] in parameter,
|
||||||
* Remove parameters fields use to generate auto fields
|
* Remove parameters fields use to generate auto fields
|
||||||
@@ -486,4 +506,28 @@ object OtpEntryFields {
|
|||||||
newCustomFields.add(Field(OTP_TOKEN_FIELD))
|
newCustomFields.add(Field(OTP_TOKEN_FIELD))
|
||||||
return newCustomFields
|
return newCustomFields
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field ignored for a search or a form filling
|
||||||
|
*/
|
||||||
|
fun Field.isOtpExclusion(): Boolean {
|
||||||
|
return when(name) {
|
||||||
|
OTP_FIELD -> true
|
||||||
|
TOTP_SEED_FIELD -> true
|
||||||
|
TOTP_SETTING_FIELD -> true
|
||||||
|
HMACOTP_SECRET_FIELD -> true
|
||||||
|
HMACOTP_SECRET_HEX_FIELD -> true
|
||||||
|
HMACOTP_SECRET_BASE32_FIELD -> true
|
||||||
|
HMACOTP_SECRET_BASE64_FIELD -> true
|
||||||
|
HMACOTP_SECRET_COUNTER_FIELD -> true
|
||||||
|
TIMEOTP_SECRET_FIELD -> true
|
||||||
|
TIMEOTP_SECRET_HEX_FIELD -> true
|
||||||
|
TIMEOTP_SECRET_BASE32_FIELD -> true
|
||||||
|
TIMEOTP_SECRET_BASE64_FIELD -> true
|
||||||
|
TIMEOTP_LENGTH_FIELD -> true
|
||||||
|
TIMEOTP_PERIOD_FIELD -> true
|
||||||
|
TIMEOTP_ALGORITHM_FIELD -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
117
database/src/main/java/com/kunzisoft/keepass/utils/UUIDUtils.kt
Normal file
117
database/src/main/java/com/kunzisoft/keepass/utils/UUIDUtils.kt
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.kunzisoft.keepass.utils
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
object UUIDUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specific UUID string format for KeePass database
|
||||||
|
*/
|
||||||
|
fun UUID.asHexString(): String? {
|
||||||
|
try {
|
||||||
|
val buf = uuidTo16Bytes(this)
|
||||||
|
|
||||||
|
val len = buf.size
|
||||||
|
if (len == 0) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
val sb = StringBuilder()
|
||||||
|
|
||||||
|
var bt: Short
|
||||||
|
var high: Char
|
||||||
|
var low: Char
|
||||||
|
for (b in buf) {
|
||||||
|
bt = (b.toInt() and 0xFF).toShort()
|
||||||
|
high = (bt.toInt() ushr 4).toChar()
|
||||||
|
low = (bt.toInt() and 0x0F).toChar()
|
||||||
|
sb.append(byteToChar(high))
|
||||||
|
sb.append(byteToChar(low))
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From a specific UUID KeePass database string format,
|
||||||
|
* Note : For a standard UUID string format, use UUID.fromString()
|
||||||
|
*/
|
||||||
|
fun String.asUUID(): UUID? {
|
||||||
|
if (this.length != 32) return null
|
||||||
|
|
||||||
|
val charArray = this.lowercase(Locale.getDefault()).toCharArray()
|
||||||
|
val leastSignificantChars = CharArray(16)
|
||||||
|
val mostSignificantChars = CharArray(16)
|
||||||
|
|
||||||
|
var i = 31
|
||||||
|
while (i >= 0) {
|
||||||
|
if (i >= 16) {
|
||||||
|
mostSignificantChars[32 - i] = charArray[i]
|
||||||
|
mostSignificantChars[31 - i] = charArray[i - 1]
|
||||||
|
} else {
|
||||||
|
leastSignificantChars[16 - i] = charArray[i]
|
||||||
|
leastSignificantChars[15 - i] = charArray[i - 1]
|
||||||
|
}
|
||||||
|
i = i - 2
|
||||||
|
}
|
||||||
|
val standardUUIDString = StringBuilder()
|
||||||
|
standardUUIDString.append(leastSignificantChars)
|
||||||
|
standardUUIDString.append(mostSignificantChars)
|
||||||
|
standardUUIDString.insert(8, '-')
|
||||||
|
standardUUIDString.insert(13, '-')
|
||||||
|
standardUUIDString.insert(18, '-')
|
||||||
|
standardUUIDString.insert(23, '-')
|
||||||
|
return try {
|
||||||
|
UUID.fromString(standardUUIDString.toString())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ByteArray.asUUID(): UUID {
|
||||||
|
val bb = ByteBuffer.wrap(this)
|
||||||
|
val firstLong = bb.getLong()
|
||||||
|
val secondLong = bb.getLong()
|
||||||
|
return UUID(firstLong, secondLong)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun UUID.asBytes(): ByteArray {
|
||||||
|
return ByteBuffer.allocate(16).apply {
|
||||||
|
putLong(mostSignificantBits)
|
||||||
|
putLong(leastSignificantBits)
|
||||||
|
}.array()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use short to represent unsigned byte
|
||||||
|
private fun byteToChar(bt: Char): Char {
|
||||||
|
return if (bt.code >= 10) {
|
||||||
|
('A'.code + bt.code - 10).toChar()
|
||||||
|
} else {
|
||||||
|
('0'.code + bt.code).toChar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
|
||||||
*
|
|
||||||
* This file is part of KeePassDX.
|
|
||||||
*
|
|
||||||
* KeePassDX is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* KeePassDX is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.kunzisoft.keepass.utils;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import static com.kunzisoft.keepass.utils.StreamBytesUtilsKt.uuidTo16Bytes;
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
public class UuidUtil {
|
|
||||||
|
|
||||||
public static @Nullable String toHexString(@Nullable UUID uuid) {
|
|
||||||
if (uuid == null) { return null; }
|
|
||||||
try {
|
|
||||||
byte[] buf = uuidTo16Bytes(uuid);
|
|
||||||
|
|
||||||
int len = buf.length;
|
|
||||||
if (len == 0) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
|
|
||||||
short bt;
|
|
||||||
char high, low;
|
|
||||||
for (byte b : buf) {
|
|
||||||
bt = (short) (b & 0xFF);
|
|
||||||
high = (char) (bt >>> 4);
|
|
||||||
low = (char) (bt & 0x0F);
|
|
||||||
sb.append(byteToChar(high));
|
|
||||||
sb.append(byteToChar(low));
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.toString();
|
|
||||||
} catch (Exception e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static @Nullable UUID fromHexString(@Nullable String hexString) {
|
|
||||||
if (hexString == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (hexString.length() != 32)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
char[] charArray = hexString.toLowerCase().toCharArray();
|
|
||||||
char[] leastSignificantChars = new char[16];
|
|
||||||
char[] mostSignificantChars = new char[16];
|
|
||||||
|
|
||||||
for (int i = 31; i >= 0; i = i-2) {
|
|
||||||
if (i >= 16) {
|
|
||||||
mostSignificantChars[32-i] = charArray[i];
|
|
||||||
mostSignificantChars[31-i] = charArray[i-1];
|
|
||||||
} else {
|
|
||||||
leastSignificantChars[16-i] = charArray[i];
|
|
||||||
leastSignificantChars[15-i] = charArray[i-1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
StringBuilder standardUUIDString = new StringBuilder();
|
|
||||||
standardUUIDString.append(leastSignificantChars);
|
|
||||||
standardUUIDString.append(mostSignificantChars);
|
|
||||||
standardUUIDString.insert(8, '-');
|
|
||||||
standardUUIDString.insert(13, '-');
|
|
||||||
standardUUIDString.insert(18, '-');
|
|
||||||
standardUUIDString.insert(23, '-');
|
|
||||||
try {
|
|
||||||
return UUID.fromString(standardUUIDString.toString());
|
|
||||||
} catch (Exception e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use short to represent unsigned byte
|
|
||||||
private static char byteToChar(char bt) {
|
|
||||||
if (bt >= 10) {
|
|
||||||
return (char)('A' + bt - 10);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return (char)('0' + bt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Jeremy Jamet / Kunzisoft.
|
||||||
|
*
|
||||||
|
* This file is part of KeePassDX.
|
||||||
|
*
|
||||||
|
* KeePassDX is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* KeePassDX is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
package com.kunzisoft.keepass.tests.utils
|
package com.kunzisoft.keepass.tests.utils
|
||||||
|
|
||||||
import com.kunzisoft.keepass.utils.UuidUtil
|
import com.kunzisoft.keepass.utils.UUIDUtils.asBytes
|
||||||
|
import com.kunzisoft.keepass.utils.UUIDUtils.asHexString
|
||||||
|
import com.kunzisoft.keepass.utils.UUIDUtils.asUUID
|
||||||
import junit.framework.TestCase
|
import junit.framework.TestCase
|
||||||
import java.util.*
|
import java.util.UUID
|
||||||
|
|
||||||
class UUIDTest: TestCase() {
|
class UUIDTest: TestCase() {
|
||||||
|
|
||||||
fun testUUID() {
|
fun testUUIDHexString() {
|
||||||
val randomUUID = UUID.randomUUID()
|
val randomUUID = UUID.randomUUID()
|
||||||
val hexStringUUID = UuidUtil.toHexString(randomUUID)
|
val hexStringUUID = randomUUID.asHexString()
|
||||||
val retrievedUUID = UuidUtil.fromHexString(hexStringUUID)
|
val retrievedUUID = hexStringUUID?.asUUID()
|
||||||
|
assertEquals(randomUUID, retrievedUUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testUUIDString() {
|
||||||
|
val staticUUID = "4be0643f-1d98-573b-97cd-ca98a65347dd"
|
||||||
|
val stringUUID = UUID.fromString(staticUUID).asBytes().asUUID().toString()
|
||||||
|
assertEquals(staticUUID, stringUUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testUUIDBytes() {
|
||||||
|
val randomUUID = UUID.randomUUID()
|
||||||
|
val byteArrayUUID = randomUUID.asBytes()
|
||||||
|
val retrievedUUID = byteArrayUUID.asUUID()
|
||||||
assertEquals(randomUUID, retrievedUUID)
|
assertEquals(randomUUID, retrievedUUID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1
gradle/wrapper/gradle-wrapper.properties
vendored
1
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
#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-8.14.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ android {
|
|||||||
compileSdkVersion 34
|
compileSdkVersion 34
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 14
|
minSdkVersion 19
|
||||||
targetSdkVersion 34
|
targetSdkVersion 34
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ android {
|
|||||||
compileSdkVersion 34
|
compileSdkVersion 34
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 14
|
minSdkVersion 19
|
||||||
targetSdkVersion 34
|
targetSdkVersion 34
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user