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.
This version of crypto manager might help you. It handles BadPaddingException and also IllegalBlockSizeException for large data.
Code:
fun encrypt(bytes: ByteArray, outputStream: OutputStream) {
val cipher = encryptCipher
val iv = cipher.iv
outputStream.use {
it.write(iv)
// write the payload in chunks to make sure to support larger data amounts (this would otherwise fail silently and result in corrupted data being read back)
////////////////////////////////////
val inputStream = ByteArrayInputStream(bytes)
val buffer = ByteArray(CHUNK_SIZE)
while (inputStream.available() > CHUNK_SIZE) {
inputStream.read(buffer)
val ciphertextChunk = cipher.update(buffer)
it.write(ciphertextChunk)
}
// the last chunk must be written using doFinal() because this takes the padding into account
val remainingBytes = inputStream.readBytes()
val lastChunk = cipher.doFinal(remainingBytes)
it.write(lastChunk)
//////////////////////////////////
}
}
fun decrypt(inputStream: InputStream): ByteArray {
return inputStream.use {
val iv = ByteArray(KEY_SIZE)
it.read(iv)
val cipher = getDecryptCipherForIv(iv)
val outputStream = ByteArrayOutputStream()
// read the payload in chunks to make sure to support larger data amounts (this would otherwise fail silently and result in corrupted data being read back)
////////////////////////////////////
val buffer = ByteArray(CHUNK_SIZE)
while (inputStream.available() > CHUNK_SIZE) {
inputStream.read(buffer)
val ciphertextChunk = cipher.update(buffer)
outputStream.write(ciphertextChunk)
}
// the last chunk must be read using doFinal() because this takes the padding into account
val remainingBytes = inputStream.readBytes()
val lastChunk = cipher.doFinal(remainingBytes)
outputStream.write(lastChunk)
//////////////////////////////////
outputStream.toByteArray()
}
}
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.
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