refactor(android): clean and modernize kotlin code

This commit is contained in:
Hugo Pointcheval 2022-05-24 18:43:14 +02:00
parent 2fe4172131
commit 41354e3dc4
Signed by: hugo
GPG Key ID: A9E8E9615379254F
18 changed files with 379 additions and 198 deletions

View File

@ -2,14 +2,14 @@ group 'fr.pointcheval.native_crypto_android'
version '1.0-SNAPSHOT'
buildscript {
ext.kotlin_version = '1.3.50'
ext.kotlin_version = '1.6.21'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath 'com.android.tools.build:gradle:4.1.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

View File

@ -1,6 +0,0 @@
package fr.pointcheval.native_crypto_android
abstract class Cipher {
abstract fun encrypt(data: ByteArray, key: ByteArray) : ByteArray?;
abstract fun decrypt(data: ByteArray, key: ByteArray) : ByteArray?;
}

View File

@ -1,25 +0,0 @@
package fr.pointcheval.native_crypto_android
import java.security.MessageDigest
object Hash {
fun digest(data: ByteArray?, algorithm: HashAlgorithm): ByteArray {
val func : String = when (algorithm) {
HashAlgorithm.SHA256 -> "SHA-256"
HashAlgorithm.SHA384 -> "SHA-384"
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) {
"sha256" -> HashAlgorithm.SHA256
"sha384" -> HashAlgorithm.SHA384
"sha512" -> HashAlgorithm.SHA512
else -> HashAlgorithm.SHA256
}
return digest(data, func)
}
}

View File

@ -1,15 +0,0 @@
package fr.pointcheval.native_crypto_android
enum class HashAlgorithm(val length : Int) {
SHA256(256),
SHA384(384),
SHA512(512);
fun hmac(): String {
return when (this) {
HashAlgorithm.SHA256 -> "HmacSHA256"
HashAlgorithm.SHA384 -> "HmacSHA384"
HashAlgorithm.SHA512 -> "HmacSHA512"
}
}
}

View File

@ -1,24 +0,0 @@
package fr.pointcheval.native_crypto_android
import java.security.SecureRandom
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
object Key {
fun fromSecureRandom(bitsCount: Int) : ByteArray {
val bytes = ByteArray(bitsCount / 8)
SecureRandom.getInstanceStrong().nextBytes(bytes)
return bytes
}
fun fromPBKDF2(password: String, salt: String, keyBytesCount: Int, iterations: Int, algorithm: String): ByteArray {
val availableHashAlgorithm: Map<String, String> = mapOf(
"sha256" to "PBKDF2WithHmacSHA256",
"sha384" to "PBKDF2withHmacSHA384",
"sha512" to "PBKDF2withHmacSHA512"
)
val spec = PBEKeySpec(password.toCharArray(), salt.toByteArray(), iterations, keyBytesCount * 8)
val skf: SecretKeyFactory = SecretKeyFactory.getInstance(availableHashAlgorithm[algorithm])
return skf.generateSecret(spec).encoded
}
}

View File

@ -1,109 +1,127 @@
package fr.pointcheval.native_crypto_android
import androidx.annotation.NonNull
import fr.pointcheval.native_crypto_android.ciphers.AESCipher
import fr.pointcheval.native_crypto_android.kdf.Pbkdf2
import fr.pointcheval.native_crypto_android.keys.SecretKey
import fr.pointcheval.native_crypto_android.utils.CipherAlgorithm
import fr.pointcheval.native_crypto_android.utils.Constants
import fr.pointcheval.native_crypto_android.utils.HashAlgorithm
import fr.pointcheval.native_crypto_android.utils.Task
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import java.util.*
/** NativeCryptoAndroidPlugin */
class NativeCryptoAndroidPlugin: FlutterPlugin, MethodCallHandler {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private lateinit var channel : MethodChannel
class NativeCryptoAndroidPlugin : FlutterPlugin, MethodCallHandler {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private lateinit var channel: MethodChannel
private val name = "plugins.hugop.cl/native_crypto"
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.flutterEngine.dartExecutor, "plugins.hugop.cl/native_crypto")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"digest" -> {
if (!call.hasArgument("data")) result.error("DATA_NULL", null, null);
if (!call.hasArgument("algorithm")) result.error("ALGORITHM_NULL", null, null);
val data : ByteArray = call.argument<ByteArray>("data")!!
val algorithm : String = call.argument<String>("algorithm")!!
result.success(Hash.digest(data, algorithm))
}
"generateSecretKey" -> {
if (!call.hasArgument("bitsCount")) result.error("SIZE_NULL", null, null);
val bitsCount : Int = call.argument<Int>("bitsCount")!!
result.success(Key.fromSecureRandom(bitsCount))
}
"generateKeyPair" -> {
result.notImplemented()
}
"pbkdf2" -> {
if (!call.hasArgument("password")) result.error("PASSWORD_NULL", null, null);
if (!call.hasArgument("salt")) result.error("SALT_NULL", null, null);
if (!call.hasArgument("keyBytesCount")) result.error("SIZE_NULL", null, null);
if (!call.hasArgument("iterations")) result.error("ITERATIONS_NULL", null, null);
if (!call.hasArgument("algorithm")) result.error("ALGORITHM_NULL", null, null);
val password : String = call.argument<String>("password")!!
val salt : String = call.argument<String>("salt")!!
val keyBytesCount : Int = call.argument<Int>("keyBytesCount")!!
val iterations : Int = call.argument<Int>("iterations")!!
val algorithm : String = call.argument<String>("algorithm")!!
result.success(Key.fromPBKDF2(password, salt, keyBytesCount, iterations, algorithm))
}
"encrypt" -> {
if (!call.hasArgument("data")) result.error("DATA_NULL", null, null);
if (!call.hasArgument("key")) result.error("KEY_NULL", null, null);
if (!call.hasArgument("algorithm")) result.error("ALGORITHM_NULL", null, null);
val data : ByteArray = call.argument<ByteArray>("data")!!
val key : ByteArray = call.argument<ByteArray>("key")!!
val algorithm : String = call.argument<String>("algorithm")!!
if (algorithm == "aes") {
result.success(AESCipher().encrypt(data, key))
}
}
"decrypt" -> {
if (!call.hasArgument("data")) result.error("DATA_NULL", null, null);
if (!call.hasArgument("key")) result.error("KEY_NULL", null, null);
if (!call.hasArgument("algorithm")) result.error("ALGORITHM_NULL", null, null);
val data : ByteArray = call.argument<ByteArray>("data")!!
val key : ByteArray = call.argument<ByteArray>("key")!!
val algorithm : String = call.argument<String>("algorithm")!!
if (algorithm == "aes") {
result.success(AESCipher().decrypt(data, key))
}
}
"generateSharedSecretKey" -> {
if (!call.hasArgument("salt")) result.error("SALT_NULL", null, null);
if (!call.hasArgument("keyBytesCount")) result.error("SIZE_NULL", null, null);
if (!call.hasArgument("ephemeralPrivateKey")) result.error("PRIVATE_KEY_NULL", null, null);
if (!call.hasArgument("otherPublicKey")) result.error("PUBLIC_KEY_NULL", null, null);
if (!call.hasArgument("hkdfAlgorithm")) result.error("ALGORITHM_NULL", null, null);
val salt : ByteArray = call.argument<ByteArray>("salt")!!
val keyBytesCount : Int = call.argument<Int>("keyBytesCount")!!
val ephemeralPrivateKey : ByteArray = call.argument<ByteArray>("ephemeralPrivateKey")!!
val otherPublicKey : ByteArray = call.argument<ByteArray>("otherPublicKey")!!
val hkdfAlgorithm : String = call.argument<String>("hkdfAlgorithm")!!
result.notImplemented()
}
else -> result.notImplemented()
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, name)
channel.setMethodCallHandler(this)
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
lateinit var methodCallTask: Task<*>
when (call.method) {
"digest" -> methodCallTask = handleDigest(call.arguments())
"generateSecretKey" -> methodCallTask = handleGenerateSecretKey(call.arguments())
"generateKeyPair" -> result.notImplemented()
"pbkdf2" -> methodCallTask = handlePbkdf2(call.arguments())
"encrypt" -> methodCallTask = handleEncrypt(call.arguments())
"decrypt" -> methodCallTask = handleDecrypt(call.arguments())
"generateSharedSecretKey" -> result.notImplemented()
else -> result.notImplemented()
}
methodCallTask.call()
methodCallTask.finalize { task ->
if (task.isSuccessful()) {
result.success(task.getResult())
} else {
val exception: Exception = task.getException()
val message = exception.message
result.error("native_crypto", message, null)
}
}
}
private fun handleDigest(arguments: Map<String, Any>?): Task<ByteArray> {
return Task {
val data: ByteArray =
Objects.requireNonNull(arguments?.get(Constants.DATA)) as ByteArray
val algorithm: String =
Objects.requireNonNull(arguments?.get(Constants.ALGORITHM)) as String
HashAlgorithm.digest(data, algorithm)
}
}
private fun handleGenerateSecretKey(arguments: Map<String, Any>?): Task<ByteArray> {
return Task {
val bitsCount: Int = Objects.requireNonNull(arguments?.get(Constants.BITS_COUNT)) as Int
SecretKey.fromSecureRandom(bitsCount).bytes
}
}
private fun handlePbkdf2(arguments: Map<String, Any>?): Task<ByteArray> {
return Task {
val password: String =
Objects.requireNonNull(arguments?.get(Constants.PASSWORD)) as String
val salt: String = Objects.requireNonNull(arguments?.get(Constants.SALT)) as String
val keyBytesCount: Int =
Objects.requireNonNull(arguments?.get(Constants.KEY_BYTES_COUNT)) as Int
val iterations: Int =
Objects.requireNonNull(arguments?.get(Constants.ITERATIONS)) as Int
val algorithm: String =
Objects.requireNonNull(arguments?.get(Constants.ALGORITHM)) as String
val pbkdf2: Pbkdf2 = Pbkdf2(keyBytesCount, iterations, HashAlgorithm.valueOf(algorithm))
pbkdf2.init(password, salt)
pbkdf2.derive().bytes
}
}
private fun handleEncrypt(arguments: Map<String, Any>?): Task<ByteArray> {
return Task {
val data: ByteArray =
Objects.requireNonNull(arguments?.get(Constants.DATA)) as ByteArray
val key: ByteArray = Objects.requireNonNull(arguments?.get(Constants.KEY)) as ByteArray
val algorithm: String =
Objects.requireNonNull(arguments?.get(Constants.ALGORITHM)) as String
val cipherAlgorithm: CipherAlgorithm = CipherAlgorithm.valueOf(algorithm)
val cipher = cipherAlgorithm.getCipher()
cipher.encrypt(data, key)
}
}
private fun handleDecrypt(arguments: Map<String, Any>?): Task<ByteArray> {
return Task {
val data: ByteArray =
Objects.requireNonNull(arguments?.get(Constants.DATA)) as ByteArray
val key: ByteArray = Objects.requireNonNull(arguments?.get(Constants.KEY)) as ByteArray
val algorithm: String =
Objects.requireNonNull(arguments?.get(Constants.ALGORITHM)) as String
val cipherAlgorithm: CipherAlgorithm = CipherAlgorithm.valueOf(algorithm)
val cipher = cipherAlgorithm.getCipher()
cipher.decrypt(data, key)
}
}
}

View File

@ -0,0 +1,59 @@
package fr.pointcheval.native_crypto_android.ciphers
import fr.pointcheval.native_crypto_android.interfaces.Cipher
import fr.pointcheval.native_crypto_android.utils.CipherAlgorithm
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
class AES : Cipher {
override val algorithm: CipherAlgorithm
get() = CipherAlgorithm.aes
/* override fun encrypt(data: ByteArray, key: ByteArray): ByteArray {
val sk: SecretKey = SecretKeySpec(key, "AES")
val cipher = javax.crypto.Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, sk)
// javax.crypto representation = [CIPHERTEXT(n-16) || TAG(16)]
val bytes = cipher.doFinal(data)
val iv = cipher.iv.copyOf() // 12 bytes nonce
// native.crypto representation = [NONCE(12) || CIPHERTEXT(n-28) || TAG(16)]
return iv.plus(bytes)
}
override fun decrypt(data: ByteArray, key: ByteArray): ByteArray {
val sk: SecretKey = SecretKeySpec(key, "AES")
// native.crypto representation = [NONCE(12) || CIPHERTEXT(n-16) || TAG(16)]
val iv: ByteArray = data.take(12).toByteArray()
// javax.crypto representation = [CIPHERTEXT(n-28) || TAG(16)]
val payload: ByteArray = data.drop(12).toByteArray()
val spec = GCMParameterSpec(16 * 8, iv)
val cipher = javax.crypto.Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(javax.crypto.Cipher.DECRYPT_MODE, sk, spec)
return cipher.doFinal(payload)
}*/
override fun encrypt(data: ByteArray, key: ByteArray): ByteArray {
val sk: SecretKey = SecretKeySpec(key, "AES")
val cipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, sk)
// javax.crypto representation = [CIPHERTEXT(n-16)]
val bytes = cipher.doFinal(data)
val iv = cipher.iv
// native.crypto representation = [IV(16) || CIPHERTEXT(n-16)]
return iv.plus(bytes)
}
override fun decrypt(data: ByteArray, key: ByteArray): ByteArray {
val sk: SecretKey = SecretKeySpec(key, "AES")
// native.crypto representation = [IV(16) || CIPHERTEXT(n-16)]
val iv: ByteArray = data.take(16).toByteArray()
// javax.crypto representation = [CIPHERTEXT(n-16)]
val payload: ByteArray = data.drop(16).toByteArray()
val ivSpec = IvParameterSpec(iv)
val cipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(javax.crypto.Cipher.DECRYPT_MODE, sk, ivSpec)
return cipher.doFinal(payload)
}
}

View File

@ -1,31 +0,0 @@
package fr.pointcheval.native_crypto_android.ciphers
import fr.pointcheval.native_crypto_android.Cipher
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
class AESCipher : Cipher() {
override fun encrypt(data: ByteArray, key: ByteArray): ByteArray {
val sk: SecretKey = SecretKeySpec(key, "AES")
val cipher = javax.crypto.Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, sk)
// javax.crypto representation = [CIPHERTEXT(n-28) || TAG(16)]
val bytes = cipher.doFinal(data)
val iv = cipher.iv.copyOf() // 12 bytes nonce
// native.crypto representation = [NONCE(12) || CIPHERTEXT(n-28) || TAG(16)]
return iv.plus(bytes)
}
override fun decrypt(data: ByteArray, key: ByteArray): ByteArray? {
val sk: SecretKey = SecretKeySpec(key, "AES")
// native.crypto representation = [NONCE(12) || CIPHERTEXT(n-28) || TAG(16)]
val iv = data.sliceArray(IntRange(0,11)) // 12 bytes nonce
// javax.crypto representation = [CIPHERTEXT(n-28) || TAG(16)]
val payload = data.sliceArray(IntRange(12, data.size - 1))
val spec = GCMParameterSpec(16 * 8, iv)
val cipher = javax.crypto.Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(javax.crypto.Cipher.DECRYPT_MODE, sk, spec)
return cipher.doFinal(payload)
}
}

