fix: remove ChallengeResponseViewModel and add HardwareKeyActivity

This commit is contained in:
J-Jamet
2023-01-22 15:10:36 +01:00
parent adbdb9a642
commit f9dc456032
8 changed files with 203 additions and 244 deletions

View File

@@ -156,6 +156,8 @@
android:name="com.kunzisoft.keepass.settings.SettingsAdvancedUnlockActivity" />
<activity
android:name="com.kunzisoft.keepass.settings.AutofillSettingsActivity" />
<activity
android:name="com.kunzisoft.keepass.hardware.HardwareKeyActivity" />
<activity
android:name="com.kunzisoft.keepass.activities.EntrySelectionLauncherActivity"
android:theme="@style/Theme.Transparent"

View File

@@ -36,7 +36,7 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.helpers.ExternalFileHelper
import com.kunzisoft.keepass.activities.helpers.setOpenDocumentClickListener
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.hardware.HardwareKeyResponseHelper
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.password.PasswordEntropy
import com.kunzisoft.keepass.utils.UriUtil
@@ -169,7 +169,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
hardwareKeySelectionView.selectionListener = { hardwareKey ->
hardwareKeyCheckBox.isChecked = true
hardwareKeySelectionView.error =
if (!HardwareKeyResponseHelper.isHardwareKeyAvailable(requireActivity(), hardwareKey)) {
if (!HardwareKeyActivity.isHardwareKeyAvailable(requireActivity(), hardwareKey)) {
// show hardware driver dialog if required
getString(R.string.error_driver_required, hardwareKey.toString())
} else {
@@ -231,7 +231,7 @@ class SetMainCredentialDialogFragment : DatabaseDialogFragment() {
showEmptyPasswordConfirmationDialog()
} else if (!error
&& hardwareKey != null
&& !HardwareKeyResponseHelper.isHardwareKeyAvailable(
&& !HardwareKeyActivity.isHardwareKeyAvailable(
requireActivity(), hardwareKey, false)
) {
// show hardware driver dialog if required

View File

@@ -9,7 +9,6 @@ import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.MainCredential
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.viewmodels.ChallengeResponseViewModel
import com.kunzisoft.keepass.viewmodels.DatabaseViewModel
abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
@@ -18,12 +17,10 @@ abstract class DatabaseActivity: StylishActivity(), DatabaseRetrieval {
protected var mDatabaseTaskProvider: DatabaseTaskProvider? = null
protected var mDatabase: Database? = null
private val mChallengeResponseViewModel: ChallengeResponseViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mDatabaseTaskProvider = DatabaseTaskProvider(this, mChallengeResponseViewModel)
mDatabaseTaskProvider = DatabaseTaskProvider(this)
mDatabaseTaskProvider?.onDatabaseRetrieved = { database ->
val databaseWasReloaded = database?.wasReloaded == true

View File

@@ -19,7 +19,6 @@
*/
package com.kunzisoft.keepass.database.action
import android.app.Service
import android.content.*
import android.content.Context.*
import android.net.Uri
@@ -43,13 +42,13 @@ import com.kunzisoft.keepass.database.element.database.CompressionAlgorithm
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.database.exception.InvalidCredentialsDatabaseException
import com.kunzisoft.keepass.hardware.HardwareKey
import com.kunzisoft.keepass.hardware.HardwareKeyResponseHelper
import com.kunzisoft.keepass.hardware.HardwareKeyActivity
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.model.ProgressMessage
import com.kunzisoft.keepass.model.SnapFileDatabaseInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_CHALLENGE_RESPONDED
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_ASSIGN_PASSWORD_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_COPY_NODES_TASK
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_CREATE_ENTRY_TASK
@@ -86,7 +85,6 @@ import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment
import com.kunzisoft.keepass.tasks.ProgressTaskDialogFragment.Companion.PROGRESS_TASK_DIALOG_TAG
import com.kunzisoft.keepass.utils.DATABASE_START_TASK_ACTION
import com.kunzisoft.keepass.utils.DATABASE_STOP_TASK_ACTION
import com.kunzisoft.keepass.viewmodels.ChallengeResponseViewModel
import kotlinx.coroutines.launch
import java.util.*
@@ -94,10 +92,11 @@ import java.util.*
* Utility class to connect an activity or a service to the DatabaseTaskNotificationService,
* Useful to retrieve a database instance and sending tasks commands
*/
class DatabaseTaskProvider {
class DatabaseTaskProvider(private var context: Context) {
private var activity: FragmentActivity? = null
private var context: Context
// To show dialog only if context is an activity
private var activity: FragmentActivity? = try { context as? FragmentActivity? }
catch (_: Exception) { null }
var onDatabaseRetrieved: ((database: Database?) -> Unit)? = null
@@ -105,7 +104,10 @@ class DatabaseTaskProvider {
actionTask: String,
result: ActionRunnable.Result) -> Unit)? = null
private var intentDatabaseTask: Intent
private var intentDatabaseTask: Intent = Intent(
context.applicationContext,
DatabaseTaskNotificationService::class.java
)
private var databaseTaskBroadcastReceiver: BroadcastReceiver? = null
private var mBinder: DatabaseTaskNotificationService.ActionTaskBinder? = null
@@ -115,54 +117,6 @@ class DatabaseTaskProvider {
private var progressTaskDialogFragment: ProgressTaskDialogFragment? = null
private var databaseChangedDialogFragment: DatabaseChangedDialogFragment? = null
private var mChallengeResponseViewModel: ChallengeResponseViewModel? = null
constructor(activity: FragmentActivity,
challengeResponseViewModel: ChallengeResponseViewModel) {
this.activity = activity
this.context = activity
this.intentDatabaseTask = Intent(activity.applicationContext,
DatabaseTaskNotificationService::class.java)
// ViewModel used to keep response if activity recreated
this.mChallengeResponseViewModel = challengeResponseViewModel
// To manage hardware key challenge response
val hardwareKeyResponseHelper = HardwareKeyResponseHelper(activity)
hardwareKeyResponseHelper.buildHardwareKeyResponse { responseData, _ ->
// TODO Verify database
// Send to view model in case activity is restarted and not yet connected to service
challengeResponseViewModel.respond(responseData ?: ByteArray(0))
}
challengeResponseViewModel.dataResponded.observe(activity) { response ->
// Consume the response
if (response != null) {
val binder = mBinder
if (binder != null) {
binder.getService().respondToChallenge(response)
challengeResponseViewModel.consumeResponse()
}
}
}
this.requestChallengeListener = object: DatabaseTaskNotificationService.RequestChallengeListener {
override fun onChallengeResponseRequested(hardwareKey: HardwareKey, seed: ByteArray?) {
if (HardwareKeyResponseHelper.isHardwareKeyAvailable(activity, hardwareKey)) {
hardwareKeyResponseHelper.launchChallengeForResponse(hardwareKey, seed)
} else {
throw InvalidCredentialsDatabaseException(
context.getString(R.string.error_driver_required, hardwareKey.toString())
)
}
}
}
}
constructor(service: Service) {
this.context = service
this.intentDatabaseTask = Intent(service.applicationContext,
DatabaseTaskNotificationService::class.java)
}
fun destroy() {
this.activity = null
this.onDatabaseRetrieved = null
@@ -172,7 +126,6 @@ class DatabaseTaskProvider {
this.serviceConnection = null
this.progressTaskDialogFragment = null
this.databaseChangedDialogFragment = null
this.mChallengeResponseViewModel = null
}
private val actionTaskListener = object: DatabaseTaskNotificationService.ActionTaskListener {
@@ -235,7 +188,19 @@ class DatabaseTaskProvider {
}
}
private var requestChallengeListener: DatabaseTaskNotificationService.RequestChallengeListener? = null
private var requestChallengeListener = object: DatabaseTaskNotificationService.RequestChallengeListener {
override fun onChallengeResponseRequested(
hardwareKey: HardwareKey,
seed: ByteArray?
) {
HardwareKeyActivity
.launchHardwareKeyActivity(
context,
hardwareKey,
seed
)
}
}
private fun startDialog(progressMessage: ProgressMessage) {
activity?.let { activity ->
@@ -280,7 +245,6 @@ class DatabaseTaskProvider {
getService().checkDatabaseInfo()
getService().checkAction()
}
mChallengeResponseViewModel?.resendResponse()
}
override fun onServiceDisconnected(name: ComponentName?) {
@@ -295,9 +259,7 @@ class DatabaseTaskProvider {
service?.addDatabaseListener(databaseListener)
service?.addDatabaseFileInfoListener(databaseInfoListener)
service?.addActionTaskListener(actionTaskListener)
requestChallengeListener?.let {
service?.addRequestChallengeListener(it)
}
service?.setRequestChallengeListener(requestChallengeListener)
}
private fun removeServiceListeners(service: DatabaseTaskNotificationService.ActionTaskBinder?) {
@@ -762,6 +724,13 @@ class DatabaseTaskProvider {
, ACTION_DATABASE_SAVE)
}
fun startChallengeResponded(response: ByteArray?) {
start(Bundle().apply {
putByteArray(DatabaseTaskNotificationService.DATA_BYTES, response)
}
, ACTION_CHALLENGE_RESPONDED)
}
companion object {
private val TAG = DatabaseTaskProvider::class.java.name
}

View File

@@ -0,0 +1,150 @@
package com.kunzisoft.keepass.hardware
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.legacy.DatabaseActivity
import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.utils.UriUtil
/**
* Special activity to deal with hardware key drivers,
* return the response to the database service once finished
*/
class HardwareKeyActivity: DatabaseActivity(){
// To manage hardware key challenge response
private val resultCallback = ActivityResultCallback<ActivityResult> { result ->
if (result.resultCode == Activity.RESULT_OK) {
val challengeResponse: ByteArray? = result.data?.getByteArrayExtra(HARDWARE_KEY_RESPONSE_KEY)
Log.d(TAG, "Response form challenge")
mDatabaseTaskProvider?.startChallengeResponded(challengeResponse ?: ByteArray(0))
} else {
Log.e(TAG, "Response from challenge error")
mDatabaseTaskProvider?.startChallengeResponded(ByteArray(0))
}
finish()
}
private var activityResultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
resultCallback
)
override fun onDatabaseRetrieved(database: Database?) {
super.onDatabaseRetrieved(database)
val hardwareKey = HardwareKey.getHardwareKeyFromString(
intent.getStringExtra(DATA_HARDWARE_KEY)
)
if (isHardwareKeyAvailable(this, hardwareKey)) {
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()
}
}
} else {
finish()
}
}
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)
}
)
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)
})
}
fun isHardwareKeyAvailable(
context: Context,
hardwareKey: HardwareKey?,
showDialog: Boolean = true
): Boolean {
if (hardwareKey == null)
return false
return when (hardwareKey) {
/*
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2 under development
if (showDialog)
UnderDevelopmentFeatureDialogFragment()
.show(activity.supportFragmentManager, "underDevFeatureDialog")
false
}
*/
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
// Check available intent
val yubikeyDriverAvailable =
Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT)
.resolveActivity(context.packageManager) != null
if (showDialog && !yubikeyDriverAvailable)
showHardwareKeyDriverNeeded(context, hardwareKey)
yubikeyDriverAvailable
}
}
}
private fun showHardwareKeyDriverNeeded(
context: Context,
hardwareKey: HardwareKey
) {
val builder = AlertDialog.Builder(context)
builder
.setMessage(
context.getString(R.string.error_driver_required, hardwareKey.toString())
)
.setPositiveButton(R.string.download) { _, _ ->
UriUtil.openExternalApp(context, context.getString(R.string.key_driver_app_id))
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
builder.create().show()
}
}
}

