CryptoInputStream.java
- /*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package org.apache.commons.crypto.stream;
- import java.io.IOException;
- import java.io.InputStream;
- import java.lang.reflect.Method;
- import java.nio.ByteBuffer;
- import java.nio.channels.ReadableByteChannel;
- import java.security.GeneralSecurityException;
- import java.security.Key;
- import java.security.spec.AlgorithmParameterSpec;
- import java.util.Objects;
- import java.util.Properties;
- import javax.crypto.BadPaddingException;
- import javax.crypto.Cipher;
- import javax.crypto.IllegalBlockSizeException;
- import javax.crypto.ShortBufferException;
- import javax.crypto.spec.IvParameterSpec;
- import org.apache.commons.crypto.Crypto;
- import org.apache.commons.crypto.cipher.CryptoCipher;
- import org.apache.commons.crypto.stream.input.ChannelInput;
- import org.apache.commons.crypto.stream.input.Input;
- import org.apache.commons.crypto.stream.input.StreamInput;
- import org.apache.commons.crypto.utils.AES;
- import org.apache.commons.crypto.utils.Utils;
- /**
- * CryptoInputStream reads input data and decrypts data in stream manner. It
- * supports any mode of operations such as AES CBC/CTR/GCM mode in concept.It is
- * not thread-safe.
- *
- */
- public class CryptoInputStream extends InputStream implements ReadableByteChannel {
- /**
- * The configuration key of the buffer size for stream.
- */
- public static final String STREAM_BUFFER_SIZE_KEY = Crypto.CONF_PREFIX
- + "stream.buffer.size";
- // stream related configuration keys
- /**
- * The default value of the buffer size for stream.
- */
- private static final int STREAM_BUFFER_SIZE_DEFAULT = 8192;
- private static final int MIN_BUFFER_SIZE = 512;
- /**
- * The index value when the end of the stream has been reached {@code -1}.
- *
- * @since 1.1
- */
- public static final int EOS = -1;
- /**
- * Checks and floors buffer size.
- *
- * @param cipher the {@link CryptoCipher} instance.
- * @param bufferSize the buffer size.
- * @return the remaining buffer size.
- */
- static int checkBufferSize(final CryptoCipher cipher, final int bufferSize) {
- Utils.checkArgument(bufferSize >= CryptoInputStream.MIN_BUFFER_SIZE,
- "Minimum value of buffer size is " + CryptoInputStream.MIN_BUFFER_SIZE + ".");
- return bufferSize - bufferSize % cipher.getBlockSize();
- }
- /**
- * Checks whether the cipher is supported streaming.
- *
- * @param cipher the {@link CryptoCipher} instance.
- * @throws IOException if an I/O error occurs.
- */
- static void checkStreamCipher(final CryptoCipher cipher) throws IOException {
- if (!cipher.getAlgorithm().equals(AES.CTR_NO_PADDING)) {
- throw new IOException(AES.CTR_NO_PADDING + " is required");
- }
- }
- /**
- * Forcibly free the direct buffer.
- *
- * @param buffer the bytebuffer to be freed.
- */
- static void freeDirectBuffer(final ByteBuffer buffer) {
- if (buffer != null) {
- try {
- /*
- * Using reflection to implement sun.nio.ch.DirectBuffer.cleaner() .clean();
- */
- final String SUN_CLASS = "sun.nio.ch.DirectBuffer";
- final Class<?>[] interfaces = buffer.getClass().getInterfaces();
- final Object[] EMPTY_OBJECT_ARRAY = {};
- for (final Class<?> clazz : interfaces) {
- if (clazz.getName().equals(SUN_CLASS)) {
- /* DirectBuffer#cleaner() */
- final Method getCleaner = Class.forName(SUN_CLASS).getMethod("cleaner");
- final Object cleaner = getCleaner.invoke(buffer, EMPTY_OBJECT_ARRAY);
- /* Cleaner#clean() */
- final Method cleanMethod = Class.forName("sun.misc.Cleaner").getMethod("clean");
- cleanMethod.invoke(cleaner, EMPTY_OBJECT_ARRAY);
- return;
- }
- }
- } catch (final ReflectiveOperationException e) { // NOPMD
- // Ignore the Reflection exception.
- }
- }
- }
- /**
- * Reads crypto buffer size.
- *
- * @param props The {@code Properties} class represents a set of
- * properties.
- * @return the buffer size.
- * */
- static int getBufferSize(final Properties props) {
- final String bufferSizeStr = props.getProperty(CryptoInputStream.STREAM_BUFFER_SIZE_KEY, "");
- return bufferSizeStr.isEmpty() ? CryptoInputStream.STREAM_BUFFER_SIZE_DEFAULT : Integer.parseInt(bufferSizeStr);
- }
- private final byte[] oneByteBuf = new byte[1];
- /** The CryptoCipher instance. */
- final CryptoCipher cipher; // package protected for access by crypto classes; do not expose further
- /** The buffer size. */
- private final int bufferSize;
- /** Crypto key for the cipher. */
- final Key key; // package protected for access by crypto classes; do not expose further
- /** the algorithm parameters */
- private final AlgorithmParameterSpec params;
- /** Flag to mark whether the input stream is closed. */
- private boolean closed;
- /**
- * Flag to mark whether do final of the cipher to end the decrypting stream.
- */
- private boolean finalDone;
- /** The input data. */
- Input input; // package protected for access by crypto classes; do not expose further
- /**
- * Input data buffer. The data starts at inBuffer.position() and ends at to
- * inBuffer.limit().
- */
- ByteBuffer inBuffer; // package protected for access by crypto classes; do not expose further
- /**
- * The decrypted data buffer. The data starts at outBuffer.position() and
- * ends at outBuffer.limit().
- */
- ByteBuffer outBuffer; // package protected for access by crypto classes; do not expose further
- /**
- * Constructs a {@link CryptoInputStream}.
- *
- * @param input the input data.
- * @param cipher the cipher instance.
- * @param bufferSize the bufferSize.
- * @param key crypto key for the cipher.
- * @param params the algorithm parameters.
- * @throws IOException if an I/O error occurs.
- */
- protected CryptoInputStream(final Input input, final CryptoCipher cipher, final int bufferSize,
- final Key key, final AlgorithmParameterSpec params) throws IOException {
- this.input = input;
- this.cipher = cipher;
- this.bufferSize = CryptoInputStream.checkBufferSize(cipher, bufferSize);
- this.key = key;
- this.params = params;
- if (!(params instanceof IvParameterSpec)) {
- // other AlgorithmParameterSpec such as GCMParameterSpec is not
- // supported now.
- throw new IOException("Illegal parameters");
- }
- inBuffer = ByteBuffer.allocateDirect(this.bufferSize);
- outBuffer = ByteBuffer.allocateDirect(this.bufferSize + cipher.getBlockSize());
- outBuffer.limit(0);
- initCipher();
- }
- /**
- * Constructs a {@link CryptoInputStream}.
- *
- * @param cipher the cipher instance.
- * @param inputStream the input stream.
- * @param bufferSize the bufferSize.
- * @param key crypto key for the cipher.
- * @param params the algorithm parameters.
- * @throws IOException if an I/O error occurs.
- */
- @SuppressWarnings("resource") // Closing the instance closes the StreamInput
- protected CryptoInputStream(final InputStream inputStream, final CryptoCipher cipher,
- final int bufferSize, final Key key, final AlgorithmParameterSpec params)
- throws IOException {
- this(new StreamInput(inputStream, bufferSize), cipher, bufferSize, key, params);
- }
- /**
- * Constructs a {@link CryptoInputStream}.
- *
- * @param channel the ReadableByteChannel instance.
- * @param cipher the cipher instance.
- * @param bufferSize the bufferSize.
- * @param key crypto key for the cipher.
- * @param params the algorithm parameters.
- * @throws IOException if an I/O error occurs.
- */
- @SuppressWarnings("resource") // Closing the instance closes the ChannelInput
- protected CryptoInputStream(final ReadableByteChannel channel, final CryptoCipher cipher,
- final int bufferSize, final Key key, final AlgorithmParameterSpec params)
- throws IOException {
- this(new ChannelInput(channel), cipher, bufferSize, key, params);
- }
- /**
- * Constructs a {@link CryptoInputStream}.
- *
- * @param transformation the name of the transformation, e.g.,
- * <i>AES/CBC/PKCS5Padding</i>.
- * See the Java Cryptography Architecture Standard Algorithm Name Documentation
- * for information about standard transformation names.
- * @param properties The {@code Properties} class represents a set of
- * properties.
- * @param inputStream the input stream.
- * @param key crypto key for the cipher.
- * @param params the algorithm parameters.
- * @throws IOException if an I/O error occurs.
- */
- @SuppressWarnings("resource") // The CryptoCipher returned by getCipherInstance() is closed by CryptoInputStream.
- public CryptoInputStream(final String transformation,
- final Properties properties, final InputStream inputStream, final Key key,
- final AlgorithmParameterSpec params) throws IOException {
- this(inputStream, Utils.getCipherInstance(transformation, properties),
- CryptoInputStream.getBufferSize(properties), key, params);
- }
- /**
- * Constructs a {@link CryptoInputStream}.
- *
- * @param transformation the name of the transformation, e.g.,
- * <i>AES/CBC/PKCS5Padding</i>.
- * See the Java Cryptography Architecture Standard Algorithm Name Documentation
- * for information about standard transformation names.
- * @param properties The {@code Properties} class represents a set of
- * properties.
- * @param channel the ReadableByteChannel object.
- * @param key crypto key for the cipher.
- * @param params the algorithm parameters.
- * @throws IOException if an I/O error occurs.
- */
- @SuppressWarnings("resource") // The CryptoCipher returned by getCipherInstance() is closed by CryptoInputStream.
- public CryptoInputStream(final String transformation,
- final Properties properties, final ReadableByteChannel channel, final Key key,
- final AlgorithmParameterSpec params) throws IOException {
- this(channel, Utils.getCipherInstance(transformation, properties), CryptoInputStream
- .getBufferSize(properties), key, params);
- }
- /**
- * Overrides the {@link InputStream#available()}. Returns an estimate of the
- * number of bytes that can be read (or skipped over) from this input stream
- * without blocking by the next invocation of a method for this input
- * stream.
- *
- * @return an estimate of the number of bytes that can be read (or skipped
- * over) from this input stream without blocking or {@code 0} when
- * it reaches the end of the input stream.
- * @throws IOException if an I/O error occurs.
- */
- @Override
- public int available() throws IOException {
- checkStream();
- return input.available() + outBuffer.remaining();
- }
- /**
- * Checks whether the stream is closed.
- *
- * @throws IOException if an I/O error occurs.
- */
- protected void checkStream() throws IOException {
- if (closed) {
- throw new IOException("Stream closed");
- }
- }
- /**
- * Overrides the {@link InputStream#close()}. Closes this input stream and
- * releases any system resources associated with the stream.
- *
- * @throws IOException if an I/O error occurs.
- */
- @Override
- public void close() throws IOException {
- if (closed) {
- return;
- }
- input.close();
- freeBuffers();
- cipher.close();
- super.close();
- closed = true;
- }
- /**
- * Does the decryption using inBuffer as input and outBuffer as output. Upon
- * return, inBuffer is cleared; the decrypted data starts at
- * outBuffer.position() and ends at outBuffer.limit().
- *
- * @throws IOException if an I/O error occurs.
- */
- protected void decrypt() throws IOException {
- // Prepare the input buffer and clear the out buffer
- inBuffer.flip();
- outBuffer.clear();
- try {
- cipher.update(inBuffer, outBuffer);
- } catch (final ShortBufferException e) {
- throw new IOException(e);
- }
- // Clear the input buffer and prepare out buffer
- inBuffer.clear();
- outBuffer.flip();
- }
- /**
- * Does final of the cipher to end the decrypting stream.
- *
- * @throws IOException if an I/O error occurs.
- */
- protected void decryptFinal() throws IOException {
- // Prepare the input buffer and clear the out buffer
- inBuffer.flip();
- outBuffer.clear();
- try {
- cipher.doFinal(inBuffer, outBuffer);
- finalDone = true;
- } catch (final ShortBufferException | IllegalBlockSizeException | BadPaddingException e) {
- throw new IOException(e);
- }
- // Clear the input buffer and prepare out buffer
- inBuffer.clear();
- outBuffer.flip();
- }
- /**
- * Decrypts more data by reading the under layer stream. The decrypted data
- * will be put in the output buffer. If the end of the under stream reached,
- * we will do final of the cipher to finish all the decrypting of data.
- *
- * @return The number of decrypted data.
- * return -1 (if end of the decrypted stream)
- * return 0 (no data now, but could have more later)
- * @throws IOException if an I/O error occurs.
- */
- protected int decryptMore() throws IOException {
- if (finalDone) {
- return EOS;
- }
- final int n = input.read(inBuffer);
- if (n < 0) {
- // The stream is end, finalize the cipher stream
- decryptFinal();
- // Satisfy the read with the remaining
- final int remaining = outBuffer.remaining();
- if (remaining > 0) {
- return remaining;
- }
- // End of the stream
- return EOS;
- }
- if (n == 0) {
- // No data is read, but the stream is not end yet
- return 0;
- }
- decrypt();
- return outBuffer.remaining();
- }
- /** Forcibly free the direct buffers. */
- protected void freeBuffers() {
- CryptoInputStream.freeDirectBuffer(inBuffer);
- CryptoInputStream.freeDirectBuffer(outBuffer);
- }
- /**
- * Gets the buffer size.
- *
- * @return the bufferSize.
- */
- protected int getBufferSize() {
- return bufferSize;
- }
- /**
- * Gets the internal CryptoCipher.
- *
- * @return the cipher instance.
- */
- protected CryptoCipher getCipher() {
- return cipher;
- }
- /**
- * Gets the input.
- *
- * @return the input.
- */
- protected Input getInput() {
- return input;
- }
- /**
- * Gets the key.
- *
- * @return the key.
- */
- protected Key getKey() {
- return key;
- }
- /**
- * Gets the specification of cryptographic parameters.
- *
- * @return the params.
- */
- protected AlgorithmParameterSpec getParams() {
- return params;
- }
- /**
- * Initializes the cipher.
- *
- * @throws IOException if an I/O error occurs.
- */
- protected void initCipher() throws IOException {
- try {
- cipher.init(Cipher.DECRYPT_MODE, key, params);
- } catch (final GeneralSecurityException e) {
- throw new IOException(e);
- }
- }
- /**
- * Overrides the {@link java.nio.channels.Channel#isOpen()}.
- *
- * @return {@code true} if, and only if, this channel is open.
- */
- @Override
- public boolean isOpen() {
- return !closed;
- }
- /**
- * Overrides the {@link InputStream#markSupported()}.
- *
- * @return false,the {@link CtrCryptoInputStream} don't support the mark
- * method.
- */
- @Override
- public boolean markSupported() {
- return false;
- }
- /**
- * Overrides the {@link java.io.InputStream#read()}. Reads the next byte of
- * data from the input stream.
- *
- * @return the next byte of data, or {@code EOS (-1)} if the end of the
- * stream is reached.
- * @throws IOException if an I/O error occurs.
- */
- @Override
- public int read() throws IOException {
- int n;
- while ((n = read(oneByteBuf, 0, 1)) == 0) { //NOPMD
- /* no op */
- }
- return n == EOS ? EOS : oneByteBuf[0] & 0xff;
- }
- /**
- * Overrides the {@link java.io.InputStream#read(byte[], int, int)}.
- * Decryption is buffer based. If there is data in {@link #outBuffer}, then
- * read it out of this buffer. If there is no data in {@link #outBuffer},
- * then read more from the underlying stream and do the decryption.
- *
- * @param array the buffer into which the decrypted data is read.
- * @param off the buffer offset.
- * @param len the maximum number of decrypted data bytes to read.
- * @return int the total number of decrypted data bytes read into the
- * buffer.
- * @throws IOException if an I/O error occurs.
- */
- @Override
- public int read(final byte[] array, final int off, final int len) throws IOException {
- checkStream();
- Objects.requireNonNull(array, "array");
- if (off < 0 || len < 0 || len > array.length - off) {
- throw new IndexOutOfBoundsException();
- }
- if (len == 0) {
- return 0;
- }
- final int remaining = outBuffer.remaining();
- if (remaining > 0) {
- // Satisfy the read with the existing data
- final int n = Math.min(len, remaining);
- outBuffer.get(array, off, n);
- return n;
- }
- // No data in the out buffer, try read new data and decrypt it
- // we loop for new data
- int nd = 0;
- while (nd == 0) {
- nd = decryptMore();
- }
- if (nd < 0) {
- return nd;
- }
- final int n = Math.min(len, outBuffer.remaining());
- outBuffer.get(array, off, n);
- return n;
- }
- /**
- * Overrides the
- * {@link java.nio.channels.ReadableByteChannel#read(ByteBuffer)}. Reads a
- * sequence of bytes from this channel into the given buffer.
- *
- * @param dst The buffer into which bytes are to be transferred.
- * @return The number of bytes read, possibly zero, or {@code EOS (-1)} if the
- * channel has reached end-of-stream.
- * @throws IOException if an I/O error occurs.
- */
- @Override
- public int read(final ByteBuffer dst) throws IOException {
- checkStream();
- int remaining = outBuffer.remaining();
- if (remaining <= 0) {
- // Decrypt more data
- // we loop for new data
- int nd = 0;
- while (nd == 0) {
- nd = decryptMore();
- }
- if (nd < 0) {
- return EOS;
- }
- }
- // Copy decrypted data from outBuffer to dst
- remaining = outBuffer.remaining();
- final int toRead = dst.remaining();
- if (toRead <= remaining) {
- final int limit = outBuffer.limit();
- outBuffer.limit(outBuffer.position() + toRead);
- dst.put(outBuffer);
- outBuffer.limit(limit);
- return toRead;
- }
- dst.put(outBuffer);
- return remaining;
- }
- /**
- * Overrides the {@link java.io.InputStream#skip(long)}. Skips over and
- * discards {@code n} bytes of data from this input stream.
- *
- * @param n the number of bytes to be skipped.
- * @return the actual number of bytes skipped.
- * @throws IOException if an I/O error occurs.
- */
- @Override
- public long skip(final long n) throws IOException {
- Utils.checkArgument(n >= 0, "Negative skip length.");
- checkStream();
- if (n == 0) {
- return 0;
- }
- long remaining = n;
- int nd;
- while (remaining > 0) {
- if (remaining <= outBuffer.remaining()) {
- // Skip in the remaining buffer
- final int pos = outBuffer.position() + (int) remaining;
- outBuffer.position(pos);
- remaining = 0;
- break;
- }
- remaining -= outBuffer.remaining();
- outBuffer.clear();
- // we loop for new data
- nd = 0;
- while (nd == 0) {
- nd = decryptMore();
- }
- if (nd < 0) {
- break;
- }
- }
- return n - remaining;
- }
- }