Kriptografi berdiri di atas satu bahan baku yang tampak sepele: angka acak. Token sesi, kata sandi sekali pakai, salt, IV untuk AES-GCM, kunci API — semuanya hanya sekuat keacakan yang membangkitkannya. Java menyediakan dua generator angka acak yang tinggal di paket berbeda: java.util.Random yang dirancang untuk simulasi dan pengujian, dan java.security.SecureRandom yang dirancang untuk menahan penyerang yang tahu persis bagaimana generator itu bekerja. Kekeliruan memilih yang pertama untuk keperluan keamanan termasuk celah yang paling sering muncul di laporan audit — dan paling fatal dampaknya. Bab ini membuktikan sendiri sifat kedua generator itu lewat program yang bisa Anda jalankan ulang, lalu menetapkan aturan praktis membangkitkan token yang layak dipakai di produksi.

Random itu Deterministik dan Itu Persis Masalahnya #

java.util.Random adalah pseudorandom number generator (PRNG): deret angkanya tampak acak, tetapi deret itu sepenuhnya ditentukan oleh satu angka awal bernama seed. Mesin internalnya menyimpan status 48-bit; setiap panggilan nextInt() mengubah status itu dengan rumus matematika tetap. Konsekuensinya sederhana namun dahsyat: siapa pun yang mengetahui seed — atau bahkan hanya beberapa puluh nilai keluaran berurutan — dapat merekonstruksi seluruh status dan memprediksi semua angka berikutnya, termasuk angka yang belum keluar. Program berikut membuktikannya dengan dua instance ber-seed sama:

import java.security.SecureRandom;
import java.util.Random;

public class PembuatToken {

    public static void main(String[] args) {
        // Bagian 1: Random ber-seed sepenuhnya deterministik.
        Random generatorA = new Random(42);
        Random generatorB = new Random(42);

        System.out.println("=== java.util.Random dengan seed sama (dua instance) ===");
        for (int i = 1; i <= 3; i++) {
            System.out.println("nilai ke-" + i + ": " + generatorA.nextInt(1_000_000)
                    + " vs " + generatorB.nextInt(1_000_000));
        }
        System.out.println("Kedua deret identik: bila seed bocor, seluruh deret bisa diprediksi.");

        // Bagian 2: SecureRandom tanpa seed - tidak diprediksi.
        SecureRandom acakAman = new SecureRandom();
        byte[] token = new byte[16];
        acakAman.nextBytes(token);

        System.out.println();
        System.out.println("=== SecureRandom: token sesi 16 byte ===");
        System.out.println("Panjang token   : " + (token.length * 8) + " bit");
        System.out.println("Isi token       : <tidak dicetak - berbeda pada tiap eksekusi>");
        System.out.println("Sumber acak     : entropi sistem operasi, tanpa seed aplikasi");
    }
}

Output yang Dihasilkan:

text
=== java.util.Random dengan seed sama (dua instance) ===
nilai ke-1: 431130 vs 431130
nilai ke-2: 392763 vs 392763
nilai ke-3: 211248 vs 211248
Kedua deret identik: bila seed bocor, seluruh deret bisa diprediksi.

=== SecureRandom: token sesi 16 byte ===
Panjang token   : 128 bit
Isi token       : <tidak dicetak - berbeda pada tiap eksekusi>
Sumber acak     : entropi sistem operasi, tanpa seed aplikasi

Bagian pertama output adalah pengakuan yang jujur: dua instance Random(42) menghasilkan deret yang identik persis — 431130, 392763, 211248 — karena keduanya memulai dari seed sama dan mengikuti rumus transisi yang sama. Determinisme ini berguna untuk pengujian (bab-bab algoritma di Course ini kerap memakainya agar output teruji dapat dibandingkan), tetapi untuk keamanan determinisme adalah kegagalan total: token “acak” yang bisa diprediksi bukan pertahanan sama sekali. Sejarah mencatat layanan nyata yang reset kata sandi bisa diprediksi karena generator acaknya di-seed dengan waktu sistem — nilai yang bisa ditebak dalam hitungan milidetik.

