org.jasig.cas.extension.clearpass.EncryptedMapDecorator.java Source code

Java tutorial

Introduction

Here is the source code for org.jasig.cas.extension.clearpass.EncryptedMapDecorator.java

Source

/*
 * Licensed to Apereo under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Apereo 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 org.jasig.cas.util.CompressionUtils;
import com.google.common.io.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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 java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Set;

/**
 * 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 static final int DEFAULT_SALT_SIZE = 8;
    private static final int DEFAULT_SECRET_KEY_SIZE = 32;
    private static final int BYTE_BUFFER_CAPACITY_SIZE = 4;
    private static final int HEX_RIGHT_SHIFT_COEFFICIENT = 4;
    private static final int HEX_HIGH_BITS_BITWISE_FLAG = 0x0f;

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @NotNull
    private final Map<String, String> decoratedMap;

    @NotNull
    private final MessageDigest messageDigest;

    @NotNull
    private final ByteSource salt;

    @NotNull
    private final Key key;

    @NotNull
    private int ivSize;

    @NotNull
    private final String secretKeyAlgorithm;

    private boolean cloneNotSupported;

    /**
     * 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(DEFAULT_SALT_SIZE), getRandomSalt(DEFAULT_SECRET_KEY_SIZE));
    }

    /**
     * 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(Charset.defaultCharset()), 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 RuntimeException if the algorithm cannot be found or the iv size cant be determined.
     */
    public EncryptedMapDecorator(final Map<String, String> decoratedMap, final String hashAlgorithm,
            final byte[] salt, final String secretKeyAlgorithm, final Key secretKey) {
        try {
            this.decoratedMap = decoratedMap;
            this.key = secretKey;
            this.salt = ByteSource.wrap(salt);
            this.secretKeyAlgorithm = secretKeyAlgorithm;
            this.messageDigest = MessageDigest.getInstance(hashAlgorithm);
            this.ivSize = getIvSize();
        } catch (final Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Gets the random salt.
     *
     * @param size the size
     * @return the random salt
     */
    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();
    }

    /**
     * Construct hashed key.
     *
     * @param key the key
     * @return the string
     */
    protected String constructHashedKey(final String key) {
        if (key == null) {
            return null;
        }

        final MessageDigest messageDigest = getMessageDigest();
        messageDigest.update(consumeByteSourceOrNull(this.salt));
        messageDigest.update(key.toLowerCase().getBytes(Charset.defaultCharset()));
        final String hash = getFormattedText(messageDigest.digest());

        logger.debug("Generated hash of value [{}] for key [{}].", hash, key);
        return hash;
    }

    /**
     * Decrypt the value.
     *
     * @param value the value
     * @param hashedKey the hashed key
     * @return the string
     */
    protected String decrypt(final String value, final String hashedKey) {
        if (value == null) {
            return null;
        }

        try {
            final Cipher cipher = getCipherObject();
            final byte[] ivCiphertext = CompressionUtils.decodeBase64ToByteArray(value);
            final int ivSize = byte2int(Arrays.copyOfRange(ivCiphertext, 0, INTEGER_LEN));
            final byte[] ivValue = Arrays.copyOfRange(ivCiphertext, INTEGER_LEN, (INTEGER_LEN + ivSize));
            final byte[] ciphertext = Arrays.copyOfRange(ivCiphertext, INTEGER_LEN + ivSize, ivCiphertext.length);
            final IvParameterSpec ivSpec = new IvParameterSpec(ivValue);

            cipher.init(Cipher.DECRYPT_MODE, this.key, ivSpec);

            final byte[] plaintext = cipher.doFinal(ciphertext);

            return new String(plaintext, Charset.defaultCharset());
        } catch (final Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Read the contents of the source into a byte array.
     * @param source  the byte array source
     * @return the byte[] read from the source or null
     */
    private byte[] consumeByteSourceOrNull(final ByteSource source) {
        try {
            if (source == null || source.isEmpty()) {
                return null;
            }
            return source.read();
        } catch (final IOException e) {
            logger.warn("Could not consume the byte array source", e);
            return null;
        }
    }

    /**
     * Gets the iv size.
     *
     * @return the iv size
     * @throws NoSuchAlgorithmException the no such algorithm exception
     * @throws NoSuchPaddingException the no such padding exception
     */
    private int getIvSize() throws NoSuchAlgorithmException, NoSuchPaddingException {
        return getCipherObject().getBlockSize();
    }

    /**
     * Generate iv.
     *
     * @param size the size
     * @return the iv value
     */
    private static byte[] generateIV(final int size) {
        final SecureRandom srand = new SecureRandom();
        final byte[] ivValue = new byte[size];
        srand.nextBytes(ivValue);
        return ivValue;
    }

    /**
     * Encrypt.
     *
     * @param value the value
     * @return the string
     */
    protected String encrypt(final String value) {
        return encrypt(value, null);
    }

    /**
     * Encrypt.
     *
     * @param value the value
     * @param hashedKey the hashed key
     * @return the string
     */
    protected String encrypt(final String value, final String hashedKey) {
        if (value == null) {
            return null;
        }

        try {
            final Cipher cipher = getCipherObject();
            final byte[] ivValue = generateIV(this.ivSize);
            final IvParameterSpec ivSpec = new IvParameterSpec(ivValue);

            cipher.init(Cipher.ENCRYPT_MODE, this.key, ivSpec);

            final byte[] ciphertext = cipher.doFinal(value.getBytes(Charset.defaultCharset()));
            final 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 CompressionUtils.encodeBase64(ivCiphertext);
        } catch (final Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Int to byte.
     *
     * @param i the i
     * @return the byte[]
     * @throws UnsupportedEncodingException the unsupported encoding exception
     */
    protected static byte[] int2byte(final int i) throws UnsupportedEncodingException {
        return ByteBuffer.allocate(BYTE_BUFFER_CAPACITY_SIZE).putInt(i).array();
    }

    /**
     * Byte to int.
     *
     * @param bytes the bytes
     * @return the int
     * @throws UnsupportedEncodingException the unsupported encoding exception
     */
    protected static int byte2int(final byte[] bytes) throws UnsupportedEncodingException {
        return ByteBuffer.wrap(bytes).getInt();
    }

    /**
     * Byte to char.
     *
     * @param bytes the bytes
     * @return the string
     * @throws UnsupportedEncodingException the unsupported encoding exception
     */
    protected static String byte2char(final byte[] bytes) throws UnsupportedEncodingException {
        return new String(bytes, "UTF-8");
    }

    /**
     * Char to byte.
     *
     * @param chars the chars
     * @return the byte[]
     * @throws UnsupportedEncodingException the unsupported encoding exception
     */
    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.
     *
     * @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 >> HEX_RIGHT_SHIFT_COEFFICIENT & HEX_HIGH_BITS_BITWISE_FLAG]);
            buf.append(HEX_DIGITS[b & HEX_HIGH_BITS_BITWISE_FLAG]);
        }
        return buf.toString();
    }

    /**
     * Gets the cipher object for the {@link #CIPHER_ALGORITHM}.
     *
     * @return the cipher object
     * @throws NoSuchAlgorithmException - if transformation is null, empty, in an invalid format, or if no Provider
     * supports a CipherSpi implementation for the specified algorithm. 
     * @throws NoSuchPaddingException - if transformation contains a padding scheme that is not available.
     * @see Cipher#getInstance(String)
     */
    private Cipher getCipherObject() throws NoSuchAlgorithmException, NoSuchPaddingException {
        return Cipher.getInstance(CIPHER_ALGORITHM);
    }

    /**
     * Gets the secret key.
     *
     * @param secretKeyAlgorithm the secret key algorithm
     * @param secretKey the secret key
     * @param salt the salt
     * @return the secret key
     * @throws Exception the exception
     */
    private static Key getSecretKey(final String secretKeyAlgorithm, final String secretKey, final String salt)
            throws Exception {

        final SecretKeyFactory factory = SecretKeyFactory.getInstance(SECRET_KEY_FACTORY_ALGORITHM);
        final KeySpec spec = new PBEKeySpec(secretKey.toCharArray(), char2byte(salt), 65536, 128);
        final SecretKey tmp = factory.generateSecret(spec);
        return new SecretKeySpec(tmp.getEncoded(), secretKeyAlgorithm);
    }

    public String getSecretKeyAlgorithm() {
        return secretKeyAlgorithm;
    }
}