Keys, Credentials dan Storage pada Android
() translation by (you can also view the original English article)
Pada postingan sebelumnya tentang keamanan data pengguna Android, kita melihat pada proses enkripsi data melewati user-supplied passcode. Pada tutorial ini akan memfokuskan pada credential dan key storage. Saya akan memulai dengan memperkenalkan account credential dan diakhiri dengan mengamankan data menggunakan KeyStore.
- KeamananMenyimpan Data yang Aman di AndroidCollin Stuart
- AndroidBagaimana Mengamankan Aplikasi AndroidAshraff Hathibelagal
Sering, ketika bekerja dengan third-party service, akan terdapat beberapa bentuk dari autentikasi yang diperlukan. Ini akan terlihat sederhana seperti /login
pada titik akhir yang menerima username dan password.
Itu akan terlihat sesuatu bahwa solusi yang sederhana untuk membangun UI yang meminta user untuk login dan kemudian mengambil dan menyimpan login credential mereka. Namun, ini bukan cara yang baik karena aplikasi kita tidak perlu tahu credential dari akun third-party. Tapi, kita dapat menggunakan Account Manager, dimana akan melimpahkan informasi sensitf pada kita.
Account Manager
Account Manager merupakan suatu helper yang terpusat untuk user account credential jadi aplikasi Anda tidak perlu untuk menangani banyak password secara langsung. Ini biasanya akan menyiapkan sebuah token pada tempat dimana username dan password yang asli dapat digunakan untuk membuat suatu permintaan autentikasi untuk sebuah service. Sebagai contoh adalah ketika meminta request beberapa OAuth2 token.
Terkadang, semua informasi yang dibutuhkan sudah tersimpan pada suatu perangkat, dan pada kesempatan lain suatu Account Manager akan membutuhkan panggilan sebuah server untuk mendapatkan token yang baru. Anda mungkin sudah melihat bagian Akun pada suatu pengaturan perangkat untuk berbagai macam aplikasi. Kita dapat mengambil semua daftar yang tersedia pada akun seperti ini:
1 |
AccountManager accountManager = AccountManager.get(this); |
2 |
Account[] accounts = accountManager.getAccounts(); |
Code ini akan membutuhkan izin android.permission.GET_ACCOUNTS
. Jika Anda mencari suatu akun yang spesifik, Anda bisa mencarinya seperti ini:
1 |
AccountManager accountManager = AccountManager.get(this); |
2 |
Account[] accounts = accountManager.getAccountsByType("com.google"); |
Ketika Anda telah mendapatkan akun tersebut, sebuah token untuk akun bisa diambil dengan cara memanggil metohod getAuthToken(Account, String, Bundle, Activity, AccountManagerCallback, Handler)
. Token kemudian bisa digunakan untuk membuat autentikasi API requests ke suatu service. Ini bisa jadi adalah sebuah RESTful API dimana Anda bisa menyampaikan pada parameter token ketika dilakukan HTTPS request, tanpa sama sekali untuk mengetahui detail dari akun pribadi pengguna
Karena setiap service akan mempunyai cara yang berbeda untuk melakukan autentikasi dan menyimpan suatu private credentials, Account Manager menyediakan authenticator module untuk third-party service untuk di implementasi. Ketika Android telah melakukan implementasi untuk banyak service yang popular, ini berarti Anda dapat menulis authenticator Anda sendiri untuk menangani account authentication untuk aplikasi Anda dan tempat penyimpanan credential. Ini memungkinkan Anda untuk memastikan credentialnya ter-enkripsi. Yang harus diingat, Ini juga berarti credential pada Account Manager digunakan untuk service lain yang mungkin disimpan dalam teks asli, membuat mereka mudah dilihat oleh siapapun yang telah melakukan root pada perangkatnya.
Dari pada credential yang sederhana, ada waktunya ketika Anda membutuhkan suatu kunci atau sertifikat untuk individual atau suatu kelompok, sebagai contoh, ketika suatu third-party mengirimkan Anda sebuah file sertifikat yang Anda harus simpan. Skenario yang paling umum adalah ketika suatu aplikasi membutuhkan autentikasi untuk private organization’s server.
Pada tutorial selanjutnya, kita akan melihat cara menggunakan sertifikat untuk autentikasi dan mengamankan suatu komunikasi, tapi saya akan mengingatkan bagaimana cara menyimpan item ini pada sementara waktu. Pada dasarnya Keychain API dibuat untuk sesuatu yang spesifik, melakukan install private key atau pasangan sertifikat dari sebuah PKCS#12 file.
Keychain
Diperkenalkan pada Android 4.0(API level 14), Keychain API terlibat dengan key management. Secara spesifik, itu dapat bekerja dengan PrivateKey
dan X509Certificate
objek dan menyediakan container yang lebih aman daripada menggunakan penyimpanan data aplikasi Anda. Itu karena izin untuk sebuah private key hanya memperbolehkan untuk aplikasi Anda, dan hanya setelah dilakukan autorisasi. Ini berarti suatu layar kunci harus di siapkan sebelum mengakses sebuah penyimpanan credential. Juga, objek di Keychain mungkin terikat pada secure hardware, jika tersedia.
Code ini akan meng-install sebuah sertifikat seperti:
1 |
Intent intent = KeyChain.createInstallIntent(); |
2 |
byte[] p12Bytes = //... read from file, such as example.pfx or example.p12... |
3 |
intent.putExtra(KeyChain.EXTRA_PKCS12, p12Bytes); |
4 |
startActivity(intent); |
Pengguna akan diminta sebuah password untuk mengakses private key dan pilihan untuk nama sertifikat. Untuk mengambil kunci, kode berikut menyajikan UI yang memperbolehkan user untuk memilih daftar dari kunci yang sudah terinstall.
1 |
KeyChain.choosePrivateKeyAlias(this, this, new String[]{"RSA"}, null, null, -1, null); |
Ketika sudah membuat pilihan, sebuah string atau nama di tampilkan pada sebuah alias(final String alias)
dimana Anda bisa mengakses private key atau rantai sertifikat secara langsung.
1 |
public class KeychainTest extends Activity implements ..., KeyChainAliasCallback |
2 |
{
|
3 |
//...
|
4 |
|
5 |
@Override
|
6 |
public void alias(final String alias) |
7 |
{
|
8 |
Log.e("MyApp", "Alias is " + alias); |
9 |
|
10 |
try
|
11 |
{
|
12 |
PrivateKey privateKey = KeyChain.getPrivateKey(this, alias); |
13 |
X509Certificate[] certificateChain = KeyChain.getCertificateChain(this, alias); |
14 |
}
|
15 |
catch ... |
16 |
}
|
17 |
|
18 |
//...
|
19 |
}
|
Dengan memiliki pengetahuan tersebut, mari kita melihat bagaimana kita menggunakan penyimpanan credential untuk menyimpan data sensitif Anda.
KeyStore
Pada tutorial sebelumnya, kita melihat proses melindungi data menggunakan user-supplied passcode. Cara seperti ini bagus, tetapi kebutuhan suatu aplikasi biasanya menjauhi user untuk melakukan log in setiap waktu dan mengingat suatu passcode tambahan.
Itulah dimana KeyStore API bisa digunakan. Sejak API 1, KeyStore telah digunakan oleh sistem untuk menyimpan Wifi dan VPN credential. Seperti 4.3 (API 18), memungkinkan Anda untuk bekerja dengan app-specific asymmetric keys Anda sendiri, dan pada Android M (API 23) itu bisa menyimpan AES symmetric key. Jadi ketika API tidak memperbolehkan untuk menyimpan data sensitif secara langsung, kunci ini bisa disimpan dan kemudian digunakan untuk mengenkripsi kalimat.
Keunggulan dari menyimpan suatu kunci di KeyStore itu adalah memperbolehkan kunci untuk dioperasikan tanpa mengekspos konten rahasia dari si kunci; data kunci tidak masuk pada penyimpanan aplikasi. Ingat bahwa kunci diamankan menggunakan izin jadi hanya aplikasi Anda yang dapat mengakses mereka, dan mereka juga dapat menggunakan perangkat keras yang didukung jika perangkat tersebut mendukung. Ini membuat container lebih sulit untuk di ekstrak dari perangkat.
Menghasilkan sebuah Random Key Baru
Sebagai contoh, daripada menghasilkan kunci AES dari user-supplied passcode, kita bisa menghasilkan kunci acak secara otomatis yang akan diamankan di KeyStore. Kita bisa melakukan ini dengan membuat KeyGenerator
instance, siapkan untuk "AndroidKeyStore"
provider.
1 |
//Generate a key and store it in the KeyStore
|
2 |
final KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); |
3 |
final KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder("MyKeyAlias", |
4 |
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) |
5 |
.setBlockModes(KeyProperties.BLOCK_MODE_GCM) |
6 |
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) |
7 |
//.setUserAuthenticationRequired(true) //requires lock screen, invalidated if lock screen is disabled
|
8 |
//.setUserAuthenticationValidityDurationSeconds(120) //only available x seconds from password authentication. -1 requires finger print - every time
|
9 |
.setRandomizedEncryptionRequired(true) //different ciphertext for same plaintext on each call |
10 |
.build(); |
11 |
keyGenerator.init(keyGenParameterSpec); |
12 |
keyGenerator.generateKey(); |
Bagian penting yang perlu dilihat disini adalah .setUserAuthenticationRequired(true)
dan spesifikasi .setUserAuthenticationValidityDurationSeconds(120)
. Ini dibutuhkan untuk mengunci layar yang di siapkan dan akan terkunci sampai user ter-autentikasi.
Melihat dari dokumentasi untuk .setUserAuthenticationValidityDurationSeconds()
, Anda berarti akan melihat kunci hanya tersedia beberapa detik dari suatu autentikasi password, dan dikirim pada -1
yang dibutuhkan untuk fingerprint authentication setiap kali Anda ingin mengakses kunci. Memungkinkan kebutuhan untuk autentikasi juga memilki efek untuk menolak kunci ketika user menghapus atau mengubah kunci pada layar.
Karena menyimpan sesuatu yang tidak terlindungi bersamaan dengan data yang terenkripsi seperti menaruh kunci rumah dibawah keset, pilihan ini mencoba untuk mengamankan kunci di setiap kegiatan yang ada pada perangkat. Sebagai contoh seperti offline data dump dari suatu perangkat. Tanpa sebuah password yang diketahui untuk sebuah perangkat, data yang dirender tidaklah berguna.
Pengaturan .setRandomizedEncryptionRequired(true)
memungkinkan kebutuhan yang cukup untuk melakukan sesuatu secara acak (sebuah random IV setiap saat) jadi jika data yang sama terenkripsi setiap detik, hasil enkripsi akan tetap berbeda. Ini akan mencegah penyerang untuk mendapatkan sebuah petunjuk tentang ciphertext dari data yang sama.
Pengaturan yang lain yang perlu dicatat adalah setUserAuthenticationValidWhileOnBody(boolean remainsValid)
, yang mengunci sebuah kunci ketika perangkat terdeteksi tidak pada pemilik yang asli.
Mengenkripsi Data
Sekarang kunci tersebut disimpan di KeyStore, kita bisa membuat method yang mengenkripsi data menggunakan Cipher
object, yang diberikan SecretKey
. Itu akan membalikan sebuah HashMap
yang mengandung data terenkripsi dan sebuah randomize IV yang akan dibutuhkan untuk mendekripsi data. Enkripsi data, bersama dengan IV, bisa kemudian disimpan pada sebuah file atau pada shared preferences.
1 |
private HashMap<String, byte[]> encrypt(final byte[] decryptedBytes) |
2 |
{
|
3 |
final HashMap<String, byte[]> map = new HashMap<String, byte[]>(); |
4 |
try
|
5 |
{
|
6 |
//Get the key
|
7 |
final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); |
8 |
keyStore.load(null); |
9 |
final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry)keyStore.getEntry("MyKeyAlias", null); |
10 |
final SecretKey secretKey = secretKeyEntry.getSecretKey(); |
11 |
|
12 |
//Encrypt data
|
13 |
final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); |
14 |
cipher.init(Cipher.ENCRYPT_MODE, secretKey); |
15 |
final byte[] ivBytes = cipher.getIV(); |
16 |
final byte[] encryptedBytes = cipher.doFinal(decryptedBytes); |
17 |
map.put("iv", ivBytes); |
18 |
map.put("encrypted", encryptedBytes); |
19 |
}
|
20 |
catch (Throwable e) |
21 |
{
|
22 |
e.printStackTrace(); |
23 |
}
|
24 |
|
25 |
return map; |
26 |
}
|
Mendekripsi ke Byte Array
Untuk mendekripsi, suatu reverse diperlukan. Cipher
object di inisialisasi menggunakan konstan DECRYPT_MODE
, dan sebuah dekripsi byte[]
array yang akan dikembalikan.
1 |
private byte[] decrypt(final HashMap<String, byte[]> map) |
2 |
{
|
3 |
byte[] decryptedBytes = null; |
4 |
try
|
5 |
{
|
6 |
//Get the key
|
7 |
final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); |
8 |
keyStore.load(null); |
9 |
final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry)keyStore.getEntry("MyKeyAlias", null); |
10 |
final SecretKey secretKey = secretKeyEntry.getSecretKey(); |
11 |
|
12 |
//Extract info from map
|
13 |
final byte[] encryptedBytes = map.get("encrypted"); |
14 |
final byte[] ivBytes = map.get("iv"); |
15 |
|
16 |
//Decrypt data
|
17 |
final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); |
18 |
final GCMParameterSpec spec = new GCMParameterSpec(128, ivBytes); |
19 |
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec); |
20 |
decryptedBytes = cipher.doFinal(encryptedBytes); |
21 |
}
|
22 |
catch (Throwable e) |
23 |
{
|
24 |
e.printStackTrace(); |
25 |
}
|
26 |
|
27 |
return decryptedBytes; |
28 |
}
|
Menguji Contoh
Sekarang kita dapat mencoba sebuah contoh!
1 |
@TargetApi(Build.VERSION_CODES.M) |
2 |
private void testEncryption() |
3 |
{
|
4 |
try
|
5 |
{
|
6 |
//Generate a key and store it in the KeyStore
|
7 |
final KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); |
8 |
final KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder("MyKeyAlias", |
9 |
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) |
10 |
.setBlockModes(KeyProperties.BLOCK_MODE_GCM) |
11 |
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) |
12 |
//.setUserAuthenticationRequired(true) //requires lock screen, invalidated if lock screen is disabled
|
13 |
//.setUserAuthenticationValidityDurationSeconds(120) //only available x seconds from password authentication. -1 requires finger print - every time
|
14 |
.setRandomizedEncryptionRequired(true) //different ciphertext for same plaintext on each call |
15 |
.build(); |
16 |
keyGenerator.init(keyGenParameterSpec); |
17 |
keyGenerator.generateKey(); |
18 |
|
19 |
//Test
|
20 |
final HashMap<String, byte[]> map = encrypt("My very sensitive string!".getBytes("UTF-8")); |
21 |
final byte[] decryptedBytes = decrypt(map); |
22 |
final String decryptedString = new String(decryptedBytes, "UTF-8"); |
23 |
Log.e("MyApp", "The decrypted string is " + decryptedString); |
24 |
}
|
25 |
catch (Throwable e) |
26 |
{
|
27 |
e.printStackTrace(); |
28 |
}
|
29 |
}
|
Menggunakan RSA Asymmetric Keys untuk Perangkat yang Lama
Ini adalah solusi yang bagus untuk menyimpan data dari versi M dan lebih tinggi, tapi bagaimana jika aplikasi Anda mendukung versi sebelumnya? Sementara kunci AES symmetric tidak mendukung dibawah M, RSA asymmetric keys. Itu berarti kita bisa menggunakan RSA keys dan enkripsi untuk mendapatkan sesuatu yang sama.
Perbedaan utama disini adalah asymmetric keypair mengandung dua kunci, private dan public key, dimana public key mengenkripsi data dan private key mendeskripsinya. Sebuah KeyPairGeneratorSpec
dikirim ke KeyPairGenerator
yang di inisialisai dengan KEY_ALGORITHM_RSA
dan "AndroidKeyStore"
provider.
1 |
private void testPreMEncryption() |
2 |
{
|
3 |
try
|
4 |
{
|
5 |
//Generate a keypair and store it in the KeyStore
|
6 |
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); |
7 |
keyStore.load(null); |
8 |
|
9 |
Calendar start = Calendar.getInstance(); |
10 |
Calendar end = Calendar.getInstance(); |
11 |
end.add(Calendar.YEAR, 10); |
12 |
KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(this) |
13 |
.setAlias("MyKeyAlias") |
14 |
.setSubject(new X500Principal("CN=MyKeyName, O=Android Authority")) |
15 |
.setSerialNumber(new BigInteger(1024, new Random())) |
16 |
.setStartDate(start.getTime()) |
17 |
.setEndDate(end.getTime()) |
18 |
.setEncryptionRequired() //on API level 18, encrypted at rest, requires lock screen to be set up, changing lock screen removes key |
19 |
.build(); |
20 |
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore"); |
21 |
keyPairGenerator.initialize(spec); |
22 |
keyPairGenerator.generateKeyPair(); |
23 |
|
24 |
//Encryption test
|
25 |
final byte[] encryptedBytes = rsaEncrypt("My secret string!".getBytes("UTF-8")); |
26 |
final byte[] decryptedBytes = rsaDecrypt(encryptedBytes); |
27 |
final String decryptedString = new String(decryptedBytes, "UTF-8"); |
28 |
Log.e("MyApp", "Decrypted string is " + decryptedString); |
29 |
}
|
30 |
catch (Throwable e) |
31 |
{
|
32 |
e.printStackTrace(); |
33 |
}
|
34 |
}
|
Untuk mengenkripsi, kita mengambil RSAPublicKey
dari Keypair dan menggunakannya dengan Cipher
object.
1 |
public byte[] rsaEncrypt(final byte[] decryptedBytes) |
2 |
{
|
3 |
byte[] encryptedBytes = null; |
4 |
try
|
5 |
{
|
6 |
final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); |
7 |
keyStore.load(null); |
8 |
final KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry("MyKeyAlias", null); |
9 |
final RSAPublicKey publicKey = (RSAPublicKey)privateKeyEntry.getCertificate().getPublicKey(); |
10 |
|
11 |
final Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL"); |
12 |
cipher.init(Cipher.ENCRYPT_MODE, publicKey); |
13 |
|
14 |
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); |
15 |
final CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher); |
16 |
cipherOutputStream.write(decryptedBytes); |
17 |
cipherOutputStream.close(); |
18 |
|
19 |
encryptedBytes = outputStream.toByteArray(); |
20 |
|
21 |
}
|
22 |
catch (Throwable e) |
23 |
{
|
24 |
e.printStackTrace(); |
25 |
}
|
26 |
return encryptedBytes; |
27 |
}
|
Dekripsi dilakukan dengan menggunakan objek RSAPrivateKey
.
1 |
public byte[] rsaDecrypt(final byte[] encryptedBytes) |
2 |
{
|
3 |
byte[] decryptedBytes = null; |
4 |
try
|
5 |
{
|
6 |
final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); |
7 |
keyStore.load(null); |
8 |
final KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry("MyKeyAlias", null); |
9 |
final RSAPrivateKey privateKey = (RSAPrivateKey)privateKeyEntry.getPrivateKey(); |
10 |
|
11 |
final Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL"); |
12 |
cipher.init(Cipher.DECRYPT_MODE, privateKey); |
13 |
|
14 |
final CipherInputStream cipherInputStream = new CipherInputStream(new ByteArrayInputStream(encryptedBytes), cipher); |
15 |
final ArrayList<Byte> arrayList = new ArrayList<>(); |
16 |
int nextByte; |
17 |
while ( (nextByte = cipherInputStream.read()) != -1 ) |
18 |
{
|
19 |
arrayList.add((byte)nextByte); |
20 |
}
|
21 |
|
22 |
decryptedBytes = new byte[arrayList.size()]; |
23 |
for(int i = 0; i < decryptedBytes.length; i++) |
24 |
{
|
25 |
decryptedBytes[i] = arrayList.get(i); |
26 |
}
|
27 |
}
|
28 |
catch (Throwable e) |
29 |
{
|
30 |
e.printStackTrace(); |
31 |
}
|
32 |
|
33 |
return decryptedBytes; |
34 |
}
|
Satu hal tentang RSA adalah enkripsi lebih lambat dari AES. Ini tidak apa-apa untuk data yang jumlahnya sedikit, seperti mengamankan shared preference strings. Jika Anda menemukan masalah performa mengenkripsi sebuah data, namun, Anda bisa menggunakan contoh ini untuk mengenkripsi dan menyimpannya sama seperti AES key. Kemudian, gunakan enkripsi AES yang lebih cepat yang telah kita diskusikan pada tutorial sebelumnya untuk sisa data Anda. Anda bisa membuat AES key baru dan mengubahnya menjadi byte[]
array yang cocok seperti contoh berikut.
1 |
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); |
2 |
keyGenerator.init(256); //AES-256 |
3 |
SecretKey secretKey = keyGenerator.generateKey(); |
4 |
byte[] keyBytes = secretKey.getEncoded(); |
Untuk mendapatkan key kembali dari bytes, lakukan ini:
1 |
SecretKey key = new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES"); |
Ada banyak sekali kode! Untuk membuat semua contoh mudah, saya telah menghilangkan secara menyeluruh exception handling. Tapi yang perlu diingat adalah produksi dari kode Anda, ini tidak di rekomendasikan untuk mengambil semua Throwable
cases pada satu statement.
Kesimpulan
Ini melengkapi tutorial tentang bekerja pada credential dan keys. Sangat membingunkan pada keys dan storage karena evolusi pada Android OS, tetapi Anda bisa memilih solusi dari API level yang aplikasi Anda dukung.
Sekarang kita sudah mencakup cara terbaik untuk mengamankan data seluruhnya, tutorial berikutnya akan fokus pada mengamankan data di transit.