Created
April 8, 2020 16:25
-
-
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.
This file contains 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
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