package org.bubblecloud.ilves.security; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.ArrayUtils; import org.apache.log4j.Logger; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.util.encoders.Base64; import org.bouncycastle.util.encoders.Hex; import org.bubblecloud.ilves.model.User; import org.bubblecloud.ilves.util.PropertiesUtil; import org.bubblecloud.ilves.util.StringUtil; import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.io.File; import java.io.IOException; import java.math.BigInteger; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.Security; import java.util.Arrays; /** * Security utility methods. * * @author Tommi S.E. Laukkanen */ public class SecurityUtil { /** The logger. */ private static final Logger LOGGER = Logger.getLogger(SecurityUtil.class); static { if (Security.getProvider("BC") == null) { Security.addProvider(new BouncyCastleProvider()); } } /** * The security provider. */ public static final String PROVIDER = "BC"; /** * The symmetric encryption algorithm. */ public static final String SYMMETRIC_ENCRYPTION_ALGORITHM = "AES"; /** * The symmetric encryption key size. */ public static final int SYMMETRIC_ENCRYPTION_KEY_SIZE = 128; /** * The charset used to get bytes from password string for hashing. */ public static final Charset CHARSET = Charset.forName("UTF-8"); /** * Configuration encoding secret key. */ public static final byte[] CONFIGURATION_ENCODING_SECRET_KEY = Hex.decode("8cf46ed9ec6db5243e634b6cfe965788"); /** * Default initialization vector for encrypted configuration. */ public static final byte[] CONFIGURATION_ENCRYPTION_IV = Hex.decode("1aa13e4a6f1a022b51b550fffcd43021"); /** The secure random. */ private static SecureRandom random = new SecureRandom(); /** The access token lifetime in milliseconds. */ public static final long ACCESS_TOKEN_LIFETIME_MILLIS = 15 * 60 * 1000; /** * Method for generating keyt encryption secret key. * * @return new system secret key */ public static String generateKeyEncryptionSecretKey() { final SecureRandom secureRandom = new SecureRandom(); byte[] secretKeyBytes = new byte[SYMMETRIC_ENCRYPTION_KEY_SIZE / 8]; secureRandom.nextBytes(secretKeyBytes); return encodeConfiguration(Hex.toHexString(secretKeyBytes)); } /** * Encrypts plain text. * * @param iv the initialization vector * @param secretKey the secret key * @param plainText the plain text * @return the cipher text */ private static String encrypt(final byte[] iv, final byte[] secretKey, final String plainText) { try { final SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, SYMMETRIC_ENCRYPTION_ALGORITHM); final Cipher cipher = Cipher.getInstance(secretKeySpec.getAlgorithm() + "/CBC/PKCS5Padding", PROVIDER); cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, new IvParameterSpec(iv)); return new String(Base64.encode(cipher.doFinal(plainText.getBytes(CHARSET))), CHARSET); } catch (final Exception e) { throw new SecurityException("Error encoding", e); } } /** * Decrypts cipher text. * * @param iv the initialization vector * @param secretKey the secret key * @param cipherText the cipher text * @return the plain text */ private static String decrypt(final byte[] iv, final byte[] secretKey, final String cipherText) { try { final SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, SYMMETRIC_ENCRYPTION_ALGORITHM); final Cipher cipher = Cipher.getInstance(secretKeySpec.getAlgorithm() + "/CBC/PKCS5Padding", PROVIDER); cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new IvParameterSpec(iv)); return new String(cipher.doFinal(Base64.decode(cipherText.getBytes(CHARSET))), CHARSET); } catch (final Exception e) { throw new SecurityException("Error dencoding", e); } } /** * Encode configuration. * * @param plainText the configuration text to encrypt * @return the encoded configuration text */ private static String encodeConfiguration(final String plainText) { return encrypt(CONFIGURATION_ENCRYPTION_IV, CONFIGURATION_ENCODING_SECRET_KEY, plainText); } /** * Decode configuration. * * @param encodedText the encoded configuration to decrypt * @return the plain configuration */ private static String decodeConfiguration(final String encodedText) { return decrypt(CONFIGURATION_ENCRYPTION_IV, CONFIGURATION_ENCODING_SECRET_KEY, encodedText); } /** * Encrypts with key encryption secret key. * * @param plainText the plain text * @return the cipher text */ public static String encryptSecretKey(final String plainText) { final String systemEncodedSecretKey = PropertiesUtil.getProperty("site", "key-encryption-secret-key"); final String systemSecretKey = decodeConfiguration(systemEncodedSecretKey); return encrypt(CONFIGURATION_ENCRYPTION_IV, Hex.decode(systemSecretKey), plainText); } /** * Decrypts with key encryption secret key. * * @param cipherText the cipher text * @return the cipher text */ public static String decryptSecretKey(final String cipherText) { if (!PropertiesUtil.hasProperty("site", "key-encryption-secret-key")) { LOGGER.error("Server key encryption key is not defined. Candidate key generated to key-encryption-secret-key-candidate.properties. Please copy the line to site-ext.properties "); try { FileUtils.writeStringToFile(new File("key-encryption-secret-key-candidate.properties"), "key-encryption-secret-key = " + generateKeyEncryptionSecretKey(), false); } catch (IOException e) { LOGGER.error("Attempt to write candidate key to key-encryption-secret-key-candidate.properties failed", e); } } final String systemEncodedSecretKey = PropertiesUtil.getProperty("site", "key-encryption-secret-key"); final String systemSecretKey = decodeConfiguration(systemEncodedSecretKey); return decrypt(CONFIGURATION_ENCRYPTION_IV, Hex.decode(systemSecretKey), cipherText); } /** * Calculate hash for string. * @param stringValue the string value * @return the hash as hex encoded string */ public static String calculateHash(String stringValue) { final MessageDigest md; try { md = MessageDigest.getInstance("SHA-256"); md.update(stringValue.getBytes("UTF-8")); // Change this to "UTF-16" if needed } catch (final Exception e) { throw new RuntimeException("Unable to compute session ID hash.", e); } return org.apache.commons.codec.binary.Hex.encodeHexString(md.digest()); } /** * Generates access token. * @return the access token */ public static char[] generateAccessToken() { final char[] accessToken; synchronized (random) { accessToken = org.apache.commons.codec.binary.Hex.encodeHex(new BigInteger(130, random).toByteArray()); } return accessToken; } public static String getSecretHash(final char[] secret) { final byte[] accessTokenHashBytes = convertCharactersToBytes(secret); final MessageDigest md; try { md = MessageDigest.getInstance("SHA-256"); } catch (NoSuchAlgorithmException e) { throw new SecurityException(e); } return StringUtil.toHexString(md.digest(accessTokenHashBytes)); } /** * Converts character array to byte array * @param characters the character array * @return the byte array */ public static byte[] convertCharactersToBytes(char[] characters) { CharBuffer charBuffer = CharBuffer.wrap(characters); final ByteBuffer byteBuffer = Charset.forName("UTF-8").encode(charBuffer); byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()); Arrays.fill(charBuffer.array(), '\u0000'); Arrays.fill(byteBuffer.array(), (byte) 0); return bytes; } }