Advertisement
  1. Code
  2. JavaScript
  3. Web APIs

Mga Keys, Credentials at Storage sa Android

Scroll to top

() translation by (you can also view the original English article)

Sa nakaraang post tungkol sa seguridad ng Android user data, tinignan natin ang pag-eencrypt ng datos sa pamamagitan ng passcode na binigay ng user. Ililipat ng pagtuturong ito ang focus sa credential at key storage. Sisismulan ko sa pamamagitan ng pagpapakilala ng mga account credentials at tatapusin ito sa pamamagitan ng isang halimbawa ng paprotekta ng data gamit ang KeyStore.

Kadalasan, kapag gumagamit ng isang third-party service, mangangailangan ng ilang uri ng authentication. Maaaring kasingsimple lang ito ng isang /login endpoint na tumatanggap ng isang username at password.

Sa una, mukhang ang pinaksimpleng solusyon ay bumuo ng isang UI na nanghihingi sa user na maglog-in, pagkatapos ay kunin at iimbak ang kanilang mga login credentials. Gayunpaman, hindi ito ang pinakamahusay na kasanayan sapagkat hindi dapat kailanganing alamin ng ating app ang ating mga credentials para sa isang third-party account. Sa halip, maaari nating gamitin ang Account Manager, na nagtatalaga ng pag-aasikaso ng sensitibong kaaalaman na ito para sa atin.

Ang Account Manager

Ang Account Manager ay isang sentralisadong taga-alalay para sa mga credentials ng user account para hindi na tuwirang alalahanin ng iyong app ang pag-aasikaso ng mga password. Kadalasan itong nagbibigay ng isang token kapalit ng tunay na username at password na maaaring gamitin upang gumawa ng mga authenticated na mga requests sa isang service. Ang isang halimbawa ay kapag nag-rerequest ng isang token ng OAuth2.

Minsan, ang kinakailangan lang na impormasyon ay nakalagay na sa device, at sa ibang pagkakataon naman ay kakailanganin ng Account Manager tumawag sa server para sa isang na-refresh na token. Maaaring nakita mo na ang Accounts section sa Settings ng iyong device para sa sari-saring mga apps. Maaari tayong makakuha ng listahan ng mga available accounts na gaya nito:

1
AccountManager accountManager = AccountManager.get(this);
2
Account[] accounts = accountManager.getAccounts();

Ang code ay mangangailangan ng permission ng android.permission.GET_ACCOUNTS Kung may isang tiyak na account kang hinahanap, maaari mo itong hanapin sa ganitong paraan:

1
AccountManager accountManager = AccountManager.get(this);
2
Account[] accounts = accountManager.getAccountsByType("com.google");

Kapag nasa iyo na ang account, makakakuha ka ng isang token para sa account sa pamamagitan ng pagtawag sa getAuthToken (Account, String, Bundle, Activity, AccountManagerCallback, Handler) method. Ang token ay maaaring gamitin upang gumawa ng mga authenticated na API requests sa isang service. Maaari itong maging isang RESTful na API, kung saan ipinapasa mo ang isang token parameter habang ginagawa ang HTTPS request, ng hindi inaalam ang pribadong account details ng user.

Dahil ang bawat serbisyo ay may iba-ibang paraan ng pag-aauthenticate at pag-iimbak ng mga pribadong credentials, binibigay ng Account Manager ang mga authenticator modules para maipatupad ang isang third-party service. Habang ang Android ay mayroong mga pagpapatupad para sa maraming popular na mga serbisyo, nangangahuluhan iyong maaari kang magsulat ng sarili mong authenticator upang asikasuhin ang account authentication at pag-iimbak ng credential ng iyong app. Binibigyan ka nito ng kakayahang siguruhing ang mga credentials ay encrypted. Tandaan, nangangahulugan din ito na ang mga credentials sa Account Manager na ginagamit ng ibang mga serbisyo ay maaaring iimbak sa clear text, na ginagawa itong madaling makita sa sinumang naka-root ang device.

Sa halip na simpleng credentials lang, may mga panahong kakailanganin mong gumamit ng isang key o isang certificate para sa isang indibidwal o entity-halimbawa, kapag ang isang third party ay nagpadala sa iyo ng isang certificate file na kailangan mong itago. Ang pinakamadalas na scenario ay kapag ang app ay kailangang mag-authenticate sa server ng isang pribadong organisasyon.

Sa susunod na pagtuturo, tatalakayin natin ang paggamit ng mga certificates para sa authentication at ligtas na komunikasyon, ngunit gusto ko munang talakayin kung papaano iimbak ang mga items na ito. Ang Keychain API ay orihinal na binuo para sa tiyak na gamit na ito-pag-iinstall ng isang pribadong key o pares ng certificate mula sa isang PKCS#12 na file.

Ang Keychain

