Skip to content

Commit c9960c4

Browse files
committed
Fix package and path issue
1 parent b23b9a1 commit c9960c4

File tree

9 files changed

+429
-3
lines changed

9 files changed

+429
-3
lines changed

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1-
## 0.0.1
1+
## 1.0.0
22

3-
* TODO: Describe initial release.
3+
* The first version of flutter_key_cyptography is a basic key store crypto plugin.
4+
* This exposes 3 functions `getPublicKey`, `sign` & `verify`.
5+
* The key is RSA type, SHA256withRSA algorith and 2048 bit size only.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.plugin.flutter.cryptography.key_store
2+
3+
enum class CallMethodTypeEnum {
4+
getPublicKey,
5+
encrypt,
6+
verify
7+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
2+
package com.plugin.flutter.cryptography.key_store
3+
4+
import android.content.Context
5+
import android.content.SharedPreferences
6+
import android.util.Base64
7+
import java.nio.charset.Charset
8+
import java.security.Key
9+
import java.security.SecureRandom
10+
import javax.crypto.Cipher
11+
import javax.crypto.spec.IvParameterSpec
12+
import javax.crypto.spec.SecretKeySpec
13+
class CipherImplementation {
14+
15+
private val keyStoreCipher: KeyStoreImplementation
16+
private val rsaImplementation: RsaImplementation
17+
18+
companion object {
19+
private val ivSize = 16
20+
private val keySize = 16
21+
private const val AES_KEY_ALGORITHM = "AES"
22+
private val cipher: Cipher = Cipher.getInstance("AES/CBC/PKCS7Padding")
23+
private val secureRandom = SecureRandom()
24+
private var secretKey: Key ? = null
25+
private var aesKey: String ? = null
26+
private lateinit var rsaPublicKey: String
27+
private lateinit var rsaPrivateKey: String
28+
private var sharedPreferenceName: String = "cipherImplementationKeystore"
29+
private var SP_PUBLIC_KEY_NAME = "PUBLIC_KEY"
30+
private var SP_PRIVATE_KEY_NAME = "PRIVATE_KEY"
31+
private var SP_AES_KEY_NAME = "AES_KEY"
32+
}
33+
34+
constructor(context: Context) {
35+
keyStoreCipher = KeyStoreImplementation(context)
36+
rsaImplementation = RsaImplementation()
37+
var sharedPreference: SharedPreferences = context.getSharedPreferences(sharedPreferenceName, Context.MODE_PRIVATE)
38+
val editor: SharedPreferences.Editor = sharedPreference.edit()
39+
40+
/// aes key is generated and stored in shared preference, this key is used to encrypt and decrypt
41+
aesKey = sharedPreference.getString(SP_AES_KEY_NAME, null)
42+
val rsaPublicKeyEncrypted = sharedPreference.getString(SP_PUBLIC_KEY_NAME, null)
43+
val rsaPrivateKeyEncrypted = sharedPreference.getString(SP_PRIVATE_KEY_NAME, null)
44+
if (aesKey != null) {
45+
var encrypted: ByteArray
46+
try {
47+
encrypted = Base64.decode(aesKey, Base64.DEFAULT)
48+
secretKey = keyStoreCipher.unwrap(encrypted, AES_KEY_ALGORITHM)
49+
rsaPublicKey = decryptWithKeyStore(rsaPublicKeyEncrypted)
50+
rsaPrivateKey = decryptWithKeyStore(rsaPrivateKeyEncrypted)
51+
// if there is any expection in getting the keystore the exception is caught and
52+
// new key sets are created
53+
return
54+
} catch (exception: Exception) {
55+
encrypted = ByteArray(0)
56+
}
57+
}
58+
59+
val key = ByteArray(keySize)
60+
secretKey = SecretKeySpec(key, AES_KEY_ALGORITHM)
61+
val encryptedKey = keyStoreCipher.wrap(secretKey!!)
62+
val keyPair = rsaImplementation.getRsaKeyPair()
63+
secureRandom.nextBytes(key)
64+
65+
aesKey = Base64.encodeToString(encryptedKey, Base64.DEFAULT)
66+
// this public and private key is generated to sign the data
67+
// these keys are encrypted using the keystore and stored in shared preference
68+
rsaPublicKey = encodeToBase64(keyPair.public.encoded)
69+
rsaPrivateKey = encodeToBase64(keyPair.private.encoded)
70+
71+
editor.putString(SP_PUBLIC_KEY_NAME, encryptWithKeystore(rsaPublicKey))
72+
editor.putString(SP_PRIVATE_KEY_NAME, encryptWithKeystore(rsaPrivateKey))
73+
editor.putString(SP_AES_KEY_NAME, aesKey)
74+
editor.commit()
75+
}
76+
77+
private fun encodeToBase64(byteArray: ByteArray): String {
78+
return Base64.encodeToString(byteArray, Base64.DEFAULT)
79+
}
80+
81+
fun getPublicKey(): String {
82+
return rsaPublicKey
83+
}
84+
85+
fun encrypt(plainInput: String): String {
86+
return rsaImplementation.sign(plainInput, rsaPrivateKey)
87+
}
88+
89+
fun verify(plainText: String, signature: String): Boolean {
90+
return rsaImplementation.verify(rsaPublicKey, plainText, signature)
91+
}
92+
93+
private fun encryptWithKeystore(plainInput: String): String {
94+
val inputByteArray = plainInput.toByteArray()
95+
val iv = ByteArray(ivSize)
96+
secureRandom.nextBytes(iv)
97+
val ivParameterSpec = IvParameterSpec(iv)
98+
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec)
99+
val payload = cipher.doFinal(inputByteArray)
100+
val combined = ByteArray(iv.size + payload.size)
101+
System.arraycopy(iv, 0, combined, 0, iv.size)
102+
System.arraycopy(payload, 0, combined, iv.size, payload.size)
103+
return encodeToBase64(combined)
104+
}
105+
106+
private fun decryptWithKeyStore(input: String): String {
107+
val iv = ByteArray(ivSize)
108+
val inputByteArray = Base64.decode(input, 0)
109+
System.arraycopy(inputByteArray, 0, iv, 0, iv.size)
110+
val ivParameterSpec = IvParameterSpec(iv)
111+
val payloadSize = inputByteArray.size - ivSize
112+
val payload = ByteArray(payloadSize)
113+
System.arraycopy(inputByteArray, iv.size, payload, 0, payloadSize)
114+
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec)
115+
return String(cipher.doFinal(payload), Charset.forName("UTF-8"))
116+
}
117+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.plugin.flutter.cryptography.key_store
2+
3+
import android.util.Log
4+
import androidx.annotation.NonNull
5+
import io.flutter.embedding.engine.plugins.FlutterPlugin
6+
import io.flutter.plugin.common.MethodCall
7+
import io.flutter.plugin.common.MethodChannel
8+
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
9+
import io.flutter.plugin.common.MethodChannel.Result
10+
import io.flutter.plugin.common.PluginRegistry.Registrar
11+
12+
/** CipherPlugin */
13+
public class CipherPlugin : FlutterPlugin, MethodCallHandler {
14+
15+
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
16+
val channel = MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "cipher")
17+
channel.setMethodCallHandler(CipherPlugin())
18+
}
19+
20+
// This static function is optional and equivalent to onAttachedToEngine. It supports the old
21+
// pre-Flutter-1.12 Android projects. You are encouraged to continue supporting
22+
// plugin registration via this function while apps migrate to use the new Android APIs
23+
// post-flutter-1.12 via https://flutter.dev/go/android-project-migration.
24+
//
25+
// It is encouraged to share logic between onAttachedToEngine and registerWith to keep
26+
// them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called
27+
// depending on the user's project. onAttachedToEngine or registerWith must both be defined
28+
// in the same class.
29+
companion object {
30+
private lateinit var cipherImpl: CipherImplementation
31+
@JvmStatic
32+
fun registerWith(registrar: Registrar) {
33+
val channel = MethodChannel(registrar.messenger(), "cipher")
34+
cipherImpl = CipherImplementation(registrar.context())
35+
channel.setMethodCallHandler(CipherPlugin())
36+
}
37+
}
38+
39+
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
40+
when(call.method) {
41+
CallMethodTypeEnum.getPublicKey.name -> result.success(cipherImpl.getPublicKey())
42+
CallMethodTypeEnum.encrypt.name -> result.success(cipherImpl.encrypt(call.arguments as String))
43+
CallMethodTypeEnum.verify.name -> result.success(verify(call.arguments))
44+
else -> result.notImplemented()
45+
}
46+
}
47+
48+
private fun verify(arguments: Any): Boolean {
49+
val arguments: Map<String, String> = arguments as Map<String, String>
50+
val plainText = arguments[VerifyArgumentsMapKeysEnum.plainText.name] as String
51+
val signature = arguments[VerifyArgumentsMapKeysEnum.signature.name] as String
52+
return cipherImpl.verify(plainText, signature)
53+
}
54+
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
55+
}
56+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.plugin.flutter.cryptography.key_store
2+
3+
class ExceptionMessage {
4+
companion object {
5+
const val NO_ALGORITHM = "Could not reconstruct the public key, the given algorithm could not be found."
6+
const val INVALID_KEY= "Could not reconstruct the public key"
7+
const val CANNOT_CREATE_KEY = "Cannot create the keys"
8+
const val CANNOT_CREATE_KEY_WITH_STRONGBOX = "Cannot create keys for Android v 28 and higher, when strongbox is not available"
9+
const val NOT_PRIVATE_KEY = "Not an instance of private key"
10+
const val KEY_NOT_FOUND = "No key found with defined alias"
11+
const val CERTIFICATE_NOT_FOUND = "No certificate found with defined key alias"
12+
}
13+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package com.plugin.flutter.cryptography.key_store
2+
3+
import android.annotation.TargetApi
4+
import android.content.Context
5+
import android.os.Build
6+
import android.security.KeyPairGeneratorSpec
7+
import android.security.keystore.KeyGenParameterSpec
8+
import android.security.keystore.KeyProperties
9+
import android.security.keystore.StrongBoxUnavailableException
10+
import java.math.BigInteger
11+
import java.security.*
12+
import java.security.spec.AlgorithmParameterSpec
13+
import java.util.*
14+
import javax.crypto.Cipher
15+
import javax.security.auth.x500.X500Principal
16+
17+
class KeyStoreImplementation(context: Context) {
18+
private val KEY_ALIAS: String
19+
private val KEYSTORE_PROVIDER_ANDROID = "AndroidKeyStore"
20+
private val TYPE_RSA = "RSA"
21+
private val start = Calendar.getInstance()
22+
private val end = Calendar.getInstance()
23+
24+
init {
25+
end.add(Calendar.MONTH, 3) // TODO decide on validity
26+
KEY_ALIAS = "get_package" + "com.plugin.flutter.cryptography.key_store"
27+
createKeysIfNotExists(context)
28+
}
29+
30+
private fun createKeysIfNotExists(context: Context) {
31+
val keyStore: KeyStore = KeyStore.getInstance(KEYSTORE_PROVIDER_ANDROID)
32+
keyStore.load(null)
33+
val privateKey = keyStore.getKey(KEY_ALIAS, null)
34+
// if the keys are not present in keystore then create a new pair
35+
if (privateKey == null)
36+
createKeys(context)
37+
}
38+
39+
private fun createKeys(context: Context) {
40+
val keyPairGenerator = KeyPairGenerator.getInstance(TYPE_RSA, KEYSTORE_PROVIDER_ANDROID)
41+
val algorithmParameterSpec: AlgorithmParameterSpec = getAlgorithmSpec(context)
42+
43+
try {
44+
initializeKeyPair(keyPairGenerator, algorithmParameterSpec)
45+
} catch (exception: Exception) {
46+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && exception is StrongBoxUnavailableException)
47+
generateKeysForAndroidPWithoutStrongBox(keyPairGenerator)
48+
else
49+
throw Exception(ExceptionMessage.CANNOT_CREATE_KEY)
50+
}
51+
}
52+
53+
@TargetApi(Build.VERSION_CODES.P)
54+
private fun generateKeysForAndroidPWithoutStrongBox(keyPairGenerator: KeyPairGenerator) {
55+
try {
56+
val algorithmParameterSpecWithoutStrongBox = keyPairBuilder().build()
57+
initializeKeyPair(keyPairGenerator, algorithmParameterSpecWithoutStrongBox)
58+
} catch (exception: Exception) {
59+
throw Exception(ExceptionMessage.CANNOT_CREATE_KEY_WITH_STRONGBOX)
60+
}
61+
}
62+
63+
private fun initializeKeyPair(keyPairGenerator: KeyPairGenerator, algorithmParameterSpec: AlgorithmParameterSpec) {
64+
keyPairGenerator.initialize(algorithmParameterSpec)
65+
keyPairGenerator.generateKeyPair()
66+
}
67+
68+
@TargetApi(Build.VERSION_CODES.P)
69+
private fun keyPairBuilder(): KeyGenParameterSpec.Builder {
70+
71+
return KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_DECRYPT or KeyProperties.PURPOSE_ENCRYPT)
72+
.setCertificateSubject(X500Principal("CN=$KEY_ALIAS"))
73+
.setDigests(KeyProperties.DIGEST_SHA256)
74+
.setBlockModes(KeyProperties.BLOCK_MODE_ECB)
75+
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
76+
.setCertificateSerialNumber(BigInteger.valueOf(1))
77+
.setCertificateNotBefore(start.time)
78+
.setCertificateNotAfter(end.time)
79+
}
80+
81+
@TargetApi(Build.VERSION_CODES.M)
82+
private fun keyPairBuilderDeprecated(context: Context): KeyPairGeneratorSpec.Builder {
83+
return KeyPairGeneratorSpec.Builder(context)
84+
.setAlias(KEY_ALIAS)
85+
.setSubject(X500Principal("CN=$KEY_ALIAS"))
86+
.setSerialNumber(BigInteger.valueOf(1))
87+
.setStartDate(start.time)
88+
.setEndDate(end.time)
89+
}
90+
91+
private fun getAlgorithmSpec(context: Context): AlgorithmParameterSpec {
92+
val algorithmParameterSpec: AlgorithmParameterSpec
93+
if (isAndroidBelowM()) {
94+
algorithmParameterSpec = keyPairBuilderDeprecated(context).build()
95+
} else {
96+
val keyPairSpecBuilder = keyPairBuilder()
97+
setStrongBox(keyPairSpecBuilder)
98+
algorithmParameterSpec = keyPairSpecBuilder.build()
99+
}
100+
101+
return algorithmParameterSpec
102+
}
103+
private fun setStrongBox(keyPairSpecBuilder: KeyGenParameterSpec.Builder) {
104+
if (isAndroidIsGreaterThanEqualP()) {
105+
keyPairSpecBuilder.setIsStrongBoxBacked(true)
106+
}
107+
}
108+
109+
private fun isAndroidBelowM(): Boolean {
110+
return Build.VERSION.SDK_INT < Build.VERSION_CODES.M
111+
}
112+
113+
private fun isAndroidIsGreaterThanEqualP(): Boolean {
114+
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
115+
}
116+
117+
private fun getPrivateKey(): PrivateKey {
118+
val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER_ANDROID)
119+
keyStore.load(null)
120+
val key: Key = keyStore.getKey(KEY_ALIAS, null) ?: throw Exception(ExceptionMessage.KEY_NOT_FOUND)
121+
if (key !is PrivateKey) {
122+
throw Exception(ExceptionMessage.NOT_PRIVATE_KEY)
123+
}
124+
return key
125+
}
126+
127+
fun getPublicKey(): PublicKey {
128+
val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER_ANDROID)
129+
keyStore.load(null)
130+
131+
val certificate = keyStore.getCertificate(KEY_ALIAS) ?: throw Exception(ExceptionMessage.CERTIFICATE_NOT_FOUND)
132+
val publicKey: PublicKey
133+
publicKey = certificate.getPublicKey()
134+
return publicKey
135+
}
136+
137+
fun wrap(key: Key): ByteArray {
138+
val publicKey: PublicKey = getPublicKey()
139+
val cipher = getRSACipher()
140+
cipher.init(Cipher.WRAP_MODE, publicKey)
141+
return cipher.wrap(key)
142+
}
143+
144+
fun unwrap(wrappedKey: ByteArray, algorithm: String): Key {
145+
val privateKey = getPrivateKey()
146+
val cipher = getRSACipher()
147+
cipher.init(Cipher.UNWRAP_MODE, privateKey)
148+
return cipher.unwrap(wrappedKey, algorithm, Cipher.SECRET_KEY)
149+
}
150+
// todo with app pin creation
151+
fun encrypt(input: String): ByteArray {
152+
val byteArrayInput = input.toByteArray()
153+
val publicKey = getPublicKey()
154+
val cipher = getRSACipher()
155+
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
156+
return cipher.doFinal(byteArrayInput)
157+
}
158+
159+
@Throws(Exception::class)
160+
private fun getRSACipher(): Cipher {
161+
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
162+
// error in android 6: InvalidKeyException: Need RSA private or public key
163+
Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL")
164+
} else {
165+
// error in android 5: NoSuchProviderException: Provider not available: AndroidKeyStoreBCWorkaround
166+
Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidKeyStoreBCWorkaround")
167+
}
168+
}
169+
}

0 commit comments

Comments
 (0)