diff --git a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/BeginSignInRequest.java b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/BeginSignInRequest.java index 3ae1daa19b..a1ffedf1aa 100644 --- a/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/BeginSignInRequest.java +++ b/play-services-auth/src/main/java/com/google/android/gms/auth/api/identity/BeginSignInRequest.java @@ -43,6 +43,8 @@ public class BeginSignInRequest extends AbstractSafeParcelable { private final PasskeysRequestOptions passkeysRequestOptions; @Field(value = 7, getterName = "getPasskeyJsonRequestOptions") private final PasskeyJsonRequestOptions passkeyJsonRequestOptions; + @Field(value = 8, getterName = "isPreferImmediatelyAvailableCredentials") + private final boolean preferImmediatelyAvailableCredentials; @NonNull @Override @@ -55,11 +57,12 @@ public String toString() { .field("theme", theme) .field("PasskeysRequestOptions", passkeysRequestOptions) .field("PasskeyJsonRequestOptions", passkeyJsonRequestOptions) + .field("preferImmediatelyAvailableCredentials", preferImmediatelyAvailableCredentials) .end(); } @Constructor - BeginSignInRequest(@Param(1) PasswordRequestOptions passwordRequestOptions, @Param(2) GoogleIdTokenRequestOptions googleIdTokenRequestOptions, @Param(3) String sessionId, @Param(4) boolean autoSelectEnabled, @Param(5) int theme, @Param(6) PasskeysRequestOptions passkeysRequestOptions, @Param(7) PasskeyJsonRequestOptions passkeyJsonRequestOptions) { + BeginSignInRequest(@Param(1) PasswordRequestOptions passwordRequestOptions, @Param(2) GoogleIdTokenRequestOptions googleIdTokenRequestOptions, @Param(3) String sessionId, @Param(4) boolean autoSelectEnabled, @Param(5) int theme, @Param(6) PasskeysRequestOptions passkeysRequestOptions, @Param(7) PasskeyJsonRequestOptions passkeyJsonRequestOptions, @Param(8) boolean preferImmediatelyAvailableCredentials) { this.passwordRequestOptions = passwordRequestOptions; this.googleIdTokenRequestOptions = googleIdTokenRequestOptions; this.sessionId = sessionId; @@ -67,6 +70,7 @@ public String toString() { this.theme = theme; this.passkeysRequestOptions = passkeysRequestOptions; this.passkeyJsonRequestOptions = passkeyJsonRequestOptions; + this.preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials; } @NonNull @@ -107,6 +111,10 @@ public boolean isAutoSelectEnabled() { return autoSelectEnabled; } + public boolean isPreferImmediatelyAvailableCredentials() { + return preferImmediatelyAvailableCredentials; + } + public static class Builder { } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt index c936970d8c..41d46066d7 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/credentials/identity/IdentitySignInService.kt @@ -46,6 +46,7 @@ import org.microg.gms.auth.signin.CLIENT_PACKAGE_NAME import org.microg.gms.auth.signin.GOOGLE_SIGN_IN_OPTIONS import org.microg.gms.auth.signin.performSignOut import org.microg.gms.common.GmsService +import org.microg.gms.fido.core.Database 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 @@ -94,6 +95,7 @@ class IdentitySignInServiceImpl(private val context: Context, private val client fun JSONArray.map(fn: (JSONObject) -> T): List = (0 until length()).map { fn(getJSONObject(it)) } fun JSONArray.map(fn: (String) -> T): List = (0 until length()).map { fn(getString(it)) } val json = JSONObject(request.passkeyJsonRequestOptions.requestJson) + val rpId = json.getString("rpId") val options = PublicKeyCredentialRequestOptions.Builder() .setAllowList(json.getArrayOrNull("allowCredentials")?.let { allowCredentials -> allowCredentials.map { credential: JSONObject -> PublicKeyCredentialDescriptor( @@ -106,9 +108,14 @@ class IdentitySignInServiceImpl(private val context: Context, private val client } }) .setChallenge(Base64.decode(json.getString("challenge"), Base64.URL_SAFE)) .setRequireUserVerification(json.optString("userVerification").takeIf { it.isNotBlank() }?.let { UserVerificationRequirement.fromString(it) }) - .setRpId(json.getString("rpId")) + .setRpId(rpId) .setTimeoutSeconds(json.optDouble("timeout", -1.0).takeIf { it > 0 }) .build() + if (request.isPreferImmediatelyAvailableCredentials && Database(context).getKnownRegistrationInfo(rpId).isEmpty()) { + Log.d(TAG, "need available Credential") + callback.onResult(Status.CANCELED, null) + return + } val bundle = bundleOf( KEY_SERVICE to GmsService.IDENTITY_SIGN_IN.SERVICE_ID, KEY_SOURCE to "app", diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/folsom/KeyRetrievalService.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/folsom/KeyRetrievalService.kt index 97403697e1..b3dc7c77b5 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/folsom/KeyRetrievalService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/folsom/KeyRetrievalService.kt @@ -94,14 +94,14 @@ class KeyRetrievalServiceImpl(val context: Context) : IKeyRetrievalService.Stub( callback: ISharedKeyCallback?, accountName: String?, metadata: ApiMetadata? ) { Log.d(TAG, "Not implemented getKeyMaterial accountName:$accountName metadata:$metadata") - callback?.onResult(Status.SUCCESS, emptyArray()) + callback?.onResult(Status.INTERNAL_ERROR, emptyArray()) } override fun setKeyMaterial( callback: IKeyRetrievalCallback?, accountName: String?, keys: Array?, metadata: ApiMetadata? ) { Log.d(TAG, "Not implemented setKeyMaterial accountName:$accountName keys:$keys metadata:$metadata") - callback?.onResult(Status.SUCCESS) + callback?.onResult(Status.INTERNAL_ERROR) } override fun getRecoveredSecurityDomains( diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/Database.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/Database.kt index 377f2d11b0..40cb964b72 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/Database.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/Database.kt @@ -8,11 +8,13 @@ package org.microg.gms.fido.core import android.content.ContentValues import android.content.Context import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteDatabase.CONFLICT_IGNORE import android.database.sqlite.SQLiteDatabase.CONFLICT_REPLACE import android.database.sqlite.SQLiteOpenHelper +import android.util.Log import androidx.core.database.getLongOrNull +import androidx.core.database.getStringOrNull import org.microg.gms.fido.core.transport.Transport +import org.microg.gms.fido.core.ui.TAG class Database(context: Context) : SQLiteOpenHelper(context, "fido.db", null, VERSION) { @@ -31,6 +33,23 @@ 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), "$COLUMN_RP_ID=?", arrayOf(rpId), null, null, null + ) + val result = mutableListOf() + cursor.use { c -> + while (c.moveToNext()) { + val credentialId = c.getString(0) + val userJson = c.getStringOrNull(1) ?: continue + val transport = c.getStringOrNull(2) ?: continue + Log.d(TAG, "getKnownRegistrationInfo: credential: $credentialId user: $userJson transport: $transport") + result.add(CredentialUserInfo(credentialId, userJson, Transport.valueOf(transport))) + } + } + result + } + fun insertPrivileged(packageName: String, signatureDigest: String) = writableDatabase.use { it.insertWithOnConflict(TABLE_PRIVILEGED_APPS, null, ContentValues().apply { put(COLUMN_PACKAGE_NAME, packageName) @@ -39,13 +58,33 @@ class Database(context: Context) : SQLiteOpenHelper(context, "fido.db", null, VE }, CONFLICT_REPLACE) } - fun insertKnownRegistration(rpId: String, credentialId: String, transport: Transport) = writableDatabase.use { - it.insertWithOnConflict(TABLE_KNOWN_REGISTRATIONS, null, ContentValues().apply { - put(COLUMN_RP_ID, rpId) + fun insertKnownRegistration(rpId: String, credentialId: String, transport: Transport, userJson: String? = null) = writableDatabase.use { + Log.d(TAG, "insertKnownRegistration: $rpId $credentialId $transport $userJson") + val values = ContentValues().apply { put(COLUMN_CREDENTIAL_ID, credentialId) put(COLUMN_TRANSPORT, transport.name) put(COLUMN_TIMESTAMP, System.currentTimeMillis()) - }, CONFLICT_REPLACE) + if (userJson != null) { + put(COLUMN_REGISTER_USER, userJson) + } + } + + 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)) + } + + if (updated == 0) { + val insertValues = ContentValues().apply { + put(COLUMN_RP_ID, rpId) + put(COLUMN_CREDENTIAL_ID, credentialId) + put(COLUMN_TRANSPORT, transport.name) + put(COLUMN_TIMESTAMP, System.currentTimeMillis()) + userJson?.let { json -> put(COLUMN_REGISTER_USER, json) } + } + it.insert(TABLE_KNOWN_REGISTRATIONS, null, insertValues) + } } override fun onCreate(db: SQLiteDatabase) { @@ -59,10 +98,13 @@ class Database(context: Context) : SQLiteOpenHelper(context, "fido.db", null, VE if (oldVersion < 2) { db.execSQL("CREATE TABLE $TABLE_KNOWN_REGISTRATIONS($COLUMN_RP_ID TEXT, $COLUMN_CREDENTIAL_ID TEXT, $COLUMN_TRANSPORT TEXT, $COLUMN_TIMESTAMP INT, UNIQUE($COLUMN_RP_ID, $COLUMN_CREDENTIAL_ID) ON CONFLICT REPLACE)") } + if (oldVersion < 3) { + db.execSQL("ALTER TABLE $TABLE_KNOWN_REGISTRATIONS ADD COLUMN $COLUMN_REGISTER_USER TEXT") + } } companion object { - const val VERSION = 2 + const val VERSION = 3 private const val TABLE_PRIVILEGED_APPS = "privileged_apps" private const val TABLE_KNOWN_REGISTRATIONS = "known_registrations" private const val COLUMN_PACKAGE_NAME = "package_name" @@ -71,6 +113,7 @@ class Database(context: Context) : SQLiteOpenHelper(context, "fido.db", null, VE private const val COLUMN_RP_ID = "rp_id" private const val COLUMN_CREDENTIAL_ID = "credential_id" private const val COLUMN_TRANSPORT = "transport" + private const val COLUMN_REGISTER_USER = "register_user" } } diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/RequestHandling.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/RequestHandling.kt index 46b36f048a..e3bb3f6bbc 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/RequestHandling.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/RequestHandling.kt @@ -20,6 +20,7 @@ import org.json.JSONArray import org.json.JSONObject import org.microg.gms.fido.core.RequestOptionsType.REGISTER import org.microg.gms.fido.core.RequestOptionsType.SIGN +import org.microg.gms.fido.core.transport.Transport import org.microg.gms.utils.* import java.net.HttpURLConnection import java.security.MessageDigest @@ -30,6 +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) enum class RequestOptionsType { REGISTER, SIGN } val RequestOptions.registerOptions: PublicKeyCredentialCreationOptions @@ -71,6 +73,12 @@ val RequestOptions.rpId: String SIGN -> signOptions.rpId } +val RequestOptions.user: String? + get() = when (type) { + REGISTER -> registerOptions.user.toJson() + SIGN -> null + } + val PublicKeyCredentialCreationOptions.skipAttestation: Boolean get() = attestationConveyancePreference in setOf(AttestationConveyancePreference.NONE, null) @@ -79,7 +87,7 @@ fun topDomainOf(string: String?) = fun JSONArray.map(fn: JSONArray.(Int) -> T): List = (0 until length()).map { fn(this, it) } -private suspend fun isFacetIdTrusted(context: Context, facetId: String, appId: String): Boolean { +private suspend fun isFacetIdTrusted(context: Context, facetIds: Set, appId: String): Boolean { val trustedFacets = try { val deferred = CompletableDeferred() HttpURLConnection.setFollowRedirects(false) @@ -97,14 +105,12 @@ private suspend fun isFacetIdTrusted(context: Context, facetId: String, appId: S // Ignore and fail emptyList() } - return trustedFacets.contains(facetId) + return facetIds.any { trustedFacets.contains(it) } } private const val ASSET_LINK_REL = "delegate_permission/common.get_login_creds" -private suspend fun isAssetLinked(context: Context, rpId: String, facetId: String, packageName: String?): Boolean { +private suspend fun isAssetLinked(context: Context, rpId: String, fp: String, packageName: String?): Boolean { try { - if (!facetId.startsWith("android:apk-key-hash-sha256:")) return false - val fp = Base64.decode(facetId.substring(28), HASH_BASE64_FLAGS).toHexString(":") val deferred = CompletableDeferred() HttpURLConnection.setFollowRedirects(true) val url = "https://$rpId/.well-known/assetlinks.json" @@ -129,7 +135,7 @@ private suspend fun isAssetLinked(context: Context, rpId: String, facetId: Strin } // Note: This assumes the RP ID is allowed -private suspend fun isAppIdAllowed(context: Context, appId: String, facetId: String, rpId: String): Boolean { +private suspend fun isAppIdAllowed(context: Context, appId: String, facetIds: Set, rpId: String): Boolean { return try { when { topDomainOf(Uri.parse(appId).host) == topDomainOf(rpId) -> { @@ -141,7 +147,7 @@ private suspend fun isAppIdAllowed(context: Context, appId: String, facetId: Str // This is gonna save us a ton of requests true } - isFacetIdTrusted(context, facetId, appId) -> { + isFacetIdTrusted(context, facetIds, appId) -> { // Valid: Allowed by TrustedFacets list true } @@ -154,29 +160,24 @@ private suspend fun isAppIdAllowed(context: Context, appId: String, facetId: Str } } -suspend fun RequestOptions.checkIsValid(context: Context, facetId: String, packageName: String?) { - if (type == SIGN) { - if (signOptions.allowList.isNullOrEmpty()) { - throw RequestHandlingException(NOT_ALLOWED_ERR, "Request doesn't have a valid list of allowed credentials.") - } - } - if (facetId.startsWith("https://")) { - if (topDomainOf(Uri.parse(facetId).host) != topDomainOf(rpId)) { - throw RequestHandlingException(NOT_ALLOWED_ERR, "RP ID $rpId not allowed from facet $facetId") +suspend fun RequestOptions.checkIsValid(context: Context, origin: String, packageName: String?) { + val allApplicableFacetIds = hashSetOf() + if (origin.startsWith("https://")) { + allApplicableFacetIds.add(origin) + if (topDomainOf(Uri.parse(origin).host) != topDomainOf(rpId)) { + throw RequestHandlingException(NOT_ALLOWED_ERR, "RP ID $rpId not allowed from origin $origin") } // FIXME: Standard suggests doing additional checks, but this is already sensible enough - } else if (facetId.startsWith("android:apk-key-hash:") && packageName != null) { - val sha256FacetId = getAltFacetId(context, packageName, facetId) ?: - throw RequestHandlingException(NOT_ALLOWED_ERR, "Can't resolve $facetId to SHA-256 Facet") - if (!isAssetLinked(context, rpId, sha256FacetId, packageName)) { - throw RequestHandlingException(NOT_ALLOWED_ERR, "RP ID $rpId not allowed from facet $sha256FacetId") - } - } else if (facetId.startsWith("android:apk-key-hash-sha256:")) { - if (!isAssetLinked(context, rpId, facetId, packageName)) { - throw RequestHandlingException(NOT_ALLOWED_ERR, "RP ID $rpId not allowed from facet $facetId") + } else if ((origin.startsWith("android:apk-key-hash:") || origin.startsWith("android:apk-key-hash-sha256:")) && packageName != null) { + allApplicableFacetIds.addAll(getAllFacetIdCandidates(context, packageName, origin)) + val sha256facetId = allApplicableFacetIds.firstOrNull { it.startsWith("android:apk-key-hash-sha256:") } + ?: throw RequestHandlingException(NOT_ALLOWED_ERR, "RP ID $rpId not allowed from origin $origin") + val fp = Base64.decode(sha256facetId.substring(28), HASH_BASE64_FLAGS).toHexString(":") + if (!isAssetLinked(context, rpId, fp, packageName)) { + throw RequestHandlingException(NOT_ALLOWED_ERR, "RP ID $rpId not allowed from origin $origin (expected fingerprint $fp)") } } else { - throw RequestHandlingException(NOT_SUPPORTED_ERR, "Facet $facetId not supported") + throw RequestHandlingException(NOT_SUPPORTED_ERR, "Origin $origin not supported") } val appId = authenticationExtensions?.fidoAppIdExtension?.appId if (appId != null) { @@ -186,11 +187,8 @@ suspend fun RequestOptions.checkIsValid(context: Context, facetId: String, packa if (Uri.parse(appId).host.isNullOrEmpty()) { throw RequestHandlingException(NOT_ALLOWED_ERR, "AppId $appId must have a valid hostname") } - val altFacetId = packageName?.let { getAltFacetId(context, it, facetId) } - if (!isAppIdAllowed(context, appId, facetId, rpId) && - (altFacetId == null || !isAppIdAllowed(context, appId, altFacetId, rpId)) - ) { - throw RequestHandlingException(NOT_ALLOWED_ERR, "AppId $appId not allowed from facet $facetId/$altFacetId") + if (!isAppIdAllowed(context, appId, allApplicableFacetIds, rpId)) { + throw RequestHandlingException(NOT_ALLOWED_ERR, "AppId $appId not allowed from facets [${allApplicableFacetIds.joinToString()}]") } } } @@ -212,34 +210,35 @@ fun getApplicationName(context: Context, options: RequestOptions, callingPackage else -> context.packageManager.getApplicationLabel(callingPackage).toString() } -fun getApkKeyHashFacetId(context: Context, packageName: String): String { - val digest = context.packageManager.getFirstSignatureDigest(packageName, "SHA1") +fun getApkKeyHashOrigin(context: Context, packageName: String): String { + val digest = context.packageManager.getFirstSignatureDigest(packageName, "SHA-256") ?: throw RequestHandlingException(NOT_ALLOWED_ERR, "Unknown package $packageName") return "android:apk-key-hash:${digest.toBase64(HASH_BASE64_FLAGS)}" } -fun getAltFacetId(context: Context, packageName: String, facetId: String): String? { +fun getAllFacetIdCandidates(context: Context, packageName: String, origin: String): List { val firstSignature = context.packageManager.getSignatures(packageName).firstOrNull() ?: throw RequestHandlingException(NOT_ALLOWED_ERR, "Unknown package $packageName") - return when (facetId) { - "android:apk-key-hash:${firstSignature.digest("SHA1").toBase64(HASH_BASE64_FLAGS)}" -> { - "android:apk-key-hash-sha256:${firstSignature.digest("SHA-256").toBase64(HASH_BASE64_FLAGS)}" - } - "android:apk-key-hash-sha256:${firstSignature.digest("SHA-256").toBase64(HASH_BASE64_FLAGS)}" -> { - "android:apk-key-hash:${firstSignature.digest("SHA1").toBase64(HASH_BASE64_FLAGS)}" - } - else -> null - } + val sha1 = firstSignature.digest("SHA1").toBase64(HASH_BASE64_FLAGS) + val sha256 = firstSignature.digest("SHA-256").toBase64(HASH_BASE64_FLAGS) + val candidates = arrayListOf( + "android:apk-key-hash:$sha1", + "android:apk-key-hash:$sha256", + "android:apk-key-hash-sha256:$sha256", + ) + if (!candidates.contains(origin)) + throw RequestHandlingException(NOT_ALLOWED_ERR, "Unknown package $packageName ($origin)") + return candidates } -fun getFacetId(context: Context, options: RequestOptions, callingPackage: String): String = when { +fun getOrigin(context: Context, options: RequestOptions, callingPackage: String): String = when { options is BrowserRequestOptions -> { if (options.origin.scheme == null || options.origin.authority == null) { throw RequestHandlingException(NOT_ALLOWED_ERR, "Bad url ${options.origin}") } "${options.origin.scheme}://${options.origin.authority}" } - else -> getApkKeyHashFacetId(context, callingPackage) + else -> getApkKeyHashOrigin(context, callingPackage) } fun ByteArray.digest(md: String): ByteArray = MessageDigest.getInstance(md).digest(this) @@ -252,7 +251,7 @@ fun getClientDataAndHash( val clientData: ByteArray? var clientDataHash = (options as? BrowserRequestOptions)?.clientDataHash if (clientDataHash == null) { - clientData = options.getWebAuthnClientData(callingPackage, getFacetId(context, options, callingPackage)) + clientData = options.getWebAuthnClientData(callingPackage, getOrigin(context, options, callingPackage)) clientDataHash = clientData.digest("SHA-256") } else { clientData = "".toByteArray() diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/protocol/CredentialId.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/protocol/CredentialId.kt index 445dab191a..4f0bc5471c 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/protocol/CredentialId.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/protocol/CredentialId.kt @@ -5,7 +5,9 @@ package org.microg.gms.fido.core.protocol +import android.util.Base64 import org.microg.gms.fido.core.digest +import org.microg.gms.utils.toBase64 import java.nio.ByteBuffer import java.security.PublicKey @@ -16,7 +18,15 @@ class CredentialId(val type: Byte, val data: ByteArray, val rpId: String, val pu put((rpId.toByteArray() + publicKey.encoded).digest("SHA-256")) }.array() + fun toBase64(): String = encode().toBase64(Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING) + companion object { + + fun decodeTypeAndDataByBase64(base64: String): Pair { + val bytes = Base64.decode(base64, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING) + return decodeTypeAndData(bytes) + } + fun decodeTypeAndData(bytes: ByteArray): Pair { val buffer = ByteBuffer.wrap(bytes) val type = buffer.get() diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/TransportHandler.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/TransportHandler.kt index c7f2baeda1..222c18b4ba 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/TransportHandler.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/TransportHandler.kt @@ -44,7 +44,7 @@ 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): AuthenticatorResponse = + open suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean = false, pin: String? = null, userInfo: String? = null): AuthenticatorResponse = throw RequestHandlingException(ErrorCode.NOT_SUPPORTED_ERR) open fun shouldBeUsedInstantly(options: RequestOptions): Boolean = false diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/nfc/NfcTransportHandler.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/nfc/NfcTransportHandler.kt index 490435c3aa..da0e05b3b2 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/nfc/NfcTransportHandler.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/nfc/NfcTransportHandler.kt @@ -103,7 +103,7 @@ class NfcTransportHandler(private val activity: Activity, callback: TransportHan } - override suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean, pin: String?): AuthenticatorResponse { + override suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean, pin: String?, userInfo: String?): AuthenticatorResponse { val adapter = NfcAdapter.getDefaultAdapter(activity) val newIntentListener = Consumer { if (it?.action != NfcAdapter.ACTION_TECH_DISCOVERED) return@Consumer diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt index dae2197af6..ff72cf5569 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/screenlock/ScreenLockTransportHandler.kt @@ -7,6 +7,7 @@ package org.microg.gms.fido.core.transport.screenlock import android.app.KeyguardManager import android.os.Build.VERSION.SDK_INT +import android.util.Base64 import android.util.Log import androidx.annotation.RequiresApi import androidx.biometric.BiometricPrompt @@ -22,6 +23,7 @@ import org.microg.gms.fido.core.protocol.* import org.microg.gms.fido.core.transport.Transport import org.microg.gms.fido.core.transport.TransportHandler import org.microg.gms.fido.core.transport.TransportHandlerCallback +import org.microg.gms.utils.toBase64 import java.security.Signature import java.security.interfaces.ECPublicKey import kotlin.coroutines.resume @@ -31,6 +33,7 @@ import kotlin.coroutines.resumeWithException class ScreenLockTransportHandler(private val activity: FragmentActivity, callback: TransportHandlerCallback? = null) : TransportHandler(Transport.SCREEN_LOCK, callback) { private val store by lazy { ScreenLockCredentialStore(activity) } + private val database by lazy { Database(activity) } override val isSupported: Boolean get() = activity.getSystemService()?.isDeviceSecure == true @@ -106,8 +109,11 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac callerPackage: String ): AuthenticatorAttestationResponse { if (options.type != RequestOptionsType.REGISTER) throw RequestHandlingException(ErrorCode.INVALID_STATE_ERR) + val knownRegistrationInfo = database.getKnownRegistrationInfo(options.rpId) for (descriptor in options.registerOptions.excludeList.orEmpty()) { - if (store.containsKey(options.rpId, descriptor.id)) { + val credentialBase64 = descriptor.id.toBase64(Base64.NO_WRAP or Base64.NO_PADDING or Base64.URL_SAFE) + val excluded = knownRegistrationInfo.any { it.credential == credentialBase64 } + if (store.containsKey(options.rpId, descriptor.id) || excluded) { throw RequestHandlingException( ErrorCode.NOT_ALLOWED_ERR, "An excluded credential has already been registered with the device" @@ -187,7 +193,8 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac suspend fun sign( options: RequestOptions, - callerPackage: String + callerPackage: String, + userInfo: String? ): AuthenticatorAssertionResponse { if (options.type != RequestOptionsType.SIGN) throw RequestHandlingException(ErrorCode.INVALID_STATE_ERR) val candidates = mutableListOf() @@ -201,6 +208,17 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac // Not in store or unknown id } } + val knownRegistrationInfo = database.getKnownRegistrationInfo(options.rpId) + candidates.ifEmpty { + knownRegistrationInfo.mapNotNull { + val (type, data) = CredentialId.decodeTypeAndDataByBase64(it.credential) + if (type == 1.toByte() && store.containsKey(options.rpId, data)) { + CredentialId(type, data, options.rpId, store.getPublicKey(options.rpId, data)!!) + } else null + }.forEach { + candidates.add(it) + } + } if (candidates.isEmpty()) { // Show a biometric prompt even if no matching key to effectively rate-limit showBiometricPrompt(getApplicationName(activity, options, callerPackage), null) @@ -211,8 +229,11 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac } val (clientData, clientDataHash) = getClientDataAndHash(activity, options, callerPackage) - - val credentialId = candidates.first() + val credentialUserInfo = if (userInfo != null) { + knownRegistrationInfo.firstOrNull { it.userJson == userInfo } + } else knownRegistrationInfo.firstOrNull() + val userHandle = credentialUserInfo?.let { PublicKeyCredentialUserEntity.parseJson(it.userJson).id } + val credentialId = candidates.firstOrNull { credentialUserInfo?.credential != null && credentialUserInfo.credential == it.toBase64() } ?: candidates.first() val keyId = credentialId.data val authenticatorData = getAuthenticatorData(options.rpId, null) @@ -225,15 +246,15 @@ class ScreenLockTransportHandler(private val activity: FragmentActivity, callbac clientData, authenticatorData.encode(), sig, - null + userHandle ) } @RequiresApi(24) - override suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean, pin: String?): AuthenticatorResponse = + override suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean, pin: String?, userInfo: String?): AuthenticatorResponse = when (options.type) { RequestOptionsType.REGISTER -> register(options, callerPackage) - RequestOptionsType.SIGN -> sign(options, callerPackage) + RequestOptionsType.SIGN -> sign(options, callerPackage, userInfo) } override fun shouldBeUsedInstantly(options: RequestOptions): Boolean { diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/usb/UsbTransportHandler.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/usb/UsbTransportHandler.kt index b26c9d8890..9074d74f9c 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/usb/UsbTransportHandler.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/transport/usb/UsbTransportHandler.kt @@ -137,7 +137,7 @@ class UsbTransportHandler(private val context: Context, callback: TransportHandl } } - override suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean, pin: String?): AuthenticatorResponse { + override suspend fun start(options: RequestOptions, callerPackage: String, pinRequested: Boolean, pin: String?, userInfo: String?): AuthenticatorResponse { for (device in context.usbManager?.deviceList?.values.orEmpty()) { val iface = getCtapHidInterface(device) ?: continue try { diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivity.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivity.kt index 09550115fa..74bddc2c06 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivity.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivity.kt @@ -18,9 +18,9 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.commit import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.NavHostFragment -import com.google.android.gms.fido.Fido import com.google.android.gms.fido.Fido.* import com.google.android.gms.fido.fido2.api.common.* +import com.google.android.gms.fido.fido2.api.common.AuthenticationExtensionsPrfOutputs import com.google.android.gms.fido.fido2.api.common.ErrorCode.* import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job @@ -125,15 +125,15 @@ class AuthenticatorActivity : AppCompatActivity(), TransportHandlerCallback { @RequiresApi(24) suspend fun handleRequest(options: RequestOptions, allowInstant: Boolean = true) { try { - val facetId = getFacetId(this, options, callerPackage) - options.checkIsValid(this, facetId, callerPackage) + val origin = getOrigin(this, options, callerPackage) + options.checkIsValid(this, origin, callerPackage) val appName = getApplicationName(this, options, callerPackage) val callerName = packageManager.getApplicationLabel(callerPackage).toString() val requiresPrivilege = options is BrowserRequestOptions && !database.isPrivileged(callerPackage, callerSignature) - Log.d(TAG, "facetId=$facetId, appName=$appName") + Log.d(TAG, "origin=$origin, appName=$appName") // Check if we can directly open screen lock handling if (!requiresPrivilege && allowInstant) { @@ -154,6 +154,7 @@ class AuthenticatorActivity : AppCompatActivity(), TransportHandlerCallback { val next = if (!requiresPrivilege) { val knownRegistrationTransports = mutableSetOf() val allowedTransports = mutableSetOf() + val localSavedUserKey = mutableSetOf() if (options.type == RequestOptionsType.SIGN) { 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)) @@ -176,10 +177,13 @@ class AuthenticatorActivity : AppCompatActivity(), TransportHandlerCallback { } } } + database.getKnownRegistrationInfo(options.rpId).forEach { localSavedUserKey.add(it.userJson) } } val preselectedTransport = knownRegistrationTransports.singleOrNull() ?: allowedTransports.singleOrNull() if (database.wasUsed()) { - when (preselectedTransport) { + if (localSavedUserKey.isNotEmpty()) { + R.id.signInSelectionFragment + } else when (preselectedTransport) { USB -> R.id.usbFragment NFC -> R.id.nfcFragment else -> R.id.transportSelectionFragment @@ -226,14 +230,31 @@ class AuthenticatorActivity : AppCompatActivity(), TransportHandlerCallback { else -> null } val id = rawId?.toBase64(Base64.URL_SAFE, Base64.NO_WRAP, Base64.NO_PADDING) - if (rpId != null && id != null) database.insertKnownRegistration(rpId, id, transport) - finishWithCredential(PublicKeyCredential.Builder() + + if (rpId != null && id != null) { + database.insertKnownRegistration(rpId, id, transport, options?.user) + } + + val prfFirst = rawId?.let { java.security.MessageDigest.getInstance("SHA-256").digest(it) }?.copyOf(32) + val prfOutputs = prfFirst?.let { AuthenticationExtensionsPrfOutputs(true, it, null) } + + val clientExtResults = AuthenticationExtensionsClientOutputs( + null, + null, + AuthenticationExtensionsCredPropsOutputs(true), + prfOutputs, + null + ) + + val pkc = PublicKeyCredential.Builder() .setResponse(response) .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) .build() - ) + + finishWithCredential(pkc) } private fun finishWithCredential(publicKeyCredential: PublicKeyCredential) { @@ -258,10 +279,10 @@ class AuthenticatorActivity : AppCompatActivity(), TransportHandlerCallback { } @RequiresApi(24) - fun startTransportHandling(transport: Transport, instant: Boolean = false, pinRequested: Boolean = false, authenticatorPin: String? = null): Job = lifecycleScope.launchWhenResumed { + fun startTransportHandling(transport: Transport, instant: Boolean = false, pinRequested: Boolean = false, authenticatorPin: String? = null, userInfo: String? = null): Job = lifecycleScope.launchWhenResumed { val options = options ?: return@launchWhenResumed try { - finishWithSuccessResponse(getTransportHandler(transport)!!.start(options, callerPackage, pinRequested, authenticatorPin), transport) + finishWithSuccessResponse(getTransportHandler(transport)!!.start(options, callerPackage, pinRequested, authenticatorPin, userInfo), transport) } catch (e: SecurityException) { Log.w(TAG, e) if (instant) { diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivityFragment.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivityFragment.kt index 92c576c5c8..c8718dbabf 100644 --- a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivityFragment.kt +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/AuthenticatorActivityFragment.kt @@ -30,7 +30,8 @@ abstract class AuthenticatorActivityFragment : Fragment() { val options: RequestOptions? get() = authenticatorActivity?.options - fun startTransportHandling(transport: Transport) = authenticatorActivity?.startTransportHandling(transport, pinRequested = pinViewModel.pinRequest, authenticatorPin = pinViewModel.pin) + fun startTransportHandling(transport: Transport, userInfo: String? = null) = + authenticatorActivity?.startTransportHandling(transport, pinRequested = pinViewModel.pinRequest, authenticatorPin = pinViewModel.pin, userInfo = userInfo) fun shouldStartTransportInstantly(transport: Transport) = authenticatorActivity?.shouldStartTransportInstantly(transport) == true abstract override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? diff --git a/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/SignInSelectionFragment.kt b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/SignInSelectionFragment.kt new file mode 100644 index 0000000000..f2764511a2 --- /dev/null +++ b/play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/SignInSelectionFragment.kt @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.fido.core.ui + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.gms.common.images.ImageManager +import com.google.android.gms.fido.fido2.api.common.ErrorCode +import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialUserEntity +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.databinding.FidoSignInSelectionFragmentBinding +import org.microg.gms.fido.core.rpId +import org.microg.gms.fido.core.transport.Transport +import androidx.core.view.isGone + +class SignInSelectionFragment : AuthenticatorActivityFragment() { + private lateinit var binding: FidoSignInSelectionFragmentBinding + + private val database by lazy { Database(requireContext()) } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = FidoSignInSelectionFragmentBinding.inflate(inflater, container, false) + binding.data = data + binding.signInKeyRecycler.layoutManager = LinearLayoutManager(requireContext()) + binding.signInKeyBack.setOnClickListener { requireActivity().finish() } + return binding.root.apply { isGone } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val rpId = options?.rpId + if (rpId.isNullOrEmpty()) { + authenticatorActivity?.finishWithError(ErrorCode.UNKNOWN_ERR, "Missing rpId") + return + } + val knownRegistrationInfo = database.getKnownRegistrationInfo(rpId) + if (knownRegistrationInfo.isEmpty()) { + findNavController().navigate(R.id.openWelcomeFragment) + } else if (knownRegistrationInfo.size == 1) { + val info = knownRegistrationInfo.first() + startTransportHandling(info.transport, info.userJson) + } else { + binding.root.apply { isVisible } + binding.signInKeyRecycler.adapter = SignInKeyAdapter(knownRegistrationInfo) { user, transport -> + startTransportHandling(transport, user) + } + } + } +} + +internal class SignInKeyAdapter(val data: List, val onKeyClick: (String, Transport) -> Unit) : + RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SignInHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.fido_sign_in_item_layout, parent, false) + return SignInHolder(view) + } + + override fun onBindViewHolder(holder: SignInHolder, position: Int) { + val item = data[position] + val user = PublicKeyCredentialUserEntity.parseJson(item.userJson) + holder.signInKeyName.text = user.displayName + holder.signInKeyEmail.text = user.name + user.icon?.let { ImageManager.create(holder.itemView.context).loadImage(it, holder.signInKeyLogo) } + holder.itemView.setOnClickListener { onKeyClick(item.userJson, item.transport) } + } + + override fun getItemCount(): Int { + return data.size + } + + inner class SignInHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val signInKeyLogo: ImageView = itemView.findViewById(R.id.sign_in_key_logo) + val signInKeyName: TextView = itemView.findViewById(R.id.sign_in_key_name) + val signInKeyEmail: TextView = itemView.findViewById(R.id.sign_in_key_email) + } +} diff --git a/play-services-fido/core/src/main/res/drawable/ic_fido_close_btn.xml b/play-services-fido/core/src/main/res/drawable/ic_fido_close_btn.xml new file mode 100644 index 0000000000..599b58a261 --- /dev/null +++ b/play-services-fido/core/src/main/res/drawable/ic_fido_close_btn.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/play-services-fido/core/src/main/res/layout/fido_sign_in_item_layout.xml b/play-services-fido/core/src/main/res/layout/fido_sign_in_item_layout.xml new file mode 100644 index 0000000000..e81501f813 --- /dev/null +++ b/play-services-fido/core/src/main/res/layout/fido_sign_in_item_layout.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + diff --git a/play-services-fido/core/src/main/res/layout/fido_sign_in_selection_fragment.xml b/play-services-fido/core/src/main/res/layout/fido_sign_in_selection_fragment.xml new file mode 100644 index 0000000000..c801fbc54d --- /dev/null +++ b/play-services-fido/core/src/main/res/layout/fido_sign_in_selection_fragment.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/play-services-fido/core/src/main/res/navigation/nav_fido_authenticator.xml b/play-services-fido/core/src/main/res/navigation/nav_fido_authenticator.xml index 81fe4bf83e..8e1143b1ff 100644 --- a/play-services-fido/core/src/main/res/navigation/nav_fido_authenticator.xml +++ b/play-services-fido/core/src/main/res/navigation/nav_fido_authenticator.xml @@ -77,4 +77,13 @@ android:name="org.microg.gms.fido.core.ui.PinFragment" tools:layout="@layout/fido_pin_fragment" /> + + + + diff --git a/play-services-fido/core/src/main/res/values-zh-rCN/strings.xml b/play-services-fido/core/src/main/res/values-zh-rCN/strings.xml index 466f7c9b14..06e774b30d 100644 --- a/play-services-fido/core/src/main/res/values-zh-rCN/strings.xml +++ b/play-services-fido/core/src/main/res/values-zh-rCN/strings.xml @@ -28,4 +28,7 @@ 取消 4 到 63 个字符 更改如何使用你的安全密钥 + 选择账号 + 以继续使用 %1$s + 通行密钥 diff --git a/play-services-fido/core/src/main/res/values-zh-rTW/strings.xml b/play-services-fido/core/src/main/res/values-zh-rTW/strings.xml index 80e1b15c10..fbb7329767 100644 --- a/play-services-fido/core/src/main/res/values-zh-rTW/strings.xml +++ b/play-services-fido/core/src/main/res/values-zh-rTW/strings.xml @@ -25,4 +25,7 @@ 透過 USB 使用安全金鑰 使用此裝置的螢幕鎖定功能 更改使用安全金鑰的方式 + 選擇帳號 + 以繼續使用 %1$s + 通行金鑰 diff --git a/play-services-fido/core/src/main/res/values/strings.xml b/play-services-fido/core/src/main/res/values/strings.xml index 8872009105..be756989ed 100644 --- a/play-services-fido/core/src/main/res/values/strings.xml +++ b/play-services-fido/core/src/main/res/values/strings.xml @@ -30,4 +30,7 @@ Cancel Wrong PIN entered! Change how to use your security key + Select an account + to continue using %1$s + Passkey diff --git a/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/AuthenticationExtensions.java b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/AuthenticationExtensions.java index d5a97b611c..81e5406841 100644 --- a/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/AuthenticationExtensions.java +++ b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/AuthenticationExtensions.java @@ -9,16 +9,20 @@ package com.google.android.gms.fido.fido2.api.common; import android.os.Parcel; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; + import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + import org.microg.gms.common.Hide; import org.microg.gms.common.PublicApi; import org.microg.gms.utils.ToStringHelper; import java.util.Arrays; +import java.util.Objects; /** * Represents extensions that can be passed into FIDO2 APIs. This container class corresponds to the additional @@ -39,12 +43,52 @@ public class AuthenticationExtensions extends AbstractSafeParcelable { @Field(value = 4, getterName = "getUserVerificationMethodExtension") @Nullable private UserVerificationMethodExtension userVerificationMethodExtension; + @Field(value = 5, getterName = "getGoogleMultiAssertionExtension") + @Nullable + private GoogleMultiAssertionExtension googleMultiAssertionExtension; + @Field(value = 6, getterName = "getGoogleSessionIdExtension") + @Nullable + private GoogleSessionIdExtension googleSessionIdExtension; + @Field(value = 7, getterName = "getGoogleSilentVerificationExtension") + @Nullable + private GoogleSilentVerificationExtension googleSilentVerificationExtension; + @Field(value = 8, getterName = "getDevicePublicKeyExtension") + @Nullable + private DevicePublicKeyExtension devicePublicKeyExtension; + @Field(value = 9, getterName = "getGoogleTunnelServerIdExtension") + @Nullable + private GoogleTunnelServerIdExtension googleTunnelServerIdExtension; + @Field(value = 10, getterName = "getGoogleThirdPartyPaymentExtension") + @Nullable + private GoogleThirdPartyPaymentExtension googleThirdPartyPaymentExtension; + @Field(value = 11, getterName = "getPrfExtension") + @Nullable + private PrfExtension prfExtension; + @Field(value = 12, getterName = "getSimpleTransactionAuthorizationExtension") + @Nullable + private SimpleTransactionAuthorizationExtension simpleTransactionAuthorizationExtension; + @Field(value = 13, getterName = "getHmacSecretExtension") + @Nullable + private HmacSecretExtension hmacSecretExtension; + @Field(value = 14, getterName = "getPaymentExtension") + @Nullable + private PaymentExtension paymentExtension; @Constructor - public AuthenticationExtensions(@Param(2) @Nullable FidoAppIdExtension fidoAppIdExtension, @Param(3) @Nullable CableAuthenticationExtension cableAuthenticationExtension, @Param(4) @Nullable UserVerificationMethodExtension userVerificationMethodExtension) { + public AuthenticationExtensions(@Param(2) @Nullable FidoAppIdExtension fidoAppIdExtension, @Param(3) @Nullable CableAuthenticationExtension cableAuthenticationExtension, @Param(4) @Nullable UserVerificationMethodExtension userVerificationMethodExtension, @Param(5) @Nullable GoogleMultiAssertionExtension googleMultiAssertionExtension, @Param(6) @Nullable GoogleSessionIdExtension googleSessionIdExtension, @Param(7) @Nullable GoogleSilentVerificationExtension googleSilentVerificationExtension, @Param(8) @Nullable DevicePublicKeyExtension devicePublicKeyExtension, @Param(9) @Nullable GoogleTunnelServerIdExtension googleTunnelServerIdExtension, @Param(10) @Nullable GoogleThirdPartyPaymentExtension googleThirdPartyPaymentExtension, @Param(11) @Nullable PrfExtension prfExtension, @Param(12) @Nullable SimpleTransactionAuthorizationExtension simpleTransactionAuthorizationExtension, @Param(13) @Nullable HmacSecretExtension hmacSecretExtension, @Param(14) @Nullable PaymentExtension paymentExtension) { this.fidoAppIdExtension = fidoAppIdExtension; this.cableAuthenticationExtension = cableAuthenticationExtension; this.userVerificationMethodExtension = userVerificationMethodExtension; + this.googleMultiAssertionExtension = googleMultiAssertionExtension; + this.googleSessionIdExtension = googleSessionIdExtension; + this.googleSilentVerificationExtension = googleSilentVerificationExtension; + this.devicePublicKeyExtension = devicePublicKeyExtension; + this.googleTunnelServerIdExtension = googleTunnelServerIdExtension; + this.googleThirdPartyPaymentExtension = googleThirdPartyPaymentExtension; + this.prfExtension = prfExtension; + this.simpleTransactionAuthorizationExtension = simpleTransactionAuthorizationExtension; + this.hmacSecretExtension = hmacSecretExtension; + this.paymentExtension = paymentExtension; } @Nullable @@ -63,6 +107,56 @@ public UserVerificationMethodExtension getUserVerificationMethodExtension() { return userVerificationMethodExtension; } + @Nullable + public GoogleMultiAssertionExtension getGoogleMultiAssertionExtension() { + return googleMultiAssertionExtension; + } + + @Nullable + public GoogleSessionIdExtension getGoogleSessionIdExtension() { + return googleSessionIdExtension; + } + + @Nullable + public GoogleSilentVerificationExtension getGoogleSilentVerificationExtension() { + return googleSilentVerificationExtension; + } + + @Nullable + public DevicePublicKeyExtension getDevicePublicKeyExtension() { + return devicePublicKeyExtension; + } + + @Nullable + public GoogleTunnelServerIdExtension getGoogleTunnelServerIdExtension() { + return googleTunnelServerIdExtension; + } + + @Nullable + public GoogleThirdPartyPaymentExtension getGoogleThirdPartyPaymentExtension() { + return googleThirdPartyPaymentExtension; + } + + @Nullable + public PrfExtension getPrfExtension() { + return prfExtension; + } + + @Nullable + public SimpleTransactionAuthorizationExtension getSimpleTransactionAuthorizationExtension() { + return simpleTransactionAuthorizationExtension; + } + + @Nullable + public HmacSecretExtension getHmacSecretExtension() { + return hmacSecretExtension; + } + + @Nullable + public PaymentExtension getPaymentExtension() { + return paymentExtension; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -70,25 +164,29 @@ public boolean equals(Object o) { AuthenticationExtensions that = (AuthenticationExtensions) o; - if (fidoAppIdExtension != null ? !fidoAppIdExtension.equals(that.fidoAppIdExtension) : that.fidoAppIdExtension != null) - return false; - if (cableAuthenticationExtension != null ? !cableAuthenticationExtension.equals(that.cableAuthenticationExtension) : that.cableAuthenticationExtension != null) - return false; - return userVerificationMethodExtension != null ? userVerificationMethodExtension.equals(that.userVerificationMethodExtension) : that.userVerificationMethodExtension == null; + if (!Objects.equals(fidoAppIdExtension, that.fidoAppIdExtension)) return false; + if (!Objects.equals(cableAuthenticationExtension, that.cableAuthenticationExtension)) return false; + if (!Objects.equals(userVerificationMethodExtension, that.userVerificationMethodExtension)) return false; + if (!Objects.equals(googleMultiAssertionExtension, that.googleMultiAssertionExtension)) return false; + if (!Objects.equals(googleSessionIdExtension, that.googleSessionIdExtension)) return false; + if (!Objects.equals(googleSilentVerificationExtension, that.googleSilentVerificationExtension)) return false; + if (!Objects.equals(devicePublicKeyExtension, that.devicePublicKeyExtension)) return false; + if (!Objects.equals(googleTunnelServerIdExtension, that.googleTunnelServerIdExtension)) return false; + if (!Objects.equals(googleThirdPartyPaymentExtension, that.googleThirdPartyPaymentExtension)) return false; + if (!Objects.equals(prfExtension, that.prfExtension)) return false; + if (!Objects.equals(simpleTransactionAuthorizationExtension, that.simpleTransactionAuthorizationExtension)) return false; + if (!Objects.equals(hmacSecretExtension, that.hmacSecretExtension)) return false; + return Objects.equals(paymentExtension, that.paymentExtension); } @Override public int hashCode() { - return Arrays.hashCode(new Object[]{fidoAppIdExtension, cableAuthenticationExtension, userVerificationMethodExtension}); + return Arrays.hashCode(new Object[]{fidoAppIdExtension, cableAuthenticationExtension, userVerificationMethodExtension, googleMultiAssertionExtension, googleSessionIdExtension, googleSilentVerificationExtension, devicePublicKeyExtension, googleTunnelServerIdExtension, googleThirdPartyPaymentExtension, prfExtension, simpleTransactionAuthorizationExtension, hmacSecretExtension, paymentExtension}); } @Override public String toString() { - return ToStringHelper.name("AuthenticationExtensions") - .field("fidoAppIdExtension", fidoAppIdExtension != null ? fidoAppIdExtension.getAppId() : null) - .field("cableAuthenticationExtension", cableAuthenticationExtension) - .field("userVerificationMethodExtension", userVerificationMethodExtension != null ? userVerificationMethodExtension.getUvm() : null) - .end(); + return ToStringHelper.name("AuthenticationExtensions").field("fidoAppIdExtension", fidoAppIdExtension != null ? fidoAppIdExtension.getAppId() : null).field("cableAuthenticationExtension", cableAuthenticationExtension).field("userVerificationMethodExtension", userVerificationMethodExtension != null ? userVerificationMethodExtension.getUvm() : null).field("googleMultiAssertionExtension", googleMultiAssertionExtension).field("googleSessionIdExtension", googleSessionIdExtension).field("googleSilentVerificationExtension", googleSilentVerificationExtension).field("devicePublicKeyExtension", devicePublicKeyExtension).field("googleTunnelServerIdExtension", googleTunnelServerIdExtension).field("googleThirdPartyPaymentExtension", googleThirdPartyPaymentExtension).field("prfExtension", prfExtension).field("simpleTransactionAuthorizationExtension", simpleTransactionAuthorizationExtension).field("hmacSecretExtension", hmacSecretExtension).field("paymentExtension", paymentExtension).end(); } /** @@ -129,7 +227,7 @@ public Builder setUserVerificationMethodExtension(@Nullable UserVerificationMeth */ @NonNull public AuthenticationExtensions build() { - return new AuthenticationExtensions(fidoAppIdExtension, null, userVerificationMethodExtension); + return new AuthenticationExtensions(fidoAppIdExtension, null, userVerificationMethodExtension, null, null, null, null, null, null, null, null, null, null); } } diff --git a/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/AuthenticationExtensionsClientOutputs.java b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/AuthenticationExtensionsClientOutputs.java index 53c90f408b..26b2fd4ebf 100644 --- a/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/AuthenticationExtensionsClientOutputs.java +++ b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/AuthenticationExtensionsClientOutputs.java @@ -9,15 +9,19 @@ package com.google.android.gms.fido.fido2.api.common; import android.os.Parcel; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; + import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; import com.google.android.gms.common.internal.safeparcel.SafeParcelableSerializer; + import org.microg.gms.common.PublicApi; import java.util.Arrays; +import java.util.Objects; /** * This container class represents client output for extensions that can be passed into FIDO2 APIs. @@ -29,9 +33,29 @@ public class AuthenticationExtensionsClientOutputs extends AbstractSafeParcelabl @Nullable private UvmEntries uvmEntries; + @Field(value = 2, getterName = "getDevicePublicKeyOutputs") + @Nullable + private AuthenticationExtensionsDevicePublicKeyOutputs devicePublicKeyOutputs; + + @Field(value = 3, getterName = "getCredProps") + @Nullable + private AuthenticationExtensionsCredPropsOutputs credProps; + + @Field(value = 4, getterName = "getPrfOutputs") + @Nullable + private AuthenticationExtensionsPrfOutputs prfOutputs; + + @Field(value = 5, getterName = "getTxAuthSimple") + @Nullable + private String txAuthSimple; + @Constructor - AuthenticationExtensionsClientOutputs(@Param(1)@Nullable UvmEntries uvmEntries) { + 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) { this.uvmEntries = uvmEntries; + this.devicePublicKeyOutputs = devicePublicKeyOutputs; + this.credProps = credProps; + this.prfOutputs = prfOutputs; + this.txAuthSimple = txAuthSimple; } @Nullable @@ -39,6 +63,26 @@ public UvmEntries getUvmEntries() { return uvmEntries; } + @Nullable + public AuthenticationExtensionsDevicePublicKeyOutputs getDevicePublicKeyOutputs() { + return devicePublicKeyOutputs; + } + + @Nullable + public AuthenticationExtensionsCredPropsOutputs getCredProps() { + return credProps; + } + + @Nullable + public AuthenticationExtensionsPrfOutputs getPrfOutputs() { + return prfOutputs; + } + + @Nullable + public String getTxAuthSimple() { + return txAuthSimple; + } + /** * Serializes the {@link AuthenticationExtensionsClientOutputs} to bytes. * Use {@link #deserializeFromBytes(byte[])} to deserialize. @@ -64,13 +108,12 @@ public boolean equals(Object o) { if (!(o instanceof AuthenticationExtensionsClientOutputs)) return false; AuthenticationExtensionsClientOutputs that = (AuthenticationExtensionsClientOutputs) o; - - return uvmEntries != null ? uvmEntries.equals(that.uvmEntries) : that.uvmEntries == null; + 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)); } @Override public int hashCode() { - return Arrays.hashCode(new Object[]{uvmEntries}); + return Arrays.hashCode(new Object[]{uvmEntries, devicePublicKeyOutputs, credProps, prfOutputs, txAuthSimple}); } /** @@ -79,6 +122,14 @@ public int hashCode() { public static class Builder { @Nullable private UvmEntries uvmEntries; + @Nullable + private AuthenticationExtensionsDevicePublicKeyOutputs devicePublicKeyOutputs; + @Nullable + private AuthenticationExtensionsCredPropsOutputs credProps; + @Nullable + private AuthenticationExtensionsPrfOutputs prfOutputs; + @Nullable + private String txAuthSimple; /** * The constructor of {@link AuthenticationExtensionsClientOutputs.Builder}. @@ -95,12 +146,43 @@ public Builder setUserVerificationMethodEntries(@Nullable UvmEntries uvmEntries) return this; } + /** + * Set Device Public Key client outputs + */ + public Builder setDevicePublicKeyOutputs(@Nullable AuthenticationExtensionsDevicePublicKeyOutputs dpkOutputs) { + this.devicePublicKeyOutputs = dpkOutputs; + return this; + } + + /** + * Set Credential Properties client outputs (e.g., rk=true) + */ + public Builder setCredProps(@Nullable AuthenticationExtensionsCredPropsOutputs credProps) { + this.credProps = credProps; + return this; + } + + /** + * Set PRF client outputs + */ + public Builder setPrfOutputs(@Nullable AuthenticationExtensionsPrfOutputs prfOutputs) { + this.prfOutputs = prfOutputs; + return this; + } + + /** + * Set txAuthSimple string + */ + public Builder setTxAuthSimple(@Nullable String txAuthSimple) { + this.txAuthSimple = txAuthSimple; + return this; + } + /** * Builds the {@link AuthenticationExtensionsClientOutputs} object. */ - @NonNull public AuthenticationExtensionsClientOutputs build() { - return new AuthenticationExtensionsClientOutputs(uvmEntries); + return new AuthenticationExtensionsClientOutputs(uvmEntries, devicePublicKeyOutputs, credProps, prfOutputs, txAuthSimple); } } diff --git a/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/AuthenticationExtensionsDevicePublicKeyOutputs.java b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/AuthenticationExtensionsDevicePublicKeyOutputs.java new file mode 100644 index 0000000000..55aeefc386 --- /dev/null +++ b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/AuthenticationExtensionsDevicePublicKeyOutputs.java @@ -0,0 +1,111 @@ +/* + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fido.fido2.api.common; + +import android.os.Parcel; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableSerializer; + +import org.microg.gms.common.PublicApi; +import org.microg.gms.utils.ToStringHelper; + +import java.util.Arrays; + +@PublicApi +@SafeParcelable.Class +public class AuthenticationExtensionsDevicePublicKeyOutputs extends AbstractSafeParcelable { + + @Field(value = 1, getterName = "getDevicePublicKey") + @Nullable + private final byte[] devicePublicKey; + + @Field(value = 2, getterName = "getSignature") + @Nullable + private final byte[] signature; + + @Constructor + public AuthenticationExtensionsDevicePublicKeyOutputs(@Param(1) byte[] devicePublicKey, @Param(2) byte[] signature) { + this.devicePublicKey = devicePublicKey; + this.signature = signature; + } + + @Nullable + public byte[] getDevicePublicKey() { + return devicePublicKey; + } + + @Nullable + public byte[] getSignature() { + return signature; + } + + @PublicApi + public static class Builder { + @Nullable + private byte[] devicePublicKey; + @Nullable + private byte[] signature; + + public Builder() { + } + + public Builder setDevicePublicKey(@Nullable byte[] devicePublicKey) { + this.devicePublicKey = devicePublicKey; + return this; + } + + public Builder setSignature(@Nullable byte[] signature) { + this.signature = signature; + return this; + } + + public AuthenticationExtensionsDevicePublicKeyOutputs build() { + return new AuthenticationExtensionsDevicePublicKeyOutputs(devicePublicKey, signature); + } + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + @PublicApi + @NonNull + public byte[] serializeToBytes() { + return SafeParcelableSerializer.serializeToBytes(this); + } + + @PublicApi + @NonNull + public static AuthenticationExtensionsDevicePublicKeyOutputs deserializeFromBytes(@NonNull byte[] serializedBytes) { + return SafeParcelableSerializer.deserializeFromBytes(serializedBytes, CREATOR); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof AuthenticationExtensionsDevicePublicKeyOutputs)) return false; + AuthenticationExtensionsDevicePublicKeyOutputs that = (AuthenticationExtensionsDevicePublicKeyOutputs) o; + return Arrays.equals(devicePublicKey, that.devicePublicKey) && Arrays.equals(signature, that.signature); + } + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[]{Arrays.hashCode(devicePublicKey), Arrays.hashCode(signature)}); + } + + @Override + public String toString() { + return ToStringHelper.name("AuthenticationExtensionsDevicePublicKeyOutputs").field("devicePublicKey", devicePublicKey != null ? devicePublicKey.length : null).field("signature", signature != null ? signature.length : null).end(); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(AuthenticationExtensionsDevicePublicKeyOutputs.class); +} diff --git a/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/AuthenticationExtensionsPrfOutputs.java b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/AuthenticationExtensionsPrfOutputs.java new file mode 100644 index 0000000000..dd9cd5450b --- /dev/null +++ b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/AuthenticationExtensionsPrfOutputs.java @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fido.fido2.api.common; + +import android.os.Parcel; +import android.util.Base64; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +import org.microg.gms.utils.ToStringHelper; + +@SafeParcelable.Class +public class AuthenticationExtensionsPrfOutputs extends AbstractSafeParcelable { + + @Field(value = 1, getterName = "isEnabled") + private final boolean enabled; + + @Field(value = 2, getterName = "getFirst") + @Nullable + private final byte[] first; + + @Field(value = 3, getterName = "getSecond") + @Nullable + private final byte[] second; + + @Constructor + public AuthenticationExtensionsPrfOutputs(@Param(1) boolean enabled, @Param(2) @Nullable byte[] first, @Param(3) @Nullable byte[] second) { + this.enabled = enabled; + this.first = first; + this.second = second; + } + + public boolean isEnabled() { + return enabled; + } + + @Nullable + public byte[] getFirst() { + return first; + } + + @Nullable + public byte[] getSecond() { + return second; + } + + private String b64url(byte[] v) { + return Base64.encodeToString(v, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(AuthenticationExtensionsPrfOutputs.class); + + @Override + public String toString() { + return ToStringHelper.name("AuthenticationExtensionsPrfOutputs").field("enabled", enabled).field("first", first).field("second", second).end(); + } +} diff --git a/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/CredentialPropertiesOutput.java b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/CredentialPropertiesOutput.java new file mode 100644 index 0000000000..53f215c366 --- /dev/null +++ b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/CredentialPropertiesOutput.java @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fido.fido2.api.common; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +import org.microg.gms.common.PublicApi; + +import java.util.Arrays; + +@PublicApi +@SafeParcelable.Class +public class CredentialPropertiesOutput extends AbstractSafeParcelable { + @Field(value = 1, getterName = "isResidentKey") + private final boolean residentKey; + + @Constructor + public CredentialPropertiesOutput(@Param(1) boolean residentKey) { + this.residentKey = residentKey; + } + + public boolean isResidentKey() { + return residentKey; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof CredentialPropertiesOutput)) return false; + CredentialPropertiesOutput that = (CredentialPropertiesOutput) o; + return residentKey == that.residentKey; + } + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[]{residentKey}); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = findCreator(CredentialPropertiesOutput.class); +} diff --git a/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/DevicePublicKeyExtension.java b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/DevicePublicKeyExtension.java new file mode 100644 index 0000000000..fb6b3ed2a3 --- /dev/null +++ b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/DevicePublicKeyExtension.java @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fido.fido2.api.common; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +import org.microg.gms.common.PublicApi; +import org.microg.gms.utils.ToStringHelper; + +@PublicApi +@SafeParcelable.Class +public class DevicePublicKeyExtension extends AbstractSafeParcelable { + @Field(value = 1, getterName = "isDevicePublicKey") + private final boolean devicePublicKey; + + @Constructor + public DevicePublicKeyExtension(@Param(1) boolean devicePublicKey) { + this.devicePublicKey = devicePublicKey; + } + + public boolean isDevicePublicKey() { + return devicePublicKey; + } + + @Override + public String toString() { + return ToStringHelper.name("DevicePublicKeyExtension").field("isDevicePublicKey", devicePublicKey).end(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = AbstractSafeParcelable.findCreator(DevicePublicKeyExtension.class); +} diff --git a/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/GoogleMultiAssertionExtension.java b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/GoogleMultiAssertionExtension.java new file mode 100644 index 0000000000..7d1696f87e --- /dev/null +++ b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/GoogleMultiAssertionExtension.java @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fido.fido2.api.common; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +import org.microg.gms.common.PublicApi; + +@PublicApi +@SafeParcelable.Class +public class GoogleMultiAssertionExtension extends AbstractSafeParcelable { + @Field(value = 1, getterName = "isRequestForMultiAssertion") + private final boolean requestForMultiAssertion; + + @Constructor + public GoogleMultiAssertionExtension(@Param(1) boolean requestForMultiAssertion) { + this.requestForMultiAssertion = requestForMultiAssertion; + } + + public boolean isRequestForMultiAssertion() { + return requestForMultiAssertion; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = AbstractSafeParcelable.findCreator(GoogleMultiAssertionExtension.class); +} diff --git a/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/GoogleSessionIdExtension.java b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/GoogleSessionIdExtension.java new file mode 100644 index 0000000000..e0c96b1882 --- /dev/null +++ b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/GoogleSessionIdExtension.java @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fido.fido2.api.common; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +import org.microg.gms.common.PublicApi; +import org.microg.gms.utils.ToStringHelper; + +@PublicApi +@SafeParcelable.Class +public class GoogleSessionIdExtension extends AbstractSafeParcelable { + @Field(value = 1, getterName = "getSessionId") + private final long sessionId; + + @Constructor + public GoogleSessionIdExtension(@Param(1) long sessionId) { + this.sessionId = sessionId; + } + + public long getSessionId() { + return sessionId; + } + + @Override + public String toString() { + return ToStringHelper.name("GoogleSessionIdExtension").field("sessionId", sessionId).end(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = AbstractSafeParcelable.findCreator(GoogleSessionIdExtension.class); +} diff --git a/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/GoogleSilentVerificationExtension.java b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/GoogleSilentVerificationExtension.java new file mode 100644 index 0000000000..ebf994784e --- /dev/null +++ b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/GoogleSilentVerificationExtension.java @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fido.fido2.api.common; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +import org.microg.gms.common.PublicApi; +import org.microg.gms.utils.ToStringHelper; + +@PublicApi +@SafeParcelable.Class +public class GoogleSilentVerificationExtension extends AbstractSafeParcelable { + @Field(value = 1, getterName = "isSilentVerification") + private final boolean silentVerification; + + @Constructor + public GoogleSilentVerificationExtension(@Param(1) boolean silentVerification) { + this.silentVerification = silentVerification; + } + + public boolean isSilentVerification() { + return silentVerification; + } + + @Override + public String toString() { + return ToStringHelper.name("GoogleSilentVerificationExtension").field("silentVerification", silentVerification).end(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = AbstractSafeParcelable.findCreator(GoogleSilentVerificationExtension.class); +} diff --git a/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/GoogleThirdPartyPaymentExtension.java b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/GoogleThirdPartyPaymentExtension.java new file mode 100644 index 0000000000..fd2cc31ad0 --- /dev/null +++ b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/GoogleThirdPartyPaymentExtension.java @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fido.fido2.api.common; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +import org.microg.gms.common.PublicApi; +import org.microg.gms.utils.ToStringHelper; + +@PublicApi +@SafeParcelable.Class +public class GoogleThirdPartyPaymentExtension extends AbstractSafeParcelable { + @Field(value = 1, getterName = "isThirdPartyPayment") + private final boolean thirdPartyPayment; + + @Constructor + public GoogleThirdPartyPaymentExtension(@Param(1) boolean thirdPartyPayment) { + this.thirdPartyPayment = thirdPartyPayment; + } + + public boolean isThirdPartyPayment() { + return thirdPartyPayment; + } + + @Override + public String toString() { + return ToStringHelper.name("GoogleThirdPartyPaymentExtension").field("thirdPartyPayment", thirdPartyPayment).end(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = AbstractSafeParcelable.findCreator(GoogleThirdPartyPaymentExtension.class); +} diff --git a/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/GoogleTunnelServerIdExtension.java b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/GoogleTunnelServerIdExtension.java new file mode 100644 index 0000000000..5a56cb7c0e --- /dev/null +++ b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/GoogleTunnelServerIdExtension.java @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fido.fido2.api.common; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +import org.microg.gms.common.PublicApi; +import org.microg.gms.utils.ToStringHelper; + +@PublicApi +@SafeParcelable.Class +public class GoogleTunnelServerIdExtension extends AbstractSafeParcelable { + @Field(value = 1, getterName = "getTunnelServerId") + @NonNull + private final String tunnelServerId; + + @Constructor + public GoogleTunnelServerIdExtension(@Param(1) @NonNull String tunnelServerId) { + this.tunnelServerId = tunnelServerId; + } + + @NonNull + public String getTunnelServerId() { + return tunnelServerId; + } + + @Override + public String toString() { + return ToStringHelper.name("GoogleTunnelServerIdExtension").field("tunnelServerId", tunnelServerId).end(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = AbstractSafeParcelable.findCreator(GoogleTunnelServerIdExtension.class); +} diff --git a/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/HmacSecretExtension.java b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/HmacSecretExtension.java new file mode 100644 index 0000000000..cd1930b2af --- /dev/null +++ b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/HmacSecretExtension.java @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fido.fido2.api.common; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +import org.microg.gms.common.PublicApi; +import org.microg.gms.utils.ToStringHelper; + +@PublicApi +@SafeParcelable.Class +public class HmacSecretExtension extends AbstractSafeParcelable { + + @Field(value = 1, getterName = "getCoseKeyAgreement") + private final byte[] coseKeyAgreement; + + @Field(value = 2, getterName = "getSaltEnc") + private final byte[] saltEnc; + + @Field(value = 3, getterName = "getSaltAuth") + private final byte[] saltAuth; + + @Field(value = 4, getterName = "getPinUvAuthProtocol") + private final int pinUvAuthProtocol; + + @Constructor + public HmacSecretExtension(@Param(1) byte[] coseKeyAgreement, @Param(2) byte[] saltEnc, @Param(3) byte[] saltAuth, @Param(4) int pinUvAuthProtocol) { + this.coseKeyAgreement = coseKeyAgreement; + this.saltEnc = saltEnc; + this.saltAuth = saltAuth; + this.pinUvAuthProtocol = pinUvAuthProtocol; + } + + public byte[] getCoseKeyAgreement() { + return coseKeyAgreement; + } + + public byte[] getSaltEnc() { + return saltEnc; + } + + public byte[] getSaltAuth() { + return saltAuth; + } + + public int getPinUvAuthProtocol() { + return pinUvAuthProtocol; + } + + @Override + public String toString() { + return ToStringHelper.name("HmacSecretExtension").field("coseKeyAgreement", coseKeyAgreement == null ? "" : coseKeyAgreement.length).field("saltEnc", saltEnc == null ? "" : saltEnc.length).field("saltAuth", saltAuth == null ? "" : saltAuth.length).field("pinUvAuthProtocol", pinUvAuthProtocol).end(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = AbstractSafeParcelable.findCreator(HmacSecretExtension.class); +} diff --git a/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/PaymentExtension.java b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/PaymentExtension.java new file mode 100644 index 0000000000..0d9eac31bd --- /dev/null +++ b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/PaymentExtension.java @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fido.fido2.api.common; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +import org.microg.gms.common.PublicApi; +import org.microg.gms.utils.ToStringHelper; + +@PublicApi +@SafeParcelable.Class +public class PaymentExtension extends AbstractSafeParcelable { + @Field(value = 1, getterName = "isPayment") + private final boolean payment; + + @Constructor + public PaymentExtension(@Param(1) boolean payment) { + this.payment = payment; + } + + public boolean isPayment() { + return payment; + } + + @Override + public String toString() { + return ToStringHelper.name("PaymentExtension").field("payment", payment).end(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = AbstractSafeParcelable.findCreator(PaymentExtension.class); +} \ No newline at end of file diff --git a/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/PrfExtension.java b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/PrfExtension.java new file mode 100644 index 0000000000..6958e5351b --- /dev/null +++ b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/PrfExtension.java @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fido.fido2.api.common; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +import org.microg.gms.common.PublicApi; + +@PublicApi +@SafeParcelable.Class +public class PrfExtension extends AbstractSafeParcelable { + @Field(value = 1, getterName = "getEntries") + private final byte[][] entries; + + @Constructor + public PrfExtension(@Param(1) byte[][] entries) { + this.entries = entries; + } + + @NonNull + public byte[][] getEntries() { + return entries; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = + AbstractSafeParcelable.findCreator(PrfExtension.class); +} diff --git a/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/PublicKeyCredential.java b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/PublicKeyCredential.java index d7738066b8..6f88e3bf81 100644 --- a/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/PublicKeyCredential.java +++ b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/PublicKeyCredential.java @@ -9,6 +9,8 @@ package com.google.android.gms.fido.fido2.api.common; import android.os.Parcel; +import android.util.Base64; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; @@ -17,8 +19,10 @@ import com.google.android.gms.common.internal.safeparcel.SafeParcelableSerializer; import org.microg.gms.common.Hide; import org.microg.gms.common.PublicApi; +import org.microg.gms.utils.ToStringHelper; import java.util.Arrays; +import java.util.Objects; /** * This class is contains the attributes that are returned to the caller when a new credential is created, or a new @@ -177,6 +181,22 @@ public PublicKeyCredential build() { } } + @NonNull + @Override + public String toString() { + String rawIdB64 = Base64.encodeToString(rawId, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING); + return ToStringHelper.name("PublicKeyCredential") + .field("id", id) + .field("type", type) + .field("rawId", rawIdB64) + .field("registerResponse", registerResponse) + .field("signResponse", signResponse) + .field("errorResponse", errorResponse) + .field("clientExtensionResults", clientExtensionResults) + .field("authenticatorAttachment", authenticatorAttachment) + .end(); + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -184,16 +204,14 @@ public boolean equals(Object o) { PublicKeyCredential that = (PublicKeyCredential) o; - if (id != null ? !id.equals(that.id) : that.id != null) return false; - if (type != null ? !type.equals(that.type) : that.type != null) return false; + if (!Objects.equals(id, that.id)) return false; + if (!Objects.equals(type, that.type)) return false; if (!Arrays.equals(rawId, that.rawId)) return false; - if (registerResponse != null ? !registerResponse.equals(that.registerResponse) : that.registerResponse != null) return false; - if (signResponse != null ? !signResponse.equals(that.signResponse) : that.signResponse != null) return false; - if (errorResponse != null ? !errorResponse.equals(that.errorResponse) : that.errorResponse != null) return false; - if (clientExtensionResults != null ? !clientExtensionResults.equals(that.clientExtensionResults) : that.clientExtensionResults != null) return false; - if (authenticatorAttachment != null ? !authenticatorAttachment.equals(that.authenticatorAttachment) : that.authenticatorAttachment != null) - return false; - return true; + if (!Objects.equals(registerResponse, that.registerResponse)) return false; + if (!Objects.equals(signResponse, that.signResponse)) return false; + if (!Objects.equals(errorResponse, that.errorResponse)) return false; + if (!Objects.equals(clientExtensionResults, that.clientExtensionResults)) return false; + return Objects.equals(authenticatorAttachment, that.authenticatorAttachment); } @Override diff --git a/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/PublicKeyCredentialUserEntity.java b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/PublicKeyCredentialUserEntity.java index 0470ea279a..038a15a304 100644 --- a/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/PublicKeyCredentialUserEntity.java +++ b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/PublicKeyCredentialUserEntity.java @@ -9,11 +9,16 @@ package com.google.android.gms.fido.fido2.api.common; import android.os.Parcel; +import android.util.Base64; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelable; import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +import org.json.JSONException; +import org.json.JSONObject; import org.microg.gms.common.Hide; import org.microg.gms.common.PublicApi; import org.microg.gms.utils.ToStringHelper; @@ -42,6 +47,24 @@ public class PublicKeyCredentialUserEntity extends AbstractSafeParcelable { private PublicKeyCredentialUserEntity() { } + public String toJson() throws JSONException { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("id", Base64.encodeToString(id, Base64.NO_WRAP | Base64.URL_SAFE | Base64.NO_PADDING)); + jsonObject.put("name", name); + jsonObject.put("icon", icon); + jsonObject.put("displayName", displayName); + return jsonObject.toString(); + } + + public static PublicKeyCredentialUserEntity parseJson(String json) throws JSONException { + JSONObject jsonObject = new JSONObject(json); + byte[] id = Base64.decode(jsonObject.getString("id"), Base64.NO_WRAP | Base64.URL_SAFE | Base64.NO_PADDING); + String name = jsonObject.optString("name"); + String icon = jsonObject.optString("icon"); + String displayName = jsonObject.optString("displayName"); + return new PublicKeyCredentialUserEntity(id, name, icon, displayName); + } + @Constructor public PublicKeyCredentialUserEntity(@Param(2) @NonNull byte[] id, @Param(3) @NonNull String name, @Param(4) @Nullable String icon, @Param(5) @NonNull String displayName) { this.id = id; diff --git a/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/SimpleTransactionAuthorizationExtension.java b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/SimpleTransactionAuthorizationExtension.java new file mode 100644 index 0000000000..a9f0207bdf --- /dev/null +++ b/play-services-fido/src/main/java/com/google/android/gms/fido/fido2/api/common/SimpleTransactionAuthorizationExtension.java @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2025 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.fido.fido2.api.common; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +import com.google.android.gms.common.internal.safeparcel.AbstractSafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelable; +import com.google.android.gms.common.internal.safeparcel.SafeParcelableCreatorAndWriter; + +import org.microg.gms.common.PublicApi; +import org.microg.gms.utils.ToStringHelper; + +@PublicApi +@SafeParcelable.Class +public class SimpleTransactionAuthorizationExtension extends AbstractSafeParcelable { + @Field(value = 1, getterName = "getText") + private final String text; + + @Constructor + public SimpleTransactionAuthorizationExtension(@Param(1) String text) { + this.text = text; + } + + public String getText() { + return text; + } + + @Override + public String toString() { + return ToStringHelper.name("SimpleTransactionAuthorizationExtension").field("text", text).end(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + CREATOR.writeToParcel(this, dest, flags); + } + + public static final SafeParcelableCreatorAndWriter CREATOR = AbstractSafeParcelable.findCreator(SimpleTransactionAuthorizationExtension.class); +}