Hashing with MessageDigest
Compute SHA-256 fingerprints with MessageDigest for ETags, checksums and token lookups, and know why a fast hash must never store passwords.
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(chunk); // repeat per chunk
byte[] hash = md.digest(); // 32 bytes, resets md
String hex = HexFormat.of().formatHex(hash);
boolean same = MessageDigest.isEqual(hash, other);A cryptographic hash turns any number of bytes into a short, fixed-size fingerprint. SHA-256 always gives 32 bytes, usually shown as 64 hex characters. The same input always gives the same output, a one-bit change gives a completely different output, and there is no practical way to go from the hash back to the input or to find two inputs with the same hash.
The API
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
byte[] hash = sha256.digest(body.getBytes(StandardCharsets.UTF_8));
String hex = HexFormat.of().formatHex(hash);
getInstance takes an algorithm name; the JDK's built-in provider supports SHA-256, SHA-512 and the SHA-3 family such as SHA3-256. An unknown name throws the checked NoSuchAlgorithmException. A hash works on bytes, not text, so always pick the charset yourself: getBytes() with no argument uses the default charset, UTF-8 since Java 18 (JEP 400) but still overridable with -Dfile.encoding, so naming it keeps the hash independent of how the JVM was started. HexFormat (Java 17) replaces the hand-written hex loops found in older code.
For large input, call update once per chunk and digest() at the end, so a 2 GB upload never has to sit in memory. digest() also resets the object, ready for the next input.
Where hashes fit in a back end
- ETags. Hash the response body and send it as
ETag; whenIf-None-Matchmatches, reply 304 with no body. - Checksums and deduplication. Store the SHA-256 of each uploaded file and skip storing a second copy with the same hash.
- API keys and reset tokens. Store the hash of a long random token rather than the token itself. A leaked table is then useless, and lookup still works because you hash the incoming value and search for it.
Mistakes people make
- Hashing passwords with SHA-256. It is designed to be fast: a single graphics card tries billions of guesses a second. Passwords need a slow, salted algorithm such as PBKDF2 (next lesson). Long random tokens are safe with a fast hash because there is nothing to guess.
- Using MD5 or SHA-1 for anything security related. Both have practical collision attacks. They survive only as non-security checksums for legacy systems.
- Sharing one instance between threads.
MessageDigestholds state and is not thread-safe.getInstanceis cheap, so call it inside the method that needs it. - Comparing with
Arrays.equalsor hexequalswhen the value is a secret. UseMessageDigest.isEqual, which takes the same time however many leading bytes match.
Example
When to use it
- Generate an ETag from a JSON response body so unchanged resources return 304 Not Modified.
- Store a SHA-256 checksum for every uploaded file to detect corruption and skip duplicate uploads.
- Keep only the hashes of API keys in the database and authenticate requests by hashing the presented key.
More examples
An ETag for a JSON response
The client sends the ETag back in If-None-Match, and an unchanged body earns a 304 with no payload. Any change to the body changes the hash, so stale caches are never served.
String etag(String body) throws NoSuchAlgorithmException {
byte[] hash = MessageDigest.getInstance("SHA-256")
.digest(body.getBytes(StandardCharsets.UTF_8));
return "\"" + HexFormat.of().formatHex(hash, 0, 8) + "\""; // first 8 bytes is plenty
}
int status(String body, String ifNoneMatch) throws NoSuchAlgorithmException {
return etag(body).equals(ifNoneMatch) ? 304 : 200;
}
void main() throws Exception {
String body = "{\"id\":7,\"name\":\"Desk lamp\",\"pricePence\":2499}";
String tag = etag(body);
IO.println("ETag: " + tag);
IO.println("first request: " + status(body, null));
IO.println("unchanged: " + status(body, tag));
String changed = body.replace("2499", "1999");
IO.println("price changed: " + status(changed, tag) + " new ETag " + etag(changed));
}
// Output:
// ETag: "1cc35cfff2389332"
// first request: 200
// unchanged: 304
// price changed: 200 new ETag "0aebf83e5dca2602"Hashing a stream in chunks
update() can be called any number of times before digest(), so memory use stays at one buffer however large the input is. The result is identical to hashing all the bytes at once.
void main() throws Exception {
byte[] upload = "line one\nline two\nline three\n".repeat(1000).getBytes(StandardCharsets.UTF_8);
MessageDigest md = MessageDigest.getInstance("SHA-256");
int chunks = 0;
try (InputStream in = new ByteArrayInputStream(upload)) { // a request body or file in real code
byte[] buffer = new byte[8192];
int n;
while ((n = in.read(buffer)) != -1) {
md.update(buffer, 0, n);
chunks++;
}
}
byte[] streamed = md.digest();
byte[] oneShot = MessageDigest.getInstance("SHA-256").digest(upload);
IO.println("size: " + upload.length + " bytes in " + chunks + " chunks");
IO.println("streamed equals one-shot: " + MessageDigest.isEqual(streamed, oneShot));
}
// Output:
// size: 29000 bytes in 4 chunks
// streamed equals one-shot: trueLooking up API keys by their hash
Hashing the presented key and looking up the hash means a database leak does not leak usable keys. A fast hash is fine here because the keys are long and random, unlike passwords.
String sha256Hex(String value) throws NoSuchAlgorithmException {
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
.digest(value.getBytes(StandardCharsets.UTF_8)));
}
void main() throws Exception {
// The table stores only hashes; the plain keys were shown to clients once, at creation.
Map<String, String> clientByKeyHash = new HashMap<>();
clientByKeyHash.put(sha256Hex("demo_key_8f3kQ2pZ7xLw1mNc"), "billing-service");
clientByKeyHash.put(sha256Hex("demo_key_Vb9tR4yU6iO0pAsD"), "reporting-job");
for (String presented : List.of("demo_key_Vb9tR4yU6iO0pAsD", "demo_key_guess")) {
String client = clientByKeyHash.get(sha256Hex(presented));
IO.println(presented + " -> " + (client == null ? "401 Unauthorized" : client));
}
}
// Output:
// demo_key_Vb9tR4yU6iO0pAsD -> reporting-job
// demo_key_guess -> 401 Unauthorized
Discussion