PeachCredentialProviderService.kt

4.0 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.BeginGetCredentialRequest9import androidx.credentials.provider.BeginGetCredentialResponse10import androidx.credentials.provider.BeginGetPublicKeyCredentialOption11import androidx.credentials.provider.CredentialProviderService12import androidx.credentials.provider.ProviderClearCredentialStateRequest13import androidx.credentials.provider.PublicKeyCredentialEntry14import android.os.OutcomeReceiver15import androidx.credentials.exceptions.ClearCredentialException16import androidx.credentials.exceptions.CreateCredentialException17import androidx.credentials.exceptions.GetCredentialException18import androidx.annotation.RequiresApi19import com.peachpasswords.R20import com.peachpasswords.vault.VaultRepository21import dagger.hilt.android.AndroidEntryPoint22import java.time.Instant23import javax.inject.Inject2425/**26 * Android 14+ credential provider surface for Peach passkeys.27 *28 * The Begin* callbacks MUST be answered or the Credential Manager times out and29 * reports a provider error; the previous scaffold left them uninvoked. This30 * version always completes the callbacks with a valid (currently empty)31 * response, so Peach registers cleanly as a provider without erroring.32 *33 * Begin-get now surfaces unlocked, matching passkeys as system credential rows.34 * The selected pending intent still returns an explicit unsupported result until35 * the ES256/WebAuthn assertion ceremony lands.36 */37@AndroidEntryPoint38@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)39class PeachCredentialProviderService : CredentialProviderService() {4041    @Inject lateinit var vault: VaultRepository4243    override fun onBeginGetCredentialRequest(44        request: BeginGetCredentialRequest,45        cancellationSignal: CancellationSignal,46        callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,47    ) {48        if (!vault.settings.value.passkeyProviderEnabled || !vault.isUnlocked.value) {49            callback.onResult(BeginGetCredentialResponse())50            return51        }52        val passkeyOptions = request.beginGetCredentialOptions53            .filterIsInstance<BeginGetPublicKeyCredentialOption>()54        if (passkeyOptions.isEmpty()) {55            callback.onResult(BeginGetCredentialResponse())56            return57        }5859        val icon = Icon.createWithResource(this, R.drawable.ic_peach_mark)60        val entries = passkeyOptions.flatMap { option ->61            PasskeyCredentialPlanner.candidates(vault.getEntries(), option.requestJson).map { candidate ->62                PublicKeyCredentialEntry(63                    context = this,64                    username = candidate.username,65                    pendingIntent = PasskeyCredentialSelectionActivity.pendingIntent(66                        this,67                        candidate.entryId,68                        option.requestJson,69                    ),70                    beginGetPublicKeyCredentialOption = option,71                    displayName = candidate.displayName,72                    lastUsedTime = Instant.ofEpochMilli(candidate.lastUsedAt ?: 0L),73                    icon = icon,74                    isAutoSelectAllowed = false,75                )76            }77        }78        callback.onResult(BeginGetCredentialResponse(credentialEntries = entries))79    }8081    override fun onBeginCreateCredentialRequest(82        request: BeginCreateCredentialRequest,83        cancellationSignal: CancellationSignal,84        callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>,85    ) {86        callback.onResult(BeginCreateCredentialResponse())87    }8889    override fun onClearCredentialStateRequest(90        request: ProviderClearCredentialStateRequest,91        cancellationSignal: CancellationSignal,92        callback: OutcomeReceiver<Void?, ClearCredentialException>,93    ) {94        callback.onResult(null)95    }96}