Skip to content

Instantly share code, notes, and snippets.

@rdev5
Last active December 28, 2015 22:19
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rdev5/7570750 to your computer and use it in GitHub Desktop.
Save rdev5/7570750 to your computer and use it in GitHub Desktop.
Patch for EncryptedMapDecorator.java (cas-server-extension-clearpass). Returns IV (16 bytes) prepended to ciphertext on encrypt(); mandatory for proper decryption. Useful in clustered environments where IVs saved to thread-safe ConcurrentHashMaps are being stored locally in memory, thus rendering other systems in the cluster unable to decrypt a …
/*
* Licensed to Yavapai College under one or more contributor license
* agreements. See the NOTICE file distributed with this work
* for additional information regarding copyright ownership.
* Jasig 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 the following location:
*
* 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.
*/
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Random;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Key;
import java.security.spec.KeySpec;
import javax.validation.constraints.NotNull;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.spec.PBEKeySpec;
import org.apache.commons.codec.binary.Base64;
/**
* Decorator for a map that will hash the key and encrypt the value.
*
* @author Matt Borja
* @version $Revision$ $Date$
* @since 1.0.0
*/
public class CryptDemo {
private static final String ENCRYPTION_ALGORITHM = "AES";
private static final String HASH_ALGORITHM = "SHA-512";
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
private static final String SECRET_KEY_FACTORY_ALGORITHM = "PBKDF2WithHmacSHA1";
private static final int RAND_MIN = 8;
private static final int RAND_MAX = 16;
private static final int INTEGER_LEN = 4;
private Cipher cipher;
@NotNull
private Key key;
@NotNull
private int ivSize;
public static void main(String[] args) {
try {
new CryptDemo().Start(args);
} catch(final Exception e) {
throw new RuntimeException(e);
}
}
private Cipher getCipherObject() throws NoSuchAlgorithmException, NoSuchPaddingException {
return Cipher.getInstance(CIPHER_ALGORITHM);
}
private static int getIvSize() throws NoSuchAlgorithmException, NoSuchPaddingException {
return Cipher.getInstance(CIPHER_ALGORITHM).getBlockSize();
}
private static byte[] generateIV(final int size) {
SecureRandom srand = new SecureRandom();
byte[] iv_value = new byte[size];
srand.nextBytes(iv_value);
return iv_value;
}
private static byte[] generateKey() {
Random rand = new Random();
return generateKey(rand.nextInt(RAND_MAX) + RAND_MIN);
}
private static byte[] generateKey(final int size) {
SecureRandom srand = new SecureRandom();
byte[] salt = new byte[size];
srand.nextBytes(salt);
return salt;
}
private static byte[] generateSalt() {
Random rand = new Random();
return generateSalt(rand.nextInt(RAND_MAX) + RAND_MIN);
}
private static byte[] generateSalt(final int size) {
SecureRandom srand = new SecureRandom();
byte[] salt = new byte[size];
srand.nextBytes(salt);
return salt;
}
private static byte[] encode(final byte[] bytes) {
return new Base64().encode(bytes);
}
private static byte[] decode(final byte[] bytes) {
return new Base64().decode(bytes);
}
// BIG_ENDIAN
private static int getInt(final byte[] bytes) {
return ByteBuffer.wrap(bytes).getInt();
}
public void Start(String[] args) throws Exception {
if(args.length < 2) {
StackTraceElement[] stack = Thread.currentThread ().getStackTrace ();
StackTraceElement main = stack[stack.length - 1];
String mainClass = main.getClassName ();
System.out.println("Usage: " + mainClass + " <mode> <plaintext> [key] [salt]");
System.exit(0);
}
int mode = new String(args[0]).equals("encrypt") ? 1 : (new String(args[0]).equals("decrypt") ? 0 : -1);
String input = args[1];
Boolean keySpecified = args.length >= 3;
Boolean saltSpecified = args.length >= 4;
byte[] key, salt;
try {
this.ivSize = getIvSize();
} catch (final Exception e) {
throw new RuntimeException(e);
}
switch(mode) {
// ENCRYPT_MODE
case 1:
key = keySpecified ? args[2].getBytes() : generateKey();
if(!keySpecified) System.out.println("[WARNING] New key required for decryption: " + new String(encode(key)));
salt = saltSpecified ? args[3].getBytes() : generateSalt();
if(!saltSpecified) System.out.println("[WARNING] New salt required for decryption: " + new String(encode(salt)));
cipher = getCipherObject();
this.key = deriveSecretKey(ENCRYPTION_ALGORITHM, new String(key), new String(salt));
System.out.println(encrypt(input));
break;
// DECRYPT_MODE
case 0:
if(!keySpecified || !saltSpecified) {
System.out.println("Key and salt required");
System.exit(0);
}
key = args[2].getBytes();
salt = args[3].getBytes();
cipher = getCipherObject();
this.key = deriveSecretKey(ENCRYPTION_ALGORITHM, new String(key), new String(salt));
System.out.println(decrypt(input));
break;
default:
System.out.println("Invalid mode specified (" + args[0] + " - " + mode + "). Valid options are: encrypt, decrypt");
System.exit(1);
break;
}
}
protected String encrypt(String plaintext) {
if (plaintext == null) {
return null;
}
try {
byte[] iv_value = generateIV(this.ivSize);
IvParameterSpec iv_spec = new IvParameterSpec(iv_value);
cipher.init(Cipher.ENCRYPT_MODE, this.key, iv_spec);
byte[] ciphertext = cipher.doFinal(plaintext.getBytes());
byte[] iv_ciphertext = new byte[INTEGER_LEN + this.ivSize + ciphertext.length];
System.arraycopy(int2byte(this.ivSize), 0, iv_ciphertext, 0, INTEGER_LEN);
System.arraycopy(iv_value, 0, iv_ciphertext, INTEGER_LEN, this.ivSize);
System.arraycopy(ciphertext, 0, iv_ciphertext, INTEGER_LEN + this.ivSize, ciphertext.length);
return new String(encode(iv_ciphertext));
} catch(final Exception e) {
throw new RuntimeException(e);
}
}
protected String decrypt(String value) {
if (value == null) {
return null;
}
try {
byte[] iv_ciphertext = decode(value.getBytes());
int ivSize = byte2int(Arrays.copyOfRange(iv_ciphertext, 0, INTEGER_LEN));
byte[] iv_value = Arrays.copyOfRange(iv_ciphertext, INTEGER_LEN, (INTEGER_LEN + ivSize));
byte[] ciphertext = Arrays.copyOfRange(iv_ciphertext, INTEGER_LEN + ivSize, iv_ciphertext.length);
IvParameterSpec iv_spec = new IvParameterSpec(iv_value);
cipher.init(Cipher.DECRYPT_MODE, this.key, iv_spec);
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext);
} catch(final Exception e) {
System.exit(1); // Invalid key, salt, or ciphertext
throw new RuntimeException(e);
}
}
/*
* Utility functions
*/
protected static byte[] int2byte(final int i) throws UnsupportedEncodingException {
return ByteBuffer.allocate(4).putInt(i).array();
}
protected static int byte2int(final byte[] bytes) throws UnsupportedEncodingException {
return ByteBuffer.wrap(bytes).getInt();
}
protected static String byte2char(final byte[] bytes) throws UnsupportedEncodingException {
return new String(bytes, "UTF-8");
}
protected static byte[] char2byte(final String chars) throws UnsupportedEncodingException {
return chars.getBytes("UTF-8");
}
private static Key deriveSecretKey(String secretKeyAlgorithm, String secretKey, String salt) throws Exception {
SecretKeyFactory factory = SecretKeyFactory.getInstance(SECRET_KEY_FACTORY_ALGORITHM);
KeySpec spec = new PBEKeySpec(secretKey.toCharArray(), char2byte(salt), 65536, 128);
SecretKey tmp = factory.generateSecret(spec);
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), secretKeyAlgorithm);
return secret;
}
}
/*
* Licensed to Jasig under one or more contributor license
* agreements. See the NOTICE file distributed with this work
* for additional information regarding copyright ownership.
* Jasig 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 the following location:
*
* 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.jasig.cas.extension.clearpass;
import java.nio.ByteBuffer;
import java.io.UnsupportedEncodingException;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import javax.validation.constraints.NotNull;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Decorator for a map that will hash the key and encrypt the value.
*
* @author Scott Battaglia
* @since 1.0.6
*/
public final class EncryptedMapDecorator implements Map<String, String> {
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
private static final String SECRET_KEY_FACTORY_ALGORITHM = "PBKDF2WithHmacSHA1";
private static final String DEFAULT_HASH_ALGORITHM = "SHA-512";
private static final String DEFAULT_ENCRYPTION_ALGORITHM = "AES";
private static final int INTEGER_LEN = 4;
private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
'e', 'f'};
private final Logger logger = LoggerFactory.getLogger(getClass());
@NotNull
private final Map<String, String> decoratedMap;
@NotNull
private final MessageDigest messageDigest;
@NotNull
private final byte[] salt;
@NotNull
private final Key key;
@NotNull
private int ivSize;
@NotNull
private final String secretKeyAlgorithm;
private boolean cloneNotSupported;
private ConcurrentHashMap<Object, IvParameterSpec> algorithmParametersHashMap =
new ConcurrentHashMap<Object, IvParameterSpec>();
/**
* Decorates a map using the default algorithm {@link #DEFAULT_HASH_ALGORITHM} and a
* {@link #DEFAULT_ENCRYPTION_ALGORITHM}.
* <p>The salt is randomly constructed when the object is created in memory.
* This constructor is sufficient to decorate
* a cache that only lives in-memory.
*
* @param decoratedMap the map to decorate. CANNOT be NULL.
* @throws Exception if the algorithm cannot be found. Should not happen in this case, or if the key spec is not found
* or if the key is invalid. Check the exception type for more details on the nature of the error.
*/
public EncryptedMapDecorator(final Map<String, String> decoratedMap) throws Exception {
this(decoratedMap, getRandomSalt(8), getRandomSalt(32));
}
/**
* Decorates a map using the default algorithm {@link #DEFAULT_HASH_ALGORITHM}
* and a {@link #DEFAULT_ENCRYPTION_ALGORITHM}.
* <p>Takes a salt and secretKey so that it can work with a distributed cache.
*
* @param decoratedMap the map to decorate. CANNOT be NULL.
* @param salt the salt, as a String. Gets converted to bytes. CANNOT be NULL.
* @param secretKey the secret to use for the key. Gets converted to bytes. CANNOT be NULL.
* @throws Exception if the algorithm cannot be found. Should not happen in this case, or if the key spec is not found
* or if the key is invalid. Check the exception type for more details on the nature of the error.
*/
public EncryptedMapDecorator(final Map<String, String> decoratedMap, final String salt,
final String secretKey) throws Exception {
this(decoratedMap, DEFAULT_HASH_ALGORITHM, salt, DEFAULT_ENCRYPTION_ALGORITHM, secretKey);
}
/**
* Decorates a map using the provided algorithms.
* <p>Takes a salt and secretKey so that it can work with a distributed cache.
*
* @param decoratedMap the map to decorate. CANNOT be NULL.
* @param hashAlgorithm the algorithm to use for hashing. CANNOT BE NULL.
* @param salt the salt, as a String. Gets converted to bytes. CANNOT be NULL.
* @param secretKeyAlgorithm the encryption algorithm. CANNOT BE NULL.
* @param secretKey the secret to use for the key. Gets converted to bytes. CANNOT be NULL.
* @throws Exception if the algorithm cannot be found. Should not happen in this case, or if the key spec is not found
* or if the key is invalid. Check the exception type for more details on the nature of the error.
*/
public EncryptedMapDecorator(final Map<String, String> decoratedMap, final String hashAlgorithm, final String salt,
final String secretKeyAlgorithm, final String secretKey) throws Exception {
this(decoratedMap, hashAlgorithm, salt.getBytes(), secretKeyAlgorithm,
getSecretKey(secretKeyAlgorithm, secretKey, salt));
}
/**
* Decorates a map using the provided algorithms.
* <p>Takes a salt and secretKey so that it can work with a distributed cache.
*
* @param decoratedMap the map to decorate. CANNOT be NULL.
* @param hashAlgorithm the algorithm to use for hashing. CANNOT BE NULL.
* @param salt the salt, as a String. Gets converted to bytes. CANNOT be NULL.
* @param secretKeyAlgorithm the encryption algorithm. CANNOT BE NULL.
* @param secretKey the secret to use. CANNOT be NULL.
* @throws NoSuchAlgorithmException if the algorithm cannot be found. Should not happen in this case.
*/
public EncryptedMapDecorator(final Map<String, String> decoratedMap, final String hashAlgorithm, final byte[] salt,
final String secretKeyAlgorithm, final Key secretKey) throws NoSuchAlgorithmException {
this.decoratedMap = decoratedMap;
this.key = secretKey;
this.salt = salt;
this.secretKeyAlgorithm = secretKeyAlgorithm;
this.messageDigest = MessageDigest.getInstance(hashAlgorithm);
try {
this.ivSize = getIvSize();
} catch (final Exception e) {
throw new RuntimeException(e);
}
}
private static String getRandomSalt(final int size) {
final SecureRandom secureRandom = new SecureRandom();
final byte[] bytes = new byte[size];
secureRandom.nextBytes(bytes);
return getFormattedText(bytes);
}
@Override
public int size() {
return this.decoratedMap.size();
}
@Override
public boolean isEmpty() {
return this.decoratedMap.isEmpty();
}
@Override
public boolean containsKey(final Object key) {
final String hashedKey = constructHashedKey(key.toString());
return this.decoratedMap.containsKey(hashedKey);
}
@Override
public boolean containsValue(final Object value) {
if (!(value instanceof String)) {
return false;
}
final String encryptedValue = encrypt((String) value);
return this.decoratedMap.containsValue(encryptedValue);
}
@Override
public String get(final Object key) {
final String hashedKey = constructHashedKey(key == null ? null : key.toString());
return decrypt(this.decoratedMap.get(hashedKey), hashedKey);
}
@Override
public String put(final String key, final String value) {
final String hashedKey = constructHashedKey(key);
final String hashedValue = encrypt(value, hashedKey);
final String oldValue = this.decoratedMap.put(hashedKey, hashedValue);
return decrypt(oldValue, hashedKey);
}
@Override
public String remove(final Object key) {
final String hashedKey = constructHashedKey(key.toString());
return decrypt(this.decoratedMap.remove(hashedKey), hashedKey);
}
@Override
public void putAll(final Map<? extends String, ? extends String> m) {
for (final Entry<? extends String, ? extends String> entry : m.entrySet()) {
this.put(entry.getKey(), entry.getValue());
}
}
@Override
public void clear() {
this.decoratedMap.clear();
}
@Override
public Set<String> keySet() {
throw new UnsupportedOperationException();
}
@Override
public Collection<String> values() {
throw new UnsupportedOperationException();
}
@Override
public Set<Entry<String, String>> entrySet() {
throw new UnsupportedOperationException();
}
protected String constructHashedKey(final String key) {
if (key == null) {
return null;
}
final MessageDigest messageDigest = getMessageDigest();
messageDigest.update(this.salt);
messageDigest.update(key.getBytes());
final String hash = getFormattedText(messageDigest.digest());
logger.debug(String.format("Generated hash of value [%s] for key [%s].", hash, key));
return hash;
}
protected String decrypt(final String value, final String hashedKey) {
if (value == null) {
return null;
}
try {
final Cipher cipher = getCipherObject();
byte[] ivCiphertext = decode(value.getBytes());
int ivSize = byte2int(Arrays.copyOfRange(ivCiphertext, 0, INTEGER_LEN));
byte[] ivValue = Arrays.copyOfRange(ivCiphertext, INTEGER_LEN, (INTEGER_LEN + ivSize));
byte[] ciphertext = Arrays.copyOfRange(ivCiphertext, INTEGER_LEN + ivSize, ivCiphertext.length);
IvParameterSpec ivSpec = new IvParameterSpec(ivValue);
cipher.init(Cipher.DECRYPT_MODE, this.key, ivSpec);
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext);
} catch (final Exception e) {
throw new RuntimeException(e);
}
}
private static int getIvSize() throws NoSuchAlgorithmException, NoSuchPaddingException {
return Cipher.getInstance(CIPHER_ALGORITHM).getBlockSize();
}
private static byte[] generateIV(final int size) {
SecureRandom srand = new SecureRandom();
byte[] ivValue = new byte[size];
srand.nextBytes(ivValue);
return ivValue;
}
private static byte[] encode(final byte[] bytes) {
return new Base64().encode(bytes);
}
private static byte[] decode(final byte[] bytes) {
return new Base64().decode(bytes);
}
protected String encrypt(final String value) {
return encrypt(value, null);
}
protected String encrypt(final String value, final String hashedKey) {
if (value == null) {
return null;
}
try {
final Cipher cipher = getCipherObject();
byte[] ivValue = generateIV(this.ivSize);
IvParameterSpec ivSpec = new IvParameterSpec(ivValue);
cipher.init(Cipher.ENCRYPT_MODE, this.key, ivSpec);
byte[] ciphertext = cipher.doFinal(value.getBytes());
byte[] ivCiphertext = new byte[INTEGER_LEN + this.ivSize + ciphertext.length];
System.arraycopy(int2byte(this.ivSize), 0, ivCiphertext, 0, INTEGER_LEN);
System.arraycopy(ivValue, 0, ivCiphertext, INTEGER_LEN, this.ivSize);
System.arraycopy(ciphertext, 0, ivCiphertext, INTEGER_LEN + this.ivSize, ciphertext.length);
return new String(encode(ivCiphertext));
} catch(final Exception e) {
throw new RuntimeException(e);
}
}
protected static byte[] int2byte(final int i) throws UnsupportedEncodingException {
return ByteBuffer.allocate(4).putInt(i).array();
}
protected static int byte2int(final byte[] bytes) throws UnsupportedEncodingException {
return ByteBuffer.wrap(bytes).getInt();
}
protected static String byte2char(final byte[] bytes) throws UnsupportedEncodingException {
return new String(bytes, "UTF-8");
}
protected static byte[] char2byte(final String chars) throws UnsupportedEncodingException {
return chars.getBytes("UTF-8");
}
/**
* Tries to clone the {@link MessageDigest} that was created during construction. If the clone fails
* that is remembered and from that point on new {@link MessageDigest} instances will be created on
* every call.
* <p>
* Adopted from the Spring EhCache Annotations project.
*
* @return Generates a {@link MessageDigest} to use
*/
protected MessageDigest getMessageDigest() {
if (this.cloneNotSupported) {
final String algorithm = this.messageDigest.getAlgorithm();
try {
return MessageDigest.getInstance(algorithm);
} catch (final NoSuchAlgorithmException e) {
throw new IllegalStateException("MessageDigest algorithm '" + algorithm + "' was supported when "
+ this.getClass().getSimpleName()
+ " was created but is not now. This should not be possible.", e);
}
}
try {
return (MessageDigest) this.messageDigest.clone();
} catch (final CloneNotSupportedException e) {
this.cloneNotSupported = true;
final String msg = String.format("Could not clone MessageDigest using algorithm '%s'. "
+ "MessageDigest.getInstance will be used from now on which will be much more expensive.",
this.messageDigest.getAlgorithm());
logger.warn(msg, e);
return this.getMessageDigest();
}
}
/**
* Takes the raw bytes from the digest and formats them correct.
*
* @param bytes the raw bytes from the digest.
* @return the formatted bytes.
*/
private static String getFormattedText(final byte[] bytes) {
final StringBuilder buf = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
buf.append(HEX_DIGITS[b >> 4 & 0x0f]);
buf.append(HEX_DIGITS[b & 0x0f]);
}
return buf.toString();
}
private Cipher getCipherObject() throws NoSuchAlgorithmException, NoSuchPaddingException {
return Cipher.getInstance(CIPHER_ALGORITHM);
}
private static Key getSecretKey(final String secretKeyAlgorithm, final String secretKey,
final String salt) throws Exception {
SecretKeyFactory factory = SecretKeyFactory.getInstance(SECRET_KEY_FACTORY_ALGORITHM);
KeySpec spec = new PBEKeySpec(secretKey.toCharArray(), char2byte(salt), 65536, 128);
SecretKey tmp = factory.generateSecret(spec);
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), secretKeyAlgorithm);
return secret;
}
public String getSecretKeyAlgorithm() {
return secretKeyAlgorithm;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment