Storing credentials in the Android app

10,554

Solution 1

In android applications you may store data in the SharedPreferences but since this data is actually stored in a file, anyone with root access to a phone may access it. That means a security leak if you want to store credentials or any other sensitive data.

In order to avoid other persons to see this data in plain text a solution is to encrypt the data before storing it. From API 18 Android introduced the KeyStore which is able to store keys in which you encrypt and decrypt the data.

Problem until API 23 is that you were not able to store AES keys in KeyStore so the most reliable key for encryption was RSA with private and public key.

So the solution I came up with was:

For APIs below 23

  • You generate an RSA private and public key and save it in KeyStore, generate an AES key, encrypt it with RSA public key and save it to SharedPreferences.
  • Every time you need to save encrypted data in SharedPreferences with the AES key, you get the encrypted AES key from SharedPreferences, decrypt it with RSA private key and encrypt the data you want to save to SharedPreferences with the already decrypted AES key.
  • To decrypt the data the process is pretty much the same, get encrypted AES key from SharedPreferences, decrypt it with the RSA private key, get the encrypted data from SharedPreferences you want to decrypt, and decrypt it with the decrypted AES key.

For API 23 and above

  • just generate and store an AES key in KeyStore, and access it whenever you want for data encryption/decryption.

Also added a generated IV for the encryption.

Code:

public class KeyHelper{


    private static final String RSA_MODE =  "RSA/ECB/PKCS1Padding";
    private static final String AES_MODE_M = "AES/GCM/NoPadding";

    private static final String KEY_ALIAS = "KEY";
    private static final String AndroidKeyStore = "AndroidKeyStore";
    public static final String SHARED_PREFENCE_NAME = "SAVED_TO_SHARED";
    public static final String ENCRYPTED_KEY = "ENCRYPTED_KEY";
    public static final String PUBLIC_IV = "PUBLIC_IV";


    private KeyStore keyStore;
    private static KeyHelper keyHelper;

    public static KeyHelper getInstance(Context ctx){
        if(keyHelper == null){
            try{
                keyHelper = new KeyHelper(ctx);
            } catch (NoSuchPaddingException | NoSuchProviderException | NoSuchAlgorithmException | InvalidAlgorithmParameterException | KeyStoreException | CertificateException | IOException e){
                e.printStackTrace();
            }
        }
        return keyHelper;
    }

