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.BeginGetPasswordOption12import androidx.credentials.provider.BeginGetPublicKeyCredentialOption13import androidx.credentials.provider.CredentialEntry14import androidx.credentials.provider.CreateEntry15import androidx.credentials.provider.AuthenticationAction16import androidx.credentials.provider.CredentialProviderService17import androidx.credentials.provider.ProviderClearCredentialStateRequest18import androidx.credentials.provider.PublicKeyCredentialEntry19import androidx.credentials.provider.PasswordCredentialEntry20import android.os.OutcomeReceiver21import androidx.credentials.exceptions.ClearCredentialException22import androidx.credentials.exceptions.CreateCredentialException23import androidx.credentials.exceptions.GetCredentialException24import androidx.annotation.RequiresApi25import com.peachpasswords.R26import com.peachpasswords.autofill.appLabel27import com.peachpasswords.autofill.appLinkDomains28import com.peachpasswords.credentials.PasswordCredentialPlanner29import com.peachpasswords.credentials.PasswordCredentialSelectionActivity30import com.peachpasswords.license.LicenseRepository31import com.peachpasswords.license.ProFeature32import com.peachpasswords.license.ProNoticeStore33import com.peachpasswords.shared.extractDomain34import com.peachpasswords.vault.VaultRepository35import dagger.hilt.android.AndroidEntryPoint36import java.time.Instant37import javax.inject.Inject3839404142434445464748495051525354@AndroidEntryPoint55@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)56class PeachCredentialProviderService : CredentialProviderService() {5758 @Inject lateinit var vault: VaultRepository59 @Inject lateinit var license: LicenseRepository60 @Inject lateinit var proNoticeStore: ProNoticeStore6162 override fun onBeginGetCredentialRequest(63 request: BeginGetCredentialRequest,64 cancellationSignal: CancellationSignal,65 callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,66 ) {67 if (!vault.isUnlocked.value) {68 69 70 callback.onResult(71 BeginGetCredentialResponse(72 authenticationActions = listOf(73 AuthenticationAction(74 title = "Unlock Peach",75 pendingIntent = PasskeyUnlockIntent.pendingIntent(this),76 ),77 ),78 ),79 )80 return81 }82 val entries = passkeyEntries(request) + passwordEntries(request)83 callback.onResult(BeginGetCredentialResponse(credentialEntries = entries))84 }8586 private fun passkeyEntries(request: BeginGetCredentialRequest): List<CredentialEntry> {87 if (!vault.settings.value.passkeyProviderEnabled) return emptyList()88 if (!license.hasFeature(ProFeature.Passkeys)) {89 proNoticeStore.record(ProFeature.Passkeys)90 return emptyList()91 }92 val passkeyOptions = request.beginGetCredentialOptions93 .filterIsInstance<BeginGetPublicKeyCredentialOption>()94 if (passkeyOptions.isEmpty()) return emptyList()9596 val icon = Icon.createWithResource(this, R.drawable.ic_peach_mark)97 98 99 val callerOrigin = request.callingAppInfo?.let {100 PasskeyCallerOrigin.resolve(it, PasskeyCallerOrigin.privilegedAllowlist(this))101 }102 return passkeyOptions.flatMap { option ->103 PasskeyCredentialPlanner.candidates(vault.getEntries(), option.requestJson, callerOrigin).map { candidate ->104 PublicKeyCredentialEntry(105 context = this,106 username = candidate.username,107 pendingIntent = PasskeyCredentialSelectionActivity.pendingIntent(108 this,109 candidate.entryId,110 option.requestJson,111 ),112 beginGetPublicKeyCredentialOption = option,113 displayName = candidate.displayName,114 lastUsedTime = Instant.ofEpochMilli(candidate.lastUsedAt ?: 0L),115 icon = icon,116 isAutoSelectAllowed = false,117 )118 }119 }120 }121122 123124125126127128129 private fun passwordEntries(request: BeginGetCredentialRequest): List<CredentialEntry> {130 val option = request.beginGetCredentialOptions131 .filterIsInstance<BeginGetPasswordOption>()132 .firstOrNull() ?: return emptyList()133 val info = request.callingAppInfo ?: return emptyList()134135 val allowlist = PasskeyCallerOrigin.privilegedAllowlist(this)136 val webOrigin = runCatching { info.getOrigin(allowlist) }.getOrNull()137 val domains: Set<String>138 val appTitle: String?139 if (!webOrigin.isNullOrBlank()) {140 domains = setOfNotNull(extractDomain(webOrigin))141 appTitle = null142 } else {143 domains = appLinkDomains(this, info.packageName).toSet()144 appTitle = appLabel(this, info.packageName)145 }146147 val candidates = PasswordCredentialPlanner.candidates(148 vault.getEntries(),149 domains,150 appTitle,151 option.allowedUserIds,152 )153 if (candidates.isEmpty()) return emptyList()154155 val icon = Icon.createWithResource(this, R.drawable.ic_peach_mark)156 return candidates.map { candidate ->157 PasswordCredentialEntry.Builder(158 this,159 candidate.username,160 PasswordCredentialSelectionActivity.pendingIntent(this, candidate.entryId),161 option,162 )163 .setDisplayName(candidate.title)164 .setLastUsedTime(Instant.ofEpochMilli(candidate.lastUsedAt ?: 0L))165 .setIcon(icon)166 .setAutoSelectAllowed(false)167 .build()168 }169 }170171 override fun onBeginCreateCredentialRequest(172 request: BeginCreateCredentialRequest,173 cancellationSignal: CancellationSignal,174 callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>,175 ) {176 val publicKeyRequest = request as? BeginCreatePublicKeyCredentialRequest177 if (publicKeyRequest == null || !vault.settings.value.passkeyProviderEnabled || !vault.isUnlocked.value) {178 callback.onResult(BeginCreateCredentialResponse())179 return180 }181 if (!license.hasFeature(ProFeature.Passkeys)) {182 proNoticeStore.record(ProFeature.Passkeys)183 callback.onResult(BeginCreateCredentialResponse())184 return185 }186 val count = vault.getEntries().count { it.passkey != null }187 val entry = CreateEntry(188 accountName = "Peach",189 pendingIntent = PasskeyCredentialSelectionActivity.createPendingIntent(this, publicKeyRequest.requestJson),190 description = "Save passkey to Peach",191 lastUsedTime = Instant.now(),192 icon = Icon.createWithResource(this, R.drawable.ic_peach_mark),193 passwordCredentialCount = null,194 publicKeyCredentialCount = count,195 totalCredentialCount = vault.getEntries().size,196 isAutoSelectAllowed = false,197 )198 callback.onResult(BeginCreateCredentialResponse(createEntries = listOf(entry)))199 }200201 override fun onClearCredentialStateRequest(202 request: ProviderClearCredentialStateRequest,203 cancellationSignal: CancellationSignal,204 callback: OutcomeReceiver<Void?, ClearCredentialException>,205 ) {206 callback.onResult(null)207 }208}