From 2f67938b09236068f7d44ae11b3576d26518e3ce Mon Sep 17 00:00:00 2001 From: GT610 <79314033+GT-610@users.noreply.github.com> Date: Sat, 21 Mar 2026 23:33:31 +0800 Subject: [PATCH] feat (Private Key Editing): Added private key format normalization (#1080) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat (Private Key Editing): Added private key format normalization Added the _normalizePrivateKey method to normalize private key formats: - Removes whitespace characters from Base64 content - Ensures the standard format of 64 characters per line - Ensures that the private key ends with a newline character * fix(private_key): Fixes PEM private key formatting issues while preserving metadata headers Properly handles metadata headers (such as Proc-Type and DEK-Info) in encrypted PEM keys and preserves these headers when cleaning up Base64 content. Additionally, optimizes the logic for removing whitespace characters and improves performance by using precompiled regular expressions. * refactor(ui): Remove unused ctx parameters and optimize the selection window caching logic - Remove unused egui::Context parameters from functions related to settings_page - Add a check for the length of items in the selection window cache to improve cache validity - Simplify the cache data structure and remove unnecessary online data validation logic * fix(private_key): Fixed an issue with matching header and footer tags in PEM-format private keys Added validation for consistency of header and footer tags in PEM-format private keys to ensure that the content following “BEGIN” and “END” is identical --- lib/view/page/private_key/edit.dart | 62 ++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/lib/view/page/private_key/edit.dart b/lib/view/page/private_key/edit.dart index 80f0b06c..50c61892 100644 --- a/lib/view/page/private_key/edit.dart +++ b/lib/view/page/private_key/edit.dart @@ -12,6 +12,9 @@ import 'package:server_box/data/provider/private_key.dart'; import 'package:server_box/data/res/misc.dart'; const _format = 'text/plain'; +final _whitespaceRegex = RegExp(r'\s+'); +final _pemBeginRegex = RegExp(r'^-----BEGIN ([A-Z0-9 ]+)-----$'); +final _pemEndRegex = RegExp(r'^-----END ([A-Z0-9 ]+)-----$'); final class PrivateKeyEditPageArgs { final PrivateKeyInfo? pki; @@ -116,6 +119,63 @@ class _PrivateKeyEditPageState extends ConsumerState { return value.replaceAll('\r\n', '\n').replaceAll('\r', '\n'); } + /// Normalizes the private key format: + /// - Removes whitespace from Base64 content (spaces, tabs, etc.) + /// - Ensures the key ends with a newline + String _normalizePrivateKey(String key) { + final lines = key.split('\n'); + // Guard: need at least header + body + footer (3 lines) for valid PEM + if (lines.length < 3) return key; + + final header = lines.first; + final footer = lines.last; + + // Validate PEM boundaries before mutating input + final headerMatch = _pemBeginRegex.firstMatch(header); + final footerMatch = _pemEndRegex.firstMatch(footer); + if (headerMatch == null || footerMatch == null) { + return key; + } + + // Ensure header and footer labels match + final headerLabel = headerMatch.group(1); + final footerLabel = footerMatch.group(1); + if (headerLabel != footerLabel) { + return key; + } + + // Extract Base64 content (everything between header and footer) + final bodyLines = lines.sublist(1, lines.length - 1); + + // Check for RFC 1421 metadata headers (e.g., Proc-Type, DEK-Info) + // These appear in encrypted PEM keys and must be preserved + final hasMetadataHeaders = bodyLines.any( + (line) => line.contains(':') && !line.startsWith('-----'), + ); + + if (hasMetadataHeaders) { + // For encrypted keys, preserve structure and just ensure trailing newline + if (!key.endsWith('\n')) { + return '$key\n'; + } + return key; + } + + // Remove all whitespace from Base64 content + final cleanBody = bodyLines.join('').replaceAll(_whitespaceRegex, ''); + + // Rebuild the key with standard formatting (64 chars per line) + final buffer = StringBuffer(); + buffer.writeln(header); + for (var i = 0; i < cleanBody.length; i += 64) { + final end = (i + 64 < cleanBody.length) ? i + 64 : cleanBody.length; + buffer.writeln(cleanBody.substring(i, end)); + } + buffer.writeln(footer); + + return buffer.toString(); + } + Widget _buildFAB() { return FloatingActionButton(tooltip: l10n.save, onPressed: _onTapSave, child: const Icon(Icons.save)); } @@ -186,7 +246,7 @@ class _PrivateKeyEditPageState extends ConsumerState { void _onTapSave() async { final name = _nameController.text; - final key = _standardizeLineSeparators(_keyController.text.trim()); + final key = _normalizePrivateKey(_standardizeLineSeparators(_keyController.text.trim())); final pwd = _pwdController.text; if (name.isEmpty || key.isEmpty) { context.showSnackBar(libL10n.empty);