    public KeyHelper(Context ctx) throws  NoSuchPaddingException,NoSuchProviderException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, KeyStoreException, CertificateException, IOException  {
        this.generateEncryptKey(ctx);
        this.generateRandomIV(ctx);
        if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M){
            try{
                this.generateAESKey(ctx);
            } catch(Exception e){
                e.printStackTrace();
            }
        }
    }


    private void generateEncryptKey(Context ctx) throws  NoSuchProviderException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, KeyStoreException, CertificateException, IOException {

        keyStore = KeyStore.getInstance(AndroidKeyStore);
        keyStore.load(null);

        if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M){
            if (!keyStore.containsAlias(KEY_ALIAS)) {
                KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, AndroidKeyStore);
                keyGenerator.init(
                        new KeyGenParameterSpec.Builder(KEY_ALIAS,
                            KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                                .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                                .setRandomizedEncryptionRequired(false)
                                .build());
                keyGenerator.generateKey();
            }
        } else{
            if (!keyStore.containsAlias(KEY_ALIAS)) {
                // Generate a key pair for encryption
                Calendar start = Calendar.getInstance();
                Calendar end = Calendar.getInstance();
                end.add(Calendar.YEAR, 30);
                KeyPairGeneratorSpec spec = new   KeyPairGeneratorSpec.Builder(ctx)
                        .setAlias(KEY_ALIAS)
                        .setSubject(new X500Principal("CN=" + KEY_ALIAS))
                        .setSerialNumber(BigInteger.TEN)
                        .setStartDate(start.getTime())
                        .setEndDate(end.getTime())
                        .build();
                KeyPairGenerator kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, AndroidKeyStore);
                kpg.initialize(spec);
                kpg.generateKeyPair();
            }
        }


    }

    private byte[] rsaEncrypt(byte[] secret) throws Exception{
        KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(KEY_ALIAS, null);
        // Encrypt the text
        Cipher inputCipher = Cipher.getInstance(RSA_MODE, "AndroidOpenSSL");
        inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.getCertificate().getPublicKey());

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, inputCipher);
        cipherOutputStream.write(secret);
        cipherOutputStream.close();

        return outputStream.toByteArray();
    }

    private  byte[]  rsaDecrypt(byte[] encrypted) throws Exception {
        KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(KEY_ALIAS, null);
        Cipher output = Cipher.getInstance(RSA_MODE, "AndroidOpenSSL");
        output.init(Cipher.DECRYPT_MODE, privateKeyEntry.getPrivateKey());
        CipherInputStream cipherInputStream = new CipherInputStream(
            new ByteArrayInputStream(encrypted), output);
        ArrayList<Byte> values = new ArrayList<>();
        int nextByte;
        while ((nextByte = cipherInputStream.read()) != -1) {
            values.add((byte)nextByte);
        }

        byte[] bytes = new byte[values.size()];
        for(int i = 0; i < bytes.length; i++) {
            bytes[i] = values.get(i).byteValue();
        }
        return bytes;
    }

    private void generateAESKey(Context context) throws  Exception{
        SharedPreferences pref = context.getSharedPreferences(SHARED_PREFENCE_NAME, Context.MODE_PRIVATE);
        String enryptedKeyB64 = pref.getString(ENCRYPTED_KEY, null);
        if (enryptedKeyB64 == null) {
            byte[] key = new byte[16];
            SecureRandom secureRandom = new SecureRandom();
            secureRandom.nextBytes(key);
            byte[] encryptedKey = rsaEncrypt(key);
            enryptedKeyB64 = Base64.encodeToString(encryptedKey, Base64.DEFAULT);
            SharedPreferences.Editor edit = pref.edit();
            edit.putString(ENCRYPTED_KEY, enryptedKeyB64);
            edit.apply();
        }
    }


    private Key getAESKeyFromKS() throws  NoSuchProviderException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, KeyStoreException, CertificateException, IOException, UnrecoverableKeyException{
        keyStore = KeyStore.getInstance(AndroidKeyStore);
        keyStore.load(null);
        SecretKey key = (SecretKey)keyStore.getKey(KEY_ALIAS,null);
        return key;
    }


    private Key getSecretKey(Context context) throws Exception{
        SharedPreferences pref = context.getSharedPreferences(SHARED_PREFENCE_NAME, Context.MODE_PRIVATE);
        String enryptedKeyB64 = pref.getString(ENCRYPTED_KEY, null);

        byte[] encryptedKey = Base64.decode(enryptedKeyB64, Base64.DEFAULT);
        byte[] key = rsaDecrypt(encryptedKey);
        return new SecretKeySpec(key, "AES");
    }

    public String encrypt(Context context, String input) throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException, BadPaddingException, IllegalBlockSizeException, UnsupportedEncodingException {
        Cipher c;
        SharedPreferences pref = context.getSharedPreferences(SHARED_PREFENCE_NAME, Context.MODE_PRIVATE);
        String publicIV = pref.getString(PUBLIC_IV, null);

        if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M){
            c = Cipher.getInstance(AES_MODE_M);
            try{
                c.init(Cipher.ENCRYPT_MODE, getAESKeyFromKS(), new GCMParameterSpec(128,Base64.decode(publicIV, Base64.DEFAULT)));
            } catch(Exception e){
                e.printStackTrace();
            }
        } else{
            c = Cipher.getInstance(AES_MODE_M);
            try{
                c.init(Cipher.ENCRYPT_MODE, getSecretKey(context),new GCMParameterSpec(128,Base64.decode(publicIV, Base64.DEFAULT)));
            } catch (Exception e){
                e.printStackTrace();
            }
        }
        byte[] encodedBytes = c.doFinal(input.getBytes("UTF-8"));
        return Base64.encodeToString(encodedBytes, Base64.DEFAULT);
    }





    public String decrypt(Context context, String encrypted) throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException, BadPaddingException, IllegalBlockSizeException, UnsupportedEncodingException {
        Cipher c;
        SharedPreferences pref = context.getSharedPreferences(SHARED_PREFENCE_NAME, Context.MODE_PRIVATE);
        String publicIV = pref.getString(PUBLIC_IV, null);


        if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M){
            c = Cipher.getInstance(AES_MODE_M);
            try{
                c.init(Cipher.DECRYPT_MODE, getAESKeyFromKS(), new GCMParameterSpec(128,Base64.decode(publicIV, Base64.DEFAULT)));

            } catch(Exception e){
                e.printStackTrace();
            }
        } else{
            c = Cipher.getInstance(AES_MODE_M);
            try{
                c.init(Cipher.DECRYPT_MODE, getSecretKey(context), new GCMParameterSpec(128,Base64.decode(publicIV, Base64.DEFAULT)));
            } catch (Exception e){
                e.printStackTrace();
            }
        }

        byte[] decodedValue = Base64.decode(encrypted.getBytes("UTF-8"), Base64.DEFAULT);
        byte[] decryptedVal = c.doFinal(decodedValue);
        return new String(decryptedVal);
    }

    public void generateRandomIV(Context ctx){
        SharedPreferences pref = ctx.getSharedPreferences(SHARED_PREFENCE_NAME, Context.MODE_PRIVATE);
        String publicIV = pref.getString(PUBLIC_IV, null);

        if(publicIV == null){
            SecureRandom random = new SecureRandom();
            byte[] generated = random.generateSeed(12);
            String generatedIVstr = Base64.encodeToString(generated, Base64.DEFAULT);
            SharedPreferences.Editor edit = pref.edit();
            edit.putString(PUBLIC_IV_PERSONAL, generatedIVstr);
            edit.apply();
        }
    }

    private String getStringFromSharedPrefs(String key, Context ctx){
        SharedPreferences prefs = ctx.getSharedPreferences(MyConstants.APP_SHAREDPREFS, 0);
        return prefs.getString(key, null);
    }
}