Bedah baris-per-baris untuk bagian yang paling penting:

  1. Baris 9-10 (new Random(42) dua kali): seed yang sama selalu menghasilkan deret yang sama — inilah kontrak PRNG. Bila aplikasi membangkitkan token dengan Random yang di-seed waktu sistem (System.currentTimeMillis()), penyerang yang tahu kira-kira kapan akun dibuat cukup mencoba beberapa ribu kandidat seed.
  2. Baris 24-25 (new SecureRandom() tanpa seed): constructor tanpa argumen meminta JVM mengambil entropi dari sistem operasi (di Linux lewat getrandom//dev/urandom). Tidak ada seed aplikasi yang bisa bocor, karena aplikasi tidak pernah menetapkannya.
  3. Baris 27 (acakAman.nextBytes(token)): method ini mengisi seluruh array token dengan byte acak secara kriptografis. Kita sengaja tidak mencetak isinya — nilai itu berbeda pada setiap eksekusi, dan mencetak token ke output adalah kebiasaan buruk yang tidak akan kita mulai sekarang.

Mengukur Kekuatan Token #

Kata “128 bit” pada output bukan sekadar satuan panjang — kata itu mengukur ruang kemungkinan yang harus dijelajahi penyerang secara membabi buta. Token 16 byte memiliki 2¹²⁸ kemungkinan nilai; mesin yang mencoba satu miliar kandidat per detik butuh sekitar 10²² tahun untuk menyapu seluruhnya. Perbandingannya perlu dipahami secara relatif:

Ukuran tokenJumlah kemungkinanPenilaian
4 byte (32 bit)2³² ≈ 4,3 miliarSekali sapuan GPU modern; tidak layak untuk keamanan
8 byte (64 bit)2⁶⁴ ≈ 1,8 × 10¹⁹Cukup untuk ID non-rahasia (ID pesan, ID pelacakan)
16 byte (128 bit)2¹²⁸ ≈ 3,4 × 10³⁸Standar untuk token sesi, salt, dan API key

Panduan praktisnya: nilai yang dirahasiakan (token sesi, kata sandi sekali pakai) membutuhkan 128 bit keacakan; nilai yang hanya perlu unik tanpa dirahasiakan cukup 64 bit. Panjang tanpa entropi tidak menolong — token 32 karakter yang dibangkitkan dari Random ber-seed tetap hanya memiliki ruang kemungkinan sekecil seed-nya.

Dua kebiasaan penutup melengkapi pemakaian SecureRandom. Pertama, buat satu instance dan pakai ulang — objek SecureRandom aman untuk dipakai lintas thread, dan membuatkannya ulang tiap permintaan justru membebani sumber entropi. Kedua, saat membandingkan token yang dikirim pengguna dengan token tersimpan (misal tautan reset kata sandi), gunakan MessageDigest.isEqual() yang berjalan waktu konstan — perbandingan String.equals() yang berhenti pada byte pertama yang berbeda bocorkan informasi seberapa banyak awalan token yang sudah benar.

Dengan sumber acak yang aman di tangan, kita siap membangun lapisan autentikasi yang menyentuh pengguna langsung: kata sandi. Bab berikutnya membahas Password Hashing — mengapa SHA-256 polos justru pilihan buruk untuk kata sandi, dan bagaimana PBKDF2 dengan salt unik memperlambat tebakan penyerang hingga tidak lagi layak dicoba.

Referensi #

Berkas Sumber

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

PembuatToken.java

Unduh berkas
java
import java.security.SecureRandom;
import java.util.Random;

public class PembuatToken {

    public static void main(String[] args) {
        // Bagian 1: Random ber-seed sepenuhnya deterministik.
        Random generatorA = new Random(42);
        Random generatorB = new Random(42);

        System.out.println("=== java.util.Random dengan seed sama (dua instance) ===");
        for (int i = 1; i <= 3; i++) {
            System.out.println("nilai ke-" + i + ": " + generatorA.nextInt(1_000_000)
                    + " vs " + generatorB.nextInt(1_000_000));
        }
        System.out.println("Kedua deret identik: bila seed bocor, seluruh deret bisa diprediksi.");

        // Bagian 2: SecureRandom tanpa seed - tidak diprediksi.
        SecureRandom acakAman = new SecureRandom();
        byte[] token = new byte[16];
        acakAman.nextBytes(token);

        System.out.println();
        System.out.println("=== SecureRandom: token sesi 16 byte ===");
        System.out.println("Panjang token   : " + (token.length * 8) + " bit");
        System.out.println("Isi token       : <tidak dicetak - berbeda pada tiap eksekusi>");
        System.out.println("Sumber acak     : entropi sistem operasi, tanpa seed aplikasi");
    }
}