From 5bfaf0388627f85a8fb26d34a6b1fb214ae36691 Mon Sep 17 00:00:00 2001 From: Martin Asprusten Date: Wed, 28 Feb 2024 22:04:07 +0100 Subject: [PATCH 1/5] Added a screen for choosing credentials --- .../org/microg/gms/auth/login/FidoHandler.kt | 6 +- .../microg/gms/fido/core/RequestHandling.kt | 13 +- .../msgs/AuthenticatorGetNextAssertion.kt | 12 ++ .../fido/core/transport/TransportHandler.kt | 63 ++++++-- .../core/transport/nfc/NfcTransportHandler.kt | 14 +- .../screenlock/ScreenLockCredentialStore.kt | 150 +++++++++++++++++- .../screenlock/ScreenLockTransportHandler.kt | 109 ++++++++++--- .../core/transport/usb/UsbTransportHandler.kt | 12 +- .../gms/fido/core/ui/AuthenticatorActivity.kt | 11 +- .../core/ui/CredentialSelectorFragment.kt | 98 ++++++++++++ .../fido_credential_selector_fragment.xml | 18 +++ .../fido_credential_selector_list_item.xml | 51 ++++++ .../res/navigation/nav_fido_authenticator.xml | 14 ++ .../core/src/main/res/values/strings.xml | 2 + 14 files changed, 507 insertions(+), 66 deletions(-) create mode 100644 play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/protocol/msgs/AuthenticatorGetNextAssertion.kt create mode 100644 play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/CredentialSelectorFragment.kt create mode 100644 play-services-fido/core/src/main/res/layout/fido_credential_selector_fragment.xml create mode 100644 play-services-fido/core/src/main/res/layout/fido_credential_selector_list_item.xml diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/login/FidoHandler.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/login/FidoHandler.kt index 24a12116a0..b52e4aefba 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/login/FidoHandler.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/login/FidoHandler.kt @@ -14,6 +14,7 @@ import com.google.android.gms.fido.fido2.api.common.* import kotlinx.coroutines.CancellationException import org.json.JSONArray import org.json.JSONObject +import org.microg.gms.fido.core.AuthenticatorResponseWrapper import org.microg.gms.fido.core.RequestHandlingException import org.microg.gms.fido.core.transport.Transport import org.microg.gms.fido.core.transport.TransportHandlerCallback @@ -68,9 +69,10 @@ class FidoHandler(private val activity: LoginActivity) : TransportHandlerCallbac }) } - private fun sendSuccessResult(response: AuthenticatorResponse, transport: Transport) { - Log.d(TAG, "Finish with success response: $response") + private suspend fun sendSuccessResult(responseWrapper: AuthenticatorResponseWrapper, transport: Transport) { + val response = responseWrapper.responseChoices.get(0).second.invoke() if (response is AuthenticatorAssertionResponse) { + Log.d(TAG, "Finish with success response: $response") sendResult(JSONObject().apply { val base64Flags = Base64.NO_PADDING + Base64.NO_WRAP + Base64.URL_SAFE put("keyHandle", response.keyHandle?.toBase64(base64Flags)) diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/RequestHandling.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/RequestHandling.kt index 1963428b67..e0bf49b203 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/RequestHandling.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/RequestHandling.kt @@ -28,6 +28,14 @@ class MissingPinException(message: String? = null): Exception(message) class WrongPinException(message: String? = null): Exception(message) enum class RequestOptionsType { REGISTER, SIGN } +class UserInfo( + val name: String, + val displayName: String? = null, + val icon: String? = null +) +class AuthenticatorResponseWrapper ( + val responseChoices: List AuthenticatorResponse>> +) val RequestOptions.registerOptions: PublicKeyCredentialCreationOptions get() = when (this) { @@ -150,11 +158,6 @@ private suspend fun isAppIdAllowed(context: Context, appId: String, facetId: Str } suspend fun RequestOptions.checkIsValid(context: Context, facetId: String, packageName: String?) { - if (type == SIGN) { - if (signOptions.allowList.isNullOrEmpty()) { - throw RequestHandlingException(NOT_ALLOWED_ERR, "Request doesn't have a valid list of allowed credentials.") - } - } if (facetId.startsWith("https://")) { if (topDomainOf(Uri.parse(facetId).host) != topDomainOf(rpId)) { throw RequestHandlingException(NOT_ALLOWED_ERR, "RP ID $rpId not allowed from facet $facetId") diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/protocol/msgs/AuthenticatorGetNextAssertion.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/protocol/msgs/AuthenticatorGetNextAssertion.kt new file mode 100644 index 0000000000..aba604b1fc --- /dev/null +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/protocol/msgs/AuthenticatorGetNextAssertion.kt @@ -0,0 +1,12 @@ +package org.microg.gms.fido.core.protocol.msgs + +import com.upokecenter.cbor.CBORObject + +class AuthenticatorGetNextAssertionCommand(request: AuthenticatorGetNextAssertionRequest) : + Ctap2Command(request) { + override fun decodeResponse(obj: CBORObject) = AuthenticatorGetAssertionResponse.decodeFromCbor(obj) + override val timeout: Long + get() = 60000 +} + +class AuthenticatorGetNextAssertionRequest() : Ctap2Request(0x08) \ No newline at end of file diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/TransportHandler.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/TransportHandler.kt index c7f2baeda1..96eb94a39c 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/TransportHandler.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/TransportHandler.kt @@ -44,7 +44,7 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor open val isSupported: Boolean get() = false - open suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean = false, pin: String? = null): AuthenticatorResponse = + open suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean = false, pin: String? = null): AuthenticatorResponseWrapper = throw RequestHandlingException(ErrorCode.NOT_SUPPORTED_ERR) open fun shouldBeUsedInstantly(options: RequestOptions): Boolean = false @@ -197,7 +197,7 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor callerPackage: String, pinRequested: Boolean, pin: String? - ): AuthenticatorAttestationResponse { + ): suspend () -> AuthenticatorAttestationResponse { val (clientData, clientDataHash) = getClientDataAndHash(context, options, callerPackage) val requireResidentKey = when (options.registerOptions.authenticatorSelection?.residentKeyRequirement) { @@ -253,12 +253,13 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor connection.hasCtap1Support -> ctap1register(connection, options, clientDataHash) else -> throw IllegalStateException() } - return AuthenticatorAttestationResponse( + val authenticatorResponse = AuthenticatorAttestationResponse( keyHandle ?: ByteArray(0).also { Log.w(TAG, "keyHandle was null") }, clientData, AnyAttestationObject(response.authData, response.fmt, response.attStmt).encode(), connection.transports.toTypedArray() ) + return suspend { authenticatorResponse } } @@ -268,7 +269,9 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor clientDataHash: ByteArray, requireUserVerification: Boolean, pinToken: ByteArray? = null - ): Pair { + ): List> { + val responseList = ArrayList>() + val reqOptions = AuthenticatorGetAssertionRequest.Companion.Options( // The specification states that the WebAuthn requireUserVerification option should map to // the CTAP2 "uv" flag OR pinAuth/pinProtocol. Therefore, set this flag to false if @@ -304,7 +307,16 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor pinProtocol ) val ctap2Response = connection.runCommand(AuthenticatorGetAssertionCommand(request)) - return ctap2Response to ctap2Response.credential?.id + responseList.add(ctap2Response to ctap2Response.credential?.id) + + for (i in 1..< (ctap2Response.numberOfCredentials ?: 0)) { + val nextRequest = AuthenticatorGetNextAssertionRequest() + val nextResponse = connection.runCommand(AuthenticatorGetNextAssertionCommand(nextRequest)) + + responseList.add(nextResponse to nextResponse.credential?.id) + } + + return responseList } @RequiresApi(23) @@ -396,7 +408,7 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor options: RequestOptions, clientDataHash: ByteArray, rpIdHash: ByteArray - ): Pair { + ): List> { val cred = options.signOptions.allowList.orEmpty().firstOrNull { cred -> ctap1DeviceHasCredential(connection, clientDataHash, rpIdHash, cred) } ?: options.signOptions.allowList!!.first() @@ -412,7 +424,7 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor null, null ) - return ctap2Response to cred.id + return listOf(ctap2Response to cred.id) } catch (e: CtapHidMessageStatusException) { if (e.status != 0x6985) { throw e @@ -426,7 +438,7 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor connection: CtapConnection, options: RequestOptions, clientDataHash: ByteArray - ): Pair { + ): List> { try { val rpIdHash = options.rpId.toByteArray().digest("SHA-256") return ctap1sign(connection, options, clientDataHash, rpIdHash) @@ -451,10 +463,10 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor callerPackage: String, pinRequested: Boolean, pin: String? - ): AuthenticatorAssertionResponse { + ): List AuthenticatorAssertionResponse>> { val (clientData, clientDataHash) = getClientDataAndHash(context, options, callerPackage) - val (response, credentialId) = when { + val responses: List> = when { connection.hasCtap2Support -> { try { var pinToken: ByteArray? = null @@ -512,13 +524,30 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor connection.hasCtap1Support -> ctap1sign(connection, options, clientDataHash) else -> throw IllegalStateException() } - return AuthenticatorAssertionResponse( - credentialId ?: ByteArray(0).also { Log.w(TAG, "keyHandle was null") }, - clientData, - response.authData, - response.signature, - null - ) + + val assertionResponses = ArrayList AuthenticatorAssertionResponse>>() + + for ((response, credentialId) in responses) { + var name = response.user?.name + var displayName = response.user?.displayName + var icon = response.user?.icon + + var userInfo: UserInfo? = null + if (name != null) { + userInfo = UserInfo(name, displayName, icon) + } + + val assertionResponse = AuthenticatorAssertionResponse( + credentialId ?: ByteArray(0).also { Log.w(TAG, "keyHandle was null for key with display name $displayName") }, + clientData, + response.authData, + response.signature, + null + ) + assertionResponses.add(userInfo to suspend { assertionResponse }) + } + + return assertionResponses } companion object { diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/nfc/NfcTransportHandler.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/nfc/NfcTransportHandler.kt index 7bd8561599..12746daf4b 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/nfc/NfcTransportHandler.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/nfc/NfcTransportHandler.kt @@ -22,8 +22,10 @@ import com.google.android.gms.fido.fido2.api.common.AuthenticatorResponse import com.google.android.gms.fido.fido2.api.common.RequestOptions import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred +import org.microg.gms.fido.core.AuthenticatorResponseWrapper import org.microg.gms.fido.core.MissingPinException import org.microg.gms.fido.core.RequestOptionsType +import org.microg.gms.fido.core.UserInfo import org.microg.gms.fido.core.WrongPinException import org.microg.gms.fido.core.transport.Transport import org.microg.gms.fido.core.transport.TransportHandler @@ -58,7 +60,7 @@ class NfcTransportHandler(private val activity: Activity, callback: TransportHan tag: Tag, pinRequested: Boolean, pin: String? - ): AuthenticatorAttestationResponse { + ): suspend () -> AuthenticatorAttestationResponse { return CtapNfcConnection(activity, tag).open { register(it, activity, options, callerPackage, pinRequested, pin) } @@ -70,7 +72,7 @@ class NfcTransportHandler(private val activity: Activity, callback: TransportHan tag: Tag, pinRequested: Boolean, pin: String? - ): AuthenticatorAssertionResponse { + ): List AuthenticatorAssertionResponse>> { return CtapNfcConnection(activity, tag).open { sign(it, activity, options, callerPackage, pinRequested, pin) } @@ -83,15 +85,15 @@ class NfcTransportHandler(private val activity: Activity, callback: TransportHan tag: Tag, pinRequested: Boolean, pin: String? - ): AuthenticatorResponse { + ): AuthenticatorResponseWrapper { return when (options.type) { - RequestOptionsType.REGISTER -> register(options, callerPackage, tag, pinRequested, pin) - RequestOptionsType.SIGN -> sign(options, callerPackage, tag, pinRequested, pin) + RequestOptionsType.REGISTER -> AuthenticatorResponseWrapper(listOf(Pair(null, register(options, callerPackage, tag, pinRequested, pin)))) + RequestOptionsType.SIGN -> AuthenticatorResponseWrapper(sign(options, callerPackage, tag, pinRequested, pin)) } } - override suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean, pin: String?): AuthenticatorResponse { + override suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean, pin: String?): AuthenticatorResponseWrapper { val adapter = NfcAdapter.getDefaultAdapter(activity) val newIntentListener = Consumer { if (it?.action != NfcAdapter.ACTION_TECH_DISCOVERED) return@Consumer diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockCredentialStore.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockCredentialStore.kt index b42f5fb8a8..256084bfe9 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockCredentialStore.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockCredentialStore.kt @@ -17,14 +17,20 @@ import android.security.keystore.StrongBoxUnavailableException import android.util.Base64 import android.util.Log import androidx.annotation.RequiresApi +import org.microg.gms.fido.core.UserInfo import org.microg.gms.utils.toBase64 -import java.security.* +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.PrivateKey +import java.security.ProviderException +import java.security.PublicKey +import java.security.Signature import java.security.cert.Certificate import java.security.spec.ECGenParameterSpec import kotlin.random.Random @RequiresApi(23) -class ScreenLockCredentialStore(val context: Context) { +class ScreenLockCredentialStore(val context: Context) : SQLiteOpenHelper(context, "screenlockcredentials.db", null, DATABASE_VERSION) { private val keyStore by lazy { KeyStore.getInstance("AndroidKeyStore").apply { load(null) } } private fun getAlias(rpId: String, keyId: ByteArray): String = @@ -34,6 +40,8 @@ class ScreenLockCredentialStore(val context: Context) { @RequiresApi(23) fun createKey(rpId: String, challenge: ByteArray): ByteArray { + clearInvalidatedKeys() + var useStrongbox = false if (SDK_INT >= 28) useStrongbox = context.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE) val keyId = Random.nextBytes(32) @@ -81,12 +89,53 @@ class ScreenLockCredentialStore(val context: Context) { } } } - return keyId } - fun getPublicKey(rpId: String, keyId: ByteArray): PublicKey? = - keyStore.getCertificate(getAlias(rpId, keyId))?.publicKey + fun getPublicKey(rpId: String, keyId: ByteArray): PublicKey? { + clearInvalidatedKeys() + return keyStore.getCertificate(getAlias(rpId, keyId))?.publicKey + } + + fun getPublicKeys(rpId: String): Collection> { + clearInvalidatedKeys() + + val keys = ArrayList>() + for (alias in keyStore.aliases()) { + if (alias.endsWith(".$rpId")) { + val key = keyStore.getCertificate(alias).publicKey + keys.add(Pair(alias, key)) + } + } + + return keys + } + + fun clearInvalidatedKeys() { + // Iterate through the keys, try to initiate them, and delete them if this throws an + // invalidated exception + val keysToDelete = ArrayList() + for (alias in keyStore.aliases()) { + try { + // This is a bit of a hack, but if you try to initSign on a key that has been + // invalidated, it throws a KeyPermanentlyInvalidatedException if the key is + // invalidated. Otherwise, it throws an exception that I assume is related to + // the lack of biometric authentication + val key = keyStore.getKey(alias, null) as? PrivateKey + val signature = Signature.getInstance("SHA256withECDSA") + signature.initSign(key) + } catch (e: KeyPermanentlyInvalidatedException) { + keysToDelete.add(alias) + } catch (e: Exception) { + // Any other exception, we just continue + } + } + + for (alias in keysToDelete) { + keyStore.deleteEntry(alias) + } + } + fun getCertificateChain(rpId: String, keyId: ByteArray): Array = keyStore.getCertificateChain(getAlias(rpId, keyId)) @@ -107,5 +156,96 @@ class ScreenLockCredentialStore(val context: Context) { companion object { const val TAG = "FidoLockStore" + + const val DATABASE_VERSION = 1 + + const val TABLE_DISPLAY_NAMES = "DISPLAY_NAMES_TABLE" + const val COLUMN_KEY_ALIAS = "KEY_ALIAS_COLUMN" + const val COLUMN_NAME = "NAME_COLUMN" + const val COLUMN_DISPLAY_NAME = "DISPLAY_NAME_COLUMN" + const val COLUMN_ICON = "ICON_COLUMN" + } + + override fun onCreate(db: SQLiteDatabase?) { + onUpgrade(db, 0, DATABASE_VERSION) + } + + override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { + if (db == null) { + return + } + if (oldVersion < 1) { + db.execSQL("CREATE TABLE $TABLE_DISPLAY_NAMES($COLUMN_KEY_ALIAS TEXT NOT NULL, $COLUMN_NAME TEXT NOT NULL, $COLUMN_DISPLAY_NAME TEXT, $COLUMN_ICON TEXT, UNIQUE($COLUMN_KEY_ALIAS) ON CONFLICT REPLACE)") + } + } + + fun addUserInfo(rpId: String, keyId: ByteArray, userInfo: UserInfo) { + addUserInfo(rpId, keyId, userInfo.name, userInfo.displayName, userInfo.icon) + } + + fun addUserInfo(rpId: String, keyId: ByteArray, name: String, displayName: String? = null, icon: String? = null) = writableDatabase.use { + // Since this function is not called very often, calling cleanDatabase here will probably not + // slow things down by much, and it will avoid the database growing larger than necessary + cleanDatabase(it) + + // The key alias and display names are both coming from outside sources. Don't trust them + val keyAlias = getAlias(rpId, keyId) + val insertStatement = it.compileStatement("INSERT INTO $TABLE_DISPLAY_NAMES($COLUMN_KEY_ALIAS, $COLUMN_NAME, $COLUMN_DISPLAY_NAME, $COLUMN_ICON) VALUES(?, ?, ?, ?)") + insertStatement.bindString(1, keyAlias) + insertStatement.bindString(2, name) + if (displayName != null) insertStatement.bindString(3, displayName) + if (icon != null) insertStatement.bindString(4, icon) + insertStatement.executeInsert() + } + + fun getUserInfo(rpId: String, keyId: ByteArray): UserInfo? = writableDatabase.use { + // Same argument as above, this function is not called often, so cleaning every time it's called + // should not slow down the phone a lot + cleanDatabase(it) + + val keyAlias = getAlias(rpId, keyId) + val userInfoQuery = it.query(TABLE_DISPLAY_NAMES, arrayOf(COLUMN_NAME, COLUMN_DISPLAY_NAME, COLUMN_ICON), "$COLUMN_KEY_ALIAS = ?", arrayOf(keyAlias), null, null, null, null) + + var name: String? = null + var displayName: String? = null + var icon: String? = null + userInfoQuery.use { cursor -> + if (cursor.moveToNext()) { + name = cursor.getString(0) + displayName = cursor.getString(1) + icon = cursor.getString(2) + } + } + + if (name != null) { + return UserInfo(name!!, displayName, icon) + } else { + return null + } + } + + private fun cleanDatabase(db: SQLiteDatabase) { + // Remove all display names that don't have an alias in the keystore + val aliases = HashSet() + for (alias in keyStore.aliases()) { + aliases.add(alias) + } + + val aliasesToDelete = HashSet() + val knownAliases = db.query(TABLE_DISPLAY_NAMES, arrayOf(COLUMN_KEY_ALIAS), null, null, null, null, null) + knownAliases.use { cursor -> + while (cursor.moveToNext()) { + val databaseAlias = cursor.getString(0) + if (!aliases.contains(databaseAlias)) aliasesToDelete.add(databaseAlias) + } + } + + // Since key IDs come from outside microG, treat them as potentially suspicious + // Use prepared statements to avoid SQL injections + val preparedDeleteStatement = db.compileStatement("DELETE FROM $TABLE_DISPLAY_NAMES WHERE $COLUMN_KEY_ALIAS = ?") + for (aliasToDelete in aliasesToDelete) { + preparedDeleteStatement.bindString(1, aliasToDelete) + preparedDeleteStatement.executeUpdateDelete() + } } } diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt index dae2197af6..06eea9c225 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt @@ -7,21 +7,44 @@ package org.microg.gms.fido.core.transport.screenlock import android.app.KeyguardManager import android.os.Build.VERSION.SDK_INT +import android.util.Base64 import android.util.Log import androidx.annotation.RequiresApi import androidx.biometric.BiometricPrompt import androidx.core.content.getSystemService import androidx.fragment.app.FragmentActivity -import com.google.android.gms.fido.fido2.api.common.* +import com.google.android.gms.fido.fido2.api.common.AuthenticatorAssertionResponse +import com.google.android.gms.fido.fido2.api.common.AuthenticatorAttestationResponse +import com.google.android.gms.fido.fido2.api.common.EC2Algorithm +import com.google.android.gms.fido.fido2.api.common.ErrorCode +import com.google.android.gms.fido.fido2.api.common.RequestOptions import com.google.android.gms.safetynet.SafetyNet import com.google.android.gms.tasks.await import kotlinx.coroutines.suspendCancellableCoroutine import org.microg.gms.common.Constants -import org.microg.gms.fido.core.* -import org.microg.gms.fido.core.protocol.* +import org.microg.gms.fido.core.AuthenticatorResponseWrapper +import org.microg.gms.fido.core.R +import org.microg.gms.fido.core.RequestHandlingException +import org.microg.gms.fido.core.RequestOptionsType +import org.microg.gms.fido.core.UserInfo +import org.microg.gms.fido.core.digest +import org.microg.gms.fido.core.getApplicationName +import org.microg.gms.fido.core.getClientDataAndHash +import org.microg.gms.fido.core.protocol.AndroidKeyAttestationObject +import org.microg.gms.fido.core.protocol.AndroidSafetyNetAttestationObject +import org.microg.gms.fido.core.protocol.AttestedCredentialData +import org.microg.gms.fido.core.protocol.AuthenticatorData +import org.microg.gms.fido.core.protocol.CoseKey +import org.microg.gms.fido.core.protocol.CredentialId +import org.microg.gms.fido.core.protocol.NoneAttestationObject +import org.microg.gms.fido.core.registerOptions +import org.microg.gms.fido.core.rpId +import org.microg.gms.fido.core.signOptions +import org.microg.gms.fido.core.skipAttestation import org.microg.gms.fido.core.transport.Transport import org.microg.gms.fido.core.transport.TransportHandler import org.microg.gms.fido.core.transport.TransportHandlerCallback +import org.microg.gms.fido.core.type import java.security.Signature import java.security.interfaces.ECPublicKey import kotlin.coroutines.resume @@ -104,7 +127,7 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac suspend fun register( options: RequestOptions, callerPackage: String - ): AuthenticatorAttestationResponse { + ): suspend () -> AuthenticatorAttestationResponse { if (options.type != RequestOptionsType.REGISTER) throw RequestHandlingException(ErrorCode.INVALID_STATE_ERR) for (descriptor in options.registerOptions.excludeList.orEmpty()) { if (store.containsKey(options.rpId, descriptor.id)) { @@ -118,7 +141,14 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac val aaguid = if (options.registerOptions.skipAttestation) ByteArray(16) else AAGUID val keyId = store.createKey(options.rpId, clientDataHash) val publicKey = - store.getPublicKey(options.rpId, keyId) ?: throw RequestHandlingException(ErrorCode.INVALID_STATE_ERR) + store.getPublicKey(options.rpId, keyId) + ?: throw RequestHandlingException(ErrorCode.INVALID_STATE_ERR) + + val name = options.registerOptions.user.name + val displayName = options.registerOptions.user.displayName + val icon = options.registerOptions.user.icon + + store.addUserInfo(options.rpId, keyId, name, displayName, icon) // We're ignoring the signature object as we don't need it for registration val signature = getActiveSignature(options, callerPackage, keyId) @@ -145,12 +175,14 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac } } - return AuthenticatorAttestationResponse( + val response = AuthenticatorAttestationResponse( credentialId.encode(), clientData, attestationObject.encode(), arrayOf("internal") ) + + return suspend { response } } @RequiresApi(24) @@ -188,7 +220,7 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac suspend fun sign( options: RequestOptions, callerPackage: String - ): AuthenticatorAssertionResponse { + ): List AuthenticatorAssertionResponse>> { if (options.type != RequestOptionsType.SIGN) throw RequestHandlingException(ErrorCode.INVALID_STATE_ERR) val candidates = mutableListOf() for (descriptor in options.signOptions.allowList.orEmpty()) { @@ -201,6 +233,27 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac // Not in store or unknown id } } + + // If there is no allowlist, add all keys with the given rpId as possible keys + if (options.signOptions.allowList?.isEmpty() != false) { + val keys = store.getPublicKeys(options.rpId) + for ((alias, key) in keys) { + val aliasSplit = alias.split(Regex("\\."), 3) + if (aliasSplit.size != 3) continue + val type: Int = aliasSplit[0].toIntOrNull() ?: continue + if (type != 1) continue + + val data: ByteArray + try { + data = Base64.decode(aliasSplit[1], Base64.DEFAULT) + } catch (e: Exception) { + continue + } + + candidates.add(CredentialId(type.toByte(), data, options.rpId, key)) + } + } + if (candidates.isEmpty()) { // Show a biometric prompt even if no matching key to effectively rate-limit showBiometricPrompt(getApplicationName(activity, options, callerPackage), null) @@ -212,28 +265,38 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac val (clientData, clientDataHash) = getClientDataAndHash(activity, options, callerPackage) - val credentialId = candidates.first() - val keyId = credentialId.data - val authenticatorData = getAuthenticatorData(options.rpId, null) + val credentialList = ArrayList AuthenticatorAssertionResponse>>() - val signature = getActiveSignature(options, callerPackage, keyId) - signature.update(authenticatorData.encode() + clientDataHash) - val sig = signature.sign() + for (credentialId in candidates) { + val keyId = credentialId.data + val authenticatorData = getAuthenticatorData(options.rpId, null) + val userInfo: UserInfo? = store.getUserInfo(options.rpId, keyId) - return AuthenticatorAssertionResponse( - credentialId.encode(), - clientData, - authenticatorData.encode(), - sig, - null - ) + val responseCallable = suspend { + val signature = getActiveSignature(options, callerPackage, keyId) + signature.update(authenticatorData.encode() + clientDataHash) + val sig = signature.sign() + + AuthenticatorAssertionResponse( + credentialId.encode(), + clientData, + authenticatorData.encode(), + sig, + null + ) + } + + credentialList.add(userInfo to responseCallable) + } + + return credentialList } @RequiresApi(24) - override suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean, pin: String?): AuthenticatorResponse = + override suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean, pin: String?): AuthenticatorResponseWrapper = when (options.type) { - RequestOptionsType.REGISTER -> register(options, callerPackage) - RequestOptionsType.SIGN -> sign(options, callerPackage) + RequestOptionsType.REGISTER -> AuthenticatorResponseWrapper(listOf(Pair(null, register(options, callerPackage)))) + RequestOptionsType.SIGN -> AuthenticatorResponseWrapper(sign(options, callerPackage)) } override fun shouldBeUsedInstantly(options: RequestOptions): Boolean { diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/usb/UsbTransportHandler.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/usb/UsbTransportHandler.kt index 608c1e10b3..960aa155c9 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/usb/UsbTransportHandler.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/usb/UsbTransportHandler.kt @@ -84,7 +84,7 @@ class UsbTransportHandler(private val context: Context, callback: TransportHandl iface: UsbInterface, pinRequested: Boolean, pin: String? - ): AuthenticatorAttestationResponse { + ): suspend () -> AuthenticatorAttestationResponse { return CtapHidConnection(context, device, iface).open { register(it, context, options, callerPackage, pinRequested, pin) } @@ -97,7 +97,7 @@ class UsbTransportHandler(private val context: Context, callback: TransportHandl iface: UsbInterface, pinRequested: Boolean, pin: String? - ): AuthenticatorAssertionResponse { + ): List AuthenticatorAssertionResponse>> { return CtapHidConnection(context, device, iface).open { sign(it, context, options, callerPackage, pinRequested, pin) } @@ -126,22 +126,22 @@ class UsbTransportHandler(private val context: Context, callback: TransportHandl iface: UsbInterface, pinRequested: Boolean, pin: String? - ): AuthenticatorResponse { + ): AuthenticatorResponseWrapper { Log.d(TAG, "Trying to use ${device.productName} for ${options.type}") invokeStatusChanged( TransportHandlerCallback.STATUS_WAITING_FOR_USER, Bundle().apply { putParcelable(UsbManager.EXTRA_DEVICE, device) }) try { return when (options.type) { - RequestOptionsType.REGISTER -> register(options, callerPackage, device, iface, pinRequested, pin) - RequestOptionsType.SIGN -> sign(options, callerPackage, device, iface, pinRequested, pin) + RequestOptionsType.REGISTER -> AuthenticatorResponseWrapper(listOf(Pair(null, register(options, callerPackage, device, iface, pinRequested, pin)))) + RequestOptionsType.SIGN -> AuthenticatorResponseWrapper(sign(options, callerPackage, device, iface, pinRequested, pin)) } } finally { this.device = null } } - override suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean, pin: String?): AuthenticatorResponse { + override suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean, pin: String?): AuthenticatorResponseWrapper { for (device in context.usbManager?.deviceList?.values.orEmpty()) { val iface = getCtapHidInterface(device) ?: continue try { diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivity.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivity.kt index 491ae1ac3b..a3c2e76d60 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivity.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivity.kt @@ -15,10 +15,10 @@ import android.util.Log import android.widget.Toast import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity +import androidx.core.os.bundleOf import androidx.fragment.app.commit import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.NavHostFragment -import com.google.android.gms.fido.Fido import com.google.android.gms.fido.Fido.* import com.google.android.gms.fido.fido2.api.common.* import com.google.android.gms.fido.fido2.api.common.ErrorCode.* @@ -216,7 +216,14 @@ class AuthenticatorActivity : AppCompatActivity(), TransportHandlerCallback { ) } - fun finishWithSuccessResponse(response: AuthenticatorResponse, transport: Transport) { + suspend fun finishWithSuccessResponse(responseWrapper: AuthenticatorResponseWrapper, transport: Transport) { + if (responseWrapper.responseChoices.size != 1) { + val bundle = bundleOf("responseChoices" to responseWrapper.responseChoices, "transport" to transport) + navHostFragment.navController.navigate(R.id.openCredentialSelector, bundle) + return + } + val response = responseWrapper.responseChoices[0].second.invoke() + Log.d(TAG, "Finish with success response: $response") if (options is BrowserRequestOptions) database.insertPrivileged(callerPackage, callerSignature) val rpId = options?.rpId diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/CredentialSelectorFragment.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/CredentialSelectorFragment.kt new file mode 100644 index 0000000000..a7691fe539 --- /dev/null +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/CredentialSelectorFragment.kt @@ -0,0 +1,98 @@ +package org.microg.gms.fido.core.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import androidx.navigation.navOptions +import com.google.android.gms.fido.fido2.api.common.AuthenticatorResponse +import com.google.android.gms.fido.fido2.api.common.ErrorCode +import kotlinx.coroutines.launch +import org.microg.gms.fido.core.AuthenticatorResponseWrapper +import org.microg.gms.fido.core.UserInfo +import org.microg.gms.fido.core.R +import org.microg.gms.fido.core.databinding.FidoCredentialSelectorFragmentBinding +import org.microg.gms.fido.core.databinding.FidoCredentialSelectorListItemBinding +import org.microg.gms.fido.core.transport.Transport + +class CredentialListAdapter(private val responseChoices: List AuthenticatorResponse>>, private val listSelectionFunction: (suspend () -> AuthenticatorResponse) -> Unit) : BaseAdapter() { + override fun getCount(): Int { + return responseChoices.size + } + + override fun getItem(position: Int): Pair AuthenticatorResponse> { + return responseChoices[position] + } + + override fun getItemId(position: Int): Long { + return position.toLong() + } + + override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { + var view = convertView + val inflater = LayoutInflater.from(parent?.context) + if (convertView == null) { + view = inflater.inflate(R.layout.fido_credential_selector_list_item, parent, false) + } + + val (userInfo, function) = getItem(position) + // TODO: Set icons + if (userInfo != null) { + view?.findViewById(R.id.credentialNameTextView)?.setText(userInfo.name) + if (userInfo.displayName != null) view?.findViewById(R.id.credentialDisplayNameTextView)?.setText(userInfo.displayName) + } + + val binding = FidoCredentialSelectorListItemBinding.bind(view!!) + binding.onCredentialSelection = View.OnClickListener { + view.setBackgroundColor(ContextCompat.getColor(it.context, androidx.appcompat.R.color.abc_color_highlight_material)) + listSelectionFunction(function) + } + + return view + } + +} + +class CredentialSelectorFragment : AuthenticatorActivityFragment() { + private lateinit var binding: FidoCredentialSelectorFragmentBinding + private lateinit var transport: Transport + + @Suppress("UNCHECKED_CAST") + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FidoCredentialSelectorFragmentBinding.inflate(inflater, container, false) + + transport = arguments?.get("transport") as Transport + val responseChoices = arguments?.get("responseChoices") as List AuthenticatorResponse>> + val adapter = CredentialListAdapter(responseChoices, this::onListSelection) + binding.fidoCredentialListView.adapter = adapter + + return binding.root + } + + fun onListSelection(function: suspend () -> AuthenticatorResponse) { + val authenticator = authenticatorActivity + authenticator?.lifecycleScope?.launch { + try { + authenticator.finishWithSuccessResponse(AuthenticatorResponseWrapper(listOf(null to function)), transport) + } catch (e: Exception) { + authenticator.finishWithError(ErrorCode.UNKNOWN_ERR, e.message ?: e.javaClass.simpleName) + } + } + if (!findNavController().navigateUp()) { + findNavController().navigate( + R.id.transportSelectionFragment, + arguments, + navOptions { popUpTo(R.id.usbFragment) { inclusive = true } }) + } + } + +} \ No newline at end of file diff --git a/play-services-fido/core/src/main/res/layout/fido_credential_selector_fragment.xml b/play-services-fido/core/src/main/res/layout/fido_credential_selector_fragment.xml new file mode 100644 index 0000000000..b83bf03e5e --- /dev/null +++ b/play-services-fido/core/src/main/res/layout/fido_credential_selector_fragment.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/play-services-fido/core/src/main/res/layout/fido_credential_selector_list_item.xml b/play-services-fido/core/src/main/res/layout/fido_credential_selector_list_item.xml new file mode 100644 index 0000000000..33e6aef702 --- /dev/null +++ b/play-services-fido/core/src/main/res/layout/fido_credential_selector_list_item.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/play-services-fido/core/src/main/res/navigation/nav_fido_authenticator.xml b/play-services-fido/core/src/main/res/navigation/nav_fido_authenticator.xml index 81fe4bf83e..636bafee9e 100644 --- a/play-services-fido/core/src/main/res/navigation/nav_fido_authenticator.xml +++ b/play-services-fido/core/src/main/res/navigation/nav_fido_authenticator.xml @@ -52,6 +52,9 @@ + @@ -62,6 +65,9 @@ + + + + diff --git a/play-services-fido/core/src/main/res/values/strings.xml b/play-services-fido/core/src/main/res/values/strings.xml index 26e4e45f2b..26d8337a59 100644 --- a/play-services-fido/core/src/main/res/values/strings.xml +++ b/play-services-fido/core/src/main/res/values/strings.xml @@ -29,4 +29,6 @@ OK Cancel Wrong PIN entered! + Unknown credential + Credential icon From 02b084c4dea11cef3d78337cc65db5c37bf3b897 Mon Sep 17 00:00:00 2001 From: Martin Asprusten Date: Wed, 28 Feb 2024 23:04:29 +0100 Subject: [PATCH 2/5] Added buttons to delete on-device credentials in credential list --- play-services-fido/core/build.gradle | 1 + .../microg/gms/fido/core/RequestHandling.kt | 9 +++-- .../screenlock/ScreenLockCredentialStore.kt | 4 +++ .../screenlock/ScreenLockTransportHandler.kt | 15 ++++++-- .../gms/fido/core/ui/AuthenticatorActivity.kt | 2 +- .../core/ui/CredentialSelectorFragment.kt | 34 ++++++++++++++++--- .../fido_credential_selector_list_item.xml | 22 ++++++++++-- .../core/src/main/res/values/strings.xml | 1 + 8 files changed, 74 insertions(+), 14 deletions(-) diff --git a/play-services-fido/core/build.gradle b/play-services-fido/core/build.gradle index 157e0643d8..8d6a1a2c3e 100644 --- a/play-services-fido/core/build.gradle +++ b/play-services-fido/core/build.gradle @@ -7,6 +7,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'maven-publish' apply plugin: 'signing' +apply plugin: 'kotlin-parcelize' dependencies { api project(':play-services-fido') diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/RequestHandling.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/RequestHandling.kt index e0bf49b203..231d0ea740 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/RequestHandling.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/RequestHandling.kt @@ -7,6 +7,7 @@ package org.microg.gms.fido.core import android.content.Context import android.net.Uri +import android.os.Parcelable import android.util.Base64 import com.android.volley.toolbox.JsonArrayRequest import com.android.volley.toolbox.JsonObjectRequest @@ -15,6 +16,7 @@ import com.google.android.gms.fido.fido2.api.common.* import com.google.android.gms.fido.fido2.api.common.ErrorCode.* import com.google.common.net.InternetDomainName import kotlinx.coroutines.CompletableDeferred +import kotlinx.parcelize.Parcelize import org.json.JSONArray import org.json.JSONObject import org.microg.gms.fido.core.RequestOptionsType.REGISTER @@ -33,9 +35,12 @@ class UserInfo( val displayName: String? = null, val icon: String? = null ) + +@Parcelize class AuthenticatorResponseWrapper ( - val responseChoices: List AuthenticatorResponse>> -) + val responseChoices: List AuthenticatorResponse>>, + val deleteFunctions: List<() -> Unit> = listOf() +) : Parcelable val RequestOptions.registerOptions: PublicKeyCredentialCreationOptions get() = when (this) { diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockCredentialStore.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockCredentialStore.kt index 256084bfe9..d07f43ccd6 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockCredentialStore.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockCredentialStore.kt @@ -111,6 +111,10 @@ class ScreenLockCredentialStore(val context: Context) : SQLiteOpenHelper(context return keys } + fun deleteKey(rpId:String, keyId: ByteArray) { + keyStore.deleteEntry(getAlias(rpId, keyId)) + } + fun clearInvalidatedKeys() { // Iterate through the keys, try to initiate them, and delete them if this throws an // invalidated exception diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt index 06eea9c225..237f04a7b0 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt @@ -220,7 +220,7 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac suspend fun sign( options: RequestOptions, callerPackage: String - ): List AuthenticatorAssertionResponse>> { + ): Pair AuthenticatorAssertionResponse>>, List<() -> Unit>> { if (options.type != RequestOptionsType.SIGN) throw RequestHandlingException(ErrorCode.INVALID_STATE_ERR) val candidates = mutableListOf() for (descriptor in options.signOptions.allowList.orEmpty()) { @@ -266,6 +266,7 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac val (clientData, clientDataHash) = getClientDataAndHash(activity, options, callerPackage) val credentialList = ArrayList AuthenticatorAssertionResponse>>() + val deleteFunctions = ArrayList<() -> Unit>() for (credentialId in candidates) { val keyId = credentialId.data @@ -286,17 +287,25 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac ) } + val deleteFunction = { + store.deleteKey(options.rpId, keyId) + } + credentialList.add(userInfo to responseCallable) + deleteFunctions.add(deleteFunction) } - return credentialList + return credentialList to deleteFunctions } @RequiresApi(24) override suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean, pin: String?): AuthenticatorResponseWrapper = when (options.type) { RequestOptionsType.REGISTER -> AuthenticatorResponseWrapper(listOf(Pair(null, register(options, callerPackage)))) - RequestOptionsType.SIGN -> AuthenticatorResponseWrapper(sign(options, callerPackage)) + RequestOptionsType.SIGN -> { + val (responseChoices, deleteFunctions) = sign(options, callerPackage) + AuthenticatorResponseWrapper(responseChoices, deleteFunctions) + } } override fun shouldBeUsedInstantly(options: RequestOptions): Boolean { diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivity.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivity.kt index a3c2e76d60..0a040f1dad 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivity.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivity.kt @@ -218,7 +218,7 @@ class AuthenticatorActivity : AppCompatActivity(), TransportHandlerCallback { suspend fun finishWithSuccessResponse(responseWrapper: AuthenticatorResponseWrapper, transport: Transport) { if (responseWrapper.responseChoices.size != 1) { - val bundle = bundleOf("responseChoices" to responseWrapper.responseChoices, "transport" to transport) + val bundle = bundleOf("responseWrapper" to responseWrapper, "transport" to transport) navHostFragment.navController.navigate(R.id.openCredentialSelector, bundle) return } diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/CredentialSelectorFragment.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/CredentialSelectorFragment.kt index a7691fe539..e14c2d6b59 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/CredentialSelectorFragment.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/CredentialSelectorFragment.kt @@ -5,8 +5,10 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.BaseAdapter +import android.widget.Button import android.widget.TextView import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.navigation.navOptions @@ -20,13 +22,17 @@ import org.microg.gms.fido.core.databinding.FidoCredentialSelectorFragmentBindin import org.microg.gms.fido.core.databinding.FidoCredentialSelectorListItemBinding import org.microg.gms.fido.core.transport.Transport -class CredentialListAdapter(private val responseChoices: List AuthenticatorResponse>>, private val listSelectionFunction: (suspend () -> AuthenticatorResponse) -> Unit) : BaseAdapter() { +class CredentialListAdapter( + private val responseWrapper: AuthenticatorResponseWrapper, + private val listSelectionFunction: (suspend () -> AuthenticatorResponse) -> Unit, + private val deleteCredentialFunction: (() -> Unit) -> Unit + ) : BaseAdapter() { override fun getCount(): Int { - return responseChoices.size + return responseWrapper.responseChoices.size } override fun getItem(position: Int): Pair AuthenticatorResponse> { - return responseChoices[position] + return responseWrapper.responseChoices[position] } override fun getItemId(position: Int): Long { @@ -53,6 +59,15 @@ class CredentialListAdapter(private val responseChoices: List(R.id.deleteCredentialButton) + deleteButton.visibility = View.VISIBLE + + binding.onDeleteCredential = View.OnClickListener { + deleteCredentialFunction(responseWrapper.deleteFunctions[position]) + } + } + return view } @@ -71,8 +86,8 @@ class CredentialSelectorFragment : AuthenticatorActivityFragment() { binding = FidoCredentialSelectorFragmentBinding.inflate(inflater, container, false) transport = arguments?.get("transport") as Transport - val responseChoices = arguments?.get("responseChoices") as List AuthenticatorResponse>> - val adapter = CredentialListAdapter(responseChoices, this::onListSelection) + val responseWrapper = arguments?.get("responseWrapper") as AuthenticatorResponseWrapper + val adapter = CredentialListAdapter(responseWrapper, this::onListSelection, this::credentialDeletion) binding.fidoCredentialListView.adapter = adapter return binding.root @@ -95,4 +110,13 @@ class CredentialSelectorFragment : AuthenticatorActivityFragment() { } } + fun credentialDeletion(deleteFunction: () -> Unit) { + deleteFunction.invoke() + if (!findNavController().navigateUp()) { + findNavController().navigate( + R.id.transportSelectionFragment, + arguments, + navOptions { popUpTo(R.id.usbFragment) { inclusive = true } }) + } + } } \ No newline at end of file diff --git a/play-services-fido/core/src/main/res/layout/fido_credential_selector_list_item.xml b/play-services-fido/core/src/main/res/layout/fido_credential_selector_list_item.xml index 33e6aef702..9f9308b0c7 100644 --- a/play-services-fido/core/src/main/res/layout/fido_credential_selector_list_item.xml +++ b/play-services-fido/core/src/main/res/layout/fido_credential_selector_list_item.xml @@ -1,13 +1,17 @@ - + + + @@ -47,5 +51,17 @@ android:textAppearance="@style/TextAppearance.AppCompat.Medium" /> +