Do most of the proto and database groundwork for the new mediaName.

This commit is contained in:
Greyson Parrelli
2025-06-20 11:47:54 -04:00
committed by Cody Henthorne
parent e705495638
commit 38c8f852bf
431 changed files with 600 additions and 781 deletions

View File

@@ -7,10 +7,8 @@
package org.whispersystems.signalservice.api;
import org.signal.core.util.StreamUtil;
import org.signal.core.util.stream.LimitedInputStream;
import org.signal.libsignal.protocol.InvalidMessageException;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.api.attachment.AttachmentDownloadResult;
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil;
@@ -27,7 +25,6 @@ import org.whispersystems.signalservice.internal.util.Util;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.ZonedDateTime;
@@ -68,7 +65,7 @@ public class SignalServiceMessageReceiver {
*/
public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, long maxSizeBytes)
throws IOException, InvalidMessageException, MissingConfigurationException {
return retrieveAttachment(pointer, destination, maxSizeBytes, null).getDataStream();
return retrieveAttachment(pointer, destination, maxSizeBytes, null);
}
public InputStream retrieveProfileAvatar(String path, File destination, ProfileKey profileKey, long maxSizeBytes)
@@ -99,9 +96,9 @@ public class SignalServiceMessageReceiver {
* @throws IOException
* @throws InvalidMessageException
*/
public AttachmentDownloadResult retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, long maxSizeBytes, ProgressListener listener)
public InputStream retrieveAttachment(SignalServiceAttachmentPointer pointer, File destination, long maxSizeBytes, ProgressListener listener)
throws IOException, InvalidMessageException, MissingConfigurationException {
if (!pointer.getDigest().isPresent()) throw new InvalidMessageException("No attachment digest!");
if (pointer.getDigest().isEmpty()) throw new InvalidMessageException("No attachment digest!");
if (pointer.getKey() == null) throw new InvalidMessageException("No key!");
socket.retrieveAttachment(pointer.getCdnNumber(), Collections.emptyMap(), pointer.getRemoteId(), destination, maxSizeBytes, listener);
@@ -111,16 +108,13 @@ public class SignalServiceMessageReceiver {
StreamUtil.readFully(tempStream, iv);
}
return new AttachmentDownloadResult(
AttachmentCipherInputStream.createForAttachment(
destination,
pointer.getSize().orElse(0),
pointer.getKey(),
pointer.getDigest().get(),
null,
0
),
iv
return AttachmentCipherInputStream.createForAttachment(
destination,
pointer.getSize().orElse(0),
pointer.getKey(),
pointer.getDigest().get(),
null,
0
);
}
@@ -136,13 +130,13 @@ public class SignalServiceMessageReceiver {
*
* @return An InputStream that streams the plaintext attachment contents.
*/
public AttachmentDownloadResult retrieveArchivedAttachment(@Nonnull MediaRootBackupKey.MediaKeyMaterial archivedMediaKeyMaterial,
@Nonnull Map<String, String> readCredentialHeaders,
@Nonnull File archiveDestination,
@Nonnull SignalServiceAttachmentPointer pointer,
@Nonnull File attachmentDestination,
long maxSizeBytes,
@Nullable ProgressListener listener)
public InputStream retrieveArchivedAttachment(@Nonnull MediaRootBackupKey.MediaKeyMaterial archivedMediaKeyMaterial,
@Nonnull Map<String, String> readCredentialHeaders,
@Nonnull File archiveDestination,
@Nonnull SignalServiceAttachmentPointer pointer,
@Nonnull File attachmentDestination,
long maxSizeBytes,
@Nullable ProgressListener listener)
throws IOException, InvalidMessageException, MissingConfigurationException
{
if (pointer.getDigest().isEmpty()) {
@@ -160,29 +154,16 @@ public class SignalServiceMessageReceiver {
.map(s -> AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(s)))
.orElse(0L);
// There's two layers of encryption -- one from the backup, and one from the attachment. This only strips the outermost backup encryption layer.
try (InputStream backupDecrypted = AttachmentCipherInputStream.createForArchivedMediaOuterLayer(archivedMediaKeyMaterial, archiveDestination, originalCipherLength)) {
try (FileOutputStream fos = new FileOutputStream(attachmentDestination)) {
// TODO [backup] I don't think we should be doing the full copy here. This is basically doing the entire download inline in this single line.
StreamUtil.copy(backupDecrypted, fos);
}
}
byte[] iv = new byte[16];
try (InputStream tempStream = new FileInputStream(attachmentDestination)) {
StreamUtil.readFully(tempStream, iv);
}
LimitedInputStream dataStream = AttachmentCipherInputStream.createForAttachment(
return AttachmentCipherInputStream.createForArchivedMedia(
archivedMediaKeyMaterial,
attachmentDestination,
originalCipherLength,
pointer.getSize().orElse(0),
pointer.getKey(),
pointer.getDigest().get(),
null,
0
);
return new AttachmentDownloadResult(dataStream, iv);
}
/**
@@ -197,13 +178,13 @@ public class SignalServiceMessageReceiver {
*
* @return An InputStream that streams the plaintext attachment contents.
*/
public AttachmentDownloadResult retrieveArchivedThumbnail(@Nonnull MediaRootBackupKey.MediaKeyMaterial archivedMediaKeyMaterial,
@Nonnull Map<String, String> readCredentialHeaders,
@Nonnull File archiveDestination,
@Nonnull SignalServiceAttachmentPointer pointer,
@Nonnull File attachmentDestination,
long maxSizeBytes,
@Nullable ProgressListener listener)
public InputStream retrieveArchivedThumbnail(@Nonnull MediaRootBackupKey.MediaKeyMaterial archivedMediaKeyMaterial,
@Nonnull Map<String, String> readCredentialHeaders,
@Nonnull File archiveDestination,
@Nonnull SignalServiceAttachmentPointer pointer,
@Nonnull File attachmentDestination,
long maxSizeBytes,
@Nullable ProgressListener listener)
throws IOException, InvalidMessageException, MissingConfigurationException
{
if (pointer.getKey() == null) {
@@ -217,26 +198,13 @@ public class SignalServiceMessageReceiver {
.map(s -> AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(s)))
.orElse(0L);
// There's two layers of encryption -- one from the backup, and one from the attachment. This only strips the outermost backup encryption layer.
try (InputStream backupDecrypted = AttachmentCipherInputStream.createForArchivedMediaOuterLayer(archivedMediaKeyMaterial, archiveDestination, originalCipherLength)) {
try (FileOutputStream fos = new FileOutputStream(attachmentDestination)) {
// TODO [backup] I don't think we should be doing the full copy here. This is basically doing the entire download inline in this single line.
StreamUtil.copy(backupDecrypted, fos);
}
}
byte[] iv = new byte[16];
try (InputStream tempStream = new FileInputStream(attachmentDestination)) {
StreamUtil.readFully(tempStream, iv);
}
LimitedInputStream dataStream = AttachmentCipherInputStream.createForArchiveThumbnailInnerLayer(
return AttachmentCipherInputStream.createForArchivedThumbnail(
archivedMediaKeyMaterial,
attachmentDestination,
originalCipherLength,
pointer.getSize().orElse(0),
pointer.getKey()
);
return new AttachmentDownloadResult(dataStream, iv);
}
public void retrieveBackup(int cdnNumber, Map<String, String> headers, String cdnPath, File destination, ProgressListener listener) throws MissingConfigurationException, IOException {

View File

@@ -91,7 +91,6 @@ class AttachmentApi(
remoteId = SignalServiceAttachmentRemoteId.V4(attachmentData.resumableUploadSpec.cdnKey),
cdnNumber = attachmentData.resumableUploadSpec.cdnNumber,
key = resumableUploadSpec.attachmentKey,
iv = resumableUploadSpec.attachmentIv,
digest = digestInfo.digest,
incrementalDigest = digestInfo.incrementalDigest,
incrementalDigestChunkSize = digestInfo.incrementalMacChunkSize,

View File

@@ -14,7 +14,6 @@ class AttachmentUploadResult(
val remoteId: SignalServiceAttachmentRemoteId,
val cdnNumber: Int,
val key: ByteArray,
val iv: ByteArray,
val digest: ByteArray,
val incrementalDigest: ByteArray?,
val incrementalDigestChunkSize: Int,

View File

@@ -14,8 +14,8 @@ import org.signal.core.util.Hex
value class MediaName(val name: String) {
companion object {
fun fromDigest(digest: ByteArray) = MediaName(Hex.toStringCondensed(digest))
fun fromDigestForThumbnail(digest: ByteArray) = MediaName("${Hex.toStringCondensed(digest)}_thumbnail")
fun fromPlaintextHashAndRemoteKey(plaintextHash: ByteArray, remoteKey: ByteArray) = MediaName(Hex.toStringCondensed(plaintextHash + remoteKey))
fun fromPlaintextHashAndRemoteKeyForThumbnail(plaintextHash: ByteArray, remoteKey: ByteArray) = MediaName(Hex.toStringCondensed(plaintextHash + remoteKey) + "_thumbnail")
fun forThumbnailFromMediaName(mediaName: String) = MediaName("${mediaName}_thumbnail")
/**

View File

@@ -96,56 +96,6 @@ object AttachmentCipherInputStream {
)
}
/**
* After removing the server layer of encryption using [createForArchivedMediaOuterLayer], use this to decrypt the inner layer of the attachment.
* Thumbnails have a special path because we don't do any additional digest/hash validation on them.
*/
@JvmStatic
@Throws(InvalidMessageException::class, IOException::class)
fun createForArchiveThumbnailInnerLayer(
file: File,
plaintextLength: Long,
combinedKeyMaterial: ByteArray
): LimitedInputStream {
return create(
streamSupplier = { FileInputStream(file) },
streamLength = file.length(),
plaintextLength = plaintextLength,
combinedKeyMaterial = combinedKeyMaterial,
digest = null,
incrementalDigest = null,
incrementalMacChunkSize = 0,
ignoreDigest = true
)
}
/**
* When you archive an attachment, you give the server an encrypted attachment, and the server wraps it in *another* layer of encryption.
* This will return a stream that unwraps the server's layer of encryption, giving you a stream that contains a "normally-encrypted" attachment.
*
* Because we're validating the encryptedDigest/plaintextHash of the inner layer, there's no additional out-of-band validation of this outer layer.
*/
@JvmStatic
@Throws(InvalidMessageException::class, IOException::class)
fun createForArchivedMediaOuterLayer(archivedMediaKeyMaterial: MediaKeyMaterial, file: File, originalCipherTextLength: Long): LimitedInputStream {
val mac = initMac(archivedMediaKeyMaterial.macKey)
if (file.length() <= BLOCK_SIZE + mac.macLength) {
throw InvalidMessageException("Message shorter than crypto overhead!")
}
FileInputStream(file).use { macVerificationStream ->
verifyMac(macVerificationStream, file.length(), mac, null)
}
val encryptedStream = FileInputStream(file)
val encryptedStreamExcludingMac = LimitedInputStream(encryptedStream, file.length() - mac.macLength)
val cipher = createCipher(encryptedStreamExcludingMac, archivedMediaKeyMaterial.aesKey)
val inputStream: InputStream = BetterCipherInputStream(encryptedStreamExcludingMac, cipher)
return LimitedInputStream(inputStream, originalCipherTextLength)
}
/**
* When you archive an attachment, you give the server an encrypted attachment, and the server wraps it in *another* layer of encryption.
*
@@ -156,7 +106,7 @@ object AttachmentCipherInputStream {
*/
@JvmStatic
@Throws(InvalidMessageException::class, IOException::class)
fun createForArchivedMediaOuterAndInnerLayers(
fun createForArchivedMedia(
archivedMediaKeyMaterial: MediaKeyMaterial,
file: File,
originalCipherTextLength: Long,
@@ -185,6 +135,42 @@ object AttachmentCipherInputStream {
)
}
/**
* When you archive an attachment, you give the server an encrypted attachment, and the server wraps it in *another* layer of encryption.
*
* This creates a stream decrypt both the inner and outer layers of an archived attachment at the same time by basically double-decrypting it.
*
* @param incrementalDigest If null, incremental mac validation is disabled.
* @param incrementalMacChunkSize If 0, incremental mac validation is disabled.
*/
@JvmStatic
@Throws(InvalidMessageException::class, IOException::class)
fun createForArchivedThumbnail(
archivedMediaKeyMaterial: MediaKeyMaterial,
file: File,
originalCipherTextLength: Long,
plaintextLength: Long,
combinedKeyMaterial: ByteArray
): LimitedInputStream {
val keyMaterial = CombinedKeyMaterial.from(combinedKeyMaterial)
val mac = initMac(keyMaterial.macKey)
if (originalCipherTextLength <= BLOCK_SIZE + mac.macLength) {
throw InvalidMessageException("Message shorter than crypto overhead!")
}
return create(
streamSupplier = { createForArchivedMediaOuterLayer(archivedMediaKeyMaterial, file, originalCipherTextLength) },
streamLength = originalCipherTextLength,
plaintextLength = plaintextLength,
combinedKeyMaterial = combinedKeyMaterial,
digest = null,
incrementalDigest = null,
incrementalMacChunkSize = 0,
ignoreDigest = true
)
}
/**
* Creates a stream to decrypt sticker data. Stickers have a special path because the key material is derived from the pack key.
*/
@@ -209,6 +195,33 @@ object AttachmentCipherInputStream {
return BetterCipherInputStream(encryptedStreamExcludingMac, cipher)
}
/**
* When you archive an attachment, you give the server an encrypted attachment, and the server wraps it in *another* layer of encryption.
* This will return a stream that unwraps the server's layer of encryption, giving you a stream that contains a "normally-encrypted" attachment.
*
* Because we're validating the encryptedDigest/plaintextHash of the inner layer, there's no additional out-of-band validation of this outer layer.
*/
@JvmStatic
@Throws(InvalidMessageException::class, IOException::class)
private fun createForArchivedMediaOuterLayer(archivedMediaKeyMaterial: MediaKeyMaterial, file: File, originalCipherTextLength: Long): LimitedInputStream {
val mac = initMac(archivedMediaKeyMaterial.macKey)
if (file.length() <= BLOCK_SIZE + mac.macLength) {
throw InvalidMessageException("Message shorter than crypto overhead!")
}
FileInputStream(file).use { macVerificationStream ->
verifyMac(macVerificationStream, file.length(), mac, null)
}
val encryptedStream = FileInputStream(file)
val encryptedStreamExcludingMac = LimitedInputStream(encryptedStream, file.length() - mac.macLength)
val cipher = createCipher(encryptedStreamExcludingMac, archivedMediaKeyMaterial.aesKey)
val inputStream: InputStream = BetterCipherInputStream(encryptedStreamExcludingMac, cipher)
return LimitedInputStream(inputStream, originalCipherTextLength)
}
@JvmStatic
@Throws(InvalidMessageException::class, IOException::class)
private fun create(

View File

@@ -9,7 +9,6 @@ import org.junit.Assert
import org.junit.Test
import org.signal.core.util.StreamUtil
import org.signal.core.util.allMatch
import org.signal.core.util.copyTo
import org.signal.core.util.readFully
import org.signal.core.util.stream.LimitedInputStream
import org.signal.libsignal.protocol.InvalidMessageException
@@ -224,89 +223,72 @@ class AttachmentCipherTest {
}
@Test
fun attachment_encryptDecryptPaddedContent() {
val lengths = intArrayOf(531, 600, 724, 1019, 1024)
for (length in lengths) {
val plaintextInput = ByteArray(length)
for (i in 0..<length) {
plaintextInput[i] = 0x97.toByte()
}
val key = Util.getSecretBytes(64)
val iv = Util.getSecretBytes(16)
val inputStream = ByteArrayInputStream(plaintextInput)
val paddedInputStream = PaddingInputStream(inputStream, length.toLong())
val destinationOutputStream = ByteArrayOutputStream()
val encryptingOutputStream = AttachmentCipherOutputStreamFactory(key, iv).createFor(destinationOutputStream)
paddedInputStream.copyTo(encryptingOutputStream)
val encryptedData = destinationOutputStream.toByteArray()
val digest = encryptingOutputStream.transmittedDigest
val cipherFile = writeToFile(encryptedData)
val decryptedStream: InputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, length.toLong(), key, digest, null, 0)
val plaintextOutput = readInputStreamFully(decryptedStream)
assertThat(plaintextOutput).isEqualTo(plaintextInput)
cipherFile.delete()
}
}
@Test
fun archive_encryptDecrypt() {
val key = Util.getSecretBytes(64)
val keyMaterial = createMediaKeyMaterial(key)
val plaintextInput = "Peter Parker".toByteArray()
val encryptResult = encryptData(plaintextInput, key, false)
val cipherFile = writeToFile(encryptResult.ciphertext)
val inputStream = AttachmentCipherInputStream.createForArchivedMediaOuterLayer(keyMaterial, cipherFile, plaintextInput.size.toLong())
val plaintextOutput = readInputStreamFully(inputStream)
assertThat(plaintextOutput).isEqualTo(plaintextInput)
cipherFile.delete()
}
@Test
fun archive_encryptDecryptEmpty() {
val key = Util.getSecretBytes(64)
val keyMaterial = createMediaKeyMaterial(key)
fun archiveInnerAndOuterLayer_encryptDecryptEmpty() {
val innerKey = Util.getSecretBytes(64)
val plaintextInput = "".toByteArray()
val encryptResult = encryptData(plaintextInput, key, false)
val cipherFile = writeToFile(encryptResult.ciphertext)
val innerEncryptResult = encryptData(plaintextInput, innerKey, withIncremental = false)
val outerKey = Util.getSecretBytes(64)
val inputStream: InputStream = AttachmentCipherInputStream.createForArchivedMediaOuterLayer(keyMaterial, cipherFile, plaintextInput.size.toLong())
val plaintextOutput = readInputStreamFully(inputStream)
val outerEncryptResult = encryptData(innerEncryptResult.ciphertext, outerKey, false)
val cipherFile = writeToFile(outerEncryptResult.ciphertext)
val keyMaterial = createMediaKeyMaterial(outerKey)
val decryptedStream: LimitedInputStream = AttachmentCipherInputStream.createForArchivedMedia(
archivedMediaKeyMaterial = keyMaterial,
file = cipherFile,
originalCipherTextLength = innerEncryptResult.ciphertext.size.toLong(),
plaintextLength = plaintextInput.size.toLong(),
combinedKeyMaterial = innerKey,
digest = innerEncryptResult.digest,
incrementalDigest = innerEncryptResult.incrementalDigest,
incrementalMacChunkSize = innerEncryptResult.chunkSizeChoice
)
val plaintextOutput = decryptedStream.readFully(autoClose = false)
assertThat(plaintextOutput).isEqualTo(plaintextInput)
assertThat(decryptedStream.leftoverStream().allMatch { it == 0.toByte() }).isTrue()
cipherFile.delete()
}
@Test
fun archive_decryptFailOnBadKey() {
fun archiveInnerAndOuterLayer_decryptFailOnBadKey_nonIncremental() {
archiveInnerAndOuterLayer_decryptFailOnBadKey(incremental = false)
}
@Test
fun archiveInnerAndOuterLayer_decryptFailOnBadKey_incremental() {
archiveInnerAndOuterLayer_decryptFailOnBadKey(incremental = true)
}
private fun archiveInnerAndOuterLayer_decryptFailOnBadKey(incremental: Boolean) {
var cipherFile: File? = null
var hitCorrectException = false
try {
val key = Util.getSecretBytes(64)
val badKey = Util.getSecretBytes(64)
val keyMaterial = createMediaKeyMaterial(badKey)
val innerKey = Util.getSecretBytes(64)
val badInnerKey = Util.getSecretBytes(64)
val plaintextInput = "Gwen Stacy".toByteArray()
val encryptResult = encryptData(plaintextInput, key, false)
cipherFile = writeToFile(encryptResult.ciphertext)
val innerEncryptResult = encryptData(plaintextInput, innerKey, incremental)
val outerKey = Util.getSecretBytes(64)
AttachmentCipherInputStream.createForArchivedMediaOuterLayer(keyMaterial, cipherFile, plaintextInput.size.toLong())
val outerEncryptResult = encryptData(innerEncryptResult.ciphertext, outerKey, false)
val cipherFile = writeToFile(outerEncryptResult.ciphertext)
val keyMaterial = createMediaKeyMaterial(badInnerKey)
AttachmentCipherInputStream.createForArchivedMedia(
archivedMediaKeyMaterial = keyMaterial,
file = cipherFile,
originalCipherTextLength = innerEncryptResult.ciphertext.size.toLong(),
plaintextLength = plaintextInput.size.toLong(),
combinedKeyMaterial = innerKey,
digest = innerEncryptResult.digest,
incrementalDigest = innerEncryptResult.incrementalDigest,
incrementalMacChunkSize = innerEncryptResult.chunkSizeChoice
)
} catch (e: InvalidMessageException) {
hitCorrectException = true
} finally {
@@ -316,41 +298,6 @@ class AttachmentCipherTest {
assertThat(hitCorrectException).isTrue()
}
@Test
fun archive_encryptDecryptPaddedContent() {
val lengths = intArrayOf(531, 600, 724, 1019, 1024)
for (length in lengths) {
val plaintextInput = ByteArray(length)
for (i in 0..<length) {
plaintextInput[i] = 0x97.toByte()
}
val key = Util.getSecretBytes(64)
val iv = Util.getSecretBytes(16)
val inputStream = ByteArrayInputStream(plaintextInput)
val paddedInputStream = PaddingInputStream(inputStream, length.toLong())
val destinationOutputStream = ByteArrayOutputStream()
val encryptingOutputStream = AttachmentCipherOutputStreamFactory(key, iv).createFor(destinationOutputStream)
paddedInputStream.copyTo(encryptingOutputStream)
val encryptedData = destinationOutputStream.toByteArray()
val cipherFile = writeToFile(encryptedData)
val keyMaterial = createMediaKeyMaterial(key)
val decryptedStream: InputStream = AttachmentCipherInputStream.createForArchivedMediaOuterLayer(keyMaterial, cipherFile, length.toLong())
val plaintextOutput = readInputStreamFully(decryptedStream)
Assert.assertArrayEquals(plaintextInput, plaintextOutput)
cipherFile.delete()
}
}
@Test
fun archiveInnerAndOuter_encryptDecrypt_nonIncremental() {
archiveInnerAndOuter_encryptDecrypt(incremental = false, fileSize = MEBIBYTE)
@@ -387,7 +334,7 @@ class AttachmentCipherTest {
val cipherFile = writeToFile(outerEncryptResult.ciphertext)
val keyMaterial = createMediaKeyMaterial(outerKey)
val decryptedStream: LimitedInputStream = AttachmentCipherInputStream.createForArchivedMediaOuterAndInnerLayers(
val decryptedStream: LimitedInputStream = AttachmentCipherInputStream.createForArchivedMedia(
archivedMediaKeyMaterial = keyMaterial,
file = cipherFile,
originalCipherTextLength = innerEncryptResult.ciphertext.size.toLong(),
@@ -406,22 +353,38 @@ class AttachmentCipherTest {
}
@Test
fun archive_decryptFailOnBadMac() {
fun archiveEncryptDecrypt_decryptFailOnBadMac() {
var cipherFile: File? = null
var hitCorrectException = false
try {
val key = Util.getSecretBytes(64)
val innerKey = Util.getSecretBytes(64)
val badInnerKey = Util.getSecretBytes(64)
val plaintextInput = Util.getSecretBytes(MEBIBYTE)
val encryptResult = encryptData(plaintextInput, key, true)
val badMacCiphertext = encryptResult.ciphertext.copyOf(encryptResult.ciphertext.size)
badMacCiphertext[badMacCiphertext.size - 1] = (badMacCiphertext[badMacCiphertext.size - 1] + 1).toByte()
val innerEncryptResult = encryptData(plaintextInput, innerKey, withIncremental = true)
val outerKey = Util.getSecretBytes(64)
cipherFile = writeToFile(badMacCiphertext)
val outerEncryptResult = encryptData(innerEncryptResult.ciphertext, outerKey, false)
val badMacOuterCiphertext = outerEncryptResult.ciphertext.copyOf(outerEncryptResult.ciphertext.size)
badMacOuterCiphertext[badMacOuterCiphertext.size - 1] = (badMacOuterCiphertext[badMacOuterCiphertext.size - 1] + 1).toByte()
cipherFile = writeToFile(badMacOuterCiphertext)
val keyMaterial = createMediaKeyMaterial(badInnerKey)
AttachmentCipherInputStream.createForArchivedMedia(
archivedMediaKeyMaterial = keyMaterial,
file = cipherFile,
originalCipherTextLength = innerEncryptResult.ciphertext.size.toLong(),
plaintextLength = plaintextInput.size.toLong(),
combinedKeyMaterial = innerKey,
digest = innerEncryptResult.digest,
incrementalDigest = innerEncryptResult.incrementalDigest,
incrementalMacChunkSize = innerEncryptResult.chunkSizeChoice
)
val keyMaterial = createMediaKeyMaterial(key)
AttachmentCipherInputStream.createForArchivedMediaOuterLayer(keyMaterial, cipherFile, plaintextInput.size.toLong())
Assert.fail()
} catch (e: InvalidMessageException) {
hitCorrectException = true