fix: Hardware key #2196

This commit is contained in:
J-Jamet
2025-10-13 10:32:43 +02:00
parent 65857596a6
commit f8d80525d9
6 changed files with 216 additions and 78 deletions

View File

@@ -178,19 +178,22 @@
<activity
android:name="com.kunzisoft.keepass.settings.AppearanceSettingsActivity" />
<activity
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity"
android:name="com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity"
android:theme="@style/Theme.Transparent"
android:exported="false" />
android:exported="false"
android:excludeFromRecents="true" />
<activity
android:name="com.kunzisoft.keepass.credentialprovider.activity.AutofillLauncherActivity"
android:theme="@style/Theme.Transparent"
android:configChanges="keyboardHidden"
android:excludeFromRecents="true"/>
android:exported="false"
android:excludeFromRecents="true" />
<activity
android:name="com.kunzisoft.keepass.credentialprovider.activity.EntrySelectionLauncherActivity"
android:theme="@style/Theme.Transparent"
android:launchMode="singleInstance"
android:exported="true">
android:exported="true"
android:excludeFromRecents="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
@@ -209,8 +212,8 @@
android:name="com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity"
android:theme="@style/Theme.Transparent"
android:configChanges="keyboardHidden"
android:excludeFromRecents="true"
android:exported="false"
android:excludeFromRecents="true"
tools:targetApi="upside_down_cake" />
<service
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"

View File

@@ -37,7 +37,7 @@ import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.UriUtil.openUrl

View File

