Last active
December 23, 2024 12:49
-
-
Save Hayk985/05e5624b619ae3f248c0ab345b063b30 to your computer and use it in GitHub Desktop.
Android KeyStore API Tutorial
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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