diff --git a/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/ProteusClientCoreCryptoImpl.kt b/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/ProteusClientCoreCryptoImpl.kt index 9fbcd75953e..ff9e90bb817 100644 --- a/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/ProteusClientCoreCryptoImpl.kt +++ b/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/ProteusClientCoreCryptoImpl.kt @@ -22,6 +22,7 @@ import com.wire.crypto.CoreCrypto import com.wire.crypto.CoreCryptoException import com.wire.crypto.client.toByteArray import com.wire.kalium.cryptography.exceptions.ProteusException +import com.wire.kalium.cryptography.exceptions.ProteusStorageMigrationException import io.ktor.util.decodeBase64Bytes import io.ktor.util.encodeBase64 import java.io.File @@ -46,6 +47,7 @@ class ProteusClientCoreCryptoImpl private constructor( override suspend fun remoteFingerPrint(sessionId: CryptoSessionId): ByteArray = wrapException { coreCrypto.proteusFingerprintRemote(sessionId.value).toByteArray() } + override suspend fun getFingerprintFromPreKey(preKey: PreKeyCrypto): ByteArray = wrapException { coreCrypto.proteusFingerprintPrekeybundle(preKey.encodedData.decodeBase64Bytes()).toByteArray() } @@ -157,31 +159,43 @@ class ProteusClientCoreCryptoImpl private constructor( acc && File(rootDir).resolve(file).deleteRecursively() } - private suspend fun migrateFromCryptoBoxIfNecessary(coreCrypto: CoreCrypto, rootDir: String) { - if (cryptoBoxFilesExists(File(rootDir))) { - kaliumLogger.i("migrating from crypto box at: $rootDir") - coreCrypto.proteusCryptoboxMigrate(rootDir) - kaliumLogger.i("migration successful") - - if (deleteCryptoBoxFiles(rootDir)) { - kaliumLogger.i("successfully deleted old crypto box files") - } else { - kaliumLogger.e("Failed to deleted old crypto box files at $rootDir") - } - } - } - - @Suppress("TooGenericExceptionCaught") + @Suppress("TooGenericExceptionCaught", "ThrowsCount") suspend operator fun invoke(coreCrypto: CoreCrypto, rootDir: String): ProteusClientCoreCryptoImpl { try { migrateFromCryptoBoxIfNecessary(coreCrypto, rootDir) coreCrypto.proteusInit() return ProteusClientCoreCryptoImpl(coreCrypto) + } catch (exception: ProteusStorageMigrationException) { + throw exception } catch (e: CoreCryptoException) { - throw ProteusException(e.message, ProteusException.fromProteusCode(coreCrypto.proteusLastErrorCode().toInt()), e.cause) + throw ProteusException( + message = e.message, + code = ProteusException.fromProteusCode(coreCrypto.proteusLastErrorCode().toInt()), + cause = e.cause + ) } catch (e: Exception) { throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, e.cause) } } + + @Suppress("TooGenericExceptionCaught") + private suspend fun migrateFromCryptoBoxIfNecessary(coreCrypto: CoreCrypto, rootDir: String) { + try { + if (cryptoBoxFilesExists(File(rootDir))) { + kaliumLogger.i("migrating from crypto box at: $rootDir") + coreCrypto.proteusCryptoboxMigrate(rootDir) + kaliumLogger.i("migration successful") + + if (deleteCryptoBoxFiles(rootDir)) { + kaliumLogger.i("successfully deleted old crypto box files") + } else { + kaliumLogger.e("Failed to deleted old crypto box files at $rootDir") + } + } + } catch (exception: Exception) { + kaliumLogger.e("Failed to migrate from crypto box to core crypto, exception: $exception") + throw ProteusStorageMigrationException("Failed to migrate from crypto box at $rootDir", exception) + } + } } } diff --git a/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/exceptions/ProteusException.kt b/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/exceptions/ProteusException.kt index ca85a709fd5..c3983e8f19e 100644 --- a/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/exceptions/ProteusException.kt +++ b/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/exceptions/ProteusException.kt @@ -18,7 +18,7 @@ package com.wire.kalium.cryptography.exceptions -class ProteusException(message: String?, val code: Code, cause: Throwable? = null) : Exception(message, cause) { +open class ProteusException(message: String?, val code: Code, cause: Throwable? = null) : Exception(message, cause) { constructor(message: String?, code: Int, cause: Throwable? = null) : this(message, fromNativeCode(code), cause) @@ -191,3 +191,6 @@ class ProteusException(message: String?, val code: Code, cause: Throwable? = nul } } } + +class ProteusStorageMigrationException(override val message: String, val rootCause: Throwable? = null) : + ProteusException(message, Int.MIN_VALUE, null) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusClientProvider.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusClientProvider.kt index 013deac601f..4afb1d51bcb 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusClientProvider.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusClientProvider.kt @@ -18,9 +18,11 @@ package com.wire.kalium.logic.data.client +import com.wire.kalium.cryptography.CoreCryptoCentral import com.wire.kalium.cryptography.ProteusClient import com.wire.kalium.cryptography.coreCryptoCentral import com.wire.kalium.cryptography.cryptoboxProteusClient +import com.wire.kalium.cryptography.exceptions.ProteusStorageMigrationException import com.wire.kalium.logger.KaliumLogLevel import com.wire.kalium.logger.obfuscateId import com.wire.kalium.logic.CoreFailure @@ -58,20 +60,15 @@ class ProteusClientProviderImpl( private val userId: UserId, private val passphraseStorage: PassphraseStorage, private val kaliumConfigs: KaliumConfigs, - private val dispatcher: KaliumDispatcher = KaliumDispatcherImpl + private val dispatcher: KaliumDispatcher = KaliumDispatcherImpl, + private val proteusMigrationRecoveryHandler: ProteusMigrationRecoveryHandler ) : ProteusClientProvider { private var _proteusClient: ProteusClient? = null private val mutex = Mutex() override suspend fun clearLocalFiles() { - mutex.withLock { - withContext(dispatcher.io) { - _proteusClient?.close() - _proteusClient = null - FileUtil.deleteDirectory(rootProteusPath) - } - } + mutex.withLock { removeLocalFiles() } } override suspend fun getOrCreate(): ProteusClient { @@ -119,7 +116,7 @@ class ProteusClientProviderImpl( kaliumLogger.logStructuredJson(KaliumLogLevel.ERROR, TAG, logMap) throw e } - central.proteusClient() + getCentralProteusClientOrError(central) } else { cryptoboxProteusClient( rootDir = rootProteusPath, @@ -129,6 +126,35 @@ class ProteusClientProviderImpl( } } + private suspend fun getCentralProteusClientOrError(central: CoreCryptoCentral): ProteusClient { + return try { + central.proteusClient() + } catch (exception: ProteusStorageMigrationException) { + proteusMigrationRecoveryHandler.clearClientData { removeLocalFiles() } + val logMap = mapOf( + "userId" to userId.value.obfuscateId(), + "exception" to exception, + "message" to exception.message, + "stackTrace" to exception.stackTraceToString() + ) + kaliumLogger.withTextTag(TAG).logStructuredJson(KaliumLogLevel.ERROR, "Proteus storage migration failed", logMap) + throw exception + } + } + + /** + * Actually deletes the proteus local files. + * Important! It is the caller responsibility to use the mutex, DON'T add a mutex here or it will be dead lock it. + */ + private suspend fun removeLocalFiles() { + withContext(dispatcher.io) { + _proteusClient?.close() + _proteusClient = null + FileUtil.deleteDirectory(rootProteusPath) + } + } + + private companion object { const val TAG = "ProteusClientProvider" } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusMigrationRecoveryHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusMigrationRecoveryHandler.kt new file mode 100644 index 00000000000..9fe6880ecc4 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusMigrationRecoveryHandler.kt @@ -0,0 +1,30 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.data.client + +import com.wire.kalium.logic.data.logout.LogoutReason + +/** + * Handles the migration error of a proteus client storage from CryptoBox to CoreCrypto. + * It will perform a logout, using [LogoutReason.MIGRATION_TO_CC_FAILED] as the reason. + * + * This achieves that the client data is cleared and the user is logged out without losing content. + */ +interface ProteusMigrationRecoveryHandler { + suspend fun clearClientData(clearLocalFiles: suspend () -> Unit) +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/logout/LogoutReason.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/logout/LogoutReason.kt index 578ce4d1617..8475d397dcc 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/logout/LogoutReason.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/logout/LogoutReason.kt @@ -46,5 +46,11 @@ enum class LogoutReason { /** * Session Expired. */ - SESSION_EXPIRED; + SESSION_EXPIRED, + + /** + * The migration to CC failed. + * This will trigger a cleanup of the local client data and prepare for a fresh start without losing data. + */ + MIGRATION_TO_CC_FAILED; } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionMapper.kt index fb7dbb0181b..c602409c213 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionMapper.kt @@ -106,6 +106,7 @@ internal class SessionMapperImpl : SessionMapper { LogoutReason.REMOVED_CLIENT -> LogoutReasonEntity.REMOVED_CLIENT LogoutReason.DELETED_ACCOUNT -> LogoutReasonEntity.DELETED_ACCOUNT LogoutReason.SESSION_EXPIRED -> LogoutReasonEntity.SESSION_EXPIRED + LogoutReason.MIGRATION_TO_CC_FAILED -> LogoutReasonEntity.MIGRATION_TO_CC_FAILED } override fun toSsoIdEntity(ssoId: SsoId?): SsoIdEntity? = @@ -131,6 +132,7 @@ internal class SessionMapperImpl : SessionMapper { LogoutReasonEntity.REMOVED_CLIENT -> LogoutReason.REMOVED_CLIENT LogoutReasonEntity.DELETED_ACCOUNT -> LogoutReason.DELETED_ACCOUNT LogoutReasonEntity.SESSION_EXPIRED -> LogoutReason.SESSION_EXPIRED + LogoutReasonEntity.MIGRATION_TO_CC_FAILED -> LogoutReason.MIGRATION_TO_CC_FAILED } override fun fromEntityToProxyCredentialsDTO(proxyCredentialsEntity: ProxyCredentialsEntity): ProxyCredentialsDTO = diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt index 7527415262b..08c2a5bb477 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt @@ -51,6 +51,7 @@ import com.wire.kalium.logic.data.client.MLSClientProvider import com.wire.kalium.logic.data.client.MLSClientProviderImpl import com.wire.kalium.logic.data.client.ProteusClientProvider import com.wire.kalium.logic.data.client.ProteusClientProviderImpl +import com.wire.kalium.logic.data.client.ProteusMigrationRecoveryHandler import com.wire.kalium.logic.data.client.remote.ClientRemoteDataSource import com.wire.kalium.logic.data.client.remote.ClientRemoteRepository import com.wire.kalium.logic.data.connection.ConnectionDataSource @@ -181,6 +182,7 @@ import com.wire.kalium.logic.feature.client.IsAllowedToRegisterMLSClientUseCase import com.wire.kalium.logic.feature.client.IsAllowedToRegisterMLSClientUseCaseImpl import com.wire.kalium.logic.feature.client.MLSClientManager import com.wire.kalium.logic.feature.client.MLSClientManagerImpl +import com.wire.kalium.logic.feature.client.ProteusMigrationRecoveryHandlerImpl import com.wire.kalium.logic.feature.client.RegisterMLSClientUseCase import com.wire.kalium.logic.feature.client.RegisterMLSClientUseCaseImpl import com.wire.kalium.logic.feature.connection.ConnectionScope @@ -617,12 +619,17 @@ class UserSessionScope internal constructor( private val updateKeyingMaterialThresholdProvider: UpdateKeyingMaterialThresholdProvider get() = UpdateKeyingMaterialThresholdProviderImpl(kaliumConfigs) + private val proteusMigrationRecoveryHandler: ProteusMigrationRecoveryHandler by lazy { + ProteusMigrationRecoveryHandlerImpl(lazy { logout }) + } + val proteusClientProvider: ProteusClientProvider by lazy { ProteusClientProviderImpl( rootProteusPath = rootPathsProvider.rootProteusPath(userId), userId = userId, passphraseStorage = globalPreferences.passphraseStorage, - kaliumConfigs = kaliumConfigs + kaliumConfigs = kaliumConfigs, + proteusMigrationRecoveryHandler = proteusMigrationRecoveryHandler ) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/auth/LogoutUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/auth/LogoutUseCase.kt index d3be95db8b5..4609d23c225 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/auth/LogoutUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/auth/LogoutUseCase.kt @@ -31,6 +31,7 @@ import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.client.ClearClientDataUseCase import com.wire.kalium.logic.feature.session.DeregisterTokenUseCase import com.wire.kalium.logic.featureFlags.KaliumConfigs +import com.wire.kalium.logic.kaliumLogger import com.wire.kalium.logic.sync.UserSessionWorkScheduler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel @@ -106,6 +107,9 @@ internal class LogoutUseCaseImpl @Suppress("LongParameterList") constructor( } LogoutReason.SELF_SOFT_LOGOUT -> clearCurrentClientIdAndFirebaseTokenFlag() + LogoutReason.MIGRATION_TO_CC_FAILED -> prepareForCoreCryptoMigrationRecovery() + }.also { + kaliumLogger.withTextTag(TAG).d("Logout reason: $reason") } userConfigRepository.clearE2EISettings() @@ -115,6 +119,13 @@ internal class LogoutUseCaseImpl @Suppress("LongParameterList") constructor( }.let { if (waitUntilCompletes) it.join() else it } } + private suspend fun prepareForCoreCryptoMigrationRecovery() { + clearClientDataUseCase() + logoutRepository.clearClientRelatedLocalMetadata() + clientRepository.clearRetainedClientId() + pushTokenRepository.setUpdateFirebaseTokenFlag(true) + } + private suspend fun clearCurrentClientIdAndFirebaseTokenFlag() { clientRepository.clearCurrentClientId() clientRepository.clearNewClients() @@ -146,5 +157,6 @@ internal class LogoutUseCaseImpl @Suppress("LongParameterList") constructor( companion object { const val CLEAR_DATA_DELAY = 1000L + const val TAG = "LogoutUseCase" } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/ProteusMigrationRecoveryHandlerImpl.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/ProteusMigrationRecoveryHandlerImpl.kt new file mode 100644 index 00000000000..c2b7e42bfbb --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/ProteusMigrationRecoveryHandlerImpl.kt @@ -0,0 +1,52 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.client + +import com.wire.kalium.logic.data.client.ProteusMigrationRecoveryHandler +import com.wire.kalium.logic.data.logout.LogoutReason +import com.wire.kalium.logic.feature.auth.LogoutUseCase +import com.wire.kalium.logic.kaliumLogger + +internal class ProteusMigrationRecoveryHandlerImpl( + private val logoutUseCase: Lazy +) : ProteusMigrationRecoveryHandler { + + /** + * Handles the migration error of a proteus client storage from CryptoBox to CoreCrypto. + * It will perform a logout, using [LogoutReason.MIGRATION_TO_CC_FAILED] as the reason. + * + * This achieves that the client data is cleared and the user is logged out without losing content. + */ + @Suppress("TooGenericExceptionCaught") + override suspend fun clearClientData(clearLocalFiles: suspend () -> Unit) { + try { + kaliumLogger.withTextTag(TAG).i("Starting the recovery from failed Proteus storage migration") + clearLocalFiles() + logoutUseCase.value(LogoutReason.MIGRATION_TO_CC_FAILED, true) + } catch (e: Exception) { + kaliumLogger.withTextTag(TAG).e("Fatal, error while clearing client data: $e") + throw e + } finally { + kaliumLogger.withTextTag(TAG).i("Finished the recovery from failed Proteus storage migration") + } + } + + private companion object { + const val TAG = "ProteusMigrationRecoveryHandler" + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/RegisterClientUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/RegisterClientUseCase.kt index 0a123c63388..27730c0ee4b 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/RegisterClientUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/RegisterClientUseCase.kt @@ -36,6 +36,7 @@ import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.flatMap import com.wire.kalium.logic.functional.fold import com.wire.kalium.logic.functional.map +import com.wire.kalium.logic.kaliumLogger import com.wire.kalium.network.exceptions.AuthenticationCodeFailure import com.wire.kalium.network.exceptions.KaliumException import com.wire.kalium.network.exceptions.authenticationCodeFailure @@ -144,8 +145,9 @@ class RegisterClientUseCaseImpl @OptIn(DelicateKaliumApi::class) internal constr cookieLabel, verificationCode ) - }.fold({ - RegisterClientResult.Failure.Generic(it) + }.fold({ error -> + kaliumLogger.withTextTag(TAG).e("There was an error while registering the client $error") + RegisterClientResult.Failure.Generic(error) }, { registerClientParam -> clientRepository.registerClient(registerClientParam) // todo? separate this in mls client usesCase register! separate everything @@ -237,4 +239,8 @@ class RegisterClientUseCaseImpl @OptIn(DelicateKaliumApi::class) internal constr } } } + + private companion object { + const val TAG = "RegisterClientUseCase" + } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncCriteriaProvider.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncCriteriaProvider.kt index 49f0567d0a1..fb9f20448c0 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncCriteriaProvider.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncCriteriaProvider.kt @@ -116,6 +116,7 @@ internal class SlowSlowSyncCriteriaProviderImpl( LogoutReason.SESSION_EXPIRED -> "Logout: SESSION_EXPIRED" LogoutReason.REMOVED_CLIENT -> "Logout: REMOVED_CLIENT" LogoutReason.DELETED_ACCOUNT -> "Logout: DELETED_ACCOUNT" + LogoutReason.MIGRATION_TO_CC_FAILED -> "Logout: MIGRATION_TO_CC_FAILED" null -> null }?.let { MissingRequirement(it) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/auth/LogoutUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/auth/LogoutUseCaseTest.kt index a515c4a3c2c..518c7ab97df 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/auth/LogoutUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/auth/LogoutUseCaseTest.kt @@ -351,6 +351,39 @@ class LogoutUseCaseTest { .wasInvoked(exactly = calls.size.time) } + @Test + fun givenAMigrationFailedLogout_whenLoggingOut_thenExecuteAllRequiredActions() = runTest { + val reason = LogoutReason.MIGRATION_TO_CC_FAILED + val (arrangement, logoutUseCase) = Arrangement() + .withLogoutResult(Either.Right(Unit)) + .withSessionLogoutResult(Either.Right(Unit)) + .withAllValidSessionsResult(Either.Right(listOf(Arrangement.VALID_ACCOUNT_INFO))) + .withDeregisterTokenResult(DeregisterTokenUseCase.Result.Success) + .withClearCurrentClientIdResult(Either.Right(Unit)) + .withClearRetainedClientIdResult(Either.Right(Unit)) + .withUserSessionScopeGetResult(null) + .withFirebaseTokenUpdate() + .withNoOngoingCalls() + .arrange() + + logoutUseCase.invoke(reason) + arrangement.globalTestScope.advanceUntilIdle() + + coVerify { + arrangement.clearClientDataUseCase.invoke() + }.wasInvoked(exactly = once) + coVerify { + arrangement.logoutRepository.clearClientRelatedLocalMetadata() + }.wasInvoked(exactly = once) + + coVerify { + arrangement.clientRepository.clearRetainedClientId() + }.wasInvoked(exactly = once) + coVerify { + arrangement.pushTokenRepository.setUpdateFirebaseTokenFlag(eq(true)) + }.wasInvoked(exactly = once) + } + private class Arrangement { @Mock val logoutRepository = mock(classOf()) diff --git a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/data/client/ProteusClientProviderTest.kt b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/data/client/ProteusClientProviderTest.kt new file mode 100644 index 00000000000..8d4c8caa2f5 --- /dev/null +++ b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/data/client/ProteusClientProviderTest.kt @@ -0,0 +1,73 @@ +package com.wire.kalium.logic.data.client + +import com.wire.kalium.cryptography.exceptions.ProteusStorageMigrationException +import com.wire.kalium.logic.featureFlags.KaliumConfigs +import com.wire.kalium.logic.framework.TestUser +import com.wire.kalium.persistence.dbPassphrase.PassphraseStorage +import com.wire.kalium.util.FileUtil +import com.wire.kalium.util.KaliumDispatcherImpl +import io.mockative.Mock +import io.mockative.any +import io.mockative.coVerify +import io.mockative.every +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.nio.file.Paths +import kotlin.io.path.createDirectory +import kotlin.io.path.createFile +import kotlin.io.path.exists + +class ProteusClientProviderTest { + + @Test + fun givenGettingOrCreatingAProteusClient_whenMigrationPerformedAndFails_thenCatchErrorAndStartRecovery() = runTest { + // given + val (arrangement, proteusClientProvider) = Arrangement() + .withCorruptedProteusStorage() + .arrange() + + // when - then + try { + proteusClientProvider.getOrCreate() + } catch (e: ProteusStorageMigrationException) { + coVerify { arrangement.proteusMigrationRecoveryHandler.clearClientData(any()) }.wasInvoked(once) + } + } + + private class Arrangement { + + @Mock + val passphraseStorage = mock(PassphraseStorage::class) + + @Mock + val proteusMigrationRecoveryHandler = mock(ProteusMigrationRecoveryHandler::class) + + init { + every { passphraseStorage.getPassphrase(any()) }.returns("passphrase") + } + + /** + * Corrupted because it's just an empty file called "prekeys". + * But nothing to migrate, this is just to test that we are calling recovery. + */ + fun withCorruptedProteusStorage() = apply { + val rootProteusPath = Paths.get("/tmp/rootProteusPath") + if (rootProteusPath.exists()) { + FileUtil.deleteDirectory(rootProteusPath.toString()) + } + rootProteusPath.createDirectory() + rootProteusPath.resolve("prekeys").createFile() + } + + fun arrange() = this to ProteusClientProviderImpl( + rootProteusPath = "/tmp/rootProteusPath", + userId = TestUser.USER_ID, + passphraseStorage = passphraseStorage, + kaliumConfigs = KaliumConfigs(encryptProteusStorage = true), + dispatcher = KaliumDispatcherImpl, + proteusMigrationRecoveryHandler = proteusMigrationRecoveryHandler + ) + } +} diff --git a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/client/ProteusMigrationRecoveryHandlerTest.kt b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/client/ProteusMigrationRecoveryHandlerTest.kt new file mode 100644 index 00000000000..7a52607adb6 --- /dev/null +++ b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/client/ProteusMigrationRecoveryHandlerTest.kt @@ -0,0 +1,36 @@ +package com.wire.kalium.logic.feature.client + +import com.wire.kalium.logic.data.logout.LogoutReason +import com.wire.kalium.logic.feature.auth.LogoutUseCase +import io.mockative.Mock +import io.mockative.coVerify +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ProteusMigrationRecoveryHandlerTest { + + @Test + fun givenGettingOrCreatingAProteusClient_whenMigrationPerformedAndFails_thenCatchErrorAndStartRecovery() = runTest { + // given + val (arrangement, proteusMigrationRecoveryHandler) = Arrangement().arrange() + + // when + val clearLocalFiles: suspend () -> Unit = { } + proteusMigrationRecoveryHandler.clearClientData(clearLocalFiles) + + // then + coVerify { arrangement.logoutUseCase(LogoutReason.MIGRATION_TO_CC_FAILED, true) }.wasInvoked(once) + } + + private class Arrangement { + + @Mock + val logoutUseCase = mock(LogoutUseCase::class) + + fun arrange() = this to ProteusMigrationRecoveryHandlerImpl( + lazy { logoutUseCase } + ) + } +} diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/model/LogoutReason.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/model/LogoutReason.kt index 6d0ed44cc61..747df39893c 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/model/LogoutReason.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/model/LogoutReason.kt @@ -38,5 +38,11 @@ enum class LogoutReason { /** * Session Expired. */ - SESSION_EXPIRED; + SESSION_EXPIRED, + + /** + * The migration to CC failed. + * This will trigger a cleanup of the local client data and prepare for a fresh start without losing data. + */ + MIGRATION_TO_CC_FAILED; }