Has MD5 Encryption Been Cracked Again? 5 Practical Tips to Reassess Hash Algorithms!

Hello everyone, today we are going to talk about a topic that countless backend developers both love and hate—MD5 encryption.

Have you encountered these scenarios:

  • The interviewer asks: “Is MD5 an encryption algorithm?” You immediately reply “yes,” and then get criticized thoroughly.
  • You store user passwords using MD5, only to have them cracked in seconds by a rainbow table, leading to a complete data leak.
  • You performed an MD5 check, but the file transfer still failed, and after searching for a long time, you discovered the MD5 collision issue.
  • Your boss asks you to “encrypt” sensitive data, and you use MD5, only to find out that it cannot be decrypted at all.

I once worked at an internet company where my insufficient understanding of MD5 led to user passwords being brute-forced, and I almost got fired. After in-depth study and practice, I summarized the correct usage of MD5, and today I will share it all!

Let’s help you thoroughly understand what MD5 really is!

1. What is MD5? Stop calling it “encryption!”

First, let’s correct a huge misconception:MD5 is not an encryption algorithm, but a hash algorithm (digest algorithm)!

1. Confused between encryption and hashing?

Encryption algorithms:

  • Can encrypt and decrypt
  • Involves the concept of keys
  • The purpose is to protect data from being seen
  • Examples: AES, RSA, DES

Hash algorithms:

  • Can only compute one way, cannot be reversed
  • No concept of keys
  • The purpose is to generate a “fingerprint” of the data
  • Examples: MD5, SHA-1, SHA-256
// Incorrect example: using MD5 as encryption
public class WrongExample {
    public static void main(String[] args) {
        String password = "123456";
        String encrypted = MD5Util.encrypt(password);  // ❌ Incorrect! MD5 is not encryption
        System.out.println("Encrypted: " + encrypted);
        
        // Want to decrypt? Not possible!
        // String decrypted = MD5Util.decrypt(encrypted);  // ❌ This method does not exist
    }
}

// Correct example: MD5 is a hash
public class CorrectExample {
    public static void main(String[] args) {
        String password = "123456";
        String hash = MD5Util.hash(password);  // ✅ Correct! Generates hash value
        System.out.println("Hash value: " + hash);  // e10adc3949ba59abbe56e057f20f883e
        
        // Verify password
        String inputPassword = "123456";
        boolean isValid = MD5Util.hash(inputPassword).equals(hash);  // ✅ This is how to verify
        System.out.println("Password correct: " + isValid);
    }
}

2. Core characteristics of MD5

Fixed length output:

  • No matter how long the input, MD5 always outputs 128 bits (32 hexadecimal characters)
  • “a” → 0cc175b9c0f1b6a831c399e269772661
  • “A very long piece of text…” → also 32 bits

One-way and irreversible:

  • MD5(“123456”) = “e10adc3949ba59abbe56e057f20f883e”
  • But you can never reverse “e10adc3949ba59abbe56e057f20f883e” back to “123456”

Avalanche effect:

  • A slight change in input results in a completely different output
  • MD5(“123456”) = “e10adc3949ba59abbe56e057f20f883e”
  • MD5(“123457”) = “fcea920f7412b5da7be0cf42b8c93759”

2. The principle of the MD5 algorithm, helping you thoroughly understand the internal mechanism

1. The 4 steps of the MD5 algorithm

public class MD5Algorithm {
    
    /**
     * The 4 core steps of the MD5 algorithm
     */
    public String md5(String input) {
        // Step 1: Pad the message
        byte[] paddedMessage = padMessage(input.getBytes());
        
        // Step 2: Initialize MD buffer
        int[] md = initializeMD();
        
        // Step 3: Process message blocks
        processMessageBlocks(paddedMessage, md);
        
        // Step 4: Output result
        return formatOutput(md);
    }
    
    /**
     * Step 1: Message padding
     * Goal: Make message length ≡ 448 (mod 512)
     */
    private byte[] padMessage(byte[] message) {
        int originalLength = message.length;
        int bitLength = originalLength * 8;
        
        // Calculate the length to be padded
        int paddingLength = (448 - (bitLength % 512) + 512) % 512;
        if (paddingLength == 0) paddingLength = 512;
        
        // Create padded message
        int totalLength = originalLength + (paddingLength / 8) + 8;
        byte[] paddedMessage = new byte[totalLength];
        
        // Copy original message
        System.arraycopy(message, 0, paddedMessage, 0, originalLength);
        
        // Add 1 bit of '1' and several bits of '0' (simplified)
        paddedMessage[originalLength] = (byte) 0x80;  // 10000000
        
        // Add original length (64 bits)
        for (int i = 0; i < 8; i++) {
            paddedMessage[totalLength - 8 + i] = (byte) (bitLength >>> (i * 8));
        }
        
        return paddedMessage;
    }
    
    /**
     * Step 2: Initialize MD buffer
     * 4 32-bit registers: A, B, C, D
     */
    private int[] initializeMD() {
        return new int[]{
            0x67452301,  // A
            0xEFCDAB89,  // B
            0x98BADCFE,  // C
            0x10325476   // D
        };
    }
    
    /**
     * Step 3: Process message blocks (core algorithm)
     * Each block is 512 bits, performing 64 rounds of operations
     */
    private void processMessageBlocks(byte[] message, int[] md) {
        // 4 rounds, each with 16 steps, totaling 64 steps
        int[] X = new int[16];  // Current 512-bit block
        
        for (int i = 0; i < message.length; i += 64) {
            // Convert 64 bytes to 16 ints
            for (int j = 0; j < 16; j++) {
                X[j] = bytesToInt(message, i + j * 4);
            }
            
            // Save current MD value
            int A = md[0], B = md[1], C = md[2], D = md[3];
            
            // Round 1: F function
            A = round1(A, B, C, D, X[0], 7, 0xD76AA478);
            D = round1(D, A, B, C, X[1], 12, 0xE8C7B756);
            // ... other 15 steps
            
            // Round 2: G function
            // Round 3: H function  
            // Round 4: I function
            // ...
            
            // Accumulate to MD buffer
            md[0] += A;
            md[1] += B;
            md[2] += C;
            md[3] += D;
        }
    }
    
    /**
     * 4 auxiliary functions of MD5
     */
    private int F(int x, int y, int z) {
        return (x && y) | (~x && z);
    }
    
    private int G(int x, int y, int z) {
        return (x && z) | (y && ~z);
    }
    
    private int H(int x, int y, int z) {
        return x ^ y ^ z;
    }
    
    private int I(int x, int y, int z) {
        return y ^ (x | ~z);
    }
}

At this point, you might feel overwhelmed. But don’t worry, in actual development, we don’t need to implement the MD5 algorithm ourselves; Java provides ready-made tools.

2. Correct usage of MD5 in Java

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class MD5Util {
    
    /**
     * Standard MD5 hash
     */
    public static String md5(String input) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] hashBytes = md.digest(input.getBytes("UTF-8"));
            
            // Convert to hexadecimal string
            StringBuilder sb = new StringBuilder();
            for (byte b : hashBytes) {
                sb.append(String.format("%02x", b));
            }
            
            return sb.toString();
        } catch (Exception e) {
            throw new RuntimeException("MD5 hash failed", e);
        }
    }
    
    /**
     * MD5 with salt (recommended)
     */
    public static String md5WithSalt(String input, String salt) {
        return md5(input + salt);
    }
    
    /**
     * Multiple rounds of MD5 hash (enhanced security)
     */
    public static String md5Multiple(String input, int rounds) {
        String result = input;
        for (int i = 0; i < rounds; i++) {
            result = md5(result);
        }
        return result;
    }
    
    /**
     * Verify MD5 hash
     */
    public static boolean verify(String input, String hash) {
        return md5(input).equals(hash);
    }
    
    /**
     * Verify MD5 with salt
     */
    public static boolean verifyWithSalt(String input, String salt, String hash) {
        return md5WithSalt(input, salt).equals(hash);
    }
}

// Usage example
public class MD5Example {
    public static void main(String[] args) {
        String password = "123456";
        
        // Basic MD5
        String hash1 = MD5Util.md5(password);
        System.out.println("Basic MD5: " + hash1);
        
        // MD5 with salt
        String salt = "mySecretSalt";
        String hash2 = MD5Util.md5WithSalt(password, salt);
        System.out.println("MD5 with salt: " + hash2);
        
        // Multiple rounds of MD5
        String hash3 = MD5Util.md5Multiple(password, 1000);
        System.out.println("Multiple rounds MD5: " + hash3);
        
        // Verification
        boolean isValid = MD5Util.verify("123456", hash1);
        System.out.println("Verification result: " + isValid);
    }
}

3. 5 Practical Application Scenarios of MD5

Scenario 1: Password Storage – Stop Exposing Yourself!

Incorrect approach:

// ❌ Never do this!
public class BadPasswordStorage {
    public void saveUser(String username, String password) {
        // Directly storing plaintext passwords is simply suicidal
        userDao.save(new User(username, password));
    }
}

Correct approach:

// ✅ Recommended password storage scheme
@Service
public class SecurePasswordService {
    
    private final String GLOBAL_SALT = "MyApp_Secret_Salt_2024";
    
    /**
     * Register user - securely store password
     */
    public void registerUser(String username, String password) {
        // 1. Generate user-specific salt
        String userSalt = generateUserSalt(username);
        
        // 2. Multiple hashing
        String hashedPassword = hashPassword(password, userSalt);
        
        // 3. Store user information
        User user = new User();
        user.setUsername(username);
        user.setPasswordHash(hashedPassword);
        user.setSalt(userSalt);
        
        userDao.save(user);
    }
    
    /**
     * User login - verify password
     */
    public boolean loginUser(String username, String password) {
        User user = userDao.findByUsername(username);
        if (user == null) {
            return false;
        }
        
        // Hash the input password using the same method
        String hashedInput = hashPassword(password, user.getSalt());
        
        // Compare hash values
        return hashedInput.equals(user.getPasswordHash());
    }
    
    /**
     * Password hash algorithm
     */
    private String hashPassword(String password, String userSalt) {
        // Combine: password + user salt + global salt
        String combined = password + userSalt + GLOBAL_SALT;
        
        // Multiple rounds of MD5 to enhance security
        return MD5Util.md5Multiple(combined, 3000);
    }
    
    /**
     * Generate user-specific salt
     */
    private String generateUserSalt(String username) {
        // Generate a unique salt based on username and timestamp
        String saltSource = username + System.currentTimeMillis() + Math.random();
        return MD5Util.md5(saltSource).substring(0, 16);
    }
}

Scenario 2: File Integrity Check – Prevent Data Corruption

@Service
public class FileIntegrityService {
    
    /**
     * Generate file MD5 checksum
     */
    public String generateFileMD5(File file) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            
            try (FileInputStream fis = new FileInputStream(file);
                 DigestInputStream dis = new DigestInputStream(fis, md)) {
                
                byte[] buffer = new byte[8192];
                while (dis.read(buffer) != -1) {
                    // DigestInputStream will automatically update MD5
                }
            }
            
            // Get final MD5 value
            byte[] digest = md.digest();
            StringBuilder sb = new StringBuilder();
            for (byte b : digest) {
                sb.append(String.format("%02x", b));
            }
            
            return sb.toString();
            
        } catch (Exception e) {
            throw new RuntimeException("Failed to generate file MD5", e);
        }
    }
    
    /**
     * Verify file integrity
     */
    public boolean verifyFileIntegrity(File file, String expectedMD5) {
        String actualMD5 = generateFileMD5(file);
        return actualMD5.equalsIgnoreCase(expectedMD5);
    }
    
    /**
     * File upload integrity check
     */
    @PostMapping("/upload")
    public ResponseEntity uploadFile(@RequestParam("file") MultipartFile file,
                                      @RequestParam("md5") String clientMD5) {
        try {
            // Save temporary file
            File tempFile = File.createTempFile("upload_", ".tmp");
            file.transferTo(tempFile);
            
            // Verify MD5
            if (!verifyFileIntegrity(tempFile, clientMD5)) {
                tempFile.delete();
                return ResponseEntity.badRequest().body("File MD5 check failed, please re-upload");
            }
            
            // MD5 verification passed, move to final directory
            String finalPath = moveToFinalLocation(tempFile, file.getOriginalFilename());
            
            return ResponseEntity.ok(Map.of(
                "message", "Upload successful",
                "path", finalPath,
                "md5", clientMD5
            ));
            
        } catch (Exception e) {
            return ResponseEntity.status(500).body("Upload failed: " + e.getMessage());
        }
    }
}

Scenario 3: Cache Key Generation – Making Caching Smarter

@Service
public class SmartCacheService {
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    /**
     * Smart cache key generation
     */
    public String generateCacheKey(String prefix, Object... params) {
        // Concatenate all parameters
        StringBuilder keyBuilder = new StringBuilder();
        for (Object param : params) {
            keyBuilder.append(param.toString()).append("|");
        }
        
        String paramString = keyBuilder.toString();
        
        // If parameters are too long, use MD5 to compress
        if (paramString.length() > 100) {
            String md5Key = MD5Util.md5(paramString);
            return prefix + ":" + md5Key;
        } else {
            return prefix + ":" + paramString.replaceAll("[^a-zA-Z0-9]", "_");
        }
    }
    
    /**
     * User personalized recommendation cache
     */
    public List getUserRecommendations(Long userId, String category, 
                                              List tags, Map filters) {
        
        // Generate complex cache key
        String cacheKey = generateCacheKey("user_recommend", 
            userId, category, String.join(",", tags), filters.toString());
        
        // Try to get from cache
        List cached = (List) redisTemplate.opsForValue().get(cacheKey);
        if (cached != null) {
            return cached;
        }
        
        // Cache miss, calculate recommendation results
        List recommendations = calculateRecommendations(userId, category, tags, filters);
        
        // Store in cache, expires in 1 hour
        redisTemplate.opsForValue().set(cacheKey, recommendations, Duration.ofHours(1));
        
        return recommendations;
    }
    
    /**
     * API response caching
     */
    public Object cacheApiResponse(String apiPath, Map params) {
        // Generate API cache key
        String cacheKey = generateCacheKey("api_cache", apiPath, params);
        
        Object cached = redisTemplate.opsForValue().get(cacheKey);
        if (cached != null) {
            return cached;
        }
        
        // Execute actual API call
        Object result = executeApiCall(apiPath, params);
        
        // Cache result
        redisTemplate.opsForValue().set(cacheKey, result, Duration.ofMinutes(30));
        
        return result;
    }
}

Scenario 4: Distributed Lock – Solving Concurrency Issues

@Component
public class DistributedLockService {
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    /**
     * Acquire distributed lock
     */
    public boolean acquireLock(String resource, String requester, int expireSeconds) {
        // Use MD5 to generate lock key
        String lockKey = "distributed_lock:" + MD5Util.md5(resource);
        
        // Lock value contains requester information and timestamp
        String lockValue = requester + ":" + System.currentTimeMillis();
        
        // Try to acquire lock
        Boolean success = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, lockValue, Duration.ofSeconds(expireSeconds));
        
        return Boolean.TRUE.equals(success);
    }
    
    /**
     * Release distributed lock
     */
    public boolean releaseLock(String resource, String requester) {
        String lockKey = "distributed_lock:" + MD5Util.md5(resource);
        
        // Lua script to ensure atomicity
        String luaScript = """
            local lockKey = KEYS[1]
            local expectedValue = ARGV[1]
            local actualValue = redis.call('GET', lockKey)
            
            if actualValue and string.find(actualValue, expectedValue) == 1 then
                return redis.call('DEL', lockKey)
            else
                return 0
            end
        """;
        
        DefaultRedisScript script = new DefaultRedisScript<>();
        script.setScriptText(luaScript);
        script.setResultType(Long.class);
        
        Long result = redisTemplate.execute(script, 
            Collections.singletonList(lockKey), requester);
        
        return result != null && result == 1;
    }
    
    /**
     * Example of stock deduction
     */
    @Transactional
    public boolean decreaseStock(Long productId, int quantity) {
        String lockResource = "product_stock:" + productId;
        String requester = Thread.currentThread().getName();
        
        // Acquire lock
        if (!acquireLock(lockResource, requester, 10)) {
            throw new RuntimeException("Failed to acquire stock lock, please try again later");
        }
        
        try {
            // Execute stock deduction
            Product product = productService.findById(productId);
            if (product.getStock() < quantity) {
                return false;
            }
            
            product.setStock(product.getStock() - quantity);
            productService.update(product);
            
            return true;
            
        } finally {
            // Release lock
            releaseLock(lockResource, requester);
        }
    }
}

Scenario 5: Data Deduplication – Prevent Duplicate Processing

@Service
public class DataDeduplicationService {
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    /**
     * Order deduplication processing
     */
    public boolean processOrderIdempotent(OrderRequest request) {
        // Generate idempotency key
        String idempotentKey = generateIdempotentKey("order", request);
        
        // Check if already processed
        if (isAlreadyProcessed(idempotentKey)) {
            log.info("Order has been processed, skipping: {}", request.getOrderNo());
            return true;
        }
        
        try {
            // Mark as processing
            markProcessing(idempotentKey);
            
            // Execute order processing logic
            Order order = createOrder(request);
            paymentService.processPayment(order);
            notificationService.sendOrderConfirmation(order);
            
            // Mark as processed
            markProcessed(idempotentKey, order.getId());
            
            return true;
            
        } catch (Exception e) {
            // Processing failed, clear mark
            clearProcessingMark(idempotentKey);
            throw e;
        }
    }
    
