22.3 Hashing, Message Digests, and Password Security
This section covers cryptographic hash functions, message authentication codes, and secure password hashing with practical implementations.
Cryptographic Hash Functions
Hash functions produce a fixed-size fingerprint of data. Critical properties include determinism, avalanche effect, and collision resistance.
// Cryptographic Hash Functions (One-way)
public class CryptographicHashing {
// SHA-256 hashing
public static String hashWithSHA256(String input) throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = digest.digest(input.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(hashBytes);
}
// SHA-512 hashing
public static String hashWithSHA512(String input) throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-512");
byte[] hashBytes = digest.digest(input.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(hashBytes);
}
// Demonstrate hash properties
public static void demonstrateHashProperties() throws NoSuchAlgorithmException {
String input = "Hello, World!";
String hash1 = hashWithSHA256(input);
String hash2 = hashWithSHA256(input);
System.out.println("Input: " + input);
System.out.println("Hash (first): " + hash1);
System.out.println("Hash (second): " + hash2);
System.out.println("Deterministic: " + hash1.equals(hash2)); // Always true
// Avalanche effect: Small input change = big hash change
String input2 = "Hello, World?"; // Different by 1 character
String hash3 = hashWithSHA256(input2);
System.out.println("\nInput 2: " + input2);
System.out.println("Hash: " + hash3);
System.out.println("Hash similarity: " + (hash1.equals(hash3) ? "0%" : "Different");
}
// Hashing file contents
public static String hashFile(String filePath) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (InputStream is = new FileInputStream(filePath)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
digest.update(buffer, 0, bytesRead);
}
}
byte[] hashBytes = digest.digest();
return HexFormat.of().formatHex(hashBytes);
}
// Hash algorithm comparison
public static void compareHashAlgorithms(String input) throws NoSuchAlgorithmException {
System.out.println("Input: " + input);
System.out.println("\nHash Algorithm Comparison:");
String[] algorithms = {"MD5", "SHA-1", "SHA-256", "SHA-512"};
for (String algo : algorithms) {
try {
MessageDigest digest = MessageDigest.getInstance(algo);
byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
System.out.printf("%s (%d bits): %s\n",
algo, hash.length * 8, HexFormat.of().formatHex(hash));
} catch (NoSuchAlgorithmException e) {
System.out.println(algo + ": Not available");
}
}
}
}
Hash Algorithm Deprecation
// Algorithm Deprecation Status
public class HashAlgorithmDeprecation {
public static void printAlgorithmStatus() {
System.out.println("=== HASH ALGORITHM STATUS ===");
System.out.println("\nSHA-1 (160-bit):");
System.out.println(" Status: DEPRECATED for cryptographic use");
System.out.println(" Reason: Collision vulnerabilities found (SHAttered attack)");
System.out.println(" Use: Only for legacy systems");
System.out.println(" Alternative: Use SHA-256");
System.out.println("\nMD5 (128-bit):");
System.out.println(" Status: BROKEN, not cryptographically secure");
System.out.println(" Reason: Practical collision attacks exist");
System.out.println(" Use: NEVER for security, only checksums");
System.out.println(" Alternative: Use SHA-256");
System.out.println("\nSHA-256 (256-bit):");
System.out.println(" Status: RECOMMENDED");
System.out.println(" Security: Secure against all known attacks");
System.out.println(" Use: General purpose hashing");
System.out.println("\nSHA-512 (512-bit):");
System.out.println(" Status: RECOMMENDED");
System.out.println(" Security: Very high security");
System.out.println(" Use: When higher security needed");
System.out.println("\nSHA-3 (variable):");
System.out.println(" Status: RECOMMENDED for new systems");
System.out.println(" Security: Modern design, not SHA-2 vulnerable");
System.out.println(" Use: When available");
}
// Check if algorithm is deprecated
public static boolean isAlgorithmDeprecated(String algorithm) {
return algorithm.equalsIgnoreCase("MD5") ||
algorithm.equalsIgnoreCase("SHA-1");
}
}
Message Authentication Codes (MAC)
MAC provides both integrity and authenticity verification:
// Message Authentication Code (MAC/HMAC)
public class MessageAuthenticationCode {
// Generate HMAC with given key
public static String generateHMAC(String message, SecretKey key, String algorithm)
throws Exception {
Mac mac = Mac.getInstance(algorithm);
mac.init(key);
byte[] hmacBytes = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(hmacBytes);
}
// HMAC-SHA256 example (most common)
public static class HMACSHA256Example {
public static SecretKey generateKey() throws NoSuchAlgorithmException {
KeyGenerator keyGen = KeyGenerator.getInstance("HmacSHA256");
keyGen.init(256, new SecureRandom());
return keyGen.generateKey();
}
public static byte[] computeHMAC(String message, SecretKey key)
throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(key);
return mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
}
public static void demonstrateHMAC() throws Exception {
SecretKey key = generateKey();
String message = "Important data";
// Compute HMAC
byte[] hmac = computeHMAC(message, key);
System.out.println("Message: " + message);
System.out.println("HMAC: " + HexFormat.of().formatHex(hmac));
// Verify HMAC (same message and key produce same HMAC)
byte[] hmac2 = computeHMAC(message, key);
System.out.println("Verification: " + java.util.Arrays.equals(hmac, hmac2));
}
}
// Message-authentication pattern
public static class MessageWithAuthentication {
public byte[] message;
public byte[] authenticationTag;
public MessageWithAuthentication(String message, SecretKey key)
throws Exception {
this.message = message.getBytes(StandardCharsets.UTF_8);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(key);
this.authenticationTag = mac.doFinal(this.message);
}
public boolean verifyAuthentication(SecretKey key) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(key);
byte[] computedTag = mac.doFinal(this.message);
// Use constant-time comparison
return java.util.Arrays.equals(this.authenticationTag, computedTag);
}
}
// CMAC for block cipher-based authentication
public static byte[] generateCMAC(String message, SecretKey key)
throws Exception {
// Note: CMAC typically requires BouncyCastle provider
Mac mac = Mac.getInstance("CMAC", "BC");
mac.init(key);
return mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
}
}
Secure Password Hashing
Password hashing is fundamentally different from data hashing. Passwords need extra protections:
// Secure Password Hashing
public class SecurePasswordHashing {
// PBKDF2 password hashing (built-in Java)
public static class PBKDF2PasswordHashing {
private static final String ALGORITHM = "PBKDF2WithHmacSHA256";
private static final int ITERATIONS = 120000; // NIST recommendation
private static final int KEY_LENGTH = 256; // bits
private static final int SALT_LENGTH = 16; // bytes
// Generate random salt
private static byte[] generateSalt() {
byte[] salt = new byte[SALT_LENGTH];
new SecureRandom().nextBytes(salt);
return salt;
}
// Hash password with generated salt
public static String hashPassword(String password) throws Exception {
byte[] salt = generateSalt();
return hashPassword(password, salt);
}
// Hash password with provided salt
public static String hashPassword(String password, byte[] salt)
throws Exception {
PBEKeySpec spec = new PBEKeySpec(
password.toCharArray(),
salt,
ITERATIONS,
KEY_LENGTH
);
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance(ALGORITHM);
byte[] hash = factory.generateSecret(spec).getEncoded();
// Combine salt and hash for storage
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write(salt);
baos.write(hash);
byte[] combined = baos.toByteArray();
// Return Base64 encoded for easy storage/transmission
return java.util.Base64.getEncoder().encodeToString(combined);
} finally {
spec.clearPassword();
}
}
// Verify password against stored hash
public static boolean verifyPassword(String password, String storedHash)
throws Exception {
byte[] combined = java.util.Base64.getDecoder().decode(storedHash);
// Extract salt from stored hash
byte[] salt = new byte[SALT_LENGTH];
System.arraycopy(combined, 0, salt, 0, SALT_LENGTH);
// Hash provided password with stored salt
String newHash = hashPassword(password, salt);
// Compare using constant-time comparison
byte[] newHashBytes = java.util.Base64.getDecoder().decode(newHash);
return java.util.Arrays.equals(
combined,
java.util.Base64.getDecoder().decode(newHash)
);
}
public static void demonstratePBKDF2() throws Exception {
String password = "MySecurePassword123!";
// Hash password
String hashedPassword = hashPassword(password);
System.out.println("Hashed password: " + hashedPassword);
// Verify correct password
boolean correct = verifyPassword(password, hashedPassword);
System.out.println("Correct password verified: " + correct);
// Verify incorrect password
boolean incorrect = verifyPassword("WrongPassword", hashedPassword);
System.out.println("Wrong password rejected: " + !incorrect);
}
}
// Bcrypt password hashing (requires Spring Security or jbcrypt)
// More resistant to GPU attacks than PBKDF2
public static class BcryptPasswordHashing {
// Note: Requires dependency: org.springframework.security:spring-security-crypto
// Or: de.mkammerer:argon2-jvm
public static void demonstrateBcrypt() {
System.out.println("Bcrypt requires external library:");
System.out.println(" - org.springframework.security:spring-security-crypto");
System.out.println(" - de.mkammerer:argon2-jvm for Argon2");
}
}
// Password hashing best practices
public static class PasswordHashingBestPractices {
public static void printBestPractices() {
System.out.println("=== PASSWORD HASHING BEST PRACTICES ===");
System.out.println("\n1. Use PBKDF2, bcrypt, scrypt, or Argon2");
System.out.println(" - NOT simple SHA-256 or SHA-512");
System.out.println(" - These are intentionally SLOW");
System.out.println("\n2. Use unique salt for each password");
System.out.println(" - Prevents rainbow table attacks");
System.out.println(" - Salt should be >= 128 bits");
System.out.println("\n3. Use high iteration count");
System.out.println(" - PBKDF2: >= 120,000 iterations");
System.out.println(" - Increases attack cost");
System.out.println(" - Update over time as computers get faster");
System.out.println("\n4. Never use MD5 for passwords");
System.out.println(" - Rainbow tables exist for common passwords");
System.out.println(" - Collision attacks possible");
System.out.println("\n5. Implement rate limiting");
System.out.println(" - Limit login attempts");
System.out.println(" - Slow down brute force attacks");
System.out.println("\n6. Use constant-time comparison");
System.out.println(" - Prevents timing attacks");
System.out.println(" - Arrays.equals() is NOT constant-time");
System.out.println("\n7. Store only hash, never plaintext");
System.out.println(" - Database breach doesn't expose passwords");
System.out.println(" - Passwords cannot be recovered");
}
}
}
Constant-Time Comparison
Prevent timing attacks when comparing hashes:
// Constant-Time Comparison
public class ConstantTimeComparison {
// Safe comparison for authentication codes
public static boolean constantTimeEquals(byte[] a, byte[] b) {
if (a.length != b.length) {
return false;
}
int result = 0;
for (int i = 0; i < a.length; i++) {
result |= a[i] ^ b[i];
}
return result == 0;
}
// Demonstrate timing attack vulnerability
public static void demonstrateTimingAttack() throws Exception {
// VULNERABLE: Early return reveals information
public static boolean vulnerableEquals(String provided, String expected) {
for (int i = 0; i < Math.min(provided.length(), expected.length()); i++) {
if (provided.charAt(i) != expected.charAt(i)) {
return false; // Returns early - timing reveals position
}
}
return provided.length() == expected.length();
}
// CORRECT: Constant time
public static boolean secureEquals(String provided, String expected) {
byte[] a = provided.getBytes(StandardCharsets.UTF_8);
byte[] b = expected.getBytes(StandardCharsets.UTF_8);
int result = 0;
int length = Math.max(a.length, b.length);
for (int i = 0; i < length; i++) {
byte aVal = (i < a.length) ? a[i] : 0;
byte bVal = (i < b.length) ? b[i] : 0;
result |= aVal ^ bVal;
}
return result == 0 && a.length == b.length;
}
}
// Using MessageDigest.isEqual() (Java 6+)
public static boolean secureEqualsBuiltin(byte[] digest1, byte[] digest2) {
return MessageDigest.isEqual(digest1, digest2);
}
}
Salting and Key Derivation
// Salt and Key Derivation Functions
public class SaltingAndKeyDerivation {
// Generate random salt
public static byte[] generateSalt(int lengthInBytes) {
byte[] salt = new byte[lengthInBytes];
new SecureRandom().nextBytes(salt);
return salt;
}
// Key derivation with salt
public static byte[] deriveKey(String password, byte[] salt, int keyLengthBits)
throws Exception {
PBEKeySpec spec = new PBEKeySpec(
password.toCharArray(),
salt,
100000, // iterations
keyLengthBits
);
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance(
"PBKDF2WithHmacSHA256");
return factory.generateSecret(spec).getEncoded();
} finally {
spec.clearPassword();
}
}
// Password-based key derivation with salt
public static class PasswordBasedKeyDerivation {
private static final byte[] FIXED_SALT =
new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
// WRONG: Using fixed salt
public static byte[] deriveWithFixedSalt(String password)
throws Exception {
return deriveKey(password, FIXED_SALT, 256);
}
// CORRECT: Generate unique salt
public static class DerivedKeyWithSalt {
public byte[] key;
public byte[] salt;
public DerivedKeyWithSalt(String password) throws Exception {
this.salt = generateSalt(16);
this.key = deriveKey(password, salt, 256);
}
}
}
}
Hashing for Data Integrity
// Hashing for Data Integrity Verification
public class DataIntegrityHashing {
// Structure for data with integrity hash
public static class IntegrityVerifiedData {
public byte[] data;
public byte[] hash;
public IntegrityVerifiedData(byte[] data) throws NoSuchAlgorithmException {
this.data = data;
this.hash = computeHash(data);
}
public boolean verifyIntegrity() throws NoSuchAlgorithmException {
byte[] currentHash = computeHash(this.data);
return MessageDigest.isEqual(currentHash, this.hash);
}
}
// Compute hash of data
private static byte[] computeHash(byte[] data) throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return digest.digest(data);
}
// Batch file verification
public static class FileIntegrityValidator {
private final Map<String, String> fileHashes = new HashMap<>();
public void addFile(String filepath) throws Exception {
String hash = hashFile(filepath);
fileHashes.put(filepath, hash);
}
public boolean verifyFile(String filepath) throws Exception {
String storedHash = fileHashes.get(filepath);
if (storedHash == null) {
return false;
}
String currentHash = hashFile(filepath);
return storedHash.equals(currentHash);
}
private String hashFile(String filepath) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (InputStream is = new FileInputStream(filepath)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
digest.update(buffer, 0, bytesRead);
}
}
return HexFormat.of().formatHex(digest.digest());
}
}
}
Best Practices for Hashing
- Use SHA-256 or stronger: For general hashing, never use MD5 or SHA-1.
- Use dedicated password hashing: PBKDF2, bcrypt, scrypt, or Argon2 for passwords.
- Generate unique salts: At least 128 bits of random data per password.
- Use high iteration counts: PBKDF2 at least 120,000 iterations (NIST 2023).
- Implement constant-time comparison: Use
MessageDigest.isEqual()or custom implementation. - Never store plaintext: Hash passwords immediately upon receipt.
- Use secure random for salts: Never use predictable or hardcoded salts.
- Verify integrity only: Hash is one-way; cannot recover original data.
- Update iterations over time: Increase as computing power increases.
- Use dedicated libraries: Don't implement password hashing yourself.