PeachCredentialProviderService.kt

3.9 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(this, candidate.entryId),66                    beginGetPublicKeyCredentialOption = option,67                    displayName = candidate.displayName,68                    lastUsedTime = Instant.ofEpochMilli(candidate.lastUsedAt ?: 0L),69                    icon = icon,70                    isAutoSelectAllowed = false,71                )72            }73        }74        callback.onResult(BeginGetCredentialResponse(credentialEntries = entries))75    }7677    override fun onBeginCreateCredentialRequest(78        request: BeginCreateCredentialRequest,79        cancellationSignal: CancellationSignal,80        callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>,81    ) {82        callback.onResult(BeginCreateCredentialResponse())83    }8485    override fun onClearCredentialStateRequest(86        request: ProviderClearCredentialStateRequest,87        cancellationSignal: CancellationSignal,88        callback: OutcomeReceiver<Void?, ClearCredentialException>,89    ) {90        callback.onResult(null)91    }92}