Add password support for protected certificates
Some checks failed
build / build (push) Has been cancelled
build / publish (push) Has been cancelled

Introduces UI and logic to handle password-protected certificate files. The CertViewerControl now prompts for a password if needed, and attempts to reload the certificate with the provided password. Refactored certificate loading flow to support this feature.
This commit is contained in:
ema
2025-12-23 14:41:59 +08:00
parent 59f07a6cf3
commit 06694e0b16
5 changed files with 106 additions and 37 deletions

View File

@@ -8,15 +8,17 @@ internal sealed class CertLoadResult
public X509Certificate2 Certificate { get; } public X509Certificate2 Certificate { get; }
public string Message { get; } public string Message { get; }
public string RawContent { get; } public string RawContent { get; }
public bool NeedsPassword { get; }
public CertLoadResult(bool success, X509Certificate2 certificate, string message, string rawContent) public CertLoadResult(bool success, X509Certificate2 certificate, string message, string rawContent, bool needsPassword = false)
{ {
Success = success; Success = success;
Certificate = certificate; Certificate = certificate;
Message = message; Message = message;
RawContent = rawContent; RawContent = rawContent;
NeedsPassword = needsPassword;
} }
public static CertLoadResult From(bool success, X509Certificate2 certificate, string message, string rawContent) public static CertLoadResult From(bool success, X509Certificate2 certificate, string message, string rawContent, bool needsPassword = false)
=> new CertLoadResult(success, certificate, message, rawContent); => new CertLoadResult(success, certificate, message, rawContent, needsPassword);
} }

View File

