Hai% nama pengguna%. Terlepas dari topik laporan, saya selalu ditanya di konferensi pertanyaan yang sama - โbagaimana cara menyimpan token dengan aman di perangkat pengguna?โ. Biasanya saya mencoba menjawab, tetapi waktu tidak memungkinkan untuk sepenuhnya mengungkapkan topik. Dengan artikel ini saya ingin menutup masalah ini sepenuhnya.
Saya menganalisis selusin aplikasi untuk melihat bagaimana mereka bekerja dengan token. Semua aplikasi yang saya analisis memproses data penting dan memungkinkan saya menetapkan kode pin untuk input sebagai perlindungan tambahan. Mari kita lihat kesalahan paling umum:
- Mengirim kode PIN ke API bersama dengan RefreshToken untuk mengonfirmasi otentikasi dan menerima token baru. - Buruk, RefreshToken tidak aman di penyimpanan lokal, dengan akses fisik ke perangkat atau cadangan, Anda dapat menghapusnya, serta malware dapat melakukannya.
- Menyimpan kode pin dalam pesan dengan RefreshToken, lalu verifikasi lokal kode pin dan mengirim RefreshToken ke API. - Mimpi buruk, RefreshToken tidak aman bersama dengan pin, yang memungkinkan mereka untuk diekstraksi, di samping itu, vektor lain muncul menyarankan memotong otentikasi lokal.
- Enkripsi RefreshToken yang salah dengan kode pin, yang memungkinkan Anda mengembalikan kode pin dan RefreshToken dari ciphertext. - Kasus khusus dari kesalahan sebelumnya, dieksploitasi sedikit lebih rumit. Tetapi perhatikan bahwa ini adalah cara yang benar.
Setelah melihat kesalahan umum, Anda dapat melanjutkan dengan memikirkan logika penyimpanan token yang aman di aplikasi Anda. Layak dimulai dengan aset dasar yang terkait dengan otentikasi / otorisasi selama pengoperasian aplikasi dan mengajukan beberapa persyaratan untuknya:
Kredensial - (nama pengguna + kata sandi) - digunakan untuk mengautentikasi pengguna dalam sistem.
+ kata sandi tidak pernah disimpan di perangkat dan harus segera dihapus dari RAM setelah dikirim ke API
+ tidak ditransmisikan oleh metode GET dalam parameter kueri dari permintaan HTTP, sebaliknya permintaan POST digunakan
+ cache keyboard dinonaktifkan untuk bidang teks pemrosesan kata sandi
+ clipboard dinonaktifkan untuk bidang teks yang berisi kata sandi
+ kata sandi tidak diungkapkan melalui antarmuka pengguna (mereka menggunakan tanda bintang), juga kata sandi tidak masuk ke tangkapan layar
AccessToken - digunakan untuk mengonfirmasi otorisasi pengguna.
+ tidak pernah disimpan dalam memori jangka panjang dan hanya disimpan dalam RAM
+ tidak ditransmisikan oleh metode GET dalam parameter kueri dari permintaan HTTP, sebaliknya permintaan POST digunakan
RefreshToken - digunakan untuk mendapatkan bundel AccessToken + RefreshToken baru.
+ tidak disimpan dalam bentuk apa pun dalam RAM dan harus segera dihapus darinya setelah menerima dari API dan menyimpannya ke memori jangka panjang atau setelah menerima dari memori jangka panjang dan menggunakan
+ disimpan hanya dalam bentuk terenkripsi dalam memori jangka panjang
+ dienkripsi dengan pin menggunakan sihir dan aturan tertentu (aturan akan dijelaskan di bawah), yaitu jika pin belum disetel, maka jangan menyimpan sama sekali
+ tidak ditransmisikan oleh metode GET dalam parameter kueri dari permintaan HTTP, sebaliknya permintaan POST digunakan
PIN - (biasanya nomor 4 atau 6 digit) - digunakan untuk mengenkripsi / mendekripsi RefreshToken.
+ tidak pernah disimpan di mana pun di perangkat dan harus segera dihapus dari RAM setelah digunakan
+ tidak pernah meninggalkan batas aplikasi, itu tidak dikirim ke mana pun
+ hanya digunakan untuk enkripsi / dekripsi RefreshToken
OTP adalah kode satu kali untuk 2FA.
+ OTP tidak pernah disimpan di perangkat dan harus segera dihapus dari RAM setelah mengirim ke API
+ tidak ditransmisikan oleh metode GET dalam parameter kueri dari permintaan HTTP, sebaliknya permintaan POST digunakan
+ cache keyboard dinonaktifkan untuk bidang teks yang memproses OTP
+ clipboard dinonaktifkan untuk bidang teks yang mengandung OTP
+ OTP tidak masuk ke tangkapan layar
+ aplikasi menghapus OTP dari layar ketika ia pergi ke latar belakang
Sekarang mari kita beralih ke
keajaiban kriptografi. Persyaratan utama adalah bahwa dalam keadaan apa pun Anda tidak boleh mengizinkan implementasi mekanisme enkripsi RefreshToken, di mana Anda dapat memvalidasi hasil dekripsi secara lokal. Artinya, jika seorang penyerang mengambil ciphertext, ia seharusnya tidak dapat mengambil kunci. Satu-satunya validator adalah API. Ini adalah satu-satunya cara untuk membatasi upaya pemilihan kunci dan membatalkan token jika terjadi serangan Brute-Force.
Saya akan memberikan contoh yang baik, katakanlah kita ingin mengenkripsi UUID
aec27f0f-b8a3-43cb-b076-e075a095abfe
dengan set AES / CBC / PKCS5Padding ini, menggunakan PIN sebagai kunci. Tampaknya algoritma itu baik, semuanya didasarkan pada pedoman, tetapi ada titik kunci - kuncinya berisi sangat sedikit entropi. Mari kita lihat apa yang menyebabkan hal ini:
- Padding - karena token kami menempati 36 byte, dan AES adalah mode enkripsi blok dengan blok 128 bit, maka algoritma harus menyelesaikan token hingga 48 byte (yang merupakan kelipatan dari 128 bit). Dalam versi kami, ekor akan ditambahkan sesuai dengan standar PKCS5Padding, mis. nilai setiap byte yang ditambahkan sama dengan jumlah byte yang ditambahkan
01
02 02
03 03 03
04 04 04 04
05 05 05 05 05
06 06 06 06 06 06
dll.
Blok terakhir kami akan terlihat seperti ini:
... | 61 62 66 65 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C 0C |
Dan ada masalah, melihat padding ini, kita dapat memfilter data (dengan blok terakhir yang tidak valid) didekripsi oleh kunci yang salah dan, dengan demikian, menentukan RefreshToken yang valid dari tumpukan terpelintir. - Format token yang dapat diprediksi - bahkan jika kita membuat token kita kelipatan 128 bit (misalnya, menghilangkan tanda hubung) untuk menghindari penambahan padding, kita akan menemukan masalah berikut. Masalahnya adalah bahwa semua tumpukan bengkok yang sama kita bisa mengumpulkan garis dan menentukan mana yang termasuk dalam format UUID. UUID dalam bentuk teks kanoniknya adalah 32 digit dalam format heksadesimal yang dipisahkan oleh tanda hubung menjadi 5 kelompok 8-4-4-4-12
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
di mana M adalah versi dan N adalah opsi. Semua ini cukup untuk memfilter token yang didekripsi dengan kunci yang salah, meninggalkan format UUID RefreshToken yang sesuai.
Dengan semua hal di atas, Anda dapat melanjutkan ke implementasi, saya memilih opsi sederhana untuk menghasilkan 64 byte acak dan membungkusnya di base64:
public String createRefreshToken() { byte[] refreshToken = new byte[64]; final SecureRandom secureRandom = new SecureRandom(); secureRandom.nextBytes(refreshToken); return Base64.getUrlEncoder().withoutPadding() .encodeToString(refreshToken); }
Berikut adalah contoh token tersebut:
YmI8rF9pwB1KjJAZKY9JzqsCu3kFz4xt4GkRCzXS9-FS_kbN3-CF9RGiRuuGqwqMo-VxFDhgQNmgjlQFD2GvbA
Sekarang mari kita lihat tampilannya secara algoritmik (di Android dan iOS algoritmanya akan sama):
private static final String ALGORITHM = "AES"; private static final String CIPHER_SUITE = "AES/CBC/NoPadding"; private static final int AES_KEY_SIZE = 16; private static final int AES_BLOCK_SIZE = 16; public String encryptToken(String token, String pin) { decodedToken = decodeToken(token);
Baris apa yang perlu diperhatikan:
private static final String CIPHER_SUITE = "AES/CBC/NoPadding";
Tidak ada bantalan, yah, Anda ingat.
decodedToken = decodeToken(token);
Anda tidak bisa hanya mengambil dan mengenkripsi token di representasi base64, karena representasi ini memiliki format tertentu (yah, Anda ingat).
byte[] key = kdf.deriveKey(rawPin, salt, AES_KEY_SIZE);
Pada output, kami mendapatkan kunci ukuran AES_KEY_SIZE, cocok untuk algoritma AES. Fungsi derivasi kunci apa pun yang direkomendasikan oleh Argon2, SHA-3, Scrypt dapat digunakan sebagai kdf dalam kasus kehidupan buruk pbkdf2 (paralel dengan FPGA).
Token terenkripsi akhir dapat disimpan dengan aman di perangkat dan tidak khawatir bahwa seseorang dapat mencurinya, baik itu malware atau entitas yang tidak terbebani oleh prinsip-prinsip moral.
Beberapa rekomendasi lainnya:
- Kecualikan token dari cadangan.
- Di iOS, simpan token di gantungan kunci dengan atribut kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly.
- Jangan hamburkan aset yang dibahas dalam artikel ini (kunci, pin, kata sandi, dll.) Di seluruh aplikasi.
- Timpa aset segera setelah tidak diperlukan, jangan menyimpannya dalam ingatan Anda lebih lama dari yang diperlukan.
- Gunakan SecureRandom di Android dan SecRandomCopyBytes di iOS untuk menghasilkan byte acak dalam konteks kriptografi.
Kami memeriksa sejumlah jebakan saat menyimpan token, yang, menurut pendapat saya, harus diketahui oleh setiap orang yang mengembangkan aplikasi yang bekerja dengan data penting. Topik ini, di mana Anda bisa bingung pada langkah apa pun, jika ada pertanyaan, tanyakan di komentar. Komentar pada teks juga diterima.
Referensi:
CWE-311: Enkripsi Data Sensitif yang HilangCWE-327: Penggunaan Algoritma Kriptografi yang Rusak atau BerisikoCWE-327: CWE-338: Penggunaan Pseudo-Random Number Generator (PRNG) yang Lemah Secara Kriptografis.CWE-598: Paparan Informasi Melalui String Permintaan dalam Permintaan GET