From 0690300b5f86902159609f91b25a4d119a226408 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Schwaab Date: Thu, 21 Nov 2024 01:45:32 +0100 Subject: [PATCH 1/3] chore: create backup module This commit introduces a new cross-platform backup module to the project with support for iOS, Web, and Android. Additionally, it enables JavaScript multiplatform support in existing `kalium/network-model` and `kalium/data` projects. --- backup/README.md | 57 ++++++++++++++++ backup/build.gradle.kts | 115 +++++++++++++++++++++++++++++++++ data/build.gradle.kts | 7 +- network-model/build.gradle.kts | 2 +- 4 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 backup/README.md create mode 100644 backup/build.gradle.kts diff --git a/backup/README.md b/backup/README.md new file mode 100644 index 00000000000..ecc16d0105c --- /dev/null +++ b/backup/README.md @@ -0,0 +1,57 @@ +# Cross-platform Backup + +This module implements the Wire Cross-platform Backup solution. +Its purpose is to create a common implementation to be used by iOS, Web, and Android. + +## Capabilities + +> [!TIP] +> The backup blob/file will be referred in this document as **backup artifact**, or simply +> **artifact**. +> The clients (iOS, Web, and Android) will be referred as **callers**. + +### Creation of backup artifact + +The library should be able to transform data like messages, conversations, users into an artifact, +_i.e._ create a backup file. + +### Restoration of original data + +It should also be able to revert this process, _i.e._ read a backup artifact and understand all of +its contents, allowing the caller to get access to the original data. + +### Optional Encryption + +The artifact may _(or may not)_ be Encrypted. Clients using this library can provide an optional +passphrase to encrypt and decrypt the artifact. + +### Optional UserID verification + +When creating an artifact, callers need to provide a qualified user ID. This qualified user ID will +be stored within the artifact. +When restoring the original data, the library _can_ compare the user ID in the artifact with a user +ID provided by the caller. This may not be wanted in all features, _e.g._ in case of chat history +sharing across different users. + +### Peak into artifact + +Clients can ask the library if a piece of data is an actual cross-platform artifact. +The library should respond yes/no, and provide more details in case of a positive answer: is it +encrypted? Was it created by the same user that is restoring the artifact? + +-------- + +# Development + +Using Kotlin Multiplatform, it should at least be available for iOS, JS (with TypeScript types), +Android, and JVM. + +### Building + +Before we can automate the deployment of this library, we can manually generate and send library +artifacts (not backup artifacts) using the following Gradle tasks: + +- iOS: `./gradlew :backup:assembleBackupDebugXCFramework` +- Web: `./gradlew :backup:jsBrowserDevelopmentLibraryDistribution` + +**Output:** the results will be in `backup/build` directory. iOS needs the whole `backup.xcframework` directory, Web/JS needs the whole directory that contains `package.json` diff --git a/backup/build.gradle.kts b/backup/build.gradle.kts new file mode 100644 index 00000000000..72b8b96de01 --- /dev/null +++ b/backup/build.gradle.kts @@ -0,0 +1,115 @@ +import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework + +/* + * 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/. + */ +plugins { + id(libs.plugins.android.library.get().pluginId) + id(libs.plugins.kotlin.multiplatform.get().pluginId) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.ksp) + id(libs.plugins.kalium.library.get().pluginId) +} + +kaliumLibrary { + multiplatform { enableJs.set(true) } +} + +@Suppress("UnusedPrivateProperty") +kotlin { + val xcf = XCFramework() + val appleTargets = listOf(iosX64(), iosArm64(), iosSimulatorArm64(), macosArm64(), macosX64()) + appleTargets.forEach { + it.binaries.framework { + baseName = "backup" + xcf.add(this) + } + } + js { + browser() + binaries.library() + generateTypeScriptDefinitions() + } + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":data")) + implementation(project(":protobuf")) + implementation(libs.pbandk.runtime.common) + + implementation(libs.coroutines.core) + implementation(libs.ktxDateTime) + implementation(libs.ktxSerialization) + + implementation(libs.okio.core) + + // Libsodium + implementation(libs.libsodiumBindingsMP) + } + } + val commonTest by getting { + dependencies { + implementation(libs.coroutines.test) + implementation(libs.okio.test) + } + } + + val nonJsMain by creating { + dependsOn(commonMain) + } + val nonJsTest by creating { + dependsOn(commonTest) + } + val androidMain by getting { + dependsOn(nonJsMain) + } + val jvmMain by getting { + dependsOn(nonJsMain) + } + + val iosX64Main by getting { + dependsOn(nonJsMain) + dependencies { + implementation(libs.pbandk.runtime.iosX64) + } + } + val iosArm64Main by getting { + dependsOn(nonJsMain) + dependencies { + implementation(libs.pbandk.runtime.iosArm64) + } + } + val iosSimulatorArm64Main by getting { + dependsOn(nonJsMain) + dependencies { + implementation(libs.pbandk.runtime.iosSimulatorArm64) + } + } + val macosX64Main by getting { + dependsOn(nonJsMain) + dependencies { + implementation(libs.pbandk.runtime.macX64) + } + } + val macosArm64Main by getting { + dependsOn(nonJsMain) + dependencies { + implementation(libs.pbandk.runtime.macArm64) + } + } + } +} diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 11e58a18b0c..e16faaae75d 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -24,13 +24,12 @@ plugins { } kaliumLibrary { - multiplatform { - enableJs.set(false) - } + multiplatform { } } + +@Suppress("UnusedPrivateProperty") kotlin { sourceSets { - @Suppress("UnusedPrivateProperty") val commonMain by getting { dependencies { implementation(project(":network-model")) diff --git a/network-model/build.gradle.kts b/network-model/build.gradle.kts index 4f2eb1c7497..ae1abd96807 100644 --- a/network-model/build.gradle.kts +++ b/network-model/build.gradle.kts @@ -25,7 +25,7 @@ plugins { kaliumLibrary { multiplatform { - enableJs.set(false) + enableJs.set(true) } } From b824fb5642308bea6ea26407841945d995a6828e Mon Sep 17 00:00:00 2001 From: Vitor Hugo Schwaab Date: Fri, 22 Nov 2024 01:54:33 +0100 Subject: [PATCH 2/3] feat: add initial xplatform backup serialization This commit introduces a new multiplatform backup feature including export and import capabilities. It adds data classes, serialization, tests, and Gradle dependencies to support cross-platform backup data handling, including proper ProtoBuf serialization. --- backup/build.gradle.kts | 18 ++- .../kotlin/com/wire/backup/MPBackup.kt | 25 ++++ .../kotlin/com/wire/backup/data/BackupData.kt | 105 ++++++++++++++++ .../com/wire/backup/data/BackupMetadata.kt | 29 +++++ .../kotlin/com/wire/backup/data/Mapping.kt | 23 ++++ .../com/wire/backup/dump/MPBackupExporter.kt | 113 ++++++++++++++++++ .../wire/backup/ingest/BackupImportResult.kt | 27 +++++ .../wire/backup/ingest/MPBackupImporter.kt | 54 +++++++++ .../com/wire/backup/ingest/MPBackupMapper.kt | 68 +++++++++++ .../com/wire/backup/BackupEndToEndTest.kt | 60 ++++++++++ .../com/wire/backup/data/BackupDateTime.kt | 31 +++++ .../com/wire/backup/dump/MPBackupExporter.kt | 24 ++++ .../wire/backup/ingest/MPBackupImporter.kt | 21 ++++ .../com/wire/backup/data/BackupDateTime.kt | 30 +++++ .../com/wire/backup/dump/MPBackupExporter.kt | 22 ++++ .../wire/backup/ingest/MPBackupImporter.kt | 40 +++++++ gradle/libs.versions.toml | 2 +- protobuf-codegen/src/main/proto/backup.proto | 68 +++++++++++ 18 files changed, 753 insertions(+), 7 deletions(-) create mode 100644 backup/src/commonMain/kotlin/com/wire/backup/MPBackup.kt create mode 100644 backup/src/commonMain/kotlin/com/wire/backup/data/BackupData.kt create mode 100644 backup/src/commonMain/kotlin/com/wire/backup/data/BackupMetadata.kt create mode 100644 backup/src/commonMain/kotlin/com/wire/backup/data/Mapping.kt create mode 100644 backup/src/commonMain/kotlin/com/wire/backup/dump/MPBackupExporter.kt create mode 100644 backup/src/commonMain/kotlin/com/wire/backup/ingest/BackupImportResult.kt create mode 100644 backup/src/commonMain/kotlin/com/wire/backup/ingest/MPBackupImporter.kt create mode 100644 backup/src/commonMain/kotlin/com/wire/backup/ingest/MPBackupMapper.kt create mode 100644 backup/src/commonTest/kotlin/com/wire/backup/BackupEndToEndTest.kt create mode 100644 backup/src/jsMain/kotlin/com/wire/backup/data/BackupDateTime.kt create mode 100644 backup/src/jsMain/kotlin/com/wire/backup/dump/MPBackupExporter.kt create mode 100644 backup/src/jsMain/kotlin/com/wire/backup/ingest/MPBackupImporter.kt create mode 100644 backup/src/nonJsMain/kotlin/com/wire/backup/data/BackupDateTime.kt create mode 100644 backup/src/nonJsMain/kotlin/com/wire/backup/dump/MPBackupExporter.kt create mode 100644 backup/src/nonJsMain/kotlin/com/wire/backup/ingest/MPBackupImporter.kt create mode 100644 protobuf-codegen/src/main/proto/backup.proto diff --git a/backup/build.gradle.kts b/backup/build.gradle.kts index 72b8b96de01..b3a04f69097 100644 --- a/backup/build.gradle.kts +++ b/backup/build.gradle.kts @@ -77,36 +77,42 @@ kotlin { val androidMain by getting { dependsOn(nonJsMain) } + val androidInstrumentedTest by getting { + dependsOn(nonJsTest) + } val jvmMain by getting { dependsOn(nonJsMain) } - - val iosX64Main by getting { + val jvmTest by getting { + dependsOn(nonJsTest) + } + val appleMain by getting { dependsOn(nonJsMain) + } + val appleTest by getting { + dependsOn(nonJsTest) + } + val iosX64Main by getting { dependencies { implementation(libs.pbandk.runtime.iosX64) } } val iosArm64Main by getting { - dependsOn(nonJsMain) dependencies { implementation(libs.pbandk.runtime.iosArm64) } } val iosSimulatorArm64Main by getting { - dependsOn(nonJsMain) dependencies { implementation(libs.pbandk.runtime.iosSimulatorArm64) } } val macosX64Main by getting { - dependsOn(nonJsMain) dependencies { implementation(libs.pbandk.runtime.macX64) } } val macosArm64Main by getting { - dependsOn(nonJsMain) dependencies { implementation(libs.pbandk.runtime.macArm64) } diff --git a/backup/src/commonMain/kotlin/com/wire/backup/MPBackup.kt b/backup/src/commonMain/kotlin/com/wire/backup/MPBackup.kt new file mode 100644 index 00000000000..a739ae2b917 --- /dev/null +++ b/backup/src/commonMain/kotlin/com/wire/backup/MPBackup.kt @@ -0,0 +1,25 @@ +/* + * 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.backup + +import kotlin.js.JsExport + +@JsExport +object MPBackup { + const val ZIP_ENTRY_DATA = "data.wmbu" +} diff --git a/backup/src/commonMain/kotlin/com/wire/backup/data/BackupData.kt b/backup/src/commonMain/kotlin/com/wire/backup/data/BackupData.kt new file mode 100644 index 00000000000..2e1ce6585db --- /dev/null +++ b/backup/src/commonMain/kotlin/com/wire/backup/data/BackupData.kt @@ -0,0 +1,105 @@ +/* + * 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/. + */ +@file:OptIn(ExperimentalObjCRefinement::class, ExperimentalObjCName::class) + +package com.wire.backup.data + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlin.experimental.ExperimentalObjCName +import kotlin.experimental.ExperimentalObjCRefinement +import kotlin.js.JsExport +import kotlin.native.ObjCName +import kotlin.native.ShouldRefineInSwift + +@JsExport +class BackupData( + val metadata: BackupMetadata, + @ShouldRefineInSwift + val users: Array, + @ShouldRefineInSwift + val conversations: Array, + @ShouldRefineInSwift + val messages: Array +) { + @ObjCName("users") + val userList: List get() = users.toList() + + @ObjCName("conversations") + val conversationList: List get() = conversations.toList() + + @ObjCName("messages") + val messageList: List get() = messages.toList() +} + +@JsExport +@Serializable +data class BackupQualifiedId( + @SerialName("id") + val id: String, + @SerialName("domain") + val domain: String, +) { + override fun toString() = "$id@$domain" + + companion object { + private const val QUALIFIED_ID_COMPONENT_COUNT = 2 + + fun fromEncodedString(id: String): BackupQualifiedId? { + val components = id.split("@") + if (components.size != QUALIFIED_ID_COMPONENT_COUNT) return null + return BackupQualifiedId(components[0], components[1]) + } + } +} + +@JsExport +data class BackupUser( + val id: BackupQualifiedId, + val name: String, + val handle: String, +) + +@JsExport +data class BackupConversation( + val id: BackupQualifiedId, + val name: String, +) + +@JsExport +data class BackupMessage( + val id: String, + val conversationId: BackupQualifiedId, + val senderUserId: BackupQualifiedId, + val senderClientId: String, + val creationDate: BackupDateTime, + val content: BackupMessageContent +) + +expect class BackupDateTime + +expect fun BackupDateTime(timestampMillis: Long): BackupDateTime +expect fun BackupDateTime.toLongMilliseconds(): Long + +@JsExport +sealed class BackupMessageContent { + data class Text(val text: String) : BackupMessageContent() + + // TODO: Not _yet_ implemented + data class Asset(val todo: String) : BackupMessageContent() +} diff --git a/backup/src/commonMain/kotlin/com/wire/backup/data/BackupMetadata.kt b/backup/src/commonMain/kotlin/com/wire/backup/data/BackupMetadata.kt new file mode 100644 index 00000000000..bfd60e29c7e --- /dev/null +++ b/backup/src/commonMain/kotlin/com/wire/backup/data/BackupMetadata.kt @@ -0,0 +1,29 @@ +/* + * 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.backup.data + +import kotlin.js.JsExport + +@JsExport +data class BackupMetadata( + val version: String, + val userId: BackupQualifiedId, + val creationTime: BackupDateTime, + val clientId: String? +) diff --git a/backup/src/commonMain/kotlin/com/wire/backup/data/Mapping.kt b/backup/src/commonMain/kotlin/com/wire/backup/data/Mapping.kt new file mode 100644 index 00000000000..9bba048f2f6 --- /dev/null +++ b/backup/src/commonMain/kotlin/com/wire/backup/data/Mapping.kt @@ -0,0 +1,23 @@ +/* + * 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.backup.data + +import com.wire.kalium.protobuf.backup.ExportedQualifiedId + +internal fun BackupQualifiedId.toProtoModel() = ExportedQualifiedId(id, domain) +internal fun ExportedQualifiedId.toModel() = BackupQualifiedId(value, domain) diff --git a/backup/src/commonMain/kotlin/com/wire/backup/dump/MPBackupExporter.kt b/backup/src/commonMain/kotlin/com/wire/backup/dump/MPBackupExporter.kt new file mode 100644 index 00000000000..a64ba4ab910 --- /dev/null +++ b/backup/src/commonMain/kotlin/com/wire/backup/dump/MPBackupExporter.kt @@ -0,0 +1,113 @@ +/* + * 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.backup.dump + +import com.wire.backup.data.BackupConversation +import com.wire.backup.data.BackupMessage +import com.wire.backup.data.BackupMessageContent +import com.wire.backup.data.BackupQualifiedId +import com.wire.backup.data.BackupUser +import com.wire.backup.data.toLongMilliseconds +import com.wire.backup.data.toProtoModel +import com.wire.kalium.protobuf.backup.BackupData +import com.wire.kalium.protobuf.backup.BackupInfo +import com.wire.kalium.protobuf.backup.ExportUser +import com.wire.kalium.protobuf.backup.ExportedConversation +import com.wire.kalium.protobuf.backup.ExportedMessage +import com.wire.kalium.protobuf.backup.ExportedText +import kotlinx.datetime.Clock +import pbandk.encodeToByteArray +import kotlin.experimental.ExperimentalObjCName +import kotlin.experimental.ExperimentalObjCRefinement +import kotlin.js.JsExport +import kotlin.native.ObjCName +import kotlin.native.ShouldRefineInSwift + +/** + * Entity able to serialize [BackupData] entities, like [BackupMessage], [BackupConversation], [BackupUser] + * into a cross-platform [BackupData] format. + */ +@OptIn(ExperimentalObjCName::class, ExperimentalObjCRefinement::class) +@JsExport +abstract class CommonMPBackupExporter( + private val selfUserId: BackupQualifiedId +) { + private val allUsers = mutableListOf() + private val allConversations = mutableListOf() + private val allMessages = mutableListOf() + + // TODO: Replace `ObjCName` with `JsName` in the future and flip it around. + // Unfortunately the IDE doesn't understand this right now and + // keeps complaining if making the other way around + @ObjCName("add") + fun addUser(user: BackupUser) { + allUsers.add(user) + } + + @ObjCName("add") + fun addConversation(conversation: BackupConversation) { + allConversations.add(conversation) + } + + @ObjCName("add") + fun addMessage(message: BackupMessage) { + allMessages.add(message) + } + + @OptIn(ExperimentalStdlibApi::class) + @ShouldRefineInSwift // Hidden in Swift + fun serialize(): ByteArray { + val backupData = BackupData( + BackupInfo( + platform = "Common", + version = "1.0", + userId = selfUserId.toProtoModel(), + creationTime = Clock.System.now().toEpochMilliseconds(), + clientId = "lol" + ), + allConversations.map { ExportedConversation(it.id.toProtoModel(), it.name) }, + allMessages.map { + ExportedMessage( + id = it.id, + timeIso = it.creationDate.toLongMilliseconds(), + senderUserId = it.senderUserId.toProtoModel(), + senderClientId = it.senderClientId, + conversationId = it.conversationId.toProtoModel(), + content = when (val content = it.content) { + is BackupMessageContent.Asset -> + ExportedMessage.Content.Text(ExportedText("FAKE ASSET")) // TODO: Support assets + is BackupMessageContent.Text -> + ExportedMessage.Content.Text(ExportedText(content.text)) + } + ) + }, + allUsers.map { + ExportUser( + id = it.id.toProtoModel(), + name = it.name, + handle = it.handle + ) + }, + ) + return backupData.encodeToByteArray().also { + println("XPlatform Backup POC. Exported data bytes: ${it.toHexString()}") + } + } +} + +expect class MPBackupExporter : CommonMPBackupExporter diff --git a/backup/src/commonMain/kotlin/com/wire/backup/ingest/BackupImportResult.kt b/backup/src/commonMain/kotlin/com/wire/backup/ingest/BackupImportResult.kt new file mode 100644 index 00000000000..33b1f9c281e --- /dev/null +++ b/backup/src/commonMain/kotlin/com/wire/backup/ingest/BackupImportResult.kt @@ -0,0 +1,27 @@ +/* + * 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.backup.ingest + +import com.wire.backup.data.BackupData +import kotlin.js.JsExport + +@JsExport +sealed class BackupImportResult { + data object ParsingFailure : BackupImportResult() + data class Success(val backupData: BackupData) : BackupImportResult() +} diff --git a/backup/src/commonMain/kotlin/com/wire/backup/ingest/MPBackupImporter.kt b/backup/src/commonMain/kotlin/com/wire/backup/ingest/MPBackupImporter.kt new file mode 100644 index 00000000000..adca839eeb9 --- /dev/null +++ b/backup/src/commonMain/kotlin/com/wire/backup/ingest/MPBackupImporter.kt @@ -0,0 +1,54 @@ +/* + * 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.backup.ingest + +import com.wire.backup.data.BackupData +import com.wire.kalium.protobuf.backup.BackupData as ProtoBackupData +import pbandk.decodeFromByteArray +import kotlin.experimental.ExperimentalObjCRefinement +import kotlin.js.JsExport +import kotlin.native.ShouldRefineInSwift + +/** + * Entity able to parse backed-up data and returns + * digestible data in [BackupData] format. + */ +@OptIn(ExperimentalObjCRefinement::class) +@JsExport +abstract class CommonMPBackupImporter(selfUserDomain: String) { + private val mapper = MPBackupMapper(selfUserDomain) + + /** + * Attempts to deserialize backed-up data. + */ + @OptIn(ExperimentalStdlibApi::class) + @ShouldRefineInSwift // Function not visible in Swift + @Suppress("TooGenericExceptionCaught") + fun importBackup(data: ByteArray): BackupImportResult = try { + println("XPlatform Backup POC. Imported data bytes: ${data.toHexString()}") + BackupImportResult.Success( + mapper.fromProtoToBackupModel(ProtoBackupData.decodeFromByteArray(data)) + ) + } catch (e: Exception) { + e.printStackTrace() + println(e) + BackupImportResult.ParsingFailure + } +} + +expect class MPBackupImporter : CommonMPBackupImporter diff --git a/backup/src/commonMain/kotlin/com/wire/backup/ingest/MPBackupMapper.kt b/backup/src/commonMain/kotlin/com/wire/backup/ingest/MPBackupMapper.kt new file mode 100644 index 00000000000..8f5874b06a1 --- /dev/null +++ b/backup/src/commonMain/kotlin/com/wire/backup/ingest/MPBackupMapper.kt @@ -0,0 +1,68 @@ +/* + * 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.backup.ingest + +import com.wire.backup.data.BackupConversation +import com.wire.backup.data.BackupData +import com.wire.backup.data.BackupDateTime +import com.wire.backup.data.BackupMessage +import com.wire.backup.data.BackupMessageContent +import com.wire.backup.data.BackupMetadata +import com.wire.backup.data.BackupUser +import com.wire.backup.data.toModel +import com.wire.kalium.protobuf.backup.ExportedMessage.Content +import com.wire.kalium.protobuf.backup.BackupData as ProtoBackupData + +internal class MPBackupMapper(val selfUserDomain: String) { + + fun fromProtoToBackupModel( + protobufData: ProtoBackupData + ): BackupData = protobufData.run { + BackupData( + BackupMetadata( + info.version, + info.userId.toModel(), + BackupDateTime(info.creationTime), + info.clientId + ), + users.map { user -> + BackupUser(user.id.toModel(), user.name, user.handle) + }.toTypedArray(), + conversations.map { conversation -> + BackupConversation(conversation.id.toModel(), conversation.name) + }.toTypedArray(), + messages.map { message -> + val content = when (val proContent = message.content) { + is Content.Text -> { + BackupMessageContent.Text(proContent.value.content) + } + + null -> TODO() + } + BackupMessage( + id = message.id, + conversationId = message.conversationId.toModel(), + senderUserId = message.senderUserId.toModel(), + senderClientId = message.senderClientId, + creationDate = BackupDateTime(message.timeIso), + content = content + ) + }.toTypedArray() + ) + } +} diff --git a/backup/src/commonTest/kotlin/com/wire/backup/BackupEndToEndTest.kt b/backup/src/commonTest/kotlin/com/wire/backup/BackupEndToEndTest.kt new file mode 100644 index 00000000000..f3df4bac36b --- /dev/null +++ b/backup/src/commonTest/kotlin/com/wire/backup/BackupEndToEndTest.kt @@ -0,0 +1,60 @@ +/* + * 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.backup + +import com.wire.backup.data.BackupDateTime +import com.wire.backup.data.BackupMessage +import com.wire.backup.data.BackupMessageContent +import com.wire.backup.data.BackupQualifiedId +import com.wire.backup.dump.CommonMPBackupExporter +import com.wire.backup.ingest.BackupImportResult +import com.wire.backup.ingest.CommonMPBackupImporter +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertIs + +class BackupEndToEndTest { + + @Test + fun givenBackedUpMessages_whenRestoring_thenShouldReadTheSameContent() = runTest { + val expectedMessage = BackupMessage( + "messageId", + BackupQualifiedId("value", "domain"), + BackupQualifiedId("senderID", "senderDomain"), + "senderClientId", + BackupDateTime(24232L), + BackupMessageContent.Text("Hello from the backup!") + ) + val exporter = object : CommonMPBackupExporter(BackupQualifiedId("eghyue", "potato")) {} + exporter.addMessage(expectedMessage) + val encoded = exporter.serialize() + + val importer = object : CommonMPBackupImporter("potato") {} + val result = importer.importBackup(encoded) + assertIs(result) + assertContentEquals(arrayOf(expectedMessage), result.backupData.messages) + } + + @Test + fun givenBackUpDataIsUnrecognisable_whenRestoring_thenShouldReturnParsingError() = runTest { + val importer = object : CommonMPBackupImporter("potato") {} + val result = importer.importBackup(byteArrayOf(0x42, 0x42, 0x42)) + assertIs(result) + } +} diff --git a/backup/src/jsMain/kotlin/com/wire/backup/data/BackupDateTime.kt b/backup/src/jsMain/kotlin/com/wire/backup/data/BackupDateTime.kt new file mode 100644 index 00000000000..323651e415b --- /dev/null +++ b/backup/src/jsMain/kotlin/com/wire/backup/data/BackupDateTime.kt @@ -0,0 +1,31 @@ +/* + * 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.backup.data + +import kotlin.js.Date + +@JsExport +actual data class BackupDateTime(val date: Date) + +actual fun BackupDateTime(timestampMillis: Long): BackupDateTime { + return BackupDateTime(Date(timestampMillis)) +} + +actual fun BackupDateTime.toLongMilliseconds(): Long { + return date.getTime().toLong() +} diff --git a/backup/src/jsMain/kotlin/com/wire/backup/dump/MPBackupExporter.kt b/backup/src/jsMain/kotlin/com/wire/backup/dump/MPBackupExporter.kt new file mode 100644 index 00000000000..5630fea7f54 --- /dev/null +++ b/backup/src/jsMain/kotlin/com/wire/backup/dump/MPBackupExporter.kt @@ -0,0 +1,24 @@ +/* + * 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.backup.dump + +import com.wire.backup.data.BackupQualifiedId + +// JS uses the common one. Only handles Bytes / ByteArrays. +@JsExport +actual class MPBackupExporter(selfUserId: BackupQualifiedId) : CommonMPBackupExporter(selfUserId) diff --git a/backup/src/jsMain/kotlin/com/wire/backup/ingest/MPBackupImporter.kt b/backup/src/jsMain/kotlin/com/wire/backup/ingest/MPBackupImporter.kt new file mode 100644 index 00000000000..c69b32a08ad --- /dev/null +++ b/backup/src/jsMain/kotlin/com/wire/backup/ingest/MPBackupImporter.kt @@ -0,0 +1,21 @@ +/* + * 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.backup.ingest + +@JsExport +actual class MPBackupImporter(selfUserDomain: String) : CommonMPBackupImporter(selfUserDomain) diff --git a/backup/src/nonJsMain/kotlin/com/wire/backup/data/BackupDateTime.kt b/backup/src/nonJsMain/kotlin/com/wire/backup/data/BackupDateTime.kt new file mode 100644 index 00000000000..a8eb64377b8 --- /dev/null +++ b/backup/src/nonJsMain/kotlin/com/wire/backup/data/BackupDateTime.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.backup.data + +import kotlinx.datetime.Instant + +actual data class BackupDateTime(val instant: Instant) + +actual fun BackupDateTime(timestampMillis: Long): BackupDateTime { + return BackupDateTime(Instant.fromEpochMilliseconds(timestampMillis)) +} + +actual fun BackupDateTime.toLongMilliseconds(): Long { + return this.instant.toEpochMilliseconds() +} diff --git a/backup/src/nonJsMain/kotlin/com/wire/backup/dump/MPBackupExporter.kt b/backup/src/nonJsMain/kotlin/com/wire/backup/dump/MPBackupExporter.kt new file mode 100644 index 00000000000..da200efbd85 --- /dev/null +++ b/backup/src/nonJsMain/kotlin/com/wire/backup/dump/MPBackupExporter.kt @@ -0,0 +1,22 @@ +/* + * 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.backup.dump + +import com.wire.backup.data.BackupQualifiedId + +actual class MPBackupExporter(selfUserId: BackupQualifiedId) : CommonMPBackupExporter(selfUserId) diff --git a/backup/src/nonJsMain/kotlin/com/wire/backup/ingest/MPBackupImporter.kt b/backup/src/nonJsMain/kotlin/com/wire/backup/ingest/MPBackupImporter.kt new file mode 100644 index 00000000000..88184b50690 --- /dev/null +++ b/backup/src/nonJsMain/kotlin/com/wire/backup/ingest/MPBackupImporter.kt @@ -0,0 +1,40 @@ +/* + * 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.backup.ingest + +import okio.FileSystem +import okio.Path.Companion.toPath +import okio.SYSTEM +import kotlin.experimental.ExperimentalObjCName +import kotlin.native.ObjCName + +@OptIn(ExperimentalObjCName::class) +actual class MPBackupImporter(selfUserDomain: String) : CommonMPBackupImporter(selfUserDomain) { + + /** + * Imports a backup from the specified root path. + * + * @param multiplatformBackupFilePath the path to the decrypted, unzipped backup data file + */ + @ObjCName("importFile") + fun importFromFile(multiplatformBackupFilePath: String): BackupImportResult { + return FileSystem.SYSTEM.read(multiplatformBackupFilePath.toPath()) { + importBackup(readByteArray()) + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9a8e027eb86..b5ee841521b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,7 +35,7 @@ moduleGraph = "0.7.0" # and delete the workaround in the dev.mk file sqldelight = "2.0.1" sqlcipher-android = "4.6.1" -pbandk = "0.14.2" +pbandk = "0.15.0" turbine = "1.1.0" avs = "10.0.1" jna = "5.14.0" diff --git a/protobuf-codegen/src/main/proto/backup.proto b/protobuf-codegen/src/main/proto/backup.proto new file mode 100644 index 00000000000..71a16948bde --- /dev/null +++ b/protobuf-codegen/src/main/proto/backup.proto @@ -0,0 +1,68 @@ +/* + * Wire + * Copyright (C) 2021 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/. + * + */ + +syntax = "proto2"; + +option java_package = "com.wire.kalium.protobuf.backup"; + +message BackupData { + required BackupInfo info = 1; + repeated ExportedConversation conversations = 2; + repeated ExportedMessage messages = 3; + repeated ExportUser users = 4; +} + +message BackupInfo { + required string platform = 1; + required string version = 2; + required ExportedQualifiedId userId = 3; + required int64 creation_time = 4; + required string clientId = 5; +} + +message ExportUser { + required ExportedQualifiedId id = 1; + required string name = 2; + required string handle = 3; +} + +message ExportedQualifiedId { + required string value = 1; + required string domain = 2; +} + +message ExportedConversation { + required ExportedQualifiedId id = 1; + required string name = 2; +} + +message ExportedMessage { + required string id = 1; + required int64 time_iso = 2; + required ExportedQualifiedId sender_user_id = 3; + required string sender_client_id = 4; + required ExportedQualifiedId conversation_id = 5; + oneof content { + ExportedText text = 6; + } +} + +message ExportedText { + required string content = 1; +} From bc8ddebf2b46eb5fa85a50a8f761f2bddba71496 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Schwaab Date: Fri, 22 Nov 2024 15:10:12 +0100 Subject: [PATCH 3/3] fix: date comparison and exception handling on JS --- .../com/wire/backup/ingest/MPBackupImporter.kt | 2 +- .../kotlin/com/wire/backup/data/BackupDateTime.kt | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/backup/src/commonMain/kotlin/com/wire/backup/ingest/MPBackupImporter.kt b/backup/src/commonMain/kotlin/com/wire/backup/ingest/MPBackupImporter.kt index adca839eeb9..9b03f28e027 100644 --- a/backup/src/commonMain/kotlin/com/wire/backup/ingest/MPBackupImporter.kt +++ b/backup/src/commonMain/kotlin/com/wire/backup/ingest/MPBackupImporter.kt @@ -44,7 +44,7 @@ abstract class CommonMPBackupImporter(selfUserDomain: String) { BackupImportResult.Success( mapper.fromProtoToBackupModel(ProtoBackupData.decodeFromByteArray(data)) ) - } catch (e: Exception) { + } catch (e: Throwable) { e.printStackTrace() println(e) BackupImportResult.ParsingFailure diff --git a/backup/src/jsMain/kotlin/com/wire/backup/data/BackupDateTime.kt b/backup/src/jsMain/kotlin/com/wire/backup/data/BackupDateTime.kt index 323651e415b..b909e296846 100644 --- a/backup/src/jsMain/kotlin/com/wire/backup/data/BackupDateTime.kt +++ b/backup/src/jsMain/kotlin/com/wire/backup/data/BackupDateTime.kt @@ -20,7 +20,20 @@ package com.wire.backup.data import kotlin.js.Date @JsExport -actual data class BackupDateTime(val date: Date) +actual data class BackupDateTime(val date: Date) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class.js != other::class.js) return false + + other as BackupDateTime + + return this.toLongMilliseconds() == other.toLongMilliseconds() + } + + override fun hashCode(): Int { + return date.hashCode() + } +} actual fun BackupDateTime(timestampMillis: Long): BackupDateTime { return BackupDateTime(Date(timestampMillis))