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.license.LicenseRepository24import com.peachpasswords.license.ProFeature25import com.peachpasswords.license.ProNoticeStore26import com.peachpasswords.vault.VaultRepository27import dagger.hilt.android.AndroidEntryPoint28import java.time.Instant29import javax.inject.Inject3031323334353637383940414243@AndroidEntryPoint44@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)45class PeachCredentialProviderService : CredentialProviderService() {4647 @Inject lateinit var vault: VaultRepository48 @Inject lateinit var license: LicenseRepository49 @Inject lateinit var proNoticeStore: ProNoticeStore5051 override fun onBeginGetCredentialRequest(52 request: BeginGetCredentialRequest,53 cancellationSignal: CancellationSignal,54 callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,55 ) {56 if (!vault.settings.value.passkeyProviderEnabled) {57 callback.onResult(BeginGetCredentialResponse())58 return59 }60 if (!license.hasFeature(ProFeature.Passkeys)) {61 proNoticeStore.record(ProFeature.Passkeys)62 callback.onResult(BeginGetCredentialResponse())63 return64 }65 if (!vault.isUnlocked.value) {66 callback.onResult(67 BeginGetCredentialResponse(68 authenticationActions = listOf(69 AuthenticationAction(70 title = "Unlock Peach",71 pendingIntent = PasskeyUnlockIntent.pendingIntent(this),72 ),73 ),74 ),75 )76 return77 }78 val passkeyOptions = request.beginGetCredentialOptions79 .filterIsInstance<BeginGetPublicKeyCredentialOption>()80 if (passkeyOptions.isEmpty()) {81 callback.onResult(BeginGetCredentialResponse())82 return83 }8485 val icon = Icon.createWithResource(this, R.drawable.ic_peach_mark)86 87 88 val callerOrigin = request.callingAppInfo?.let {89 PasskeyCallerOrigin.resolve(it, PasskeyCallerOrigin.privilegedAllowlist(this))90 }91 val entries = passkeyOptions.flatMap { option ->92 PasskeyCredentialPlanner.candidates(vault.getEntries(), option.requestJson, callerOrigin).map { candidate ->93 PublicKeyCredentialEntry(94 context = this,95 username = candidate.username,96 pendingIntent = PasskeyCredentialSelectionActivity.pendingIntent(97 this,98 candidate.entryId,99 option.requestJson,100 ),101 beginGetPublicKeyCredentialOption = option,102 displayName = candidate.displayName,103 lastUsedTime = Instant.ofEpochMilli(candidate.lastUsedAt ?: 0L),104 icon = icon,105 isAutoSelectAllowed = false,106 )107 }108 }109 callback.onResult(BeginGetCredentialResponse(credentialEntries = entries))110 }111112 override fun onBeginCreateCredentialRequest(113 request: BeginCreateCredentialRequest,114 cancellationSignal: CancellationSignal,115 callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>,116 ) {117 val publicKeyRequest = request as? BeginCreatePublicKeyCredentialRequest118 if (publicKeyRequest == null || !vault.settings.value.passkeyProviderEnabled || !vault.isUnlocked.value) {119 callback.onResult(BeginCreateCredentialResponse())120 return121 }122 if (!license.hasFeature(ProFeature.Passkeys)) {123 proNoticeStore.record(ProFeature.Passkeys)124 callback.onResult(BeginCreateCredentialResponse())125 return126 }127 val count = vault.getEntries().count { it.passkey != null }128 val entry = CreateEntry(129 accountName = "Peach",130 pendingIntent = PasskeyCredentialSelectionActivity.createPendingIntent(this, publicKeyRequest.requestJson),131 description = "Save passkey to Peach",132 lastUsedTime = Instant.now(),133 icon = Icon.createWithResource(this, R.drawable.ic_peach_mark),134 passwordCredentialCount = null,135 publicKeyCredentialCount = count,136 totalCredentialCount = vault.getEntries().size,137 isAutoSelectAllowed = false,138 )139 callback.onResult(BeginCreateCredentialResponse(createEntries = listOf(entry)))140 }141142 override fun onClearCredentialStateRequest(143 request: ProviderClearCredentialStateRequest,144 cancellationSignal: CancellationSignal,145 callback: OutcomeReceiver<Void?, ClearCredentialException>,146 ) {147 callback.onResult(null)148 }149}