PositionedCryptoInputStream.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.nio.ByteBuffer;
- import java.security.GeneralSecurityException;
- import java.util.Properties;
- import java.util.Queue;
- import java.util.concurrent.ConcurrentLinkedQueue;
- import javax.crypto.Cipher;
- import javax.crypto.spec.IvParameterSpec;
- import org.apache.commons.crypto.cipher.CryptoCipher;
- import org.apache.commons.crypto.stream.input.Input;
- import org.apache.commons.crypto.utils.AES;
- import org.apache.commons.crypto.utils.IoUtils;
- import org.apache.commons.crypto.utils.Utils;
- /**
- * PositionedCryptoInputStream provides the capability to decrypt the stream
- * starting at random position as well as provides the foundation for positioned
- * read for decrypting. This needs a stream cipher mode such as AES CTR mode.
- */
- public class PositionedCryptoInputStream extends CtrCryptoInputStream {
- private static class CipherState {
- private final CryptoCipher cryptoCipher;
- private boolean reset;
- /**
- * Constructs a new instance.
- *
- * @param cryptoCipher the CryptoCipher instance.
- */
- public CipherState(final CryptoCipher cryptoCipher) {
- this.cryptoCipher = cryptoCipher;
- this.reset = false;
- }
- /**
- * Gets the CryptoCipher instance.
- *
- * @return the cipher.
- */
- public CryptoCipher getCryptoCipher() {
- return cryptoCipher;
- }
- /**
- * Gets the reset.
- *
- * @return the value of reset.
- */
- public boolean isReset() {
- return reset;
- }
- /**
- * Sets the value of reset.
- *
- * @param reset the reset.
- */
- public void reset(final boolean reset) {
- this.reset = reset;
- }
- }
- /**
- * DirectBuffer pool
- */
- private final Queue<ByteBuffer> byteBufferPool = new ConcurrentLinkedQueue<>();
- /**
- * CryptoCipher pool
- */
- private final Queue<CipherState> cipherStatePool = new ConcurrentLinkedQueue<>();
- /**
- * properties for constructing a CryptoCipher
- */
- private final Properties properties;
- /**
- * Constructs a {@link PositionedCryptoInputStream}.
- *
- * @param properties The {@code Properties} class represents a set of
- * properties.
- * @param in the input data.
- * @param key crypto key for the cipher.
- * @param iv Initialization vector for the cipher.
- * @param streamOffset the start offset in the data.
- * @throws IOException if an I/O error occurs.
- */
- @SuppressWarnings("resource") // The CryptoCipher returned by getCipherInstance() is closed by PositionedCryptoInputStream.
- public PositionedCryptoInputStream(final Properties properties, final Input in, final byte[] key,
- final byte[] iv, final long streamOffset) throws IOException {
- this(properties, in, Utils.getCipherInstance(AES.CTR_NO_PADDING, properties),
- CryptoInputStream.getBufferSize(properties), key, iv, streamOffset);
- }
- /**
- * Constructs a {@link PositionedCryptoInputStream}.
- *
- * @param properties the properties of stream
- * @param input the input data.
- * @param cipher the CryptoCipher instance.
- * @param bufferSize the bufferSize.
- * @param key crypto key for the cipher.
- * @param iv Initialization vector for the cipher.
- * @param streamOffset the start offset in the data.
- * @throws IOException if an I/O error occurs.
- */
- protected PositionedCryptoInputStream(final Properties properties, final Input input, final CryptoCipher cipher,
- final int bufferSize, final byte[] key, final byte[] iv, final long streamOffset)
- throws IOException {
- super(input, cipher, bufferSize, key, iv, streamOffset);
- this.properties = properties;
- }
- /** Cleans direct buffer pool */
- private void cleanByteBufferPool() {
- ByteBuffer buf;
- while ((buf = byteBufferPool.poll()) != null) {
- CryptoInputStream.freeDirectBuffer(buf);
- }
- }
- /** Cleans direct buffer pool */
- private void cleanCipherStatePool() {
- CipherState cs;
- while ((cs = cipherStatePool.poll()) != null) {
- try {
- cs.getCryptoCipher().close();
- } catch (IOException ignored) {
- // ignore
- }
- }
- }
- /**
- * Overrides the {@link CryptoInputStream#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 (!isOpen()) {
- return;
- }
- cleanByteBufferPool();
- cleanCipherStatePool();
- super.close();
- }
- /**
- * 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().
- *
- * @param state the CipherState instance.
- * @param inByteBuffer the input buffer.
- * @param outByteBuffer the output buffer.
- * @param padding the padding.
- * @throws IOException if an I/O error occurs.
- */
- private void decrypt(final CipherState state, final ByteBuffer inByteBuffer,
- final ByteBuffer outByteBuffer, final byte padding) throws IOException {
- Utils.checkState(inByteBuffer.position() >= padding);
- if (inByteBuffer.position() == padding) {
- // There is no real data in inBuffer.
- return;
- }
- inByteBuffer.flip();
- outByteBuffer.clear();
- decryptBuffer(state, inByteBuffer, outByteBuffer);
- inByteBuffer.clear();
- outByteBuffer.flip();
- if (padding > 0) {
- /*
- * The plain text and cipher text have a 1:1 mapping, they start at
- * the same position.
- */
- outByteBuffer.position(padding);
- }
- }
- /**
- * Decrypts length bytes in buffer starting at offset. Output is also put
- * into buffer starting at offset. It is thread-safe.
- *
- * @param buffer the buffer into which the data is read.
- * @param offset the start offset in the data.
- * @param position the offset from the start of the stream.
- * @param length the maximum number of bytes to read.
- * @throws IOException if an I/O error occurs.
- */
- protected void decrypt(final long position, final byte[] buffer, final int offset, final int length)
- throws IOException {
- final ByteBuffer inByteBuffer = getBuffer();
- final ByteBuffer outByteBuffer = getBuffer();
- CipherState state = null;
- try {
- state = getCipherState();
- final byte[] iv = getInitIV().clone();
- resetCipher(state, position, iv);
- byte padding = getPadding(position);
- inByteBuffer.position(padding); // Set proper position for input data.
- int n = 0;
- while (n < length) {
- final int toDecrypt = Math.min(length - n, inByteBuffer.remaining());
- inByteBuffer.put(buffer, offset + n, toDecrypt);
- // Do decryption
- decrypt(state, inByteBuffer, outByteBuffer, padding);
- outByteBuffer.get(buffer, offset + n, toDecrypt);
- n += toDecrypt;
- padding = postDecryption(state, inByteBuffer, position + n, iv);
- }
- } finally {
- returnToPool(inByteBuffer);
- returnToPool(outByteBuffer);
- returnToPool(state);
- }
- }
- /**
- * Does the decryption using inBuffer as input and outBuffer as output.
- *
- * @param state the CipherState instance.
- * @param inByteBuffer the input buffer.
- * @param outByteBuffer the output buffer.
- * @throws IOException if an I/O error occurs.
- */
- @SuppressWarnings("resource") // getCryptoCipher does not allocate
- private void decryptBuffer(final CipherState state, final ByteBuffer inByteBuffer,
- final ByteBuffer outByteBuffer) throws IOException {
- final int inputSize = inByteBuffer.remaining();
- try {
- final int n = state.getCryptoCipher().update(inByteBuffer, outByteBuffer);
- if (n < inputSize) {
- /**
- * Typically code will not get here. CryptoCipher#update will
- * consume all input data and put result in outBuffer.
- * CryptoCipher#doFinal will reset the cipher context.
- */
- state.getCryptoCipher().doFinal(inByteBuffer, outByteBuffer);
- state.reset(true);
- }
- } catch (final GeneralSecurityException e) {
- throw new IOException(e);
- }
- }
- /**
- * Gets direct buffer from pool. Caller MUST also call {@link #returnToPool(ByteBuffer)}.
- *
- * @return the buffer.
- * @see #returnToPool(ByteBuffer)
- */
- private ByteBuffer getBuffer() {
- final ByteBuffer buffer = byteBufferPool.poll();
- return buffer != null ? buffer : ByteBuffer.allocateDirect(getBufferSize());
- }
- /**
- * Gets CryptoCipher from pool. Caller MUST also call {@link #returnToPool(CipherState)}.
- *
- * @return the CipherState instance.
- * @throws IOException if an I/O error occurs.
- */
- @SuppressWarnings("resource") // Caller calls #returnToPool(CipherState)
- private CipherState getCipherState() throws IOException {
- final CipherState state = cipherStatePool.poll();
- return state != null ? state : new CipherState(Utils.getCipherInstance(AES.CTR_NO_PADDING, properties));
- }
- /**
- * This method is executed immediately after decryption. Check whether
- * cipher should be updated and recalculate padding if needed.
- *
- * @param state the CipherState instance.
- * @param inByteBuffer the input buffer.
- * @param position the offset from the start of the stream.
- * @param iv the iv.
- * @return the padding.
- */
- private byte postDecryption(final CipherState state, final ByteBuffer inByteBuffer,
- final long position, final byte[] iv) {
- byte padding = 0;
- if (state.isReset()) {
- /*
- * This code is generally not executed since the cipher usually
- * maintains cipher context (e.g. the counter) internally. However,
- * some implementations can't maintain context so a re-init is
- * necessary after each decryption call.
- */
- resetCipher(state, position, iv);
- padding = getPadding(position);
- inByteBuffer.position(padding);
- }
- return padding;
- }
- /**
- * Reads up to the specified number of bytes from a given position within a
- * stream and return the number of bytes read. This does not change the
- * current offset of the stream, and is thread-safe.
- *
- * @param buffer the buffer into which the data is read.
- * @param length the maximum number of bytes to read.
- * @param offset the start offset in the data.
- * @param position the offset from the start of the stream.
- * @throws IOException if an I/O error occurs.
- * @return int the total number of decrypted data bytes read into the
- * buffer.
- */
- public int read(final long position, final byte[] buffer, final int offset, final int length)
- throws IOException {
- checkStream();
- final int n = input.read(position, buffer, offset, length);
- if (n > 0) {
- // This operation does not change the current offset of the file
- decrypt(position, buffer, offset, n);
- }
- return n;
- }
- /**
- * Reads the specified number of bytes from a given position within a
- * stream. This does not change the current offset of the stream and is
- * thread-safe.
- *
- * @param position the offset from the start of the stream.
- * @param buffer the buffer into which the data is read.
- * @throws IOException if an I/O error occurs.
- */
- public void readFully(final long position, final byte[] buffer) throws IOException {
- readFully(position, buffer, 0, buffer.length);
- }
- /**
- * Reads the specified number of bytes from a given position within a
- * stream. This does not change the current offset of the stream and is
- * thread-safe.
- *
- * @param buffer the buffer into which the data is read.
- * @param length the maximum number of bytes to read.
- * @param offset the start offset in the data.
- * @param position the offset from the start of the stream.
- * @throws IOException if an I/O error occurs.
- */
- public void readFully(final long position, final byte[] buffer, final int offset, final int length)
- throws IOException {
- checkStream();
- IoUtils.readFully(input, position, buffer, offset, length);
- if (length > 0) {
- // This operation does not change the current offset of the file
- decrypt(position, buffer, offset, length);
- }
- }
- /**
- * Calculates the counter and iv, reset the cipher.
- *
- * @param state the CipherState instance.
- * @param position the offset from the start of the stream.
- * @param iv the iv.
- */
- @SuppressWarnings("resource") // getCryptoCipher does not allocate
- private void resetCipher(final CipherState state, final long position, final byte[] iv) {
- final long counter = getCounter(position);
- CtrCryptoInputStream.calculateIV(getInitIV(), counter, iv);
- try {
- state.getCryptoCipher().init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
- } catch (final GeneralSecurityException e) {
- // Ignore
- }
- state.reset(false);
- }
- /**
- * Returns direct buffer to pool.
- *
- * @param buf the buffer.
- */
- private void returnToPool(final ByteBuffer buf) {
- if (buf != null) {
- buf.clear();
- byteBufferPool.add(buf);
- }
- }
- /**
- * Returns CryptoCipher to pool.
- *
- * @param state the CipherState instance.
- */
- private void returnToPool(final CipherState state) {
- if (state != null) {
- cipherStatePool.add(state);
- }
- }
- }