Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FIDO: Add credential selection #2463

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
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
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down
1 change: 1 addition & 0 deletions play-services-fido/core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -28,6 +30,17 @@ 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
)

@Parcelize
class AuthenticatorResponseWrapper (
val responseChoices: List<Pair<UserInfo?, suspend () -> AuthenticatorResponse>>,
val deleteFunctions: List<suspend () -> Boolean> = ArrayList()
) : Parcelable

val RequestOptions.registerOptions: PublicKeyCredentialCreationOptions
get() = when (this) {
Expand Down Expand Up @@ -150,11 +163,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")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.microg.gms.fido.core.protocol.msgs

import com.upokecenter.cbor.CBORObject

class AuthenticatorGetNextAssertionCommand(request: AuthenticatorGetNextAssertionRequest) :
Ctap2Command<AuthenticatorGetNextAssertionRequest, AuthenticatorGetAssertionResponse>(request) {
override fun decodeResponse(obj: CBORObject) = AuthenticatorGetAssertionResponse.decodeFromCbor(obj)
override val timeout: Long
get() = 60000
}

class AuthenticatorGetNextAssertionRequest() : Ctap2Request(0x08)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 }
}


Expand All @@ -268,7 +269,9 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor
clientDataHash: ByteArray,
requireUserVerification: Boolean,
pinToken: ByteArray? = null
): Pair<AuthenticatorGetAssertionResponse, ByteArray?> {
): List<Pair<AuthenticatorGetAssertionResponse, ByteArray?>> {
val responseList = ArrayList<Pair<AuthenticatorGetAssertionResponse, ByteArray?>>()

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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -396,7 +408,7 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor
options: RequestOptions,
clientDataHash: ByteArray,
rpIdHash: ByteArray
): Pair<AuthenticatorGetAssertionResponse, ByteArray> {
): List<Pair<AuthenticatorGetAssertionResponse, ByteArray>> {
val cred = options.signOptions.allowList.orEmpty().firstOrNull { cred ->
ctap1DeviceHasCredential(connection, clientDataHash, rpIdHash, cred)
} ?: options.signOptions.allowList!!.first()
Expand All @@ -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
Expand All @@ -426,7 +438,7 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor
connection: CtapConnection,
options: RequestOptions,
clientDataHash: ByteArray
): Pair<AuthenticatorGetAssertionResponse, ByteArray> {
): List<Pair<AuthenticatorGetAssertionResponse, ByteArray>> {
try {
val rpIdHash = options.rpId.toByteArray().digest("SHA-256")
return ctap1sign(connection, options, clientDataHash, rpIdHash)
Expand All @@ -451,10 +463,10 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor
callerPackage: String,
pinRequested: Boolean,
pin: String?
): AuthenticatorAssertionResponse {
): List<Pair<UserInfo?, suspend () -> AuthenticatorAssertionResponse>> {
val (clientData, clientDataHash) = getClientDataAndHash(context, options, callerPackage)

val (response, credentialId) = when {
val responses: List<Pair<AuthenticatorGetAssertionResponse, ByteArray?>> = when {
connection.hasCtap2Support -> {
try {
var pinToken: ByteArray? = null
Expand Down Expand Up @@ -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<Pair<UserInfo?, suspend () -> 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -70,7 +72,7 @@ class NfcTransportHandler(private val activity: Activity, callback: TransportHan
tag: Tag,
pinRequested: Boolean,
pin: String?
): AuthenticatorAssertionResponse {
): List<Pair<UserInfo?, suspend () -> AuthenticatorAssertionResponse>> {
return CtapNfcConnection(activity, tag).open {
sign(it, activity, options, callerPackage, pinRequested, pin)
}
Expand All @@ -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<Intent> {
if (it?.action != NfcAdapter.ACTION_TECH_DISCOVERED) return@Consumer
Expand Down
Loading