Last active
June 25, 2019 06:46
-
-
Save alapshin/c82a87d30c4a0cc3015381c454e0ebf2 to your computer and use it in GitHub Desktop.
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
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() | |
} | |
} |
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
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" | |
} |
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
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