/*
 *  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 javax.crypto;

import java.nio.ByteBuffer;
import java.security.AlgorithmParameters;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.InvalidParameterException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Provider;
import java.security.Provider.Service;
import java.security.ProviderException;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.security.spec.AlgorithmParameterSpec;
import java.util.ArrayList;
import java.util.Locale;
import java.util.Set;
import org.apache.harmony.crypto.internal.NullCipherSpi;
import org.apache.harmony.security.fortress.Engine;

/**
 * This class provides access to implementations of cryptographic ciphers for
 * encryption and decryption. Cipher classes can not be instantiated directly,
 * one has to call the Cipher's {@code getInstance} method with the name of a
 * requested transformation, optionally with a provider. A transformation
 * specifies an operation (or a set of operations) as a string in the form:
 * <ul>
 * <li><i>"algorithm/mode/padding"</i></li> or
 * <li><i>"algorithm"</i></li>
 * </ul>
 * <i>algorithm</i> is the name of a cryptographic algorithm, <i>mode</i> is the
 * name of a feedback mode and <i>padding</i> is the name of a padding scheme.
 * If <i>mode</i> and/or <i>padding</i> values are omitted, provider specific
 * default values will be used.
 * <p>
 * A valid transformation would be:
 * <ul>
 * {@code Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");}
 * </ul>
 * When a block cipher is requested in stream cipher mode, the number of bits
 * to be processed at a time can be optionally specified by appending it to the
 * mode name. e.g. <i>"AES/CFB8/NoPadding"</i>. If no number is specified, a
 * provider specific default value is used.
 */
public class Cipher {

    /**
     * Constant for decryption operation mode.
     */
    public static final int DECRYPT_MODE = 2;

    /**
     * Constant for encryption operation mode.
     */
    public static final int ENCRYPT_MODE = 1;

    /**
     * Constant indicating that the key to be unwrapped is a private key.
     */
    public static final int PRIVATE_KEY = 2;

    /**
     * Constant indicating that the key to be unwrapped is a public key.
     */
    public static final int PUBLIC_KEY = 1;

    /**
     * Constant indicating that the key to be unwrapped is a secret key.
     */
    public static final int SECRET_KEY = 3;

    /**
     * Constant for key unwrapping operation mode.
     */
    public static final int UNWRAP_MODE = 4;

    /**
     * Constant for key wrapping operation mode.
     */
    public static final int WRAP_MODE = 3;

    private int mode;

    /** Items that need to be set on the Cipher instance. */
    private enum NeedToSet {
        NONE, MODE, PADDING, BOTH,
    };

    /**
     * Used to keep track of which underlying {@code CipherSpi#engineInit(...)}
     * variant to call when testing suitability.
     */
    private static enum InitType {
        KEY, ALGORITHM_PARAMS, ALGORITHM_PARAM_SPEC,
    };

    /**
     * Keeps track of the possible arguments to {@code Cipher#init(...)}.
     */
    private static class InitParams {
        private final InitType initType;
        private final int opmode;
        private final Key key;
        private final SecureRandom random;
        private final AlgorithmParameterSpec spec;
        private final AlgorithmParameters params;

        private InitParams(InitType initType, int opmode, Key key, SecureRandom random,
                AlgorithmParameterSpec spec, AlgorithmParameters params) {
            this.initType = initType;
            this.opmode = opmode;
            this.key = key;
            this.random = random;
            this.spec = spec;
            this.params = params;
        }
    }

    /**
     * Expresses the various types of transforms that may be used during
     * initialization.
     */
    private static class Transform {
        private final String name;
        private final NeedToSet needToSet;

        public Transform(String name, NeedToSet needToSet) {
            this.name = name;
            this.needToSet = needToSet;
        }
    }

    /**
     * The service name.
     */
    private static final String SERVICE = "Cipher";

    /**
     * Used to access common engine functionality.
     */
    private static final Engine ENGINE = new Engine(SERVICE);

    /** The attribute used for supported paddings. */
    private static final String ATTRIBUTE_PADDINGS = "SupportedPaddings";

    /** The attribute used for supported modes. */
    private static final String ATTRIBUTE_MODES = "SupportedModes";

    /**
     * The provider.
     */
    private Provider provider;

    /**
     * The provider specified when instance created.
     */
    private final Provider specifiedProvider;

    /**
     * The SPI implementation.
     */
    private CipherSpi spiImpl;

    /**
     * The SPI implementation.
     */
    private final CipherSpi specifiedSpi;

    /**
     * The transformation.
     */
    private final String transformation;

    /**
     * The transformation split into parts.
     */
    private final String[] transformParts;

    /**
     * Lock held while the SPI is initializing.
     */
    private final Object initLock = new Object();

    private static SecureRandom secureRandom;

    /**
     * Creates a new Cipher instance.
     *
     * @param cipherSpi
     *            the implementation delegate of the cipher.
     * @param provider
     *            the provider of the implementation of this cipher.
     * @param transformation
     *            the name of the transformation that this cipher performs.
     * @throws NullPointerException
     *             if either cipherSpi is {@code null} or provider is {@code
     *             null} and {@code cipherSpi} is a {@code NullCipherSpi}.
     */
    protected Cipher(CipherSpi cipherSpi, Provider provider, String transformation) {
        if (cipherSpi == null) {
            throw new NullPointerException("cipherSpi == null");
        }
        if (!(cipherSpi instanceof NullCipherSpi) && provider == null) {
            throw new NullPointerException("provider == null");
        }
        this.specifiedProvider = provider;
        this.specifiedSpi = cipherSpi;
        this.transformation = transformation;
        this.transformParts = null;
    }

    private Cipher(String transformation, String[] transformParts, Provider provider) {
        this.transformation = transformation;
        this.transformParts = transformParts;
        this.specifiedProvider = provider;
        this.specifiedSpi = null;
    }


    /**
     * Creates a new Cipher for the specified transformation. The installed
     * providers are searched in order for an implementation of the specified
     * transformation. The first found provider providing the transformation is
     * used to create the cipher. If no provider is found an exception is
     * thrown.
     *
     * @param transformation
     *            the name of the transformation to create a cipher for.
     * @return a cipher for the requested transformation.
     * @throws NoSuchAlgorithmException
     *             if no installed provider can provide the
     *             <i>transformation</i>, or it is {@code null}, empty or in an
     *             invalid format.
     * @throws NoSuchPaddingException
     *             if no installed provider can provide the padding scheme in
     *             the <i>transformation</i>.
     */
    public static final Cipher getInstance(String transformation)
            throws NoSuchAlgorithmException, NoSuchPaddingException {
        return getCipher(transformation, null);
    }

    /**
     * Creates a new cipher for the specified transformation provided by the
     * specified provider.
     *
     * @param transformation
     *            the name of the transformation to create a cipher for.
     * @param provider
     *            the name of the provider to ask for the transformation.
     * @return a cipher for the requested transformation.
     * @throws NoSuchAlgorithmException
     *             if the specified provider can not provide the
     *             <i>transformation</i>, or it is {@code null}, empty or in an
     *             invalid format.
     * @throws NoSuchProviderException
     *             if no provider with the specified name can be found.
     * @throws NoSuchPaddingException
     *             if the requested padding scheme in the <i>transformation</i>
     *             is not available.
     * @throws IllegalArgumentException
     *             if the specified provider is {@code null}.
     */
    public static final Cipher getInstance(String transformation,
            String provider) throws NoSuchAlgorithmException,
            NoSuchProviderException, NoSuchPaddingException {

        if (provider == null) {
            throw new IllegalArgumentException("provider == null");
        }

        Provider p = Security.getProvider(provider);
        if (p == null) {
            throw new NoSuchProviderException("Provider not available: " + provider);
        }
        return getInstance(transformation, p);
    }

