PeachCredentialProviderService.kt

5.6 KB · raw

1package com.peachpasswords.passkeys23import android.os.Build4import android.os.CancellationSignal5import android.graphics.drawable.Icon6import androidx.credentials.provider.BeginCreateCredentialRequest7import androidx.credentials.provider.BeginCreateCredentialResponse8import androidx.credentials.provider.BeginCreatePublicKeyCredentialRequest9import androidx.credentials.provider.BeginGetCredentialRequest10import androidx.credentials.provider.BeginGetCredentialResponse11import androidx.credentials.provider.BeginGetPublicKeyCredentialOption12import androidx.credentials.provider.CreateEntry13import androidx.credentials.provider.AuthenticationAction14import androidx.credentials.provider.CredentialProviderService15import androidx.credentials.provider.ProviderClearCredentialStateRequest16import androidx.credentials.provider.PublicKeyCredentialEntry17import android.os.OutcomeReceiver18import androidx.credentials.exceptions.ClearCredentialException19import androidx.credentials.exceptions.CreateCredentialException20import androidx.credentials.exceptions.GetCredentialException21import androidx.annotation.RequiresApi22import com.peachpasswords.R23import com.peachpasswords.vault.VaultRepository24import dagger.hilt.android.AndroidEntryPoint25import java.time.Instant26import javax.inject.Inject2728/**29 * Android 14+ credential provider surface for Peach passkeys.30 *31 * The Begin* callbacks MUST be answered or the Credential Manager times out and32 * reports a provider error; the previous scaffold left them uninvoked. This33 * version always completes the callbacks with a valid (currently empty)34 * response, so Peach registers cleanly as a provider without erroring.35 *36 * Begin-get now surfaces unlocked, matching passkeys as system credential rows.37 * The selected pending intent still returns an explicit unsupported result until38 * the ES256/WebAuthn assertion ceremony lands.39 */40@AndroidEntryPoint41@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)42class PeachCredentialProviderService : CredentialProviderService() {4344    @Inject lateinit var vault: VaultRepository4546    override fun onBeginGetCredentialRequest(47        request: BeginGetCredentialRequest,48        cancellationSignal: CancellationSignal,49        callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,50    ) {51        if (!vault.settings.value.passkeyProviderEnabled) {52            callback.onResult(BeginGetCredentialResponse())53            return54        }55        if (!vault.isUnlocked.value) {56            callback.onResult(57                BeginGetCredentialResponse(58                    authenticationActions = listOf(59                        AuthenticationAction(60                            title = "Unlock Peach",61                            pendingIntent = PasskeyUnlockIntent.pendingIntent(this),62                        ),63                    ),64                ),65            )66            return67        }68        val passkeyOptions = request.beginGetCredentialOptions69            .filterIsInstance<BeginGetPublicKeyCredentialOption>()70        if (passkeyOptions.isEmpty()) {71            callback.onResult(BeginGetCredentialResponse())72            return73        }7475        val icon = Icon.createWithResource(this, R.drawable.ic_peach_mark)76        val entries = passkeyOptions.flatMap { option ->77            PasskeyCredentialPlanner.candidates(vault.getEntries(), option.requestJson).map { candidate ->78                PublicKeyCredentialEntry(79                    context = this,80                    username = candidate.username,81                    pendingIntent = PasskeyCredentialSelectionActivity.pendingIntent(82                        this,83                        candidate.entryId,84                        option.requestJson,85                    ),86                    beginGetPublicKeyCredentialOption = option,87                    displayName = candidate.displayName,88                    lastUsedTime = Instant.ofEpochMilli(candidate.lastUsedAt ?: 0L),89                    icon = icon,90                    isAutoSelectAllowed = false,91                )92            }93        }94        callback.onResult(BeginGetCredentialResponse(credentialEntries = entries))95    }9697    override fun onBeginCreateCredentialRequest(98        request: BeginCreateCredentialRequest,99        cancellationSignal: CancellationSignal,100        callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>,101    ) {102        val publicKeyRequest = request as? BeginCreatePublicKeyCredentialRequest103        if (publicKeyRequest == null || !vault.settings.value.passkeyProviderEnabled || !vault.isUnlocked.value) {104            callback.onResult(BeginCreateCredentialResponse())105            return106        }107        val count = vault.getEntries().count { it.passkey != null }108        val entry = CreateEntry(109            accountName = "Peach",110            pendingIntent = PasskeyCredentialSelectionActivity.createPendingIntent(this, publicKeyRequest.requestJson),111            description = "Save passkey to Peach",112            lastUsedTime = Instant.now(),113            icon = Icon.createWithResource(this, R.drawable.ic_peach_mark),114            passwordCredentialCount = null,115            publicKeyCredentialCount = count,116            totalCredentialCount = vault.getEntries().size,117            isAutoSelectAllowed = false,118        )119        callback.onResult(BeginCreateCredentialResponse(createEntries = listOf(entry)))120    }121122    override fun onClearCredentialStateRequest(123        request: ProviderClearCredentialStateRequest,124        cancellationSignal: CancellationSignal,125        callback: OutcomeReceiver<Void?, ClearCredentialException>,126    ) {127        callback.onResult(null)128    }129}