View File

@ -0,0 +1,10 @@
package fr.pointcheval.native_crypto_android.interfaces
import fr.pointcheval.native_crypto_android.utils.CipherAlgorithm
interface Cipher {
val algorithm: CipherAlgorithm
fun encrypt(data: ByteArray, key: ByteArray): ByteArray
fun decrypt(data: ByteArray, key: ByteArray): ByteArray
}

View File

@ -0,0 +1,5 @@
package fr.pointcheval.native_crypto_android.interfaces
interface Key {
val bytes: ByteArray
}

View File

@ -0,0 +1,10 @@
package fr.pointcheval.native_crypto_android.interfaces
import fr.pointcheval.native_crypto_android.keys.SecretKey
import fr.pointcheval.native_crypto_android.utils.KdfAlgorithm
interface KeyDerivation {
val algorithm: KdfAlgorithm
fun derive(): SecretKey
}

View File

@ -0,0 +1,39 @@
package fr.pointcheval.native_crypto_android.kdf
import fr.pointcheval.native_crypto_android.interfaces.KeyDerivation
import fr.pointcheval.native_crypto_android.keys.SecretKey
import fr.pointcheval.native_crypto_android.utils.HashAlgorithm
import fr.pointcheval.native_crypto_android.utils.KdfAlgorithm
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
class Pbkdf2(
private val keyBytesCount: Int, private val iterations: Int,
private val hash: HashAlgorithm = HashAlgorithm.sha256
) : KeyDerivation {
private var password: String? = null
private var salt: String? = null
fun init(password: String, salt: String) {
this.password = password
this.salt = salt
}
override val algorithm: KdfAlgorithm
get() = KdfAlgorithm.pbkdf2
override fun derive(): SecretKey {
if (password == null || salt == null) {
throw Exception("Password and Salt must be initialized.")
}
val spec = PBEKeySpec(
password!!.toCharArray(),
salt!!.toByteArray(),
iterations,
keyBytesCount * 8
)
val skf: SecretKeyFactory = SecretKeyFactory.getInstance(hash.pbkdf2String())
return SecretKey(skf.generateSecret(spec).encoded)
}
}

View File

@ -0,0 +1,14 @@
package fr.pointcheval.native_crypto_android.keys
import fr.pointcheval.native_crypto_android.interfaces.Key
import java.security.SecureRandom
class SecretKey(override val bytes: ByteArray) : Key {
companion object {
fun fromSecureRandom(bitsCount: Int): SecretKey {
val bytes = ByteArray(bitsCount / 8)
SecureRandom.getInstanceStrong().nextBytes(bytes)
return SecretKey(bytes)
}
}
}

View File

@ -0,0 +1,14 @@
package fr.pointcheval.native_crypto_android.utils
import fr.pointcheval.native_crypto_android.ciphers.AES
import fr.pointcheval.native_crypto_android.interfaces.Cipher
enum class CipherAlgorithm {
aes;
fun getCipher(): Cipher {
return when (this) {
aes -> AES()
}
}
}

View File

@ -0,0 +1,14 @@
package fr.pointcheval.native_crypto_android.utils
object Constants {
const val ALGORITHM = "algorithm"
const val BITS_COUNT = "bitsCount"
const val DATA = "data"
const val PASSWORD = "password"
const val SALT = "salt"
const val KEY = "key"
const val KEY_BYTES_COUNT = "keyBytesCount"
const val ITERATIONS = "iterations"
const val EPHEMERAL_PRIVATE_KEY = "ephemeralPrivateKey"
const val OTHER_PUBLIC_KEY = "otherPublicKey"
}

View File

@ -0,0 +1,50 @@
package fr.pointcheval.native_crypto_android.utils
import java.security.MessageDigest
@Suppress("EnumEntryName")
enum class HashAlgorithm(val bitsCount: Int) {
sha256(256),
sha384(384),
sha512(512);
fun messageDigestString(): String {
return when (this) {
sha256 -> "SHA-256"
sha384 -> "SHA-384"
sha512 -> "SHA-512"
}
}
fun hmacString(): String {
return when (this) {
sha256 -> "HmacSHA256"
sha384 -> "HmacSHA384"
sha512 -> "HmacSHA512"
}
}
fun pbkdf2String(): String {
return when (this) {
sha256 -> "PBKDF2WithHmacSHA256"
sha384 -> "PBKDF2WithHmacSHA384"
sha512 -> "PBKDF2WithHmacSHA512"
}
}
fun digest(data: ByteArray): ByteArray {
val md = MessageDigest.getInstance(messageDigestString())
return md.digest(data)
}
companion object {
fun digest(data: ByteArray, algorithm: String): ByteArray {
for (h in values()) {
if (h.name == algorithm) {
return h.digest(data)
}
}
throw Exception("Unknown HashAlgorithm: $algorithm")
}
}
}

View File

@ -0,0 +1,5 @@
package fr.pointcheval.native_crypto_android.utils
enum class KdfAlgorithm {
pbkdf2
}

View File

@ -0,0 +1,44 @@
package fr.pointcheval.native_crypto_android.utils
class Task<T>(private var task: () -> T) {
private var successful = false
private var result: T? = null
private var exception: Exception? = null
fun isSuccessful(): Boolean {
return successful
}
fun getResult(): T {
if (successful && result != null) {
return result!!
} else {
throw Exception("No result found!")
}
}
fun getException(): Exception {
if (exception != null) {
return exception!!
} else {
throw Exception("No exception found!")
}
}
fun call() {
try {
result = task()
exception = null
successful = true
} catch (e: Exception) {
exception = e
result = null
successful = false
}
}
fun finalize(callback: (task: Task<T>) -> Unit) {
callback(this)
}
}