Skip to content

Commit

Permalink
Identity: Add support for handling Fido requests
Browse files Browse the repository at this point in the history
  • Loading branch information
mar-v-in committed Aug 21, 2024
1 parent 8baf694 commit f263255
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 24 deletions.
7 changes: 7 additions & 0 deletions play-services-core/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,13 @@
</intent-filter>
</service>

<activity
android:name="org.microg.gms.auth.credentials.identity.IdentityFidoProxyActivity"
android:exported="false"
android:process=":ui"
android:theme="@style/Theme.App.Translucent"
android:excludeFromRecents="true"/>

<activity
android:theme="@style/Theme.AppCompat.Dialog.Alert"
android:name="org.microg.gms.auth.signin.AssistedSignInActivity"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* SPDX-FileCopyrightText: 2024 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/

package org.microg.gms.auth.credentials.identity

import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.google.android.gms.auth.api.identity.SignInCredential
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.common.api.Status
import com.google.android.gms.common.internal.safeparcel.SafeParcelableSerializer
import com.google.android.gms.fido.Fido.FIDO2_KEY_CREDENTIAL_EXTRA
import com.google.android.gms.fido.fido2.api.common.AuthenticatorErrorResponse
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredential
import org.microg.gms.auth.AuthConstants
import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.KEY_CALLER

private const val REQUEST_CODE = 1586077619

class IdentityFidoProxyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivityForResult(Intent("org.microg.gms.fido.AUTHENTICATE").apply {
`package` = packageName
putExtras(intent.extras ?: Bundle())
putExtra(KEY_CALLER, callingActivity?.packageName)
}, REQUEST_CODE)
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE) {
if (resultCode == RESULT_OK) {
val publicKeyCredential = PublicKeyCredential.deserializeFromBytes(data?.getByteArrayExtra(FIDO2_KEY_CREDENTIAL_EXTRA))
if (publicKeyCredential.response is AuthenticatorErrorResponse) {
val errorResponse = publicKeyCredential.response as AuthenticatorErrorResponse
setResult(RESULT_OK, Intent().apply {
putExtra(AuthConstants.STATUS, SafeParcelableSerializer.serializeToBytes(Status(CommonStatusCodes.ERROR, errorResponse.errorMessage)))
})
} else {
setResult(RESULT_OK, Intent().apply {
putExtra(
AuthConstants.SIGN_IN_CREDENTIAL, SafeParcelableSerializer.serializeToBytes(
SignInCredential(
publicKeyCredential.id,
null, null, null, null, null, null, null,
publicKeyCredential
)
)
)
putExtra(AuthConstants.STATUS, SafeParcelableSerializer.serializeToBytes(Status.SUCCESS))
})
}
} else {
setResult(resultCode)
}
finish()
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Base64
import android.util.Log
import androidx.core.app.PendingIntentCompat
import androidx.core.os.bundleOf
import com.google.android.gms.auth.api.identity.BeginSignInRequest
import com.google.android.gms.auth.api.identity.BeginSignInResult
import com.google.android.gms.auth.api.identity.GetPhoneNumberHintIntentRequest
Expand All @@ -28,6 +30,12 @@ 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.common.internal.safeparcel.SafeParcelableSerializer
import com.google.android.gms.fido.common.Transport
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialDescriptor
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialRequestOptions
import com.google.android.gms.fido.fido2.api.common.UserVerificationRequirement
import org.json.JSONArray
import org.json.JSONObject
import org.microg.gms.BaseService
import org.microg.gms.auth.AuthConstants
import org.microg.gms.auth.signin.ACTION_ASSISTED_SIGN_IN
Expand All @@ -39,6 +47,11 @@ import org.microg.gms.auth.signin.GOOGLE_SIGN_IN_OPTIONS
import org.microg.gms.auth.signin.performSignOut
import org.microg.gms.common.Constants
import org.microg.gms.common.GmsService
import org.microg.gms.fido.core.ui.AuthenticatorActivity
import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.KEY_OPTIONS
import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.KEY_SERVICE
import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.KEY_SOURCE
import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.KEY_TYPE

private const val TAG = "IdentitySignInService"

Expand All @@ -54,36 +67,67 @@ class IdentitySignInService : BaseService(TAG, GmsService.IDENTITY_SIGN_IN) {
}
}

