Kriptografi menyediakan alat untuk menjaga kerahasiaan, integritas, dan keaslian data. Tiga tujuan tersebut dicapai melalui kelompok algoritma yang berbeda: enkripsi simetris dan asimetris menjaga kerahasiaan, fungsi hash menjaga integritas tanpa kunci, serta HMAC dan tanda tangan digital menambahkan autentikasi. Pemilihan algoritma yang tepat untuk konteks tertentu memengaruhi keamanan sekaligus performa. Pada chapter ini kita akan mempraktikkan empat konsep inti menggunakan API bawaan JDK (java.security dan javax.crypto): hashing dengan SHA-256, enkripsi simetris dengan AES, enkripsi asimetris dengan RSA, serta key derivation dengan PBKDF2.

Hashing dengan SHA-256 #

Hash function mengubah input apa pun menjadi output panjang tetap (256 bit untuk SHA-256) yang tidak dapat dibalik. Hash cocok untuk verifikasi integritas: dua file dengan hash identik diyakini memiliki konten yang sama. Hash tidak cocok untuk menyimpan password karena hash cepat dihitung—penyerang dapat menebak jutaan password per detik menggunakan rainbow table. Untuk password, gunakan algoritma khusus seperti PBKDF2 atau bcrypt (akan dibahas pada chapter berikutnya).

kotlin SHA-256 hash sederhana
import java.security.MessageDigest

fun sha256Hex(input: String): String {
    val md = MessageDigest.getInstance("SHA-256")
    val bytes = md.digest(input.toByteArray(Charsets.UTF_8))
    return bytes.joinToString("") { "%02x".format(it) }
}

Satu perubahan kecil pada input menghasilkan hash yang sepenuhnya berbeda. Properti ini disebut avalanche effect dan menjadi ciri hash function yang baik.

HMAC: Hash dengan Kunci #

Hash biasa tidak membawa kunci, sehingga siapa pun dapat menghitungnya. HMAC (Hash-based Message Authentication Code) menambahkan kunci rahasia ke proses hashing, menghasilkan nilai yang hanya dapat dihitung oleh pihak yang memiliki kunci tersebut. HMAC dipakai untuk memverifikasi bahwa pesan tidak dimodifikasi dan memang berasal dari pengirim yang diketahui.

kotlin HMAC-SHA256
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

fun hmacSha256Hex(key: ByteArray, message: String): String {
    val mac = Mac.getInstance("HmacSHA256")
    mac.init(SecretKeySpec(key, "HmacSHA256"))
    val bytes = mac.doFinal(message.toByteArray(Charsets.UTF_8))
    return bytes.joinToString("") { "%02x".format(it) }
}

Kunci ByteArray(32) sebaiknya dihasilkan dengan SecureRandom pada produksi. Pada demo kita memakai seed stabil agar output dapat diverifikasi.

Enkripsi Simetris vs Asimetris #

Algoritma simetris memakai kunci yang sama untuk enkripsi dan dekripsi. AES adalah standar de facto, mendukung ukuran kunci 128, 192, dan 256 bit. Kunci simetris dapat didistribusikan dengan cepat tetapi tetap perlu saluran aman untuk pertukaran kunci.

Algoritma asimetris memakai sepasang kunci: kunci publik untuk enkripsi, kunci privat untuk dekripsi. RSA adalah algoritma asimetris paling umum. Operasi RSA jauh lebih lambat daripada AES, sehingga pada praktiknya RSA dipakai untuk menukar kunci sesi AES—bukan untuk enkripsi data besar secara langsung.

kotlin
// cryptography_fundamentals.kt
// Symmetric (AES) vs asymmetric (RSA). Hashing (SHA-256). HMAC. Key derivation (PBKDF2).

import java.security.KeyPairGenerator
import java.security.MessageDigest
import java.security.SecureRandom
import java.security.Signature
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.Mac
import javax.crypto.SecretKey
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec

// --- SHA-256 hash: one-way, fixed 256-bit output ---
fun sha256Hex(input: String): String {
    val md = MessageDigest.getInstance("SHA-256")
    val bytes = md.digest(input.toByteArray(Charsets.UTF_8))
    return bytes.joinToString("") { "%02x".format(it) }
}

// --- HMAC-SHA256: keyed hash for integrity/authentication ---
fun hmacSha256Hex(key: ByteArray, message: String): String {
    val mac = Mac.getInstance("HmacSHA256")
    mac.init(SecretKeySpec(key, "HmacSHA256"))
    val bytes = mac.doFinal(message.toByteArray(Charsets.UTF_8))
    return bytes.joinToString("") { "%02x".format(it) }
}

// --- AES-256 symmetric encryption with seeded key for deterministic demo ---
// Note: SHA1PRNG with setSeed gives deterministic key derivation, unlike default SecureRandom.
private val DEMO_RANDOM: SecureRandom = SecureRandom.getInstance("SHA1PRNG").apply {
    setSeed("kotlin-course-stable-seed".toByteArray())
}

fun aesDemo(): Triple<String, String, String> {
    val keyGen = KeyGenerator.getInstance("AES")
    keyGen.init(256, DEMO_RANDOM)
    val secretKey: SecretKey = keyGen.generateKey()

    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    cipher.init(Cipher.ENCRYPT_MODE, secretKey)
    val plaintext = "Transaksi: Budi transfer Rp 50000 ke Siti".toByteArray(Charsets.UTF_8)
    val ciphertext = cipher.doFinal(plaintext)

    cipher.init(Cipher.DECRYPT_MODE, secretKey, cipher.parameters)
    val decrypted = cipher.doFinal(ciphertext)

    // Ciphertext bytes are non-deterministic due to random IV in GCM mode (security feature).
    // Show only length to emphasize the symmetric encryption roundtrip.
    return Triple(
        plaintext.toString(Charsets.UTF_8),
        "len=${ciphertext.size} bytes (acak per enkripsi)",
        decrypted.toString(Charsets.UTF_8)
    )
}

// --- RSA-2048 asymmetric with seeded keypair for deterministic demo ---
fun rsaDemo(): Triple<String, String, String> {
    val keyPairGen = KeyPairGenerator.getInstance("RSA")
    keyPairGen.initialize(2048, DEMO_RANDOM)
    val keyPair = keyPairGen.generateKeyPair()

    val cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")
    cipher.init(Cipher.ENCRYPT_MODE, keyPair.public)
    val plaintext = "Rahasia bersama: kunci sesi AES".toByteArray(Charsets.UTF_8)
    val ciphertext = cipher.doFinal(plaintext)

    cipher.init(Cipher.DECRYPT_MODE, keyPair.private)
    val decrypted = cipher.doFinal(ciphertext)

    // OAEP padding uses random salt; ciphertext bytes change per encryption.
    return Triple(
        plaintext.toString(Charsets.UTF_8),
        "len=${ciphertext.size} bytes (acak per enkripsi)",
        decrypted.toString(Charsets.UTF_8)
    )
}

// --- PBKDF2 key derivation: derive a 256-bit key from a password ---
fun pbkdf2DerivedKey(password: String, salt: ByteArray): String {
    val spec = PBEKeySpec(password.toCharArray(), salt, 100_000, 256)
    val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
    val derivedKey = factory.generateSecret(spec).encoded
    return derivedKey.joinToString("") { "%02x".format(it) }
}

fun main() {
    // Hashing demo: same input -> same output, irreversible
    println("sha256('kotlin') = ${sha256Hex("kotlin")}")
    println("sha256('Kotlin') = ${sha256Hex("Kotlin")} (beda case = beda hash)")
    println()

    // HMAC: protects both integrity and authenticity
    val sharedKey = ByteArray(32).also { DEMO_RANDOM.nextBytes(it) }
    val msg1 = "transfer Rp 50000 ke Siti"
    val msg2 = "transfer Rp 500000 ke Siti"
    println("hmac(msg1) = ${hmacSha256Hex(sharedKey, msg1).take(32)}...")
    println("hmac(msg2) = ${hmacSha256Hex(sharedKey, msg2).take(32)}...")
    println()

    // AES symmetric: same key for encrypt/decrypt
    val (pt1, ct1, dt1) = aesDemo()
    println("AES plaintext   : $pt1")
    println("AES ciphertext  : $ct1")
    println("AES decrypted   : $dt1")
    println()

    // RSA asymmetric: different keys for encrypt/decrypt
    val (pt2, ct2, dt2) = rsaDemo()
    println("RSA plaintext   : $pt2")
    println("RSA ciphertext  : $ct2")
    println("RSA decrypted   : $dt2")
    println()

    // PBKDF2 key derivation: stable, salted
    val salt = ByteArray(16).also { DEMO_RANDOM.nextBytes(it) }
    println("PBKDF2 derived  : ${pbkdf2DerivedKey("rahasia-budi", salt).take(32)}...")
}

