Skip to content

Commit

Permalink
feat: create obfuscated copy of local db
Browse files Browse the repository at this point in the history
  • Loading branch information
Garzas authored and MohamadJaara committed Jan 22, 2025
1 parent bbddfe5 commit 4a4e20d
Show file tree
Hide file tree
Showing 16 changed files with 540 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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()
)
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ internal interface ConversationFolderRepository {
suspend fun removeFolder(folderId: String): Either<CoreFailure, Unit>
suspend fun syncConversationFoldersFromLocal(): Either<CoreFailure, Unit>
suspend fun observeFolders(): Flow<Either<CoreFailure, List<ConversationFolder>>>
suspend fun addFolder(folder: ConversationFolder): Either<CoreFailure, Unit>
}

internal class ConversationFolderDataSource internal constructor(
Expand Down Expand Up @@ -165,4 +166,10 @@ internal class ConversationFolderDataSource internal constructor(
.wrapStorageRequest()
.mapRight { folderEntities -> folderEntities.map { it.toModel() } }
}

override suspend fun addFolder(folder: ConversationFolder): Either<CoreFailure, Unit> {
return wrapStorageRequest {
conversationFolderDAO.addFolder(folder.toDao())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
)
}
Original file line number Diff line number Diff line change
@@ -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<CoreFailure, Pair<Path, Long>> {
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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -394,4 +396,6 @@ class ConversationScope internal constructor(
get() = MoveConversationToFolderUseCaseImpl(conversationFolderRepository)
val removeConversationFromFolder: RemoveConversationFromFolderUseCase
get() = RemoveConversationFromFolderUseCaseImpl(conversationFolderRepository)
val createConversationFolder: CreateConversationFolderUseCase
get() = CreateConversationFolderUseCaseImpl(conversationFolderRepository)
}
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,8 @@ internal actual fun getDatabaseAbsoluteFileLocation(
null
}
}

internal actual fun createEmptyDatabaseFile(
platformDatabaseData: PlatformDatabaseData,
userId: UserIDEntity,
): String? = TODO()
Loading

0 comments on commit 4a4e20d

Please sign in to comment.