Cleartext refers to text data that is not encrypted.
Any time an application stores a password as cleartext, its value is potentially exposed in a variety of ways. Obviously this exposure must be limited. While a program will receive the password from the user as cleartext, this should be the last time it is in this form. Hash functions allow programs to indirectly compare an input password to the original, without storing a cleartext or decryptable version of the password. This approach will therefore minimize the exposure of the password without presenting any practical disadvantages.
Cryptographic Hash Functions
The value that a hash function outputs is called the hash value. Another term for hash value is message digest. Hash functions are computationally feasible functions whose inverses are computationally infeasible. This means that in practice, one can encode a password to a hash value quickly, while they are also unable to decode it. The equality of the passwords can be tested through the equality of their hash values.
Java's MessageDigest class provides the functionality of various cryptographic hash functions. Be careful not to use any defective hash functions, such as MD5.
It is also important that you append a salt to the password you are hashing. A salt is a piece of data that is randomly generated during the creation of the program (and consistent throughout the rest of its implementation). The use of a salt helps prevents dictionary attacks against the hash value, provided the salt is long enough.
Noncompliant Code Example
class Password { public static void main(String[] args) throws IOException { char[] password = new char[100]; BufferedReader br = new BufferedReader(new InputStreamReader( new FileInputStream("credentials.txt"))); // Reads the password into the char array, returns the number of bytes read int n = br.read(password); // Decrypt password, perform operations for(int i = n - 1; i >= 0; i--) { // Manually clear out the password immediately after use password[i] = 0; } br.close(); } }
This code is incorrect because it decrypts the password stored in credentials.txt
. An attacker could potentially decrypt this file to find out what the password is. This attacker could be someone knows or has figured out the encryption scheme being used by the program.
Noncompliant Code Example
import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public final class HashExamples { private String salt = "ia0942980234241sadfaewvo32"; //Randomly generated private void setPassword(String pass) throws Exception { MessageDigest sha_1 = MessageDigest.getInstance("SHA-1"); byte[] hashVal = sha_1.digest((pass+salt).getBytes()); //encode the string and salt saveBytes(hashVal,"credentials.pw"); //save the hash value to credentials.pw } private boolean checkPassword(String pass) throws Exception { MessageDigest sha_1 = MessageDigest.getInstance("SHA-1"); byte[] hashVal1 = sha_1.digest((pass+salt).getBytes()); //encode the string and salt byte[] hashVal2 = loadBytes("credentials.pw"); //load the hash value stored in credentials.pw return Arrays.equals(hashVal1, hashVal2); } }
This code examples implements the SHA-1
hash function through the MessageDigest
class in order to compare hash values instead of cleartext strings. While this fixes the above decryption problem, at runtime this code may inadvertently store the passwords as cleartext. This is due to the fact that the pass
arguments may not be cleared from memory by the Java garbage collector. See "MSC10-J. Limit the lifetime of sensitive data", for more information.
Compliant Solution
import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public final class HashExamples { private byte[] salt = "ia0942980234241sadfaewvo32".getBytes(); //Randomly generated private void setPassword(byte[] pass) throws Exception { byte[] input = appendArrays(pass, salt); MessageDigest sha_1 = MessageDigest.getInstance("SHA-1"); byte[] hashVal = sha_1.digest(input); //encode the string and salt  clearArray(pass);  clearArray(input);  saveBytes(hashVal,"credentials.pw"); //save the hash value to credentials.pw } private boolean checkPassword(byte[] pass) throws Exception { byte[] input = appendArrays(pass, salt); MessageDigest sha_1 = MessageDigest.getInstance("SHA-1"); byte[] hashVal1 = sha_1.digest(input); //encode the string and salt clearArray(pass); clearArray(input); byte[] hashVal2 = loadBytes("credentials.pw"); //load the hash value stored in credentials.pw return Arrays.equals(hashVal1, hashVal2); } private appendArrays(byte[] a, byte[] b) { //Return a new array of a appended to b } private clearArray(byte[] a) { //set all of the elements in a to zero } }
This solution fixes the vulnerabilities in the previous noncompliant examples. In both setPassword
and checkPassword
, the cleartext representation of the password is erased as soon as it is converted into a hash value. After this happens, there is no way for an attacker to get the password as cleartext.
Exceptions
There are a few cases where you may be forced to encrypt passwords or store them as cleartext. These cases will happen when you are extending code or an application that you cannot change. For example, a password manager may need to input passwords into other programs as cleartext. Another example is if you are working with a library that gives you the password as a Java string
object, causing the same vulnerability as in the second noncompliant example. In these cases your best strategy may be to use slightly vulnerable methods such as encryption, unless you can change the other code.