    /**
     * Generate idempotency key
     */
    private String generateIdempotentKey(String operation, Object request) {
        // Serialize request object to JSON
        String requestJson = JSON.toJSONString(request);
        
        // Generate MD5 as unique identifier
        String md5Key = MD5Util.md5(requestJson);
        
        return String.format("idempotent:%s:%s", operation, md5Key);
    }
    
    /**
     * Check if already processed
     */
    private boolean isAlreadyProcessed(String key) {
        String status = redisTemplate.opsForValue().get(key);
        return "PROCESSED".equals(status);
    }
    
    /**
     * Mark as processing
     */
    private void markProcessing(String key) {
                redisTemplate.opsForValue().set(key, "PROCESSING", Duration.ofMinutes(5));
    }
    
    /**
     * Mark as processed
     */
    private void markProcessed(String key, Long orderId) {
        redisTemplate.opsForValue().set(key, "PROCESSED:" + orderId, Duration.ofHours(24));
    }
    
    /**
     * Clear processing mark
     */
    private void clearProcessingMark(String key) {
        redisTemplate.delete(key);
    }
    
    /**
     * Message deduplication example
     */
    public void processMessageIdempotent(String messageId, String messageContent) {
        // Generate deduplication key based on message ID and content
        String dedupeKey = "message_dedupe:" + MD5Util.md5(messageId + messageContent);
        
        // Check if already processed
        if (redisTemplate.hasKey(dedupeKey)) {
            log.info("Message duplicate, skipping processing: {}", messageId);
            return;
        }
        
        // Mark message as processed
        redisTemplate.opsForValue().set(dedupeKey, "processed", Duration.ofHours(2));
        
        // Process message
        handleMessage(messageContent);
    }
}

4. 3 Fatal Weaknesses of MD5 You Must Know!

Weakness 1: Rainbow Table Attack – Common Passwords Cracked Instantly

What is a rainbow table? A rainbow table is a pre-computed “password → MD5” lookup table. Attackers trade space for time by pre-computing the MD5 values of common passwords.

// Dangerous example: directly using MD5 to store passwords
public class VulnerablePasswordStorage {
    public void savePassword(String username, String password) {
        String md5Hash = MD5Util.md5(password);
        // Storing passwords this way is easily cracked by rainbow tables
        userDao.updatePassword(username, md5Hash);
    }
}

// MD5 values of common passwords (attackers already know)
// "123456" → "e10adc3949ba59abbe56e057f20f883e"
// "password" → "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
// "admin" → "21232f297a57a5a743894a0e4a801fc3"

Defense strategy:

// Secure practice: use salt
public class SecurePasswordStorage {
    public void savePassword(String username, String password) {
        // 1. Generate random salt
        String salt = generateRandomSalt();
        
        // 2. Hash password + salt
        String secureHash = MD5Util.md5(password + salt);
        
        // 3. Store hash value and salt
        userDao.save(new User(username, secureHash, salt));
    }
    
    private String generateRandomSalt() {
        return UUID.randomUUID().toString().replace("-", "");
    }
}

Weakness 2: MD5 Collision – Different Inputs Produce the Same Output

In 2004, Chinese cryptography expert Wang Xiaoyun proved that MD5 has a collision vulnerability. Although it is difficult to exploit in practical applications, there is indeed a theoretical security risk.

// MD5 collision demonstration (theoretically exists, practically hard to construct)
public class MD5CollisionDemo {
    public static void main(String[] args) {
        // Theoretically, there exist two different inputs that produce the same MD5
        // However, constructing such a collision in practice requires enormous computational resources
        
        String input1 = "input1";
        String input2 = "input2";
        
        String hash1 = MD5Util.md5(input1);
        String hash2 = MD5Util.md5(input2);
        
        // Normally they should not be equal
        System.out.println("Hash1: " + hash1);
        System.out.println("Hash2: " + hash2);
        System.out.println("Equal: " + hash1.equals(hash2));
    }
}

