diff --git a/lib/controller/applets/piv.dart b/lib/controller/applets/piv.dart index 707ed39..1a742ca 100644 --- a/lib/controller/applets/piv.dart +++ b/lib/controller/applets/piv.dart @@ -52,6 +52,7 @@ class PivController extends Controller { } var cert = X509Certificate.fromAsn1(o); slotInfo.cert = cert; + slotInfo.certBytes = bytes; } } slots[slot] = slotInfo; diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index ffb1cfe..d69f497 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -203,7 +203,13 @@ class MessageLookup extends MessageLookupByLibrary { "New Management Key should be 24 bytes long. Please save it in a safe place."), "pivChangePUK": MessageLookupByLibrary.simpleMessage("Change PUK"), "pivChangePUKPrompt": m5, + "pivDelete": MessageLookupByLibrary.simpleMessage("Delete"), "pivEmpty": MessageLookupByLibrary.simpleMessage("Empty"), + "pivExport": MessageLookupByLibrary.simpleMessage("Export"), + "pivExportCertificate": + MessageLookupByLibrary.simpleMessage("Export Certificate"), + "pivGenerate": MessageLookupByLibrary.simpleMessage("Generate"), + "pivImport": MessageLookupByLibrary.simpleMessage("Import"), "pivKeyManagement": MessageLookupByLibrary.simpleMessage("Key Management"), "pivManagementKeyVerificationFailed": diff --git a/lib/generated/intl/messages_zh_Hans.dart b/lib/generated/intl/messages_zh_Hans.dart index 9cff890..0f0ee11 100644 --- a/lib/generated/intl/messages_zh_Hans.dart +++ b/lib/generated/intl/messages_zh_Hans.dart @@ -168,7 +168,12 @@ class MessageLookup extends MessageLookupByLibrary { "pivChangeManagementKeyPrompt": MessageLookupByLibrary.simpleMessage( "新管理密钥的长度应当为 24 字节。请妥善保管管理密钥,否则您将无法管理 PIV 应用。"), "pivChangePUK": MessageLookupByLibrary.simpleMessage("修改 PUK"), + "pivDelete": MessageLookupByLibrary.simpleMessage("删除"), "pivEmpty": MessageLookupByLibrary.simpleMessage("空"), + "pivExport": MessageLookupByLibrary.simpleMessage("导出"), + "pivExportCertificate": MessageLookupByLibrary.simpleMessage("导出证书"), + "pivGenerate": MessageLookupByLibrary.simpleMessage("生成"), + "pivImport": MessageLookupByLibrary.simpleMessage("导入"), "pivKeyManagement": MessageLookupByLibrary.simpleMessage("密钥管理(Key Management)"), "pivManagementKeyVerificationFailed": diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 71eb22a..170dad7 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -1910,6 +1910,56 @@ class S { ); } + /// `Import` + String get pivImport { + return Intl.message( + 'Import', + name: 'pivImport', + desc: '', + args: [], + ); + } + + /// `Generate` + String get pivGenerate { + return Intl.message( + 'Generate', + name: 'pivGenerate', + desc: '', + args: [], + ); + } + + /// `Export` + String get pivExport { + return Intl.message( + 'Export', + name: 'pivExport', + desc: '', + args: [], + ); + } + + /// `Delete` + String get pivDelete { + return Intl.message( + 'Delete', + name: 'pivDelete', + desc: '', + args: [], + ); + } + + /// `Export Certificate` + String get pivExportCertificate { + return Intl.message( + 'Export Certificate', + name: 'pivExportCertificate', + desc: '', + args: [], + ); + } + /// `Please input a valid hexadecimal string.` String get validationHexString { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 213b334..46ad383 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -185,6 +185,11 @@ "pivOriginGenerated": "Generated", "pivOriginImported": "Imported", "pivCertificate": "Certificate", + "pivImport": "Import", + "pivGenerate": "Generate", + "pivExport": "Export", + "pivDelete": "Delete", + "pivExportCertificate": "Export Certificate", "validationHexString": "Please input a valid hexadecimal string.", "validationExactLength": "Need exact {length} characters" } \ No newline at end of file diff --git a/lib/l10n/intl_zh_Hans.arb b/lib/l10n/intl_zh_Hans.arb index 464ff82..049f5b1 100644 --- a/lib/l10n/intl_zh_Hans.arb +++ b/lib/l10n/intl_zh_Hans.arb @@ -185,6 +185,11 @@ "pivOriginGenerated": "内部生成", "pivOriginImported": "外部导入", "pivCertificate": "证书", + "pivImport": "导入", + "pivGenerate": "生成", + "pivExport": "导出", + "pivDelete": "删除", + "pivExportCertificate": "导出证书", "validationHexString": "请输入十六进制字符串", "validationExactLength": "需要 {length} 个字符" } \ No newline at end of file diff --git a/lib/models/piv.dart b/lib/models/piv.dart index 1d1c039..686f35d 100644 --- a/lib/models/piv.dart +++ b/lib/models/piv.dart @@ -136,6 +136,7 @@ class SlotInfo { final bool defaultValue; final int retriesCount; final int remainingCount; + List? certBytes; X509Certificate? cert; SlotInfo(this.number, this.algorithm, this.pinPolicy, this.touchPolicy, this.origin, this.public, this.defaultValue, this.retriesCount, this.remainingCount); diff --git a/lib/views/applets/piv.dart b/lib/views/applets/piv.dart index d70565d..2d4b686 100644 --- a/lib/views/applets/piv.dart +++ b/lib/views/applets/piv.dart @@ -1,4 +1,6 @@ +import 'dart:convert'; import 'dart:math'; +import 'dart:typed_data'; import 'package:canokey_console/controller/applets/piv.dart'; import 'package:canokey_console/generated/l10n.dart'; @@ -22,10 +24,12 @@ import 'package:canokey_console/helper/widgets/validators.dart'; import 'package:canokey_console/models/piv.dart'; import 'package:canokey_console/views/layout/layout.dart'; import 'package:convert/convert.dart'; +import 'package:file_saver/file_saver.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:logging/logging.dart'; import 'package:lucide_icons/lucide_icons.dart'; +import 'package:pem/pem.dart'; final log = Logger('Console:PIV:View'); @@ -550,7 +554,7 @@ class _PivPageState extends State with SingleTickerProviderStateMixin, elevation: 0, padding: Spacing.xy(20, 16), backgroundColor: contentTheme.primary, - child: CustomizedText.labelMedium('Generate', color: contentTheme.onSecondary), + child: CustomizedText.labelMedium(S.of(context).pivGenerate, color: contentTheme.onSecondary), ), Spacing.width(12), CustomizedButton.rounded( @@ -558,16 +562,56 @@ class _PivPageState extends State with SingleTickerProviderStateMixin, elevation: 0, padding: Spacing.xy(20, 16), backgroundColor: contentTheme.primary, - child: CustomizedText.labelMedium('Import', color: contentTheme.onPrimary), + child: CustomizedText.labelMedium(S.of(context).pivImport, color: contentTheme.onPrimary), ), if (slot != null) ...[ Spacing.width(12), CustomizedButton.rounded( - onPressed: () {}, + onPressed: () { + // A dialog with two buttons: DER and PEM + Get.dialog(Dialog( + child: SizedBox( + width: 300, + child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ + Padding(padding: Spacing.all(16), child: CustomizedText.labelLarge(S.of(context).pivExportCertificate)), + Divider(height: 0, thickness: 1), + Padding( + padding: Spacing.all(16), + child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + CustomizedButton.rounded( + onPressed: () async { + // Export DER + await FileSaver.instance.saveFile(name: 'certificate.der', bytes: slot.certBytes! as Uint8List); + if (mounted) { + Navigator.pop(context); + } + }, + elevation: 0, + padding: Spacing.xy(20, 16), + backgroundColor: contentTheme.primary, + child: CustomizedText.labelMedium('DER', color: contentTheme.onPrimary), + ), + Spacing.width(12), + CustomizedButton.rounded( + onPressed: () async { + String pem = PemCodec(PemLabel.certificate).encode(slot.certBytes!); + await FileSaver.instance.saveFile(name: 'certificate.pem', bytes: utf8.encode(pem)); + if (mounted) { + Navigator.pop(context); + } + }, + elevation: 0, + padding: Spacing.xy(20, 16), + backgroundColor: contentTheme.primary, + child: CustomizedText.labelMedium('PEM', color: contentTheme.onPrimary), + ) + ])) + ])))); + }, elevation: 0, padding: Spacing.xy(20, 16), backgroundColor: contentTheme.primary, - child: CustomizedText.labelMedium('Export', color: contentTheme.onPrimary), + child: CustomizedText.labelMedium(S.of(context).pivExport, color: contentTheme.onPrimary), ), Spacing.width(12), CustomizedButton.rounded( @@ -575,7 +619,7 @@ class _PivPageState extends State with SingleTickerProviderStateMixin, elevation: 0, padding: Spacing.xy(20, 16), backgroundColor: contentTheme.danger, - child: CustomizedText.labelMedium('Delete', color: contentTheme.onDanger), + child: CustomizedText.labelMedium(S.of(context).pivDelete, color: contentTheme.onDanger), ), ], ], diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index c378e4e..e1c3111 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include @@ -14,6 +15,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) ccid_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "CcidPlugin"); ccid_plugin_register_with_registrar(ccid_registrar); + g_autoptr(FlPluginRegistrar) file_saver_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin"); + file_saver_plugin_register_with_registrar(file_saver_registrar); g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin"); flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index bdd41bf..8207d1f 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ccid + file_saver flutter_webrtc url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c97fb48..70e936b 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import ccid import cryptography_flutter import device_info_plus +import file_saver import flutter_webrtc import mobile_scanner import path_provider_foundation @@ -18,6 +19,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { CcidPlugin.register(with: registry.registrar(forPlugin: "CcidPlugin")) CryptographyFlutterPlugin.register(with: registry.registrar(forPlugin: "CryptographyFlutterPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) diff --git a/pubspec.yaml b/pubspec.yaml index 9718118..ebb5bda 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,6 +45,8 @@ dependencies: dart_des: ^1.0.2 x509: ^0.2.4+2 asn1lib: ^1.5.2 + pem: ^2.0.5 + file_saver: ^0.2.12 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index b459b89..2cb9d13 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,12 +7,15 @@ #include "generated_plugin_registrant.h" #include +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { CcidPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("CcidPluginCApi")); + FileSaverPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSaverPlugin")); FlutterWebRTCPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 42f55cd..93f8573 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ccid + file_saver flutter_webrtc url_launcher_windows )