CtrCryptoOutputStream.java

 /*
 * 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.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
import java.security.GeneralSecurityException;
import java.util.Properties;

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

import org.apache.commons.crypto.cipher.CryptoCipher;
import org.apache.commons.crypto.stream.output.ChannelOutput;
import org.apache.commons.crypto.stream.output.Output;
import org.apache.commons.crypto.stream.output.StreamOutput;
import org.apache.commons.crypto.utils.AES;
import org.apache.commons.crypto.utils.Utils;

/**
 * <p>
 * CtrCryptoOutputStream encrypts data. It is not thread-safe. AES CTR mode is
 * required in order to ensure that the plain text and cipher text have a 1:1
 * mapping. The encryption is buffer based. The key points of the encryption are
 * (1) calculating counter and (2) padding through stream position.
 * </p>
 * <p>
 * counter = base + pos/(algorithm blocksize); padding = pos%(algorithm
 * blocksize);
 * </p>
 * <p>
 * The underlying stream offset is maintained as state.
 * </p>
 * <p>
 * This class should only be used with blocking sinks. Using this class to wrap
 * a non-blocking sink may lead to high CPU usage.
 * </p>
 */
public class CtrCryptoOutputStream extends CryptoOutputStream {
    /**
     * Underlying stream offset.
     */
    private long streamOffset;

    /**
     * The initial IV.
     */
    private final byte[] initIV;

    /**
     * Initialization vector for the cipher.
     */
    private final byte[] iv;

    /**
     * Padding = pos%(algorithm blocksize); Padding is put into
     * {@link #inBuffer} before any other data goes in. The purpose of padding
     * is to put input data at proper position.
     */
    private byte padding;

    /**
     * Flag to mark whether the cipher has been reset
     */
    private boolean cipherReset;

    /**
     * Constructs a {@link CtrCryptoOutputStream}.
     *
     * @param output the Output instance.
     * @param cipher the CryptoCipher instance.
     * @param bufferSize the bufferSize.
     * @param key crypto key for the cipher.
     * @param iv Initialization vector for the cipher.
     * @throws IOException if an I/O error occurs.
     */
    protected CtrCryptoOutputStream(final Output output, final CryptoCipher cipher,
            final int bufferSize, final byte[] key, final byte[] iv) throws IOException {
        this(output, cipher, bufferSize, key, iv, 0);
    }

    /**
     * Constructs a {@link CtrCryptoOutputStream}.
     *
     * @param output the output stream.
     * @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 CtrCryptoOutputStream(final Output output, final CryptoCipher cipher,
            final int bufferSize, final byte[] key, final byte[] iv, final long streamOffset)
            throws IOException {
        super(output, cipher, bufferSize, AES.newSecretKeySpec(key),
                new IvParameterSpec(iv));

        CryptoInputStream.checkStreamCipher(cipher);
        this.streamOffset = streamOffset;
        this.initIV = iv.clone();
        this.iv = iv.clone();

        resetCipher();
    }

    /**
     * Constructs a {@link CtrCryptoOutputStream}.
     *
     * @param out the output stream.
     * @param cipher the CryptoCipher instance.
     * @param bufferSize the bufferSize.
     * @param key crypto key for the cipher.
     * @param iv Initialization vector for the cipher.
     * @throws IOException if an I/O error occurs.
     */
    protected CtrCryptoOutputStream(final OutputStream out, final CryptoCipher cipher,
            final int bufferSize, final byte[] key, final byte[] iv) throws IOException {
        this(out, cipher, bufferSize, key, iv, 0);
    }

    /**
     * Constructs a {@link CtrCryptoOutputStream}.
     *
     * @param outputStream the output stream.
     * @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.
     */
    @SuppressWarnings("resource") // Closing the instance closes the StreamOutput
    protected CtrCryptoOutputStream(final OutputStream outputStream, final CryptoCipher cipher,
            final int bufferSize, final byte[] key, final byte[] iv, final long streamOffset)
            throws IOException {
        this(new StreamOutput(outputStream, bufferSize), cipher, bufferSize, key, iv, streamOffset);
    }

    /**
     * Constructs a {@link CtrCryptoOutputStream}.
     *
     * @param props The {@code Properties} class represents a set of
     *        properties.
     * @param out the output stream.
     * @param key crypto key for the cipher.
     * @param iv Initialization vector for the cipher.
     * @throws IOException if an I/O error occurs.
     */
    public CtrCryptoOutputStream(final Properties props, final OutputStream out,
            final byte[] key, final byte[] iv) throws IOException {
        this(props, out, key, iv, 0);
    }

    /**
     * Constructs a {@link CtrCryptoOutputStream}.
     *
     * @param properties The {@code Properties} class represents a set of
     *        properties.
     * @param outputStream the output stream.
     * @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 CtrCryptoOutputStream.
    public CtrCryptoOutputStream(final Properties properties, final OutputStream outputStream,
            final byte[] key, final byte[] iv, final long streamOffset) throws IOException {
        this(outputStream, Utils.getCipherInstance(
                AES.CTR_NO_PADDING, properties),
                CryptoInputStream.getBufferSize(properties), key, iv, streamOffset);
    }

    /**
     * Constructs a {@link CtrCryptoOutputStream}.
     *
     * @param props The {@code Properties} class represents a set of
     *        properties.
     * @param out the WritableByteChannel instance.
     * @param key crypto key for the cipher.
     * @param iv Initialization vector for the cipher.
     * @throws IOException if an I/O error occurs.
     */
    public CtrCryptoOutputStream(final Properties props, final WritableByteChannel out,
            final byte[] key, final byte[] iv) throws IOException {
        this(props, out, key, iv, 0);
    }

