Advertisement
  1. Code
  2. JavaScript
  3. Web APIs

Menyimpan Data dengan Aman di Android

Scroll to top

() 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.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.