class IdentitySignInServiceImpl(private val mContext: Context, private val clientPackageName: String) :
class IdentitySignInServiceImpl(private val context: Context, private val clientPackageName: String) :
ISignInService.Stub() {

private val requestMap = mutableMapOf<String, GoogleSignInOptions>()

override fun beginSignIn(callback: IBeginSignInCallback, request: BeginSignInRequest) {
Log.d(TAG, "method 'beginSignIn' called")
Log.d(TAG, "request-> $request")
if (request.googleIdTokenRequestOptions.serverClientId.isNullOrEmpty()) {
Log.d(TAG, "serverClientId is empty, return CANCELED ")
callback.onResult(Status.CANCELED, null)
return
}
val bundle = Bundle().apply {
val options = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(request.googleIdTokenRequestOptions.serverClientId).build()
putByteArray(BEGIN_SIGN_IN_REQUEST, SafeParcelableSerializer.serializeToBytes(request))
putByteArray(GOOGLE_SIGN_IN_OPTIONS, SafeParcelableSerializer.serializeToBytes(options))
putString(CLIENT_PACKAGE_NAME, clientPackageName)
requestMap[request.sessionId] = options
if (request.googleIdTokenRequestOptions.isSupported) {
if (request.googleIdTokenRequestOptions.serverClientId.isNullOrEmpty()) {
Log.d(TAG, "serverClientId is empty, return CANCELED ")
callback.onResult(Status.CANCELED, null)
return
}
val bundle = Bundle().apply {
val options = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(request.googleIdTokenRequestOptions.serverClientId).build()
putByteArray(BEGIN_SIGN_IN_REQUEST, SafeParcelableSerializer.serializeToBytes(request))
putByteArray(GOOGLE_SIGN_IN_OPTIONS, SafeParcelableSerializer.serializeToBytes(options))
putString(CLIENT_PACKAGE_NAME, clientPackageName)
requestMap[request.sessionId] = options
}
callback.onResult(Status.SUCCESS, BeginSignInResult(performGooogleSignIn(bundle)))
} else if (request.passkeyJsonRequestOptions.isSupported) {
fun JSONObject.getArrayOrNull(key: String) = if (has(key)) getJSONArray(key) else null
fun <T> JSONArray.map(fn: (JSONObject) -> T): List<T> = (0 until length()).map { fn(getJSONObject(it)) }
fun <T> JSONArray.map(fn: (String) -> T): List<T> = (0 until length()).map { fn(getString(it)) }
val json = JSONObject(request.passkeyJsonRequestOptions.requestJson)
val options = PublicKeyCredentialRequestOptions.Builder()
.setAllowList(json.getArrayOrNull("allowCredentials")?.let { allowCredentials -> allowCredentials.map { credential: JSONObject ->
PublicKeyCredentialDescriptor(
credential.getString("type"),
Base64.decode(credential.getString("id"), Base64.URL_SAFE),
credential.getArrayOrNull("transports")?.let { transports -> transports.map { transportString: String ->
Transport.fromString(transportString)
} }.orEmpty()
)
} })
.setChallenge(Base64.decode(json.getString("challenge"), Base64.URL_SAFE))
.setRequireUserVerification(json.optString("userVerification").takeIf { it.isNotBlank() }?.let { UserVerificationRequirement.fromString(it) })
.setRpId(json.getString("rpId"))
.setTimeoutSeconds(json.optDouble("timeout", -1.0).takeIf { it > 0 })
.build()
val bundle = bundleOf(
KEY_SERVICE to GmsService.IDENTITY_SIGN_IN.SERVICE_ID,
KEY_SOURCE to "app",
KEY_TYPE to "sign",
KEY_OPTIONS to options.serializeToBytes()
)
callback.onResult(Status.SUCCESS, BeginSignInResult(performFidoSignIn(bundle)))
} else {
callback.onResult(Status.INTERNAL_ERROR, null)
}
callback.onResult(Status.SUCCESS, BeginSignInResult(performSignIn(bundle)))
}

override fun signOut(callback: IStatusCallback, requestTag: String) {
Log.d(TAG, "method signOut called, requestTag=$requestTag")
if (requestMap.containsKey(requestTag)) {
val accounts = AccountManager.get(mContext).getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE)
val accounts = AccountManager.get(context).getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE)
if (accounts.isNotEmpty()) {
accounts.forEach { performSignOut(mContext, clientPackageName, requestMap[requestTag], it) }
accounts.forEach { performSignOut(context, clientPackageName, requestMap[requestTag], it) }
}
}
callback.onResult(Status.SUCCESS)
Expand All @@ -108,7 +152,7 @@ class IdentitySignInServiceImpl(private val mContext: Context, private val clien
putString(CLIENT_PACKAGE_NAME, clientPackageName)
requestMap[request.sessionId] = options
}
callback.onResult(Status.SUCCESS, performSignIn(bundle))
callback.onResult(Status.SUCCESS, performGooogleSignIn(bundle))
}

