At some point in time, we all wanted to keep our data secure from being hacked/reverse engineered. There are many known methods that don’t work so efficiently when the apps are reverse engineered. To understand that, you can try various mechanisms you know to secure keys here. But rather there’s one mechanism that can help us in securing secret key in Android-Keystore Encryption. 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 the 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 is generally used by applications for storing very sensitive data like Credit Card details, Bank Account, and such.
Implementation
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
- 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
- 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 here.
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.