Java MongoDB API - Custom Password Handling

115 views
Skip to first unread message

philip dicke

unread,
Jul 7, 2016, 7:46:49 PM7/7/16
to mongodb-user
We have a requirement to handle passwords as encrypted on disk and then only decrypt them in memory for the time they are used.  Think about retrieving a password from an encrypted storage (eg password manager) I'm having trouble implementing this without copying large amounts of code from com.mongodb.ConnectionString and seem to be blocked at every turn by the API.  We are still want users to be able to specify their own options, so we still want them to use the mongo:// URI for specifying all parameters.

Here is what I've come up with so far, comments in the code for what the holes are.  Before I post any bug reports to the MongoDB API, I'd like to see if anyone has any suggestions that I haven't seen.

package test

import java.io.Closeable;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
import com.whatever.Wallet;


import com.mongodb.MongoClientURI;
import com.mongodb.MongoCredential;

/*****************************************************************************
 * Class that implements the MongoClientURI class with specific requirements
 * for the XXX system, specifically supporting in-memory decryption of the
 * user name and password.  Note that this class will decrypt passwords and
 * store them in memory and {@link #close()} should be called to wipe those
 * passwords.
 * @author Philip J. Dicke
 *****************************************************************************/

public class MyMongoDbClientUri extends MongoClientURI implements Closeable {

   
private String myPassEncrypted;
   
private String myUserEncrypted;
   
private volatile char[] myDecryptedPass; // cached, leave as volatile so that
                                             
// shredding the array is not optimized out

   
/**
     * Creates a MyMongDbClientUri from system properties.  The following properties are read:
     * <ul>
     * <li>(db_prefix)_jdbc - property contains the (required) MongoDB URI, for example:
     *    <tt>mongodb://localhost/magedb</tt></li>
     * <li>(db_prefix)_username - property contains encrypted user name (default null)</li>
     * <li>(db_prefix)_password - property contains encrypted password (default null)</li>
     * </ul>
     * <b>IMPORTANT:</b> The caller of this function is now responsible for calling TapsMongoDbClientUri#close()
     * which will clear the decrypted passwords from memory.
     */

   
public static MyMongoDbClientUri getMongoClientUri(String uriStr, String name) {
       
MyMongoDbClientUri clientUri = new MyMongoDbClientUri(uriStr);
       
String userEncrypted = Wallet.getEncryptedUser(name);
       clientUri
.setUserEncrypted(userEncrypted);
       
String passEncrypted = Wallet.getEncryptedPassword(name);
       clientUri
.setPasswordEncrypted(passEncrypted);
       
return clientUri;
   
}
   

   
/**
     * Constructor
     * @param uri MongoDB client connection URI.  For example: mongdb://localhost/blah
     * @throws NullPointerException from MongoClientUri(String) if uri is null
     */

   
public MyMongoDbClientUri(String uri) {
       
super(uri);
   
}
   
   
/** Returns the decrypted password.  The reference is to the internally cached
     * password char[] so that users of this class don't have to worry about clearing
     * the array, only calling {@link #close()}
     */

   
@Override
   
public char[] getPassword() {
       
// These functions are never called from the MongoClient code, so need to implement getCredentials
       
if(myDecryptedPass != null) {
           
return myDecryptedPass; //we specifically want to return the internal array reference
                                   
//because that is the one that will be cleared in the close method
       
}
        myDecryptedPass
= Wallet.decrypt(myPassEncrypted);
       
return myDecryptedPass;
   
}
   
   
@Override
   
public String getUsername() {
       
if(myUserEncrypted == null) {
           
return myUserEncrypted;
       
}
       
// stuck as a String in memory, so might as well not cache it
       
return Wallet.decrypt(myUserEncrypted);
   
}
   
   
public void setPasswordEncrypted(String passEncrypted) {
        myPassEncrypted
= passEncrypted;
   
}

   
public void setUserEncrypted(String userEncrypted) {
        myUserEncrypted
= userEncrypted;
   
}
   
   
@Override
   
public MongoCredential getCredentials() {
       
MongoCredential existing = super.getCredentials();
       
       
// MongoCredential is a final class, so, I can't do what I want here is derive from (or proxy) MongoCredential
       
// and simply replace the username/password with what is in this class, that way all the
       
// mechanism properties are properly copied.  Also there is no mechanism to get a list of the
       
// specific authentication mechanism parameters to be able to copy them.  
       
// The getUserName() and getPassword() functions are
       
// never called from this class, so I have to implement this to replace them.
       
       
// IMPORTANT: LOTS OF Copied code (what I could and what I needed from) from ConnectionString#createCredentials()
       
// MISSING: authentication mechanism specific options - needs more copied code
       
Map<String, String> params = null;
       
try {
           
params = parseQueryString(getURI(), "UTF-8");
       
} catch (UnsupportedEncodingException e) {
           
// should never happen
           
return null;
       
}
       
MongoCredential newCred = null;
       
String authSource = (getDatabase() == null) ? "admin" : getDatabase();

       
String authSourceParam = params.get("authSource");
       
if(authSourceParam != null) {
            authSource
= authSourceParam;
       
}
       
// TODO parsing other parameters - needs more copied code

       
if(existing.getAuthenticationMechanism() == null) {
           
return MongoCredential.createCredential(getUsername(), authSource, getPassword());
       
}

       
switch(existing.getAuthenticationMechanism()) {
       
case GSSAPI:
            newCred
= MongoCredential.createGSSAPICredential(getUsername());
           
break;
       
case MONGODB_CR:
            newCred
= MongoCredential.createMongoCRCredential(getUsername(), authSource, getPassword());
           
break;
       
case MONGODB_X509:
            newCred
= MongoCredential.createMongoX509Credential(getUsername());
           
break;
       
case PLAIN:
            newCred
= MongoCredential.createPlainCredential(getUsername(), authSource, getPassword());
           
break;
       
case SCRAM_SHA_1:
            newCred
= MongoCredential.createScramSha1Credential(getUsername(), authSource, getPassword());
           
break;
       
default:
           
// this should never happen, but new versions might bring new methods
           
throw new UnsupportedOperationException(
                   
String.format("The connection string contains an invalid authentication mechanism'. "
                   
+ "'%s' is not a supported authentication mechanism", existing.getMechanism()));
       
}
       
// currently there isn't any way to copy the
       
return newCred;
   
}

   
/** Call in a finally block, wipes the plain text passwords from memory */
   
@Override
   
public void close() {
       
if(myDecryptedPass != null) {
           
Arrays.fill(myDecryptedPass, (char) 0x0);
       
}
   
}



   
private static Map<String, String> parseQueryString (final String uriString, String charSet)
           
throws UnsupportedEncodingException {
        URI uri
;
       
try {
            uri
= new URI(uriString);
       
} catch (URISyntaxException e) {
           
throw new IllegalArgumentException("Invalid URI " + uriString ,e);
       
}
       
final Map <String, String> qps = new HashMap<String, String> ();
       
final StringTokenizer pairs = new StringTokenizer (uri.getQuery (), "&");
       
while (pairs.hasMoreTokens ()) {
           
final String pair = pairs.nextToken ();
           
final StringTokenizer parts = new StringTokenizer (pair, "=");
           
final String name = URLDecoder.decode (parts.nextToken (), charSet);
           
final String value = URLDecoder.decode (parts.nextToken (), charSet);
            qps
.put (name, value);
       
}
       
return qps;
   
}


}