    /**
     * Creates a new cipher for the specified transformation. The
     * {@code provider} supplied does not have to be registered.
     *
     * @param transformation
     *            the name of the transformation to create a cipher for.
     * @param provider
     *            the provider to ask for the transformation.
     * @return a cipher for the requested transformation.
     * @throws NoSuchAlgorithmException
     *             if the specified provider can not provide the
     *             <i>transformation</i>, or it is {@code null}, empty or in an
     *             invalid format.
     * @throws NoSuchPaddingException
     *             if the requested padding scheme in the <i>transformation</i>
     *             is not available.
     * @throws IllegalArgumentException
     *             if the provider is {@code null}.
     */
    public static final Cipher getInstance(String transformation,
            Provider provider) throws NoSuchAlgorithmException,
            NoSuchPaddingException {
        if (provider == null) {
            throw new IllegalArgumentException("provider == null");
        }
        return getCipher(transformation, provider);
    }

    private static NoSuchAlgorithmException invalidTransformation(String transformation)
            throws NoSuchAlgorithmException {
        throw new NoSuchAlgorithmException("Invalid transformation: " + transformation);
    }

    /**
     * Create a Cipher instance but don't choose a CipherSpi until we have more
     * information.
     */
    private static Cipher getCipher(String transformation, Provider provider)
            throws NoSuchAlgorithmException, NoSuchPaddingException {
        if (transformation == null || transformation.isEmpty()) {
            throw invalidTransformation(transformation);
        }

        String[] transformParts = checkTransformation(transformation);

        Engine.SpiAndProvider sap;
        try {
            sap = tryCombinations(null /* params */, provider, transformation, transformParts);
        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
            // should never happen since we passed in null params
            throw new ProviderException(e);
        }

        if (sap == null) {
            if (provider == null) {
                throw new NoSuchAlgorithmException("No provider found for " + transformation);
            } else {
                throw new NoSuchAlgorithmException("Provider " + provider.getName()
                        + " does not provide " + transformation);
            }
        }
        return new Cipher(transformation, transformParts, provider);
    }

    /**
     * Checks that the provided algorithm {@code transformation} string is a
     * valid input. The algorithm is the only mandatory field and input can be
     * of the form:
     * <ul>
     * <li><code>"[cipher]"</code>
     * <li><code>"[cipher]/[mode]/[padding]"</code>
     * <li><code>"[cipher]//[padding]"</code>
     * <li><code>"[cipher]/[mode]"</code>
     * </ul>
     * <p>
     * Returns the specified transformation split up into three parts
     * corresponding to their function:
     * <p>
     * <code>
     * {&lt;algorithm&gt;, &lt;mode&gt;, &lt;padding&gt;}
     * </code>
     * <p>
     */
    private static String[] checkTransformation(String transformation)
            throws NoSuchAlgorithmException {
        // ignore an extra prefix / characters such as in
        // "/DES/CBC/PKCS5Padding" http://b/3387688
        if (transformation.startsWith("/")) {
            transformation = transformation.substring(1);
        }
        // 'transformation' should be of the form "algorithm/mode/padding".
        String[] pieces = transformation.split("/");
        if (pieces.length > 3) {
            throw invalidTransformation(transformation);
        }
        // Empty or missing pieces are represented by null.
        String[] result = new String[3];
        for (int i = 0; i < pieces.length; ++i) {
            String piece = pieces[i].trim();
            if (!piece.isEmpty()) {
                result[i] = piece;
            }
        }
        // You MUST specify an algorithm.
        if (result[0] == null) {
            throw invalidTransformation(transformation);
        }
        if (!(result[1] == null && result[2] == null) && (result[1] == null || result[2] == null)) {
            throw invalidTransformation(transformation);
        }
        return result;
    }

    /**
     * Makes sure a CipherSpi that matches this type is selected. If
     * {@code key != null} then it assumes that a suitable provider exists for
     * this instance (used by {@link #init}. If the {@code initParams} is passed
     * in, then the {@code CipherSpi} returned will be initialized.
     *
     * @throws InvalidKeyException if the specified key cannot be used to
     *             initialize this cipher.
     * @throws InvalidAlgorithmParameterException
     */
    private CipherSpi getSpi(InitParams initParams) throws InvalidKeyException,
            InvalidAlgorithmParameterException {
        if (specifiedSpi != null) {
            return specifiedSpi;
        }

        synchronized (initLock) {
            // This is not only a matter of performance. Many methods like update, doFinal, etc.
            // call {@code #getSpi()} (ie, {@code #getSpi(null /* params */)}) and without this
            // shortcut they would override an spi that was chosen using the key.
            if (spiImpl != null && initParams == null) {
                return spiImpl;
            }

            final Engine.SpiAndProvider sap = tryCombinations(initParams, specifiedProvider,
                    transformation, transformParts);
            if (sap == null) {
                throw new ProviderException("No provider found for " + transformation);
            }

            spiImpl = (CipherSpi) sap.spi;
            provider = sap.provider;

            return spiImpl;
        }
    }

    /**
     * Convenience call when the Key is not available.
     */
    private CipherSpi getSpi() {
        try {
            return getSpi(null);
        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
            throw new ProviderException("Exception thrown when params == null", e);
        }
    }

    /**
     * Returns the {@code CipherSpi} backing this {@code Cipher} or {@code null} if no
     * {@code CipherSpi} is backing this {@code Cipher}.
     *
     * @hide
     */
    public CipherSpi getCurrentSpi() {
        if (specifiedSpi != null) {
            return specifiedSpi;
        }

        synchronized (initLock) {
            return spiImpl;
        }
    }

    /**
     * Tries to find the correct {@code Cipher} transform to use. Returns a
     * {@link Engine.SpiAndProvider}, throws the first exception that was
     * encountered during attempted initialization, or {@code null} if there are
     * no providers that support the {@code initParams}.
     * <p>
     * {@code transformParts} must be in the format returned by
     * {@link #checkTransformation(String)}. The combinations of mode strings
     * tried are as follows:
     * <ul>
     * <li><code>[cipher]/[mode]/[padding]</code>
     * <li><code>[cipher]/[mode]</code>
     * <li><code>[cipher]//[padding]</code>
     * <li><code>[cipher]</code>
     * </ul>
     */
    private static Engine.SpiAndProvider tryCombinations(InitParams initParams, Provider provider,
            String transformation, String[] transformParts) throws InvalidKeyException,
            InvalidAlgorithmParameterException {
        // Enumerate all the transforms we need to try
        ArrayList<Transform> transforms = new ArrayList<Transform>();
        if (transformParts[1] != null && transformParts[2] != null) {
            transforms.add(new Transform(transformParts[0] + "/" + transformParts[1] + "/"
                    + transformParts[2], NeedToSet.NONE));
        }
        if (transformParts[1] != null) {
            transforms.add(new Transform(transformParts[0] + "/" + transformParts[1],
                    NeedToSet.PADDING));
        }
        if (transformParts[2] != null) {
            transforms.add(new Transform(transformParts[0] + "//" + transformParts[2],
                    NeedToSet.MODE));
        }
        transforms.add(new Transform(transformParts[0], NeedToSet.BOTH));

        // Try each of the transforms and keep track of the first exception
        // encountered.
        Exception cause = null;
        for (Transform transform : transforms) {
            if (provider != null) {
                Provider.Service service = provider.getService(SERVICE, transform.name);
                if (service == null) {
                    continue;
                }
                return tryTransformWithProvider(initParams, transformParts, transform.needToSet,
                        service);
            }
            ArrayList<Provider.Service> services = ENGINE.getServices(transform.name);
            if (services == null || services.isEmpty()) {
                continue;
            }
            for (Provider.Service service : services) {
                if (initParams == null || initParams.key == null
                        || service.supportsParameter(initParams.key)) {
                    try {
                        Engine.SpiAndProvider sap = tryTransformWithProvider(initParams,
                                transformParts, transform.needToSet, service);
                        if (sap != null) {
                            return sap;
                        }
                    } catch (Exception e) {
                        if (cause == null) {
                            cause = e;
                        }
                    }
                }
            }
        }
        if (cause instanceof InvalidKeyException) {
            throw (InvalidKeyException) cause;
        } else if (cause instanceof InvalidAlgorithmParameterException) {
            throw (InvalidAlgorithmParameterException) cause;
        } else if (cause instanceof RuntimeException) {
            throw (RuntimeException) cause;
        } else if (cause != null) {
            throw new InvalidKeyException("No provider can be initialized with given key", cause);
        } else if (initParams == null || initParams.key == null) {
            return null;
        } else {
            // Since the key is not null, a suitable provider exists,
            // and it is an InvalidKeyException.
            throw new InvalidKeyException("No provider offers " + transformation + " for "
                    + initParams.key.getAlgorithm() + " key of class "
                    + initParams.key.getClass().getName() + " and export format "
                    + initParams.key.getFormat());
        }
    }

    /**
     * Tries to initialize the {@code Cipher} from a given {@code service}. If
     * initialization is successful, the initialized {@code spi} is returned. If
     * the {@code service} cannot be initialized with the specified
     * {@code initParams}, then it's expected to throw
     * {@code InvalidKeyException} or {@code InvalidAlgorithmParameterException}
     * as a hint to the caller that it should continue searching for a
     * {@code Service} that will work.
     */
    private static Engine.SpiAndProvider tryTransformWithProvider(InitParams initParams,
            String[] transformParts, NeedToSet type, Provider.Service service)
            throws InvalidKeyException, InvalidAlgorithmParameterException {
        try {
            /*
             * Check to see if the Cipher even supports the attributes before
             * trying to instantiate it.
             */
            if (!matchAttribute(service, ATTRIBUTE_MODES, transformParts[1])
                    || !matchAttribute(service, ATTRIBUTE_PADDINGS, transformParts[2])) {
                return null;
            }

            Engine.SpiAndProvider sap = ENGINE.getInstance(service, null);
            if (sap.spi == null || sap.provider == null) {
                return null;
            }
            CipherSpi spi = (CipherSpi) sap.spi;
            if (((type == NeedToSet.MODE) || (type == NeedToSet.BOTH))
                    && (transformParts[1] != null)) {
                spi.engineSetMode(transformParts[1]);
            }
            if (((type == NeedToSet.PADDING) || (type == NeedToSet.BOTH))
                    && (transformParts[2] != null)) {
                spi.engineSetPadding(transformParts[2]);
            }

            if (initParams != null) {
                switch (initParams.initType) {
                    case ALGORITHM_PARAMS:
                        spi.engineInit(initParams.opmode, initParams.key, initParams.params,
                                initParams.random);
                        break;
                    case ALGORITHM_PARAM_SPEC:
                        spi.engineInit(initParams.opmode, initParams.key, initParams.spec,
                                initParams.random);
                        break;
                    case KEY:
                        spi.engineInit(initParams.opmode, initParams.key, initParams.random);
                        break;
                    default:
                        throw new AssertionError("This should never be reached");
                }
            }
            return sap;
        } catch (NoSuchAlgorithmException ignored) {
        } catch (NoSuchPaddingException ignored) {
        }
        return null;
    }

    /**
     * If the attribute listed exists, check that it matches the regular
     * expression.
     */
    private static boolean matchAttribute(Service service, String attr, String value) {
        if (value == null) {
            return true;
        }
        final String pattern = service.getAttribute(attr);
        if (pattern == null) {
            return true;
        }
        final String valueUc = value.toUpperCase(Locale.US);
        return valueUc.matches(pattern.toUpperCase(Locale.US));
    }

    /**
     * Returns the provider of this cipher instance.
     *
     * @return the provider of this cipher instance.
     */
    public final Provider getProvider() {
        getSpi();
        return provider;
    }

    /**
     * Returns the name of the algorithm of this cipher instance.
     * <p>
     * This is the name of the <i>transformation</i> argument used in the
     * {@code getInstance} call creating this object.
     *
     * @return the name of the algorithm of this cipher instance.
     */
    public final String getAlgorithm() {
        return transformation;
    }

    /**
     * Returns this ciphers block size (in bytes).
     *
     * @return this ciphers block size.
     */
    public final int getBlockSize() {
        return getSpi().engineGetBlockSize();
    }

    /**
     * Returns the length in bytes an output buffer needs to be when this cipher
     * is updated with {@code inputLen} bytes.
     *
     * @param inputLen
     *            the number of bytes of the input.
     * @return the output buffer length for the input length.
     * @throws IllegalStateException
     *             if this cipher instance is in an invalid state.
     */
    public final int getOutputSize(int inputLen) {
        if (mode == 0) {
            throw new IllegalStateException("Cipher has not yet been initialized");
        }
        return getSpi().engineGetOutputSize(inputLen);
    }

    /**
     * Returns the <i>initialization vector</i> for this cipher instance.
     *
     * @return the <i>initialization vector</i> for this cipher instance.
     */
    public final byte[] getIV() {
        return getSpi().engineGetIV();
    }

    /**
     * Returns the parameters that where used to create this cipher instance.
     * <p>
     * These may be a the same parameters that were used to create this cipher
     * instance, or may be a combination of default and random parameters,
     * depending on the underlying cipher implementation.
     *
     * @return the parameters that where used to create this cipher instance, or
     *         {@code null} if this cipher instance does not have any
     *         parameters.
     */
    public final AlgorithmParameters getParameters() {
        return getSpi().engineGetParameters();
    }

    /**
     * Returns the exemption mechanism associated with this cipher.
     *
     * @return currently {@code null}
     */
    public final ExemptionMechanism getExemptionMechanism() {
        //FIXME implement getExemptionMechanism

        //        try {
        //            return ExemptionMechanism.getInstance(transformation, provider);
        //        } catch (NoSuchAlgorithmException e) {
        return null;
        //        }

    }

    /**
     * Checks that the provided {@code mode} is one that is valid for
     * {@code Cipher}.
     */
    private void checkMode(int mode) {
        if (mode != ENCRYPT_MODE && mode != DECRYPT_MODE && mode != UNWRAP_MODE
                && mode != WRAP_MODE) {
            throw new InvalidParameterException("Invalid mode: " + mode);
        }
    }

    /**
     * Initializes this cipher instance with the specified key.
     * <p>
     * The cipher is initialized for the specified operational mode (one of:
     * encryption, decryption, key wrapping or key unwrapping) depending on
     * {@code opmode}.
     * <p>
     * If this cipher instance needs any algorithm parameters or random values
     * that the specified key can not provide, the underlying implementation of
     * this cipher is supposed to generate the required parameters (using its
     * provider or random values).
     * <p>
     * When a cipher instance is initialized by a call to any of the {@code
     * init} methods, the state of the instance is overridden, meaning that it
     * is equivalent to creating a new instance and calling its {@code init}
     * method.
     *
     * @param opmode
     *            the operation this cipher instance should be initialized for
     *            (one of: {@code ENCRYPT_MODE}, {@code DECRYPT_MODE}, {@code
     *            WRAP_MODE} or {@code UNWRAP_MODE}).
     * @param key
     *            the input key for the operation.
     * @throws InvalidKeyException
     *             if the specified key can not be used to initialize this
     *             cipher instance.
     */
    public final void init(int opmode, Key key) throws InvalidKeyException {
        if (secureRandom == null) {
            // In theory it might be thread-unsafe but in the given case it's OK
            // since it does not matter which SecureRandom instance is passed
            // to the init()
            secureRandom = new SecureRandom();
        }
        init(opmode, key, secureRandom);
    }

    /**
     * Initializes this cipher instance with the specified key and a source of
     * randomness.
     * <p>
     * The cipher is initialized for the specified operational mode (one of:
     * encryption, decryption, key wrapping or key unwrapping) depending on
     * {@code opmode}.
     * <p>
     * If this cipher instance needs any algorithm parameters or random values
     * that the specified key can not provide, the underlying implementation of
     * this cipher is supposed to generate the required parameters (using its
     * provider or random values). Random values are generated using {@code
     * random};
     * <p>
     * When a cipher instance is initialized by a call to any of the {@code
     * init} methods, the state of the instance is overridden, means it is
     * equivalent to creating a new instance and calling it {@code init} method.
     *
     * @param opmode
     *            the operation this cipher instance should be initialized for
     *            (one of: {@code ENCRYPT_MODE}, {@code DECRYPT_MODE}, {@code
     *            WRAP_MODE} or {@code UNWRAP_MODE}).
     * @param key
     *            the input key for the operation.
     * @param random
     *            the source of randomness to use.
     * @throws InvalidKeyException
     *             if the specified key can not be used to initialize this
     *             cipher instance.
     * @throws InvalidParameterException
     *             if the specified opmode is invalid.
     */
    public final void init(int opmode, Key key, SecureRandom random) throws InvalidKeyException {
        checkMode(opmode);
        //        FIXME InvalidKeyException
        //        if keysize exceeds the maximum allowable keysize
        //        (jurisdiction policy files)
        try {
            getSpi(new InitParams(InitType.KEY, opmode, key, random, null, null));
        } catch (InvalidAlgorithmParameterException e) {
            // Should never happen since we only specified the key.
            throw new ProviderException("Invalid parameters when params == null", e);
        }
        mode = opmode;
    }

    /**
     * Initializes this cipher instance with the specified key and algorithm
     * parameters.
     * <p>
     * The cipher is initialized for the specified operational mode (one of:
     * encryption, decryption, key wrapping or key unwrapping).
     * <p>
     * If this cipher instance needs any algorithm parameters and {@code params}
     * is {@code null}, the underlying implementation of this cipher is supposed
     * to generate the required parameters (using its provider or random
     * values).
     * <p>
     * When a cipher instance is initialized by a call to any of the {@code
     * init} methods, the state of the instance is overridden, means it is
     * equivalent to creating a new instance and calling it {@code init} method.
     *
     * @param opmode
     *            the operation this cipher instance should be initialized for
     *            (one of: {@code ENCRYPT_MODE}, {@code DECRYPT_MODE}, {@code
     *            WRAP_MODE} or {@code UNWRAP_MODE}).
     * @param key
     *            the input key for the operation.
     * @param params
     *            the algorithm parameters.
     * @throws InvalidKeyException
     *             if the specified key can not be used to initialize this
     *             cipher instance.
     * @throws InvalidAlgorithmParameterException
     *             it the specified parameters are inappropriate for this
     *             cipher.
     */
    public final void init(int opmode, Key key, AlgorithmParameterSpec params)
            throws InvalidKeyException, InvalidAlgorithmParameterException {
        if (secureRandom == null) {
            secureRandom = new SecureRandom();
        }
        init(opmode, key, params, secureRandom);
    }

    /**
     * Initializes this cipher instance with the specified key, algorithm
     * parameters and a source of randomness.
     * <p>
     * The cipher is initialized for the specified operational mode (one of:
     * encryption, decryption, key wrapping or key unwrapping) depending on
     * {@code opmode}.
     * <p>
     * If this cipher instance needs any algorithm parameters and {@code params}
     * is {@code null}, the underlying implementation of this cipher is supposed
     * to generate the required parameters (using its provider or random
     * values). Random values are generated using {@code random};
     * <p>
     * When a cipher instance is initialized by a call to any of the {@code
     * init} methods, the state of the instance is overridden, meaning that it
     * is equivalent to creating a new instance and calling it {@code init}
     * method.
     *
     * @param opmode
     *            the operation this cipher instance should be initialized for
     *            (one of: {@code ENCRYPT_MODE}, {@code DECRYPT_MODE}, {@code
     *            WRAP_MODE} or {@code UNWRAP_MODE}).
     * @param key
     *            the input key for the operation.
     * @param params
     *            the algorithm parameters.
     * @param random
     *            the source of randomness to use.
     * @throws InvalidKeyException
     *             if the specified key can not be used to initialize this
     *             cipher instance.
     * @throws InvalidAlgorithmParameterException
     *             it the specified parameters are inappropriate for this
     *             cipher.
     * @throws InvalidParameterException
     *             if the specified {@code opmode} is invalid.
     */
    public final void init(int opmode, Key key, AlgorithmParameterSpec params,
            SecureRandom random) throws InvalidKeyException,
            InvalidAlgorithmParameterException {
        checkMode(opmode);
        //        FIXME InvalidKeyException
        //        if keysize exceeds the maximum allowable keysize
        //        (jurisdiction policy files)
        //        FIXME InvalidAlgorithmParameterException
        //        cryptographic strength exceed the legal limits
        //        (jurisdiction policy files)
        getSpi(new InitParams(InitType.ALGORITHM_PARAM_SPEC, opmode, key, random, params, null));
        mode = opmode;
    }

    /**
     * Initializes this cipher instance with the specified key and algorithm
     * parameters.
     * <p>
     * The cipher is initialized for the specified operation (one of:
     * encryption, decryption, key wrapping or key unwrapping) depending on
     * {@code opmode}.
     * <p>
     * If this cipher instance needs any algorithm parameters and {@code params}
     * is {@code null}, the underlying implementation of this cipher is supposed
     * to generate the required parameters (using its provider or random
     * values).
     * <p>
     * When a cipher instance is initialized by a call to any of the {@code
     * init} methods, the state of the instance is overridden, meaning that it
     * is equivalent to creating a new instance and calling it {@code init}
     * method.
     *
     * @param opmode
     *            the operation this cipher instance should be initialized for
     *            (one of: {@code ENCRYPT_MODE}, {@code DECRYPT_MODE}, {@code
     *            WRAP_MODE} or {@code UNWRAP_MODE}).
     * @param key
     *            the input key for the operation.
     * @param params
     *            the algorithm parameters.
     * @throws InvalidKeyException
     *             if the specified key can not be used to initialize this
     *             cipher instance.
     * @throws InvalidAlgorithmParameterException
     *             it the specified parameters are inappropriate for this
     *             cipher.
     */
    public final void init(int opmode, Key key, AlgorithmParameters params)
            throws InvalidKeyException, InvalidAlgorithmParameterException {
        if (secureRandom == null) {
            secureRandom = new SecureRandom();
        }
        init(opmode, key, params, secureRandom);
    }