View File

@@ -1,144 +0,0 @@
package com.kunzisoft.keepass.hardware
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.utils.UriUtil
import kotlinx.coroutines.launch
class HardwareKeyResponseHelper {
private var activity: FragmentActivity? = null
private var fragment: Fragment? = null
private var getChallengeResponseResultLauncher: ActivityResultLauncher<Intent>? = null
constructor(context: FragmentActivity) {
this.activity = context
this.fragment = null
}
constructor(context: Fragment) {
this.activity = context.activity
this.fragment = context
}
fun buildHardwareKeyResponse(onChallengeResponded: (challengeResponse: ByteArray?,
extra: Bundle?) -> Unit) {
val resultCallback = ActivityResultCallback<ActivityResult> { result ->
if (result.resultCode == Activity.RESULT_OK) {
val challengeResponse: ByteArray? = result.data?.getByteArrayExtra(HARDWARE_KEY_RESPONSE_KEY)
Log.d(TAG, "Response form challenge")
onChallengeResponded.invoke(challengeResponse,
result.data?.getBundleExtra(EXTRA_BUNDLE_KEY))
} else {
Log.e(TAG, "Response from challenge error")
onChallengeResponded.invoke(null,
result.data?.getBundleExtra(EXTRA_BUNDLE_KEY))
}
}
getChallengeResponseResultLauncher = if (fragment != null) {
fragment?.registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
resultCallback
)
} else {
activity?.registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
resultCallback
)
}
}
fun launchChallengeForResponse(hardwareKey: HardwareKey, seed: ByteArray?) {
when (hardwareKey) {
/*
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2 under development
throw Exception("FIDO2 not implemented")
}
*/
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
// 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
getChallengeResponseResultLauncher!!.launch(
Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT).apply {
putExtra(HARDWARE_KEY_CHALLENGE_KEY, challenge)
}
)
Log.d(TAG, "Challenge sent")
}
}
}
companion object {
private val TAG = HardwareKeyResponseHelper::class.java.simpleName
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"
private const val EXTRA_BUNDLE_KEY = "EXTRA_BUNDLE_KEY"
fun isHardwareKeyAvailable(
activity: FragmentActivity,
hardwareKey: HardwareKey,
showDialog: Boolean = true
): Boolean {
return when (hardwareKey) {
/*
HardwareKey.FIDO2_SECRET -> {
// TODO FIDO2 under development
if (showDialog)
UnderDevelopmentFeatureDialogFragment()
.show(activity.supportFragmentManager, "underDevFeatureDialog")
false
}
*/
HardwareKey.CHALLENGE_RESPONSE_YUBIKEY -> {
// Check available intent
val yubikeyDriverAvailable =
Intent(YUBIKEY_CHALLENGE_RESPONSE_INTENT)
.resolveActivity(activity.packageManager) != null
if (showDialog && !yubikeyDriverAvailable)
showHardwareKeyDriverNeeded(activity, hardwareKey)
yubikeyDriverAvailable
}
}
}
private fun showHardwareKeyDriverNeeded(
activity: FragmentActivity,
hardwareKey: HardwareKey
) {
activity.lifecycleScope.launch {
val builder = AlertDialog.Builder(activity)
builder
.setMessage(
activity.getString(R.string.error_driver_required, hardwareKey.toString())
)
.setPositiveButton(R.string.download) { _, _ ->
UriUtil.openExternalApp(activity, activity.getString(R.string.key_driver_app_id))
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
builder.create().show()
}
}
}
}

