Chapter Data Privacy pada section data membahas privasi dari sisi praktik pengelolaan dataset: k-anonymity, derau Laplace, dan federated learning. Section keamanan ini mengambil sudut yang berbeda namun saling melengkapi: data yang sudah “dilindungi” tetap diserang, bukan lewat kebocoran server, melainkan lewat gabungan informasi publik yang tampak tidak berbahaya. Kita akan mendemonstrasikan dua skenario nyata — join quasi-identifier yang mengembalikan nama pada tabel anonim, dan tabel pra-hitung yang meretas hash kata sandi tanpa salt — lalu menutup celahnya dengan teknik yang benar.
Ancaman pada Data yang Sudah “Anonim” #
Menghapus kolom nama sering dianggap cukup untuk membuat data aman dibagikan. Kenyataannya, kombinasi kolom yang tersisa — kota, usia, tanggal pembelian — cukup unik untuk menyempitkan identitas seseorang; kombinasi semacam itu disebut quasi-identifier. Penyerang tidak memerlukan akses ilegal: cukup satu tabel publik kecil yang memuat nama dan kolom yang sama, lalu kedua tabel digabungkan (join). Chapter Data Privacy menunjukkan cara mengukur risiko ini dengan k-anonymity; di sini kita melihat serangannya berjalan langsung.
Berkas reidentifikasi.py memuat tabel riset “anonim” sepuluh baris (kota, usia, diagnosa) dan daftar peserta seminar kesehatan yang publik (nama, kota, usia). Kedua tabel digabungkan pada pasangan (kota, usia), lalu setiap baris anonim diperiksa: berapa kandidat nama yang cocok. Bagian kedua dan ketiga berpindah ke penyimpanan kredensial: hash polos diretas oleh tabel pra-hitung kata sandi umum, sedangkan hash dengan salt per pengguna menahan serangan yang sama:
"""Privasi data sebagai masalah keamanan: re-identification lewat
quasi-identifier, lalu penyimpanan kata sandi dengan hash + salt.
Bagian 1: tabel "anonim" digabungkan dengan tabel publik kecil lewat
kombinasi kolom yang tampak tidak berbahaya (kota + usia).
Bagian 2: hash polos diretas oleh tabel pra-hitung; salt membuat
tabel itu tidak berguna.
Seluruh data fiktif (Budi, Siti, kota umum).
"""
import hashlib
# ---------- 1. Re-identification lewat quasi-identifier ----------
# "Data riset yang sudah dibuang kolom namanya"
anonim = [
("Bandung", 34, "hipertensi"),
("Bandung", 36, "flu"),
("Bandung", 61, "asma"),
("Bandung", 35, "flu"),
("Surabaya", 29, "alergi"),
("Surabaya", 63, "diabetes"),
("Surabaya", 62, "diabetes"),
("Surabaya", 33, "flu"),
("Bandung", 36, "migrain"),
("Semarang", 40, "flu"),
]
# "Data publik" dengan nama: daftar peserta seminar kesehatan
publik = [
("Budi", "Bandung", 34),
("Siti", "Bandung", 36),
("Lina", "Bandung", 36),
("Andi", "Bandung", 61),
("Tono", "Bandung", 35),
("Rina", "Surabaya", 29),
("Dewi", "Surabaya", 63),
("Wati", "Surabaya", 62),
("Joko", "Surabaya", 33),
]
peta_publik = {}
for nama, kota, usia in publik:
peta_publik.setdefault((kota, usia), []).append(nama)
print("===== SERANGAN RE-IDENTIFICATION (join kota + usia) =====")
unik = 0
for kota, usia, diagnosa in anonim:
kandidat = peta_publik.get((kota, usia), [])
if len(kandidat) == 1:
status, kesimpulan = kandidat[0], "TERIDENTIFIKASI"
unik += 1
elif kandidat:
status = "/".join(kandidat)
kesimpulan = "RAGU (kandidat > 1)"
else:
status, kesimpulan = "-", "aman (tidak ada pasangan)"
print(f"({kota:9s}, {usia:2d}) diagnosa={diagnosa:10s} "
f"-> {status:10s} [{kesimpulan}]")
print(f"baris yang kembali teridentifikasi unik: {unik}/{len(anonim)}")
print()
# ---------- 2. Hash polos vs tabel pra-hitung ----------
def hash_polos(teks):
return hashlib.sha256(teks.encode()).hexdigest()
kata_sandi = {"budi": "kucing123",
"siti": "sandiRahasia79",
"tono": "kucing123"}
# penyerang membawa tabel pra-hitung kata sandi umum (rainbow table mini)
umum = ["123456", "password", "kucing123", "qwerty123", "iloveyou"]
tabel_prahitung = {hash_polos(w): w for w in umum}
print("===== PENYIMPANAN HASH POLOS (tanpa salt) =====")
for pengguna, sandi in kata_sandi.items():
ringkas = hash_polos(sandi)[:16]
retak = tabel_prahitung.get(hash_polos(sandi))
hasil = f"RETAK -> '{retak}'" if retak else "tahan (tidak ada di tabel)"
print(f"{pengguna}: sha256={ringkas}... {hasil}")
print("dua pengguna memakai kata sandi sama -> dua hash identik, "
"retak sekali maka retak semua")
print()
# ---------- 3. Hash + salt: satu garam acak per pengguna ----------
garam = {"budi": "9f13c2a1b47d5e60",
"siti": "2c8e4d7f1a3b6c90",
"tono": "77a1b2c3d4e5f601"}
def hash_bergaram(teks, salt):
return hashlib.sha256((salt + teks).encode()).hexdigest()
print("===== PENYIMPANAN HASH + SALT =====")
for pengguna, sandi in kata_sandi.items():
h = hash_bergaram(sandi, garam[pengguna])
retak = tabel_prahitung.get(h)
hasil = f"RETAK -> '{retak}'" if retak else "tahan"
print(f"{pengguna}: sha256(salt+sandi)={h[:16]}... {hasil}")
print("kata sandi sama ('kucing123') -> hash budi dan tono berbeda")
print("tabel pra-hitung penyerang tidak cocok: ia harus menghitung "
"ulang per salt")Output yang Dihasilkan:
===== SERANGAN RE-IDENTIFICATION (join kota + usia) =====
(Bandung , 34) diagnosa=hipertensi -> Budi [TERIDENTIFIKASI]
(Bandung , 36) diagnosa=flu -> Siti/Lina [RAGU (kandidat > 1)]
(Bandung , 61) diagnosa=asma -> Andi [TERIDENTIFIKASI]
(Bandung , 35) diagnosa=flu -> Tono [TERIDENTIFIKASI]
(Surabaya , 29) diagnosa=alergi -> Rina [TERIDENTIFIKASI]
(Surabaya , 63) diagnosa=diabetes -> Dewi [TERIDENTIFIKASI]
(Surabaya , 62) diagnosa=diabetes -> Wati [TERIDENTIFIKASI]
(Surabaya , 33) diagnosa=flu -> Joko [TERIDENTIFIKASI]
(Bandung , 36) diagnosa=migrain -> Siti/Lina [RAGU (kandidat > 1)]
(Semarang , 40) diagnosa=flu -> - [aman (tidak ada pasangan)]
baris yang kembali teridentifikasi unik: 7/10
===== PENYIMPANAN HASH POLOS (tanpa salt) =====
budi: sha256=87a858578d3ec546... RETAK -> 'kucing123'
siti: sha256=9025a03d1b339d6f... tahan (tidak ada di tabel)
tono: sha256=87a858578d3ec546... RETAK -> 'kucing123'
dua pengguna memakai kata sandi sama -> dua hash identik, retak sekali maka retak semua
===== PENYIMPANAN HASH + SALT =====
budi: sha256(salt+sandi)=f7fe677f23a5eab9... tahan
siti: sha256(salt+sandi)=1c029f4cfa998a40... tahan
tono: sha256(salt+sandi)=0fec47a91584c14f... tahan
kata sandi sama ('kucing123') -> hash budi dan tono berbeda
tabel pra-hitung penyerang tidak cocok: ia harus menghitung ulang per saltContoh di atas diuji nyata dengan Python 3.12 dan modul standar hashlib, selesai dalam waktu di bawah satu detik. Mari kita telaah kode di atas baris per baris:
- Baris 15-26 (tabel anonim): sepuluh baris data riset tanpa kolom nama; kolom kota dan usia tetap ada dan justru menjadi kunci serangan — persis kolom yang biasanya dianggap tidak sensitif.
- Baris 29-43 (tabel publik): daftar peserta seminar memuat nama plus quasi-identifier yang sama;
peta_publikmembangun kamus dari pasangan (kota, usia) ke daftar nama, menirukan langkah persiapan paling sederhana milik penyerang. - Baris 46-59 (join): setiap baris anonim dicocokkan ke kamus; tujuh dari sepuluh baris hanya punya satu kandidat sehingga statusnya
TERIDENTIFIKASI, dua baris ragu karena dua kandidat, dan hanya satu baris aman karena quasi-identifier-nya tidak muncul di tabel publik. - Baris 63-72 (hash polos):
hash_polosmenghitung SHA-256 langsung dari kata sandi;tabel_prahitungberisi hash lima kata sandi umum — model kecil dari rainbow table yang bisa diunduh siapa pun. - Baris 75-80 (hasil retasan): kata sandi
kucing123milik Budi dan Tono menghasilkan hash identik87a858...dan keduanya retak sekali jalan;sandiRahasia79tahan hanya karena kebetulan tidak ada di tabel, bukan karena mekanismenya aman. - Baris 85-96 (hash + salt): setiap pengguna diberi garam berbeda yang disimpan bersama hash; kata sandi sama kini menghasilkan dua hash berbeda sehingga tabel pra-hitung tidak bisa dipakai ulang antar akun — penyerang wajib menghitung ulang dari nol untuk tiap salt.
Penyimpanan Aman dan Akses Minimal #
Angka output memberi tiga pelajaran sekaligus. Pertama, anonimitas bukan properti satu tabel melainkan properti gabungan seluruh data yang beredar — tujuh dari sepuluh baris “anonim” kembali bernama hanya dengan join sederhana. Kedua, hash tanpa salt menghukum pengguna yang memakai kata sandi populer dan menghukum pengguna lain yang kebetulan memilih kata sandi yang sama. Ketiga, salt murah (satu string acak per akun) namun memaksa penyerang menghitung ulang seluruh tabel pra-hitung untuk setiap akun yang diserang. Untuk penyimpanan nyata, fungsi hash kata sandi yang dirancang lambat seperti bcrypt atau Argon2 lebih tepat daripada SHA-256, karena kecepatan justru merugikan pihak pembela; dokumentasi hashlib merinci pilihan yang tersedia di pustaka standar.
Data yang tersimpan aman belum menjamin keamanan setelah model dilatih: model itu sendiri bisa menjadi saluran bocoran. Dua chapter berikutnya membuktikannya dari dua arah — Model Inversion dan Data Leakage mengekstraksi keanggotaan data latih dari keluaran model, sementara bab berikutnya menunjukkan bahwa input yang dirancang khusus dapat mengubah prediksi tanpa menyentuh data sama sekali.