diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupReader.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupReader.kt index 1c597850b6..7031bca71f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupReader.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupReader.kt @@ -41,18 +41,21 @@ class EncryptedBackupReader( val stream: InputStream init { - val keyMaterial = key.deriveSecrets(aci) - - val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply { - init(Cipher.DECRYPT_MODE, SecretKeySpec(keyMaterial.cipherKey, "AES"), IvParameterSpec(keyMaterial.iv)) - } + val keyMaterial = key.deriveBackupSecrets(aci) validateMac(keyMaterial.macKey, streamLength, dataStream()) + val inputStream = dataStream() + val iv = inputStream.readNBytesOrThrow(16) + + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply { + init(Cipher.DECRYPT_MODE, SecretKeySpec(keyMaterial.cipherKey, "AES"), IvParameterSpec(iv)) + } + stream = GZIPInputStream( CipherInputStream( TruncatingInputStream( - wrapped = dataStream(), + wrapped = inputStream, maxBytes = streamLength - MAC_SIZE ), cipher diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupWriter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupWriter.kt index 97828c6f9e..fcbb64d904 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupWriter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupWriter.kt @@ -9,6 +9,7 @@ import org.signal.core.util.stream.MacOutputStream import org.signal.core.util.writeVarInt32 import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo import org.thoughtcrime.securesms.backup.v2.proto.Frame +import org.thoughtcrime.securesms.util.Util import org.whispersystems.signalservice.api.backup.BackupKey import org.whispersystems.signalservice.api.push.ServiceId.ACI import java.io.IOException @@ -36,14 +37,19 @@ class EncryptedBackupWriter( private val macStream: MacOutputStream init { - val keyMaterial = key.deriveSecrets(aci) + val keyMaterial = key.deriveBackupSecrets(aci) + + val iv: ByteArray = Util.getSecretBytes(16) + outputStream.write(iv) + outputStream.flush() val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding").apply { - init(Cipher.ENCRYPT_MODE, SecretKeySpec(keyMaterial.cipherKey, "AES"), IvParameterSpec(keyMaterial.iv)) + init(Cipher.ENCRYPT_MODE, SecretKeySpec(keyMaterial.cipherKey, "AES"), IvParameterSpec(iv)) } val mac = Mac.getInstance("HmacSHA256").apply { init(SecretKeySpec(keyMaterial.macKey, "HmacSHA256")) + update(iv) } macStream = MacOutputStream(outputStream, mac) diff --git a/app/src/test/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupReaderWriterTest.kt b/app/src/test/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupReaderWriterTest.kt index 7692ea5e39..f9849b3b61 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupReaderWriterTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/backup/v2/stream/EncryptedBackupReaderWriterTest.kt @@ -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.Base64 import org.signal.core.util.Hex import org.thoughtcrime.securesms.backup.v2.proto.AccountData import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo @@ -56,7 +57,7 @@ class EncryptedBackupReaderWriterTest { val key = BackupKey(Util.getSecretBytes(32)) val aci = ACI.from(UUID.randomUUID()) - val sizes = (1..10) + val uniqueSizes = (1..10) .map { frameCount -> val outputStream = ByteArrayOutputStream() @@ -72,6 +73,29 @@ class EncryptedBackupReaderWriterTest { } .toSet() - assertEquals(1, sizes.size) + assertEquals(1, uniqueSizes.size) + } + + @Test + fun `using a different IV every time`() { + val key = BackupKey(Util.getSecretBytes(32)) + val aci = ACI.from(UUID.randomUUID()) + val count = 10 + + val uniqueOutputs = (0 until count) + .map { + val outputStream = ByteArrayOutputStream() + + EncryptedBackupWriter(key, aci, outputStream, append = { outputStream.write(it) }).use { writer -> + writer.write(BackupInfo(version = 1, backupTimeMs = 1000L)) + writer.write(Frame(account = AccountData(username = "static-data"))) + } + + outputStream.toByteArray() + } + .map { Base64.encodeWithPadding(it) } + .toSet() + + assertEquals(count, uniqueOutputs.size) } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java index 4c37b55cb7..d004801ade 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java @@ -184,7 +184,7 @@ public class SignalServiceMessageReceiver { * * @return An InputStream that streams the plaintext attachment contents. */ - public InputStream retrieveArchivedAttachment(@Nonnull BackupKey.KeyMaterial archivedMediaKeyMaterial, + public InputStream retrieveArchivedAttachment(@Nonnull BackupKey.MediaKeyMaterial archivedMediaKeyMaterial, @Nonnull Map readCredentialHeaders, @Nonnull File archiveDestination, @Nonnull SignalServiceAttachmentPointer pointer, diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/BackupKey.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/BackupKey.kt index aed7e88af9..8c05a55eea 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/BackupKey.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/backup/BackupKey.kt @@ -22,16 +22,15 @@ class BackupKey(val value: ByteArray) { ) } - fun deriveSecrets(aci: ACI): KeyMaterial { + fun deriveBackupSecrets(aci: ACI): BackupKeyMaterial { val backupId = deriveBackupId(aci) val extendedKey = HKDF.deriveSecrets(this.value, backupId.value, "20231003_Signal_Backups_EncryptMessageBackup".toByteArray(), 80) - return KeyMaterial( + return BackupKeyMaterial( id = backupId, macKey = extendedKey.copyOfRange(0, 32), - cipherKey = extendedKey.copyOfRange(32, 64), - iv = extendedKey.copyOfRange(64, 80) + cipherKey = extendedKey.copyOfRange(32, 64) ) } @@ -39,14 +38,14 @@ class BackupKey(val value: ByteArray) { return MediaId(HKDF.deriveSecrets(value, mediaName.toByteArray(), "Media ID".toByteArray(), 15)) } - fun deriveMediaSecrets(mediaName: MediaName): KeyMaterial { + fun deriveMediaSecrets(mediaName: MediaName): MediaKeyMaterial { return deriveMediaSecrets(deriveMediaId(mediaName)) } - fun deriveMediaSecrets(mediaId: MediaId): KeyMaterial { + private fun deriveMediaSecrets(mediaId: MediaId): MediaKeyMaterial { val extendedKey = HKDF.deriveSecrets(this.value, mediaId.value, "20231003_Signal_Backups_EncryptMedia".toByteArray(), 80) - return KeyMaterial( + return MediaKeyMaterial( id = mediaId, macKey = extendedKey.copyOfRange(0, 32), cipherKey = extendedKey.copyOfRange(32, 64), @@ -54,16 +53,22 @@ class BackupKey(val value: ByteArray) { ) } - class KeyMaterial ( - val id: Id, + class BackupKeyMaterial( + val id: BackupId, + val macKey: ByteArray, + val cipherKey: ByteArray + ) + + class MediaKeyMaterial( + val id: MediaId, val macKey: ByteArray, val cipherKey: ByteArray, val iv: ByteArray ) { companion object { @JvmStatic - fun forMedia(id: ByteArray, keyMac: ByteArray, iv: ByteArray): KeyMaterial { - return KeyMaterial( + fun forMedia(id: ByteArray, keyMac: ByteArray, iv: ByteArray): MediaKeyMaterial { + return MediaKeyMaterial( MediaId(id), keyMac.copyOfRange(32, 64), keyMac.copyOfRange(0, 32), diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherInputStream.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherInputStream.java index 66857a07aa..0ad0447092 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherInputStream.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherInputStream.java @@ -110,7 +110,7 @@ public class AttachmentCipherInputStream extends FilterInputStream { /** * Decrypt archived media to it's original attachment encrypted blob. */ - public static InputStream createForArchivedMedia(BackupKey.KeyMaterial archivedMediaKeyMaterial, File file, long originalCipherTextLength) + public static InputStream createForArchivedMedia(BackupKey.MediaKeyMaterial archivedMediaKeyMaterial, File file, long originalCipherTextLength) throws InvalidMessageException, IOException { try { diff --git a/libsignal-service/src/test/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherTest.java b/libsignal-service/src/test/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherTest.java index 9fa91212ee..4ad0ea38e3 100644 --- a/libsignal-service/src/test/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherTest.java +++ b/libsignal-service/src/test/java/org/whispersystems/signalservice/api/crypto/AttachmentCipherTest.java @@ -93,7 +93,7 @@ public final class AttachmentCipherTest { @Test public void archive_encryptDecrypt() throws IOException, InvalidMessageException { byte[] key = Util.getSecretBytes(64); - BackupKey.KeyMaterial keyMaterial = BackupKey.KeyMaterial.forMedia(Util.getSecretBytes(15), key, Util.getSecretBytes(16)); + BackupKey.MediaKeyMaterial keyMaterial = BackupKey.MediaKeyMaterial.forMedia(Util.getSecretBytes(15), key, Util.getSecretBytes(16)); byte[] plaintextInput = "Peter Parker".getBytes(); EncryptResult encryptResult = encryptData(plaintextInput, key, false); File cipherFile = writeToFile(encryptResult.ciphertext); @@ -108,7 +108,7 @@ public final class AttachmentCipherTest { @Test public void archive_encryptDecryptEmpty() throws IOException, InvalidMessageException { byte[] key = Util.getSecretBytes(64); - BackupKey.KeyMaterial keyMaterial = BackupKey.KeyMaterial.forMedia(Util.getSecretBytes(15), key, Util.getSecretBytes(16)); + BackupKey.MediaKeyMaterial keyMaterial = BackupKey.MediaKeyMaterial.forMedia(Util.getSecretBytes(15), key, Util.getSecretBytes(16)); byte[] plaintextInput = "".getBytes(); EncryptResult encryptResult = encryptData(plaintextInput, key, false); File cipherFile = writeToFile(encryptResult.ciphertext); @@ -128,7 +128,7 @@ public final class AttachmentCipherTest { try { byte[] key = Util.getSecretBytes(64); byte[] badKey = Util.getSecretBytes(64); - BackupKey.KeyMaterial keyMaterial = BackupKey.KeyMaterial.forMedia(Util.getSecretBytes(15), badKey, Util.getSecretBytes(16)); + BackupKey.MediaKeyMaterial keyMaterial = BackupKey.MediaKeyMaterial.forMedia(Util.getSecretBytes(15), badKey, Util.getSecretBytes(16)); byte[] plaintextInput = "Gwen Stacy".getBytes(); EncryptResult encryptResult = encryptData(plaintextInput, key, false); @@ -270,7 +270,7 @@ public final class AttachmentCipherTest { File cipherFile = writeToFile(encryptedData); - BackupKey.KeyMaterial keyMaterial = BackupKey.KeyMaterial.forMedia(Util.getSecretBytes(15), key, Util.getSecretBytes(16)); + BackupKey.MediaKeyMaterial keyMaterial = BackupKey.MediaKeyMaterial.forMedia(Util.getSecretBytes(15), key, Util.getSecretBytes(16)); InputStream decryptedStream = AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, length); byte[] plaintextOutput = readInputStreamFully(decryptedStream); @@ -348,7 +348,7 @@ public final class AttachmentCipherTest { cipherFile = writeToFile(badMacCiphertext); - BackupKey.KeyMaterial keyMaterial = BackupKey.KeyMaterial.forMedia(Util.getSecretBytes(15), key, Util.getSecretBytes(16)); + BackupKey.MediaKeyMaterial keyMaterial = BackupKey.MediaKeyMaterial.forMedia(Util.getSecretBytes(15), key, Util.getSecretBytes(16)); AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, plaintextInput.length); fail(); } catch (InvalidMessageException e) {