override fun getPhoneNumberHintIntent(
Expand All @@ -118,13 +162,21 @@ class IdentitySignInServiceImpl(private val mContext: Context, private val clien
callback.onResult(Status.CANCELED, null)
}

private fun performSignIn(bundle: Bundle): PendingIntent {
private fun performGooogleSignIn(bundle: Bundle): PendingIntent {
val intent = Intent(ACTION_ASSISTED_SIGN_IN).apply {
`package` = Constants.GMS_PACKAGE_NAME
`package` = context.packageName
putExtras(bundle)
}
val flags = PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT
return PendingIntentCompat.getActivity(context, 0, intent, flags, false)!!
}

private fun performFidoSignIn(bundle: Bundle): PendingIntent {
val intent = Intent(context, IdentityFidoProxyActivity::class.java).apply {
putExtras(bundle)
}
val flags = PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT
return PendingIntentCompat.getActivity(mContext, 0, intent, flags, false)!!
return PendingIntentCompat.getActivity(context, 0, intent, flags, false)!!
}

}
7 changes: 6 additions & 1 deletion play-services-fido/core/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
android:configChanges="orientation|keyboard|keyboardHidden|screenSize"
android:exported="false"
android:process=":ui"
android:theme="@style/Theme.Translucent" />
android:theme="@style/Theme.Translucent">
<intent-filter>
<action android:name="org.microg.gms.fido.AUTHENTICATE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package org.microg.gms.fido.core
import android.content.Context
import android.net.Uri
import android.util.Base64
import android.util.Log
import com.android.volley.toolbox.JsonArrayRequest
import com.android.volley.toolbox.JsonObjectRequest
import com.android.volley.toolbox.Volley
Expand All @@ -23,6 +24,8 @@ import org.microg.gms.utils.*
import java.net.HttpURLConnection
import java.security.MessageDigest

private const val TAG = "Fido"

class RequestHandlingException(val errorCode: ErrorCode, message: String? = null) : Exception(message)
class MissingPinException(message: String? = null): Exception(message)
class WrongPinException(message: String? = null): Exception(message)
Expand Down Expand Up @@ -117,8 +120,10 @@ private suspend fun isAssetLinked(context: Context, rpId: String, facetId: Strin
if (fingerprint.equals(fp, ignoreCase = true)) return true
}
}
Log.w(TAG, "No matching asset link")
return false
} catch (e: Exception) {
Log.w(TAG, "Failed fetching asset link", e)
return false
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class AuthenticatorActivity : AppCompatActivity(), TransportHandlerCallback {

try {

val callerPackage = callingActivity?.packageName ?: return finish()
val callerPackage = (if (callingActivity?.packageName == packageName && intent.hasExtra(KEY_CALLER)) intent.getStringExtra(KEY_CALLER) else callingActivity?.packageName) ?: return finish()
if (!intent.extras?.keySet().orEmpty().containsAll(REQUIRED_EXTRAS)) {
return finishWithError(UNKNOWN_ERR, "Extra missing from request")
}
Expand Down Expand Up @@ -245,7 +245,7 @@ class AuthenticatorActivity : AppCompatActivity(), TransportHandlerCallback {
} else {
intent.putExtra(FIDO2_KEY_RESPONSE_EXTRA, response.serializeToBytes())
}
setResult(-1, intent)
setResult(RESULT_OK, intent)
finish()
}

Expand Down Expand Up @@ -334,6 +334,8 @@ class AuthenticatorActivity : AppCompatActivity(), TransportHandlerCallback {
const val TYPE_REGISTER = "register"
const val TYPE_SIGN = "sign"

const val KEY_CALLER = "caller"

val IMPLEMENTED_TRANSPORTS = setOf(USB, SCREEN_LOCK, NFC)
val INSTANT_SUPPORTED_TRANSPORTS = setOf(SCREEN_LOCK)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ public static class Builder {
private TokenBinding tokenBinding;
@Nullable
private AuthenticationExtensions authenticationExtensions;
@Nullable
private UserVerificationRequirement requireUserVerification;

/**
* The constructor of {@link PublicKeyCredentialRequestOptions.Builder}.
Expand All @@ -180,6 +182,7 @@ public Builder() {
* Sets a list of public key credentials which constrain authentication to authenticators that contain a
* private key for at least one of the supplied public keys.
*/
@NonNull
public Builder setAllowList(@Nullable List<PublicKeyCredentialDescriptor> allowList) {
this.allowList = allowList;
return this;
Expand All @@ -189,6 +192,7 @@ public Builder setAllowList(@Nullable List<PublicKeyCredentialDescriptor> allowL
* Sets additional extensions that may dictate some client behavior during an exchange with a connected
* authenticator.
*/
@NonNull
public Builder setAuthenticationExtensions(@Nullable AuthenticationExtensions authenticationExtensions) {
this.authenticationExtensions = authenticationExtensions;
return this;
Expand All @@ -198,6 +202,7 @@ public Builder setAuthenticationExtensions(@Nullable AuthenticationExtensions au
* Sets the nonce value that the authenticator should sign using a private key corresponding to a public key
* credential that is acceptable for this authentication session.
*/
@NonNull
public Builder setChallenge(@NonNull byte[] challenge) {
this.challenge = challenge;
return this;
Expand All @@ -208,11 +213,19 @@ public Builder setChallenge(@NonNull byte[] challenge) {
* time that the server initiates a single FIDO2 request to the client and receives reply) on a single device.
* This field is optional.
*/
@NonNull
public Builder setRequestId(@Nullable Integer requestId) {
this.requestId = requestId;
return this;
}

@Hide
@NonNull
public Builder setRequireUserVerification(@Nullable UserVerificationRequirement requireUserVerification) {
this.requireUserVerification = requireUserVerification;
return this;
}

/**
* Sets identifier for a relying party, on whose behalf a given authentication operation is being performed.
* A public key credential can only be used for authentication with the same replying party it was registered
Expand All @@ -222,11 +235,13 @@ public Builder setRequestId(@Nullable Integer requestId) {
* context (aka https connection). Apps-facing API needs to check the package signature against Digital Asset
* Links, whose resource is the RP ID with prepended "//". Privileged (browser) API doesn't need the check.
*/
@NonNull
public Builder setRpId(@NonNull String rpId) {
this.rpId = rpId;
return this;
}

@NonNull
public Builder setTimeoutSeconds(@Nullable Double timeoutSeconds) {
this.timeoutSeconds = timeoutSeconds;
return this;
Expand All @@ -235,6 +250,7 @@ public Builder setTimeoutSeconds(@Nullable Double timeoutSeconds) {
/**
* Sets the {@link TokenBinding} associated with the calling origin.
*/
@NonNull
public Builder setTokenBinding(@Nullable TokenBinding tokenBinding) {
this.tokenBinding = tokenBinding;
return this;
Expand All @@ -243,8 +259,9 @@ public Builder setTokenBinding(@Nullable TokenBinding tokenBinding) {
/**
* Builds the {@link PublicKeyCredentialRequestOptions} object.
*/
@NonNull
public PublicKeyCredentialRequestOptions build() {
return new PublicKeyCredentialRequestOptions(challenge, timeoutSeconds, rpId, allowList, requestId, tokenBinding, null, authenticationExtensions);
return new PublicKeyCredentialRequestOptions(challenge, timeoutSeconds, rpId, allowList, requestId, tokenBinding, requireUserVerification, authenticationExtensions);
}
}

Expand Down

0 comments on commit f263255

Please sign in to comment.