Skip to content

Instantly share code, notes, and snippets.

@alapshin
Last active June 25, 2019 06:46
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 alapshin/c82a87d30c4a0cc3015381c454e0ebf2 to your computer and use it in GitHub Desktop.
Save alapshin/c82a87d30c4a0cc3015381c454e0ebf2 to your computer and use it in GitHub Desktop.
package com.example.crypto
import android.annotation.TargetApi
import android.content.Context
import android.os.Build
import android.security.KeyPairGeneratorSpec
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.io.FileNotFoundException
import java.math.BigInteger
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.PrivateKey
import java.util.Calendar
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.inject.Inject
import javax.security.auth.x500.X500Principal
/**
* [KeyProvider] implementation that stores keys using
* <a href="https://developer.android.com/training/articles/keystore">Android KeyStore</a>
*
* On Android L and older KeyStore doesn't support generation and storage of symmetric keys.
* This implementation works around this by generating symmetric keys using different provider and storing
* them in encrypted file in internal storage. File is encrypted using private key of asymmetric key pair generated by
* and stored in KeyStore.
*
* On Android M and later symmetric keys are generated and stored in Android KeyStore directly.
*
* @see <a href="https://proandroiddev.com/secure-data-in-android-encryption-7eda33e68f58">Secure data in Android</a>
*/
class AndroidKeyProvider @Inject constructor(private val context: Context) : KeyProvider {
companion object {
// Common Name for self-signed certificate that is used for KeyPair generation on Android before M
const val CN = "self-signed"
// Alias of asymmetric key used for encrypting symmetric keys before writing them to file on Android before M
const val ALIAS_WRAP_KEY = "compat_wrap_key"
// File name prefix for files that are used for storage of encrypted secrets key on Android before M
const val SECRET_KEY_FILE_NAME_PREFIX = "kf_"
const val PROVIDER_ANDROID = "AndroidKeyStore"
const val PROVIDER_BOUNCY_CASTLE = "BC"
}
private val keystore = KeyStore.getInstance(PROVIDER_ANDROID).apply { load(null) }
/**
* Get [KeyPair] from KeyStore or generate new one if it doesn't exists
*/
override fun getOrCreateKeyPair(alias: String): KeyPair {
val publicKey = keystore.getCertificate(alias)?.publicKey
val privateKey = keystore.getKey(alias, null) as PrivateKey?
return if (publicKey == null || privateKey == null) {
generateKeyPair(alias)
} else {
return KeyPair(publicKey, privateKey)
}
}
/**
* Generate new [KeyPair] and store it in KeyStore
*/
private fun generateKeyPair(alias: String): KeyPair {
val spec = if (Build.VERSION.SDK_INT < 23) {
val calendar = Calendar.getInstance()
val startDate = calendar.apply { set(1970, 0, 1) }.time
val expirationDate = calendar.apply { set(2048, 0, 1) }.time
KeyPairGeneratorSpec.Builder(context)
.setAlias(alias)
.setSerialNumber(BigInteger.ONE)
.setSubject(X500Principal("CN=$CN"))
.setStartDate(startDate)
.setEndDate(expirationDate)
.setKeySize(CryptoConstants.KEY_SIZE_RSA)
.build()
} else {
KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setKeySize(CryptoConstants.KEY_SIZE_RSA)
.setBlockModes(KeyProperties.BLOCK_MODE_ECB)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
.build()
}
val generator = KeyPairGenerator.getInstance(CryptoConstants.KEY_ALGORITHM_RSA, PROVIDER_ANDROID).apply {
initialize(spec)
}
return generator.generateKeyPair()
}
/**
* Get [SecretKey] from KeyStore or generate new one if it doesn't exists
*/
override fun getOrCreateSecretKey(alias: String): SecretKey {
val secretKey = if (Build.VERSION.SDK_INT < 23) {
getSecretKeyV18(alias)
} else {
getSecretKeyV23(alias)
}
return secretKey ?: generateSecretKey(alias)
}
/**
* Get [SecretKey] on Android before M
*
* On Android before M KeyStore doesn't support storage of symmetric keys.
* This implementation reads encrypted key from file in internal storage, decrypts and returns it.
*/
@Suppress("SwallowedException")
private fun getSecretKeyV18(alias: String): SecretKey? {
return try {
val privateKey = getOrCreateKeyPair(ALIAS_WRAP_KEY).private
val cipher = Cipher.getInstance(CryptoConstants.TRANSFORMATION_ASYMMETRIC).apply {
init(Cipher.UNWRAP_MODE, privateKey)
}
val wrappedSecretKey = context.openFileInput(SECRET_KEY_FILE_NAME_PREFIX + alias).use { input ->
input.readBytes()
}
cipher.unwrap(wrappedSecretKey, CryptoConstants.KEY_ALGORITHM_AES, Cipher.SECRET_KEY) as SecretKey
} catch (e: FileNotFoundException) {
null // Expected exception if key doesn't exist
}
}
/**
* Get [SecretKey] from KeyStore on Android M and later
*/
private fun getSecretKeyV23(alias: String): SecretKey? {
return keystore.getKey(alias, null) as SecretKey?
}
/**
* Generate new [SecretKey] and store it in KeyStore
*/
private fun generateSecretKey(alias: String): SecretKey {
return if (Build.VERSION.SDK_INT < 23) {
generateSecretKeyV18(alias)
} else {
generateSecretKeyV23(alias)
}
}
/**
* Generate new [SecretKey] on Android before M
*
* On Android before M KeyStore doesn't support generation and storage of symmetric keys.
* This implementation generates secret key using Bouncy Castle, encrypts it with private key from [KeyPair] and
* saves it to file in internal storage.
*/
private fun generateSecretKeyV18(alias: String): SecretKey {
val generator = KeyGenerator.getInstance(CryptoConstants.KEY_ALGORITHM_AES, PROVIDER_BOUNCY_CASTLE).apply {
init(CryptoConstants.KEY_SIZE_AES)
}
val secretKey = generator.generateKey()
val publicKey = getOrCreateKeyPair(ALIAS_WRAP_KEY).public
val cipher = Cipher.getInstance(CryptoConstants.TRANSFORMATION_ASYMMETRIC).apply {
init(Cipher.WRAP_MODE, publicKey)
}
val wrappedSecretKey = cipher.wrap(secretKey)
context.openFileOutput(SECRET_KEY_FILE_NAME_PREFIX + alias, Context.MODE_PRIVATE).use { output ->
output.write(wrappedSecretKey)
}
return secretKey
}
/**
* Generate new [SecretKey] and store it in KeyStore on Android M and later
*/
@TargetApi(23)
private fun generateSecretKeyV23(alias: String): SecretKey {
val spec = KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setKeySize(CryptoConstants.KEY_SIZE_AES)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
val generator = KeyGenerator.getInstance(CryptoConstants.KEY_ALGORITHM_AES, PROVIDER_ANDROID).apply {
init(spec)
}
return generator.generateKey()
}
}
package com.example.crypto
object CryptoConstants {
const val BLOCK_MODE_ECB = "ECB"
const val BLOCK_MODE_GCM = "GCM"
const val KEY_SIZE_AES = 256
const val KEY_SIZE_RSA = 4096
const val KEY_ALGORITHM_AES = "AES"
const val KEY_ALGORITHM_RSA = "RSA"
const val ENCRYPTION_PADDING_NONE = "NoPadding"
const val ENCRYPTION_PADDING_RSA_PKCS1 = "PKCS1Padding"
const val TRANSFORMATION_SYMMETRIC = "$KEY_ALGORITHM_AES/$BLOCK_MODE_GCM/$ENCRYPTION_PADDING_NONE"
const val TRANSFORMATION_ASYMMETRIC = "$KEY_ALGORITHM_RSA/$BLOCK_MODE_ECB/$ENCRYPTION_PADDING_RSA_PKCS1"
}
package com.example.crypto
import java.security.KeyPair
import javax.crypto.SecretKey
/**
* KeyProvider interface
*/
interface KeyProvider {
fun getOrCreateKeyPair(alias: String): KeyPair
fun getOrCreateSecretKey(alias: String): SecretKey
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment