PasskeyCredentialSelectionActivity.kt

7.4 KB · raw

1package com.peachpasswords.passkeys23import android.app.PendingIntent4import android.content.Context5import android.content.Intent6import android.os.Build7import android.os.Bundle8import androidx.activity.ComponentActivity9import androidx.credentials.GetCredentialResponse10import androidx.credentials.GetPublicKeyCredentialOption11import androidx.credentials.CreatePublicKeyCredentialRequest12import androidx.credentials.CreatePublicKeyCredentialResponse13import androidx.credentials.PublicKeyCredential14import androidx.credentials.exceptions.CreateCredentialUnsupportedException15import androidx.credentials.exceptions.GetCredentialUnsupportedException16import androidx.credentials.provider.PendingIntentHandler17import com.peachpasswords.shared.extractDomain18import androidx.lifecycle.lifecycleScope19import com.peachpasswords.vault.VaultRepository20import com.peachpasswords.vault.isLogin21import dagger.hilt.android.AndroidEntryPoint22import javax.inject.Inject23import kotlinx.coroutines.launch2425@AndroidEntryPoint26class PasskeyCredentialSelectionActivity : ComponentActivity() {27    @Inject lateinit var vault: VaultRepository2829    override fun onCreate(savedInstanceState: Bundle?) {30        super.onCreate(savedInstanceState)31        lifecycleScope.launch { completeSelection() }32    }3334    private suspend fun completeSelection() {35        if (intent.getStringExtra(EXTRA_MODE) == MODE_CREATE) {36            completeCreate()37            return38        }39        val result = Intent()40        val entryId = intent.getStringExtra(EXTRA_ENTRY_ID)41        val providerRequest = runCatching {42            PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)43        }.getOrNull()44        val option = providerRequest?.credentialOptions45            ?.filterIsInstance<GetPublicKeyCredentialOption>()46            ?.firstOrNull()47        val requestJson = option?.requestJson ?: intent.getStringExtra(EXTRA_REQUEST_JSON)48        val entry = entryId?.let(vault::getEntry)49        val origin = providerRequest?.callingAppInfo?.let { info ->50            runCatching { info.getOrigin(info.packageName) }.getOrNull()51        }52        val assertion = if (entry != null && requestJson != null) {53            PasskeyAssertion.build(54                entry = entry,55                requestJson = requestJson,56                origin = origin,57                clientDataHashOverride = option?.clientDataHash,58            )59        } else {60            null61        }6263        if (entry != null && assertion != null) {64            val passkey = entry.passkey65            val persisted = if (passkey != null) {66                val now = System.currentTimeMillis()67                runCatching {68                    vault.updateEntry(69                        entry.copy(70                            passkey = passkey.copy(signCount = assertion.nextSignCount),71                            signCount = assertion.nextSignCount,72                            lastUsedAt = now,73                        ),74                    )75                }.isSuccess76            } else {77                false78            }79            if (persisted) {80                PendingIntentHandler.setGetCredentialResponse(81                    result,82                    GetCredentialResponse(PublicKeyCredential(assertion.responseJson)),83                )84            } else {85                PendingIntentHandler.setGetCredentialException(86                    result,87                    GetCredentialUnsupportedException("Peach could not update this passkey."),88                )89            }90        } else {91            PendingIntentHandler.setGetCredentialException(92                result,93                GetCredentialUnsupportedException("Peach cannot sign this passkey yet."),94            )95        }96        setResult(RESULT_OK, result)97        finish()98    }99100    private suspend fun completeCreate() {101        val result = Intent()102        val providerRequest = runCatching {103            PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)104        }.getOrNull()105        val request = providerRequest?.callingRequest as? CreatePublicKeyCredentialRequest106        val requestJson = request?.requestJson ?: intent.getStringExtra(EXTRA_REQUEST_JSON)107        val origin = request?.origin ?: providerRequest?.callingAppInfo?.let { info ->108            runCatching { info.getOrigin(info.packageName) }.getOrNull()109        }110        val registration = requestJson?.let { PasskeyRegistration.build(it, origin) }111        val stored = registration?.let { storeRegistration(it.entryDraft) } ?: false112        if (registration != null && stored) {113            PendingIntentHandler.setCreateCredentialResponse(114                result,115                CreatePublicKeyCredentialResponse(registration.responseJson),116            )117        } else {118            PendingIntentHandler.setCreateCredentialException(119                result,120                CreateCredentialUnsupportedException("Peach cannot create this passkey yet."),121            )122        }123        setResult(RESULT_OK, result)124        finish()125    }126127    private suspend fun storeRegistration(draft: com.peachpasswords.vault.VaultEntry): Boolean {128        val rpId = draft.passkey?.rpId ?: return false129        val target = vault.getEntries().firstOrNull { entry ->130                entry.deletedAt == null &&131                entry.passkey == null &&132                entry.isLogin() &&133                listOfNotNull(entry.url, *entry.additionalUrls.orEmpty().toTypedArray())134                    .any { extractDomain(it) == rpId }135        }136        return runCatching {137            if (target != null) {138                vault.updateEntry(139                    target.copy(140                        passkey = draft.passkey,141                        username = target.username ?: draft.username,142                    ),143                )144            } else {145                vault.addEntry(draft)146            }147        }.isSuccess148    }149150    companion object {151        private const val EXTRA_ENTRY_ID = "com.peachpasswords.passkeys.EXTRA_ENTRY_ID"152        private const val EXTRA_REQUEST_JSON = "com.peachpasswords.passkeys.EXTRA_REQUEST_JSON"153        private const val EXTRA_MODE = "com.peachpasswords.passkeys.EXTRA_MODE"154        private const val MODE_CREATE = "create"155156        fun pendingIntent(context: Context, entryId: String, requestJson: String): PendingIntent {157            val intent = Intent(context, PasskeyCredentialSelectionActivity::class.java)158                .putExtra(EXTRA_ENTRY_ID, entryId)159                .putExtra(EXTRA_REQUEST_JSON, requestJson)160            val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0161            return PendingIntent.getActivity(162                context,163                "$entryId:$requestJson".hashCode(),164                intent,165                PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag,166            )167        }168169        fun createPendingIntent(context: Context, requestJson: String): PendingIntent {170            val intent = Intent(context, PasskeyCredentialSelectionActivity::class.java)171                .putExtra(EXTRA_MODE, MODE_CREATE)172                .putExtra(EXTRA_REQUEST_JSON, requestJson)173            val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0174            return PendingIntent.getActivity(175                context,176                requestJson.hashCode(),177                intent,178                PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag,179            )180        }181    }182}