Files
proxypin/lib/ui/toolbox/cert_hash.dart
2025-06-18 01:51:16 +08:00

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;
}
}
}