    /**
     * Constructs a {@link CtrCryptoOutputStream}.
     *
     * @param properties The {@code Properties} class represents a set of
     *        properties.
     * @param channel the WritableByteChannel instance.
     * @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 CtrCryptoOutputStream.
    public CtrCryptoOutputStream(final Properties properties, final WritableByteChannel channel,
            final byte[] key, final byte[] iv, final long streamOffset) throws IOException {
        this(channel, Utils.getCipherInstance(
                AES.CTR_NO_PADDING, properties),
                CryptoInputStream.getBufferSize(properties), key, iv, streamOffset);
    }

    /**
     * Constructs a {@link CtrCryptoOutputStream}.
     *
     * @param channel the WritableByteChannel instance.
     * @param cipher the CryptoCipher instance.
     * @param bufferSize the bufferSize.
     * @param key crypto key for the cipher.
     * @param iv Initialization vector for the cipher.
     * @throws IOException if an I/O error occurs.
     */
    protected CtrCryptoOutputStream(final WritableByteChannel channel,
            final CryptoCipher cipher, final int bufferSize, final byte[] key, final byte[] iv)
            throws IOException {
        this(channel, cipher, bufferSize, key, iv, 0);
    }

    /**
     * Constructs a {@link CtrCryptoOutputStream}.
     *
     * @param channel the WritableByteChannel instance.
     * @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.
     */
   @SuppressWarnings("resource") // Closing the instance closes the ChannelOutput
   protected CtrCryptoOutputStream(final WritableByteChannel channel,
            final CryptoCipher cipher, final int bufferSize, final byte[] key, final byte[] iv,
            final long streamOffset) throws IOException {
       this(new ChannelOutput(channel), cipher, bufferSize, key, iv, streamOffset);
    }

    /**
     * Does the encryption, input is {@link #inBuffer} and output is
     * {@link #outBuffer}.
     *
     * @throws IOException if an I/O error occurs.
     */
    @Override
    protected void encrypt() throws IOException {
        Utils.checkState(inBuffer.position() >= padding);
        if (inBuffer.position() == padding) {
            // There is no real data in the inBuffer.
            return;
        }

        inBuffer.flip();
        outBuffer.clear();
        encryptBuffer(outBuffer);
        inBuffer.clear();
        outBuffer.flip();

        if (padding > 0) {
            /*
             * The plain text and cipher text have a 1:1 mapping, they start at
             * the same position.
             */
            outBuffer.position(padding);
            padding = 0;
        }

        final int len = output.write(outBuffer);
        streamOffset += len;
        if (cipherReset) {
            /*
             * This code is generally not executed since the encryptor usually
             * maintains encryption context (e.g. the counter) internally.
             * However, some implementations can't maintain context so a re-init
             * is necessary after each encryption call.
             */
            resetCipher();
        }
    }

    /**
     * Does the encryption if the ByteBuffer data.
     *
     * @param out the output ByteBuffer.
     * @throws IOException if an I/O error occurs.
     */
    private void encryptBuffer(final ByteBuffer out) throws IOException {
        final int inputSize = inBuffer.remaining();
        try {
            final int n = cipher.update(inBuffer, out);
            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.
                 */
                cipher.doFinal(inBuffer, out);
                cipherReset = true;
            }
        } catch (final GeneralSecurityException e) {
            throw new IOException(e);
        }
    }

    /**
     * Does final encryption of the last data.
     *
     * @throws IOException if an I/O error occurs.
     */
    @Override
    protected void encryptFinal() throws IOException {
        // The same as the normal encryption for Counter mode
        encrypt();
    }

    /**
     * Get the underlying stream offset
     *
     * @return the underlying stream offset
     */
    protected long getStreamOffset() {
        return streamOffset;
    }

    /**
     * Overrides the {@link CryptoOutputStream#initCipher()}. Initializes the
     * cipher.
     */
    @Override
    protected void initCipher() {
        // Do nothing for initCipher
        // Will reset the cipher considering the stream offset
    }

    /**
     * Resets the {@link #cipher}: calculate counter and {@link #padding}.
     *
     * @throws IOException if an I/O error occurs.
     */
    private void resetCipher() throws IOException {
        final long counter = streamOffset
                / cipher.getBlockSize();
        padding = (byte) (streamOffset % cipher.getBlockSize());
        inBuffer.position(padding); // Set proper position for input data.

        CtrCryptoInputStream.calculateIV(initIV, counter, iv);
        try {
            cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
        } catch (final GeneralSecurityException e) {
            throw new IOException(e);
        }
        cipherReset = false;
    }

    /**
     * Set the underlying stream offset
     *
     * @param streamOffset the underlying stream offset
     */
    protected void setStreamOffset(final long streamOffset) {
        this.streamOffset = streamOffset;
    }
}