/*
 * 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.myfaces.shared_tomahawk.util;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.UnsupportedEncodingException;
import java.security.AccessController;
import java.security.NoSuchAlgorithmException;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Arrays;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.faces.FacesException;
import javax.faces.application.ViewExpiredException;
import javax.faces.context.ExternalContext;
import javax.servlet.ServletContext;

import org.apache.commons.codec.binary.Base64;
import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFWebConfigParam;
import org.apache.myfaces.shared_tomahawk.util.serial.SerialFactory;

/**
 * <p>This Class exposes a handful of methods related to encryption,
 * compression and serialization of the view state.</p>
 * 
 * <ul>
 * <li>ISO-8859-1 is the character set used.</li>
 * <li>GZIP is used for all compression/decompression.</li>
 * <li>Base64 is used for all encoding and decoding.</li>
 * <li>DES is the default encryption algorithm</li>
 * <li>ECB is the default mode</li>
 * <li>PKCS5Padding is the default padding</li>
 * <li>HmacSHA1 is the default MAC algorithm</li>
 * <li>The default algorithm can be overridden using the
 * <i>org.apache.myfaces.ALGORITHM</i> parameter</li>
 * <li>The default mode and padding can be overridden using the
 * <i>org.apache.myfaces.ALGORITHM.PARAMETERS</i> parameter</li>
 * <li>This class has not been tested with modes other than ECB and CBC</li>
 * <li>An initialization vector can be specified via the
 * <i>org.apache.myfaces.ALGORITHM.IV</i> parameter</li>
 * <li>The default MAC algorithm can be overridden using the
 * <i>org.apache.myfaces.MAC_ALGORITHM</i> parameter</li>
 * </ul>
 *
 * <p>The secret is interpretted as base 64 encoded.  In other
 * words, if your secret is "76543210", you would put "NzY1NDMyMTA=" in
 * the deployment descriptor.  This is needed so that key values are not
 * limited to just values composed of printable characters.</p>
 *
 * <p>If you are using CBC mode encryption, you <b>must</b> specify an
 * initialization vector.</p>
 *
 * <p>If you are using the AES algorithm and getting a SecurityException
 * complaining about keysize, you most likely need to get the unlimited
 * strength jurisdiction policy files from a place like
 * http://java.sun.com/j2se/1.4.2/download.html .</p>
 *
 * @author Dennis C. Byrne
 * @see org.apache.myfaces.webapp.StartupServletContextListener
 */
public final class StateUtils {

    //private static final Log log = LogFactory.getLog(StateUtils.class);
    private static final Logger log = Logger.getLogger(StateUtils.class.getName());

    public static final String ZIP_CHARSET = "ISO-8859-1";

    public static final String DEFAULT_ALGORITHM = "DES";
    public static final String DEFAULT_ALGORITHM_PARAMS = "ECB/PKCS5Padding";

    public static final String INIT_PREFIX = "org.apache.myfaces.";
    
    /**
     * Indicate if the view state is encrypted or not. By default, encryption is enabled.
     */
    @JSFWebConfigParam(name="org.apache.myfaces.USE_ENCRYPTION",since="1.1",defaultValue="true",expectedValues="true,false",group="state")
    public static final String USE_ENCRYPTION = INIT_PREFIX + "USE_ENCRYPTION";
    
    /**
     * Defines the secret (Base64 encoded) used to initialize the secret key
     * for encryption algorithm. See MyFaces wiki/web site documentation 
     * for instructions on how to configure an application for 
     * different encryption strengths.
     */
    @JSFWebConfigParam(name="org.apache.myfaces.SECRET",since="1.1",group="state")
    public static final String INIT_SECRET = INIT_PREFIX + "SECRET";
    
    /**
     * Indicate the encryption algorithm used for encrypt the view state.
     */
    @JSFWebConfigParam(name="org.apache.myfaces.ALGORITHM",since="1.1",defaultValue="DES",group="state",tags="performance")
    public static final String INIT_ALGORITHM = INIT_PREFIX + "ALGORITHM";

    /**
     * If is set to "false", the secret key used for encryption algorithm is not cached. This is used
     * when the returned SecretKey for encryption algorithm is not thread safe. 
     */
    @JSFWebConfigParam(name="org.apache.myfaces.SECRET.CACHE",since="1.1",group="state")
    public static final String INIT_SECRET_KEY_CACHE = INIT_SECRET + ".CACHE";
    
    /**
     * Defines the initialization vector (Base64 encoded) used for the encryption algorithm
     */
    @JSFWebConfigParam(name="org.apache.myfaces.ALGORITHM.IV",since="1.1",group="state")
    public static final String INIT_ALGORITHM_IV = INIT_ALGORITHM + ".IV";
    
    /**
     * Defines the default mode and padding used for the encryption algorithm
     */
    @JSFWebConfigParam(name="org.apache.myfaces.ALGORITHM.PARAMETERS",since="1.1",defaultValue="ECB/PKCS5Padding",group="state")
    public static final String INIT_ALGORITHM_PARAM = INIT_ALGORITHM + ".PARAMETERS";
    
    /**
     * Defines the factory class name using for serialize/deserialize the view state returned 
     * by state manager into a byte array. The expected class must implement
     * org.apache.myfaces.shared_tomahawk.util.serial.SerialFactory interface.
     */
    @JSFWebConfigParam(name="org.apache.myfaces.SERIAL_FACTORY", since="1.1",group="state",tags="performance")
    public static final String SERIAL_FACTORY = INIT_PREFIX + "SERIAL_FACTORY";
    
    /**
     * Indicate if the view state should be compressed before encrypted(optional) and encoded
     */
    @JSFWebConfigParam(name="org.apache.myfaces.COMPRESS_STATE_IN_CLIENT",since="1.1",defaultValue="false",expectedValues="true,false",group="state",tags="performance")
    public static final String COMPRESS_STATE_IN_CLIENT = INIT_PREFIX + "COMPRESS_STATE_IN_CLIENT";

    public static final String DEFAULT_MAC_ALGORITHM = "HmacSHA1";

    /**
     * Indicate the algorithm used to calculate the Message Authentication Code that is
     * added to the view state.
     */
    @JSFWebConfigParam(name="org.apache.myfaces.MAC_ALGORITHM",defaultValue="HmacSHA1",group="state",tags="performance")
    public static final String INIT_MAC_ALGORITHM = "org.apache.myfaces.MAC_ALGORITHM";
    
    /**
     * Define the initialization code that are used to initialize the secret key used
     * on the Message Authentication Code algorithm
     */
    @JSFWebConfigParam(name="org.apache.myfaces.MAC_SECRET",group="state")
    public static final String INIT_MAC_SECRET = "org.apache.myfaces.MAC_SECRET";

    /**
     * If is set to "false", the secret key used for MAC algorithm is not cached. This is used
     * when the returned SecretKey for mac algorithm is not thread safe. 
     */
    @JSFWebConfigParam(name="org.apache.myfaces.MAC_SECRET.CACHE",group="state")
    public static final String INIT_MAC_SECRET_KEY_CACHE = "org.apache.myfaces.MAC_SECRET.CACHE";
    
    /** Utility class, do not instatiate */
    private StateUtils()
    {
        //nope
    }

    private static void testConfiguration(ExternalContext ctx){

        String algorithmParams = ctx.getInitParameter(INIT_ALGORITHM_PARAM);
        
        if (algorithmParams == null)
        {
            algorithmParams = ctx.getInitParameter(INIT_ALGORITHM_PARAM.toLowerCase());
        }
        String iv = ctx.getInitParameter(INIT_ALGORITHM_IV);
        
        if (iv == null)
        {
            iv = ctx.getInitParameter(INIT_ALGORITHM_IV.toLowerCase());
        }
        
        if (algorithmParams != null && algorithmParams.startsWith("CBC") )
        {
            if(iv == null)
                throw new FacesException(INIT_ALGORITHM_PARAM +
                                    " parameter has been set with CBC mode," +
                                    " but no initialization vector has been set " +
                                    " with " + INIT_ALGORITHM_IV);
        }

    }
    
    public static boolean enableCompression(ExternalContext ctx)
    {
        if(ctx == null)
            throw new NullPointerException("ExternalContext ctx");
    
        return "true".equals(ctx.getInitParameter(COMPRESS_STATE_IN_CLIENT));
    }
    
    public static boolean isSecure(ExternalContext ctx)
    {
        
        if(ctx == null)
            throw new NullPointerException("ExternalContext ctx");
        
        return ! "false".equals(ctx.getInitParameter(USE_ENCRYPTION));
    }

    /**
     * This fires during the Render Response phase, saving state.
     */

    public static final String construct(Object object, ExternalContext ctx){
        byte[] bytes = getAsByteArray(object, ctx);
        if( enableCompression(ctx) )
                bytes = compress(bytes);
        if(isSecure(ctx))
                bytes = encrypt(bytes, ctx);
        bytes = encode(bytes);
        try
        {
            return new String(bytes, ZIP_CHARSET);
        }
        catch (UnsupportedEncodingException e)
        {
            throw new FacesException(e);
        }
    }

    /**
     * Performs serialization with the serialization provider created by the 
     * SerialFactory.  
     * 
     * @param object
     * @param ctx
     * @return
     */
    
    public static final byte[] getAsByteArray(Object object, ExternalContext ctx)
    {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        
        // get the Factory that was instantiated @ startup
        SerialFactory serialFactory = (SerialFactory) ctx.getApplicationMap().get(SERIAL_FACTORY);
        
        if(serialFactory == null)
            throw new NullPointerException("serialFactory");
        
        try
        {
            ObjectOutputStream writer = serialFactory.getObjectOutputStream(outputStream);
            writer.writeObject(object);
            byte[] bytes = outputStream.toByteArray();
            writer.close();
            outputStream.close();
            writer = null;
            outputStream = null;
            return bytes;
        }
        catch (IOException e)
        {
            throw new FacesException(e);
        }
    }

    public static byte[] encrypt(byte[] insecure, ExternalContext ctx)
    {

        if (ctx == null)
            throw new NullPointerException("ExternalContext ctx");

        testConfiguration(ctx);
        
        SecretKey secretKey = (SecretKey) getSecret(ctx);
        String algorithm = findAlgorithm(ctx);
        String algorithmParams = findAlgorithmParams(ctx);
        byte[] iv = findInitializationVector(ctx);
        
        SecretKey macSecretKey = (SecretKey) getMacSecret(ctx);
        String macAlgorithm = findMacAlgorithm(ctx);
                
        try
        {
            // keep local to avoid threading issue
            Mac mac = Mac.getInstance(macAlgorithm);
            mac.init(macSecretKey);
            Cipher cipher = Cipher.getInstance(algorithm + "/" + algorithmParams);
            if (iv != null)
            {
                IvParameterSpec ivSpec = new IvParameterSpec(iv);
                cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
            }
            else
            {
                cipher.init(Cipher.ENCRYPT_MODE, secretKey);
            }
            if (log.isLoggable(Level.FINE))
            {
                log.fine("encrypting w/ " + algorithm + "/" + algorithmParams);
            }
            
            //EtM Composition Approach
            int macLenght = mac.getMacLength();
            byte[] secure = new byte[cipher.getOutputSize(insecure.length)+ macLenght];
            int secureCount = cipher.doFinal(insecure,0,insecure.length,secure);
            mac.update(secure, 0, secureCount);
            mac.doFinal(secure, secureCount);
                        
            return secure;
        }
        catch (Exception e)
        {
            throw new FacesException(e);
        }
    }

    public static final byte[] compress(byte[] bytes)
    {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try
        {
            GZIPOutputStream gzip = new GZIPOutputStream(baos);
            gzip.write(bytes, 0, bytes.length);
            gzip.finish();
            byte[] fewerBytes = baos.toByteArray();
            gzip.close();
            baos.close();
            gzip = null;
            baos = null;
            return fewerBytes;
        }
        catch (IOException e)
        {
            throw new FacesException(e);
        }
    }

    public static final byte[] encode(byte[] bytes)
    {
          return new Base64().encode(bytes);
    }

    /**
     * This fires during the Restore View phase, restoring state.
     */
    public static final Object reconstruct(String string, ExternalContext ctx)
    {
        byte[] bytes;
        try
        {
            if(log.isLoggable(Level.FINE))
                log.fine("Processing state : "+string);

            bytes = string.getBytes(ZIP_CHARSET);
            bytes = decode(bytes);
            if(isSecure(ctx))
                bytes = decrypt(bytes, ctx);
            if( enableCompression(ctx) )
                bytes = decompress(bytes);
            return getAsObject(bytes, ctx);
        }
        catch (Throwable e)
        {
            if (log.isLoggable(Level.FINE))
            {
                log.log(Level.FINE, "View State cannot be reconstructed", e);
            }
            return null;
        }
    }

    public static final byte[] decode(byte[] bytes)
    {
          return new Base64().decode(bytes);
    }

    public static final byte[] decompress(byte[] bytes)
    {
        if(bytes == null)
            throw new NullPointerException("byte[] bytes");
        
        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buffer = new byte[bytes.length];
        int length;

        try
        {
            GZIPInputStream gis = new GZIPInputStream(bais);
            while ((length = gis.read(buffer)) != -1)
            {
                baos.write(buffer, 0, length);
            }

            byte[] moreBytes = baos.toByteArray();
            baos.close();
            bais.close();
            gis.close();
            baos = null;
            bais = null;
            gis = null;
            return moreBytes;
        }
        catch (IOException e)
        {
            throw new FacesException(e);
        }
    }
    
    public static byte[] decrypt(byte[] secure, ExternalContext ctx)
    {
        if (ctx == null)
            throw new NullPointerException("ExternalContext ctx");

        testConfiguration(ctx);
                
        SecretKey secretKey = (SecretKey) getSecret(ctx);
        String algorithm = findAlgorithm(ctx);
        String algorithmParams = findAlgorithmParams(ctx);
        byte[] iv = findInitializationVector(ctx);
        
        SecretKey macSecretKey = (SecretKey) getMacSecret(ctx);
        String macAlgorithm = findMacAlgorithm(ctx);

        try
        {
            // keep local to avoid threading issue
            Mac mac = Mac.getInstance(macAlgorithm);
            mac.init(macSecretKey);
            Cipher cipher = Cipher.getInstance(algorithm + "/"
                    + algorithmParams);
            if (iv != null)
            {
                IvParameterSpec ivSpec = new IvParameterSpec(iv);
                cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);
            }
            else
            {
                cipher.init(Cipher.DECRYPT_MODE, secretKey);
            }
            if (log.isLoggable(Level.FINE))
            {
                log.fine("decrypting w/ " + algorithm + "/" + algorithmParams);
            }

            //EtM Composition Approach
            int macLenght = mac.getMacLength();
            mac.update(secure, 0, secure.length-macLenght);
            byte[] signedDigestHash = mac.doFinal();

            boolean isMacEqual = true;
            for (int i = 0; i < signedDigestHash.length; i++)
            {
                if (signedDigestHash[i] != secure[secure.length-macLenght+i])
                {
                    isMacEqual = false;
                    // MYFACES-2934 Must compare *ALL* bytes of the hash, 
                    // otherwise a side-channel timing attack is theorically possible
                    // but with a very very low probability, because the
                    // comparison time is too small to be measured compared to
                    // the overall request time and in real life applications,
                    // there are too many uncertainties involved.
                    //break;
                }
            }
            if (!isMacEqual)
            {
                throw new ViewExpiredException();
            }
            
            return cipher.doFinal(secure, 0, secure.length-macLenght);
        }
        catch (Exception e)
        {
            throw new FacesException(e);
        }
    }

    /**
     * Performs deserialization with the serialization provider created from the
     * SerialFactory.
     * 
     * @param bytes
     * @param ctx
     * @return
     */
    
    public static final Object getAsObject(byte[] bytes, ExternalContext ctx)
    {
        ByteArrayInputStream input = null;

        try
        {
            input = new ByteArrayInputStream(bytes);

            // get the Factory that was instantiated @ startup
            SerialFactory serialFactory = (SerialFactory) ctx.getApplicationMap().get(SERIAL_FACTORY);
            
            if(serialFactory == null)
                throw new NullPointerException("serialFactory");
            
            ObjectInputStream s = null;
            Exception pendingException = null;
            try
            {
                s = serialFactory.getObjectInputStream(input); 
                Object object = null;
                if (System.getSecurityManager() != null)
                {
                    final ObjectInputStream ois = s;
                    object = AccessController.doPrivileged(new PrivilegedExceptionAction<Object>()
                    {
                        //Put IOException and ClassNotFoundException as "checked" exceptions,
                        //so AccessController wrap them in a PrivilegedActionException
                        public Object run() throws  PrivilegedActionException, IOException, ClassNotFoundException
                        {
                            return ois.readObject();
                        }
                    });
                    // Since s has the same instance as ois,
                    // we don't need to close it here, rather
                    // close it on the finally block related to s
                    // and avoid duplicate close exceptions
                    // finally
                    // {
                    //    ois.close();
                    // }
                }
                else
                {
                    object = s.readObject();
                }
                return object;
            }
            catch (Exception e)
            {
                pendingException = e;
                throw new FacesException(e);
            }
            finally
            {
                if (s != null)
                {
                    try
                    {
                        s.close();
                    }
                    catch (IOException e)
                    {
                        // If a previous exception is thrown 
                        // ignore this, but if not, wrap it in a
                        // FacesException and throw it. In this way
                        // we preserve the original semantic of this
                        // method, but we handle correctly the case
                        // when we close a stream. Obviously, the 
                        // information about this exception is lost,
                        // but note that the interesting information 
                        // is always on pendingException, since we
                        // only do a readObject() on the outer try block.
                        if (pendingException == null)
                        {
                            throw new FacesException(e);
                        }                        
                    }
                    finally
                    {
                        s = null;
                    }
                }
            }
        }
        finally
        {
            if (input != null)
            {
                try
                {
                    input.close();
                }
                catch (IOException e)
                {
                    //ignore it, because ByteArrayInputStream.close has
                    //no effect, but it is better to call close and preserve
                    //semantic from previous code.
                }
                finally
                {
                    input = null;
                }
            }
        }
    }

    /**
     * Utility method for generating base 64 encoded strings.
     * 
     * @param args
     * @throws UnsupportedEncodingException
     */
    public static void main (String[] args) throws UnsupportedEncodingException
    {
        byte[] bytes = encode(args[0].getBytes(ZIP_CHARSET));
          System.out.println(new String(bytes, ZIP_CHARSET));
    }

    private static byte[] findInitializationVector(ExternalContext ctx) {
        
        byte[] iv = null;
        String _iv = ctx.getInitParameter(INIT_ALGORITHM_IV);
        
        if(_iv == null)
        {
            _iv = ctx.getInitParameter(INIT_ALGORITHM_IV.toLowerCase());
        }
        
        if (_iv != null)
            iv = new Base64().decode(_iv.getBytes());
        
        return iv;
    }

    private static String findAlgorithmParams(ExternalContext ctx) {
        
        String algorithmParams = ctx.getInitParameter(INIT_ALGORITHM_PARAM);
        
        if (algorithmParams == null)
        {
            algorithmParams = ctx.getInitParameter(INIT_ALGORITHM_PARAM.toLowerCase());
        }
        
        if (algorithmParams == null)
        {
            algorithmParams = DEFAULT_ALGORITHM_PARAMS;
        }
        
        if (log.isLoggable(Level.FINE))
        {
            log.fine("Using algorithm paramaters " + algorithmParams);
        }
        
        return algorithmParams;
    }

    private static String findAlgorithm(ExternalContext ctx) {
        
        String algorithm = ctx.getInitParameter(INIT_ALGORITHM);
        
        if (algorithm == null)
        {
            algorithm = ctx.getInitParameter(INIT_ALGORITHM.toLowerCase());
        }

        return findAlgorithm( algorithm );
    }
    
    private static String findAlgorithm(ServletContext ctx) {

        String algorithm = ctx.getInitParameter(INIT_ALGORITHM);
        
        if (algorithm == null)
        {
            algorithm = ctx.getInitParameter(INIT_ALGORITHM.toLowerCase());
        }

        return findAlgorithm( algorithm );
    }
    
    private static String findAlgorithm(String initParam) {
        
        if (initParam == null)
        {
            initParam = DEFAULT_ALGORITHM;
        }
        
        if (log.isLoggable(Level.FINE))
        {
            log.fine("Using algorithm " + initParam);
        }
        
        return initParam;
        
    }

    /**
     * Does nothing if the user has disabled the SecretKey cache. This is
     * useful when dealing with a JCA provider whose SecretKey 
     * implementation is not thread safe.
     * 
     * Instantiates a SecretKey instance based upon what the user has 
     * specified in the deployment descriptor.  The SecretKey is then 
     * stored in application scope where it can be used for all requests.
     */
    
    public static void initSecret(ServletContext ctx){
        
        if(ctx == null)
            throw new NullPointerException("ServletContext ctx");
        
        if (log.isLoggable(Level.FINE))
            log.fine("Storing SecretKey @ " + INIT_SECRET_KEY_CACHE);

        // Create and store SecretKey on application scope
        String cache = ctx.getInitParameter(INIT_SECRET_KEY_CACHE);
        
        if(cache == null)
        {
            cache = ctx.getInitParameter(INIT_SECRET_KEY_CACHE.toLowerCase());
        }
        
        if (!"false".equals(cache))
        {
            String algorithm = findAlgorithm(ctx);
            // you want to create this as few times as possible
            ctx.setAttribute(INIT_SECRET_KEY_CACHE, new SecretKeySpec(findSecret(ctx, algorithm), algorithm));
        }

        if (log.isLoggable(Level.FINE))
            log.fine("Storing SecretKey @ " + INIT_MAC_SECRET_KEY_CACHE);
        
        String macCache = ctx.getInitParameter(INIT_MAC_SECRET_KEY_CACHE);
        
        if(macCache == null)
        {
            macCache = ctx.getInitParameter(INIT_MAC_SECRET_KEY_CACHE.toLowerCase());
        }
        
        if (!"false".equals(macCache))
        {
            String macAlgorithm = findMacAlgorithm(ctx);
            // init mac secret and algorithm 
            ctx.setAttribute(INIT_MAC_SECRET_KEY_CACHE, new SecretKeySpec(findMacSecret(ctx, macAlgorithm), macAlgorithm));
        }
    }
    
    private static SecretKey getSecret(ExternalContext ctx)
    {
        Object secretKey = (SecretKey) ctx.getApplicationMap().get(INIT_SECRET_KEY_CACHE);
        
        if (secretKey == null)
        {
            String cache = ctx.getInitParameter(INIT_SECRET_KEY_CACHE);
            
            if(cache == null)
            {
                cache = ctx.getInitParameter(INIT_SECRET_KEY_CACHE.toLowerCase());
            }
            
            if ("false".equals(cache))
            {
                // No cache is used. This option is activated
                String secret = ctx.getInitParameter(INIT_SECRET);
                
                if (secret == null)
                {
                    secret = ctx.getInitParameter(INIT_SECRET.toLowerCase());
                }

                if (secret == null)
                {
                    throw new NullPointerException("Could not find secret using key '" + INIT_SECRET + "'");
                }
                
                String algorithm = findAlgorithm(ctx);
                
                secretKey = new SecretKeySpec(findSecret(ctx, algorithm), algorithm);
            }
            else
            {
                throw new NullPointerException("Could not find SecretKey in application scope using key '" 
                        + INIT_SECRET_KEY_CACHE + "'");
            }
        }
        
        if( ! ( secretKey instanceof SecretKey ) )
            throw new ClassCastException("Did not find an instance of SecretKey "
                    + "in application scope using the key '" + INIT_SECRET_KEY_CACHE + "'");

        
        return (SecretKey) secretKey;
    }

    private static byte[] findSecret(ExternalContext ctx, String algorithm)
    {
        String secret = ctx.getInitParameter(INIT_SECRET);
        
        if (secret == null)
        {
            secret = ctx.getInitParameter(INIT_SECRET.toLowerCase());
        }
        
        return findSecret(secret, algorithm);
    }    
    
    private static byte[] findSecret(ServletContext ctx, String algorithm)
    {
        String secret = ctx.getInitParameter(INIT_SECRET);
        
        if (secret == null)
        {
            secret = ctx.getInitParameter(INIT_SECRET.toLowerCase());
        }
        
        return findSecret(secret, algorithm);
    }
    
    private static byte[] findSecret(String secret, String algorithm) {
        byte[] bytes = null;
        
        if(secret == null)
        {
            try
            {
                KeyGenerator kg = KeyGenerator.getInstance(algorithm);
                bytes = kg.generateKey().getEncoded();
                
                if(log.isLoggable(Level.FINE))
                    log.fine("generated random password of length " + bytes.length);
            }
            catch (NoSuchAlgorithmException e)
            {
                // Generate random password length 8, 
                int length = 8;
                bytes = new byte[length];
                new Random().nextBytes(bytes);
                
                if(log.isLoggable(Level.FINE))
                    log.fine("generated random password of length " + length);
            }
        }
        else 
        {
            bytes = new Base64().decode(secret.getBytes());
        }
        
        return bytes;
    }

    private static String findMacAlgorithm(ExternalContext ctx) {
        
        String algorithm = ctx.getInitParameter(INIT_MAC_ALGORITHM);
        
        if (algorithm == null)
        {
            algorithm = ctx.getInitParameter(INIT_MAC_ALGORITHM.toLowerCase());
        }

        return findMacAlgorithm( algorithm );

    }
    
    private static String findMacAlgorithm(ServletContext ctx) {

        String algorithm = ctx.getInitParameter(INIT_MAC_ALGORITHM);
        
        if (algorithm == null)
        {
            algorithm = ctx.getInitParameter(INIT_MAC_ALGORITHM.toLowerCase());
        }

        return findMacAlgorithm( algorithm );
        
    }
    
    private static String findMacAlgorithm(String initParam) {
        
        if (initParam == null)
        {
            initParam = DEFAULT_MAC_ALGORITHM;
        }
        
        if (log.isLoggable(Level.FINE))
        {
            log.fine("Using algorithm " + initParam);
        }
        
        return initParam;
        
    }
    
    private static SecretKey getMacSecret(ExternalContext ctx)
    {
        Object secretKey = (SecretKey) ctx.getApplicationMap().get(INIT_MAC_SECRET_KEY_CACHE);
        
        if (secretKey == null)
        {
            String cache = ctx.getInitParameter(INIT_MAC_SECRET_KEY_CACHE);
            
            if(cache == null)
            {
                cache = ctx.getInitParameter(INIT_MAC_SECRET_KEY_CACHE.toLowerCase());
            }
            
            if ("false".equals(cache))
            {
                // No cache is used. This option is activated
                String secret = ctx.getInitParameter(INIT_MAC_SECRET);
                
                if (secret == null)
                {
                    secret = ctx.getInitParameter(INIT_MAC_SECRET.toLowerCase());
                }
                
                if (secret == null)
                {
                    throw new NullPointerException("Could not find secret using key '" + INIT_MAC_SECRET + "'");
                }
                
                String macAlgorithm = findMacAlgorithm(ctx);

                secretKey = new SecretKeySpec(findMacSecret(ctx, macAlgorithm), macAlgorithm);
            }
            else
            {
                throw new NullPointerException("Could not find SecretKey in application scope using key '" 
                        + INIT_MAC_SECRET_KEY_CACHE + "'");
            }
        }
        
        if( ! ( secretKey instanceof SecretKey ) )
            throw new ClassCastException("Did not find an instance of SecretKey "
                    + "in application scope using the key '" + INIT_MAC_SECRET_KEY_CACHE + "'");

        
        return (SecretKey) secretKey;
    }

    private static byte[] findMacSecret(ExternalContext ctx, String algorithm)
    {
        String secret = ctx.getInitParameter(INIT_MAC_SECRET);
        
        if (secret == null)
        {
            secret = ctx.getInitParameter(INIT_MAC_SECRET.toLowerCase());
        }
 
        return findMacSecret(secret, algorithm);
    }    
    
    private static byte[] findMacSecret(ServletContext ctx, String algorithm)
    {
        String secret = ctx.getInitParameter(INIT_MAC_SECRET);
        
        if (secret == null)
        {
            secret = ctx.getInitParameter(INIT_MAC_SECRET.toLowerCase());
        }
        
        return findMacSecret(secret, algorithm);
    }

    private static byte[] findMacSecret(String secret, String algorithm) {
        byte[] bytes = null;
        
        if(secret == null)
        {
            try
            {
                KeyGenerator kg = KeyGenerator.getInstance(algorithm);
                bytes = kg.generateKey().getEncoded();
                
                if(log.isLoggable(Level.FINE))
                    log.fine("generated random mac password of length " + bytes.length);
            }
            catch (NoSuchAlgorithmException e)
            {
                // Generate random password length 8, 
                int length = 8;
                bytes = new byte[length];
                new Random().nextBytes(bytes);
                
                if(log.isLoggable(Level.FINE))
                    log.fine("generated random mac password of length " + length);
            }
        }
        else 
        {
            bytes = new Base64().decode(secret.getBytes());
        }
        
        return bytes;
    }
}
