mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-04-14 20:53:34 +08:00
169 lines
5.9 KiB
Dart
169 lines
5.9 KiB
Dart
/*
|
|
* Copyright 2024 Hongen Wang All rights reserved.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* https://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import 'dart:io';
|
|
|
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
|
import 'package:file_picker/file_picker.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:proxypin/l10n/app_localizations.dart';
|
|
import 'package:flutter_toastr/flutter_toastr.dart';
|
|
import 'package:proxypin/network/util/cert/x509.dart';
|
|
import 'package:proxypin/ui/component/buttons.dart';
|
|
import 'package:proxypin/ui/component/text_field.dart';
|
|
import 'package:proxypin/utils/platform.dart';
|
|
|
|
///证书哈希名称查看
|
|
///@author Hongen Wang
|
|
class CertHashPage extends StatefulWidget {
|
|
final int? windowId;
|
|
|
|
const CertHashPage({super.key, this.windowId});
|
|
|
|
@override
|
|
State<StatefulWidget> createState() {
|
|
return _CertHashPageState();
|
|
}
|
|
}
|
|
|
|
class _CertHashPageState extends State<CertHashPage> {
|
|
var input = TextEditingController();
|
|
TextEditingController decodeData = TextEditingController();
|
|
|
|
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
if (Platforms.isDesktop() && widget.windowId != null) {
|
|
HardwareKeyboard.instance.addHandler(onKeyEvent);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
input.dispose();
|
|
decodeData.dispose();
|
|
HardwareKeyboard.instance.removeHandler(onKeyEvent);
|
|
super.dispose();
|
|
}
|
|
|
|
bool onKeyEvent(KeyEvent event) {
|
|
if (widget.windowId == null) return false;
|
|
if ((HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) &&
|
|
event.logicalKey == LogicalKeyboardKey.keyW) {
|
|
HardwareKeyboard.instance.removeHandler(onKeyEvent);
|
|
WindowController.fromWindowId(widget.windowId!).close();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: Text(localizations.systemCertName, style: TextStyle(fontSize: 16)), centerTitle: true),
|
|
resizeToAvoidBottomInset: false,
|
|
body: ListView(children: [
|
|
Wrap(alignment: WrapAlignment.end, children: [
|
|
ElevatedButton.icon(
|
|
onPressed: () async {
|
|
FilePickerResult? result = await FilePicker.platform
|
|
.pickFiles(type: FileType.custom, allowedExtensions: ['crt', 'pem', 'cer', 'der']);
|
|
if (result == null) return;
|
|
|
|
File file = File(result.files.single.path!);
|
|
var bytes = await file.readAsBytes();
|
|
input.text = tryDerFormat(bytes) ?? String.fromCharCodes(bytes);
|
|
getSubjectName();
|
|
},
|
|
style: Buttons.buttonStyle,
|
|
icon: const Icon(Icons.folder_open),
|
|
label: Text("File")),
|
|
const SizedBox(width: 15),
|
|
ElevatedButton.icon(
|
|
onPressed: () => input.clear(),
|
|
style: Buttons.buttonStyle,
|
|
icon: const Icon(Icons.clear),
|
|
label: const Text("Clear")),
|
|
const SizedBox(width: 15),
|
|
FilledButton.icon(
|
|
onPressed: () {
|
|
getSubjectName();
|
|
FocusScope.of(context).unfocus();
|
|
},
|
|
style: Buttons.buttonStyle,
|
|
icon: const Icon(Icons.play_arrow_rounded),
|
|
label: const Text("Run")),
|
|
const SizedBox(width: 15),
|
|
]),
|
|
const SizedBox(width: 10),
|
|
Container(
|
|
padding: const EdgeInsets.all(10),
|
|
height: 350,
|
|
child: TextFormField(
|
|
maxLines: 50,
|
|
controller: input,
|
|
onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
keyboardType: TextInputType.text,
|
|
decoration: decoration(context, label: localizations.inputContent))),
|
|
Align(
|
|
alignment: Alignment.bottomLeft,
|
|
child: TextButton(
|
|
onPressed: () {}, child: Text("${localizations.output}:", style: TextStyle(fontSize: 16)))),
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(10),
|
|
height: 150,
|
|
child: TextFormField(
|
|
maxLines: 30,
|
|
readOnly: true,
|
|
controller: decodeData,
|
|
decoration: decoration(context, label: 'Android ${localizations.systemCertName}'))),
|
|
]));
|
|
}
|
|
|
|
getSubjectName() {
|
|
var content = input.text;
|
|
if (content.isEmpty) return;
|
|
try {
|
|
var caCert = X509Utils.x509CertificateFromPem(content);
|
|
var subject = caCert.subject;
|
|
var subjectHashName = X509Utils.getSubjectHashName(subject);
|
|
decodeData.text = '$subjectHashName.0';
|
|
} catch (e) {
|
|
FlutterToastr.show(localizations.decodeFail, context, duration: 3, backgroundColor: Colors.red);
|
|
}
|
|
}
|
|
|
|
String? tryDerFormat(Uint8List data) {
|
|
try {
|
|
final bytes = data.sublist(0, 4);
|
|
|
|
// Check if the bytes match the DER format (ASN.1 encoding)
|
|
// DER encoded certificates typically start with 0x30 (SEQUENCE) or 0xA0 (APPLICATION)
|
|
if (bytes[0] == 0x30 || bytes[0] == 0xA0) {
|
|
return X509Utils.crlDerToPem(data);
|
|
}
|
|
return null;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|