001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.commons.crypto.stream;
019
020import java.io.IOException;
021import java.nio.ByteBuffer;
022import java.security.GeneralSecurityException;
023import java.util.Properties;
024import java.util.Queue;
025import java.util.concurrent.ConcurrentLinkedQueue;
026
027import javax.crypto.Cipher;
028import javax.crypto.spec.IvParameterSpec;
029
030import org.apache.commons.crypto.cipher.CryptoCipher;
031import org.apache.commons.crypto.stream.input.Input;
032import org.apache.commons.crypto.utils.AES;
033import org.apache.commons.crypto.utils.IoUtils;
034import org.apache.commons.crypto.utils.Utils;
035
036/**
037 * PositionedCryptoInputStream provides the capability to decrypt the stream
038 * starting at random position as well as provides the foundation for positioned
039 * read for decrypting. This needs a stream cipher mode such as AES CTR mode.
040 */
041public class PositionedCryptoInputStream extends CtrCryptoInputStream {
042
043    private static class CipherState {
044
045        private final CryptoCipher cryptoCipher;
046        private boolean reset;
047
048        /**
049         * Constructs a new instance.
050         *
051         * @param cryptoCipher the CryptoCipher instance.
052         */
053        public CipherState(final CryptoCipher cryptoCipher) {
054            this.cryptoCipher = cryptoCipher;
055            this.reset = false;
056        }
057
058        /**
059         * Gets the CryptoCipher instance.
060         *
061         * @return the cipher.
062         */
063        public CryptoCipher getCryptoCipher() {
064            return cryptoCipher;
065        }
066
067        /**
068         * Gets the reset.
069         *
070         * @return the value of reset.
071         */
072        public boolean isReset() {
073            return reset;
074        }
075
076        /**
077         * Sets the value of reset.
078         *
079         * @param reset the reset.
080         */
081        public void reset(final boolean reset) {
082            this.reset = reset;
083        }
084    }
085
086    /**
087     * DirectBuffer pool
088     */
089    private final Queue<ByteBuffer> byteBufferPool = new ConcurrentLinkedQueue<>();
090
091    /**
092     * CryptoCipher pool
093     */
094    private final Queue<CipherState> cipherStatePool = new ConcurrentLinkedQueue<>();
095
096    /**
097     * properties for constructing a CryptoCipher
098     */
099    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}