PositionedCryptoInputStream.java

  1.  /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one
  3.  * or more contributor license agreements.  See the NOTICE file
  4.  * distributed with this work for additional information
  5.  * regarding copyright ownership.  The ASF licenses this file
  6.  * to you under the Apache License, Version 2.0 (the
  7.  * "License"); you may not use this file except in compliance
  8.  * with the License.  You may obtain a copy of the License at
  9.  *
  10.  *     http://www.apache.org/licenses/LICENSE-2.0
  11.  *
  12.  * Unless required by applicable law or agreed to in writing, software
  13.  * distributed under the License is distributed on an "AS IS" BASIS,
  14.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15.  * See the License for the specific language governing permissions and
  16.  * limitations under the License.
  17.  */
  18. package org.apache.commons.crypto.stream;

  19. import java.io.IOException;
  20. import java.nio.ByteBuffer;
  21. import java.security.GeneralSecurityException;
  22. import java.util.Properties;
  23. import java.util.Queue;
  24. import java.util.concurrent.ConcurrentLinkedQueue;

  25. import javax.crypto.Cipher;
  26. import javax.crypto.spec.IvParameterSpec;

  27. import org.apache.commons.crypto.cipher.CryptoCipher;
  28. import org.apache.commons.crypto.stream.input.Input;
  29. import org.apache.commons.crypto.utils.AES;
  30. import org.apache.commons.crypto.utils.IoUtils;
  31. import org.apache.commons.crypto.utils.Utils;

  32. /**
  33.  * PositionedCryptoInputStream provides the capability to decrypt the stream
  34.  * starting at random position as well as provides the foundation for positioned
  35.  * read for decrypting. This needs a stream cipher mode such as AES CTR mode.
  36.  */
  37. public class PositionedCryptoInputStream extends CtrCryptoInputStream {

  38.     private static class CipherState {

  39.         private final CryptoCipher cryptoCipher;
  40.         private boolean reset;

  41.         /**
  42.          * Constructs a new instance.
  43.          *
  44.          * @param cryptoCipher the CryptoCipher instance.
  45.          */
  46.         public CipherState(final CryptoCipher cryptoCipher) {
  47.             this.cryptoCipher = cryptoCipher;
  48.             this.reset = false;
  49.         }

  50.         /**
  51.          * Gets the CryptoCipher instance.
  52.          *
  53.          * @return the cipher.
  54.          */
  55.         public CryptoCipher getCryptoCipher() {
  56.             return cryptoCipher;
  57.         }

  58.         /**
  59.          * Gets the reset.
  60.          *
  61.          * @return the value of reset.
  62.          */
  63.         public boolean isReset() {
  64.             return reset;
  65.         }

  66.         /**
  67.          * Sets the value of reset.
  68.          *
  69.          * @param reset the reset.
  70.          */
  71.         public void reset(final boolean reset) {
  72.             this.reset = reset;
  73.         }
  74.     }

  75.     /**
  76.      * DirectBuffer pool
  77.      */
  78.     private final Queue<ByteBuffer> byteBufferPool = new ConcurrentLinkedQueue<>();

  79.     /**
  80.      * CryptoCipher pool
  81.      */
  82.     private final Queue<CipherState> cipherStatePool = new ConcurrentLinkedQueue<>();

  83.     /**
  84.      * properties for constructing a CryptoCipher
  85.      */
  86.     private final Properties properties;

  87.     /**
  88.      * Constructs a {@link PositionedCryptoInputStream}.
  89.      *
  90.      * @param properties The {@code Properties} class represents a set of
  91.      *        properties.
  92.      * @param in the input data.
  93.      * @param key crypto key for the cipher.
  94.      * @param iv Initialization vector for the cipher.
  95.      * @param streamOffset the start offset in the data.
  96.      * @throws IOException if an I/O error occurs.
  97.      */
  98.     @SuppressWarnings("resource") // The CryptoCipher returned by getCipherInstance() is closed by PositionedCryptoInputStream.
  99.     public PositionedCryptoInputStream(final Properties properties, final Input in, final byte[] key,
  100.             final byte[] iv, final long streamOffset) throws IOException {
  101.         this(properties, in, Utils.getCipherInstance(AES.CTR_NO_PADDING, properties),
  102.                 CryptoInputStream.getBufferSize(properties), key, iv, streamOffset);
  103.     }

  104.     /**
  105.      * Constructs a {@link PositionedCryptoInputStream}.
  106.      *
  107.      * @param properties the properties of stream
  108.      * @param input the input data.
  109.      * @param cipher the CryptoCipher instance.
  110.      * @param bufferSize the bufferSize.
  111.      * @param key crypto key for the cipher.
  112.      * @param iv Initialization vector for the cipher.
  113.      * @param streamOffset the start offset in the data.
  114.      * @throws IOException if an I/O error occurs.
  115.      */
  116.     protected PositionedCryptoInputStream(final Properties properties, final Input input, final CryptoCipher cipher,
  117.             final int bufferSize, final byte[] key, final byte[] iv, final long streamOffset)
  118.             throws IOException {
  119.         super(input, cipher, bufferSize, key, iv, streamOffset);
  120.         this.properties = properties;
  121.     }

  122.     /** Cleans direct buffer pool */
  123.     private void cleanByteBufferPool() {
  124.         ByteBuffer buf;
  125.         while ((buf = byteBufferPool.poll()) != null) {
  126.             CryptoInputStream.freeDirectBuffer(buf);
  127.         }
  128.     }

  129.     /** Cleans direct buffer pool */
  130.     private void cleanCipherStatePool() {
  131.         CipherState cs;
  132.         while ((cs = cipherStatePool.poll()) != null) {
  133.             try {
  134.                 cs.getCryptoCipher().close();
  135.             } catch (IOException ignored) {
  136.                 // ignore
  137.             }
  138.         }
  139.     }

  140.     /**
  141.      * Overrides the {@link CryptoInputStream#close()}. Closes this input stream
  142.      * and releases any system resources associated with the stream.
  143.      *
  144.      * @throws IOException if an I/O error occurs.
  145.      */
  146.     @Override
  147.     public void close() throws IOException {
  148.         if (!isOpen()) {
  149.             return;
  150.         }

  151.         cleanByteBufferPool();
  152.         cleanCipherStatePool();
  153.         super.close();
  154.     }

  155.     /**
  156.      * Does the decryption using inBuffer as input and outBuffer as output. Upon
  157.      * return, inBuffer is cleared; the decrypted data starts at
  158.      * outBuffer.position() and ends at outBuffer.limit().
  159.      *
  160.      * @param state the CipherState instance.
  161.      * @param inByteBuffer the input buffer.
  162.      * @param outByteBuffer the output buffer.
  163.      * @param padding the padding.
  164.      * @throws IOException if an I/O error occurs.
  165.      */
  166.     private void decrypt(final CipherState state, final ByteBuffer inByteBuffer,
  167.             final ByteBuffer outByteBuffer, final byte padding) throws IOException {
  168.         Utils.checkState(inByteBuffer.position() >= padding);
  169.         if (inByteBuffer.position() == padding) {
  170.             // There is no real data in inBuffer.
  171.             return;
  172.         }
  173.         inByteBuffer.flip();
  174.         outByteBuffer.clear();
  175.         decryptBuffer(state, inByteBuffer, outByteBuffer);
  176.         inByteBuffer.clear();
  177.         outByteBuffer.flip();
  178.         if (padding > 0) {
  179.             /*
  180.              * The plain text and cipher text have a 1:1 mapping, they start at
  181.              * the same position.
  182.              */
  183.             outByteBuffer.position(padding);
  184.         }
  185.     }

  186.     /**
  187.      * Decrypts length bytes in buffer starting at offset. Output is also put
  188.      * into buffer starting at offset. It is thread-safe.
  189.      *
  190.      * @param buffer the buffer into which the data is read.
  191.      * @param offset the start offset in the data.
  192.      * @param position the offset from the start of the stream.
  193.      * @param length the maximum number of bytes to read.
  194.      * @throws IOException if an I/O error occurs.
  195.      */
  196.     protected void decrypt(final long position, final byte[] buffer, final int offset, final int length)
  197.             throws IOException {
  198.         final ByteBuffer inByteBuffer = getBuffer();
  199.         final ByteBuffer outByteBuffer = getBuffer();
  200.         CipherState state = null;
  201.         try {
  202.             state = getCipherState();
  203.             final byte[] iv = getInitIV().clone();
  204.             resetCipher(state, position, iv);
  205.             byte padding = getPadding(position);
  206.             inByteBuffer.position(padding); // Set proper position for input data.

  207.             int n = 0;
  208.             while (n < length) {
  209.                 final int toDecrypt = Math.min(length - n, inByteBuffer.remaining());
  210.                 inByteBuffer.put(buffer, offset + n, toDecrypt);

  211.                 // Do decryption
  212.                 decrypt(state, inByteBuffer, outByteBuffer, padding);

  213.                 outByteBuffer.get(buffer, offset + n, toDecrypt);
  214.                 n += toDecrypt;
  215.                 padding = postDecryption(state, inByteBuffer, position + n, iv);
  216.             }
  217.         } finally {
  218.             returnToPool(inByteBuffer);
  219.             returnToPool(outByteBuffer);
  220.             returnToPool(state);
  221.         }
  222.     }

  223.     /**
  224.      * Does the decryption using inBuffer as input and outBuffer as output.
  225.      *
  226.      * @param state the CipherState instance.
  227.      * @param inByteBuffer the input buffer.
  228.      * @param outByteBuffer the output buffer.
  229.      * @throws IOException if an I/O error occurs.
  230.      */
  231.     @SuppressWarnings("resource") // getCryptoCipher does not allocate
  232.     private void decryptBuffer(final CipherState state, final ByteBuffer inByteBuffer,
  233.             final ByteBuffer outByteBuffer) throws IOException {
  234.         final int inputSize = inByteBuffer.remaining();
  235.         try {
  236.             final int n = state.getCryptoCipher().update(inByteBuffer, outByteBuffer);
  237.             if (n < inputSize) {
  238.                 /**
  239.                  * Typically code will not get here. CryptoCipher#update will
  240.                  * consume all input data and put result in outBuffer.
  241.                  * CryptoCipher#doFinal will reset the cipher context.
  242.                  */
  243.                 state.getCryptoCipher().doFinal(inByteBuffer, outByteBuffer);
  244.                 state.reset(true);
  245.             }
  246.         } catch (final GeneralSecurityException e) {
  247.             throw new IOException(e);
  248.         }
  249.     }

  250.     /**
  251.      * Gets direct buffer from pool. Caller MUST also call {@link #returnToPool(ByteBuffer)}.
  252.      *
  253.      * @return the buffer.
  254.      * @see #returnToPool(ByteBuffer)
  255.      */
  256.     private ByteBuffer getBuffer() {
  257.         final ByteBuffer buffer = byteBufferPool.poll();
  258.         return buffer != null ? buffer : ByteBuffer.allocateDirect(getBufferSize());
  259.     }

  260.     /**
  261.      * Gets CryptoCipher from pool. Caller MUST also call {@link #returnToPool(CipherState)}.
  262.      *
  263.      * @return the CipherState instance.
  264.      * @throws IOException if an I/O error occurs.
  265.      */
  266.     @SuppressWarnings("resource") // Caller calls #returnToPool(CipherState)
  267.     private CipherState getCipherState() throws IOException {
  268.         final CipherState state = cipherStatePool.poll();
  269.         return state != null ? state : new CipherState(Utils.getCipherInstance(AES.CTR_NO_PADDING, properties));
  270.     }

  271.     /**
  272.      * This method is executed immediately after decryption. Check whether
  273.      * cipher should be updated and recalculate padding if needed.
  274.      *
  275.      * @param state the CipherState instance.
  276.      * @param inByteBuffer the input buffer.
  277.      * @param position the offset from the start of the stream.
  278.      * @param iv the iv.
  279.      * @return the padding.
  280.      */
  281.     private byte postDecryption(final CipherState state, final ByteBuffer inByteBuffer,
  282.             final long position, final byte[] iv) {
  283.         byte padding = 0;
  284.         if (state.isReset()) {
  285.             /*
  286.              * This code is generally not executed since the cipher usually
  287.              * maintains cipher context (e.g. the counter) internally. However,
  288.              * some implementations can't maintain context so a re-init is
  289.              * necessary after each decryption call.
  290.              */
  291.             resetCipher(state, position, iv);
  292.             padding = getPadding(position);
  293.             inByteBuffer.position(padding);
  294.         }
  295.         return padding;
  296.     }

  297.     /**
  298.      * Reads up to the specified number of bytes from a given position within a
  299.      * stream and return the number of bytes read. This does not change the
  300.      * current offset of the stream, and is thread-safe.
  301.      *
  302.      * @param buffer the buffer into which the data is read.
  303.      * @param length the maximum number of bytes to read.
  304.      * @param offset the start offset in the data.
  305.      * @param position the offset from the start of the stream.
  306.      * @throws IOException if an I/O error occurs.
  307.      * @return int the total number of decrypted data bytes read into the
  308.      *         buffer.
  309.      */
  310.     public int read(final long position, final byte[] buffer, final int offset, final int length)
  311.             throws IOException {
  312.         checkStream();
  313.         final int n = input.read(position, buffer, offset, length);
  314.         if (n > 0) {
  315.             // This operation does not change the current offset of the file
  316.             decrypt(position, buffer, offset, n);
  317.         }
  318.         return n;
  319.     }

  320.     /**
  321.      * Reads the specified number of bytes from a given position within a
  322.      * stream. This does not change the current offset of the stream and is
  323.      * thread-safe.
  324.      *
  325.      * @param position the offset from the start of the stream.
  326.      * @param buffer the buffer into which the data is read.
  327.      * @throws IOException if an I/O error occurs.
  328.      */
  329.     public void readFully(final long position, final byte[] buffer) throws IOException {
  330.         readFully(position, buffer, 0, buffer.length);
  331.     }

  332.     /**
  333.      * Reads the specified number of bytes from a given position within a
  334.      * stream. This does not change the current offset of the stream and is
  335.      * thread-safe.
  336.      *
  337.      * @param buffer the buffer into which the data is read.
  338.      * @param length the maximum number of bytes to read.
  339.      * @param offset the start offset in the data.
  340.      * @param position the offset from the start of the stream.
  341.      * @throws IOException if an I/O error occurs.
  342.      */
  343.     public void readFully(final long position, final byte[] buffer, final int offset, final int length)
  344.             throws IOException {
  345.         checkStream();
  346.         IoUtils.readFully(input, position, buffer, offset, length);
  347.         if (length > 0) {
  348.             // This operation does not change the current offset of the file
  349.             decrypt(position, buffer, offset, length);
  350.         }
  351.     }

  352.     /**
  353.      * Calculates the counter and iv, reset the cipher.
  354.      *
  355.      * @param state the CipherState instance.
  356.      * @param position the offset from the start of the stream.
  357.      * @param iv the iv.
  358.      */
  359.     @SuppressWarnings("resource") // getCryptoCipher does not allocate
  360.     private void resetCipher(final CipherState state, final long position, final byte[] iv) {
  361.         final long counter = getCounter(position);
  362.         CtrCryptoInputStream.calculateIV(getInitIV(), counter, iv);
  363.         try {
  364.             state.getCryptoCipher().init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
  365.         } catch (final GeneralSecurityException e) {
  366.             // Ignore
  367.         }
  368.         state.reset(false);
  369.     }

  370.     /**
  371.      * Returns direct buffer to pool.
  372.      *
  373.      * @param buf the buffer.
  374.      */
  375.     private void returnToPool(final ByteBuffer buf) {
  376.         if (buf != null) {
  377.             buf.clear();
  378.             byteBufferPool.add(buf);
  379.         }
  380.     }

  381.     /**
  382.      * Returns CryptoCipher to pool.
  383.      *
  384.      * @param state the CipherState instance.
  385.      */
  386.     private void returnToPool(final CipherState state) {
  387.         if (state != null) {
  388.             cipherStatePool.add(state);
  389.         }
  390.     }
  391. }