From 624a880f26bfa127a088b3756eef81cceca90ca7 Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Fri, 3 Apr 2026 17:05:11 +0800 Subject: [PATCH 1/3] Add boot count service (#3374) Co-authored-by: Marvin W --- .../internal/IBootCountCallbacks.aidl | 7 ++++ .../clearcut/internal/IBootCountService.aidl | 7 ++++ .../src/main/AndroidManifest.xml | 6 +++ .../microg/gms/clearcut/BootCountService.kt | 39 +++++++++++++++++++ 4 files changed, 59 insertions(+) create mode 100644 play-services-clearcut/src/main/aidl/com/google/android/gms/clearcut/internal/IBootCountCallbacks.aidl create mode 100644 play-services-clearcut/src/main/aidl/com/google/android/gms/clearcut/internal/IBootCountService.aidl create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/clearcut/BootCountService.kt diff --git a/play-services-clearcut/src/main/aidl/com/google/android/gms/clearcut/internal/IBootCountCallbacks.aidl b/play-services-clearcut/src/main/aidl/com/google/android/gms/clearcut/internal/IBootCountCallbacks.aidl new file mode 100644 index 0000000000..3232c969f7 --- /dev/null +++ b/play-services-clearcut/src/main/aidl/com/google/android/gms/clearcut/internal/IBootCountCallbacks.aidl @@ -0,0 +1,7 @@ +package com.google.android.gms.clearcut.internal; + +import com.google.android.gms.common.api.Status; + +interface IBootCountCallbacks { + void onBootCount(in Status status, int bootCount) = 0; +} diff --git a/play-services-clearcut/src/main/aidl/com/google/android/gms/clearcut/internal/IBootCountService.aidl b/play-services-clearcut/src/main/aidl/com/google/android/gms/clearcut/internal/IBootCountService.aidl new file mode 100644 index 0000000000..3ed035ddeb --- /dev/null +++ b/play-services-clearcut/src/main/aidl/com/google/android/gms/clearcut/internal/IBootCountService.aidl @@ -0,0 +1,7 @@ +package com.google.android.gms.clearcut.internal; + +import com.google.android.gms.clearcut.internal.IBootCountCallbacks; + +interface IBootCountService { + void getBootCount(IBootCountCallbacks callbacks) = 0; +} diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index 2787802214..680d81777d 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -1250,6 +1250,12 @@ + + + + + + diff --git a/play-services-core/src/main/kotlin/org/microg/gms/clearcut/BootCountService.kt b/play-services-core/src/main/kotlin/org/microg/gms/clearcut/BootCountService.kt new file mode 100644 index 0000000000..4736ad429f --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/clearcut/BootCountService.kt @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package org.microg.gms.clearcut + +import android.content.Context +import android.os.Parcel +import android.provider.Settings +import android.util.Log +import com.google.android.gms.clearcut.internal.IBootCountCallbacks +import com.google.android.gms.clearcut.internal.IBootCountService +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.api.Status +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import org.microg.gms.BaseService +import org.microg.gms.common.GmsService +import org.microg.gms.utils.warnOnTransactionIssues + +private const val TAG = "BootCountService" + +class BootCountService : BaseService(TAG, GmsService.BOOT_COUNT) { + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + Log.d(TAG, "handleServiceRequest") + callback.onPostInitComplete(CommonStatusCodes.SUCCESS, BootCountServiceImpl(this), null) + } +} + +class BootCountServiceImpl(private val context: Context) : IBootCountService.Stub() { + override fun getBootCount(callbacks: IBootCountCallbacks?) { + Log.d(TAG, "getBootCount called") + val bootCount = Settings.Global.getInt(context.contentResolver, Settings.Global.BOOT_COUNT, 1) + callbacks?.onBootCount(Status.SUCCESS, bootCount) + } + + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = + warnOnTransactionIssues(code, reply, flags, TAG) { super.onTransact(code, data, reply, flags) } +} From 587d69396225a2a79bbf95d08f7c5815d0c5e1e6 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Fri, 3 Apr 2026 10:35:02 +0200 Subject: [PATCH 2/3] SemanticLocation: Announce new features Removes popup in Google Maps --- .../android/gms/semanticlocation/SemanticLocationService.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/play-services-core/src/main/java/com/google/android/gms/semanticlocation/SemanticLocationService.kt b/play-services-core/src/main/java/com/google/android/gms/semanticlocation/SemanticLocationService.kt index 70b3653861..c0bd6946e5 100644 --- a/play-services-core/src/main/java/com/google/android/gms/semanticlocation/SemanticLocationService.kt +++ b/play-services-core/src/main/java/com/google/android/gms/semanticlocation/SemanticLocationService.kt @@ -21,6 +21,9 @@ private const val TAG = "SemanticLocationService" private val FEATURES = arrayOf( Feature("semanticlocation_events", 1L), + Feature("semanticlocation_events_listener", 1L), + Feature("semanticlocation_registration_check", 1L), + Feature("semanticlocation_personalization_signal", 1L), ) class SemanticLocationService : BaseService(TAG, GmsService.SEMANTIC_LOCATION) { From 94a488be5cb61b6d04dfa892ef7927ce23a04cc4 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Mon, 30 Mar 2026 01:03:03 +0200 Subject: [PATCH 3/3] Credential Manager --- build.gradle | 2 +- .../gms/auth/api/identity/package-info.java | 12 - .../auth/api/identity/BeginSignInRequest.java | 124 +++++++++ play-services-core/build.gradle | 1 + .../src/main/AndroidManifest.xml | 67 +++++ .../credman/service/GoogleIdService.kt | 10 + .../service/PasswordAndPasskeyService.kt | 10 + .../credman/service/RemoteService.kt | 10 + .../identity/IdentityFidoProxyActivity.kt | 3 +- .../provider/CredentialProviderActivity.kt | 105 +++++++ .../credentials/provider/GoogleIdService.kt | 200 ++++++++++++++ .../credentials/provider/JsonExtensions.kt | 257 ++++++++++++++++++ .../provider/PasswordAndPasskeyService.kt | 187 +++++++++++++ .../provider/PublicKeyProxyActivity.kt | 155 +++++++++++ .../credentials/provider/RemoteService.kt | 133 +++++++++ .../provider/SignInProxyActivity.kt | 113 ++++++++ .../gms/auth/signin/AssistedSignInActivity.kt | 2 +- .../src/main/res/values/strings.xml | 2 + .../xml/credentials_provider_google_id.xml | 10 + .../res/xml/credentials_provider_passkey.xml | 8 + .../res/xml/credentials_provider_remote.xml | 8 + .../org/microg/gms/fido/core/Database.kt | 13 +- .../microg/gms/fido/core/RequestHandling.kt | 2 +- .../org/microg/gms/fido/core/protocol/Cbor.kt | 7 - .../microg/gms/fido/core/protocol/CoseKey.kt | 42 +++ .../protocol/msgs/AuthenticatorClientPIN.kt | 3 +- .../fido/core/transport/TransportHandler.kt | 25 +- .../hybrid/HybridTransportHandler.kt | 3 +- .../core/transport/nfc/NfcTransportHandler.kt | 10 +- .../screenlock/ScreenLockTransportHandler.kt | 30 +- .../core/transport/usb/UsbTransportHandler.kt | 8 +- .../gms/fido/core/ui/AuthenticatorActivity.kt | 28 +- .../core/ui/AuthenticatorActivityFragment.kt | 10 +- .../fido/core/ui/SignInSelectionFragment.kt | 7 +- .../main/res/drawable/ic_fido_bluetooth.xml | 1 - .../main/res/drawable/ic_fido_fingerprint.xml | 1 - .../src/main/res/drawable/ic_fido_nfc.xml | 1 - .../src/main/res/drawable/ic_fido_passkey.xml | 2 +- .../src/main/res/drawable/ic_fido_qr_code.xml | 3 +- .../src/main/res/drawable/ic_fido_usb.xml | 1 - .../api/common/AuthenticationExtensions.java | 8 + ...AuthenticationExtensionsClientOutputs.java | 17 +- .../fido2/api/common/PublicKeyCredential.java | 8 +- .../PublicKeyCredentialRequestOptions.java | 24 +- 44 files changed, 1572 insertions(+), 101 deletions(-) delete mode 100644 play-services-auth-api/src/main/java/com/google/android/gms/auth/api/identity/package-info.java create mode 100644 play-services-core/src/main/kotlin/com/google/android/gms/auth/api/credentials/credman/service/GoogleIdService.kt create mode 100644 play-services-core/src/main/kotlin/com/google/android/gms/auth/api/credentials/credman/service/PasswordAndPasskeyService.kt create mode 100644 play-services-core/src/main/kotlin/com/google/android/gms/auth/api/credentials/credman/service/RemoteService.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/CredentialProviderActivity.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/GoogleIdService.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/JsonExtensions.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/PasswordAndPasskeyService.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/PublicKeyProxyActivity.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/RemoteService.kt create mode 100644 play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/SignInProxyActivity.kt create mode 100644 play-services-core/src/main/res/xml/credentials_provider_google_id.xml create mode 100644 play-services-core/src/main/res/xml/credentials_provider_passkey.xml create mode 100644 play-services-core/src/main/res/xml/credentials_provider_remote.xml diff --git a/build.gradle b/build.gradle index c05a2b956f..296bd87097 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,7 @@ buildscript { ext.appcompatVersion = '1.6.1' ext.biometricVersion = '1.1.0' ext.coreVersion = '1.12.0' + ext.credentialsVersion = '1.2.0' ext.fragmentVersion = '1.6.2' ext.lifecycleVersion = '2.7.0' ext.loaderVersion = '1.1.0' @@ -123,4 +124,3 @@ subprojects { if (hasModule("hms", false)) maven {url 'https://developer.huawei.com/repo/'} } } - diff --git a/play-services-auth-api/src/main/java/com/google/android/gms/auth/api/identity/package-info.java b/play-services-auth-api/src/main/java/com/google/android/gms/auth/api/identity/package-info.java deleted file mode 100644 index 9784dfd67d..0000000000 --- a/play-services-auth-api/src/main/java/com/google/android/gms/auth/api/identity/package-info.java +++ /dev/null @@ -1,12 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 microG Project Team - * SPDX-License-Identifier: CC-BY-4.0 - * Notice: Portions of this file are reproduced from work created and shared by Google and used - * according to terms described in the Creative Commons 4.0 Attribution License. - * See https://developers.google.com/readme/policies for details. - */ -/** - * Provides facilities to retrieve or save credentials that can be used to - sign the user into your application or sign up a new user. - */ -package com.google.android.gms.auth.api.identity; diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/BeginSignInRequest.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/BeginSignInRequest.java index a1ffedf1aa..8122a5b55b 100644 --- a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/BeginSignInRequest.java +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/BeginSignInRequest.java @@ -115,8 +115,132 @@ public boolean isPreferImmediatelyAvailableCredentials() { return preferImmediatelyAvailableCredentials; } + /** + * Builder for {@link BeginSignInRequest}. + */ public static class Builder { + private PasswordRequestOptions passwordRequestOptions; + private GoogleIdTokenRequestOptions googleIdTokenRequestOptions; + private String sessionId; + private boolean autoSelectEnabled; + private int theme; + private PasskeysRequestOptions passkeysRequestOptions; + private PasskeyJsonRequestOptions passkeyJsonRequestOptions; + private boolean preferImmediatelyAvailableCredentials; + + /** + * Returns the built {@link BeginSignInRequest}. + */ + @NonNull + public BeginSignInRequest build() { + return new BeginSignInRequest( + passwordRequestOptions, + googleIdTokenRequestOptions, + sessionId, + autoSelectEnabled, + theme, + passkeysRequestOptions, + passkeyJsonRequestOptions, + preferImmediatelyAvailableCredentials + ); + } + /** + * Sets whether to enable auto-select for the credential. + *

+ * If enabled and only one credential is available, it will be automatically selected. + * + * @param autoSelectEnabled whether to enable auto-select + */ + @NonNull + public Builder setAutoSelectEnabled(boolean autoSelectEnabled) { + this.autoSelectEnabled = autoSelectEnabled; + return this; + } + + /** + * Sets options for requesting Google ID token-backed credentials. + * + * @param googleIdTokenRequestOptions the Google ID token request options + */ + @NonNull + public Builder setGoogleIdTokenRequestOptions(@Nullable GoogleIdTokenRequestOptions googleIdTokenRequestOptions) { + this.googleIdTokenRequestOptions = googleIdTokenRequestOptions; + return this; + } + + /** + * Sets options for requesting passkey credentials using JSON format. + * + * @param passkeyJsonRequestOptions the passkey JSON request options + */ + @NonNull + public Builder setPasskeyJsonRequestOptions(@Nullable PasskeyJsonRequestOptions passkeyJsonRequestOptions) { + this.passkeyJsonRequestOptions = passkeyJsonRequestOptions; + return this; + } + + /** + * Sets options for requesting passkey credentials. + * + * @param passkeysRequestOptions the passkey request options + * @deprecated Use {@link #setPasskeyJsonRequestOptions(PasskeyJsonRequestOptions)} instead + */ + @Deprecated + @NonNull + public Builder setPasskeysRequestOptions(@Nullable PasskeysRequestOptions passkeysRequestOptions) { + this.passkeysRequestOptions = passkeysRequestOptions; + return this; + } + + /** + * Sets options for requesting password-backed credentials. + * + * @param passwordRequestOptions the password request options + */ + @NonNull + public Builder setPasswordRequestOptions(@Nullable PasswordRequestOptions passwordRequestOptions) { + this.passwordRequestOptions = passwordRequestOptions; + return this; + } + + /** + * Sets whether to prefer immediately available credentials. + *

+ * If true, the API will only return credentials that are immediately available + * without requiring user interaction. + * + * @param preferImmediatelyAvailableCredentials whether to prefer immediately available credentials + */ + @NonNull + public Builder setPreferImmediatelyAvailableCredentials(boolean preferImmediatelyAvailableCredentials) { + this.preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials; + return this; + } + + /** + * Sets the session ID for this sign-in request. + * + * @param sessionId the session ID + */ + @Hide + @NonNull + public Builder setSessionId(@Nullable String sessionId) { + this.sessionId = sessionId; + return this; + } + + /** + * Sets the theme for the sign-in UI. + * + * @param theme the theme resource ID + */ + @Hide + @NonNull + public Builder setTheme(int theme) { + this.theme = theme; + return this; + } } /** diff --git a/play-services-core/build.gradle b/play-services-core/build.gradle index 8c8beef87e..a348337ce1 100644 --- a/play-services-core/build.gradle +++ b/play-services-core/build.gradle @@ -103,6 +103,7 @@ dependencies { implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" + implementation "androidx.credentials:credentials:$credentialsVersion" implementation "androidx.work:work-runtime-ktx:$workVersion" } diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index 680d81777d..a17d7f03ef 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -147,6 +147,8 @@ + + @@ -185,6 +187,7 @@ tools:overrideLibrary="androidx.compose.ui.tooling, androidx.compose.material3, androidx.activity.compose, + androidx.credentials, androidx.compose.material.icons, androidx.compose.material.ripple, androidx.compose.foundation, @@ -674,6 +677,70 @@ android:exported="true" android:theme="@style/Theme.LoginBlue"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Log.e(TAG, "Error handling passkey", e) + finishWithException(e.localizedMessage) + } + } + + // TODO: Turn these into suspendable and make sure that returning the correct result type becomes mandatory + abstract fun onProviderCreateCredentialRequest(request: ProviderCreateCredentialRequest) + abstract fun onProviderGetCredentialRequest(request: ProviderGetCredentialRequest) + + fun finishWithSuccess(response: CreateCredentialResponse) { + if (!isCreateRequest) return finishWithException() + setResult( + RESULT_OK, + Intent().also { PendingIntentHandler.setCreateCredentialResponse(it, response) } + ) + finish() + } + + fun finishWithSuccess(response: GetCredentialResponse) { + if (!isGetRequest) return finishWithException() + setResult( + RESULT_OK, + Intent().also { PendingIntentHandler.setGetCredentialResponse(it, response) } + ) + finish() + } + + fun finishWithException( + message: String? = null, + createExceptionCreator: (String?) -> CreateCredentialException = { CreateCredentialUnknownException(it) }, + getExceptionCreator: (String?) -> GetCredentialException = { GetCredentialUnknownException(it) } + ) { + when { + isCreateRequest -> { + setResult( + RESULT_OK, + Intent().also { + PendingIntentHandler.setCreateCredentialException( + it, + createExceptionCreator(message) + ) + } + ) + } + + isGetRequest -> { + setResult( + RESULT_OK, + Intent().also { PendingIntentHandler.setGetCredentialException(it, getExceptionCreator(message)) } + ) + } + + else -> { + if (message != null) Log.w(TAG, message) + setResult(RESULT_CANCELED) + } + } + finish() + } +} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/GoogleIdService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/GoogleIdService.kt new file mode 100644 index 0000000000..36e00c4fdb --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/GoogleIdService.kt @@ -0,0 +1,200 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.auth.credentials.provider + +import android.accounts.AccountManager +import android.app.PendingIntent +import android.content.Intent +import android.graphics.drawable.Icon +import android.os.Bundle +import android.os.CancellationSignal +import android.os.OutcomeReceiver +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.credentials.exceptions.ClearCredentialException +import androidx.credentials.exceptions.CreateCredentialException +import androidx.credentials.exceptions.GetCredentialException +import androidx.credentials.exceptions.GetCredentialUnknownException +import androidx.credentials.provider.BeginCreateCredentialRequest +import androidx.credentials.provider.BeginCreateCredentialResponse +import androidx.credentials.provider.BeginGetCredentialOption +import androidx.credentials.provider.BeginGetCredentialRequest +import androidx.credentials.provider.BeginGetCredentialResponse +import androidx.credentials.provider.ProviderClearCredentialStateRequest +import androidx.credentials.provider.CredentialEntry +import androidx.credentials.provider.CredentialProviderService +import androidx.credentials.provider.CustomCredentialEntry +import com.google.android.gms.R +import org.microg.gms.auth.credentials.provider.GoogleIdRequestParams.Companion.toGoogleIdRequestParams +import org.microg.gms.auth.AuthConstants + +private const val TAG = "GoogleIdService" + +/** + * Google ID Credential Provider Service + * Handles Google Sign-In and Google ID Token credentials + * Note: This service only handles GET operations (sign-in), not CREATE operations + */ +@RequiresApi(34) +open class GoogleIdService : CredentialProviderService() { + + override fun onBeginGetCredentialRequest( + request: BeginGetCredentialRequest, + cancellationSignal: CancellationSignal, + callback: OutcomeReceiver + ) { + runCatching { + val credentialEntries = request.beginGetCredentialOptions + .flatMap { option -> + when (option.type) { + TYPE_GOOGLE_ID_TOKEN_CREDENTIAL -> handleGoogleIdTokenRequest(option, request) + else -> emptyList().also { + Log.d(TAG, "Unsupported credential type: ${option.type}") + } + } + } + + callback.onResult(BeginGetCredentialResponse.Builder() + .setCredentialEntries(credentialEntries) + .build() + .also { Log.d(TAG, "Returning ${credentialEntries.size} credential entries") }) + + }.onFailure { e -> + Log.e(TAG, "Error in onBeginGetCredential", e) + callback.onError(GetCredentialUnknownException(e.message)) + } + } + + private fun handleGoogleIdTokenRequest( + option: BeginGetCredentialOption, + request: BeginGetCredentialRequest + ): List = option.candidateQueryData.toGoogleIdRequestParams().let { params -> + val callingPackage = request.callingAppInfo?.packageName.orEmpty() + val accounts = AccountManager.get(this).getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) + if (accounts.isEmpty()) { + listOf(createCredentialEntry( + option = option, + params = params, + callingPackage = callingPackage, + title = getString(R.string.credentials_service_sign_in_with_google_label), + requestCode = TAG.hashCode(), + )) + } else { + accounts.map { account -> + createCredentialEntry( + option = option, + params = params, + callingPackage = callingPackage, + accountName = account.name, + title = account.name, + requestCode = account.name.hashCode() + ) + } + } + } + + private fun createCredentialEntry( + option: BeginGetCredentialOption, + params: GoogleIdRequestParams, + callingPackage: String, + accountName: String? = null, + title: String, + requestCode: Int + ): CredentialEntry { + val intent = createGoogleIdIntent(params, callingPackage, accountName) + val pendingIntent = PendingIntent.getActivity( + this, + requestCode, + intent, + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + + return CustomCredentialEntry( + this, + title, + pendingIntent, + option, + accountName?.takeIf { it != title }, + null, + null, + Icon.createWithResource(this, R.drawable.ic_google_logo) + ) + } + + private fun createGoogleIdIntent( + params: GoogleIdRequestParams, + callingPackage: String, + accountName: String? + ): Intent = Intent(this, SignInProxyActivity::class.java).apply { + accountName?.let { putExtra(GOOGLE_ID_SIWG_ACCOUNT_NAME, it) } + putExtra(GOOGLE_ID_SIWG_SERVER_CLIENT_ID, params.serverClientId ?: "") + putExtra(GOOGLE_ID_SIWG_NONCE, params.nonce) + putExtra(GOOGLE_ID_SIWG_CALLER_PACKAGE, callingPackage) + } + + override fun onBeginCreateCredentialRequest( + request: BeginCreateCredentialRequest, + cancellationSignal: CancellationSignal, + callback: OutcomeReceiver + ) { + // Google ID Service only handles GET (sign-in), not CREATE + callback.onResult(BeginCreateCredentialResponse.Builder().build()) + } + + override fun onClearCredentialStateRequest( + request: ProviderClearCredentialStateRequest, + cancellationSignal: CancellationSignal, + callback: OutcomeReceiver + ) { + callback.onResult(null) + } +} + +const val GOOGLE_ID_SUBTYPE = "com.google.android.libraries.identity.googleid.BUNDLE_KEY_GOOGLE_ID_TOKEN_SUBTYPE" +const val GOOGLE_ID_TYPE_SIWG = "com.google.android.libraries.identity.googleid.TYPE_GOOGLE_ID_TOKEN_SIWG_CREDENTIAL" +const val GOOGLE_ID_SERVER_CLIENT_ID = "com.google.android.libraries.identity.googleid.BUNDLE_KEY_SERVER_CLIENT_ID" +const val GOOGLE_ID_NONCE = "com.google.android.libraries.identity.googleid.BUNDLE_KEY_NONCE" +const val GOOGLE_ID_LINKED_SERVICE_ID = "com.google.android.libraries.identity.googleid.BUNDLE_KEY_LINKED_SERVICE_ID" +const val GOOGLE_ID_REQUEST_VERIFIED_PHONE = "com.google.android.libraries.identity.googleid.BUNDLE_KEY_REQUEST_VERIFIED_PHONE_NUMBER" +const val GOOGLE_ID_SIWG_CALLER_PACKAGE = "com.google.android.libraries.identity.googleid.siwg.BUNDLE_KEY_CALLER_PACKAGE" +const val GOOGLE_ID_SIWG_ACCOUNT_NAME = "com.google.android.libraries.identity.googleid.siwg.BUNDLE_KEY_ACCOUNT_NAME" +const val GOOGLE_ID_SIWG_SERVER_CLIENT_ID = "com.google.android.libraries.identity.googleid.siwg.BUNDLE_KEY_SERVER_CLIENT_ID" +const val GOOGLE_ID_SIWG_NONCE = "com.google.android.libraries.identity.googleid.siwg.BUNDLE_KEY_NONCE" +const val GOOGLE_ID_SIWG_HOSTED_DOMAIN = "com.google.android.libraries.identity.googleid.siwg.BUNDLE_KEY_HOSTED_DOMAIN_FILTER" +const val GOOGLE_ID_ANDROIDX_AUTO_SELECT = "androidx.credentials.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED" +const val GOOGLE_ID_BUNDLE_KEY_ID = "com.google.android.libraries.identity.googleid.BUNDLE_KEY_ID" +const val GOOGLE_ID_BUNDLE_KEY_ID_TOKEN = "com.google.android.libraries.identity.googleid.BUNDLE_KEY_ID_TOKEN" +const val GOOGLE_ID_BUNDLE_KEY_DISPLAY_NAME = "com.google.android.libraries.identity.googleid.BUNDLE_KEY_DISPLAY_NAME" +const val GOOGLE_ID_BUNDLE_KEY_PROFILE_PICTURE_URI = "com.google.android.libraries.identity.googleid.BUNDLE_KEY_PROFILE_PICTURE_URI" + +// Credential types +const val TYPE_GOOGLE_ID_TOKEN_CREDENTIAL = "com.google.android.libraries.identity.googleid.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL" + +data class GoogleIdRequestParams( + val isSignInWithGoogle: Boolean, + val serverClientId: String?, + val nonce: String?, + val hostedDomainFilter: String?, + val isAutoSelectAllowed: Boolean = false, + val filterByAuthorized: Boolean = false, + val linkedServiceId: String?, + val requestVerifiedPhoneNumber: Boolean = false +) { + companion object { + fun Bundle.toGoogleIdRequestParams(): GoogleIdRequestParams { + val isSignInWithGoogle = getString(GOOGLE_ID_SUBTYPE) == GOOGLE_ID_TYPE_SIWG + return GoogleIdRequestParams( + isSignInWithGoogle = isSignInWithGoogle, + serverClientId = getString(if (isSignInWithGoogle) GOOGLE_ID_SIWG_SERVER_CLIENT_ID else GOOGLE_ID_SERVER_CLIENT_ID), + nonce = getString(if (isSignInWithGoogle) GOOGLE_ID_SIWG_NONCE else GOOGLE_ID_NONCE), + hostedDomainFilter = getString(GOOGLE_ID_SIWG_HOSTED_DOMAIN), + isAutoSelectAllowed = getBoolean(GOOGLE_ID_ANDROIDX_AUTO_SELECT, false), + linkedServiceId = getString(GOOGLE_ID_LINKED_SERVICE_ID), + requestVerifiedPhoneNumber = getBoolean(GOOGLE_ID_REQUEST_VERIFIED_PHONE, false) + ) + } + } +} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/JsonExtensions.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/JsonExtensions.kt new file mode 100644 index 0000000000..2e3b0148c3 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/JsonExtensions.kt @@ -0,0 +1,257 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.auth.credentials.provider + +import android.util.Base64 +import com.google.android.gms.fido.common.Transport +import com.google.android.gms.fido.fido2.api.common.Attachment +import com.google.android.gms.fido.fido2.api.common.AttestationConveyancePreference +import com.google.android.gms.fido.fido2.api.common.AuthenticationExtensions +import com.google.android.gms.fido.fido2.api.common.AuthenticationExtensionsClientOutputs +import com.google.android.gms.fido.fido2.api.common.AuthenticationExtensionsCredPropsOutputs +import com.google.android.gms.fido.fido2.api.common.AuthenticationExtensionsPrfOutputs +import com.google.android.gms.fido.fido2.api.common.AuthenticatorAssertionResponse +import com.google.android.gms.fido.fido2.api.common.AuthenticatorAttestationResponse +import com.google.android.gms.fido.fido2.api.common.AuthenticatorErrorResponse +import com.google.android.gms.fido.fido2.api.common.AuthenticatorSelectionCriteria +import com.google.android.gms.fido.fido2.api.common.FidoAppIdExtension +import com.google.android.gms.fido.fido2.api.common.PublicKeyCredential +import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialCreationOptions +import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialDescriptor +import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialParameters +import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialRequestOptions +import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialRpEntity +import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialUserEntity +import com.google.android.gms.fido.fido2.api.common.ResidentKeyRequirement +import com.google.android.gms.fido.fido2.api.common.TokenBinding +import com.google.android.gms.fido.fido2.api.common.TokenBinding.TokenBindingStatus +import com.google.android.gms.fido.fido2.api.common.UserVerificationRequirement +import com.google.android.gms.fido.fido2.api.common.UvmEntries +import com.google.android.gms.fido.fido2.api.common.UvmEntry +import org.json.JSONArray +import org.json.JSONObject +import org.microg.gms.fido.core.protocol.AttestationObject +import org.microg.gms.fido.core.protocol.AuthenticatorData +import org.microg.gms.fido.core.protocol.CoseKey + +const val BASE64_URL_FLAGS = Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING +fun String.decodeBase64Url(): ByteArray = Base64.decode(this, BASE64_URL_FLAGS) +fun ByteArray.encodeBase64Url(): String = Base64.encodeToString(this, BASE64_URL_FLAGS) + +fun PublicKeyCredential.toJson() = toJsonObject().toString() + +fun PublicKeyCredential.toJsonObject() = JSONObject().apply { + val response = response + val id = id + val rawId = rawId + val clientExtensionResults = clientExtensionResults + + if (id != null) put("id", id) + if (rawId != null && rawId.isNotEmpty()) put("rawId", rawId.encodeBase64Url()) + when (response) { + is AuthenticatorAttestationResponse -> put("response", response.toJsonObject()) + is AuthenticatorAssertionResponse -> put("response", response.toJsonObject()) + is AuthenticatorErrorResponse -> put("error", response.toJsonObject()) + } + if (authenticatorAttachment != null) put("authenticatorAttachment", authenticatorAttachment) + put("clientExtensionResults", clientExtensionResults?.toJsonObject() ?: JSONObject()) + put("type", type) +} + +fun AuthenticatorAttestationResponse.toJsonObject() = JSONObject().apply { + val decodedAttestationObject = AttestationObject.decode(attestationObject) + val decodedAuthenticatorData = AuthenticatorData.decode(decodedAttestationObject.authData) + val publicKey = decodedAuthenticatorData.attestedCredentialData?.publicKey?.let { CoseKey.decode(it) } + ?: throw IllegalArgumentException("Missing publicKey") + + put("clientDataJSON", clientDataJSON.encodeBase64Url()) + put("authenticatorData", decodedAttestationObject.authData.encodeBase64Url()) + put("transports", JSONArray(transports.asList().map { if (it == Transport.HYBRID.toString()) "hybrid" else it })) + put("publicKey", publicKey.asCryptoKey()?.encoded?.encodeBase64Url()) + put("publicKeyAlgorithm", publicKey.algorithm.algoValue.toLong()) + put("attestationObject", attestationObject.encodeBase64Url()) +} + +fun AuthenticatorAssertionResponse.toJsonObject() = JSONObject().apply { + val userHandle = userHandle + + put("clientDataJSON", clientDataJSON.encodeBase64Url()) + put("authenticatorData", authenticatorData.encodeBase64Url()) + put("signature", signature.encodeBase64Url()) + if (userHandle != null) put("userHandle", userHandle.encodeBase64Url()) +} + +fun AuthenticatorErrorResponse.toJsonObject() = JSONObject().apply { + val errorMessage = errorMessage + + put("code", errorCodeAsInt) + if (errorMessage != null) put("message", errorMessage) +} + +fun AuthenticationExtensionsClientOutputs.toJsonObject() = JSONObject().apply { + val uvmEntries = uvmEntries + val credProps = credProps + val prf = prf + val txAuthSimple = txAuthSimple + + if (uvmEntries != null) put("uvm", uvmEntries.toJsonArray()) + if (credProps != null) put("credProps", credProps.toJsonObject()) + if (prf != null) put("prf", prf.toJsonObject()) + if (txAuthSimple != null) put("txAuthSimple", txAuthSimple) +} + +fun UvmEntries.toJsonArray() = JSONArray().apply { + val uvmEntryList = uvmEntryList + if (uvmEntryList != null) { + for (uvmEntry in uvmEntryList) { + put(uvmEntry.toJsonArray()) + } + } +} + +fun UvmEntry.toJsonArray() = JSONArray().apply { + put(userVerificationMethod) + put(keyProtectionType) + put(matcherProtectionType) +} + +fun AuthenticationExtensionsCredPropsOutputs.toJsonObject() = JSONObject().apply { + put("rk", isDiscoverableCredential) +} + +fun AuthenticationExtensionsPrfOutputs.toJsonObject() = JSONObject().apply { + val first = first + val second = second + + if (isEnabled) put("enabled", true) + if (first != null) put("first", first.encodeBase64Url()) + if (second != null) put("second", second.encodeBase64Url()) +} + +fun JSONObject.parsePublicKeyCredentialRequestOptions(): PublicKeyCredentialRequestOptions { + val builder = PublicKeyCredentialRequestOptions.Builder() + builder.setChallenge(getString("challenge").decodeBase64Url()) + if (has("timeout")) { + builder.setTimeoutSeconds(getDouble("timeout") / 1000.0) + } else if (has("timeoutSeconds")) { + builder.setTimeoutSeconds(getDouble("timeoutSeconds")) + } + builder.setRpId(getString("rpId")) + + val allowCredentials = when { + has("allowList") -> getJSONArray("allowList") + has("allowCredentials") -> getJSONArray("allowCredentials") + else -> null + } + if (allowCredentials != null) { + val allowList = arrayListOf() + for (i in 0.. TokenBinding.SUPPORTED + TokenBindingStatus.NOT_SUPPORTED -> TokenBinding.NOT_SUPPORTED + TokenBindingStatus.PRESENT -> TokenBinding(getString("id")) +} + +fun JSONObject.parseAuthenticationExtensions(): AuthenticationExtensions { + val builder = AuthenticationExtensions.Builder() + if (has("fidoAppIdExtension")) builder.setFido2Extension(FidoAppIdExtension(getJSONObject("fidoAppIdExtension").getString("appId"))) + if (has("appid")) builder.setFido2Extension(FidoAppIdExtension(getString("appId"))) + // TODO: Add support for other extensions + return builder.build() +} + +fun JSONObject.parsePublicKeyCredentialDescriptor() = PublicKeyCredentialDescriptor( + getString("type"), + getString("id").decodeBase64Url(), + optJSONArray("transports")?.let { Transport.parseTransports(it) } +) + +fun JSONObject.parsePublicKeyCredentialCreationOptions(): PublicKeyCredentialCreationOptions { + val builder = PublicKeyCredentialCreationOptions.Builder() + builder.setRp(getJSONObject("rp").parsePublicKeyCredentialRpEntity()) + builder.setUser(getJSONObject("user").parsePublicKeyCredentialUserEntity()) + builder.setChallenge(getString("challenge").decodeBase64Url()) + val pubKeyCredParams = getJSONArray("pubKeyCredParams") + val parameters = arrayListOf() + for (i in 0..() + for (i in 0.. + ) { + runCatching { + val credentialEntries = request.beginGetCredentialOptions + .flatMap { option -> + when (option.type) { + TYPE_PUBLIC_KEY_CREDENTIAL -> handlePublicKeyCredentialRequest(option as BeginGetPublicKeyCredentialOption, request) + TYPE_PASSWORD_CREDENTIAL -> emptyList() // TODO: handle password credential request + else -> emptyList().also { + Log.d(TAG, "Unsupported credential type: ${option.type}") + } + } + } + + callback.onResult(BeginGetCredentialResponse.Builder() + .setCredentialEntries(credentialEntries) + .build() + .also { Log.d(TAG, "Returning ${credentialEntries.size} credential entries") }) + + }.onFailure { e -> + Log.e(TAG, "Error in onBeginGetCredential", e) + callback.onError(GetCredentialUnknownException(e.message)) + } + } + + private fun handlePublicKeyCredentialRequest( + option: BeginGetPublicKeyCredentialOption, + request: BeginGetCredentialRequest + ): List = runCatching { + val options = JSONObject(option.requestJson).parsePublicKeyCredentialRequestOptions() + + var entries = Database(this).getKnownRegistrationInfo(options.rpId) + .filter { it.transport == Transport.SCREEN_LOCK } // TODO: Also show known remote credentials? + .filter { info -> options.allowList.isNullOrEmpty() || options.allowList!!.any { it.id.encodeBase64Url() == info.credential } } + .also { Log.d(TAG, "Found ${it.size} credentials for rpId: ${options.rpId}") } + .mapNotNull { credentialInfo -> + createSignInCredentialEntry(credentialInfo, option, !options.allowList.isNullOrEmpty()) + } + + if (!RemoteService.hasPermissionForRemoteEntry(this)) { + entries += CustomCredentialEntry( + context = this, + title = getString(R.string.fido_transport_selection_hybrid), + pendingIntent = getPendingIntent(0), + beginGetCredentialOption = option, + subtitle = getString(com.google.android.gms.R.string.credentials_service_remote_custom_subtitle), + lastUsedTime = Instant.ofEpochMilli(0) + ) + } + + entries + }.getOrElse { e -> + Log.e(TAG, "Error handling public key credential request", e) + emptyList() + } + + private fun createSignInCredentialEntry( + credentialInfo: CredentialUserInfo, + option: BeginGetPublicKeyCredentialOption, + isAutoSelectAllowed: Boolean = false + ): CredentialEntry? = runCatching { + val user = PublicKeyCredentialUserEntity.parseJson(credentialInfo.userJson) + + val pendingIntent = getPendingIntent(credentialInfo.credential.hashCode(), credentialInfo.credential) + + PublicKeyCredentialEntry( + context = this, + username = user.name, + pendingIntent = pendingIntent, + beginGetPublicKeyCredentialOption = option, + displayName = user.displayName, + lastUsedTime = Instant.ofEpochMilli(credentialInfo.timestamp), + isAutoSelectAllowed = isAutoSelectAllowed + ) + }.getOrElse { e -> + Log.e(TAG, "Error parsing credential user info", e) + null + } + + override fun onBeginCreateCredentialRequest( + request: BeginCreateCredentialRequest, + cancellationSignal: CancellationSignal, + callback: OutcomeReceiver + ) { + runCatching { + when (request.type) { + TYPE_PUBLIC_KEY_CREDENTIAL -> handleCreatePublicKeyCredential(request as BeginCreatePublicKeyCredentialRequest, callback) + TYPE_PASSWORD_CREDENTIAL -> error("Password credential creation not supported") + else -> callback.onError(CreateCredentialUnknownException()) + .also { Log.w(TAG, "Unsupported credential type: ${request.type}") } + } + }.onFailure { e -> + Log.e(TAG, "Error in onBeginCreateCredential", e) + callback.onError(CreateCredentialUnknownException(e.message)) + } + } + + @SuppressLint("MutableImplicitPendingIntent") + private fun handleCreatePublicKeyCredential( + request: BeginCreatePublicKeyCredentialRequest, + callback: OutcomeReceiver + ) = runCatching { + val options = JSONObject(request.requestJson).parsePublicKeyCredentialCreationOptions() + + val pendingIntent = getPendingIntent(request.requestJson.hashCode()) + + callback.onResult(BeginCreateCredentialResponse.Builder() + .addCreateEntry(CreateEntry(options.user.name, pendingIntent)) + .build() + .also { Log.d(TAG, "Returning create credential response for passkey") }) + + }.onFailure { e -> + Log.e(TAG, "Error creating public key credential entry", e) + callback.onError(CreateCredentialUnknownException(e.message)) + } + + override fun onClearCredentialStateRequest( + request: ProviderClearCredentialStateRequest, + cancellationSignal: CancellationSignal, + callback: OutcomeReceiver + ) { + callback.onResult(null) + } + + private fun getPendingIntent(requestCode: Int, credentialIdString: String? = null) = PendingIntent.getActivity( + this, + requestCode, + Intent(this, PublicKeyProxyActivity::class.java).apply { + if (credentialIdString != null) putExtra(KEY_CREDENTIAL_ID, credentialIdString) + }, + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) +} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/PublicKeyProxyActivity.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/PublicKeyProxyActivity.kt new file mode 100644 index 0000000000..c2d4076990 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/PublicKeyProxyActivity.kt @@ -0,0 +1,155 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.auth.credentials.provider + +import android.content.Intent +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.net.toUri +import androidx.credentials.CreatePublicKeyCredentialRequest +import androidx.credentials.CreatePublicKeyCredentialResponse +import androidx.credentials.GetCredentialResponse +import androidx.credentials.GetPublicKeyCredentialOption +import androidx.credentials.provider.ProviderCreateCredentialRequest +import androidx.credentials.provider.ProviderGetCredentialRequest +import com.google.android.gms.fido.Fido.FIDO2_KEY_CREDENTIAL_EXTRA +import com.google.android.gms.fido.fido2.api.common.* +import org.json.JSONObject +import org.microg.gms.common.GmsService +import org.microg.gms.fido.core.ui.ACTION_FIDO_AUTHENTICATE +import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.KEY_CALLER +import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.KEY_CREDENTIAL_ID +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 +import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.SOURCE_APP +import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.SOURCE_BROWSER +import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.TYPE_REGISTER +import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.TYPE_SIGN + +private const val TAG = "PublicKeyProxyActivity" +private const val REQUEST_CODE_FIDO = 1001 + +fun String?.isHttpsUrl(): Boolean = this?.startsWith("https://") == true + +@RequiresApi(34) +class PublicKeyProxyActivity : CredentialProviderActivity() { + + override fun onProviderGetCredentialRequest(request: ProviderGetCredentialRequest) { + val option = request.credentialOptions.firstOrNull() as? GetPublicKeyCredentialOption ?: throw IllegalArgumentException() + + Log.d(TAG, "get request json: ${option.requestJson}") + + val isBrowserRequest = request.callingAppInfo.origin.isHttpsUrl() + + val options = JSONObject(option.requestJson).parsePublicKeyCredentialRequestOptions() + val credentialIdString = intent.getStringExtra(KEY_CREDENTIAL_ID) + + val (optionsBytes, source) = buildRequestOptions(options, isBrowserRequest, request.callingAppInfo.origin, option.clientDataHash) + val fidoIntent = createFidoIntent(source, optionsBytes, request.callingAppInfo.packageName, TYPE_SIGN, credentialIdString) + startActivityForResult(fidoIntent, REQUEST_CODE_FIDO) + } + + fun buildRequestOptions( + baseOptions: PublicKeyCredentialRequestOptions, isBrowserRequest: Boolean, origin: String?, clientDataHash: ByteArray? + ): Pair = if (isBrowserRequest && origin != null) { + BrowserPublicKeyCredentialRequestOptions.Builder().setPublicKeyCredentialRequestOptions(baseOptions).setOrigin(origin.toUri()).apply { clientDataHash?.let(::setClientDataHash) }.build() + .serializeToBytes() to SOURCE_BROWSER + } else { + baseOptions.serializeToBytes() to SOURCE_APP + } + + override fun onProviderCreateCredentialRequest(request: ProviderCreateCredentialRequest) { + val callingPackage = request.callingAppInfo.packageName + val origin = request.callingAppInfo.origin + val isBrowserRequest = origin.isHttpsUrl() + val publicKeyRequest = request.callingRequest as CreatePublicKeyCredentialRequest + + Log.d(TAG, "Creating passkey for: $callingPackage, browser=$isBrowserRequest") + + val options = JSONObject(publicKeyRequest.requestJson).parsePublicKeyCredentialCreationOptions() + + Log.d(TAG, "handlePasskeyCreate: options: $options") + + val (optionsBytes, source) = buildCreationOptions(options, isBrowserRequest, origin, publicKeyRequest.clientDataHash) + val fidoIntent = createFidoIntent(source, optionsBytes, callingPackage, TYPE_REGISTER) + + startActivityForResult(fidoIntent, REQUEST_CODE_FIDO) + Log.d(TAG, "Launched FIDO authenticator by PasskeyCreate") + } + + fun buildCreationOptions( + baseOptions: PublicKeyCredentialCreationOptions, isBrowserRequest: Boolean, origin: String?, clientDataHash: ByteArray? + ): Pair = if (isBrowserRequest && origin != null) { + BrowserPublicKeyCredentialCreationOptions.Builder().setPublicKeyCredentialCreationOptions(baseOptions).setOrigin(origin.toUri()).apply { clientDataHash?.let(::setClientDataHash) }.build() + .serializeToBytes() to SOURCE_BROWSER + } else { + baseOptions.serializeToBytes() to SOURCE_APP + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + if (requestCode != REQUEST_CODE_FIDO) return + if (resultCode != RESULT_OK || data == null) { + Log.w(TAG, "FIDO activity canceled or failed") + return finishWithException("Sign in canceled") + } + + runCatching { + val credentialBytes = data.getByteArrayExtra(FIDO2_KEY_CREDENTIAL_EXTRA) + ?: return@runCatching finishWithException("No credential data in FIDO result") + + val publicKeyCredential = PublicKeyCredential.deserializeFromBytes(credentialBytes) + + (publicKeyCredential.response as? AuthenticatorErrorResponse)?.let { error -> + Log.e(TAG, "FIDO error: ${error.errorMessage}") + return@runCatching finishWithException(error.errorMessage) + } + + handleFidoSuccess(publicKeyCredential) + }.onFailure { e -> + Log.e(TAG, "Error processing FIDO result", e) + finishWithException(e.localizedMessage) + } + } + + fun createFidoIntent( + source: String, optionsBytes: ByteArray, callingPackage: String, type: String, credentialIdString: String? = null + ): Intent = Intent(ACTION_FIDO_AUTHENTICATE).apply { + `package` = packageName + putExtra(KEY_SERVICE, GmsService.FIDO2_API.SERVICE_ID) + putExtra(KEY_SOURCE, source) + putExtra(KEY_TYPE, type) + putExtra(KEY_OPTIONS, optionsBytes) + putExtra(KEY_CALLER, callingPackage) + credentialIdString?.let { putExtra(KEY_CREDENTIAL_ID, it) } + } + + + private fun handleFidoSuccess(publicKeyCredential: PublicKeyCredential) = runCatching { + when (val response = publicKeyCredential.response) { + is AuthenticatorAttestationResponse -> { + val responseJson = publicKeyCredential.toJson() + Log.d(TAG, "Passkey created successfully: $responseJson") + finishWithSuccess(CreatePublicKeyCredentialResponse(responseJson)) + } + is AuthenticatorAssertionResponse -> { + val responseJson = publicKeyCredential.toJson() + Log.d(TAG, "Passkey authentication successful: $responseJson") + finishWithSuccess(GetCredentialResponse(androidx.credentials.PublicKeyCredential(responseJson))) + } + else -> { + Log.e(TAG, "Unknown response type: ${response.javaClass.simpleName}") + finishWithException() + } + } + }.onFailure { e -> + Log.e(TAG, "Error handling FIDO success", e) + finishWithException(e.localizedMessage) + } +} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/RemoteService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/RemoteService.kt new file mode 100644 index 0000000000..671ade3fee --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/RemoteService.kt @@ -0,0 +1,133 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.auth.credentials.provider + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.drawable.Icon +import android.os.CancellationSignal +import android.os.OutcomeReceiver +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.credentials.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL +import androidx.credentials.exceptions.ClearCredentialException +import androidx.credentials.exceptions.CreateCredentialException +import androidx.credentials.exceptions.CreateCredentialNoCreateOptionException +import androidx.credentials.exceptions.CreateCredentialUnknownException +import androidx.credentials.exceptions.GetCredentialException +import androidx.credentials.exceptions.GetCredentialUnknownException +import androidx.credentials.exceptions.NoCredentialException +import androidx.credentials.provider.BeginCreateCredentialRequest +import androidx.credentials.provider.BeginCreateCredentialResponse +import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest +import androidx.credentials.provider.BeginGetCredentialRequest +import androidx.credentials.provider.BeginGetCredentialResponse +import androidx.credentials.provider.BeginGetPublicKeyCredentialOption +import androidx.credentials.provider.CreateEntry +import androidx.credentials.provider.CredentialProviderService +import androidx.credentials.provider.CustomCredentialEntry +import androidx.credentials.provider.ProviderClearCredentialStateRequest +import androidx.credentials.provider.PublicKeyCredentialEntry +import androidx.credentials.provider.RemoteEntry +import com.google.android.gms.fido.fido2.api.common.Attachment +import com.squareup.wire.Instant +import org.json.JSONObject + +private const val TAG = "RemoteCredentialService" + +/** + * RemoteService - Provides cross-device passkey functionality + * + * RemoteChimeraService corresponding to GMS, realizing cross-device authentication function: + * - Connect other devices via QR code or Bluetooth + * - Authenticate with a passkey on another device + * - Create passkeys on other devices + */ +@RequiresApi(34) +open class RemoteService : CredentialProviderService() { + + override fun onBeginGetCredentialRequest( + request: BeginGetCredentialRequest, + cancellationSignal: CancellationSignal, + callback: OutcomeReceiver + ) { + runCatching { + val option = request.beginGetCredentialOptions.firstOrNull { it.type == TYPE_PUBLIC_KEY_CREDENTIAL } + as? BeginGetPublicKeyCredentialOption? + ?: return callback.onError(NoCredentialException()) + + // TODO: Don't offer when allowedCredentials set and all of them local + + val responseBuilder = BeginGetCredentialResponse.Builder() + val pendingIntent = createPendingIntent() + + responseBuilder.setRemoteEntry(RemoteEntry(pendingIntent)) + + callback.onResult(responseBuilder.build()) + }.onFailure { e -> + Log.e(TAG, "Error in onBeginGetCredential", e) + callback.onError(GetCredentialUnknownException(e.message)) + } + } + + override fun onBeginCreateCredentialRequest( + request: BeginCreateCredentialRequest, + cancellationSignal: CancellationSignal, + callback: OutcomeReceiver + ) { + runCatching { + if (request.type != TYPE_PUBLIC_KEY_CREDENTIAL) { + return callback.onError(CreateCredentialNoCreateOptionException()) + .also { Log.d(TAG, "Not a PublicKeyCredential create request") } + } + + val publicKeyRequest = request as BeginCreatePublicKeyCredentialRequest + val options = JSONObject(publicKeyRequest.requestJson).parsePublicKeyCredentialCreationOptions() + + if (options.authenticatorSelection?.attachment == Attachment.PLATFORM) { + return callback.onError(CreateCredentialNoCreateOptionException()) + .also { Log.d(TAG, "Platform attachment required, remote not supported") } + } + + val responseBuilder = BeginCreateCredentialResponse.Builder() + val pendingIntent = createPendingIntent() + + responseBuilder.setRemoteEntry(RemoteEntry(pendingIntent)) + + callback.onResult(responseBuilder.build()) + + }.onFailure { e -> + Log.e(TAG, "Error in onBeginCreateCredential", e) + callback.onError(CreateCredentialUnknownException(e.message)) + } + } + + override fun onClearCredentialStateRequest( + request: ProviderClearCredentialStateRequest, + cancellationSignal: CancellationSignal, + callback: OutcomeReceiver + ) { + Log.d(TAG, "onClearCredentialState: No-op for remote service") + callback.onResult(null) + } + + private fun createPendingIntent(): PendingIntent { + return PendingIntent.getActivity( + this, + TAG.hashCode(), + Intent(this, PublicKeyProxyActivity::class.java), + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + } + + companion object { + fun hasPermissionForRemoteEntry(context: Context): Boolean { + return context.checkSelfPermission("android.permission.PROVIDE_DEFAULT_ENABLED_CREDENTIAL_SERVICE") == PackageManager.PERMISSION_GRANTED + } + } +} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/SignInProxyActivity.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/SignInProxyActivity.kt new file mode 100644 index 0000000000..16ff2da1f4 --- /dev/null +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/SignInProxyActivity.kt @@ -0,0 +1,113 @@ +/* + * SPDX-FileCopyrightText: 2026 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.auth.credentials.provider + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.credentials.CustomCredential +import androidx.credentials.GetCredentialResponse +import androidx.credentials.exceptions.GetCredentialCancellationException +import androidx.credentials.provider.PendingIntentHandler +import androidx.credentials.provider.ProviderCreateCredentialRequest +import androidx.credentials.provider.ProviderGetCredentialRequest +import com.google.android.gms.auth.api.identity.GetSignInIntentRequest +import com.google.android.gms.auth.api.identity.SignInCredential +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.common.internal.safeparcel.SafeParcelableSerializer +import org.microg.gms.auth.AuthConstants +import org.microg.gms.auth.signin.ACTION_ASSISTED_SIGN_IN +import org.microg.gms.auth.signin.CLIENT_PACKAGE_NAME +import org.microg.gms.auth.signin.GET_SIGN_IN_INTENT_REQUEST +import org.microg.gms.auth.signin.GOOGLE_SIGN_IN_OPTIONS + +private const val TAG = "SignInProxyActivity" +private const val REQUEST_CODE_SIGN_IN = 100 + +@RequiresApi(34) +class SignInProxyActivity : CredentialProviderActivity() { + + override fun onProviderGetCredentialRequest(request: ProviderGetCredentialRequest) { + val bundle = Bundle().apply { + val signInRequest = GetSignInIntentRequest.builder() + .setServerClientId(intent.getStringExtra(GOOGLE_ID_SIWG_SERVER_CLIENT_ID) ?: "") + .apply { + intent.getStringExtra(GOOGLE_ID_SIWG_NONCE)?.let { setNonce(it) } + } + .build() + + val googleSignInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestEmail() + .requestIdToken(intent.getStringExtra(GOOGLE_ID_SIWG_SERVER_CLIENT_ID) ?: "") + .apply { intent.getStringExtra(GOOGLE_ID_SIWG_ACCOUNT_NAME)?.let { setAccountName(it) } } + .build() + + putByteArray(GET_SIGN_IN_INTENT_REQUEST, SafeParcelableSerializer.serializeToBytes(signInRequest)) + putByteArray(GOOGLE_SIGN_IN_OPTIONS, SafeParcelableSerializer.serializeToBytes(googleSignInOptions)) + putString(CLIENT_PACKAGE_NAME, intent.getStringExtra(GOOGLE_ID_SIWG_CALLER_PACKAGE)) + } + startActivityForResult( + Intent(ACTION_ASSISTED_SIGN_IN).apply { + `package` = packageName + putExtras(bundle) + }, + REQUEST_CODE_SIGN_IN + ) + } + + override fun onProviderCreateCredentialRequest(request: ProviderCreateCredentialRequest) { + finishWithException("Unsupported create credential request") + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode != REQUEST_CODE_SIGN_IN) return + if (resultCode != RESULT_OK || data == null) { + Log.w(TAG, "SignIn activity canceled or failed") + return finishWithException("Sign in canceled", getExceptionCreator = ::GetCredentialCancellationException) + } + Log.d(TAG, "onActivityResult - requestCode: $requestCode, resultCode: $resultCode") + handleSignInSuccess(data) + } + + private fun handleSignInSuccess(data: Intent?) = runCatching { + Log.d(TAG, "handleSignInSuccess") + + val credentialBytes = data?.getByteArrayExtra(AuthConstants.SIGN_IN_CREDENTIAL) + ?: run { + Log.e(TAG, "No credential data in result") + return@runCatching finishWithException("No credential data") + } + + val signInCredential = SafeParcelableSerializer.deserializeFromBytes( + credentialBytes, + SignInCredential.CREATOR + ) + + Log.d(TAG, "Got SignInCredential: email=${signInCredential.id}, googleIdToken=${signInCredential.googleIdToken?.take(50)}...") + + val credentialData = Bundle().apply { + putString(GOOGLE_ID_BUNDLE_KEY_ID, signInCredential.id) + putString(GOOGLE_ID_BUNDLE_KEY_ID_TOKEN, signInCredential.googleIdToken) + putString(GOOGLE_ID_BUNDLE_KEY_DISPLAY_NAME, signInCredential.displayName) + putString(GOOGLE_ID_BUNDLE_KEY_PROFILE_PICTURE_URI, signInCredential.profilePictureUri?.toString()) + } + + val credential = CustomCredential(TYPE_GOOGLE_ID_TOKEN_CREDENTIAL, credentialData) + val response = GetCredentialResponse(credential) + + val resultIntent = Intent() + PendingIntentHandler.setGetCredentialResponse(resultIntent, response) + setResult(RESULT_OK, resultIntent) + + Log.d(TAG, "Returning credential to Credential Manager") + finish() + }.onFailure { e -> + Log.e(TAG, "Error processing sign-in result", e) + finishWithException(e.message) + } +} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AssistedSignInActivity.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AssistedSignInActivity.kt index 15667eae1a..a4be92ce34 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AssistedSignInActivity.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AssistedSignInActivity.kt @@ -124,7 +124,7 @@ class AssistedSignInActivity : AppCompatActivity() { googleSignInAccount.displayName, googleSignInAccount.givenName, googleSignInAccount.familyName, - null, + googleSignInAccount.photoUrl, null, googleSignInAccount.idToken, null, diff --git a/play-services-core/src/main/res/values/strings.xml b/play-services-core/src/main/res/values/strings.xml index d2b9119405..ed08fecd18 100644 --- a/play-services-core/src/main/res/values/strings.xml +++ b/play-services-core/src/main/res/values/strings.xml @@ -342,6 +342,8 @@ Please set up a password, PIN, or pattern lock screen." Account abnormality Sign in with Google Add Account + Sign in with Google + Security key, smartphone or tablet You are using the microG Limited Services. Unlike the usual microG Services, this flavor only works with apps using microG libraries, not those on Google Play. This means that most applications will ignore these services. I understand diff --git a/play-services-core/src/main/res/xml/credentials_provider_google_id.xml b/play-services-core/src/main/res/xml/credentials_provider_google_id.xml new file mode 100644 index 0000000000..8144f81860 --- /dev/null +++ b/play-services-core/src/main/res/xml/credentials_provider_google_id.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/play-services-core/src/main/res/xml/credentials_provider_passkey.xml b/play-services-core/src/main/res/xml/credentials_provider_passkey.xml new file mode 100644 index 0000000000..ab5c73cc87 --- /dev/null +++ b/play-services-core/src/main/res/xml/credentials_provider_passkey.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/play-services-core/src/main/res/xml/credentials_provider_remote.xml b/play-services-core/src/main/res/xml/credentials_provider_remote.xml new file mode 100644 index 0000000000..ab5c73cc87 --- /dev/null +++ b/play-services-core/src/main/res/xml/credentials_provider_remote.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/Database.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/Database.kt index 521beb5c1f..7a1133fd86 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/Database.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/Database.kt @@ -36,7 +36,7 @@ class Database(context: Context) : SQLiteOpenHelper(context, "fido.db", null, VE fun getKnownRegistrationInfo(rpId: String) = readableDatabase.use { val cursor = it.query( TABLE_KNOWN_REGISTRATIONS, - arrayOf(COLUMN_CREDENTIAL_ID, COLUMN_REGISTER_USER, COLUMN_TRANSPORT), + arrayOf(COLUMN_CREDENTIAL_ID, COLUMN_REGISTER_USER, COLUMN_TRANSPORT, COLUMN_TIMESTAMP), "$COLUMN_RP_ID=?", arrayOf(rpId), null, @@ -49,8 +49,9 @@ class Database(context: Context) : SQLiteOpenHelper(context, "fido.db", null, VE val credentialId = c.getString(0) val userJson = c.getStringOrNull(1) ?: continue val transport = c.getStringOrNull(2) ?: continue + val timestamp = c.getLongOrNull(3) ?: 0 Log.d(TAG, "getKnownRegistrationInfo: credential: $credentialId user: $userJson transport: $transport") - result.add(CredentialUserInfo(credentialId, userJson, Transport.valueOf(transport))) + result.add(CredentialUserInfo(credentialId, userJson, Transport.valueOf(transport), timestamp)) } } result @@ -75,11 +76,7 @@ class Database(context: Context) : SQLiteOpenHelper(context, "fido.db", null, VE } } - val updated = if (userJson == null) { - it.update(TABLE_KNOWN_REGISTRATIONS, values, "$COLUMN_RP_ID = ? AND $COLUMN_CREDENTIAL_ID = ?", arrayOf(rpId, credentialId)) - } else { - it.update(TABLE_KNOWN_REGISTRATIONS, values, "$COLUMN_RP_ID = ? AND $COLUMN_REGISTER_USER = ?", arrayOf(rpId, userJson)) - } + val updated = it.update(TABLE_KNOWN_REGISTRATIONS, values, "$COLUMN_RP_ID = ? AND $COLUMN_CREDENTIAL_ID = ?", arrayOf(rpId, credentialId)) if (updated == 0) { val insertValues = ContentValues().apply { @@ -138,4 +135,4 @@ fun SQLiteDatabase.count(table: String, selection: String? = null, vararg select } finally { it.close() } -} \ No newline at end of file +} diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/RequestHandling.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/RequestHandling.kt index e170888378..93ecbd8d3e 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/RequestHandling.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/RequestHandling.kt @@ -31,7 +31,7 @@ class RequestHandlingException(val errorCode: ErrorCode, message: String? = null class MissingPinException(message: String? = null): Exception(message) class WrongPinException(message: String? = null): Exception(message) -data class CredentialUserInfo(val credential: String, val userJson: String, val transport: Transport) +data class CredentialUserInfo(val credential: String, val userJson: String, val transport: Transport, val timestamp: Long) enum class RequestOptionsType { REGISTER, SIGN } val RequestOptions.registerOptions: PublicKeyCredentialCreationOptions diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/protocol/Cbor.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/protocol/Cbor.kt index 645a1cbca5..1322d71370 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/protocol/Cbor.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/protocol/Cbor.kt @@ -64,13 +64,6 @@ fun CBORObject.decodeAsPublicKeyCredentialUserEntity() = PublicKeyCredentialUser get("displayName")?.AsString() ?: "".also { Log.w(TAG, "displayName was not present") } ) -fun CBORObject.decodeAsCoseKey() = CoseKey( - getAlgorithm(get(CoseKey.ALG).AsInt32Value()), - get(CoseKey.X).GetByteString(), - get(CoseKey.Y).GetByteString(), - get(CoseKey.CRV).AsInt32Value() -) - fun getAlgorithm(algorithmInt: Int): Algorithm { return when (algorithmInt) { -65535 -> RSAAlgorithm.RS1 diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/protocol/CoseKey.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/protocol/CoseKey.kt index 0aca9f14b1..61bec1fec4 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/protocol/CoseKey.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/protocol/CoseKey.kt @@ -6,9 +6,18 @@ package org.microg.gms.fido.core.protocol import com.google.android.gms.fido.fido2.api.common.Algorithm +import com.google.android.gms.fido.fido2.api.common.EC2Algorithm +import com.google.android.gms.fido.fido2.api.common.RSAAlgorithm import com.upokecenter.cbor.CBOREncodeOptions import com.upokecenter.cbor.CBORObject import java.math.BigInteger +import java.security.AlgorithmParameters +import java.security.KeyFactory +import java.security.PublicKey +import java.security.spec.ECGenParameterSpec +import java.security.spec.ECParameterSpec +import java.security.spec.ECPoint +import java.security.spec.ECPublicKeySpec class CoseKey( val algorithm: Algorithm, @@ -29,6 +38,30 @@ class CoseKey( set(Y, y.encodeAsCbor()) } + fun asCryptoKey(): PublicKey? { + return when(algorithm) { + is EC2Algorithm -> { + val curveName = when (curveId) { + 1 -> "secp256r1" + 2 -> "secp384r1" + 3 -> "secp521r1" + 4 -> "x25519" + 5 -> "x448" + 6 -> "Ed25519" + 7 -> "Ed448" + else -> return null + } + + val parameters = AlgorithmParameters.getInstance("EC") + parameters.init(ECGenParameterSpec(curveName)) + val parameterSpec = parameters.getParameterSpec(ECParameterSpec::class.java) + val keySpec = ECPublicKeySpec(ECPoint(BigInteger(1, x), BigInteger(1, y)), parameterSpec) + KeyFactory.getInstance("EC").generatePublic(keySpec) + } + else -> null + } + } + companion object { const val KTY = 1 const val ALG = 3 @@ -36,6 +69,15 @@ class CoseKey( const val X = -2 const val Y = -3 + fun decode(bytes: ByteArray): CoseKey = decodeFromCbor(CBORObject.DecodeFromBytes(bytes)) + + fun decodeFromCbor(obj: CBORObject): CoseKey = CoseKey( + getAlgorithm(obj.get(CoseKey.ALG).AsInt32Value()), + obj.get(CoseKey.X).GetByteString(), + obj.get(CoseKey.Y).GetByteString(), + obj.get(CoseKey.CRV).AsInt32Value() + ) + fun BigInteger.toByteArray(size: Int): ByteArray { val res = ByteArray(size) val orig = toByteArray() diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/protocol/msgs/AuthenticatorClientPIN.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/protocol/msgs/AuthenticatorClientPIN.kt index 2738306ff8..f7024981a0 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/protocol/msgs/AuthenticatorClientPIN.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/protocol/msgs/AuthenticatorClientPIN.kt @@ -2,7 +2,6 @@ package org.microg.gms.fido.core.protocol.msgs import com.upokecenter.cbor.CBORObject import org.microg.gms.fido.core.protocol.CoseKey -import org.microg.gms.fido.core.protocol.decodeAsCoseKey import org.microg.gms.fido.core.protocol.encodeAsCbor class AuthenticatorClientPINCommand(request: AuthenticatorClientPINRequest) : @@ -67,7 +66,7 @@ class AuthenticatorClientPINResponse( companion object { fun decodeFromCbor(obj: CBORObject) = AuthenticatorClientPINResponse( - obj.get(0x01)?.decodeAsCoseKey(), + obj.get(0x01)?.let { CoseKey.decodeFromCbor(it) }, obj.get(0x02)?.GetByteString(), obj.get(0x03)?.AsInt32Value() ) diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/TransportHandler.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/TransportHandler.kt index d1b4a664fa..e3a3669782 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/TransportHandler.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/TransportHandler.kt @@ -22,17 +22,11 @@ import org.microg.gms.fido.core.protocol.CoseKey.Companion.toByteArray import org.microg.gms.fido.core.protocol.msgs.* import org.microg.gms.fido.core.transport.nfc.CtapNfcMessageStatusException import org.microg.gms.fido.core.transport.usb.ctaphid.CtapHidMessageStatusException -import java.math.BigInteger import java.nio.charset.StandardCharsets -import java.security.AlgorithmParameters -import java.security.KeyFactory import java.security.KeyPairGenerator import java.security.MessageDigest import java.security.interfaces.ECPublicKey import java.security.spec.ECGenParameterSpec -import java.security.spec.ECParameterSpec -import java.security.spec.ECPoint -import java.security.spec.ECPublicKeySpec import javax.crypto.Cipher import javax.crypto.KeyAgreement import javax.crypto.Mac @@ -45,10 +39,16 @@ 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, user: PublicKeyCredentialUserEntity? = null): AuthenticatorResponseWithUser<*> = + open suspend fun start( + options: RequestOptions, + callerPackage: String, + pinRequested: Boolean = false, + pin: String? = null, + credentialIdString: String? = null + ): AuthenticatorResponseWithUser<*> = throw RequestHandlingException(ErrorCode.NOT_SUPPORTED_ERR) - open fun shouldBeUsedInstantly(options: RequestOptions): Boolean = false + open fun shouldBeUsedInstantly(options: RequestOptions, credential: String? = null): Boolean = false fun invokeStatusChanged(status: String, extras: Bundle? = null) = callback?.onStatusChanged(transport, status, extras) @@ -327,9 +327,6 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor return null; } - val x = sharedSecretResponse.keyAgreement.x - val y = sharedSecretResponse.keyAgreement.y - val curveName = when (sharedSecretResponse.keyAgreement.curveId) { 1 -> "secp256r1" 2 -> "secp384r1" @@ -346,11 +343,7 @@ abstract class TransportHandler(val transport: Transport, val callback: Transpor generator.initialize(ECGenParameterSpec(curveName)) val myKeyPair = generator.generateKeyPair() - val parameters = AlgorithmParameters.getInstance("EC") - parameters.init(ECGenParameterSpec(curveName)) - val parameterSpec = parameters.getParameterSpec(ECParameterSpec::class.java) - val serverKey = KeyFactory.getInstance("EC") - .generatePublic(ECPublicKeySpec(ECPoint(BigInteger(1, x), BigInteger(1, y)), parameterSpec)) + val serverKey = sharedSecretResponse.keyAgreement.asCryptoKey() val keyAgreement = KeyAgreement.getInstance("ECDH") keyAgreement.init(myKeyPair.private) keyAgreement.doPhase(serverKey, true) diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/hybrid/HybridTransportHandler.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/hybrid/HybridTransportHandler.kt index ddafc4019d..4d1e031b9e 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/hybrid/HybridTransportHandler.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/hybrid/HybridTransportHandler.kt @@ -16,7 +16,6 @@ import androidx.core.content.getSystemService import androidx.core.os.bundleOf import com.google.android.gms.fido.fido2.api.common.AuthenticatorAssertionResponse import com.google.android.gms.fido.fido2.api.common.AuthenticatorAttestationResponse -import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialUserEntity import com.google.android.gms.fido.fido2.api.common.RequestOptions import com.google.android.gms.fido.fido2.api.common.UserVerificationRequirement import com.upokecenter.cbor.CBORObject @@ -47,7 +46,7 @@ class HybridTransportHandler(private val context: Context, callback: TransportHa @RequiresPermission(Manifest.permission.BLUETOOTH_SCAN) override suspend fun start( - options: RequestOptions, callerPackage: String, pinRequested: Boolean, pin: String?, user: PublicKeyCredentialUserEntity? + options: RequestOptions, callerPackage: String, pinRequested: Boolean, pin: String?, credentialIdString: String? ): AuthenticatorResponseWithUser<*> { val staticKey = generateEcKeyPair() val hybridClientController = HybridClientController(context, staticKey) diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/nfc/NfcTransportHandler.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/nfc/NfcTransportHandler.kt index dceb7189e7..9d0e367cb9 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/nfc/NfcTransportHandler.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/nfc/NfcTransportHandler.kt @@ -20,8 +20,6 @@ import androidx.core.app.PendingIntentCompat import androidx.core.util.Consumer import com.google.android.gms.fido.fido2.api.common.AuthenticatorAssertionResponse import com.google.android.gms.fido.fido2.api.common.AuthenticatorAttestationResponse -import com.google.android.gms.fido.fido2.api.common.AuthenticatorResponse -import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialUserEntity import com.google.android.gms.fido.fido2.api.common.RequestOptions import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred @@ -105,7 +103,13 @@ class NfcTransportHandler(private val activity: Activity, callback: TransportHan } - override suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean, pin: String?, user: PublicKeyCredentialUserEntity?): AuthenticatorResponseWithUser<*> { + override suspend fun start( + options: RequestOptions, + callerPackage: String, + pinRequested: Boolean, + pin: String?, + credentialIdString: String? + ): AuthenticatorResponseWithUser<*> { val adapter = NfcAdapter.getDefaultAdapter(activity) val newIntentListener = Consumer { if (it?.action != NfcAdapter.ACTION_TECH_DISCOVERED) return@Consumer diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt index 04ba68c66b..ec4bcf8b55 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt @@ -198,18 +198,19 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac suspend fun sign( options: RequestOptions, callerPackage: String, - user: PublicKeyCredentialUserEntity? + credentialIdString: String? ): AuthenticatorResponseWithUser { if (options.type != RequestOptionsType.SIGN) throw RequestHandlingException(ErrorCode.INVALID_STATE_ERR) - if (!options.signOptions.allowList.isNullOrEmpty() && user != null) throw RequestHandlingException(ErrorCode.NOT_ALLOWED_ERR) + if (options.signOptions.allowList.isNullOrEmpty() && credentialIdString == null) throw RequestHandlingException(ErrorCode.NOT_ALLOWED_ERR) val knownRegistrationInfo = database.getKnownRegistrationInfo(options.rpId) .filter { it.transport == Transport.SCREEN_LOCK } .associateBy { runCatching { CredentialId.decodeTypeAndDataByBase64(it.credential) }.getOrNull() } .filterKeys { it != null && it.first == 1.toByte() && store.containsKey(options.rpId, it.second) } .mapKeys { CredentialId(it.key!!.first, it.key!!.second, options.rpId, store.getPublicKey(options.rpId, it.key!!.second)!!) } + val credential = runCatching { credentialIdString?.let { CredentialId.decodeTypeAndDataByBase64(it) } }.getOrNull() val candidates = if (options.signOptions.allowList.isNullOrEmpty()) { knownRegistrationInfo - .filterValues { user == null || PublicKeyCredentialUserEntity.parseJson(it.userJson).id.contentEquals(user.id) } + .filterKeys { credential == null || (credential.first == it.type && credential.second.contentEquals(it.data)) } } else { options.signOptions.allowList.orEmpty() .mapNotNull { runCatching { CredentialId.decodeTypeAndData(it.id) }.getOrNull() } @@ -248,13 +249,19 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac } @RequiresApi(24) - override suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean, pin: String?, user: PublicKeyCredentialUserEntity?): AuthenticatorResponseWithUser<*> = + override suspend fun start( + options: RequestOptions, + callerPackage: String, + pinRequested: Boolean, + pin: String?, + credentialIdString: String? + ): AuthenticatorResponseWithUser<*> = when (options.type) { RequestOptionsType.REGISTER -> register(options, callerPackage) - RequestOptionsType.SIGN -> sign(options, callerPackage, user) + RequestOptionsType.SIGN -> sign(options, callerPackage, credentialIdString) } - override fun shouldBeUsedInstantly(options: RequestOptions): Boolean { + override fun shouldBeUsedInstantly(options: RequestOptions, credential: String?): Boolean { if (options.type != RequestOptionsType.SIGN) return false for (descriptor in options.signOptions.allowList.orEmpty()) { try { @@ -266,6 +273,17 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac // Ignore } } + if (credential != null && + database.getKnownRegistrationTransport(options.rpId, credential) == Transport.SCREEN_LOCK) { + try { + val (type, data) = CredentialId.decodeTypeAndDataByBase64(credential) + if (type == 1.toByte() && store.containsKey(options.rpId, data)) { + return true + } + } catch (e: Exception) { + // Ignore + } + } return false } diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/usb/UsbTransportHandler.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/usb/UsbTransportHandler.kt index f7db2bba45..f023c9bd63 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/usb/UsbTransportHandler.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/usb/UsbTransportHandler.kt @@ -138,7 +138,13 @@ class UsbTransportHandler(private val context: Context, callback: TransportHandl } } - override suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean, pin: String?, user: PublicKeyCredentialUserEntity?): AuthenticatorResponseWithUser<*> { + override suspend fun start( + options: RequestOptions, + callerPackage: String, + pinRequested: Boolean, + pin: String?, + credentialIdString: String? + ): AuthenticatorResponseWithUser<*> { for (device in context.usbManager?.deviceList?.values.orEmpty()) { val iface = getCtapHidInterface(device) ?: continue try { diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivity.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivity.kt index 0d3ce0681f..d3cfdf3567 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivity.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivity.kt @@ -40,6 +40,7 @@ import org.microg.gms.utils.getFirstSignatureDigest import org.microg.gms.utils.toBase64 const val TAG = "FidoUi" +const val ACTION_FIDO_AUTHENTICATE = "org.microg.gms.fido.AUTHENTICATE" class AuthenticatorActivity : AppCompatActivity(), TransportHandlerCallback { val options: RequestOptions? @@ -77,6 +78,7 @@ class AuthenticatorActivity : AppCompatActivity(), TransportHandlerCallback { lateinit var callerPackage: String lateinit var callerSignature: String private lateinit var navHostFragment: NavHostFragment + private var preselectedCredentialId: String? = null private inline fun getTransportHandler(): T? = transportHandlers.filterIsInstance().firstOrNull { it.isSupported } @@ -103,15 +105,18 @@ class AuthenticatorActivity : AppCompatActivity(), TransportHandlerCallback { this.callerPackage = callerPackage this.callerSignature = packageManager.getFirstSignatureDigest(callerPackage, "SHA-256")?.toBase64() ?: return finishWithError(UNKNOWN_ERR, "Could not determine signature of app") + this.preselectedCredentialId = intent.getStringExtra(KEY_CREDENTIAL_ID) - Log.d(TAG, "onCreate caller=$callerPackage options=$options") + Log.d(TAG, "onCreate caller=$callerPackage options=$options preselectedCredentialId=$preselectedCredentialId") val requiresPrivilege = source == SOURCE_BROWSER && !database.isPrivileged(callerPackage, callerSignature) // Check if we can directly open screen lock handling if (!requiresPrivilege) { - val instantTransport = transportHandlers.firstOrNull { it.isSupported && it.shouldBeUsedInstantly(options) } + val instantTransport = transportHandlers.firstOrNull { + it.isSupported && it.shouldBeUsedInstantly(options, preselectedCredentialId) + } if (instantTransport != null && instantTransport.transport in INSTANT_SUPPORTED_TRANSPORTS) { window.setBackgroundDrawable(ColorDrawable(0)) window.statusBarColor = Color.TRANSPARENT @@ -150,9 +155,11 @@ class AuthenticatorActivity : AppCompatActivity(), TransportHandlerCallback { // Check if we can directly open screen lock handling if (!requiresPrivilege && allowInstant && !noLocalUserForSignInstantBlock) { - val instantTransport = transportHandlers.firstOrNull { it.isSupported && it.shouldBeUsedInstantly(options) } + val instantTransport = transportHandlers.firstOrNull { + it.isSupported && it.shouldBeUsedInstantly(options, preselectedCredentialId) + } if (instantTransport != null && instantTransport.transport in INSTANT_SUPPORTED_TRANSPORTS) { - startTransportHandling(instantTransport.transport, true) + startTransportHandling(instantTransport.transport, true, credentialIdString = preselectedCredentialId) return } } @@ -168,6 +175,11 @@ class AuthenticatorActivity : AppCompatActivity(), TransportHandlerCallback { val knownRegistrationTransports = mutableSetOf() val allowedTransports = mutableSetOf() if (options.type == RequestOptionsType.SIGN) { + if (preselectedCredentialId != null) { + val knownTransport = database.getKnownRegistrationTransport(options.rpId, preselectedCredentialId!!) + if (knownTransport != null && knownTransport in IMPLEMENTED_TRANSPORTS) + knownRegistrationTransports.add(knownTransport) + } for (descriptor in options.signOptions.allowList.orEmpty()) { val knownTransport = database.getKnownRegistrationTransport(options.rpId, descriptor.id.toBase64(Base64.URL_SAFE, Base64.NO_WRAP, Base64.NO_PADDING)) if (knownTransport != null && knownTransport in IMPLEMENTED_TRANSPORTS) @@ -269,7 +281,7 @@ class AuthenticatorActivity : AppCompatActivity(), TransportHandlerCallback { .setRawId(rawId ?: ByteArray(0).also { Log.w(TAG, "rawId was null") }) .setId(id ?: "".also { Log.w(TAG, "id was null") }) .setAuthenticatorAttachment(if (transport == SCREEN_LOCK) "platform" else "cross-platform") - .setAuthenticationExtensionsClientOutputs(clientExtResults) + .setAuthenticationExtensionsClientOutputs(if (response is AuthenticatorAttestationResponse) clientExtResults else null) .build() finishWithCredential(pkc, user) @@ -300,10 +312,11 @@ class AuthenticatorActivity : AppCompatActivity(), TransportHandlerCallback { } @RequiresApi(24) - fun startTransportHandling(transport: Transport, instant: Boolean = false, pinRequested: Boolean = false, authenticatorPin: String? = null, user: PublicKeyCredentialUserEntity? = null): Job = lifecycleScope.launchWhenResumed { + fun startTransportHandling(transport: Transport, instant: Boolean = false, pinRequested: Boolean = false, authenticatorPin: String? = null, credentialIdString: String? = null): Job = lifecycleScope.launchWhenResumed { val options = options ?: return@launchWhenResumed try { - val result = getTransportHandler(transport)!!.start(options, callerPackage, pinRequested, authenticatorPin, user) + val result = getTransportHandler(transport)!! + .start(options, callerPackage, pinRequested, authenticatorPin, credentialIdString) finishWithSuccessResponse(result.response, transport, result.user) } catch (e: SecurityException) { Log.w(TAG, e) @@ -370,6 +383,7 @@ class AuthenticatorActivity : AppCompatActivity(), TransportHandlerCallback { const val KEY_TYPE = "type" const val KEY_OPTIONS = "options" const val KEY_USER_JSON = "userInfo" + const val KEY_CREDENTIAL_ID = "credential" val REQUIRED_EXTRAS = setOf(KEY_SOURCE, KEY_TYPE, KEY_OPTIONS) const val SOURCE_BROWSER = "browser" diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivityFragment.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivityFragment.kt index 4b00a17a88..70679b1c84 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivityFragment.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivityFragment.kt @@ -6,19 +6,13 @@ package org.microg.gms.fido.core.ui import android.annotation.TargetApi -import android.content.Intent import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.lifecycle.lifecycleScope -import com.google.android.gms.fido.fido2.api.common.ErrorCode -import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialUserEntity import com.google.android.gms.fido.fido2.api.common.RequestOptions -import org.microg.gms.fido.core.* import org.microg.gms.fido.core.transport.Transport @TargetApi(24) @@ -31,8 +25,8 @@ abstract class AuthenticatorActivityFragment : Fragment() { val options: RequestOptions? get() = authenticatorActivity?.options - fun startTransportHandling(transport: Transport, user: PublicKeyCredentialUserEntity? = null) = - authenticatorActivity?.startTransportHandling(transport, pinRequested = pinViewModel.pinRequest, authenticatorPin = pinViewModel.pin, user = user) + fun startTransportHandling(transport: Transport, credentialIdString: String? = null) = + authenticatorActivity?.startTransportHandling(transport, pinRequested = pinViewModel.pinRequest, authenticatorPin = pinViewModel.pin, credentialIdString = credentialIdString) fun shouldStartTransportInstantly(transport: Transport) = authenticatorActivity?.shouldStartTransportInstantly(transport) == true abstract override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/SignInSelectionFragment.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/SignInSelectionFragment.kt index a22dd0fd24..0d82758b61 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/SignInSelectionFragment.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/SignInSelectionFragment.kt @@ -24,7 +24,6 @@ import org.microg.gms.fido.core.databinding.FidoSignInSelectionFragmentBinding import org.microg.gms.fido.core.rpId import org.microg.gms.fido.core.transport.Transport import androidx.core.view.isGone -import androidx.core.view.isVisible import org.microg.gms.fido.core.signOptions class SignInSelectionFragment : AuthenticatorActivityFragment() { @@ -58,8 +57,8 @@ class SignInSelectionFragment : AuthenticatorActivityFragment() { } } else { binding.root.isGone = false - binding.signInKeyRecycler.adapter = SignInKeyAdapter(knownRegistrationInfo) { userJson, transport -> - startTransportHandling(transport, PublicKeyCredentialUserEntity.parseJson(userJson)) + binding.signInKeyRecycler.adapter = SignInKeyAdapter(knownRegistrationInfo) { credentialIdString, transport -> + startTransportHandling(transport, credentialIdString) } } } @@ -79,7 +78,7 @@ internal class SignInKeyAdapter(val data: List, val onKeyCli holder.signInKeyName.text = user.displayName holder.signInKeyEmail.text = user.name user.icon?.takeIf { it.isNotBlank() }?.let { ImageManager.create(holder.itemView.context).loadImage(it, holder.signInKeyLogo) } - holder.itemView.setOnClickListener { onKeyClick(item.userJson, item.transport) } + holder.itemView.setOnClickListener { onKeyClick(item.credential, item.transport) } } override fun getItemCount(): Int { diff --git a/play-services-fido/core/src/main/res/drawable/ic_fido_bluetooth.xml b/play-services-fido/core/src/main/res/drawable/ic_fido_bluetooth.xml index d1b5b9246f..8c4448ea53 100644 --- a/play-services-fido/core/src/main/res/drawable/ic_fido_bluetooth.xml +++ b/play-services-fido/core/src/main/res/drawable/ic_fido_bluetooth.xml @@ -7,7 +7,6 @@ + xmlns:android="http://schemas.android.com/apk/res/android"> diff --git a/play-services-fido/core/src/main/res/drawable/ic_fido_qr_code.xml b/play-services-fido/core/src/main/res/drawable/ic_fido_qr_code.xml index d6efab01db..02600b4b72 100644 --- a/play-services-fido/core/src/main/res/drawable/ic_fido_qr_code.xml +++ b/play-services-fido/core/src/main/res/drawable/ic_fido_qr_code.xml @@ -7,8 +7,7 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?attr/colorControlNormal"> + android:viewportHeight="24"> diff --git a/play-services-fido/core/src/main/res/drawable/ic_fido_usb.xml b/play-services-fido/core/src/main/res/drawable/ic_fido_usb.xml index e1772cd5fe..985a5a92e4 100644 --- a/play-services-fido/core/src/main/res/drawable/ic_fido_usb.xml +++ b/play-services-fido/core/src/main/res/drawable/ic_fido_usb.xml @@ -7,7 +7,6 @@ allowList,@Param(6) @Nullable Integer requestId,@Param(7) @Nullable TokenBinding tokenBinding,@Param(8) @Nullable UserVerificationRequirement requireUserVerification, @Param(9)@Nullable AuthenticationExtensions authenticationExtensions) { + public PublicKeyCredentialRequestOptions(@Param(2)@NonNull byte[] challenge,@Param(3) @Nullable Double timeoutSeconds, @Param(4)@NonNull String rpId, @Param(5)@Nullable List allowList,@Param(6) @Nullable Integer requestId,@Param(7) @Nullable TokenBinding tokenBinding,@Param(8) @Nullable UserVerificationRequirement requireUserVerification, @Param(9)@Nullable AuthenticationExtensions authenticationExtensions, @Param(10) @Nullable Long longRequestId) { this.challenge = challenge; this.timeoutSeconds = timeoutSeconds; this.rpId = rpId; @@ -65,6 +65,7 @@ public PublicKeyCredentialRequestOptions(@Param(2)@NonNull byte[] challenge,@Par this.tokenBinding = tokenBinding; this.requireUserVerification = requireUserVerification; this.authenticationExtensions = authenticationExtensions; + this.longRequestId = longRequestId; } @Nullable @@ -90,6 +91,12 @@ public byte[] getChallenge() { return challenge; } + @Hide + @Nullable + public Long getLongRequestId() { + return longRequestId; + } + @Override @Nullable public Integer getRequestId() { @@ -171,6 +178,8 @@ public static class Builder { private AuthenticationExtensions authenticationExtensions; @Nullable private UserVerificationRequirement requireUserVerification; + @Nullable + private Long longRequestId; /** * The constructor of {@link PublicKeyCredentialRequestOptions.Builder}. @@ -208,6 +217,13 @@ public Builder setChallenge(@NonNull byte[] challenge) { return this; } + @Hide + @NonNull + public Builder setLongRequestId(@Nullable Long longRequestId) { + this.longRequestId = longRequestId; + return this; + } + /** * Sets the request id in order to link together events into a single session (the span of events between the * time that the server initiates a single FIDO2 request to the client and receives reply) on a single device. @@ -261,7 +277,7 @@ public Builder setTokenBinding(@Nullable TokenBinding tokenBinding) { */ @NonNull public PublicKeyCredentialRequestOptions build() { - return new PublicKeyCredentialRequestOptions(challenge, timeoutSeconds, rpId, allowList, requestId, tokenBinding, requireUserVerification, authenticationExtensions); + return new PublicKeyCredentialRequestOptions(challenge, timeoutSeconds, rpId, allowList, requestId, tokenBinding, requireUserVerification, authenticationExtensions, longRequestId); } }