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 }