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 string Message { 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;
Certificate = certificate;
Message = message;
RawContent = rawContent;
NeedsPassword = needsPassword;
}
public static CertLoadResult From(bool success, X509Certificate2 certificate, string message, string rawContent)
=> new CertLoadResult(success, certificate, message, rawContent);
public static CertLoadResult From(bool success, X509Certificate2 certificate, string message, string rawContent, bool needsPassword = false)
=> new CertLoadResult(success, certificate, message, rawContent, needsPassword);
}

View File

@@ -14,7 +14,7 @@ internal static class CertUtils
/// - Message: an informational or error message
/// - RawContent: original file text or hex when parsing failed
/// </summary>
public static CertLoadResult TryLoadCertificate(string path)
public static CertLoadResult TryLoadCertificate(string path, string password = null)
{
try
{
@@ -24,12 +24,16 @@ internal static class CertUtils
{
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);
}
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="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<DataGrid x:Name="PropertyList"
Grid.Column="0"
Margin="8"
AutoGenerateColumns="False"
IsReadOnly="True"
HeadersVisibility="Column"
RowHeaderWidth="0">
<DataGrid.Columns>
<DataGridTextColumn Header="Field" Binding="{Binding Key}" Width="150" />
<DataGridTextColumn Header="Value" Binding="{Binding Value}" Width="*" />
</DataGrid.Columns>
</DataGrid>
<!-- Password panel (hidden by default) -->
<StackPanel x:Name="PasswordPanel" Grid.Row="0" Grid.ColumnSpan="2" Orientation="Horizontal" Margin="8" Visibility="Collapsed" VerticalAlignment="Center">
<TextBlock Text="This certificate appears to be password-protected. Enter password:" VerticalAlignment="Center" Margin="0,0,8,0" />
<PasswordBox x:Name="InlinePasswordBox" Width="220" Margin="0,0,8,0" />
<Button x:Name="LoadWithPasswordButton" Content="Load" Width="80" Margin="0,0,8,0" Click="LoadWithPasswordButton_Click" />
<Button x:Name="CancelPasswordButton" Content="Cancel" Width="80" Click="CancelPasswordButton_Click" />
</StackPanel>
<TextBox x:Name="RawText"
Grid.Column="1"
Margin="8"
AcceptsReturn="True"
HorizontalScrollBarVisibility="Auto"
IsReadOnly="True"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto" />
<TabControl Grid.Row="1" Grid.ColumnSpan="2" Margin="8">
<TabItem Header="Details">
<DataGrid x:Name="PropertyList"
AutoGenerateColumns="False"
IsReadOnly="True"
HeadersVisibility="Column"
RowHeaderWidth="0"
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>
</UserControl>

View File

@@ -7,13 +7,43 @@ namespace QuickLook.Plugin.CertViewer;
public partial class CertViewerControl : UserControl, IDisposable
{
private string _currentPath;
public CertViewerControl()
{
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)
{
PasswordPanel.Visibility = System.Windows.Visibility.Collapsed;
var items = new List<KeyValuePair<string, string>>
{
new("Subject", cert.Subject),
@@ -32,6 +62,7 @@ public partial class CertViewerControl : UserControl, IDisposable
public void LoadRaw(string path, string message, string content)
{
PasswordPanel.Visibility = System.Windows.Visibility.Collapsed;
PropertyList.ItemsSource = new List<KeyValuePair<string, string>>
{
new("Path", path),
@@ -44,4 +75,34 @@ public partial class CertViewerControl : UserControl, IDisposable
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;
var result = CertUtils.TryLoadCertificate(path);
_control = new CertViewerControl();
if (result.Success && result.Certificate != null)
{
_control.LoadCertificate(result.Certificate);
}
else
{
_control.LoadRaw(path, result.Message, result.RawContent);
}
_control.LoadFromPath(path);
context.ViewerContent = _control;
context.Title = Path.GetFileName(path);