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.