View Javadoc
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  
20  import java.io.IOException;
21  import java.nio.ByteBuffer;
22  import java.security.GeneralSecurityException;
23  import java.util.Properties;
24  import java.util.Queue;
25  import java.util.concurrent.ConcurrentLinkedQueue;
26  
27  import javax.crypto.Cipher;
28  import javax.crypto.spec.IvParameterSpec;
29  
30  import org.apache.commons.crypto.cipher.CryptoCipher;
31  import org.apache.commons.crypto.stream.input.Input;
32  import org.apache.commons.crypto.utils.AES;
33  import org.apache.commons.crypto.utils.IoUtils;
34  import org.apache.commons.crypto.utils.Utils;
35  
36  /**
37   * PositionedCryptoInputStream provides the capability to decrypt the stream
38   * starting at random position as well as provides the foundation for positioned
39   * read for decrypting. This needs a stream cipher mode such as AES CTR mode.
40   */
41  public class PositionedCryptoInputStream extends CtrCryptoInputStream {
42  
43      private static class CipherState {
44  
45          private final CryptoCipher cryptoCipher;
46          private boolean reset;
47  
48          /**
49           * Constructs a new instance.
50           *
51           * @param cryptoCipher the CryptoCipher instance.
52           */
53          public CipherState(final CryptoCipher cryptoCipher) {
54              this.cryptoCipher = cryptoCipher;
55              this.reset = false;
56          }
57  
58          /**
59           * Gets the CryptoCipher instance.
60           *
61           * @return the cipher.
62           */
63          public CryptoCipher getCryptoCipher() {
64              return cryptoCipher;
65          }
66  
67          /**
68           * Gets the reset.
69           *
70           * @return the value of reset.
71           */
72          public boolean isReset() {
73              return reset;
74          }
75  
76          /**
77           * Sets the value of reset.
78           *
79           * @param reset the reset.
80           */
81          public void reset(final boolean reset) {
82              this.reset = reset;
83          }
84      }
85  
86      /**
87       * DirectBuffer pool
88       */
89      private final Queue<ByteBuffer> byteBufferPool = new ConcurrentLinkedQueue<>();
90  
91      /**
92       * CryptoCipher pool
93       */
94      private final Queue<CipherState> cipherStatePool = new ConcurrentLinkedQueue<>();
95  
96      /**
97       * properties for constructing a CryptoCipher
98       */
99      private final Properties properties;
100 
101     /**
102      * Constructs a {@link PositionedCryptoInputStream}.
103      *
104      * @param properties The {@code Properties} class represents a set of
105      *        properties.
106      * @param in the input data.
107      * @param key crypto key for the cipher.
108      * @param iv Initialization vector for the cipher.
109      * @param streamOffset the start offset in the data.
110      * @throws IOException if an I/O error occurs.
111      */
112     @SuppressWarnings("resource") // The CryptoCipher returned by getCipherInstance() is closed by PositionedCryptoInputStream.
113     public PositionedCryptoInputStream(final Properties properties, final Input in, final byte[] key,
114             final byte[] iv, final long streamOffset) throws IOException {
115         this(properties, in, Utils.getCipherInstance(AES.CTR_NO_PADDING, properties),
116                 CryptoInputStream.getBufferSize(properties), key, iv, streamOffset);
117     }
118 
119     /**
120      * Constructs a {@link PositionedCryptoInputStream}.
121      *
122      * @param properties the properties of stream
123      * @param input the input data.
124      * @param cipher the CryptoCipher instance.
125      * @param bufferSize the bufferSize.
126      * @param key crypto key for the cipher.
127      * @param iv Initialization vector for the cipher.
128      * @param streamOffset the start offset in the data.
129      * @throws IOException if an I/O error occurs.
130      */
131     protected PositionedCryptoInputStream(final Properties properties, final Input input, final CryptoCipher cipher,
132             final int bufferSize, final byte[] key, final byte[] iv, final long streamOffset)
133             throws IOException {
134         super(input, cipher, bufferSize, key, iv, streamOffset);
135         this.properties = properties;
136     }
137 
138     /** Cleans direct buffer pool */
139     private void cleanByteBufferPool() {
140         ByteBuffer buf;
141         while ((buf = byteBufferPool.poll()) != null) {
142             CryptoInputStream.freeDirectBuffer(buf);
143         }
144     }
145 
146     /** Cleans direct buffer pool */
147     private void cleanCipherStatePool() {
148         CipherState cs;
149         while ((cs = cipherStatePool.poll()) != null) {
150             try {
151                 cs.getCryptoCipher().close();
152             } catch (IOException ignored) {
153                 // ignore
154             }
155         }
156     }
157 
158     /**
159      * Overrides the {@link CryptoInputStream#close()}. Closes this input stream
160      * and releases any system resources associated with the stream.
161      *
162      * @throws IOException if an I/O error occurs.
163      */
164     @Override
165     public void close() throws IOException {
166         if (!isOpen()) {
167             return;
168         }
169 
170         cleanByteBufferPool();
171         cleanCipherStatePool();
172         super.close();
173     }
174 
175     /**
176      * Does the decryption using inBuffer as input and outBuffer as output. Upon
177      * return, inBuffer is cleared; the decrypted data starts at
178      * outBuffer.position() and ends at outBuffer.limit().
179      *
180      * @param state the CipherState instance.
181      * @param inByteBuffer the input buffer.
182      * @param outByteBuffer the output buffer.
183      * @param padding the padding.
184      * @throws IOException if an I/O error occurs.
185      */
186     private void decrypt(final CipherState state, final ByteBuffer inByteBuffer,
187             final ByteBuffer outByteBuffer, final byte padding) throws IOException {
188         Utils.checkState(inByteBuffer.position() >= padding);
189         if (inByteBuffer.position() == padding) {
190             // There is no real data in inBuffer.
191             return;
192         }
193         inByteBuffer.flip();
194         outByteBuffer.clear();
195         decryptBuffer(state, inByteBuffer, outByteBuffer);
196         inByteBuffer.clear();
197         outByteBuffer.flip();
198         if (padding > 0) {
199             /*
200              * The plain text and cipher text have a 1:1 mapping, they start at
201              * the same position.
202              */
203             outByteBuffer.position(padding);
204         }
205     }
206 
207     /**
208      * Decrypts length bytes in buffer starting at offset. Output is also put
209      * into buffer starting at offset. It is thread-safe.
210      *
211      * @param buffer the buffer into which the data is read.
212      * @param offset the start offset in the data.
213      * @param position the offset from the start of the stream.
214      * @param length the maximum number of bytes to read.
215      * @throws IOException if an I/O error occurs.
216      */
217     protected void decrypt(final long position, final byte[] buffer, final int offset, final int length)
218             throws IOException {
219         final ByteBuffer inByteBuffer = getBuffer();
220         final ByteBuffer outByteBuffer = getBuffer();
221         CipherState state = null;
222         try {
223             state = getCipherState();
224             final byte[] iv = getInitIV().clone();
225             resetCipher(state, position, iv);
226             byte padding = getPadding(position);
227             inByteBuffer.position(padding); // Set proper position for input data.
228 
229             int n = 0;
230             while (n < length) {
231                 final int toDecrypt = Math.min(length - n, inByteBuffer.remaining());
232                 inByteBuffer.put(buffer, offset + n, toDecrypt);
233 
234                 // Do decryption
235                 decrypt(state, inByteBuffer, outByteBuffer, padding);
236 
237                 outByteBuffer.get(buffer, offset + n, toDecrypt);
238                 n += toDecrypt;
239                 padding = postDecryption(state, inByteBuffer, position + n, iv);
240             }
241         } finally {
242             returnToPool(inByteBuffer);
243             returnToPool(outByteBuffer);
244             returnToPool(state);
245         }
246     }
247 
248     /**
249      * Does the decryption using inBuffer as input and outBuffer as output.
250      *
251      * @param state the CipherState instance.
252      * @param inByteBuffer the input buffer.
253      * @param outByteBuffer the output buffer.
254      * @throws IOException if an I/O error occurs.
255      */
256     @SuppressWarnings("resource") // getCryptoCipher does not allocate
257     private void decryptBuffer(final CipherState state, final ByteBuffer inByteBuffer,
258             final ByteBuffer outByteBuffer) throws IOException {
259         final int inputSize = inByteBuffer.remaining();
260         try {
261             final int n = state.getCryptoCipher().update(inByteBuffer, outByteBuffer);
262             if (n < inputSize) {
263                 /**
264                  * Typically code will not get here. CryptoCipher#update will
265                  * consume all input data and put result in outBuffer.
266                  * CryptoCipher#doFinal will reset the cipher context.
267                  */
268                 state.getCryptoCipher().doFinal(inByteBuffer, outByteBuffer);
269                 state.reset(true);
270             }
271         } catch (final GeneralSecurityException e) {
272             throw new IOException(e);
273         }
274     }
275 
276     /**
277      * Gets direct buffer from pool. Caller MUST also call {@link #returnToPool(ByteBuffer)}.
278      *
279      * @return the buffer.
280      * @see #returnToPool(ByteBuffer)
281      */
282     private ByteBuffer getBuffer() {
283         final ByteBuffer buffer = byteBufferPool.poll();
284         return buffer != null ? buffer : ByteBuffer.allocateDirect(getBufferSize());
285     }
286 
287     /**
288      * Gets CryptoCipher from pool. Caller MUST also call {@link #returnToPool(CipherState)}.
289      *
290      * @return the CipherState instance.
291      * @throws IOException if an I/O error occurs.
292      */
293     @SuppressWarnings("resource") // Caller calls #returnToPool(CipherState)
294     private CipherState getCipherState() throws IOException {
295         final CipherState state = cipherStatePool.poll();
296         return state != null ? state : new CipherState(Utils.getCipherInstance(AES.CTR_NO_PADDING, properties));
297     }
298 
299     /**
300      * This method is executed immediately after decryption. Check whether
301      * cipher should be updated and recalculate padding if needed.
302      *
303      * @param state the CipherState instance.
304      * @param inByteBuffer the input buffer.
305      * @param position the offset from the start of the stream.
306      * @param iv the iv.
307      * @return the padding.
308      */
309     private byte postDecryption(final CipherState state, final ByteBuffer inByteBuffer,
310             final long position, final byte[] iv) {
311         byte padding = 0;
312         if (state.isReset()) {
313             /*
314              * This code is generally not executed since the cipher usually
315              * maintains cipher context (e.g. the counter) internally. However,
316              * some implementations can't maintain context so a re-init is
317              * necessary after each decryption call.
318              */
319             resetCipher(state, position, iv);
320             padding = getPadding(position);
321             inByteBuffer.position(padding);
322         }
323         return padding;
324     }
325 
326     /**
327      * Reads up to the specified number of bytes from a given position within a
328      * stream and return the number of bytes read. This does not change the
329      * current offset of the stream, and is thread-safe.
330      *
331      * @param buffer the buffer into which the data is read.
332      * @param length the maximum number of bytes to read.
333      * @param offset the start offset in the data.
334      * @param position the offset from the start of the stream.
335      * @throws IOException if an I/O error occurs.
336      * @return int the total number of decrypted data bytes read into the
337      *         buffer.
338      */
339     public int read(final long position, final byte[] buffer, final int offset, final int length)
340             throws IOException {
341         checkStream();
342         final int n = input.read(position, buffer, offset, length);
343         if (n > 0) {
344             // This operation does not change the current offset of the file
345             decrypt(position, buffer, offset, n);
346         }
347         return n;
348     }
349 
350     /**
351      * Reads the specified number of bytes from a given position within a
352      * stream. This does not change the current offset of the stream and is
353      * thread-safe.
354      *
355      * @param position the offset from the start of the stream.
356      * @param buffer the buffer into which the data is read.
357      * @throws IOException if an I/O error occurs.
358      */
359     public void readFully(final long position, final byte[] buffer) throws IOException {
360         readFully(position, buffer, 0, buffer.length);
361     }
362 
363     /**
364      * Reads the specified number of bytes from a given position within a
365      * stream. This does not change the current offset of the stream and is
366      * thread-safe.
367      *
368      * @param buffer the buffer into which the data is read.
369      * @param length the maximum number of bytes to read.
370      * @param offset the start offset in the data.
371      * @param position the offset from the start of the stream.
372      * @throws IOException if an I/O error occurs.
373      */
374     public void readFully(final long position, final byte[] buffer, final int offset, final int length)
375             throws IOException {
376         checkStream();
377         IoUtils.readFully(input, position, buffer, offset, length);
378         if (length > 0) {
379             // This operation does not change the current offset of the file
380             decrypt(position, buffer, offset, length);
381         }
382     }
383 
384     /**
385      * Calculates the counter and iv, reset the cipher.
386      *
387      * @param state the CipherState instance.
388      * @param position the offset from the start of the stream.
389      * @param iv the iv.
390      */
391     @SuppressWarnings("resource") // getCryptoCipher does not allocate
392     private void resetCipher(final CipherState state, final long position, final byte[] iv) {
393         final long counter = getCounter(position);
394         CtrCryptoInputStream.calculateIV(getInitIV(), counter, iv);
395         try {
396             state.getCryptoCipher().init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
397         } catch (final GeneralSecurityException e) {
398             // Ignore
399         }
400         state.reset(false);
401     }
402 
403     /**
404      * Returns direct buffer to pool.
405      *
406      * @param buf the buffer.
407      */
408     private void returnToPool(final ByteBuffer buf) {
409         if (buf != null) {
410             buf.clear();
411             byteBufferPool.add(buf);
412         }
413     }
414 
415     /**
416      * Returns CryptoCipher to pool.
417      *
418      * @param state the CipherState instance.
419      */
420     private void returnToPool(final CipherState state) {
421         if (state != null) {
422             cipherStatePool.add(state);
423         }
424     }
425 }