diff --git a/src/Files.App.CsWin32/NativeMethods.txt b/src/Files.App.CsWin32/NativeMethods.txt index 294dcf68ab36..a758d0ef4c51 100644 --- a/src/Files.App.CsWin32/NativeMethods.txt +++ b/src/Files.App.CsWin32/NativeMethods.txt @@ -236,3 +236,36 @@ GetMenuItemCount GetMenuItemInfo IsWow64Process2 GetCurrentProcess +CertFreeCertificateContext +CryptMsgGetParam +CryptMsgClose +CryptMsgOpenToDecode +CryptMsgUpdate +CertOpenStore +CryptDecodeObject +CertFindCertificateInStore +CertComparePublicKeyInfo +CryptQueryObject +CertCloseStore +WinVerifyTrust +FileTimeToSystemTime +FileTimeToLocalFileTime +SystemTimeToFileTime +CRYPTOAPI_BLOB +CMSG_SIGNER_INFO +SignDataHandle +CRYPT_ATTRIBUTE +FILETIME +CRYPT_BIT_BLOB +CERT_ALT_NAME_INFO +CERT_CONTEXT +CERT_INFO +CRYPT_ALGORITHM_IDENTIFIER +CERT_PUBLIC_KEY_INFO +CATALOG_INFO +WINTRUST_FILE_INFO +WINTRUST_DATA +HCERTSTORE +HCRYPTMSG +CERT_QUERY_ENCODING_TYPE +CertGetNameString diff --git a/src/Files.App/Data/Enums/PropertiesNavigationViewItemType.cs b/src/Files.App/Data/Enums/PropertiesNavigationViewItemType.cs index 4b33779e6c1d..811d70c3474e 100644 --- a/src/Files.App/Data/Enums/PropertiesNavigationViewItemType.cs +++ b/src/Files.App/Data/Enums/PropertiesNavigationViewItemType.cs @@ -47,5 +47,10 @@ public enum PropertiesNavigationViewItemType /// Shortcut page type /// Shortcut, + + /// + /// Signatures page type + /// + Signatures, } } diff --git a/src/Files.App/Data/Factories/PropertiesNavigationViewItemFactory.cs b/src/Files.App/Data/Factories/PropertiesNavigationViewItemFactory.cs index f546301858e4..0723f321df0d 100644 --- a/src/Files.App/Data/Factories/PropertiesNavigationViewItemFactory.cs +++ b/src/Files.App/Data/Factories/PropertiesNavigationViewItemFactory.cs @@ -61,8 +61,15 @@ public static ObservableCollection Initialize ItemType = PropertiesNavigationViewItemType.Compatibility, ThemedIconStyle = (Style)Application.Current.Resources["App.ThemedIcons.Properties.Compatability"], }; + var signaturesItem = new NavigationViewItemButtonStyleItem() + { + Name = Strings.Signatures.GetLocalizedResource(), + ItemType = PropertiesNavigationViewItemType.Signatures, + ThemedIconStyle = (Style)Application.Current.Resources["App.ThemedIcons.Properties.Signatures"], + }; PropertiesNavigationViewItems.Add(generalItem); + PropertiesNavigationViewItems.Add(signaturesItem); PropertiesNavigationViewItems.Add(securityItem); PropertiesNavigationViewItems.Add(hashesItem); PropertiesNavigationViewItems.Add(shortcutItem); @@ -89,6 +96,7 @@ public static ObservableCollection Initialize PropertiesNavigationViewItems.Remove(securityItem); PropertiesNavigationViewItems.Remove(customizationItem); PropertiesNavigationViewItems.Remove(hashesItem); + PropertiesNavigationViewItems.Remove(signaturesItem); } else if (item is ListedItem listedItem) { @@ -102,6 +110,11 @@ public static ObservableCollection Initialize var detailsItemEnabled = !(isFolder && !listedItem.IsArchive) && !isLibrary && !listedItem.IsRecycleBinItem; var customizationItemEnabled = !isLibrary && (isFolder && !listedItem.IsArchive || isShortcut && !listedItem.IsLinkItem); var compatibilityItemEnabled = FileExtensionHelpers.IsExecutableFile(listedItem is IShortcutItem sht ? sht.TargetPath : fileExt, true); + var signaturesItemEnabled = + !isFolder && + !isLibrary && + !listedItem.IsRecycleBinItem && + FileExtensionHelpers.IsSignableFile(fileExt, true); if (!securityItemEnabled) PropertiesNavigationViewItems.Remove(securityItem); @@ -109,6 +122,9 @@ public static ObservableCollection Initialize if (!hashItemEnabled) PropertiesNavigationViewItems.Remove(hashesItem); + if (!signaturesItemEnabled) + PropertiesNavigationViewItems.Remove(signaturesItem); + if (!isShortcut) PropertiesNavigationViewItems.Remove(shortcutItem); @@ -132,6 +148,7 @@ public static ObservableCollection Initialize PropertiesNavigationViewItems.Remove(detailsItem); PropertiesNavigationViewItems.Remove(customizationItem); PropertiesNavigationViewItems.Remove(compatibilityItem); + PropertiesNavigationViewItems.Remove(signaturesItem); } return PropertiesNavigationViewItems; diff --git a/src/Files.App/Data/Items/CertNodeInfoItem.cs b/src/Files.App/Data/Items/CertNodeInfoItem.cs new file mode 100644 index 000000000000..abed64927c7a --- /dev/null +++ b/src/Files.App/Data/Items/CertNodeInfoItem.cs @@ -0,0 +1,18 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +namespace Files.App.Data.Items +{ + public class CertNodeInfoItem + { + public string IssuedTo { get; set; } = string.Empty; + + public string IssuedBy { get; set; } = string.Empty; + + public string Version { get; set; } = string.Empty; + + public string ValidFrom { get; set; } = string.Empty; + + public string ValidTo { get; set; } = string.Empty; + } +} diff --git a/src/Files.App/Data/Models/SignatureInfoItem.cs b/src/Files.App/Data/Models/SignatureInfoItem.cs new file mode 100644 index 000000000000..6b5e8ffc5022 --- /dev/null +++ b/src/Files.App/Data/Models/SignatureInfoItem.cs @@ -0,0 +1,92 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Files.App.Utils.Signatures; +using System.Windows.Input; +using Windows.Win32.Foundation; + +namespace Files.App.Data.Models +{ + public sealed partial class SignatureInfoItem : ObservableObject + { + private readonly string _fileName; + + private readonly HWND _hwndParent; + + private readonly int _index; + + private string _Version = string.Empty; + public string Version + { + get => _Version; + set => SetProperty(ref _Version, value); + } + + private string _IssuedBy = string.Empty; + public string IssuedBy + { + get => _IssuedBy; + set => SetProperty(ref _IssuedBy, value); + } + + private string _IssuedTo = string.Empty; + public string IssuedTo + { + get => _IssuedTo; + set => SetProperty(ref _IssuedTo, value); + } + + private string _ValidFromTimestamp = string.Empty; + public string ValidFromTimestamp + { + get => _ValidFromTimestamp; + set => SetProperty(ref _ValidFromTimestamp, value); + } + + private string _ValidToTimestamp = string.Empty; + public string ValidToTimestamp + { + get => _ValidToTimestamp; + set => SetProperty(ref _ValidToTimestamp, value); + } + + private string _VerifiedTimestamp = string.Empty; + public string VerifiedTimestamp + { + get => _VerifiedTimestamp; + set => SetProperty(ref _VerifiedTimestamp, value); + } + + private bool _Verified = false; + public bool Verified + { + get => _Verified; + set + { + if (SetProperty(ref _Verified, value)) + OnPropertyChanged(nameof(Glyph)); + } + } + + public List SignChain { get; } + + public string Glyph => Verified ? "\uE930" : "\uEA39"; + + public ICommand OpenDetailsCommand { get; } + + public SignatureInfoItem(string fileName, int index, HWND hWnd, List chain) + { + _fileName = fileName; + _hwndParent = hWnd; + _index = index; + SignChain = chain ?? new List(); + OpenDetailsCommand = new AsyncRelayCommand(DoOpenDetails); + } + + private Task DoOpenDetails() + { + DigitalSignaturesUtil.DisplaySignerInfoDialog(_fileName, _hwndParent, _index); + return Task.CompletedTask; + } + } +} diff --git a/src/Files.App/Helpers/Win32/Win32Helper.Storage.cs b/src/Files.App/Helpers/Win32/Win32Helper.Storage.cs index 93b28ad04429..51bc82a35a73 100644 --- a/src/Files.App/Helpers/Win32/Win32Helper.Storage.cs +++ b/src/Files.App/Helpers/Win32/Win32Helper.Storage.cs @@ -902,13 +902,13 @@ public static SafeFileHandle OpenFileForRead(string filePath, bool readWrite = f (uint)FILE_ACCESS_RIGHTS.FILE_GENERIC_READ | (uint)(readWrite ? FILE_ACCESS_RIGHTS.FILE_GENERIC_WRITE : 0u), (uint)(Win32PInvoke.FILE_SHARE_READ | (readWrite ? 0 : Win32PInvoke.FILE_SHARE_WRITE)), IntPtr.Zero, Win32PInvoke.OPEN_EXISTING, (uint)Win32PInvoke.File_Attributes.BackupSemantics | flags, IntPtr.Zero), true); } - public static bool GetFileDateModified(string filePath, out FILETIME dateModified) + public static bool GetFileDateModified(string filePath, out System.Runtime.InteropServices.ComTypes.FILETIME dateModified) { using var hFile = new SafeFileHandle(Win32PInvoke.CreateFileFromApp(filePath, (uint)FILE_ACCESS_RIGHTS.FILE_GENERIC_READ, Win32PInvoke.FILE_SHARE_READ, IntPtr.Zero, Win32PInvoke.OPEN_EXISTING, (uint)Win32PInvoke.File_Attributes.BackupSemantics, IntPtr.Zero), true); return Win32PInvoke.GetFileTime(hFile.DangerousGetHandle(), out _, out _, out dateModified); } - public static bool SetFileDateModified(string filePath, FILETIME dateModified) + public static bool SetFileDateModified(string filePath, System.Runtime.InteropServices.ComTypes.FILETIME dateModified) { using var hFile = new SafeFileHandle(Win32PInvoke.CreateFileFromApp(filePath, (uint)FILE_ACCESS_RIGHTS.FILE_WRITE_ATTRIBUTES, 0, IntPtr.Zero, Win32PInvoke.OPEN_EXISTING, (uint)Win32PInvoke.File_Attributes.BackupSemantics, IntPtr.Zero), true); return Win32PInvoke.SetFileTime(hFile.DangerousGetHandle(), new(), new(), dateModified); diff --git a/src/Files.App/Helpers/Win32/Win32PInvoke.Methods.cs b/src/Files.App/Helpers/Win32/Win32PInvoke.Methods.cs index 5d700736a2fa..e52a08638811 100644 --- a/src/Files.App/Helpers/Win32/Win32PInvoke.Methods.cs +++ b/src/Files.App/Helpers/Win32/Win32PInvoke.Methods.cs @@ -1,9 +1,8 @@ -// Copyright (c) 2024 Files Community -// Licensed under the MIT License. See the LICENSE. +// Copyright (c) Files Community +// Licensed under the MIT License. using System.IO; using System.Runtime.InteropServices; -using System.Runtime.InteropServices.ComTypes; using System.Text; using Windows.Win32.Foundation; using Windows.Win32.System.Com; @@ -232,17 +231,17 @@ public static extern bool WriteFileEx( [DllImport("api-ms-win-core-file-l1-2-1.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)] public static extern bool GetFileTime( [In] IntPtr hFile, - out FILETIME lpCreationTime, - out FILETIME lpLastAccessTime, - out FILETIME lpLastWriteTime + out System.Runtime.InteropServices.ComTypes.FILETIME lpCreationTime, + out System.Runtime.InteropServices.ComTypes.FILETIME lpLastAccessTime, + out System.Runtime.InteropServices.ComTypes.FILETIME lpLastWriteTime ); [DllImport("api-ms-win-core-file-l1-2-1.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)] public static extern bool SetFileTime( [In] IntPtr hFile, - in FILETIME lpCreationTime, - in FILETIME lpLastAccessTime, - in FILETIME lpLastWriteTime + in System.Runtime.InteropServices.ComTypes.FILETIME lpCreationTime, + in System.Runtime.InteropServices.ComTypes.FILETIME lpLastAccessTime, + in System.Runtime.InteropServices.ComTypes.FILETIME lpLastWriteTime ); [DllImport("api-ms-win-core-file-l2-1-1.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)] @@ -288,7 +287,7 @@ IntPtr hFindFile [DllImport("api-ms-win-core-timezone-l1-1-0.dll", SetLastError = true)] public static extern bool FileTimeToSystemTime( - ref FILETIME lpFileTime, + ref System.Runtime.InteropServices.ComTypes.FILETIME lpFileTime, out SYSTEMTIME lpSystemTime ); @@ -347,5 +346,9 @@ public static extern int SHGetKnownFolderPath( IntPtr hToken, out IntPtr pszPath ); + + // cryptui.dll + [DllImport("cryptui.dll", SetLastError = true, CharSet = CharSet.Auto)] + public unsafe static extern bool CryptUIDlgViewSignerInfo(CRYPTUI_VIEWSIGNERINFO_STRUCT* pViewInfo); } } diff --git a/src/Files.App/Helpers/Win32/Win32PInvoke.Structs.cs b/src/Files.App/Helpers/Win32/Win32PInvoke.Structs.cs index d54e6ee441e7..d1453d5cd285 100644 --- a/src/Files.App/Helpers/Win32/Win32PInvoke.Structs.cs +++ b/src/Files.App/Helpers/Win32/Win32PInvoke.Structs.cs @@ -3,7 +3,8 @@ using System.IO; using System.Runtime.InteropServices; -using System.Runtime.InteropServices.ComTypes; +using Windows.Win32.Foundation; +using Windows.Win32.Security.Cryptography; namespace Files.App.Helpers { @@ -90,9 +91,9 @@ public struct REPARSE_DATA_BUFFER public struct WIN32_FILE_ATTRIBUTE_DATA { public FileAttributes dwFileAttributes; - public FILETIME ftCreationTime; - public FILETIME ftLastAccessTime; - public FILETIME ftLastWriteTime; + public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime; + public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime; + public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime; public uint nFileSizeHigh; public uint nFileSizeLow; } @@ -183,9 +184,9 @@ public struct WIN32_FIND_DATA { public uint dwFileAttributes; - public FILETIME ftCreationTime; - public FILETIME ftLastAccessTime; - public FILETIME ftLastWriteTime; + public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime; + public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime; + public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime; public uint nFileSizeHigh; public uint nFileSizeLow; @@ -198,5 +199,37 @@ public struct WIN32_FIND_DATA [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] public string cAlternateFileName; } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct SignDataHandle + { + public uint dwObjSize; + public CMSG_SIGNER_INFO* pSignerInfo; + public HCERTSTORE hCertStoreHandle; + } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct CRYPTOAPI_BLOB + { + public uint cbData; + public void* pbData; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public unsafe struct CRYPTUI_VIEWSIGNERINFO_STRUCT + { + public uint dwSize; + public HWND hwndParent; + public uint dwFlags; + public PCSTR szTitle; + public CMSG_SIGNER_INFO* pSignerInfo; + public void* hMsg; + public PCSTR pszOID; + public uint? dwReserved; + public uint cStores; + public HCERTSTORE* rghStores; + public uint cPropPages; + public void* rgPropPages; + } } } diff --git a/src/Files.App/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw index fe935a3293a4..7c07b9676f9b 100644 --- a/src/Files.App/Strings/en-US/Resources.resw +++ b/src/Files.App/Strings/en-US/Resources.resw @@ -4273,6 +4273,27 @@ Filename + + Signatures + + + Signature list + + + Issued by: + + + Issued to: + + + Valid from: + + + Valid to: + + + No signature was found. + Show option to open folders in Windows Terminal diff --git a/src/Files.App/Utils/Signatures/DigitalSignaturesUtil.cs b/src/Files.App/Utils/Signatures/DigitalSignaturesUtil.cs new file mode 100644 index 000000000000..2c6a15b19bb1 --- /dev/null +++ b/src/Files.App/Utils/Signatures/DigitalSignaturesUtil.cs @@ -0,0 +1,990 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using System.Runtime.InteropServices; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Security.Cryptography; +using Windows.Win32.Security.WinTrust; +using static Files.App.Helpers.Win32PInvoke; + +namespace Files.App.Utils.Signatures +{ + public static class DigitalSignaturesUtil + { + // OIDs + private const string szOID_NESTED_SIGNATURE = "1.3.6.1.4.1.311.2.4.1"; + private const string szOID_RSA_counterSign = "1.2.840.113549.1.9.6"; + private const string szOID_RSA_signingTime = "1.2.840.113549.1.9.5"; + private const string szOID_RFC3161_counterSign = "1.3.6.1.4.1.311.3.3.1"; + private const string szOID_OIWSEC_sha1 = "1.3.14.3.2.26"; + private const string szOID_RSA_MD5 = "1.2.840.113549.2.5"; + private const string szOID_NIST_sha256 = "2.16.840.1.101.3.4.2.1"; + + // Flags + private const uint CERT_NAME_SIMPLE_DISPLAY_TYPE = 4; + private const uint CERT_SYSTEM_STORE_CURRENT_USER = 0x00010000; + private const uint PKCS_7_ASN_ENCODING = 0x00010000; + private const uint CRYPT_ASN_ENCODING = 0x00000001; + private const CERT_QUERY_ENCODING_TYPE ENCODING = + CERT_QUERY_ENCODING_TYPE.X509_ASN_ENCODING | CERT_QUERY_ENCODING_TYPE.PKCS_7_ASN_ENCODING; + + private const uint CMSG_SIGNER_INFO_PARAM = 6; + + // Version numbers + private const uint CERT_V1 = 0; + private const uint CERT_V2 = 1; + private const uint CERT_V3 = 2; + + private static readonly byte[] SG_ProtoCoded = [ + 0x30, 0x82 + ]; + + private static readonly byte[] SG_SignedData = [ + 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x07, 0x02 + ]; + + private static readonly IDateTimeFormatter formatter = Ioc.Default.GetRequiredService(); + + public static void LoadItemSignatures( + string filePath, + ObservableCollection signatures, + HWND hWnd, + CancellationToken ct) + { + var signChain = new List(); + GetSignerCertificateInfo(filePath, signChain, ct); + + foreach (var signNode in signChain) + { + if (signNode.CertChain.Count == 0) + continue; + + var signatureInfo = new SignatureInfoItem(filePath, signNode.Index, hWnd, signNode.CertChain) + { + Version = signNode.Version, + IssuedBy = signNode.CertChain[0].IssuedBy, + IssuedTo = signNode.CertChain[0].IssuedTo, + ValidFromTimestamp = signNode.CertChain[0].ValidFrom, + ValidToTimestamp = signNode.CertChain[0].ValidTo, + VerifiedTimestamp = signNode.CounterSign.TimeStamp, + Verified = signNode.IsValid, + }; + signatures.Add(signatureInfo); + } + } + + public unsafe static void DisplaySignerInfoDialog(string filePath, HWND hwndParent, int index) + { + if (string.IsNullOrEmpty(filePath)) + return; + + void* hAuthCryptMsg = null; + var signHandle = new SignDataHandle(); + var signDataChain = new List(); + + try + { + var result = TryGetSignerInfo( + filePath, + out hAuthCryptMsg, + out signHandle.hCertStoreHandle, + out signHandle.pSignerInfo, + out signHandle.dwObjSize + ); + if (!result || signHandle.pSignerInfo is null) + return; + + signDataChain.Add(signHandle); + GetNestedSignerInfo(ref signHandle, signDataChain); + if (index >= signDataChain.Count) + return; + + signHandle = signDataChain[index]; + var issuer = signHandle.pSignerInfo->Issuer; + var pCertContext = PInvoke.CertFindCertificateInStore( + signHandle.hCertStoreHandle, + ENCODING, + 0, + CERT_FIND_FLAGS.CERT_FIND_ISSUER_NAME, + &issuer, + null + ); + if (pCertContext is null) + return; + + var viewInfo = new CRYPTUI_VIEWSIGNERINFO_STRUCT + { + dwSize = (uint)Marshal.SizeOf(), + hwndParent = hwndParent, + dwFlags = 0, + szTitle = (PCSTR)null, + pSignerInfo = signHandle.pSignerInfo, + hMsg = hAuthCryptMsg, + pszOID = (PCSTR)null, + dwReserved = null, + cStores = 1, + rghStores = (HCERTSTORE*)NativeMemory.Alloc((uint)sizeof(void*)), + cPropPages = 0, + rgPropPages = null + }; + *(viewInfo.rghStores) = signHandle.hCertStoreHandle; + + result = CryptUIDlgViewSignerInfo(&viewInfo); + + PInvoke.CertFreeCertificateContext(pCertContext); + } + finally + { + // Since signDataChain contains nested signatures, + // you must release them starting from the last one. + for (int i = signDataChain.Count - 1; i >= 0; i--) + { + if (signDataChain[i].pSignerInfo is not null) + NativeMemory.Free(signDataChain[i].pSignerInfo); + + if (!signDataChain[i].hCertStoreHandle.IsNull) + PInvoke.CertCloseStore(signDataChain[i].hCertStoreHandle, 0); + } + + if (hAuthCryptMsg is not null) + PInvoke.CryptMsgClose(hAuthCryptMsg); + } + } + + private unsafe static bool GetSignerSignatureInfo( + HCERTSTORE hSystemStore, + HCERTSTORE hCertStore, + CERT_CONTEXT* pOrigContext, + ref CERT_CONTEXT* pCurrContext, + SignNodeInfo signNode) + { + var pCertInfo = pCurrContext->pCertInfo; + var certNode = new CertNodeInfoItem(); + + (_, certNode.Version) = CalculateSignVersion(pCertInfo->dwVersion); + GetStringFromCertContext(pCurrContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, certNode); + GetStringFromCertContext(pCurrContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, 1, certNode); + + var pft = &(pCertInfo->NotBefore); + certNode.ValidFrom = TimeToString(pft); + pft = &(pCertInfo->NotAfter); + certNode.ValidTo = TimeToString(pft); + + signNode.CertChain.Add(certNode); + + pCurrContext = PInvoke.CertFindCertificateInStore( + hCertStore, + ENCODING, + 0, + CERT_FIND_FLAGS.CERT_FIND_SUBJECT_NAME, + &(pCertInfo->Issuer), + null + ); + + if (pCurrContext is null) + { + pCurrContext = PInvoke.CertFindCertificateInStore( + hSystemStore, + ENCODING, + 0, + CERT_FIND_FLAGS.CERT_FIND_SUBJECT_NAME, + &(pCertInfo->Issuer), + null + ); + } + + if (pCurrContext is null) + return false; + + var result = PInvoke.CertComparePublicKeyInfo( + ENCODING, + &pCurrContext->pCertInfo->SubjectPublicKeyInfo, + &pOrigContext->pCertInfo->SubjectPublicKeyInfo + ); + + return !result; + } + + private unsafe static bool GetSignerCertificateInfo(string fileName, List signChain, CancellationToken ct) + { + var succeded = false; + var authSignData = new SignDataHandle() { dwObjSize = 0, hCertStoreHandle = HCERTSTORE.Null, pSignerInfo = null }; + var signDataChain = new List(); + signChain.Clear(); + + var cert_store_prov_system = (PCSTR)(byte*)10; + var root = "Root"; + var pRoot = &root; + var hSystemStore = PInvoke.CertOpenStore( + cert_store_prov_system, + ENCODING, + HCRYPTPROV_LEGACY.Null, + (CERT_OPEN_STORE_FLAGS)CERT_SYSTEM_STORE_CURRENT_USER, + pRoot + ); + if (hSystemStore == IntPtr.Zero) + return false; + + void* hAuthCryptMsg = null; + var result = TryGetSignerInfo( + fileName, + out hAuthCryptMsg, + out authSignData.hCertStoreHandle, + out authSignData.pSignerInfo, + out authSignData.dwObjSize + ); + + if (hAuthCryptMsg is not null) + { + PInvoke.CryptMsgClose(hAuthCryptMsg); + hAuthCryptMsg = null; + } + + if (!result) + { + if (authSignData.hCertStoreHandle != IntPtr.Zero) + PInvoke.CertCloseStore(authSignData.hCertStoreHandle, 0); + + PInvoke.CertCloseStore(hSystemStore, 0); + return false; + } + + signDataChain.Add(authSignData); + GetNestedSignerInfo(ref authSignData, signDataChain); + + for (var i = 0; i < signDataChain.Count; i++) + { + if (ct.IsCancellationRequested) + { + PInvoke.CertCloseStore(hSystemStore, 0); + return false; + } + + CERT_CONTEXT* pCurrContext = null; + CMSG_SIGNER_INFO* pCounterSigner = null; + var signNode = new SignNodeInfo(); + + GetCounterSignerInfo(signDataChain[i].pSignerInfo, &pCounterSigner); + if (pCounterSigner is not null) + GetCounterSignerData(pCounterSigner, signNode.CounterSign); + else + GetGeneralizedTimeStamp(signDataChain[i].pSignerInfo, signNode.CounterSign); + + var pszObjId = signDataChain[i].pSignerInfo->HashAlgorithm.pszObjId; + var szObjId = new string((sbyte*)(byte*)pszObjId); + CalculateDigestAlgorithm(szObjId, signNode); + (_, signNode.Version) = CalculateSignVersion(signDataChain[i].pSignerInfo->dwVersion); + + + var pIssuer = &(signDataChain[i].pSignerInfo->Issuer); + pCurrContext = PInvoke.CertFindCertificateInStore( + signDataChain[i].hCertStoreHandle, + ENCODING, + 0, + CERT_FIND_FLAGS.CERT_FIND_ISSUER_NAME, + pIssuer, + null + ); + + result = pCurrContext is not null; + while (result) + { + var pOrigContext = pCurrContext; + result = GetSignerSignatureInfo( + hSystemStore, + signDataChain[i].hCertStoreHandle, + pOrigContext, + ref pCurrContext, + signNode + ); + PInvoke.CertFreeCertificateContext(pOrigContext); + } + + if (pCurrContext is not null) + PInvoke.CertFreeCertificateContext(pCurrContext); + + if (pCounterSigner is not null) + NativeMemory.Free(pCounterSigner); + + if (signDataChain[i].pSignerInfo is not null) + NativeMemory.Free(signDataChain[i].pSignerInfo); + + if (!signDataChain[i].hCertStoreHandle.IsNull) + PInvoke.CertCloseStore(signDataChain[i].hCertStoreHandle, 0); + + succeded = true; + signNode.IsValid = VerifyySignature(fileName); + signNode.Index = i; + signChain.Add(signNode); + } + + PInvoke.CertCloseStore(hSystemStore, 0); + return succeded; + } + + private unsafe static bool VerifyySignature(string certPath) + { + int res = 1; + var sFileInfo = (uint)Marshal.SizeOf(); + var sData = (uint)Marshal.SizeOf(); + var actionGuid = new Guid("{00AAC56B-CD44-11D0-8CC2-00C04FC295EE}"); + + fixed (char* pCertPath = certPath) + { + var fileInfo = new WINTRUST_FILE_INFO + { + cbStruct = sFileInfo, + pcwszFilePath = (PCWSTR)pCertPath, + hFile = (HANDLE)null, + pgKnownSubject = null + }; + + var wintrustData = new WINTRUST_DATA + { + cbStruct = sData, + pPolicyCallbackData = null, + pSIPClientData = null, + dwUIChoice = WINTRUST_DATA_UICHOICE.WTD_UI_NONE, + fdwRevocationChecks = 0, // No revocation checking + dwUnionChoice = WINTRUST_DATA_UNION_CHOICE.WTD_CHOICE_FILE, + dwStateAction = WINTRUST_DATA_STATE_ACTION.WTD_STATEACTION_VERIFY, + hWVTStateData = (HANDLE)null, + pwszURLReference = null, + dwUIContext = 0, + Anonymous = new WINTRUST_DATA._Anonymous_e__Union + { + pFile = &fileInfo, + }, + }; + + res = PInvoke.WinVerifyTrust((HWND)null, ref actionGuid, &wintrustData); + + // Release hWVTStateData + wintrustData.dwStateAction = WINTRUST_DATA_STATE_ACTION.WTD_STATEACTION_CLOSE; + PInvoke.WinVerifyTrust((HWND)null, ref actionGuid, &wintrustData); + } + + return res == 0; + } + + private unsafe static bool TryGetSignerInfo( + string fileName, + out void* hMsg, + out HCERTSTORE hCertStore, + out CMSG_SIGNER_INFO* pSignerInfo, + out uint signerSize, + uint index = 0) + { + CERT_QUERY_ENCODING_TYPE encoding = 0; + CERT_QUERY_CONTENT_TYPE dummy = 0; + CERT_QUERY_FORMAT_TYPE dummy2 = 0; + void* pDummy = null; + BOOL result = false; + + HCERTSTORE hCertStoreTmp = HCERTSTORE.Null; + void* hMsgTmp = null; + + fixed (char* pFileName = fileName) + { + result = PInvoke.CryptQueryObject( + CERT_QUERY_OBJECT_TYPE.CERT_QUERY_OBJECT_FILE, + pFileName, + CERT_QUERY_CONTENT_TYPE_FLAGS.CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED, + CERT_QUERY_FORMAT_TYPE_FLAGS.CERT_QUERY_FORMAT_FLAG_BINARY, + 0, + &encoding, + &dummy, + &dummy2, + &hCertStoreTmp, + &hMsgTmp, + &pDummy + ); + } + + hCertStore = hCertStoreTmp; + hMsg = hMsgTmp; + pSignerInfo = null; + signerSize = 0; + + if (!result) + return false; + + var vpSignerInfo = (void*)pSignerInfo; + result = CustomCryptMsgGetParam( + hMsg, + CMSG_SIGNER_INFO_PARAM, + index, + ref vpSignerInfo, + ref signerSize + ); + pSignerInfo = (CMSG_SIGNER_INFO*)vpSignerInfo; + + return result; + } + + private unsafe static bool GetCounterSignerInfo( + CMSG_SIGNER_INFO* pSignerInfo, + CMSG_SIGNER_INFO** pTargetSigner) + { + uint objSize = 0; + if (pSignerInfo is null || pTargetSigner is null) + return false; + + try + { + *pTargetSigner = null; + CRYPT_ATTRIBUTE* attr = null; + var res = TryGetUnauthAttr(pSignerInfo, szOID_RSA_counterSign, ref attr); + if (!res || attr is null) + return false; + + var pkcs7_signer_info = (PCSTR)(byte*)500; + var result = PInvoke.CryptDecodeObject( + ENCODING, + pkcs7_signer_info, + attr->rgValue[0].pbData, + attr->rgValue[0].cbData, + 0, + null, + &objSize + ); + if (!result) + return false; + + *pTargetSigner = (CMSG_SIGNER_INFO*)NativeMemory.Alloc(objSize); + if (*pTargetSigner is null) + return false; + + result = PInvoke.CryptDecodeObject( + ENCODING, + pkcs7_signer_info, + attr->rgValue[0].pbData, + attr->rgValue[0].cbData, + 0, + *pTargetSigner, + &objSize + ); + if (!result) + return false; + } + finally + { + } + + return true; + } + + private unsafe static bool GetCounterSignerData(CMSG_SIGNER_INFO* pSignerInfo, SignCounterSign counterSign) + { + CRYPT_ATTRIBUTE* attr = null; + var res = TryGetAuthAttr(pSignerInfo, szOID_RSA_signingTime, ref attr); + if (!res || attr is null) + return false; + + var data = (uint)Marshal.SizeOf(); + var ft = (System.Runtime.InteropServices.ComTypes.FILETIME*)NativeMemory.Alloc(data); + try + { + var pkcs_utc_time = (PCSTR)(byte*)17; + var result = PInvoke.CryptDecodeObject( + ENCODING, + pkcs_utc_time, + attr->rgValue[0].pbData, + attr->rgValue[0].cbData, + 0, + ft, + &data + ); + if (!result) + return false; + + PInvoke.FileTimeToLocalFileTime(*ft, out var lft); + PInvoke.FileTimeToSystemTime(lft, out var st); + counterSign.TimeStamp = TimeToString(null, &st); + + return true; + } + finally + { + NativeMemory.Free(ft); + } + } + + private unsafe static bool ParseDERFindType( + int typeSearch, + byte* pbSignature, + uint size, + ref uint positionFound, + ref uint lengthFound) + { + uint position = 0; + uint sizeFound = 0; + uint bytesParsed = 0; + var iType = 0; + var iClass = 0; + positionFound = 0; + lengthFound = 0; + if (pbSignature is null) + return false; + + while (size > position) + { + if (!SafeToReadNBytes(size, position, 2)) + return false; + + ParseDERType(pbSignature[position], ref iType, ref iClass); + switch (iType) + { + case 0x05: // Null + ++position; + if (pbSignature[position] != 0x00) + return false; + + ++position; + break; + + case 0x06: // Object Identifier + ++position; + if (!SafeToReadNBytes(size - position, 1, pbSignature[position])) + return false; + + position += 1u + pbSignature[position]; + break; + + case 0x00: // ? + case 0x01: // Boolean + case 0x02: // Integer + case 0x03: // Bit String + case 0x04: // Octet String + case 0x0A: // enumerated + case 0x0C: // UTF8string + case 0x13: // printable string + case 0x14: // T61 string + case 0x16: // IA5String + case 0x17: // UTC time + case 0x18: // Generalized time + case 0x1E: // BMPstring + ++position; + if (!ParseDERSize( + pbSignature + position, + size - position, + ref sizeFound, + ref bytesParsed)) + { + return false; + } + + position += bytesParsed; + if (!SafeToReadNBytes(size - position, 0, sizeFound)) + return false; + + if (typeSearch == iType) + { + positionFound = position; + lengthFound = sizeFound; + + return true; + } + + position += sizeFound; + break; + + case 0x20: // context specific + case 0x21: // context specific + case 0x23: // context specific + case 0x24: // context specific + case 0x30: // sequence + case 0x31: // set + position++; + if (!ParseDERSize( + pbSignature + position, + size - position, + ref sizeFound, + ref bytesParsed)) + { + return false; + } + + position += bytesParsed; + break; + + case 0x22: // ? + position += 2; + break; + + default: + return false; + } + } + + return false; + } + + private unsafe static bool GetGeneralizedTimeStamp( + CMSG_SIGNER_INFO* pSignerInfo, + SignCounterSign counter) + { + uint positionFound = 0; + uint lengthFound = 0; + CRYPT_ATTRIBUTE* attr = null; + var res = TryGetUnauthAttr(pSignerInfo, szOID_RFC3161_counterSign, ref attr); + if (!res || attr is null) + return false; + + var result = ParseDERFindType( + 0x04, + attr->rgValue[0].pbData, + attr->rgValue[0].cbData, + ref positionFound, + ref lengthFound); + if (!result) + return false; + + // Counter Signer Timstamp + var pbOctetString = attr->rgValue[0].pbData + positionFound; + counter.TimeStamp = GetTimeStampFromDER(pbOctetString, lengthFound, ref positionFound); + + return true; + } + + private unsafe static string GetTimeStampFromDER(byte* pbOctetString, uint lengthFound, ref uint positionFound) + { + var result = ParseDERFindType( + 0x18, + pbOctetString, + lengthFound, + ref positionFound, + ref lengthFound + ); + if (!result) + return string.Empty; + + var st = new Windows.Win32.Foundation.SYSTEMTIME(); + var buffer = new string((sbyte*)(pbOctetString + positionFound)); + + _ = ushort.TryParse(buffer.AsSpan(0, 4), out st.wYear); + _ = ushort.TryParse(buffer.AsSpan(4, 2), out st.wMonth); + _ = ushort.TryParse(buffer.AsSpan(6, 2), out st.wDay); + _ = ushort.TryParse(buffer.AsSpan(8, 2), out st.wHour); + _ = ushort.TryParse(buffer.AsSpan(10, 2), out st.wMinute); + _ = ushort.TryParse(buffer.AsSpan(12, 2), out st.wSecond); + _ = ushort.TryParse(buffer.AsSpan(15, 3), out st.wMilliseconds); + + PInvoke.SystemTimeToFileTime(st, out var fft); + PInvoke.FileTimeToLocalFileTime(fft, out var lft); + PInvoke.FileTimeToSystemTime(lft, out var lst); + var timestamp = TimeToString(null, &lst); + + return timestamp; + } + + private unsafe static bool GetStringFromCertContext(CERT_CONTEXT* pCertContext, uint dwType, uint flag, CertNodeInfoItem info) + { + var data = PInvoke.CertGetNameString(pCertContext, dwType, flag, null, (PWSTR)null, 0); + if (data == 0) + { + PInvoke.CertFreeCertificateContext(pCertContext); + return false; + } + + var pszTempName = (PWSTR)NativeMemory.Alloc(data * sizeof(char)); + if (pszTempName.Value is null) + { + PInvoke.CertFreeCertificateContext(pCertContext); + NativeMemory.Free(pszTempName); + return false; + } + + data = PInvoke.CertGetNameString(pCertContext, dwType, flag, null, pszTempName, data); + if (data == 0) + { + NativeMemory.Free(pszTempName); + return false; + } + + var name = pszTempName.AsSpan().ToString(); + NativeMemory.Free(pszTempName); + if (flag == 0) + info.IssuedTo = StripString(name); + else + info.IssuedBy = StripString(name); + + return true; + } + + private unsafe static bool TryGetUnauthAttr(CMSG_SIGNER_INFO* pSignerInfo, string oid, ref CRYPT_ATTRIBUTE* attr) + { + int n = 0; + attr = null; + for (; n < pSignerInfo->UnauthAttrs.cAttr; n++) + { + attr = &pSignerInfo->UnauthAttrs.rgAttr[n]; + var objId = new string((sbyte*)(byte*)attr->pszObjId); + if (objId == oid) + break; + } + + return n < pSignerInfo->UnauthAttrs.cAttr; + } + + private unsafe static bool TryGetAuthAttr(CMSG_SIGNER_INFO* pSignerInfo, string oid, ref CRYPT_ATTRIBUTE* attr) + { + int n = 0; + attr = null; + for (; n < pSignerInfo->AuthAttrs.cAttr; n++) + { + attr = &pSignerInfo->AuthAttrs.rgAttr[n]; + var objId = new string((sbyte*)(byte*)attr->pszObjId); + if (objId == oid) + break; + } + + return n < pSignerInfo->AuthAttrs.cAttr; + } + + private unsafe static bool GetNestedSignerInfo(ref SignDataHandle AuthSignData, List NestedChain) + { + var succeded = false; + void* hNestedMsg = null; + if (AuthSignData.pSignerInfo is null) + return false; + + try + { + CRYPT_ATTRIBUTE* attr = null; + var res = TryGetUnauthAttr(AuthSignData.pSignerInfo, szOID_NESTED_SIGNATURE, ref attr); + if (!res || attr is null) + return false; + + var cbCurrData = attr->rgValue[0].cbData; + var pbCurrData = attr->rgValue[0].pbData; + var upperBound = AuthSignData.pSignerInfo + AuthSignData.dwObjSize; + while (pbCurrData > AuthSignData.pSignerInfo && pbCurrData < upperBound) + { + var nestedHandle = new SignDataHandle() { dwObjSize = 0, pSignerInfo = null, hCertStoreHandle = HCERTSTORE.Null }; + if (!Memcmp(pbCurrData, SG_ProtoCoded) || + !Memcmp(pbCurrData + 6, SG_SignedData)) + { + break; + } + + hNestedMsg = PInvoke.CryptMsgOpenToDecode( + PKCS_7_ASN_ENCODING | CRYPT_ASN_ENCODING, + 0, + 0, + HCRYPTPROV_LEGACY.Null, + null, + null + ); + if (hNestedMsg is null) + return false; + + cbCurrData = XCHWordLitend(*(ushort*)(pbCurrData + 2)) + 4u; + var pbNextData = pbCurrData; + pbNextData += EightByteAlign(cbCurrData, (long)pbCurrData); + var result = PInvoke.CryptMsgUpdate(hNestedMsg, pbCurrData, cbCurrData, true); + pbCurrData = pbNextData; + if (!result) + continue; + + var pSignerInfo = (void*)nestedHandle.pSignerInfo; + result = CustomCryptMsgGetParam( + hNestedMsg, + CMSG_SIGNER_INFO_PARAM, + 0, + ref pSignerInfo, + ref nestedHandle.dwObjSize + ); + nestedHandle.pSignerInfo = (CMSG_SIGNER_INFO*)pSignerInfo; + if (!result) + continue; + + var cert_store_prov_msg = (PCSTR)(byte*)1; + nestedHandle.hCertStoreHandle = PInvoke.CertOpenStore( + cert_store_prov_msg, + ENCODING, + HCRYPTPROV_LEGACY.Null, + 0, + hNestedMsg + ); + + succeded = true; + NestedChain.Add(nestedHandle); + } + } + finally + { + if (hNestedMsg is not null) + PInvoke.CryptMsgClose(hNestedMsg); + } + + return succeded; + } + + private unsafe static bool CustomCryptMsgGetParam( + void* hCryptMsg, + uint paramType, + uint index, + ref void* pParam, + ref uint outSize) + { + bool result; + uint size = 0; + + result = PInvoke.CryptMsgGetParam( + hCryptMsg, + paramType, + index, + null, + ref size + ); + if (!result) + return false; + + pParam = NativeMemory.Alloc(size); + if (pParam is null) + return false; + + result = PInvoke.CryptMsgGetParam( + hCryptMsg, + paramType, + index, + pParam, + ref size + ); + if (!result) + return false; + + outSize = size; + return true; + } + + private static ushort XCHWordLitend(uint num) + => (ushort)(((((ushort)num) & 0xFF00) >> 8) | (((ushort)num) & 0x00FF) << 8); + + private static long EightByteAlign(long offset, long b) + => ((offset + b + 7) & 0xFFFFFFF8L) - (b & 0xFFFFFFF8L); + + private unsafe static bool Memcmp(byte* ptr1, byte[] arr) + { + for (var i = 0; i < arr.Length; i++) + { + if (ptr1[i] != arr[i]) + return false; + } + + return true; + } + + private static (bool, string) CalculateSignVersion(uint versionNumber) + { + var res = versionNumber switch + { + CERT_V1 => "V1", + CERT_V2 => "V2", + CERT_V3 => "V3", + _ => "Unknown", + }; + return (true, res); + } + + private static bool CalculateDigestAlgorithm(string pszObjId, SignNodeInfo info) + { + if (string.IsNullOrWhiteSpace(pszObjId)) + info.DigestAlgorithm = "Unknown"; + else if (pszObjId == szOID_OIWSEC_sha1) + info.DigestAlgorithm = "SHA1"; + else if (pszObjId == szOID_RSA_MD5) + info.DigestAlgorithm = "MD5"; + else if (pszObjId == szOID_NIST_sha256) + info.DigestAlgorithm = "SHA256"; + else + info.DigestAlgorithm = StripString(pszObjId); + + return true; + } + + private static bool SafeToReadNBytes(uint size, uint start, uint requestSize) + => size - start >= requestSize; + + private static void ParseDERType(byte bIn, ref int iType, ref int iClass) + { + iType = bIn & 0x3F; + iClass = bIn >> 6; + } + + private unsafe static uint ReadNumberFromNBytes(byte* pbSignature, uint start, uint requestSize) + { + uint number = 0; + for (var i = 0; i < requestSize; i++) + number = number * 0x100 + pbSignature[start + i]; + + return number; + } + + private unsafe static bool ParseDERSize(byte* pbSignature, uint size, ref uint sizeFound, ref uint bytesParsed) + { + if (pbSignature[0] > 0x80 && !SafeToReadNBytes(size, 1, pbSignature[0] - 0x80u)) + return false; + + if (pbSignature[0] <= 0x80) + { + sizeFound = pbSignature[0]; + bytesParsed = 1; + } + else + { + sizeFound = ReadNumberFromNBytes(pbSignature, 1, pbSignature[0] - 0x80u); + bytesParsed = pbSignature[0] - 0x80u + 1; + } + + return true; + } + + private static string StripString(string? str) + { + return str? + .Replace("\t", "")? + .Replace("\n", "")? + .Replace("\r", "")? + .Replace(((char)0).ToString(), "") ?? string.Empty; + } + + private unsafe static string TimeToString( + System.Runtime.InteropServices.ComTypes.FILETIME* pftIn, + Windows.Win32.Foundation.SYSTEMTIME* pstIn = null) + { + if (pstIn is null) + { + if (pftIn is null) + return string.Empty; + + PInvoke.FileTimeToSystemTime(*pftIn, out var sysTime); + pstIn = &sysTime; + } + + var date = new DateTime( + pstIn->wYear, pstIn->wMonth, pstIn->wDay, + pstIn->wHour, pstIn->wMinute, pstIn->wSecond + ); + + return formatter.ToLongLabel(date); + } + + class SignCounterSign + { + public string TimeStamp { get; set; } = string.Empty; + } + + class SignNodeInfo + { + public bool IsValid { get; set; } = false; + public string DigestAlgorithm { get; set; } = string.Empty; + public string Version { get; set; } = string.Empty; + public int Index { get; set; } = 0; + public SignCounterSign CounterSign { get; set; } = new(); + public List CertChain { get; set; } = []; + } + } +} diff --git a/src/Files.App/ViewModels/Properties/MainPropertiesViewModel.cs b/src/Files.App/ViewModels/Properties/MainPropertiesViewModel.cs index c95b9b60b056..c08207483881 100644 --- a/src/Files.App/ViewModels/Properties/MainPropertiesViewModel.cs +++ b/src/Files.App/ViewModels/Properties/MainPropertiesViewModel.cs @@ -40,7 +40,8 @@ public NavigationViewItemButtonStyleItem SelectedNavigationViewItem PropertiesNavigationViewItemType.Security => typeof(SecurityPage), PropertiesNavigationViewItemType.Customization => typeof(CustomizationPage), PropertiesNavigationViewItemType.Compatibility => typeof(CompatibilityPage), - PropertiesNavigationViewItemType.Hashes => typeof(HashesPage), + PropertiesNavigationViewItemType.Hashes => typeof(HashesPage), + PropertiesNavigationViewItemType.Signatures => typeof(SignaturesPage), _ => typeof(GeneralPage), }; diff --git a/src/Files.App/ViewModels/Properties/SignaturesViewModel.cs b/src/Files.App/ViewModels/Properties/SignaturesViewModel.cs new file mode 100644 index 000000000000..4dc1200a76e0 --- /dev/null +++ b/src/Files.App/ViewModels/Properties/SignaturesViewModel.cs @@ -0,0 +1,37 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Files.App.Utils.Signatures; +using Microsoft.UI.Windowing; +using Windows.Win32.Foundation; + +namespace Files.App.ViewModels.Properties +{ + public sealed partial class SignaturesViewModel : ObservableObject, IDisposable + { + private CancellationTokenSource _cancellationTokenSource; + + public ObservableCollection Signatures { get; set; } + + public bool NoSignatureFound => Signatures.Count == 0; + + public SignaturesViewModel(ListedItem item, AppWindow appWindow) + { + _cancellationTokenSource = new(); + Signatures = new(); + var hWnd = new HWND(Microsoft.UI.Win32Interop.GetWindowFromWindowId(appWindow.Id)); + Signatures.CollectionChanged += (s, e) => OnPropertyChanged(nameof(NoSignatureFound)); + DigitalSignaturesUtil.LoadItemSignatures( + item.ItemPath, + Signatures, + hWnd, + _cancellationTokenSource.Token + ); + } + + public void Dispose() + { + _cancellationTokenSource.Cancel(); + } + } +} diff --git a/src/Files.App/ViewModels/Settings/AboutViewModel.cs b/src/Files.App/ViewModels/Settings/AboutViewModel.cs index 13ae41d4f00b..e84cc061182a 100644 --- a/src/Files.App/ViewModels/Settings/AboutViewModel.cs +++ b/src/Files.App/ViewModels/Settings/AboutViewModel.cs @@ -79,6 +79,7 @@ public AboutViewModel() new ("https://github.com/microsoft/CsWinRT", "CsWinRT"), new ("https://github.com/GihanSoft/NaturalStringComparer", "NaturalStringComparer"), new ("https://github.com/dongle-the-gadget/GuidRVAGen", "Dongle.GuidRVAGen"), + new ("https://github.com/leeqwind/PESignAnalyzer", "PESignAnalyzer"), ]; CopyAppVersionCommand = new RelayCommand(CopyAppVersion); diff --git a/src/Files.App/Views/Properties/SignaturesPage.xaml b/src/Files.App/Views/Properties/SignaturesPage.xaml new file mode 100644 index 000000000000..37c51a6ac4ea --- /dev/null +++ b/src/Files.App/Views/Properties/SignaturesPage.xaml @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +