DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 963,274 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Cover image for Securing secret key in Android using Keystore
Varun Dwarkani
Varun Dwarkani

Posted on

Securing secret key in Android using Keystore

At some point in time, we all wanted to keep our data secure from being hacked/reverse engineered. The basic security mechanisms like,

a. ProGuard, ShrinkResources & minifyEnabled
b. Hiding in Manifest
c. Hiding in Build.Gradle
d. Storing in MySQL DB/Room DB/SharedPreference
e. Hiding in Strings.xml.

All these methods fail to provide maximum security. The most common and known logic of securing the key can be bypassed either by reversing and taking it from dex->jar file or by rooting the device and accessing the device storage.
But, there is one more method that beats them all. The mechanism which is generally used by applications for storing very sensitive data like Credit Card details, Bank Account and such.

Keystore is bound to hardware security which is generally used to store cryptographic keys. It becomes incredibly difficult for hackers to get access to it since a Keystore is very specific to every application. More of introduction done let us jump to the code now -

Declare few variables in Cryptor.java

private static final String TRANSFORMATION = β€œAES/GCM/NoPadding”;
private static final String ANDROID_KEY_STORE = β€œAndroidKeyStore”;
private byte[] iv;
private KeyStore keyStore;
private static final String SAMPLE_ALIAS = β€œMYALIAS”;

a. TRANSFORMATION is used for setting the algorithm which will be used for encoding.
b. iv is known as Initialization Vector which is an arbitrary number used along with a secret key for encryption. (It can be stored in public storage like SharedPreference, Room DB or MySQL DB).
c. SAMPLE_ALIAS is used to access the entity stored inside the Keystore.

ENCRYPTING DATA

For encrypting a value with the Keystore, we can do it by,

a. Create an object of the Cryptor class.
b. Use a setIv method to init the cipher using a secret key in Cryptor class.
c. Encrypt the text using an encryption function defined in Cryptor class.
d. Store the Iv and encrypted text (The Iv can be made public and it does not cause any issue) in SharedPreference or Room Database.

In RegistrationActivity.java

Cryptor cryptor = new Cryptor();
try {
    cryptor.setIv();
    prefs.edit().putString("encryptedKey", cryptor.encryptText("text_to_be_encrypted")).apply();
    prefs.edit().putString("keyIv", cryptor.getIv_string()).apply();
    Intent intent = new Intent(RegistrationActivity.this, HomeScreen.class);
    startActivity(intent);
    finish();
} catch (NoSuchPaddingException e) {
    unexpectedError();
    e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
    unexpectedError();
    e.printStackTrace();
} catch (NoSuchProviderException e) {
    unexpectedError();
    e.printStackTrace();
} catch (InvalidAlgorithmParameterException e) {
    unexpectedError();
    e.printStackTrace();
} catch (InvalidKeyException e) {
    unexpectedError();
    e.printStackTrace();
}

In Cryptor.java, we define the following functions

1. setIv() method :

public void setIv() throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, InvalidKeyException {
    Cipher cipher = Cipher.getInstance(TRANSFORMATION);
    cipher.init(Cipher.ENCRYPT_MODE, getSecretKey_en());
    iv = cipher.getIV();
}

2. getSecretKey_en() method :

