I am trying to encode a byte array in base64 before sending it over the network. The output of the encode() function below is not a valid base64 because what I read from my research was that the length of the string needs to a multiple of 4. Also, the string is not padded with "=" if the length is not a multiple of 4. I am assuming since no one has had this problem before, is there something wrong in the code below which is making the base64 to not be a multiple of 4?
private val messageAdapter = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
.adapter(RealTimePayload::class.java)
fun encode(realTimePayload: RealTimePayload): String? {
val jsonPayload = messageAdapter.toJson(realTimePayload)
val compressedData = zip(jsonPayload)
return Base64Utils.encode(compressedData)
}
fun decode(encodedString: String): RealTimePayload? {
val gzippedData = Base64Utils.decode(encodedString)
val jsonData = unzip(gzippedData)
return messageAdapter.fromJson(jsonData)
}
private fun unzip(content: ByteArray): String =
GZIPInputStream(content.inputStream()).bufferedReader().use { it.readText() }
private fun zip(content: String): ByteArray {
val bos = ByteArrayOutputStream()
GZIPOutputStream(bos).bufferedWriter().use { it.write(content) }
return bos.toByteArray()
}
}
Edit: I should mention that the final encoded string has some trailing '='s but the length is still not a multiple of 4.
Let me know if I should provide more information? Thanks.
Are there some line terminators in the encoded string?
The com.google.android.gms.common.util.Base64Utils#encode passes flag 0 to the Base64#encodeToString. This means the result will be a multi-lined, padded, non url-safed string.
Related
Description
I'm attempting to encrypt a token along with its IV to a pair of ByteArrays, serialize it, then write it to a Room database. The steps are obviously reversed when attempting to decrypt and read it.
When repeating the encryption/serialization/deserialization/decryption steps, but without writing it to a database, the given ByteArray decrypts just fine. Writing it gives me the following error on decryption:
java.io.StreamCorruptedException: invalid stream header
I'm struggling to understand why this happens, and I'd appreciate the help.
Code
ByteArray Functions
#Suppress("UNCHECKED_CAST")
fun <T : Serializable> fromByteArray(byteArray: ByteArray): T {
val inputStream = ByteArrayInputStream(byteArray)
val objectInput = ObjectInputStream(inputStream)
val result = objectInput.readObject() as T
objectInput.close()
inputStream.close()
return result
}
fun Serializable.toByteArray(): ByteArray {
val outputStream = ByteArrayOutputStream()
val objectOutput = ObjectOutputStream(outputStream)
objectOutput.writeObject(this)
objectOutput.flush()
val result = outputStream.toByteArray()
outputStream.close()
objectOutput.close()
return result
}
Encryption Functions
override fun <T : Serializable> encryptData(data: T): Pair<ByteArray, ByteArray> {
var temp = data.toByteArray()
if (temp.size % 16 != 0) {
temp = temp.copyOf(
(temp.size + 16) - (temp.size % 16)
)
}
cipher.init(Cipher.ENCRYPT_MODE, getKey())
val ivBytes = cipher.iv
val encryptedArray = cipher.doFinal(temp)
return Pair(ivBytes, encryptedArray)
}
#Suppress("UNCHECKED_CAST")
override fun <T> decryptData(ivBytes: ByteArray, data: ByteArray): T {
val ivSpec = IvParameterSpec(ivBytes)
cipher.init(Cipher.DECRYPT_MODE, getKey(), ivSpec)
val tempArray: ByteArray = cipher.doFinal(data)
return fromByteArray(tempArray) as T
}
Room Data Class
data class UserData(
val profilePictureId: Long?,
val savedTimestamp: Long = System.currentTimeMillis(),
#PrimaryKey
val username: String = "",
val userToken: Pair<ByteArray, ByteArray>?
)
Database Class
#Database(entities = [UserData::class], version = 1)
#TypeConverters(UserDataConverters::class)
abstract class UserDataDatabase : RoomDatabase() {
abstract val userDataDao: UserDataDao
companion object {
const val DB_NAME = "user_data_db"
}
}
Database DAO
#Dao
interface UserDataDao {
#Query("SELECT * FROM UserData")
fun loadUserData(): Flow<UserData>
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun updateUserData(userData: UserData)
}
Database Type Converters
class UserDataConverters {
#TypeConverter
fun fromTokenPair(pair: Pair<ByteArray, ByteArray>): String {
return Json.encodeToString(pair)
}
#TypeConverter
fun toTokenPair(serializedPair: String): Pair<ByteArray, ByteArray> {
return Json.decodeFromString(serializedPair)
}
}
So this isn't actually related to Room. My mistake.
I didn't realise that serializing objects to ByteArrays with ObjectInputStream also writes a header for later serialization with ObjectOutputStream.
When encrypting the serialized data, I was using CDC block mode, which requires padding to a block size divisble by 16. That extra padding caused the aforementioned header to become invalid for accompanying data.
Removing the padding raises issues with detecting when padding stops and content starts (copyOf adds zeroes). With that in mind, and after later finding out that CBC is less secure than GCM (which requires no padding), I changed the block mode to GCM.
See below for resultant code (irrelevent blocks removed):
private val keyGenParameterSpec = KeyGenParameterSpec.Builder(
"TroupetentKeyAlias",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
Also, when decrypting, the previous spec ivParameterSpec() couldn't be used, as GCM requires a tag length. That was also changed:
#Suppress("UNCHECKED_CAST")
override fun <T> decryptData(ivBytes: ByteArray, data: ByteArray): T {
val ivSpec = GCMParameterSpec(128, ivBytes)
cipher.init(Cipher.DECRYPT_MODE, getKey(), ivSpec)
val decryptedArray: ByteArray = cipher.doFinal(data)
return fromByteArray(decryptedArray) as T
}
I read Mark Allison's blog post about combining the new Android DataStore with encryption with the use of the Android Keystore.
I'm using the same exact SecretKey properties (AES/CBC/PKCS7) and Encrypt/Decrypt found in his blog.
class AesCipherProvider(
private val keyName: String,
private val keyStore: KeyStore,
private val keyStoreName: String
) : CipherProvider {
override val encryptCipher: Cipher
get() = Cipher.getInstance(TRANSFORMATION).apply {
init(Cipher.ENCRYPT_MODE, getOrCreateKey())
}
override fun decryptCipher(iv: ByteArray): Cipher =
Cipher.getInstance(TRANSFORMATION).apply {
init(Cipher.DECRYPT_MODE, getOrCreateKey(), IvParameterSpec(iv))
}
private fun getOrCreateKey(): SecretKey =
(keyStore.getEntry(keyName, null) as? KeyStore.SecretKeyEntry)?.secretKey
?: generateKey()
private fun generateKey(): SecretKey =
KeyGenerator.getInstance(ALGORITHM, keyStoreName)
.apply { init(keyGenParams) }
.generateKey()
private val keyGenParams =
KeyGenParameterSpec.Builder(
keyName,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
).apply {
setBlockModes(BLOCK_MODE)
setEncryptionPaddings(PADDING)
setUserAuthenticationRequired(false)
setRandomizedEncryptionRequired(true)
}.build()
private companion object {
const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
const val BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC
const val PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
const val TRANSFORMATION = "$ALGORITHM/$BLOCK_MODE/$PADDING"
}
}
class CryptoImpl constructor(private val cipherProvider: CipherProvider) : Crypto {
override fun encrypt(rawBytes: ByteArray, outputStream: OutputStream) {
val cipher = cipherProvider.encryptCipher
val encryptedBytes = cipher.doFinal(rawBytes)
with(outputStream) {
write(cipher.iv.size)
write(cipher.iv)
write(encryptedBytes.size)
write(encryptedBytes)
}
}
override fun decrypt(inputStream: InputStream): ByteArray {
val ivSize = inputStream.read()
val iv = ByteArray(ivSize)
inputStream.read(iv)
val encryptedDataSize = inputStream.read()
val encryptedData = ByteArray(encryptedDataSize)
inputStream.read(encryptedData)
val cipher = cipherProvider.decryptCipher(iv)
return cipher.doFinal(encryptedData)
}
}
I'm using following super simple ProtocolBuffer with only one String field.
syntax = "proto3";
option java_package = "my.package.model";
message SimpleData {
string text = 1;
}
I'm using following code to test this implementation.
class SecureSimpleDataSerializer(private val crypto: Crypto) :
Serializer<SimpleData> {
override fun readFrom(input: InputStream): SimpleData {
return if (input.available() != 0) {
try {
SimpleData.ADAPTER.decode(crypto.decrypt(input))
} catch (exception: IOException) {
throw CorruptionException("Cannot read proto", exception)
}
} else {
SimpleData("")
}
}
override fun writeTo(t: SimpleData, output: OutputStream) {
crypto.encrypt(SimpleData.ADAPTER.encode(t), output)
}
override val defaultValue: SimpleData = SimpleData()
}
private val simpleDataStore = createDataStore(
fileName = "SimpleDataStoreTest.pb",
serializer = SecureSimpleDataSerializer(
CryptoImpl(
AesCipherProvider(
"SimpleDataKey",
KeyStore.getInstance("AndroidKeyStore").apply { load(null) },
"AndroidKeyStore"
)
)
)
)
When I try to serialize and deserialize a simple String it works like intended.
simpleDataStore.updateData { it.copy(text = "simple-string") }
println(simpleDataStore.data.first())
// "simple-string"
However when I try the same with a longer String (note smaller than the max size for Proto's).
The save works, but upon killing the app and relaunching the app to retrieve the value it crashes.
simpleDataStore.updateData { it.copy(text = "eyJraWQiOiJyc2ExIiwiYWxnIjoiUlMyNTYifQeyJhdWQiOiJ2cnRudS1zaXRlIiwic3ViIjoiNmRlNjg1MjctNGVjMi00MmUwLTg0YmEtNGU5ZjE3ZTQ4MmY2IiwiaXNzIjoiaHR0cHM6XC9cL2xvZ2luLnZydC5iZSIsInNjb3BlcyI6ImFkZHJlc3Msb3BlbmlkLHByb2ZpbGUsbGVnYWN5aWQsbWlkLGVtYWlsIiwiZXhwIjoxNjEwMjc4OTQ0LCJpYXQiOjE2MTAyNzUzNDQsImp0aSI6Ijc0MDk3MzFiLTg5OGUtNGVmNS1iNWMwLTEzODM2ZWZjN2ZjOCJ9kSkuI9Z0XLLBtfC0SpHA4wV0299ZOd6Xj99hNkemim7fRP1ooCD8YkqbM0hhBKiiYbvhqmfc1NSKYHAehA7Z9c6XluPTIpZkljHIBH7BLd0IGznraUEOMYDh0I2aQKZxxvwV6RlWetdCBUf3KtQuDO7snywbE5jmhzq75Y") }
println(simpleDataStore.data.first())
Process: com.stylingandroid.datastore, PID: 13706
javax.crypto.IllegalBlockSizeException
at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:513)
at javax.crypto.Cipher.doFinal(Cipher.java:2055)
at com.stylingandroid.datastore.security.CryptoImpl.decrypt(Crypto.kt:33)
at com.stylingandroid.datastore.ui.MainActivity$SecureSimpleDataSerializer.readFrom(MainActivity.kt:32)
at com.stylingandroid.datastore.ui.MainActivity$SecureSimpleDataSerializer.readFrom(MainActivity.kt:26)
at androidx.datastore.core.SingleProcessDataStore.readData(SingleProcessDataStore.kt:249)
at androidx.datastore.core.SingleProcessDataStore.readDataOrHandleCorruption(SingleProcessDataStore.kt:227)
at androidx.datastore.core.SingleProcessDataStore.readAndInitOnce(SingleProcessDataStore.kt:190)
at androidx.datastore.core.SingleProcessDataStore$actor$1.invokeSuspend(SingleProcessDataStore.kt:154)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
Caused by: android.security.KeyStoreException: Invalid input length
at android.security.KeyStore.getKeyStoreException(KeyStore.java:1301)
at android.security.keystore.KeyStoreCryptoOperationChunkedStreamer.doFinal(KeyStoreCryptoOperationChunkedStreamer.java:176)
at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:506)
at javax.crypto.Cipher.doFinal(Cipher.java:2055)
at com.stylingandroid.datastore.security.CryptoImpl.decrypt(Crypto.kt:33)
at com.stylingandroid.datastore.ui.MainActivity$SecureSimpleDataSerializer.readFrom(MainActivity.kt:32)
at com.stylingandroid.datastore.ui.MainActivity$SecureSimpleDataSerializer.readFrom(MainActivity.kt:26)
at androidx.datastore.core.SingleProcessDataStore.readData(SingleProcessDataStore.kt:249)
at androidx.datastore.core.SingleProcessDataStore.readDataOrHandleCorruption(SingleProcessDataStore.kt:227)
at androidx.datastore.core.SingleProcessDataStore.readAndInitOnce(SingleProcessDataStore.kt:190)
at androidx.datastore.core.SingleProcessDataStore$actor$1.invokeSuspend(SingleProcessDataStore.kt:154)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
2021-01-10 14:08:09.907 13706-13706/com.stylingandroid.datastore I/Process: Sending signal. PID: 13706 SIG: 9
Does anybody know?
Is it specific to the String's length in combination with the chosen encryption algorithm?
Is the decryption function wrong?
Thanks in advance.
The problem is reproducible on my machine. It occurs when the encrypted data encryptedBytes in CryptoImpl.encrypt has a length of more than 255 bytes. The reason is that starting with 256 bytes encryptedBytes.size cannot be stored on one byte, while the methods int InputStream.read() or void OutputStream.write(int) read or write only one byte.
Therefore, if the size of the ciphertext is to be written, a sufficiently large bytes buffer must be used in CryptoImpl.encrypt, e.g. 4 bytes:
with(outputStream) {
write(cipher.iv.size)
write(cipher.iv)
write(ByteBuffer.allocate(4).putInt(encryptedBytes.size).array()) // Convert Int to 4 bytes buffer
write(encryptedBytes)
}
and for reading in CryptoImpl.decrypt:
val ivSize = inputStream.read()
val iv = ByteArray(ivSize)
inputStream.read(iv)
val encryptedDataSizeBytes = ByteArray(4)
inputStream.read(encryptedDataSizeBytes)
val encryptedDataSize = ByteBuffer.wrap(encryptedDataSizeBytes).int // Convert 4 bytes buffer to Int
val encryptedData = ByteArray(encryptedDataSize)
inputStream.read(encryptedData)
However, writing the sizes is actually not necessary. The size of the IV is known, it corresponds to the block size, i.e. 16 bytes for AES, so that the criterion for the separation of IV and ciphertext is defined. Thus, the data can be written in CryptoImpl.encrypt as follows:
with(outputStream) {
write(cipher.iv) // Write 16 bytes IV
write(encryptedBytes) // Write ciphertext
}
And for reading in CryptoImpl.decrypt:
val iv = ByteArray(16)
inputStream.read(iv) // Read IV (first 16 bytes)
val encryptedData = inputStream.readBytes() // Read ciphertext (remaining data)
I am developing an android app.
I want to encrypt/decrypt some sensitive data (jwt token) into SharedPreference.
So I wrote the below code.
fun initKeyStore() {
val alias = "${packageName}.rsakeypairs"
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply {
load(null)
}
if (keyStore.containsAlias(alias)) {
} else {
SLog.d(LogTag.SECURE, "[cipher] No keypair for $alias, creating a new one")
with(KeyPairGenerator.getInstance(KEY_ALGORITHM_RSA, "AndroidKeyStore"), {
val spec = KeyGenParameterSpec.Builder(alias,
PURPOSE_ENCRYPT or PURPOSE_DECRYPT)
.setAlgorithmParameterSpec(RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4))
.setBlockModes(BLOCK_MODE_CBC)
.setEncryptionPaddings(ENCRYPTION_PADDING_RSA_PKCS1)
.setDigests(DIGEST_SHA512, DIGEST_SHA384, DIGEST_SHA256)
.setUserAuthenticationRequired(false)
.build()
initialize(spec)
generateKeyPair()
})
}
keyEntry = keyStore.getEntry(alias, null)
}
fun String.encrypt(): String? {
cipher.init(Cipher.ENCRYPT_MODE, (keyEntry as KeyStore.PrivateKeyEntry).certificate.publicKey)
val bytes = this.toByteArray(Charsets.UTF_8)
val encryptedBytes = cipher.doFinal(bytes)
val base64EncryptedBytes = Base64.encode(encryptedBytes, Base64.DEFAULT)
return String(base64EncryptedBytes)
}
fun String.decrypt(): String {
cipher.init(Cipher.DECRYPT_MODE, (keyEntry as KeyStore.PrivateKeyEntry).privateKey)
val base64EncryptedBytes = this.toByteArray(Charsets.UTF_8)
val encryptedBytes = Base64.decode(base64EncryptedBytes, Base64.DEFAULT)
val decryptedBytes = cipher.doFinal(encryptedBytes)
return String(decryptedBytes)
}
But when the app tries to decrypt the encrypted data, Exception occurred.
javax.crypto.IllegalBlockSizeException
at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:513)
at javax.crypto.Cipher.doFinal(Cipher.java:2055)
...
Caused by: android.security.KeyStoreException: Invalid input length
at android.security.KeyStore.getKeyStoreException(KeyStore.java:1539)
at android.security.keystore.KeyStoreCryptoOperationChunkedStreamer.update(KeyStoreCryptoOperationChunkedStreamer.java:132)
The length of the JWT token that I am using is very long. (More than 800)
If I try to encrypt/decrypt short text, it works fine...
How can I encrypt/decrypt the long text?
In order to encrypt a long text, you either increase the key size (which is probably a bad idea, since it will take much more time to generate this key), or you split the text into chunks, encrypt those chunks one by one, and save them as a string array.
The maximum limit of Assymmetric Encryption is 245 character.
it can be fixed with the chunks of the Long String
object SecurePreferencesHelper {
private const val chunkSize = 240
private fun getNumberOfChunksKey(key: String) = "${key}_numberOfChunks"
fun setLongStringValue(key: String, value: String) {
val chunks = value.chunked(chunkSize)
SecurePreferences.setValue(getNumberOfChunksKey(key), chunks.size)
chunks.forEachIndexed { index, chunk ->
SecurePreferences.setValue("$key$index", chunk)
}
}
fun getLongStringValue(key: String): String? {
val numberOfChunks = SecurePreferences.getIntValue(getNumberOfChunksKey(key), 0)
if (numberOfChunks == 0) {
return null
}
return (0 until numberOfChunks)
.map { index ->
val string = SecurePreferences.getStringValue("$key$index", null) ?: run {
return null
}
string
}.reduce { accumulator, chunk -> accumulator + chunk }
}
fun removeLongStringValue(key: String) {
val numberOfChunks = SecurePreferences.getIntValue(getNumberOfChunksKey(key), 0)
(0 until numberOfChunks).map { SecurePreferences.removeValue("$key$it") }
SecurePreferences.removeValue(getNumberOfChunksKey(key))
}
fun containsLongStringValue(key: String): Boolean {
return SecurePreferences.contains(getNumberOfChunksKey(key))
}
}
For reference pls refer link
click here
I am getting a string from my database that represents a image. I want to transform it into an image again so first I made it a Byte Array and then tried to decode it. What happens is that for some reason it returns null which shouldn't be the case. I printed the size of the array to see if maybe there was an error in the transformation but there wasn't any problem there.
private fun imageBit(image: String): Bitmap{
val b = image.toByteArray()
d("DEBUG2", "${b.size}") //prints 47109
val ins = ByteArrayInputStream(b)
d("DEBUG3", "${ByteArrayInputStream(b).readBytes().size}") //prints 47109
return BitmapFactory.decodeStream(ins)
}
Try this out :
private fun imgBit(image: String): Bitmap{
val imgBytes = Base64.decode(image, 0)
return BitmapFactory.decodeByteArray(imgBytes, 0, imgBytes.size)
}
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".
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" ... >
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.
When you change your authentiation tag length from iv.count() to 128 it will work.
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