NOTE: This is only for API 18 and above

Solution 2

Regarding your question about security on rooted device, I would recommend you the following paper:

Analysis of Secure Key Storage Solutions on Android

Share:
10,554
SolderingIronMen
Author by

SolderingIronMen

Updated on June 04, 2022

Comments

  • SolderingIronMen
    SolderingIronMen almost 2 years

    How can we safely storing credentials data for access to the smtp-server in Android app? These data are constants and only the developer should know them. At the moment they are stored in the code, but this is not safe, because they can be seen by decompiling the application.

    Is it possible to use Android Keystore System for this purpose and how? And most importantly, will Android Keystore be effective on rooted devices?

  • Prerak Sola
    Prerak Sola about 7 years
    You could mark the question as a duplicate if the referred question answer's OP's question. If not, add the relevant code from the link in your answer and add the link as a reference.
  • SolderingIronMen
    SolderingIronMen about 7 years
    Thanks for the answer. Do I understand correctly that no password is required to access Keysore? And there can not be a third-party application to access it?
  • SolderingIronMen
    SolderingIronMen about 7 years
    So, how is the data encryption in keystore ensured?
  • SolderingIronMen
    SolderingIronMen about 7 years
    Ok, thanks for the answer. But how can I save the access data in the apk-file at the stage of app creation?
  • Ricardo
    Ricardo about 7 years
    You dont save any data in the apk-file besides what your code generates
  • SolderingIronMen
    SolderingIronMen about 7 years
    And what about the key that we sign our application?
  • Ricardo
    Ricardo about 7 years
    That key is provided by Google so that you can sign the APK in order to be able to upload it to play store
  • SolderingIronMen
    SolderingIronMen about 7 years
    Is it possible to use a signed apk certificate in our app?
  • Ricardo
    Ricardo about 7 years
    what do you mean?
  • SolderingIronMen
    SolderingIronMen about 7 years
    1. We encrypt our credentials with a unique key and write them into the code as constants. 2. We sign the apk-file with a same unique key when creating. 3. Then we use this key to decrypt the credentials data in the code. Is it possible?
  • Ricardo
    Ricardo about 7 years
    Using this code, you can encrypt and decrypt anything you want, incluiding a given key. Thing is, to encrypt that key, you need to hardcode it because it is not a generated key from the key generator
  • SolderingIronMen
    SolderingIronMen about 7 years
    You have provided a code that allows you to encrypt the credentials received during the operation of the application (for example, entered by the user). And we need to encrypt the constants data created once during the production phase of the application (for example, [email protected], 123456). We can not hardcode them in their pure form, since they can be seen by means of decompilation. Therefore, the question arises, how to transfer credentials to our application? Preferably, without the use of a server.
  • SolderingIronMen
    SolderingIronMen about 7 years
  • SolderingIronMen
    SolderingIronMen about 7 years
    That is, there are no safe methods of data storing on rooted devices?
  • Gregor Ažbe
    Gregor Ažbe over 5 years
    It worked very slow for me because it is decrypting AES key every time you need it. I fixed it with loading and decryipting it in constructor.
  • Ricardo
    Ricardo over 5 years
    You should use the getInstance method, it will only run the creation of your KeyHelper once.
  • ahmetvefa53
    ahmetvefa53 about 3 years
    hi. why you are generating AES key below 23? you can encyrpt password with public key and decrpyt with private key directly . Is it not safe to use rsa keys directly?
  • Admin
    Admin almost 2 years
    "but since this data is actually stored in a file, anyone with root access to a phone may access it" -- if an attacker has root access, then the attacker can also get access to any keys that are stored on the device. So (in the case of root), encrypting the data as opposed to using plain SharedPreferences does not make a difference. This is still true with KeyStore, as root can just request that the TEE/SoC/HSM decrypt the data for it.
  • Admin
    Admin almost 2 years
    @SolderingIronMen The KeyStore is not used to store encrypted data. Its meant to store encryption keys that are never revealed to the OS. The OS can request that data is encrypted/decrypted with the keys but the OS will never see the keys. This is important when the OS is compromised because an attacker will never learn the private keys of the device. Note, however, this does not stop the attacker from requesting the KeyStore to decrypt data that is on the device. Therefore, the KeyStore does not protect against root from reading data stored on the device that is encrypted with the KeyStore.