mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
fix: Add webOrigin, fix title and add verification state
This commit is contained in:
@@ -9,6 +9,7 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper
|
|||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.isIntentSenderMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
import com.kunzisoft.keepass.credentialprovider.TypeMode
|
||||||
|
import com.kunzisoft.keepass.model.RegisterInfo
|
||||||
import com.kunzisoft.keepass.model.SearchInfo
|
import com.kunzisoft.keepass.model.SearchInfo
|
||||||
import com.kunzisoft.keepass.settings.PreferencesUtil
|
import com.kunzisoft.keepass.settings.PreferencesUtil
|
||||||
import com.kunzisoft.keepass.view.ToolbarSpecial
|
import com.kunzisoft.keepass.view.ToolbarSpecial
|
||||||
@@ -113,7 +114,8 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
|
|
||||||
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
mSpecialMode = EntrySelectionHelper.retrieveSpecialModeFromIntent(intent)
|
||||||
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
mTypeMode = EntrySelectionHelper.retrieveTypeModeFromIntent(intent)
|
||||||
val searchInfo: SearchInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)?.searchInfo
|
val registerInfo: RegisterInfo? = EntrySelectionHelper.retrieveRegisterInfoFromIntent(intent)
|
||||||
|
val searchInfo: SearchInfo? = registerInfo?.searchInfo
|
||||||
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
?: EntrySelectionHelper.retrieveSearchInfoFromIntent(intent)
|
||||||
|
|
||||||
// To show the selection mode
|
// To show the selection mode
|
||||||
@@ -137,7 +139,7 @@ abstract class DatabaseModeActivity : DatabaseActivity() {
|
|||||||
if (mTypeMode != TypeMode.DEFAULT)
|
if (mTypeMode != TypeMode.DEFAULT)
|
||||||
title = "$title (${getString(typeModeStringId)})"
|
title = "$title (${getString(typeModeStringId)})"
|
||||||
// Populate subtitle
|
// Populate subtitle
|
||||||
subtitle = searchInfo?.getName(resources)
|
subtitle = registerInfo?.getName(resources) ?: searchInfo?.getName(resources)
|
||||||
|
|
||||||
// Show the toolbar or not
|
// Show the toolbar or not
|
||||||
visible = when (mSpecialMode) {
|
visible = when (mSpecialMode) {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addSe
|
|||||||
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.getVerifiedClientDataResponse
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getVerifiedGETClientDataResponse
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeAppOrigin
|
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.retrieveAppOrigin
|
||||||
@@ -85,37 +85,31 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
|
|||||||
val responseIntent = Intent()
|
val responseIntent = Intent()
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "Passkey selection result")
|
Log.d(TAG, "Passkey selection result")
|
||||||
val passkey = intent?.retrievePasskey()
|
if (intent == null)
|
||||||
val appOrigin = intent?.retrieveAppOrigin()
|
throw IOException("Intent is null")
|
||||||
intent?.removePasskey()
|
val passkey = intent.retrievePasskey()
|
||||||
intent?.removeAppOrigin()
|
?: throw IOException("Passkey is null")
|
||||||
passkey?.let {
|
val appOrigin = intent.retrieveAppOrigin()
|
||||||
mUsageParameters?.let { usageParameters ->
|
?: throw IOException("App origin is null")
|
||||||
// Check verified origin
|
intent.removePasskey()
|
||||||
getVerifiedClientDataResponse(
|
intent.removeAppOrigin()
|
||||||
usageParameters = usageParameters,
|
mUsageParameters?.let { usageParameters ->
|
||||||
appOrigin = appOrigin,
|
// Check verified origin
|
||||||
onOriginChecked = { clientDataResponse ->
|
PendingIntentHandler.setGetCredentialResponse(
|
||||||
PendingIntentHandler.setGetCredentialResponse(
|
responseIntent,
|
||||||
responseIntent,
|
GetCredentialResponse(
|
||||||
GetCredentialResponse(
|
buildPasskeyPublicKeyCredential(
|
||||||
buildPasskeyPublicKeyCredential(
|
requestOptions = usageParameters.publicKeyCredentialRequestOptions,
|
||||||
requestOptions = usageParameters.publicKeyCredentialRequestOptions,
|
clientDataResponse = getVerifiedGETClientDataResponse(
|
||||||
clientDataResponse = clientDataResponse,
|
usageParameters = usageParameters,
|
||||||
passkey = passkey
|
appOrigin = appOrigin
|
||||||
)
|
),
|
||||||
)
|
passkey = passkey
|
||||||
)
|
)
|
||||||
},
|
|
||||||
onOriginNotChecked = {
|
|
||||||
throw SecurityException("Wrong signature for ${usageParameters.androidApp.id}")
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
} ?: run {
|
)
|
||||||
throw IOException("Usage parameters is null")
|
|
||||||
}
|
|
||||||
} ?: run {
|
} ?: run {
|
||||||
throw IOException("Passkey is null")
|
throw IOException("Usage parameters is null")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to create selection response for passkey", e)
|
Log.e(TAG, "Unable to create selection response for passkey", e)
|
||||||
@@ -197,7 +191,8 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
|
|||||||
|
|
||||||
private fun autoSelectPasskeyAndSetResult(
|
private fun autoSelectPasskeyAndSetResult(
|
||||||
database: ContextualDatabase?,
|
database: ContextualDatabase?,
|
||||||
nodeId: UUID
|
nodeId: UUID,
|
||||||
|
appOrigin: AppOrigin
|
||||||
) {
|
) {
|
||||||
mUsageParameters?.let { usageParameters ->
|
mUsageParameters?.let { usageParameters ->
|
||||||
// To get the passkey from the database
|
// To get the passkey from the database
|
||||||
@@ -213,7 +208,10 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
|
|||||||
GetCredentialResponse(
|
GetCredentialResponse(
|
||||||
buildPasskeyPublicKeyCredential(
|
buildPasskeyPublicKeyCredential(
|
||||||
requestOptions = usageParameters.publicKeyCredentialRequestOptions,
|
requestOptions = usageParameters.publicKeyCredentialRequestOptions,
|
||||||
clientDataResponse = usageParameters.clientDataResponse,
|
clientDataResponse = getVerifiedGETClientDataResponse(
|
||||||
|
usageParameters = usageParameters,
|
||||||
|
appOrigin = appOrigin
|
||||||
|
),
|
||||||
passkey = passkey
|
passkey = passkey
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -230,23 +228,20 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
|
|||||||
private suspend fun launchSelection(
|
private suspend fun launchSelection(
|
||||||
database: ContextualDatabase?,
|
database: ContextualDatabase?,
|
||||||
nodeId: UUID?,
|
nodeId: UUID?,
|
||||||
searchInfo: SearchInfo?,
|
searchInfo: SearchInfo,
|
||||||
appOrigin: AppOrigin?
|
appOrigin: AppOrigin
|
||||||
) {
|
) {
|
||||||
Log.d(TAG, "Launch passkey selection")
|
Log.d(TAG, "Launch passkey selection")
|
||||||
retrievePasskeyUsageRequestParameters(
|
retrievePasskeyUsageRequestParameters(
|
||||||
intent = intent,
|
intent = intent,
|
||||||
assetManager = assets,
|
assetManager = assets,
|
||||||
appOrigin = appOrigin
|
appOrigin = appOrigin,
|
||||||
) { usageParameters ->
|
) { 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()) {
|
autoSelectPasskeyAndSetResult(database, nodeId, appOrigin)
|
||||||
throw SecurityException("Wrong signature for ${usageParameters.androidApp.id}")
|
|
||||||
}
|
|
||||||
autoSelectPasskeyAndSetResult(database, nodeId)
|
|
||||||
} ?: run {
|
} ?: run {
|
||||||
SearchHelper.checkAutoSearchInfo(
|
SearchHelper.checkAutoSearchInfo(
|
||||||
context = this,
|
context = this,
|
||||||
|
|||||||
@@ -19,11 +19,12 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
import com.kunzisoft.keepass.model.AppIdentifier
|
import com.kunzisoft.keepass.model.AndroidOrigin
|
||||||
|
import com.kunzisoft.keepass.model.WebOrigin
|
||||||
|
|
||||||
data class PublicKeyCredentialUsageParameters(
|
data class PublicKeyCredentialUsageParameters(
|
||||||
val publicKeyCredentialRequestOptions: PublicKeyCredentialRequestOptions,
|
val publicKeyCredentialRequestOptions: PublicKeyCredentialRequestOptions,
|
||||||
val clientDataResponse: ClientDataResponse,
|
val clientDataResponse: ClientDataResponse,
|
||||||
val androidApp: AppIdentifier,
|
val androidOrigin: AndroidOrigin,
|
||||||
val androidAppVerified: Boolean
|
val webOrigin: WebOrigin,
|
||||||
)
|
)
|
||||||
@@ -25,8 +25,10 @@ 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.AppIdentifier
|
import com.kunzisoft.keepass.model.AndroidOrigin
|
||||||
import com.kunzisoft.keepass.model.AppOrigin
|
import com.kunzisoft.keepass.model.AppOrigin
|
||||||
|
import com.kunzisoft.keepass.model.Verification
|
||||||
|
import com.kunzisoft.keepass.model.WebOrigin
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@@ -37,7 +39,8 @@ import kotlinx.coroutines.withContext
|
|||||||
class OriginManager(
|
class OriginManager(
|
||||||
private val providedClientDataHash: ByteArray?,
|
private val providedClientDataHash: ByteArray?,
|
||||||
private val callingAppInfo: CallingAppInfo?,
|
private val callingAppInfo: CallingAppInfo?,
|
||||||
private val assets: AssetManager
|
private val assets: AssetManager,
|
||||||
|
private val relyingParty: String,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,20 +53,23 @@ class OriginManager(
|
|||||||
onOriginCreated: (appInfoToStore: AppOrigin, origin: String) -> Unit
|
onOriginCreated: (appInfoToStore: AppOrigin, origin: String) -> Unit
|
||||||
) {
|
) {
|
||||||
getOrigin(
|
getOrigin(
|
||||||
onOriginRetrieved = { appIdentifier, callOrigin, clientDataHash ->
|
onOriginRetrieved = { androidOrigin, webOrigin, callOrigin, clientDataHash ->
|
||||||
onOriginRetrieved(
|
onOriginRetrieved(
|
||||||
AppOrigin().apply {
|
AppOrigin().apply {
|
||||||
// Do not store Web Browser AppId -> addIdentifier(appIdentifier)
|
addAndroidOrigin(androidOrigin)
|
||||||
addWebDomain(callOrigin)
|
addWebOrigin(webOrigin)
|
||||||
},
|
},
|
||||||
clientDataHash
|
clientDataHash
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onOriginNotRetrieved = { appIdentifier ->
|
onOriginNotRetrieved = { appIdentifier, webOrigin ->
|
||||||
// Create a new Android Origin and prepare the signature app storage
|
// Create a new Android Origin and prepare the signature app storage
|
||||||
onOriginCreated(
|
onOriginCreated(
|
||||||
AppOrigin().apply { addIdentifier(appIdentifier) },
|
AppOrigin().apply {
|
||||||
appIdentifier.buildAndroidOrigin()
|
addAndroidOrigin(appIdentifier)
|
||||||
|
addWebOrigin(webOrigin)
|
||||||
|
},
|
||||||
|
appIdentifier.toAndroidOrigin()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -75,25 +81,27 @@ class OriginManager(
|
|||||||
* calls [onOriginCreated] if the origin was created manually, origin is verified if present in the KeePass database
|
* calls [onOriginCreated] if the origin was created manually, origin is verified if present in the KeePass database
|
||||||
*/
|
*/
|
||||||
suspend fun getOriginAtUsage(
|
suspend fun getOriginAtUsage(
|
||||||
appOrigin: AppOrigin?,
|
appOrigin: AppOrigin,
|
||||||
onOriginRetrieved: (appIdentifier: AppIdentifier, clientDataHash: ByteArray) -> Unit,
|
onOriginRetrieved: (androidOrigin: AndroidOrigin, webOrigin: WebOrigin, clientDataHash: ByteArray) -> Unit,
|
||||||
onOriginCreated: (appIdentifier: AppIdentifier, origin: String, originVerified: Boolean) -> Unit
|
onOriginCreated: (androidOrigin: AndroidOrigin, webOrigin: WebOrigin) -> Unit
|
||||||
) {
|
) {
|
||||||
getOrigin(
|
getOrigin(
|
||||||
onOriginRetrieved = { appIdentifier, origin, clientDataHash ->
|
onOriginRetrieved = { androidOrigin, webOrigin, origin, clientDataHash ->
|
||||||
onOriginRetrieved(appIdentifier, clientDataHash)
|
onOriginRetrieved(androidOrigin, webOrigin, clientDataHash)
|
||||||
},
|
},
|
||||||
onOriginNotRetrieved = { appIdentifierToCheck ->
|
onOriginNotRetrieved = { appIdentifierToCheck, webOrigin ->
|
||||||
// Verify the app signature to retrieve the origin
|
// Check the app signature in the appOrigin, webOrigin cannot be checked now
|
||||||
val androidOrigin = appIdentifierToCheck.buildAndroidOrigin()
|
onOriginCreated(
|
||||||
appIdentifierToCheck.checkInAppOrigin(
|
AndroidOrigin(
|
||||||
appOrigin = appOrigin,
|
packageName = appIdentifierToCheck.packageName,
|
||||||
onOriginChecked = {
|
signature = appIdentifierToCheck.signature,
|
||||||
onOriginCreated(appIdentifierToCheck, androidOrigin, true)
|
verification =
|
||||||
},
|
if (appOrigin.containsVerifiedAndroidOrigin(appIdentifierToCheck))
|
||||||
onOriginNotChecked = {
|
Verification.MANUALLY_VERIFIED
|
||||||
onOriginCreated(appIdentifierToCheck, androidOrigin, false)
|
else
|
||||||
}
|
Verification.NOT_VERIFIED
|
||||||
|
),
|
||||||
|
webOrigin
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -106,8 +114,8 @@ class OriginManager(
|
|||||||
* call [onOriginNotRetrieved] if the origin is not retrieved from the system
|
* call [onOriginNotRetrieved] if the origin is not retrieved from the system
|
||||||
*/
|
*/
|
||||||
private suspend fun getOrigin(
|
private suspend fun getOrigin(
|
||||||
onOriginRetrieved: (appInfoRetrieved: AppIdentifier, origin: String, clientDataHash: ByteArray) -> Unit,
|
onOriginRetrieved: (androidOrigin: AndroidOrigin, webOrigin: WebOrigin, origin: String, clientDataHash: ByteArray) -> Unit,
|
||||||
onOriginNotRetrieved: (appInfoRetrieved: AppIdentifier) -> Unit
|
onOriginNotRetrieved: (androidOrigin: AndroidOrigin, webOrigin: WebOrigin) -> Unit
|
||||||
) {
|
) {
|
||||||
if (callingAppInfo == null) {
|
if (callingAppInfo == null) {
|
||||||
throw SecurityException("Calling app info cannot be retrieved")
|
throw SecurityException("Calling app info cannot be retrieved")
|
||||||
@@ -119,17 +127,37 @@ 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(
|
val androidOrigin = AndroidOrigin(
|
||||||
id = callingAppInfo.packageName,
|
packageName = callingAppInfo.packageName,
|
||||||
signature = callingAppInfo.signingInfo
|
signature = callingAppInfo.signingInfo
|
||||||
.getApplicationSignatures()
|
.getApplicationSignatures(),
|
||||||
|
verification = Verification.NOT_VERIFIED
|
||||||
)
|
)
|
||||||
|
// Check if the webDomain is validated for the
|
||||||
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(appIdentifier, callOrigin, providedClientDataHash)
|
onOriginRetrieved(
|
||||||
|
AndroidOrigin(
|
||||||
|
packageName = androidOrigin.packageName,
|
||||||
|
signature = androidOrigin.signature,
|
||||||
|
verification = Verification.AUTOMATICALLY_VERIFIED
|
||||||
|
),
|
||||||
|
WebOrigin.fromRelyingParty(
|
||||||
|
relyingParty = relyingParty,
|
||||||
|
verification = Verification.AUTOMATICALLY_VERIFIED
|
||||||
|
),
|
||||||
|
callOrigin,
|
||||||
|
providedClientDataHash
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
onOriginNotRetrieved(appIdentifier)
|
onOriginNotRetrieved(
|
||||||
|
androidOrigin,
|
||||||
|
WebOrigin.fromRelyingParty(
|
||||||
|
relyingParty = relyingParty,
|
||||||
|
verification = Verification.NOT_VERIFIED
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,42 +165,5 @@ class OriginManager(
|
|||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,11 +51,11 @@ 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.credentialprovider.passkey.util.OriginManager.Companion.checkInAppOrigin
|
|
||||||
import com.kunzisoft.keepass.model.AppOrigin
|
import com.kunzisoft.keepass.model.AppOrigin
|
||||||
import com.kunzisoft.keepass.model.EntryInfo
|
import com.kunzisoft.keepass.model.EntryInfo
|
||||||
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.model.Verification
|
||||||
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
import com.kunzisoft.keepass.utils.StringUtil.toHexString
|
||||||
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
|
||||||
import com.kunzisoft.random.KeePassDXRandom
|
import com.kunzisoft.random.KeePassDXRandom
|
||||||
@@ -364,7 +364,8 @@ object PasskeyHelper {
|
|||||||
OriginManager(
|
OriginManager(
|
||||||
providedClientDataHash = clientDataHash,
|
providedClientDataHash = clientDataHash,
|
||||||
callingAppInfo = callingAppInfo,
|
callingAppInfo = callingAppInfo,
|
||||||
assets = assetManager
|
assets = assetManager,
|
||||||
|
relyingParty = relyingParty
|
||||||
).getOriginAtCreation(
|
).getOriginAtCreation(
|
||||||
onOriginRetrieved = { appInfoToStore, clientDataHash ->
|
onOriginRetrieved = { appInfoToStore, clientDataHash ->
|
||||||
passkeyCreated.invoke(
|
passkeyCreated.invoke(
|
||||||
@@ -441,7 +442,7 @@ object PasskeyHelper {
|
|||||||
suspend fun retrievePasskeyUsageRequestParameters(
|
suspend fun retrievePasskeyUsageRequestParameters(
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
assetManager: AssetManager,
|
assetManager: AssetManager,
|
||||||
appOrigin: AppOrigin?,
|
appOrigin: AppOrigin,
|
||||||
result: (PublicKeyCredentialUsageParameters) -> Unit
|
result: (PublicKeyCredentialUsageParameters) -> Unit
|
||||||
) {
|
) {
|
||||||
val getCredentialRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
|
val getCredentialRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
|
||||||
@@ -456,30 +457,32 @@ object PasskeyHelper {
|
|||||||
OriginManager(
|
OriginManager(
|
||||||
providedClientDataHash = clientDataHash,
|
providedClientDataHash = clientDataHash,
|
||||||
callingAppInfo = callingAppInfo,
|
callingAppInfo = callingAppInfo,
|
||||||
assets = assetManager
|
assets = assetManager,
|
||||||
|
relyingParty = requestOptions.rpId
|
||||||
).getOriginAtUsage(
|
).getOriginAtUsage(
|
||||||
appOrigin = appOrigin,
|
appOrigin = appOrigin,
|
||||||
onOriginRetrieved = { appIdentifier, clientDataHash ->
|
onOriginRetrieved = { androidOrigin, webOrigin, clientDataHash ->
|
||||||
result.invoke(
|
result.invoke(
|
||||||
PublicKeyCredentialUsageParameters(
|
PublicKeyCredentialUsageParameters(
|
||||||
publicKeyCredentialRequestOptions = requestOptions,
|
publicKeyCredentialRequestOptions = requestOptions,
|
||||||
clientDataResponse = ClientDataDefinedResponse(clientDataHash),
|
clientDataResponse = ClientDataDefinedResponse(clientDataHash),
|
||||||
androidApp = appIdentifier,
|
androidOrigin = androidOrigin,
|
||||||
androidAppVerified = true
|
webOrigin = webOrigin
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onOriginCreated = { appIdentifier, origin, verified ->
|
onOriginCreated = { androidOrigin, webOrigin ->
|
||||||
|
// By default we crate an usage parameter with Android origin
|
||||||
result.invoke(
|
result.invoke(
|
||||||
PublicKeyCredentialUsageParameters(
|
PublicKeyCredentialUsageParameters(
|
||||||
publicKeyCredentialRequestOptions = requestOptions,
|
publicKeyCredentialRequestOptions = requestOptions,
|
||||||
clientDataResponse = ClientDataBuildResponse(
|
clientDataResponse = ClientDataBuildResponse(
|
||||||
type = ClientDataBuildResponse.Type.GET,
|
type = ClientDataBuildResponse.Type.GET,
|
||||||
challenge = requestOptions.challenge,
|
challenge = requestOptions.challenge,
|
||||||
origin = origin
|
origin = androidOrigin.toAndroidOrigin()
|
||||||
),
|
),
|
||||||
androidApp = appIdentifier,
|
androidOrigin = androidOrigin,
|
||||||
androidAppVerified = verified
|
webOrigin = webOrigin
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -518,30 +521,35 @@ object PasskeyHelper {
|
|||||||
* Verify that the application signature is contained in the [appOrigin]
|
* Verify that the application signature is contained in the [appOrigin]
|
||||||
* or that the webDomain contains the origin
|
* or that the webDomain contains the origin
|
||||||
*/
|
*/
|
||||||
fun getVerifiedClientDataResponse(
|
fun getVerifiedGETClientDataResponse(
|
||||||
usageParameters: PublicKeyCredentialUsageParameters,
|
usageParameters: PublicKeyCredentialUsageParameters,
|
||||||
appOrigin: AppOrigin?,
|
appOrigin: AppOrigin
|
||||||
onOriginChecked: (clientDataResponse: ClientDataResponse) -> Unit,
|
): ClientDataResponse {
|
||||||
onOriginNotChecked: () -> Unit
|
val appToCheck = usageParameters.androidOrigin
|
||||||
) {
|
val webToCheck = usageParameters.webOrigin
|
||||||
if (usageParameters.androidAppVerified) {
|
if (appToCheck.verification == Verification.AUTOMATICALLY_VERIFIED) {
|
||||||
onOriginChecked(usageParameters.clientDataResponse)
|
return usageParameters.clientDataResponse
|
||||||
} else {
|
} else {
|
||||||
usageParameters.androidApp.checkInAppOrigin(
|
if (appOrigin.containsVerifiedAndroidOrigin(appToCheck)) {
|
||||||
appOrigin = appOrigin,
|
if (webToCheck.verification.verified
|
||||||
onOriginChecked = { origin ->
|
|| appOrigin.containsVerifiedWebOrigin(webToCheck)) {
|
||||||
// Origin checked by Android app signature
|
// Origin checked by URL
|
||||||
onOriginChecked(
|
return ClientDataBuildResponse(
|
||||||
ClientDataBuildResponse(
|
type = ClientDataBuildResponse.Type.GET,
|
||||||
type = ClientDataBuildResponse.Type.GET,
|
challenge = usageParameters.publicKeyCredentialRequestOptions.challenge,
|
||||||
challenge = usageParameters.publicKeyCredentialRequestOptions.challenge,
|
origin = webToCheck.toWebOrigin()
|
||||||
origin = origin
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
onOriginNotChecked
|
// Origin checked by Android app signature
|
||||||
)
|
return ClientDataBuildResponse(
|
||||||
|
type = ClientDataBuildResponse.Type.GET,
|
||||||
|
challenge = usageParameters.publicKeyCredentialRequestOptions.challenge,
|
||||||
|
origin = appOrigin.firstVerifiedWebOrigin()?.toWebOrigin()
|
||||||
|
?: appToCheck.toAndroidOrigin()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
throw SecurityException("Wrong signature for ${appToCheck.packageName}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -24,38 +24,96 @@ import kotlinx.parcelize.Parcelize
|
|||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class AppOrigin(
|
data class AppOrigin(
|
||||||
val appIdentifiers: MutableList<AppIdentifier> = mutableListOf(),
|
val androidOrigins: MutableList<AndroidOrigin> = mutableListOf(),
|
||||||
val webDomains: MutableList<String> = mutableListOf()
|
val webOrigins: MutableList<WebOrigin> = mutableListOf()
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
fun addIdentifier(appIdentifier: AppIdentifier) {
|
fun addAndroidOrigin(androidOrigin: AndroidOrigin) {
|
||||||
appIdentifiers.add(appIdentifier)
|
androidOrigins.add(androidOrigin)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addWebDomain(webDomain: String) {
|
fun addWebOrigin(webOrigin: WebOrigin) {
|
||||||
this.webDomains.add(webDomain)
|
this.webOrigins.add(webOrigin)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAppElement(appIdentifier: AppIdentifier) {
|
fun containsVerifiedAndroidOrigin(androidOrigin: AndroidOrigin): Boolean {
|
||||||
appIdentifiers.remove(appIdentifier)
|
return androidOrigins.any {
|
||||||
|
it.packageName == androidOrigin.packageName
|
||||||
|
&& it.signature == androidOrigin.signature
|
||||||
|
&& it.verification.verified
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeWebDomain(webDomain: String) {
|
fun containsVerifiedWebOrigin(webOrigin: WebOrigin): Boolean {
|
||||||
this.webDomains.remove(webDomain)
|
return this.webOrigins.any {
|
||||||
|
it.origin == webOrigin.origin
|
||||||
|
&& it.verification.verified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun firstVerifiedWebOrigin(): WebOrigin? {
|
||||||
|
return webOrigins.first {
|
||||||
|
it.verification.verified
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
appIdentifiers.clear()
|
androidOrigins.clear()
|
||||||
webDomains.clear()
|
webOrigins.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isEmpty(): Boolean {
|
fun isEmpty(): Boolean {
|
||||||
return appIdentifiers.isEmpty() && webDomains.isEmpty()
|
return androidOrigins.isEmpty() && webOrigins.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toName(): String? {
|
||||||
|
return if (androidOrigins.isNotEmpty()) {
|
||||||
|
androidOrigins.first().packageName
|
||||||
|
} else if (webOrigins.isNotEmpty()){
|
||||||
|
webOrigins.first().origin
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Verification {
|
||||||
|
MANUALLY_VERIFIED, AUTOMATICALLY_VERIFIED, NOT_VERIFIED;
|
||||||
|
|
||||||
|
val verified: Boolean
|
||||||
|
get() = this == MANUALLY_VERIFIED || this == AUTOMATICALLY_VERIFIED
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class AndroidOrigin(
|
||||||
|
val packageName: String,
|
||||||
|
val signature: String? = null,
|
||||||
|
val verification: Verification = Verification.AUTOMATICALLY_VERIFIED,
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
fun toAndroidOrigin(): String {
|
||||||
|
return "androidapp://${packageName}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class AppIdentifier(
|
data class WebOrigin(
|
||||||
val id: String,
|
val origin: String,
|
||||||
val signature: String? = null,
|
val assetLinks: String? = null,
|
||||||
) : Parcelable
|
val verification: Verification = Verification.AUTOMATICALLY_VERIFIED,
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
fun toWebOrigin(): String {
|
||||||
|
return origin
|
||||||
|
}
|
||||||
|
|
||||||
|
fun defaultAssetLinks(): String {
|
||||||
|
return "${origin}/.well-known/assetlinks.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val RELYING_PARTY_DEFAULT_PROTOCOL = "https"
|
||||||
|
fun fromRelyingParty(relyingParty: String, verification: Verification): WebOrigin = WebOrigin(
|
||||||
|
origin ="$RELYING_PARTY_DEFAULT_PROTOCOL://$relyingParty",
|
||||||
|
verification = verification
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,8 +48,8 @@ object AppOriginEntryField {
|
|||||||
}
|
}
|
||||||
}.takeWhile { it != null }
|
}.takeWhile { it != null }
|
||||||
.forEach { pair ->
|
.forEach { pair ->
|
||||||
appOrigin.addIdentifier(
|
appOrigin.addAndroidOrigin(
|
||||||
AppIdentifier(pair!!.first, pair.second)
|
AndroidOrigin(pair!!.first, pair.second)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Get Domains
|
// Get Domains
|
||||||
@@ -58,7 +58,7 @@ object AppOriginEntryField {
|
|||||||
val domainKey = WEB_DOMAIN_FIELD_NAME + suffixFieldNamePosition(domainFieldPosition)
|
val domainKey = WEB_DOMAIN_FIELD_NAME + suffixFieldNamePosition(domainFieldPosition)
|
||||||
val domainValue = getField(domainKey)
|
val domainValue = getField(domainKey)
|
||||||
if (domainValue != null) {
|
if (domainValue != null) {
|
||||||
appOrigin.addWebDomain(domainValue)
|
appOrigin.addWebOrigin(WebOrigin(origin = domainValue))
|
||||||
domainFieldPosition++
|
domainFieldPosition++
|
||||||
} else {
|
} else {
|
||||||
break // No more domain found
|
break // No more domain found
|
||||||
@@ -137,11 +137,12 @@ object AppOriginEntryField {
|
|||||||
* Only if [customFieldsAllowed] is true
|
* Only if [customFieldsAllowed] is true
|
||||||
*/
|
*/
|
||||||
fun EntryInfo.setAppOrigin(appOrigin: AppOrigin?, customFieldsAllowed: Boolean) {
|
fun EntryInfo.setAppOrigin(appOrigin: AppOrigin?, customFieldsAllowed: Boolean) {
|
||||||
appOrigin?.appIdentifiers?.forEach { appIdentifier ->
|
appOrigin?.androidOrigins?.forEach { appIdentifier ->
|
||||||
setApplicationId(appIdentifier.id, appIdentifier.signature)
|
setApplicationId(appIdentifier.packageName, appIdentifier.signature)
|
||||||
}
|
}
|
||||||
appOrigin?.webDomains?.forEach { webDomain ->
|
appOrigin?.webOrigins?.forEach { webOrigin ->
|
||||||
setWebDomain(webDomain, null, customFieldsAllowed)
|
if (webOrigin.verification.verified)
|
||||||
|
setWebDomain(webOrigin.origin, null, customFieldsAllowed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,7 +176,16 @@ class EntryInfo : NodeInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add searchInfo to current EntryInfo, return true if new data, false if no modification
|
* Capitalize and remove suffix of a title
|
||||||
|
*/
|
||||||
|
fun String.toTitle(): String {
|
||||||
|
return this.replaceFirstChar {
|
||||||
|
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add searchInfo to current EntryInfo
|
||||||
*/
|
*/
|
||||||
fun saveSearchInfo(database: Database?, searchInfo: SearchInfo) {
|
fun saveSearchInfo(database: Database?, searchInfo: SearchInfo) {
|
||||||
searchInfo.otpString?.let { otpString ->
|
searchInfo.otpString?.let { otpString ->
|
||||||
@@ -191,20 +200,13 @@ class EntryInfo : NodeInfo {
|
|||||||
setApplicationId(applicationId)
|
setApplicationId(applicationId)
|
||||||
}
|
}
|
||||||
if (title.isEmpty()) {
|
if (title.isEmpty()) {
|
||||||
title = searchInfo.toTitle()
|
title = searchInfo.toString().toTitle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Capitalize and remove suffix of web domain to create a title
|
* Add registerInfo to current EntryInfo
|
||||||
*/
|
*/
|
||||||
fun SearchInfo.toTitle(): String {
|
|
||||||
val webDomain = this.webDomain
|
|
||||||
return webDomain?.substring(0, webDomain.lastIndexOf('.'))?.replaceFirstChar {
|
|
||||||
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
|
|
||||||
} ?: this.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveRegisterInfo(database: Database?, registerInfo: RegisterInfo) {
|
fun saveRegisterInfo(database: Database?, registerInfo: RegisterInfo) {
|
||||||
saveSearchInfo(database, registerInfo.searchInfo)
|
saveSearchInfo(database, registerInfo.searchInfo)
|
||||||
registerInfo.username?.let { username = it }
|
registerInfo.username?.let { username = it }
|
||||||
@@ -215,6 +217,9 @@ class EntryInfo : NodeInfo {
|
|||||||
registerInfo.appOrigin,
|
registerInfo.appOrigin,
|
||||||
database?.allowEntryCustomFields() == true
|
database?.allowEntryCustomFields() == true
|
||||||
)
|
)
|
||||||
|
if (title.isEmpty()) {
|
||||||
|
title = registerInfo.toString().toTitle()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getVisualTitle(): String {
|
fun getVisualTitle(): String {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.kunzisoft.keepass.model
|
package com.kunzisoft.keepass.model
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import com.kunzisoft.keepass.utils.ObjectNameResource
|
||||||
import com.kunzisoft.keepass.utils.readParcelableCompat
|
import com.kunzisoft.keepass.utils.readParcelableCompat
|
||||||
|
|
||||||
data class RegisterInfo(
|
data class RegisterInfo(
|
||||||
@@ -11,12 +13,12 @@ data class RegisterInfo(
|
|||||||
val creditCard: CreditCard? = null,
|
val creditCard: CreditCard? = null,
|
||||||
val passkey: Passkey? = null,
|
val passkey: Passkey? = null,
|
||||||
val appOrigin: AppOrigin? = null
|
val appOrigin: AppOrigin? = null
|
||||||
): Parcelable {
|
) : ObjectNameResource, Parcelable {
|
||||||
|
|
||||||
constructor(parcel: Parcel) : this(
|
constructor(parcel: Parcel) : this(
|
||||||
searchInfo = parcel.readParcelableCompat() ?: SearchInfo(),
|
searchInfo = parcel.readParcelableCompat() ?: SearchInfo(),
|
||||||
username = parcel.readString() ?: "",
|
username = parcel.readString(),
|
||||||
password = parcel.readString() ?: "",
|
password = parcel.readString(),
|
||||||
creditCard = parcel.readParcelableCompat(),
|
creditCard = parcel.readParcelableCompat(),
|
||||||
passkey = parcel.readParcelableCompat(),
|
passkey = parcel.readParcelableCompat(),
|
||||||
appOrigin = parcel.readParcelableCompat()
|
appOrigin = parcel.readParcelableCompat()
|
||||||
@@ -35,6 +37,20 @@ data class RegisterInfo(
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getName(resources: Resources): String {
|
||||||
|
return username
|
||||||
|
?: passkey?.relyingParty
|
||||||
|
?: appOrigin?.toName()
|
||||||
|
?: searchInfo.getName(resources)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return username
|
||||||
|
?: passkey?.relyingParty
|
||||||
|
?: appOrigin?.toName()
|
||||||
|
?: searchInfo.toString()
|
||||||
|
}
|
||||||
|
|
||||||
companion object CREATOR : Parcelable.Creator<RegisterInfo> {
|
companion object CREATOR : Parcelable.Creator<RegisterInfo> {
|
||||||
override fun createFromParcel(parcel: Parcel): RegisterInfo {
|
override fun createFromParcel(parcel: Parcel): RegisterInfo {
|
||||||
return RegisterInfo(parcel)
|
return RegisterInfo(parcel)
|
||||||
|
|||||||
Reference in New Issue
Block a user