// Copyright 2007 Google Inc. // // 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.google.enterprise.connector.instantiator; import com.google.common.base.Charsets; import com.google.enterprise.connector.common.PropertiesUtils; import com.google.enterprise.connector.common.SecurityUtils; import com.google.enterprise.connector.util.Base64; import com.google.enterprise.connector.util.Base64DecoderException; import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.channels.FileLock; import java.security.InvalidKeyException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.Arrays; import java.util.Enumeration; import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.KeyGenerator; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; /** * Extended version of {@link * org.springframework.beans.factory.config.PropertyPlaceholderConfigurer} * that looks for encrypted sensitive properties (like passwords) * and decrypts them when reading them back. The JCE API is used * for the relevant key storage and cryptography. * * <p> You can configure the following parameters used by this class: * <ul> * <li>File in which keystore is kept. Use #setKeyStorePath. * <li>File in which password securing keystore is kept. Use * #setKeyStorePasswdPath. * <li>The format of the keystore. Your JCE provider must support this. Use #setKeyStoreType. The default is "JCEKS", which is * provided by the default Sun JCE provider. * <li>The algorithm used for encryption. Your JCE provider must support * this. Use #setKeyStoreCryptoAlgo. The default is "AES", * which is provided by the default Sun JCE provider. * </ul> * * <p> * You can also provide these parameters in the {@code config-param} * section of the web application's configuration file (web.xml). The * relevant parameters are: {@code keystore_file, keystore_passwd_file, * keystore_type, keystore_crypto_algo}. */ public class EncryptedPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer { private static final Logger LOGGER = Logger.getLogger(EncryptedPropertyPlaceholderConfigurer.class.getName()); private static final String ENCRYPT_MSG = "Could not encrypt "; private static final String DECRYPT_MSG = "Could not decrypt "; private static final String GENERIC_PROP_NAME = "password"; private static final String KEY_NAME = "EXTERNAL_CM_KEY"; private static String keyStorePath = "external_cm.keystore"; private static String keyStorePasswdPath = null; private static String keyStoreType = "JCEKS"; private static String keyStoreCryptoAlgo = "AES"; private static SecretKey secretKey = null; /* * Overridden from the base class implementation. This looks for properties * with a sensitive name and decrypts them. */ @Override public void convertProperties(Properties properties) { decryptSensitiveProperties(properties); super.convertProperties(properties); } public static void encryptSensitiveProperties(Properties properties) { // New style properties file, encrypt any key with 'password' in it. PropertiesUtils.stampPropertiesVersion(properties); Enumeration<?> props = properties.propertyNames(); while (props.hasMoreElements()) { String prop = (String) props.nextElement(); if (SecurityUtils.isKeySensitive(prop)) { properties.setProperty(prop, encryptString(prop, properties.getProperty(prop))); } } } public static void decryptSensitiveProperties(Properties properties) { int version = PropertiesUtils.getPropertiesVersion(properties); Enumeration<?> props = properties.propertyNames(); while (props.hasMoreElements()) { String prop = (String) props.nextElement(); // Older properties files (before we started versioning them) only // encrypted a property called "Password". Newer property files // encrypt any property with case-insensitive 'password' in the key. boolean doCrypt = (version < 1) ? prop.equals("Password") : (SecurityUtils.isKeySensitive(prop)); if (doCrypt) { properties.setProperty(prop, decryptString(prop, properties.getProperty(prop))); } } } public static void setKeyStorePath(String k) { keyStorePath = k; LOGGER.config("Using keystore " + k); } public static String getKeyStorePath() { return keyStorePath; } public static void setKeyStorePasswdPath(String k) { keyStorePasswdPath = k; LOGGER.config("Using keystore password file " + k); } public static void setKeyStoreType(String t) { keyStoreType = t; } public static void setKeyStoreCryptoAlgo(String a) { keyStoreCryptoAlgo = a; } public static String getKeyStoreType() { return keyStoreType; } public static String getKeyStoreCryptoAlgo() { return keyStoreCryptoAlgo; } /* * Reads a KeyStore from the supplied file, or creates a new KeyStore * if none exists. */ private static KeyStore readKeyStore(RandomAccessFile keyStoreFile, char[] keyPassChars) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException { KeyStore keyStore = KeyStore.getInstance(keyStoreType); // If the file is non-empty, read in the KeyStore, otherwise // create an empty KeyStore. ByteArrayInputStream bais = null; if (keyStoreFile.length() > 0L) { byte[] buffer = new byte[(int) keyStoreFile.length()]; keyStoreFile.seek(0L); keyStoreFile.read(buffer); bais = new ByteArrayInputStream(buffer); } keyStore.load(bais, keyPassChars); return keyStore; } /* * Writes a KeyStore to the supplied file, overwriting it completely. */ private static void writeKeyStore(KeyStore keyStore, RandomAccessFile keyStoreFile, char[] keyPassChars) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); keyStore.store(baos, keyPassChars); keyStoreFile.seek(0L); keyStoreFile.setLength(0L); keyStoreFile.write(baos.toByteArray()); keyStoreFile.getChannel().force(false); } /* * Reads in password used to secure keystore. */ private static String getKeyStorePasswd() { if (keyStorePasswdPath == null) { return ""; } try { File f = new File(keyStorePasswdPath); BufferedReader in = new BufferedReader(new FileReader(f)); try { String passwd = in.readLine(); return passwd; } finally { in.close(); } } catch (FileNotFoundException e) { LOGGER.fine("Keystore passwd file does not exist"); } catch (IOException e) { LOGGER.warning("Could not open keystore passwd file"); } return ""; } /* * Reads in secret key from keystore, or generates one and stores it in the * keystore if none exists. */ private static synchronized SecretKey getSecretKey() throws NoSuchAlgorithmException, KeyStoreException, CertificateException, UnrecoverableKeyException, IOException { if (secretKey != null) { return secretKey; } char[] keyStorePasswdChars = getKeyStorePasswd().toCharArray(); File keyStoreFile = new File(keyStorePath); keyStoreFile.createNewFile(); LOGGER.config("Accessing keystore at " + keyStoreFile.getAbsolutePath()); // Lock the keystore file to avoid concurrent key generation. RandomAccessFile raf = new RandomAccessFile(keyStoreFile, "rw"); try { FileLock lock = raf.getChannel().lock(); try { KeyStore keyStore = readKeyStore(raf, keyStorePasswdChars); secretKey = (SecretKey)keyStore.getKey(KEY_NAME, keyStorePasswdChars); if (secretKey == null) { // key did not exist --- create a new key, and store it LOGGER.config("Creating new key for password encryption"); secretKey = KeyGenerator.getInstance(keyStoreCryptoAlgo).generateKey(); keyStore.setKeyEntry(KEY_NAME, secretKey, keyStorePasswdChars, null); writeKeyStore(keyStore, raf, keyStorePasswdChars); } } finally { lock.release(); } } finally { raf.close(); } return secretKey; } public static String encryptString(String plainText) { return encryptString(GENERIC_PROP_NAME, plainText); } public static String encryptString(String name, String plainText) { // Convert the String into bytes using utf-8 byte[] bytes = plainText.getBytes(Charsets.UTF_8); String cipherText = encryptBytes(name, bytes); // Overwrite the temporary array. Arrays.fill(bytes, (byte) 0); return cipherText; } public static String encryptChars(char[] plainText) { return encryptChars(GENERIC_PROP_NAME, plainText); } public static String encryptChars(String name, char[] plainText) { // Convert the char[] into bytes using utf-8 // We do this the hard way to avoid using a String we can't overwrite. ByteBuffer buffer = Charsets.UTF_8.encode(CharBuffer.wrap(plainText)); byte[] bytes = new byte[buffer.remaining()]; buffer.get(bytes); String cipherText = encryptBytes(name, bytes); // Overwrite the temporary arrays. Arrays.fill(bytes, (byte) 0); Arrays.fill(buffer.array(), (byte) 0); return cipherText; } public static String encryptBytes(byte[] plainText) { return encryptBytes(GENERIC_PROP_NAME, plainText); } public static String encryptBytes(String name, byte[] plainText) { try { SecretKey key = getSecretKey(); Cipher encryptor = Cipher.getInstance(keyStoreCryptoAlgo); encryptor.init(Cipher.ENCRYPT_MODE, key); // Encrypt the supplied byte buffer. byte[] enc = encryptor.doFinal(plainText); // Encode bytes to base64 to get a string return Base64.encode(enc); } catch (NoSuchAlgorithmException e) { throw newRuntimeException(ENCRYPT_MSG, name, "provider does not have algorithm", e); } catch (IOException e) { throw newRuntimeException(ENCRYPT_MSG, name, "I/O error", e); } catch (NoSuchPaddingException e) { throw newRuntimeException(ENCRYPT_MSG, name, null, e); } catch (InvalidKeyException e) { throw newRuntimeException(ENCRYPT_MSG, name, null, e); } catch (UnrecoverableKeyException e) { throw newRuntimeException(ENCRYPT_MSG, name, "key cannot be recovered from keystore", e); } catch (KeyStoreException e) { throw newRuntimeException(ENCRYPT_MSG, name, null, e); } catch (CertificateException e) { throw newRuntimeException(ENCRYPT_MSG, name, null, e); } catch (IllegalStateException e) { throw newRuntimeException(ENCRYPT_MSG, name, null, e); } catch (IllegalBlockSizeException e) { throw newRuntimeException(ENCRYPT_MSG, name, null, e); } catch (BadPaddingException e) { throw newRuntimeException(ENCRYPT_MSG, name, null, e); } } public static String decryptString(String cipherText) { return decryptString(GENERIC_PROP_NAME, cipherText); } public static String decryptString(String name, String cipherText) { // Not all providers support decrypting an empty byte array. if (cipherText.isEmpty()) { return ""; } try { SecretKey key = getSecretKey(); Cipher decryptor = Cipher.getInstance(keyStoreCryptoAlgo); decryptor.init(Cipher.DECRYPT_MODE, key); // Decode base64 to get bytes byte[] dec = Base64.decode(cipherText); // Decrypt byte[] utf8 = decryptor.doFinal(dec); // Decode using utf-8 return new String(utf8, Charsets.UTF_8); } catch (NoSuchAlgorithmException e) { throw newRuntimeException(DECRYPT_MSG, name, "provider does not have algorithm", e); } catch (IOException e) { throw newRuntimeException(DECRYPT_MSG, name, "I/O error", e); } catch (KeyStoreException e) { throw newRuntimeException(DECRYPT_MSG, name, null, e); } catch (CertificateException e) { throw newRuntimeException(DECRYPT_MSG, name, null, e); } catch (NoSuchPaddingException e) { throw newRuntimeException(DECRYPT_MSG, name, null, e); } catch (InvalidKeyException e) { throw newRuntimeException(DECRYPT_MSG, name, null, e); } catch (UnrecoverableKeyException e) { throw newRuntimeException(DECRYPT_MSG, name, "key cannot be recovered from keystore", e); } catch (IllegalStateException e) { throw newRuntimeException(DECRYPT_MSG, name, null, e); } catch (BadPaddingException e) { throw newRuntimeException(DECRYPT_MSG, name, "it might be unencrypted or encrypted with a different algorithm", e); } catch (IllegalBlockSizeException e) { throw newRuntimeException(DECRYPT_MSG, name, "it might be unencrypted or encrypted with a different algorithm", e); } catch (Base64DecoderException e) { throw newRuntimeException(DECRYPT_MSG, name, "it might not be encrypted at all", e); } } private static RuntimeException newRuntimeException(String prefix, String name, String suffix, Exception e) { String msg = prefix + name + ((suffix == null) ? "" : ( ": " + suffix)); return new RuntimeException(msg, e); } }