Weakness 3: Too Fast Calculation Speed – A Breeding Ground for Brute Force Cracking

The calculation speed of MD5 is very fast, which becomes a disadvantage in the face of brute force cracking. Modern GPUs can compute billions of MD5 hashes per second.

// Demonstration: brute force cracking simple passwords
public class BruteForceDemo {
    
    // Simulate brute force cracking (for educational purposes only, do not use for illegal purposes)
    public String bruteForceSimplePassword(String targetMD5) {
        String chars = "0123456789";
        
        // Try 4-digit numeric passwords
        for (int i = 0; i < 10000; i++) {
            String password = String.format("%04d", i);
            String hash = MD5Util.md5(password);
            
            if (hash.equals(targetMD5)) {
                return password;
            }
        }
        
        return null;
    }
    
    public static void main(String[] args) {
        BruteForceDemo demo = new BruteForceDemo();
        
        // Password to crack: "1234"
        String targetHash = MD5Util.md5("1234");
        System.out.println("Target hash: " + targetHash);
        
        long startTime = System.currentTimeMillis();
        String cracked = demo.bruteForceSimplePassword(targetHash);
        long endTime = System.currentTimeMillis();
        
        System.out.println("Cracking result: " + cracked);
        System.out.println("Time taken: " + (endTime - startTime) + "ms");
    }
}

5. Alternatives to MD5 for a More Secure System

1. SHA-256 – The Modern Alternative to MD5

import java.security.MessageDigest;

public class SHA256Util {
    
    public static String sha256(String input) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(input.getBytes("UTF-8"));
            
            StringBuilder hexString = new StringBuilder();
            for (byte b : hash) {
                String hex = Integer.toHexString(0xff && b);
                if (hex.length() == 1) {
                    hexString.append('0');
                }
                hexString.append(hex);
            }
            
            return hexString.toString();
        } catch (Exception e) {
            throw new RuntimeException("SHA-256 hash failed", e);
        }
    }
    
    // Usage example
    public static void main(String[] args) {
        String password = "123456";
        
        String md5 = MD5Util.md5(password);
        String sha256 = SHA256Util.sha256(password);
        
        System.out.println("MD5   (32 bits): " + md5);
        System.out.println("SHA256(64 bits): " + sha256);
    }
}

2. BCrypt – A Hash Algorithm Designed Specifically for Passwords

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Service
public class BCryptService {
    
    private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
    
    /**
     * Encrypt password
     */
    public String encodePassword(String password) {
        return encoder.encode(password);
    }
    
    /**
     * Verify password
     */
    public boolean matches(String password, String hash) {
        return encoder.matches(password, hash);
    }
    
    /**
     * User registration example
     */
    public void registerUser(String username, String password) {
        // BCrypt automatically handles salt, no need to add manually
        String hashedPassword = encodePassword(password);
        
        User user = new User();
        user.setUsername(username);
        user.setPassword(hashedPassword);
        
        userDao.save(user);
    }
    
    /**
     * User login example
     */
    public boolean login(String username, String password) {
        User user = userDao.findByUsername(username);
        if (user == null) {
            return false;
        }
        
        return matches(password, user.getPassword());
    }
}

3. PBKDF2 – Industrial-Grade Password Hashing

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.SecureRandom;
import java.util.Base64;

public class PBKDF2Util {
    
    private static final String ALGORITHM = "PBKDF2WithHmacSHA256";
    private static final int ITERATIONS = 100000;  // 100,000 iterations
    private static final int KEY_LENGTH = 256;     // 256-bit key
    
    /**
     * Generate PBKDF2 hash
     */
    public static String generatePBKDF2(String password, byte[] salt) {
        try {
            PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, KEY_LENGTH);
            SecretKeyFactory factory = SecretKeyFactory.getInstance(ALGORITHM);
            byte[] hash = factory.generateSecret(spec).getEncoded();
            
            return Base64.getEncoder().encodeToString(hash);
        } catch (Exception e) {
            throw new RuntimeException("PBKDF2 hash failed", e);
        }
    }
    
    /**
     * Generate random salt
     */
    public static byte[] generateSalt() {
        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[16];
        random.nextBytes(salt);
        return salt;
    }
    
    /**
     * Verify password
     */
    public static boolean verify(String password, String hash, byte[] salt) {
        String computedHash = generatePBKDF2(password, salt);
        return computedHash.equals(hash);
    }
}

