Pemilihan angka acak adalah fondasi banyak mekanisme keamanan: token sesi, nonce kriptografi, salt password, session ID. Bila angka “acak” tersebut sebenarnya dapat diprediksi, penyerang dapat menebak token sah dan mengambil alih akun. Pada chapter ini kita membandingkan dua sumber angka acak di C: rand() dari pustaka standar yang deterministik dan tidak aman untuk kriptografi, serta /dev/urandom yang menyediakan entropi kernel dan aman untuk sebagian besar kasus kriptografi.
rand(): PRNG Deterministik #
rand() adalah Pseudo-Random Number Generator (PRNG) berbasis rumus matematika. Output sepenuhnya ditentukan oleh seed awal yang diatur dengan srand(). Bila kita memberi seed yang sama, output selalu identik—sifat yang berguna untuk reproduksi hasil simulasi tetapi fatal untuk keamanan. Selain itu, algoritma rand() bervariasi antar implementasi pustaka (glibc, musl, MSVC) sehingga tidak ada jaminan kualitas statistik.
/* random_number_generation.c */
/* Bandingkan rand() yang tidak aman dengan /dev/urandom. */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <time.h>
/* rand() — PRNG deterministik, TIDAK aman untuk kriptografi. */
static void demo_rand(void) {
srand(42); /* seed tetap supaya output reproducible */
printf("[rand] seed=42: ");
for (int i = 0; i < 5; i++) {
printf("%d ", rand() % 100);
}
printf("\n");
/* Karena seed tetap, hasilnya selalu sama — bahaya untuk kunci/token. */
srand(42);
printf("[rand] ulang seed=42: ");
for (int i = 0; i < 5; i++) {
printf("%d ", rand() % 100);
}
printf("\n");
}
/* /dev/urandom — sumber entropi kernel, AMAN untuk kriptografi. */
static int demo_urandom(void) {
int fd = open("/dev/urandom", O_RDONLY);
if (fd < 0) {
fprintf(stderr, "[urandom] open gagal: %s\n", strerror(errno));
return -1;
}
unsigned char buf[8];
ssize_t got = read(fd, buf, sizeof buf);
close(fd);
if (got != (ssize_t)sizeof buf) {
fprintf(stderr, "[urandom] read tidak lengkap\n");
return -1;
}
printf("[urandom] 8 byte acak: ");
for (size_t i = 0; i < sizeof buf; i++) {
printf("%02x", buf[i]);
}
printf("\n");
return 0;
}
/* Bandingkan: dua pemanggilan urandom menghasilkan nilai berbeda. */
static void demo_urandom_unique(void) {
printf("[urandom] panggilan kedua: ");
int fd = open("/dev/urandom", O_RDONLY);
if (fd < 0) return;
unsigned char buf[8];
if (read(fd, buf, sizeof buf) == (ssize_t)sizeof buf) {
for (size_t i = 0; i < sizeof buf; i++) printf("%02x", buf[i]);
printf("\n");
}
close(fd);
}
int main(void) {
printf("=== 1. rand() — PRNG deterministik ===\n");
demo_rand();
printf("\n=== 2. /dev/urandom — entropi kernel ===\n");
demo_urandom();
demo_urandom_unique();
printf("\n=== Catatan ===\n");
printf("- rand() cocok untuk simulasi, BUKAN untuk token/kunci/sesi.\n");
printf("- /dev/urandom aman untuk sebagian besar kasus kriptografi.\n");
printf("- Untuk portabilitas, pertimbangkan getrandom() (Linux) atau libsodium.\n");
return 0;
}Output yang Dihasilkan:
=== 1. rand() — PRNG deterministik ===
[rand] seed=42: 66 40 81 41 12
[rand] ulang seed=42: 66 40 81 41 12
=== 2. /dev/urandom — entropi kernel ===
[urandom] 8 byte acak: ddffdbcdaf34204f
[urandom] panggilan kedua: a43ee750e5dd2728
=== Catatan ===
- rand() cocok untuk simulasi, BUKAN untuk token/kunci/sesi.
- /dev/urandom aman untuk sebagian besar kasus kriptografi.
- Untuk portabilitas, pertimbangkan getrandom() (Linux) atau libsodium.Mari kita telaah baris-baris kode di atas:
- Baris 11-25 (
demo_rand):srand(42)menetapkan seed PRNG ke42. Lima angka pertama darirand() % 100adalah66 40 81 41 12. Pemanggilan kedua dengan seed yang sama menghasilkan urutan identik—setiap kali program dijalankan dengan seed42, output selalu sama. Sifat deterministik inilah yang membuatrand()tidak boleh dipakai untuk token sesi atau kunci: penyerang cukup menebak seed (misalnyatime(NULL)) untuk memprediksi seluruh urutan. - Baris 28-46 (
demo_urandom):/dev/urandomadalah device file yang dipelihara kernel Linux. Setiap kali dibaca, kernel mengembalikan byte acak yang dihasilkan dari entropy pool—campuran noise hardware (interrupt timing, RDRAND, jitter). Dua pemanggilan menghasilkan dua nilai yang berbeda (ddffdbcdaf34204flalua43ee750e5dd2728)—tidak ada determinisme yang dapat dieksploitasi penyerang. - Baris 27 (
int fd = open(...)): Membuka/dev/urandommemerlukan satu syscallopenlaluread. Bila program membaca acak berkali-kali, lebih efisien membiarkan fd terbuka dan membaca berulang—tidak ada biaya tambahan per read. - Baris 39 (
got != sizeof buf): Pemeriksaan jumlah byte yang dibaca.readdapat mengembalikan lebih sedikit byte dari yang diminta—wajib periksa dan ulangi bila perlu. Untuk/dev/urandomdi Linux,readselalu mengembalikan penuh tetapi defensiveness tetap baik. - Output (Catatan): Tiga kategori penggunaan.
rand()untuk simulasi/permainan non-keamanan./dev/urandomuntuk sebagian besar kasus kriptografi.getrandom()syscall (Linux 3.17+) atau pustaka pihak ketiga seperti libsodium untuk portabilitas.
Mengapa time(NULL) sebagai Seed Berbahaya #
Pola umum yang sering dijumpai:
/* BERBAHAYA */
srand(time(NULL));
int token = rand();Penyerang tahu bahwa program dipanggil pada waktu tertentu (dalam beberapa detik). time(NULL) mengembalikan jumlah detik sejak Unix epoch—hanya sekitar 1,5 juta kemungkinan seed dalam sehari. Penyerang dapat menebak semua kemungkinan seed dalam hitungan menit dan menemukan token yang dihasilkan program kita.
Token Sesi 128-bit dari /dev/urandom #
Implementasi praktis: hasilkan 16 byte (128 bit) entropi dari /dev/urandom, encode ke hex, gunakan sebagai token sesi:
/* extra_random_number_generation.c — token sesi 16 byte (128 bit). */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
/* Hasilkan token sesi 16 byte (128 bit entropi). */
static int generate_session_token(unsigned char *out, size_t n) {
int fd = open("/dev/urandom", O_RDONLY);
if (fd < 0) return -1;
size_t got = 0;
while (got < n) {
ssize_t r = read(fd, out + got, n - got);
if (r <= 0) { close(fd); return -1; }
got += (size_t)r;
}
close(fd);
return 0;
}
/* Encode byte ke hex string (2 char per byte + null). */
static void to_hex(const unsigned char *in, size_t n, char *out) {
const char *hex = "0123456789abcdef";
for (size_t i = 0; i < n; i++) {
out[2*i] = hex[(in[i] >> 4) & 0xF];
out[2*i + 1] = hex[in[i] & 0xF];
}
out[2*n] = '\0';
}
int main(void) {
unsigned char raw[16];
if (generate_session_token(raw, sizeof raw) != 0) {
fprintf(stderr, "gagal: %s\n", strerror(errno));
return 1;
}
char hex[2 * sizeof raw + 1];
to_hex(raw, sizeof raw, hex);
printf("Session token (128 bit, hex): %s\n", hex);
printf("Panjang: %zu karakter = %zu bit entropi\n",
strlen(hex), sizeof raw * 8);
return 0;
}Output yang Dihasilkan:
Session token (128 bit, hex): 8b33798388d22094bbf9c534292ab452
Panjang: 32 karakter = 128 bit entropiToken 128-bit dari /dev/urandom memberikan entropi yang cukup untuk melawan brute force—jumlah kemungkinan adalah 2^128 ≈ 3.4 × 10^38, terlalu besar untuk ditebak bahkan dengan ribuan GPU. Encode hex (base16) menghasilkan 32 karakter ASCII yang aman untuk transmisi HTTP, cookie, atau URL.
Mendapatkan Entropi yang Aman: Tiga Pilihan #
1. /dev/urandom (Linux, BSD, macOS) #
Paling portabel antar Unix-like. Buka sekali, baca berkali-kali.
static int urandom_fd = -1;
void random_bytes(void *buf, size_t n) {
if (urandom_fd < 0) urandom_fd = open("/dev/urandom", O_RDONLY);
size_t got = 0;
while (got < n) {
ssize_t r = read(urandom_fd, (char*)buf + got, n - got);
if (r <= 0) { /* error */ continue; }
got += (size_t)r;
}
}2. getrandom() (Linux 3.17+) #
Syscall langsung tanpa membuka file descriptor—menghindari kebocoran fd ke proses anak dan masalah saat /dev belum tersedia pada early boot.
#include <sys/random.h>
ssize_t got = getrandom(buf, sizeof buf, GRND_NONBLOCK);3. libsodium (cross-platform) #
Pustaka pihak ketiga yang menyediakan API sederhana dan portable, termasuk Windows:
#include <sodium.h>
randombytes_buf(buf, sizeof buf);libsodium mengabstraksi detail platform dan menyediakan implementasi yang telah diaudit.
Rand() dalam Crypto: Studi Kasus Kerentanan Nyata #
Pada 1996, Netscape SSL 2.0 dirusak karena memakai kombinasi getpid() dan time(NULL) sebagai seed PRNG. Penyerang dapat menebak PID (hanya 32.768 kemungkinan) dan waktu (presisi detik) lalu memutar ulang PRNG untuk menemukan master secret SSL. Studi kasus ini menjadi pelajaran klasik tentang pentingnya entropi yang kuat. CVE-2020-12800 pada sistem tertentu juga melibatkan pemakaian rand() untuk ID sesi yang dapat diprediksi.