mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-26 11:51:10 +01:00
Do most of the proto and database groundwork for the new mediaName.
This commit is contained in:
committed by
Cody Henthorne
parent
e705495638
commit
38c8f852bf
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user