Skip to content

Instantly share code, notes, and snippets.

@lambdapioneer
Created April 8, 2020 16:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lambdapioneer/135619bef91144fd58a8c12aed7ae2e0 to your computer and use it in GitHub Desktop.
Save lambdapioneer/135619bef91144fd58a8c12aed7ae2e0 to your computer and use it in GitHub Desktop.
Quick benchmark to measure the throughput of AES encryption/decryption operations of the secure element on Android.
package com.lambdapioneer.androidtee
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties.*
import android.util.Log
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.IvParameterSpec
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
private enum class SecureElementCipher(val blockMode: String, val transformation: String) {
AES_CTR(BLOCK_MODE_CTR, "AES/CTR/NoPadding"),
AES_GCM(BLOCK_MODE_GCM, "AES/GCM/NoPadding"),
}
private data class SecureElementEncryptionResult(val ciphertext: ByteArray, val iv: ByteArray)
private class SecureElementBackedBox(
private val keyAlias: String,
private val cipher: SecureElementCipher,
private val keyLength: Int
) {
fun createOrResetKey() {
val keyGenSpec = KeyGenParameterSpec.Builder(
keyAlias,
PURPOSE_ENCRYPT or PURPOSE_DECRYPT
).run {
setIsStrongBoxBacked(true) // Forces use of secure element
setBlockModes(cipher.blockMode)
setEncryptionPaddings(ENCRYPTION_PADDING_NONE)
setKeySize(keyLength)
build()
}
val keyGen = KeyGenerator.getInstance(KEY_ALGORITHM_AES, ANDROID_KEY_STORE)
keyGen.init(keyGenSpec)
keyGen.generateKey()
}
fun encrypt(plaintext: ByteArray): SecureElementEncryptionResult {
val keyHandle = getKeyHandle()
return Cipher.getInstance(cipher.transformation).run {
init(Cipher.ENCRYPT_MODE, keyHandle)
SecureElementEncryptionResult(
ciphertext = doFinal(plaintext),
iv = iv
)
}
}
fun decrypt(ciphertext: SecureElementEncryptionResult): ByteArray {
val keyHandle = getKeyHandle()
val parameterSpec = when (cipher.blockMode) {
BLOCK_MODE_CTR -> IvParameterSpec(ciphertext.iv)
BLOCK_MODE_GCM -> GCMParameterSpec(128, ciphertext.iv)
else -> throw IllegalArgumentException()
}
return Cipher.getInstance(cipher.transformation).run {
init(Cipher.DECRYPT_MODE, keyHandle, parameterSpec)
doFinal(ciphertext.ciphertext)
}
}
private fun getKeyHandle() = KeyStore.getInstance(ANDROID_KEY_STORE).run {
load(null)
getKey(keyAlias, null)
}
}
private data class BenchmarkConfig(
val cipher: SecureElementCipher,
val keyLength: Int,
val sizeBytes: Int
) {
override fun toString() = String.format(
"%s_%s len=%-5d", cipher.blockMode, keyLength, sizeBytes
)
}
private data class BenchmarkResult(
val config: BenchmarkConfig,
val timeMsEncrypt: Long,
val timeMsDecrypt: Long
) {
fun inKiBperSecond(time: Long) =
String.format("%4.1f KiB/s", config.sizeBytes.toDouble() / 1.024 / time.toDouble())
override fun toString(): String = String.format(
"ENC: %4dms, %s; DEC: %4dms, %s",
timeMsEncrypt, inKiBperSecond(timeMsEncrypt),
timeMsDecrypt, inKiBperSecond(timeMsDecrypt)
)
}
private fun measure(config: BenchmarkConfig, iterations: Int = 16): BenchmarkResult {
val instance = SecureElementBackedBox("key_alias", config.cipher, config.keyLength)
instance.createOrResetKey()
// measure encryption
val timeStartEnc = System.nanoTime()
for (i in 1..iterations) instance.encrypt(ByteArray(config.sizeBytes))
val timeEncMs = (System.nanoTime() - timeStartEnc) / 1_000_000 / iterations
// measure decryption
val ciphertext = instance.encrypt(ByteArray(config.sizeBytes))
val timeStartDec = System.nanoTime()
for (i in 1..iterations) instance.decrypt(ciphertext)
val timeDecMs = (System.nanoTime() - timeStartDec) / 1_000_000 / iterations
return BenchmarkResult(config, timeEncMs, timeDecMs)
}
fun benchmark() {
val configs = arrayOf(
BenchmarkConfig(SecureElementCipher.AES_CTR, 128, 128 / 8),
BenchmarkConfig(SecureElementCipher.AES_CTR, 128, 1024),
BenchmarkConfig(SecureElementCipher.AES_CTR, 128, 16 * 1024),
BenchmarkConfig(SecureElementCipher.AES_CTR, 128, 32 * 1024),
BenchmarkConfig(SecureElementCipher.AES_CTR, 256, 128 / 8),
BenchmarkConfig(SecureElementCipher.AES_CTR, 256, 1024),
BenchmarkConfig(SecureElementCipher.AES_CTR, 256, 16 * 1024),
BenchmarkConfig(SecureElementCipher.AES_CTR, 256, 32 * 1024),
BenchmarkConfig(SecureElementCipher.AES_GCM, 128, 128 / 8),
BenchmarkConfig(SecureElementCipher.AES_GCM, 128, 1024),
BenchmarkConfig(SecureElementCipher.AES_GCM, 128, 16 * 1024),
BenchmarkConfig(SecureElementCipher.AES_GCM, 128, 32 * 1024),
BenchmarkConfig(SecureElementCipher.AES_GCM, 256, 128 / 8),
BenchmarkConfig(SecureElementCipher.AES_GCM, 256, 1024),
BenchmarkConfig(SecureElementCipher.AES_GCM, 256, 16 * 1024),
BenchmarkConfig(SecureElementCipher.AES_GCM, 256, 32 * 1024)
)
for (config in configs) {
val result = measure(config)
Log.i("Benchmark", "$config -> $result")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment