 /*
 * 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);
        }
    }
}
