/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 1997-2010 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://glassfish.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package renderkits.util;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.faces.context.FacesContext;
import java.security.Key;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* This utility class provides services to encrypt or decrypt a byte array.
* The algorithm used to encrypt byte array is 3DES with CBC
* The algorithm used to create the message authentication code (MAC) is SHA1
* <p/>
* Original author Inderjeet Singh, J2EE Blue Prints Team. Modified to suit JSF
* needs.
*/
public class ByteArrayGuard {
public static final int DEFAULT_KEY_LENGTH = 24;
public static final int DEFAULT_MAC_LENGTH = 20;
public static final int DEFAULT_IV_LENGTH = 8;
public static final String SESSION_KEY_FOR_PASSWORD =
"com.sun.faces.clientside-state.password-key";
public static final int DEFAULT_PASSWORD_LENGTH = 24;
// Log instance for this class
private static Logger logger;
static {
logger = Util.getLogger(Util.FACES_LOGGER);
}
/**
* @param ps the password strategy to create password for encryption and decryption
* uses default values for the length of the encryption key, MAC key, and
* the initialization vector
*
* @see DEFAULT_KEY_LENGTH
* @see DEFAULT_MAC_LENGTH
* @see DEFAULT_IV_LENGTH
*/
public ByteArrayGuard() {
this(DEFAULT_KEY_LENGTH, DEFAULT_MAC_LENGTH, DEFAULT_IV_LENGTH);
}
/**
* @param keyLength the length of the key used for encryption
* @param macLength the length of the message authentication used
* @param ivLength length of the initialization vector used by the block cipher
* @param
*/
public ByteArrayGuard(int keyLength, int macLength, int ivLength) {
this.keyLength = keyLength;
this.macLength = macLength;
this.ivLength = ivLength;
}
/**
* Encrypts the specified plaindata using the specified password. It also
* stores the MAC and the IV in the output. The 20-byte MAC is stored
* first, followed by the 8-byte IV, followed by the encrypted
* contents of the file.
*
* @param context FacesContext for this request
* @param plaindata The plain text that needs to be encrypted
*
* @return The encrypted contents
*/
public byte[] encrypt(FacesContext context, byte[] plaindata) {
try {
// generate a key that can be used for encryption from the
// supplied password
byte[] rawKey =
convertPasswordToKey(getPasswordToSecureState(context));
// choose block encryption algorithm
Cipher cipher = getBlockCipherForEncryption(rawKey);
// encrypt the plaintext
byte[] encdata = cipher.doFinal(plaindata);
// choose mac algorithm
Mac mac = getMac(rawKey);
// generate MAC for the initialization vector of the cipher
byte[] iv = cipher.getIV();
mac.update(iv);
// generate MAC for the encrypted data
mac.update(encdata);
// generate MAC
byte[] macBytes = mac.doFinal();
// concat byte arrays for MAC, IV, and encrypted data
// Note that the order is important here. MAC and IV are
// of fixed length and need to appear before the encrypted data
// for easy extraction while decrypting.
byte[] tmp = concatBytes(macBytes, iv);
byte[] securedata = concatBytes(tmp, encdata);
return securedata;
} catch (Exception e) {
if (logger.isLoggable(Level.SEVERE)) {
logger.log(Level.SEVERE, e.getMessage(), e.getCause());
}
throw new RuntimeException(e);
}
}
/**
* Decrypts the specified byte array using the specified password, and
* generates an inputstream from it. The file must be encrypted by the
* above method for encryption. The method also verifies the MAC. It
* uses the IV present in the file for decryption.
*
* @param context Faces Context for this request
* @param securedata The encrypted data (including mac and initialization
* vector) that needs to be decrypted
*
* @return A byte array containing the decrypted contents
*/
public byte[] decrypt(FacesContext context, byte[] securedata) {
try {
// Extract MAC
byte[] macBytes = new byte[macLength];
System.arraycopy(securedata, 0, macBytes, 0, macBytes.length);
// Extract initialization vector used for encryption
byte[] iv = new byte[ivLength];
System.arraycopy(securedata, macBytes.length, iv, 0, iv.length);
// Extract encrypted data
byte[] encdata =
new byte[securedata.length - macBytes.length - iv.length];
System.arraycopy(securedata,
macBytes.length + iv.length,
encdata,
0,
encdata.length);
// verify MAC by regenerating it and comparing it with the received value
byte[] rawKey =
convertPasswordToKey(getPasswordToSecureState(context));
Mac mac = getMac(rawKey);
mac.update(iv);
mac.update(encdata);
byte[] macBytesCalculated = mac.doFinal();
if (Arrays.equals(macBytes, macBytesCalculated)) {
// decrypt data only if the MAC was valid
Cipher cipher = getBlockCipherForDecryption(rawKey, iv);
byte[] plaindata = cipher.doFinal(encdata);
return plaindata;
} else {
if (logger.isLoggable(Level.WARNING)) {
// PENDING (visvan) localize
logger.warning("ERROR: MAC did not verify!");
}
return null;
}
} catch (Exception e) {
if (logger.isLoggable(Level.SEVERE)) {
logger.log(Level.SEVERE, e.getMessage(), e.getCause());
}
throw new RuntimeException(e);
}
}
/**
* This method provides a password to be used for encryption/decryption of
* client-side state.
*/
private String getPasswordToSecureState(FacesContext context) {
Map sessionMap = context.getExternalContext().getSessionMap();
if (sessionMap == null) {
// Setting it to an arbitrary value. As long as the same value is used
// by both serializer and deserializer, the encryption will work.
// However, the encryption will be useless since this arbitrary
// value can be guessed by an attacker.
// PENDING (visvan) localize
if (logger.isLoggable(Level.WARNING)) {
logger.warning(
"Key to retrieve password from session could not " +
"be found. Using default value. This will enable " +
"the encryption and decryption to work, but the " +
"client-side state saving method is NO longer secure.");
}
password = "easy-to-guess-password";
} else {
password = (String) sessionMap.get(SESSION_KEY_FOR_PASSWORD);
if (password == null) {
password = (String)
ByteArrayGuard.getRandomString(DEFAULT_PASSWORD_LENGTH);
sessionMap.put(SESSION_KEY_FOR_PASSWORD, password);
}
}
return password;
}
/**
* This method converts the specified password into a key in a
* deterministic manner. The key is then usable for creating ciphers
* and MACs.
*
* @return a byte array containing a key based on the specified
* password. The length of the returned byte array is KEY_LENGTH.
*/
private byte[] convertPasswordToKey(byte[] password) {
try {
MessageDigest md = MessageDigest.getInstance("SHA");
byte[] seed = md.digest(password);
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
random.setSeed(seed);
byte[] rawkey = new byte[keyLength];
random.nextBytes(rawkey);
return rawkey;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* A convenience alias to the above method which takes a string as
* the password.
*/
private byte[] convertPasswordToKey(String password) {
return convertPasswordToKey(password.getBytes());
}
/**
* @param rawKey must be 24 bytes in length.
*
* @return a 3DES block cipher to be used for encryption based on the
* specified key
*/
private Cipher getBlockCipherForEncryption(byte[] rawKey) {
try {
SecretKeyFactory keygen = SecretKeyFactory.getInstance("DESede");
DESedeKeySpec keyspec = new DESedeKeySpec(rawKey);
Key key = keygen.generateSecret(keyspec);
Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
byte[] iv = new byte[ivLength];
getPRNG().nextBytes(iv);
IvParameterSpec ivspec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, key, ivspec, getPRNG());
return cipher;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static Cipher getBlockCipherForDecryption(byte[] rawKey, byte[]
iv) {
try {
SecretKeyFactory keygen = SecretKeyFactory.getInstance("DESede");
DESedeKeySpec keyspec = new DESedeKeySpec(rawKey);
Key key = keygen.generateSecret(keyspec);
Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
IvParameterSpec ivspec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, key, ivspec, getPRNG());
return cipher;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private Mac getMac(byte[] rawKey) {
try {
Mac mac = Mac.getInstance("HmacSHA1");
SecretKeySpec key =
new SecretKeySpec(rawKey, 0, macLength, "HmacSHA1");
mac.init(key);
return mac;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Generates a cryptographically random string
*
* @param size the desired length of the string
*/
static String getRandomString(int size) {
byte[] data = new byte[size];
getPRNG().nextBytes(data);
return new String(data);
}
private static int getRandomInt() {
byte[] data = new byte[4];
getPRNG().nextBytes(data);
return data[0] + data[1] * 256 + data[2] * 65536 + data[3] * 16777216;
}
private static SecureRandom getPRNG() {
try {
if (prng == null) {
prng = SecureRandom.getInstance("SHA1PRNG");
}
return prng;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static String getHexString(byte[] b) {
StringBuffer buf = new StringBuffer(b.length);
for (int i = 0; i < b.length; ++i) {
byte2hex(b[i], buf);
}
return buf.toString();
}
/**
* This method concatenates two byte arrays
*
* @param array1 first byte array to be concatenated
* @param array2 second byte array to be concatenated
*
* @return a byte array of array1||array2
*/
private static byte[] concatBytes(byte[] array1, byte[] array2) {
byte[] cBytes = new byte[array1.length + array2.length];
try {
System.arraycopy(array1, 0, cBytes, 0, array1.length);
System.arraycopy(array2, 0, cBytes, array1.length, array2.length);
} catch (Exception e) {
throw new RuntimeException(e);
}
return cBytes;
}
/** Converts a byte to hex digit and writes to the supplied buffer */
private static void byte2hex(byte b, StringBuffer buf) {
char[] hexChars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F'};
int high = ((b & 0xf0) >> 4);
int low = (b & 0x0f);
buf.append(hexChars[high]);
buf.append(hexChars[low]);
}
private int keyLength;
private int macLength;
private int ivLength;
private String password = null;
private static SecureRandom prng = null;
}