Hi developers,

I am very new to Tomcat but am already getting my feet wet with a web application. A requirement for this application is form-based password authentication, and I would like to store passwords in a database using salted SHA-512 digests, recognizing that this is not state-of-the-art password protection, but it is a more secure method than unsalted digests in the event that the password table is compromised.

I saw that Tomcat doesn't support this out of the box, so I am wondering if there is any interest in changing that. I wrote a custom Realm by extending DataSourceRealm and overriding the few necessary methods. This realm, which I've called SaltedDataSourceRealm, reads three database columns (username, salt, password). It doesn't assume a fixed-length salt, and it works with any supported digest algorithms. It took me quite a while to figure everything out because the documentation I found online isn't clear enough to a beginner, and some forum posts refer to very old versions of Tomcat. I would be willing to contribute an example of how to implement this custom realm to the Tomcat documentation if there is interest.

Anyway, I tested the custom realm and it seems to be working as intended. I went ahead and checked out the Tomcat 8 code and wrote the class where it would go, and I am attaching the java file of that class in case there is interest in considering an implementation like it in future versions of Tomcat. (I've hard-coded the length of the salt, but that should be changed to make it an argument of the XML file.)

After working a bit with the realms code I get the impression that it could be cleaned up a bit. I think it would be good to move several methods away from RealmBase so that no code there implements any specific authentication logic or makes assumptions about authentication. And I think it would be good for users to have the option of using salts with the other password-based realms too. I'm not sure that I have the experience required to do those changes, and I certainly won't work on it without first hearing back from you.

This is my first message to the Tomcat developer community. All your comments will be appreciated.

Regards,
Gabriel
package org.apache.catalina.realm;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.SecureRandom;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.bind.DatatypeConverter;
import org.apache.catalina.LifecycleException;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;

/**
 * Extension of the DataSourceRealm included in Apache Tomcat to handle salted
 * passwords for increased security.
 * 
 * @author gsanmar
 */
public class SaltedDataSourceRealm extends DataSourceRealm {
   
    
    // ----------------------------------------------------- Instance Variables
    
    
    // The column in the user table that holds the user's salt.
    protected String userSaltCol = null;
    
    // Descriptive information about this Realm implementation.
    protected static final String name = "SaltedDataSourceRealm";
    
    // The generated string for the salts PreparedStatement
    private String preparedSalt = null;
    
    // Logger
    private static final Log log = 
            LogFactory.getLog(SaltedDataSourceRealm.class);
        
    
    // ------------------------------------------------------------- Properties
    
    
    public String getUserSaltCol() {
        return userSaltCol;
    }

    public void setUserSaltCol(String userSaltCol) {
        this.userSaltCol = userSaltCol;
    }
    
    @Override
    protected String getName() {
        return name;
    }
        
    
    // --------------------------------------------------------- Public Methods
    
    
    // -------------------------------------------------------- Package Methods


    // ------------------------------------------------------ Protected Methods
    
    
    /**
     * Return the Principal associated with the specified username and
     * credentials, if there is one; otherwise return <code>null</code>.
     * 
     * This method differs from its counterpart in parent class DataSourceRealm
     * in that it gets the salt and passes it as an argument for validation.
     * 
     * @param dbConnection
     * @param username
     * @param credentials Password supplied by user, cleartext.
     * @return Principal associated with the specified username and
     * credentials, if there is one; otherwise <code>null</code>
     */
    @Override
    protected Principal authenticate(Connection dbConnection, String username, 
            String credentials) {
        String dbCredentials = getPassword(dbConnection, username);
        String dbSalt = getSalt(dbConnection, username);

        // Validate the user's credentials
        boolean validated = compareCredentials(credentials, dbSalt, 
                dbCredentials);

        if (validated) {
            if (containerLog.isTraceEnabled())
                containerLog.trace(
                    sm.getString("dataSourceRealm.authenticateSuccess",
                                 username));
        } else {
            if (containerLog.isTraceEnabled())
                containerLog.trace(
                    sm.getString("dataSourceRealm.authenticateFailure",
                                 username));
            return (null);
        }

        ArrayList<String> list = getRoles(dbConnection, username);

        // Create and return a suitable Principal for this user
        return new GenericPrincipal(username, credentials, list);
    }
        
    /**
     * Return the salt associated with the given username.
     * @param username
     * @return salt associated with the given username
     */
    protected String getSalt(String username) {
        Connection dbConnection = null;
        
        // Ensure that we have an open database connection
        dbConnection = open();
        if (dbConnection == null) {
            return null;
        }

        try {
            return getSalt(dbConnection, username);            
        } finally {
            close(dbConnection);
        }
    }
    
    /**
     * Return the salt associated with the given username.
     * @param dbConnection
     * @param username
     * @return salt associated with the given username
     */
    protected String getSalt(Connection dbConnection, String username) {

        ResultSet rs = null;
        PreparedStatement stmt = null;
        String dbSalt = null;

        try {
            stmt = salt(dbConnection, username);
            rs = stmt.executeQuery();
            if (rs.next()) {
                dbSalt = rs.getString(1);
            }

            return (dbSalt != null) ? dbSalt.trim() : null;
            
        } catch(SQLException e) {
            containerLog.error(
                    sm.getString("SaltedDataSourceRealm.getSalt.exception",
                                 username), e);
        } finally {
            try {
                if (rs != null) {
                    rs.close();
                }
                if (stmt != null) {
                    stmt.close();
                }
            } catch (SQLException e) {
                    containerLog.error(
                        sm.getString("SaltedDataSourceRealm.getSalt.exception",
                             username), e);
                
            }
        }
        
        return null;
    }
    
    /**
     * Parses salt and server digest hex strings into bytes, salts and digests 
     * the cleartext user password, and compares the resulting digest with the
     * server digest for equality.  This method is used instead of the one in
     * RealmBase because that one does not salt.
     * 
     * @param userPassword Password provided by user, cleartext
     * @param salt Salt added to password before digesting
     * @param serverHash Salted and digested password stored on the server
     * @return true if salted and digested passwords match, otherwise false
     */
    protected boolean compareCredentials(String userPassword, String salt, 
            String serverHash) {
        if (userPassword == null || salt == null || serverHash == null) {
            return false;
        }
                
        byte[] saltBytes = DatatypeConverter.parseHexBinary(salt);
        byte[] serverDigestBytes = DatatypeConverter.parseHexBinary(serverHash);
        
        // Digest user password
        byte[] userDigestBytes;
        synchronized (this) {
            md.reset();
            
            // User password
            md.update(userPassword.getBytes());
            
            // Add salt
            md.update(saltBytes);

            userDigestBytes = md.digest();
        }
            
        // TODO Constant time comparison to foil timing attacks.
        return Arrays.equals(userDigestBytes, serverDigestBytes);
    }
    
    
    // -------------------------------------------------------- Private Methods
    
    
    /**
     * Return a PreparedStatement configured to perform the SELECT required
     * to retrieve salt used before digesting the specified user's password.
     * 
     * @param dbConnection the database connection to be used
     * @param username username for which salt is retrieved
     * @return PreparedStatement configured to perform the SELECT required
     * to retrieve salt used before digesting the specified user's password
     * @throws SQLException 
     */
    private PreparedStatement salt(Connection dbConnection, String username) 
            throws SQLException {
        PreparedStatement salt = dbConnection.prepareStatement(preparedSalt);
        salt.setString(1, username);
        return salt;
    }

    
    // --------------------------------------------------------- Static Methods
    
    
    /**
     * Generate random salt, use it to salt and digest the user's password, 
     * and return an object containing the generated random salt and the 
     * digested password.  This method can be used to salt and digest password
     * in a way that can later be used for authentication.
     * 
     * @param credentials user's password, cleartext
     * @param algorithm algorithm used to digest
     * @param encoding character encoding of the string to digest
     * @return object containing the newly generated random salt and password
     * digest
     */
    public static SaltAndDigest saltAndDigest(String credentials, 
            String algorithm, String encoding) {
        
        // Number of bytes used to salt.
        // TODO This should be an argument specifyable in the xml config file.
        final int NBYTES = 64;
        
        // Generate random salt
        SecureRandom r = new SecureRandom();
        byte[] salt = new byte[NBYTES];
        r.nextBytes(salt);
        String saltString = DatatypeConverter.printHexBinary(salt);
        
        // Hash
        try {
            // Obtain a new message digest with "digest" encryption
            MessageDigest md =
                (MessageDigest) MessageDigest.getInstance(algorithm).clone();

            // encode the credentials
            // Should use the digestEncoding, but that's not a static field
            if (encoding == null) {
                md.update(credentials.getBytes());
                md.update(salt);
            } else {
                md.update(credentials.getBytes(encoding));     
                md.update(salt);
            }
            
            String digestString = DatatypeConverter.printHexBinary(md.digest());
            
            // Digest the credentials and return as hexadecimal
            return new SaltAndDigest(saltString, digestString);
            
        } catch(Exception ex) {
            log.error(ex);
            return null;
        }
    }
    
    /**
     * Class containing a salt and digest.
     */
    public static class SaltAndDigest {
        private final String salt;
        private final String digest;

        public SaltAndDigest(String salt, String digest) {
            this.salt = salt;
            this.digest = digest;
        }

        public String getSalt() {
            return salt;
        }

        public String getDigest() {
            return digest;
        }
        
    }
    
    
    // ------------------------------------------------------ Lifecycle Methods
    
    
    /**
     * Prepare for the beginning of active use of the public methods of this
     * component and implement the requirements of
     * {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
     * 
     * @exception LifecycleException if this component detects a fatal error
     *  that prevents this component from being used 
     */
    @Override
    protected void startInternal() throws LifecycleException {
        
        // Create the salt PreparedStatement string
        StringBuilder temp = new StringBuilder("SELECT ");
        temp.append(userSaltCol);
        temp.append(" FROM ");
        temp.append(userTable);
        temp.append(" WHERE ");
        temp.append(userNameCol);
        temp.append(" = ?");
        preparedSalt = temp.toString();
        
        super.startInternal();
    }
    
}

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to