View File

@@ -124,8 +124,8 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
mActionTaskListeners.remove(actionTaskListener)
}
@ExperimentalCoroutinesApi
fun addRequestChallengeListener(requestChallengeListener: RequestChallengeListener) {
@OptIn(ExperimentalCoroutinesApi::class)
fun setRequestChallengeListener(requestChallengeListener: RequestChallengeListener) {
mainScope.launch {
val requestChannel = mRequestChallengeListenerChannel
if (requestChannel == null || requestChannel.isEmpty) {
@@ -169,7 +169,10 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
}
interface RequestChallengeListener {
fun onChallengeResponseRequested(hardwareKey: HardwareKey, seed: ByteArray?)
fun onChallengeResponseRequested(
hardwareKey: HardwareKey,
seed: ByteArray?
)
}
fun checkDatabase() {
@@ -270,8 +273,8 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
mResponseChallengeChannel = null
}
@ExperimentalCoroutinesApi
fun respondToChallenge(response: ByteArray) {
@OptIn(ExperimentalCoroutinesApi::class)
private fun respondToChallenge(response: ByteArray) {
mainScope.launch {
val responseChannel = mResponseChallengeChannel
if (responseChannel == null || responseChannel.isEmpty) {
@@ -323,6 +326,12 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
stopSelf()
}
if (intentAction == ACTION_CHALLENGE_RESPONDED) {
intent.getByteArrayExtra(DATA_BYTES)?.let {
respondToChallenge(it)
}
}
val actionRunnable: ActionRunnable? = when (intentAction) {
ACTION_DATABASE_CREATE_TASK -> buildDatabaseCreateActionTask(intent, database)
ACTION_DATABASE_LOAD_TASK -> buildDatabaseLoadActionTask(intent, database)
@@ -756,7 +765,6 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
databaseToMergeUri,
databaseToMergeMainCredential,
{ hardwareKey, seed ->
// TODO fix first challenge response
retrieveResponseFromChallenge(hardwareKey, seed)
},
database,
@@ -1157,6 +1165,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
const val ACTION_DATABASE_UPDATE_PARALLELISM_TASK = "ACTION_DATABASE_UPDATE_PARALLELISM_TASK"
const val ACTION_DATABASE_UPDATE_ITERATIONS_TASK = "ACTION_DATABASE_UPDATE_ITERATIONS_TASK"
const val ACTION_DATABASE_SAVE = "ACTION_DATABASE_SAVE"
const val ACTION_CHALLENGE_RESPONDED = "ACTION_CHALLENGE_RESPONDED"
const val DATABASE_TASK_TITLE_KEY = "DATABASE_TASK_TITLE_KEY"
const val DATABASE_TASK_MESSAGE_KEY = "DATABASE_TASK_MESSAGE_KEY"
@@ -1180,6 +1189,7 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
const val NEW_NODES_KEY = "NEW_NODES_KEY"
const val OLD_ELEMENT_KEY = "OLD_ELEMENT_KEY" // Warning type of this thing change every time
const val NEW_ELEMENT_KEY = "NEW_ELEMENT_KEY" // Warning type of this thing change every time
const val DATA_BYTES = "DATA_BYTES"
fun getListNodesFromBundle(database: Database, bundle: Bundle): List<Node> {
val nodesAction = ArrayList<Node>()

View File

@@ -1,25 +0,0 @@
package com.kunzisoft.keepass.viewmodels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class ChallengeResponseViewModel: ViewModel() {
val dataResponded : LiveData<ByteArray?> get() = _dataResponded
private val _dataResponded = MutableLiveData<ByteArray?>()
fun respond(byteArray: ByteArray) {
_dataResponded.value = byteArray
}
fun resendResponse() {
dataResponded.value?.let {
_dataResponded.value = it
}
}
fun consumeResponse() {
_dataResponded.value = null
}
}