mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
feat: Add User Verification #2283
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
KeePassDX(4.3.0)
|
KeePassDX(4.3.0)
|
||||||
* Manual change of app language #1884 #1990
|
* Manual change of app language #1884 #1990
|
||||||
|
* Add Passkey User Verification #2283
|
||||||
* Fix autofill username detection #2276
|
* Fix autofill username detection #2276
|
||||||
* Fix Passkey in passwordless mode #2282
|
* Fix Passkey in passwordless mode #2282
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.biometric.BiometricPrompt
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.kunzisoft.keepass.R
|
import com.kunzisoft.keepass.R
|
||||||
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
import com.kunzisoft.keepass.activities.FileDatabaseSelectActivity
|
||||||
@@ -44,8 +46,13 @@ import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.setActivity
|
|||||||
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.credentialprovider.passkey.data.AndroidPrivilegedApp
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.AndroidPrivilegedApp
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.ALLOWED_AUTHENTICATORS
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAppOrigin
|
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.addUserVerificationRequired
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.isAuthenticatorsAllowed
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.isUserVerificationRequired
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removeUserVerificationRequired
|
||||||
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.CredentialLauncherViewModel
|
||||||
import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel
|
import com.kunzisoft.keepass.credentialprovider.viewmodel.PasskeyLauncherViewModel
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
@@ -54,6 +61,7 @@ import com.kunzisoft.keepass.model.SearchInfo
|
|||||||
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService.Companion.ACTION_DATABASE_UPDATE_ENTRY_TASK
|
||||||
import com.kunzisoft.keepass.tasks.ActionRunnable
|
import com.kunzisoft.keepass.tasks.ActionRunnable
|
||||||
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
import com.kunzisoft.keepass.utils.AppUtil.randomRequestCode
|
||||||
|
import com.kunzisoft.keepass.utils.LOCK_ACTION
|
||||||
import com.kunzisoft.keepass.view.toastError
|
import com.kunzisoft.keepass.view.toastError
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -82,7 +90,60 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
// To manage https://github.com/Kunzisoft/KeePassDX/issues/2283
|
||||||
|
if (intent.isUserVerificationRequired()) {
|
||||||
|
if (isAuthenticatorsAllowed().not()) {
|
||||||
|
intent.removeUserVerificationRequired()
|
||||||
|
sendBroadcast(Intent(LOCK_ACTION))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// super.onCreate must be after UserVerification to allow database lock
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
// Biometric must be after super.onCreate
|
||||||
|
if (intent.isUserVerificationRequired()) {
|
||||||
|
if (isAuthenticatorsAllowed()) {
|
||||||
|
BiometricPrompt(
|
||||||
|
this, ContextCompat.getMainExecutor(this),
|
||||||
|
object : BiometricPrompt.AuthenticationCallback() {
|
||||||
|
override fun onAuthenticationError(
|
||||||
|
errorCode: Int,
|
||||||
|
errString: CharSequence
|
||||||
|
) {
|
||||||
|
super.onAuthenticationError(errorCode, errString)
|
||||||
|
when (errorCode) {
|
||||||
|
BiometricPrompt.ERROR_CANCELED,
|
||||||
|
BiometricPrompt.ERROR_NEGATIVE_BUTTON,
|
||||||
|
BiometricPrompt.ERROR_USER_CANCELED -> {
|
||||||
|
// No operation
|
||||||
|
Log.i(TAG, "$errString")
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
toastError(SecurityException("Authentication error: $errString"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
passkeyLauncherViewModel.cancelResult()
|
||||||
|
}
|
||||||
|
override fun onAuthenticationSucceeded(
|
||||||
|
result: BiometricPrompt.AuthenticationResult
|
||||||
|
) {
|
||||||
|
super.onAuthenticationSucceeded(result)
|
||||||
|
passkeyLauncherViewModel.launchAction(intent, mSpecialMode)
|
||||||
|
}
|
||||||
|
override fun onAuthenticationFailed() {
|
||||||
|
super.onAuthenticationFailed()
|
||||||
|
toastError(SecurityException(getString(R.string.device_unlock_not_recognized)))
|
||||||
|
passkeyLauncherViewModel.cancelResult()
|
||||||
|
}
|
||||||
|
}).authenticate(
|
||||||
|
BiometricPrompt.PromptInfo.Builder()
|
||||||
|
.setTitle(getString(R.string.user_verification_required))
|
||||||
|
.setAllowedAuthenticators(ALLOWED_AUTHENTICATORS)
|
||||||
|
.setConfirmationRequired(false)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
// Initialize the parameters
|
// Initialize the parameters
|
||||||
passkeyLauncherViewModel.initialize()
|
passkeyLauncherViewModel.initialize()
|
||||||
@@ -278,7 +339,8 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
|||||||
specialMode: SpecialMode,
|
specialMode: SpecialMode,
|
||||||
searchInfo: SearchInfo? = null,
|
searchInfo: SearchInfo? = null,
|
||||||
appOrigin: AppOrigin? = null,
|
appOrigin: AppOrigin? = null,
|
||||||
nodeId: UUID? = null
|
nodeId: UUID? = null,
|
||||||
|
userVerificationRequired: Boolean = false
|
||||||
): PendingIntent? {
|
): PendingIntent? {
|
||||||
return PendingIntent.getActivity(
|
return PendingIntent.getActivity(
|
||||||
context,
|
context,
|
||||||
@@ -290,6 +352,7 @@ class PasskeyLauncherActivity : DatabaseLockActivity() {
|
|||||||
addAppOrigin(appOrigin)
|
addAppOrigin(appOrigin)
|
||||||
addNodeId(nodeId)
|
addNodeId(nodeId)
|
||||||
addAuthCode(nodeId)
|
addAuthCode(nodeId)
|
||||||
|
addUserVerificationRequired(userVerificationRequired)
|
||||||
},
|
},
|
||||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
|||||||
import com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity
|
import com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.UserVerificationRequirement
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.isAuthenticatorsAllowed
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
|
import com.kunzisoft.keepass.database.exception.RegisterInReadOnlyDatabaseException
|
||||||
@@ -65,6 +67,7 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
private var mDatabaseTaskProvider: DatabaseTaskProvider? = null
|
||||||
private var mDatabase: ContextualDatabase? = null
|
private var mDatabase: ContextualDatabase? = null
|
||||||
private lateinit var defaultIcon: Icon
|
private lateinit var defaultIcon: Icon
|
||||||
|
private lateinit var relaunchIcon: Icon
|
||||||
private var isAutoSelectAllowed: Boolean = false
|
private var isAutoSelectAllowed: Boolean = false
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@@ -83,6 +86,11 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
setTintBlendMode(BlendMode.DST)
|
setTintBlendMode(BlendMode.DST)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
relaunchIcon = Icon.createWithResource(
|
||||||
|
this@PasskeyProviderService,
|
||||||
|
R.drawable.ic_clock_loader_red_24dp
|
||||||
|
)
|
||||||
|
|
||||||
isAutoSelectAllowed = isPasskeyAutoSelectEnable(this)
|
isAutoSelectAllowed = isPasskeyAutoSelectEnable(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,69 +157,91 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
val credentialIdList = publicKeyCredentialRequestOptions.allowCredentials
|
val credentialIdList = publicKeyCredentialRequestOptions.allowCredentials
|
||||||
.map { b64Encode(it.id) }
|
.map { b64Encode(it.id) }
|
||||||
val searchInfo = buildPasskeySearchInfo(relyingPartyId, credentialIdList)
|
val searchInfo = buildPasskeySearchInfo(relyingPartyId, credentialIdList)
|
||||||
Log.d(TAG, "Build passkey search for relying party $relyingPartyId, credentialIds $credentialIdList")
|
val userVerificationRequired = publicKeyCredentialRequestOptions
|
||||||
|
.userVerification == UserVerificationRequirement.REQUIRED
|
||||||
|
Log.d(TAG, "Build passkey search for UV $userVerificationRequired, " +
|
||||||
|
"RP $relyingPartyId and Credential IDs $credentialIdList")
|
||||||
SearchHelper.checkAutoSearchInfo(
|
SearchHelper.checkAutoSearchInfo(
|
||||||
context = this,
|
context = this,
|
||||||
database = mDatabase,
|
database = mDatabase,
|
||||||
searchInfo = searchInfo,
|
searchInfo = searchInfo,
|
||||||
onItemsFound = { database, items ->
|
onItemsFound = { database, items ->
|
||||||
Log.d(TAG, "Add pending intent for passkey selection with found items")
|
manageUserVerification(
|
||||||
for (passkeyEntry in items) {
|
passkeyEntries = passkeyEntries,
|
||||||
PasskeyLauncherActivity.getPendingIntent(
|
searchInfo = searchInfo,
|
||||||
context = applicationContext,
|
option = option,
|
||||||
specialMode = SpecialMode.SELECTION,
|
userVerificationRequired = userVerificationRequired
|
||||||
nodeId = passkeyEntry.id,
|
) {
|
||||||
appOrigin = passkeyEntry.appOrigin
|
Log.d(TAG, "Add pending intent for passkey selection with found items")
|
||||||
)?.let { usagePendingIntent ->
|
for (passkeyEntry in items) {
|
||||||
val passkey = passkeyEntry.passkey
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
passkeyEntries.add(
|
context = applicationContext,
|
||||||
PublicKeyCredentialEntry(
|
specialMode = SpecialMode.SELECTION,
|
||||||
context = applicationContext,
|
nodeId = passkeyEntry.id,
|
||||||
username = passkey?.username ?: "Unknown",
|
appOrigin = passkeyEntry.appOrigin,
|
||||||
icon = passkeyEntry.buildIcon(this@PasskeyProviderService, database)?.apply {
|
userVerificationRequired = userVerificationRequired
|
||||||
setTintBlendMode(BlendMode.DST)
|
)?.let { usagePendingIntent ->
|
||||||
} ?: defaultIcon,
|
val passkey = passkeyEntry.passkey
|
||||||
pendingIntent = usagePendingIntent,
|
passkeyEntries.add(
|
||||||
beginGetPublicKeyCredentialOption = option,
|
PublicKeyCredentialEntry(
|
||||||
displayName = passkeyEntry.getVisualTitle(),
|
context = applicationContext,
|
||||||
isAutoSelectAllowed = isAutoSelectAllowed
|
username = passkey?.username ?: "Unknown",
|
||||||
|
icon = passkeyEntry.buildIcon(
|
||||||
|
this@PasskeyProviderService,
|
||||||
|
database
|
||||||
|
)?.apply {
|
||||||
|
setTintBlendMode(BlendMode.DST)
|
||||||
|
} ?: defaultIcon,
|
||||||
|
pendingIntent = usagePendingIntent,
|
||||||
|
beginGetPublicKeyCredentialOption = option,
|
||||||
|
displayName = passkeyEntry.getVisualTitle(),
|
||||||
|
isAutoSelectAllowed = isAutoSelectAllowed
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
callback(passkeyEntries)
|
callback(passkeyEntries)
|
||||||
},
|
},
|
||||||
onItemNotFound = { _ ->
|
onItemNotFound = { _ ->
|
||||||
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId")
|
manageUserVerification(
|
||||||
if (credentialIdList.isEmpty()) {
|
passkeyEntries = passkeyEntries,
|
||||||
Log.d(TAG, "Add pending intent for passkey selection in opened database")
|
searchInfo = searchInfo,
|
||||||
PasskeyLauncherActivity.getPendingIntent(
|
option = option,
|
||||||
context = applicationContext,
|
userVerificationRequired = userVerificationRequired
|
||||||
specialMode = SpecialMode.SELECTION,
|
) {
|
||||||
searchInfo = searchInfo
|
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId")
|
||||||
)?.let { pendingIntent ->
|
if (credentialIdList.isEmpty()) {
|
||||||
passkeyEntries.add(
|
Log.d(TAG, "Add pending intent for passkey selection in opened database")
|
||||||
PublicKeyCredentialEntry(
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
context = applicationContext,
|
context = applicationContext,
|
||||||
username = getString(R.string.passkey_database_username),
|
specialMode = SpecialMode.SELECTION,
|
||||||
displayName = getString(R.string.passkey_selection_description),
|
searchInfo = searchInfo,
|
||||||
icon = defaultIcon,
|
userVerificationRequired = userVerificationRequired
|
||||||
pendingIntent = pendingIntent,
|
)?.let { pendingIntent ->
|
||||||
beginGetPublicKeyCredentialOption = option,
|
passkeyEntries.add(
|
||||||
lastUsedTime = Instant.now(),
|
PublicKeyCredentialEntry(
|
||||||
isAutoSelectAllowed = isAutoSelectAllowed
|
context = applicationContext,
|
||||||
|
username = getString(R.string.passkey_database_username),
|
||||||
|
displayName = getString(R.string.passkey_selection_description),
|
||||||
|
icon = defaultIcon,
|
||||||
|
pendingIntent = pendingIntent,
|
||||||
|
beginGetPublicKeyCredentialOption = option,
|
||||||
|
lastUsedTime = Instant.now(),
|
||||||
|
isAutoSelectAllowed = isAutoSelectAllowed
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
callback(passkeyEntries)
|
||||||
|
} else {
|
||||||
|
throw IOException(
|
||||||
|
getString(
|
||||||
|
R.string.error_passkey_credential_id,
|
||||||
|
relyingPartyId,
|
||||||
|
credentialIdList
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
callback(passkeyEntries)
|
|
||||||
} else {
|
|
||||||
throw IOException(
|
|
||||||
getString(
|
|
||||||
R.string.error_passkey_credential_id,
|
|
||||||
relyingPartyId,
|
|
||||||
credentialIdList
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDatabaseClosed = {
|
onDatabaseClosed = {
|
||||||
@@ -240,6 +270,41 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To easily manage user verification condition
|
||||||
|
*/
|
||||||
|
private fun manageUserVerification(
|
||||||
|
passkeyEntries: MutableList<CredentialEntry>,
|
||||||
|
searchInfo: SearchInfo,
|
||||||
|
option: BeginGetPublicKeyCredentialOption,
|
||||||
|
userVerificationRequired: Boolean,
|
||||||
|
standardAction: () -> Unit
|
||||||
|
) {
|
||||||
|
if (userVerificationRequired && isAuthenticatorsAllowed().not()) {
|
||||||
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
|
context = applicationContext,
|
||||||
|
specialMode = SpecialMode.SELECTION,
|
||||||
|
searchInfo = searchInfo,
|
||||||
|
userVerificationRequired = true
|
||||||
|
)?.let { pendingIntent ->
|
||||||
|
passkeyEntries.add(
|
||||||
|
PublicKeyCredentialEntry(
|
||||||
|
context = applicationContext,
|
||||||
|
username = getString(R.string.passkey_database_username),
|
||||||
|
displayName = getString(R.string.passkey_relaunch_database_description),
|
||||||
|
icon = relaunchIcon,
|
||||||
|
pendingIntent = pendingIntent,
|
||||||
|
beginGetPublicKeyCredentialOption = option,
|
||||||
|
lastUsedTime = Instant.now(),
|
||||||
|
isAutoSelectAllowed = isAutoSelectAllowed
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
standardAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBeginCreateCredentialRequest(
|
override fun onBeginCreateCredentialRequest(
|
||||||
request: BeginCreateCredentialRequest,
|
request: BeginCreateCredentialRequest,
|
||||||
cancellationSignal: CancellationSignal,
|
cancellationSignal: CancellationSignal,
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ import android.security.keystore.KeyProperties
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.biometric.BiometricManager
|
||||||
|
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
|
||||||
|
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||||
|
import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS
|
||||||
import androidx.credentials.CreatePublicKeyCredentialRequest
|
import androidx.credentials.CreatePublicKeyCredentialRequest
|
||||||
import androidx.credentials.CreatePublicKeyCredentialResponse
|
import androidx.credentials.CreatePublicKeyCredentialResponse
|
||||||
import androidx.credentials.GetPublicKeyCredentialOption
|
import androidx.credentials.GetPublicKeyCredentialOption
|
||||||
@@ -91,6 +95,7 @@ object PasskeyHelper {
|
|||||||
private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin"
|
private const val EXTRA_APP_ORIGIN = "com.kunzisoft.keepass.extra.appOrigin"
|
||||||
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"
|
||||||
|
private const val EXTRA_UV_REQUIRED = "com.kunzisoft.keepass.extra.userVerification"
|
||||||
|
|
||||||
private const val SEPARATOR = "_"
|
private const val SEPARATOR = "_"
|
||||||
|
|
||||||
@@ -107,6 +112,40 @@ object PasskeyHelper {
|
|||||||
|
|
||||||
private val internalSecureRandom: SecureRandom = SecureRandom()
|
private val internalSecureRandom: SecureRandom = SecureRandom()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the user verification to the intent
|
||||||
|
*/
|
||||||
|
fun Intent.addUserVerificationRequired(userVerification: Boolean) {
|
||||||
|
putExtra(EXTRA_UV_REQUIRED, userVerification)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the user verification is required
|
||||||
|
*/
|
||||||
|
fun Intent.isUserVerificationRequired(): Boolean {
|
||||||
|
return getBooleanExtra(EXTRA_UV_REQUIRED, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the user verification from the intent
|
||||||
|
*/
|
||||||
|
fun Intent.removeUserVerificationRequired() {
|
||||||
|
removeExtra(EXTRA_UV_REQUIRED)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allowed authenticators for the User Verification
|
||||||
|
*/
|
||||||
|
const val ALLOWED_AUTHENTICATORS = BIOMETRIC_WEAK or DEVICE_CREDENTIAL
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the device supports the biometric prompt for User Verification
|
||||||
|
*/
|
||||||
|
fun Context.isAuthenticatorsAllowed(): Boolean {
|
||||||
|
return BiometricManager.from(this)
|
||||||
|
.canAuthenticate(ALLOWED_AUTHENTICATORS) == BIOMETRIC_SUCCESS
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an authentication code generated by an entry to the intent
|
* Add an authentication code generated by an entry to the intent
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ abstract class CredentialLauncherViewModel(application: Application): AndroidVie
|
|||||||
|
|
||||||
protected var isResultLauncherRegistered: Boolean = false
|
protected var isResultLauncherRegistered: Boolean = false
|
||||||
private var mSelectionResult: ActivityResult? = null
|
private var mSelectionResult: ActivityResult? = null
|
||||||
|
|
||||||
protected val mCredentialUiState = MutableStateFlow<CredentialState>(CredentialState.Loading)
|
protected val mCredentialUiState = MutableStateFlow<CredentialState>(CredentialState.Loading)
|
||||||
val credentialUiState: StateFlow<CredentialState> = mCredentialUiState
|
val credentialUiState: StateFlow<CredentialState> = mCredentialUiState
|
||||||
|
|
||||||
@@ -56,7 +55,7 @@ abstract class CredentialLauncherViewModel(application: Application): AndroidVie
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onDatabaseRetrieved(database: ContextualDatabase) {
|
fun onDatabaseRetrieved(database: ContextualDatabase) {
|
||||||
mDatabase = database
|
mDatabase = database
|
||||||
mSelectionResult?.let { selectionResult ->
|
mSelectionResult?.let { selectionResult ->
|
||||||
manageSelectionResult(database, selectionResult)
|
manageSelectionResult(database, selectionResult)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.build
|
|||||||
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.getVerifiedGETClientDataResponse
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.getVerifiedGETClientDataResponse
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.isUserVerificationRequired
|
||||||
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
|
||||||
@@ -149,14 +150,27 @@ class PasskeyLauncherViewModel(application: Application): CredentialLauncherView
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun launchAction(
|
||||||
|
intent: Intent,
|
||||||
|
specialMode: SpecialMode,
|
||||||
|
) {
|
||||||
|
super.launchActionIfNeeded(intent, specialMode, mDatabase)
|
||||||
|
}
|
||||||
|
|
||||||
override fun launchActionIfNeeded(
|
override fun launchActionIfNeeded(
|
||||||
intent: Intent,
|
intent: Intent,
|
||||||
specialMode: SpecialMode,
|
specialMode: SpecialMode,
|
||||||
database: ContextualDatabase?
|
database: ContextualDatabase?
|
||||||
) {
|
) {
|
||||||
// Launch with database when a nodeId is present
|
if (intent.isUserVerificationRequired()) {
|
||||||
if ((database != null && database.loaded) || intent.retrieveNodeId() == null) {
|
if (database != null) {
|
||||||
super.launchActionIfNeeded(intent, specialMode, database)
|
onDatabaseRetrieved(database)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Launch with database when a nodeId is present
|
||||||
|
if ((database != null && database.loaded) || intent.retrieveNodeId() == null) {
|
||||||
|
super.launchActionIfNeeded(intent, specialMode, database)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
9
app/src/main/res/drawable/ic_clock_loader_red_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_clock_loader_red_24dp.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:width="24dp"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:viewportWidth="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="#E50808"
|
||||||
|
android:pathData="M480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM253,707L480,480L480,160Q346,160 253,253Q160,346 160,480Q160,544 184,603Q208,662 253,707Z"/>
|
||||||
|
</vector>
|
||||||
@@ -767,6 +767,7 @@
|
|||||||
<string name="passkey_selection_description">Select an existing passkey</string>
|
<string name="passkey_selection_description">Select an existing passkey</string>
|
||||||
<string name="passkey_database_username">KeePassDX Database</string>
|
<string name="passkey_database_username">KeePassDX Database</string>
|
||||||
<string name="passkey_locked_database_description">Select to unlock</string>
|
<string name="passkey_locked_database_description">Select to unlock</string>
|
||||||
|
<string name="passkey_relaunch_database_description">Reauthenticate (No Device Auth)</string>
|
||||||
<string name="passkey_username">Passkey Username</string>
|
<string name="passkey_username">Passkey Username</string>
|
||||||
<string name="passkey_private_key">Passkey Private Key</string>
|
<string name="passkey_private_key">Passkey Private Key</string>
|
||||||
<string name="passkey_credential_id">Passkey Credential Id</string>
|
<string name="passkey_credential_id">Passkey Credential Id</string>
|
||||||
@@ -776,4 +777,5 @@
|
|||||||
<string name="passkey_backup_state">Passkey Backup State</string>
|
<string name="passkey_backup_state">Passkey Backup State</string>
|
||||||
<string name="error_passkey_result">Unable to return the passkey</string>
|
<string name="error_passkey_result">Unable to return the passkey</string>
|
||||||
<string name="error_passkey_credential_id">No passkey found with relying party %1$s and credentialIds %2$s</string>
|
<string name="error_passkey_credential_id">No passkey found with relying party %1$s and credentialIds %2$s</string>
|
||||||
|
<string name="user_verification_required">User verification required</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
* Manual change of app language #1884 #1990
|
* Manual change of app language #1884 #1990
|
||||||
|
* Add Passkey User Verification #2283
|
||||||
* Fix autofill username detection #2276
|
* Fix autofill username detection #2276
|
||||||
* Fix Passkey in passwordless mode #2282
|
* Fix Passkey in passwordless mode #2282
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
* Changement manuel de la langue de l'appli #1884 #1990
|
* Changement manuel de la langue de l'appli #1884 #1990
|
||||||
|
* Ajout de la Verification Utilisateur pour Passkey #2283
|
||||||
* Correction de la détection du nom d'utilisateur pour le remplissage auto #2276
|
* Correction de la détection du nom d'utilisateur pour le remplissage auto #2276
|
||||||
* Correction de Passkey en mode passwordless #2282
|
* Correction de Passkey en mode passwordless #2282
|
||||||
Reference in New Issue
Block a user