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:

text
=== 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 SAH

Dua 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:

  1. Baris 34-35 (MessageDigest.getInstance("SHA-256")): pola JCA yang sudah akrab — nama algoritme, provider yang menyediakan. Objek MessageDigest bisa dipakai ulang lewat reset(), tetapi membuat instance baru per penggunaan lebih aman dari kesalahan status tersisa.
  2. Baris 36 (digest(teks.getBytes(...))): satu panggilan ini menyerap seluruh byte lalu mengembalikan sidik final. Untuk berkas besar, pakai update() berulang (atau DigestInputStream) agar tidak memuat seluruh berkas ke memori — digest() pada akhirnya tetap memproses apa yang sudah diserap.
  3. Baris 41-42 (Mac.getInstance("HmacSHA256") dan mac.init(...)): Mac adalah mesin HMAC — hash yang setiap perhitungannya dilumuri kunci rahasia. SecretKeySpec membungkus byte kunci mentah; kunci HMAC-SHA256 yang baik panjangnya minimal sama dengan keluaran hash atau setidaknya 16 byte.
  4. Baris 43 (mac.doFinal(...)): hasilnya disebut tag — 32 byte yang tidak bisa dipalsukan tanpa kunci. Perhatikan perbandingan memakai MessageDigest.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:

java Digest berkas besar tanpa memuat seluruhnya ke memori
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.

java Pola sign dan verify dengan RSA di JCA
// 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 cocok

Nama 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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

SidikDanHmac.java

Unduh berkas
java
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));
    }
}