diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderMappers.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderMappers.kt index fc20894926b..96fa82315c7 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderMappers.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderMappers.kt @@ -84,3 +84,9 @@ fun ConversationFolderTypeEntity.toModel(): FolderType = when (this) { ConversationFolderTypeEntity.USER -> FolderType.USER ConversationFolderTypeEntity.FAVORITE -> FolderType.FAVORITE } + +fun ConversationFolder.toDao() = ConversationFolderEntity( + id = id, + name = name, + type = type.toDao() +) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt index 74eb3c227fb..4357881f204 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt @@ -60,6 +60,7 @@ internal interface ConversationFolderRepository { suspend fun removeFolder(folderId: String): Either suspend fun syncConversationFoldersFromLocal(): Either suspend fun observeFolders(): Flow>> + suspend fun addFolder(folder: ConversationFolder): Either } internal class ConversationFolderDataSource internal constructor( @@ -165,4 +166,10 @@ internal class ConversationFolderDataSource internal constructor( .wrapStorageRequest() .mapRight { folderEntities -> folderEntities.map { it.toModel() } } } + + override suspend fun addFolder(folder: ConversationFolder): Either { + return wrapStorageRequest { + conversationFolderDAO.addFolder(folder.toDao()) + } + } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/backup/BackupScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/backup/BackupScope.kt index d3a70a03fa8..23984f237d6 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/backup/BackupScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/backup/BackupScope.kt @@ -28,6 +28,7 @@ import com.wire.kalium.logic.feature.message.PersistMigratedMessagesUseCase import com.wire.kalium.logic.sync.slow.RestartSlowSyncProcessForRecoveryUseCase import com.wire.kalium.logic.util.SecurityHelperImpl import com.wire.kalium.persistence.kmmSettings.GlobalPrefProvider +import com.wire.kalium.util.DelicateKaliumApi @Suppress("LongParameterList") class BackupScope internal constructor( @@ -72,4 +73,14 @@ class BackupScope internal constructor( restoreWeb ) + + @DelicateKaliumApi("this is NOT a backup feature, but a feature to create an unencrypted and obfuscated copy of the database") + val createUnEncryptedCopy: CreateUnEncryptedCopyUseCase + get() = CreateUnEncryptedCopyUseCase( + userId, + clientIdProvider, + userRepository, + kaliumFileSystem, + userStorage.database.obfuscatedCopyExporter, + ) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/backup/CreateUnEncryptedCopyUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/backup/CreateUnEncryptedCopyUseCase.kt new file mode 100644 index 00000000000..e9f09428263 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/backup/CreateUnEncryptedCopyUseCase.kt @@ -0,0 +1,185 @@ +/* + * Wire + * Copyright (C) 2025 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.backup + +import com.wire.kalium.cryptography.backup.BackupCoder +import com.wire.kalium.cryptography.backup.Passphrase +import com.wire.kalium.cryptography.utils.ChaCha20Encryptor.encryptBackupFile +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.clientPlatform +import com.wire.kalium.logic.data.asset.KaliumFileSystem +import com.wire.kalium.logic.data.id.CurrentClientIdProvider +import com.wire.kalium.logic.data.id.IdMapper +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.data.user.UserRepository +import com.wire.kalium.logic.di.MapperProvider +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.nullableFold +import com.wire.kalium.logic.kaliumLogger +import com.wire.kalium.logic.util.createCompressedFile +import com.wire.kalium.persistence.backup.ObfuscatedCopyExporter +import com.wire.kalium.util.DateTimeUtil +import com.wire.kalium.util.DelicateKaliumApi +import com.wire.kalium.util.KaliumDispatcher +import com.wire.kalium.util.KaliumDispatcherImpl +import kotlinx.coroutines.withContext +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import okio.FileNotFoundException +import okio.Path +import okio.Path.Companion.toPath +import okio.Sink +import okio.Source +import okio.buffer +import okio.use + +@DelicateKaliumApi("This class is used for debugging purposes only") +class CreateUnEncryptedCopyUseCase internal constructor( + private val userId: UserId, + private val clientIdProvider: CurrentClientIdProvider, + private val userRepository: UserRepository, + private val kaliumFileSystem: KaliumFileSystem, + private val obfuscatedCopyExporter: ObfuscatedCopyExporter, + private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl, + private val idMapper: IdMapper = MapperProvider.idMapper(), +) { + + @DelicateKaliumApi("This function is used for debugging purposes only") + suspend operator fun invoke(password: String?): CreateBackupResult = withContext(dispatchers.default) { + val userHandle = userRepository.getSelfUser()?.handle?.replace(".", "-") + val timeStamp = DateTimeUtil.currentSimpleDateTimeString() + val backupName = createFinalZipName(userHandle, timeStamp) + val backupFilePath = kaliumFileSystem.tempFilePath(backupName) + deletePreviousBackupFiles(backupFilePath) + + val plainDBPath = + obfuscatedCopyExporter.exportToPlainDB()?.toPath() + ?: return@withContext CreateBackupResult.Failure(StorageFailure.DataNotFound) + + try { + createBackupFile(userId, plainDBPath, backupFilePath).fold( + { error -> CreateBackupResult.Failure(error) }, + { (backupFilePath, backupSize) -> + if (password != null) { + encryptAndCompressFile(backupFilePath, password) + } else CreateBackupResult.Success(backupFilePath, backupSize, backupFilePath.name) + }) + } finally { + obfuscatedCopyExporter.deleteCopyFile() + } + } + + private suspend fun encryptAndCompressFile(backupFilePath: Path, password: String): CreateBackupResult { + val encryptedBackupFilePath = kaliumFileSystem.tempFilePath(COPY_ENCRYPTED_FILE_NAME) + val backupEncryptedDataSize = encryptBackup( + kaliumFileSystem.source(backupFilePath), + kaliumFileSystem.sink(encryptedBackupFilePath), + Passphrase(password) + ) + if (backupEncryptedDataSize == 0L) + return CreateBackupResult.Failure(StorageFailure.Generic(RuntimeException("Failed to encrypt backup file"))) + + val finalBackupFilePath = kaliumFileSystem.tempFilePath("encrypted-${backupFilePath.name}") + + return createCompressedFile( + listOf(kaliumFileSystem.source(encryptedBackupFilePath) to encryptedBackupFilePath.name), + kaliumFileSystem.sink(finalBackupFilePath) + ).fold({ + CreateBackupResult.Failure(StorageFailure.Generic(RuntimeException("Failed to compress encrypted backup file"))) + }, { backupEncryptedCompressedDataSize -> + deleteTempFiles(backupFilePath, encryptedBackupFilePath) + + if (backupEncryptedCompressedDataSize > 0) { + CreateBackupResult.Success(finalBackupFilePath, backupEncryptedCompressedDataSize, finalBackupFilePath.name) + } else { + CreateBackupResult.Failure(StorageFailure.Generic(RuntimeException("Failed to encrypt backup file"))) + } + }) + } + + private fun deletePreviousBackupFiles(backupFilePath: Path) { + if (kaliumFileSystem.exists(backupFilePath)) + kaliumFileSystem.delete(backupFilePath) + } + + private fun deleteTempFiles(backupFilePath: Path, encryptedBackupFilePath: Path) { + kaliumFileSystem.delete(backupFilePath) + kaliumFileSystem.delete(encryptedBackupFilePath) + kaliumFileSystem.delete(kaliumFileSystem.tempFilePath(COPY_METADATA_FILE_NAME)) + } + + private suspend fun encryptBackup(backupFileSource: Source, encryptedBackupSink: Sink, passphrase: Passphrase) = + encryptBackupFile(backupFileSource, encryptedBackupSink, idMapper.toCryptoModel(userId), passphrase) + + private suspend fun createMetadataFile(userId: UserId): Path { + val clientId = clientIdProvider().nullableFold({ null }, { it.value }) + val creationTime = DateTimeUtil.currentIsoDateTimeString() + val metadata = BackupMetadata( + clientPlatform, + BackupCoder.version, + userId.toString(), + creationTime, + clientId + ) + val metadataJson = Json.encodeToString(metadata) + + val metadataFilePath = kaliumFileSystem.tempFilePath(COPY_METADATA_FILE_NAME) + kaliumFileSystem.sink(metadataFilePath).buffer().use { + it.write(metadataJson.encodeToByteArray()) + } + return metadataFilePath + } + + private suspend fun createBackupFile( + userId: UserId, + plainDBPath: Path, + backupZipFilePath: Path + ): Either> { + return try { + val backupSink = kaliumFileSystem.sink(backupZipFilePath) + val backupMetadataPath = createMetadataFile(userId) + val filesList = listOf( + kaliumFileSystem.source(backupMetadataPath) to COPY_METADATA_FILE_NAME, + kaliumFileSystem.source(plainDBPath) to COPY_USER_DB_NAME + ) + + createCompressedFile(filesList, backupSink).flatMap { compressedFileSize -> + Either.Right(backupZipFilePath to compressedFileSize) + } + } catch (e: FileNotFoundException) { + kaliumLogger.e("There was an error when fetching the user db data path", e) + Either.Left(StorageFailure.DataNotFound) + } + } + + private fun createFinalZipName(userHandle: String?, timestampIso: String) = // file names cannot have special characters + "$COPY_FILE_NAME_PREFIX-$userHandle-${timestampIso.replace(":", "-")}.zip" + + private companion object { + const val COPY_FILE_NAME_PREFIX = "OBFUSCATED_COPY_WBX" + const val COPY_ENCRYPTED_FILE_NAME = "obfuscated-copy-user-backup.cc20" + + // BACKUP_METADATA_FILE_NAME and BACKUP_USER_DB_NAME must not be changed + // if there is a need to change them, please create a new file names and add it to the list of acceptedFileNames() + const val COPY_USER_DB_NAME = "user-backup-database.db" + const val COPY_METADATA_FILE_NAME = "export.json" + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt index 6f4256b7ae5..0b6bfae80d9 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt @@ -53,6 +53,8 @@ import com.wire.kalium.logic.feature.connection.ObservePendingConnectionRequests import com.wire.kalium.logic.feature.connection.ObservePendingConnectionRequestsUseCaseImpl import com.wire.kalium.logic.feature.conversation.folder.AddConversationToFavoritesUseCase import com.wire.kalium.logic.feature.conversation.folder.AddConversationToFavoritesUseCaseImpl +import com.wire.kalium.logic.feature.conversation.folder.CreateConversationFolderUseCase +import com.wire.kalium.logic.feature.conversation.folder.CreateConversationFolderUseCaseImpl import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCase import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCaseImpl import com.wire.kalium.logic.feature.conversation.folder.MoveConversationToFolderUseCase @@ -394,4 +396,6 @@ class ConversationScope internal constructor( get() = MoveConversationToFolderUseCaseImpl(conversationFolderRepository) val removeConversationFromFolder: RemoveConversationFromFolderUseCase get() = RemoveConversationFromFolderUseCaseImpl(conversationFolderRepository) + val createConversationFolder: CreateConversationFolderUseCase + get() = CreateConversationFolderUseCaseImpl(conversationFolderRepository) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/CreateConversationFolderUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/CreateConversationFolderUseCase.kt new file mode 100644 index 00000000000..e7e72c4032e --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/CreateConversationFolderUseCase.kt @@ -0,0 +1,66 @@ +/* + * 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.conversation.folder + +import com.benasher44.uuid.uuid4 +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.ConversationFolder +import com.wire.kalium.logic.data.conversation.FolderType +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.functional.fold +import com.wire.kalium.util.KaliumDispatcher +import com.wire.kalium.util.KaliumDispatcherImpl +import kotlinx.coroutines.withContext + +/** + * This use case will create a new conversation folder. + */ +interface CreateConversationFolderUseCase { + /** + * @param folderName the name of the folder + * @return the [Result] indicating a successful operation, otherwise a [CoreFailure] + */ + suspend operator fun invoke( + folderName: String + ): Result + + sealed interface Result { + data class Success(val folderId: String) : Result + data class Failure(val cause: CoreFailure) : Result + } +} + +internal class CreateConversationFolderUseCaseImpl( + private val conversationFolderRepository: ConversationFolderRepository, + private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl +) : CreateConversationFolderUseCase { + override suspend fun invoke(folderName: String): CreateConversationFolderUseCase.Result = withContext(dispatchers.io) { + val folder = ConversationFolder( + id = uuid4().toString(), + name = folderName, + type = FolderType.USER + ) + conversationFolderRepository.addFolder(folder) + .fold({ + CreateConversationFolderUseCase.Result.Failure(it) + }, { + CreateConversationFolderUseCase.Result.Success(folder.id) + }) + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepositoryTest.kt index 2fadbc3e3fe..9483c5015f0 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepositoryTest.kt @@ -18,6 +18,7 @@ package com.wire.kalium.logic.data.conversation.folders import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.data.conversation.ConversationFolder import com.wire.kalium.logic.data.conversation.FolderType import com.wire.kalium.logic.data.conversation.FolderWithConversations import com.wire.kalium.logic.data.id.toDao @@ -230,6 +231,20 @@ class ConversationFolderRepositoryTest { coVerify { arrangement.conversationFolderDAO.removeFolder(eq(folderId)) }.wasInvoked() } + @Test + fun givenValidFolderWhenAddingFolderThenShouldAddSuccessfully() = runTest { + // given + val folder = ConversationFolder(id = "folder1", name = "New Folder", type = FolderType.USER) + val arrangement = Arrangement().withSuccessfulFolderAddition() + + // when + val result = arrangement.repository.addFolder(folder) + + // then + result.shouldSucceed() + coVerify { arrangement.conversationFolderDAO.addFolder(eq(folder.toDao())) }.wasInvoked() + } + private class Arrangement { @Mock @@ -297,5 +312,10 @@ class ConversationFolderRepositoryTest { coEvery { conversationFolderDAO.removeFolder(any()) }.returns(Unit) return this } + + suspend fun withSuccessfulFolderAddition(): Arrangement { + coEvery { conversationFolderDAO.addFolder(any()) }.returns(Unit) + return this + } } } diff --git a/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt b/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt index 84f63d0a743..cc157b0ef1c 100644 --- a/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt +++ b/persistence/src/androidMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt @@ -28,6 +28,7 @@ import com.wire.kalium.persistence.UserDatabase import com.wire.kalium.persistence.dao.UserIDEntity import com.wire.kalium.persistence.db.support.SupportOpenHelperFactory import com.wire.kalium.persistence.util.FileNameUtil +import com.wire.kalium.util.FileUtil import kotlinx.coroutines.CoroutineDispatcher import net.zetetic.database.sqlcipher.SQLiteDatabase import java.io.File @@ -124,3 +125,16 @@ internal actual fun getDatabaseAbsoluteFileLocation( null } } + +internal actual fun createEmptyDatabaseFile( + platformDatabaseData: PlatformDatabaseData, + userId: UserIDEntity, +): String? = + (FileNameUtil.userDBName(userId)).let { fileName -> + platformDatabaseData.context.getDatabasePath(fileName).let { + it.delete() + it.createNewFile() + it.absolutePath + } + } + diff --git a/persistence/src/appleMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt b/persistence/src/appleMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt index 56f8d4c6e20..54c1a272eb7 100644 --- a/persistence/src/appleMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt +++ b/persistence/src/appleMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt @@ -121,3 +121,8 @@ internal actual fun getDatabaseAbsoluteFileLocation( null } } + +internal actual fun createEmptyDatabaseFile( + platformDatabaseData: PlatformDatabaseData, + userId: UserIDEntity, +): String? = TODO() diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/backup/ObfiscatedCopyExporter.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/backup/ObfiscatedCopyExporter.kt new file mode 100644 index 00000000000..e0c49b844d1 --- /dev/null +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/backup/ObfiscatedCopyExporter.kt @@ -0,0 +1,178 @@ +/* + * Wire + * Copyright (C) 2025 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.persistence.backup + +import com.wire.kalium.persistence.dao.UserIDEntity +import com.wire.kalium.persistence.db.PlatformDatabaseData +import com.wire.kalium.persistence.db.UserDBSecret +import com.wire.kalium.persistence.db.UserDatabaseBuilder +import com.wire.kalium.persistence.db.createEmptyDatabaseFile +import com.wire.kalium.persistence.db.nuke +import com.wire.kalium.persistence.db.userDatabaseDriverByPath +import com.wire.kalium.persistence.kaliumLogger +import com.wire.kalium.util.DelicateKaliumApi +import com.wire.kalium.util.FileUtil +import com.wire.kalium.util.KaliumDispatcher +import com.wire.kalium.util.KaliumDispatcherImpl +import kotlinx.coroutines.withContext + +@DelicateKaliumApi("This class is used for debugging purposes only") +class ObfuscatedCopyExporter internal constructor( + user: UserIDEntity, + private val platformDatabaseData: PlatformDatabaseData, + private val localDatabase: UserDatabaseBuilder, + private val kaliumDispatcher: KaliumDispatcher = KaliumDispatcherImpl +) { + private val emptyPlainFIleNameUserId = user.copy(value = ("obfuscated-backup-" + user.value)) + + fun deleteCopyFile() { + nuke(emptyPlainFIleNameUserId, platformDatabaseData) + } + + @Suppress("TooGenericExceptionCaught", "ReturnCount") + suspend fun exportToPlainDB(): String? { + + val emptyPlainFilePath: String = createEmptyDatabaseFile(platformDatabaseData, emptyPlainFIleNameUserId) ?: return null + + attachAndExport(localDatabase, emptyPlainFilePath) + val obfuscateResult = obfuscatePlainCopy(emptyPlainFilePath) + + return if (obfuscateResult) { + emptyPlainFilePath + } else { + FileUtil.deleteDirectory(emptyPlainFilePath) + null + } + } + + private suspend fun attachAndExport(localDatabase: UserDatabaseBuilder, emptyPlainFilePath: String) = withContext(kaliumDispatcher.io) { + localDatabase.sqlDriver.execute( + null, + "ATTACH DATABASE ? AS $OBFUSCATED_PLAIN_DB KEY ''", + 0 + ) { + bindString(0, emptyPlainFilePath) + } + + localDatabase.sqlDriver.executeQuery( + null, + "SELECT sqlcipher_export('$OBFUSCATED_PLAIN_DB')", + { cursor -> cursor.next() }, + 0 + ) + + localDatabase.sqlDriver.execute( + null, + "DETACH DATABASE $OBFUSCATED_PLAIN_DB", + 0 + ) + } + + @Suppress("TooGenericExceptionCaught", "ReturnCount") + private suspend fun obfuscatePlainCopy( + plainDBPath: String, + ): Boolean = withContext(kaliumDispatcher.io) { + val plainDBDriver = userDatabaseDriverByPath( + platformDatabaseData, + plainDBPath, + UserDBSecret(ByteArray(0)), + false + ) + + try { + plainDBDriver.execute( + null, "UPDATE MessageTextContent " + + "SET text_body = CASE " + + " WHEN text_body IS NOT NULL " + + " THEN substr(hex(randomblob(length(text_body))), 1, length(text_body)) " + + " ELSE NULL " + + "END " + + "WHERE text_body IS NOT NULL;", 0 + ) + + plainDBDriver.execute( + null, + "UPDATE MessageLinkPreview " + + "SET " + + " url = CASE " + + " WHEN url IS NOT NULL " + + " THEN substr(hex(randomblob(length(url))), 1, length(url)) " + + " ELSE NULL " + + " END, " + + " permanent_url = CASE " + + " WHEN permanent_url IS NOT NULL " + + " THEN substr(hex(randomblob(length(permanent_url))), 1, length(permanent_url)) " + + " ELSE NULL " + + " END, " + + " title = CASE " + + " WHEN title IS NOT NULL " + + " THEN substr(hex(randomblob(length(title))), 1, length(title)) " + + " ELSE NULL " + + " END, " + + " summary = CASE " + + " WHEN summary IS NOT NULL " + + " THEN substr(hex(randomblob(length(summary))), 1, length(summary)) " + + " ELSE NULL " + + " END " + + "WHERE url IS NOT NULL " + + " OR permanent_url IS NOT NULL " + + " OR title IS NOT NULL " + + " OR summary IS NOT NULL;", + 0 + ) + + plainDBDriver.execute( + null, + "UPDATE MessageAssetContent " + + "SET " + + " asset_sha256 = CASE " + + " WHEN asset_sha256 IS NOT NULL " + + " THEN randomblob(length(asset_sha256)) " + + " ELSE NULL " + + " END " + + "WHERE asset_otr_key IS NOT NULL " + + " OR asset_sha256 IS NOT NULL;", + 0 + ) + + plainDBDriver.execute( + null, + "UPDATE MessageDraft " + + "SET " + + " text = CASE " + + " WHEN text IS NOT NULL " + + " THEN randomblob(length(text)) " + + " ELSE NULL " + + " END " + + "WHERE text IS NOT NULL;", + 0 + ) + } catch (e: Exception) { + kaliumLogger.e("Failed to attach the local DB to the plain DB: ${e.stackTraceToString()}") + return@withContext false + } finally { + plainDBDriver.close() + } + return@withContext true + } + + private companion object { + // THIS MUST MATCH THE PLAIN DATABASE ALIAS IN DumpContent.sq DO NOT CHANGE + const val OBFUSCATED_PLAIN_DB = "obfuscated_plain_db" + } +} diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt index c936fed9da3..9926ba8a05f 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt @@ -30,4 +30,5 @@ interface ConversationFolderDAO { suspend fun removeConversationFromFolder(conversationId: QualifiedIDEntity, folderId: String) suspend fun observeFolders(): Flow> suspend fun removeFolder(folderId: String) + suspend fun addFolder(folder: ConversationFolderEntity) } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt index 1f71b24e653..5094c75878e 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt @@ -50,6 +50,10 @@ class ConversationFolderDAOImpl internal constructor( conversationFoldersQueries.deleteFolder(folderId) } + override suspend fun addFolder(folder: ConversationFolderEntity) = withContext(coroutineContext) { + conversationFoldersQueries.upsertFolder(folder.id, folder.name, folder.type) + } + override suspend fun getFoldersWithConversations(): List = withContext(coroutineContext) { val labeledConversationList = conversationFoldersQueries.getAllFoldersWithConversations().executeAsList().map(::toEntity) diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt index 7bbebca1f26..2133f94af96 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt @@ -26,6 +26,7 @@ import com.wire.kalium.persistence.backup.DatabaseExporter import com.wire.kalium.persistence.backup.DatabaseExporterImpl import com.wire.kalium.persistence.backup.DatabaseImporter import com.wire.kalium.persistence.backup.DatabaseImporterImpl +import com.wire.kalium.persistence.backup.ObfuscatedCopyExporter import com.wire.kalium.persistence.cache.FlowCache import com.wire.kalium.persistence.dao.ConnectionDAO import com.wire.kalium.persistence.dao.ConnectionDAOImpl @@ -250,6 +251,9 @@ class UserDatabaseBuilder internal constructor( val databaseExporter: DatabaseExporter get() = DatabaseExporterImpl(userId, platformDatabaseData, this) + val obfuscatedCopyExporter: ObfuscatedCopyExporter + get() = ObfuscatedCopyExporter(userId, platformDatabaseData, this) + val callDAO: CallDAO get() = CallDAOImpl(database.callsQueries, queriesContext) @@ -317,7 +321,7 @@ class UserDatabaseBuilder internal constructor( get() = DebugExtension( sqlDriver = sqlDriver, metaDataDao = metadataDAO, - isEncrypted = isEncrypted + isEncrypted = isEncrypted, ) /** @@ -345,6 +349,11 @@ internal expect fun getDatabaseAbsoluteFileLocation( userId: UserIDEntity ): String? +internal expect fun createEmptyDatabaseFile( + platformDatabaseData: PlatformDatabaseData, + userId: UserIDEntity, +): String? + @Suppress("TooGenericExceptionCaught") fun SqlDriver.migrate(sqlSchema: SqlSchema>): Boolean { val oldVersion = this.executeQuery(null, "PRAGMA user_version;", { diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOTest.kt index eeeef07a24e..307801b6ede 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOTest.kt @@ -285,6 +285,24 @@ class ConversationFolderDAOTest : BaseDatabaseTest() { assertTrue(result.any { it.id == "folderId2" }) } + @Test + fun givenFolder_whenAddingFolder_thenFolderShouldBeAddedToDatabase() = runTest { + val folderId = "folder1" + val folderName = "Test Folder" + val folderType = ConversationFolderTypeEntity.USER + val folder = ConversationFolderEntity(id = folderId, name = folderName, type = folderType) + + db.conversationFolderDAO.addFolder(folder) + + val result = db.conversationFolderDAO.getFoldersWithConversations() + + assertEquals(1, result.size) + val addedFolder = result.first() + assertEquals(folderId, addedFolder.id) + assertEquals(folderName, addedFolder.name) + assertEquals(folderType, addedFolder.type) + } + companion object { fun folderWithConversationsEntity( id: String = "folderId", diff --git a/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt b/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt index f281ead0572..4b7c8461b13 100644 --- a/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt +++ b/persistence/src/jsMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt @@ -50,3 +50,8 @@ internal actual fun getDatabaseAbsoluteFileLocation( platformDatabaseData: PlatformDatabaseData, userId: UserIDEntity ): String? = TODO() + +internal actual fun createEmptyDatabaseFile( + platformDatabaseData: PlatformDatabaseData, + userId: UserIDEntity, +): String? = TODO() diff --git a/persistence/src/jvmMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt b/persistence/src/jvmMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt index cf5b97a23e0..71ca541820d 100644 --- a/persistence/src/jvmMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt +++ b/persistence/src/jvmMain/kotlin/com/wire/kalium/persistence/db/UserDatabase.kt @@ -86,6 +86,12 @@ internal actual fun getDatabaseAbsoluteFileLocation( return if (dbFile.exists()) dbFile.absolutePath else null } +internal actual fun createEmptyDatabaseFile( + platformDatabaseData: PlatformDatabaseData, + userId: UserIDEntity, +): String? = TODO() + + /** * Creates an in-memory user database, * or returns an existing one if it already exists.