Skip to content

Instantly share code, notes, and snippets.

@Hayk985
Last active April 16, 2024 11:49
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Hayk985/05e5624b619ae3f248c0ab345b063b30 to your computer and use it in GitHub Desktop.
Save Hayk985/05e5624b619ae3f248c0ab345b063b30 to your computer and use it in GitHub Desktop.
Android KeyStore API Tutorial
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.unit.dp
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
// Source - https://medium.com/@hayk.mkrtchyan8998/shedding-light-on-android-encryption-android-crypto-api-part-3-android-keystore-0054fb386a98
interface CipherManager {
/**
* Generic function to encrypt our data
* @param inputText - the data we want to encrypt
* @return the encrypted data
*/
@Throws(Exception::class)
fun encrypt(inputText: String): String // Consider returning ByteArray instead of String
/**
* Generic function to decrypt our data. Consider passing a ByteArray.
* @param data - the data we want to decrypt.
* @return the decrypted data
*/
@Throws(Exception::class)
fun decrypt(data: String): String
}
class CipherManagerImpl : CipherManager {
private val keyAlias = "aes_key_alias" // TODO - Better to keep it secure
private val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE).apply {
load(null) // With load function we initialize our keystore
}
/**
* Gets a key from the keystore. If it doesn't exist, it creates a new one
*/
@Throws(Exception::class)
private fun getOrCreateKey(): SecretKey {
val existingKey = keyStore.getEntry(keyAlias, null) as? KeyStore.SecretKeyEntry
return existingKey?.secretKey ?: createKey()
}
/**
* Creates a new key using KeyGenerator and returns it
* First we initialize our KeyGenerator by passing KeyGenParameterSpec and then we generate the key
*/
@Throws(Exception::class)
private fun createKey(): SecretKey {
return KeyGenerator.getInstance(AES_ALGORITHM).apply {
init(
KeyGenParameterSpec.Builder(
keyAlias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(BLOCK_MODE)
.setEncryptionPaddings(PADDING)
.setUserAuthenticationRequired(false)
.setRandomizedEncryptionRequired(true)
.build()
)
}.generateKey()
}
@Throws(Exception::class)
override fun encrypt(inputText: String): String {
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey())
val encryptedBytes = cipher.doFinal(inputText.toByteArray())
val iv = cipher.iv
val encryptedDataWithIV = ByteArray(iv.size + encryptedBytes.size)
System.arraycopy(iv, 0, encryptedDataWithIV, 0, iv.size)
System.arraycopy(encryptedBytes, 0, encryptedDataWithIV, iv.size, encryptedBytes.size)
return Base64.encodeToString(encryptedDataWithIV, Base64.DEFAULT)
}
@Throws(Exception::class)
override fun decrypt(data: String): String {
val encryptedDataWithIV = Base64.decode(data, Base64.DEFAULT)
val cipher = Cipher.getInstance(TRANSFORMATION)
val iv = encryptedDataWithIV.copyOfRange(0, cipher.blockSize)
cipher.init(Cipher.DECRYPT_MODE, getOrCreateKey(), IvParameterSpec(iv))
val encryptedData =
encryptedDataWithIV.copyOfRange(cipher.blockSize, encryptedDataWithIV.size)
val decryptedBytes = cipher.doFinal(encryptedData)
return String(decryptedBytes, Charsets.UTF_8)
}
companion object {
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
private const val AES_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 = "$AES_ALGORITHM/$BLOCK_MODE/$PADDING"
}
}
// ------------------------------ UI ------------------------------
/**
* You can use this HomeScreen composable in your MainActivity and test it.
*/
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Composable
fun HomeScreen() {
// It would be better to use DI to inject CipherManager as a dependency
val cipherManager: CipherManager = CipherManagerImpl()
var input by remember { mutableStateOf("") }
var encryptedText by remember { mutableStateOf("") }
var decryptedText by remember { mutableStateOf("") }
val keyboardController = LocalSoftwareKeyboardController.current
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(vertical = 48.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
OutlinedTextField(
value = input,
onValueChange = { input = it },
)
Button(
modifier = Modifier.padding(top = 16.dp),
enabled = input.isNotEmpty() && input.isNotBlank(),
onClick = {
runCatching {
decryptedText = ""
encryptedText = cipherManager.encrypt(input)
keyboardController?.hide()
}
}
) {
Text(text = "Encrypt")
}
if (encryptedText.isNotEmpty() || decryptedText.isNotEmpty()) {
Card(
modifier = Modifier.padding(all = 24.dp),
colors = CardDefaults.cardColors(containerColor = Color.Transparent),
border = BorderStroke(2.dp, MaterialTheme.colorScheme.primary),
) {
Text(
modifier = Modifier
.padding(all = 4.dp) // Add padding for text to not touch the border
.fillMaxWidth()
.fillMaxHeight(0.3f),
text = decryptedText.ifEmpty { encryptedText },
)
}
Button(
modifier = Modifier.padding(top = 16.dp),
enabled = encryptedText.isNotEmpty(),
onClick = {
runCatching {
decryptedText = cipherManager.decrypt(encryptedText)
encryptedText = ""
keyboardController?.hide()
}
}
) {
Text(text = "Decrypt")
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment