/*
 *   
 *
 * Copyright  1990-2006 Sun Microsystems, Inc. All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version
 * 2 only, as published by the Free Software Foundation. 
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License version 2 for more details (a copy is
 * included at /legal/license.txt). 
 * 
 * You should have received a copy of the GNU General Public License
 * version 2 along with this work; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA 
 * 
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
 * Clara, CA 95054 or visit www.sun.com if you need additional
 * information or have any questions. 
 */

package com.sun.midp.crypto;

/**
 * Class providing common functionality for DES ciphers.
 */
abstract class BlockCipherBase extends Cipher {

    /** Block size. */
    private int blockSize;

    /** ENCRYPT or DECRYPT. */
    protected int mode;

    /** The padder. */
    protected Padder padder;

    /** Contains the data that is not encrypted or decrypted yet. */
    protected byte[] holdData;

    /** The holding buffer counter. */
    protected int holdCount;

    /** Indicates if this cipher object has been updated or not. */
    protected boolean isUpdated;

    /** Saved state variable. */
    private byte[] savedHoldData;

    /** Saved state variable. */
    private int savedHoldCount;

    /** Saved state variable. */
    private boolean savedIsUpdated;

    /** Initial vector. */
    protected byte[] IV;

    /** True in decryption with padder mode. */
    private boolean keepLastBlock;

    /**
     * Constructor.
     *
     * @param blockSize block size
     */
    protected BlockCipherBase(int blockSize) {
        this.blockSize = blockSize;

        holdData = new byte[blockSize];
        mode = MODE_UNINITIALIZED;
    }

    /**
     * Sets the padder.
     *
     * @param padding Upper case padding arg from the transformation given to
     *               Cipher.getInstance
     *
     * @exception NoSuchPaddingException if <code>padding</code>
     * contains a padding scheme that is not available.
     */
    protected void setPadding(String padding)
        throws NoSuchPaddingException {
        
        if (padding.equals("") || padding.equals("PKCS5PADDING")) {
            padder = new PKCS5Padding(blockSize);
        } else if (!padding.equals("NOPADDING")) {
            throw new NoSuchPaddingException(padding);
        }
    }

    /**
     * Initializes a cipher object with the key and sets
     * encryption or decryption mode.
     *
     * @param mode either encryption or decription mode
     * @param keyAlgorithm algorithm the key should have
     * @param key key to be used
     * @param needIV true if this algorithm accepts IV parameter
     * @param params the algorithm parameters
     *
     * @exception InvalidKeyException if the given key
     * is inappropriate for initializing this cipher
     * @exception InvalidAlgorithmParameterException
     * if the given algorithm parameters are inappropriate for this cipher
     */
    protected void doInit(int mode, String keyAlgorithm, Key key,
            boolean needIV, CryptoParameter params)
            throws InvalidKeyException, InvalidAlgorithmParameterException {

        byte[] IV;

        if (needIV) {
            if (params == null) {
                if (mode == Cipher.DECRYPT_MODE) {
                    throw new InvalidAlgorithmParameterException();
                }
                IV = new byte[blockSize];
            } else {
                if (! (params instanceof IvParameter)) {
                    throw new InvalidAlgorithmParameterException();
                }
                IV = Util.cloneArray(((IvParameter)params).getIV());
                if (IV.length != blockSize)  {
                    throw new InvalidAlgorithmParameterException();
                }
            }
        } else {
            if (params != null) {
                throw new InvalidAlgorithmParameterException();
            }
            IV = null;
        }

        if (!(key instanceof SecretKey &&
              keyAlgorithm.equals(key.getAlgorithm()))) {
            throw new InvalidKeyException();
        }

        initKey(key.getEncoded(), mode);

        holdCount = 0;
        isUpdated = false;
        this.mode = mode;
        this.IV = IV;
        keepLastBlock = mode == Cipher.DECRYPT_MODE && padder != null;
    }

