Skip to content

Commit

Permalink
Fido: Update to latest API version features
Browse files Browse the repository at this point in the history
Also handle empty strings from apps as none.

Fixes #2021, #2022
  • Loading branch information
mar-v-in committed Sep 6, 2023
1 parent 7f5fbe5 commit 046218e
Show file tree
Hide file tree
Showing 25 changed files with 567 additions and 45 deletions.
2 changes: 2 additions & 0 deletions play-services-fido/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,6 @@ dependencies {
api project(':play-services-base')
api project(':play-services-basement')
api project(':play-services-tasks')

annotationProcessor project(':safe-parcel-processor')
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.google.android.gms.common.internal.ConnectionInfo
import com.google.android.gms.common.internal.GetServiceRequest
import com.google.android.gms.common.internal.IGmsCallbacks
import com.google.android.gms.fido.fido2.api.IBooleanCallback
import com.google.android.gms.fido.fido2.api.ICredentialListCallback
import com.google.android.gms.fido.fido2.api.common.BrowserPublicKeyCredentialCreationOptions
import com.google.android.gms.fido.fido2.api.common.BrowserPublicKeyCredentialRequestOptions
import com.google.android.gms.fido.fido2.internal.privileged.IFido2PrivilegedCallbacks
Expand Down Expand Up @@ -60,7 +61,7 @@ class Fido2PrivilegedService : BaseService(TAG, FIDO2_PRIVILEGED) {

class Fido2PrivilegedServiceImpl(private val context: Context, private val lifecycle: Lifecycle) :
IFido2PrivilegedService.Stub(), LifecycleOwner {
override fun register(callbacks: IFido2PrivilegedCallbacks, options: BrowserPublicKeyCredentialCreationOptions) {
override fun getRegisterPendingIntent(callbacks: IFido2PrivilegedCallbacks, options: BrowserPublicKeyCredentialCreationOptions) {
lifecycleScope.launchWhenStarted {
val intent = Intent(context, AuthenticatorActivity::class.java)
.putExtra(KEY_SERVICE, FIDO2_PRIVILEGED.SERVICE_ID)
Expand All @@ -74,7 +75,7 @@ class Fido2PrivilegedServiceImpl(private val context: Context, private val lifec
}
}

override fun sign(callbacks: IFido2PrivilegedCallbacks, options: BrowserPublicKeyCredentialRequestOptions) {
override fun getSignPendingIntent(callbacks: IFido2PrivilegedCallbacks, options: BrowserPublicKeyCredentialRequestOptions) {
lifecycleScope.launchWhenStarted {
val intent = Intent(context, AuthenticatorActivity::class.java)
.putExtra(KEY_SERVICE, FIDO2_PRIVILEGED.SERVICE_ID)
Expand All @@ -99,6 +100,12 @@ class Fido2PrivilegedServiceImpl(private val context: Context, private val lifec
}
}

override fun getCredentialList(callbacks: ICredentialListCallback, rpId: String) {
lifecycleScope.launchWhenStarted {
runCatching { callbacks.onCredentialList(emptyList()) }
}
}

override fun getLifecycle(): Lifecycle = lifecycle

override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ fun Boolean.encodeAsCbor() = CBORObject.FromObject(this)

fun PublicKeyCredentialRpEntity.encodeAsCbor() = CBORObject.NewMap().apply {
set("id", id.encodeAsCbor())
if (name != null) set("name", name.encodeAsCbor())
if (icon != null) set("icon", icon.encodeAsCbor())
if (!name.isNullOrBlank()) set("name", name.encodeAsCbor())
if (!icon.isNullOrBlank()) set("icon", icon.encodeAsCbor())
}

fun PublicKeyCredentialUserEntity.encodeAsCbor() = CBORObject.NewMap().apply {
set("id", id.encodeAsCbor())
if (name != null) set("name", name.encodeAsCbor())
if (icon != null) set("icon", icon.encodeAsCbor())
if (displayName != null) set("displayName", displayName.encodeAsCbor())
if (!name.isNullOrBlank()) set("name", name.encodeAsCbor())
if (!icon.isNullOrBlank()) set("icon", icon.encodeAsCbor())
if (!displayName.isNullOrBlank()) set("displayName", displayName.encodeAsCbor())
}

fun CBORObject.decodeAsPublicKeyCredentialUserEntity() = PublicKeyCredentialUserEntity(
Expand All @@ -57,6 +57,11 @@ fun PublicKeyCredentialParameters.encodeAsCbor() = CBORObject.NewMap().apply {
set("type", typeAsString.encodeAsCbor())
}

fun CBORObject.decodeAsPublicKeyCredentialParameters() = PublicKeyCredentialParameters(
get("type").AsString(),
get("alg").AsInt32Value()
)

fun PublicKeyCredentialDescriptor.encodeAsCbor() = CBORObject.NewMap().apply {
set("type", typeAsString.encodeAsCbor())
set("id", id.encodeAsCbor())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@

package org.microg.gms.fido.core.protocol.msgs

import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialParameters
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.fido.core.protocol.decodeAsPublicKeyCredentialParameters
import org.microg.gms.utils.ToStringHelper

class AuthenticatorGetInfoCommand : Ctap2Command<AuthenticatorGetInfoRequest, AuthenticatorGetInfoResponse>(AuthenticatorGetInfoRequest()) {
Expand All @@ -22,7 +24,22 @@ class AuthenticatorGetInfoResponse(
val aaguid: ByteArray,
val options: Options,
val maxMsgSize: Int?,
val pinProtocols: List<Int>
val pinUvAuthProtocols: List<Int>,
val maxCredentialCountInList: Int?,
val maxCredentialIdLength: Int?,
val transports: List<String>?,
val algorithms: List<PublicKeyCredentialParameters>?,
val maxSerializedLargeBlobArray: Int?,
val forcePINChange: Boolean,
val minPINLength: Int?,
val firmwareVersion: Int?,
val maxCredBlobLength: Int?,
val maxRPIDsForSetMinPINLength: Int?,
val preferredPlatformUvAttempts: Int?,
val uvModality: Int?,
val certifications: Map<String, Int>?,
val remainingDiscoverableCredentials: Int?,
val vendorPrototypeConfigCommands: List<Int>?,
) : Ctap2Response {

companion object {
Expand Down Expand Up @@ -103,11 +120,48 @@ class AuthenticatorGetInfoResponse(
?: throw IllegalArgumentException("Not a valid response for authenticatorGetInfo"),
options = Options.decodeFromCbor(obj.get(4)),
maxMsgSize = obj.get(5)?.AsInt32Value(),
pinProtocols = obj.get(6)?.AsInt32Sequence()?.toList().orEmpty()
pinUvAuthProtocols = obj.get(6)?.AsInt32Sequence()?.toList().orEmpty(),
maxCredentialCountInList = obj.get(7)?.AsInt32Value(),
maxCredentialIdLength = obj.get(8)?.AsInt32Value(),
transports = obj.get(9)?.AsStringSequence()?.toList(),
algorithms = runCatching { obj.get(10)?.values?.map { it.decodeAsPublicKeyCredentialParameters() } }.getOrNull(),
maxSerializedLargeBlobArray = obj.get(11)?.AsInt32Value(),
forcePINChange = obj.get(12)?.AsBoolean() == true,
minPINLength = obj.get(13)?.AsInt32Value(),
firmwareVersion = obj.get(14)?.AsInt32Value(),
maxCredBlobLength = obj.get(15)?.AsInt32Value(),
maxRPIDsForSetMinPINLength = obj.get(16)?.AsInt32Value(),
preferredPlatformUvAttempts = obj.get(17)?.AsInt32Value(),
uvModality = obj.get(18)?.AsInt32Value(),
certifications = obj.get(19)?.entries?.mapNotNull { runCatching { it.key.AsString() to it.value.AsInt32Value() }.getOrNull() }?.toMap(),
remainingDiscoverableCredentials = obj.get(20)?.AsInt32Value(),
vendorPrototypeConfigCommands = obj.get(20)?.AsInt32Sequence()?.toList(),

This comment has been minimized.

Copy link
@ale5000-git

ale5000-git Sep 11, 2023

Member

@mar-v-in
Is it intended to have obj.get(20) two times?

This comment has been minimized.

Copy link
@mar-v-in

mar-v-in Sep 11, 2023

Author Member

Of course not ;) Good catch!

)
}

override fun toString(): String {
return "AuthenticatorGetInfoResponse(versions=$versions, extensions=$extensions, aaguid=${aaguid.contentToString()}, options=$options, maxMsgSize=$maxMsgSize, pinProtocols=$pinProtocols)"
return ToStringHelper.name("AuthenticatorGetInfoResponse")
.field("versions", versions)
.field("extensions", extensions)
.field("aaguid", aaguid)
.field("options", options)
.field("maxMsgSize", maxMsgSize)
.field("pinUvAuthProtocols", pinUvAuthProtocols)
.field("maxCredentialCountInList", maxCredentialCountInList)
.field("maxCredentialIdLength", maxCredentialIdLength)
.field("transports", transports)
.field("algorithms", algorithms)
.field("maxSerializedLargeBlobArray", maxSerializedLargeBlobArray)
.field("forcePINChange", forcePINChange)
.field("minPINLength", minPINLength)
.field("firmwareVersion", firmwareVersion)
.field("maxCredBlobLength", maxCredBlobLength)
.field("maxRPIDsForSetMinPINLength", maxRPIDsForSetMinPINLength)
.field("preferredPlatformUvAttempts", preferredPlatformUvAttempts)
.field("uvModality", uvModality)
.field("certifications", certifications)
.field("remainingDiscoverableCredentials", remainingDiscoverableCredentials)
.field("vendorPrototypeConfigCommands", vendorPrototypeConfigCommands)
.end()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ 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
const val CAPABILITY_USER_VERIFICATION = 1 shl 6
const val CAPABILITY_RESIDENT_KEY = 1 shl 7

interface CtapConnection {
val capabilities: Int
val transports: List<String>

val hasCtap1Support: Boolean
get() = capabilities and CAPABILITY_CTAP_1 > 0
Expand All @@ -31,6 +34,10 @@ interface CtapConnection {
get() = capabilities and CAPABILITY_WINK > 0
val canMakeCredentialWithoutUserVerification: Boolean
get() = capabilities and CAPABILITY_MAKE_CRED_WITHOUT_UV > 0
val hasUserVerificationSupport: Boolean
get() = capabilities and CAPABILITY_USER_VERIFICATION > 0
val hasResidentKey: Boolean
get() = capabilities and CAPABILITY_RESIDENT_KEY > 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ 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.google.android.gms.fido.fido2.api.common.ResidentKeyRequirement.*
import com.google.android.gms.fido.fido2.api.common.UserVerificationRequirement.*
import com.upokecenter.cbor.CBORObject
import kotlinx.coroutines.delay
import org.microg.gms.fido.core.*
Expand Down Expand Up @@ -51,9 +52,19 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor
options: RequestOptions,
clientDataHash: ByteArray
): Pair<AuthenticatorMakeCredentialResponse, ByteArray?> {
connection.capabilities
val reqOptions = AuthenticatorMakeCredentialRequest.Companion.Options(
options.registerOptions.authenticatorSelection?.requireResidentKey == true,
options.registerOptions.authenticatorSelection?.requireUserVerification == REQUIRED
when (options.registerOptions.authenticatorSelection?.residentKeyRequirement) {
RESIDENT_KEY_REQUIRED -> true
RESIDENT_KEY_PREFERRED -> connection.hasResidentKey
RESIDENT_KEY_DISCOURAGED -> false
else -> options.registerOptions.authenticatorSelection?.requireResidentKey == true
},
when (options.registerOptions.authenticatorSelection?.requireUserVerification) {
REQUIRED -> true
DISCOURAGED -> false
else -> connection.hasUserVerificationSupport
}
)
val extensions = mutableMapOf<String, CBORObject>()
if (options.authenticationExtensions?.fidoAppIdExtension?.appId != null) {
Expand Down Expand Up @@ -167,7 +178,8 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor
return AuthenticatorAttestationResponse(
keyHandle,
clientData,
AnyAttestationObject(response.authData, response.fmt, response.attStmt).encode()
AnyAttestationObject(response.authData, response.fmt, response.attStmt).encode(),
connection.transports.toTypedArray()
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class CtapNfcConnection(
) : CtapConnection {
private val isoDep = IsoDep.get(tag)
override var capabilities: Int = 0
override var transports: List<String> = listOf("nfc")

override suspend fun <Q : Ctap1Request, S : Ctap1Response> runCommand(command: Ctap1Command<Q, S>): S {
require(hasCtap1Support)
Expand Down Expand Up @@ -70,7 +71,10 @@ class CtapNfcConnection(
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)
(if (response.options.clientPin == true) CAPABILITY_CLIENT_PIN else 0) or
(if (response.options.userVerification == true) CAPABILITY_USER_VERIFICATION else 0) or
(if (response.options.residentKey == true) CAPABILITY_RESIDENT_KEY else 0)
if (response.transports != null) transports = response.transports
}

suspend fun open(): Boolean = withContext(Dispatchers.IO) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac
return AuthenticatorAttestationResponse(
credentialId.encode(),
clientData,
attestationObject.encode()
attestationObject.encode(),
arrayOf("internal")
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class CtapHidConnection(
private val outEndpoint = iface.endpoints.first { it.direction == USB_DIR_OUT }
private var channelIdentifier = 0xffffffff.toInt()
override var capabilities: Int = 0
override var transports: List<String> = listOf("usb")

suspend fun open(): Boolean {
Log.d(TAG, "Opening connection")
Expand Down Expand Up @@ -76,7 +77,10 @@ class CtapHidConnection(
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)
(if (response.options.clientPin == true) CAPABILITY_CLIENT_PIN else 0) or
(if (response.options.userVerification == true) CAPABILITY_USER_VERIFICATION else 0) or
(if (response.options.residentKey == true) CAPABILITY_RESIDENT_KEY else 0)
if (response.transports != null) transports = response.transports
}

suspend fun sendRequest(request: CtapHidRequest) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,13 @@ class AuthenticatorActivity : AppCompatActivity(), TransportHandlerCallback {
}
val id = rawId?.toBase64(Base64.URL_SAFE, Base64.NO_WRAP, Base64.NO_PADDING)
if (rpId != null && id != null) database.insertKnownRegistration(rpId, id, transport)
finishWithCredential(PublicKeyCredential.Builder().setResponse(response).setRawId(rawId).setId(id).build())
finishWithCredential(PublicKeyCredential.Builder()
.setResponse(response)
.setRawId(rawId)
.setId(id)
.setAuthenticatorAttachment(if (transport == SCREEN_LOCK) "platform" else "cross-platform")
.build()
)
}

private fun finishWithCredential(publicKeyCredential: PublicKeyCredential) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.google.android.gms.fido.fido2.api;

import com.google.android.gms.common.api.Status;
import com.google.android.gms.fido.fido2.api.common.FidoCredentialDetails;

interface ICredentialListCallback {
void onCredentialList(in List<FidoCredentialDetails> value) = 0;
void onError(in Status status) = 1;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.google.android.gms.fido.fido2.api.common;

parcelable FidoCredentialDetails;
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package com.google.android.gms.fido.fido2.internal.privileged;

import com.google.android.gms.fido.fido2.internal.privileged.IFido2PrivilegedCallbacks;
import com.google.android.gms.fido.fido2.api.IBooleanCallback;
import com.google.android.gms.fido.fido2.api.ICredentialListCallback;
import com.google.android.gms.fido.fido2.api.common.BrowserPublicKeyCredentialCreationOptions;
import com.google.android.gms.fido.fido2.api.common.BrowserPublicKeyCredentialRequestOptions;

interface IFido2PrivilegedService {
void register(IFido2PrivilegedCallbacks callbacks, in BrowserPublicKeyCredentialCreationOptions options) = 0;
void sign(IFido2PrivilegedCallbacks callbacks, in BrowserPublicKeyCredentialRequestOptions options) = 1;
void getRegisterPendingIntent(IFido2PrivilegedCallbacks callbacks, in BrowserPublicKeyCredentialCreationOptions options) = 0;
void getSignPendingIntent(IFido2PrivilegedCallbacks callbacks, in BrowserPublicKeyCredentialRequestOptions options) = 1;
void isUserVerifyingPlatformAuthenticatorAvailable(IBooleanCallback callbacks) = 2;
void getCredentialList(ICredentialListCallback callbacks, String rpId) = 3;
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ public enum Transport implements Parcelable {
NFC("nfc"),
USB("usb"),
INTERNAL("internal"),
@PublicApi(exclude = true)
CABLE("cable");
HYBRID("cable");

private final String value;

Expand Down Expand Up @@ -60,6 +59,9 @@ public static Transport fromString(String transport) throws UnsupportedTransport
for (Transport value : values()) {
if (value.value.equals(transport)) return value;
}
if (transport.equals("hybrid")) {
return HYBRID;
}
throw new UnsupportedTransportException("Transport " + transport + " not supported");
}

Expand Down
Loading

0 comments on commit 046218e

Please sign in to comment.