Keamanan Siber

Autentikasi Dua Langkah TOTP untuk Aplikasi PHP Kecil

Autentikasi Dua Langkah TOTP untuk Aplikasi PHP Kecil

Sebuah password saja melindungi akun hanya selama password itu tetap rahasia. Password yang dipakai ulang bocor di satu peretasan, lalu dicoba di tempat lain. Time-based one-time password (TOTP) menambahkan "sesuatu yang kamu miliki" pada "sesuatu yang kamu ketahui": kode yang berubah setiap tiga puluh detik dan bergantung pada shared secret yang tersimpan di aplikasi authenticator milik pengguna. Bagi aplikasi PHP kecil, daya tariknya nyata, tetapi ruang untuk kesalahan halus juga besar. Algoritmanya terlihat pendek, tetapi clock drift, penyimpanan secret, dan penanganan replay bisa menentukan apakah fitur ini membantu atau justru menjadi permukaan serangan baru.

Artikel ini menguraikan bagaimana TOTP dihitung, bagaimana aplikasi kecil bisa memverifikasi sebuah kode tanpa melemahkan keamanannya sendiri, serta di mana letak batas jujur dari pendekatan ini. Tulisan ini tidak berasumsi pada identity provider tingkat perusahaan, dan bukan tutorial yang mengklaim sudah dipasang di produksi. Kode yang ditampilkan di bawah ditulis dan diperiksa terhadap test vector yang diterbitkan di dalam standar itu sendiri.

Mengapa faktor kedua mengurangi ketergantungan pada password

Banyak akun dibobol bukan lewat eksploit yang eksotis, melainkan lewat password yang lemah, dipakai ulang, atau terbocor dalam suatu insiden. OWASP Multifactor Authentication Cheat Sheet merangkum masalahnya dengan sudut pandang developer: asumsikan password akan dibobol suatu saat, lalu rancang sistem agar tahan terhadap hal itu. Mengharuskan kode yang berubah seiring waktu berarti password yang dicuri atau ditebak tak lagi cukup sendirian.

Sebisa mungkin kita perlu memperjelas apa arti "sesuatu yang kamu miliki". TOTP adalah faktor berbasis kepemilikan: pengguna memegang perangkat atau aplikasi yang bisa menghasilkan kode shared secret. Meminta dua hal yang sama-sama pengguna ketahui, misalnya password dan PIN, bukan autentikasi multifaktor, karena kedua faktor itu bisa dibobol lewat jenis serangan yang sama. Faktor-faktornya harus saling independen.

Independensi itu tidak mutlak. Kode TOTP tetap harus diketik di suatu tempat, sehingga phishing tetap mungkin terjadi ketika pengguna ditipu memasukkan kode yang masih aktif ke halaman palsu. TOTP secara luas dianggap peningkatan yang berarti dibandingkan password saja, tetapi ia tidak tahan-phishing seperti token challenge-response berbasis perangkat keras atau WebAuthn.

Apa yang sebenarnya dihitung TOTP

TOTP bukan penemuan yang terpisah. Ia adalah bentuk berbasis waktu dari algoritma one-time password berbasis counter bernama HOTP, yang dibakukan dalam RFC 4226. HOTP didefinisikan sebagai nilai HMAC yang dipotong:

HOTP(K, C) = Truncate(HMAC-SHA-1(K, C))

Di sini K adalah shared secret dan C adalah counter. RFC 6238 mendefinisikan TOTP dengan mengganti counter itu menjadi nilai yang diturunkan dari waktu saat ini. Secara konkret, faktor penggeraknya adalah

T = floor((current_unix_time - T0) / X)

dengan X adalah time step dan T0 adalah waktu saat penghitungan dimulai. Nilai default dalam standar adalah time step 30 detik dan T0 nol (Unix epoch). Hasilnya, aplikasi authenticator dan server sama-sama menurunkan kode enam digit yang sama untuk jendela tiga puluh detik yang sama, karena keduanya berbagi secret yang sama dan konsep waktu yang sama.

RFC 6238 mencatat bahwa implementasi boleh memakai HMAC-SHA-256 atau HMAC-SHA-512 selain SHA-1 yang dipakai konstruksi HOTP asli. Pilihan ini terutama memengaruhi cara shared secret diukur dan seberapa luas aplikasi diharapkan saling terhubung.

Shared secret adalah jantung sistem

