mirror of
https://github.com/microg/android_packages_apps_GmsCore
synced 2026-08-06 14:26:05 -04:00
Credential Manager
This commit is contained in:
parent
587d693962
commit
94a488be5c
44 changed files with 1572 additions and 101 deletions
|
|
@ -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/'}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -115,8 +115,132 @@ public class BeginSignInRequest extends AbstractSafeParcelable {
|
|||
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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -147,6 +147,8 @@
|
|||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
|
||||
<uses-permission android:name="android.permission.PROVIDE_REMOTE_CREDENTIALS" />
|
||||
<uses-permission android:name="android.permission.PROVIDE_DEFAULT_ENABLED_CREDENTIAL_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||
|
|
@ -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"/>
|
||||
|
||||
<!-- Credential Provider Services -->
|
||||
<service
|
||||
android:name="com.google.android.gms.auth.api.credentials.credman.service.PasswordAndPasskeyService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"
|
||||
tools:targetApi="34">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.credentials.CredentialProviderService"/>
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.credentials.provider"
|
||||
android:resource="@xml/credentials_provider_passkey" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="com.google.android.gms.auth.api.credentials.credman.service.RemoteService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"
|
||||
tools:targetApi="34">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.credentials.system.CredentialProviderService"/>
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.credentials.provider"
|
||||
android:resource="@xml/credentials_provider_remote" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="com.google.android.gms.auth.api.credentials.credman.service.GoogleIdService"
|
||||
android:exported="true"
|
||||
android:icon="@drawable/ic_google_logo"
|
||||
android:label="@string/credentials_service_sign_in_with_google_label"
|
||||
android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE"
|
||||
tools:targetApi="34">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.credentials.system.CredentialProviderService"/>
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.credentials.provider"
|
||||
android:resource="@xml/credentials_provider_google_id" />
|
||||
</service>
|
||||
|
||||
|
||||
<activity
|
||||
android:name="org.microg.gms.auth.credentials.provider.PublicKeyProxyActivity"
|
||||
android:exported="false"
|
||||
android:process=":ui"
|
||||
android:theme="@style/Theme.App.Translucent"
|
||||
android:excludeFromRecents="true"
|
||||
tools:targetApi="34" />
|
||||
|
||||
<activity
|
||||
android:name="org.microg.gms.auth.credentials.provider.SignInProxyActivity"
|
||||
android:exported="false"
|
||||
android:process=":ui"
|
||||
android:theme="@style/Theme.App.Translucent"
|
||||
android:excludeFromRecents="true"
|
||||
tools:targetApi="34">
|
||||
<intent-filter>
|
||||
<action android:name="org.microg.gms.auth.credentials.GOOGLE_ID_PROXY_INTENT"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Games -->
|
||||
|
||||
<activity
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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<BeginGetCredentialResponse, GetCredentialException>
|
||||
) {
|
||||
runCatching {
|
||||
val credentialEntries = request.beginGetCredentialOptions
|
||||
.flatMap { option ->
|
||||
when (option.type) {
|
||||
TYPE_GOOGLE_ID_TOKEN_CREDENTIAL -> handleGoogleIdTokenRequest(option, request)
|
||||
else -> emptyList<CredentialEntry>().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<CredentialEntry> = 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<BeginCreateCredentialResponse, CreateCredentialException>
|
||||
) {
|
||||
// Google ID Service only handles GET (sign-in), not CREATE
|
||||
callback.onResult(BeginCreateCredentialResponse.Builder().build())
|
||||
}
|
||||
|
||||
override fun onClearCredentialStateRequest(
|
||||
request: ProviderClearCredentialStateRequest,
|
||||
cancellationSignal: CancellationSignal,
|
||||
callback: OutcomeReceiver<Void?, ClearCredentialException>
|
||||
) {
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<PublicKeyCredentialDescriptor>()
|
||||
for (i in 0..<allowCredentials.length()) {
|
||||
allowList.add(allowCredentials.getJSONObject(i).parsePublicKeyCredentialDescriptor())
|
||||
}
|
||||
builder.setAllowList(allowList)
|
||||
}
|
||||
if (has("requestId")) {
|
||||
builder.setRequestId(getInt("requestId"))
|
||||
}
|
||||
if (has("tokenBinding")) {
|
||||
builder.setTokenBinding(getJSONObject("tokenBinding").parseTokenBinding())
|
||||
}
|
||||
if (has("userVerification")) {
|
||||
builder.setRequireUserVerification(UserVerificationRequirement.fromString(getString("userVerification")))
|
||||
}
|
||||
if (has("authenticationExtensions")) {
|
||||
builder.setAuthenticationExtensions(getJSONObject("authenticationExtensions").parseAuthenticationExtensions())
|
||||
} else if (has("extensions")) {
|
||||
builder.setAuthenticationExtensions(getJSONObject("extensions").parseAuthenticationExtensions())
|
||||
}
|
||||
if (has("longRequestId")) {
|
||||
builder.setLongRequestId(getLong("longRequestId"))
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun JSONObject.parseTokenBinding() = when (TokenBindingStatus.fromString(getString("status"))) {
|
||||
TokenBindingStatus.SUPPORTED -> 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<PublicKeyCredentialParameters>()
|
||||
for (i in 0..<pubKeyCredParams.length()) {
|
||||
parameters.add(pubKeyCredParams.getJSONObject(i).parsePublicKeyCredentialParameters())
|
||||
}
|
||||
builder.setParameters(parameters)
|
||||
if (has("timeout")) {
|
||||
builder.setTimeoutSeconds(getDouble("timeout") / 1000.0)
|
||||
}
|
||||
if (has("excludeCredentials")) {
|
||||
val excludeCredentials = getJSONArray("excludeCredentials")
|
||||
val excludeList = arrayListOf<PublicKeyCredentialDescriptor>()
|
||||
for (i in 0..<excludeCredentials.length()) {
|
||||
excludeList.add(excludeCredentials.getJSONObject(i).parsePublicKeyCredentialDescriptor())
|
||||
}
|
||||
builder.setExcludeList(excludeList)
|
||||
}
|
||||
if (has("authenticatorSelection")) {
|
||||
builder.setAuthenticatorSelection(getJSONObject("authenticatorSelection").parseAuthenticatorSelectionCriteria())
|
||||
}
|
||||
if (has("extensions")) {
|
||||
builder.setAuthenticationExtensions(getJSONObject("extensions").parseAuthenticationExtensions())
|
||||
}
|
||||
if (has("attestation")) {
|
||||
try {
|
||||
builder.setAttestationConveyancePreference(AttestationConveyancePreference.fromString(getString("attestation")))
|
||||
} catch (e: Exception) {
|
||||
builder.setAttestationConveyancePreference(AttestationConveyancePreference.NONE)
|
||||
}
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun JSONObject.parsePublicKeyCredentialRpEntity() = PublicKeyCredentialRpEntity(
|
||||
getString("id"),
|
||||
getString("name"),
|
||||
if (has("icon")) optString("icon") else null
|
||||
)
|
||||
|
||||
fun JSONObject.parsePublicKeyCredentialUserEntity() = PublicKeyCredentialUserEntity(
|
||||
getString("id").decodeBase64Url(),
|
||||
getString("name"),
|
||||
if (has("icon")) optString("icon") else null,
|
||||
optString("displayName")
|
||||
)
|
||||
|
||||
fun JSONObject.parsePublicKeyCredentialParameters() = PublicKeyCredentialParameters(
|
||||
getString("type"),
|
||||
getInt("alg")
|
||||
)
|
||||
|
||||
fun JSONObject.parseAuthenticatorSelectionCriteria() = AuthenticatorSelectionCriteria.Builder()
|
||||
.setAttachment(if (has("authenticatorAttachment")) optString("authenticatorAttachment").let { Attachment.fromString(it) } else null)
|
||||
.setRequireResidentKey(if (has("requireResidentKey")) optBoolean("requireResidentKey") else null)
|
||||
.setRequireUserVerification(if (has("userVerification")) optString("userVerification").let { UserVerificationRequirement.fromString(it) } else null)
|
||||
.setResidentKeyRequirement(if (has("residentKey")) optString("residentKey").let { ResidentKeyRequirement.fromString(it) } else null)
|
||||
.build()
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2026 microG Project Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package org.microg.gms.auth.credentials.provider
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.os.CancellationSignal
|
||||
import android.os.OutcomeReceiver
|
||||
import androidx.credentials.provider.BeginCreateCredentialRequest
|
||||
import androidx.credentials.provider.BeginCreateCredentialResponse
|
||||
import androidx.credentials.provider.BeginGetCredentialRequest
|
||||
import androidx.credentials.provider.BeginGetCredentialResponse
|
||||
import androidx.credentials.provider.ProviderClearCredentialStateRequest
|
||||
import androidx.credentials.provider.CreateEntry
|
||||
import androidx.credentials.provider.CredentialEntry
|
||||
import androidx.credentials.provider.CredentialProviderService
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.credentials.PasswordCredential.Companion.TYPE_PASSWORD_CREDENTIAL
|
||||
import androidx.credentials.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
|
||||
import androidx.credentials.exceptions.ClearCredentialException
|
||||
import androidx.credentials.exceptions.CreateCredentialException
|
||||
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
||||
import androidx.credentials.exceptions.GetCredentialException
|
||||
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||
import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest
|
||||
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
|
||||
import androidx.credentials.provider.CustomCredentialEntry
|
||||
import androidx.credentials.provider.PublicKeyCredentialEntry
|
||||
import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialUserEntity
|
||||
import com.squareup.wire.Instant
|
||||
import org.json.JSONObject
|
||||
import org.microg.gms.fido.core.CredentialUserInfo
|
||||
import org.microg.gms.fido.core.Database
|
||||
import org.microg.gms.fido.core.R
|
||||
import org.microg.gms.fido.core.transport.Transport
|
||||
import org.microg.gms.fido.core.ui.AuthenticatorActivity.Companion.KEY_CREDENTIAL_ID
|
||||
|
||||
private const val TAG = "PasswordAndPasskey"
|
||||
|
||||
/**
|
||||
* Password and Passkey Credential Provider Service
|
||||
* Handles both password and passkey (FIDO2/WebAuthn) credentials
|
||||
*/
|
||||
@RequiresApi(34)
|
||||
open class PasswordAndPasskeyService : CredentialProviderService() {
|
||||
|
||||
override fun onBeginGetCredentialRequest(
|
||||
request: BeginGetCredentialRequest,
|
||||
cancellationSignal: CancellationSignal,
|
||||
callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>
|
||||
) {
|
||||
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<CredentialEntry>().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<CredentialEntry> = 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<BeginCreateCredentialResponse, CreateCredentialException>
|
||||
) {
|
||||
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<BeginCreateCredentialResponse, CreateCredentialException>
|
||||
) = 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<Void?, ClearCredentialException>
|
||||
) {
|
||||
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
|
||||
)
|
||||
}
|
||||
|
|
@ -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<ByteArray, String> = 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<ByteArray, String> = 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<BeginGetCredentialResponse, GetCredentialException>
|
||||
) {
|
||||
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<BeginCreateCredentialResponse, CreateCredentialException>
|
||||
) {
|
||||
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<Void?, ClearCredentialException>
|
||||
) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -124,7 +124,7 @@ class AssistedSignInActivity : AppCompatActivity() {
|
|||
googleSignInAccount.displayName,
|
||||
googleSignInAccount.givenName,
|
||||
googleSignInAccount.familyName,
|
||||
null,
|
||||
googleSignInAccount.photoUrl,
|
||||
null,
|
||||
googleSignInAccount.idToken,
|
||||
null,
|
||||
|
|
|
|||
|
|
@ -342,6 +342,8 @@ Please set up a password, PIN, or pattern lock screen."</string>
|
|||
<string name="credentials_assisted_choose_account_error_tips">Account abnormality</string>
|
||||
<string name="credentials_assisted_signin_button_text_long">Sign in with Google</string>
|
||||
<string name="credentials_assisted_signin_add_account">Add Account</string>
|
||||
<string name="credentials_service_sign_in_with_google_label">Sign in with Google</string>
|
||||
<string name="credentials_service_remote_custom_subtitle">Security key, smartphone or tablet</string>
|
||||
|
||||
<string name="limited_services_dialog_information">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.</string>
|
||||
<string name="limited_services_dialog_information_ack">I understand</string>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<credential-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:settingsSubtitle="@string/credentials_service_sign_in_with_google_label"
|
||||
android:settingsActivity="org.microg.gms.ui.SettingsActivity"
|
||||
tools:targetApi="34">
|
||||
<capabilities>
|
||||
<capability name="com.google.android.libraries.identity.googleid.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL"/>
|
||||
</capabilities>
|
||||
</credential-provider>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<credential-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:targetApi="34">
|
||||
<capabilities>
|
||||
<capability name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL"/>
|
||||
</capabilities>
|
||||
</credential-provider>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<credential-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:targetApi="34">
|
||||
<capabilities>
|
||||
<capability name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL"/>
|
||||
</capabilities>
|
||||
</credential-provider>
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<Intent> {
|
||||
if (it?.action != NfcAdapter.ACTION_TECH_DISCOVERED) return@Consumer
|
||||
|
|
|
|||
|
|
@ -198,18 +198,19 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac
|
|||
suspend fun sign(
|
||||
options: RequestOptions,
|
||||
callerPackage: String,
|
||||
user: PublicKeyCredentialUserEntity?
|
||||
credentialIdString: String?
|
||||
): AuthenticatorResponseWithUser<AuthenticatorAssertionResponse> {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 <reified T : TransportHandler> getTransportHandler(): T? =
|
||||
transportHandlers.filterIsInstance<T>().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<Transport>()
|
||||
val allowedTransports = mutableSetOf<Transport>()
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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<CredentialUserInfo>, 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 {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
-->
|
||||
|
||||
<vector android:width="24dp" android:height="24dp" android:viewportWidth="960" android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#000"
|
||||
android:pathData="M120,800L120,688Q120,654 137.5,625.5Q155,597 184,582Q246,551 310,535.5Q374,520 440,520Q460,520 480,521.5Q500,523 520,526Q516,584 541,635.5Q566,687 614,720L614,800L120,800ZM760,920L700,860L700,674Q656,661 628,624.5Q600,588 600,540Q600,482 641,441Q682,400 740,400Q798,400 839,441Q880,482 880,540Q880,585 854.5,620Q829,655 790,670L840,720L780,780L840,840L760,920ZM440,480Q374,480 327,433Q280,386 280,320Q280,254 327,207Q374,160 440,160Q506,160 553,207Q600,254 600,320Q600,386 553,433Q506,480 440,480ZM768.5,548.5Q780,537 780,520Q780,503 768.5,491.5Q757,480 740,480Q723,480 711.5,491.5Q700,503 700,520Q700,537 711.5,548.5Q723,560 740,560Q757,560 768.5,548.5Z"/>
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@
|
|||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M3,11h8V3H3V11zM5,5h4v4H5V5z"/>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
|
|
|
|||
|
|
@ -197,6 +197,8 @@ public class AuthenticationExtensions extends AbstractSafeParcelable {
|
|||
private FidoAppIdExtension fidoAppIdExtension;
|
||||
@Nullable
|
||||
private UserVerificationMethodExtension userVerificationMethodExtension;
|
||||
@Nullable
|
||||
private PrfExtension prfExtension;
|
||||
|
||||
/**
|
||||
* The constructor of {@link AuthenticationExtensions.Builder}.
|
||||
|
|
@ -213,6 +215,12 @@ public class AuthenticationExtensions extends AbstractSafeParcelable {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Hide
|
||||
public Builder setPrfExtension(@Nullable PrfExtension prfExtension) {
|
||||
this.prfExtension = prfExtension;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the User Verification Method extension, which allows the relying party to ascertain up to three
|
||||
* authentication methods that were used.
|
||||
|
|
|
|||
|
|
@ -43,18 +43,18 @@ public class AuthenticationExtensionsClientOutputs extends AbstractSafeParcelabl
|
|||
|
||||
@Field(value = 4, getterName = "getPrfOutputs")
|
||||
@Nullable
|
||||
private AuthenticationExtensionsPrfOutputs prfOutputs;
|
||||
private AuthenticationExtensionsPrfOutputs prf;
|
||||
|
||||
@Field(value = 5, getterName = "getTxAuthSimple")
|
||||
@Nullable
|
||||
private String txAuthSimple;
|
||||
|
||||
@Constructor
|
||||
public AuthenticationExtensionsClientOutputs(@Param(1) @Nullable UvmEntries uvmEntries, @Param(2) @Nullable AuthenticationExtensionsDevicePublicKeyOutputs devicePublicKeyOutputs, @Param(3) @Nullable AuthenticationExtensionsCredPropsOutputs credProps, @Param(4) @Nullable AuthenticationExtensionsPrfOutputs prfOutputs, @Param(5) @Nullable String txAuthSimple) {
|
||||
public AuthenticationExtensionsClientOutputs(@Param(1) @Nullable UvmEntries uvmEntries, @Param(2) @Nullable AuthenticationExtensionsDevicePublicKeyOutputs devicePublicKeyOutputs, @Param(3) @Nullable AuthenticationExtensionsCredPropsOutputs credProps, @Param(4) @Nullable AuthenticationExtensionsPrfOutputs prf, @Param(5) @Nullable String txAuthSimple) {
|
||||
this.uvmEntries = uvmEntries;
|
||||
this.devicePublicKeyOutputs = devicePublicKeyOutputs;
|
||||
this.credProps = credProps;
|
||||
this.prfOutputs = prfOutputs;
|
||||
this.prf = prf;
|
||||
this.txAuthSimple = txAuthSimple;
|
||||
}
|
||||
|
||||
|
|
@ -74,8 +74,13 @@ public class AuthenticationExtensionsClientOutputs extends AbstractSafeParcelabl
|
|||
}
|
||||
|
||||
@Nullable
|
||||
public AuthenticationExtensionsPrfOutputs getPrf() {
|
||||
return prf;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public AuthenticationExtensionsPrfOutputs getPrfOutputs() {
|
||||
return prfOutputs;
|
||||
return prf;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
|
@ -108,12 +113,12 @@ public class AuthenticationExtensionsClientOutputs extends AbstractSafeParcelabl
|
|||
if (!(o instanceof AuthenticationExtensionsClientOutputs)) return false;
|
||||
|
||||
AuthenticationExtensionsClientOutputs that = (AuthenticationExtensionsClientOutputs) o;
|
||||
return (Objects.equals(uvmEntries, that.uvmEntries)) && (Objects.equals(devicePublicKeyOutputs, that.devicePublicKeyOutputs)) && (Objects.equals(credProps, that.credProps)) && (Objects.equals(prfOutputs, that.prfOutputs)) && (Objects.equals(txAuthSimple, that.txAuthSimple));
|
||||
return (Objects.equals(uvmEntries, that.uvmEntries)) && (Objects.equals(devicePublicKeyOutputs, that.devicePublicKeyOutputs)) && (Objects.equals(credProps, that.credProps)) && (Objects.equals(prf, that.prf)) && (Objects.equals(txAuthSimple, that.txAuthSimple));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(new Object[]{uvmEntries, devicePublicKeyOutputs, credProps, prfOutputs, txAuthSimple});
|
||||
return Arrays.hashCode(new Object[]{uvmEntries, devicePublicKeyOutputs, credProps, prf, txAuthSimple});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -32,13 +32,13 @@ import java.util.Objects;
|
|||
@SafeParcelable.Class
|
||||
public class PublicKeyCredential extends AbstractSafeParcelable {
|
||||
@Field(value = 1, getterName = "getId")
|
||||
@NonNull
|
||||
@Nullable
|
||||
private String id;
|
||||
@Field(value = 2, getterName = "getType")
|
||||
@NonNull
|
||||
private String type;
|
||||
@Field(value = 3, getterName = "getRawId")
|
||||
@NonNull
|
||||
@Nullable
|
||||
private byte[] rawId;
|
||||
@Field(value = 4, getter = "$object.getResponse() instanceof $type ? ($type) $object.getResponse() : null")
|
||||
@Nullable
|
||||
|
|
@ -85,12 +85,12 @@ public class PublicKeyCredential extends AbstractSafeParcelable {
|
|||
return clientExtensionResults;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Nullable
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Nullable
|
||||
public byte[] getRawId() {
|
||||
return rawId;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,12 +51,12 @@ public class PublicKeyCredentialRequestOptions extends RequestOptions {
|
|||
@Field(value = 9, getterName = "getAuthenticationExtensions")
|
||||
@Nullable
|
||||
private AuthenticationExtensions authenticationExtensions;
|
||||
@Field(10)
|
||||
@Field(value = 10, getterName = "getLongRequestId")
|
||||
@Nullable
|
||||
Long longRequestId;
|
||||
private final Long longRequestId;
|
||||
|
||||
@Constructor
|
||||
public PublicKeyCredentialRequestOptions(@Param(2)@NonNull byte[] challenge,@Param(3) @Nullable Double timeoutSeconds, @Param(4)@NonNull String rpId, @Param(5)@Nullable List<PublicKeyCredentialDescriptor> 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<PublicKeyCredentialDescriptor> 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 class PublicKeyCredentialRequestOptions extends RequestOptions {
|
|||
this.tokenBinding = tokenBinding;
|
||||
this.requireUserVerification = requireUserVerification;
|
||||
this.authenticationExtensions = authenticationExtensions;
|
||||
this.longRequestId = longRequestId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
|
@ -90,6 +91,12 @@ public class PublicKeyCredentialRequestOptions extends RequestOptions {
|
|||
return challenge;
|
||||
}
|
||||
|
||||
@Hide
|
||||
@Nullable
|
||||
public Long getLongRequestId() {
|
||||
return longRequestId;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Integer getRequestId() {
|
||||
|
|
@ -171,6 +178,8 @@ public class PublicKeyCredentialRequestOptions extends RequestOptions {
|
|||
private AuthenticationExtensions authenticationExtensions;
|
||||
@Nullable
|
||||
private UserVerificationRequirement requireUserVerification;
|
||||
@Nullable
|
||||
private Long longRequestId;
|
||||
|
||||
/**
|
||||
* The constructor of {@link PublicKeyCredentialRequestOptions.Builder}.
|
||||
|
|
@ -208,6 +217,13 @@ public class PublicKeyCredentialRequestOptions extends RequestOptions {
|
|||
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 class PublicKeyCredentialRequestOptions extends RequestOptions {
|
|||
*/
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue