blob: 3077a1ee29e3884b12f95c3c6da1164d977b3f2b [file] [log] [blame]
/*
* 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.application.viewstate;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
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.myfaces.buildtools.maven2.plugin.builder.annotation.JSFWebConfigParam;
import org.apache.myfaces.util.lang.Assert;
import org.apache.myfaces.spi.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>AES is the default encryption algorithm</li>
* <li>ECB is the default mode</li>
* <li>PKCS5Padding is the default padding</li>
* <li>HmacSHA256 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 interpreted 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 .</br>
* Since https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8170157
* unlimited cryptographic policy is enabled by default.</p>
*
*
* See org.apache.myfaces.webapp.StartupServletContextListener
*/
public final class StateUtils
{
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 = "AES";
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="AES",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
* {@link org.apache.myfaces.spi.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 = "HmacSHA256";
/**
* 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="HmacSHA256",
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.startsWith("CBC"))
{
String iv = ctx.getInitParameter(INIT_ALGORITHM_IV);
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 Cipher createCipher(ExternalContext externalContext, int mode) throws Exception
{
SecretKey secretKey = (SecretKey) getSecret(externalContext);
String algorithm = findAlgorithm(externalContext);
String algorithmParams = findAlgorithmParams(externalContext);
byte[] iv = findInitializationVector(externalContext);
Cipher cipher = Cipher.getInstance(algorithm + '/' + algorithmParams);
if (iv != null)
{
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(mode, secretKey, ivSpec);
}
else
{
cipher.init(mode, secretKey);
}
if (log.isLoggable(Level.FINE))
{
log.fine("de/encrypting with " + algorithm + '/' + algorithmParams);
}
return cipher;
}
public static Mac createMac(ExternalContext externalContext) throws Exception
{
SecretKey macSecretKey = (SecretKey) getMacSecret(externalContext);
String macAlgorithm = findMacAlgorithm(externalContext);
Mac mac = Mac.getInstance(macAlgorithm);
mac.init(macSecretKey);
return mac;
}
public static boolean enableCompression(ExternalContext externalContext)
{
Assert.notNull(externalContext, "externalContext");
return "true".equals(externalContext.getInitParameter(COMPRESS_STATE_IN_CLIENT));
}
public static boolean isSecure(ExternalContext externalContext)
{
Assert.notNull(externalContext, "externalContext");
return !"false".equals(externalContext.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)
{
// get the Factory that was instantiated @ startup
SerialFactory serialFactory = (SerialFactory) ctx.getApplicationMap().get(SERIAL_FACTORY);
Assert.notNull(serialFactory, "serialFactory");
try
{
return serialFactory.toByteArray(object);
}
catch (IOException e)
{
throw new FacesException(e);
}
}
public static byte[] encrypt(byte[] insecure, ExternalContext externalContext)
{
Assert.notNull(externalContext, "externalContext");
testConfiguration(externalContext);
try
{
Mac mac = createMac(externalContext);
Cipher cipher = createCipher(externalContext, Cipher.ENCRYPT_MODE);
//EtM (Encrypt-then-MAC) 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)
{
try
{
try (ByteArrayOutputStream baos = new ByteArrayOutputStream())
{
try (GZIPOutputStream gzip = new GZIPOutputStream(baos))
{
gzip.write(bytes, 0, bytes.length);
gzip.finish();
gzip.flush();
return baos.toByteArray();
}
}
}
catch (IOException e)
{
throw new FacesException(e);
}
}
public static final byte[] encode(byte[] bytes)
{
return Base64.getEncoder().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);
log.fine("Processing serialized viewstate string with hashCode : " + string.hashCode());
}
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 Base64.getDecoder().decode(bytes);
}
public static final byte[] decompress(byte[] bytes)
{
Assert.notNull(bytes, "bytes");
try
{
try (ByteArrayOutputStream baos = new ByteArrayOutputStream())
{
try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes))
{
try (GZIPInputStream gis = new GZIPInputStream(bais))
{
byte[] buffer = new byte[bytes.length];
int length;
while ((length = gis.read(buffer)) != -1)
{
baos.write(buffer, 0, length);
}
}
}
return baos.toByteArray();
}
}
catch (IOException e)
{
throw new FacesException(e);
}
}
public static byte[] decrypt(byte[] secure, ExternalContext externalContext)
{
Assert.notNull(externalContext, "externalContext");
testConfiguration(externalContext);
try
{
Mac mac = createMac(externalContext);
Cipher cipher = createCipher(externalContext, Cipher.DECRYPT_MODE);
//EtM (Encrypt-then-MAC) 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)
{
// get the Factory that was instantiated @ startup
SerialFactory serialFactory = (SerialFactory) ctx.getApplicationMap().get(SERIAL_FACTORY);
Assert.notNull(serialFactory, "serialFactory");
try
{
return serialFactory.toObject(bytes);
}
catch (Exception e)
{
throw new FacesException(e);
}
}
/**
* 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 ivString = ctx.getInitParameter(INIT_ALGORITHM_IV);
if (ivString != null)
{
iv = decode(ivString.getBytes());
}
return iv;
}
private static String findAlgorithmParams(ExternalContext ctx)
{
String algorithmParams = ctx.getInitParameter(INIT_ALGORITHM_PARAM);
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);
return findAlgorithm( algorithm );
}
private static String findAlgorithm(ServletContext ctx)
{
String algorithm = ctx.getInitParameter(INIT_ALGORITHM);
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 servletContext)
{
Assert.notNull(servletContext, "servletContext");
if (log.isLoggable(Level.FINE))
{
log.fine("Storing SecretKey @ " + INIT_SECRET_KEY_CACHE);
}
// Create and store SecretKey on application scope
String cache = servletContext.getInitParameter(INIT_SECRET_KEY_CACHE);
if (!"false".equals(cache))
{
String algorithm = findAlgorithm(servletContext);
// you want to create this as few times as possible
servletContext.setAttribute(INIT_SECRET_KEY_CACHE, new SecretKeySpec(
findSecret(servletContext, algorithm), algorithm));
}
if (log.isLoggable(Level.FINE))
{
log.fine("Storing SecretKey @ " + INIT_MAC_SECRET_KEY_CACHE);
}
String macCache = servletContext.getInitParameter(INIT_MAC_SECRET_KEY_CACHE);
if (!"false".equals(macCache))
{
String macAlgorithm = findMacAlgorithm(servletContext);
// init mac secret and algorithm
servletContext.setAttribute(INIT_MAC_SECRET_KEY_CACHE, new SecretKeySpec(
findMacSecret(servletContext, 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 ("false".equals(cache))
{
// No cache is used. This option is activated
String secret = ctx.getInitParameter(INIT_SECRET);
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);
return findSecret(secret, algorithm);
}
private static byte[] findSecret(ServletContext ctx, String algorithm)
{
String secret = ctx.getInitParameter(INIT_SECRET);
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 16,
int length = 16;
bytes = new byte[length];
new Random().nextBytes(bytes);
if (log.isLoggable(Level.FINE))
{
log.fine("generated random password of length " + length);
}
}
}
else
{
bytes = decode(secret.getBytes());
}
return bytes;
}
private static String findMacAlgorithm(ExternalContext ctx)
{
String algorithm = ctx.getInitParameter(INIT_MAC_ALGORITHM);
return findMacAlgorithm(algorithm);
}
private static String findMacAlgorithm(ServletContext ctx)
{
String algorithm = ctx.getInitParameter(INIT_MAC_ALGORITHM);
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 ("false".equals(cache))
{
// No cache is used. This option is activated
String secret = ctx.getInitParameter(INIT_MAC_SECRET);
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);
return findMacSecret(secret, algorithm);
}
private static byte[] findMacSecret(ServletContext ctx, String algorithm)
{
String secret = ctx.getInitParameter(INIT_MAC_SECRET);
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 = decode(secret.getBytes());
}
return bytes;
}
}