@NonNull
private SecretKey getSecretKey_en() throws NoSuchAlgorithmException,
        NoSuchProviderException, InvalidAlgorithmParameterException {
    final KeyGenerator keyGenerator;
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
        keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
        keyGenerator.init(new KeyGenParameterSpec.Builder(Cryptor.SAMPLE_ALIAS,
                KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                .build());
        return keyGenerator.generateKey();
    } else {
        keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
        keyGenerator.init(new KeyGenParameterSpec.Builder(Cryptor.SAMPLE_ALIAS,
                KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                .build());
        return keyGenerator.generateKey();
    }
}

3. encryptText(String string_to_encrypt) :

public String encryptText(String string_to_encrypt) {
    try {
        final byte[] encryptedText = encryptData(string_to_encrypt);
        return Base64.encodeToString(encryptedText, Base64.DEFAULT);
    } catch (NoSuchAlgorithmException | NoSuchProviderException |
            NoSuchPaddingException | InvalidKeyException e) {
        e.printStackTrace();
    } catch (InvalidAlgorithmParameterException |
            IllegalBlockSizeException | BadPaddingException e) {
        e.printStackTrace();
    }
    return "";
}

4. encryptData(String text_to_encrypt) :

private byte[] encryptData(final String textToEncrypt)
        throws NoSuchAlgorithmException,
        NoSuchProviderException, NoSuchPaddingException, InvalidKeyException,
        InvalidAlgorithmParameterException, BadPaddingException,
        IllegalBlockSizeException {
    final Cipher cipher = Cipher.getInstance(TRANSFORMATION);
    cipher.init(Cipher.ENCRYPT_MODE, getSecretKey_en());
    iv = cipher.getIV();
    return (cipher.doFinal(textToEncrypt.getBytes(StandardCharsets.UTF_8)));
}

5. getIv_string() :

public String getIv_string() {
    return Base64.encodeToString(iv, Base64.DEFAULT);
}

Explanation: We generate a secret key using the keyStore with specific algorithms and the ALIAS. the secret key which is generated is used to init the cipher and get the IV. The encrypt text function uses the text and the iv to encrypt the text in the Keystore and gives the encrypted text which can be stored in any general storage medium.

DECRYPTING DATA

For decrypting a value with the Keystore, we can do it by,
a. Create an object of the Cryptor class.
b. Initialize the KeyStore instance.
c. Use the decrypt function by passing the encrypted text and the iv (stored in SharedPreference or Room Database).

In HomeScreen.java

String iv = prefs.getString("keyIv", "null");
String encrypted = prefs.getString("encryptedKey", "");
try {
    Cryptor cryptor = new Cryptor();
    cryptor.initKeyStore();
    String decrypted = cryptor.decryptText(encrypted, iv);
} catch (KeyStoreException e) {
    e.printStackTrace();
} catch (CertificateException e) {
    e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

In Cryptor.java, add the following functions

1.initKeyStore() :

public void initKeyStore() throws KeyStoreException, CertificateException,
        NoSuchAlgorithmException, IOException {
    keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
    keyStore.load(null);
}

2. decryptText(String encrypted_string, String iv) :

public String decryptText(String encrypted, String iv) {
    try {
        return decryptData(encrypted, Base64.decode(iv,Base64.DEFAULT));
    } catch (UnrecoverableEntryException | NoSuchAlgorithmException |
            KeyStoreException | NoSuchPaddingException | InvalidKeyException e) {
        e.printStackTrace();
    } catch (IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) {
        e.printStackTrace();
    }
    return "";
}

3. decryptData(String encrypted_string, byte[] Iv) :

private String decryptData(String encrypted, final byte[] encryptionIv)
        throws UnrecoverableEntryException, NoSuchAlgorithmException, KeyStoreException,
        NoSuchPaddingException, InvalidKeyException,
        BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {
    final Cipher cipher = Cipher.getInstance(TRANSFORMATION);
    final GCMParameterSpec spec = new GCMParameterSpec(128, encryptionIv);
    cipher.init(Cipher.DECRYPT_MODE, getSecretKey_de(), spec);
    byte[] encryptedData = Base64.decode(encrypted,Base64.DEFAULT);
    return new String(cipher.doFinal(encryptedData), StandardCharsets.UTF_8);
}

Explanation: While decrypting, we get the stored Iv and encrypted text stored in our one of the storage medium. We initialize the Keystore using the ANDROID_KEY_STORE and decrypt the text using the Iv and by the init and doFinal method of the Cipher.

CONCLUSION:

So, with the above implementation, secrets are now safe in the KeyStore. Why it is probably the best method is because KeyStore is very specific to the application. It cannot be retrieved and hence the text cannot be decrypted without it. Many applications which stores the Credit Card and other sensitive data of the users use this encryption method to keep it safe.
For the entire code, you can look into my GitHub repository.
Explaining the code, I have defined files like β€” Cryptor.java, RegistrationActivity.java and HomeScreen.java. I have also used Room Database (launched by Google in I/O 2018) which provides high-level security than SQLLite (can be accessed if the device is rooted) to store the username and password to authenticate the registered users.

https://github.com/varundwarkani/KeystoreEncryption-Android

Top comments (0)

🌚 Browsing with dark mode makes you a better developer.

It's a scientific fact.