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-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/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 2787802214..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"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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) {
diff --git a/play-services-core/src/main/kotlin/com/google/android/gms/auth/api/credentials/credman/service/GoogleIdService.kt b/play-services-core/src/main/kotlin/com/google/android/gms/auth/api/credentials/credman/service/GoogleIdService.kt
new file mode 100644
index 0000000000..ee2b68a647
--- /dev/null
+++ b/play-services-core/src/main/kotlin/com/google/android/gms/auth/api/credentials/credman/service/GoogleIdService.kt
@@ -0,0 +1,10 @@
+/*
+ * SPDX-FileCopyrightText: 2026 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.google.android.gms.auth.api.credentials.credman.service
+
+import org.microg.gms.auth.credentials.provider.GoogleIdService
+
+class GoogleIdService : GoogleIdService()
diff --git a/play-services-core/src/main/kotlin/com/google/android/gms/auth/api/credentials/credman/service/PasswordAndPasskeyService.kt b/play-services-core/src/main/kotlin/com/google/android/gms/auth/api/credentials/credman/service/PasswordAndPasskeyService.kt
new file mode 100644
index 0000000000..fdd228cdce
--- /dev/null
+++ b/play-services-core/src/main/kotlin/com/google/android/gms/auth/api/credentials/credman/service/PasswordAndPasskeyService.kt
@@ -0,0 +1,10 @@
+/*
+ * SPDX-FileCopyrightText: 2026 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.google.android.gms.auth.api.credentials.credman.service
+
+import org.microg.gms.auth.credentials.provider.PasswordAndPasskeyService
+
+class PasswordAndPasskeyService : PasswordAndPasskeyService()
diff --git a/play-services-core/src/main/kotlin/com/google/android/gms/auth/api/credentials/credman/service/RemoteService.kt b/play-services-core/src/main/kotlin/com/google/android/gms/auth/api/credentials/credman/service/RemoteService.kt
new file mode 100644
index 0000000000..c198a578c9
--- /dev/null
+++ b/play-services-core/src/main/kotlin/com/google/android/gms/auth/api/credentials/credman/service/RemoteService.kt
@@ -0,0 +1,10 @@
+/*
+ * SPDX-FileCopyrightText: 2026 microG Project Team
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package com.google.android.gms.auth.api.credentials.credman.service
+
+import org.microg.gms.auth.credentials.provider.RemoteService
+
+class RemoteService : RemoteService()
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentityFidoProxyActivity.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentityFidoProxyActivity.kt
index 4f7aecdf23..fac71f554a 100644
--- a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentityFidoProxyActivity.kt
+++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentityFidoProxyActivity.kt
@@ -16,6 +16,7 @@ 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.ACTION_FIDO_AUTHENTICATE
import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.KEY_CALLER
private const val REQUEST_CODE = 1586077619
@@ -23,7 +24,7 @@ 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 {
+ startActivityForResult(Intent(ACTION_FIDO_AUTHENTICATE).apply {
`package` = packageName
putExtras(intent.extras ?: Bundle())
putExtra(KEY_CALLER, callingActivity?.packageName)
diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/CredentialProviderActivity.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/CredentialProviderActivity.kt
new file mode 100644
index 0000000000..56c0308e42
--- /dev/null
+++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/provider/CredentialProviderActivity.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.appcompat.app.AppCompatActivity
+import androidx.credentials.CreateCredentialResponse
+import androidx.credentials.GetCredentialResponse
+import androidx.credentials.exceptions.CreateCredentialException
+import androidx.credentials.exceptions.CreateCredentialUnknownException
+import androidx.credentials.exceptions.GetCredentialException
+import androidx.credentials.exceptions.GetCredentialUnknownException
+import androidx.credentials.provider.PendingIntentHandler
+import androidx.credentials.provider.ProviderCreateCredentialRequest
+import androidx.credentials.provider.ProviderGetCredentialRequest
+
+private const val TAG = "CredentialProviderActivity"
+
+abstract class CredentialProviderActivity : AppCompatActivity() {
+ var isCreateRequest = false
+ private set
+ var isGetRequest = false
+ private set
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Log.d(TAG, "onCreate: intent: ${intent?.extras?.keySet()}")
+ runCatching {
+ val createRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
+ if (createRequest != null) {
+ isCreateRequest = true
+ return onProviderCreateCredentialRequest(createRequest)
+ }
+ val getRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
+ if (getRequest != null) {
+ isGetRequest = true
+ return onProviderGetCredentialRequest(getRequest)
+ }
+ finishWithException("Unknown request")
+ }.onFailure { e ->
+ 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/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) }
+}
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);
}
}