Semuanya bertumpu pada sebuah secret yang dibagi antara aplikasi authenticator dan server. Jika secret itu bocor, seluruh faktor kedua ikut bocor, karena siapa pun yang memegang secret bisa mereproduksi semua kode di masa depan. RFC 4226 menetapkan panjang secret minimum 128 bit dan merekomendasikan 160 bit. RFC 6238 menambahkan bahwa kunci harus dipilih acak, mengikuti panduan keacakan dalam RFC 4086, dan disimpan aman di sistem validasi.

Di PHP, sumber acak yang aman secara kriptografis tersedia lewat random_bytes(), yang menurut manual cocok untuk secret jangka panjang seperti encryption key. Praktik umum adalah membuat secret sekali saat pendaftaran, menampilkannya ke pengguna agar bisa dimasukkan ke aplikasi authenticator, lalu menyimpan salinan otoritatifnya di sisi server.

Di sinilah aplikasi kecil paling sering memotong jalan. Menyimpan secret mentah di kolom database yang bisa dijangkau database yang di-dump, cadangan, atau query baca saja berarti membocorkan semua kode yang dihasilkan. Salah satu mitigasi umum adalah mengenkripsi shared secret agar hanya aplikasi, yang memegang kunci dekripsi, yang bisa membacanya. Cara lain adalah meletakkan secret di lokasi dengan akses terbatas, terpisah dari nilai yang akan diganti jika dicurigai kompromi. Poin yang jujur adalah secret akan tetap ada dalam bentuk plaintext saat aplikasi memakainya untuk memverifikasi kode; tujuannya adalah menghilangkan salinan yang tak perlu dan pembaca yang tak sah.

Memverifikasi kode di PHP

Untuk memverifikasi kode yang dikirim, server menghitung ulang TOTP untuk time step saat ini lalu membandingkannya dengan nilai yang dimasukkan pengguna. Fungsi berikut mengimplementasikan algoritma RFC 6238 dengan hash_hmac(). Fungsi ini menerima shared secret sebagai byte biner mentah, menghitung time step, lalu menerapkan dynamic truncation yang dijelaskan RFC 4226:

<?php
declare(strict_types=1);

function currentTotp(string $secretBinary, int $timeStep = 30, int $digits = 6, string $algo = 'sha1'): string
{
    $counter = intdiv($time(), $timeStep);
    // Moving factor berupa integer big-endian 8 byte.
    $data = pack('NN', $counter >> 32, $counter & 0xFFFFFFFF);
    $hash = hash_hmac($algo, $data, $secretBinary, true);

    // Dynamic truncation: ambil offset dari byte terakhir.
    $offset = ord($hash[strlen($hash) - 1]) & 0x0f;
    $binary =
        ((ord($hash[$offset]) & 0x7f) << 24) |
        ((ord($hash[$offset + 1]) & 0xff) << 16) |
        ((ord($hash[$offset + 2]) & 0xff) << 8) |
        (ord($hash[$offset + 3]) & 0xff);

    $otp = $binary % (10 ** $digits);
    return str_pad((string) $otp, $digits, '0', STR_PAD_LEFT);
}

Catatan tentang ketepatan: faktor penggerak harus diperlakukan sebagai integer big-endian delapan byte, dan kunci serta pesan HMAC harus berupa biner mentah, bukan teks hex. Salah satunya salah saja akan menghasilkan kode yang tidak cocok dengan aplikasi authenticator resmi. Versi di atas aku tulis sebagai script mandiri dan memeriksanya terhadap nilai contoh di RFC 6238 Appendix B; sembilan test vector (SHA-1 dan SHA-256) semuanya cocok.

Verifikasi sebaiknya membandingkan string dalam waktu konstan sehingga tebakan yang salah tidak membocorkan seberapa dekat tebakan itu. PHP menyediakan hash_equals() untuk tujuan ini. Manual memperingatkan bahwa string yang dikirim pengguna harus menjadi argumen kedua dan kedua string harus sama panjang:

if (hash_equals($expected, $submitted)) {
    // kode benar
}

Clock drift butuh jendela toleransi yang kecil

Jam yang sama-sama dipakai adalah asumsi tersembunyi. Jika jam ponsel pengguna meleset bahkan semenit, aplikasi dan server tidak lagi sepakat pada time step saat ini, dan kode yang valid mulai ditolak. RFC 6238 merekomendasikan validator mempertimbangkan jendela kecil beberapa time step di sekitar saat ini untuk menoleransi pergeseran itu.

