fix: Allow to check multiple app signatures #1421

This commit is contained in:
J-Jamet
2025-08-27 23:09:51 +02:00
parent fcf723849b
commit 5f27f161a5
13 changed files with 311 additions and 164 deletions

View File

@@ -23,7 +23,6 @@ import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
@@ -42,16 +41,18 @@ import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
import com.kunzisoft.keepass.credentialprovider.passkey.util.OriginManager.Companion.checkInAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addNodeId import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addNodeId
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addOriginAppInfo
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addSearchInfo import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addSearchInfo
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildCreatePublicKeyCredentialResponse import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildCreatePublicKeyCredentialResponse
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyPublicKeyCredential import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyPublicKeyCredential
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.checkSecurity import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.checkSecurity
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveAppOrigin
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveNodeId import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveNodeId
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveOriginAppInfo
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskey import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskey
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationRequestParameters import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationRequestParameters
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters
@@ -59,13 +60,13 @@ import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retri
import com.kunzisoft.keepass.database.ContextualDatabase import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.helper.SearchHelper import com.kunzisoft.keepass.database.helper.SearchHelper
import com.kunzisoft.keepass.model.OriginApp import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.Passkey import com.kunzisoft.keepass.model.Passkey
import com.kunzisoft.keepass.model.RegisterInfo import com.kunzisoft.keepass.model.RegisterInfo
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.IOException
import java.io.InvalidObjectException import java.io.InvalidObjectException
import java.util.UUID import java.util.UUID
@@ -75,34 +76,49 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
private var mUsageParameters: PublicKeyCredentialUsageParameters? = null private var mUsageParameters: PublicKeyCredentialUsageParameters? = null
private var mCreationParameters: PublicKeyCredentialCreationParameters? = null private var mCreationParameters: PublicKeyCredentialCreationParameters? = null
private var mPasskey: Passkey? = null private var mPasskey: Passkey? = null
private var mSearchInfo: SearchInfo = SearchInfo()
private var mOriginApp: OriginApp = OriginApp()
private var mPasskeySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? = private var mPasskeySelectionActivityResultLauncher: ActivityResultLauncher<Intent>? =
this.buildActivityResultLauncher( this.buildActivityResultLauncher(
lockDatabase = true, lockDatabase = true,
dataTransformation = { intent -> dataTransformation = { intent ->
Log.d(TAG, "Passkey selection result")
val passkey = intent?.retrievePasskey()
intent?.removePasskey()
// Build a new formatted response from the selection response // Build a new formatted response from the selection response
val responseIntent = Intent() val responseIntent = Intent()
passkey?.let { try {
mUsageParameters?.let { usageParameters -> Log.d(TAG, "Passkey selection result")
PendingIntentHandler.setGetCredentialResponse( val passkey = intent?.retrievePasskey()
responseIntent, val appOrigin = intent?.retrieveAppOrigin()
GetCredentialResponse( intent?.removePasskey()
buildPasskeyPublicKeyCredential( intent?.removeAppOrigin()
usageParameters = usageParameters, passkey?.let {
passkey = passkey mUsageParameters?.let { usageParameters ->
) // Check verified origin
usageParameters.androidApp.checkInAppOrigin(
appOrigin = appOrigin,
onOriginChecked = {
usageParameters.androidAppVerified = true
PendingIntentHandler.setGetCredentialResponse(
responseIntent,
GetCredentialResponse(
buildPasskeyPublicKeyCredential(
usageParameters = usageParameters,
passkey = passkey
)
)
)
},
onOriginNotChecked = {
throw SecurityException("Wrong signature for ${usageParameters.androidApp.id}")
}
) )
) } ?: run {
throw IOException("Usage parameters is null")
}
} ?: run { } ?: run {
Log.e(TAG, "Unable to return passkey, usage parameters are empty") throw IOException("Passkey is null")
} }
} ?: run { } catch (e: Exception) {
Log.e(TAG, "Unable to get the passkey for response") Log.e(TAG, "Unable to create selection response for passkey", e)
showError(e)
} }
// Return the response // Return the response
responseIntent responseIntent
@@ -113,21 +129,29 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
this.buildActivityResultLauncher( this.buildActivityResultLauncher(
lockDatabase = true, lockDatabase = true,
dataTransformation = { intent -> dataTransformation = { intent ->
Log.d(TAG, "Passkey registration result")
val passkey = intent?.retrievePasskey()
intent?.removePasskey()
// Build a new formatted response from the creation response // Build a new formatted response from the creation response
val responseIntent = Intent() val responseIntent = Intent()
// If registered passkey is the same as the one we want to validate, try {
if (mPasskey == passkey) { Log.d(TAG, "Passkey registration result")
mCreationParameters?.let { val passkey = intent?.retrievePasskey()
PendingIntentHandler.setCreateCredentialResponse( intent?.removePasskey()
intent = responseIntent, intent?.removeAppOrigin()
response = buildCreatePublicKeyCredentialResponse( // If registered passkey is the same as the one we want to validate,
publicKeyCredentialCreationParameters = it if (mPasskey == passkey) {
mCreationParameters?.let {
PendingIntentHandler.setCreateCredentialResponse(
intent = responseIntent,
response = buildCreatePublicKeyCredentialResponse(
publicKeyCredentialCreationParameters = it
)
) )
) }
} else {
throw SecurityException("Passkey was modified before registration")
} }
} catch (e: Exception) {
Log.e(TAG, "Unable to create registration response for passkey", e)
showError(e)
} }
responseIntent responseIntent
} }
@@ -141,31 +165,27 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
return false return false
} }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mSearchInfo = intent.retrieveSearchInfo() ?: mSearchInfo
mOriginApp = intent.retrieveOriginAppInfo() ?: mOriginApp
}
override fun onDatabaseRetrieved(database: ContextualDatabase?) { override fun onDatabaseRetrieved(database: ContextualDatabase?) {
super.onDatabaseRetrieved(database) super.onDatabaseRetrieved(database)
lifecycleScope.launch(CoroutineExceptionHandler { _, e -> lifecycleScope.launch(CoroutineExceptionHandler { _, e ->
Log.e(TAG, "Passkey launch error", e) Log.e(TAG, "Passkey launch error", e)
Toast.makeText(this, e.localizedMessage, Toast.LENGTH_LONG).show() showError(e)
setResult(RESULT_CANCELED) setResult(RESULT_CANCELED)
finish() finish()
}) { }) {
val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo()
val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin()
val nodeId = intent.retrieveNodeId() val nodeId = intent.retrieveNodeId()
checkSecurity(intent, nodeId) checkSecurity(intent, nodeId)
when (mSpecialMode) { when (mSpecialMode) {
SpecialMode.SELECTION -> { SpecialMode.SELECTION -> {
launchSelection(database, nodeId, mSearchInfo, mOriginApp) launchSelection(database, nodeId, searchInfo, appOrigin)
} }
SpecialMode.REGISTRATION -> { SpecialMode.REGISTRATION -> {
// TODO Registration in predefined group // TODO Registration in predefined group
// launchRegistration(database, nodeId, mSearchInfo) // launchRegistration(database, nodeId, mSearchInfo)
launchRegistration(database, null, mSearchInfo) launchRegistration(database, null, searchInfo)
} }
else -> { else -> {
throw InvalidObjectException("Passkey launch mode not supported") throw InvalidObjectException("Passkey launch mode not supported")
@@ -209,14 +229,17 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
database: ContextualDatabase?, database: ContextualDatabase?,
nodeId: UUID?, nodeId: UUID?,
searchInfo: SearchInfo?, searchInfo: SearchInfo?,
originApp: OriginApp? appOrigin: AppOrigin?
) { ) {
Log.d(TAG, "Launch passkey selection") Log.d(TAG, "Launch passkey selection")
retrievePasskeyUsageRequestParameters(intent, assets, originApp) { usageParameters -> retrievePasskeyUsageRequestParameters(intent, assets, appOrigin) { usageParameters ->
// Save the requested parameters // Save the requested parameters
mUsageParameters = usageParameters mUsageParameters = usageParameters
// Manage the passkey to use // Manage the passkey to use
nodeId?.let { nodeId -> nodeId?.let { nodeId ->
if (usageParameters.androidAppVerified.not()) {
throw SecurityException("Wrong signature for ${usageParameters.androidApp.id}")
}
autoSelectPasskeyAndSetResult(database, nodeId) autoSelectPasskeyAndSetResult(database, nodeId)
} ?: run { } ?: run {
SearchHelper.checkAutoSearchInfo( SearchHelper.checkAutoSearchInfo(
@@ -290,7 +313,7 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
val registerInfo = RegisterInfo( val registerInfo = RegisterInfo(
searchInfo = searchInfo, searchInfo = searchInfo,
passkey = passkey, passkey = passkey,
originApp = appInfoToStore appOrigin = appInfoToStore
) )
// If nodeId already provided // If nodeId already provided
nodeId?.let { nodeId -> nodeId?.let { nodeId ->
@@ -336,6 +359,10 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
) )
} }
private fun showError(e: Throwable) {
Toast.makeText(this, e.localizedMessage, Toast.LENGTH_LONG).show()
}
companion object { companion object {
private val TAG = PasskeyLauncherActivity::class.java.name private val TAG = PasskeyLauncherActivity::class.java.name
@@ -352,7 +379,7 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
context: Context, context: Context,
specialMode: SpecialMode, specialMode: SpecialMode,
searchInfo: SearchInfo? = null, searchInfo: SearchInfo? = null,
originApp: OriginApp? = null, appOrigin: AppOrigin? = null,
nodeId: UUID? = null nodeId: UUID? = null
): PendingIntent? { ): PendingIntent? {
return PendingIntent.getActivity( return PendingIntent.getActivity(
@@ -362,7 +389,7 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
addSpecialMode(specialMode) addSpecialMode(specialMode)
addTypeMode(TypeMode.PASSKEY) addTypeMode(TypeMode.PASSKEY)
addSearchInfo(searchInfo) addSearchInfo(searchInfo)
addOriginAppInfo(originApp) addAppOrigin(appOrigin)
addNodeId(nodeId) addNodeId(nodeId)
addAuthCode(nodeId) addAuthCode(nodeId)
}, },

View File

@@ -144,7 +144,7 @@ class PasskeyProviderService : CredentialProviderService() {
context = applicationContext, context = applicationContext,
specialMode = SpecialMode.SELECTION, specialMode = SpecialMode.SELECTION,
nodeId = passkeyEntry.id, nodeId = passkeyEntry.id,
originApp = passkeyEntry.originApp appOrigin = passkeyEntry.appOrigin
)?.let { usagePendingIntent -> )?.let { usagePendingIntent ->
val passkey = passkeyEntry.passkey val passkey = passkeyEntry.passkey
passkeyEntries.add( passkeyEntries.add(

View File

@@ -19,7 +19,11 @@
*/ */
package com.kunzisoft.keepass.credentialprovider.passkey.data package com.kunzisoft.keepass.credentialprovider.passkey.data
import com.kunzisoft.keepass.model.AppIdentifier
data class PublicKeyCredentialUsageParameters( data class PublicKeyCredentialUsageParameters(
val publicKeyCredentialRequestOptions: PublicKeyCredentialRequestOptions, val publicKeyCredentialRequestOptions: PublicKeyCredentialRequestOptions,
val clientDataResponse: ClientDataResponse val clientDataResponse: ClientDataResponse,
val androidApp: AppIdentifier,
var androidAppVerified: Boolean
) )

View File

@@ -25,7 +25,8 @@ import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.credentials.provider.CallingAppInfo import androidx.credentials.provider.CallingAppInfo
import com.kunzisoft.encrypt.HashManager.getApplicationSignatures import com.kunzisoft.encrypt.HashManager.getApplicationSignatures
import com.kunzisoft.keepass.model.OriginApp import com.kunzisoft.keepass.model.AppIdentifier
import com.kunzisoft.keepass.model.AppOrigin
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -37,47 +38,62 @@ class OriginManager(
) { ) {
suspend fun getOriginAtCreation( suspend fun getOriginAtCreation(
onOriginRetrieved: (appInfoToStore: OriginApp, clientDataHash: ByteArray) -> Unit, onOriginRetrieved: (appInfoToStore: AppOrigin, clientDataHash: ByteArray) -> Unit,
onOriginCreated: (appInfoToStore: OriginApp, origin: String) -> Unit onOriginCreated: (appInfoToStore: AppOrigin, origin: String) -> Unit
) { ) {
getOrigin( getOrigin(
onOriginRetrieved = { callOrigin, clientDataHash -> onOriginRetrieved = { appIdentifier, callOrigin, clientDataHash ->
onOriginRetrieved(OriginApp(webDomain = callOrigin), clientDataHash) onOriginRetrieved(
AppOrigin().apply {
addIdentifier(appIdentifier)
addWebDomain(callOrigin)
},
clientDataHash
)
}, },
onOriginNotRetrieved = { storeAppInfo -> onOriginNotRetrieved = { appIdentifier ->
// Create a new Android Origin and prepare the signature app storage // Create a new Android Origin and prepare the signature app storage
onOriginCreated( onOriginCreated(
storeAppInfo, AppOrigin().apply { addIdentifier(appIdentifier) },
buildAndroidOrigin(storeAppInfo.appId) appIdentifier.buildAndroidOrigin()
) )
} }
) )
} }
/**
* Retrieve the Android origin from an [AppOrigin],
* call [onOriginRetrieved] if the origin is already calculated by the system
* call [onOriginCreated] if the origin was created manually, origin is verified if present in the KeePass database
*/
suspend fun getOriginAtUsage( suspend fun getOriginAtUsage(
appInfoStored: OriginApp?, appOrigin: AppOrigin?,
onOriginRetrieved: (clientDataHash: ByteArray) -> Unit, onOriginRetrieved: (appIdentifier: AppIdentifier, clientDataHash: ByteArray) -> Unit,
onOriginCreated: (origin: String) -> Unit onOriginCreated: (appIdentifier: AppIdentifier, origin: String, originVerified: Boolean) -> Unit
) { ) {
getOrigin( getOrigin(
onOriginRetrieved = { origin, clientDataHash -> onOriginRetrieved = { appIdentifier, origin, clientDataHash ->
onOriginRetrieved(clientDataHash) onOriginRetrieved(appIdentifier, clientDataHash)
}, },
onOriginNotRetrieved = { appInfoCalled -> onOriginNotRetrieved = { appIdentifierToCheck ->
// Verify the app signature to retrieve the origin // Verify the app signature to retrieve the origin
if (appInfoCalled.appId == appInfoStored?.appId val androidOrigin = appIdentifierToCheck.buildAndroidOrigin()
&& appInfoCalled.appSignature == appInfoStored?.appSignature) { appIdentifierToCheck.checkInAppOrigin(
onOriginCreated(buildAndroidOrigin(appInfoCalled.appId)) appOrigin = appOrigin,
} else { onOriginChecked = {
throw SecurityException("Wrong signature for ${appInfoCalled.appId}, ${appInfoCalled.appSignature} retrieved but ${appInfoStored?.appSignature} expected") onOriginCreated(appIdentifierToCheck, androidOrigin, true)
} },
onOriginNotChecked = {
onOriginCreated(appIdentifierToCheck, androidOrigin, false)
}
)
} }
) )
} }
private suspend fun getOrigin( private suspend fun getOrigin(
onOriginRetrieved: (origin: String, clientDataHash: ByteArray) -> Unit, onOriginRetrieved: (appInfoRetrieved: AppIdentifier, origin: String, clientDataHash: ByteArray) -> Unit,
onOriginNotRetrieved: (appInfoRetrieved: OriginApp) -> Unit onOriginNotRetrieved: (appInfoRetrieved: AppIdentifier) -> Unit
) { ) {
if (callingAppInfo == null) { if (callingAppInfo == null) {
throw SecurityException("Calling app info cannot be retrieved") throw SecurityException("Calling app info cannot be retrieved")
@@ -89,34 +105,60 @@ class OriginManager(
} }
// for trusted browsers like Chrome and Firefox // for trusted browsers like Chrome and Firefox
callOrigin = callingAppInfo.getOrigin(privilegedAllowlist)?.removeSuffix("/") callOrigin = callingAppInfo.getOrigin(privilegedAllowlist)?.removeSuffix("/")
val appIdentifier = AppIdentifier(
id = callingAppInfo.packageName,
signature = callingAppInfo.signingInfo
.getApplicationSignatures()
)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (callOrigin != null && providedClientDataHash != null) { if (callOrigin != null && providedClientDataHash != null) {
Log.d(TAG, "Origin $callOrigin retrieved from callingAppInfo") Log.d(TAG, "Origin $callOrigin retrieved from callingAppInfo")
onOriginRetrieved(callOrigin, providedClientDataHash) onOriginRetrieved(appIdentifier, callOrigin, providedClientDataHash)
} else { } else {
onOriginNotRetrieved( onOriginNotRetrieved(appIdentifier)
OriginApp(
appId = callingAppInfo.packageName,
appSignature = callingAppInfo.signingInfo.getApplicationSignatures()
)
)
} }
} }
} }
} }
/**
* Builds an Android Origin from a package name.
*/
private fun buildAndroidOrigin(packageName: String?): String {
if (packageName.isNullOrEmpty())
throw SecurityException("Package name cannot be empty")
val packageOrigin = "androidapp://${packageName}"
Log.d(TAG, "Origin $packageOrigin retrieved from package name")
return packageOrigin
}
companion object { companion object {
private val TAG = OriginManager::class.simpleName private val TAG = OriginManager::class.simpleName
/**
* Verify that the application signature is contained in the [appOrigin]
*/
fun AppIdentifier.checkInAppOrigin(
appOrigin: AppOrigin?,
onOriginChecked: (origin: String) -> Unit,
onOriginNotChecked: () -> Unit
) {
// Verify the app signature to retrieve the origin
val appIdentifierStored = appOrigin?.appIdentifiers?.filter {
it.id == this.id
}
if (appIdentifierStored?.any { it.signature == this.signature } == true) {
onOriginChecked(this.buildAndroidOrigin())
} else {
onOriginNotChecked()
}
}
/**
* Builds an Android Origin from a AppIdentifier
*/
fun AppIdentifier.buildAndroidOrigin(): String {
return buildAndroidOrigin(this.id)
}
/**
* Builds an Android Origin from a package name.
*/
private fun buildAndroidOrigin(packageName: String?): String {
if (packageName.isNullOrEmpty())
throw SecurityException("Package name cannot be empty")
val packageOrigin = "androidapp://${packageName}"
Log.d(TAG, "Origin $packageOrigin retrieved from package name")
return packageOrigin
}
} }
} }

View File

@@ -50,8 +50,8 @@ import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredential
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Encode import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Encode
import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.OriginApp
import com.kunzisoft.keepass.model.Passkey import com.kunzisoft.keepass.model.Passkey
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.utils.StringUtil.toHexString import com.kunzisoft.keepass.utils.StringUtil.toHexString
@@ -68,13 +68,13 @@ import javax.crypto.SecretKey
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
object PasskeyHelper { object PasskeyHelper {
private const val EXTRA_PASSKEY_ELEMENT = "com.kunzisoft.keepass.passkey.extra.EXTRA_PASSKEY_ELEMENT" private const val EXTRA_PASSKEY = "com.kunzisoft.keepass.passkey.extra.passkey"
private const val HMAC_TYPE = "HmacSHA256" private const val HMAC_TYPE = "HmacSHA256"
private const val EXTRA_SEARCH_INFO = "com.kunzisoft.keepass.extra.SEARCH_INFO" private const val EXTRA_SEARCH_INFO = "com.kunzisoft.keepass.extra.searchInfo"
private const val EXTRA_ORIGIN_APP_INFO = "com.kunzisoft.keepass.extra.ORIGIN_INFO" private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin"
private const val EXTRA_NODE_ID = "com.kunzisoft.keepass.extra.nodeId" private const val EXTRA_NODE_ID = "com.kunzisoft.keepass.extra.nodeId"
private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp" private const val EXTRA_TIMESTAMP = "com.kunzisoft.keepass.extra.timestamp"
private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode" private const val EXTRA_AUTHENTICATION_CODE = "com.kunzisoft.keepass.extra.authenticationCode"
@@ -103,7 +103,8 @@ object PasskeyHelper {
entryInfo.passkey?.let { entryInfo.passkey?.let {
val mReplyIntent = Intent() val mReplyIntent = Intent()
Log.d(javaClass.name, "Success Passkey manual selection") Log.d(javaClass.name, "Success Passkey manual selection")
mReplyIntent.putExtra(EXTRA_PASSKEY_ELEMENT, entryInfo.passkey) mReplyIntent.putExtra(EXTRA_PASSKEY, entryInfo.passkey)
mReplyIntent.putExtra(EXTRA_APP_ORIGIN, entryInfo.appOrigin)
extras?.let { extras?.let {
mReplyIntent.putExtras(it) mReplyIntent.putExtras(it)
} }
@@ -132,11 +133,11 @@ object PasskeyHelper {
} }
fun Intent.retrievePasskey(): Passkey? { fun Intent.retrievePasskey(): Passkey? {
return this.getParcelableExtraCompat(EXTRA_PASSKEY_ELEMENT) return this.getParcelableExtraCompat(EXTRA_PASSKEY)
} }
fun Intent.removePasskey() { fun Intent.removePasskey() {
return this.removeExtra(EXTRA_PASSKEY_ELEMENT) return this.removeExtra(EXTRA_PASSKEY)
} }
fun Intent.addSearchInfo(searchInfo: SearchInfo?) { fun Intent.addSearchInfo(searchInfo: SearchInfo?) {
@@ -149,14 +150,18 @@ object PasskeyHelper {
return this.getParcelableExtraCompat(EXTRA_SEARCH_INFO) return this.getParcelableExtraCompat(EXTRA_SEARCH_INFO)
} }
fun Intent.addOriginAppInfo(originApp: OriginApp?) { fun Intent.addAppOrigin(appOrigin: AppOrigin?) {
originApp?.let { appOrigin?.let {
putExtra(EXTRA_ORIGIN_APP_INFO, originApp) putExtra(EXTRA_APP_ORIGIN, appOrigin)
} }
} }
fun Intent.retrieveOriginAppInfo(): OriginApp? { fun Intent.retrieveAppOrigin(): AppOrigin? {
return this.getParcelableExtraCompat(EXTRA_ORIGIN_APP_INFO) return this.getParcelableExtraCompat(EXTRA_APP_ORIGIN)
}
fun Intent.removeAppOrigin() {
return this.removeExtra(EXTRA_APP_ORIGIN)
} }
fun Intent.addNodeId(nodeId: UUID?) { fun Intent.addNodeId(nodeId: UUID?) {
@@ -261,7 +266,7 @@ object PasskeyHelper {
suspend fun retrievePasskeyCreationRequestParameters( suspend fun retrievePasskeyCreationRequestParameters(
intent: Intent, intent: Intent,
assetManager: AssetManager, assetManager: AssetManager,
passkeyCreated: (Passkey, OriginApp?, PublicKeyCredentialCreationParameters) -> Unit passkeyCreated: (Passkey, AppOrigin?, PublicKeyCredentialCreationParameters) -> Unit
) { ) {
val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent) val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
if (createCredentialRequest == null) if (createCredentialRequest == null)
@@ -362,7 +367,7 @@ object PasskeyHelper {
suspend fun retrievePasskeyUsageRequestParameters( suspend fun retrievePasskeyUsageRequestParameters(
intent: Intent, intent: Intent,
assetManager: AssetManager, assetManager: AssetManager,
originApp: OriginApp?, appOrigin: AppOrigin?,
result: (PublicKeyCredentialUsageParameters) -> Unit result: (PublicKeyCredentialUsageParameters) -> Unit
) { ) {
val getCredentialRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent) val getCredentialRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
@@ -379,16 +384,18 @@ object PasskeyHelper {
callingAppInfo = callingAppInfo, callingAppInfo = callingAppInfo,
assets = assetManager assets = assetManager
).getOriginAtUsage( ).getOriginAtUsage(
appInfoStored = originApp, appOrigin = appOrigin,
onOriginRetrieved = { clientDataHash -> onOriginRetrieved = { appIdentifier, clientDataHash ->
result.invoke( result.invoke(
PublicKeyCredentialUsageParameters( PublicKeyCredentialUsageParameters(
publicKeyCredentialRequestOptions = requestOptions, publicKeyCredentialRequestOptions = requestOptions,
clientDataResponse = ClientDataDefinedResponse(clientDataHash) clientDataResponse = ClientDataDefinedResponse(clientDataHash),
androidApp = appIdentifier,
androidAppVerified = true
) )
) )
}, },
onOriginCreated = { origin -> onOriginCreated = { appIdentifier, origin, verified ->
result.invoke( result.invoke(
PublicKeyCredentialUsageParameters( PublicKeyCredentialUsageParameters(
publicKeyCredentialRequestOptions = requestOptions, publicKeyCredentialRequestOptions = requestOptions,
@@ -397,7 +404,9 @@ object PasskeyHelper {
challenge = requestOptions.challenge, challenge = requestOptions.challenge,
origin = origin, origin = origin,
crossOrigin = false // TODO should always be false? crossOrigin = false // TODO should always be false?
) ),
androidApp = appIdentifier,
androidAppVerified = verified
) )
) )
} }

View File

@@ -534,10 +534,15 @@ abstract class TemplateAbstractView<
} }
protected fun getCustomField(fieldName: String): Field { protected fun getCustomField(fieldName: String): Field {
return getCustomFieldOrNull(fieldName)
?: Field(fieldName, ProtectedString(false))
}
protected fun getCustomFieldOrNull(fieldName: String): Field? {
return getCustomField(fieldName, return getCustomField(fieldName,
templateFieldNotEmpty = false, templateFieldNotEmpty = false,
retrieveDefaultValues = false retrieveDefaultValues = false
) ?: Field(fieldName, ProtectedString(false)) )
} }
private fun getCustomField(fieldName: String, private fun getCustomField(fieldName: String,

View File

@@ -20,7 +20,7 @@ import com.kunzisoft.keepass.database.helper.getLocalizedName
import com.kunzisoft.keepass.database.helper.isStandardPasswordName import com.kunzisoft.keepass.database.helper.isStandardPasswordName
import com.kunzisoft.keepass.model.DataDate import com.kunzisoft.keepass.model.DataDate
import com.kunzisoft.keepass.model.DataTime import com.kunzisoft.keepass.model.DataTime
import com.kunzisoft.keepass.model.OriginAppEntryField import com.kunzisoft.keepass.model.AppOriginEntryField
import com.kunzisoft.keepass.model.PasskeyEntryFields import com.kunzisoft.keepass.model.PasskeyEntryFields
import com.kunzisoft.keepass.otp.OtpEntryFields import com.kunzisoft.keepass.otp.OtpEntryFields
@@ -260,15 +260,12 @@ class TemplateEditView @JvmOverloads constructor(context: Context,
override fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean, override fun populateEntryInfoWithViews(templateFieldNotEmpty: Boolean,
retrieveDefaultValues: Boolean) { retrieveDefaultValues: Boolean) {
super.populateEntryInfoWithViews(templateFieldNotEmpty, retrieveDefaultValues) super.populateEntryInfoWithViews(templateFieldNotEmpty, retrieveDefaultValues)
mEntryInfo?.otpModel = OtpEntryFields.parseFields { key -> val getField: (id: String) -> String? = { key ->
getCustomField(key).protectedValue.toString() getCustomFieldOrNull(key)?.protectedValue?.stringValue
}?.otpModel
mEntryInfo?.passkey = PasskeyEntryFields.parseFields { key ->
getCustomField(key).protectedValue.toString()
}
mEntryInfo?.originApp = OriginAppEntryField.parseFields { key ->
getCustomField(key).protectedValue.toString()
} }
mEntryInfo?.otpModel = OtpEntryFields.parseFields(getField)?.otpModel
mEntryInfo?.passkey = PasskeyEntryFields.parseFields(getField)
mEntryInfo?.appOrigin = AppOriginEntryField.parseFields(getField)
} }
override fun onRestoreEntryInstanceState(state: SavedState) { override fun onRestoreEntryInstanceState(state: SavedState) {

View File

@@ -36,7 +36,7 @@ import androidx.core.text.util.LinkifyCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.kunzisoft.keepass.R import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.OriginAppEntryField.APPLICATION_ID_FIELD_NAME import com.kunzisoft.keepass.model.AppOriginEntryField.APPLICATION_ID_FIELD_NAME
import com.kunzisoft.keepass.utils.UriUtil.openExternalApp import com.kunzisoft.keepass.utils.UriUtil.openExternalApp

View File

@@ -33,9 +33,9 @@ import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.database.element.node.NodeIdUUID import com.kunzisoft.keepass.database.element.node.NodeIdUUID
import com.kunzisoft.keepass.database.element.node.Type import com.kunzisoft.keepass.database.element.node.Type
import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.AppOriginEntryField
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.OriginApp
import com.kunzisoft.keepass.model.OriginAppEntryField
import com.kunzisoft.keepass.model.Passkey import com.kunzisoft.keepass.model.Passkey
import com.kunzisoft.keepass.model.PasskeyEntryFields import com.kunzisoft.keepass.model.PasskeyEntryFields
import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpElement
@@ -367,9 +367,9 @@ class Entry : Node, EntryVersionedInterface<Group> {
return null return null
} }
fun getOriginApp(): OriginApp? { fun getAppOrigin(): AppOrigin? {
entryKDBX?.let { entryKDBX?.let {
return OriginAppEntryField.parseFields { key -> return AppOriginEntryField.parseFields { key ->
it.getFieldValue(key)?.toString() it.getFieldValue(key)?.toString()
} }
} }
@@ -494,7 +494,7 @@ class Entry : Node, EntryVersionedInterface<Group> {
entryInfo.otpModel = getOtpElement()?.otpModel entryInfo.otpModel = getOtpElement()?.otpModel
// Add Passkey // Add Passkey
entryInfo.passkey = getPasskey() entryInfo.passkey = getPasskey()
entryInfo.originApp = getOriginApp() entryInfo.appOrigin = getAppOrigin()
if (!raw) { if (!raw) {
// Replace parameter fields by generated OTP fields // Replace parameter fields by generated OTP fields
entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields) entryInfo.customFields = OtpEntryFields.generateAutoFields(entryInfo.customFields)

View File

@@ -23,8 +23,39 @@ import android.os.Parcelable
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class OriginApp( data class AppOrigin(
val appId: String? = null, val appIdentifiers: MutableList<AppIdentifier> = mutableListOf(),
val appSignature: String? = null, val webDomains: MutableList<String> = mutableListOf()
val webDomain: String? = null ) : Parcelable {
fun addIdentifier(appIdentifier: AppIdentifier) {
appIdentifiers.add(appIdentifier)
}
fun addWebDomain(webDomain: String) {
this.webDomains.add(webDomain)
}
fun removeAppElement(appIdentifier: AppIdentifier) {
appIdentifiers.remove(appIdentifier)
}
fun removeWebDomain(webDomain: String) {
this.webDomains.remove(webDomain)
}
fun clear() {
appIdentifiers.clear()
webDomains.clear()
}
fun isEmpty(): Boolean {
return appIdentifiers.isEmpty() && webDomains.isEmpty()
}
}
@Parcelize
data class AppIdentifier(
val id: String,
val signature: String? = null,
) : Parcelable ) : Parcelable

View File

@@ -21,25 +21,51 @@ package com.kunzisoft.keepass.model
import com.kunzisoft.keepass.database.element.Field import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.security.ProtectedString import com.kunzisoft.keepass.database.element.security.ProtectedString
import com.kunzisoft.keepass.model.EntryInfo.Companion.suffixFieldNamePosition
object OriginAppEntryField { object AppOriginEntryField {
const val WEB_DOMAIN_FIELD_NAME = "URL" const val WEB_DOMAIN_FIELD_NAME = "URL"
const val APPLICATION_ID_FIELD_NAME = "AndroidApp" const val APPLICATION_ID_FIELD_NAME = "AndroidApp"
const val APPLICATION_SIGNATURE_FIELD_NAME = "AndroidApp Signature" const val APPLICATION_SIGNATURE_FIELD_NAME = "AndroidApp Signature"
/** /**
* Parse fields of an entry to retrieve a an OriginApp * Parse fields of an entry to retrieve a an AppOrigin
*/ */
fun parseFields(getField: (id: String) -> String?): OriginApp { fun parseFields(getField: (id: String) -> String?): AppOrigin {
val appIdField = getField(APPLICATION_ID_FIELD_NAME) val appOrigin = AppOrigin()
val appSignatureField = getField(APPLICATION_SIGNATURE_FIELD_NAME) // Get Application identifiers
val webDomainField = getField(WEB_DOMAIN_FIELD_NAME) generateSequence(0) { it + 1 }
return OriginApp( .map { position ->
appId = appIdField, val appId = getField(APPLICATION_ID_FIELD_NAME + suffixFieldNamePosition(position))
appSignature = appSignatureField, val appSignature = getField(APPLICATION_SIGNATURE_FIELD_NAME + suffixFieldNamePosition(position))
webDomain = webDomainField // Pair them up, if appId is null, we stop
) if (appId != null) {
appId to (appSignature ?: "")
} else {
// Stop
null
}
}.takeWhile { it != null }
.forEach { pair ->
appOrigin.addIdentifier(
AppIdentifier(pair!!.first, pair.second)
)
}
// Get Domains
var domainFieldPosition = 0
while (true) {
val domainKey = WEB_DOMAIN_FIELD_NAME + suffixFieldNamePosition(domainFieldPosition)
val domainValue = getField(domainKey)
if (domainValue != null) {
appOrigin.addWebDomain(domainValue)
domainFieldPosition++
} else {
break // No more domain found
}
}
return appOrigin
} }
/** /**
@@ -102,10 +128,16 @@ object OriginAppEntryField {
} }
} }
fun EntryInfo.setOriginApp(originApp: OriginApp?, customFieldsAllowed: Boolean) { /**
if (originApp != null) { * Assign an AppOrigin to an EntryInfo,
setApplicationId(originApp.appId, originApp.appSignature) * Only if [customFieldsAllowed] is true
setWebDomain(originApp.webDomain, null, customFieldsAllowed) */
fun EntryInfo.setAppOrigin(appOrigin: AppOrigin?, customFieldsAllowed: Boolean) {
appOrigin?.appIdentifiers?.forEach { appIdentifier ->
setApplicationId(appIdentifier.id, appIdentifier.signature)
}
appOrigin?.webDomains?.forEach { webDomain ->
setWebDomain(webDomain, null, customFieldsAllowed)
} }
} }
} }

View File

@@ -27,10 +27,10 @@ import com.kunzisoft.keepass.database.element.Database
import com.kunzisoft.keepass.database.element.Field import com.kunzisoft.keepass.database.element.Field
import com.kunzisoft.keepass.database.element.Tags import com.kunzisoft.keepass.database.element.Tags
import com.kunzisoft.keepass.database.element.entry.AutoType import com.kunzisoft.keepass.database.element.entry.AutoType
import com.kunzisoft.keepass.model.AppOriginEntryField.setAppOrigin
import com.kunzisoft.keepass.model.AppOriginEntryField.setApplicationId
import com.kunzisoft.keepass.model.AppOriginEntryField.setWebDomain
import com.kunzisoft.keepass.model.CreditCardEntryFields.setCreditCard import com.kunzisoft.keepass.model.CreditCardEntryFields.setCreditCard
import com.kunzisoft.keepass.model.OriginAppEntryField.setApplicationId
import com.kunzisoft.keepass.model.OriginAppEntryField.setOriginApp
import com.kunzisoft.keepass.model.OriginAppEntryField.setWebDomain
import com.kunzisoft.keepass.model.PasskeyEntryFields.isPasskeyExclusion import com.kunzisoft.keepass.model.PasskeyEntryFields.isPasskeyExclusion
import com.kunzisoft.keepass.model.PasskeyEntryFields.setPasskey import com.kunzisoft.keepass.model.PasskeyEntryFields.setPasskey
import com.kunzisoft.keepass.otp.OtpElement import com.kunzisoft.keepass.otp.OtpElement
@@ -59,7 +59,7 @@ class EntryInfo : NodeInfo {
var autoType: AutoType = AutoType() var autoType: AutoType = AutoType()
var otpModel: OtpModel? = null var otpModel: OtpModel? = null
var passkey: Passkey? = null var passkey: Passkey? = null
var originApp: OriginApp? = null var appOrigin: AppOrigin? = null
var isTemplate: Boolean = false var isTemplate: Boolean = false
constructor() : super() constructor() : super()
@@ -80,7 +80,7 @@ class EntryInfo : NodeInfo {
autoType = parcel.readParcelableCompat() ?: autoType autoType = parcel.readParcelableCompat() ?: autoType
otpModel = parcel.readParcelableCompat() ?: otpModel otpModel = parcel.readParcelableCompat() ?: otpModel
passkey = parcel.readParcelableCompat() ?: passkey passkey = parcel.readParcelableCompat() ?: passkey
originApp = parcel.readParcelableCompat() ?: originApp appOrigin = parcel.readParcelableCompat() ?: appOrigin
isTemplate = parcel.readBooleanCompat() isTemplate = parcel.readBooleanCompat()
} }
@@ -103,7 +103,7 @@ class EntryInfo : NodeInfo {
parcel.writeParcelable(autoType, flags) parcel.writeParcelable(autoType, flags)
parcel.writeParcelable(otpModel, flags) parcel.writeParcelable(otpModel, flags)
parcel.writeParcelable(passkey, flags) parcel.writeParcelable(passkey, flags)
parcel.writeParcelable(originApp, flags) parcel.writeParcelable(appOrigin, flags)
parcel.writeBooleanCompat(isTemplate) parcel.writeBooleanCompat(isTemplate)
} }
@@ -138,13 +138,6 @@ class EntryInfo : NodeInfo {
} ?: customFields.add(field) } ?: customFields.add(field)
} }
/**
* Create a field name suffix depending on the field position
*/
private fun suffixFieldNamePosition(position: Int): String {
return if (position > 0) "_$position" else ""
}
/** /**
* Add a field to the custom fields list with a suffix position, * Add a field to the custom fields list with a suffix position,
* replace if name already exists * replace if name already exists
@@ -218,8 +211,8 @@ class EntryInfo : NodeInfo {
registerInfo.password?.let { password = it } registerInfo.password?.let { password = it }
setCreditCard(registerInfo.creditCard) setCreditCard(registerInfo.creditCard)
setPasskey(registerInfo.passkey) setPasskey(registerInfo.passkey)
setOriginApp( setAppOrigin(
registerInfo.originApp, registerInfo.appOrigin,
database?.allowEntryCustomFields() == true database?.allowEntryCustomFields() == true
) )
} }
@@ -250,7 +243,7 @@ class EntryInfo : NodeInfo {
if (autoType != other.autoType) return false if (autoType != other.autoType) return false
if (otpModel != other.otpModel) return false if (otpModel != other.otpModel) return false
if (passkey != other.passkey) return false if (passkey != other.passkey) return false
if (originApp != other.originApp) return false if (appOrigin != other.appOrigin) return false
if (isTemplate != other.isTemplate) return false if (isTemplate != other.isTemplate) return false
return true return true
@@ -271,7 +264,7 @@ class EntryInfo : NodeInfo {
result = 31 * result + autoType.hashCode() result = 31 * result + autoType.hashCode()
result = 31 * result + (otpModel?.hashCode() ?: 0) result = 31 * result + (otpModel?.hashCode() ?: 0)
result = 31 * result + (passkey?.hashCode() ?: 0) result = 31 * result + (passkey?.hashCode() ?: 0)
result = 31 * result + (originApp?.hashCode() ?: 0) result = 31 * result + (appOrigin?.hashCode() ?: 0)
result = 31 * result + isTemplate.hashCode() result = 31 * result + isTemplate.hashCode()
return result return result
} }
@@ -279,6 +272,13 @@ class EntryInfo : NodeInfo {
companion object { companion object {
/**
* Create a field name suffix depending on the field position
*/
fun suffixFieldNamePosition(position: Int): String {
return if (position > 0) "_$position" else ""
}
@JvmField @JvmField
val CREATOR: Parcelable.Creator<EntryInfo> = object : Parcelable.Creator<EntryInfo> { val CREATOR: Parcelable.Creator<EntryInfo> = object : Parcelable.Creator<EntryInfo> {
override fun createFromParcel(parcel: Parcel): EntryInfo { override fun createFromParcel(parcel: Parcel): EntryInfo {

View File

@@ -10,7 +10,7 @@ data class RegisterInfo(
val password: String? = null, val password: String? = null,
val creditCard: CreditCard? = null, val creditCard: CreditCard? = null,
val passkey: Passkey? = null, val passkey: Passkey? = null,
val originApp: OriginApp? = null val appOrigin: AppOrigin? = null
): Parcelable { ): Parcelable {
constructor(parcel: Parcel) : this( constructor(parcel: Parcel) : this(
@@ -19,7 +19,7 @@ data class RegisterInfo(
password = parcel.readString() ?: "", password = parcel.readString() ?: "",
creditCard = parcel.readParcelableCompat(), creditCard = parcel.readParcelableCompat(),
passkey = parcel.readParcelableCompat(), passkey = parcel.readParcelableCompat(),
originApp = parcel.readParcelableCompat() appOrigin = parcel.readParcelableCompat()
) )
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(parcel: Parcel, flags: Int) {
@@ -28,7 +28,7 @@ data class RegisterInfo(
parcel.writeString(password) parcel.writeString(password)
parcel.writeParcelable(creditCard, flags) parcel.writeParcelable(creditCard, flags)
parcel.writeParcelable(passkey, flags) parcel.writeParcelable(passkey, flags)
parcel.writeParcelable(originApp, flags) parcel.writeParcelable(appOrigin, flags)
} }
override fun describeContents(): Int { override fun describeContents(): Int {