6. Practical Experience: When Should You Use MD5?

✅ Scenarios Suitable for Using MD5

  1. File Integrity Check
// Verify integrity after downloading a file
String downloadedFileMD5 = FileUtil.calculateMD5(downloadedFile);
if (!downloadedFileMD5.equals(expectedMD5)) {
    throw new RuntimeException("File download incomplete, please re-download");
}
  1. Cache Key Generation
// Generate cache key for complex query conditions
String cacheKey = "user_search:" + MD5Util.md5(queryConditions.toString());
  1. Data Deduplication Identifier
// Generate unique identifier for data
String dataId = MD5Util.md5(dataContent);
  1. Load Balancing Hash
// Select server based on user ID
int serverIndex = Math.abs(MD5Util.md5(userId).hashCode()) % serverList.size();

❌ Scenarios Not Suitable for Using MD5

  1. Password Storage – Use BCrypt or PBKDF2
  2. Digital Signatures – Use RSA or ECDSA
  3. High Security Requirement Scenarios – Use SHA-256 or more advanced algorithms
  4. Legal Compliance Requirements – Some industries prohibit the use of MD5

7. Performance Comparison: Choosing Various Hash Algorithms

// Performance testing code
public class HashPerformanceTest {
    
    private static final String TEST_DATA = "This is a test data used to compare the performance of various hash algorithms";
    private static final int ITERATIONS = 100000;
    
    public static void main(String[] args) {
        System.out.println("Hash algorithm performance comparison (" + ITERATIONS + " iterations):");
        
        // MD5 performance test
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < ITERATIONS; i++) {
            MD5Util.md5(TEST_DATA + i);
        }
        long md5Time = System.currentTimeMillis() - startTime;
        
        // SHA-256 performance test
        startTime = System.currentTimeMillis();
        for (int i = 0; i < ITERATIONS; i++) {
            SHA256Util.sha256(TEST_DATA + i);
        }
        long sha256Time = System.currentTimeMillis() - startTime;
        
        // BCrypt performance test (limited testing due to slowness)
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10);
        startTime = System.currentTimeMillis();
        for (int i = 0; i < 100; i++) {  // Only test 100 times
            encoder.encode(TEST_DATA + i);
        }
        long bcryptTime = System.currentTimeMillis() - startTime;
        
        System.out.println("MD5    : " + md5Time + "ms");
        System.out.println("SHA-256: " + sha256Time + "ms");
        System.out.println("BCrypt : " + bcryptTime + "ms (only 100 times)");
        
        // Result analysis
        System.out.println("\nConclusion:");
        System.out.println("- MD5 is the fastest, suitable for processing large amounts of data");
        System.out.println("- SHA-256 is slightly slower, but more secure");
        System.out.println("- BCrypt is the slowest, but most suitable for password storage");
    }
}

8. Conclusion: The Golden Rules for Using MD5

3 Core Points

  1. MD5 is not encryption, it is hashing – One-way and irreversible, with no concept of keys
  2. Security is outdated – Do not use for password storage and security-sensitive scenarios
  3. Performance is still excellent – Suitable for file verification, cache keys, and other non-security scenarios

5 Usage Recommendations

  1. Use BCrypt for password storage: Automatically salts, resistant to brute force cracking
  2. Use MD5 for file verification: Fast, can detect data corruption
  3. Use MD5 for cache key generation: Short and concise, low collision probability
  4. Use SHA-256 for new projects: Higher security, longer-lasting for the future
  5. Implement multiple protections for critical business: Use MD5 in combination with other algorithms

Best Practice Checklist

  • [ ] Understand that MD5 is a hash algorithm, not an encryption algorithm
  • [ ] Use BCrypt for password storage, not MD5
  • [ ] MD5 can be used for file verification
  • [ ] MD5 can be used for cache key generation
  • [ ] Be aware of MD5’s security weaknesses
  • [ ] Choose stronger algorithms in security-sensitive scenarios

Remember the words of an experienced developer: Tools are not good or bad, only suitable or unsuitable. MD5 may be old, but when used in the right place, it is still a powerful tool!

Now, will you still say MD5 is an encryption algorithm? 😏

If you find this useful, please like, share, and forward! In the next issue, we will discuss “How to Design a High-Performance Image Upload System,” stay tuned!

Follow our public account: Selected Backend Technology to share practical experience in backend architecture design every week, making technology warmer!

Leave a Comment