Standar itu merekomendasikan paling banyak satu time step untuk penundaan jaringan, dan merekomendasikan tiga puluh detik sebagai langkah default untuk menyeimbangkan keamanan dan kemudahan. Jendela yang lebih besar membuat login lebih toleran tetapi juga memperpanjang masa kode yang tertangkap tetap bisa dipakai. Keseimbangan ini adalah keputusan produk yang bergantung pada apakah kode melintasi saluran tepercaya.

Satu konsekuensi halus dari jendela adalah resynchronization sebaiknya diingat per akun. Jika jam pengguna konsisten dua langkah lebih cepat, server bisa mencatat offset itu dan memvalidasi terhadap langkah yang sudah dikoreksi. Tanpa pencatatan itu, setiap login membayar biaya memeriksa beberapa langkah.

Setiap kode harus sekali pakai

RFC 6238 secara eksplisit menyatakan verifier TIDAK BOLEH menerima one-time password yang sama dua kali. Dalam satu time step yang sama kode yang dihasilkan sama berulang, sehingga kode yang tertangkap dan diputar ulang segera akan lolos selamanya tanpa penjagaan itu. Karena itu server perlu mengingat langkah mana yang sudah terpakai dan menolak kode berulang meskipun masih valid secara teknis.

Ada beberapa cara melakukan ini di sistem kecil. Untuk TOTP, cara paling sederhana adalah melacak, per akun, time step terakhir yang diterima dan menolak kode apa pun yang langkahnya tidak lebih baru dari yang tercatat. Ini mirip aturan sekali-pakai sisi klien yang dijelaskan RFC 4226 untuk varian berbasis counter.

Persyaratan sekali pakai ini terpisah dari pilihan jumlah digit. RFC 4226 mewajibkan setidaknya enam digit dan menyarankan tujuh atau lebih untuk keamanan yang lebih kuat. OWASP pun merekomendasikan mempertimbangkan kode delapan digit atau lebih jika kemudahan mengizinkan. Pertukaran keyspace adalah aritmetika sederhana: kode enam digit punya sekitar satu juta kemungkinan, sehingga server yang tidak membatasi percobaan akan mengundang serangan brute-force lewat volume murni.

Batasi percobaan, karena kode bisa ditebak dari volumenya

Bahkan dengan perbandingan waktu konstan, penyerang yang bisa mengirim banyak kode bisa menyapu ruang nilai enam digit. RFC 4226 membahas ini langsung, merekomendasikan parameter throttling yang membatasi jumlah percobaan verifikasi yang gagal, dan mencatat throttle harus melintasi sesi login untuk mengalahkan tebakan paralel. OWASP menggemakan ini dengan syarat menerapkan batas percobaan yang ketat pada kode sekali pakai.

Di sinilah server harus hati-hati agar tidak menciptakan penolakan layanannya sendiri. Rate limiting per akun masuk akal, tetapi mengunci akun secara global setelah beberapa kegagalan bisa membuat pengunjung mana pun mengunci keluar pengguna sah dengan sengaja mengetik salah. Ketika password yang dimasukkan benar tetapi faktor kedua berulang gagal, situasi itu bisa berarti password sudah terkompromi, sehingga aplikasi sebaiknya bereaksi lebih dari sekadar penolakan diam-diam.

Mendaftarkan authenticator baru

Provisioning biasanya dimulai dengan server membuat secret dan pengguna memindai kode QR. Format key URI yang didokumentasikan Google Authenticator yang banyak dipakai adalah teks sederhana yang mengodekan tipe, label akun, dan parameter:

otpauth://totp/Example:[email protected]?secret=BASE32SECRET&issuer=Example

Nilai secret dienkode Base32 sesuai RFC 3548, padding dihilangkan. Issuer sangat disarankan agar akun dari layanan berbeda yang berbagi nama akun yang sama tidak bertabrakan di aplikasi.

Alur pendaftaran yang aman mengonfirmasi pengaturan sebelum mempercayainya. Setelah pengguna memindai kode, minta mereka memasukkan kode yang baru dihasilkan dan verifikasi sebelum menandai faktor itu aktif. Ini menangkap kesalahan umum seperti memindai ke aplikasi yang salah atau salah mengetik secret. Hanya setelah tantangan berhasil faktor dua langkah dianggap aktif.

Recovery tidak boleh menjadi jalan pintas

