MessageDigest in Java: SHA-256, SHA-512 & MD5 Hashing

A cryptographic hash function maps arbitrary input to a fixed-length digest. Java's java.security.MessageDigest provides SHA-256, SHA-512, SHA-3, and MD5 through the JCA (Java Cryptography Architecture). This article covers hashing strings and files, hex encoding, algorithm selection, and common pitfalls.

Share

Algorithm Comparison

AlgorithmOutput sizeSpeedUse caseRecommendation
MD5128 bits (16 bytes)Very fastNon-security checksums⚠️ Broken for security: collisions found
SHA-1160 bits (20 bytes)FastLegacy systems⚠️ Deprecated: collision attacks known
SHA-256256 bits (32 bytes)FastGeneral security, HMAC, signatures✅ Default choice
SHA-512512 bits (64 bytes)Fast on 64-bitHigh-security contexts✅ Prefer on 64-bit hardware
SHA3-256256 bitsModeratePost-quantum resistance✅ Future-proof

Hashing a String

Obtain a MessageDigest instance via getInstance(algorithm), feed bytes in with update(), and call digest() to get the raw byte array.

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

public class HashExample {

    public static byte[] sha256(String input) throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        return md.digest(input.getBytes(StandardCharsets.UTF_8));
    }

    /** Convert raw bytes to lowercase hex string */
    public static String toHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    public static void main(String[] args) throws Exception {
        String input = "Hello, World!";
        byte[] hash = sha256(input);
        System.out.println(toHex(hash));
        // dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986d
    }
}
MessageDigest instances are not thread-safe. Always call getInstance() per thread or per operation: do not share a single instance across threads.

HexFormat: Cleaner Hex Encoding (Java 17+)

Java 17 introduced java.util.HexFormat, which replaces the manual format loop:

Java
import java.util.HexFormat;
import java.security.MessageDigest;

MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest("hello".getBytes(StandardCharsets.UTF_8));

// Java 17+
String hex = HexFormat.of().formatHex(hash);
System.out.println(hex);
// 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824

// Upper-case with delimiter
String upper = HexFormat.ofDelimiter(":").withUpperCase().formatHex(hash);
System.out.println(upper);  // 2C:F2:4D:BA:...

Hashing a File: Streaming Approach

For large files, feed the bytes in chunks using update(byte[]) to avoid loading the entire file into memory.

Java
import java.io.*;
import java.nio.file.*;
import java.security.*;

public static String hashFile(Path path, String algorithm) throws Exception {
    MessageDigest md = MessageDigest.getInstance(algorithm);
    try (InputStream is = Files.newInputStream(path)) {
        byte[] buffer = new byte[8192];
        int bytesRead;
        while ((bytesRead = is.read(buffer)) != -1) {
            md.update(buffer, 0, bytesRead);
        }
    }
    return HexFormat.of().formatHex(md.digest());
}

// Usage
String sha256 = hashFile(Path.of("/data/archive.zip"), "SHA-256");
System.out.println("SHA-256: " + sha256);

String md5 = hashFile(Path.of("/data/archive.zip"), "MD5");
System.out.println("MD5    : " + md5);

Alternatively, use DigestInputStream to wrap an existing stream:

Java
MessageDigest md = MessageDigest.getInstance("SHA-256");
try (DigestInputStream dis = new DigestInputStream(
        Files.newInputStream(Path.of("/data/file.bin")), md)) {
    // read all bytes — digest is updated automatically
    dis.transferTo(OutputStream.nullOutputStream());
}
String hex = HexFormat.of().formatHex(md.digest());

Incremental Multi-Part Hashing

You can call update() multiple times before the final digest(): useful for computing a hash over data that arrives in parts.

Java
MessageDigest md = MessageDigest.getInstance("SHA-256");

// Hash three parts as one logical message
md.update("part1".getBytes(StandardCharsets.UTF_8));
md.update("part2".getBytes(StandardCharsets.UTF_8));
md.update("part3".getBytes(StandardCharsets.UTF_8));

byte[] hash = md.digest();   // resets the digest for reuse

// Equivalent single call
byte[] combined = "part1part2part3".getBytes(StandardCharsets.UTF_8);
byte[] expected = MessageDigest.getInstance("SHA-256").digest(combined);

System.out.println(MessageDigest.isEqual(hash, expected));  // true
Use MessageDigest.isEqual(a, b) for constant-time comparison. Comparing with Arrays.equals() or == is vulnerable to timing attacks when used for security checks.

SHA-3 Family (Java 9+)

Java 9 added the SHA-3 family (SHA3-224, SHA3-256, SHA3-384, SHA3-512), which uses the Keccak sponge construction: a fundamentally different algorithm from SHA-2.

Java
// SHA3-256
byte[] hash = MessageDigest.getInstance("SHA3-256")
        .digest("data".getBytes(StandardCharsets.UTF_8));
System.out.println(HexFormat.of().formatHex(hash));

// List all available digest algorithms
for (Provider p : Security.getProviders()) {
    p.getServices().stream()
     .filter(s -> "MessageDigest".equals(s.getType()))
     .forEach(s -> System.out.println(s.getAlgorithm()));
}

Common Pitfalls

PitfallFix
Using MD5 or SHA-1 for password storageNever: use PBKDF2, bcrypt, or Argon2 instead
Sharing MessageDigest across threadsCreate a new instance per thread/call
Comparing digests with == or equals()Use MessageDigest.isEqual() for constant-time comparison
Forgetting to specify charset when converting String to bytesAlways use StandardCharsets.UTF_8
Hashing Object.toString() outputHash meaningful serialised content, not the default object representation
Using MD5 to detect malicious tamperingUse HMAC-SHA256: MD5 alone provides no authentication