Skip to content

Commit dba41ac

Browse files
committed
Add certificate viewer plugin
Introduces QuickLook.Plugin.CertViewer for viewing certificate files (.pfx, .cer, .pem, etc.) in QuickLook. The plugin loads and displays certificate details or raw content, and is integrated into the solution and project files.
1 parent 154ec05 commit dba41ac

File tree

8 files changed

+390
-0
lines changed

8 files changed

+390
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.Security.Cryptography.X509Certificates;
2+
3+
namespace QuickLook.Plugin.CertViewer;
4+
5+
internal sealed class CertLoadResult
6+
{
7+
public bool Success { get; }
8+
public X509Certificate2 Certificate { get; }
9+
public string Message { get; }
10+
public string RawContent { get; }
11+
12+
public CertLoadResult(bool success, X509Certificate2 certificate, string message, string rawContent)
13+
{
14+
Success = success;
15+
Certificate = certificate;
16+
Message = message;
17+
RawContent = rawContent;
18+
}
19+
20+
public static CertLoadResult From(bool success, X509Certificate2 certificate, string message, string rawContent)
21+
=> new CertLoadResult(success, certificate, message, rawContent);
22+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Security.Cryptography.X509Certificates;
5+
6+
namespace QuickLook.Plugin.CertViewer;
7+
8+
internal static class CertUtils
9+
{
10+
/// <summary>
11+
/// TryLoadCertificate returns a <see cref="CertLoadResult"/> containing:
12+
/// - Success: whether loading/parsing succeeded
13+
/// - Certificate: the parsed X509Certificate2 (may be null)
14+
/// - Message: an informational or error message
15+
/// - RawContent: original file text or hex when parsing failed
16+
/// </summary>
17+
public static CertLoadResult TryLoadCertificate(string path)
18+
{
19+
try
20+
{
21+
var ext = Path.GetExtension(path)?.ToLowerInvariant();
22+
23+
if (ext == ".pfx" || ext == ".p12")
24+
{
25+
try
26+
{
27+
var cert = new X509Certificate2(path);
28+
return new CertLoadResult(true, cert, string.Empty, null);
29+
}
30+
catch (Exception ex)
31+
{
32+
return new CertLoadResult(false, null, "Failed to load PFX/P12: " + ex.Message, null);
33+
}
34+
}
35+
36+
// Try DER/PEM style cert (.cer/.crt/.pem)
37+
var text = File.ReadAllText(path);
38+
39+
const string begin = "-----BEGIN CERTIFICATE-----";
40+
const string end = "-----END CERTIFICATE-----";
41+
42+
if (text.Contains(begin))
43+
{
44+
var startIdx = text.IndexOf(begin, StringComparison.Ordinal);
45+
var endIdx = text.IndexOf(end, StringComparison.Ordinal);
46+
if (startIdx >= 0 && endIdx > startIdx)
47+
{
48+
var b64 = text.Substring(startIdx + begin.Length, endIdx - (startIdx + begin.Length));
49+
b64 = new string(b64.Where(c => !char.IsWhiteSpace(c)).ToArray());
50+
try
51+
{
52+
var raw = Convert.FromBase64String(b64);
53+
var cert = new X509Certificate2(raw);
54+
return new CertLoadResult(true, cert, string.Empty, text);
55+
}
56+
catch (Exception ex)
57+
{
58+
return new CertLoadResult(false, null, "PEM decode failed: " + ex.Message, text);
59+
}
60+
}
61+
}
62+
63+
// Try raw DER
64+
try
65+
{
66+
var bytes = File.ReadAllBytes(path);
67+
// heuristics: if starts with 0x30 (ASN.1 SEQUENCE) it's likely DER encoded
68+
if (bytes.Length > 0 && bytes[0] == 0x30)
69+
{
70+
try
71+
{
72+
var cert = new X509Certificate2(bytes);
73+
return new CertLoadResult(true, cert, string.Empty, null);
74+
}
75+
catch
76+
{
77+
// not a certificate DER
78+
}
79+
}
80+
}
81+
catch
82+
{
83+
}
84+
85+
// Unsupported or not parseable: return raw text or hex
86+
try
87+
{
88+
var rawText = File.ReadAllText(path);
89+
return new CertLoadResult(false, null, "Could not parse as certificate; showing raw content.", rawText);
90+
}
91+
catch
92+
{
93+
// fallback to hex
94+
try
95+
{
96+
var bytes = File.ReadAllBytes(path);
97+
var hex = BitConverter.ToString(bytes).Replace("-", " ");
98+
return new CertLoadResult(false, null, "Could not parse as certificate; showing hex.", hex);
99+
}
100+
catch (Exception ex)
101+
{
102+
return new CertLoadResult(false, null, "Failed to read file: " + ex.Message, null);
103+
}
104+
}
105+
}
106+
catch (Exception ex)
107+
{
108+
return new CertLoadResult(false, null, "Internal error: " + ex.Message, null);
109+
}
110+
}
111+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<UserControl x:Class="QuickLook.Plugin.CertViewer.CertViewerControl"
2+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
5+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
6+
d:DesignHeight="600"
7+
d:DesignWidth="800"
8+
mc:Ignorable="d">
9+
<Grid>
10+
<Grid.ColumnDefinitions>
11+
<ColumnDefinition Width="*" />
12+
<ColumnDefinition Width="*" />
13+
</Grid.ColumnDefinitions>
14+
<Grid.RowDefinitions>
15+
<RowDefinition Height="*" />
16+
</Grid.RowDefinitions>
17+
18+
<ListView x:Name="PropertyList"
19+
Grid.Column="0"
20+
Margin="8">
21+
<ListView.View>
22+
<GridView>
23+
<GridViewColumn Width="150"
24+
DisplayMemberBinding="{Binding Key}"
25+
Header="Field" />
26+
<GridViewColumn Width="300"
27+
DisplayMemberBinding="{Binding Value}"
28+
Header="Value" />
29+
</GridView>
30+
</ListView.View>
31+
</ListView>
32+
33+
<TextBox x:Name="RawText"
34+
Grid.Column="1"
35+
Margin="8"
36+
AcceptsReturn="True"
37+
HorizontalScrollBarVisibility="Auto"
38+
IsReadOnly="True"
39+
TextWrapping="Wrap"
40+
VerticalScrollBarVisibility="Auto" />
41+
</Grid>
42+
</UserControl>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Security.Cryptography.X509Certificates;
4+
using System.Windows.Controls;
5+
6+
namespace QuickLook.Plugin.CertViewer;
7+
8+
public partial class CertViewerControl : UserControl, IDisposable
9+
{
10+
public CertViewerControl()
11+
{
12+
InitializeComponent();
13+
}
14+
15+
public void LoadCertificate(X509Certificate2 cert)
16+
{
17+
var items = new List<KeyValuePair<string, string>>
18+
{
19+
new("Subject", cert.Subject),
20+
new("Issuer", cert.Issuer),
21+
new("Thumbprint", cert.Thumbprint),
22+
new("SerialNumber", cert.SerialNumber),
23+
new("NotBefore", cert.NotBefore.ToString()),
24+
new("NotAfter", cert.NotAfter.ToString()),
25+
new("SignatureAlgorithm", cert.SignatureAlgorithm.FriendlyName ?? cert.SignatureAlgorithm.Value),
26+
new("PublicKey", cert.PublicKey.Oid.FriendlyName ?? cert.PublicKey.Oid.Value),
27+
};
28+
29+
PropertyList.ItemsSource = items;
30+
RawText.Text = string.Empty;
31+
}
32+
33+
public void LoadRaw(string path, string message, string content)
34+
{
35+
PropertyList.ItemsSource = new List<KeyValuePair<string, string>>
36+
{
37+
new("Path", path),
38+
new("Info", message)
39+
};
40+
41+
RawText.Text = content ?? "(No content to display)";
42+
}
43+
44+
public void Dispose()
45+
{
46+
}
47+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using QuickLook.Common.Plugin;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.IO;
5+
using System.Windows;
6+
7+
namespace QuickLook.Plugin.CertViewer;
8+
9+
public class Plugin : IViewer
10+
{
11+
private static readonly HashSet<string> WellKnownExtensions = new(StringComparer.OrdinalIgnoreCase)
12+
{
13+
".p12",
14+
".pfx",
15+
".cer",
16+
".crt",
17+
".pem",
18+
".snk",
19+
".pvk",
20+
".spc",
21+
".mobileprovision",
22+
".certSigningRequest",
23+
".csr",
24+
".keystore",
25+
};
26+
27+
private CertViewerControl _control;
28+
private string _currentPath;
29+
30+
public int Priority => 0;
31+
32+
public void Init()
33+
{
34+
}
35+
36+
public bool CanHandle(string path)
37+
{
38+
if (Directory.Exists(path))
39+
return false;
40+
41+
var ext = Path.GetExtension(path);
42+
if (!string.IsNullOrEmpty(ext) && WellKnownExtensions.Contains(ext))
43+
return true;
44+
45+
return false;
46+
}
47+
48+
public void Prepare(string path, ContextObject context)
49+
{
50+
context.PreferredSize = new Size { Width = 800, Height = 600 };
51+
}
52+
53+
public void View(string path, ContextObject context)
54+
{
55+
_currentPath = path;
56+
57+
context.IsBusy = true;
58+
59+
var result = CertUtils.TryLoadCertificate(path);
60+
61+
_control = new CertViewerControl();
62+
63+
if (result.Success && result.Certificate != null)
64+
{
65+
_control.LoadCertificate(result.Certificate);
66+
}
67+
else
68+
{
69+
_control.LoadRaw(path, result.Message, result.RawContent);
70+
}
71+
72+
context.ViewerContent = _control;
73+
context.Title = Path.GetFileName(path);
74+
context.IsBusy = false;
75+
}
76+
77+
public void Cleanup()
78+
{
79+
_control?.Dispose();
80+
_control = null;
81+
}
82+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
2+
3+
<PropertyGroup>
4+
<OutputType>Library</OutputType>
5+
<TargetFramework>net462</TargetFramework>
6+
<RootNamespace>QuickLook.Plugin.CertViewer</RootNamespace>
7+
<AssemblyName>QuickLook.Plugin.CertViewer</AssemblyName>
8+
<FileAlignment>512</FileAlignment>
9+
<SignAssembly>false</SignAssembly>
10+
<UseWPF>true</UseWPF>
11+
<LangVersion>preview</LangVersion>
12+
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
13+
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
14+
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
15+
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
16+
<ProjectGuid>{C8B9F6D1-1234-4AAB-9C3D-ABCDEF123456}</ProjectGuid>
17+
</PropertyGroup>
18+
19+
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
20+
<DebugSymbols>true</DebugSymbols>
21+
<DebugType>full</DebugType>
22+
<Optimize>false</Optimize>
23+
<OutputPath>..\..\Build\Debug\QuickLook.Plugin\QuickLook.Plugin.CertViewer\</OutputPath>
24+
<DefineConstants>DEBUG;TRACE</DefineConstants>
25+
<PlatformTarget>x86</PlatformTarget>
26+
<ErrorReport>prompt</ErrorReport>
27+
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
28+
</PropertyGroup>
29+
30+
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
31+
<DebugType>pdbonly</DebugType>
32+
<Optimize>true</Optimize>
33+
<OutputPath>..\..\Build\Release\QuickLook.Plugin\QuickLook.Plugin.CertViewer\</OutputPath>
34+
<DefineConstants>TRACE</DefineConstants>
35+
<PlatformTarget>x86</PlatformTarget>
36+
<ErrorReport>prompt</ErrorReport>
37+
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
38+
</PropertyGroup>
39+
40+
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|AnyCPU'">
41+
<DebugSymbols>true</DebugSymbols>
42+
<DebugType>full</DebugType>
43+
<Optimize>false</Optimize>
44+
<OutputPath>..\..\Build\Debug\QuickLook.Plugin\QuickLook.Plugin.CertViewer\</OutputPath>
45+
<DefineConstants>DEBUG;TRACE</DefineConstants>
46+
<PlatformTarget>AnyCPU</PlatformTarget>
47+
<ErrorReport>prompt</ErrorReport>
48+
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
49+
</PropertyGroup>
50+
51+
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|AnyCPU'">
52+
<DebugType>pdbonly</DebugType>
53+
<Optimize>true</Optimize>
54+
<OutputPath>..\..\Build\Release\QuickLook.Plugin\QuickLook.Plugin.CertViewer\</OutputPath>
55+
<DefineConstants>TRACE</DefineConstants>
56+
<PlatformTarget>AnyCPU</PlatformTarget>
57+
<ErrorReport>prompt</ErrorReport>
58+
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
59+
</PropertyGroup>
60+
61+
<ItemGroup>
62+
<Reference Include="WindowsBase" />
63+
<Reference Include="System" />
64+
<Reference Include="System.Core" />
65+
</ItemGroup>
66+
67+
<ItemGroup>
68+
<ProjectReference Include="..\..\QuickLook.Common\QuickLook.Common.csproj">
69+
<Project>{85FDD6BA-871D-46C8-BD64-F6BB0CB5EA95}</Project>
70+
<Name>QuickLook.Common</Name>
71+
<Private>False</Private>
72+
</ProjectReference>
73+
</ItemGroup>
74+
75+
<ItemGroup>
76+
<Compile Include="..\..\GitVersion.cs">
77+
<Link>Properties\GitVersion.cs</Link>
78+
</Compile>
79+
</ItemGroup>
80+
81+
</Project>

0 commit comments

Comments
 (0)