| 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; |
| } |
| |
| } |