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..9b60f807ae --- /dev/null +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/protocol/msgs/AuthenticatorGetNextAssertion.kt @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2022 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.fido.core.protocol.msgs + +import com.upokecenter.cbor.CBORObject + +class AuthenticatorGetNextAssertionCommand : + Ctap2Command( + AuthenticatorGetNextAssertionRequest() + ) { + override fun decodeResponse(obj: CBORObject) = AuthenticatorGetAssertionResponse.decodeFromCbor(obj) + override val timeout: Long + get() = 60000 +} + +class AuthenticatorGetNextAssertionRequest : + Ctap2Request(Ctap2CommandCode.AuthenticatorGetNextAssertion, null) { + override fun toString() = "AuthenticatorGetNextAssertionRequest" +} 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 aca219cbda..30ff92809e 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 @@ -5,16 +5,24 @@ package org.microg.gms.fido.core.transport +import android.app.Activity import android.content.Context import android.os.Build.VERSION.SDK_INT import android.os.Bundle import android.util.Log import androidx.annotation.RequiresApi +import androidx.appcompat.app.AlertDialog import com.google.android.gms.fido.fido2.api.common.* import com.google.android.gms.fido.fido2.api.common.ResidentKeyRequirement.* import com.google.android.gms.fido.fido2.api.common.UserVerificationRequirement.* +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.upokecenter.cbor.CBORObject +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.timeout import org.microg.gms.fido.core.* import org.microg.gms.fido.core.protocol.* import org.microg.gms.fido.core.protocol.CoseKey.Companion.toByteArray @@ -30,6 +38,7 @@ import javax.crypto.KeyAgreement import javax.crypto.Mac import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec +import kotlin.time.Duration.Companion.seconds class AuthenticatorResponseWithUser(val response: T, val user: PublicKeyCredentialUserEntity?) @@ -430,14 +439,85 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor } } + @OptIn(FlowPreview::class) + suspend fun chooseFromList( + activity: Activity, + items: List>, + ) = callbackFlow { + val items = items.filter { it.first.user != null } + val dialog = try { + MaterialAlertDialogBuilder(activity) + } catch (_: Exception) { + AlertDialog.Builder(activity) + } + .setTitle(activity.getString(R.string.fido_sign_in_selection_title)) + .setItems(items.map { it.first.user!!.name }.toTypedArray()) { _, i -> + trySend(items[i]) + }.setNegativeButton(android.R.string.cancel) { dialog, _ -> + dialog.dismiss() + trySend(null) + }.show() + awaitClose { + dialog.dismiss() + } + }.timeout(10.seconds) + .firstOrNull() + internal suspend fun sign( connection: CtapConnection, - context: Context, + activity: Activity, options: RequestOptions, callerPackage: String, pinRequested: Boolean, pin: String? ): AuthenticatorResponseWithUser { + val getAssertionRes = getAssertion(connection, activity, options, callerPackage, pinRequested, pin) + var response = getAssertionRes.response + var credentialId = getAssertionRes.credentialId + val nCreds = getAssertionRes.response.numberOfCredentials ?: 0 + if (nCreds > 1) { + val creds = mutableListOf(response to credentialId) + for (k in 2..nCreds) { + try { + val ctap2Response = + connection.runCommand(AuthenticatorGetNextAssertionCommand()) + creds += ctap2Response to ctap2Response.credential?.id + } catch (e: Exception) { + Log.e(TAG, "Got an exception while getting next assertion", e) + break + } + } + chooseFromList(activity, creds)?.let { (r, c) -> + response = r + credentialId = c + } + } + return AuthenticatorResponseWithUser( + AuthenticatorAssertionResponse( + credentialId ?: ByteArray(0).also { Log.w(TAG, "keyHandle was null") }, + getAssertionRes.clientData, + response.authData, + response.signature, + response.user?.id + ), + response.user + ) + } + + private class GetAssertionRes( + val clientData: ByteArray, + val response: AuthenticatorGetAssertionResponse, + val credentialId: ByteArray? + ) + + private suspend fun getAssertion( + connection: CtapConnection, + context: Context, + options: RequestOptions, + callerPackage: String, + pinRequested: Boolean, + pin: String? + ): GetAssertionRes { val (clientData, clientDataHash) = getClientDataAndHash(context, options, callerPackage) val (response, credentialId) = when { @@ -498,16 +578,7 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor connection.hasCtap1Support -> ctap1sign(connection, options, clientDataHash) else -> throw IllegalStateException() } - return AuthenticatorResponseWithUser( - AuthenticatorAssertionResponse( - credentialId ?: ByteArray(0).also { Log.w(TAG, "keyHandle was null") }, - clientData, - response.authData, - response.signature, - response.user?.id - ), - response.user - ) + return GetAssertionRes(clientData, response, credentialId) } companion object { 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 292ff4cb8c..d81752734a 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 @@ -5,6 +5,7 @@ package org.microg.gms.fido.core.transport.usb +import android.app.Activity import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -32,12 +33,12 @@ import org.microg.gms.fido.core.transport.usb.ctaphid.CtapHidConnection import org.microg.gms.utils.toBase64 @RequiresApi(21) -class UsbTransportHandler(private val context: Context, callback: TransportHandlerCallback? = null) : +class UsbTransportHandler(private val activity: Activity, callback: TransportHandlerCallback? = null) : TransportHandler(Transport.USB, callback) { override val isSupported: Boolean - get() = context.packageManager.hasSystemFeature("android.hardware.usb.host") && context.usbManager != null + get() = activity.packageManager.hasSystemFeature("android.hardware.usb.host") && activity.usbManager != null - private val devicePermissionManager by lazy { UsbDevicePermissionManager(context) } + private val devicePermissionManager by lazy { UsbDevicePermissionManager(activity) } private var device: UsbDevice? = null @@ -55,7 +56,7 @@ class UsbTransportHandler(private val context: Context, callback: TransportHandl Log.d(TAG, "${device.productName} has suitable hid interface ${iface.id}") if (!devicePermissionManager.awaitPermission(device)) continue Log.d(TAG, "${device.productName} has permission") - val match = context.usbManager?.openDevice(device)?.use { connection -> + val match = activity.usbManager?.openDevice(device)?.use { connection -> if (connection.claimInterface(iface, true)) { val buf = ByteArray(256) val read = connection.controlTransfer(0x81, 0x06, 0x2200, iface.id, buf, buf.size, 5000) @@ -83,8 +84,8 @@ class UsbTransportHandler(private val context: Context, callback: TransportHandl pinRequested: Boolean, pin: String? ): AuthenticatorResponseWithUser { - return CtapHidConnection(context, device, iface).open { - register(it, context, options, callerPackage, pinRequested, pin) + return CtapHidConnection(activity, device, iface).open { + register(it, activity, options, callerPackage, pinRequested, pin) } } @@ -96,8 +97,8 @@ class UsbTransportHandler(private val context: Context, callback: TransportHandl pinRequested: Boolean, pin: String? ): AuthenticatorResponseWithUser { - return CtapHidConnection(context, device, iface).open { - sign(it, context, options, callerPackage, pinRequested, pin) + return CtapHidConnection(activity, device, iface).open { + sign(it, activity, options, callerPackage, pinRequested, pin) } } @@ -110,10 +111,10 @@ class UsbTransportHandler(private val context: Context, callback: TransportHandl deferred.complete(device) } } - ContextCompat.registerReceiver(context, receiver, IntentFilter(UsbManager.ACTION_USB_DEVICE_ATTACHED), RECEIVER_NOT_EXPORTED) + ContextCompat.registerReceiver(activity, receiver, IntentFilter(UsbManager.ACTION_USB_DEVICE_ATTACHED), RECEIVER_NOT_EXPORTED) invokeStatusChanged(TransportHandlerCallback.STATUS_WAITING_FOR_DEVICE) val device = deferred.await() - context.unregisterReceiver(receiver) + activity.unregisterReceiver(receiver) return device } @@ -146,7 +147,7 @@ class UsbTransportHandler(private val context: Context, callback: TransportHandl pin: String?, credentialIdString: String? ): AuthenticatorResponseWithUser<*> { - for (device in context.usbManager?.deviceList?.values.orEmpty()) { + for (device in activity.usbManager?.deviceList?.values.orEmpty()) { val iface = getCtapHidInterface(device) ?: continue try { return handle(options, callerPackage, device, iface, pinRequested, pin)