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
Related
I'm developing an offline auth process for users to access their accounts. My idea was to store their hashed & encrypted email and passwords and let them introduce their email and password to auth , i'm using a Room database to store their credentials . The problem is when i try to decrypt their passwords and check if those match the decryption function throws an exception (-1) ,(I'm using the Keystore Sistem , Cipher , tha salt changes constantly with AES-256 algorithm). Any other suggestion to make an offline auth?
fun encryptText(bytes:ByteArray, outputStream: OutputStream ):ByteArray{
val encryptedBytes = encryptCipher.doFinal(bytes)
outputStream.use {
it.write(encryptCipher.iv.size)
it.write(encryptCipher.iv)
it.write(encryptedBytes.size)
it.write(encryptedBytes)
}
return encryptedBytes
}
fun decryptText(inputStream:InputStream):ByteArray{
Log.d("Test" ,"input decryptor = " +inputStream.readBytes().decodeToString())
return inputStream.use {
val ivSize = inputStream.read()
val iv = ByteArray(ivSize)
it.read(iv)
val encryptedBytesSize = inputStream.read()
val encryptedBytes = ByteArray(encryptedBytesSize)
it.read(encryptedBytes)
getDecryptCipherForIv(iv).doFinal(encryptedBytes)
}
}
#Entity(tableName = "account_credentials")
data class AccountCredentials(#PrimaryKey
val uuid:String,
val firebaseId:String?,
#ColumnInfo(name = "password_bytes",typeAffinity = ColumnInfo.BLOB) val hashedPassword:ByteArray,
#ColumnInfo(name = "email_bytes",typeAffinity = ColumnInfo.BLOB) val email:ByteArray
)
private fun encryptCredentials(credentials: AccountCredentials):AccountCredentials{
val passwordStream = ByteArrayOutputStream()
val emailStream = ByteArrayOutputStream()
CryptographicManager().encryptText(credentials.email , emailStream)
CryptographicManager().encryptText(credentials.hashedPassword , passwordStream)
return credentials.copy(hashedPassword = passwordStream.toByteArray() , email =emailStream.toByteArray() )
}
I have tried to change the data class field to string instead of ByteArray but the same result .
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'm using Encryption and Decryption with KeyStore like this.
This is the flow. After i pass a string to encrypt method i save both encrypted and iv so i can retrive value with it later.Problem is some times, some encrypted values can not retrive correctly... not all of them! So think i encrypted 10 items and save them in some where( both encrypted and iv). Then when i wanna retrive one of them can not retrive correcly!
init {
keyStore = KeyStore.getInstance(ANDROID_KEY_STORE)
keyStore.load(null)
cipher = Cipher.getInstance("AES/GCM/NoPadding")
}
fun encryptData(text: String): Pair<ByteArray, String>? {
try {
cipher.init(Cipher.ENCRYPT_MODE, getSecretKet(ALIAS))
val iv = cipher.iv.toString(Charsets.ISO_8859_1)
val result = cipher.doFinal(text.toByteArray(Charsets.ISO_8859_1))
Timber.i("$TAG encrypted data $result")
Timber.i("$TAG encrypted iv $iv")
return if (result != null) {
Pair(result, iv)
} else {
null
}
} catch (e: Exception) {
Timber.e("$TAG error encryptData", e)
return null
}
}
fun decryptData(encryptedData: ByteArray, iv: ByteArray): String {
return try {
val spec = GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, getSecretKet(ALIAS), spec)
val result = cipher.doFinal(encryptedData).toString(Charsets.ISO_8859_1)
Timber.i("$TAG decrypted data $result")
result
} catch (e: Exception) {
Timber.e("$TAG decryptData error may string was not encrypted", e)
encryptedData.toString()
}
}
And this is for get secret key. First i thougth may be problem is with my key, so I implemented it in this way and this class is singletone. But thing is some time when i re-open application some how this key is not same ( I think, because iv and encrypted value and also cipher are fixed). I also use Charsets.ISO_8859_1 caue find that this charset is better to keep all characters and lose less.
Then I thought may be proble is with saving place so for test i just move from Room db with sstring field to SharePref with string. But the issue is same so now i'm prettry sure it is not about savig repository.
private fun getSecretKet(alias: String): Key {
if (keyStore.containsAlias(alias)) {
//Try for existing key
return keyStore.getKey(alias, null)
} else {
//Key is not present, create new one.
val keyGenerator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val kGenerator =
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE)
val specs = KeyGenParameterSpec
.Builder(
alias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
kGenerator.init(specs)
kGenerator
} else {
KeyGenerator.getInstance(ANDROID_KEY_STORE);
}
return keyGenerator.generateKey()
}
}
I think this sould works as well as works mostly but eventually i can not get some of my encrypted data as I mentioned above. Any body have any idea?
I finnaly solve this issue by go more deep to AES changing implemtation.
The main thing was IV! In nutshell Iv is a tool for making encryption more complicated. We create an IV and pass it to cipher, cipher do encrypting data and mixing byte in many rounds and for blocks of bytes and in every round it updates IV with a logic and use it for next round or block of bytes. So if you get IV from cipher after finish encryption you will see that it changed!
Second thing is i imelemneted cipher singleTone but i change its creation encryption and decryption method itself. So every string for encoding will have new IV and after encoding finished I pass it with encrypted string to later decyption.
First I thought it will from string encryption with help of #PresidentJamesMovelenPolk but I test that with this impelemntation even with ISO_8859_1 it works well. And this is final impelemntationds:
companion object {
const val TRANSFORMATION = "AES/GCM/NoPadding"
const val ANDROID_KEY_STORE = "AndroidKeyStore"
const val ALIAS = "MyApp"
const val TAG = "KeyStoreManager"
}
private var keyStore: KeyStore
init {
keyStore = KeyStore.getInstance(ANDROID_KEY_STORE)
keyStore.load(null)
}
fun encryptData(text: String): Pair<ByteArray, String>? {
try {
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, getSecretKet(ALIAS))
val iv = cipher.iv
val result = cipher.doFinal(text.toByteArray(Charsets.ISO_8859_1))
val resultIv = Base64.encodeToString(iv, Base64.NO_WRAP)
Timber.i("$TAG encrypted data $result")
Timber.i("$TAG encrypted iv $iv")
return if (result != null) {
Pair(result, resultIv)
} else {
null
}
} catch (e: Exception) {
Timber.e("$TAG error encryptData", e)
return null
}
}
fun encryptString(text: String): SecuredData? {
return try {
val result = encryptData(text)
if (result != null) {
SecuredData(result.first.toString(Charsets.ISO_8859_1), result.second)
} else {
null
}
} catch (e: Exception) {
Timber.e("$TAG error encryptString", e)
null
}
}
/**
Get pair of encrypted value and iv
*/
fun decryptString(encryptedString: String, iv: String): String {
return try {
val result = decryptData(
encryptedString.toByteArray(Charsets.ISO_8859_1),
Base64.decode(iv, Base64.NO_WRAP)
)
result
} catch (e: java.lang.Exception) {
Timber.e("Error in convert to Base64")
encryptedString
}
}
fun decryptData(encryptedData: ByteArray, iv: ByteArray): String {
return try {
val cipher = Cipher.getInstance(TRANSFORMATION)
val spec = GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, getSecretKet(ALIAS), spec)
val result = cipher.doFinal(encryptedData).toString(Charsets.ISO_8859_1)
Timber.i("$TAG decrypted data $result")
result
} catch (e: Exception) {
Timber.e("$TAG decryptData error may string was not encrypted", e)
encryptedData.toString()
}
}
private fun getSecretKet(alias: String): Key {
if (keyStore.containsAlias(alias)) {
// Try for existing key
return keyStore.getKey(alias, null)
} else {
// Key is not present, create new one.
val keyGenerator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val kGenerator =
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE)
val specs = KeyGenParameterSpec
.Builder(
alias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
kGenerator.init(specs)
kGenerator
} else {
KeyGenerator.getInstance(ANDROID_KEY_STORE)
}
return keyGenerator.generateKey()
}
}
#Keep
data class SecuredData(val value: String, val iv: String)
I'm developing an app that shows sensible information so I have to
encrypt all informations stored on Room Database.
After a lot of research, I choosed to encypt using AES, generating a random key
and storing on KeyStore (minSdk: 24 btw).
private fun encript(plain: String): String? {
return try {
generateKey()
encData(plain)
} catch (e: Throwable) {
if (!BuildConfig.DEBUG){
Crashlytics.logException(e)
}
null
}
}
private fun encData(plain: String): String? {
val sKey = getSecretKey()
iv = ByteArray(12)
secRng = SecureRandom()
secRng.nextBytes(iv)
val cipher = Cipher.getInstance(AES_MODE)
val parameterSpec = GCMParameterSpec(128, iv)
cipher.init(Cipher.ENCRYPT_MODE, sKey, parameterSpec)
cipText = cipher.doFinal(plain.toByteArray())
return encBuffer()
}
private fun encBuffer(): String? {
val byteBuffer = ByteBuffer.allocate(4 + iv.size + cipText.size)
byteBuffer.putInt(iv.size)
byteBuffer.put(iv)
byteBuffer.put(cipText)
val cipherMessage = byteBuffer.array()
//clean()
return Base64.encodeToString(cipherMessage, Base64.DEFAULT)
}
So i must show all these informations on a list, so Im decrypting all information
on viewholder. the problem is that is too slow when it shows a lot of items,
so i decided to try an async descrypt inside the viewholder and for my surprise
i got a lot of "Unitialized keystore" exception, letting y data encrypted,
it's weird because when i scroll down and up, some viewholders decrypt successfull
and others not, it's pretty random, an it seems that viewholder try to decrypt more
than once. PS: I can't cache decrypted data on SharedPrerences for security reasons
fun decription(encStr: String): String? {
return try {
dec(encStr)
} catch (e: Throwable) {
Log.d("cripto", "Here, Trowing Unitialized Keystore")
if (!BuildConfig.DEBUG){
Crashlytics.logException(e)
}
null
}
}
private fun dec(encStr: String): String {
val byteBuffer = ByteBuffer.wrap(Base64.decode(encStr, Base64.DEFAULT))
val ivLength = byteBuffer.int
if (ivLength < 12 || ivLength >= 16) { // check input parameter
throw IllegalArgumentException("invalid iv length")
}
val iv = ByteArray(ivLength)
byteBuffer.get(iv)
val cipherText = ByteArray(byteBuffer.remaining())
byteBuffer.get(cipherText)
return callCip(cipherText, iv)
}
private fun callCip(cipText: ByteArray, iv: ByteArray): String {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), GCMParameterSpec(128, iv))
val plainText = cipher.doFinal(cipText)
return String(plainText)
}
And my viewholder code:
doAsync {
var name = ""
var lname = ""
var patRecord = ""
if (value?.patient != null){
name = PatSecHelper.nToPat(value.patient?.name?.trim() ?: "")
lname = PatSecHelper.nToPat(value.patient?.lastName?.trim() ?: "")
patRecord = PatSecHelper.nToPat(value.patient?.patientRecord?.trim() ?: "")
}
onComplete {
if (value?.patient == null){
view.textViewPatientId.visibility = View.GONE
}else{
if (name == "" || lname == "") {
view.textViewPatient.text = "Error..."
}else{
view.textViewPatient.text = "$name $lname"
}
if (patRecord == ""){
view.textViewPatientId.text = "Error..."
}else{
view.textViewPatientId.text = patRecord
}
}
}
}
**EDIT:
Here is the code that i'm using to genarete and get keys
private fun generateKey(){
keyStore = KeyStore.getInstance(AndroidKeyStore)
keyStore.load(null)
if (!keyStore.containsAlias(KEY_ALIAS)) {
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, AndroidKeyStore)
keyGenerator.init(
KeyGenParameterSpec.Builder(KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM).setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setRandomizedEncryptionRequired(false)
.build())
keyGenerator.generateKey()
}
}
#Throws(Exception::class)
private fun getSecretKey(): java.security.Key {
keyStore = KeyStore.getInstance(AndroidKeyStore)
keyStore.load(null)
return keyStore.getKey(KEY_ALIAS, null)
}
Here are the problems I see:
I am not sure what the implementation is of generateKey() but if it does what it says on the tin then don't generate your key every time you encrypt. As I believe you are doing here:
private fun encript(plain: String): String? {
return try {
generateKey() // <- why are you doing this every time? are you storing a unique key with everything you encrypt?
encData(plain)
} catch (e: Throwable) {
if (!BuildConfig.DEBUG) {
Crashlytics.logException(e)
}
null
}
}
This will mean you have a new key every time you encrypt something? Rather, this should be initialized and ready before you try to encrypt/decrypt.
And, if this is the only place you do generate your key, then how do you know that when you call this:
private fun callCip(cipText: ByteArray, iv: ByteArray): String {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), GCMParameterSpec(128, iv)) // <- Are you sure you have called encrypt before callin this?
val plainText = cipher.doFinal(cipText)
return String(plainText)
}
That you have a key generated to be fetched? Pulling out generateKey() and making sure you have called it before you do any encrypt/decrypt actions should fix your problem I think.