    /**
     * Initializes this cipher instance with the specified key, algorithm
     * parameters and a source of randomness.
     * <p>
     * The cipher will be initialized for the specified operation (one of:
     * encryption, decryption, key wrapping or key unwrapping) depending on
     * {@code opmode}.
     * <p>
     * If this cipher instance needs any algorithm parameters and {@code params}
     * is {@code null}, the underlying implementation of this cipher is supposed
     * to generate the required parameters (using its provider or random
     * values). Random values are generated using {@code random}.
     * <p>
     * When a cipher instance is initialized by a call to any of the {@code
     * init} methods, the state of the instance is overridden, means it is
     * equivalent to creating a new instance and calling it {@code init} method.
     *
     * @param opmode
     *            the operation this cipher instance should be initialized for
     *            (one of: {@code ENCRYPT_MODE}, {@code DECRYPT_MODE}, {@code
     *            WRAP_MODE} or {@code UNWRAP_MODE}).
     * @param key
     *            the input key for the operation.
     * @param params
     *            the algorithm parameters.
     * @param random
     *            the source of randomness to use.
     * @throws InvalidKeyException
     *             if the specified key can not be used to initialize this
     *             cipher instance.
     * @throws InvalidAlgorithmParameterException
     *             if the specified parameters are inappropriate for this
     *             cipher.
     * @throws InvalidParameterException
     *             if the specified {@code opmode} is invalid.
     */
    public final void init(int opmode, Key key, AlgorithmParameters params,
            SecureRandom random) throws InvalidKeyException,
            InvalidAlgorithmParameterException {
        checkMode(opmode);
        //        FIXME InvalidKeyException
        //        if keysize exceeds the maximum allowable keysize
        //        (jurisdiction policy files)
        //        FIXME InvalidAlgorithmParameterException
        //        cryptographic strength exceed the legal limits
        //        (jurisdiction policy files)
        getSpi(new InitParams(InitType.ALGORITHM_PARAMS, opmode, key, random, null, params));
        mode = opmode;
    }

    /**
     * Initializes this cipher instance with the public key from the specified
     * certificate.
     * <p>
     * The cipher will be initialized for the specified operation (one of:
     * encryption, decryption, key wrapping or key unwrapping) depending on
     * {@code opmode}.
     * <p>
     * It the type of the certificate is X.509 and the certificate has a <i>key
     * usage</i> extension field marked as critical, the specified {@code
     * opmode} has the be enabled for this key, otherwise an {@code
     * InvalidKeyException} is thrown.
     * <p>
     * If this cipher instance needs any algorithm parameters that the key in
     * the certificate can not provide, the underlying implementation of this
     * cipher is supposed to generate the required parameters (using its
     * provider or random values).
     * <p>
     * When a cipher instance is initialized by a call to any of the {@code
     * init} methods, the state of the instance is overridden, means it is
     * equivalent to creating a new instance and calling it {@code init} method.
     *
     * @param opmode
     *            the operation this cipher instance should be initialized for
     *            (one of: {@code ENCRYPT_MODE}, {@code DECRYPT_MODE}, {@code
     *            WRAP_MODE} or {@code UNWRAP_MODE}).
     * @param certificate
     *            the certificate.
     * @throws InvalidKeyException
     *             if the public key in the certificate can not be used to
     *             initialize this cipher instance.
     */
    public final void init(int opmode, Certificate certificate)
            throws InvalidKeyException {
        if (secureRandom == null) {
            secureRandom = new SecureRandom();
        }
        init(opmode, certificate, secureRandom);
    }

    /**
     * Initializes this cipher instance with the public key from the specified
     * certificate and a source of randomness.
     * <p>
     * The cipher will be initialized for the specified operation (one of:
     * encryption, decryption, key wrapping or key unwrapping) depending on
     * {@code opmode}.
     * <p>
     * It the type of the certificate is X.509 and the certificate has a <i>key
     * usage</i> extension field marked as critical, the specified {@code
     * opmode} has the be enabled for this key, otherwise an {@code
     * InvalidKeyException} is thrown.
     * <p>
     * If this cipher instance needs any algorithm parameters that the key in
     * the certificate can not provide, the underlying implementation of this
     * cipher is supposed to generate the required parameters (using its
     * provider or random values). Random values are generated using {@code
     * random}.
     * <p>
     * When a cipher instance is initialized by a call to any of the {@code
     * init} methods, the state of the instance is overridden, means it is
     * equivalent to creating a new instance and calling it {@code init} method.
     *
     * @param opmode
     *            the operation this cipher instance should be initialized for
     *            (one of: {@code ENCRYPT_MODE}, {@code DECRYPT_MODE}, {@code
     *            WRAP_MODE} or {@code UNWRAP_MODE}).
     * @param certificate
     *            the certificate.
     * @param random
     *            the source of randomness to be used.
     * @throws InvalidKeyException
     *             if the public key in the certificate can not be used to
     *             initialize this cipher instance.
     */
    public final void init(int opmode, Certificate certificate,
            SecureRandom random) throws InvalidKeyException {
        checkMode(opmode);
        if (certificate instanceof X509Certificate) {
            Set<String> ce = ((X509Certificate) certificate).getCriticalExtensionOIDs();
            boolean critical = false;
            if (ce != null && !ce.isEmpty()) {
                for (String oid : ce) {
                    if (oid.equals("2.5.29.15")) { // KeyUsage OID = 2.5.29.15
                        critical = true;
                        break;
                    }
                }
                if (critical) {
                    boolean[] keyUsage = ((X509Certificate) certificate).getKeyUsage();
                    // As specified in RFC 3280:
                    //   Internet X.509 Public Key Infrastructure
                    //   Certificate and Certificate Revocation List (CRL) Profile.
                    // Section 4.2.1.3  Key Usage
                    // http://www.ietf.org/rfc/rfc3280.txt
                    //
                    // KeyUsage ::= BIT STRING {digitalSignature (0),
                    //                          nonRepudiation   (1),
                    //                          keyEncipherment  (2),
                    //                          dataEncipherment (3),
                    //                          keyAgreement     (4),
                    //                          keyCertSign      (5),
                    //                          cRLSign          (6),
                    //                          encipherOnly     (7),
                    //                          decipherOnly     (8) }
                    if (keyUsage != null) {
                        if (opmode == ENCRYPT_MODE && !keyUsage[3]) {
                            throw new InvalidKeyException("The public key in the certificate "
                                                          + "cannot be used for ENCRYPT_MODE");
                        } else if (opmode == WRAP_MODE && !keyUsage[2]) {
                            throw new InvalidKeyException("The public key in the certificate "
                                                          + "cannot be used for WRAP_MODE");
                        }
                    }
                }
            }
        }
        //        FIXME InvalidKeyException
        //        if keysize exceeds the maximum allowable keysize
        //        (jurisdiction policy files)
        final Key key = certificate.getPublicKey();
        try {
            getSpi(new InitParams(InitType.KEY, opmode, key, random, null, null));
        } catch (InvalidAlgorithmParameterException e) {
            // Should never happen since we only specified the key.
            throw new ProviderException("Invalid parameters when params == null", e);
        }
        mode = opmode;
    }

    /**
     * Continues a multi-part transformation (encryption or decryption). The
     * transformed bytes are returned.
     *
     * @param input
     *            the input bytes to transform.
     * @return the transformed bytes in a new buffer, or {@code null} if the
     *         input has zero length.
     * @throws IllegalStateException
     *             if this cipher instance is not initialized for encryption or
     *             decryption.
     * @throws IllegalArgumentException
     *             if the input is {@code null}.
     */
    public final byte[] update(byte[] input) {
        if (mode != ENCRYPT_MODE && mode != DECRYPT_MODE) {
            throw new IllegalStateException();
        }
        if (input == null) {
            throw new IllegalArgumentException("input == null");
        }
        if (input.length == 0) {
            return null;
        }
        return getSpi().engineUpdate(input, 0, input.length);
    }

    /**
     * Continues a multi-part transformation (encryption or decryption). The
     * transformed bytes are returned.
     *
     * @param input
     *            the input bytes to transform.
     * @param inputOffset
     *            the offset in the input to start.
     * @param inputLen
     *            the length of the input to transform.
     * @return the transformed bytes in a new buffer, or {@code null} if {@code inputLen} is zero.
     * @throws IllegalStateException
     *             if this cipher instance is not initialized for encryption or
     *             decryption.
     * @throws IllegalArgumentException
     *             if {@code input} is {@code null}, or if {@code inputOffset} and
     *             {@code inputLen} do not specify a valid chunk in the input
     *             buffer.
     */
    public final byte[] update(byte[] input, int inputOffset, int inputLen) {
        if (mode != ENCRYPT_MODE && mode != DECRYPT_MODE) {
            throw new IllegalStateException();
        }
        if (input == null) {
            throw new IllegalArgumentException("input == null");
        }
        checkInputOffsetAndCount(input.length, inputOffset, inputLen);
        if (inputLen == 0) {
            return null;
        }
        return getSpi().engineUpdate(input, inputOffset, inputLen);
    }

    private static void checkInputOffsetAndCount(int inputArrayLength,
                                                 int inputOffset,
                                                 int inputLen) {
        if ((inputOffset | inputLen) < 0
                || inputOffset > inputArrayLength
                || inputArrayLength - inputOffset < inputLen) {
            throw new IllegalArgumentException("input.length=" + inputArrayLength
                                               + "; inputOffset=" + inputOffset
                                               + "; inputLen=" + inputLen);
        }
    }

    /**
     * Continues a multi-part transformation (encryption or decryption). The
     * transformed bytes are stored in the {@code output} buffer.
     * <p>
     * If the size of the {@code output} buffer is too small to hold the result,
     * a {@code ShortBufferException} is thrown. Use
     * {@link Cipher#getOutputSize getOutputSize} to check for the size of the
     * output buffer.
     *
     * @param input
     *            the input bytes to transform.
     * @param inputOffset
     *            the offset in the input to start.
     * @param inputLen
     *            the length of the input to transform.
     * @param output
     *            the output buffer.
     * @return the number of bytes placed in output.
     * @throws ShortBufferException
     *             if the size of the {@code output} buffer is too small.
     * @throws IllegalStateException
     *             if this cipher instance is not initialized for encryption or
     *             decryption.
     * @throws IllegalArgumentException
     *             if the input is {@code null}, the output is {@code null}, or
     *             if {@code inputOffset} and {@code inputLen} do not specify a
     *             valid chunk in the input buffer.
     */
    public final int update(byte[] input, int inputOffset, int inputLen,
            byte[] output) throws ShortBufferException {
        return update(input, inputOffset, inputLen, output, 0);
    }

    /**
     * Continues a multi-part transformation (encryption or decryption). The
     * transformed bytes are stored in the {@code output} buffer.
     * <p>
     * If the size of the {@code output} buffer is too small to hold the result,
     * a {@code ShortBufferException} is thrown. Use
     * {@link Cipher#getOutputSize getOutputSize} to check for the size of the
     * output buffer.
     *
     * @param input
     *            the input bytes to transform.
     * @param inputOffset
     *            the offset in the input to start.
     * @param inputLen
     *            the length of the input to transform.
     * @param output
     *            the output buffer.
     * @param outputOffset
     *            the offset in the output buffer.
     * @return the number of bytes placed in output.
     * @throws ShortBufferException
     *             if the size of the {@code output} buffer is too small.
     * @throws IllegalStateException
     *             if this cipher instance is not initialized for encryption or
     *             decryption.
     * @throws IllegalArgumentException
     *             if the input is {@code null}, the output is {@code null}, or
     *             if {@code inputOffset} and {@code inputLen} do not specify a
     *             valid chunk in the input buffer.
     */
    public final int update(byte[] input, int inputOffset, int inputLen,
            byte[] output, int outputOffset) throws ShortBufferException {
        if (mode != ENCRYPT_MODE && mode != DECRYPT_MODE) {
            throw new IllegalStateException();
        }
        if (input == null) {
            throw new IllegalArgumentException("input == null");
        }
        if (output == null) {
            throw new IllegalArgumentException("output == null");
        }
        if (outputOffset < 0) {
            throw new IllegalArgumentException("outputOffset < 0. outputOffset=" + outputOffset);
        }
        checkInputOffsetAndCount(input.length, inputOffset, inputLen);
        if (input.length == 0) {
            return 0;
        }
        return getSpi().engineUpdate(input, inputOffset, inputLen, output,
                outputOffset);
    }

    /**
     * Continues a multi-part transformation (encryption or decryption). The
     * {@code input.remaining()} bytes starting at {@code input.position()} are
     * transformed and stored in the {@code output} buffer.
     * <p>
     * If the {@code output.remaining()} is too small to hold the transformed
     * bytes a {@code ShortBufferException} is thrown. Use
     * {@link Cipher#getOutputSize getOutputSize} to check for the size of the
     * output buffer.
     *
     * @param input
     *            the input buffer to transform.
     * @param output
     *            the output buffer to store the result within.
     * @return the number of bytes stored in the output buffer.
     * @throws ShortBufferException
     *             if the size of the {@code output} buffer is too small.
     * @throws IllegalStateException
     *             if this cipher instance is not initialized for encryption or
     *             decryption.
     * @throws IllegalArgumentException
     *             if the input buffer and the output buffer are the identical
     *             object.
     */
    public final int update(ByteBuffer input, ByteBuffer output)
            throws ShortBufferException {
        if (mode != ENCRYPT_MODE && mode != DECRYPT_MODE) {
            throw new IllegalStateException();
        }
        if (input == output) {
            throw new IllegalArgumentException("input == output");
        }
        return getSpi().engineUpdate(input, output);
    }

    /**
     * Continues a multi-part transformation (encryption or decryption) with
     * Authenticated Additional Data (AAD). AAD may only be added after the
     * {@code Cipher} is initialized and before any data is passed to the
     * instance.
     * <p>
     * This is only usable with cipher modes that support Authenticated
     * Encryption with Additional Data (AEAD) such as Galois/Counter Mode (GCM).
     *
     * @param input bytes of AAD to use with the cipher
     * @throws IllegalStateException
     *             if this cipher instance is not initialized for encryption or
     *             decryption.
     * @throws IllegalArgumentException
     *             if {@code input} is {@code null}
     * @throws UnsupportedOperationException if the cipher does not support AEAD
     * @since 1.7
     */
    public final void updateAAD(byte[] input) {
        if (input == null) {
            throw new IllegalArgumentException("input == null");
        }
        if (mode != ENCRYPT_MODE && mode != DECRYPT_MODE) {
            throw new IllegalStateException();
        }
        if (input.length == 0) {
            return;
        }
        getSpi().engineUpdateAAD(input, 0, input.length);
    }

    /**
     * Continues a multi-part transformation (encryption or decryption) with
     * Authenticated Additional Data (AAD). AAD may only be added after the
     * {@code Cipher} is initialized and before any data is passed to the
     * instance.
     * <p>
     * This is only usable with cipher modes that support Authenticated
     * Encryption with Additional Data (AEAD) such as Galois/Counter Mode (GCM).
     *
     * @param input bytes of AAD to use with the cipher
     * @param inputOffset offset within bytes of additional data to add to cipher
     * @param inputLen length of bytes of additional data to add to cipher
     * @throws IllegalStateException
     *             if this cipher instance is not initialized for encryption or
     *             decryption.
     * @throws IllegalArgumentException
     *             if {@code input} is {@code null}, or if {@code inputOffset} and
     *             {@code inputLen} do not specify a valid chunk in the input
     *             buffer.
     * @throws UnsupportedOperationException if the cipher does not support AEAD
     * @since 1.7
     */
    public final void updateAAD(byte[] input, int inputOffset, int inputLen) {
        if (mode != ENCRYPT_MODE && mode != DECRYPT_MODE) {
            throw new IllegalStateException();
        }
        if (input == null) {
            throw new IllegalArgumentException("input == null");
        }
        checkInputOffsetAndCount(input.length, inputOffset, inputLen);
        if (input.length == 0) {
            return;
        }
        getSpi().engineUpdateAAD(input, inputOffset, inputLen);
    }

    /**
     * Continues a multi-part transformation (encryption or decryption) with
     * Authenticated Additional Data (AAD). AAD may only be added after the
     * {@code Cipher} is initialized and before any data is passed to the
     * instance.
     * <p>
     * This is only usable with cipher modes that support Authenticated
     * Encryption with Additional Data (AEAD) such as Galois/Counter Mode (GCM).
     *
     * @param input buffer of AAD to be used
     * @throws IllegalStateException
     *             if this cipher instance is not initialized for encryption or
     *             decryption.
     * @throws UnsupportedOperationException if the cipher does not support AEAD
     * @since 1.7
     */
    public final void updateAAD(ByteBuffer input) {
        if (mode != ENCRYPT_MODE && mode != DECRYPT_MODE) {
            throw new IllegalStateException("Cipher is not initialized");
        }
        if (input == null) {
            throw new IllegalArgumentException("input == null");
        }
        getSpi().engineUpdateAAD(input);
    }

    /**
     * Finishes a multi-part transformation (encryption or decryption).
     * <p>
     * Processes any bytes that may have been buffered in previous {@code
     * update} calls.
     *
     * @return the final bytes from the transformation.
     * @throws IllegalBlockSizeException
     *             if the size of the resulting bytes is not a multiple of the
     *             cipher block size.
     * @throws BadPaddingException
     *             if the padding of the data does not match the padding scheme.
     * @throws IllegalStateException
     *             if this cipher instance is not initialized for encryption or
     *             decryption.
     */
    public final byte[] doFinal() throws IllegalBlockSizeException,
            BadPaddingException {
        if (mode != ENCRYPT_MODE && mode != DECRYPT_MODE) {
            throw new IllegalStateException();
        }
        return getSpi().engineDoFinal(null, 0, 0);
    }

    /**
     * Finishes a multi-part transformation (encryption or decryption).
     * <p>
     * Processes any bytes that may have been buffered in previous {@code
     * update} calls.
     * <p>
     * The final transformed bytes are stored in the {@code output} buffer.
     *
     * @param output
     *            the output buffer.
     * @param outputOffset
     *            the offset in the output buffer.
     * @return the number of bytes placed in the output buffer.
     * @throws IllegalBlockSizeException
     *             if the size of the resulting bytes is not a multiple of the
     *             cipher block size.
     * @throws ShortBufferException
     *             if the size of the {@code output} buffer is too small.
     * @throws BadPaddingException
     *             if the padding of the data does not match the padding scheme.
     * @throws IllegalStateException
     *             if this cipher instance is not initialized for encryption or
     *             decryption.
     */
    public final int doFinal(byte[] output, int outputOffset)
            throws IllegalBlockSizeException, ShortBufferException,
            BadPaddingException {
        if (mode != ENCRYPT_MODE && mode != DECRYPT_MODE) {
            throw new IllegalStateException();
        }
        if (outputOffset < 0) {
            throw new IllegalArgumentException("outputOffset < 0. outputOffset=" + outputOffset);
        }
        return getSpi().engineDoFinal(null, 0, 0, output, outputOffset);
    }

    /**
     * Finishes a multi-part transformation (encryption or decryption).
     * <p>
     * Processes the bytes in {@code input} buffer, and any bytes that have been
     * buffered in previous {@code update} calls.
     *
     * @param input
     *            the input buffer.
     * @return the final bytes from the transformation.
     * @throws IllegalBlockSizeException
     *             if the size of the resulting bytes is not a multiple of the
     *             cipher block size.
     * @throws BadPaddingException
     *             if the padding of the data does not match the padding scheme.
     * @throws IllegalStateException
     *             if this cipher instance is not initialized for encryption or
     *             decryption.
     */
    public final byte[] doFinal(byte[] input) throws IllegalBlockSizeException,
            BadPaddingException {
        if (mode != ENCRYPT_MODE && mode != DECRYPT_MODE) {
            throw new IllegalStateException();
        }
        return getSpi().engineDoFinal(input, 0, input.length);
    }

    /**
     * Finishes a multi-part transformation (encryption or decryption).
     * <p>
     * Processes the {@code inputLen} bytes in {@code input} buffer at {@code
     * inputOffset}, and any bytes that have been buffered in previous {@code
     * update} calls.
     *
     * @param input
     *            the input buffer.
     * @param inputOffset
     *            the offset in the input buffer.
     * @param inputLen
     *            the length of the input
     * @return the final bytes from the transformation.
     * @throws IllegalBlockSizeException
     *             if the size of the resulting bytes is not a multiple of the
     *             cipher block size.
     * @throws BadPaddingException
     *             if the padding of the data does not match the padding scheme.
     * @throws IllegalStateException
     *             if this cipher instance is not initialized for encryption or
     *             decryption.
     * @throws IllegalArgumentException
     *             if {@code inputOffset} and {@code inputLen} do not specify an
     *             valid chunk in the input buffer.
     */
    public final byte[] doFinal(byte[] input, int inputOffset, int inputLen)
            throws IllegalBlockSizeException, BadPaddingException {
        if (mode != ENCRYPT_MODE && mode != DECRYPT_MODE) {
            throw new IllegalStateException();
        }
        checkInputOffsetAndCount(input.length, inputOffset, inputLen);
        return getSpi().engineDoFinal(input, inputOffset, inputLen);
    }

    /**
     * Finishes a multi-part transformation (encryption or decryption).
     * <p>
     * Processes the {@code inputLen} bytes in {@code input} buffer at {@code
     * inputOffset}, and any bytes that have been buffered in previous {@code
     * update} calls.
     *
     * @param input
     *            the input buffer.
     * @param inputOffset
     *            the offset in the input buffer.
     * @param inputLen
     *            the length of the input.
     * @param output
     *            the output buffer for the transformed bytes.
     * @return the number of bytes placed in the output buffer.
     * @throws ShortBufferException
     *             if the size of the {@code output} buffer is too small.
     * @throws IllegalBlockSizeException
     *             if the size of the resulting bytes is not a multiple of the
     *             cipher block size.
     * @throws BadPaddingException
     *             if the padding of the data does not match the padding scheme.
     * @throws IllegalStateException
     *             if this cipher instance is not initialized for encryption or
     *             decryption.
     * @throws IllegalArgumentException
     *             if {@code inputOffset} and {@code inputLen} do not specify an
     *             valid chunk in the input buffer.
     */
    public final int doFinal(byte[] input, int inputOffset, int inputLen,
            byte[] output) throws ShortBufferException,
            IllegalBlockSizeException, BadPaddingException {
        return doFinal(input, inputOffset, inputLen, output, 0);
    }

    /**
     * Finishes a multi-part transformation (encryption or decryption).
     * <p>
     * Processes the {@code inputLen} bytes in {@code input} buffer at {@code
     * inputOffset}, and any bytes that have been buffered in previous {@code
     * update} calls.
     *
     * @param input
     *            the input buffer.
     * @param inputOffset
     *            the offset in the input buffer.
     * @param inputLen
     *            the length of the input.
     * @param output
     *            the output buffer for the transformed bytes.
     * @param outputOffset
     *            the offset in the output buffer.
     * @return the number of bytes placed in the output buffer.
     * @throws ShortBufferException
     *             if the size of the {@code output} buffer is too small.
     * @throws IllegalBlockSizeException
     *             if the size of the resulting bytes is not a multiple of the
     *             cipher block size.
     * @throws BadPaddingException
     *             if the padding of the data does not match the padding scheme.
     * @throws IllegalStateException
     *             if this cipher instance is not initialized for encryption or
     *             decryption.
     * @throws IllegalArgumentException
     *             if {@code inputOffset} and {@code inputLen} do not specify an
     *             valid chunk in the input buffer.
     */
    public final int doFinal(byte[] input, int inputOffset, int inputLen,
            byte[] output, int outputOffset) throws ShortBufferException,
            IllegalBlockSizeException, BadPaddingException {
        if (mode != ENCRYPT_MODE && mode != DECRYPT_MODE) {
            throw new IllegalStateException();
        }
        checkInputOffsetAndCount(input.length, inputOffset, inputLen);
        return getSpi().engineDoFinal(input, inputOffset, inputLen, output,
                outputOffset);
    }

    /**
     * Finishes a multi-part transformation (encryption or decryption).
     * <p>
     * Processes the {@code input.remaining()} bytes in {@code input} buffer at
     * {@code input.position()}, and any bytes that have been buffered in
     * previous {@code update} calls. The transformed bytes are placed into
     * {@code output} buffer.
     *
     * @param input
     *            the input buffer.
     * @param output
     *            the output buffer.
     * @return the number of bytes placed into the output buffer.
     * @throws ShortBufferException
     *             if the size of the {@code output} buffer is too small.
     * @throws IllegalBlockSizeException
     *             if the size of the resulting bytes is not a multiple of the
     *             cipher block size.
     * @throws BadPaddingException
     *             if the padding of the data does not match the padding scheme.
     * @throws IllegalArgumentException
     *             if the input buffer and the output buffer are the same
     *             object.
     * @throws IllegalStateException
     *             if this cipher instance is not initialized for encryption or
     *             decryption.
     */
    public final int doFinal(ByteBuffer input, ByteBuffer output)
            throws ShortBufferException, IllegalBlockSizeException,
            BadPaddingException {
        if (mode != ENCRYPT_MODE && mode != DECRYPT_MODE) {
            throw new IllegalStateException();
        }
        if (input == output) {
            throw new IllegalArgumentException("input == output");
        }
        return getSpi().engineDoFinal(input, output);
    }

    /**
     * Wraps a key using this cipher instance.
     *
     * @param key
     *            the key to wrap.
     * @return the wrapped key.
     * @throws IllegalBlockSizeException
     *             if the size of the resulting bytes is not a multiple of the
     *             cipher block size.
     * @throws InvalidKeyException
     *             if this cipher instance can not wrap this key.
     * @throws IllegalStateException
     *             if this cipher instance is not initialized for wrapping.
     */
    public final byte[] wrap(Key key) throws IllegalBlockSizeException,
            InvalidKeyException {
        if (mode != WRAP_MODE) {
            throw new IllegalStateException();
        }
        return getSpi().engineWrap(key);
    }

    /**
     * Unwraps a key using this cipher instance.
     *
     * @param wrappedKey
     *            the wrapped key to unwrap.
     * @param wrappedKeyAlgorithm
     *            the algorithm for the wrapped key.
     * @param wrappedKeyType
     *            the type of the wrapped key (one of: {@code SECRET_KEY
     *            <code>, <code>PRIVATE_KEY} or {@code PUBLIC_KEY})
     * @return the unwrapped key
     * @throws InvalidKeyException
     *             if the {@code wrappedKey} can not be unwrapped to a key of
     *             type {@code wrappedKeyType} for the {@code
     *             wrappedKeyAlgorithm}.
     * @throws NoSuchAlgorithmException
     *             if no provider can be found that can create a key of type
     *             {@code wrappedKeyType} for the {@code wrappedKeyAlgorithm}.
     * @throws IllegalStateException
     *             if this cipher instance is not initialized for unwrapping.
     */
    public final Key unwrap(byte[] wrappedKey, String wrappedKeyAlgorithm,
            int wrappedKeyType) throws InvalidKeyException,
            NoSuchAlgorithmException {
        if (mode != UNWRAP_MODE) {
            throw new IllegalStateException();
        }
        return getSpi().engineUnwrap(wrappedKey, wrappedKeyAlgorithm,
                wrappedKeyType);
    }

    /**
     * Returns the maximum key length for the specified transformation.
     *
     * @param transformation
     *            the transformation name.
     * @return the maximum key length, currently {@code Integer.MAX_VALUE}.
     * @throws NoSuchAlgorithmException
     *             if no provider for the specified {@code transformation} can
     *             be found.
     * @throws NullPointerException
     *             if {@code transformation} is {@code null}.
     */
    public static final int getMaxAllowedKeyLength(String transformation)
            throws NoSuchAlgorithmException {
        if (transformation == null) {
            throw new NullPointerException("transformation == null");
        }
        checkTransformation(transformation);
        //FIXME jurisdiction policy files
        return Integer.MAX_VALUE;
    }

    /**
     * Returns the maximum cipher parameter value for the specified
     * transformation. If there is no maximum limit, {@code null} is returned.
     *
     * @param transformation
     *            the transformation name.
     * @return a parameter spec holding the maximum value or {@code null}.
     *         Currently {@code null}.
     * @throws NoSuchAlgorithmException
     *             if no provider for the specified {@code transformation} can
     *             be found.
     * @throws NullPointerException
     *             if {@code transformation} is {@code null}.
     */
    public static final AlgorithmParameterSpec getMaxAllowedParameterSpec(
            String transformation) throws NoSuchAlgorithmException {
        if (transformation == null) {
            throw new NullPointerException("transformation == null");
        }
        checkTransformation(transformation);
        //FIXME jurisdiction policy files
        return null;
    }
}