Output yang Dihasilkan:

text
sha256('kotlin') = 19a67c2a53695668e4916e9e64cf0613352ec614fd682a8bf8bf98a471db5e8d
sha256('Kotlin') = c78f6c97923e81a2f04f09c5e87b69e085c1e47066a1136b5f590bfde696e2eb (beda case = beda hash)

hmac(msg1) = fbb732577d0621d1cf086cece792e01c...
hmac(msg2) = 25276e1f585ba768bb2746e41153f6fc...

AES plaintext   : Transaksi: Budi transfer Rp 50000 ke Siti
AES ciphertext  : len=57 bytes (acak per enkripsi)
AES decrypted   : Transaksi: Budi transfer Rp 50000 ke Siti

RSA plaintext   : Rahasia bersama: kunci sesi AES
RSA ciphertext  : len=256 bytes (acak per enkripsi)
RSA decrypted   : Rahasia bersama: kunci sesi AES

PBKDF2 derived  : d9b4321ba3677c438d4d61af093324a4...

Mari kita bedah bagian penting dari kode di atas:

  1. sha256Hex: mengubah string menjadi 32 byte hash, lalu menyandikan setiap byte menjadi dua digit heksadesimal. Output selalu 64 karakter terlepas dari panjang input.
  2. hmacSha256Hex: memakai Mac.getInstance("HmacSHA256") lalu diinisialisasi dengan SecretKeySpec. Kunci dan pesan sama-sama di-encode ke UTF-8 agar konsisten lintas platform.
  3. aesDemo: KeyGenerator menghasilkan kunci 256-bit acak. Mode AES/GCM/NoPadding menyediakan authenticated encryption—enkripsi sekaligus tag autentikasi. IV acak dibangkitkan otomatis dan ikut disertakan pada cipher.parameters, sehingga dekripsi dapat memulihkan IV tanpa menyimpannya terpisah.
  4. rsaDemo: KeyPairGenerator membangkitkan pasangan kunci 2048-bit. Mode OAEPWithSHA-256AndMGF1Padding adalah padding modern yang direkomendasikan. Plaintext dibatasi sekitar 190 byte untuk RSA 2048 dengan OAEP—di sinilah letak alasan RSA tidak cocok untuk data besar.
  5. pbkdf2DerivedKey: PBEKeySpec menerima password, salt, jumlah iterasi (100.000 kali), dan panjang kunci yang diinginkan. Iterasi yang tinggi memperlambat serangan tebakan password. SecretKeyFactory menghasilkan kunci turunan yang sama setiap kali diberi input yang sama.

Key Derivation dengan PBKDF2 #

Password manusia umumnya pendek dan tidak acak. Mengubah password langsung menjadi kunci AES berbahaya karena kunci harus benar-benar acak. PBKDF2 (Password-Based Key Derivation Function 2) menjembatani kedua dunia: mengambil password dan salt, lalu melakukan iterasi hash ribuan kali untuk menghasilkan kunci yang lebih kuat.

kotlin PBKDF2 dengan 100.000 iterasi
val spec = PBEKeySpec(password.toCharArray(), salt, 100_000, 256)
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
val derivedKey = factory.generateSecret(spec).encoded

Iterasi yang tinggi memaksa penyerang yang mencoba menebak password harus melakukan komputasi sama untuk setiap tebakan—jutaan kali lebih lambat daripada hash biasa.

Pemilihan Mode dan Padding #

Pemilihan mode enkripsi sama pentingnya dengan algoritma. ECB (Electronic Codebook) tidak aman karena pola input terlihat pada output. CBC aman bila IV acak per pesan. GCM (Galois/Counter Mode) menyediakan authenticated encryption dan merupakan pilihan default yang baik. Untuk RSA, hindari padding PKCS1 lama—selalu pakai OAEP.

Referensi Lanjutan #

  • javax.crypto package — dokumentasi paket javax.crypto Java SE 21 untuk operasi enkripsi simetris dan asimetris
  • java.security package — dokumentasi paket java.security Java SE 21 untuk primitif kriptografi

Penutup #

Empat konsep pada chapter ini—hashing, HMAC, enkripsi simetris, dan asimetris—saling melengkapi untuk membentuk sistem yang aman. Pada chapter berikutnya kita akan membahas secure randomness: bagaimana membangkitkan nilai acak yang aman secara kriptografis dengan SecureRandom versus Random biasa.