How do I generate a SALT in Java for Salted-Hash?
Inspired from this post and that post, I use this code to generate and verify hashed salted passwords. It only uses JDK provided classes, no external dependency.
The process is:
- you create a salt with
getNextSalt
- you ask the user his password and use the
hash
method to generate a salted and hashed password. The method returns abyte[]
which you can save as is in a database with the salt - to authenticate a user, you ask his password, retrieve the salt and hashed password from the database and use the
isExpectedPassword
method to check that the details match
/** * A utility class to hash passwords and check passwords vs hashed values. It uses a combination of hashing and unique * salt. The algorithm used is PBKDF2WithHmacSHA1 which, although not the best for hashing password (vs. bcrypt) is * still considered robust and <a href="https://security.stackexchange.com/a/6415/12614"> recommended by NIST </a>. * The hashed value has 256 bits. */public class Passwords { private static final Random RANDOM = new SecureRandom(); private static final int ITERATIONS = 10000; private static final int KEY_LENGTH = 256; /** * static utility class */ private Passwords() { } /** * Returns a random salt to be used to hash a password. * * @return a 16 bytes random salt */ public static byte[] getNextSalt() { byte[] salt = new byte[16]; RANDOM.nextBytes(salt); return salt; } /** * Returns a salted and hashed password using the provided hash.<br> * Note - side effect: the password is destroyed (the char[] is filled with zeros) * * @param password the password to be hashed * @param salt a 16 bytes salt, ideally obtained with the getNextSalt method * * @return the hashed password with a pinch of salt */ public static byte[] hash(char[] password, byte[] salt) { PBEKeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_LENGTH); Arrays.fill(password, Character.MIN_VALUE); try { SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); return skf.generateSecret(spec).getEncoded(); } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { throw new AssertionError("Error while hashing a password: " + e.getMessage(), e); } finally { spec.clearPassword(); } } /** * Returns true if the given password and salt match the hashed value, false otherwise.<br> * Note - side effect: the password is destroyed (the char[] is filled with zeros) * * @param password the password to check * @param salt the salt used to hash the password * @param expectedHash the expected hashed value of the password * * @return true if the given password and salt match the hashed value, false otherwise */ public static boolean isExpectedPassword(char[] password, byte[] salt, byte[] expectedHash) { byte[] pwdHash = hash(password, salt); Arrays.fill(password, Character.MIN_VALUE); if (pwdHash.length != expectedHash.length) return false; for (int i = 0; i < pwdHash.length; i++) { if (pwdHash[i] != expectedHash[i]) return false; } return true; } /** * Generates a random password of a given length, using letters and digits. * * @param length the length of the password * * @return a random password */ public static String generateRandomPassword(int length) { StringBuilder sb = new StringBuilder(length); for (int i = 0; i < length; i++) { int c = RANDOM.nextInt(62); if (c <= 9) { sb.append(String.valueOf(c)); } else if (c < 36) { sb.append((char) ('a' + c - 10)); } else { sb.append((char) ('A' + c - 36)); } } return sb.toString(); }}
You were right regarding how you want to generate salt i.e. its nothing but a random number. For this particular case it would protect your system from possible Dictionary attacks. Now, for the second problem what you could do is instead of using UTF-8 encoding you may want to use Base64. Here, is a sample for generating a hash. I am using Apache Common Codecs for doing the base64 encoding you may select one of your own
public byte[] generateSalt() { SecureRandom random = new SecureRandom(); byte bytes[] = new byte[20]; random.nextBytes(bytes); return bytes; }public String bytetoString(byte[] input) { return org.apache.commons.codec.binary.Base64.encodeBase64String(input); }public byte[] getHashWithSalt(String input, HashingTechqniue technique, byte[] salt) throws NoSuchAlgorithmException { MessageDigest digest = MessageDigest.getInstance(technique.value); digest.reset(); digest.update(salt); byte[] hashedBytes = digest.digest(stringToByte(input)); return hashedBytes; }public byte[] stringToByte(String input) { if (Base64.isBase64(input)) { return Base64.decodeBase64(input); } else { return Base64.encodeBase64(input.getBytes()); } }
Here is some additional reference of the standard practice in password hashing directly from OWASP
Another version using SHA-3, I am using bouncycastle:
The interface:
public interface IPasswords { /** * Generates a random salt. * * @return a byte array with a 64 byte length salt. */ byte[] getSalt64(); /** * Generates a random salt * * @return a byte array with a 32 byte length salt. */ byte[] getSalt32(); /** * Generates a new salt, minimum must be 32 bytes long, 64 bytes even better. * * @param size the size of the salt * @return a random salt. */ byte[] getSalt(final int size); /** * Generates a new hashed password * * @param password to be hashed * @param salt the randomly generated salt * @return a hashed password */ byte[] hash(final String password, final byte[] salt); /** * Expected password * * @param password to be verified * @param salt the generated salt (coming from database) * @param hash the generated hash (coming from database) * @return true if password matches, false otherwise */ boolean isExpectedPassword(final String password, final byte[] salt, final byte[] hash); /** * Generates a random password * * @param length desired password length * @return a random password */ String generateRandomPassword(final int length);}
The implementation:
import org.apache.commons.lang3.ArrayUtils;import org.apache.commons.lang3.Validate;import org.apache.log4j.Logger;import org.bouncycastle.jcajce.provider.digest.SHA3;import java.io.Serializable;import java.io.UnsupportedEncodingException;import java.security.SecureRandom;import java.util.ArrayList;import java.util.Arrays;import java.util.List;import java.util.Random;public final class Passwords implements IPasswords, Serializable { /*serialVersionUID*/ private static final long serialVersionUID = 8036397974428641579L; private static final Logger LOGGER = Logger.getLogger(Passwords.class); private static final Random RANDOM = new SecureRandom(); private static final int DEFAULT_SIZE = 64; private static final char[] symbols; static { final StringBuilder tmp = new StringBuilder(); for (char ch = '0'; ch <= '9'; ++ch) { tmp.append(ch); } for (char ch = 'a'; ch <= 'z'; ++ch) { tmp.append(ch); } symbols = tmp.toString().toCharArray(); } @Override public byte[] getSalt64() { return getSalt(DEFAULT_SIZE); } @Override public byte[] getSalt32() { return getSalt(32); } @Override public byte[] getSalt(int size) { final byte[] salt; if (size < 32) { final String message = String.format("Size < 32, using default of: %d", DEFAULT_SIZE); LOGGER.warn(message); salt = new byte[DEFAULT_SIZE]; } else { salt = new byte[size]; } RANDOM.nextBytes(salt); return salt; } @Override public byte[] hash(String password, byte[] salt) { Validate.notNull(password, "Password must not be null"); Validate.notNull(salt, "Salt must not be null"); try { final byte[] passwordBytes = password.getBytes("UTF-8"); final byte[] all = ArrayUtils.addAll(passwordBytes, salt); SHA3.DigestSHA3 md = new SHA3.Digest512(); md.update(all); return md.digest(); } catch (UnsupportedEncodingException e) { final String message = String .format("Caught UnsupportedEncodingException e: <%s>", e.getMessage()); LOGGER.error(message); } return new byte[0]; } @Override public boolean isExpectedPassword(final String password, final byte[] salt, final byte[] hash) { Validate.notNull(password, "Password must not be null"); Validate.notNull(salt, "Salt must not be null"); Validate.notNull(hash, "Hash must not be null"); try { final byte[] passwordBytes = password.getBytes("UTF-8"); final byte[] all = ArrayUtils.addAll(passwordBytes, salt); SHA3.DigestSHA3 md = new SHA3.Digest512(); md.update(all); final byte[] digest = md.digest(); return Arrays.equals(digest, hash); }catch(UnsupportedEncodingException e){ final String message = String.format("Caught UnsupportedEncodingException e: <%s>", e.getMessage()); LOGGER.error(message); } return false; } @Override public String generateRandomPassword(final int length) { if (length < 1) { throw new IllegalArgumentException("length must be greater than 0"); } final char[] buf = new char[length]; for (int idx = 0; idx < buf.length; ++idx) { buf[idx] = symbols[RANDOM.nextInt(symbols.length)]; } return shuffle(new String(buf)); } private String shuffle(final String input){ final List<Character> characters = new ArrayList<Character>(); for(char c:input.toCharArray()){ characters.add(c); } final StringBuilder output = new StringBuilder(input.length()); while(characters.size()!=0){ int randPicker = (int)(Math.random()*characters.size()); output.append(characters.remove(randPicker)); } return output.toString(); }}
The test cases:
public class PasswordsTest { private static final Logger LOGGER = Logger.getLogger(PasswordsTest.class); @Before public void setup(){ BasicConfigurator.configure(); } @Test public void testGeSalt() throws Exception { IPasswords passwords = new Passwords(); final byte[] bytes = passwords.getSalt(0); int arrayLength = bytes.length; assertThat("Expected length is", arrayLength, is(64)); } @Test public void testGeSalt32() throws Exception { IPasswords passwords = new Passwords(); final byte[] bytes = passwords.getSalt32(); int arrayLength = bytes.length; assertThat("Expected length is", arrayLength, is(32)); } @Test public void testGeSalt64() throws Exception { IPasswords passwords = new Passwords(); final byte[] bytes = passwords.getSalt64(); int arrayLength = bytes.length; assertThat("Expected length is", arrayLength, is(64)); } @Test public void testHash() throws Exception { IPasswords passwords = new Passwords(); final byte[] hash = passwords.hash("holacomoestas", passwords.getSalt64()); assertThat("Array is not null", hash, Matchers.notNullValue()); } @Test public void testSHA3() throws UnsupportedEncodingException { SHA3.DigestSHA3 md = new SHA3.Digest256(); md.update("holasa".getBytes("UTF-8")); final byte[] digest = md.digest(); assertThat("expected digest is:",digest,Matchers.notNullValue()); } @Test public void testIsExpectedPasswordIncorrect() throws Exception { String password = "givemebeer"; IPasswords passwords = new Passwords(); final byte[] salt64 = passwords.getSalt64(); final byte[] hash = passwords.hash(password, salt64); //The salt and the hash go to database. final boolean isPasswordCorrect = passwords.isExpectedPassword("jfjdsjfsd", salt64, hash); assertThat("Password is not correct", isPasswordCorrect, is(false)); } @Test public void testIsExpectedPasswordCorrect() throws Exception { String password = "givemebeer"; IPasswords passwords = new Passwords(); final byte[] salt64 = passwords.getSalt64(); final byte[] hash = passwords.hash(password, salt64); //The salt and the hash go to database. final boolean isPasswordCorrect = passwords.isExpectedPassword("givemebeer", salt64, hash); assertThat("Password is correct", isPasswordCorrect, is(true)); } @Test public void testGenerateRandomPassword() throws Exception { IPasswords passwords = new Passwords(); final String randomPassword = passwords.generateRandomPassword(10); LOGGER.info(randomPassword); assertThat("Random password is not null", randomPassword, Matchers.notNullValue()); }}
pom.xml (only dependencies):
<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>6.1.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-all</artifactId> <version>1.3</version> <scope>test</scope> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.51</version> <type>jar</type> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.3.2</version> </dependency></dependencies>