From e73985cb88864446751d2bb49a42620817bf49c3 Mon Sep 17 00:00:00 2001 From: Louis CAD Date: Tue, 28 Apr 2026 11:06:13 +0200 Subject: [PATCH 1/3] fix: Add missing AttemptingToConnect state emission --- .../src/commonMain/kotlin/internal/AuthenticatorFacadeImpl.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/multiplatform-lib/src/commonMain/kotlin/internal/AuthenticatorFacadeImpl.kt b/multiplatform-lib/src/commonMain/kotlin/internal/AuthenticatorFacadeImpl.kt index 57575101..df1341b1 100644 --- a/multiplatform-lib/src/commonMain/kotlin/internal/AuthenticatorFacadeImpl.kt +++ b/multiplatform-lib/src/commonMain/kotlin/internal/AuthenticatorFacadeImpl.kt @@ -365,6 +365,7 @@ internal class AuthenticatorFacadeImpl( private suspend fun FlowCollector.restoreFromBackupAttempts(account: AccountEntity) { withRetries(userId = account.id) { + emit(Account.Status.NotConnected.AttemptingToConnect) migrationManager.restore(account = account) { userId, token -> authenticatorBridge.persistTokenForAccount(userId, token) } From 3e0193df60da104fdbb5cc6f17662a40eb3e13bc Mon Sep 17 00:00:00 2001 From: Louis CAD Date: Tue, 28 Apr 2026 11:08:30 +0200 Subject: [PATCH 2/3] chore: Add pre-made key filters in KeyPairManager --- .../src/commonMain/kotlin/internal/KeyManager.kt | 5 +++++ .../internal/managers/AuthenticatorManager.kt | 14 ++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/multiplatform-lib/src/commonMain/kotlin/internal/KeyManager.kt b/multiplatform-lib/src/commonMain/kotlin/internal/KeyManager.kt index 9c5f95cb..a4c5449e 100644 --- a/multiplatform-lib/src/commonMain/kotlin/internal/KeyManager.kt +++ b/multiplatform-lib/src/commonMain/kotlin/internal/KeyManager.kt @@ -39,4 +39,9 @@ internal interface KeyPairManager { val privateKeyPurposes = KeyPurposes.privateKeyDefaults val publicKeyPurposes = KeyPurposes.publicKeyDefaults } + + object Filters { + fun forUserId(userId: Long): (name: String) -> Boolean = { it.startsWith("$userId-") } + fun forPasskeyId(passkeyId: String): (name: String) -> Boolean = { "-$passkeyId-" in it } + } } diff --git a/multiplatform-lib/src/commonMain/kotlin/internal/managers/AuthenticatorManager.kt b/multiplatform-lib/src/commonMain/kotlin/internal/managers/AuthenticatorManager.kt index aef5e36c..58f4da9e 100644 --- a/multiplatform-lib/src/commonMain/kotlin/internal/managers/AuthenticatorManager.kt +++ b/multiplatform-lib/src/commonMain/kotlin/internal/managers/AuthenticatorManager.kt @@ -19,6 +19,7 @@ package com.infomaniak.auth.lib.internal.managers import com.infomaniak.auth.lib.internal.CryptoObjectsBuilder import com.infomaniak.auth.lib.internal.Failure +import com.infomaniak.auth.lib.internal.KeyPairManager import com.infomaniak.auth.lib.internal.KeyPairManagerImpl import com.infomaniak.auth.lib.internal.extensions.firstOrElse import com.infomaniak.auth.lib.internal.models.ClientExtensionResults @@ -32,6 +33,7 @@ import com.infomaniak.auth.lib.models.migration.ApiToken import io.ktor.utils.io.core.toByteArray import kotlinx.serialization.json.Json import okio.ByteString.Companion.toByteString +import com.infomaniak.auth.lib.internal.KeyPairManager.Filters as KeyFilters internal class AuthenticatorManager( private val webAuthnRepository: WebAuthnRepository, @@ -39,7 +41,7 @@ internal class AuthenticatorManager( ) { private val cryptoObjectsBuilder by lazy { CryptoObjectsBuilder() } - private val keyPairManager by lazy { KeyPairManagerImpl() } + val keyPairManager: KeyPairManager by lazy { KeyPairManagerImpl() } private val base64NoPadding get() = cryptoObjectsBuilder.base64UrlSafeNoPadding @@ -74,7 +76,7 @@ internal class AuthenticatorManager( userId: Long, keyIdOrDefault: String? = null, ): Xor { - val keyId = keyIdOrDefault ?: keyPairManager.findKeyIdFor { it.startsWith("$userId-") } + val keyId = keyIdOrDefault ?: keyPairManager.findKeyIdFor(KeyFilters.forUserId(userId)) ?: return Xor.Second(Failure.KeyManagement.KeyNotFound("No key found for user $userId")) val authenticationOptions = webAuthnRepository.challenge(clientId) @@ -124,22 +126,22 @@ internal class AuthenticatorManager( } suspend fun removeAccount(token: String, userId: Long) { - val passkeyId = keyPairManager.findKeyIdFor { it.startsWith("$userId-") } + val passkeyId = keyPairManager.findKeyIdFor(KeyFilters.forUserId(userId)) if (passkeyId != null) { // If we have a passkey for this account, revoke it against the backend and delete it webAuthnRepository.deletePasskey(token, passkeyId) - val _ = keyPairManager.deleteKeysMatching { "-$passkeyId-" in it } + val _ = keyPairManager.deleteKeysMatching(KeyFilters.forPasskeyId(passkeyId)) } accountsRepository.deleteAccount(userId) } suspend fun deleteKeysFor(userId: Long) { - val _ = keyPairManager.deleteKeysMatching { it.startsWith("$userId-") } + val _ = keyPairManager.deleteKeysMatching(KeyFilters.forUserId(userId)) } suspend fun getKeyIdFor(userId: Long): String? { - return keyPairManager.findKeyIdFor { it.startsWith("$userId-") } + return keyPairManager.findKeyIdFor(KeyFilters.forUserId(userId)) } } From e3cce74545d859114c14f029588300da268ab4de Mon Sep 17 00:00:00 2001 From: Louis CAD Date: Tue, 28 Apr 2026 11:08:59 +0200 Subject: [PATCH 3/3] fix: Stop removing all passkeys after restoration from backup --- .../kotlin/internal/managers/MigrationManager.kt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/multiplatform-lib/src/commonMain/kotlin/internal/managers/MigrationManager.kt b/multiplatform-lib/src/commonMain/kotlin/internal/managers/MigrationManager.kt index 9a030b4e..7d2641d4 100644 --- a/multiplatform-lib/src/commonMain/kotlin/internal/managers/MigrationManager.kt +++ b/multiplatform-lib/src/commonMain/kotlin/internal/managers/MigrationManager.kt @@ -41,6 +41,7 @@ import kotlinx.io.IOException import org.kotlincrypto.macs.hmac.sha2.HmacSHA256 import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid +import com.infomaniak.auth.lib.internal.KeyPairManager.Filters as keyFilters internal class MigrationManager( private val accountsDatabase: AccountsDatabase, @@ -61,15 +62,15 @@ internal class MigrationManager( } suspend fun restore(account: AccountEntity, persistToken: suspend (userId: Long, token: String) -> Unit) { - val keyId = authenticatorManager.getKeyIdFor(account.id) ?: return + val oldKeyId = authenticatorManager.getKeyIdFor(account.id) ?: return //TODO: Handle multiple passkeys present. // Get token with previous passkey - val token = authenticatorManager.getToken( + val tokenFromOldPasskey = authenticatorManager.getToken( clientId = clientId, userId = account.id, - keyIdOrDefault = keyId, + keyIdOrDefault = oldKeyId, ).firstOrElse { error(it) } // Register a new passkey - val newKeyId = authenticatorManager.registerPasskey(token.accessToken, account.id) + val newKeyId = authenticatorManager.registerPasskey(tokenFromOldPasskey.accessToken, account.id) // Getting a new token with the new passkey val tokenWithNewPassKey = authenticatorManager.getToken( clientId = clientId, @@ -77,10 +78,10 @@ internal class MigrationManager( keyIdOrDefault = newKeyId, ).firstOrElse { error(it) } persistToken(account.id, tokenWithNewPassKey.accessToken) - dao.upsert(account.copy(status = AccountEntity.Status.LoggedIn)) // We can safely delete the old passkey, as the new one is working and the old token won't be valid anymore - authenticatorManager.deleteKeysFor(account.id) - webAuthnRepository.deletePasskey(tokenWithNewPassKey.accessToken, keyId) + webAuthnRepository.deletePasskey(tokenWithNewPassKey.accessToken, oldKeyId) + val _ = authenticatorManager.keyPairManager.deleteKeysMatching(keyFilters.forPasskeyId(oldKeyId)) + dao.upsert(account.copy(status = AccountEntity.Status.LoggedIn)) } suspend fun addLegacyAccountsToDB() {