diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 856146e..367f019 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -319,6 +319,9 @@ "nowTimestamp": "Now timestamp", "hosts": "Hosts", "toAddress": "To Address", + "encrypt": "Encrypt", + "decrypt": "Decrypt", + "cipher": "Cipher", "appUpdateCheckVersion": "Check for Updates", "appUpdateNotAvailableMsg": "Already Using The Latest Version", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 6cbac32..5e631b7 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -318,6 +318,9 @@ "nowTimestamp": "当前时间戳(秒)", "hosts": "Hosts 映射", "toAddress": "映射地址", + "encrypt": "加密", + "decrypt": "解密", + "cipher": "加解密", "appUpdateCheckVersion": "检查更新", "appUpdateNotAvailableMsg": "已是最新版本", diff --git a/lib/ui/component/multi_window.dart b/lib/ui/component/multi_window.dart index 462b6c7..246638f 100644 --- a/lib/ui/component/multi_window.dart +++ b/lib/ui/component/multi_window.dart @@ -28,22 +28,23 @@ import 'package:proxypin/network/components/manager/rewrite_rule.dart'; import 'package:proxypin/network/components/manager/script_manager.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/util/logger.dart'; -import 'package:proxypin/ui/component/toolbox/cert_hash.dart'; import 'package:proxypin/ui/component/device.dart'; -import 'package:proxypin/ui/component/toolbox/encoder.dart'; -import 'package:proxypin/ui/component/toolbox/js_run.dart'; -import 'package:proxypin/ui/component/toolbox/qr_code_page.dart'; -import 'package:proxypin/ui/component/toolbox/regexp.dart'; import 'package:proxypin/ui/component/utils.dart'; import 'package:proxypin/ui/content/body.dart'; import 'package:proxypin/ui/desktop/request/request_editor.dart'; import 'package:proxypin/ui/desktop/toolbar/setting/request_rewrite.dart'; import 'package:proxypin/ui/desktop/toolbar/setting/script.dart'; +import 'package:proxypin/ui/toolbox/aes_page.dart'; import 'package:proxypin/utils/platform.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:window_manager/window_manager.dart'; -import 'toolbox/timestamp.dart'; +import '../toolbox/cert_hash.dart'; +import '../toolbox/encoder.dart'; +import '../toolbox/js_run.dart'; +import '../toolbox/qr_code_page.dart'; +import '../toolbox/regexp.dart'; +import '../toolbox/timestamp.dart'; bool isMultiWindow = false; @@ -100,6 +101,10 @@ Widget multiWindow(int windowId, Map argument) { return TimestampPage(windowId: windowId); } + if (argument['name'] == 'AesPage') { + return AesPage(); + } + //脚本日志 if (argument['name'] == 'ScriptConsoleWidget') { return ScriptConsoleWidget(windowId: windowId); diff --git a/lib/ui/content/body.dart b/lib/ui/content/body.dart index ffa20c7..c71fcb7 100644 --- a/lib/ui/content/body.dart +++ b/lib/ui/content/body.dart @@ -28,7 +28,6 @@ import 'package:proxypin/network/components/manager/rewrite_rule.dart'; import 'package:proxypin/network/http/content_type.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/util/logger.dart'; -import 'package:proxypin/ui/component/toolbox/encoder.dart'; import 'package:proxypin/ui/component/json/json_viewer.dart'; import 'package:proxypin/ui/component/json/theme.dart'; import 'package:proxypin/ui/component/multi_window.dart'; @@ -41,6 +40,7 @@ import 'package:proxypin/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; import '../component/json/json_text.dart'; +import '../toolbox/encoder.dart'; ///请求响应的body部分 ///@Author wanghongen diff --git a/lib/ui/desktop/desktop.dart b/lib/ui/desktop/desktop.dart index 5afd810..3a02cb6 100644 --- a/lib/ui/desktop/desktop.dart +++ b/lib/ui/desktop/desktop.dart @@ -24,7 +24,6 @@ import 'package:proxypin/network/channel/channel_context.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/http/websocket.dart'; import 'package:proxypin/ui/component/memory_cleanup.dart'; -import 'package:proxypin/ui/component/toolbox/toolbox.dart'; import 'package:proxypin/ui/component/widgets.dart'; import 'package:proxypin/ui/configuration.dart'; import 'package:proxypin/ui/content/panel.dart'; @@ -37,6 +36,7 @@ import 'package:proxypin/utils/listenable_list.dart'; import '../app_update/app_update_repository.dart'; import '../component/split_view.dart'; +import '../toolbox/toolbox.dart'; /// @author wanghongen /// 2023/10/8 diff --git a/lib/ui/mobile/menu/drawer.dart b/lib/ui/mobile/menu/drawer.dart index 3597102..459882b 100644 --- a/lib/ui/mobile/menu/drawer.dart +++ b/lib/ui/mobile/menu/drawer.dart @@ -23,7 +23,7 @@ import 'package:proxypin/network/components/manager/request_block_manager.dart'; import 'package:proxypin/network/components/manager/request_rewrite_manager.dart'; import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/storage/histories.dart'; -import 'package:proxypin/ui/component/toolbox/toolbox.dart'; +import 'package:proxypin/ui/toolbox/toolbox.dart'; import 'package:proxypin/ui/component/utils.dart'; import 'package:proxypin/ui/configuration.dart'; import 'package:proxypin/ui/mobile/setting/preference.dart'; diff --git a/lib/ui/mobile/mobile.dart b/lib/ui/mobile/mobile.dart index c0060c1..e1450d3 100644 --- a/lib/ui/mobile/mobile.dart +++ b/lib/ui/mobile/mobile.dart @@ -33,7 +33,7 @@ import 'package:proxypin/network/http/http.dart'; import 'package:proxypin/network/http/websocket.dart'; import 'package:proxypin/network/http/http_client.dart'; import 'package:proxypin/ui/component/memory_cleanup.dart'; -import 'package:proxypin/ui/component/toolbox/toolbox.dart'; +import 'package:proxypin/ui/toolbox/toolbox.dart'; import 'package:proxypin/ui/configuration.dart'; import 'package:proxypin/ui/content/panel.dart'; import 'package:proxypin/ui/launch/launch.dart'; diff --git a/lib/ui/toolbox/aes_page.dart b/lib/ui/toolbox/aes_page.dart new file mode 100644 index 0000000..113d81e --- /dev/null +++ b/lib/ui/toolbox/aes_page.dart @@ -0,0 +1,222 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_toastr/flutter_toastr.dart'; +import 'package:proxypin/network/util/logger.dart'; + +import '../component/buttons.dart'; +import '../component/text_field.dart'; +import '../../utils/aes.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class AesPage extends StatefulWidget { + const AesPage({super.key}); + + @override + State createState() => _AesWidgetState(); +} + +class _AesWidgetState extends State { + final TextEditingController inputController = TextEditingController(); + final TextEditingController outputController = TextEditingController(); + final TextEditingController keyController = TextEditingController(); + final TextEditingController ivController = TextEditingController(); + + String selectedMode = 'ECB'; + String selectedPadding = 'PKCS7'; + int selectedKeyLength = 128; + final List modes = ['ECB', 'CBC']; + final List paddingModes = ['PKCS7', 'ZeroPadding']; + final List keyLengths = [128, 192, 256]; + + void encryptText() { + try { + final input = Uint8List.fromList(utf8.encode(inputController.text)); + final encrypted = AesUtils.encrypt(input, + key: keyController.text, + mode: selectedMode, + iv: ivController.text, + keyLength: selectedKeyLength, + padding: selectedPadding); + outputController.text = base64.encode(encrypted); + } catch (e) { + logger.e("Encryption error: $e"); + FlutterToastr.show("Encryption failed", context, duration: 3, backgroundColor: Colors.red); + } + } + + void decryptText() { + try { + final input = base64.decode(inputController.text); + final decrypted = AesUtils.decrypt(input, + key: keyController.text, + mode: selectedMode, + iv: ivController.text, + keyLength: selectedKeyLength, + padding: selectedPadding); + outputController.text = utf8.decode(decrypted); + } catch (e) { + outputController.text = ""; + logger.e("Decryption error: $e"); + FlutterToastr.show("Decryption failed", context, duration: 3, backgroundColor: Colors.red); + } + } + + AppLocalizations get localizations => AppLocalizations.of(context)!; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("AES", style: TextStyle(fontSize: 16)), centerTitle: true), + body: Padding( + padding: const EdgeInsets.all(15), + child: ListView(children: [ + const SizedBox(height: 5), + SizedBox( + height: 150, + child: TextField( + controller: inputController, + maxLines: 8, + onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(), + decoration: decoration(context, label: localizations.inputContent))), + const SizedBox(height: 15), + Wrap(spacing: 18, runSpacing: 5, crossAxisAlignment: WrapCrossAlignment.center, children: [ + SizedBox( + width: 120, + child: Row(children: [ + Text("Mode"), + const SizedBox(width: 15), + DropdownButton( + value: selectedMode, + items: modes.map((mode) { + return DropdownMenuItem(value: mode, child: Text(mode)); + }).toList(), + onChanged: (value) { + setState(() { + selectedMode = value!; + }); + }, + ), + ])), + SizedBox( + width: 195, + child: Row(children: [ + Text("Padding"), + const SizedBox(width: 15), + DropdownButton( + value: selectedPadding, + items: paddingModes.map((mode) { + return DropdownMenuItem(value: mode, child: Text(mode)); + }).toList(), + onChanged: (value) { + setState(() { + selectedPadding = value!; + }); + }, + ), + ])), + SizedBox( + width: 190, + child: Row(children: [ + Text("Key Length"), + const SizedBox(width: 15), + DropdownButton( + value: selectedKeyLength, + items: keyLengths.map((length) { + return DropdownMenuItem(value: length, child: Text("$length bits")); + }).toList(), + onChanged: (value) { + setState(() { + selectedKeyLength = value!; + }); + }, + ), + ])) + ]), + const SizedBox(height: 15), + Wrap( + spacing: 18.0, // 主轴方向子组件的间距 + runSpacing: 10.0, // 交叉轴方向子组件的间距 + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + SizedBox( + width: 230, + child: Row(children: [ + const SizedBox(width: 25, child: Text("Key")), + const SizedBox(width: 15), + SizedBox( + width: 180, + height: 45, + child: TextField( + controller: keyController, + maxLength: 32, + onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(), + style: TextStyle(fontSize: 14), + decoration: InputDecoration( + border: const OutlineInputBorder(), + counterText: "", + contentPadding: const EdgeInsets.symmetric(horizontal: 6)))), + ])), + SizedBox( + width: 260, + child: Row(children: [ + const SizedBox(width: 25, child: Text("IV")), + const SizedBox(width: 15), + SizedBox( + width: 180, + height: 45, + child: TextField( + controller: ivController, + maxLength: 32, + onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(), + style: TextStyle(fontSize: 14), + decoration: InputDecoration( + border: const OutlineInputBorder(), + counterText: "", + contentPadding: const EdgeInsets.symmetric(horizontal: 6)))), + ])), + ]), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FilledButton( + style: ButtonStyle( + shape: WidgetStateProperty.all( + RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)))), + onPressed: encryptText, + child: Text(localizations.encrypt)), + const SizedBox(width: 60), + FilledButton( + style: ButtonStyle( + shape: WidgetStateProperty.all( + RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)))), + onPressed: decryptText, + child: Text(localizations.decrypt)), + ], + ), + const SizedBox(height: 5), + Text(localizations.output), + const SizedBox(height: 5), + TextFormField( + controller: outputController, + readOnly: true, + minLines: 5, + maxLines: 10, + decoration: const InputDecoration(border: OutlineInputBorder()), + ), + const SizedBox(height: 10), + FilledButton.icon( + style: Buttons.buttonStyle, + onPressed: () { + Clipboard.setData(ClipboardData(text: outputController.text)); + FlutterToastr.show(localizations.copied, context); + }, + icon: const Icon(Icons.copy), + label: Text(localizations.copy), + ), + ]), + ), + ); + } +} diff --git a/lib/ui/component/toolbox/cert_hash.dart b/lib/ui/toolbox/cert_hash.dart similarity index 100% rename from lib/ui/component/toolbox/cert_hash.dart rename to lib/ui/toolbox/cert_hash.dart diff --git a/lib/ui/component/toolbox/encoder.dart b/lib/ui/toolbox/encoder.dart similarity index 100% rename from lib/ui/component/toolbox/encoder.dart rename to lib/ui/toolbox/encoder.dart diff --git a/lib/ui/component/toolbox/js_run.dart b/lib/ui/toolbox/js_run.dart similarity index 100% rename from lib/ui/component/toolbox/js_run.dart rename to lib/ui/toolbox/js_run.dart diff --git a/lib/ui/component/toolbox/qr_code_page.dart b/lib/ui/toolbox/qr_code_page.dart similarity index 100% rename from lib/ui/component/toolbox/qr_code_page.dart rename to lib/ui/toolbox/qr_code_page.dart diff --git a/lib/ui/component/toolbox/regexp.dart b/lib/ui/toolbox/regexp.dart similarity index 100% rename from lib/ui/component/toolbox/regexp.dart rename to lib/ui/toolbox/regexp.dart diff --git a/lib/ui/component/toolbox/timestamp.dart b/lib/ui/toolbox/timestamp.dart similarity index 99% rename from lib/ui/component/toolbox/timestamp.dart rename to lib/ui/toolbox/timestamp.dart index d27db1f..fd525e6 100644 --- a/lib/ui/component/toolbox/timestamp.dart +++ b/lib/ui/toolbox/timestamp.dart @@ -9,7 +9,8 @@ import 'package:proxypin/ui/component/buttons.dart'; import 'package:proxypin/utils/lang.dart'; import 'package:proxypin/utils/platform.dart'; -import '../text_field.dart'; +import '../component/text_field.dart'; + /// Timestamp page /// @author Hongen Wang diff --git a/lib/ui/component/toolbox/toolbox.dart b/lib/ui/toolbox/toolbox.dart similarity index 85% rename from lib/ui/component/toolbox/toolbox.dart rename to lib/ui/toolbox/toolbox.dart index c57327b..3b0be54 100644 --- a/lib/ui/component/toolbox/toolbox.dart +++ b/lib/ui/toolbox/toolbox.dart @@ -5,17 +5,19 @@ import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:proxypin/network/bin/server.dart'; -import 'package:proxypin/ui/component/toolbox/cert_hash.dart'; -import 'package:proxypin/ui/component/toolbox/encoder.dart'; -import 'package:proxypin/ui/component/toolbox/js_run.dart'; import 'package:proxypin/ui/component/multi_window.dart'; -import 'package:proxypin/ui/component/toolbox/qr_code_page.dart'; -import 'package:proxypin/ui/component/toolbox/regexp.dart'; -import 'package:proxypin/ui/component/toolbox/timestamp.dart'; import 'package:proxypin/ui/mobile/request/request_editor.dart'; +import 'package:proxypin/ui/toolbox/qr_code_page.dart'; +import 'package:proxypin/ui/toolbox/regexp.dart'; +import 'package:proxypin/ui/toolbox/timestamp.dart'; import 'package:proxypin/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; +import 'aes_page.dart'; +import 'cert_hash.dart'; +import 'encoder.dart'; +import 'js_run.dart'; + class Toolbox extends StatefulWidget { final ProxyServer? proxyServer; @@ -79,7 +81,7 @@ class _ToolboxState extends State { ]), const Divider(thickness: 0.3), Text(localizations.encode, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - Row( + Wrap( children: [ InkWell( onTap: () => encodeWindow(EncoderType.url, context), @@ -106,11 +108,31 @@ class _ToolboxState extends State { onTap: () => encodeWindow(EncoderType.md5, context), child: Container( padding: const EdgeInsets.all(10), - child: const Column(children: [Icon(Icons.enhanced_encryption), SizedBox(height: 3), Text('MD5')]), + child: const Column(children: [ + Icon(IconData(0x23, fontFamily: 'MaterialIcons')), // “#” + SizedBox(height: 3), + Text('MD5') + ]), )), ], ), const Divider(thickness: 0.3), + Text("加解密", style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), + Wrap(children: [ + InkWell( + onTap: () { + if (Platforms.isMobile()) { + Navigator.of(context).push(MaterialPageRoute(builder: (context) => const AesPage())); + return; + } + MultiWindow.openWindow("AES", "AesPage", size: const Size(700, 660)); + }, + child: Container( + padding: const EdgeInsets.all(10), + child: const Column(children: [Icon(Icons.enhanced_encryption), SizedBox(height: 3), Text('AES')]), + )), + ]), + const Divider(thickness: 0.3), Text(localizations.other, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), Wrap( children: [ diff --git a/lib/utils/aes.dart b/lib/utils/aes.dart new file mode 100644 index 0000000..dbe5fe8 --- /dev/null +++ b/lib/utils/aes.dart @@ -0,0 +1,47 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:pointycastle/export.dart'; + +class AesUtils { + static Uint8List encrypt(Uint8List input, + {required String key, required int keyLength, required String mode, required String padding, String? iv}) { + return process(input, true, key: key, keyLength: keyLength, mode: mode, padding: padding, iv: iv); + } + + static Uint8List decrypt(Uint8List input, + {required String key, required int keyLength, required String mode, required String padding, String? iv}) { + var data = process(input, false, key: key, keyLength: keyLength, mode: mode, padding: padding, iv: iv); + // 移除填充零字节 + if (padding == 'ZeroPadding') { + int lastNonZeroIndex = data.lastIndexWhere((byte) => byte != 0); + data = data.sublist(0, lastNonZeroIndex + 1); + } + return data; + } + + static Uint8List process(Uint8List input, bool isEncrypt, + {required String key, required int keyLength, required String mode, required String padding, String? iv}) { + int keySize = keyLength ~/ 8; + + final aesKey = Uint8List.fromList(utf8.encode(key.padRight(keySize, '0'))); + final aesIv = mode == 'CBC' ? Uint8List.fromList(utf8.encode(iv!.padRight(keySize, '0'))) : null; + + BlockCipher cipher = BlockCipher(mode == 'CBC' ? 'AES/CBC' : 'AES/ECB'); + CipherParameters params = + aesIv == null ? KeyParameter(aesKey) : ParametersWithIV(KeyParameter(aesKey), aesIv); + + if (padding == 'PKCS7') { + cipher = PaddedBlockCipherImpl(PKCS7Padding(), cipher); + params = PaddedBlockCipherParameters(params, null); + } + + // 检查输入长度是否为块大小的整数倍 + if (input.length % cipher.blockSize != 0 && padding == 'ZeroPadding') { + input = Uint8List.fromList(input + Uint8List(cipher.blockSize - (input.length % cipher.blockSize))); + } + + cipher.init(isEncrypt, params); + return cipher.process(input); + } +}