Menyimpan Data dengan Aman di Android

() translation by (you can also view the original English article)
Kredibilitas aplikasi saat ini sangat bergantung pada bagaimana data pribadi pengguna dikelola. Tumpukan Android memiliki banyak API powerful yang mengelilingi penyimpanan kredensial dan penyimpan key, dengan fitur khusus hanya tersedia di versi tertentu. Seri singkat ini akan dimulai dengan pendekatan sederhana untuk bangun dan berjalan dengan melihat sistem penyimpanan dan cara mengenkripsi dan menyimpan data sensitif melalui kode akses pengguna. Pada tutorial kedua, kita akan melihat cara yang lebih kompleks untuk melindungi key dan kredensial.
Fundamental
Pertanyaan pertama yang harus dipikirkan adalah berapa banyak data yang sebenarnya perlu Kamu dapatkan. Pendekatan yang baik adalah menghindari penyimpanan data pribadi jika Kamu tidak benar-benar harus melakukannya.
Untuk data yang harus Kamu simpan, arsitektur Android siap membantu. Sejak 6.0 Marshmellow, enkripsi full-disk diaktifkan secara default, untuk perangkat yang memiliki kemampuan. File dan SharedPreferences
yang disimpan oleh aplikasi ditetapkan secara otomatis dengan konstanta MODE_PRIVATE
. Ini berarti data hanya bisa diakses oleh aplikasi Kamu sendiri.
Ini adalah ide bagus untuk tetap mematuhi standar ini. Kamu dapat mengaturnya secara eksplisit saat menyimpan preferensi bersama.
1 |
SharedPreferences.Editor editor = getSharedPreferences("preferenceName", MODE_PRIVATE).edit(); |
2 |
editor.putString("key", "value"); |
3 |
editor.commit(); |
Atau saat menyimpan file.
1 |
FileOutputStream fos = openFileOutput(filenameString, Context.MODE_PRIVATE); |
2 |
fos.write(data); |
3 |
fos.close(); |
Hindari menyimpan data pada penyimpanan eksternal, karena data tersebut kemudian terlihat oleh aplikasi dan pengguna lain. Sebenarnya, untuk mencegah mempersulit orang menyalin data biner dan aplikasi Kamu, Kamu dapat melarang pengguna agar tidak dapat memasang aplikasi di penyimpanan eksternal. Menambahkan android:installLocation
dengan nilai internalOnly
ke file manifes akan mencapainya.
Kamu juga dapat mencegah aplikasi dan datanya dicadangkan. Ini juga mencegah pengunduhan konten direktori data pribadi aplikasi dengan menggunakan adb backup
. Untuk melakukannya, atur atribut android:allowBackup
menjadi false
di file manifes. Secara default, atribut ini diatur ke true
.
Ini adalah best practices, namun tidak akan berfungsi untuk perangkat yang disusupi atau perangkat yang telah di root, dan enkripsi disk hanya berguna bila perangkat diamankan dengan layar kunci. Ini adalah tempat yang memiliki kata sandi sisi aplikasi yang melindungi datanya dengan enkripsi bermanfaat.
Mengamankan Data Pengguna Dengan Password
Conceal adalah pilihan tepat untuk library enkripsi karena membuat Kamu bangun dan berlari dengan sangat cepat tanpa harus khawatir dengan detail yang mendasarinya. Namun, eksploitasi yang ditargetkan untuk framework populer sekaligus akan mempengaruhi semua aplikasi yang mengandalkannya.
Penting juga untuk mengetahui bagaimana sistem enkripsi berfungsi agar bisa mengetahui apakah Kamu menggunakan framework tertentu dengan aman. Jadi, untuk postingan ini kita akan mendapatkan tangan kita kotor dengan melihat langsung penyedia kriptografi secara langsung.
AES dan Password Berbasis Key Derivation
Kami akan menggunakan standar AES yang disarankan, yang mengenkripsi data yang diberi sebuah key. Key yang sama yang digunakan untuk mengenkripsi data digunakan untuk mendekripsi data, yang disebut enkripsi simetris. Ada berbagai ukuran key, dan AES256 (256 bit) adalah ukuran yang disukai untuk digunakan dengan data sensitif.
Meskipun pengalaman pengguna aplikasi Kamu harus memaksa pengguna untuk menggunakan kode sandi yang kuat, kemungkinan kode sandi yang sama juga akan dipilih oleh pengguna lain. Menempatkan keamanan data terenkripsi kita di tangan pengguna tidak aman. Data kami perlu diamankan dengan key yang acak dan cukup besar (misal memiliki entropi yang cukup) untuk dianggap kuat. Inilah sebabnya mengapa tidak pernah disarankan untuk menggunakan kata sandi secara langsung untuk mengenkripsi data-di situlah fungsi yang disebut Fungsi Derivatif Berbasis Password (PBKDF2) ikut bermain.
PDKDF2 mendapatkan sebuah key dari sebuah kata sandi dengan menambahkannya berkali-kali dengan salt. Ini disebut key stretching. Salt hanyalah urutan data acak dan membuat key turunannya unik meski password yang sama digunakan oleh orang lain. Mari kita mulai dengan menghasilkan salt itu.
1 |
SecureRandom random = new SecureRandom(); |
2 |
byte salt[] = new byte[256]; |
3 |
random.nextBytes(salt); |
Kelas SecureRandom
menjamin bahwa output yang dihasilkan akan sulit diprediksi-ini adalah 'generator bilangan acak kriptografi yang kuat'. Kita sekarang bisa memasukkan salt dan password ke dalam sebuah objek enkripsi berbasis password: PBEKeySpec
. Konstruktor objek juga mengambil bentuk hitungan iterasi sehingga key menjadi lebih kuat. Hal ini karena meningkatkan jumlah iterasi memperluas waktu yang dibutuhkan untuk beroperasi pada satu set key selama serangan brute force. PBEKeySpec
kemudian masuk ke SecretKeyFactory
, yang akhirnya menghasilkan key sebagai array byte[]
. Kami akan membungkus byte[]
mentah itu ke objek SecretKeySpec
.
1 |
char[] passwordChar = passwordString.toCharArray(); //Turn password into char[] array |
2 |
PBEKeySpec pbKeySpec = new PBEKeySpec(passwordChar, salt, 1324, 256); //1324 iterations |
3 |
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); |
4 |
byte[] keyBytes = secretKeyFactory.generateSecret(pbKeySpec).getEncoded(); |
5 |
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES"); |
Perhatikan bahwa kata sandi dilewatkan sebagai array char[]
dan kelas PBEKeySpec
menyimpannya sebagai array char[]
juga. array char[]
biasanya digunakan untuk fungsi enkripsi karena sementara kelas String
tidak dapat diubah, array char[]
yang berisi informasi sensitif dapat ditimpa - sehingga menghapus data sensitif seluruhnya dari perangkat RAM phyc.
Vektor Inisialisasi
Kami sekarang siap mengenkripsi data, tapi ada satu hal lagi yang harus dilakukan. Ada berbagai cara enkripsi dengan AES, tapi kami akan menggunakan yang direkomendasikan: cipher block chaining (CBC). Ini beroperasi pada data kami satu blok pada satu waktu. Hal yang hebat tentang mode ini adalah bahwa setiap blok data yang tidak terenkripsi berikutnya adalah XOR'd dengan blok terenkripsi sebelumnya untuk membuat enkripsi lebih kuat. Namun, itu berarti blok pertama tidak pernah seunik yang lainnya!
Jika pesan yang akan dienkripsi adalah memulai yang sama dengan pesan lain yang akan dienkripsi, keluaran terenkripsi awal akan sama, dan itu akan memberi penyerang petunjuk untuk mencari tahu pesannya. Solusinya adalah dengan menggunakan vektor inisialisasi (IV).
IV hanyalah sebuah blok dari byte acak yang akan di XOR'd dengan blok pertama data pengguna. Karena setiap blok bergantung pada semua blok yang diproses sampai titik itu, keseluruhan pesan akan dienkripsi dengan unik - pesan identik yang dienkripsi dengan key yang sama tidak akan menghasilkan hasil yang sama. Mari membuat IV sekarang.
1 |
SecureRandom ivRandom = new SecureRandom(); //not caching previous seeded instance of SecureRandom |
2 |
byte[] iv = new byte[16]; |
3 |
ivRandom.nextBytes(iv); |
4 |
IvParameterSpec ivSpec = new IvParameterSpec(iv); |
Catatan tentang SecureRandom
. Pada versi 4.3 dan di bawah, Java Cryptography Architecture memiliki kerentanan karena inisialisasi yang tidak tepat yang mendasarinya dari number pseudorandom generator (PRNG). Jika Kamu menargetkan versi 4.3 dan versi di bawah, perbaikan tersedia.
Enkripsi Data
Berbekal IvParameterSpec
, sekarang kita bisa melakukan enkripsi sebenarnya.
1 |
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding"); |
2 |
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); |
3 |
byte[] encrypted = cipher.doFinal(plainTextBytes); |
Disini kita lewati dengan string 'AES/CBC/PKCS7Padding'
. Ini menentukan enkripsi AES dengan cipher blok cypher. Bagian terakhir dari string ini mengacu pada PKCS7, yang merupakan standar yang ditetapkan untuk data padding yang tidak sesuai dengan ukuran blok. (Blok adalah 128 bit, dan padding dilakukan sebelum enkripsi.)
Untuk melengkapi contoh kita, kita akan memasukkan kode ini ke dalam metode enkripsi yang akan mengemas hasilnya menjadi HashMap
yang berisi data terenkripsi, bersama dengan salt dan vektor inisialisasi yang diperlukan untuk dekripsi.
1 |
private HashMap<String, byte[]> encryptBytes(byte[] plainTextBytes, String passwordString) |
2 |
{
|
3 |
HashMap<String, byte[]> map = new HashMap<String, byte[]>(); |
4 |
|
5 |
try
|
6 |
{
|
7 |
//Random salt for next step
|
8 |
SecureRandom random = new SecureRandom(); |
9 |
byte salt[] = new byte[256]; |
10 |
random.nextBytes(salt); |
11 |
|
12 |
//PBKDF2 - derive the key from the password, don't use passwords directly
|
13 |
char[] passwordChar = passwordString.toCharArray(); //Turn password into char[] array |
14 |
PBEKeySpec pbKeySpec = new PBEKeySpec(passwordChar, salt, 1324, 256); //1324 iterations |
15 |
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); |
16 |
byte[] keyBytes = secretKeyFactory.generateSecret(pbKeySpec).getEncoded(); |
17 |
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES"); |
18 |
|
19 |
//Create initialization vector for AES
|
20 |
SecureRandom ivRandom = new SecureRandom(); //not caching previous seeded instance of SecureRandom |
21 |
byte[] iv = new byte[16]; |
22 |
ivRandom.nextBytes(iv); |
23 |
IvParameterSpec ivSpec = new IvParameterSpec(iv); |
24 |
|
25 |
//Encrypt
|
26 |
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding"); |
27 |
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); |
28 |
byte[] encrypted = cipher.doFinal(plainTextBytes); |
29 |
|
30 |
map.put("salt", salt); |
31 |
map.put("iv", iv); |
32 |
map.put("encrypted", encrypted); |
33 |
}
|
34 |
catch(Exception e) |
35 |
{
|
36 |
Log.e("MYAPP", "encryption exception", e); |
37 |
}
|
38 |
|
39 |
return map; |
40 |
}
|
Metode Dekripsi
Kamu hanya perlu menyimpan infus dan salt dengan data Kamu. Sementara salt dan infus dianggap umum, pastikan tidak berangsur bertambah atau digunakan kembali. Untuk mendekripsi data, yang perlu kita lakukan hanyalah mengubah mode di konstruktor Cipher
dari ENCRYPT_MODE
menjadi DECRYPT_MODE
. Metode dekripsi akan mengambil HashMap
yang berisi informasi yang dibutuhkan yang sama (data terenkripsi, salt dan IV) dan mengembalikan array byte[]
terdekripsi, dengan kata sandi yang benar. Metode dekripsi akan meregenerasi key enkripsi dari kata sandinya. Key-nya tidak boleh disimpan!
1 |
private byte[] decryptData(HashMap<String, byte[]> map, String passwordString) |
2 |
{
|
3 |
byte[] decrypted = null; |
4 |
try
|
5 |
{
|
6 |
byte salt[] = map.get("salt"); |
7 |
byte iv[] = map.get("iv"); |
8 |
byte encrypted[] = map.get("encrypted"); |
9 |
|
10 |
//regenerate key from password
|
11 |
char[] passwordChar = passwordString.toCharArray(); |
12 |
PBEKeySpec pbKeySpec = new PBEKeySpec(passwordChar, salt, 1324, 256); |
13 |
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); |
14 |
byte[] keyBytes = secretKeyFactory.generateSecret(pbKeySpec).getEncoded(); |
15 |
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES"); |
16 |
|
17 |
//Decrypt
|
18 |
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding"); |
19 |
IvParameterSpec ivSpec = new IvParameterSpec(iv); |
20 |
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); |
21 |
decrypted = cipher.doFinal(encrypted); |
22 |
}
|
23 |
catch(Exception e) |
24 |
{
|
25 |
Log.e("MYAPP", "decryption exception", e); |
26 |
}
|
27 |
|
28 |
return decrypted; |
29 |
}
|
Menguji Enkripsi dan Dekripsi
Agar contohnya tetap sederhana, kami mengabaikan pengecekan kesalahan yang akan memastikan HashMap
berisi key yang diperlukan, pasangan nilai. Sekarang kita dapat menguji metode kami untuk memastikan bahwa data terdekrip dengan benar setelah enkripsi.
1 |
//Encryption test
|
2 |
String string = "My sensitive string that I want to encrypt"; |
3 |
byte[] bytes = string.getBytes(); |
4 |
HashMap<String, byte[]> map = encryptBytes(bytes, "UserSuppliedPassword"); |
5 |
|
6 |
//Decryption test
|
7 |
byte[] decrypted = decryptData(map, "UserSuppliedPassword"); |
8 |
if (decrypted != null) |
9 |
{
|
10 |
String decryptedString = new String(decrypted); |
11 |
Log.e("MYAPP", "Decrypted String is : " + decryptedString); |
12 |
}
|
Metode menggunakan array byte[]
sehingga Kamu dapat mengenkripsi data sewenang-wenang, bukan hanya objek String
.
Menyimpan Data Terenkripsi
Sekarang kita memiliki array byte[]
terenkripsi, kita bisa menyimpannya ke penyimpanan.
1 |
FileOutputStream fos = openFileOutput("test.dat", Context.MODE_PRIVATE); |
2 |
fos.write(encrypted); |
3 |
fos.close(); |
Jika Kamu tidak ingin menyimpan IV dan salt secara terpisah, HashMap
dapat terprogram dengan kelas ObjectInputStream
dan ObjectOutputStream
.
1 |
FileOutputStream fos = openFileOutput("map.dat", Context.MODE_PRIVATE); |
2 |
ObjectOutputStream oos = new ObjectOutputStream(fos); |
3 |
oos.writeObject(map); |
4 |
oos.close(); |
Menyimpan Data yang Aman ke SharedPreferences
Kamu juga dapat menyimpan data aman ke aplikasi Kamu SharedPreferences
.
1 |
SharedPreferences.Editor editor = getSharedPreferences("prefs", Context.MODE_PRIVATE).edit(); |
2 |
String keyBase64String = Base64.encodeToString(encryptedKey, Base64.NO_WRAP); |
3 |
String valueBase64String = Base64.encodeToString(encryptedValue, Base64.NO_WRAP); |
4 |
editor.putString(keyBase64String, valueBase64String); |
5 |
editor.commit(); |
Karena SharedPreferences
adalah sistem XML yang hanya menerima primitif dan objek tertentu sebagai nilai, kita perlu mengubah data kita menjadi format yang kompatibel seperti objek String
. Base64 memungkinkan kita untuk mengubah data mentah menjadi representasi String
yang hanya berisi karakter yang diizinkan oleh format XML. Mengenkripsi kunci dan nilainya sehingga penyerang tidak dapat mengetahui nilainya. Pada contoh di atas, encryptedKey
dan encryptedValue
keduanya adalah array byte[]
terenkripsi yang dikembalikan dari metode encryptBytes()
kita. IV dan salt dapat disimpan ke dalam file preferensi atau sebagai file terpisah. Untuk mendapatkan kembali byte terenkripsi dari SharedPreferences
, kita dapat menerapkan decode Base64 pada String
yang tersimpan.
1 |
SharedPreferences preferences = getSharedPreferences("prefs", Context.MODE_PRIVATE); |
2 |
String base64EncryptedString = preferences.getString(keyBase64String, "default"); |
3 |
byte[] encryptedBytes = Base64.decode(base64EncryptedString, Base64.NO_WRAP); |
Menghapus Data yang Tidak Aman Dari Versi Lama
Setelah data tersimpan aman, mungkin Kamu memiliki versi aplikasi sebelumnya yang menyimpan data dengan tidak aman. Pada upgrade, data bisa dihapus dan dienkripsi ulang. Kode berikut menghapus file dengan menggunakan data acak.
Secara teori, Kamu bisa menghapus preferensi bersama Kamu dengan menghapus file /data/data/com.your.package.name/shared_prefs/your_prefs_name.xml dan your_prefs_name.bak, dan menghapus preferensi memori di dalam kode berikut:
1 |
getSharedPreferences("prefs", Context.MODE_PRIVATE).edit().clear().commit(); |
Namun, alih-alih mencoba menghapus data lama dan berharap berhasil, lebih baik mengenkripsinya terlebih dulu! Hal ini terutama berlaku pada umumnya untuk solid state drive yang sering menyebar data-writes ke berbagai daerah untuk mencegah keausan. Itu berarti bahwa bahkan jika Kamu menimpa file dalam filesystem, memori solid-state fisik mungkin menyimpan data Kamu di lokasi aslinya pada disk.
1 |
public static void secureWipeFile(File file) throws IOException |
2 |
{
|
3 |
if (file != null && file.exists()) |
4 |
{
|
5 |
final long length = file.length(); |
6 |
final SecureRandom random = new SecureRandom(); |
7 |
final RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rws"); |
8 |
randomAccessFile.seek(0); |
9 |
randomAccessFile.getFilePointer(); |
10 |
byte[] data = new byte[64]; |
11 |
int position = 0; |
12 |
while (position < length) |
13 |
{
|
14 |
random.nextBytes(data); |
15 |
randomAccessFile.write(data); |
16 |
position += data.length; |
17 |
}
|
18 |
randomAccessFile.close(); |
19 |
file.delete(); |
20 |
}
|
21 |
}
|
Kesimpulan
Itu membungkus tutorial kami tentang menyimpan data terenkripsi. Dalam posting ini, Kamu belajar cara mengenkripsi dan mendekripsi data sensitif dengan aman dengan kata sandi yang disediakan pengguna. Mudah dilakukan bila Kamu tahu caranya, namun penting untuk mengikuti semua praktik terbaik untuk memastikan data penggunamu benar-benar aman.
Di posting berikutnya, kita akan melihat bagaimana memanfaatkan KeyStore
dan API terkait kredensial lainnya untuk menyimpan barang dengan aman. Sementara itu, lihat beberapa artikel bagus lainnya tentang pengembangan aplikasi Android.