Bab Cryptography Fundamentals memperkenalkan hash sebagai keluarga alat; bab ini menggunakannya untuk dua pekerjaan nyata yang berbeda tingkat kepercayaannya. Message digest menjawab pertanyaan “apakah data ini utuh dan identik dengan yang dimaksud?” — satu arah, tanpa kunci, cocok untuk checksum unduhan dan pendeteksi duplikasi. HMAC menambahkan kunci rahasia sehingga pertanyaannya berubah menjadi “apakah data ini utuh dan benar-benar dikirim pihak yang memegang kunci kita?” — fondasi webhook dan API key. Lalu digital signature memakai kunci privat untuk menandatangani dan kunci publik untuk memverifikasi — mekanisme yang sama yang membuat sertifikat TLS bisa dipercaya. Ketiganya tersedia di java.security dan javax.crypto tanpa satu dependency pun.
Sidik Jari Data dengan SHA-256 #
MessageDigest menghitung sidik jari tetap untuk data berapa pun panjangnya: teks satu karakter dan berkas sepuluh gigabyte sama-sama menghasilkan 32 byte. Properti terpentingnya untuk keamanan adalah avalanche effect — perubahan sekecil apa pun pada masukan mengubah hampir seluruh bit keluaran. Program berikut membandingkan dua pesan transfer yang berbeda satu digit:
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class SidikDanHmac {
public static void main(String[] args) throws Exception {
// ============ Bagian 1: sidik jari SHA-256 ============
String pesanAsli = "Transfer 100000 ke Budi";
String pesanUbah = "Transfer 100001 ke Budi";
System.out.println("=== SHA-256: satu digit beda, sidik total beda ===");
System.out.println(" " + pesanAsli + " -> " + sha256Hex(pesanAsli));
System.out.println(" " + pesanUbah + " -> " + sha256Hex(pesanUbah));
// ============ Bagian 2: HMAC terikat kunci rahasia ============
// Kunci tetap hanya untuk demo deterministik; produksi memakai
// kunci dari proses pertukaran yang aman (mis. saat sesi dibuat).
byte[] kunci = "kunci-hmac-demo".getBytes(StandardCharsets.UTF_8);
String pesan = "pembayaran: Rp250.000 untuk Siti";
byte[] tag = hmac(kunci, pesan);
System.out.println();
System.out.println("=== HMAC-SHA256 atas pesan tetap ===");
System.out.println(" tag : " + HexFormat.of().formatHex(tag));
boolean pesanAsliSah = MessageDigest.isEqual(tag, hmac(kunci, pesan));
String pesanDitelan = "pembayaran: Rp950.000 untuk Siti";
boolean pesanUbahSah = MessageDigest.isEqual(tag, hmac(kunci, pesanDitelan));
System.out.println(" verifikasi pesan asli : " + (pesanAsliSah ? "SAH" : "TIDAK SAH"));
System.out.println(" verifikasi pesan ubahan: " + (pesanUbahSah ? "SAH" : "TIDAK SAH"));
}
static String sha256Hex(String teks) throws Exception {
MessageDigest pencerna = MessageDigest.getInstance("SHA-256");
byte[] sidik = pencerna.digest(teks.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(sidik);
}
static byte[] hmac(byte[] kunci, String pesan) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(kunci, "HmacSHA256"));
return mac.doFinal(pesan.getBytes(StandardCharsets.UTF_8));
}
}Output yang Dihasilkan:
=== SHA-256: satu digit beda, sidik total beda ===
Transfer 100000 ke Budi -> 99bd67820b6ad9631bef20a09d01b05cd7da341cfc3fa1b58f8ea117925e6420
Transfer 100001 ke Budi -> d6f230800fc15455de4756a879cd18fc02195bde3866ee966f6b29ec1f60e4dc
=== HMAC-SHA256 atas pesan tetap ===
tag : aace62df079ac736e4fa4b117d7d43349e8fe8f8df4a74c52bd29e0e50d11447
verifikasi pesan asli : SAH
verifikasi pesan ubahan: TIDAK SAHDua sidik pada output tidak memiliki satu blok pun yang sama — 99bd6782... melawan d6f23080... — padahal pesannya hanya berbeda satu digit. Inilah properti yang membuat sidik jari sensitif terhadap manipulasi: pengirim menghitung digest dan mengirimkannya terpisah (lewat kanal yang dipercaya); penerima menghitung ulang dan membandingkan; satu bit yang berubah di tengah perjalanan langsung terdeteksi.
Bedah baris-per-baris untuk method sha256Hex dan hmac:
- Baris 34-35 (
MessageDigest.getInstance("SHA-256")): pola JCA yang sudah akrab — nama algoritme, provider yang menyediakan. ObjekMessageDigestbisa dipakai ulang lewatreset(), tetapi membuat instance baru per penggunaan lebih aman dari kesalahan status tersisa. - Baris 36 (
digest(teks.getBytes(...))): satu panggilan ini menyerap seluruh byte lalu mengembalikan sidik final. Untuk berkas besar, pakaiupdate()berulang (atauDigestInputStream) agar tidak memuat seluruh berkas ke memori —digest()pada akhirnya tetap memproses apa yang sudah diserap. - Baris 41-42 (
Mac.getInstance("HmacSHA256")danmac.init(...)):Macadalah mesin HMAC — hash yang setiap perhitungannya dilumuri kunci rahasia.SecretKeySpecmembungkus byte kunci mentah; kunci HMAC-SHA256 yang baik panjangnya minimal sama dengan keluaran hash atau setidaknya 16 byte. - Baris 43 (
mac.doFinal(...)): hasilnya disebut tag — 32 byte yang tidak bisa dipalsukan tanpa kunci. Perhatikan perbandingan memakaiMessageDigest.isEqual()(waktu konstan) seperti pada bab Password Hashing.
Integritas Berkas dalam Praktik #
Pola digest-and-compare berskala besar dipakai di mana-mana. Halaman unduhan distribusi perangkat lunak menyertakan checksum SHA-256 di samping setiap berkas; alat manajemen paket memverifikasi digest setiap artefak sebelum memasangnya; git menyimpan setiap isi berkas di indeks internalnya sebagai SHA-1/SHA-256 dari isinya, sehingga riwayat yang dimanipulasi terdeteksi. Untuk berkas multi-gigabyte, pola implementasinya adalah streaming:
try (var masukan = java.nio.file.Files.newInputStream(java.nio.file.Path.of("arsip.zip"));
var pencerna = new java.security.DigestInputStream(masukan,
java.security.MessageDigest.getInstance("SHA-256"))) {
pencerna.transferTo(java.io.OutputStream.nullOutputStream()); // serap berkas
byte[] sidik = pencerna.getMessageDigest().digest();
// bandingkan sidik dengan nilai yang diharapkan
}DigestInputStream menghitung digest sambil data lewat — memori yang terpakai konstan kecil berapa pun ukuran berkasnya. Pola ini adalah pasangan alami dari bab Path Traversal berikutnya: sebelum membuka berkas yang diunduh, verifikasi identitasnya lebih dulu.
HMAC Menautkan Integritas dan Identitas #
Digest polos punya satu kelemahan operasional: siapa pun bisa menghitung digest pesan palsu, mengganti pesan dan digest-nya sekaligus, dan penerima tidak bisa membedakan. HMAC menutup celah ini dengan kunci rahasia bersama — tag hanya bisa dihitung oleh pemegang kunci, sehingga tag yang cocok membuktikan dua hal sekaligus: pesan tidak berubah dan dikirim pemegang kunci (atau setidaknya pihak yang diberi kunci).
Bagian kedua output memperlihatkan alur lengkapnya. Tag atas pesan pembayaran: Rp250.000 untuk Siti dihitung dengan kunci tetap; verifikasi ulang pesan asli menghasilkan SAH; verifikasi atas pesan yang nilai transfernya ditelen menjadi Rp950.000 menghasilkan TIDAK SAH — meski penyerang menulis ulang pesannya, penyerang tidak bisa menghitung tag yang cocok tanpa kunci. Webhook pembayaran dan API berbasis kunci bekerja persis begini: penyedia layanan menghitung HMAC atas body permintaan dengan kunci yang dibagikan ke kita, kita menghitung ulang, lalu membandingkan dengan MessageDigest.isEqual() sebelum memproses satu rupiah pun.
Tanda Tangan Digital #
HMAC memakai kunci bersama — kedua pihak bisa menghitung tag, jadi penerima tidak bisa membuktikan ke pihak ketiga siapa yang menandatangani. Digital signature menyelesaikan ini dengan pasangan kunci asimetris: pengirim menandatangani dengan kunci privat (yang hanya ada padanya), siapa pun bisa memverifikasi dengan kunci publik yang disebarluaskan. Tanda tangan yang sah membuktikan tiga hal: pesan tidak berubah, hanya pemegang kunci privat yang bisa membuatnya, dan pihak ketiga mana pun bisa memeriksanya sendiri — properti non-repudiation.
// Konsep - memerlukan KeyPairGenerator dan Signature dari java.security.
var generator = java.security.KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
java.security.KeyPair pasangan = generator.generateKeyPair(); // privat + publik
var penanda = java.security.Signature.getInstance("SHA256withRSA");
penanda.initSign(pasangan.getPrivate()); // menandatangani butuh kunci PRIVAT
penanda.update("kontrak: Budi membeli 100 saham".getBytes());
byte[] tandaTangan = penanda.sign();
var pemeriksa = java.security.Signature.getInstance("SHA256withRSA");
pemeriksa.initVerify(pasangan.getPublic()); // memverifikasi butuh kunci PUBLIK
pemeriksa.update("kontrak: Budi membeli 100 saham".getBytes());
boolean sah = pemeriksa.verify(tandaTangan); // true bila pesan dan tanda cocokNama transformasi "SHA256withRSA" membaca: sidik SHA-256 dari pesan, lalu ditandatangani dengan RSA — kombinasi yang menggabungkan kecepatan hash dan properti asimetris. Satu pertanyaan tersisa menggantung: pembaca bisa memverifikasi tanda tangan hanya bila pembaca mempercayai bahwa kunci publik itu benar milik Budi. Kepercayaan itu dibangun lewat rantai sertifikat yang ditandatangani otoritas sertifikat — dan itulah tepatnya pokok bahasan bab berikutnya, TLS dan Certificate Validation.
Referensi #
- Oracle: java.security MessageDigest
- Oracle: javax.crypto Mac
- Oracle: java.security Signature
- NIST FIPS 180-4: Secure Hash Standard
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
SidikDanHmac.java
Unduh berkasimport java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class SidikDanHmac {
public static void main(String[] args) throws Exception {
// ============ Bagian 1: sidik jari SHA-256 ============
String pesanAsli = "Transfer 100000 ke Budi";
String pesanUbah = "Transfer 100001 ke Budi";
System.out.println("=== SHA-256: satu digit beda, sidik total beda ===");
System.out.println(" " + pesanAsli + " -> " + sha256Hex(pesanAsli));
System.out.println(" " + pesanUbah + " -> " + sha256Hex(pesanUbah));
// ============ Bagian 2: HMAC terikat kunci rahasia ============
// Kunci tetap hanya untuk demo deterministik; produksi memakai
// kunci dari proses pertukaran yang aman (mis. saat sesi dibuat).
byte[] kunci = "kunci-hmac-demo".getBytes(StandardCharsets.UTF_8);
String pesan = "pembayaran: Rp250.000 untuk Siti";
byte[] tag = hmac(kunci, pesan);
System.out.println();
System.out.println("=== HMAC-SHA256 atas pesan tetap ===");
System.out.println(" tag : " + HexFormat.of().formatHex(tag));
boolean pesanAsliSah = MessageDigest.isEqual(tag, hmac(kunci, pesan));
String pesanDitelan = "pembayaran: Rp950.000 untuk Siti";
boolean pesanUbahSah = MessageDigest.isEqual(tag, hmac(kunci, pesanDitelan));
System.out.println(" verifikasi pesan asli : " + (pesanAsliSah ? "SAH" : "TIDAK SAH"));
System.out.println(" verifikasi pesan ubahan: " + (pesanUbahSah ? "SAH" : "TIDAK SAH"));
}
static String sha256Hex(String teks) throws Exception {
MessageDigest pencerna = MessageDigest.getInstance("SHA-256");
byte[] sidik = pencerna.digest(teks.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(sidik);
}
static byte[] hmac(byte[] kunci, String pesan) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(kunci, "HmacSHA256"));
return mac.doFinal(pesan.getBytes(StandardCharsets.UTF_8));
}
}