mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-15 07:28:30 +00:00
Convert AttachmentCipherTest to kotlin.
This commit is contained in:
@@ -127,8 +127,8 @@ fun InputStream.limit(limit: Long): LimitedInputStream {
|
||||
*
|
||||
* @param closeInputStream If true, the input stream will be closed after the copy is complete.
|
||||
*/
|
||||
fun InputStream.copyTo(outputStream: OutputStream, closeInputStream: Boolean = true): Long {
|
||||
return StreamUtil.copy(this, outputStream, closeInputStream)
|
||||
fun InputStream.copyTo(outputStream: OutputStream, closeInputStream: Boolean = true, closeOutputStream: Boolean = true): Long {
|
||||
return StreamUtil.copy(this, outputStream, closeInputStream, closeOutputStream)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -96,10 +96,10 @@ public final class StreamUtil {
|
||||
}
|
||||
|
||||
public static long copy(InputStream in, OutputStream out) throws IOException {
|
||||
return copy(in, out, true);
|
||||
return copy(in, out, true, true);
|
||||
}
|
||||
|
||||
public static long copy(InputStream in, OutputStream out, boolean closeInputStream) throws IOException {
|
||||
public static long copy(InputStream in, OutputStream out, boolean closeInputStream, boolean closeOutputStream) throws IOException {
|
||||
byte[] buffer = new byte[64 * 1024];
|
||||
int read;
|
||||
long total = 0;
|
||||
@@ -114,7 +114,10 @@ public final class StreamUtil {
|
||||
}
|
||||
|
||||
out.flush();
|
||||
out.close();
|
||||
|
||||
if (closeOutputStream) {
|
||||
out.close();
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
@@ -1,535 +0,0 @@
|
||||
package org.whispersystems.signalservice.api.crypto;
|
||||
|
||||
import org.conscrypt.Conscrypt;
|
||||
import org.junit.Test;
|
||||
import org.signal.core.util.StreamUtil;
|
||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||
import org.signal.libsignal.protocol.incrementalmac.ChunkSizeChoice;
|
||||
import org.signal.libsignal.protocol.incrementalmac.InvalidMacException;
|
||||
import org.signal.libsignal.protocol.kdf.HKDF;
|
||||
import org.whispersystems.signalservice.api.backup.MediaRootBackupKey;
|
||||
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream;
|
||||
import org.whispersystems.signalservice.internal.push.http.AttachmentCipherOutputStreamFactory;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.Security;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.whispersystems.signalservice.testutil.LibSignalLibraryUtil.assumeLibSignalSupportedOnOS;
|
||||
|
||||
public final class AttachmentCipherTest {
|
||||
|
||||
static {
|
||||
// https://github.com/google/conscrypt/issues/1034
|
||||
if (!System.getProperty("os.arch").equals("aarch64")) {
|
||||
Security.insertProviderAt(Conscrypt.newProvider(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static final int MEBIBYTE = 1024 * 1024;
|
||||
|
||||
@Test
|
||||
public void attachment_encryptDecrypt_nonIncremental() throws IOException, InvalidMessageException {
|
||||
attachment_encryptDecrypt(false, MEBIBYTE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void attachment_encryptDecrypt_incremental() throws IOException, InvalidMessageException {
|
||||
attachment_encryptDecrypt(true, MEBIBYTE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void attachment_encryptDecrypt_incremental_manyFileSizes() throws IOException, InvalidMessageException {
|
||||
// Designed to stress the various boundary conditions of reading the final mac
|
||||
for (int i = 0; i < 100; i++) {
|
||||
attachment_encryptDecrypt(true, MEBIBYTE + new Random().nextInt(1, 64 * 1024));
|
||||
}
|
||||
}
|
||||
|
||||
private void attachment_encryptDecrypt(boolean incremental, int fileSize) throws IOException, InvalidMessageException {
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] plaintextInput = Util.getSecretBytes(fileSize);
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, incremental);
|
||||
File cipherFile = writeToFile(encryptResult.ciphertext);
|
||||
InputStream inputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest, encryptResult.incrementalDigest, encryptResult.chunkSizeChoice);
|
||||
byte[] plaintextOutput = readInputStreamFully(inputStream);
|
||||
|
||||
assertArrayEquals(plaintextInput, plaintextOutput);
|
||||
|
||||
cipherFile.delete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void attachment_encryptDecryptEmpty_nonIncremental() throws IOException, InvalidMessageException {
|
||||
attachment_encryptDecryptEmpty(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void attachment_encryptDecryptEmpty_incremental() throws IOException, InvalidMessageException {
|
||||
attachment_encryptDecryptEmpty(true);
|
||||
}
|
||||
|
||||
private void attachment_encryptDecryptEmpty(boolean incremental) throws IOException, InvalidMessageException {
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] plaintextInput = "".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, incremental);
|
||||
File cipherFile = writeToFile(encryptResult.ciphertext);
|
||||
InputStream inputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest, encryptResult.incrementalDigest, encryptResult.chunkSizeChoice);
|
||||
byte[] plaintextOutput = readInputStreamFully(inputStream);
|
||||
|
||||
assertArrayEquals(plaintextInput, plaintextOutput);
|
||||
|
||||
cipherFile.delete();
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException.class)
|
||||
public void attachment_decryptFailOnBadKey_nonIncremental() throws IOException, InvalidMessageException {
|
||||
attachment_decryptFailOnBadKey(false);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException.class)
|
||||
public void attachment_decryptFailOnBadKey_incremental() throws IOException, InvalidMessageException {
|
||||
attachment_decryptFailOnBadKey(true);
|
||||
}
|
||||
|
||||
private void attachment_decryptFailOnBadKey(boolean incremental) throws IOException, InvalidMessageException {
|
||||
File cipherFile = null;
|
||||
|
||||
try {
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] plaintextInput = Util.getSecretBytes(MEBIBYTE);
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, incremental);
|
||||
byte[] badKey = new byte[64];
|
||||
|
||||
cipherFile = writeToFile(encryptResult.ciphertext);
|
||||
|
||||
AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, badKey, encryptResult.digest, null, 0);
|
||||
} finally {
|
||||
if (cipherFile != null) {
|
||||
cipherFile.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@Test(expected = InvalidMessageException.class)
|
||||
public void attachment_decryptFailOnBadMac_nonIncremental() throws IOException, InvalidMessageException {
|
||||
attachment_decryptFailOnBadMac(false);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException.class)
|
||||
public void attachment_decryptFailOnBadMac_incremental() throws IOException, InvalidMessageException {
|
||||
attachment_decryptFailOnBadMac(true);
|
||||
}
|
||||
|
||||
private void attachment_decryptFailOnBadMac(boolean incremental) throws IOException, InvalidMessageException {
|
||||
File cipherFile = null;
|
||||
|
||||
try {
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] plaintextInput = Util.getSecretBytes(MEBIBYTE);
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, incremental);
|
||||
byte[] badMacCiphertext = Arrays.copyOf(encryptResult.ciphertext, encryptResult.ciphertext.length);
|
||||
|
||||
badMacCiphertext[badMacCiphertext.length - 1] += 1;
|
||||
|
||||
cipherFile = writeToFile(badMacCiphertext);
|
||||
|
||||
InputStream stream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest, encryptResult.incrementalDigest, encryptResult.chunkSizeChoice);
|
||||
|
||||
// In incremental mode, we'll only check the digest after reading the whole thing
|
||||
if (incremental) {
|
||||
StreamUtil.readFully(stream);
|
||||
}
|
||||
} finally {
|
||||
if (cipherFile != null) {
|
||||
cipherFile.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException.class)
|
||||
public void attachment_decryptFailOnNullDigest_nonIncremental() throws IOException, InvalidMessageException {
|
||||
attachment_decryptFailOnNullDigest(false);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException.class)
|
||||
public void attachment_decryptFailOnNullDigest_incremental() throws IOException, InvalidMessageException {
|
||||
attachment_decryptFailOnNullDigest(true);
|
||||
}
|
||||
|
||||
private void attachment_decryptFailOnNullDigest(boolean incremental) throws IOException, InvalidMessageException {
|
||||
File cipherFile = null;
|
||||
|
||||
try {
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] plaintextInput = Util.getSecretBytes(MEBIBYTE);
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, incremental);
|
||||
|
||||
cipherFile = writeToFile(encryptResult.ciphertext);
|
||||
|
||||
AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, null, encryptResult.incrementalDigest, encryptResult.chunkSizeChoice);
|
||||
} finally {
|
||||
if (cipherFile != null) {
|
||||
cipherFile.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException.class)
|
||||
public void attachment_decryptFailOnBadDigest_nonIncremental() throws IOException, InvalidMessageException {
|
||||
attachment_decryptFailOnBadDigest(false);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException.class)
|
||||
public void attachment_decryptFailOnBadDigest_incremental() throws IOException, InvalidMessageException {
|
||||
attachment_decryptFailOnBadDigest(true);
|
||||
}
|
||||
|
||||
private void attachment_decryptFailOnBadDigest(boolean incremental) throws IOException, InvalidMessageException {
|
||||
File cipherFile = null;
|
||||
|
||||
try {
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] plaintextInput = Util.getSecretBytes(MEBIBYTE);
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, incremental);
|
||||
byte[] badDigest = new byte[32];
|
||||
|
||||
cipherFile = writeToFile(encryptResult.ciphertext);
|
||||
|
||||
InputStream stream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, badDigest, encryptResult.incrementalDigest, encryptResult.chunkSizeChoice);
|
||||
|
||||
// In incremental mode, we'll only check the digest after reading the whole thing
|
||||
if (incremental) {
|
||||
StreamUtil.readFully(stream);
|
||||
}
|
||||
} finally {
|
||||
if (cipherFile != null) {
|
||||
cipherFile.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void attachment_decryptFailOnBadIncrementalDigest() throws IOException {
|
||||
File cipherFile = null;
|
||||
boolean hitCorrectException = false;
|
||||
|
||||
try {
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] plaintextInput = Util.getSecretBytes(MEBIBYTE);
|
||||
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, true);
|
||||
byte[] badDigest = Util.getSecretBytes(encryptResult.incrementalDigest.length);
|
||||
|
||||
cipherFile = writeToFile(encryptResult.ciphertext);
|
||||
|
||||
|
||||
InputStream decryptedStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.length, key, encryptResult.digest, badDigest, encryptResult.chunkSizeChoice);
|
||||
byte[] plaintextOutput = readInputStreamFully(decryptedStream);
|
||||
fail();
|
||||
} catch (InvalidMacException e) {
|
||||
hitCorrectException = true;
|
||||
} catch (InvalidMessageException e) {
|
||||
hitCorrectException = false;
|
||||
} finally {
|
||||
if (cipherFile != null) {
|
||||
cipherFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(hitCorrectException);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void attachment_encryptDecryptPaddedContent() throws IOException, InvalidMessageException {
|
||||
int[] lengths = { 531, 600, 724, 1019, 1024 };
|
||||
|
||||
for (int length : lengths) {
|
||||
byte[] plaintextInput = new byte[length];
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
plaintextInput[i] = (byte) 0x97;
|
||||
}
|
||||
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] iv = Util.getSecretBytes(16);
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(plaintextInput);
|
||||
InputStream paddedInputStream = new PaddingInputStream(inputStream, length);
|
||||
ByteArrayOutputStream destinationOutputStream = new ByteArrayOutputStream();
|
||||
|
||||
DigestingOutputStream encryptingOutputStream = new AttachmentCipherOutputStreamFactory(key, iv).createFor(destinationOutputStream);
|
||||
|
||||
Util.copy(paddedInputStream, encryptingOutputStream);
|
||||
|
||||
encryptingOutputStream.flush();
|
||||
encryptingOutputStream.close();
|
||||
|
||||
byte[] encryptedData = destinationOutputStream.toByteArray();
|
||||
byte[] digest = encryptingOutputStream.getTransmittedDigest();
|
||||
|
||||
File cipherFile = writeToFile(encryptedData);
|
||||
|
||||
InputStream decryptedStream = AttachmentCipherInputStream.createForAttachment(cipherFile, length, key, digest, null, 0);
|
||||
byte[] plaintextOutput = readInputStreamFully(decryptedStream);
|
||||
|
||||
assertArrayEquals(plaintextInput, plaintextOutput);
|
||||
|
||||
cipherFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void archive_encryptDecrypt() throws IOException, InvalidMessageException {
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
MediaRootBackupKey.MediaKeyMaterial keyMaterial = AttachmentCipherTestHelper.createMediaKeyMaterial(key);
|
||||
byte[] plaintextInput = "Peter Parker".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, false);
|
||||
File cipherFile = writeToFile(encryptResult.ciphertext);
|
||||
InputStream inputStream = AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, plaintextInput.length);
|
||||
byte[] plaintextOutput = readInputStreamFully(inputStream);
|
||||
|
||||
assertArrayEquals(plaintextInput, plaintextOutput);
|
||||
|
||||
cipherFile.delete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void archive_encryptDecryptEmpty() throws IOException, InvalidMessageException {
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
MediaRootBackupKey.MediaKeyMaterial keyMaterial = AttachmentCipherTestHelper.createMediaKeyMaterial(key);
|
||||
byte[] plaintextInput = "".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, false);
|
||||
File cipherFile = writeToFile(encryptResult.ciphertext);
|
||||
InputStream inputStream = AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, plaintextInput.length);
|
||||
byte[] plaintextOutput = readInputStreamFully(inputStream);
|
||||
|
||||
assertArrayEquals(plaintextInput, plaintextOutput);
|
||||
|
||||
cipherFile.delete();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void archive_decryptFailOnBadKey() throws IOException {
|
||||
File cipherFile = null;
|
||||
boolean hitCorrectException = false;
|
||||
|
||||
try {
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] badKey = Util.getSecretBytes(64);
|
||||
MediaRootBackupKey.MediaKeyMaterial keyMaterial = AttachmentCipherTestHelper.createMediaKeyMaterial(badKey);
|
||||
byte[] plaintextInput = "Gwen Stacy".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, false);
|
||||
|
||||
cipherFile = writeToFile(encryptResult.ciphertext);
|
||||
|
||||
AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, plaintextInput.length);
|
||||
} catch (InvalidMessageException e) {
|
||||
hitCorrectException = true;
|
||||
} finally {
|
||||
if (cipherFile != null) {
|
||||
cipherFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(hitCorrectException);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void archive_encryptDecryptPaddedContent() throws IOException, InvalidMessageException {
|
||||
int[] lengths = { 531, 600, 724, 1019, 1024 };
|
||||
|
||||
for (int length : lengths) {
|
||||
byte[] plaintextInput = new byte[length];
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
plaintextInput[i] = (byte) 0x97;
|
||||
}
|
||||
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] iv = Util.getSecretBytes(16);
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(plaintextInput);
|
||||
InputStream paddedInputStream = new PaddingInputStream(inputStream, length);
|
||||
ByteArrayOutputStream destinationOutputStream = new ByteArrayOutputStream();
|
||||
|
||||
DigestingOutputStream encryptingOutputStream = new AttachmentCipherOutputStreamFactory(key, iv).createFor(destinationOutputStream);
|
||||
|
||||
Util.copy(paddedInputStream, encryptingOutputStream);
|
||||
|
||||
encryptingOutputStream.flush();
|
||||
encryptingOutputStream.close();
|
||||
|
||||
byte[] encryptedData = destinationOutputStream.toByteArray();
|
||||
|
||||
File cipherFile = writeToFile(encryptedData);
|
||||
|
||||
MediaRootBackupKey.MediaKeyMaterial keyMaterial = AttachmentCipherTestHelper.createMediaKeyMaterial(key);
|
||||
InputStream decryptedStream = AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, length);
|
||||
byte[] plaintextOutput = readInputStreamFully(decryptedStream);
|
||||
|
||||
assertArrayEquals(plaintextInput, plaintextOutput);
|
||||
|
||||
cipherFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void archive_decryptFailOnBadMac() throws IOException {
|
||||
File cipherFile = null;
|
||||
boolean hitCorrectException = false;
|
||||
|
||||
try {
|
||||
byte[] key = Util.getSecretBytes(64);
|
||||
byte[] plaintextInput = Util.getSecretBytes(MEBIBYTE);
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, key, true);
|
||||
byte[] badMacCiphertext = Arrays.copyOf(encryptResult.ciphertext, encryptResult.ciphertext.length);
|
||||
|
||||
badMacCiphertext[badMacCiphertext.length - 1] += 1;
|
||||
|
||||
cipherFile = writeToFile(badMacCiphertext);
|
||||
|
||||
MediaRootBackupKey.MediaKeyMaterial keyMaterial = AttachmentCipherTestHelper.createMediaKeyMaterial(key);
|
||||
AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, plaintextInput.length);
|
||||
fail();
|
||||
} catch (InvalidMessageException e) {
|
||||
hitCorrectException = true;
|
||||
} finally {
|
||||
if (cipherFile != null) {
|
||||
cipherFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(hitCorrectException);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sticker_encryptDecrypt() throws IOException, InvalidMessageException {
|
||||
assumeLibSignalSupportedOnOS();
|
||||
|
||||
byte[] packKey = Util.getSecretBytes(32);
|
||||
byte[] plaintextInput = Util.getSecretBytes(MEBIBYTE);
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey), true);
|
||||
InputStream inputStream = AttachmentCipherInputStream.createForStickerData(encryptResult.ciphertext, packKey);
|
||||
byte[] plaintextOutput = readInputStreamFully(inputStream);
|
||||
|
||||
assertArrayEquals(plaintextInput, plaintextOutput);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sticker_encryptDecryptEmpty() throws IOException, InvalidMessageException {
|
||||
assumeLibSignalSupportedOnOS();
|
||||
|
||||
byte[] packKey = Util.getSecretBytes(32);
|
||||
byte[] plaintextInput = "".getBytes();
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey), true);
|
||||
InputStream inputStream = AttachmentCipherInputStream.createForStickerData(encryptResult.ciphertext, packKey);
|
||||
byte[] plaintextOutput = readInputStreamFully(inputStream);
|
||||
|
||||
assertArrayEquals(plaintextInput, plaintextOutput);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sticker_decryptFailOnBadKey() throws IOException {
|
||||
assumeLibSignalSupportedOnOS();
|
||||
|
||||
boolean hitCorrectException = false;
|
||||
|
||||
try {
|
||||
byte[] packKey = Util.getSecretBytes(32);
|
||||
byte[] plaintextInput = Util.getSecretBytes(MEBIBYTE);
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey), true);
|
||||
byte[] badPackKey = new byte[32];
|
||||
|
||||
AttachmentCipherInputStream.createForStickerData(encryptResult.ciphertext, badPackKey);
|
||||
} catch (InvalidMessageException e) {
|
||||
hitCorrectException = true;
|
||||
}
|
||||
|
||||
assertTrue(hitCorrectException);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sticker_decryptFailOnBadMac() throws IOException {
|
||||
assumeLibSignalSupportedOnOS();
|
||||
|
||||
boolean hitCorrectException = false;
|
||||
|
||||
try {
|
||||
byte[] packKey = Util.getSecretBytes(32);
|
||||
byte[] plaintextInput = Util.getSecretBytes(MEBIBYTE);
|
||||
EncryptResult encryptResult = encryptData(plaintextInput, expandPackKey(packKey), true);
|
||||
byte[] badMacCiphertext = Arrays.copyOf(encryptResult.ciphertext, encryptResult.ciphertext.length);
|
||||
|
||||
badMacCiphertext[badMacCiphertext.length - 1] += 1;
|
||||
|
||||
AttachmentCipherInputStream.createForStickerData(badMacCiphertext, packKey);
|
||||
} catch (InvalidMessageException e) {
|
||||
hitCorrectException = true;
|
||||
}
|
||||
|
||||
assertTrue(hitCorrectException);
|
||||
}
|
||||
|
||||
private static EncryptResult encryptData(byte[] data, byte[] keyMaterial, boolean withIncremental) throws IOException {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
ByteArrayOutputStream incrementalDigestOut = new ByteArrayOutputStream();
|
||||
byte[] iv = Util.getSecretBytes(16);
|
||||
AttachmentCipherOutputStreamFactory factory = new AttachmentCipherOutputStreamFactory(keyMaterial, iv);
|
||||
|
||||
DigestingOutputStream encryptStream;
|
||||
final ChunkSizeChoice sizeChoice = ChunkSizeChoice.inferChunkSize(data.length);
|
||||
if (withIncremental) {
|
||||
encryptStream = factory.createIncrementalFor(outputStream, data.length, sizeChoice, incrementalDigestOut);
|
||||
} else {
|
||||
encryptStream = factory.createFor(outputStream);
|
||||
}
|
||||
|
||||
encryptStream.write(data);
|
||||
encryptStream.flush();
|
||||
encryptStream.close();
|
||||
incrementalDigestOut.close();
|
||||
|
||||
return new EncryptResult(outputStream.toByteArray(), encryptStream.getTransmittedDigest(), incrementalDigestOut.toByteArray(), sizeChoice.getSizeInBytes());
|
||||
}
|
||||
|
||||
private static File writeToFile(byte[] data) throws IOException {
|
||||
File file = File.createTempFile("temp", ".data");
|
||||
OutputStream outputStream = new FileOutputStream(file);
|
||||
|
||||
outputStream.write(data);
|
||||
outputStream.close();
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
private static byte[] readInputStreamFully(InputStream inputStream) throws IOException {
|
||||
return Util.readFullyAsBytes(inputStream);
|
||||
}
|
||||
|
||||
private static byte[] expandPackKey(byte[] shortKey) {
|
||||
return HKDF.deriveSecrets(shortKey, "Sticker Pack".getBytes(), 64);
|
||||
}
|
||||
|
||||
private static class EncryptResult {
|
||||
final byte[] ciphertext;
|
||||
final byte[] digest;
|
||||
final byte[] incrementalDigest;
|
||||
final int chunkSizeChoice;
|
||||
|
||||
private EncryptResult(byte[] ciphertext, byte[] digest, byte[] incrementalDigest, int chunkSizeChoice) {
|
||||
this.ciphertext = ciphertext;
|
||||
this.digest = digest;
|
||||
this.incrementalDigest = incrementalDigest;
|
||||
this.chunkSizeChoice = chunkSizeChoice;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
package org.whispersystems.signalservice.api.crypto
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isTrue
|
||||
import assertk.fail
|
||||
import org.conscrypt.Conscrypt
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.signal.core.util.StreamUtil
|
||||
import org.signal.core.util.copyTo
|
||||
import org.signal.libsignal.protocol.InvalidMessageException
|
||||
import org.signal.libsignal.protocol.incrementalmac.ChunkSizeChoice
|
||||
import org.signal.libsignal.protocol.incrementalmac.InvalidMacException
|
||||
import org.signal.libsignal.protocol.kdf.HKDF
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherTestHelper.createMediaKeyMaterial
|
||||
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
|
||||
import org.whispersystems.signalservice.internal.push.http.AttachmentCipherOutputStreamFactory
|
||||
import org.whispersystems.signalservice.internal.util.Util
|
||||
import org.whispersystems.signalservice.testutil.LibSignalLibraryUtil
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.lang.AssertionError
|
||||
import java.security.Security
|
||||
import java.util.Random
|
||||
|
||||
class AttachmentCipherTest {
|
||||
@Test
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
fun attachment_encryptDecrypt_nonIncremental() {
|
||||
attachment_encryptDecrypt(incremental = false, fileSize = MEBIBYTE)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
fun attachment_encryptDecrypt_incremental() {
|
||||
attachment_encryptDecrypt(incremental = true, fileSize = MEBIBYTE)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
fun attachment_encryptDecrypt_incremental_manyFileSizes() {
|
||||
// Designed to stress the various boundary conditions of reading the final mac
|
||||
for (i in 0..99) {
|
||||
attachment_encryptDecrypt(incremental = true, fileSize = MEBIBYTE + Random().nextInt(1, 64 * 1024))
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
private fun attachment_encryptDecrypt(incremental: Boolean, fileSize: Int) {
|
||||
val key = Util.getSecretBytes(64)
|
||||
val plaintextInput = Util.getSecretBytes(fileSize)
|
||||
|
||||
val encryptResult = encryptData(plaintextInput, key, incremental)
|
||||
val cipherFile = writeToFile(encryptResult.ciphertext)
|
||||
|
||||
val inputStream: InputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.size.toLong(), key, encryptResult.digest, encryptResult.incrementalDigest, encryptResult.chunkSizeChoice)
|
||||
val plaintextOutput = readInputStreamFully(inputStream)
|
||||
|
||||
assertThat(plaintextOutput).isEqualTo(plaintextInput)
|
||||
|
||||
cipherFile.delete()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
fun attachment_encryptDecryptEmpty_nonIncremental() {
|
||||
attachment_encryptDecryptEmpty(incremental = false)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
fun attachment_encryptDecryptEmpty_incremental() {
|
||||
attachment_encryptDecryptEmpty(incremental = true)
|
||||
}
|
||||
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
private fun attachment_encryptDecryptEmpty(incremental: Boolean) {
|
||||
val key = Util.getSecretBytes(64)
|
||||
val plaintextInput = "".toByteArray()
|
||||
|
||||
val encryptResult = encryptData(plaintextInput, key, incremental)
|
||||
val cipherFile = writeToFile(encryptResult.ciphertext)
|
||||
|
||||
val inputStream: InputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.size.toLong(), key, encryptResult.digest, encryptResult.incrementalDigest, encryptResult.chunkSizeChoice)
|
||||
val plaintextOutput = readInputStreamFully(inputStream)
|
||||
|
||||
Assert.assertArrayEquals(plaintextInput, plaintextOutput)
|
||||
|
||||
cipherFile.delete()
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException::class)
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
fun attachment_decryptFailOnBadKey_nonIncremental() {
|
||||
attachment_decryptFailOnBadKey(incremental = false)
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException::class)
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
fun attachment_decryptFailOnBadKey_incremental() {
|
||||
attachment_decryptFailOnBadKey(incremental = true)
|
||||
}
|
||||
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
private fun attachment_decryptFailOnBadKey(incremental: Boolean) {
|
||||
var cipherFile: File? = null
|
||||
|
||||
try {
|
||||
val key = Util.getSecretBytes(64)
|
||||
val plaintextInput = Util.getSecretBytes(MEBIBYTE)
|
||||
|
||||
val encryptResult = encryptData(plaintextInput, key, incremental)
|
||||
cipherFile = writeToFile(encryptResult.ciphertext)
|
||||
|
||||
val badKey = ByteArray(64)
|
||||
AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.size.toLong(), badKey, encryptResult.digest, null, 0)
|
||||
} finally {
|
||||
cipherFile?.delete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException::class)
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
fun attachment_decryptFailOnBadMac_nonIncremental() {
|
||||
attachment_decryptFailOnBadMac(incremental = false)
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException::class)
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
fun attachment_decryptFailOnBadMac_incremental() {
|
||||
attachment_decryptFailOnBadMac(incremental = true)
|
||||
}
|
||||
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
private fun attachment_decryptFailOnBadMac(incremental: Boolean) {
|
||||
var cipherFile: File? = null
|
||||
|
||||
try {
|
||||
val key = Util.getSecretBytes(64)
|
||||
val plaintextInput = Util.getSecretBytes(MEBIBYTE)
|
||||
|
||||
val encryptResult = encryptData(plaintextInput, key, incremental)
|
||||
val badMacCiphertext = encryptResult.ciphertext.copyOf(encryptResult.ciphertext.size)
|
||||
|
||||
badMacCiphertext[badMacCiphertext.size - 1] = (badMacCiphertext[badMacCiphertext.size - 1] + 1).toByte()
|
||||
|
||||
cipherFile = writeToFile(badMacCiphertext)
|
||||
|
||||
val stream: InputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.size.toLong(), key, encryptResult.digest, encryptResult.incrementalDigest, encryptResult.chunkSizeChoice)
|
||||
|
||||
// In incremental mode, we'll only check the digest after reading the whole thing
|
||||
if (incremental) {
|
||||
StreamUtil.readFully(stream)
|
||||
}
|
||||
} finally {
|
||||
cipherFile?.delete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException::class)
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
fun attachment_decryptFailOnNullDigest_nonIncremental() {
|
||||
attachment_decryptFailOnNullDigest(incremental = false)
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException::class)
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
fun attachment_decryptFailOnNullDigest_incremental() {
|
||||
attachment_decryptFailOnNullDigest(incremental = true)
|
||||
}
|
||||
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
private fun attachment_decryptFailOnNullDigest(incremental: Boolean) {
|
||||
var cipherFile: File? = null
|
||||
|
||||
try {
|
||||
val key = Util.getSecretBytes(64)
|
||||
val plaintextInput = Util.getSecretBytes(MEBIBYTE)
|
||||
val encryptResult = encryptData(plaintextInput, key, incremental)
|
||||
|
||||
cipherFile = writeToFile(encryptResult.ciphertext)
|
||||
|
||||
AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.size.toLong(), key, null, encryptResult.incrementalDigest, encryptResult.chunkSizeChoice)
|
||||
} finally {
|
||||
cipherFile?.delete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException::class)
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
fun attachment_decryptFailOnBadDigest_nonIncremental() {
|
||||
attachment_decryptFailOnBadDigest(incremental = false)
|
||||
}
|
||||
|
||||
@Test(expected = InvalidMessageException::class)
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
fun attachment_decryptFailOnBadDigest_incremental() {
|
||||
attachment_decryptFailOnBadDigest(incremental = true)
|
||||
}
|
||||
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
private fun attachment_decryptFailOnBadDigest(incremental: Boolean) {
|
||||
var cipherFile: File? = null
|
||||
|
||||
try {
|
||||
val key = Util.getSecretBytes(64)
|
||||
val plaintextInput = Util.getSecretBytes(MEBIBYTE)
|
||||
|
||||
val encryptResult = encryptData(plaintextInput, key, incremental)
|
||||
val badDigest = ByteArray(32)
|
||||
|
||||
cipherFile = writeToFile(encryptResult.ciphertext)
|
||||
|
||||
val stream: InputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.size.toLong(), key, badDigest, encryptResult.incrementalDigest, encryptResult.chunkSizeChoice)
|
||||
|
||||
// In incremental mode, we'll only check the digest after reading the whole thing
|
||||
if (incremental) {
|
||||
StreamUtil.readFully(stream)
|
||||
}
|
||||
} finally {
|
||||
cipherFile?.delete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun attachment_decryptFailOnBadIncrementalDigest() {
|
||||
var cipherFile: File? = null
|
||||
var hitCorrectException = false
|
||||
|
||||
try {
|
||||
val key = Util.getSecretBytes(64)
|
||||
val plaintextInput = Util.getSecretBytes(MEBIBYTE)
|
||||
|
||||
val encryptResult = encryptData(plaintextInput, key, true)
|
||||
val badDigest = Util.getSecretBytes(encryptResult.incrementalDigest.size)
|
||||
|
||||
cipherFile = writeToFile(encryptResult.ciphertext)
|
||||
|
||||
val decryptedStream: InputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintextInput.size.toLong(), key, encryptResult.digest, badDigest, encryptResult.chunkSizeChoice)
|
||||
val plaintextOutput = readInputStreamFully(decryptedStream)
|
||||
|
||||
fail(AssertionError("Expected to fail before hitting this line"))
|
||||
} catch (e: InvalidMacException) {
|
||||
hitCorrectException = true
|
||||
} catch (e: InvalidMessageException) {
|
||||
hitCorrectException = false
|
||||
} finally {
|
||||
cipherFile?.delete()
|
||||
}
|
||||
|
||||
assertThat(hitCorrectException).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
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
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
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.createForArchivedMedia(keyMaterial, cipherFile, plaintextInput.size.toLong())
|
||||
val plaintextOutput = readInputStreamFully(inputStream)
|
||||
|
||||
assertThat(plaintextOutput).isEqualTo(plaintextInput)
|
||||
|
||||
cipherFile.delete()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
fun archive_encryptDecryptEmpty() {
|
||||
val key = Util.getSecretBytes(64)
|
||||
val keyMaterial = createMediaKeyMaterial(key)
|
||||
val plaintextInput = "".toByteArray()
|
||||
|
||||
val encryptResult = encryptData(plaintextInput, key, false)
|
||||
val cipherFile = writeToFile(encryptResult.ciphertext)
|
||||
|
||||
val inputStream: InputStream = AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, plaintextInput.size.toLong())
|
||||
val plaintextOutput = readInputStreamFully(inputStream)
|
||||
|
||||
assertThat(plaintextOutput).isEqualTo(plaintextInput)
|
||||
|
||||
cipherFile.delete()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun archive_decryptFailOnBadKey() {
|
||||
var cipherFile: File? = null
|
||||
var hitCorrectException = false
|
||||
|
||||
try {
|
||||
val key = Util.getSecretBytes(64)
|
||||
val badKey = Util.getSecretBytes(64)
|
||||
val keyMaterial = createMediaKeyMaterial(badKey)
|
||||
val plaintextInput = "Gwen Stacy".toByteArray()
|
||||
|
||||
val encryptResult = encryptData(plaintextInput, key, false)
|
||||
cipherFile = writeToFile(encryptResult.ciphertext)
|
||||
|
||||
AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, plaintextInput.size.toLong())
|
||||
} catch (e: InvalidMessageException) {
|
||||
hitCorrectException = true
|
||||
} finally {
|
||||
cipherFile?.delete()
|
||||
}
|
||||
|
||||
assertThat(hitCorrectException).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
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.createForArchivedMedia(keyMaterial, cipherFile, length.toLong())
|
||||
val plaintextOutput = readInputStreamFully(decryptedStream)
|
||||
|
||||
Assert.assertArrayEquals(plaintextInput, plaintextOutput)
|
||||
|
||||
cipherFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun archive_decryptFailOnBadMac() {
|
||||
var cipherFile: File? = null
|
||||
var hitCorrectException = false
|
||||
|
||||
try {
|
||||
val key = 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()
|
||||
|
||||
cipherFile = writeToFile(badMacCiphertext)
|
||||
|
||||
val keyMaterial = createMediaKeyMaterial(key)
|
||||
AttachmentCipherInputStream.createForArchivedMedia(keyMaterial, cipherFile, plaintextInput.size.toLong())
|
||||
Assert.fail()
|
||||
} catch (e: InvalidMessageException) {
|
||||
hitCorrectException = true
|
||||
} finally {
|
||||
cipherFile?.delete()
|
||||
}
|
||||
|
||||
Assert.assertTrue(hitCorrectException)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
fun sticker_encryptDecrypt() {
|
||||
LibSignalLibraryUtil.assumeLibSignalSupportedOnOS()
|
||||
|
||||
val packKey = Util.getSecretBytes(32)
|
||||
val plaintextInput = Util.getSecretBytes(MEBIBYTE)
|
||||
val encryptResult = encryptData(plaintextInput, expandPackKey(packKey), true)
|
||||
val inputStream = AttachmentCipherInputStream.createForStickerData(encryptResult.ciphertext, packKey)
|
||||
val plaintextOutput = readInputStreamFully(inputStream)
|
||||
|
||||
Assert.assertArrayEquals(plaintextInput, plaintextOutput)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class, InvalidMessageException::class)
|
||||
fun sticker_encryptDecryptEmpty() {
|
||||
LibSignalLibraryUtil.assumeLibSignalSupportedOnOS()
|
||||
|
||||
val packKey = Util.getSecretBytes(32)
|
||||
val plaintextInput = "".toByteArray()
|
||||
val encryptResult = encryptData(plaintextInput, expandPackKey(packKey), true)
|
||||
val inputStream = AttachmentCipherInputStream.createForStickerData(encryptResult.ciphertext, packKey)
|
||||
val plaintextOutput = readInputStreamFully(inputStream)
|
||||
|
||||
Assert.assertArrayEquals(plaintextInput, plaintextOutput)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun sticker_decryptFailOnBadKey() {
|
||||
LibSignalLibraryUtil.assumeLibSignalSupportedOnOS()
|
||||
|
||||
var hitCorrectException = false
|
||||
|
||||
try {
|
||||
val packKey = Util.getSecretBytes(32)
|
||||
val plaintextInput = Util.getSecretBytes(MEBIBYTE)
|
||||
val encryptResult = encryptData(plaintextInput, expandPackKey(packKey), true)
|
||||
val badPackKey = ByteArray(32)
|
||||
|
||||
AttachmentCipherInputStream.createForStickerData(encryptResult.ciphertext, badPackKey)
|
||||
} catch (e: InvalidMessageException) {
|
||||
hitCorrectException = true
|
||||
}
|
||||
|
||||
Assert.assertTrue(hitCorrectException)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(IOException::class)
|
||||
fun sticker_decryptFailOnBadMac() {
|
||||
LibSignalLibraryUtil.assumeLibSignalSupportedOnOS()
|
||||
|
||||
var hitCorrectException = false
|
||||
|
||||
try {
|
||||
val packKey = Util.getSecretBytes(32)
|
||||
val plaintextInput = Util.getSecretBytes(MEBIBYTE)
|
||||
val encryptResult = encryptData(plaintextInput, expandPackKey(packKey), true)
|
||||
val badMacCiphertext = encryptResult.ciphertext.copyOf(encryptResult.ciphertext.size)
|
||||
|
||||
badMacCiphertext[badMacCiphertext.size - 1] = (badMacCiphertext[badMacCiphertext.size - 1] + 1).toByte()
|
||||
|
||||
AttachmentCipherInputStream.createForStickerData(badMacCiphertext, packKey)
|
||||
} catch (e: InvalidMessageException) {
|
||||
hitCorrectException = true
|
||||
}
|
||||
|
||||
Assert.assertTrue(hitCorrectException)
|
||||
}
|
||||
|
||||
private class EncryptResult(val ciphertext: ByteArray, val digest: ByteArray, val incrementalDigest: ByteArray, val chunkSizeChoice: Int)
|
||||
companion object {
|
||||
init {
|
||||
// https://github.com/google/conscrypt/issues/1034
|
||||
if (System.getProperty("os.arch") != "aarch64") {
|
||||
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
||||
}
|
||||
}
|
||||
|
||||
private const val MEBIBYTE = 1024 * 1024
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun encryptData(data: ByteArray, keyMaterial: ByteArray, withIncremental: Boolean): EncryptResult {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
val incrementalDigestOut = ByteArrayOutputStream()
|
||||
val iv = Util.getSecretBytes(16)
|
||||
val factory = AttachmentCipherOutputStreamFactory(keyMaterial, iv)
|
||||
|
||||
val encryptStream: DigestingOutputStream
|
||||
val sizeChoice = ChunkSizeChoice.inferChunkSize(data.size)
|
||||
encryptStream = if (withIncremental) {
|
||||
factory.createIncrementalFor(outputStream, data.size.toLong(), sizeChoice, incrementalDigestOut)
|
||||
} else {
|
||||
factory.createFor(outputStream)
|
||||
}
|
||||
|
||||
encryptStream.write(data)
|
||||
encryptStream.flush()
|
||||
encryptStream.close()
|
||||
incrementalDigestOut.close()
|
||||
|
||||
return EncryptResult(outputStream.toByteArray(), encryptStream.transmittedDigest, incrementalDigestOut.toByteArray(), sizeChoice.sizeInBytes)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun writeToFile(data: ByteArray): File {
|
||||
val file = File.createTempFile("temp", ".data")
|
||||
val outputStream: OutputStream = FileOutputStream(file)
|
||||
|
||||
outputStream.write(data)
|
||||
outputStream.close()
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun readInputStreamFully(inputStream: InputStream): ByteArray {
|
||||
return Util.readFullyAsBytes(inputStream)
|
||||
}
|
||||
|
||||
private fun expandPackKey(shortKey: ByteArray): ByteArray {
|
||||
return HKDF.deriveSecrets(shortKey, "Sticker Pack".toByteArray(), 64)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user