feat: Bring over the cryptography API from moxplatform

This commit is contained in:
2023-09-08 19:25:26 +02:00
parent fd91ccc46f
commit b52ca03eff
12 changed files with 623 additions and 51 deletions

View File

@@ -4,7 +4,7 @@
<application>
<provider
android:name="org.moxxy.moxxy_native.content.MoxxyFileProvider"
android:authorities="org.moxxy.moxxyv2.fileprovider"
android:authorities="org.moxxy.moxxyv2.fileprovider2"
android:exported="false"
android:grantUriPermissions="true">
<meta-data

View File

@@ -2,6 +2,9 @@ package org.moxxy.moxxy_native
const val TAG = "moxxy_native"
// The size of buffers to use for various operations
const val BUFFER_SIZE = 4096
// The data key for text entered in the notification's reply field
const val REPLY_TEXT_KEY = "key_reply_text"

View File

@@ -13,6 +13,8 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.EventChannel
import org.moxxy.moxxy_native.cryptography.CryptographyImplementation
import org.moxxy.moxxy_native.cryptography.MoxxyCryptographyApi
import org.moxxy.moxxy_native.notifications.MessagingNotification
import org.moxxy.moxxy_native.notifications.MoxxyNotificationsApi
import org.moxxy.moxxy_native.notifications.NotificationChannel
@@ -57,11 +59,13 @@ class MoxxyNativePlugin : FlutterPlugin, ActivityAware, MoxxyPickerApi, MoxxyNot
private var activity: Activity? = null
private lateinit var activityClass: Class<Any>
private lateinit var pickerListener: PickerResultListener
private val cryptographyImplementation = CryptographyImplementation()
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
MoxxyPickerApi.setUp(flutterPluginBinding.binaryMessenger, this)
MoxxyNotificationsApi.setUp(flutterPluginBinding.binaryMessenger, this)
MoxxyCryptographyApi.setUp(flutterPluginBinding.binaryMessenger, cryptographyImplementation)
pickerListener = PickerResultListener(context!!)
Log.d(TAG, "Attached to engine")
}

View File

