com.joyent.manta.client.crypto.MantaEncryptedObjectInputStreamTest.java Source code

Java tutorial

Introduction

Here is the source code for com.joyent.manta.client.crypto.MantaEncryptedObjectInputStreamTest.java

Source

/*
 * Copyright (c) 2017, Joyent, Inc. All rights reserved.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */
package com.joyent.manta.client.crypto;

import com.joyent.manta.client.MantaMetadata;
import com.joyent.manta.client.MantaObjectInputStream;
import com.joyent.manta.client.MantaObjectResponse;
import com.joyent.manta.exception.MantaClientEncryptionCiphertextAuthenticationException;
import com.joyent.manta.exception.MantaIOException;
import com.joyent.manta.http.MantaHttpHeaders;
import com.joyent.manta.http.entity.MantaInputStreamEntity;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.BoundedInputStream;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.commons.lang3.RandomUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.conn.EofSensorInputStream;
import org.mockito.Mockito;
import org.testng.Assert;
import org.testng.AssertJUnit;
import org.testng.annotations.Test;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.UncheckedIOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Base64;
import java.util.UUID;

import static java.nio.file.StandardOpenOption.READ;
import static java.nio.file.StandardOpenOption.WRITE;

@Test
public class MantaEncryptedObjectInputStreamTest {
    private final Path testFile;
    private final URL testURL;
    private final int plaintextSize;
    private final byte[] plaintextBytes;