Ipinakilala sa Android 4.0 (API_Level 14), ang Keychain_API ay tumatalakay sa pamamahala ng mga keys. Sa katiyakan, gumagana ito sa mga PrivateKey at X509Certificate na objects at nagbibigay ng isang mas ligtas na lalagyan kaysa sa paggamit ng imbakan ng datos ng iyong app. Ito’y dahil ang mga permissions para sa pribadong keys ay nagbibigay lang ng kakayahan sa iyong sariling app upang maaccess ang mga keys, matapos ang authorization ng user. Ibig sabihin nito ay kailangang magsetup ng lock screen sa device bago kayo makagamit ng imbakan ng mga credentials. Higit pa riyan, ang mga objects sa keychain ay maaaring nakatakdang bigyang-seguridad ang hardware, kung available.

Ang code para mag-install ng isang certificate ay gaya ng sumusunod:

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);

Ang user ay hihingan ng isang password upang ma-access ang pribadong key at isang pagkakataong pangalanan ang certificate. Upang mabawi ang key, ang sumusunod na code ay nagpapakita ng isang UI na hinahayaan ang user na mamili mula sa isang listahan ng mga naka-install nang mga keys.

1
KeyChain.choosePrivateKeyAlias(this, this, new String[]{"RSA"}, null, null, -1, null);

Kapag nakapili na, isang pangalan ng string alias ang ibabalik sa alias(final String alias) na callback kung saan maaari ninyong tuwirang maaccess ang pribadong key o certificate chain.

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
}

Taglay ang kaalamang ito, tignan natin kung papaano natin pwedeng gamitin ang imbakan ng mga credentials upang i-save ang sarili ninyong sensitibong datos.

Ang KeyStore

Sa nakaraang pagtuturo, tinalakay natin ang pagprotekta ng datos sa pamamagitan ng passcode na binigay ng user. Ang ganitong uri ng setup ay mabuti, ngunit ang mga pangangailangan ng app ay madalas umiiwas sa pangangailangang maglog-in ng user kada oras at umalala ng karagdagang passcode.

Dito maaaring magamit ang Keystore_API. Mula sa API 1, ginagamit na ny system ang KeyStore upang mag-imbak ng mga credentials ng WiFi at VPN. Sa 4.3 (API 18), binibigyan ka nito ng kakayahang kumilos gamit ang sarili mong app-sepcific na mga keys, at sa Android M (API 23) naman ay maaari itong mag-imbak ng AES symmetric na key. Kaya habang hindi pa pinapayagan ng API ang tuwirang pag-iimbak ng mga sensitibong strings, maaaring iimbak ang mga keys na ito at pagkatapos ay gamitin ito upang mag-encrypt ng mga strings.

Ang benepisyo ng pag-iimbak ng isang key sa KeyStore ay hinahayaan nito ang mga keys na mapatakbo ng hindi ibinubunyag ang lihim na laman ng key na ito; hindi pumapasok sa app space ang key data. Tandaan na ang mga keys ay protektado ng mga permissions upang tanging ang app ninyo lang ang makaka-access sa mga ito, at karagdagang maaaring maging maging secure hardware-backed kung kaya ng device. Gumagawa ito ng isang lalagyan na pinapahirap ang pag-eextract ng mga keys mula sa isang device.

Lumikha ng Bagong Random Key

Para sa halimbawang ito, sa halip na lumikha ng isang AES key mula sa isang passcode na binigay ng passcode, maaari tayong mag auto-generate ng isang random key na magiging protektado sa isang KeyStore. Magagawa natin ito sa pamamagitan ng paglikha ng isang KeyGenerator na instance, na naka-set sa "AndroidKeyStore".

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

Ang mahahalagang bahaging makita dito ay ang mga .setUserAuthenticationRequired(true) at .setUserAuthenticationValidityDurationSeconds(120) na specifications. Nangangailangan ang mga ito ng isang lock screen upang mai-setup at ang key na kailangang mai-lock hanggang ang user ay ma-authenticate.

Kapag tinignan natin ang documentation para sa .setUserAuthenticationValidityDurationSeconds(), makikita ninyo na nangangahulugan itong ang key ay available lamang sa loob ng iilang Segundo mula sa password authentication, at ang pagpapasa ng -1 ay nangangailangan ng fingerprint authentication sa bawat pagkakataong gusto mong i-access ang key. Ang pag-eenable ng pangangailangang mag-authenticate ay taglay din ang effect ng pagbawi ng key kapag tinanggal o binago ng user ang kanyang lock screen.

Dahil ang pag-iimbak ng isang di-protektadong key kasabay ng encrypted na datos ay gaya ng paglalagay ng susi ng bahay sa ilalim ng doormat, ang mga pagpipiliang ito ay sinusubukang protektahan ang naka-tenggang key sa oras na ang isang device ay makompromiso. Isang maaaring maging halimbawa ay ang isang offline data dump ng device. Kung wala ang password na kilala para sa device, ang datos na ito ay walang silbi.