    /**
     * Encrypts or decrypts data in a single-part operation, or finishes a
     * multiple-part operation. The data is encrypted or decrypted,
     * depending on how this cipher was initialized.
     *
     * @param in the input buffer
     * @param offset the offset in <code>input</code> where the input
     * starts
     * @param len the input length
     * @param out the buffer for the result
     * @param outOffset the offset in <code>output</code> where the result
     * is stored
     *
     * @return the number of bytes stored in <code>output</code>
     *
     * @exception IllegalStateException if this cipher is in a wrong state
     * (e.g., has not been initialized)
     * @exception IllegalBlockSizeException if this cipher is a block cipher,
     * no padding has been requested (only in encryption mode), and the total
     * input length of the data processed by this cipher is not a multiple of
     * block size
     * @exception ShortBufferException if the given output buffer is too small
     * to hold the result
     * @exception BadPaddingException if this cipher is in decryption mode,
     * and (un)padding has been requested, but the decrypted data is not
     * bounded by the appropriate padding bytes
     */
    public int doFinal(byte in[], int offset, int len,
                       byte out[], int outOffset)
        throws IllegalStateException, IllegalBlockSizeException,
	           ShortBufferException, BadPaddingException {

        Util.checkBounds(in, offset, len, out, outOffset);

        if (mode == MODE_UNINITIALIZED)  {
            throw new IllegalStateException();
        }

        if (len == 0 && ! isUpdated) {
            return 0;
        }

        boolean encrypt = mode == Cipher.ENCRYPT_MODE;

        // calculate the size of the possible out buffer
        int expectedSize = len + holdCount;
        int delta = expectedSize % blockSize;
        if (delta != 0) {
            if (! encrypt || (encrypt && padder == null)) {
                throw new IllegalBlockSizeException();
            }
            expectedSize += blockSize - delta;
        } else
        if (encrypt && padder != null) {
            expectedSize += blockSize;
        }

        int excess = outOffset + expectedSize - out.length;

        // padder may remove up to blockSize bytes after decryption
        if (excess > (keepLastBlock ? blockSize : 0)) {
            throw new ShortBufferException();
        }

        if (keepLastBlock && excess > 0) {
            // after unpadding data may not fit into output buffer
            saveState();
        }

        int counter = update(in, offset, len, out, outOffset);

        if (padder != null) {
            if (encrypt) {
                if (padder.pad(holdData, holdCount) != 0) {
                    processBlock(out, outOffset + counter);
                    counter += blockSize;
                }
            } else {
                byte[] lastBlock = new byte[blockSize];
                processBlock(lastBlock, 0);
                int tail = blockSize - padder.unPad(lastBlock, blockSize);
                if (outOffset + counter + tail > out.length) {
                    restoreState();
                    throw new ShortBufferException();
                }
                System.arraycopy(lastBlock, 0,
                                 out, outOffset + counter, tail);
                counter += tail;
            }
        }

        holdCount = 0;
        return counter;
    }

    /**
     * Continues a multiple-part encryption or decryption operation
     * (depending on how this cipher was initialized), processing another data
     * part.
     * @param in the input buffer
     * @param offset the offset in <code>input</code> where the input
     * starts
     * @param len the input length
     * @param out the buffer for the result
     * @param outOffset the offset in <code>output</code> where the result
     * is stored
     *
     * @return the number of bytes stored in <code>output</code>
     *
     * @exception IllegalStateException if this cipher is in a wrong state
     * (e.g., has not been initialized)
     * @exception ShortBufferException if the given output buffer is too small
     * to hold the result
     */
    public int update(byte in[], int offset, int len,
                      byte out[], int outOffset)
        throws IllegalStateException, ShortBufferException {

        Util.checkBounds(in, offset, len, out, outOffset);

        if (mode == MODE_UNINITIALIZED)  {
            throw new IllegalStateException();
        }

        if (len == 0)  {
            return 0;
        }

        if (((holdCount + len) / blockSize -
             (keepLastBlock ? 1 : 0)) * blockSize >
            out.length - outOffset) {
            throw new ShortBufferException();
        }

        isUpdated = true;

        if (in == out) {
            in = new byte[len];
            System.arraycopy(out, offset, in, 0, len);
            offset = 0;
        }

        int counter = 0;
        while (true)  {

            int got;
            System.arraycopy(in, offset, holdData, holdCount,
                             got = Math.min(blockSize - holdCount, len));
            offset += got;
            len -= got;
            holdCount += got;

            if (holdCount < blockSize || (len == 0 && keepLastBlock)) {
                return counter;
            }

            processBlock(out, outOffset);

            counter   += blockSize;
            outOffset += blockSize;
        }
    }

    /**
     * Returns the initialization vector (IV) in a new buffer.
     * This is useful in the case where a random IV was created.
     * @return the initialization vector in a new buffer,
     * or <code>null</code> if the underlying algorithm does
     * not use an IV.
     */
    public byte[] getIV() {
        return IV == null ? null : Util.cloneArray(IV);
    }

    /**
     * Saves cipher state.
     */
    protected void saveState() {
        savedHoldCount = holdCount;
        savedHoldData = holdCount == 0 ?
                        holdData :
                        Util.cloneArray(holdData);
        savedIsUpdated = isUpdated;
    }

    /**
     * Restores cipher state.
     */
    protected void restoreState() {
        holdCount = savedHoldCount;
        holdData = savedHoldData;
        isUpdated = savedIsUpdated;
    }

    /**
     * Depending on the mode, either encrypts or decrypts data block.
     * @param out will contain the result of encryption
     * or decryption operation
     * @param offset is the offset in out
     */
    abstract void processBlock(byte[] out, int offset);

    /**
     * Initializes key.
     * @param data key data
     * @param mode cipher mode
     * @exception InvalidKeyException if the given key is inappropriate
     * for this cipher
     */
    abstract void initKey(byte[] data, int mode) throws InvalidKeyException;
}