@@ -1,20 +1,28 @@
package com.kunzisoft.keepass.hardware
package com.kunzisoft.keepass.credentialprovider.activity
import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.util.Log
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.legacy.DatabaseModeActivity
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.addHardwareKey
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.addSeed
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.buildHardwareKeyChallenge
import com.kunzisoft.keepass.credentialprovider.viewmodel.HardwareKeyLauncherViewModel.Companion.isYubikeyDriverAvailable
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_LOAD_TASK
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.utils.AppUtil.openExternalApp
import kotlinx.coroutines.launch
/**
* Special activity to deal with hardware key drivers,
@@ -22,23 +30,12 @@ import com.kunzisoft.keepass.utils.AppUtil.openExternalApp
*/
class HardwareKeyActivity: DatabaseModeActivity(){
// To manage hardware key challenge response
private val resultCallback = ActivityResultCallback<ActivityResult> { result ->
if (result.resultCode == RESULT_OK) {
val challengeResponse: ByteArray? = result.data?.getByteArrayExtra(HARDWARE_KEY_RESPONSE_KEY)
Log.d(TAG, "Response form challenge")
mDatabaseViewModel.onChallengeResponded(challengeResponse)
} else {
Log.e(TAG, "Response from challenge error")
mDatabaseViewModel.onChallengeResponded(null)
}
finish()
}
private val mHardwareKeyLauncherViewModel: HardwareKeyLauncherViewModel by viewModels()
private var activityResultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
resultCallback
)
private var activityResultLauncher: ActivityResultLauncher<Intent> =
this.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
mHardwareKeyLauncherViewModel.manageSelectionResult(it)
}
override fun applyCustomStyle(): Boolean {
return false
@@ -48,65 +45,60 @@ class HardwareKeyActivity: DatabaseModeActivity(){
return false
}
override fun onDatabaseRetrieved(database: ContextualDatabase) {
val hardwareKey = HardwareKey.getHardwareKeyFromString(
intent.getStringExtra(DATA_HARDWARE_KEY)
)
if (isHardwareKeyAvailable(this, hardwareKey, true) {
mDatabaseViewModel.onChallengeResponded(null)
}) {
when (hardwareKey) {
/*
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2 under development
throw Exception("FIDO2 not implemented")
}
*/
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
launchYubikeyChallengeForResponse(intent.getByteArrayExtra(DATA_SEED))
}
else -> {
finish()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
mHardwareKeyLauncherViewModel.uiState.collect { uiState ->
when (uiState) {
is HardwareKeyLauncherViewModel.UIState.Loading -> {}
is HardwareKeyLauncherViewModel.UIState.LaunchChallengeActivityForResponse -> {
// Send to the driver
activityResultLauncher.launch(
buildHardwareKeyChallenge(uiState.challenge)
)
}
is HardwareKeyLauncherViewModel.UIState.OnChallengeResponded -> {
mDatabaseViewModel.onChallengeResponded(uiState.response)
}
}
}
}
}
private fun launchYubikeyChallengeForResponse(seed: ByteArray?) {
// Transform the seed before sending
var challenge: ByteArray? = null
if (seed != null) {
challenge = ByteArray(64)
seed.copyInto(challenge, 0, 0, 32)
challenge.fill(32, 32, 64)
}
// Send to the driver
activityResultLauncher.launch(
Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT).apply {
putExtra(HARDWARE_KEY_CHALLENGE_KEY, challenge)
override fun onDatabaseRetrieved(database: ContextualDatabase) {
super.onDatabaseRetrieved(database)
mHardwareKeyLauncherViewModel.launchActionIfNeeded(intent, mSpecialMode, database)
}
override fun onDatabaseActionFinished(
database: ContextualDatabase,
actionTask: String,
result: ActionRunnable.Result
) {
super.onDatabaseActionFinished(database, actionTask, result)
when (actionTask) {
ACTION_DATABASE_LOAD_TASK -> {
finish()
}
)
Log.d(TAG, "Challenge sent")
}
}
companion object {
private val TAG = HardwareKeyActivity::class.java.simpleName
private const val DATA_HARDWARE_KEY = "DATA_HARDWARE_KEY"
private const val DATA_SEED = "DATA_SEED"
private const val YUBIKEY_CHALLENGE_RESPONSE_INTENT = "android.yubikey.intent.action.CHALLENGE_RESPONSE"
private const val HARDWARE_KEY_CHALLENGE_KEY = "challenge"
private const val HARDWARE_KEY_RESPONSE_KEY = "response"
fun launchHardwareKeyActivity(
context: Context,
hardwareKey: HardwareKey,
seed: ByteArray?
) {
context.startActivity(Intent(context, HardwareKeyActivity::class.java).apply {
flags = FLAG_ACTIVITY_NEW_TASK
putExtra(DATA_HARDWARE_KEY, hardwareKey.value)
putExtra(DATA_SEED, seed)
context.startActivity(
Intent(
context,
HardwareKeyActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
addHardwareKey(hardwareKey)
addSeed(seed)
})
}
@@ -130,15 +122,14 @@ class HardwareKeyActivity: DatabaseModeActivity(){
*/
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
// Check available intent
val yubikeyDriverAvailable =
Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT)
.resolveActivity(context.packageManager) != null
if (showDialog && !yubikeyDriverAvailable
&& context is Activity)
// TODO Dialog
val yubikeyDriverAvailable = isYubikeyDriverAvailable(context)
if (showDialog && !yubikeyDriverAvailable && context is Activity) {
showHardwareKeyDriverNeeded(context, hardwareKey) {
onDialogDismissed?.onDismiss(it)
context.finish()
}
}
yubikeyDriverAvailable
}
}

View File

@@ -82,7 +82,7 @@ abstract class CredentialLauncherViewModel(application: Application): AndroidVie
mSelectionResult = null
}
abstract fun manageRegistrationResult(activityResult: ActivityResult)
open fun manageRegistrationResult(activityResult: ActivityResult) {}
open fun onExceptionOccurred(e: Throwable) {
showError(e)
@@ -93,7 +93,7 @@ abstract class CredentialLauncherViewModel(application: Application): AndroidVie
specialMode: SpecialMode,
database: ContextualDatabase?
) {
if (database != null && database.loaded) {
if (database != null) {
onDatabaseRetrieved(database)
}
if (isResultLauncherRegistered.not()) {

View File

@@ -0,0 +1,144 @@
package com.kunzisoft.keepass.credentialprovider.viewmodel
import android.app.Activity.RESULT_OK
import android.app.Application
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.activity.result.ActivityResult
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity.Companion.isHardwareKeyAvailable
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.hardware.HardwareKey
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class HardwareKeyLauncherViewModel(application: Application): CredentialLauncherViewModel(application) {
private val mUiState = MutableStateFlow<UIState>(UIState.Loading)
val uiState: StateFlow<UIState> = mUiState
override suspend fun launchAction(
intent: Intent,
specialMode: SpecialMode,
database: ContextualDatabase?
) {
val hardwareKey = HardwareKey.Companion.getHardwareKeyFromString(
intent.getStringExtra(DATA_HARDWARE_KEY)
)
if (isHardwareKeyAvailable(getApplication(), hardwareKey, true) {
mUiState.value = UIState.OnChallengeResponded(null)
}) {
when (hardwareKey) {
/*
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2 under development
throw Exception("FIDO2 not implemented")
}
*/
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
launchYubikeyChallengeForResponse(intent.getByteArrayExtra(DATA_SEED))
}
else -> {
UIState.OnChallengeResponded(null)
}
}
}
}
private fun launchYubikeyChallengeForResponse(seed: ByteArray?) {
// Transform the seed before sending
var challenge: ByteArray? = null
if (seed != null) {
challenge = ByteArray(64)
seed.copyInto(challenge, 0, 0, 32)
challenge.fill(32, 32, 64)
}
mUiState.value = UIState.LaunchChallengeActivityForResponse(challenge)
Log.d(TAG, "Challenge sent")
}
override fun manageSelectionResult(
database: ContextualDatabase,
activityResult: ActivityResult
) {
super.manageSelectionResult(database, activityResult)
if (activityResult.resultCode == RESULT_OK) {
val challengeResponse: ByteArray? =
activityResult.data?.getByteArrayExtra(HARDWARE_KEY_RESPONSE_KEY)
Log.d(TAG, "Response form challenge")
mUiState.value = UIState.OnChallengeResponded(challengeResponse)
} else {
Log.e(TAG, "Response from challenge error")
mUiState.value = UIState.OnChallengeResponded(null)
}
}
sealed class UIState {
object Loading : UIState()
data class LaunchChallengeActivityForResponse(
val challenge: ByteArray?,
): UIState() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as LaunchChallengeActivityForResponse
return challenge.contentEquals(other.challenge)
}
override fun hashCode(): Int {
return challenge?.contentHashCode() ?: 0
}
}
data class OnChallengeResponded(
val response: ByteArray?
): UIState() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as OnChallengeResponded
return response.contentEquals(other.response)
}
override fun hashCode(): Int {
return response?.contentHashCode() ?: 0
}
}
}
companion object {
private val TAG = HardwareKeyLauncherViewModel::class.java.name
private const val DATA_HARDWARE_KEY = "DATA_HARDWARE_KEY"
private const val DATA_SEED = "DATA_SEED"
// Driver call
private const val YUBIKEY_CHALLENGE_RESPONSE_INTENT = "android.yubikey.intent.action.CHALLENGE_RESPONSE"
private const val HARDWARE_KEY_CHALLENGE_KEY = "challenge"
private const val HARDWARE_KEY_RESPONSE_KEY = "response"
fun isYubikeyDriverAvailable(context: Context): Boolean {
return Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT)
.resolveActivity(context.packageManager) != null
}
fun buildHardwareKeyChallenge(challenge: ByteArray?): Intent {
return Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT).apply {
putExtra(HARDWARE_KEY_CHALLENGE_KEY, challenge)
}
}
fun Intent.addHardwareKey(hardwareKey: HardwareKey) {
putExtra(DATA_HARDWARE_KEY, hardwareKey.value)
}
fun Intent.addSeed(seed: ByteArray?) {
putExtra(DATA_SEED, seed)
}
}
}

View File

@@ -61,7 +61,7 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
import com.kunzisoft.keepass.credentialprovider.activity.HardwareKeyActivity
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.settings.PreferencesUtil