Ang .setRandomizedEncryptionRequired(true) na option ay ine-enable ang requirement na mayronng sapat na randomization(isang bagong random IV kada panahon) para kung ang parehong datos ay na-encrypt sa ikalawang pagkakataon, ang encrypted output ay magiging iba parin. Pinipigilan nito ang isang attacker sa pagkamit ng mga palatandaan tungkol sa ciphertext batay sa pag-feed sa parehong datos.

Ang isa pang pagpipiliang maaaring isaalang-alang ay ang setUserAuthenticationValidWhileOnBody(boolean remainsValid), na ikinakandado ang key kapag napag-alaman nitong ang device ay wala na sa tao.

Pag-eencrypt ng Datos

Ngayon na ang key ay nakaimbak na sa KeyStore, maaari na tayong gumawa ng isang method na nag-eencrypt ng datos gamit ang Cipher object, kapag mayroon nang SecretKey. Magbabalik ito ng isang HashMap na nilalaman ang encrypted na datos at isang randomized IV na kakailanganin upang i-decrypt ang datos. Ang encrypted na datos, kasama ng IV, ay maaaring iimbak sa isang file o tungo sa nakabahaging mga 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
}

Pag-dedecrypt patungo sa isang Byte Array

Para sa decryption, ginagamit natin ang kabaliktaran Ang Cipher object ay ini-initialize gamit ang DECRYPT_MODE constant, at ang decrypted byte na byte[] array ay ibinabalik.

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
}

Pagsubok sa Halimbawa

Maaari na nating subukin an gating halimbawa!

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
}

Gamit ang Asymmetric Keys ng RSA para sa Mga Mas Lumang Devices

Ito ay isang magandang solusyon para mag-imbak ng datos para sa bersyong M at mas mataas pa, ngunit papaano naman kung suportado ng iyong app ang mga naunang mga bersyon? Habang ang mga symmetric keys ng AES ay hindi suportado sa ilalim ng M, ang symmetric keys ng RSA ay suportado naman. Ibig sabihin nito ay maaari nating gamitin ang mga RSA keys at encryption upang magawa ang parehong bagay.

Ang pinakapinagkaiba nito ay ang isang asymmetric keypair ay naglalaman ng dalawang keys, isang pribado at publikong key,kung saan ang pampublikong key ay ine-encrypt ang datos at ang pribadong key ay dine-decrypt ito. Ang AKeyPairGeneratorSpec ay ipinapasa tungo sa KeyPairGenerator na ini-initialize gamit ang provider ng KEY_ALGORITHM_RSA at "AndroidKeyStore".

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
}

Upang mag-encrypt, kukunin natin ang RSAPublicKey mula sa keypair at gamitin ito kasama ang 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
}

Ang decryption ay ginagawa gamit ang RSAPrivateKey object.

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
}

Ang isang bagay mula sa RSA ay ang encryption ay mas mabagal kaysa sa AES. Kadalasan ay ayos lang ito para sa kakaunting dami ng impormasyon, gaya ng kapag nagse-secure ng mga strings para sa shared preference. Kung mapapagalaman mong mayroong isang problema sa performance habang nag-eencrypt ng malalaking dami ng datos, maaari mong gamitin sa halip ang halimbawang ito at mag-imbak lang ng AES key. Matapos ito, gamitin ang mas mabilis na AES encryption na tinalakay sa nakaraang pagtuturo para sa kabuuan ng iyong datos. Maaari kang mag-generate ng isang bagong AES key at i-convert ito sa isang byte[] array na compatible sa halimbawang ito.

1
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
2
keyGenerator.init(256); //AES-256

3
SecretKey secretKey = keyGenerator.generateKey();
4
byte[] keyBytes = secretKey.getEncoded();

Upang makuha ang key pabalik mula sa mga bytes, ito ang kailangang gawin:

1
SecretKey key = new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES");

Andaming code noon! Upang mapanatiling simple ang lahat ng mga halimbawang ito, tinanggal ko na ang masinsinang exception handling. Ngunit tandaan na para sa iyong production code, hindi inirerekumenda na i-catch ang lahat ng Throwable na mga cases sa loob ng isang catch statement.

Konklusyon

Dito nagtatapos ang pagtuturo sa pag-asikaso ng mga credentials at mga keys. Karamihan sa kalituhan sa mga keys at pag-iimbak ay may kinalaman sa ebolusyon ng Android OS, ngunit maaari mong piliin ang solusyon ang gusto mong gamitin kung alam mo ang antas ng API na suportado ng iyong app.

Ngayong natalakay na natin ang lahat ng pinakamahusay na kasanayan sa pagse-secure ng datos at ang iba pa, ang susunod na pagtuturo ay tututok sa pagse-secure ng datos habang inihahatid ito.

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.