Thomas Debouverie
Thomas Debouverie

Reputation: 283

KeyStoreException: Signature/MAC verification failed when trying to decrypt

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 ?

Upvotes: 35

Views: 13423

Answers (5)

Ora Code
Ora Code

Reputation: 1

I have the exact same issue. I have tried all of the above solutions and they didn't work. Apparently, the problem was due to string encoding. Just make sure that the input for decryption is the same byte array which produced from the encryption process.

Upvotes: 0

RasulOs
RasulOs

Reputation: 109

I had a similar problem. I had an application where the admin and an ordinary user could log in and both of them had a remember me option. So, when the user previously pressed the remember me option, the program needs to fetch the encrypted password, decrypt it, and put it in the input field.

I was storing both encrypted passwords with their initialization vectors in the SharedPreferences file but when I was trying to decrypt them via Cipher (The secret key was stored in the AndroidKeyStore with the same alias for the secret key) it was decrypting one password but was giving me the same error as yours when I was decrypting another password.

Then, I used 2 different aliases for these 2 passwords when I was encrypting and decrypting them and the error is gone.

Github gist: Code example

Upvotes: 1

Santhosh Joseph
Santhosh Joseph

Reputation: 934

I had similar issue. It was all about android:allowBackup="true".

Issue

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.

Solution

Try disabling android:allowBackup as follows:

<application android:allowBackup="false" ... >

Upvotes: 27

pjl91
pjl91

Reputation: 865

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.

Upvotes: 1

Hylke
Hylke

Reputation: 705

When you change your authentiation tag length from iv.count() to 128 it will work.

Upvotes: 0

Related Questions