    public MantaEncryptedObjectInputStreamTest() {
        final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        this.testURL = classLoader.getResource("test-data/chaucer.txt");

        try {
            testFile = Paths.get(testURL.toURI());
        } catch (URISyntaxException e) {
            throw new AssertionError(e);
        }

        this.plaintextSize = (int) testFile.toFile().length();

        try (InputStream in = this.testURL.openStream()) {
            this.plaintextBytes = IOUtils.readFully(in, plaintextSize);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public void canDecryptEntireObjectAuthenticatedAesCbc128() throws IOException {
        canDecryptEntireObjectAllReadModes(AesCbcCipherDetails.INSTANCE_128_BIT, true);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canDecryptEntireObjectAuthenticatedAesCbc192() throws IOException {
        canDecryptEntireObjectAllReadModes(AesCbcCipherDetails.INSTANCE_192_BIT, true);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canDecryptEntireObjectAuthenticatedAesCbc256() throws IOException {
        canDecryptEntireObjectAllReadModes(AesCbcCipherDetails.INSTANCE_256_BIT, true);
    }

    public void canDecryptEntireObjectAuthenticatedAesCtr128() throws IOException {
        canDecryptEntireObjectAllReadModes(AesCtrCipherDetails.INSTANCE_128_BIT, true);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canDecryptEntireObjectAuthenticatedAesCtr192() throws IOException {
        canDecryptEntireObjectAllReadModes(AesCtrCipherDetails.INSTANCE_192_BIT, true);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canDecryptEntireObjectAuthenticatedAesCtr256() throws IOException {
        canDecryptEntireObjectAllReadModes(AesCtrCipherDetails.INSTANCE_256_BIT, true);
    }

    public void canDecryptEntireObjectAuthenticatedAesGcm128() throws IOException {
        canDecryptEntireObjectAllReadModes(AesGcmCipherDetails.INSTANCE_128_BIT, true);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canDecryptEntireObjectAuthenticatedAesGcm192() throws IOException {
        canDecryptEntireObjectAllReadModes(AesGcmCipherDetails.INSTANCE_192_BIT, true);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canDecryptEntireObjectAuthenticatedAesGcm256() throws IOException {
        canDecryptEntireObjectAllReadModes(AesGcmCipherDetails.INSTANCE_256_BIT, true);
    }

    public void canDecryptEntireObjectUnauthenticatedAesCbc128() throws IOException {
        canDecryptEntireObjectAllReadModes(AesCbcCipherDetails.INSTANCE_128_BIT, false);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canDecryptEntireObjectUnauthenticatedAesCbc192() throws IOException {
        canDecryptEntireObjectAllReadModes(AesCbcCipherDetails.INSTANCE_192_BIT, false);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canDecryptEntireObjectUnauthenticatedAesCbc256() throws IOException {
        canDecryptEntireObjectAllReadModes(AesCbcCipherDetails.INSTANCE_256_BIT, false);
    }

    public void canDecryptEntireObjectUnauthenticatedAesCtr128() throws IOException {
        canDecryptEntireObjectAllReadModes(AesCtrCipherDetails.INSTANCE_128_BIT, false);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canDecryptEntireObjectUnauthenticatedAesCtr192() throws IOException {
        canDecryptEntireObjectAllReadModes(AesCtrCipherDetails.INSTANCE_192_BIT, false);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canDecryptEntireObjectUnauthenticatedAesCtr256() throws IOException {
        canDecryptEntireObjectAllReadModes(AesCtrCipherDetails.INSTANCE_256_BIT, false);
    }

    public void canDecryptEntireObjectUnauthenticatedAesGcm128() throws IOException {
        canDecryptEntireObjectAllReadModes(AesGcmCipherDetails.INSTANCE_128_BIT, false);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canDecryptEntireObjectUnauthenticatedAesGcm192() throws IOException {
        canDecryptEntireObjectAllReadModes(AesGcmCipherDetails.INSTANCE_192_BIT, false);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canDecryptEntireObjectUnauthenticatedAesGcm256() throws IOException {
        canDecryptEntireObjectAllReadModes(AesGcmCipherDetails.INSTANCE_256_BIT, false);
    }

    public void willErrorIfCiphertextIsModifiedAesCbc128() throws IOException {
        willErrorIfCiphertextIsModifiedAllReadModes(AesCbcCipherDetails.INSTANCE_128_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void willErrorIfCiphertextIsModifiedAesCbc192() throws IOException {
        willErrorIfCiphertextIsModifiedAllReadModes(AesCbcCipherDetails.INSTANCE_192_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void willErrorIfCiphertextIsModifiedAesCbc256() throws IOException {
        willErrorIfCiphertextIsModifiedAllReadModes(AesCbcCipherDetails.INSTANCE_256_BIT);
    }

    public void willErrorIfCiphertextIsModifiedAesCtr128() throws IOException {
        willErrorIfCiphertextIsModifiedAllReadModes(AesCtrCipherDetails.INSTANCE_128_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void willErrorIfCiphertextIsModifiedAesCtr192() throws IOException {
        willErrorIfCiphertextIsModifiedAllReadModes(AesCtrCipherDetails.INSTANCE_192_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void willErrorIfCiphertextIsModifiedAesCtr256() throws IOException {
        willErrorIfCiphertextIsModifiedAllReadModes(AesCtrCipherDetails.INSTANCE_256_BIT);
    }

    public void willErrorIfCiphertextIsModifiedAesGcm128() throws IOException {
        willErrorIfCiphertextIsModifiedAllReadModes(AesGcmCipherDetails.INSTANCE_128_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void willErrorIfCiphertextIsModifiedAesGcm192() throws IOException {
        willErrorIfCiphertextIsModifiedAllReadModes(AesGcmCipherDetails.INSTANCE_192_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void willErrorIfCiphertextIsModifiedAesGcm256() throws IOException {
        willErrorIfCiphertextIsModifiedAllReadModes(AesGcmCipherDetails.INSTANCE_256_BIT);
    }

    public void willErrorIfCiphertextIsModifiedAndNotReadFullyAesCbc128() throws IOException {
        willErrorIfCiphertextIsModifiedAndNotReadFully(AesCbcCipherDetails.INSTANCE_128_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void willErrorIfCiphertextIsModifiedAndNotReadFullyAesCbc192() throws IOException {
        willErrorIfCiphertextIsModifiedAndNotReadFully(AesCbcCipherDetails.INSTANCE_192_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void willErrorIfCiphertextIsModifiedAndNotReadFullyAesCbc256() throws IOException {
        willErrorIfCiphertextIsModifiedAndNotReadFully(AesCbcCipherDetails.INSTANCE_256_BIT);
    }

    public void willErrorIfCiphertextIsModifiedAndNotReadFullyAesCtr128() throws IOException {
        willErrorIfCiphertextIsModifiedAndNotReadFully(AesCtrCipherDetails.INSTANCE_128_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void willErrorIfCiphertextIsModifiedAndNotReadFullyAesCtr192() throws IOException {
        willErrorIfCiphertextIsModifiedAndNotReadFully(AesCtrCipherDetails.INSTANCE_192_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void willErrorIfCiphertextIsModifiedAndNotReadFullyAesCtr256() throws IOException {
        willErrorIfCiphertextIsModifiedAndNotReadFully(AesCtrCipherDetails.INSTANCE_256_BIT);
    }

    public void willErrorIfCiphertextIsModifiedAndNotReadFullyAesGcm128() throws IOException {
        willErrorIfCiphertextIsModifiedAndNotReadFully(AesGcmCipherDetails.INSTANCE_128_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void willErrorIfCiphertextIsModifiedAndNotReadFullyAesGcm192() throws IOException {
        willErrorIfCiphertextIsModifiedAndNotReadFully(AesGcmCipherDetails.INSTANCE_192_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void willErrorIfCiphertextIsModifiedAndNotReadFullyAesGcm256() throws IOException {
        willErrorIfCiphertextIsModifiedAndNotReadFully(AesGcmCipherDetails.INSTANCE_256_BIT);
    }

    public void canSkipBytesAuthenticatedAesCbc128() throws IOException {
        canSkipBytesAuthenticated(AesCbcCipherDetails.INSTANCE_128_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canSkipBytesAuthenticatedAesCbc192() throws IOException {
        canSkipBytesAuthenticated(AesCbcCipherDetails.INSTANCE_192_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canSkipBytesAuthenticatedAesCbc256() throws IOException {
        canSkipBytesAuthenticated(AesCbcCipherDetails.INSTANCE_256_BIT);
    }

    public void canSkipBytesAuthenticatedAesCtr128() throws IOException {
        canSkipBytesAuthenticated(AesCtrCipherDetails.INSTANCE_128_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canSkipBytesAuthenticatedAesCtr192() throws IOException {
        canSkipBytesAuthenticated(AesCtrCipherDetails.INSTANCE_192_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canSkipBytesAuthenticatedAesCtr256() throws IOException {
        canSkipBytesAuthenticated(AesCtrCipherDetails.INSTANCE_256_BIT);
    }

    public void canSkipBytesAuthenticatedAesGcm128() throws IOException {
        canSkipBytesAuthenticated(AesGcmCipherDetails.INSTANCE_128_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canSkipBytesAuthenticatedAesGcm192() throws IOException {
        canSkipBytesAuthenticated(AesGcmCipherDetails.INSTANCE_192_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canSkipBytesAuthenticatedAesGcm256() throws IOException {
        canSkipBytesAuthenticated(AesGcmCipherDetails.INSTANCE_256_BIT);
    }

    public void canSkipBytesUnauthenticatedAesCbc128() throws IOException {
        canSkipBytesUnauthenticated(AesCbcCipherDetails.INSTANCE_128_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canSkipBytesUnauthenticatedAesCbc192() throws IOException {
        canSkipBytesUnauthenticated(AesCbcCipherDetails.INSTANCE_192_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canSkipBytesUnauthenticatedAesCbc256() throws IOException {
        canSkipBytesUnauthenticated(AesCbcCipherDetails.INSTANCE_256_BIT);
    }

    public void canSkipBytesUnauthenticatedAesCtr128() throws IOException {
        canSkipBytesUnauthenticated(AesCtrCipherDetails.INSTANCE_128_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canSkipBytesUnauthenticatedAesCtr192() throws IOException {
        canSkipBytesUnauthenticated(AesCtrCipherDetails.INSTANCE_192_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canSkipBytesUnauthenticatedAesCtr256() throws IOException {
        canSkipBytesUnauthenticated(AesCtrCipherDetails.INSTANCE_256_BIT);
    }

    public void canSkipBytesUnauthenticatedAesGcm128() throws IOException {
        canSkipBytesUnauthenticated(AesGcmCipherDetails.INSTANCE_128_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canSkipBytesUnauthenticatedAesGcm192() throws IOException {
        canSkipBytesUnauthenticated(AesGcmCipherDetails.INSTANCE_192_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canSkipBytesUnauthenticatedAesGcm256() throws IOException {
        canSkipBytesUnauthenticated(AesGcmCipherDetails.INSTANCE_256_BIT);
    }

    public void willErrorIfCiphertextIsModifiedAndBytesAreSkippedAesCbc128() throws IOException {
        willErrorIfCiphertextIsModifiedAndBytesAreSkipped(AesCbcCipherDetails.INSTANCE_128_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void willErrorIfCiphertextIsModifiedAndBytesAreSkippedAesCbc192() throws IOException {
        willErrorIfCiphertextIsModifiedAndBytesAreSkipped(AesCbcCipherDetails.INSTANCE_192_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void willErrorIfCiphertextIsModifiedAndBytesAreSkippedAesCbc256() throws IOException {
        willErrorIfCiphertextIsModifiedAndBytesAreSkipped(AesCbcCipherDetails.INSTANCE_256_BIT);
    }

    public void willErrorIfCiphertextIsModifiedAndBytesAreSkippedAesCtr128() throws IOException {
        willErrorIfCiphertextIsModifiedAndBytesAreSkipped(AesCtrCipherDetails.INSTANCE_128_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void willErrorIfCiphertextIsModifiedAndBytesAreSkippedAesCtr192() throws IOException {
        willErrorIfCiphertextIsModifiedAndBytesAreSkipped(AesCtrCipherDetails.INSTANCE_192_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void willErrorIfCiphertextIsModifiedAndBytesAreSkippedAesCtr256() throws IOException {
        willErrorIfCiphertextIsModifiedAndBytesAreSkipped(AesCtrCipherDetails.INSTANCE_256_BIT);
    }

    public void willErrorIfCiphertextIsModifiedAndBytesAreSkippedAesGcm128() throws IOException {
        willErrorIfCiphertextIsModifiedAndBytesAreSkipped(AesGcmCipherDetails.INSTANCE_128_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void willErrorIfCiphertextIsModifiedAndBytesAreSkippedAesGcm192() throws IOException {
        willErrorIfCiphertextIsModifiedAndBytesAreSkipped(AesGcmCipherDetails.INSTANCE_192_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void willErrorIfCiphertextIsModifiedAndBytesAreSkippedAesGcm256() throws IOException {
        willErrorIfCiphertextIsModifiedAndBytesAreSkipped(AesGcmCipherDetails.INSTANCE_256_BIT);
    }

    public void canReadByteRangeStartingAtZeroEndingInFirstBlockAesCtr128() throws IOException {
        SupportedCipherDetails cipherDetails = AesCtrCipherDetails.INSTANCE_128_BIT;
        int endPos = cipherDetails.getBlockSizeInBytes() / 2;
        canReadByteRangeAllReadModes(cipherDetails, 0, endPos);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canReadByteRangeStartingAtZeroEndingInFirstBlockAesCtr192() throws IOException {
        SupportedCipherDetails cipherDetails = AesCtrCipherDetails.INSTANCE_192_BIT;
        int endPos = cipherDetails.getBlockSizeInBytes() / 2;
        canReadByteRangeAllReadModes(cipherDetails, 0, endPos);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canReadByteRangeStartingAtZeroEndingInFirstBlockAesCtr256() throws IOException {
        SupportedCipherDetails cipherDetails = AesCtrCipherDetails.INSTANCE_256_BIT;
        int endPos = cipherDetails.getBlockSizeInBytes() / 2;
        canReadByteRangeAllReadModes(cipherDetails, 0, endPos);
    }

    public void canReadByteRangeStartingAtZeroEndingInThirdBlockAesCtr128() throws IOException {
        SupportedCipherDetails cipherDetails = AesCtrCipherDetails.INSTANCE_128_BIT;
        int endPos = cipherDetails.getBlockSizeInBytes() * 2 + (cipherDetails.getBlockSizeInBytes() / 2);
        canReadByteRangeAllReadModes(cipherDetails, 0, endPos);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canReadByteRangeStartingAtZeroEndingInThirdBlockAesCtr192() throws IOException {
        SupportedCipherDetails cipherDetails = AesCtrCipherDetails.INSTANCE_192_BIT;
        int endPos = cipherDetails.getBlockSizeInBytes() * 2 + (cipherDetails.getBlockSizeInBytes() / 2);
        canReadByteRangeAllReadModes(cipherDetails, 0, endPos);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canReadByteRangeStartingAtZeroEndingInThirdBlockAesCtr256() throws IOException {
        SupportedCipherDetails cipherDetails = AesCtrCipherDetails.INSTANCE_256_BIT;
        int endPos = cipherDetails.getBlockSizeInBytes() * 2 + (cipherDetails.getBlockSizeInBytes() / 2);
        canReadByteRangeAllReadModes(cipherDetails, 0, endPos);
    }

    public void canReadByteRangeStartingAtThreeEndingInFirstBlockAesCtr128() throws IOException {
        SupportedCipherDetails cipherDetails = AesCtrCipherDetails.INSTANCE_128_BIT;
        int endPos = cipherDetails.getBlockSizeInBytes() / 2;
        canReadByteRangeAllReadModes(cipherDetails, 3, endPos);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canReadByteRangeStartingAtThreeEndingInFirstBlockAesCtr192() throws IOException {
        SupportedCipherDetails cipherDetails = AesCtrCipherDetails.INSTANCE_192_BIT;
        int endPos = cipherDetails.getBlockSizeInBytes() / 2;
        canReadByteRangeAllReadModes(cipherDetails, 3, endPos);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canReadByteRangeStartingAtThreeEndingInFirstBlockAesCtr256() throws IOException {
        SupportedCipherDetails cipherDetails = AesCtrCipherDetails.INSTANCE_256_BIT;
        int endPos = cipherDetails.getBlockSizeInBytes() / 2;
        canReadByteRangeAllReadModes(cipherDetails, 3, endPos);
    }

    public void canReadByteRangeStartingAtThirdBlockEndingInFifthBlockAesCtr128() throws IOException {
        SupportedCipherDetails cipherDetails = AesCtrCipherDetails.INSTANCE_128_BIT;
        int startPos = cipherDetails.getBlockSizeInBytes() * 3 + (cipherDetails.getBlockSizeInBytes() / 2);
        int endPos = cipherDetails.getBlockSizeInBytes() * 5 + (cipherDetails.getBlockSizeInBytes() / 2);
        canReadByteRangeAllReadModes(cipherDetails, startPos, endPos);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canReadByteRangeStartingAtThirdBlockEndingInFifthBlockAesCtr192() throws IOException {
        SupportedCipherDetails cipherDetails = AesCtrCipherDetails.INSTANCE_192_BIT;
        int startPos = cipherDetails.getBlockSizeInBytes() * 3 + (cipherDetails.getBlockSizeInBytes() / 2);
        int endPos = cipherDetails.getBlockSizeInBytes() * 5 + (cipherDetails.getBlockSizeInBytes() / 2);
        canReadByteRangeAllReadModes(cipherDetails, startPos, endPos);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canReadByteRangeStartingAtThirdBlockEndingInFifthBlockAesCtr256() throws IOException {
        SupportedCipherDetails cipherDetails = AesCtrCipherDetails.INSTANCE_256_BIT;
        int startPos = cipherDetails.getBlockSizeInBytes() * 3 + (cipherDetails.getBlockSizeInBytes() / 2);
        int endPos = cipherDetails.getBlockSizeInBytes() * 5 + (cipherDetails.getBlockSizeInBytes() / 2);
        canReadByteRangeAllReadModes(cipherDetails, startPos, endPos);
    }

    public void canReadByteRangeStarting25bytesFromEndToEndAesCtr128() throws IOException {
        SupportedCipherDetails cipherDetails = AesCtrCipherDetails.INSTANCE_128_BIT;
        int endPos = plaintextSize - 1;
        int startPos = endPos - 25;
        canReadByteRangeAllReadModes(cipherDetails, startPos, endPos);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canReadByteRangeStarting25bytesFromEndToEndAesCtr192() throws IOException {
        SupportedCipherDetails cipherDetails = AesCtrCipherDetails.INSTANCE_192_BIT;
        int endPos = plaintextSize - 1;
        int startPos = endPos - 25;
        canReadByteRangeAllReadModes(cipherDetails, startPos, endPos);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canReadByteRangeStarting25bytesFromEndToEndAesCtr256() throws IOException {
        SupportedCipherDetails cipherDetails = AesCtrCipherDetails.INSTANCE_256_BIT;
        int endPos = plaintextSize - 1;
        int startPos = endPos - 25;
        canReadByteRangeAllReadModes(cipherDetails, startPos, endPos);
    }

    public void canReadByteRangeStarting25bytesFromEndToBeyondEndAesCtr128() throws IOException {
        SupportedCipherDetails cipherDetails = AesCtrCipherDetails.INSTANCE_128_BIT;
        int endPos = plaintextSize * 2;
        int startPos = plaintextSize - 26;
        canReadByteRangeAllReadModes(cipherDetails, startPos, endPos);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canReadByteRangeStarting25bytesFromEndToBeyondEndAesCtr192() throws IOException {
        SupportedCipherDetails cipherDetails = AesCtrCipherDetails.INSTANCE_192_BIT;
        int endPos = plaintextSize * 2;
        int startPos = plaintextSize - 26;
        canReadByteRangeAllReadModes(cipherDetails, startPos, endPos);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canReadByteRangeStarting25bytesFromEndToBeyondEndAesCtr256() throws IOException {
        SupportedCipherDetails cipherDetails = AesCtrCipherDetails.INSTANCE_256_BIT;
        int endPos = plaintextSize * 2;
        int startPos = plaintextSize - 26;
        canReadByteRangeAllReadModes(cipherDetails, startPos, endPos);
    }

    public void canCopyStreamWithLargeBufferBufferAuthenticatedAesCbc128() throws IOException {
        canCopyToOutputStreamWithLargeBuffer(AesCbcCipherDetails.INSTANCE_128_BIT, true);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canCopyStreamWithLargeBufferBufferAuthenticatedAesCbc192() throws IOException {
        canCopyToOutputStreamWithLargeBuffer(AesCbcCipherDetails.INSTANCE_192_BIT, true);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canCopyStreamWithLargeBufferBufferAuthenticatedAesCbc256() throws IOException {
        canCopyToOutputStreamWithLargeBuffer(AesCbcCipherDetails.INSTANCE_256_BIT, true);
    }

    public void canCopyStreamWithLargeBufferBufferAuthenticatedAesCtr128() throws IOException {
        canCopyToOutputStreamWithLargeBuffer(AesCtrCipherDetails.INSTANCE_128_BIT, true);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canCopyStreamWithLargeBufferBufferAuthenticatedAesCtr192() throws IOException {
        canCopyToOutputStreamWithLargeBuffer(AesCtrCipherDetails.INSTANCE_192_BIT, true);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canCopyStreamWithLargeBufferBufferAuthenticatedAesCtr256() throws IOException {
        canCopyToOutputStreamWithLargeBuffer(AesCtrCipherDetails.INSTANCE_256_BIT, true);
    }

    public void canCopyStreamWithLargeBufferBufferAuthenticatedAesGcm128() throws IOException {
        canCopyToOutputStreamWithLargeBuffer(AesGcmCipherDetails.INSTANCE_128_BIT, true);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canCopyStreamWithLargeBufferBufferAuthenticatedAesGcm192() throws IOException {
        canCopyToOutputStreamWithLargeBuffer(AesGcmCipherDetails.INSTANCE_192_BIT, true);
    }

    public void canCopyStreamWithLargeBufferBufferUnauthenticatedAesCbc128() throws IOException {
        canCopyToOutputStreamWithLargeBuffer(AesCbcCipherDetails.INSTANCE_128_BIT, false);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canCopyStreamWithLargeBufferBufferUnauthenticatedAesCbc192() throws IOException {
        canCopyToOutputStreamWithLargeBuffer(AesCbcCipherDetails.INSTANCE_192_BIT, false);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canCopyStreamWithLargeBufferBufferUnauthenticatedAesCbc256() throws IOException {
        canCopyToOutputStreamWithLargeBuffer(AesCbcCipherDetails.INSTANCE_256_BIT, false);
    }

    public void canCopyStreamWithLargeBufferBufferUnauthenticatedAesCtr128() throws IOException {
        canCopyToOutputStreamWithLargeBuffer(AesCtrCipherDetails.INSTANCE_128_BIT, false);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canCopyStreamWithLargeBufferBufferUnauthenticatedAesCtr192() throws IOException {
        canCopyToOutputStreamWithLargeBuffer(AesCtrCipherDetails.INSTANCE_192_BIT, false);
    }

    @Test(groups = { "unlimited-crypto" })
    public void canCopyStreamWithLargeBufferBufferUnauthenticatedAesCtr256() throws IOException {
        canCopyToOutputStreamWithLargeBuffer(AesCtrCipherDetails.INSTANCE_256_BIT, false);
    }

    public void willErrorIfCiphertextIsMissingHmacAesCbc128() throws IOException {
        willErrorWhenMissingHMAC(AesCbcCipherDetails.INSTANCE_128_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void willErrorIfCiphertextIsMissingHmacAesCbc192() throws IOException {
        willErrorWhenMissingHMAC(AesCbcCipherDetails.INSTANCE_192_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void willErrorIfCiphertextIsMissingHmacAesCbc256() throws IOException {
        willErrorWhenMissingHMAC(AesCbcCipherDetails.INSTANCE_256_BIT);
    }

    public void willErrorIfCiphertextIsMissingHmacAesCtr128() throws IOException {
        willErrorWhenMissingHMAC(AesCtrCipherDetails.INSTANCE_128_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void willErrorIfCiphertextIsMissingHmacAesCtr192() throws IOException {
        willErrorWhenMissingHMAC(AesCtrCipherDetails.INSTANCE_192_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void willErrorIfCiphertextIsMissingHmacAesCtr256() throws IOException {
        willErrorWhenMissingHMAC(AesCtrCipherDetails.INSTANCE_256_BIT);
    }

    public void willValidateIfHmacIsReadInMultipleReadsAesCtr128() throws IOException {
        willValidateIfHmacIsReadInMultipleReads(AesCtrCipherDetails.INSTANCE_128_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void willValidateIfHmacIsReadInMultipleReadsAesCtr192() throws IOException {
        willValidateIfHmacIsReadInMultipleReads(AesCtrCipherDetails.INSTANCE_192_BIT);
    }

    @Test(groups = { "unlimited-crypto" })
    public void willValidateIfHmacIsReadInMultipleReadsAesCtr256() throws IOException {
        willValidateIfHmacIsReadInMultipleReads(AesCtrCipherDetails.INSTANCE_256_BIT);
    }

    /*
        
    removing failing tests documented in https://github.com/joyent/java-manta/issues/257
        
    public void willValidateIfHmacIsReadInMultipleReadsAesCbc128() throws IOException {
    willValidateIfHmacIsReadInMultipleReads(AesCbcCipherDetails.INSTANCE_128_BIT);
    }
        
    @Test(groups = {"unlimited-crypto"})
    public void willValidateIfHmacIsReadInMultipleReadsAesCbc192() throws IOException {
    willValidateIfHmacIsReadInMultipleReads(AesCbcCipherDetails.INSTANCE_192_BIT);
    }
        
    @Test(groups = {"unlimited-crypto"})
    public void willValidateIfHmacIsReadInMultipleReadsAesCbc256() throws IOException {
    willValidateIfHmacIsReadInMultipleReads(AesCbcCipherDetails.INSTANCE_256_BIT);
    }
        
    */

    /* TEST UTILITY CLASSES */

    private static class EncryptedFile {
        public final Cipher cipher;
        public final File file;

        public EncryptedFile(Cipher cipher, File file) {
            this.cipher = cipher;
            this.file = file;
        }
    }

    private interface ReadBytes {
        int readAll(InputStream in, byte[] target) throws IOException;
    }

    private static class SingleReads implements ReadBytes {
        @Override
        public int readAll(InputStream in, byte[] target) throws IOException {
            int totalRead = 0;
            int lastByte;

            try {
                while ((lastByte = in.read()) != -1) {
                    target[totalRead++] = (byte) lastByte;
                }
            } catch (ArrayIndexOutOfBoundsException e) {
                while (in.read() != -1) {
                    totalRead++;
                }

                String msg = String.format(
                        "%d bytes available in stream, but " + "the byte array target has a length of %d bytes",
                        totalRead, target.length);
                throw new AssertionError(msg, e);
            }

            return totalRead;
        }
    }

    private static class ByteChunkReads implements ReadBytes {
        @Override
        public int readAll(InputStream in, byte[] target) throws IOException {
            int totalRead = 0;
            int lastRead;
            int chunkSize = 16;
            byte[] chunk = new byte[chunkSize];

            try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
                while ((lastRead = in.read(chunk)) != -1) {
                    totalRead += lastRead;
                    out.write(chunk, 0, lastRead);
                }

                byte[] written = out.toByteArray();
                System.arraycopy(written, 0, target, 0, target.length);
            }

            return totalRead;
        }
    }

    private static class ByteChunkOffsetReads implements ReadBytes {
        @Override
        public int readAll(InputStream in, byte[] target) throws IOException {
            int totalRead = 0;
            int lastRead;
            int chunkSize = 128;
            byte[] chunk = new byte[512];

            try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
                while ((lastRead = in.read(chunk, 0, chunkSize)) != -1) {
                    totalRead += lastRead;
                    out.write(chunk, 0, lastRead);
                }

                byte[] written = out.toByteArray();
                System.arraycopy(written, 0, target, 0, target.length);
            }

            return totalRead;
        }
    }

    private static class PartialRead implements ReadBytes {
        @Override
        public int readAll(InputStream in, byte[] target) throws IOException {
            int read = in.read();

            if (read == -1) {
                return -1;
            }

            target[0] = (byte) read;

            return 1;
        }
    }

    private static class ReadSkipReadSkip implements ReadBytes {
        @Override
        public int readAll(InputStream in, byte[] target) throws IOException {
            int totalRead = 0;
            int lastRead;

            if ((lastRead = in.read()) == -1) {
                return -1;
            } else {
                target[totalRead++] = (byte) lastRead;
            }

            final int skipSize = 15;
            totalRead += Math.toIntExact(in.skip(skipSize));

            if ((lastRead = in.read()) == -1) {
                return -1;
            } else {
                target[totalRead++] = (byte) lastRead;
            }

            totalRead += Math.toIntExact(in.skip(skipSize));

            return totalRead;
        }
    }

    /* TEST UTILITY METHODS */

    /**
     * Test that loops through all of the ciphers and attempts to decrypt an
     * encrypted stream. An assertion fails if the original plaintext and the
     * decrypted plaintext don't match. Additionally, this test tries to read
     * data from the stream using three different read methods and makes sure
     * that all read methods function correctly.
     */
    private void canDecryptEntireObjectAllReadModes(SupportedCipherDetails cipherDetails, boolean authenticate)
            throws IOException {
        System.out.printf("Testing decryption of [%s] as full read of stream\n", cipherDetails.getCipherId());

        canDecryptEntireObject(cipherDetails, new SingleReads(), authenticate);
        canDecryptEntireObject(cipherDetails, new ByteChunkReads(), authenticate);
        canDecryptEntireObject(cipherDetails, new ByteChunkOffsetReads(), authenticate);
    }

    /**
     * Attempts to copy a {@link MantaEncryptedObjectInputStream} stream to
     * a {@link java.io.OutputStream} and close the streams. Copy is done using
     * a large buffer size and logic borrowed directly from COSBench.
     */
    private void canCopyToOutputStreamWithLargeBuffer(SupportedCipherDetails cipherDetails, boolean authenticate)
            throws IOException {
        final byte[] buffer = new byte[1024 * 1024];
        final int sourceLength = 8000;
        final byte[] sourceBytes = RandomUtils.nextBytes(sourceLength);

        SecretKey key = SecretKeyUtils.generate(cipherDetails);
        final EncryptedFile encryptedFile;

        try (InputStream in = new ByteArrayInputStream(sourceBytes)) {
            encryptedFile = encryptedFile(key, cipherDetails, sourceLength, in);
        }

        final byte[] iv = encryptedFile.cipher.getIV();

        final long ciphertextSize = encryptedFile.file.length();

        InputStream backing = new FileInputStream(encryptedFile.file);
        MantaEncryptedObjectInputStream in = createEncryptedObjectInputStream(key, backing, ciphertextSize,
                cipherDetails, iv, authenticate, sourceLength);
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        try {
            // Don't change me - I'm imitating COSBench
            for (int n; -1 != (n = in.read(buffer));) {
                out.write(buffer, 0, n);
            }
        } finally {
            in.close();
            out.close();
        }

        AssertJUnit.assertArrayEquals(sourceBytes, out.toByteArray());
    }

    /**
     * Test that loops through all of the ciphers and attempts to decrypt an
     * encrypted stream that had its ciphertext altered. An assertion fails if
     * the underlying stream fails to throw a {@link MantaClientEncryptionCiphertextAuthenticationException}.
     * Additionally, this test tries to read data from the stream using three
     * different read methods and makes sure that all read methods function
     * correctly.
     */
    private void willErrorIfCiphertextIsModifiedAllReadModes(SupportedCipherDetails cipherDetails)
            throws IOException {
        System.out.printf("Testing authentication of corrupted ciphertext with [%s] as full read of stream\n",
                cipherDetails.getCipherId());

        willThrowExceptionWhenCiphertextIsAltered(cipherDetails, new SingleReads());
        willThrowExceptionWhenCiphertextIsAltered(cipherDetails, new ByteChunkReads());
        willThrowExceptionWhenCiphertextIsAltered(cipherDetails, new ByteChunkOffsetReads());
    }

    /**
     * Test that loops through all of the ciphers and attempts to decrypt an
     * encrypted stream that had its ciphertext altered. In this case, the
     * stream is closed before all of the bytes are read from it. An assertion
     * fails if the underlying stream fails to throw a
     * {@link MantaClientEncryptionCiphertextAuthenticationException}.
     */
    private void willErrorIfCiphertextIsModifiedAndNotReadFully(SupportedCipherDetails cipherDetails)
            throws IOException {
        System.out.printf("Testing authentication of corrupted ciphertext with [%s] as partial read of stream\n",
                cipherDetails.getCipherId());

        willThrowExceptionWhenCiphertextIsAltered(cipherDetails, new PartialRead());
    }

    /**
     * Test that loops through all of the ciphers and attempts to skip bytes
     * from an encrypted stream. This test verifies that the checksums are being
     * calculated correctly even if bytes are skipped.
     */
    private void canSkipBytesAuthenticated(SupportedCipherDetails cipherDetails) throws IOException {
        System.out.printf("Testing authentication of ciphertext with [%s] as read and skips of stream\n",
                cipherDetails.getCipherId());

        canReadObject(cipherDetails, new ReadSkipReadSkip(), true);
    }

    /**
     * Test that loops through all of the ciphers and attempts to skip bytes
     * from an encrypted stream.
     */
    private void canSkipBytesUnauthenticated(SupportedCipherDetails cipherDetails) throws IOException {
        System.out.printf("Testing authentication of ciphertext with [%s] as read and skips of stream\n",
                cipherDetails.getCipherId());

        canReadObject(cipherDetails, new ReadSkipReadSkip(), false);
    }

    private void canDecryptEntireObject(SupportedCipherDetails cipherDetails, ReadBytes readBytes,
            boolean authenticate) throws IOException {
        SecretKey key = SecretKeyUtils.generate(cipherDetails);
        EncryptedFile encryptedFile = encryptedFile(key, cipherDetails, this.plaintextSize);
        long ciphertextSize = encryptedFile.file.length();

        FileInputStream in = new FileInputStream(encryptedFile.file);
        MantaEncryptedObjectInputStream min = createEncryptedObjectInputStream(key, in, ciphertextSize,
                cipherDetails, encryptedFile.cipher.getIV(), authenticate, (long) this.plaintextSize);

        try {
            byte[] actual = new byte[plaintextSize];
            readBytes.readAll(min, actual);

            AssertJUnit.assertArrayEquals("Plaintext doesn't match decrypted data", plaintextBytes, actual);
        } finally {
            min.close();
        }
    }

    /**
     * Test that attempts to skip bytes from an encrypted stream that had its
     * ciphertext altered. In this case, the stream is closed before all of the
     * bytes are read from it. An assertion fails if the underlying stream
     * fails to throw a {@link MantaClientEncryptionCiphertextAuthenticationException}.
     */
    private void willErrorIfCiphertextIsModifiedAndBytesAreSkipped(SupportedCipherDetails cipherDetails)
            throws IOException {
        System.out.printf("Testing authentication of corrupted ciphertext with [%s] as read and skips of stream\n",
                cipherDetails.getCipherId());

        willThrowExceptionWhenCiphertextIsAltered(cipherDetails, new ReadSkipReadSkip());
    }

    /**
     * Test that attempts to read a byte range from an encrypted stream starting
     * at the first byte.
     */
    private void canReadByteRangeAllReadModes(SupportedCipherDetails cipherDetails, int startPosInclusive,
            int endPosInclusive) throws IOException {
        System.out.printf("Testing byte range read starting from zero for cipher [%s] as full read of "
                + "truncated stream\n", cipherDetails.getCipherId());

        canReadByteRange(cipherDetails, new SingleReads(), startPosInclusive, endPosInclusive);
        canReadByteRange(cipherDetails, new ByteChunkReads(), startPosInclusive, endPosInclusive);
        canReadByteRange(cipherDetails, new ByteChunkOffsetReads(), startPosInclusive, endPosInclusive);
    }

    private void canReadByteRange(SupportedCipherDetails cipherDetails, ReadBytes readBytes, int startPosInclusive,
            int endPosInclusive) throws IOException {
        final byte[] content;
        try (InputStream in = testURL.openStream(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            IOUtils.copy(in, out);
            content = out.toByteArray();
        }

        /* If the plaintext size specified is greater than the actual plaintext
         * size, we adjust it here. This is only for creating the expectation
         * that we compare our output to. */
        final int endPositionExclusive;

        if (endPosInclusive + 1 > plaintextSize) {
            endPositionExclusive = plaintextSize;
        } else {
            endPositionExclusive = endPosInclusive + 1;
        }

        boolean unboundedEnd = (endPositionExclusive >= plaintextSize || endPosInclusive < 0);
        byte[] expected = Arrays.copyOfRange(content, startPosInclusive, endPositionExclusive);

        SecretKey key = SecretKeyUtils.generate(cipherDetails);
        EncryptedFile encryptedFile = encryptedFile(key, cipherDetails, this.plaintextSize);
        long ciphertextSize = encryptedFile.file.length();

        /* Here we translate the plaintext ranges to ciphertext ranges. Notice
         * that we take the input of endPos because it may overrun past the
         * size of the plaintext. */
        ByteRangeConversion ranges = cipherDetails.translateByteRange(startPosInclusive, endPosInclusive);
        long ciphertextStart = ranges.getCiphertextStartPositionInclusive();
        long plaintextLength = ((long) (endPosInclusive - startPosInclusive)) + 1L;

        /* Here, we simulate being passed only a subset of the total bytes of
         * a file - just like how the output of a HTTP range request would be
         * passed. */
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(encryptedFile.file, "r")) {
            // We seek to the start of the ciphertext to emulate a HTTP byte range request
            randomAccessFile.seek(ciphertextStart);

            long initialSkipBytes = ranges.getPlaintextBytesToSkipInitially()
                    + ranges.getCiphertextStartPositionInclusive();
            long binaryEndPositionInclusive = ranges.getCiphertextEndPositionInclusive();

            long ciphertextRangeMaxBytes = ciphertextSize - ciphertextStart;
            long ciphertextRangeRequestBytes = binaryEndPositionInclusive - ciphertextStart + 1;

            /* We then calculate the range length based on the *actual* ciphertext
             * size so that we are emulating HTTP range requests by returning a
             * the binary of the actual object (even if the range went beyond). */
            long ciphertextByteRangeLength = Math.min(ciphertextRangeMaxBytes, ciphertextRangeRequestBytes);

            if (unboundedEnd)
                ciphertextByteRangeLength = ciphertextSize - ciphertextStart; // include MAC

            byte[] rangedBytes = new byte[(int) ciphertextByteRangeLength];
            randomAccessFile.read(rangedBytes);

            try (ByteArrayInputStream bin = new ByteArrayInputStream(rangedBytes);
                    /* When creating the fake stream, we feed it a content-length equal
                     * to the size of the byte range because that is what Manta would
                     * do. Also, we pass along the incorrect plaintext length and
                     * test how the stream handles incorrect values of plaintext length. */
                    MantaEncryptedObjectInputStream min = createEncryptedObjectInputStream(key, bin,
                            ciphertextByteRangeLength, cipherDetails, encryptedFile.cipher.getIV(), false,
                            (long) this.plaintextSize, initialSkipBytes, plaintextLength, unboundedEnd)) {
                byte[] actual = new byte[expected.length];
                readBytes.readAll(min, actual);

                try {
                    AssertJUnit.assertArrayEquals("Byte range output doesn't match", expected, actual);
                } catch (AssertionError e) {
                    Assert.fail(String.format("%s\nexpected: %s\nactual  : %s", e.getMessage(),
                            new String(expected, StandardCharsets.UTF_8),
                            new String(actual, StandardCharsets.UTF_8)));
                }
            }
        }
    }

    private void canReadObject(SupportedCipherDetails cipherDetails, ReadBytes readBytes, boolean authenticate)
            throws IOException {
        SecretKey key = SecretKeyUtils.generate(cipherDetails);
        EncryptedFile encryptedFile = encryptedFile(key, cipherDetails, this.plaintextSize);
        long ciphertextSize = encryptedFile.file.length();

        FileInputStream in = new FileInputStream(encryptedFile.file);
        MantaEncryptedObjectInputStream min = createEncryptedObjectInputStream(key, in, ciphertextSize,
                cipherDetails, encryptedFile.cipher.getIV(), authenticate, (long) this.plaintextSize);

        try {
            byte[] actual = new byte[plaintextSize];
            readBytes.readAll(min, actual);
        } finally {
            min.close();
        }
    }

    private void willThrowExceptionWhenCiphertextIsAltered(SupportedCipherDetails cipherDetails,
            ReadBytes readBytes) throws IOException {
        SecretKey key = SecretKeyUtils.generate(cipherDetails);
        EncryptedFile encryptedFile = encryptedFile(key, cipherDetails, this.plaintextSize);

        try (FileChannel fc = (FileChannel.open(encryptedFile.file.toPath(), READ, WRITE))) {
            fc.position(2);
            ByteBuffer buff = ByteBuffer.wrap(new byte[] { 20, 20 });
            fc.write(buff);
        }

        long ciphertextSize = encryptedFile.file.length();

        boolean thrown = false;

        FileInputStream in = new FileInputStream(encryptedFile.file);
        MantaEncryptedObjectInputStream min = createEncryptedObjectInputStream(key, in, ciphertextSize,
                cipherDetails, encryptedFile.cipher.getIV(), true, (long) this.plaintextSize);

        try {
            byte[] actual = new byte[plaintextSize];
            readBytes.readAll(min, actual);
            min.close();
        } catch (MantaClientEncryptionCiphertextAuthenticationException e) {
            thrown = true;
        }

        Assert.assertTrue(thrown, "Ciphertext authentication exception wasn't thrown");
    }

    private void willErrorWhenMissingHMAC(final SupportedCipherDetails cipherDetails) throws IOException {
        SecretKey key = SecretKeyUtils.generate(cipherDetails);
        EncryptedFile encryptedFile = encryptedFile(key, cipherDetails, this.plaintextSize);
        long ciphertextSize = encryptedFile.file.length();
        int hmacSize = cipherDetails.getAuthenticationTagOrHmacLengthInBytes();
        long ciphertextSizeWithoutHmac = ciphertextSize - hmacSize;

        boolean thrown = false;

        try (FileInputStream fin = new FileInputStream(encryptedFile.file);
                BoundedInputStream in = new BoundedInputStream(fin, ciphertextSizeWithoutHmac);
                MantaEncryptedObjectInputStream min = createEncryptedObjectInputStream(key, in, ciphertextSize,
                        cipherDetails, encryptedFile.cipher.getIV(), true, (long) this.plaintextSize)) {
            // Do a single read to make sure that everything is working
            Assert.assertNotEquals(min.read(), -1, "The encrypted stream should not be empty");
        } catch (MantaIOException e) {
            if (e.getMessage().startsWith("No HMAC was stored at the end of the stream")) {
                thrown = true;
            } else {
                throw e;
            }
        }

        Assert.assertTrue(thrown, "Expected MantaIOException was not thrown");
    }

    private void willValidateIfHmacIsReadInMultipleReads(final SupportedCipherDetails cipherDetails)
            throws IOException {
        SecretKey key = SecretKeyUtils.generate(cipherDetails);
        EncryptedFile encryptedFile = encryptedFile(key, cipherDetails, this.plaintextSize);
        long ciphertextSize = encryptedFile.file.length();

        try (FileInputStream fin = new FileInputStream(encryptedFile.file);
                IncompleteByteReadInputStream ibrin = new IncompleteByteReadInputStream(fin);
                MantaEncryptedObjectInputStream min = createEncryptedObjectInputStream(key, ibrin, ciphertextSize,
                        cipherDetails, encryptedFile.cipher.getIV(), true, (long) this.plaintextSize);
                OutputStream out = new NullOutputStream()) {

            IOUtils.copy(min, out);
        }
    }

    private EncryptedFile encryptedFile(SecretKey key, SupportedCipherDetails cipherDetails, long plaintextSize)
            throws IOException {
        File temp = File.createTempFile("encrypted", ".data");
        FileUtils.forceDeleteOnExit(temp);

        try (InputStream in = testURL.openStream()) {
            return encryptedFile(key, cipherDetails, plaintextSize, in);
        }
    }

    private EncryptedFile encryptedFile(SecretKey key, SupportedCipherDetails cipherDetails, long plaintextSize,
            InputStream in) throws IOException {
        File temp = File.createTempFile("encrypted", ".data");
        FileUtils.forceDeleteOnExit(temp);

        try (FileOutputStream out = new FileOutputStream(temp)) {
            MantaInputStreamEntity entity = new MantaInputStreamEntity(in, plaintextSize);
            EncryptingEntity encryptingEntity = new EncryptingEntity(key, cipherDetails, entity);
            encryptingEntity.writeTo(out);

            Assert.assertEquals(temp.length(), encryptingEntity.getContentLength(),
                    "Ciphertext doesn't equal calculated size");

            return new EncryptedFile(encryptingEntity.getCipher(), temp);
        }
    }

    private MantaEncryptedObjectInputStream createEncryptedObjectInputStream(SecretKey key, InputStream in,
            long contentLength, SupportedCipherDetails cipherDetails, byte[] iv, boolean authenticate,
            long plaintextSize) {
        return createEncryptedObjectInputStream(key, in, contentLength, cipherDetails, iv, authenticate,
                plaintextSize, null, null, true);
    }

    private MantaEncryptedObjectInputStream createEncryptedObjectInputStream(SecretKey key, InputStream in,
            long ciphertextSize, SupportedCipherDetails cipherDetails, byte[] iv, boolean authenticate,
            long plaintextSize, Long skipBytes, Long plaintextLength, boolean unboundedEnd) {
        String path = String.format("/test/stor/test-%s", UUID.randomUUID());
        MantaHttpHeaders headers = new MantaHttpHeaders();
        headers.put(MantaHttpHeaders.ENCRYPTION_CIPHER, cipherDetails.getCipherId());
        headers.put(MantaHttpHeaders.ENCRYPTION_PLAINTEXT_CONTENT_LENGTH, plaintextSize);
        headers.put(MantaHttpHeaders.ENCRYPTION_KEY_ID, "unit-test-key");

        if (cipherDetails.isAEADCipher()) {
            headers.put(MantaHttpHeaders.ENCRYPTION_AEAD_TAG_LENGTH,
                    cipherDetails.getAuthenticationTagOrHmacLengthInBytes());
        } else {
            final String hmacName = SupportedHmacsLookupMap
                    .hmacNameFromInstance(cipherDetails.getAuthenticationHmac());
            headers.put(MantaHttpHeaders.ENCRYPTION_HMAC_TYPE, hmacName);
        }

        headers.put(MantaHttpHeaders.ENCRYPTION_IV, Base64.getEncoder().encodeToString(iv));

        headers.setContentLength(ciphertextSize);

        MantaMetadata metadata = new MantaMetadata();

        MantaObjectResponse response = new MantaObjectResponse(path, headers, metadata);

        CloseableHttpResponse httpResponse = Mockito.mock(CloseableHttpResponse.class);

        EofSensorInputStream eofSensorInputStream = new EofSensorInputStream(in, null);

        MantaObjectInputStream mantaObjectInputStream = new MantaObjectInputStream(response, httpResponse,
                eofSensorInputStream);

        return new MantaEncryptedObjectInputStream(mantaObjectInputStream, cipherDetails, key, authenticate,
                skipBytes, plaintextLength, unboundedEnd);
    }
}