From 563b0386d651cdd7e5ae909daa2bb8268fde016a Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Fri, 21 Jul 2023 17:30:59 +0200 Subject: [PATCH] feat: Improve code quality of the cryptography --- example/lib/main.dart | 90 ++--------- .../polynom/moxplatform_android/Constants.kt | 26 +++ .../me/polynom/moxplatform_android/Crypto.kt | 144 +++++++++++++++++ .../HashedFileOutputStream.java | 34 ---- .../MoxplatformAndroidPlugin.java | 149 +++--------------- .../lib/src/crypto_android.dart | 8 +- 6 files changed, 212 insertions(+), 239 deletions(-) create mode 100644 packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Constants.kt create mode 100644 packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Crypto.kt delete mode 100644 packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/HashedFileOutputStream.java diff --git a/example/lib/main.dart b/example/lib/main.dart index fa81818..592cb3a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -12,50 +12,22 @@ void main() { class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo', + title: 'Moxplatform Demo', theme: ThemeData( - // This is the theme of your application. - // - // Try running your application with "flutter run". You'll see the - // application has a blue toolbar. Then, without quitting the app, try - // changing the primarySwatch below to Colors.green and then invoke - // "hot reload" (press "r" in the console where you ran "flutter run", - // or simply save your changes to "hot reload" in a Flutter IDE). - // Notice that the counter didn't reset back to zero; the application - // is not restarted. primarySwatch: Colors.blue, ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), + home: const MyHomePage(), ); } } -class MyHomePage extends StatefulWidget { - const MyHomePage({Key? key, required this.title}) : super(key: key); +class MyHomePage extends StatelessWidget { + const MyHomePage({super.key}); - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - int _counter = 0; - - Future _incrementCounter() async { + Future _cryptoTest() async { final result = await FilePicker.platform.pickFiles(); if (result == null) { return; @@ -65,7 +37,7 @@ class _MyHomePageState extends State { final path = result.files.single.path; final enc = await MoxplatformPlugin.crypto.encryptFile( path!, - path + '.enc', + '$path.enc', Uint8List.fromList(List.filled(32, 1)), Uint8List.fromList(List.filled(16, 2)), CipherAlgorithm.aes256CbcPkcs7, @@ -76,13 +48,13 @@ class _MyHomePageState extends State { final diff = end.millisecondsSinceEpoch - start.millisecondsSinceEpoch; print('TIME: ${diff / 1000}s'); print('DONE (${enc != null})'); - final lengthEnc = await File(path + ".enc").length(); + final lengthEnc = await File('$path.enc').length(); final lengthOrig = await File(path).length(); print('Encrypted file is $lengthEnc Bytes large (Orig $lengthOrig)'); await MoxplatformPlugin.crypto.decryptFile( - path + '.enc', - path + '.dec', + '$path.enc', + '$path.dec', Uint8List.fromList(List.filled(32, 1)), Uint8List.fromList(List.filled(16, 2)), CipherAlgorithm.aes256CbcPkcs7, @@ -90,69 +62,41 @@ class _MyHomePageState extends State { ); print('DONE'); - final lengthDec = await File(path + ".dec").length(); + final lengthDec = await File('$path.dec').length(); print('Decrypted file is $lengthDec Bytes large (Orig $lengthOrig)'); } @override Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. return Scaffold( appBar: AppBar( - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), + title: const Text('Moxplatform Demo'), ), body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Invoke "debug painting" (press "p" in the console, choose the - // "Toggle Debug Paint" action from the Flutter Inspector in Android - // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) - // to see the wireframe for each widget. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text( - 'You have pushed the button this many times:', + ElevatedButton( + onPressed: _cryptoTest, + child: const Text('Test cryptography'), ), - Text( - '$_counter', - style: Theme.of(context).textTheme.headline4, - ), - ElevatedButton( onPressed: () { MoxplatformPlugin.contacts.recordSentMessage('Hallo', 'Welt'); }, - child: Text('Test recordSentMessage (no fallback)'), + child: const Text('Test recordSentMessage (no fallback)'), ), ElevatedButton( onPressed: () { MoxplatformPlugin.contacts.recordSentMessage('Person', 'Person', fallbackIcon: FallbackIconType.person); }, - child: Text('Test recordSentMessage (person fallback)'), + child: const Text('Test recordSentMessage (person fallback)'), ), ElevatedButton( onPressed: () { MoxplatformPlugin.contacts.recordSentMessage('Notes', 'Notes', fallbackIcon: FallbackIconType.notes); }, - child: Text('Test recordSentMessage (notes fallback)'), + child: const Text('Test recordSentMessage (notes fallback)'), ), ], ), diff --git a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Constants.kt b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Constants.kt new file mode 100644 index 0000000..21ac98b --- /dev/null +++ b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Constants.kt @@ -0,0 +1,26 @@ +package me.polynom.moxplatform_android + +// The tag we use for logging. +const val TAG = "Moxplatform" + +// The size of the buffer to hashing, encryption, and decryption in bytes. +const val BUFFER_SIZE = 8096 + +// TODO: Maybe try again to rewrite the entire plugin in Kotlin +//const val METHOD_CHANNEL_KEY = "me.polynom.moxplatform_android" +//const val BACKGROUND_METHOD_CHANNEL_KEY = METHOD_CHANNEL_KEY + "_bg" + +// https://github.com/ekasetiawans/flutter_background_service/blob/e427f3b70138ec26f9671c2617f9061f25eade6f/packages/flutter_background_service_android/android/src/main/java/id/flutter/flutter_background_service/BootReceiver.java#L20 +//const val WAKELOCK_DURATION = 10*60*1000L; + +// The name of the wakelock the background service manager holds. +//const val SERVICE_WAKELOCK_NAME = "BackgroundService.Lock" + +//const val DATA_RECEIVER_METHOD_NAME = "dataReceived" + +// Shared preferences keys +//const val SHARED_PREFERENCES_KEY = "me.polynom.moxplatform_android" +//const val SP_MANUALLY_STOPPED_KEY = "manually_stopped" +//const val SP_ENTRYPOINT_KEY = "entrypoint_handle" +//const val SP_EXTRA_DATA_KEY = "extra_data" +//const val SP_AUTO_START_AT_BOOT_KEY = "auto_start_at_boot" \ No newline at end of file diff --git a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Crypto.kt b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Crypto.kt new file mode 100644 index 0000000..52f011c --- /dev/null +++ b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/Crypto.kt @@ -0,0 +1,144 @@ +package me.polynom.moxplatform_android + +import android.util.Log + +import java.io.FileInputStream +import java.io.FileOutputStream +import java.lang.Exception +import java.security.MessageDigest +import javax.crypto.Cipher +import javax.crypto.CipherOutputStream +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +// A FileOutputStream that continuously hashes whatever it writes to the file. +private 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() + } +} + +fun getCipherSpecFromInteger(algorithmType: Int): String { + return when (algorithmType) { + 0 -> "AES_128/GCM/NoPadding" + 1 -> "AES_256/GCM/NoPadding" + 2 -> "AES_256/CBC/PKCS7PADDING" + else -> "" + } +} + +// 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 + + 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) + } + + 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? { + 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)) + + 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 + + digest.update(buffer, 0, length) + cipherOutputStream.write(buffer, 0, length) + } + + // 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? { + // 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)) + + 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 + + digest.update(buffer, 0, length) + cipherOutputStream.write(buffer, 0, length) + } + + // 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 + } +} \ No newline at end of file diff --git a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/HashedFileOutputStream.java b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/HashedFileOutputStream.java deleted file mode 100644 index 49592df..0000000 --- a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/HashedFileOutputStream.java +++ /dev/null @@ -1,34 +0,0 @@ -package me.polynom.moxplatform_android; - -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -public class HashedFileOutputStream extends FileOutputStream { - public MessageDigest digest; - - public HashedFileOutputStream(String name, String hashSpec) throws FileNotFoundException, NoSuchAlgorithmException { - super(name); - - digest = MessageDigest.getInstance(hashSpec); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - super.write(b, off, len); - - digest.update(b, off, len); - } - - public String getHexHash() { - StringBuffer result = new StringBuffer(); - for (byte b : digest.digest()) result.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1)); - return result.toString(); - } - - public byte[] getHash() { - return digest.digest(); - } -} diff --git a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/MoxplatformAndroidPlugin.java b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/MoxplatformAndroidPlugin.java index 0a17b82..0aa1b77 100644 --- a/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/MoxplatformAndroidPlugin.java +++ b/packages/moxplatform_android/android/src/main/java/me/polynom/moxplatform_android/MoxplatformAndroidPlugin.java @@ -1,6 +1,7 @@ package me.polynom.moxplatform_android; import static me.polynom.moxplatform_android.RecordSentMessageKt.recordSentMessage; +import static me.polynom.moxplatform_android.CryptoKt.*; import android.app.ActivityManager; import android.content.BroadcastReceiver; @@ -8,28 +9,17 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.drawable.Icon; -import android.os.Build; import android.util.Log; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.Person; import androidx.core.content.ContextCompat; -import androidx.core.content.pm.ShortcutInfoCompat; -import androidx.core.content.pm.ShortcutManagerCompat; -import androidx.core.graphics.drawable.IconCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import java.io.FileInputStream; import java.security.MessageDigest; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Set; import javax.crypto.Cipher; import javax.crypto.CipherOutputStream; @@ -169,7 +159,16 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt int algorithm = (int) args.get(4); String hashSpec = (String) args.get(5); - result.success(encryptFile(src, dest, key, iv, algorithm, hashSpec)); + result.success( + encryptAndHash( + src, + dest, + key, + iv, + getCipherSpecFromInteger(algorithm), + hashSpec + ) + ); } }); encryptionThread.start(); @@ -186,7 +185,16 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt int algorithm = (int) args.get(4); String hashSpec = (String) args.get(5); - result.success(decryptFile(src, dest, key, iv, algorithm, hashSpec)); + result.success( + decryptAndHash( + src, + dest, + key, + iv, + getCipherSpecFromInteger(algorithm), + hashSpec + ) + ); } }); decryptionThread.start(); @@ -254,119 +262,4 @@ public class MoxplatformAndroidPlugin extends BroadcastReceiver implements Flutt Log.d(TAG, "Detached from service"); this.service = null; } - - private String getCipherSpecFromInteger(int algorithm) { - switch (algorithm) { - case 0: return "AES_128/GCM/NoPadding"; - case 1: return "AES_256/GCM/NoPadding"; - case 2: return "AES_256/CBC/PKCS7PADDING"; - default: - Log.d(TAG, "INVALID ALGORITHM"); - return ""; - } - } - - public HashMap encryptFile(String src, String dest, byte[] key, byte[] iv, int algorithm, String hashSpec) { - String spec = getCipherSpecFromInteger(algorithm); - if (spec.isEmpty()) { - return null; - } - - // Shamelessly stolen from https://github.com/hugo-pcl/native-crypto-flutter/pull/3 - byte[] buffer = new byte[8096]; - SecretKeySpec sk = new SecretKeySpec(key, spec); - try { - MessageDigest md = MessageDigest.getInstance(hashSpec); - Cipher cipher = Cipher.getInstance(spec); - cipher.init(Cipher.ENCRYPT_MODE, sk, new IvParameterSpec(iv)); - FileInputStream fin = new FileInputStream(src); - HashedFileOutputStream fout = new HashedFileOutputStream(dest, hashSpec); - CipherOutputStream cout = new CipherOutputStream(fout, cipher); - int len = 0; - int bufLen = 0; - while (true) { - len = fin.read(buffer); - if (len != 0 && len > 0) { - md.update(buffer, 0, len); - cout.write(buffer, 0, len); - } else { - break; - } - } - cout.flush(); - cout.close(); - fin.close(); - - return new HashMap() {{ - put("plaintext_hash", md.digest()); - put("ciphertext_hash", fout.getHash()); - }}; - } catch (Exception ex) { - Log.d(TAG, "ENC: " + ex.getMessage()); - return null; - } - } - - public HashMap decryptFile(String src, String dest, byte[] key, byte[] iv, int algorithm, String hashSpec) { - String spec = getCipherSpecFromInteger(algorithm); - if (spec.isEmpty()) { - return null; - } - - // Shamelessly stolen from https://github.com/hugo-pcl/native-crypto-flutter/pull/3 - byte[] buffer = new byte[8096]; - SecretKeySpec sk = new SecretKeySpec(key, spec); - try { - Cipher cipher = Cipher.getInstance(spec); - cipher.init(Cipher.DECRYPT_MODE, sk, new IvParameterSpec(iv)); - FileInputStream fin = new FileInputStream(src); - HashedFileOutputStream fout = new HashedFileOutputStream(dest, hashSpec); - CipherOutputStream cout = new CipherOutputStream(fout, cipher); - MessageDigest md = MessageDigest.getInstance(hashSpec); - Log.d(TAG, "Reading from " + src + ", writing to " + dest); - int len = 0; - while (true) { - len = fin.read(buffer); - if (len != 0 && len > 0) { - cout.write(buffer, 0, len); - md.update(buffer, 0, len); - } else { - break; - } - } - cout.flush(); - cout.close(); - fin.close(); - - return new HashMap() {{ - put("plaintext_hash", md.digest()); - put("ciphertext_hash", fout.getHash()); - }}; - } catch (Exception ex) { - Log.d(TAG, "DEC: " + ex.getMessage()); - return null; - } - } - - public byte[] hashFile(String src, String algorithm) { - byte[] buffer = new byte[8096]; - try { - MessageDigest md = MessageDigest.getInstance(algorithm); - FileInputStream fin = new FileInputStream(src); - int len = 0; - while (true) { - len = fin.read(buffer); - if (len != 0 && len > 0) { - md.update(buffer, 0, len); - } else { - break; - } - } - - return md.digest(); - } catch (Exception ex) { - Log.d(TAG, "Hash: " + ex.getMessage()); - return null; - } - } } diff --git a/packages/moxplatform_android/lib/src/crypto_android.dart b/packages/moxplatform_android/lib/src/crypto_android.dart index 4ea1e00..a82fcd9 100644 --- a/packages/moxplatform_android/lib/src/crypto_android.dart +++ b/packages/moxplatform_android/lib/src/crypto_android.dart @@ -28,8 +28,8 @@ class AndroidCryptographyImplementation extends CryptographyImplementation { // ignore: argument_type_not_assignable final result = Map.from(resultRaw); return CryptographyResult( - result['plaintext_hash']! as Uint8List, - result['ciphertext_hash']! as Uint8List, + result['plaintextHash']! as Uint8List, + result['ciphertextHash']! as Uint8List, ); } @@ -56,8 +56,8 @@ class AndroidCryptographyImplementation extends CryptographyImplementation { // ignore: argument_type_not_assignable final result = Map.from(resultRaw); return CryptographyResult( - result['plaintext_hash']! as Uint8List, - result['ciphertext_hash']! as Uint8List, + result['plaintextHash']! as Uint8List, + result['ciphertextHash']! as Uint8List, ); }