diff --git a/core-java/src/main/java/com/baeldung/passwordhashing/PBKDF2Hasher.java b/core-java/src/main/java/com/baeldung/passwordhashing/PBKDF2Hasher.java new file mode 100644 index 0000000000..e2259e4249 --- /dev/null +++ b/core-java/src/main/java/com/baeldung/passwordhashing/PBKDF2Hasher.java @@ -0,0 +1,149 @@ +package com.baeldung.passwordhashing; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.Arrays; +import java.util.Base64; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; + +/** + * Hash passwords for storage, and test passwords against password tokens. + * + * Instances of this class can be used concurrently by multiple threads. + * + * @author erickson + * @see StackOverflow + */ +public final class PBKDF2Hasher +{ + + /** + * Each token produced by this class uses this identifier as a prefix. + */ + public static final String ID = "$31$"; + + /** + * The minimum recommended cost, used by default + */ + public static final int DEFAULT_COST = 16; + + private static final String ALGORITHM = "PBKDF2WithHmacSHA1"; + + private static final int SIZE = 128; + + private static final Pattern layout = Pattern.compile("\\$31\\$(\\d\\d?)\\$(.{43})"); + + private final SecureRandom random; + + private final int cost; + + public PBKDF2Hasher() + { + this(DEFAULT_COST); + } + + /** + * Create a password manager with a specified cost + * + * @param cost the exponential computational cost of hashing a password, 0 to 30 + */ + public PBKDF2Hasher(int cost) + { + iterations(cost); /* Validate cost */ + this.cost = cost; + this.random = new SecureRandom(); + } + + private static int iterations(int cost) + { + if ((cost < 0) || (cost > 30)) + throw new IllegalArgumentException("cost: " + cost); + return 1 << cost; + } + + /** + * Hash a password for storage. + * + * @return a secure authentication token to be stored for later authentication + */ + public String hash(char[] password) + { + byte[] salt = new byte[SIZE / 8]; + random.nextBytes(salt); + byte[] dk = pbkdf2(password, salt, 1 << cost); + byte[] hash = new byte[salt.length + dk.length]; + System.arraycopy(salt, 0, hash, 0, salt.length); + System.arraycopy(dk, 0, hash, salt.length, dk.length); + Base64.Encoder enc = Base64.getUrlEncoder().withoutPadding(); + return ID + cost + '$' + enc.encodeToString(hash); + } + + /** + * Authenticate with a password and a stored password token. + * + * @return true if the password and token match + */ + public boolean checkPassword(char[] password, String token) + { + Matcher m = layout.matcher(token); + if (!m.matches()) + throw new IllegalArgumentException("Invalid token format"); + int iterations = iterations(Integer.parseInt(m.group(1))); + byte[] hash = Base64.getUrlDecoder().decode(m.group(2)); + byte[] salt = Arrays.copyOfRange(hash, 0, SIZE / 8); + byte[] check = pbkdf2(password, salt, iterations); + int zero = 0; + for (int idx = 0; idx < check.length; ++idx) + zero |= hash[salt.length + idx] ^ check[idx]; + return zero == 0; + } + + private static byte[] pbkdf2(char[] password, byte[] salt, int iterations) + { + KeySpec spec = new PBEKeySpec(password, salt, iterations, SIZE); + try { + SecretKeyFactory f = SecretKeyFactory.getInstance(ALGORITHM); + return f.generateSecret(spec).getEncoded(); + } + catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException("Missing algorithm: " + ALGORITHM, ex); + } + catch (InvalidKeySpecException ex) { + throw new IllegalStateException("Invalid SecretKeyFactory", ex); + } + } + + /** + * Hash a password in an immutable {@code String}. + * + *
Passwords should be stored in a {@code char[]} so that it can be filled + * with zeros after use instead of lingering on the heap and elsewhere. + * + * @deprecated Use {@link #hash(char[])} instead + */ + @Deprecated + public String hash(String password) + { + return hash(password.toCharArray()); + } + + /** + * Authenticate with a password in an immutable {@code String} and a stored + * password token. + * + * @deprecated Use {@link #checkPassword(char[],String)} instead. + * @see #hash(String) + */ + @Deprecated + public boolean checkPassword(String password, String token) + { + return checkPassword(password.toCharArray(), token); + } + +} diff --git a/core-java/src/main/java/com/baeldung/passwordhashing/SHA512Hasher.java b/core-java/src/main/java/com/baeldung/passwordhashing/SHA512Hasher.java new file mode 100644 index 0000000000..4f5337f963 --- /dev/null +++ b/core-java/src/main/java/com/baeldung/passwordhashing/SHA512Hasher.java @@ -0,0 +1,35 @@ +package com.baeldung.passwordhashing; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + + +/** A really simple SHA_512 Encryption example. + * + */ +public class SHA512Hasher { + + public String hash(String passwordToHash, byte[] salt){ + String generatedPassword = null; + try { + MessageDigest md = MessageDigest.getInstance("SHA-512"); + md.update(salt); + byte[] bytes = md.digest(passwordToHash.getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + for(int i=0; i< bytes.length ;i++){ + sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1)); + } + generatedPassword = sb.toString(); + } + catch (NoSuchAlgorithmException e){ + e.printStackTrace(); + } + return generatedPassword; + } + + public boolean checkPassword(String hash, String attempt, byte[] salt){ + String generatedHash = hash(attempt, salt); + return hash.equals(generatedHash); + } +} diff --git a/core-java/src/main/java/com/baeldung/passwordhashing/SimplePBKDF2Hasher.java b/core-java/src/main/java/com/baeldung/passwordhashing/SimplePBKDF2Hasher.java new file mode 100644 index 0000000000..36c9b65070 --- /dev/null +++ b/core-java/src/main/java/com/baeldung/passwordhashing/SimplePBKDF2Hasher.java @@ -0,0 +1,18 @@ +package com.baeldung.passwordhashing; + +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import java.security.spec.KeySpec; + +/** A really simple SimplePBKDF2 Encryption example. + * + */ +public class SimplePBKDF2Hasher { + + public static String hashSimple(String password, byte[] salt) throws Exception{ + KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 65536, 128); + SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + byte[] hash = f.generateSecret(spec).getEncoded(); + return String.valueOf(hash); + } +} diff --git a/core-java/src/test/java/com/baeldung/passwordhashing/PBKDF2HasherUnitTest.java b/core-java/src/test/java/com/baeldung/passwordhashing/PBKDF2HasherUnitTest.java new file mode 100644 index 0000000000..8e90725c77 --- /dev/null +++ b/core-java/src/test/java/com/baeldung/passwordhashing/PBKDF2HasherUnitTest.java @@ -0,0 +1,41 @@ +package com.baeldung.passwordhashing; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + + +public class PBKDF2HasherUnitTest { + + private PBKDF2Hasher mPBKDF2Hasher; + + @Before + public void setUp() throws Exception { + mPBKDF2Hasher = new PBKDF2Hasher(); + } + + @Test + public void givenCorrectMessageAndHash_whenAuthenticated_checkAuthenticationSucceeds() throws Exception { + String message1 = "password123"; + + String hash1 = mPBKDF2Hasher.hash(message1.toCharArray()); + + assertTrue(mPBKDF2Hasher.checkPassword(message1.toCharArray(), hash1)); + + } + + @Test + public void givenWrongMessage_whenAuthenticated_checkAuthenticationFails() throws Exception { + String message1 = "password123"; + + String hash1 = mPBKDF2Hasher.hash(message1.toCharArray()); + + String wrongPasswordAttempt = "IamWrong"; + + assertFalse(mPBKDF2Hasher.checkPassword(wrongPasswordAttempt.toCharArray(), hash1)); + + } + + +} \ No newline at end of file diff --git a/core-java/src/test/java/com/baeldung/passwordhashing/SHA512HasherUnitTest.java b/core-java/src/test/java/com/baeldung/passwordhashing/SHA512HasherUnitTest.java new file mode 100644 index 0000000000..3acfb0ba9d --- /dev/null +++ b/core-java/src/test/java/com/baeldung/passwordhashing/SHA512HasherUnitTest.java @@ -0,0 +1,70 @@ +package com.baeldung.passwordhashing; + +import org.junit.Before; +import org.junit.Test; + +import java.security.SecureRandom; + +import static org.junit.Assert.*; + +/** + * Created by PhysicsSam on 06-Sep-18. + */ +public class SHA512HasherUnitTest { + + private SHA512Hasher hasher; + private SecureRandom secureRandom; + + @Before + public void setUp() throws Exception { + hasher = new SHA512Hasher(); + secureRandom = new SecureRandom(); + } + + @Test + public void givenSamePasswordAndSalt_whenHashed_checkResultingHashesAreEqual() throws Exception { + + byte[] salt = new byte[16]; + secureRandom.nextBytes(salt); + + String hash1 = hasher.hash("password", salt); + String hash2 = hasher.hash("password", salt); + + assertEquals(hash1, hash2); + + } + + @Test + public void givenSamePasswordAndDifferentSalt_whenHashed_checkResultingHashesNotEqual() throws Exception { + + byte[] salt = new byte[16]; + secureRandom.nextBytes(salt); + String hash1 = hasher.hash("password", salt); + //generate a second salt + byte[] secondSalt = new byte[16]; + String hash2 = hasher.hash("password", secondSalt); + + assertNotEquals(hash1, hash2); + + } + + @Test + public void givenPredefinedHash_whenCorrectAttemptGiven_checkAuthenticationSucceeds() throws Exception { + byte[] salt = new byte[16]; + secureRandom.nextBytes(salt); + + String originalHash = hasher.hash("password123", salt); + + assertTrue(hasher.checkPassword(originalHash, "password123", salt)); + } + + @Test + public void givenPredefinedHash_whenIncorrectAttemptGiven_checkAuthenticationFails() throws Exception { + byte[] salt = new byte[16]; + secureRandom.nextBytes(salt); + + String originalHash = hasher.hash("password123", salt); + + assertFalse(hasher.checkPassword(originalHash, "password124", salt)); + } +} \ No newline at end of file