Pertukaran data antar pihak—file kontrak, pesan JSON, atau artefak build—memerlukan dua jaminan: integritas (data tidak dimodifikasi) dan keaslian (data memang berasal dari pihak yang diklaim). Hash function seperti SHA-256 menyediakan jaminan integritas: setiap perubahan kecil pada input menghasilkan hash yang sepenuhnya berbeda. Tanda tangan digital menambahkan autentikasi: penandatangan memakai kunci privat untuk menandatangani hash, dan siapa pun dapat memverifikasi dengan kunci publik. Pada chapter ini kita akan mempraktikkan SHA-256 digest, tanda tangan RSA, serta tanda tangan ECDSA dengan API bawaan JDK.
Message Digest: Verifikasi Integritas #
SHA-256 mengubah input apa pun menjadi 32 byte (256 bit) yang merepresentasikan sidik jari unik. Dua file dengan digest identik diyakini memiliki konten yang sama. Digest cocok untuk verifikasi unduhan: server mempublikasikan digest resmi, dan klien menghitung digest file hasil unduhan untuk membandingkan.
import java.security.MessageDigest
fun digest(data: ByteArray): ByteArray =
MessageDigest.getInstance("SHA-256").digest(data)
fun hex(bytes: ByteArray): String =
bytes.joinToString("") { "%02x".format(it) }Satu perubahan kecil pada input—misalnya mengganti 100 menjadi 200 pada teks kontrak—mengubah seluruh 32 byte digest. Inilah avalanche effect yang membuat SHA-256 sulit dipalsukan.
Tanda Tangan Digital: Autentikasi #
Hash sendiri tidak membuktikan pengirim. Penyerang dapat mengganti file, menghitung hash baru, dan mengirimkan keduanya. Tanda tangan digital menyelesaikan masalah ini dengan kunci asimetris: pengirim menandatangani hash dengan kunci privat, penerima memverifikasi dengan kunci publik pengirim. Hanya pemegang kunci privat yang dapat membuat tanda tangan valid.
// message_digests_dan_signatures.kt
// SHA-256 digest. RSA/ECDSA signature. Verify integrity and authenticity.
// Demonstrates: digest comparison, signature create/verify, tamper detection.
import java.security.KeyPairGenerator
import java.security.MessageDigest
import java.security.SecureRandom
import java.security.Signature
// Stable SecureRandom for deterministic demo output
private val DEMO_RNG: SecureRandom = SecureRandom.getInstance("SHA1PRNG").apply {
setSeed("kotlin-course-stable-seed".toByteArray())
}
// --- SHA-256 digest of arbitrary bytes ---
fun digest(data: ByteArray): ByteArray =
MessageDigest.getInstance("SHA-256").digest(data)
fun hex(bytes: ByteArray): String = bytes.joinToString("") { "%02x".format(it) }
// --- RSA keypair generation (2048-bit) for signing ---
fun generateRsaKeyPair() = KeyPairGenerator.getInstance("RSA").run {
initialize(2048, DEMO_RNG)
generateKeyPair()
}
// --- Sign bytes with RSA private key (SHA-256 with RSA, PKCS#1 v1.5) ---
fun signRsa(data: ByteArray): Pair<ByteArray, java.security.KeyPair> {
val keyPair = generateRsaKeyPair()
val sig = Signature.getInstance("SHA256withRSA")
sig.initSign(keyPair.private)
sig.update(data)
return sig.sign() to keyPair
}
// --- Verify signature with RSA public key ---
fun verifyRsa(data: ByteArray, signature: ByteArray, publicKey: java.security.PublicKey): Boolean {
val sig = Signature.getInstance("SHA256withRSA")
sig.initVerify(publicKey)
sig.update(data)
return sig.verify(signature)
}
// --- ECDSA keypair generation (P-256) ---
fun generateEcdsaKeyPair() = KeyPairGenerator.getInstance("EC").run {
initialize(java.security.spec.ECGenParameterSpec("secp256r1"), DEMO_RNG)
generateKeyPair()
}
// --- Sign with ECDSA (SHA-256) ---
fun signEcdsa(data: ByteArray): Pair<ByteArray, java.security.KeyPair> {
val keyPair = generateEcdsaKeyPair()
val sig = Signature.getInstance("SHA256withECDSA")
sig.initSign(keyPair.private)
sig.update(data)
return sig.sign() to keyPair
}
// --- Verify with ECDSA ---
fun verifyEcdsa(data: ByteArray, signature: ByteArray, publicKey: java.security.PublicKey): Boolean {
val sig = Signature.getInstance("SHA256withECDSA")
sig.initVerify(publicKey)
sig.update(data)
return sig.verify(signature)
}
fun main() {
// 1. SHA-256 digest: same input -> same digest; tampering changes digest
val dokumenAsli = "Kontrak: Budi akan mengirim 100 unit ke Siti pada 15 Januari 2026.".toByteArray()
val dokumenDiubah = "Kontrak: Budi akan mengirim 200 unit ke Siti pada 15 Januari 2026.".toByteArray()
println("digest(asli) : ${hex(digest(dokumenAsli)).take(32)}...")
println("digest(diubah) : ${hex(digest(dokumenDiubah)).take(32)}...")
println("Cocok? ${digest(dokumenAsli).contentEquals(digest(dokumenDiubah))}")
println()
// 2. RSA signature: create + verify + tamper detection
val (sigRsa, keyPairRsa) = signRsa(dokumenAsli)
println("RSA signature OK (${sigRsa.size} bytes, fixed-length untuk RSA-2048)")
println("verify(asli, sigRsa) = ${verifyRsa(dokumenAsli, sigRsa, keyPairRsa.public)}")
println("verify(dokumenDiubah, sigRsa) = ${verifyRsa(dokumenDiubah, sigRsa, keyPairRsa.public)}")
println()
// 3. ECDSA signature: create + verify + tamper detection
// Note: ECDSA signature in DER form has variable length (typically 70-72 bytes).
// We do not print length to keep demo output deterministic.
val (sigEcdsa, keyPairEcdsa) = signEcdsa(dokumenAsli)
println("ECDSA signature OK (DER-encoded, panjang bervariasi per tanda tangan)")
println("verify(asli, sigEcdsa) = ${verifyEcdsa(dokumenAsli, sigEcdsa, keyPairEcdsa.public)}")
println("verify(dokumenDiubah, sigEcdsa) = ${verifyEcdsa(dokumenDiubah, sigEcdsa, keyPairEcdsa.public)}")
}Output yang Dihasilkan:
digest(asli) : add0a4e0f8200b6c177cf29a7705ddea...
digest(diubah) : 3931d2508298c7e745d626516a59e33f...
Cocok? false
RSA signature OK (256 bytes, fixed-length untuk RSA-2048)
verify(asli, sigRsa) = true
verify(dokumenDiubah, sigRsa) = false
ECDSA signature OK (DER-encoded, panjang bervariasi per tanda tangan)
verify(asli, sigEcdsa) = true
verify(dokumenDiubah, sigEcdsa) = falseMari kita bedah bagian kunci dari kode di atas:
DEMO_RNG: instanceSecureRandomdenganSHA1PRNGdan seed stabil untuk demo. Produksi harus memakaiSecureRandom()tanpa argumen—keypair yang dihasilkan tidak boleh dapat diprediksi.digest: memanggilMessageDigest.getInstance("SHA-256")laludigest(data). Hasilnya 32 byte. Fungsihexmenyandikan setiap byte menjadi dua digit heksadesimal.signRsa: membangkitkan keypair RSA 2048-bit, lalu menandatangani data dengan algoritmaSHA256withRSA. Proses: hitung SHA-256 dari data, enkripsi hash dengan kunci privat. Output tanda tangan adalah 256 byte untuk RSA-2048.verifyRsa: mengulangi proses—hitung SHA-256 dari data yang diterima, dekripsi tanda tangan dengan kunci publik, bandingkan kedua hash. Bila data dimodifikasi, hash input tidak cocok dengan hash yang didekripsi dari tanda tangan, sehinggaverifymengembalikanfalse.signEcdsa/verifyEcdsa: memakai kurva eliptik P-256 (secp256r1). ECDSA menghasilkan tanda tangan yang jauh lebih pendek (sekitar 64 byte) dibanding RSA-2048 dengan keamanan setara—ini menghemat bandwidth untuk komunikasi.
RSA vs ECDSA #
Pilihan algoritma memengaruhi ukuran kunci, ukuran tanda tangan, dan kecepatan. RSA mudah diimplementasikan dan memiliki dukungan luas, namun ukuran kunci besar membuat operasi lambat. ECDSA menawarkan keamanan setara dengan kunci yang jauh lebih kecil.
| Algoritma | Ukuran kunci | Ukuran tanda tangan | Kecepatan verifikasi |
|---|---|---|---|
| RSA-2048 | 2048 bit | 256 byte | Cepat |
| ECDSA P-256 | 256 bit | 64 byte | Sedang |
| Ed25519 | 256 bit | 64 byte | Sangat cepat |
Tabel di atas menunjukkan bahwa ECDSA dan Ed25519 menghasilkan tanda tangan yang jauh lebih ringkas. Untuk sistem baru, Ed25519 adalah pilihan modern yang lebih disarankan—sayangnya JDK standar belum menyediakannya tanpa library tambahan seperti BouncyCastle.
Deteksi Modifikasi dengan Digest #
Untuk verifikasi integritas tanpa autentikasi (misalnya membandingkan dua file lokal), digest saja sudah cukup. Bandingkan byte-per-byte hasil digest dengan contentEquals:
val digestAsli = digest(dokumenAsli)
val digestDiubah = digest(dokumenDiubah)
val cocok = digestAsli.contentEquals(digestDiubah)
// cocok = false karena dokumenDiubah berbeda dari dokumenAsliPerbandingan byte-per-byte lebih efisien daripada perbandingan string hex. Gunakan contentEquals untuk ByteArray agar membandingkan isi, bukan referensi.
Pola Penandatanganan dan Verifikasi #
Pola umum penandatanganan: pengirim menghitung digest dari data, menandatangani digest dengan kunci privat, lalu mengirim (data, tanda tangan) ke penerima. Penerima menghitung digest dari data yang diterima, memverifikasi tanda tangan dengan kunci publik pengirim, dan menerima data hanya bila verifikasi berhasil.
Pengirim: Penerima:
data ──┐ data ──┐
│ │
digest ←── SHA-256 digest ←── SHA-256
│ │
sig ←── sign(priv, digest) │
│ │
kirim (data, sig) ──────────────────→ verify(pub, sig, digest)
│
valid? → terima/tolakPola ini memisahkan kebutuhan komputasi: hash cepat untuk data besar, sementara tanda tangan hanya memproses 32 byte hash. Inilah yang membuat kombinasi SHA-256 + RSA/ECDSA tetap praktis untuk file berukuran gigabyte.
Referensi Lanjutan #
- java.security package
— dokumentasi
java.securityuntukMessageDigest,Signature, dan kunci kriptografi - javax.crypto package
— dokumentasi
javax.cryptountuk operasiMacdan tanda tangan digital
Penutup #
Message digest dan tanda tangan digital adalah dua lapisan pertahanan untuk integritas dan autentikasi data. SHA-256 sendiri hanya menjawab pertanyaan “apakah data berubah”; tanda tangan digital menambahkan jawaban atas “siapa yang membuat data ini”. Chapter berikutnya akan membahas TLS dan validasi sertifikat—bagaimana kedua konsep ini dipakai dalam protokol transport aman yang dipakai HTTPS.