Java tutorial
/* * Copyright 2010 George Norman * * Licensed 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 com.thruzero.common.core.security; import java.security.InvalidAlgorithmParameterException; import java.security.spec.AlgorithmParameterSpec; import java.security.spec.KeySpec; import java.util.Arrays; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.PBEParameterSpec; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import com.thruzero.common.core.bookmarks.ConfigKeysBookmark; import com.thruzero.common.core.bookmarks.EnvironmentVarKeyBookmark; import com.thruzero.common.core.config.Config.ConfigKeys; import com.thruzero.common.core.locator.ConfigLocator; import com.thruzero.common.core.support.EnvironmentHelper.EnvironmentVariableKeys; import com.thruzero.common.core.utils.StringUtilsExt; /** * Provides encryption and decryption using a pass-phrase. The pass-phrase, salt and iteration-count can be read from a * config file, environment variable or passed into the constructor (and uses default values if none are given). * <p> * Useful for encrypting and decrypting a database password stored in a config file (e.g., hibernate config). * <p> * Note: Never use this to encrypt persisted user passwords. User passwords should never be decryptable. Instead, user * passwords should be stored using a one-way hash (see * {@link com.thruzero.common.core.security.MessageDigestHelper MessageDigestHelper}). * * @author George Norman * @see http://docs.oracle.com/javase/1.4.2/docs/guide/security/jce/JCERefGuide.html */ public class SimpleCipher { private static final String PBE_WITH_MD5_AND_DES = "PBEWithMD5AndDES"; // uses a set of parameters, comprising a salt and an iteration count private final Base64 base64 = new Base64(); private final Cipher encryptionCipher; private final Cipher decryptionCipher; // ------------------------------------------------ // SimpleCipherConfigKeys // ------------------------------------------------ /** * Defines keys used to configure the cipher from a config file and are defined inside of the config file section * named by CONFIG_SECTION: "com.thruzero.common.core.security.SimpleCipher". */ @ConfigKeysBookmark public interface SimpleCipherConfigKeys extends ConfigKeys { /** The config section to use */ String CONFIG_SECTION = SimpleCipher.class.getName(); /** Config key: "SimpleCipherSalt" */ String SALT = "SimpleCipherSalt"; /** Config key: "SimpleCipherPassPhrase" */ String PASS_PHRASE = "SimpleCipherPassPhrase"; /** Config key: "SimpleCipherIterationCount" */ String ITERATION_COUNT = "SimpleCipherIterationCount"; } // ------------------------------------------------ // SimpleCipherEnvironmentVariableKeys // ------------------------------------------------ /** Defines keys used to configure the cipher from environment variables. */ @EnvironmentVarKeyBookmark public interface SimpleCipherEnvironmentVariableKeys extends EnvironmentVariableKeys { /** Environment Var key: "com_thruzero_SimpleCipher_SALT" */ String SALT_ENV_VAR = "com_thruzero_SimpleCipher_SALT"; /** Environment Var key: "com_thruzero_SimpleCipher_PASS_PHRASE" */ String PASS_PHRASE_ENV_VAR = "com_thruzero_SimpleCipher_PASS_PHRASE"; /** Environment Var key: "com_thruzero_SimpleCipher_ITERATION_COUNT" */ String ITERATION_COUNT_ENV_VAR = "com_thruzero_SimpleCipher_ITERATION_COUNT"; } // ------------------------------------------------ // SimpleCipherException // ------------------------------------------------ /** A wrapper for the {@code javax.crypto} exceptions (offers hints for the cause). */ public static class SimpleCipherException extends Exception { private static final long serialVersionUID = 100; public SimpleCipherException(final String message, final Throwable cause) { super(message, cause); } } // ------------------------------------------------ // SimpleCipherConfiguration // ------------------------------------------------ /** * Defines the {@code salt}, {@code passPhrase} and {@code iterationCount} used to construct a {@code SimpleCipher}. */ public static class SimpleCipherConfiguration { private byte[] salt; private char[] passPhrase; private Integer iterationCount; private final EnvironmentVarInitOption environmentVarInitOption; private final ConfigInitOption configInitOption; public enum EnvironmentVarInitOption { ENABLED, DISABLED }; public enum ConfigInitOption { ENABLED, DISABLED }; /** * A configuration used to construct a SimpleCipher using {@code salt}, {@code passPhrase} and * {@code iterationCount} using environment variables, a config file or default values, whichever is found first. * Each property is read independently, so the {@code salt} can be read from an environment variable and the * {@code passPhrase} can be read from a config file and the iteration-count may use the default value. */ public SimpleCipherConfiguration() { this(null, null, null, EnvironmentVarInitOption.ENABLED, ConfigInitOption.ENABLED); } /** * A configuration used to construct a SimpleCipher from the given values of {@code salt}, {@code passPhrase} and * {@code iterationCount}. For any empty value, a default value will be used. */ public SimpleCipherConfiguration(final byte[] salt, final char[] passPhrase, final Integer iterationCount) { this(salt, passPhrase, iterationCount, EnvironmentVarInitOption.DISABLED, ConfigInitOption.DISABLED); } /** * A configuration used to construct a SimpleCipher from the given values for {@code salt}, {@code passPhrase} and * {@code iterationCount}. For each empty value, the configuration will first attempt to retrieve the value from an * environment variable (if allowed), and if not found or allowed, will look in the configuration file for a value * (if allowed), and if still not found, will use a default value. */ public SimpleCipherConfiguration(final byte[] salt, final char[] passPhrase, final Integer iterationCount, final EnvironmentVarInitOption environmentVarInitOption, final ConfigInitOption configInitOption) { this.salt = salt == null ? null : Arrays.copyOf(salt, salt.length); this.passPhrase = passPhrase == null ? null : Arrays.copyOf(passPhrase, passPhrase.length); this.iterationCount = iterationCount; this.environmentVarInitOption = environmentVarInitOption; this.configInitOption = configInitOption; } /** * Returns the {@code salt} represented by this instance. The result is determined as follows: * <ol> * <li>return the value given at construction time, unless null, * <li>if environment variables are allowed, then return the environment variable using the * {@link SimpleCipherEnvironmentVariableKeys#SALT_ENV_VAR}, unless null, * <li>if config variables are allowed, then return the config value using the {@link SimpleCipherConfigKeys#SALT}, * unless null, * <li>otherwise, return the default value using the {@link #getDefaultSalt()} function. * </ol> */ public byte[] getSalt() { if (salt == null) { String saltTokenStream = null; if (environmentVarInitOption == EnvironmentVarInitOption.ENABLED) { saltTokenStream = System.getenv(SimpleCipherEnvironmentVariableKeys.SALT_ENV_VAR); } if (StringUtils.isEmpty(saltTokenStream) && configInitOption == ConfigInitOption.ENABLED) { saltTokenStream = ConfigLocator.locate().getValue(SimpleCipherConfigKeys.CONFIG_SECTION, SimpleCipherConfigKeys.SALT); } if (StringUtils.isEmpty(saltTokenStream)) { salt = getDefaultSalt(); salt = salt == null ? null : Arrays.copyOf(salt, salt.length); // copy salt (to protect against client modifying given array. } else { salt = StringUtilsExt.tokensToByteArray(saltTokenStream, ","); } } return salt == null ? null : Arrays.copyOf(salt, salt.length); // copy returned value, to prevent clients from tampering with salt } /** * Returns the {@code passPhrase} represented by this instance. The result is determined as follows: * <ol> * <li>return the value given at construction time, unless null, * <li>if environment variables are allowed, then return the environment variable using the * {@link SimpleCipherEnvironmentVariableKeys#PASS_PHRASE}, unless null, * <li>if config variables are allowed, then return the config value using the * {@link SimpleCipherConfigKeys#PASS_PHRASE}, unless null, * <li>otherwise, return the default value using the {@link #getDefaultPassPhrase()} function. * </ol> */ public char[] getPassPhrase() { if (passPhrase == null) { String passPhraseStr = null; if (environmentVarInitOption == EnvironmentVarInitOption.ENABLED) { passPhraseStr = System.getenv(SimpleCipherEnvironmentVariableKeys.PASS_PHRASE_ENV_VAR); } if (StringUtils.isEmpty(passPhraseStr) && configInitOption == ConfigInitOption.ENABLED) { passPhraseStr = ConfigLocator.locate().getValue(SimpleCipherConfigKeys.CONFIG_SECTION, SimpleCipherConfigKeys.PASS_PHRASE); } if (StringUtils.isEmpty(passPhraseStr)) { passPhrase = getDefaultPassPhrase(); passPhrase = passPhrase == null ? null : Arrays.copyOf(passPhrase, passPhrase.length); // copy passPhrase (to protect against client modifying given array. } else { passPhrase = passPhraseStr.toCharArray(); } } return passPhrase == null ? null : Arrays.copyOf(passPhrase, passPhrase.length); // copy returned value, to prevent clients from tampering with passPhrase } /** * Returns the {@code iterationCount} represented by this instance. The result is determined as follows: * <ol> * <li>return the value given at construction time, unless null, * <li>if environment variables are allowed, then return the environment variable using the * {@link SimpleCipherEnvironmentVariableKeys#ITERATION_COUNT}, unless null, * <li>if config variables are allowed, then return the config value using the * {@link SimpleCipherConfigKeys#ITERATION_COUNT}, unless null, * <li>otherwise, return the default value using the {@link #getDefaultIterationCount()} function. * </ol> */ public int getIterationCount() { if (iterationCount == null) { String iterationCountStr = null; if (environmentVarInitOption == EnvironmentVarInitOption.ENABLED) { iterationCountStr = System.getenv(SimpleCipherEnvironmentVariableKeys.ITERATION_COUNT_ENV_VAR); } if (StringUtils.isEmpty(iterationCountStr) && configInitOption == ConfigInitOption.ENABLED) { iterationCountStr = ConfigLocator.locate().getValue(SimpleCipherConfigKeys.CONFIG_SECTION, SimpleCipherConfigKeys.ITERATION_COUNT); } if (StringUtils.isEmpty(iterationCountStr)) { iterationCount = getDefaultIterationCount(); } else { iterationCount = Integer.parseInt(iterationCountStr); } } return iterationCount; } protected byte[] getDefaultSalt() { return new byte[] { 0x19, (byte) 0xc1, 0x20, 0x01, 0x31, (byte) 0xd2, (byte) 0xc3, (byte) 0xa7 }; } protected char[] getDefaultPassPhrase() { return "S1mpleC1pherDef@ultP@ssPhr@se".toCharArray(); } protected int getDefaultIterationCount() { return 20; } } // ============================================================ // SimpleCipher // ============================================================ /** * Construct using the default {@link com.thruzero.common.core.security.SimpleCipher.SimpleCipherConfiguration}. * * @throws SimpleCipherException */ public SimpleCipher() throws SimpleCipherException { this(new SimpleCipherConfiguration()); } /** * Construct using the {@code salt}, {@code passPhrase} and {@code iterationCount} defined by the given * {@code simpleCipherConfiguration}. * * @throws SimpleCipherException */ public SimpleCipher(final SimpleCipherConfiguration simpleCipherConfiguration) throws SimpleCipherException { try { int count = simpleCipherConfiguration.getIterationCount(); byte[] salt = simpleCipherConfiguration.getSalt(); KeySpec keySpec = new PBEKeySpec(simpleCipherConfiguration.getPassPhrase(), salt, count); AlgorithmParameterSpec parameterSpec = new PBEParameterSpec(salt, count); SecretKey key = SecretKeyFactory.getInstance(PBE_WITH_MD5_AND_DES).generateSecret(keySpec); encryptionCipher = Cipher.getInstance(key.getAlgorithm()); encryptionCipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec); decryptionCipher = Cipher.getInstance(key.getAlgorithm()); decryptionCipher.init(Cipher.DECRYPT_MODE, key, parameterSpec); } catch (InvalidAlgorithmParameterException e) { throw new SimpleCipherException( "Couldn't instantiate SimpleCipher because of " + ExceptionUtils.getMessage(e), e); } catch (Exception e) { throw new SimpleCipherException( "Couldn't instantiate SimpleCipher because of " + ExceptionUtils.getMessage(e), e); } } /** * @return the given {@code plaintext} as an encrypted string. * @throws SimpleCipherException */ public String encrypt(final String plaintext) throws SimpleCipherException { String result = null; try { byte[] plaintextBytes = plaintext.getBytes(); byte[] enc = encryptionCipher.doFinal(plaintextBytes); // Encrypt the plaintext result = Base64.encodeBase64URLSafeString(enc); } catch (Exception e) { throw new SimpleCipherException( "Couldn't encrypt the given plaintext because of " + ExceptionUtils.getMessage(e), e); } return result; } /** * @return the given {@code encryptedStr} as a decrypted string. * @throws SimpleCipherException */ public String decrypt(final String encryptedStr) throws SimpleCipherException { byte[] decoded = base64.decode(encryptedStr); byte[] decrypted; try { decrypted = decryptionCipher.doFinal(decoded); } catch (IllegalBlockSizeException e) { throw new SimpleCipherException( "Couldn't decrypt the given ciphertext because of " + ExceptionUtils.getMessage(e), e); } catch (BadPaddingException e) { throw new SimpleCipherException( "Couldn't decrypt the given ciphertext - Ensure you're using the same passphrase to decrypt as was used to encrypt.", e); } return new String(decrypted); } }