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) } 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)) } } 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() {