/* * Copyright (c) 2010-2017 Evolveum * * 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.evolveum.midpoint.prism.crypto; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.Key; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.SecureRandom; import java.security.UnrecoverableKeyException; import java.security.spec.InvalidKeySpecException; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; import java.util.List; import java.util.Random; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.xml.namespace.QName; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.apache.xml.security.Init; import org.apache.xml.security.algorithms.JCEMapper; import org.apache.xml.security.encryption.XMLCipher; import org.apache.xml.security.utils.Base64; import com.evolveum.midpoint.prism.PrismConstants; import com.evolveum.midpoint.util.QNameUtil; import com.evolveum.midpoint.util.exception.SchemaException; import com.evolveum.midpoint.util.exception.SystemException; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; import com.evolveum.prism.xml.ns._public.types_3.CipherDataType; import com.evolveum.prism.xml.ns._public.types_3.DigestMethodType; import com.evolveum.prism.xml.ns._public.types_3.EncryptedDataType; import com.evolveum.prism.xml.ns._public.types_3.EncryptionMethodType; import com.evolveum.prism.xml.ns._public.types_3.HashedDataType; import com.evolveum.prism.xml.ns._public.types_3.KeyInfoType; import com.evolveum.prism.xml.ns._public.types_3.ProtectedStringType; /** * Class that manages encrypted and hashed values. Java Cryptography Extension is * needed because this class is using AES-256 for encrypting/decrypting xml * data. * * @author Radovan Semancik * @author lazyman */ public class ProtectorImpl extends BaseProtector { private static final String ALGORITHM_PKKDF2_NAME = "PBKDF2WithHmacSHA512"; private static final QName ALGORITH_PBKDF2_WITH_HMAC_SHA512_QNAME = new QName(PrismConstants.NS_CRYPTO_ALGORITHM_PBKD, ALGORITHM_PKKDF2_NAME); private static final String ALGORITH_PBKDF2_WITH_HMAC_SHA512_URI = QNameUtil.qNameToUri(ALGORITH_PBKDF2_WITH_HMAC_SHA512_QNAME); private static final String KEY_DIGEST_TYPE = "SHA1"; private static final String DEFAULT_ENCRYPTION_ALGORITHM = XMLCipher.AES_128; private static final char[] KEY_PASSWORD = "midpoint".toCharArray(); private static final String DEFAULT_DIGEST_ALGORITHM = ALGORITH_PBKDF2_WITH_HMAC_SHA512_URI; // "http://www.w3.org/2009/xmlenc11#pbkdf2" private Random randomNumberGenerator; private static final Trace LOGGER = TraceManager.getTrace(ProtectorImpl.class); private String keyStorePath; private String keyStorePassword; private String encryptionKeyAlias = "default"; private String requestedJceProviderName = null; private String encryptionAlgorithm; private String digestAlgorithm; private List<TrustManager> trustManagers; private static final KeyStore keyStore; static { try { keyStore = KeyStore.getInstance("jceks"); } catch (KeyStoreException ex) { throw new SystemException(ex.getMessage(), ex); } } /** * @throws SystemException if jceks keystore is not available on {@link ProtectorImpl#getKeyStorePath} */ public void init() { InputStream stream = null; try { // Test if use file or classpath resource File f = new File(getKeyStorePath()); if (f.exists()) { LOGGER.info("Using file keystore at {}", getKeyStorePath()); if (!f.canRead()) { LOGGER.error("Provided keystore file {} is unreadable.", getKeyStorePath()); throw new EncryptionException("Provided keystore file " + getKeyStorePath() + " is unreadable."); } stream = new FileInputStream(f); // Use class path keystore } else { LOGGER.warn("Using default keystore from classpath ({}).", getKeyStorePath()); // Read from class path stream = ProtectorImpl.class.getClassLoader().getResourceAsStream(getKeyStorePath()); // ugly dirty hack to have second chance to find keystore on // class path if (stream == null) { stream = ProtectorImpl.class.getClassLoader().getResourceAsStream( "com/../../" + getKeyStorePath()); } } // Test if we have valid stream if (stream == null) { throw new EncryptionException("Couldn't load keystore as resource '" + getKeyStorePath() + "'"); } // Load keystore keyStore.load(stream, getKeyStorePassword().toCharArray()); stream.close(); // Initialze trust manager list TrustManagerFactory tmFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmFactory.init(keyStore); trustManagers = new ArrayList<TrustManager>(); for (TrustManager trustManager : tmFactory.getTrustManagers()) { trustManagers.add(trustManager); } //init apache crypto library Init.init(); } catch (Exception ex) { LOGGER.error("Unable to work with keystore {}, reason {}.", new Object[]{getKeyStorePath(), ex.getMessage()}, ex); throw new SystemException(ex.getMessage(), ex); } randomNumberGenerator = new SecureRandom(); } public String getRequestedJceProviderName() { return requestedJceProviderName; } public void setRequestedJceProviderName(String requestedJceProviderName) { this.requestedJceProviderName = requestedJceProviderName; } public String getEncryptionAlgorithm() { return encryptionAlgorithm; } public void setEncryptionAlgorithm(String encryptionAlgorithm) { this.encryptionAlgorithm = encryptionAlgorithm; } private String getCipherAlgorithm() { if (encryptionAlgorithm != null) { return encryptionAlgorithm; } else { return DEFAULT_ENCRYPTION_ALGORITHM; } } private String getDigestAlgorithm() { if (digestAlgorithm != null) { return digestAlgorithm; } else { return DEFAULT_DIGEST_ALGORITHM; } } // TODO: make it configurable private int getPbkdKeyLength() { return 256; } private int getPbkdIterations() { return 10000; } private int getPbkdSaltLength() { return 32; } /** * @return the encryptionKeyAlias * @throws IllegalStateException if encryption key digest is null or empty string */ private String getEncryptionKeyAlias() { if (StringUtils.isEmpty(encryptionKeyAlias)) { throw new IllegalStateException("Encryption key alias was not defined (is null or empty)."); } return encryptionKeyAlias; } @Override protected <T> byte[] decryptBytes(ProtectedData<T> protectedData) throws SchemaException, EncryptionException { EncryptedDataType encryptedDataType = protectedData.getEncryptedDataType(); EncryptionMethodType encryptionMethodType = encryptedDataType.getEncryptionMethod(); if (encryptionMethodType == null) { throw new SchemaException("No encryptionMethod element in protected data"); } String algorithmUri = encryptionMethodType.getAlgorithm(); if (StringUtils.isBlank(algorithmUri)) { throw new SchemaException("No algorithm URI in encryptionMethod element in protected data"); } KeyInfoType keyInfo = encryptedDataType.getKeyInfo(); if (keyInfo == null) { throw new SchemaException("No keyInfo element in protected data"); } String keyName = keyInfo.getKeyName(); if (StringUtils.isBlank(keyName)) { throw new SchemaException("No keyName defined in keyInfo element in protected data"); } SecretKey key = getSecretKeyByDigest(keyName); CipherDataType cipherData = encryptedDataType.getCipherData(); if (cipherData == null) { throw new SchemaException("No cipherData element in protected data"); } byte[] encryptedBytes = cipherData.getCipherValue(); if (encryptedBytes == null || encryptedBytes.length == 0) { throw new SchemaException("No cipherValue in cipherData element in protected data"); } byte[] decryptedData; try { decryptedData = decryptBytes(encryptedBytes, algorithmUri, key); } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | NoSuchProviderException | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) { throw new EncryptionException(e.getMessage(), e); } return decryptedData; } @Override public <T> void encrypt(ProtectedData<T> protectedData) throws EncryptionException { if (protectedData.isEncrypted()) { throw new IllegalArgumentException("Attempt to encrypt protected data that are already encrypted"); } SecretKey key = getSecretKeyByAlias(getEncryptionKeyAlias()); String algorithm = getCipherAlgorithm(); byte[] clearBytes = protectedData.getClearBytes(); byte[] encryptedBytes; try { encryptedBytes = encryptBytes(clearBytes, algorithm, key); } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | NoSuchProviderException | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) { throw new EncryptionException(e.getMessage(), e); } // Construct encryption types EncryptedDataType encryptedDataType = new EncryptedDataType(); EncryptionMethodType encryptionMethodType = new EncryptionMethodType(); encryptionMethodType.setAlgorithm(algorithm); encryptedDataType.setEncryptionMethod(encryptionMethodType); KeyInfoType keyInfoType = new KeyInfoType(); keyInfoType.setKeyName(getSecretKeyDigest(key)); encryptedDataType.setKeyInfo(keyInfoType); CipherDataType cipherDataType = new CipherDataType(); cipherDataType.setCipherValue(encryptedBytes); encryptedDataType.setCipherData(cipherDataType); protectedData.setEncryptedData(encryptedDataType); protectedData.destroyCleartext(); } private byte[] encryptBytes(byte[] clearData, String algorithmUri, Key key) throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException { Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, algorithmUri); cipher.init(Cipher.ENCRYPT_MODE, key); byte[] encryptedData = cipher.doFinal(clearData); // Place IV at the beginning of the encrypted bytes so it can be reused on decryption byte[] iv = cipher.getIV(); byte[] encryptedBytes = new byte[iv.length + encryptedData.length]; System.arraycopy(iv, 0, encryptedBytes, 0, iv.length); System.arraycopy(encryptedData, 0, encryptedBytes, iv.length, encryptedData.length); return encryptedBytes; } private byte[] decryptBytes(byte[] encryptedBytes, String algorithmUri, Key key) throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException { Cipher cipher = getCipher(Cipher.DECRYPT_MODE, algorithmUri); // Extract IV from the beginning of the encrypted bytes int ivLen = cipher.getBlockSize(); byte[] ivBytes = new byte[ivLen]; System.arraycopy(encryptedBytes, 0, ivBytes, 0, ivLen); IvParameterSpec iv = new IvParameterSpec(ivBytes); cipher.init(Cipher.DECRYPT_MODE, key, iv); byte[] decryptedData = cipher.doFinal(encryptedBytes, ivLen, encryptedBytes.length - ivLen); return decryptedData; } private Cipher getCipher(int cipherMode, String algorithmUri) throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException, InvalidKeyException, InvalidAlgorithmParameterException { String jceAlgorithm = JCEMapper.translateURItoJCEID(algorithmUri);//JCEMapper.getJCEKeyAlgorithmFromURI(algorithmUri); Cipher cipher; if (requestedJceProviderName == null) { cipher = Cipher.getInstance(jceAlgorithm); } else { cipher = Cipher.getInstance(jceAlgorithm, requestedJceProviderName); } if (LOGGER.isTraceEnabled()) { String desc; if (cipherMode == Cipher.ENCRYPT_MODE) { desc = "Encrypting"; } else if (cipherMode == Cipher.DECRYPT_MODE) { desc = "Decrypting"; } else { desc = "Ciphering (mode " + cipherMode + ")"; } LOGGER.trace("{} data by JCE algorithm {} (URI {}), cipher {}, provider {}", new Object[]{ desc, jceAlgorithm, algorithmUri, cipher.getAlgorithm(), cipher.getProvider().getName()}); } return cipher; } public String getSecretKeyDigest(SecretKey key) throws EncryptionException { MessageDigest sha1; try { sha1 = MessageDigest.getInstance(KEY_DIGEST_TYPE); } catch (NoSuchAlgorithmException ex) { throw new EncryptionException(ex.getMessage(), ex); } return Base64.encode(sha1.digest(key.getEncoded())); } @Override public List<TrustManager> getTrustManagers() { return trustManagers; } @Override public KeyStore getKeyStore() { return keyStore; } /** * @param encryptionKeyAlias Alias of the encryption key {@link SecretKey} which is used * for encryption * @throws IllegalArgumentException if encryption key digest is null or empty string */ public void setEncryptionKeyAlias(String encryptionKeyAlias) { Validate.notEmpty(encryptionKeyAlias, "Encryption key alias must not be null or empty."); this.encryptionKeyAlias = encryptionKeyAlias; } /** * @param keyStorePassword * @throws IllegalArgumentException if keystore password is null string */ public void setKeyStorePassword(String keyStorePassword) { Validate.notNull(keyStorePassword, "Keystore password must not be null."); this.keyStorePassword = keyStorePassword; } private String getKeyStorePassword() { if (keyStorePassword == null) { throw new IllegalStateException("Keystore password was not defined (null)."); } return keyStorePassword; } /** * @param keyStorePath * @throws IllegalArgumentException if keystore path is null string */ public void setKeyStorePath(String keyStorePath) { Validate.notEmpty(keyStorePath, "Key store path must not be null."); this.keyStorePath = keyStorePath; } public String getKeyStorePath() { if (StringUtils.isEmpty(keyStorePath)) { throw new IllegalStateException("Keystore path was not defined (is null or empty)."); } return keyStorePath; } private SecretKey getSecretKeyByAlias(String alias) throws EncryptionException { Key key; try { key = keyStore.getKey(alias, KEY_PASSWORD); } catch (Exception ex) { throw new EncryptionException("Couldn't obtain key '" + alias + "' from keystore, reason: " + ex.getMessage(), ex); } if (key == null || !(key instanceof SecretKey)) { throw new EncryptionException("Key with alias '" + alias + "' is not instance of SecretKey, but '" + key + "'."); } return (SecretKey) key; } private SecretKey getSecretKeyByDigest(String digest) throws EncryptionException { try { Enumeration<String> aliases = keyStore.aliases(); while (aliases.hasMoreElements()) { String alias = aliases.nextElement(); if (!keyStore.isKeyEntry(alias)) { continue; } try { Key key = keyStore.getKey(alias, KEY_PASSWORD); if (!(key instanceof SecretKey)) { continue; } String keyHash = getSecretKeyDigest((SecretKey) key); if (digest.equals(keyHash)) { return (SecretKey) key; } } catch (UnrecoverableKeyException ex) { LOGGER.trace("Couldn't recover key {} from keystore, reason: {}", new Object[]{alias, ex.getMessage()}); } } } catch (Exception ex) { throw new EncryptionException(ex.getMessage(), ex); } throw new EncryptionException("Key '" + digest + "' is not in keystore."); } @Override public <T> void hash(ProtectedData<T> protectedData) throws EncryptionException, SchemaException { if (protectedData.isHashed()) { throw new IllegalArgumentException("Attempt to hash protected data that are already hashed"); } String algorithmUri = getDigestAlgorithm(); QName algorithmQName = QNameUtil.uriToQName(algorithmUri); String algorithmNamespace = algorithmQName.getNamespaceURI(); if (algorithmNamespace == null) { throw new SchemaException("No algorithm namespace"); } HashedDataType hashedDataType; switch (algorithmNamespace) { case PrismConstants.NS_CRYPTO_ALGORITHM_PBKD: if (!protectedData.canSupportType(String.class)) { throw new SchemaException("Non-string proteted data"); } hashedDataType = hashPbkd((ProtectedData<String>) protectedData, algorithmUri, algorithmQName.getLocalPart()); break; default: throw new SchemaException("Unkown namespace "+algorithmNamespace); } protectedData.setHashedData(hashedDataType); protectedData.destroyCleartext(); protectedData.setEncryptedData(null); } private HashedDataType hashPbkd(ProtectedData<String> protectedData, String algorithmUri, String algorithmName) throws EncryptionException { char[] clearChars = getClearChars(protectedData); byte[] salt = generatePbkdSalt(); int iterations = getPbkdIterations(); SecretKeyFactory secretKeyFactory; try { secretKeyFactory = SecretKeyFactory.getInstance( algorithmName ); } catch (NoSuchAlgorithmException e) { throw new EncryptionException(e.getMessage(), e); } PBEKeySpec keySpec = new PBEKeySpec( clearChars, salt, iterations, getPbkdKeyLength() ); SecretKey key; try { key = secretKeyFactory.generateSecret( keySpec ); } catch (InvalidKeySpecException e) { throw new EncryptionException(e.getMessage(), e); } byte[] hashBytes = key.getEncoded( ); HashedDataType hashedDataType = new HashedDataType(); DigestMethodType digestMethod = new DigestMethodType(); digestMethod.setAlgorithm(algorithmUri); digestMethod.setSalt(salt); digestMethod.setWorkFactor(iterations); hashedDataType.setDigestMethod(digestMethod); hashedDataType.setDigestValue(hashBytes); return hashedDataType; } private char[] getClearChars(ProtectedData<String> protectedData) throws EncryptionException { if (protectedData.isEncrypted()) { return decryptString(protectedData).toCharArray(); } else { return protectedData.getClearValue().toCharArray(); } } private byte[] generatePbkdSalt() { byte[] salt = new byte[getPbkdSaltLength()/8]; randomNumberGenerator.nextBytes(salt); return salt; } @Override public boolean compare(ProtectedStringType a, ProtectedStringType b) throws EncryptionException, SchemaException { if (a == b) { return true; } if (a == null && b == null) { return true; } if (a == null || b == null) { return false; } if (a.isHashed() && b.isHashed()) { throw new SchemaException("Cannot compare two hased protected strings"); } if (a.isHashed() || b.isHashed()) { String clear; ProtectedStringType hashedPs; if (a.isHashed()) { hashedPs = a; clear = decryptString(b); } else { hashedPs = b; clear = decryptString(a); } if (clear == null) { return false; } return compareHashed(hashedPs, clear.toCharArray()); } else { String aClear = decryptString(a); String bClear = decryptString(b); if (aClear == null && bClear == null) { return true; } if (aClear == null || bClear == null) { return false; } return aClear.equals(bClear); } } private boolean compareHashed(ProtectedStringType hashedPs, char[] clearChars) throws SchemaException, EncryptionException { HashedDataType hashedDataType = hashedPs.getHashedDataType(); DigestMethodType digestMethodType = hashedDataType.getDigestMethod(); if (digestMethodType == null) { throw new SchemaException("No digest type"); } String algorithmUri = digestMethodType.getAlgorithm(); QName algorithmQName = QNameUtil.uriToQName(algorithmUri); String algorithmNamespace = algorithmQName.getNamespaceURI(); if (algorithmNamespace == null) { throw new SchemaException("No algorithm namespace"); } switch (algorithmNamespace) { case PrismConstants.NS_CRYPTO_ALGORITHM_PBKD: return compareHashedPbkd(hashedDataType, algorithmQName.getLocalPart(), clearChars); default: throw new SchemaException("Unkown namespace "+algorithmNamespace); } } private boolean compareHashedPbkd(HashedDataType hashedDataType, String algorithmName, char[] clearChars) throws EncryptionException { DigestMethodType digestMethodType = hashedDataType.getDigestMethod(); byte[] salt = digestMethodType.getSalt(); Integer workFactor = digestMethodType.getWorkFactor(); byte[] digestValue = hashedDataType.getDigestValue(); int keyLen = digestValue.length * 8; SecretKeyFactory secretKeyFactory; try { secretKeyFactory = SecretKeyFactory.getInstance( algorithmName ); } catch (NoSuchAlgorithmException e) { throw new EncryptionException(e.getMessage(), e); } PBEKeySpec keySpec = new PBEKeySpec( clearChars, salt, workFactor, keyLen ); SecretKey key; try { key = secretKeyFactory.generateSecret( keySpec ); } catch (InvalidKeySpecException e) { throw new EncryptionException(e.getMessage(), e); } byte[] hashBytes = key.getEncoded( ); return Arrays.equals(digestValue, hashBytes); } }