Add padding to the gzipped backup output.

This commit is contained in:
Greyson Parrelli
2024-04-19 12:09:11 -04:00
committed by Cody Henthorne
parent da43ff1e95
commit f34ae8d118
3 changed files with 86 additions and 9 deletions

View File

@@ -13,7 +13,6 @@ import org.whispersystems.signalservice.api.backup.BackupKey
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import java.io.IOException
import java.io.OutputStream
import java.util.zip.GZIPOutputStream
import javax.crypto.Cipher
import javax.crypto.CipherOutputStream
import javax.crypto.Mac
@@ -33,7 +32,7 @@ class EncryptedBackupWriter(
private val append: (ByteArray) -> Unit
) : BackupExportWriter {
private val mainStream: GZIPOutputStream
private val mainStream: PaddedGzipOutputStream
private val macStream: MacOutputStream
init {
@@ -48,13 +47,9 @@ class EncryptedBackupWriter(
}
macStream = MacOutputStream(outputStream, mac)
val cipherStream = CipherOutputStream(macStream, cipher)
mainStream = GZIPOutputStream(
CipherOutputStream(
macStream,
cipher
)
)
mainStream = PaddedGzipOutputStream(cipherStream)
}
override fun write(header: BackupInfo) {

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.stream
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
import java.io.FilterOutputStream
import java.io.OutputStream
import java.util.zip.GZIPOutputStream
/**
* GZIPs the content of the provided [outputStream], but also adds padding to the end of the stream using the same algorithm as [PaddingInputStream].
* We do this to fit files into a smaller number of size buckets to avoid fingerprinting. And it turns out that bolting on zeros to the end of a GZIP stream is
* fine, because GZIP is smart enough to ignore it. This means readers of this data don't have to do anything special.
*/
class PaddedGzipOutputStream private constructor(private val outputStream: SizeObservingOutputStream) : GZIPOutputStream(outputStream) {
constructor(outputStream: OutputStream) : this(SizeObservingOutputStream(outputStream))
override fun finish() {
super.finish()
val totalLength = outputStream.size
val paddedSize: Long = PaddingInputStream.getPaddedSize(totalLength)
val paddingToAdd: Int = (paddedSize - totalLength).toInt()
outputStream.write(ByteArray(paddingToAdd))
}
/**
* We need to know the size of the *compressed* stream to know how much padding to add at the end.
*/
private class SizeObservingOutputStream(val wrapped: OutputStream) : FilterOutputStream(wrapped) {
var size: Long = 0L
private set
override fun write(b: Int) {
wrapped.write(b)
size++
}
override fun write(b: ByteArray) {
wrapped.write(b)
size += b.size
}
override fun write(b: ByteArray, off: Int, len: Int) {
wrapped.write(b, off, len)
size += len
}
}
}

View File

@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.backup.v2.stream
import org.junit.Assert.assertEquals
import org.junit.Test
import org.signal.core.util.Hex
import org.thoughtcrime.securesms.backup.v2.proto.AccountData
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
import org.thoughtcrime.securesms.backup.v2.proto.Frame
@@ -27,13 +28,15 @@ class EncryptedBackupReaderWriterTest {
val frameCount = 10_000
EncryptedBackupWriter(key, aci, outputStream, append = { outputStream.write(it) }).use { writer ->
writer.write(BackupInfo(1, 1000L))
writer.write(BackupInfo(version = 1, backupTimeMs = 1000L))
for (i in 0 until frameCount) {
writer.write(Frame(account = AccountData(username = "username-$i")))
}
}
val ciphertext: ByteArray = outputStream.toByteArray()
println(ciphertext.size)
val frames: List<Frame> = EncryptedBackupReader(key, aci, ciphertext.size.toLong()) { ciphertext.inputStream() }.use { reader ->
assertEquals(reader.backupInfo?.version, 1L)
@@ -47,4 +50,28 @@ class EncryptedBackupReaderWriterTest {
assertEquals("username-$i", frames[i].account?.username)
}
}
@Test
fun `padding limits number of sizes`() {
val key = BackupKey(Util.getSecretBytes(32))
val aci = ACI.from(UUID.randomUUID())
val sizes = (1..10)
.map { frameCount ->
val outputStream = ByteArrayOutputStream()
EncryptedBackupWriter(key, aci, outputStream, append = { outputStream.write(it) }).use { writer ->
writer.write(BackupInfo(version = 1, backupTimeMs = 1000L))
for (i in 0 until frameCount) {
writer.write(Frame(account = AccountData(username = Hex.toStringCondensed(Util.getSecretBytes(32)))))
}
}
outputStream.toByteArray().size
}
.toSet()
assertEquals(1, sizes.size)
}
}