Skip to content

Commit

Permalink
feat(backup): support E2E runs on all clients
Browse files Browse the repository at this point in the history
  • Loading branch information
vitorhugods committed Jan 14, 2025
1 parent 6e56b39 commit ad906f4
Show file tree
Hide file tree
Showing 19 changed files with 54 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,9 @@ import com.wire.backup.data.BackupUser
import com.wire.backup.data.toProtoModel
import com.wire.backup.encryption.EncryptedStream
import com.wire.backup.encryption.XChaChaPoly1305AuthenticationData
import com.wire.backup.envelope.cryptography.BackupPassphrase
import com.wire.backup.envelope.header.BackupHeader
import com.wire.backup.envelope.header.BackupHeaderSerializer
import com.wire.backup.envelope.header.HashData
import com.wire.backup.envelope.BackupHeader
import com.wire.backup.envelope.BackupHeaderSerializer
import com.wire.backup.envelope.HashData
import com.wire.backup.filesystem.BackupEntry
import com.wire.backup.filesystem.EntryStorage
import com.wire.backup.ingest.MPBackupMapper
Expand Down Expand Up @@ -82,7 +81,7 @@ public abstract class CommonMPBackupExporter(
}

private fun flushUsers() {
if(usersChunk.isEmpty()) return
if (usersChunk.isEmpty()) return
val backupData = BackupData(backupInfo, users = usersChunk)
storage.persistEntry(BackupEntry(USERS_ENTRY_PREFIX + persistedUserChunks + ENTRY_SUFFIX, backupData.asSource()))
persistedUserChunks++
Expand All @@ -98,7 +97,7 @@ public abstract class CommonMPBackupExporter(
}

private fun flushConversations() {
if(conversationsChunk.isEmpty()) return
if (conversationsChunk.isEmpty()) return
val backupData = BackupData(backupInfo, conversations = conversationsChunk)
storage.persistEntry(BackupEntry(CONVERSATIONS_ENTRY_PREFIX + persistedConversationsChunks + ENTRY_SUFFIX, backupData.asSource()))
persistedConversationsChunks++
Expand All @@ -114,7 +113,7 @@ public abstract class CommonMPBackupExporter(
}

private fun flushMessages() {
if(messagesChunk.isEmpty()) return
if (messagesChunk.isEmpty()) return
val backupData = BackupData(backupInfo, messages = messagesChunk)
storage.persistEntry(BackupEntry(MESSAGES_ENTRY_PREFIX + persistedMessagesChunks + ENTRY_SUFFIX, backupData.asSource()))
persistedMessagesChunks++
Expand Down Expand Up @@ -146,7 +145,7 @@ public abstract class CommonMPBackupExporter(
output.buffer().use { bufferedOutput ->
bufferedOutput.write(headerBytes)
bufferedOutput.flush()
if (password == null) {
if (password.isNullOrBlank()) {
// We should skip the encryption headers, leaving empty/zeroed bytes
val skip = ByteArray(EncryptedStream.XCHACHA_20_POLY_1305_HEADER_LENGTH) { 0x00 }
bufferedOutput.write(skip)
Expand All @@ -156,7 +155,7 @@ public abstract class CommonMPBackupExporter(
zippedData,
bufferedOutput,
XChaChaPoly1305AuthenticationData(
BackupPassphrase(password),
password,
salt,
headerBytes.toUByteArray(),
header.hashData.operationsLimit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import com.ionspin.kotlin.crypto.secretstream.crypto_secretstream_xchacha20poly1
import com.ionspin.kotlin.crypto.secretstream.crypto_secretstream_xchacha20poly1305_TAG_FINAL
import com.ionspin.kotlin.crypto.secretstream.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE
import com.ionspin.kotlin.crypto.stream.crypto_stream_chacha20_KEYBYTES
import com.wire.backup.envelope.cryptography.BackupPassphrase
import com.wire.backup.hash.HASH_MEM_LIMIT
import com.wire.backup.hash.HASH_OPS_LIMIT
import okio.Buffer
Expand Down Expand Up @@ -151,7 +150,7 @@ internal interface EncryptedStream<AuthenticationData> {

private fun generateChaCha20Key(authData: XChaChaPoly1305AuthenticationData): UByteArray = PasswordHash.pwhash(
outputLength = KEY_LENGTH,
password = authData.passphrase.value,
password = authData.passphrase,
salt = authData.salt,
opsLimit = authData.hashOpsLimit,
memLimit = authData.hashMemLimit,
Expand Down Expand Up @@ -184,7 +183,7 @@ internal sealed interface DecryptionResult {
* will also fail. The idea is to do not trust any fruit from the poisoned tree.
*/
internal data class XChaChaPoly1305AuthenticationData(
val passphrase: BackupPassphrase,
val passphrase: String,
val salt: UByteArray,
val additionalData: UByteArray = ubyteArrayOf(),
val hashOpsLimit: ULong = HASH_OPS_LIMIT,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
* 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
Expand All @@ -17,7 +17,7 @@
*/
@file:OptIn(ExperimentalUnsignedTypes::class)

package com.wire.backup.envelope.header
package com.wire.backup.envelope

import com.ionspin.kotlin.crypto.pwhash.crypto_pwhash_MEMLIMIT_MIN
import com.wire.backup.data.BackupQualifiedId
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
* 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
Expand All @@ -15,7 +15,7 @@
* 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.envelope.header
package com.wire.backup.envelope

import okio.Buffer

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
* 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
Expand All @@ -15,7 +15,7 @@
* 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.envelope.header
package com.wire.backup.envelope

import okio.Buffer
import okio.Source
Expand Down

This file was deleted.

1 change: 0 additions & 1 deletion backup/src/commonMain/kotlin/com/wire/backup/hash/Hash.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
*/
package com.wire.backup.hash

import com.ionspin.kotlin.crypto.LibsodiumInitializer
import com.ionspin.kotlin.crypto.pwhash.PasswordHash
import com.ionspin.kotlin.crypto.pwhash.crypto_pwhash_ALG_DEFAULT
import com.wire.backup.data.BackupQualifiedId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ import kotlin.js.JsExport
@JsExport
public sealed class BackupAuthorValidationResult {
public data object Success : BackupAuthorValidationResult()
public data object AuthorDoesNotMatch: BackupAuthorValidationResult()
public data object AuthorDoesNotMatch : BackupAuthorValidationResult()
public data object UnknownFormat : BackupAuthorValidationResult()
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ package com.wire.backup.ingest
import com.wire.backup.data.BackupData
import com.wire.backup.encryption.EncryptedStream
import com.wire.backup.encryption.XChaChaPoly1305AuthenticationData
import com.wire.backup.envelope.cryptography.BackupPassphrase
import com.wire.backup.envelope.header.BackupHeaderSerializer
import com.wire.backup.envelope.header.HeaderParseResult
import com.wire.backup.envelope.BackupHeaderSerializer
import com.wire.backup.envelope.HeaderParseResult
import com.wire.backup.filesystem.EntryStorage
import okio.Buffer
import okio.Sink
Expand All @@ -37,31 +36,12 @@ import kotlin.js.JsExport
@JsExport
public abstract class CommonMPBackupImporter {

/**
* Peeks into the complete backup artifact, returning [BackupPeekResult] with basic information about the artifact.
*/
internal fun getBackupInfo(source: Source): BackupPeekResult = try {
val peekBuffer = source.buffer().peek()
when (val result = BackupHeaderSerializer.Default.parseHeader(peekBuffer)) {
HeaderParseResult.Failure.UnknownFormat -> BackupPeekResult.Failure.UnknownFormat
is HeaderParseResult.Failure.UnsupportedVersion -> BackupPeekResult.Failure.UnsupportedVersion(result.version.toString())
is HeaderParseResult.Success -> {
val header = result.header
BackupPeekResult.Success(header.version.toString(), header.isEncrypted)
}
}
} catch (e: Throwable) {
e.printStackTrace()
println(e)
BackupPeekResult.Failure.UnknownFormat
}

/**
* Decrypt (if needed) and unzip the backup artifact.
* The resulting [BackupImportResult.Success] contains a [BackupImportPager], that can be used to
* consume pages of backed up application data, like messages, users and conversations.
*/
internal suspend fun importBackup(source: Source, passphrase: BackupPassphrase?): BackupImportResult {
internal suspend fun importBackup(source: Source, passphrase: String?): BackupImportResult {
return when (val result = BackupHeaderSerializer.Default.parseHeader(source)) {
HeaderParseResult.Failure.UnknownFormat -> BackupImportResult.Failure.ParsingFailure
is HeaderParseResult.Failure.UnsupportedVersion -> BackupImportResult.Failure.ParsingFailure
Expand All @@ -74,7 +54,9 @@ public abstract class CommonMPBackupImporter {
} else {
if (isEncrypted && passphrase != null) {
EncryptedStream.decrypt(
source, sink, XChaChaPoly1305AuthenticationData(
source,
sink,
XChaChaPoly1305AuthenticationData(
passphrase,
header.hashData.salt,
BackupHeaderSerializer.Default.headerToBytes(header).toUByteArray(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,9 @@
package com.wire.backup.encryption

import com.ionspin.kotlin.crypto.pwhash.crypto_pwhash_SALTBYTES
import com.wire.backup.envelope.cryptography.BackupPassphrase
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import okio.Buffer
import okio.ByteString.Companion.encodeUtf8
import kotlin.random.Random
import kotlin.test.Ignore
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
Expand All @@ -46,7 +41,7 @@ class XChaChaPoly1305EncryptedStreamTest {
val originalBufferData = originalBuffer.peek().readByteArray()

val encryptedBuffer = Buffer()
val passphrase = BackupPassphrase("password")
val passphrase = "password"
val salt = XChaChaPoly1305AuthenticationData.newSalt()
val additionalData = "additionalData".encodeToByteArray().toUByteArray()
val authenticationData = XChaChaPoly1305AuthenticationData(passphrase, salt, additionalData)
Expand Down Expand Up @@ -108,7 +103,7 @@ class XChaChaPoly1305EncryptedStreamTest {
originalBuffer.writeUtf8("Hello Alice!")

val encryptedBuffer = Buffer()
val passphrase = BackupPassphrase("password")
val passphrase = "password"
val salt = XChaChaPoly1305AuthenticationData.newSalt()
val additionalData = "additionalData".encodeToByteArray().toUByteArray()
val authenticationData = XChaChaPoly1305AuthenticationData(passphrase, salt, additionalData)
Expand All @@ -119,8 +114,9 @@ class XChaChaPoly1305EncryptedStreamTest {
)

val result = stream.decrypt(
encryptedBuffer, Buffer(),
authenticationData.copy(
source = encryptedBuffer,
outputSink = Buffer(),
authenticationData = authenticationData.copy(
additionalData = "INCORRECT".encodeToByteArray().toUByteArray(),
),
)
Expand All @@ -133,7 +129,7 @@ class XChaChaPoly1305EncryptedStreamTest {
originalBuffer.writeUtf8("Hello Alice!")

val encryptedBuffer = Buffer()
val passphrase = BackupPassphrase("password")
val passphrase = "password"
val salt = XChaChaPoly1305AuthenticationData.newSalt()
val additionalData = "additionalData".encodeToByteArray().toUByteArray()
val authenticationData = XChaChaPoly1305AuthenticationData(passphrase, salt, additionalData)
Expand All @@ -148,8 +144,9 @@ class XChaChaPoly1305EncryptedStreamTest {
wrongSalt[i] = 42U
}
val result = stream.decrypt(
encryptedBuffer, Buffer(),
authenticationData.copy(
source = encryptedBuffer,
outputSink = Buffer(),
authenticationData = authenticationData.copy(
salt = wrongSalt,
),
)
Expand All @@ -162,7 +159,7 @@ class XChaChaPoly1305EncryptedStreamTest {
originalBuffer.writeUtf8("Hello Alice!")

val encryptedBuffer = Buffer()
val passphrase = BackupPassphrase("password")
val passphrase = "password"
val salt = XChaChaPoly1305AuthenticationData.newSalt()
val additionalData = "additionalData".encodeToByteArray().toUByteArray()
val authenticationData = XChaChaPoly1305AuthenticationData(passphrase, salt, additionalData)
Expand All @@ -173,9 +170,10 @@ class XChaChaPoly1305EncryptedStreamTest {
)

val result = stream.decrypt(
encryptedBuffer, Buffer(),
authenticationData.copy(
passphrase = BackupPassphrase("WRONG PASSWORD"),
source = encryptedBuffer,
outputSink = Buffer(),
authenticationData = authenticationData.copy(
passphrase = "WRONG PASSWORD",
),
)
assertEquals(DecryptionResult.Failure.AuthenticationFailure, result)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
*/
package com.wire.backup.envelope.header

import com.wire.backup.envelope.BackupHeaderSerializer
import com.wire.backup.envelope.HeaderParseResult
import com.wire.backup.util.testHeader
import okio.Buffer
import kotlin.test.Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
*/
package com.wire.backup.envelope.header

import com.wire.backup.envelope.header.HashData.Companion.HASHED_USER_ID_SIZE_IN_BYTES
import com.wire.backup.envelope.header.HashData.Companion.MINIMUM_MEMORY_LIMIT
import com.wire.backup.envelope.header.HashData.Companion.SALT_SIZE_IN_BYTES
import com.wire.backup.envelope.HashData
import com.wire.backup.envelope.HashData.Companion.HASHED_USER_ID_SIZE_IN_BYTES
import com.wire.backup.envelope.HashData.Companion.MINIMUM_MEMORY_LIMIT
import com.wire.backup.envelope.HashData.Companion.SALT_SIZE_IN_BYTES
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.assertFailsWith
Expand Down
10 changes: 5 additions & 5 deletions backup/src/commonTest/kotlin/com/wire/backup/util/TestData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
*/
package com.wire.backup.util

import com.wire.backup.envelope.header.BackupHeader
import com.wire.backup.envelope.header.HashData
import com.wire.backup.envelope.header.HashData.Companion.HASHED_USER_ID_SIZE_IN_BYTES
import com.wire.backup.envelope.header.HashData.Companion.MINIMUM_MEMORY_LIMIT
import com.wire.backup.envelope.header.HashData.Companion.SALT_SIZE_IN_BYTES
import com.wire.backup.envelope.BackupHeader
import com.wire.backup.envelope.HashData
import com.wire.backup.envelope.HashData.Companion.HASHED_USER_ID_SIZE_IN_BYTES
import com.wire.backup.envelope.HashData.Companion.MINIMUM_MEMORY_LIMIT
import com.wire.backup.envelope.HashData.Companion.SALT_SIZE_IN_BYTES

internal fun testHashData() = HashData(
hashedUserId = UByteArray(HASHED_USER_ID_SIZE_IN_BYTES) { 1U },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
package com.wire.backup.ingest

import com.wire.backup.dump.JSZip
import com.wire.backup.envelope.cryptography.BackupPassphrase
import com.wire.backup.filesystem.BackupEntry
import com.wire.backup.filesystem.EntryStorage
import com.wire.backup.filesystem.InMemoryEntryStorage
Expand All @@ -39,7 +38,7 @@ public actual class MPBackupImporter : CommonMPBackupImporter() {
public fun importFromFileData(data: ByteArray, passphrase: String?): Promise<BackupImportResult> = GlobalScope.promise {
val buffer = Buffer()
buffer.write(data)
importBackup(buffer, passphrase?.let { BackupPassphrase(it) })
importBackup(buffer, passphrase)
}

override fun getUnencryptedArchiveSink(): Sink = inMemoryUnencryptedBuffer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ public actual class MPBackupExporter(
private val fileSystem = FileSystem.SYSTEM

override val storage: EntryStorage = FileBasedEntryStorage(
fileSystem, workDirectoryPath, true
fileSystem = fileSystem,
workDirectory = workDirectoryPath,
shouldBeCleared = true
)

override val zipper: Zipper = object : Zipper {
Expand Down
Loading

0 comments on commit ad906f4

Please sign in to comment.