Risiko operasional terbesar dari autentikasi dua langkah adalah mengunci pengguna sah ketika mereka kehilangan perangkat yang memegang secret. Jawabannya bukan membiarkan faktor keamanan mati; melainkan merencanakan recovery dengan hati-hati, karena setiap jalur recovery adalah cara lain untuk autentikasi dan tidak boleh lebih lemah dari faktor itu sendiri.

OWASP menyarankan beberapa pendekatan: menerbitkan recovery code sekali pakai saat pendaftaran, mewajibkan pengguna menyiapkan lebih dari satu faktor, atau proses verifikasi identitas yang ketat. Recovery code adalah pilihan paling praktis untuk aplikasi kecil. Kode itu dibuat sekali, ditampilkan plaintext tepat satu kali, disimpan pengguna, dan terpakai saat digunakan. Recovery code sekali pakai itu sendiri adalah faktor kepemilikan, sehingga harus diperlakukan sehati-hati password.

Batas jujur dan alternatif

TOTP bukan faktor kedua terkuat, dan layak disampaikan dengan jujur daripada dibesar-besarkan. Karena pengguna mengetik kode ke halaman mana pun yang memintanya, halaman phishing yang meyakinkan bisa menangkap password dan kode yang masih aktif lalu memutarnya ulang di sisa jendelanya. Inilah sebabnya produk dengan model ancaman berbeda memilih perangkat keras tahan-phishing atau passkey berbasis WebAuthn, tempat private key tidak pernah meninggalkan perangkat dan transaksi diikat ke origin.

Bagi aplikasi self-hosted kecil, celah itu jarang menjadi alasan untuk mengabaikan TOTP sepenuhnya. Pilihan praktis biasanya TOTP dulu, dengan opsi faktor tahan-phishing belakangan, daripada tidak punya faktor kedua sama sekali. Cara menyampaikannya secara jujur adalah: TOTP menaikkan standar terhadap sebagian besar ancaman otomatis dan serangan password curian, tetapi ia tidak menghilangkan unsur manusia dari phishing.

Ada juga kewajiban perawatan. Satu pengguna satu perangkat itu mudah. Saat kumpulan akun bertambah, shared secret berlipat, masing-masing butuh penyimpanan aman, rotasi, dan rencana recovery, dan pertanyaan tentang enkripsi-at-rest tidak bisa diabaikan ketika server menyimpan secret mentah di dekat kode yang memakainya.

Pemeriksaan ringkas untuk layanan PHP kecil

  • Shared secret dibuat dari CSPRNG, setidaknya 128 bit, dan disimpan agar pembaca biasa tidak bisa menjangkaunya.
  • Salinan otoritatif secret terpisah dari nilai apa pun yang mungkin diganti jika dicurigai kompromi.
  • Counter dikemas sebagai integer big-endian delapan byte dan di-hash sebagai biner mentah, bukan hex.
  • Kode dibandingkan dalam waktu konstan dengan hash_equals().
  • Jendela resynchronization kecil menampung clock drift tanpa melebarkan jendela replay lebih dari perlu.
  • Kode yang sama ditolak setelah terpakai, sehingga kode tertangkap tidak bisa diputar ulang dalam jendelanya.
  • Percobaan gagal dibatasi lintas sesi, tanpa memudahkan orang luar mengunci pengguna sah.
  • Pendaftaran dikonfirmasi dengan kode aktif sebelum faktor diaktifkan, dan recovery code sekali pakai tersedia sebagai jalur kembali yang teruji.

Kesimpulan

TOTP terlihat menipu sederhana karena intinya hanya beberapa baris hashing dan pemotongan. Bagian tersulit ada di keputusan di sekelilingnya: di mana shared secret tinggal, bagaimana jendela toleransi diukur, bagaimana pemakaian ulang dicegah, dan bagaimana pengguna yang terkunci bisa kembali. Setiap pilihan menukar sedikit keamanan dengan sedikit kemudahan, dan keseimbangan yang tepat bergantung pada siapa yang dilayani aplikasi.

Jika kamu menambahkan TOTP ke aplikasi PHP kecil, kode di atas adalah titik awal yang bisa dipakai dan terverifikasi, bukan seluruh fiturnya. Pekerjaan yang tersisa bukan di algoritma; melainkan di penyimpanan secret, penanganan replay dan throttle, serta alur recovery yang cukup kuat agar tidak menjadi pintu yang mudah. Apakah TOTP cukup, atau apakah faktor tahan-phishing layak demi kompleksitas tambahan, adalah pertanyaan yang paling baik dijawab oleh model ancamanmu sendiri dan orang-orang yang ingin kamu lindungi.

References