@@ -0,0 +1,192 @@
// Autogenerated from Pigeon (v11.0.1), do not edit directly.
// See also: https://pub.dev/packages/pigeon
package org.moxxy.moxxy_native.cryptography
import android.util.Log
import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MessageCodec
import io.flutter.plugin.common.StandardMessageCodec
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
private fun wrapResult(result: Any?): List<Any?> {
return listOf(result)
}
private fun wrapError(exception: Throwable): List<Any?> {
if (exception is FlutterError) {
return listOf(
exception.code,
exception.message,
exception.details,
)
} else {
return listOf(
exception.javaClass.simpleName,
exception.toString(),
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception),
)
}
}
/**
* Error class for passing custom error details to Flutter via a thrown PlatformException.
* @property code The error code.
* @property message The error message.
* @property details The error details. Must be a datatype supported by the api codec.
*/
class FlutterError(
val code: String,
override val message: String? = null,
val details: Any? = null,
) : Throwable()
enum class CipherAlgorithm(val raw: Int) {
AES128GCMNOPADDING(0),
AES256GCMNOPADDING(1),
AES256CBCPKCS7(2),
;
companion object {
fun ofRaw(raw: Int): CipherAlgorithm? {
return values().firstOrNull { it.raw == raw }
}
}
}
/** Generated class from Pigeon that represents data sent in messages. */
data class CryptographyResult(
val plaintextHash: ByteArray,
val ciphertextHash: ByteArray,
) {
companion object {
@Suppress("UNCHECKED_CAST")
fun fromList(list: List<Any?>): CryptographyResult {
val plaintextHash = list[0] as ByteArray
val ciphertextHash = list[1] as ByteArray
return CryptographyResult(plaintextHash, ciphertextHash)
}
}
fun toList(): List<Any?> {
return listOf<Any?>(
plaintextHash,
ciphertextHash,
)
}
}
@Suppress("UNCHECKED_CAST")
private object MoxxyCryptographyApiCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
128.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
CryptographyResult.fromList(it)
}
}
else -> super.readValueOfType(type, buffer)
}
}
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
when (value) {
is CryptographyResult -> {
stream.write(128)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
}
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface MoxxyCryptographyApi {
fun encryptFile(sourcePath: String, destPath: String, key: ByteArray, iv: ByteArray, algorithm: CipherAlgorithm, hashSpec: String, callback: (Result<CryptographyResult?>) -> Unit)
fun decryptFile(sourcePath: String, destPath: String, key: ByteArray, iv: ByteArray, algorithm: CipherAlgorithm, hashSpec: String, callback: (Result<CryptographyResult?>) -> Unit)
fun hashFile(sourcePath: String, hashSpec: String, callback: (Result<ByteArray?>) -> Unit)
companion object {
/** The codec used by MoxxyCryptographyApi. */
val codec: MessageCodec<Any?> by lazy {
MoxxyCryptographyApiCodec
}
/** Sets up an instance of `MoxxyCryptographyApi` to handle messages through the `binaryMessenger`. */
@Suppress("UNCHECKED_CAST")
fun setUp(binaryMessenger: BinaryMessenger, api: MoxxyCryptographyApi?) {
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyCryptographyApi.encryptFile", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val sourcePathArg = args[0] as String
val destPathArg = args[1] as String
val keyArg = args[2] as ByteArray
val ivArg = args[3] as ByteArray
val algorithmArg = CipherAlgorithm.ofRaw(args[4] as Int)!!
val hashSpecArg = args[5] as String
api.encryptFile(sourcePathArg, destPathArg, keyArg, ivArg, algorithmArg, hashSpecArg) { result: Result<CryptographyResult?> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyCryptographyApi.decryptFile", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val sourcePathArg = args[0] as String
val destPathArg = args[1] as String
val keyArg = args[2] as ByteArray
val ivArg = args[3] as ByteArray
val algorithmArg = CipherAlgorithm.ofRaw(args[4] as Int)!!
val hashSpecArg = args[5] as String
api.decryptFile(sourcePathArg, destPathArg, keyArg, ivArg, algorithmArg, hashSpecArg) { result: Result<CryptographyResult?> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.moxxy_native.MoxxyCryptographyApi.hashFile", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val sourcePathArg = args[0] as String
val hashSpecArg = args[1] as String
api.hashFile(sourcePathArg, hashSpecArg) { result: Result<ByteArray?> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}

View File

@@ -0,0 +1,169 @@
package org.moxxy.moxxy_native.cryptography
import android.util.Log
import org.moxxy.moxxy_native.BUFFER_SIZE
import org.moxxy.moxxy_native.TAG
import java.io.FileInputStream
import java.security.MessageDigest
import javax.crypto.Cipher
import javax.crypto.CipherOutputStream
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.concurrent.thread
/*
* Convert the algorithm spec @algorithm to the format that Java/Android understands
* */
private fun getCipherSpecFromInteger(algorithm: CipherAlgorithm): String {
return when (algorithm) {
CipherAlgorithm.AES128GCMNOPADDING -> "AES_128/GCM/NoPadding"
CipherAlgorithm.AES256GCMNOPADDING -> "AES_256/GCM/NoPadding"
CipherAlgorithm.AES256CBCPKCS7 -> "AES_256/CBC/PKCS7PADDING"
}
}
/*
* Implementation of Moxxy's cryptography API
* */
class CryptographyImplementation : MoxxyCryptographyApi {
override fun encryptFile(
sourcePath: String,
destPath: String,
key: ByteArray,
iv: ByteArray,
algorithm: CipherAlgorithm,
hashSpec: String,
callback: (Result<CryptographyResult?>) -> Unit,
) {
thread(start = true) {
val cipherSpec = getCipherSpecFromInteger(algorithm)
val buffer = ByteArray(BUFFER_SIZE)
val secretKey = SecretKeySpec(key, cipherSpec)
val inputStream = FileInputStream(sourcePath)
try {
val digest = MessageDigest.getInstance(hashSpec)
val cipher = Cipher.getInstance(cipherSpec).apply {
init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv))
}
val fileOutputStream = HashedFileOutputStream(destPath, hashSpec)
val cipherOutputStream = CipherOutputStream(fileOutputStream, cipher)
var length: Int
while (true) {
length = inputStream.read(buffer)
if (length <= 0) break
digest.update(buffer, 0, length)
cipherOutputStream.write(buffer, 0, length)
}
// Clean up
cipherOutputStream.apply {
flush()
close()
}
// Success
callback(
Result.success(
CryptographyResult(
plaintextHash = digest.digest(),
ciphertextHash = fileOutputStream.digest(),
),
),
)
} catch (ex: Exception) {
Log.e(TAG, "Failed to encrypt file $sourcePath: ${ex.message}")
callback(Result.success(null))
} finally {
// Clean up
inputStream.close()
}
}
}
override fun decryptFile(
sourcePath: String,
destPath: String,
key: ByteArray,
iv: ByteArray,
algorithm: CipherAlgorithm,
hashSpec: String,
callback: (Result<CryptographyResult?>) -> Unit,
) {
thread(start = true) {
val cipherSpec = getCipherSpecFromInteger(algorithm)
val buffer = ByteArray(BUFFER_SIZE)
val secretKey = SecretKeySpec(key, cipherSpec)
val inputStream = FileInputStream(sourcePath)
try {
val digest = MessageDigest.getInstance(hashSpec)
val cipher = Cipher.getInstance(cipherSpec).apply {
init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(iv))
}
val fileOutputStream = HashedFileOutputStream(destPath, hashSpec)
val cipherOutputStream = CipherOutputStream(fileOutputStream, cipher)
var length: Int
while (true) {
length = inputStream.read(buffer)
if (length <= 0) break
digest.update(buffer, 0, length)
cipherOutputStream.write(buffer, 0, length)
}
// Clean up
cipherOutputStream.apply {
flush()
close()
}
// Success
callback(
Result.success(
CryptographyResult(
plaintextHash = digest.digest(),
ciphertextHash = fileOutputStream.digest(),
),
),
)
} catch (ex: Exception) {
Log.e(TAG, "Failed to decrypt file $sourcePath: ${ex.message}")
callback(Result.success(null))
} finally {
// Clean up
inputStream.close()
}
}
}
override fun hashFile(
sourcePath: String,
hashSpec: String,
callback: (Result<ByteArray?>) -> Unit,
) {
thread(start = true) {
val buffer = ByteArray(BUFFER_SIZE)
val inputStream = FileInputStream(sourcePath)
try {
val digest = MessageDigest.getInstance(hashSpec)
var length: Int
while (true) {
length = inputStream.read(buffer)
if (length <= 0) break
// Only update the digest if we read more than 0 bytes
digest.update(buffer, 0, length)
}
// Return success
callback(Result.success(digest.digest()))
} catch (ex: Exception) {
Log.e(TAG, "Failed to has file $sourcePath with $hashSpec: ${ex.message}")
callback(Result.success(null))
} finally {
// Clean up
inputStream.close()
}
}
}
}

View File

@@ -0,0 +1,25 @@
package org.moxxy.moxxy_native.cryptography
import java.io.FileOutputStream
import java.security.MessageDigest
/*
* A FileOutputStream that continuously hashes whatever it writes to the file.
*/
class HashedFileOutputStream(name: String, hashAlgorithm: String) : FileOutputStream(name) {
private val digest: MessageDigest
init {
this.digest = MessageDigest.getInstance(hashAlgorithm)
}
override fun write(buffer: ByteArray, offset: Int, length: Int) {
super.write(buffer, offset, length)
digest.update(buffer, offset, length)
}
fun digest(): ByteArray {
return digest.digest()
}
}