diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index 7b8c5989cbc..ea9f86f4d13 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -172,6 +172,9 @@ New Features * SOLR-6968: New 'cardinality' option for stats.field, uses HyperLogLog to efficiently estimate the cardinality of a field w/bounded RAM. (hossman) +* SOLR-4392: Make it possible to specify AES encrypted password in dataconfig.xml (Noble Paul) + + Bug Fixes ---------------------- diff --git a/solr/contrib/dataimporthandler/src/java/org/apache/solr/handler/dataimport/JdbcDataSource.java b/solr/contrib/dataimporthandler/src/java/org/apache/solr/handler/dataimport/JdbcDataSource.java index cb836870573..2d93b10307c 100644 --- a/solr/contrib/dataimporthandler/src/java/org/apache/solr/handler/dataimport/JdbcDataSource.java +++ b/solr/contrib/dataimporthandler/src/java/org/apache/solr/handler/dataimport/JdbcDataSource.java @@ -16,17 +16,26 @@ */ package org.apache.solr.handler.dataimport; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.apache.solr.handler.dataimport.DataImportHandlerException.wrapAndThrow; import static org.apache.solr.handler.dataimport.DataImportHandlerException.SEVERE; +import org.apache.solr.common.SolrException; +import org.apache.solr.util.CryptoKeys; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.naming.InitialContext; import javax.naming.NamingException; +import java.io.FileInputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; import java.math.BigDecimal; import java.math.BigInteger; +import java.nio.charset.StandardCharsets; import java.sql.*; import java.util.*; import java.util.concurrent.Callable; @@ -61,6 +70,7 @@ public class JdbcDataSource extends @Override public void init(Context context, Properties initProps) { + initProps = decryptPwd(initProps); Object o = initProps.get(CONVERT_TYPE); if (o != null) convertType = Boolean.parseBoolean(o.toString()); @@ -101,6 +111,34 @@ public class JdbcDataSource extends } } + private Properties decryptPwd(Properties initProps) { + String encryptionKey = initProps.getProperty("encryptKeyFile"); + if (initProps.getProperty("password") != null && encryptionKey != null) { + // this means the password is encrypted and use the file to decode it + try { + try (Reader fr = new InputStreamReader(new FileInputStream(encryptionKey), UTF_8)) { + char[] chars = new char[100];//max 100 char password + int len = fr.read(chars); + if (len < 6) + throw new DataImportHandlerException(SEVERE, "There should be a password of length 6 atleast " + encryptionKey); + Properties props = new Properties(); + props.putAll(initProps); + String password = null; + try { + password = CryptoKeys.decodeAES(initProps.getProperty("password"), new String(chars, 0, len)).trim(); + } catch (SolrException se) { + throw new DataImportHandlerException(SEVERE, "Error decoding password", se.getCause()); + } + props.put("password", password); + initProps = props; + } + } catch (IOException e) { + throw new DataImportHandlerException(SEVERE, "Could not load encryptKeyFile " + encryptionKey); + } + } + return initProps; + } + protected Callable createConnectionFactory(final Context context, final Properties initProps) { // final VariableResolver resolver = context.getVariableResolver(); @@ -395,7 +433,7 @@ public class JdbcDataSource extends } } - private Connection getConnection() throws Exception { + Connection getConnection() throws Exception { long currTime = System.nanoTime(); if (currTime - connLastUsed > CONN_TIME_OUT) { synchronized (this) { diff --git a/solr/contrib/dataimporthandler/src/test/org/apache/solr/handler/dataimport/TestJdbcDataSource.java b/solr/contrib/dataimporthandler/src/test/org/apache/solr/handler/dataimport/TestJdbcDataSource.java index 8e2d5dd1658..50a116d4172 100644 --- a/solr/contrib/dataimporthandler/src/test/org/apache/solr/handler/dataimport/TestJdbcDataSource.java +++ b/solr/contrib/dataimporthandler/src/test/org/apache/solr/handler/dataimport/TestJdbcDataSource.java @@ -16,6 +16,9 @@ */ package org.apache.solr.handler.dataimport; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.sql.Connection; import java.sql.Driver; import java.sql.DriverManager; @@ -126,6 +129,36 @@ public class TestJdbcDataSource extends AbstractDataImportHandlerTestCase { assertSame("connection", conn, connection); } + @Test + public void testRetrieveFromJndiWithCredentialsWithEncryptedPwd() throws Exception { + MockInitialContextFactory.bind("java:comp/env/jdbc/JndiDB", dataSource); + File tmpdir = File.createTempFile("test", "tmp", createTempDir().toFile()); + Files.delete(tmpdir.toPath()); + tmpdir.mkdir(); + byte[] content = "secret".getBytes(StandardCharsets.UTF_8); + createFile(tmpdir, "enckeyfile.txt", content, false); + + props.put(JdbcDataSource.JNDI_NAME, "java:comp/env/jdbc/JndiDB"); + props.put("user", "Fred"); + props.put("encryptKeyFile", new File(tmpdir, "enckeyfile.txt").getAbsolutePath()); + props.put("password", "U2FsdGVkX18QMjY0yfCqlfBMvAB4d3XkwY96L7gfO2o="); + props.put("holdability", "HOLD_CURSORS_OVER_COMMIT"); + EasyMock.expect(dataSource.getConnection("Fred", "MyPassword")).andReturn( + connection); + jdbcDataSource.init(context, props); + + connection.setAutoCommit(false); + connection.setHoldability(1); + + mockControl.replay(); + + Connection conn = jdbcDataSource.getConnection(); + + mockControl.verify(); + + assertSame("connection", conn, connection); + } + @Test public void testRetrieveFromJndiFailureNotHidden() throws Exception { MockInitialContextFactory.bind("java:comp/env/jdbc/JndiDB", dataSource); diff --git a/solr/core/src/java/org/apache/solr/util/CryptoKeys.java b/solr/core/src/java/org/apache/solr/util/CryptoKeys.java index 73583396f24..847729340bb 100644 --- a/solr/core/src/java/org/apache/solr/util/CryptoKeys.java +++ b/solr/core/src/java/org/apache/solr/util/CryptoKeys.java @@ -17,17 +17,27 @@ package org.apache.solr.util; * limitations under the License. */ +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.security.GeneralSecurityException; import java.security.InvalidKeyException; import java.security.KeyFactory; +import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import org.apache.solr.common.SolrException; import org.apache.solr.common.util.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -107,5 +117,139 @@ public final class CryptoKeys { return false; } + private static byte[][] evpBytesTokey(int key_len, int iv_len, MessageDigest md, + byte[] salt, byte[] data, int count) { + byte[][] both = new byte[2][]; + byte[] key = new byte[key_len]; + int key_ix = 0; + byte[] iv = new byte[iv_len]; + int iv_ix = 0; + both[0] = key; + both[1] = iv; + byte[] md_buf = null; + int nkey = key_len; + int niv = iv_len; + int i = 0; + if (data == null) { + return both; + } + int addmd = 0; + for (; ; ) { + md.reset(); + if (addmd++ > 0) { + md.update(md_buf); + } + md.update(data); + if (null != salt) { + md.update(salt, 0, 8); + } + md_buf = md.digest(); + for (i = 1; i < count; i++) { + md.reset(); + md.update(md_buf); + md_buf = md.digest(); + } + i = 0; + if (nkey > 0) { + for (; ; ) { + if (nkey == 0) + break; + if (i == md_buf.length) + break; + key[key_ix++] = md_buf[i]; + nkey--; + i++; + } + } + if (niv > 0 && i != md_buf.length) { + for (; ; ) { + if (niv == 0) + break; + if (i == md_buf.length) + break; + iv[iv_ix++] = md_buf[i]; + niv--; + i++; + } + } + if (nkey == 0 && niv == 0) { + break; + } + } + for (i = 0; i < md_buf.length; i++) { + md_buf[i] = 0; + } + return both; + } + + public static String decodeAES(String base64CipherTxt, String pwd) { + int[] strengths = new int[]{256, 192, 128}; + Exception e = null; + for (int strength : strengths) { + try { + return decodeAES(base64CipherTxt, pwd, strength); + } catch (Exception exp) { + e = exp; + } + } + throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Error decoding ", e); + } + + + public static String decodeAES(String base64CipherTxt, String pwd, final int keySizeBits) { + final Charset ASCII = Charset.forName("ASCII"); + final int INDEX_KEY = 0; + final int INDEX_IV = 1; + final int ITERATIONS = 1; + final int SALT_OFFSET = 8; + final int SALT_SIZE = 8; + final int CIPHERTEXT_OFFSET = SALT_OFFSET + SALT_SIZE; + + try { + byte[] headerSaltAndCipherText = Base64.base64ToByteArray(base64CipherTxt); + + // --- extract salt & encrypted --- + // header is "Salted__", ASCII encoded, if salt is being used (the default) + byte[] salt = Arrays.copyOfRange( + headerSaltAndCipherText, SALT_OFFSET, SALT_OFFSET + SALT_SIZE); + byte[] encrypted = Arrays.copyOfRange( + headerSaltAndCipherText, CIPHERTEXT_OFFSET, headerSaltAndCipherText.length); + + // --- specify cipher and digest for evpBytesTokey method --- + + Cipher aesCBC = Cipher.getInstance("AES/CBC/PKCS5Padding"); + MessageDigest md5 = MessageDigest.getInstance("MD5"); + + // --- create key and IV --- + + // the IV is useless, OpenSSL might as well have use zero's + final byte[][] keyAndIV = evpBytesTokey( + keySizeBits / Byte.SIZE, + aesCBC.getBlockSize(), + md5, + salt, + pwd.getBytes(ASCII), + ITERATIONS); + + SecretKeySpec key = new SecretKeySpec(keyAndIV[INDEX_KEY], "AES"); + IvParameterSpec iv = new IvParameterSpec(keyAndIV[INDEX_IV]); + + // --- initialize cipher instance and decrypt --- + + aesCBC.init(Cipher.DECRYPT_MODE, key, iv); + byte[] decrypted = aesCBC.doFinal(encrypted); + return new String(decrypted, ASCII); + } catch (BadPaddingException e) { + // AKA "something went wrong" + throw new IllegalStateException( + "Bad password, algorithm, mode or padding;" + + " no salt, wrong number of iterations or corrupted ciphertext.", e); + } catch (IllegalBlockSizeException e) { + throw new IllegalStateException( + "Bad algorithm, mode or corrupted (resized) ciphertext.", e); + } catch (GeneralSecurityException e) { + throw new IllegalStateException(e); + } + } }