Ross Lawley

unread,
Jul 8, 2016, 6:15:51 AM7/8/16
to mongodb-user
Hi Philip,

There is no API for mixing / overriding the Mongo connection string, other than via the providing of defaults using:
MongoClientURI(final String uri, final MongoClientOptions.Builder builder) 

You should be able to use the supplied connection string and your own custom password handling to achieve what you need, but you will either have to manually create a credentialsList. 

For example using either: 

MongoClient(final ServerAddress addr, final List<MongoCredential> credentialsList, final MongoClientOptions options)
MongoClient(final List<ServerAddress> seeds, final List<MongoCredential> credentialsList, final MongoClientOptions options)

The first will connect to a single node and the second will connect to a replicaSet or to a sharded cluster.

MongoClientURI  userURI = MongoClientURI(userString);
userURI.getHosts() - returns a list of hosts
userURI.getOptions() - returns the MongoClientOptions.

Then you will need to handle the creation of the credentialsList based on your own logic / needs.  

The other alternative is to build a valid MongoClientURI with the injected username and password.

Ross

philip dicke

unread,
Jul 8, 2016, 7:54:31 PM7/8/16
to mongodb-user
Thanks Ross for the reply.  Manually creating the credential list is essentially what my code is doing now except in a different location and it requires significant copying of code from ConnectionString, which is what I was trying to avoid and without that it really limits the types of connections that we could support if we don't do that.  I'm attempting to avoid having to monitor all the code changes in the java Mongo Driver to make sure that we support the same features. 

Building a MongoClientURI with the injected username and password, will not satisfy my requirements as it creates a java String, which cannot be zero'd out of memory (since Java Strings are final) once the password has been used. 

I feel that if a few functions were added such as MongoCredential:: List<String> getMechanismPropertyList() and a public constructor that I could atleast copy the MongoCredential object and set my custom username/password on it.  Or instead, just provide a setUsername() and setPassword() functions

Anuj Kulkarni

unread,
May 1, 2017, 6:55:25 PM5/1/17
to mongodb-user
I think you can make use of 


to create the mongo client with the provided mongo server addresses and still use the MongoCredentials to create the credentials with the provided username and decrypted password.

Reply all
Reply to author
Forward
0 new messages