I am trying to create a simple Kotlin object that wraps access to the app's shared preferences by encrypting content before saving it.
Encrypting seems to work OK but when I try to decrypt, I get an javax.crypto.AEADBadTagException which stems from an android.security.KeyStoreException with a message of "Signature/MAC verification failed".
I have tried debugging to see what's the underlying issue but I can't find anything. No search has given me any clue. I seem to follow a few guides to the letter without success.
private val context: Context?
get() = this.application?.applicationContext
private var application: Application? = null
private val transformation = "AES/GCM/NoPadding"
private val androidKeyStore = "AndroidKeyStore"
private val ivPrefix = "_iv"
private val keyStore by lazy { this.createKeyStore() }
private fun createKeyStore(): KeyStore {
val keyStore = KeyStore.getInstance(this.androidKeyStore)
keyStore.load(null)
return keyStore
}
private fun createSecretKey(alias: String): SecretKey {
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, this.androidKeyStore)
keyGenerator.init(
KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
)
return keyGenerator.generateKey()
}
private fun getSecretKey(alias: String): SecretKey {
return if (this.keyStore.containsAlias(alias)) {
(this.keyStore.getEntry(alias, null) as KeyStore.SecretKeyEntry).secretKey
} else {
this.createSecretKey(alias)
}
}
private fun removeSecretKey(alias: String) {
this.keyStore.deleteEntry(alias)
}
private fun encryptText(alias: String, textToEncrypt: String): String {
val cipher = Cipher.getInstance(this.transformation)
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(alias))
val ivString = Base64.encodeToString(cipher.iv, Base64.DEFAULT)
this.storeInSharedPrefs(alias + this.ivPrefix, ivString)
val byteArray = cipher.doFinal(textToEncrypt.toByteArray(charset("UTF-8")))
return String(byteArray)
}
private fun decryptText(alias: String, textToDecrypt: String): String? {
val ivString = this.retrieveFromSharedPrefs(alias + this.ivPrefix) ?: return null
val iv = Base64.decode(ivString, Base64.DEFAULT)
val spec = GCMParameterSpec(iv.count() * 8, iv)
val cipher = Cipher.getInstance(this.transformation)
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(alias), spec)
try {
val byteArray = cipher.doFinal(textToDecrypt.toByteArray(charset("UTF-8")))
return String(byteArray)
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
private fun storeInSharedPrefs(key: String, value: String) {
this.context?.let {
PreferenceManager.getDefaultSharedPreferences(it).edit()?.putString(key, value)?.apply()
}
}
private fun retrieveFromSharedPrefs(key: String): String? {
val validContext = this.context ?: return null
return PreferenceManager.getDefaultSharedPreferences(validContext).getString(key, null)
}
Can anyone point me in the right direction ?
I had similar issue. It was all about android:allowBackup="true".
This issue will occur while uninstalling the app and then re-installing it again. KeyStore will get cleared on uninstall but the preferences not getting removed, so will end up trying to decrypt with a new key thus exception thrown.
Try disabling android:allowBackup
as follows:
<application android:allowBackup="false" ... >
I encountered the same exception/issue 'android.security.KeyStoreException: Signature/MAC verification failed' on Cipher encryption 'AES/GCM/NoPadding'.
On my end, what helped to resolve this issue is to create a byte array holder first, with size that is obtained by calling Cipher.getOutputSize(int inputLen)
, then calling the doFinal overload Cipher.doFinal(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset)
to set the ciphertext in your byte array holder.
private var iv: ByteArray? = null
fun doEncryptionOperation() {
val keyStore = KeyStore.getInstance(PROVIDER_ANDROID_KEYSTORE).apply {
load(null)
}
// Assumption: key with alias 'secret_key' has already been stored
val entry = keyStore.getEntry("secret_key", null)
val secretKeyEntry = entry as KeyStore.SecretKeyEntry
val key secretKeyEntry.secretKey
val plainText = "Sample plain text"
val cipherText = encryptSymmetric(key, plainText.toByteArray())
val decrypted = decryptSymmetric(key, cipherText)
val decryptedStr = String(decrypted)
val same = decryptedStr == plainText
Log.d("SampleTag", "Is plaintext same from decrypted text? $same")
}
fun encryptSymmetric(key: SecretKey, plainText: ByteArray): ByteArray? {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, key)
iv = cipher.iv
val len = plainText.size
val outLen = cipher.getOutputSize(len) // get expected cipher output size
val result = ByteArray(outLen) // create byte array with outLen
cipher.doFinal(plainText, 0, len, result,0) // doFinal passing plaintext data and result array
return result
}
fun decryptSymmetric(key: SecretKey, cipherText: ByteArray): ByteArray? {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
val tagLen = 128 // default GCM tag length
cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(tagLen,iv))
cipher.update(input.data)
val result = cipher.doFinal()
return result
}
Additionally, using AEAD, don't forget to call Cipher.updateAAD()
in ENCRYPT_MODE
, and set the same AEAD tag in the DECRYPT_MODE
. Otherwise, you will encounter the same javax.crypto.AEADBadTagException.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With