Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(backup): support file peeking [WPB-10575] #3261

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ package com.wire.backup.envelope
import com.ionspin.kotlin.crypto.pwhash.crypto_pwhash_MEMLIMIT_MIN
import com.wire.backup.data.BackupQualifiedId
import com.wire.backup.encryption.XChaChaPoly1305AuthenticationData
import com.wire.backup.envelope.HashData.Companion.HASHED_USER_ID_SIZE_IN_BYTES
import com.wire.backup.envelope.HashData.Companion.SALT_SIZE_IN_BYTES
import com.wire.backup.hash.HASH_MEM_LIMIT
import com.wire.backup.hash.HASH_OPS_LIMIT
import com.wire.backup.hash.hashUserId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,21 @@
*/
package com.wire.backup.ingest

import com.rickclephas.kmp.nativecoroutines.NativeCoroutines
import com.wire.backup.data.BackupQualifiedId
import com.wire.backup.envelope.HashData
import com.wire.backup.hash.hashUserId
import kotlin.js.JsExport

@JsExport
public sealed class BackupPeekResult {
public data class Success(
val version: String,
val isEncrypted: Boolean,
/**
* The provided data corresponds to a compatible backup artifact.
*/
public class Success internal constructor(
public val version: String,
public val isEncrypted: Boolean,
internal val hashData: HashData
/** TODO: Add more info about the backup */
) : BackupPeekResult()

Expand All @@ -32,3 +40,10 @@ public sealed class BackupPeekResult {
public data class UnsupportedVersion(val backupVersion: String) : Failure()
}
}

@NativeCoroutines
public suspend fun BackupPeekResult.Success.isCreatedBySameUser(userId: BackupQualifiedId): Boolean {
val candidateHash = hashUserId(userId, hashData.salt, hashData.hashingMemoryLimit, hashData.operationsLimit)
val actualHash = hashData.hashedUserId
return candidateHash.contentEquals(actualHash)
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@ public abstract class CommonMPBackupImporter internal constructor(
private val headerSerializer: BackupHeaderSerializer = BackupHeaderSerializer.Default
) {

/**
* Peeks into a backup artifact, returning information about it.
* @see BackupPeekResult
*/
internal fun peekBackup(
source: Source,
): BackupPeekResult {
val peekBuffer = source.buffer().peek()
return when (val result = headerSerializer.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, header.hashData)
}
}
}

/**
* Decrypt (if needed) and unzip the backup artifact.
* The resulting [BackupImportResult.Success] contains a [BackupImportPager], that can be used to
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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.backup.ingest

import com.wire.backup.data.BackupQualifiedId
import com.wire.backup.envelope.HashData
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue

class BackupPeekResultTest {

@Test
fun givenResultWasCreatedBySameUserId_whenComparingResult_thenShouldReturnTrue() = runTest {
val creatorUserId = BackupQualifiedId("123", "456")
val subject = BackupPeekResult.Success("42", false, HashData.defaultFromUserId(creatorUserId))
assertTrue { subject.isCreatedBySameUser(creatorUserId) }
}

@Test
fun givenResultWasCreatedByUserIdWithDifferentDomain_whenComparingResult_thenShouldReturnFalse() = runTest {
val creatorUserId = BackupQualifiedId("123", "456")
val otherUserId = BackupQualifiedId("123", "789")
val subject = BackupPeekResult.Success("42", false, HashData.defaultFromUserId(creatorUserId))
assertFalse { subject.isCreatedBySameUser(otherUserId) }
}

@Test
fun givenResultWasCreatedByDifferentUserId_whenComparingResult_thenShouldReturnFalse() = runTest {
val creatorUserId = BackupQualifiedId("123", "456")
val otherUserId = BackupQualifiedId("234", "456")
val subject = BackupPeekResult.Success("42", false, HashData.defaultFromUserId(creatorUserId))
assertFalse { subject.isCreatedBySameUser(otherUserId) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@
*/
package com.wire.backup.ingest

import com.wire.backup.data.BackupQualifiedId
import com.wire.backup.encryption.DecryptionResult
import com.wire.backup.encryption.EncryptedStream
import com.wire.backup.encryption.XChaChaPoly1305AuthenticationData
import com.wire.backup.envelope.BackupHeader
import com.wire.backup.envelope.BackupHeaderSerializer
import com.wire.backup.envelope.HashData
import com.wire.backup.envelope.HeaderParseResult
import com.wire.backup.envelope.header.FakeHeaderSerializer
import com.wire.backup.filesystem.EntryStorage
Expand Down Expand Up @@ -121,4 +124,52 @@ class MPBackupImporterTest {
assertIs<BackupImportResult.Failure.UnzippingError>(result)
assertEquals(throwable.message, result.message)
}

@Test
fun givenBackupHeaderBuffer_whenPeeking_thenCorrectDataIsReturned() = runTest {
val userId = BackupQualifiedId("user", "domain")
val header = BackupHeader(
version = BackupHeaderSerializer.Default.MAXIMUM_SUPPORTED_VERSION,
isEncrypted = true,
hashData = HashData.defaultFromUserId(userId)
)
val data = BackupHeaderSerializer.Default.headerToBytes(header)
val buffer = Buffer()
buffer.write(data)
val subject = createSubject()

val result = subject.peekBackup(buffer)
assertIs<BackupPeekResult.Success>(result)
assertEquals(header.version.toString(), result.version)
assertEquals(header.isEncrypted, result.isEncrypted)
}

@Test
fun givenBackupIsFromAnUnsupportedVersion_whenPeeking_thenCorrectDataIsReturned() = runTest {
val userId = BackupQualifiedId("user", "domain")
val header = BackupHeader(
version = BackupHeaderSerializer.Default.MINIMUM_SUPPORTED_VERSION - 1,
isEncrypted = true,
hashData = HashData.defaultFromUserId(userId)
)
val data = BackupHeaderSerializer.Default.headerToBytes(header)
val buffer = Buffer()
buffer.write(data)
val subject = createSubject()

val result = subject.peekBackup(buffer)
assertIs<BackupPeekResult.Failure.UnsupportedVersion>(result)
assertEquals(header.version.toString(), result.backupVersion)
}

@Test
fun givenDataIsNotFromValidBackup_whenPeeking_thenCorrectDataIsReturned() = runTest {
val data = byteArrayOf(0x00, 0x01, 0x02)
val buffer = Buffer()
buffer.write(data)
val subject = createSubject()

val result = subject.peekBackup(buffer)
assertIs<BackupPeekResult.Failure.UnknownFormat>(result)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ import kotlin.js.Promise
public actual class MPBackupImporter : CommonMPBackupImporter() {
private val inMemoryUnencryptedBuffer = Buffer()

public fun peekFileData(data: ByteArray): Promise<BackupPeekResult> = GlobalScope.promise {
val buffer = Buffer()
buffer.write(data)
peekBackup(buffer)
}

public fun importFromFileData(data: ByteArray, passphrase: String?): Promise<BackupImportResult> = GlobalScope.promise {
val buffer = Buffer()
buffer.write(data)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ public actual class MPBackupImporter(
}
}

/**
* Peeks into the specified backup file and retrieves metadata about it.
*
* @param pathToBackupFile the path to the backup file to be inspected
* @return a [BackupPeekResult] that contains information about the backup,
* such as version, encryption status, etc.
*/
@ObjCName("peek")
@NativeCoroutines
public suspend fun peekBackupFile(
pathToBackupFile: String
): BackupPeekResult = peekBackup(FileSystem.SYSTEM.source(pathToBackupFile.toPath()))

/**
* Imports a backup from the specified root path.
*
Expand Down
Loading