PeachCredentialProviderService.kt

5.1 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.CredentialProviderService14import androidx.credentials.provider.ProviderClearCredentialStateRequest15import androidx.credentials.provider.PublicKeyCredentialEntry16import android.os.OutcomeReceiver17import androidx.credentials.exceptions.ClearCredentialException18import androidx.credentials.exceptions.CreateCredentialException19import androidx.credentials.exceptions.GetCredentialException20import androidx.annotation.RequiresApi21import com.peachpasswords.R22import com.peachpasswords.vault.VaultRepository23import dagger.hilt.android.AndroidEntryPoint24import java.time.Instant25import javax.inject.Inject2627/**28 * Android 14+ credential provider surface for Peach passkeys.29 *30 * The Begin* callbacks MUST be answered or the Credential Manager times out and31 * reports a provider error; the previous scaffold left them uninvoked. This32 * version always completes the callbacks with a valid (currently empty)33 * response, so Peach registers cleanly as a provider without erroring.34 *35 * Begin-get now surfaces unlocked, matching passkeys as system credential rows.36 * The selected pending intent still returns an explicit unsupported result until37 * the ES256/WebAuthn assertion ceremony lands.38 */39@AndroidEntryPoint40@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)41class PeachCredentialProviderService : CredentialProviderService() {4243    @Inject lateinit var vault: VaultRepository4445    override fun onBeginGetCredentialRequest(46        request: BeginGetCredentialRequest,47        cancellationSignal: CancellationSignal,48        callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,49    ) {50        if (!vault.settings.value.passkeyProviderEnabled || !vault.isUnlocked.value) {51            callback.onResult(BeginGetCredentialResponse())52            return53        }54        val passkeyOptions = request.beginGetCredentialOptions55            .filterIsInstance<BeginGetPublicKeyCredentialOption>()56        if (passkeyOptions.isEmpty()) {57            callback.onResult(BeginGetCredentialResponse())58            return59        }6061        val icon = Icon.createWithResource(this, R.drawable.ic_peach_mark)62        val entries = passkeyOptions.flatMap { option ->63            PasskeyCredentialPlanner.candidates(vault.getEntries(), option.requestJson).map { candidate ->64                PublicKeyCredentialEntry(65                    context = this,66                    username = candidate.username,67                    pendingIntent = PasskeyCredentialSelectionActivity.pendingIntent(68                        this,69                        candidate.entryId,70                        option.requestJson,71                    ),72                    beginGetPublicKeyCredentialOption = option,73                    displayName = candidate.displayName,74                    lastUsedTime = Instant.ofEpochMilli(candidate.lastUsedAt ?: 0L),75                    icon = icon,76                    isAutoSelectAllowed = false,77                )78            }79        }80        callback.onResult(BeginGetCredentialResponse(credentialEntries = entries))81    }8283    override fun onBeginCreateCredentialRequest(84        request: BeginCreateCredentialRequest,85        cancellationSignal: CancellationSignal,86        callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>,87    ) {88        val publicKeyRequest = request as? BeginCreatePublicKeyCredentialRequest89        if (publicKeyRequest == null || !vault.settings.value.passkeyProviderEnabled || !vault.isUnlocked.value) {90            callback.onResult(BeginCreateCredentialResponse())91            return92        }93        val count = vault.getEntries().count { it.passkey != null }94        val entry = CreateEntry(95            accountName = "Peach",96            pendingIntent = PasskeyCredentialSelectionActivity.createPendingIntent(this, publicKeyRequest.requestJson),97            description = "Save passkey to Peach",98            lastUsedTime = Instant.now(),99            icon = Icon.createWithResource(this, R.drawable.ic_peach_mark),100            passwordCredentialCount = null,101            publicKeyCredentialCount = count,102            totalCredentialCount = vault.getEntries().size,103            isAutoSelectAllowed = false,104        )105        callback.onResult(BeginCreateCredentialResponse(createEntries = listOf(entry)))106    }107108    override fun onClearCredentialStateRequest(109        request: ProviderClearCredentialStateRequest,110        cancellationSignal: CancellationSignal,111        callback: OutcomeReceiver<Void?, ClearCredentialException>,112    ) {113        callback.onResult(null)114    }115}