androidaespassword-encryptionandroid-keystore

Android Key Store doesn't decrypt passsword


I learning the cryptography in Android. Try to encrypt/decrypt user's password. Encryption method is working pretty fine, but decryption method is always getting me an error.

Here is code of the CryptoManager class:

package edu.xfoleks.komodoro.presentation.utils

import android.security.keystore.KeyProperties
import android.security.keystore.KeyProtection
import android.util.Base64
import java.nio.charset.StandardCharsets
import java.security.KeyStore
import java.util.Calendar
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.spec.IvParameterSpec

class CryptoManager {

    init {
        generateSecretKey()
    }
    private fun generateSecretKey() {

        val keyStore = KeyStore.getInstance(KEY_STORE).apply {
            load(null)
        }

        val keyGenerator = KeyGenerator.getInstance(ALGORITHM).apply {
            init(KEY_SIZE)
        }

        val secretKey = keyGenerator.generateKey()

        val startTime = Calendar.getInstance()
        val expireTime = Calendar.getInstance().apply {
            add(Calendar.YEAR, 2)
        }

        val entry = KeyStore.SecretKeyEntry(secretKey)
        val protectionParameter =
            KeyProtection.Builder(KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
                .setKeyValidityStart(startTime.time)
                .setKeyValidityEnd(expireTime.time)
                .setBlockModes(BLOCK_MODE)
                .setEncryptionPaddings(PADDING)
                .build()

        keyStore.setEntry(ALIAS, entry, protectionParameter)
    }

    fun encrypt(plainText: String): String {
        val keyStore = KeyStore.getInstance(KEY_STORE).apply { load(null) }

        val secretKey = keyStore.getKey(ALIAS, null)

        return try {
            val cipher = Cipher.getInstance(TRANSFORMATION)
            cipher.init(Cipher.ENCRYPT_MODE, secretKey)

            val cipherText = Base64.encodeToString(cipher.doFinal(plainText.toByteArray()), Base64.DEFAULT)
            val iv = Base64.encodeToString(cipher.iv, Base64.DEFAULT)

            "${cipherText}.$iv"

        } catch (ex: Exception) {
            ex.printStackTrace()
            ""
        }
    }

    fun decrypt(cipherText: String): String? {
        val keyStore = KeyStore.getInstance(KEY_STORE).apply { load(null) }

        val secretKey = keyStore.getKey(ALIAS, null)

        val array = cipherText.split(".")
        val cipherData = Base64.decode(array[0], Base64.DEFAULT)
        val iv = Base64.decode(array[1], Base64.DEFAULT)

        return try {
            val cipher = Cipher.getInstance(TRANSFORMATION)
            val spec = IvParameterSpec(iv)

            cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)

            val clearText = cipher.doFinal(cipherData)

            val encoded = String(clearText, 0, clearText.size, StandardCharsets.UTF_8)

            encoded
        } catch (ex: Exception) {
            ex.printStackTrace()
            null
        }
    }

    companion object {
        private const val KEY_STORE = "AndroidKeyStore"
        private const val KEY_SIZE = 256
        private const val ALIAS = "secret"
        private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
        private const val BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC
        private const val PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
        private const val TRANSFORMATION = "$ALGORITHM/$BLOCK_MODE/$PADDING"
    }
}

Classes, which working with decryption/encryption:

class LogInViewModel @Inject constructor(
    private val userInteractor: UserInteractor
) : ViewModel() {

    private var _user = MutableLiveData<UserInfoItem>()

    private fun decryptPassword(password: String): String? {
        val cryptoManager = CryptoManager()
        return cryptoManager.decrypt(password)
    }

    fun logIn(userName: String, password: String): Boolean {
        val user = UserInfoItem(userName, password)
        _user.value = user
        val existingUser = userInteractor.getUser(user.toUser()).toUserInfoItem()
        val decryptedPassword = decryptPassword(existingUser.password)

        return user.userName.equals(existingUser.userName) && user.password.equals(decryptedPassword)
    }
}
class RegistrationViewModel @Inject constructor(
    private val userInteractor: UserInteractor
) : ViewModel() {

    private var _user = MutableLiveData<UserInfoItem>()

    private fun encryptPassword(password: String): String {
        if (password.isEmpty()) {
            throw EmptyPasswordException()
        }

        val cryptoManager = CryptoManager()
        return cryptoManager.encrypt(password)
    }

    fun registerUser(userName: String, password: String) {
        if (userName.isEmpty() || password.isEmpty()) { throw UserNotCreatedException() }
        val encryptedPassword = encryptPassword(password)
        val user = UserInfoItem(userName, encryptedPassword)
        _user.value = user
        userInteractor.registerUser(user.toUser())
    }

}

The error message:

javax.crypto.BadPaddingException
at android.security.keystore2.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:609)
at javax.crypto.Cipher.doFinal(Cipher.java:2056)
at edu.xfoleks.komodoro.presentation.utils.CryptoManager.decrypt(CryptoManager.kt:83)
at edu.xfoleks.komodoro.presentation.screens.viewmodels.LogInViewModel.decryptPassword(LogInViewModel.kt:22)
at edu.xfoleks.komodoro.presentation.screens.viewmodels.LogInViewModel.logIn(LogInViewModel.kt:29)
at edu.xfoleks.komodoro.presentation.screens.fragments.LogInFragment.onCreateView$lambda$5(LogInFragment.kt:47)
at edu.xfoleks.komodoro.presentation.screens.fragments.LogInFragment.$r8$lambda$NaDtgxEMrBbrOBDtRje0oMjYmGs(Unknown Source:0)
at edu.xfoleks.komodoro.presentation.screens.fragments.LogInFragment$$ExternalSyntheticLambda0.onClick(Unknown Source:2)
at android.view.View.performClick(View.java:7506)
at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1211)
at android.view.View.performClickInternal(View.java:7483)
at android.view.View.-$$Nest$mperformClickInternal(Unknown Source:0)
at android.view.View$PerformClick.run(View.java:29334)
at android.os.Handler.handleCallback(Handler.java:942)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7872)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
Caused by: android.security.KeyStoreException: Invalid argument (internal Keystore code: -38 message: In KeystoreOperation::finish

Caused by:
0: In finish: KeyMint::finish failed.
1: Error::Km(ErrorCode(-38))) (public error code: 10 internal Keystore code: -38)
at android.security.KeyStore2.getKeyStoreException(KeyStore2.java:369)  
at android.security.KeyStoreOperation.handleExceptions(KeyStoreOperation.java:78)   
at android.security.KeyStoreOperation.finish(KeyStoreOperation.java:128)    
at android.security.keystore2.KeyStoreCryptoOperationChunkedStreamer$MainDataStream.finish(KeyStoreCryptoOperationChunkedStreamer.java:228)
at android.security.keystore2.KeyStoreCryptoOperationChunkedStreamer.doFinal(KeyStoreCryptoOperationChunkedStreamer.java:181)
at android.security.keystore2.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:603)
... 20 more

I had tried to save all settings for encryption and then use it for decryption, but it gave me nothing.


Solution

  • For the security of password we need to use hashing, not encryption/decryption. You can look up and implement algorithms like bcrypt, scrypt or Argon2.