Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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, AuthenticatorGetAssertionResponse>(
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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<T: AuthenticatorResponse>(val response: T, val user: PublicKeyCredentialUserEntity?)

Expand Down Expand Up @@ -430,14 +439,85 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor
}
}

@OptIn(FlowPreview::class)
suspend fun chooseFromList(
activity: Activity,
items: List<Pair<AuthenticatorGetAssertionResponse, ByteArray?>>,
) = 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<AuthenticatorAssertionResponse> {
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 {
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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)
Expand Down Expand Up @@ -83,8 +84,8 @@ class UsbTransportHandler(private val context: Context, callback: TransportHandl
pinRequested: Boolean,
pin: String?
): AuthenticatorResponseWithUser<AuthenticatorAttestationResponse> {
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)
}
}

Expand All @@ -96,8 +97,8 @@ class UsbTransportHandler(private val context: Context, callback: TransportHandl
pinRequested: Boolean,
pin: String?
): AuthenticatorResponseWithUser<AuthenticatorAssertionResponse> {
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)
}
}

Expand All @@ -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
}

Expand Down Expand Up @@ -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)
Expand Down