@@ -14,7 +14,7 @@ internal static class CertUtils
/// - Message: an informational or error message /// - Message: an informational or error message
/// - RawContent: original file text or hex when parsing failed /// - RawContent: original file text or hex when parsing failed
/// </summary> /// </summary>
public static CertLoadResult TryLoadCertificate(string path) public static CertLoadResult TryLoadCertificate(string path, string password = null)
{ {
try try
{ {
@@ -24,12 +24,16 @@ internal static class CertUtils
{ {
try try
{ {
var cert = new X509Certificate2(path); var cert = !string.IsNullOrEmpty(password)
? new X509Certificate2(path, password)
: new X509Certificate2(path);
return new CertLoadResult(true, cert, string.Empty, null); return new CertLoadResult(true, cert, string.Empty, null);
} }
catch (Exception ex) catch (Exception ex)
{ {
return new CertLoadResult(false, null, "Failed to load PFX/P12: " + ex.Message, null); var isPasswordError = ex is System.Security.Cryptography.CryptographicException ||
(ex.Message?.IndexOf("password", StringComparison.OrdinalIgnoreCase) >= 0);
return new CertLoadResult(false, null, "Failed to load PFX/P12: " + ex.Message, null, isPasswordError);
} }
} }

View File

@@ -12,29 +12,41 @@
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<DataGrid x:Name="PropertyList" <!-- Password panel (hidden by default) -->
Grid.Column="0" <StackPanel x:Name="PasswordPanel" Grid.Row="0" Grid.ColumnSpan="2" Orientation="Horizontal" Margin="8" Visibility="Collapsed" VerticalAlignment="Center">
Margin="8" <TextBlock Text="This certificate appears to be password-protected. Enter password:" VerticalAlignment="Center" Margin="0,0,8,0" />
AutoGenerateColumns="False" <PasswordBox x:Name="InlinePasswordBox" Width="220" Margin="0,0,8,0" />
IsReadOnly="True" <Button x:Name="LoadWithPasswordButton" Content="Load" Width="80" Margin="0,0,8,0" Click="LoadWithPasswordButton_Click" />
HeadersVisibility="Column" <Button x:Name="CancelPasswordButton" Content="Cancel" Width="80" Click="CancelPasswordButton_Click" />
RowHeaderWidth="0"> </StackPanel>
<DataGrid.Columns>
<DataGridTextColumn Header="Field" Binding="{Binding Key}" Width="150" />
<DataGridTextColumn Header="Value" Binding="{Binding Value}" Width="*" />
</DataGrid.Columns>
</DataGrid>
<TextBox x:Name="RawText" <TabControl Grid.Row="1" Grid.ColumnSpan="2" Margin="8">
Grid.Column="1" <TabItem Header="Details">
Margin="8" <DataGrid x:Name="PropertyList"
AcceptsReturn="True" AutoGenerateColumns="False"
HorizontalScrollBarVisibility="Auto" IsReadOnly="True"
IsReadOnly="True" HeadersVisibility="Column"
TextWrapping="Wrap" RowHeaderWidth="0"
VerticalScrollBarVisibility="Auto" /> Margin="0">
<DataGrid.Columns>
<DataGridTextColumn Header="Field" Binding="{Binding Key}" Width="150" />
<DataGridTextColumn Header="Value" Binding="{Binding Value}" Width="*" />
</DataGrid.Columns>
</DataGrid>
</TabItem>
<TabItem Header="Raw">
<TextBox x:Name="RawText"
AcceptsReturn="True"
HorizontalScrollBarVisibility="Auto"
IsReadOnly="True"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto"
Margin="0" />
</TabItem>
</TabControl>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -7,13 +7,43 @@ namespace QuickLook.Plugin.CertViewer;
public partial class CertViewerControl : UserControl, IDisposable public partial class CertViewerControl : UserControl, IDisposable
{ {
private string _currentPath;
public CertViewerControl() public CertViewerControl()
{ {
InitializeComponent(); InitializeComponent();
} }
/// <summary>
/// Load a certificate file from path. If the file appears password-protected,
/// the control will show an inline password input and allow the user to retry.
/// </summary>
public void LoadFromPath(string path)
{
_currentPath = path;
var result = CertUtils.TryLoadCertificate(path);
if (!result.Success && result.NeedsPassword)
{
// show password UI
PasswordPanel.Visibility = System.Windows.Visibility.Visible;
PropertyList.ItemsSource = null;
RawText.Text = string.Empty;
return;
}
PasswordPanel.Visibility = System.Windows.Visibility.Collapsed;
if (result.Success && result.Certificate != null)
LoadCertificate(result.Certificate);
else
LoadRaw(path, result.Message, result.RawContent);
}
public void LoadCertificate(X509Certificate2 cert) public void LoadCertificate(X509Certificate2 cert)
{ {
PasswordPanel.Visibility = System.Windows.Visibility.Collapsed;
var items = new List<KeyValuePair<string, string>> var items = new List<KeyValuePair<string, string>>
{ {
new("Subject", cert.Subject), new("Subject", cert.Subject),
@@ -32,6 +62,7 @@ public partial class CertViewerControl : UserControl, IDisposable
public void LoadRaw(string path, string message, string content) public void LoadRaw(string path, string message, string content)
{ {
PasswordPanel.Visibility = System.Windows.Visibility.Collapsed;
PropertyList.ItemsSource = new List<KeyValuePair<string, string>> PropertyList.ItemsSource = new List<KeyValuePair<string, string>>
{ {
new("Path", path), new("Path", path),
@@ -44,4 +75,34 @@ public partial class CertViewerControl : UserControl, IDisposable
public void Dispose() public void Dispose()
{ {
} }
private void LoadWithPasswordButton_Click(object sender, System.Windows.RoutedEventArgs e)
{
var pwd = InlinePasswordBox.Password;
if (string.IsNullOrEmpty(_currentPath))
return;
var result = CertUtils.TryLoadCertificate(_currentPath, pwd);
PasswordPanel.Visibility = System.Windows.Visibility.Collapsed;
if (result.Success && result.Certificate != null)
{
LoadCertificate(result.Certificate);
}
else
{
LoadRaw(_currentPath, result.Message, result.RawContent);
}
}
private void CancelPasswordButton_Click(object sender, System.Windows.RoutedEventArgs e)
{
// show failure/raw view
if (string.IsNullOrEmpty(_currentPath))
return;
var result = CertUtils.TryLoadCertificate(_currentPath);
PasswordPanel.Visibility = System.Windows.Visibility.Collapsed;
LoadRaw(_currentPath, result.Message, result.RawContent);
}
} }

View File

@@ -56,18 +56,8 @@ public class Plugin : IViewer
context.IsBusy = true; context.IsBusy = true;
var result = CertUtils.TryLoadCertificate(path);
_control = new CertViewerControl(); _control = new CertViewerControl();
_control.LoadFromPath(path);
if (result.Success && result.Certificate != null)
{
_control.LoadCertificate(result.Certificate);
}
else
{
_control.LoadRaw(path, result.Message, result.RawContent);
}
context.ViewerContent = _control; context.ViewerContent = _control;
context.Title = Path.GetFileName(path); context.Title = Path.GetFileName(path);