diff --git a/android/build.gradle b/android/build.gradle index 3aa57e5..599777e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -25,7 +25,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 28 + compileSdkVersion 29 sourceSets { main.java.srcDirs += 'src/main/kotlin' diff --git a/android/src/main/kotlin/fr/pointcheval/native_crypto/Cipher.kt b/android/src/main/kotlin/fr/pointcheval/native_crypto/Cipher.kt new file mode 100644 index 0000000..fece17c --- /dev/null +++ b/android/src/main/kotlin/fr/pointcheval/native_crypto/Cipher.kt @@ -0,0 +1,101 @@ +package fr.pointcheval.native_crypto + +import java.lang.Exception +import javax.crypto.Cipher +import javax.crypto.SecretKey +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +enum class CipherAlgorithm(val spec: String) { + AES("AES"), + BlowFish("BLOWFISH") +} + +enum class BlockCipherMode(val instance: String) { + ECB("ECB"), + CBC("CBC"), + CFB("CFB"), + GCM("GCM"), + CGM("CGM"), +} + +enum class Padding(val instance: String) { + PKCS5("PKCS5Padding"), + None("NoPadding") +} + +class CipherParameters(private val mode: BlockCipherMode, private val padding: Padding) { + override fun toString(): String { + return mode.instance + "/" + padding.instance + } +} + +class Cipher { + + fun getCipherAlgorithm(dartAlgorithm: String) : CipherAlgorithm { + return when (dartAlgorithm) { + "aes" -> CipherAlgorithm.AES + "blowfish" -> CipherAlgorithm.BlowFish + else -> CipherAlgorithm.AES + } + } + + fun getInstance(mode : String, padding : String) : CipherParameters { + val m = when (mode) { + "ecb" -> BlockCipherMode.ECB + "cbc" -> BlockCipherMode.CBC + "cfb" -> BlockCipherMode.CFB + "gcm" -> BlockCipherMode.GCM + "cgm" -> BlockCipherMode.CGM + else -> throw Exception() + } + val p = when (padding) { + "pkcs5" -> Padding.PKCS5 + else -> Padding.None + } + return CipherParameters(m,p) + } + + fun encrypt(data: ByteArray, key: ByteArray, algorithm: String, mode: String, padding: String) : List { + val algo = getCipherAlgorithm(algorithm) + val params = getInstance(mode, padding) + + val keySpecification = algo.spec + "/" + params.toString() + + val mac = Hash().digest(key + data) + val sk: SecretKey = SecretKeySpec(key, algo.spec) + + val cipher = Cipher.getInstance(keySpecification) + cipher.init(Cipher.ENCRYPT_MODE, sk) + + val encryptedBytes = cipher.doFinal(mac + data) + val iv = cipher.iv + + return listOf(encryptedBytes, iv); + } + + fun decrypt(payload: Collection, key: ByteArray, algorithm: String, mode: String, padding: String) : ByteArray? { + val algo = getCipherAlgorithm(algorithm) + val params = getInstance(mode, padding) + + val keySpecification = algo.spec + "/" + params.toString() + + val sk: SecretKey = SecretKeySpec(key, algo.spec) + val cipher = Cipher.getInstance(keySpecification); + val iv = payload.last(); + val ivSpec = IvParameterSpec(iv) + cipher.init(Cipher.DECRYPT_MODE, sk, ivSpec); + + val decryptedBytes = cipher.doFinal(payload.first()); + + val mac = decryptedBytes.copyOfRange(0, 32) + val decryptedContent : ByteArray = decryptedBytes.copyOfRange(32, decryptedBytes.size) + val verificationMac = Hash().digest(key + decryptedContent) + + return if (mac.contentEquals(verificationMac)) { + decryptedContent + } else { + null; + } + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/fr/pointcheval/native_crypto/HashAlgorithm.kt b/android/src/main/kotlin/fr/pointcheval/native_crypto/HashAlgorithm.kt new file mode 100644 index 0000000..caa201b --- /dev/null +++ b/android/src/main/kotlin/fr/pointcheval/native_crypto/HashAlgorithm.kt @@ -0,0 +1,39 @@ +package fr.pointcheval.native_crypto + +import java.security.MessageDigest + +enum class HashAlgorithm(val length : Int) { + SHA1(160), + SHA128(128), + SHA256(256), + SHA512(512); +} + + +class Hash() { + fun digest(data: ByteArray?, algorithm: HashAlgorithm): ByteArray { + val func : String = when (algorithm) { + HashAlgorithm.SHA1 -> "SHA-1" + HashAlgorithm.SHA128 -> "SHA-128" + HashAlgorithm.SHA256 -> "SHA-256" + HashAlgorithm.SHA512 -> "SHA-512" + } + val md = MessageDigest.getInstance(func) + return md.digest(data) + } + + fun digest(data: ByteArray?, algorithm: String): ByteArray { + val func : HashAlgorithm = when (algorithm) { + "sha1" -> HashAlgorithm.SHA1 + "sha128" -> HashAlgorithm.SHA128 + "sha256" -> HashAlgorithm.SHA256 + "sha512" -> HashAlgorithm.SHA512 + else -> HashAlgorithm.SHA256 + } + return digest(data, func) + } + + fun digest(data: ByteArray?): ByteArray { + return digest(data, HashAlgorithm.SHA256) + } +} diff --git a/android/src/main/kotlin/fr/pointcheval/native_crypto/KeyDerivation.kt b/android/src/main/kotlin/fr/pointcheval/native_crypto/KeyDerivation.kt new file mode 100644 index 0000000..de071c4 --- /dev/null +++ b/android/src/main/kotlin/fr/pointcheval/native_crypto/KeyDerivation.kt @@ -0,0 +1,35 @@ +package fr.pointcheval.native_crypto + +import android.os.Build +import java.lang.IllegalArgumentException +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.PBEKeySpec + +class KeyDerivation { + fun pbkdf2(password: String, salt: String, keyLength: Int, iteration: Int, algorithm: String): ByteArray { + val chars: CharArray = password.toCharArray() + val availableHashAlgorithm: Map = mapOf( + "sha1" to "PBKDF2withHmacSHA1", + "sha224" to "PBKDF2withHmacSHA224", + "sha256" to "PBKDF2WithHmacSHA256", + "sha284" to "PBKDF2withHmacSHA384", + "sha512" to "PBKDF2withHmacSHA512" + ) + + if (Build.VERSION.SDK_INT >= 26) { + // SHA-1 and SHA-2 implemented + val spec = PBEKeySpec(chars, salt.toByteArray(), iteration, keyLength * 8) + val skf: SecretKeyFactory = SecretKeyFactory.getInstance(availableHashAlgorithm[algorithm]); + return skf.generateSecret(spec).encoded + } else if (Build.VERSION.SDK_INT >= 10) { + // Only SHA-1 is implemented + if (!algorithm.equals("sha1")) { + throw PlatformVersionException("Only SHA1 is implemented on this SDK version!") + } + val spec = PBEKeySpec(chars, salt.toByteArray(), iteration, keyLength * 8) + val skf: SecretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmacSHA1"); + return skf.generateSecret(spec).encoded + } + throw PlatformVersionException("Invalid SDK version!") + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/fr/pointcheval/native_crypto/KeyGeneration.kt b/android/src/main/kotlin/fr/pointcheval/native_crypto/KeyGeneration.kt new file mode 100644 index 0000000..b937732 --- /dev/null +++ b/android/src/main/kotlin/fr/pointcheval/native_crypto/KeyGeneration.kt @@ -0,0 +1,32 @@ +package fr.pointcheval.native_crypto + +import java.security.KeyPairGenerator +import java.security.SecureRandom +import javax.crypto.KeyGenerator + +class KeyGeneration { + fun keygen(size : Int) : ByteArray { + val secureRandom = SecureRandom() + val keyGenerator = if (size in listOf(128,192,256)) { + KeyGenerator.getInstance("AES") + } else { + KeyGenerator.getInstance("BLOWFISH") + } + + keyGenerator.init(size, secureRandom) + val sk = keyGenerator.generateKey() + + return sk!!.encoded + } + + fun rsaKeypairGen(size : Int) : List { + val secureRandom = SecureRandom() + val keyGenerator = KeyPairGenerator.getInstance("RSA") + + keyGenerator.initialize(size, secureRandom) + val keypair = keyGenerator.genKeyPair() + val res : List = listOf(keypair.public.encoded, keypair.private.encoded) + + return res + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/fr/pointcheval/native_crypto/NativeCryptoPlugin.kt b/android/src/main/kotlin/fr/pointcheval/native_crypto/NativeCryptoPlugin.kt index 729da34..78218bd 100644 --- a/android/src/main/kotlin/fr/pointcheval/native_crypto/NativeCryptoPlugin.kt +++ b/android/src/main/kotlin/fr/pointcheval/native_crypto/NativeCryptoPlugin.kt @@ -11,162 +11,132 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result import io.flutter.plugin.common.PluginRegistry.Registrar -import java.security.MessageDigest -import java.security.SecureRandom -import javax.crypto.Cipher -import javax.crypto.KeyGenerator -import javax.crypto.SecretKey -import javax.crypto.SecretKeyFactory -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.PBEKeySpec -import javax.crypto.spec.SecretKeySpec /** NativeCryptoPlugin */ -public class NativeCryptoPlugin : FlutterPlugin, MethodCallHandler { - // CRYPTO CONSTS - private val HASH_FUNC = "SHA-256" - private val SYM_CRYPTO_METHOD = "AES" - private val SYM_CRYPTO_PADDING = "AES/CBC/PKCS5PADDING" - +class NativeCryptoPlugin : FlutterPlugin, MethodCallHandler { override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - val channel = MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "native.crypto.helper") + val channel = MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "native.crypto") channel.setMethodCallHandler(NativeCryptoPlugin()); } companion object { @JvmStatic fun registerWith(registrar: Registrar) { - val channel = MethodChannel(registrar.messenger(), "native.crypto.helper") + val channel = MethodChannel(registrar.messenger(), "native.crypto") channel.setMethodCallHandler(NativeCryptoPlugin()) } } override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { - if (call.method == "pbkdf2") { - val password = call.argument("password") - val salt = call.argument("salt") - val keyLength = call.argument("keyLength") - val iteration = call.argument("iteration") - val algorithm = call.argument("algorithm") + when (call.method) { + "digest" -> { + val message = call.argument("message") + val algorithm = call.argument("algorithm") - val key = pbkdf2(password!!, salt!!, keyLength!!, iteration!!, algorithm!!) - - if (key.isNotEmpty()) { - result.success(key) - } else { - result.error("PBKDF2ERROR", "PBKDF2 KEY IS NULL.", null) - } - } else if (call.method == "symKeygen") { - val keySize = call.argument("size") // 128, 192, 256 - - val aesKey = symKeygen(keySize!!) // Collection - - if (aesKey.isNotEmpty()) { - result.success(aesKey) - } else { - result.error("SYMKEYGENERROR", "GENERATED KEY IS NULL.", null) - } - } else if (call.method == "symEncrypt") { - val payload = call.argument("payload") // ByteArray - val aesKey = call.argument("aesKey") // ByteArray - - val encryptedPayload = symEncrypt(payload!!, aesKey!!) // Collection - - if (encryptedPayload.isNotEmpty()) { - result.success(encryptedPayload) - } else { - result.error("ENCRYPTIONERROR", "ENCRYPTED PAYLOAD IS NULL.", null) - } - } else if (call.method == "symDecrypt") { - val payload = call.argument>("payload") // Collection - val aesKey = call.argument("aesKey") // ByteArray - var decryptedPayload : ByteArray? = null - - try { - decryptedPayload = symDecrypt(payload!!, aesKey!!) - if (decryptedPayload != null && decryptedPayload.isNotEmpty()) { - result.success(decryptedPayload) - } else { - result.error("DECRYPTIONERROR", "DECRYPTED PAYLOAD IS NULL. MAYBE VERIFICATION MAC IS UNVALID.", null) + try { + val d = Hash().digest(message, algorithm!!) + if (d.isNotEmpty()) { + result.success(d) + } else { + result.error("DIGESTERROR", "DIGEST IS NULL.", null) + } + } catch (e : Exception) { + result.error("DIGESTEXCEPTION", e.message, null) } - } catch (e : Exception) { - result.error("DECRYPTIONERROR", "AN ERROR OCCURED WHILE DECRYPTING.", null) } - - } else { - result.notImplemented() + "pbkdf2" -> { + val password = call.argument("password") + val salt = call.argument("salt") + val keyLength = call.argument("keyLength") + val iteration = call.argument("iteration") + val algorithm = call.argument("algorithm") + + try { + val key = KeyDerivation().pbkdf2(password!!, salt!!, keyLength!!, iteration!!, algorithm!!) + if (key.isNotEmpty()) { + result.success(key) + } else { + result.error("PBKDF2ERROR", "PBKDF2 KEY IS NULL.", null) + } + } catch (e : Exception) { + result.error("PBKDF2EXCEPTION", e.message, null) + } + } + "keygen" -> { + val size = call.argument("size") // 128, 192, 256 + try { + val key = KeyGeneration().keygen(size!!) + + if (key.isNotEmpty()) { + result.success(key) + } else { + result.error("KEYGENERROR", "GENERATED KEY IS NULL.", null) + } + } catch (e : Exception) { + result.error("KEYGENEXCEPTION", e.message, null) + } + } + "rsaKeypairGen" -> { + val size = call.argument("size") + try { + val keypair = KeyGeneration().rsaKeypairGen(size!!) + + if (keypair.isNotEmpty()) { + result.success(keypair) + } else { + result.error("KEYPAIRGENERROR", "GENERATED KEYPAIR IS EMPTY.", null) + } + } catch (e : Exception) { + result.error("KEYPAIRGENEXCEPTION", e.message, null) + } + } + "encrypt" -> { + val data = call.argument("data") + val key = call.argument("key") + val algorithm = call.argument("algorithm") + val mode = call.argument("mode") + val padding = call.argument("padding") + + try { + val payload = Cipher().encrypt(data!!, key!!, algorithm!!, mode!!, padding!!) + + if (payload.isNotEmpty()) { + result.success(payload) + } else { + result.error("ENCRYPTIONERROR", "ENCRYPTED PAYLOAD IS EMPTY.", null) + } + } catch (e: Exception) { + result.error("ENCRYPTIONEXCEPTION", e.message, null) + } + + } + "decrypt" -> { + val payload = call.argument>("payload") // Collection + + val key = call.argument("key") + val algorithm = call.argument("algorithm") + val mode = call.argument("mode") + val padding = call.argument("padding") + + var decryptedPayload : ByteArray? = null + + try { + decryptedPayload = Cipher().decrypt(payload!!, key!!, algorithm!!, mode!!, padding!!) + if (decryptedPayload != null && decryptedPayload.isNotEmpty()) { + result.success(decryptedPayload) + } else { + result.error("DECRYPTIONERROR", "DECRYPTED PAYLOAD IS NULL. MAYBE VERIFICATION MAC IS UNVALID.", null) + } + } catch (e : Exception) { + result.error("DECRYPTIONEXCEPTION", e.message, null) + } + } + else -> result.notImplemented() } } override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { } - - // CRYPTO NATIVE FUNCTIONS - - private fun digest(obj: ByteArray?): ByteArray { - val md = MessageDigest.getInstance(HASH_FUNC) - return md.digest(obj) - } - - private fun pbkdf2(password : String, salt : String, keyLength : Int, iteration : Int, algorithm : String) : ByteArray { - val chars: CharArray = password.toCharArray() - - val spec = PBEKeySpec(chars, salt.toByteArray(), iteration, keyLength * 8) - val skf: SecretKeyFactory = if (algorithm == "sha1") { - SecretKeyFactory.getInstance("PBKDF2withHmacSHA1") - } else if (algorithm == "sha256") { - SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") - } else { - SecretKeyFactory.getInstance("PBKDF2withHmacSHA512") - } - return skf.generateSecret(spec).encoded - } - - private fun symKeygen(keySize : Int): ByteArray { - - val SYM_CRYPTO_BITS = keySize - - val secureRandom = SecureRandom() - val keyGenerator = KeyGenerator.getInstance(SYM_CRYPTO_METHOD) - keyGenerator?.init(SYM_CRYPTO_BITS, secureRandom) - val skey = keyGenerator?.generateKey() - - return skey!!.encoded - } - - private fun symEncrypt(payload: ByteArray, aesKey: ByteArray): Collection { - - val mac = digest(aesKey + payload) - val key: SecretKey = SecretKeySpec(aesKey, SYM_CRYPTO_METHOD) - - val cipher = Cipher.getInstance(SYM_CRYPTO_PADDING) - cipher.init(Cipher.ENCRYPT_MODE, key) - - val encryptedBytes = cipher.doFinal(mac + payload) - val iv = cipher.iv - - return listOf(encryptedBytes, iv); - } - - private fun symDecrypt(payload: Collection, aesKey: ByteArray): ByteArray? { - - val key: SecretKey = SecretKeySpec(aesKey, SYM_CRYPTO_METHOD) - val cipher = Cipher.getInstance(SYM_CRYPTO_PADDING); - val iv = payload.last(); - val ivSpec = IvParameterSpec(iv) - cipher.init(Cipher.DECRYPT_MODE, key, ivSpec); - - val decryptedBytes = cipher.doFinal(payload.first()); - - val mac = decryptedBytes.copyOfRange(0, 32) - val decryptedContent = decryptedBytes.copyOfRange(32, decryptedBytes.size) - val verificationMac = digest(aesKey + decryptedContent) - - if (mac.contentEquals(verificationMac)) return decryptedContent - - return null; - } - } diff --git a/android/src/main/kotlin/fr/pointcheval/native_crypto/PlatformVersionException.kt b/android/src/main/kotlin/fr/pointcheval/native_crypto/PlatformVersionException.kt new file mode 100644 index 0000000..4549060 --- /dev/null +++ b/android/src/main/kotlin/fr/pointcheval/native_crypto/PlatformVersionException.kt @@ -0,0 +1,5 @@ +package fr.pointcheval.native_crypto + +import java.lang.Exception + +class PlatformVersionException(message : String) : Exception(message) \ No newline at end of file