feat: Move the crypto APIs to pigeon

This commit is contained in:
2023-08-03 21:19:11 +02:00
parent 271428219a
commit 61de3cd565
8 changed files with 496 additions and 209 deletions

View File

@@ -81,6 +81,18 @@ public class Api {
}
}
public enum CipherAlgorithm {
AES128GCM_NO_PADDING(0),
AES256GCM_NO_PADDING(1),
AES256CBC_PKCS7(2);
final int index;
private CipherAlgorithm(final int index) {
this.index = index;
}
}
/** Generated class from Pigeon that represents data sent in messages. */
public static final class NotificationMessageContent {
/** The textual body of the message. */
@@ -904,6 +916,86 @@ public class Api {
}
}
/** Generated class from Pigeon that represents data sent in messages. */
public static final class CryptographyResult {
private @NonNull byte[] plaintextHash;
public @NonNull byte[] getPlaintextHash() {
return plaintextHash;
}
public void setPlaintextHash(@NonNull byte[] setterArg) {
if (setterArg == null) {
throw new IllegalStateException("Nonnull field \"plaintextHash\" is null.");
}
this.plaintextHash = setterArg;
}
private @NonNull byte[] ciphertextHash;
public @NonNull byte[] getCiphertextHash() {
return ciphertextHash;
}
public void setCiphertextHash(@NonNull byte[] setterArg) {
if (setterArg == null) {
throw new IllegalStateException("Nonnull field \"ciphertextHash\" is null.");
}
this.ciphertextHash = setterArg;
}
/** Constructor is non-public to enforce null safety; use Builder. */
CryptographyResult() {}
public static final class Builder {
private @Nullable byte[] plaintextHash;
public @NonNull Builder setPlaintextHash(@NonNull byte[] setterArg) {
this.plaintextHash = setterArg;
return this;
}
private @Nullable byte[] ciphertextHash;
public @NonNull Builder setCiphertextHash(@NonNull byte[] setterArg) {
this.ciphertextHash = setterArg;
return this;
}
public @NonNull CryptographyResult build() {
CryptographyResult pigeonReturn = new CryptographyResult();
pigeonReturn.setPlaintextHash(plaintextHash);
pigeonReturn.setCiphertextHash(ciphertextHash);
return pigeonReturn;
}
}
@NonNull
ArrayList<Object> toList() {
ArrayList<Object> toListResult = new ArrayList<Object>(2);
toListResult.add(plaintextHash);
toListResult.add(ciphertextHash);
return toListResult;
}
static @NonNull CryptographyResult fromList(@NonNull ArrayList<Object> list) {
CryptographyResult pigeonResult = new CryptographyResult();
Object plaintextHash = list.get(0);
pigeonResult.setPlaintextHash((byte[]) plaintextHash);
Object ciphertextHash = list.get(1);
pigeonResult.setCiphertextHash((byte[]) ciphertextHash);
return pigeonResult;
}
}
public interface Result<T> {
@SuppressWarnings("UnknownNullness")
void success(T result);
void error(@NonNull Throwable error);
}
private static class MoxplatformApiCodec extends StandardMessageCodec {
public static final MoxplatformApiCodec INSTANCE = new MoxplatformApiCodec();
@@ -913,16 +1005,18 @@ public class Api {
protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) {
switch (type) {
case (byte) 128:
return MessagingNotification.fromList((ArrayList<Object>) readValue(buffer));
return CryptographyResult.fromList((ArrayList<Object>) readValue(buffer));
case (byte) 129:
return NotificationEvent.fromList((ArrayList<Object>) readValue(buffer));
return MessagingNotification.fromList((ArrayList<Object>) readValue(buffer));
case (byte) 130:
return NotificationI18nData.fromList((ArrayList<Object>) readValue(buffer));
return NotificationEvent.fromList((ArrayList<Object>) readValue(buffer));
case (byte) 131:
return NotificationMessage.fromList((ArrayList<Object>) readValue(buffer));
return NotificationI18nData.fromList((ArrayList<Object>) readValue(buffer));
case (byte) 132:
return NotificationMessageContent.fromList((ArrayList<Object>) readValue(buffer));
return NotificationMessage.fromList((ArrayList<Object>) readValue(buffer));
case (byte) 133:
return NotificationMessageContent.fromList((ArrayList<Object>) readValue(buffer));
case (byte) 134:
return RegularNotification.fromList((ArrayList<Object>) readValue(buffer));
default:
return super.readValueOfType(type, buffer);
@@ -931,23 +1025,26 @@ public class Api {
@Override
protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) {
if (value instanceof MessagingNotification) {
if (value instanceof CryptographyResult) {
stream.write(128);
writeValue(stream, ((CryptographyResult) value).toList());
} else if (value instanceof MessagingNotification) {
stream.write(129);
writeValue(stream, ((MessagingNotification) value).toList());
} else if (value instanceof NotificationEvent) {
stream.write(129);
stream.write(130);
writeValue(stream, ((NotificationEvent) value).toList());
} else if (value instanceof NotificationI18nData) {
stream.write(130);
stream.write(131);
writeValue(stream, ((NotificationI18nData) value).toList());
} else if (value instanceof NotificationMessage) {
stream.write(131);
stream.write(132);
writeValue(stream, ((NotificationMessage) value).toList());
} else if (value instanceof NotificationMessageContent) {
stream.write(132);
stream.write(133);
writeValue(stream, ((NotificationMessageContent) value).toList());
} else if (value instanceof RegularNotification) {
stream.write(133);
stream.write(134);
writeValue(stream, ((RegularNotification) value).toList());
} else {
super.writeValue(stream, value);
@@ -957,7 +1054,7 @@ public class Api {
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
public interface MoxplatformApi {
/** Notification APIs */
void createNotificationChannel(@NonNull String title, @NonNull String description, @NonNull String id, @NonNull Boolean urgent);
void showMessagingNotification(@NonNull MessagingNotification notification);
@@ -969,13 +1066,19 @@ public class Api {
void setNotificationSelfAvatar(@NonNull String path);
void setNotificationI18n(@NonNull NotificationI18nData data);
/** Platform APIs */
@NonNull
String getPersistentDataPath();
@NonNull
String getCacheDataPath();
/** Cryptography APIs */
void encryptFile(@NonNull String sourcePath, @NonNull String destPath, @NonNull byte[] key, @NonNull byte[] iv, @NonNull CipherAlgorithm algorithm, @NonNull String hashSpec, @NonNull Result<CryptographyResult> result);
void decryptFile(@NonNull String sourcePath, @NonNull String destPath, @NonNull byte[] key, @NonNull byte[] iv, @NonNull CipherAlgorithm algorithm, @NonNull String hashSpec, @NonNull Result<CryptographyResult> result);
void hashFile(@NonNull String sourcePath, @NonNull String hashSpec, @NonNull Result<byte[]> result);
/** Stubs */
void eventStub(@NonNull NotificationEvent event);
/** The codec used by MoxplatformApi. */
@@ -1175,6 +1278,104 @@ public class Api {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.encryptFile", getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
ArrayList<Object> args = (ArrayList<Object>) message;
String sourcePathArg = (String) args.get(0);
String destPathArg = (String) args.get(1);
byte[] keyArg = (byte[]) args.get(2);
byte[] ivArg = (byte[]) args.get(3);
CipherAlgorithm algorithmArg = args.get(4) == null ? null : CipherAlgorithm.values()[(int) args.get(4)];
String hashSpecArg = (String) args.get(5);
Result<CryptographyResult> resultCallback =
new Result<CryptographyResult>() {
public void success(CryptographyResult result) {
wrapped.add(0, result);
reply.reply(wrapped);
}
public void error(Throwable error) {
ArrayList<Object> wrappedError = wrapError(error);
reply.reply(wrappedError);
}
};
api.encryptFile(sourcePathArg, destPathArg, keyArg, ivArg, algorithmArg, hashSpecArg, resultCallback);
});
} else {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.decryptFile", getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
ArrayList<Object> args = (ArrayList<Object>) message;
String sourcePathArg = (String) args.get(0);
String destPathArg = (String) args.get(1);
byte[] keyArg = (byte[]) args.get(2);
byte[] ivArg = (byte[]) args.get(3);
CipherAlgorithm algorithmArg = args.get(4) == null ? null : CipherAlgorithm.values()[(int) args.get(4)];
String hashSpecArg = (String) args.get(5);
Result<CryptographyResult> resultCallback =
new Result<CryptographyResult>() {
public void success(CryptographyResult result) {
wrapped.add(0, result);
reply.reply(wrapped);
}
public void error(Throwable error) {
ArrayList<Object> wrappedError = wrapError(error);
reply.reply(wrappedError);
}
};
api.decryptFile(sourcePathArg, destPathArg, keyArg, ivArg, algorithmArg, hashSpecArg, resultCallback);
});
} else {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger, "dev.flutter.pigeon.moxplatform_platform_interface.MoxplatformApi.hashFile", getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
ArrayList<Object> args = (ArrayList<Object>) message;
String sourcePathArg = (String) args.get(0);
String hashSpecArg = (String) args.get(1);
Result<byte[]> resultCallback =
new Result<byte[]>() {
public void success(byte[] result) {
wrapped.add(0, result);
reply.reply(wrapped);
}
public void error(Throwable error) {
ArrayList<Object> wrappedError = wrapError(error);
reply.reply(wrappedError);
}
};
api.hashFile(sourcePathArg, hashSpecArg, resultCallback);
});
} else {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(

View File

@@ -1,6 +1,8 @@
package me.polynom.moxplatform_android
import android.util.Log
import me.polynom.moxplatform_android.Api.CipherAlgorithm
import me.polynom.moxplatform_android.Api.CryptographyResult
import java.io.FileInputStream
import java.io.FileOutputStream
@@ -10,6 +12,7 @@ import javax.crypto.Cipher
import javax.crypto.CipherOutputStream
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.concurrent.thread
// A FileOutputStream that continuously hashes whatever it writes to the file.
private class HashedFileOutputStream(name: String, hashAlgorithm: String) : FileOutputStream(name) {
@@ -30,115 +33,126 @@ private class HashedFileOutputStream(name: String, hashAlgorithm: String) : File
}
}
fun getCipherSpecFromInteger(algorithmType: Int): String {
return when (algorithmType) {
0 -> "AES_128/GCM/NoPadding"
1 -> "AES_256/GCM/NoPadding"
2 -> "AES_256/CBC/PKCS7PADDING"
else -> ""
fun getCipherSpecFromInteger(algorithm: CipherAlgorithm): String {
return when (algorithm) {
CipherAlgorithm.AES128GCM_NO_PADDING -> "AES_128/GCM/NoPadding"
CipherAlgorithm.AES256GCM_NO_PADDING -> "AES_256/GCM/NoPadding"
CipherAlgorithm.AES256CBC_PKCS7 -> "AES_256/CBC/PKCS7PADDING"
}
}
// Compute the hash, specified by @algorithm, of the file at path @srcFile. If an exception
// occurs, returns null. If everything went well, returns the raw hash of @srcFile.
fun hashFile(srcFile: String, algorithm: String): ByteArray? {
val buffer = ByteArray(BUFFER_SIZE)
try {
val digest = MessageDigest.getInstance(algorithm)
val fInputStream = FileInputStream(srcFile)
var length: Int
fun hashFile(srcFile: String, algorithm: String, result: Api.Result<ByteArray?>) {
thread(start = true) {
val buffer = ByteArray(BUFFER_SIZE)
try {
val digest = MessageDigest.getInstance(algorithm)
val fInputStream = FileInputStream(srcFile)
var length: Int
while (true) {
length = fInputStream.read()
if (length <= 0) break
while (true) {
length = fInputStream.read()
if (length <= 0) break
// Only update the digest if we read more than 0 bytes
digest.update(buffer, 0, length)
// Only update the digest if we read more than 0 bytes
digest.update(buffer, 0, length)
}
fInputStream.close()
result.success(digest.digest())
} catch (e: Exception) {
Log.e(TAG, "[hashFile]: " + e.stackTraceToString())
result.success(null)
}
fInputStream.close()
return digest.digest()
} catch (e: Exception) {
Log.e(TAG, "[hashFile]: " + e.stackTraceToString())
return null
}
}
// Encrypt the plaintext file at @src to @dest using the secret key @key and the IV @iv. The algorithm is chosen using @cipherAlgorithm. The file is additionally
// hashed before and after encryption using the hash algorithm specified by @hashAlgorithm.
fun encryptAndHash(src: String, dest: String, key: ByteArray, iv: ByteArray, cipherAlgorithm: String, hashAlgorithm: String): HashMap<String, ByteArray>? {
val buffer = ByteArray(BUFFER_SIZE)
val secretKey = SecretKeySpec(key, cipherAlgorithm)
try {
val digest = MessageDigest.getInstance(hashAlgorithm)
val cipher = Cipher.getInstance(cipherAlgorithm)
cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv))
fun encryptAndHash(src: String, dest: String, key: ByteArray, iv: ByteArray, cipherAlgorithm: CipherAlgorithm, hashAlgorithm: String, result: Api.Result<CryptographyResult?>) {
thread(start = true) {
val cipherSpec = getCipherSpecFromInteger(cipherAlgorithm)
val buffer = ByteArray(BUFFER_SIZE)
val secretKey = SecretKeySpec(key, cipherSpec)
try {
val digest = MessageDigest.getInstance(hashAlgorithm)
val cipher = Cipher.getInstance(cipherSpec)
cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv))
val fileInputStream = FileInputStream(src)
val fileOutputStream = HashedFileOutputStream(dest, hashAlgorithm)
val cipherOutputStream = CipherOutputStream(fileOutputStream, cipher)
val fileInputStream = FileInputStream(src)
val fileOutputStream = HashedFileOutputStream(dest, hashAlgorithm)
val cipherOutputStream = CipherOutputStream(fileOutputStream, cipher)
var length: Int
while (true) {
length = fileInputStream.read(buffer)
if (length <= 0) break
var length: Int
while (true) {
length = fileInputStream.read(buffer)
if (length <= 0) break
digest.update(buffer, 0, length)
cipherOutputStream.write(buffer, 0, length)
digest.update(buffer, 0, length)
cipherOutputStream.write(buffer, 0, length)
}
// Flush and close
cipherOutputStream.flush()
cipherOutputStream.close()
fileInputStream.close()
result.success(
CryptographyResult().apply {
plaintextHash = digest.digest()
ciphertextHash = fileOutputStream.digest()
}
)
} catch (e: Exception) {
Log.e(TAG, "[encryptAndHash]: " + e.stackTraceToString())
result.success(null)
}
// Flush and close
cipherOutputStream.flush()
cipherOutputStream.close()
fileInputStream.close()
return hashMapOf(
"plaintextHash" to digest.digest(),
"ciphertextHash" to fileOutputStream.digest(),
)
} catch (e: Exception) {
Log.e(TAG, "[encryptAndHash]: " + e.stackTraceToString())
return null
}
}
// Decrypt the ciphertext file at @src to @dest using the secret key @key and the IV @iv. The algorithm is chosen using @cipherAlgorithm. The file is additionally
// hashed before and after decryption using the hash algorithm specified by @hashAlgorithm.
fun decryptAndHash(src: String, dest: String, key: ByteArray, iv: ByteArray, cipherAlgorithm: String, hashAlgorithm: String): HashMap<String, ByteArray>? {
// Shamelessly stolen from https://github.com/hugo-pcl/native-crypto-flutter/pull/3
val buffer = ByteArray(BUFFER_SIZE)
val secretKey = SecretKeySpec(key, cipherAlgorithm)
try {
val digest = MessageDigest.getInstance(hashAlgorithm)
val cipher = Cipher.getInstance(cipherAlgorithm)
cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv))
fun decryptAndHash(src: String, dest: String, key: ByteArray, iv: ByteArray, cipherAlgorithm: CipherAlgorithm, hashAlgorithm: String, result: Api.Result<CryptographyResult?>) {
thread(start = true) {
val cipherSpec = getCipherSpecFromInteger(cipherAlgorithm)
// Shamelessly stolen from https://github.com/hugo-pcl/native-crypto-flutter/pull/3
val buffer = ByteArray(BUFFER_SIZE)
val secretKey = SecretKeySpec(key, cipherSpec)
try {
val digest = MessageDigest.getInstance(hashAlgorithm)
val cipher = Cipher.getInstance(cipherSpec)
cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv))
val fileInputStream = FileInputStream(src)
val fileOutputStream = HashedFileOutputStream(dest, hashAlgorithm)
val cipherOutputStream = CipherOutputStream(fileOutputStream, cipher)
val fileInputStream = FileInputStream(src)
val fileOutputStream = HashedFileOutputStream(dest, hashAlgorithm)
val cipherOutputStream = CipherOutputStream(fileOutputStream, cipher)
// Read, decrypt, and hash until we read 0 bytes
var length: Int
while (true) {
length = fileInputStream.read(buffer)
if (length <= 0) break
// Read, decrypt, and hash until we read 0 bytes
var length: Int
while (true) {
length = fileInputStream.read(buffer)
if (length <= 0) break
digest.update(buffer, 0, length)
cipherOutputStream.write(buffer, 0, length)
digest.update(buffer, 0, length)
cipherOutputStream.write(buffer, 0, length)
}
// Flush
cipherOutputStream.flush()
cipherOutputStream.close()
fileInputStream.close()
result.success(
CryptographyResult().apply {
plaintextHash = digest.digest()
ciphertextHash = fileOutputStream.digest()
}
)
} catch (e: Exception) {
Log.e(TAG, "[hashAndDecrypt]: " + e.stackTraceToString())
result.success(null)
}
// Flush
cipherOutputStream.flush()
cipherOutputStream.close()
fileInputStream.close()
return hashMapOf(
"plaintextHash" to digest.digest(),
"ciphertextHash" to fileOutputStream.digest(),
)
} catch (e: Exception) {
Log.e(TAG, "[hashAndDecrypt]: " + e.stackTraceToString())
return null
}
}

View File

@@ -40,7 +40,7 @@ import io.flutter.plugin.common.JSONMethodCodec;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;
public class MoxplatformAndroidPlugin extends BroadcastReceiver implements FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler, ServiceAware, MoxplatformApi {
public class MoxplatformAndroidPlugin extends BroadcastReceiver implements FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler, ServiceAware, MoxplatformApi {
public static final String entrypointKey = "entrypoint_handle";
public static final String extraDataKey = "extra_data";
private static final String autoStartAtBootKey = "auto_start_at_boot";
@@ -166,53 +166,6 @@ import kotlin.jvm.functions.Function1;
}
result.success(true);
break;
case "encryptFile":
Thread encryptionThread = new Thread(new Runnable() {
@Override
public void run() {
ArrayList args = (ArrayList) call.arguments;
String src = (String) args.get(0);
String dest = (String) args.get(1);
byte[] key = (byte[]) args.get(2);
byte[] iv = (byte[]) args.get(3);
int algorithm = (int) args.get(4);
String hashSpec = (String) args.get(5);
result.success(encryptAndHash(src, dest, key, iv, getCipherSpecFromInteger(algorithm), hashSpec));
}
});
encryptionThread.start();
break;
case "decryptFile":
Thread decryptionThread = new Thread(new Runnable() {
@Override
public void run() {
ArrayList args = (ArrayList) call.arguments;
String src = (String) args.get(0);
String dest = (String) args.get(1);
byte[] key = (byte[]) args.get(2);
byte[] iv = (byte[]) args.get(3);
int algorithm = (int) args.get(4);
String hashSpec = (String) args.get(5);
result.success(decryptAndHash(src, dest, key, iv, getCipherSpecFromInteger(algorithm), hashSpec));
}
});
decryptionThread.start();
break;
case "hashFile":
Thread hashingThread = new Thread(new Runnable() {
@Override
public void run() {
ArrayList args = (ArrayList) call.arguments;
String src = (String) args.get(0);
String hashSpec = (String) args.get(1);
result.success(hashFile(src, hashSpec));
}
});
hashingThread.start();
break;
case "recordSentMessage":
ArrayList rargs = (ArrayList) call.arguments;
recordSentMessage(context, (String) rargs.get(0), (String) rargs.get(1), (String) rargs.get(2), (int) rargs.get(3));
@@ -308,6 +261,37 @@ import kotlin.jvm.functions.Function1;
return context.getCacheDir().getPath();
}
@Override
public void encryptFile(@NonNull String sourcePath, @NonNull String destPath, @NonNull byte[] key, @NonNull byte[] iv, @NonNull CipherAlgorithm algorithm, @NonNull String hashSpec, @NonNull Api.Result<CryptographyResult> result) {
CryptoKt.encryptAndHash(
sourcePath,
destPath,
key,
iv,
algorithm,
hashSpec,
result
);
}
@Override
public void decryptFile(@NonNull String sourcePath, @NonNull String destPath, @NonNull byte[] key, @NonNull byte[] iv, @NonNull CipherAlgorithm algorithm, @NonNull String hashSpec, @NonNull Api.Result<CryptographyResult> result) {
CryptoKt.decryptAndHash(
sourcePath,
destPath,
key,
iv,
algorithm,
hashSpec,
result
);
}
@Override
public void hashFile(@NonNull String sourcePath, @NonNull String hashSpec, @NonNull Api.Result<byte[]> result) {
CryptoKt.hashFile(sourcePath, hashSpec, result);
}
@Override
public void eventStub(@NonNull NotificationEvent event) {
// Stub to trick pigeon into