Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 4cd7c928 authored by Marvin W.'s avatar Marvin W. 🐿️
Browse files

Fido: Allow for PIN-less authentication when no uv is required

parent 0ade5094
Loading
Loading
Loading
Loading
+51 −2
Original line number Diff line number Diff line
@@ -8,6 +8,7 @@ package org.microg.gms.fido.core.protocol.msgs
import com.upokecenter.cbor.CBORObject
import org.microg.gms.fido.core.protocol.AsInt32Sequence
import org.microg.gms.fido.core.protocol.AsStringSequence
import org.microg.gms.utils.ToStringHelper

class AuthenticatorGetInfoCommand : Ctap2Command<AuthenticatorGetInfoRequest, AuthenticatorGetInfoResponse>(AuthenticatorGetInfoRequest()) {
    override fun decodeResponse(obj: CBORObject) = AuthenticatorGetInfoResponse.decodeFromCbor(obj)
@@ -31,6 +32,20 @@ class AuthenticatorGetInfoResponse(
            val clientPin: Boolean?,
            val userPresence: Boolean,
            val userVerification: Boolean?,
            val pinUvAuthToken: Boolean?,
            val noMcGaPermissionsWithClientPin: Boolean,
            val largeBlobs: Boolean?,
            val enterpriseAttestation: Boolean?,
            val bioEnroll: Boolean?,
            val userVerificationMgmtPreview: Boolean?,
            val uvBioEnroll: Boolean?,
            val authenticatorConfigSupported: Boolean?,
            val uvAcfg: Boolean?,
            val credentialManagementSupported: Boolean?,
            val credentialMgmtPreview: Boolean?,
            val setMinPINLengthSupported: Boolean?,
            val makeCredUvNotRqd: Boolean,
            val alwaysUv: Boolean?,
        ) {
            companion object {
                fun decodeFromCbor(map: CBORObject?) = Options(
@@ -38,12 +53,46 @@ class AuthenticatorGetInfoResponse(
                    residentKey = map?.get("rk")?.AsBoolean() == true,
                    clientPin = map?.get("clientPin")?.AsBoolean(),
                    userPresence = map?.get("up")?.AsBoolean() != false,
                    userVerification = map?.get("uv")?.AsBoolean()
                    userVerification = map?.get("uv")?.AsBoolean(),
                    pinUvAuthToken = map?.get("pinUvAuthToken")?.AsBoolean(),
                    noMcGaPermissionsWithClientPin = map?.get("noMcGaPermissionsWithClientPin")?.AsBoolean() == true,
                    largeBlobs = map?.get("largeBlobs")?.AsBoolean(),
                    enterpriseAttestation = map?.get("ep")?.AsBoolean(),
                    bioEnroll = map?.get("bioEnroll")?.AsBoolean(),
                    userVerificationMgmtPreview = map?.get("userVerificationMgmtPreview")?.AsBoolean(),
                    uvBioEnroll = map?.get("uvBioEnroll")?.AsBoolean(),
                    authenticatorConfigSupported = map?.get("authnrCfg")?.AsBoolean(),
                    uvAcfg = map?.get("uvAcfg")?.AsBoolean(),
                    credentialManagementSupported = map?.get("credMgmt")?.AsBoolean(),
                    credentialMgmtPreview = map?.get("credentialMgmtPreview")?.AsBoolean(),
                    setMinPINLengthSupported = map?.get("setMinPINLength")?.AsBoolean(),
                    makeCredUvNotRqd = map?.get("makeCredUvNotRqd")?.AsBoolean() == true,
                    alwaysUv = map?.get("alwaysUv")?.AsBoolean(),
                )
            }

            override fun toString(): String {
                return "Options(platformDevice=$platformDevice, residentKey=$residentKey, clientPin=$clientPin, userPresence=$userPresence, userVerification=$userVerification)"
                return ToStringHelper.name("Options")
                    .field("platformDevice", platformDevice)
                    .field("residentKey", residentKey)
                    .field("clientPin", clientPin)
                    .field("userPresence", userPresence)
                    .field("userVerification", userVerification)
                    .field("pinUvAuthToken", pinUvAuthToken)
                    .field("noMcGaPermissionsWithClientPin", noMcGaPermissionsWithClientPin)
                    .field("largeBlobs", largeBlobs)
                    .field("enterpriseAttestation", enterpriseAttestation)
                    .field("bioEnroll", bioEnroll)
                    .field("userVerificationMgmtPreview", userVerificationMgmtPreview)
                    .field("uvBioEnroll", uvBioEnroll)
                    .field("authenticatorConfigSupported", authenticatorConfigSupported)
                    .field("uvAcfg", uvAcfg)
                    .field("credentialManagementSupported", credentialManagementSupported)
                    .field("credentialMgmtPreview", credentialMgmtPreview)
                    .field("setMinPINLengthSupported", setMinPINLengthSupported)
                    .field("makeCredUvNotRqd", makeCredUvNotRqd)
                    .field("alwaysUv", alwaysUv)
                    .end()
            }
        }

+21 −0
Original line number Diff line number Diff line
@@ -9,10 +9,31 @@ import com.google.android.gms.fido.fido2.api.common.ErrorCode
import org.microg.gms.fido.core.RequestHandlingException
import org.microg.gms.fido.core.protocol.msgs.*

const val CAPABILITY_CTAP_1 = 1 shl 0
const val CAPABILITY_CTAP_2 = 1 shl 1
const val CAPABILITY_CTAP_2_1 = 1 shl 2
const val CAPABILITY_CLIENT_PIN = 1 shl 3
const val CAPABILITY_WINK = 1 shl 4
const val CAPABILITY_MAKE_CRED_WITHOUT_UV = 1 shl 5

interface CtapConnection {
    val capabilities: Int

    val hasCtap1Support: Boolean
        get() = capabilities and CAPABILITY_CTAP_1 > 0
    val hasCtap2Support: Boolean
        get() = capabilities and CAPABILITY_CTAP_2 > 0
    val hasCtap21Support: Boolean
        get() = capabilities and CAPABILITY_CTAP_2_1 > 0
    val hasClientPin: Boolean
        get() = capabilities and CAPABILITY_CLIENT_PIN > 0
    val hasWinkSupport: Boolean
        get() = capabilities and CAPABILITY_WINK > 0
    val canMakeCredentialWithoutUserVerification: Boolean
        get() = capabilities and CAPABILITY_MAKE_CRED_WITHOUT_UV > 0

    suspend fun <Q : Ctap1Request, S : Ctap1Response> runCommand(command: Ctap1Command<Q, S>): S
    suspend fun <Q : Ctap2Request, S : Ctap2Response> runCommand(command: Ctap2Command<Q, S>): S
}

class Ctap2StatusException(val status: Byte) : Exception("Received status ${(status.toInt() and 0xff).toString(16)}")
+41 −4
Original line number Diff line number Diff line
@@ -8,7 +8,9 @@ package org.microg.gms.fido.core.transport
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import com.google.android.gms.fido.fido2.api.common.*
import com.google.android.gms.fido.fido2.api.common.UserVerificationRequirement.REQUIRED
import com.upokecenter.cbor.CBORObject
import kotlinx.coroutines.delay
import org.microg.gms.fido.core.*
@@ -48,7 +50,7 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor
    ): Pair<AuthenticatorMakeCredentialResponse, ByteArray?> {
        val reqOptions = AuthenticatorMakeCredentialRequest.Companion.Options(
            options.registerOptions.authenticatorSelection?.requireResidentKey == true,
            options.registerOptions.authenticatorSelection?.requireUserVerification == UserVerificationRequirement.REQUIRED
            options.registerOptions.authenticatorSelection?.requireUserVerification == REQUIRED
        )
        val extensions = mutableMapOf<String, CBORObject>()
        if (options.authenticationExtensions?.fidoAppIdExtension?.appId != null) {
@@ -144,7 +146,18 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor
    ): AuthenticatorAttestationResponse {
        val (clientData, clientDataHash) = getClientDataAndHash(context, options, callerPackage)
        val (response, keyHandle) = when {
            connection.hasCtap2Support -> ctap2register(connection, options, clientDataHash)
            connection.hasCtap2Support -> {
                if (connection.hasCtap1Support &&
                    !connection.canMakeCredentialWithoutUserVerification && connection.hasClientPin &&
                    options.registerOptions.authenticatorSelection.requireUserVerification != REQUIRED &&
                    !options.registerOptions.authenticatorSelection.requireResidentKey
                ) {
                    Log.d(TAG, "Using CTAP1/U2F for PIN-less registration")
                    ctap1register(connection, options, clientDataHash)
                } else {
                    ctap2register(connection, options, clientDataHash)
                }
            }
            connection.hasCtap1Support -> ctap1register(connection, options, clientDataHash)
            else -> throw IllegalStateException()
        }
@@ -162,7 +175,7 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor
        clientDataHash: ByteArray
    ): Pair<AuthenticatorGetAssertionResponse, ByteArray?> {
        val reqOptions = AuthenticatorGetAssertionRequest.Companion.Options(
            userVerification = options.signOptions.requireUserVerification == UserVerificationRequirement.REQUIRED
            userVerification = options.signOptions.requireUserVerification == REQUIRED
        )
        val extensions = mutableMapOf<String, CBORObject>()
        if (options.authenticationExtensions?.fidoAppIdExtension?.appId != null) {
@@ -244,7 +257,27 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor
    ): AuthenticatorAssertionResponse {
        val (clientData, clientDataHash) = getClientDataAndHash(context, options, callerPackage)
        val (response, credentialId) = when {
            connection.hasCtap2Support -> ctap2sign(connection, options, clientDataHash)
            connection.hasCtap2Support -> {
                try {
                    ctap2sign(connection, options, clientDataHash)
                } catch (e: Ctap2StatusException) {
                    if (e.status == 0x2e.toByte() &&
                        connection.hasCtap1Support && connection.hasClientPin &&
                        options.signOptions.allowList.isNotEmpty() &&
                        options.signOptions.requireUserVerification != REQUIRED
                    ) {
                        Log.d(TAG, "Falling back to CTAP1/U2F")
                        try {
                            ctap1sign(connection, options, clientDataHash)
                        } catch (e2: Exception) {
                            // Throw original exception
                            throw e
                        }
                    } else {
                        throw e
                    }
                }
            }
            connection.hasCtap1Support -> ctap1sign(connection, options, clientDataHash)
            else -> throw IllegalStateException()
        }
@@ -256,6 +289,10 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor
            null
        )
    }

    companion object {
        const val TAG = "FidoTransportHandler"
    }
}

interface TransportHandlerCallback {
+23 −20
Original line number Diff line number Diff line
@@ -13,7 +13,7 @@ import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.microg.gms.fido.core.protocol.msgs.*
import org.microg.gms.fido.core.transport.CtapConnection
import org.microg.gms.fido.core.transport.*
import org.microg.gms.utils.toBase64

class CtapNfcConnection(
@@ -21,19 +21,13 @@ class CtapNfcConnection(
    val tag: Tag
) : CtapConnection {
    private val isoDep = IsoDep.get(tag)
    private var capabilities: Int = 0

    override val hasCtap1Support: Boolean
        get() = capabilities and CAPABILITY_CTAP_1 > 0

    override val hasCtap2Support: Boolean
        get() = capabilities and CAPABILITY_CTAP_2 > 0
    override var capabilities: Int = 0

    override suspend fun <Q : Ctap1Request, S : Ctap1Response> runCommand(command: Ctap1Command<Q, S>): S {
        require(hasCtap1Support)
        Log.d(TAG, "Send command: ${command.request.apdu.toBase64(Base64.NO_WRAP)}")
        Log.d(TAG, "Send CTAP1 command: ${command.request.apdu.toBase64(Base64.NO_WRAP)}")
        val (statusCode, payload) = decodeResponseApdu(isoDep.transceive(command.request.apdu))
        Log.d(TAG, "Received response(${(statusCode.toInt() and 0xffff).toString(16)}): ${payload.toBase64(Base64.NO_WRAP)}")
        Log.d(TAG, "Received CTAP1 response(${(statusCode.toInt() and 0xffff).toString(16)}): ${payload.toBase64(Base64.NO_WRAP)}")
        if (statusCode != 0x9000.toShort()) {
            throw CtapNfcMessageStatusException(statusCode.toInt() and 0xffff)
        }
@@ -43,13 +37,13 @@ class CtapNfcConnection(
    override suspend fun <Q : Ctap2Request, S : Ctap2Response> runCommand(command: Ctap2Command<Q, S>): S {
        require(hasCtap2Support)
        val request = encodeCommandApdu(0x80.toByte(), 0x10, 0x00, 0x00, byteArrayOf(command.request.commandByte) + command.request.payload, extended = true)
        Log.d(TAG, "Send command: ${request.toBase64(Base64.NO_WRAP)}")
        Log.d(TAG, "Send CTAP2 command: ${request.toBase64(Base64.NO_WRAP)}")
        var (statusCode, payload) = decodeResponseApdu(isoDep.transceive(request))
        Log.d(TAG, "Received response(${(statusCode.toInt() and 0xffff).toString(16)}): ${payload.toBase64(Base64.NO_WRAP)}")
        Log.d(TAG, "Received CTAP2 response(${(statusCode.toInt() and 0xffff).toString(16)}): ${payload.toBase64(Base64.NO_WRAP)}")
        while (statusCode == 0x9100.toShort()) {
            Log.d(TAG, "Sending GETRESPONSE")
            val res = decodeResponseApdu(isoDep.transceive(encodeCommandApdu(0x00, 0xC0.toByte(), 0x00,0x00)))
            Log.d(TAG, "Received response(${(statusCode.toInt() and 0xffff).toString(16)}): ${payload.toBase64(Base64.NO_WRAP)}")
            Log.d(TAG, "Received CTAP2 response(${(statusCode.toInt() and 0xffff).toString(16)}): ${payload.toBase64(Base64.NO_WRAP)}")
            statusCode = res.first
            payload = res.second
        }
@@ -59,7 +53,7 @@ class CtapNfcConnection(
        require(payload.isNotEmpty())
        val ctapStatusCode = payload[0]
        if (ctapStatusCode != 0x00.toByte()) {
            throw CtapNfcMessageStatusException(ctapStatusCode.toInt() and 0xff)
            throw Ctap2StatusException(ctapStatusCode)
        }
        return command.decodeResponse(payload, 1)
    }
@@ -71,6 +65,14 @@ class CtapNfcConnection(

    private fun deselect() = isoDep.transceive(encodeCommandApdu(0x80.toByte(), 0x12, 0x01, 0x02))

    private suspend fun fetchCapabilities() {
        val response = runCommand(AuthenticatorGetInfoCommand())
        Log.d(TAG, "Got info: $response")
        capabilities = capabilities or CAPABILITY_CTAP_2 or
                (if (response.versions.contains("FIDO_2_1")) CAPABILITY_CTAP_2_1 else 0) or
                (if (response.options.clientPin == true) CAPABILITY_CLIENT_PIN else 0)
    }

    suspend fun open(): Boolean = withContext(Dispatchers.IO) {
        isoDep.connect()
        val (statusCode, version) = select(FIDO2_AID)
@@ -79,15 +81,19 @@ class CtapNfcConnection(
            when (version.decodeToString()) {
                "FIDO_2_0" -> {
                    capabilities = CAPABILITY_CTAP_2
                    try {
                        fetchCapabilities()
                    } catch (e: Exception) {
                        Log.w(TAG, e)
                    }
                    true
                }
                "U2F_V2" -> {
                    capabilities = CAPABILITY_CTAP_1 or CAPABILITY_CTAP_2
                    try {
                        val response = runCommand(AuthenticatorGetInfoCommand())
                        Log.d(TAG, "Got info: $response")
                        capabilities = capabilities or (if (response.versions.contains("FIDO_2_1")) CAPABILITY_CTAP_2_1 else 0)
                        fetchCapabilities()
                    } catch (e: Exception) {
                        Log.w(TAG, e)
                        capabilities = CAPABILITY_CTAP_1
                    }
                    true
@@ -131,9 +137,6 @@ class CtapNfcConnection(
    companion object {
        const val TAG = "FidoCtapNfcConnection"
        private val FIDO2_AID = byteArrayOf(0xA0.toByte(), 0x00, 0x00, 0x06, 0x47, 0x2F, 0x00, 0x01)
        private const val CAPABILITY_CTAP_1 = 1
        private const val CAPABILITY_CTAP_2 = 2
        private const val CAPABILITY_CTAP_2_1 = 4
    }
}

+0 −2
Original line number Diff line number Diff line
@@ -214,8 +214,6 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac

        val credentialId = candidates.first()
        val keyId = credentialId.data

        val (x, y) = (credentialId.publicKey as ECPublicKey).w.let { it.affineX to it.affineY }
        val authenticatorData = getAuthenticatorData(options.rpId, null)

        val signature = getActiveSignature(options, callerPackage, keyId)
Loading