PasskeyCredentialSelectionActivity.kt

7.8 KB · raw

1package com.peachpasswords.passkeys23import android.app.PendingIntent4import android.content.Context5import android.content.Intent6import android.os.Build7import android.os.Bundle8import android.view.WindowManager9import androidx.activity.ComponentActivity10import androidx.credentials.GetCredentialResponse11import androidx.credentials.GetPublicKeyCredentialOption12import androidx.credentials.CreatePublicKeyCredentialRequest13import androidx.credentials.CreatePublicKeyCredentialResponse14import androidx.credentials.PublicKeyCredential15import androidx.credentials.exceptions.CreateCredentialUnsupportedException16import androidx.credentials.exceptions.GetCredentialUnsupportedException17import androidx.credentials.provider.PendingIntentHandler18import com.peachpasswords.BuildConfig19import com.peachpasswords.shared.extractDomain20import androidx.lifecycle.lifecycleScope21import com.peachpasswords.vault.VaultRepository22import com.peachpasswords.vault.isLogin23import dagger.hilt.android.AndroidEntryPoint24import javax.inject.Inject25import kotlinx.coroutines.launch2627@AndroidEntryPoint28class PasskeyCredentialSelectionActivity : ComponentActivity() {29    @Inject lateinit var vault: VaultRepository3031    override fun onCreate(savedInstanceState: Bundle?) {32        super.onCreate(savedInstanceState)33        // H-05: separate window from MainActivity — sign assertion and registration34        // operate on passkey material. Block screenshots / recents in release.35        if (!BuildConfig.DEBUG) {36            window.setFlags(37                WindowManager.LayoutParams.FLAG_SECURE,38                WindowManager.LayoutParams.FLAG_SECURE,39            )40        }41        lifecycleScope.launch { completeSelection() }42    }4344    private suspend fun completeSelection() {45        if (intent.getStringExtra(EXTRA_MODE) == MODE_CREATE) {46            completeCreate()47            return48        }49        val result = Intent()50        val entryId = intent.getStringExtra(EXTRA_ENTRY_ID)51        val providerRequest = runCatching {52            PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)53        }.getOrNull()54        val option = providerRequest?.credentialOptions55            ?.filterIsInstance<GetPublicKeyCredentialOption>()56            ?.firstOrNull()57        val requestJson = option?.requestJson ?: intent.getStringExtra(EXTRA_REQUEST_JSON)58        val entry = entryId?.let(vault::getEntry)59        val origin = providerRequest?.callingAppInfo?.let { info ->60            runCatching { info.getOrigin(info.packageName) }.getOrNull()61        }62        val assertion = if (entry != null && requestJson != null) {63            PasskeyAssertion.build(64                entry = entry,65                requestJson = requestJson,66                origin = origin,67                clientDataHashOverride = option?.clientDataHash,68            )69        } else {70            null71        }7273        if (entry != null && assertion != null) {74            val passkey = entry.passkey75            val persisted = if (passkey != null) {76                val now = System.currentTimeMillis()77                runCatching {78                    vault.updateEntry(79                        entry.copy(80                            passkey = passkey.copy(signCount = assertion.nextSignCount),81                            signCount = assertion.nextSignCount,82                            lastUsedAt = now,83                        ),84                    )85                }.isSuccess86            } else {87                false88            }89            if (persisted) {90                PendingIntentHandler.setGetCredentialResponse(91                    result,92                    GetCredentialResponse(PublicKeyCredential(assertion.responseJson)),93                )94            } else {95                PendingIntentHandler.setGetCredentialException(96                    result,97                    GetCredentialUnsupportedException("Peach could not update this passkey."),98                )99            }100        } else {101            PendingIntentHandler.setGetCredentialException(102                result,103                GetCredentialUnsupportedException("Peach cannot sign this passkey yet."),104            )105        }106        setResult(RESULT_OK, result)107        finish()108    }109110    private suspend fun completeCreate() {111        val result = Intent()112        val providerRequest = runCatching {113            PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)114        }.getOrNull()115        val request = providerRequest?.callingRequest as? CreatePublicKeyCredentialRequest116        val requestJson = request?.requestJson ?: intent.getStringExtra(EXTRA_REQUEST_JSON)117        val origin = request?.origin ?: providerRequest?.callingAppInfo?.let { info ->118            runCatching { info.getOrigin(info.packageName) }.getOrNull()119        }120        val registration = requestJson?.let { PasskeyRegistration.build(it, origin) }121        val stored = registration?.let { storeRegistration(it.entryDraft) } ?: false122        if (registration != null && stored) {123            PendingIntentHandler.setCreateCredentialResponse(124                result,125                CreatePublicKeyCredentialResponse(registration.responseJson),126            )127        } else {128            PendingIntentHandler.setCreateCredentialException(129                result,130                CreateCredentialUnsupportedException("Peach cannot create this passkey yet."),131            )132        }133        setResult(RESULT_OK, result)134        finish()135    }136137    private suspend fun storeRegistration(draft: com.peachpasswords.vault.VaultEntry): Boolean {138        val rpId = draft.passkey?.rpId ?: return false139        val target = vault.getEntries().firstOrNull { entry ->140                entry.deletedAt == null &&141                entry.passkey == null &&142                entry.isLogin() &&143                listOfNotNull(entry.url, *entry.additionalUrls.orEmpty().toTypedArray())144                    .any { extractDomain(it) == rpId }145        }146        return runCatching {147            if (target != null) {148                vault.updateEntry(149                    target.copy(150                        passkey = draft.passkey,151                        username = target.username ?: draft.username,152                    ),153                )154            } else {155                vault.addEntry(draft)156            }157        }.isSuccess158    }159160    companion object {161        private const val EXTRA_ENTRY_ID = "com.peachpasswords.passkeys.EXTRA_ENTRY_ID"162        private const val EXTRA_REQUEST_JSON = "com.peachpasswords.passkeys.EXTRA_REQUEST_JSON"163        private const val EXTRA_MODE = "com.peachpasswords.passkeys.EXTRA_MODE"164        private const val MODE_CREATE = "create"165166        fun pendingIntent(context: Context, entryId: String, requestJson: String): PendingIntent {167            val intent = Intent(context, PasskeyCredentialSelectionActivity::class.java)168                .putExtra(EXTRA_ENTRY_ID, entryId)169                .putExtra(EXTRA_REQUEST_JSON, requestJson)170            val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0171            return PendingIntent.getActivity(172                context,173                "$entryId:$requestJson".hashCode(),174                intent,175                PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag,176            )177        }178179        fun createPendingIntent(context: Context, requestJson: String): PendingIntent {180            val intent = Intent(context, PasskeyCredentialSelectionActivity::class.java)181                .putExtra(EXTRA_MODE, MODE_CREATE)182                .putExtra(EXTRA_REQUEST_JSON, requestJson)183            val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0184            return PendingIntent.getActivity(185                context,186                requestJson.hashCode(),187                intent,188                PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag,189            )190        }191    }192}