The resource definition in tomcat\'s server.xml
looks something like this...
Tomcat needs to know how to connect to the database, so it needs access to the plain text password. If the password in encrypted, Tomcat needs to know how to decrypt it, so you are only moving the problem somewhere else.
The real problem is: who can access server.xml
except for Tomcat? A solution is to give read access to server.xml
only to root user, requiring that Tomcat is started with root privileges: if a malicious user gains root privileges on the system, losing a database password is probably a minor concern.
Otherwise you should type the password manually at every startup, but this is seldom a viable option.
Tomcat has a Password FAQ that specifically addresses your question. In short: Keep the password in the clear and properly lock-down your server.
That page also offers some suggestions of how security-by-obscurity might be used to pass an auditor's checklist.
After 4 hours of work, search questions and answers I got the solution. Based on the answer by @Jerome Delattre here is the complete code (with the JNDI Data source configuration).
Context.xml
<Resource
name="jdbc/myDataSource"
auth="Container"
type="javax.sql.DataSource"
username="user"
password="encryptedpassword"
driverClassName="driverClass"
factory="mypackage.MyCustomBasicDataSourceFactory"
url="jdbc:blabla://..."/>
Custom Data Source Factory:
package mypackage;
public class MyCustomBasicDataSourceFactory extends org.apache.tomcat.dbcp.dbcp.BasicDataSourceFactory {
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment) throws Exception {
Object o = super.getObjectInstance(obj, name, nameCtx, environment);
if (o != null) {
BasicDataSource ds = (BasicDataSource) o;
if (ds.getPassword() != null && ds.getPassword().length() > 0) {
String pwd = MyPasswordUtilClass.unscramblePassword(ds.getPassword());
ds.setPassword(pwd);
}
return ds;
} else {
return null;
}
}
}
Data source bean:
@Bean
public DataSource dataSource() {
DataSource ds = null;
JndiTemplate jndi = new JndiTemplate();
try {
ds = jndi.lookup("java:comp/env/jdbc/myDataSource", DataSource.class);
} catch (NamingException e) {
log.error("NamingException for java:comp/env/jdbc/myDataSource", e);
}
return ds;
}
Problem: As pointed out, encrypting credentials in context.xml while storing the decryption key in the next file over is literally only moving the problem. Since the user accessing context.xml will also need access to the decryption key, all credentials are still compromised if the application or the OS user is compromised.
Solution: The only solution that would add security is one that completely removes the decryption key from the entire setup. This could be achieved by requiring someone to type a password in your application on startup which is then used to decrypt all credentials.
Further deferral of solution: In most cases, such a password would likely need to be known by a number of administrators and/or developers. By using a password sharing solution that allows sharing of passwords (e.g. 1Password), the security is then deferred to each admin/dev's individual master password used to unlock his personal password vault.
Possible degradation of solution/sarcasm: With this setup, the worst case scenario would be that someone would simply keep their master password on a sticky note attached to a monitor. Whether that is more secure than having the decryption key in a file next to encrypted values should probably be a separate SO question or maybe a future study.
We use C#'s SHA1CryptoServiceProvider
print(SHA1CryptoServiceProvider sHA1Hasher = new SHA1CryptoServiceProvider();
ASCIIEncoding enc = new ASCIIEncoding();
byte[] arrbytHashValue = sHA1Hasher.ComputeHash(enc.GetBytes(clearTextPW));
string HashData = System.BitConverter.ToString(arrbytHashValue);
HashData = HashData.Replace("-", "");
if (HashData == databaseHashedPassWO)
{
return true;
}
else
{
return false;
});
)
As @Ryan mentioned, please read Tomcat's Tomcat Password FAQ before implementing this solution. You are only adding obscurity not security.
@Jerome Delattre's answer will work for simple JDBC data sources, but not for more complicated ones that connect as part of the datasource construction (e.g. oracle.jdbc.xa.client.OracleXADataSource).
This is alternative approach that modifies the password prior to calling the existing factory. Below is an example of a factory for a basic datasource and one for an Atomikos JTA compatible XA datasource.
Basic Example:
public class MyEncryptedPasswordFactory extends BasicDataSourceFactory {
@Override
public Object getObjectInstance(Object obj, Name name, Context context, Hashtable<?, ?> environment)
throws Exception {
if (obj instanceof Reference) {
Reference ref = (Reference) obj;
DecryptPasswordUtil.replacePasswordWithDecrypted(ref, "password");
return super.getObjectInstance(obj, name, context, environment);
} else {
throw new IllegalArgumentException(
"Expecting javax.naming.Reference as object type not " + obj.getClass().getName());
}
}
}
Atomikos Example:
public class MyEncryptedAtomikosPasswordFactory extends EnhancedTomcatAtomikosBeanFactory {
@Override
public Object getObjectInstance(Object obj, Name name, Context context, Hashtable<?, ?> environment)
throws NamingException {
if (obj instanceof Reference) {
Reference ref = (Reference) obj;
DecryptPasswordUtil.replacePasswordWithDecrypted(ref, "xaProperties.password");
return super.getObjectInstance(obj, name, context, environment);
} else {
throw new IllegalArgumentException(
"Expecting javax.naming.Reference as object type not " + obj.getClass().getName());
}
}
}
Updating password value in Reference:
public class DecryptPasswordUtil {
public static void replacePasswordWithDecrypted(Reference reference, String passwordKey) {
if(reference == null) {
throw new IllegalArgumentException("Reference object must not be null");
}
// Search for password addr and replace with decrypted
for (int i = 0; i < reference.size(); i++) {
RefAddr addr = reference.get(i);
if (passwordKey.equals(addr.getType())) {
if (addr.getContent() == null) {
throw new IllegalArgumentException("Password must not be null for key " + passwordKey);
}
String decrypted = yourDecryptionMethod(addr.getContent().toString());
reference.remove(i);
reference.add(i, new StringRefAddr(passwordKey, decrypted));
break;
}
}
}
}
Once the .jar file containing these classes are in Tomcat's classpath you can update your server.xml to use them.
<Resource factory="com.mycompany.MyEncryptedPasswordFactory" username="user" password="encryptedPassword" ...other options... />
<Resource factory="com.mycompany.MyEncryptedAtomikosPasswordFactory" type="com.atomikos.jdbc.AtomikosDataSourceBean" xaProperties.user="user" xaProperties.password="encryptedPassword" ...other options... />