blob: b06d1f53e0542f7bc6fbb0eb98d9133f652cd157 [file] [log] [blame]
package org.bouncycastle.crypto.io;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.bouncycastle.crypto.BufferedBlockCipher;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.SkippingCipher;
import org.bouncycastle.crypto.StreamCipher;
import org.bouncycastle.crypto.modes.AEADBlockCipher;
import org.bouncycastle.util.Arrays;
/**
* A CipherInputStream is composed of an InputStream and a cipher so that read() methods return data
* that are read in from the underlying InputStream but have been additionally processed by the
* Cipher. The cipher must be fully initialized before being used by a CipherInputStream.
* <p>
* For example, if the Cipher is initialized for decryption, the
* CipherInputStream will attempt to read in data and decrypt them,
* before returning the decrypted data.
*/
public class CipherInputStream
extends FilterInputStream
{
private static final int INPUT_BUF_SIZE = 2048;
private SkippingCipher skippingCipher;
private byte[] inBuf;
private BufferedBlockCipher bufferedBlockCipher;
private StreamCipher streamCipher;
private AEADBlockCipher aeadBlockCipher;
private byte[] buf;
private byte[] markBuf;
private int bufOff;
private int maxBuf;
private boolean finalized;
private long markPosition;
private int markBufOff;
/**
* Constructs a CipherInputStream from an InputStream and a
* BufferedBlockCipher.
*/
public CipherInputStream(
InputStream is,
BufferedBlockCipher cipher)
{
this(is, cipher, INPUT_BUF_SIZE);
}
/**
* Constructs a CipherInputStream from an InputStream and a StreamCipher.
*/
public CipherInputStream(
InputStream is,
StreamCipher cipher)
{
this(is, cipher, INPUT_BUF_SIZE);
}
/**
* Constructs a CipherInputStream from an InputStream and an AEADBlockCipher.
*/
public CipherInputStream(
InputStream is,
AEADBlockCipher cipher)
{
this(is, cipher, INPUT_BUF_SIZE);
}
/**
* Constructs a CipherInputStream from an InputStream, a
* BufferedBlockCipher, and a specified internal buffer size.
*/
public CipherInputStream(
InputStream is,
BufferedBlockCipher cipher,
int bufSize)
{
super(is);
this.bufferedBlockCipher = cipher;
this.inBuf = new byte[bufSize];
this.skippingCipher = (cipher instanceof SkippingCipher) ? (SkippingCipher)cipher : null;
}
/**
* Constructs a CipherInputStream from an InputStream, a StreamCipher, and a specified internal buffer size.
*/
public CipherInputStream(
InputStream is,
StreamCipher cipher,
int bufSize)
{
super(is);
this.streamCipher = cipher;
this.inBuf = new byte[bufSize];
this.skippingCipher = (cipher instanceof SkippingCipher) ? (SkippingCipher)cipher : null;
}
/**
* Constructs a CipherInputStream from an InputStream, an AEADBlockCipher, and a specified internal buffer size.
*/
public CipherInputStream(
InputStream is,
AEADBlockCipher cipher,
int bufSize)
{
super(is);
this.aeadBlockCipher = cipher;
this.inBuf = new byte[bufSize];
this.skippingCipher = (cipher instanceof SkippingCipher) ? (SkippingCipher)cipher : null;
}
/**
* Read data from underlying stream and process with cipher until end of stream or some data is
* available after cipher processing.
*
* @return -1 to indicate end of stream, or the number of bytes (> 0) available.
*/
private int nextChunk()
throws IOException
{
if (finalized)
{
return -1;
}
bufOff = 0;
maxBuf = 0;
// Keep reading until EOF or cipher processing produces data
while (maxBuf == 0)
{
int read = in.read(inBuf);
if (read == -1)
{
finaliseCipher();
if (maxBuf == 0)
{
return -1;
}
return maxBuf;
}
try
{
ensureCapacity(read, false);
if (bufferedBlockCipher != null)
{
maxBuf = bufferedBlockCipher.processBytes(inBuf, 0, read, buf, 0);
}
else if (aeadBlockCipher != null)
{
maxBuf = aeadBlockCipher.processBytes(inBuf, 0, read, buf, 0);
}
else
{
streamCipher.processBytes(inBuf, 0, read, buf, 0);
maxBuf = read;
}
}
catch (Exception e)
{
throw new CipherIOException("Error processing stream ", e);
}
}
return maxBuf;
}
private void finaliseCipher()
throws IOException
{
try
{
finalized = true;
ensureCapacity(0, true);
if (bufferedBlockCipher != null)
{
maxBuf = bufferedBlockCipher.doFinal(buf, 0);
}
else if (aeadBlockCipher != null)
{
maxBuf = aeadBlockCipher.doFinal(buf, 0);
}
else
{
maxBuf = 0; // a stream cipher
}
}
catch (final InvalidCipherTextException e)
{
throw new InvalidCipherTextIOException("Error finalising cipher", e);
}
catch (Exception e)
{
throw new IOException("Error finalising cipher " + e);
}
}
/**
* Reads data from the underlying stream and processes it with the cipher until the cipher
* outputs data, and returns the next available byte.
* <p>
* If the underlying stream is exhausted by this call, the cipher will be finalised.
* </p>
* @throws IOException if there was an error closing the input stream.
* @throws InvalidCipherTextIOException if the data read from the stream was invalid ciphertext
* (e.g. the cipher is an AEAD cipher and the ciphertext tag check fails).
*/
public int read()
throws IOException
{
if (bufOff >= maxBuf)
{
if (nextChunk() < 0)
{
return -1;
}
}
return buf[bufOff++] & 0xff;
}
/**
* Reads data from the underlying stream and processes it with the cipher until the cipher
* outputs data, and then returns up to <code>b.length</code> bytes in the provided array.
* <p>
* If the underlying stream is exhausted by this call, the cipher will be finalised.
* </p>
* @param b the buffer into which the data is read.
* @return the total number of bytes read into the buffer, or <code>-1</code> if there is no
* more data because the end of the stream has been reached.
* @throws IOException if there was an error closing the input stream.
* @throws InvalidCipherTextIOException if the data read from the stream was invalid ciphertext
* (e.g. the cipher is an AEAD cipher and the ciphertext tag check fails).
*/
public int read(
byte[] b)
throws IOException
{
return read(b, 0, b.length);
}
/**
* Reads data from the underlying stream and processes it with the cipher until the cipher
* outputs data, and then returns up to <code>len</code> bytes in the provided array.
* <p>
* If the underlying stream is exhausted by this call, the cipher will be finalised.
* </p>
* @param b the buffer into which the data is read.
* @param off the start offset in the destination array <code>b</code>
* @param len the maximum number of bytes read.
* @return the total number of bytes read into the buffer, or <code>-1</code> if there is no
* more data because the end of the stream has been reached.
* @throws IOException if there was an error closing the input stream.
* @throws InvalidCipherTextIOException if the data read from the stream was invalid ciphertext
* (e.g. the cipher is an AEAD cipher and the ciphertext tag check fails).
*/
public int read(
byte[] b,
int off,
int len)
throws IOException
{
if (bufOff >= maxBuf)
{
if (nextChunk() < 0)
{
return -1;
}
}
int toSupply = Math.min(len, available());
System.arraycopy(buf, bufOff, b, off, toSupply);
bufOff += toSupply;
return toSupply;
}
public long skip(
long n)
throws IOException
{
if (n <= 0)
{
return 0;
}
if (skippingCipher != null)
{
int avail = available();
if (n <= avail)
{
bufOff += n;
return n;
}
bufOff = maxBuf;
long skip = in.skip(n - avail);
long cSkip = skippingCipher.skip(skip);
if (skip != cSkip)
{
throw new IOException("Unable to skip cipher " + skip + " bytes.");
}
return skip + avail;
}
else
{
int skip = (int)Math.min(n, available());
bufOff += skip;
return skip;
}
}
public int available()
throws IOException
{
return maxBuf - bufOff;
}
/**
* Ensure the cipher text buffer has space sufficient to accept an upcoming output.
*
* @param updateSize the size of the pending update.
* @param finalOutput <code>true</code> iff this the cipher is to be finalised.
*/
private void ensureCapacity(int updateSize, boolean finalOutput)
{
int bufLen = updateSize;
if (finalOutput)
{
if (bufferedBlockCipher != null)
{
bufLen = bufferedBlockCipher.getOutputSize(updateSize);
}
else if (aeadBlockCipher != null)
{
bufLen = aeadBlockCipher.getOutputSize(updateSize);
}
}
else
{
if (bufferedBlockCipher != null)
{
bufLen = bufferedBlockCipher.getUpdateOutputSize(updateSize);
}
else if (aeadBlockCipher != null)
{
bufLen = aeadBlockCipher.getUpdateOutputSize(updateSize);
}
}
if ((buf == null) || (buf.length < bufLen))
{
buf = new byte[bufLen];
}
}
/**
* Closes the underlying input stream and finalises the processing of the data by the cipher.
*
* @throws IOException if there was an error closing the input stream.
* @throws InvalidCipherTextIOException if the data read from the stream was invalid ciphertext
* (e.g. the cipher is an AEAD cipher and the ciphertext tag check fails).
*/
public void close()
throws IOException
{
try
{
in.close();
}
finally
{
if (!finalized)
{
// Reset the cipher, discarding any data buffered in it
// Errors in cipher finalisation trump I/O error closing input
finaliseCipher();
}
}
maxBuf = bufOff = 0;
markBufOff = 0;
markPosition = 0;
if (markBuf != null)
{
Arrays.fill(markBuf, (byte)0);
markBuf = null;
}
if (buf != null)
{
Arrays.fill(buf, (byte)0);
buf = null;
}
Arrays.fill(inBuf, (byte)0);
}
/**
* Mark the current position.
* <p>
* This method only works if markSupported() returns true - which means the underlying stream supports marking, and the cipher passed
* in to this stream's constructor is a SkippingCipher (so capable of being reset to an arbitrary point easily).
* </p>
* @param readlimit the maximum read ahead required before a reset() may be called.
*/
public void mark(int readlimit)
{
in.mark(readlimit);
if (skippingCipher != null)
{
markPosition = skippingCipher.getPosition();
}
if (buf != null)
{
markBuf = new byte[buf.length];
System.arraycopy(buf, 0, markBuf, 0, buf.length);
}
markBufOff = bufOff;
}
/**
* Reset to the last marked position, if supported.
*
* @throws IOException if marking not supported by the cipher used, or the underlying stream.
*/
public void reset()
throws IOException
{
if (skippingCipher == null)
{
throw new IOException("cipher must implement SkippingCipher to be used with reset()");
}
in.reset();
skippingCipher.seekTo(markPosition);
if (markBuf != null)
{
buf = markBuf;
}
bufOff = markBufOff;
}
/**
* Return true if mark(readlimit) is supported. This will be true if the underlying stream supports marking and the
* cipher used is a SkippingCipher,
*
* @return true if mark(readlimit) supported, false otherwise.
*/
public boolean markSupported()
{
if (skippingCipher != null)
{
return in.markSupported();
}
return false;
}
}