diff --git a/src/Microsoft.PowerShell.Commands.Management/commands/management/TimeZoneCommands.cs b/src/Microsoft.PowerShell.Commands.Management/commands/management/TimeZoneCommands.cs new file mode 100644 index 0000000000..abac5b70ab --- /dev/null +++ b/src/Microsoft.PowerShell.Commands.Management/commands/management/TimeZoneCommands.cs @@ -0,0 +1,862 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Management.Automation; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Globalization; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.PowerShell.Commands +{ + /// + /// A cmdlet to retrieve time zone information. + /// + [Cmdlet(VerbsCommon.Get, "TimeZone", DefaultParameterSetName = "Name", + HelpUri = "http://go.microsoft.com/fwlink/?LinkId=799468")] + [Alias("gtz")] + public class GetTimeZoneCommand : PSCmdlet + { + #region Parameters + + /// + /// A list of the local time zone ids that the cmdlet should look up. + /// + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Id")] + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] + public string[] Id { get; set; } + + /// + /// Specifies that the cmdlet should produce a collection of the + /// TimeZoneInfo objects that are available on the system. + /// + [Parameter(Mandatory = true, ParameterSetName = "ListAvailable")] + public SwitchParameter ListAvailable { get; set; } + + /// + /// A list of the local time zone names that the cmdlet should look up. + /// + [Parameter(Position = 0, ValueFromPipeline = true, ParameterSetName = "Name")] + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] + public string[] Name { get; set; } + + #endregion Parameters + + /// + /// Implementation of the ProcessRecord method for Get-TimeZone + /// + protected override void ProcessRecord() + { + // make sure we've got the latest time zone settings + TimeZoneHelper.ClearCachedData(); + + if (this.ParameterSetName.Equals("ListAvailable", StringComparison.OrdinalIgnoreCase)) + { + // output the list of all available time zones + WriteObject(TimeZoneInfo.GetSystemTimeZones(), true); + } + else if (this.ParameterSetName.Equals("Id", StringComparison.OrdinalIgnoreCase)) + { + // lookup each time zone id + foreach (string tzid in Id) + { + try + { + WriteObject(TimeZoneInfo.FindSystemTimeZoneById(tzid)); + } +#if CORECLR + // TimeZoneNotFoundException is thrown by TimeZoneInfo, but not + // publicly visible (so can't be caught), so for now we're catching + // the parent exception time. This should be removed once the more + // specific exception is available. + catch (Exception e) +#else + catch (TimeZoneNotFoundException e) +#endif + { + WriteError(new ErrorRecord(e, TimeZoneHelper.TimeZoneNotFoundError, + ErrorCategory.InvalidArgument, "Id")); + } + } + } + else // ParameterSetName == "Name" + { + if (null != Name) + { + // lookup each time zone name (or wildcard pattern) + foreach (string tzname in Name) + { + TimeZoneInfo[] timeZones = TimeZoneHelper.LookupSystemTimeZoneInfoByName(tzname); + if (0 < timeZones.Length) + { + // manually process each object in the array, so if there is only a single + // entry then the returned type is TimeZoneInfo and not TimeZoneInfo[], and + // it can be pipelined to Set-TimeZone more easily + foreach (TimeZoneInfo timeZone in timeZones) + { + WriteObject(timeZone); + } + } + else + { + string message = string.Format(CultureInfo.InvariantCulture, + TimeZoneResources.TimeZoneNameNotFound, tzname); +#if CORECLR + // Because .NET Core does not currently expose the TimeZoneNotFoundException + // we need to throw the more generic parent exception class for the time being. + // This should be removed once the correct exception class is available. + Exception e = new Exception(message); +#else + Exception e = new TimeZoneNotFoundException(message); +#endif + WriteError(new ErrorRecord(e, TimeZoneHelper.TimeZoneNotFoundError, + ErrorCategory.InvalidArgument, "Name")); + } + } + } + else + { + // return the current system local time zone + WriteObject(TimeZoneInfo.Local); + } + } + } + } + + + /// + /// A cmdlet to set the system's local time zone. + /// + [Cmdlet(VerbsCommon.Set, "TimeZone", + SupportsShouldProcess = true, + DefaultParameterSetName = "Name", + HelpUri = "http://go.microsoft.com/fwlink/?LinkId=799469")] + [Alias("stz")] + public class SetTimeZoneCommand : PSCmdlet + { + #region string constants + + private const string TimeZoneTarget = "Local System"; + + #endregion string constants + + #region Parameters + + /// + /// The name of the local time zone that the system should use. + /// + [Parameter(Mandatory = true, ParameterSetName = "Id", ValueFromPipelineByPropertyName = true)] + public string Id { get; set; } + + /// + /// A TimeZoneInfo object identifying the local time zone that the system should use. + /// + [Parameter(Mandatory = true, Position = 0, ParameterSetName = "InputObject", ValueFromPipeline = true)] + public TimeZoneInfo InputObject { get; set; } + + /// + /// The name of the local time zone that the system should use. + /// + [Parameter(Mandatory = true, Position = 0, ParameterSetName = "Name")] + public string Name { get; set; } + + /// + /// Request return of the new local time zone as a TimeZoneInfo object + /// + [Parameter] + public SwitchParameter PassThru { get; set; } + + #endregion Parameters + + /// + /// Implementation of the ProcessRecord method for Get-TimeZone + /// + [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly", Justification = "Since Name is not a parameter of this method, it confuses FXCop. It is the appropriate value for the exception.")] + protected override void ProcessRecord() + { + // make sure we've got fresh data, in case the requested time zone was added + // to the system (registry) after our process was started + TimeZoneHelper.ClearCachedData(); + + // acquire a TimeZoneInfo if one wasn't supplied. + if (this.ParameterSetName.Equals("Id", StringComparison.OrdinalIgnoreCase)) + { + try + { + InputObject = TimeZoneInfo.FindSystemTimeZoneById(Id); + } +#if CORECLR + // TimeZoneNotFoundException is thrown by TimeZoneInfo, but not + // publicly visible (so can't be caught), so for now we're catching + // the parent exception time. This should be removed once the more + // specific exception is available. + catch (Exception e) +#else + catch (TimeZoneNotFoundException e) +#endif + { + ThrowTerminatingError(new ErrorRecord( + e, + TimeZoneHelper.TimeZoneNotFoundError, + ErrorCategory.InvalidArgument, + "Id")); + } + } + else if (this.ParameterSetName.Equals("Name", StringComparison.OrdinalIgnoreCase)) + { + // lookup the time zone name and make sure we have one (and only one) match + TimeZoneInfo[] timeZones = TimeZoneHelper.LookupSystemTimeZoneInfoByName(Name); + if (0 == timeZones.Length) + { + string message = string.Format(CultureInfo.InvariantCulture, + TimeZoneResources.TimeZoneNameNotFound, Name); +#if CORECLR + // Because .NET Core does not currently expose the TimeZoneNotFoundException + // we need to throw the more generic parent exception class for the time being. + // This should be removed once the correct exception class is available. + Exception e = new Exception(message); +#else + Exception e = new TimeZoneNotFoundException(message); +#endif + ThrowTerminatingError(new ErrorRecord(e, + TimeZoneHelper.TimeZoneNotFoundError, + ErrorCategory.InvalidArgument, + "Name")); + } + else if (1 < timeZones.Length) + { + string message = string.Format(CultureInfo.InvariantCulture, + TimeZoneResources.MultipleMatchingTimeZones, Name); + ThrowTerminatingError(new ErrorRecord( + new PSArgumentException(message, "Name"), + TimeZoneHelper.MultipleMatchingTimeZonesError, + ErrorCategory.InvalidArgument, + "Name")); + } + else + { + InputObject = timeZones[0]; + } + } + else // ParameterSetName == "InputObject" + { + try + { + // a TimeZoneInfo object was supplied, so use it to make sure we can find + // a backing system time zone, otherwise it's an error condition + InputObject = TimeZoneInfo.FindSystemTimeZoneById(InputObject.Id); + } +#if CORECLR + // TimeZoneNotFoundException is thrown by TimeZoneInfo, but not + // publicly visible (so can't be caught), so for now we're catching + // the parent exception time. This should be removed once the more + // specific exception is available. + catch (Exception e) +#else + catch (TimeZoneNotFoundException e) +#endif + { + ThrowTerminatingError(new ErrorRecord( + e, + TimeZoneHelper.TimeZoneNotFoundError, + ErrorCategory.InvalidArgument, + "InputObject")); + } + } + + if (ShouldProcess(TimeZoneTarget)) + { + bool acquireAccess = false; + try + { + // check to see if permission to set the time zone is already enabled for this process + if (!HasAccess) + { + // acquire permissions to set the timezone + SetAccessToken(true); + acquireAccess = true; + } + } + catch (Win32Exception e) + { + ThrowTerminatingError(new ErrorRecord(e, + TimeZoneHelper.InsufficientPermissionsError, + ErrorCategory.PermissionDenied, null)); + } + + try + { + // construct and populate a new DYNAMIC_TIME_ZONE_INFORMATION structure + NativeMethods.DYNAMIC_TIME_ZONE_INFORMATION dtzi = new NativeMethods.DYNAMIC_TIME_ZONE_INFORMATION(); + dtzi.Bias -= (int)InputObject.BaseUtcOffset.TotalMinutes; + dtzi.StandardName = InputObject.StandardName; + dtzi.DaylightName = InputObject.DaylightName; + dtzi.TimeZoneKeyName = InputObject.Id; + + // Request time zone transition information for the current year + NativeMethods.TIME_ZONE_INFORMATION tzi = new NativeMethods.TIME_ZONE_INFORMATION(); + if (!NativeMethods.GetTimeZoneInformationForYear((ushort)DateTime.Now.Year, ref dtzi, ref tzi)) + { + ThrowWin32Error(); + } + + // copy over the transition times + dtzi.StandardBias = tzi.StandardBias; + dtzi.StandardDate = tzi.StandardDate; + dtzi.DaylightBias = tzi.DaylightBias; + dtzi.DaylightDate = tzi.DaylightDate; + + // set the new local time zone for the system + if (!NativeMethods.SetDynamicTimeZoneInformation(ref dtzi)) + { + ThrowWin32Error(); + } + +#if !CORECLR + // broadcast a WM_SETTINGCHANGE notification message to all top-level windows so that they + // know to update their notion of the current system time (and time zone) if applicable + int result = 0; + NativeMethods.SendMessageTimeout((IntPtr)NativeMethods.HWND_BROADCAST, NativeMethods.WM_SETTINGCHANGE, + (IntPtr)0, "intl", NativeMethods.SMTO_ABORTIFHUNG, 5000, ref result); +#endif + + // clear the time zone data or this PowerShell session + // will not recognize the new time zone settings + TimeZoneHelper.ClearCachedData(); + + if (PassThru.IsPresent) + { + // return the TimeZoneInfo object for the (new) current local time zone + WriteObject(TimeZoneInfo.Local); + } + } + catch (Win32Exception e) + { + ThrowTerminatingError(new ErrorRecord(e, + TimeZoneHelper.SetTimeZoneFailedError, + ErrorCategory.FromStdErr, null)); + } + finally + { + if (acquireAccess) + { + // reset the permissions + SetAccessToken(false); + } + } + } + else + { + if (PassThru.IsPresent) + { + // show the user the time zone settings that would have been used. + WriteObject(InputObject); + } + } + } + + #region Helper functions + + /// + /// True if the current process has access to change the time zone setting. + /// + protected bool HasAccess + { + get + { + bool hasAccess = false; + + // open the access token for the current process + IntPtr hToken = IntPtr.Zero; + IntPtr hProcess = NativeMethods.GetCurrentProcess(); + if (!NativeMethods.OpenProcessToken(hProcess, + NativeMethods.TOKEN_ADJUST_PRIVILEGES | NativeMethods.TOKEN_QUERY, ref hToken)) + { + ThrowWin32Error(); + } + + try + { + // setup the privileges being checked + NativeMethods.PRIVILEGE_SET ps = new NativeMethods.PRIVILEGE_SET() + { + PrivilegeCount = 1, + Control = 1, + Luid = 0, + Attributes = NativeMethods.SE_PRIVILEGE_ENABLED, + }; + + // lookup the Luid of the SeTimeZonePrivilege + if (!NativeMethods.LookupPrivilegeValue(null, NativeMethods.SE_TIME_ZONE_NAME, ref ps.Luid)) + { + ThrowWin32Error(); + } + + // set the privilege for the open access token + if (!NativeMethods.PrivilegeCheck(hToken, ref ps, ref hasAccess)) + { + ThrowWin32Error(); + } + } + finally + { + NativeMethods.CloseHandle(hToken); + } + + return (hasAccess); + } + } + + /// + /// Set the SeTimeZonePrivilege, which controls access to the SetDynamicTimeZoneInformation API + /// + /// Set to true to enable (or false to disable) the privilege. + protected void SetAccessToken(bool enable) + { + // open the access token for the current process + IntPtr hToken = IntPtr.Zero; + IntPtr hProcess = NativeMethods.GetCurrentProcess(); + if (!NativeMethods.OpenProcessToken(hProcess, + NativeMethods.TOKEN_ADJUST_PRIVILEGES | NativeMethods.TOKEN_QUERY, ref hToken)) + { + ThrowWin32Error(); + } + + try + { + // setup the privileges being requested + NativeMethods.TOKEN_PRIVILEGES tp = new NativeMethods.TOKEN_PRIVILEGES() + { + PrivilegeCount = 1, + Luid = 0, + Attributes = (enable ? NativeMethods.SE_PRIVILEGE_ENABLED : 0), + }; + + // lookup the Luid of the SeTimeZonePrivilege + if (!NativeMethods.LookupPrivilegeValue(null, NativeMethods.SE_TIME_ZONE_NAME, ref tp.Luid)) + { + ThrowWin32Error(); + } + + // set the privilege for the open access token + if (!NativeMethods.AdjustTokenPrivileges(hToken, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero)) + { + ThrowWin32Error(); + } + } + finally + { + NativeMethods.CloseHandle(hToken); + } + } + + /// + /// Get the Win32 error code from GetLastError and throw an exception. + /// + protected void ThrowWin32Error() + { + int error = Marshal.GetLastWin32Error(); + throw new Win32Exception(error); + } + + #endregion Helper functions + + #region Win32 interop helper + + internal class NativeMethods + { + /// + /// Private constructor to prevent instantiation + /// + private NativeMethods() + { + } + + #region Native DLL locations + +#if CORECLR + private const string SetDynamicTimeZoneApiDllName = "api-ms-win-core-timezone-l1-1-0.dll"; + private const string GetTimeZoneInformationForYearApiDllName = "api-ms-win-core-timezone-l1-1-0.dll"; + private const string GetCurrentProcessApiDllName = "api-ms-win-downlevel-kernel32-l1-1-0.dll"; + private const string OpenProcessTokenApiDllName = "api-ms-win-downlevel-advapi32-l1-1-1.dll"; + private const string LookupPrivilegeTokenApiDllName = "api-ms-win-downlevel-advapi32-l4-1-0.dll"; + private const string PrivilegeCheckApiDllName = "api-ms-win-downlevel-advapi32-l1-1-1.dll"; + private const string AdjustTokenPrivilegesApiDllName = "api-ms-win-downlevel-advapi32-l1-1-1.dll"; + private const string CloseHandleApiDllName = "api-ms-win-downlevel-kernel32-l1-1-0.dll"; + private const string SendMessageTimeoutApiDllName = "ext-ms-win-rtcore-ntuser-window-ext-l1-1-0.dll"; +#else + private const string SetDynamicTimeZoneApiDllName = "kernel32.dll"; + private const string GetTimeZoneInformationForYearApiDllName = "kernel32.dll"; + private const string GetCurrentProcessApiDllName = "kernel32.dll"; + private const string OpenProcessTokenApiDllName = "advapi32.dll"; + private const string LookupPrivilegeTokenApiDllName = "advapi32.dll"; + private const string PrivilegeCheckApiDllName = "advapi32.dll"; + private const string AdjustTokenPrivilegesApiDllName = "advapi32.dll"; + private const string CloseHandleApiDllName = "kernel32.dll"; + private const string SendMessageTimeoutApiDllName = "user32.dll"; +#endif + + #endregion Native DLL locations + + #region Win32 SetDynamicTimeZoneInformation imports + + /// + /// Used to marshal win32 SystemTime structure to managed code layer + /// + [StructLayout(LayoutKind.Sequential)] + public struct SystemTime + { + /// + /// The year. + /// + [MarshalAs(UnmanagedType.U2)] + public short Year; + /// + /// The month. + /// + [MarshalAs(UnmanagedType.U2)] + public short Month; + /// + /// The day of the week. + /// + [MarshalAs(UnmanagedType.U2)] + public short DayOfWeek; + /// + /// The day of the month. + /// + [MarshalAs(UnmanagedType.U2)] + public short Day; + /// + /// The hour. + /// + [MarshalAs(UnmanagedType.U2)] + public short Hour; + /// + /// The minute. + /// + [MarshalAs(UnmanagedType.U2)] + public short Minute; + /// + /// The second. + /// + [MarshalAs(UnmanagedType.U2)] + public short Second; + /// + /// The millisecond. + /// + [MarshalAs(UnmanagedType.U2)] + public short Milliseconds; + } + + /// + /// Used to marshal win32 DYNAMIC_TIME_ZONE_INFORMATION structure to managed code layer + /// + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct DYNAMIC_TIME_ZONE_INFORMATION + { + /// + /// The current bias for local time translation on this computer, in minutes. + /// + [MarshalAs(UnmanagedType.I4)] + public int Bias; + /// + /// A description for standard time. + /// + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)] + public string StandardName; + /// + /// A SystemTime structure that contains a date and local time when the transition from daylight saving time to standard time occurs on this operating system. + /// + public SystemTime StandardDate; + /// + /// The bias value to be used during local time translations that occur during standard time. + /// + [MarshalAs(UnmanagedType.I4)] + public int StandardBias; + /// + /// A description for daylight saving time (DST). + /// + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)] + public string DaylightName; + /// + /// A SystemTime structure that contains a date and local time when the transition from standard time to daylight saving time occurs on this operating system. + /// + public SystemTime DaylightDate; + /// + /// The bias value to be used during local time translations that occur during daylight saving time. + /// + [MarshalAs(UnmanagedType.I4)] + public int DaylightBias; + /// + /// The name of the time zone registry key on the local computer. + /// + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x80)] + public string TimeZoneKeyName; + /// + /// Indicates whether dynamic daylight saving time is disabled. + /// + [MarshalAs(UnmanagedType.U1)] + public bool DynamicDaylightTimeDisabled; + } + + /// + /// Used to marshal win32 TIME_ZONE_INFORMATION structure to managed code layer + /// + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct TIME_ZONE_INFORMATION + { + /// + /// The current bias for local time translation on this computer, in minutes. + /// + [MarshalAs(UnmanagedType.I4)] + public int Bias; + /// + /// A description for standard time. + /// + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)] + public string StandardName; + /// + /// A SystemTime structure that contains a date and local time when the transition from daylight saving time to standard time occurs on this operating system. + /// + public SystemTime StandardDate; + /// + /// The bias value to be used during local time translations that occur during standard time. + /// + [MarshalAs(UnmanagedType.I4)] + public int StandardBias; + /// + /// A description for daylight saving time (DST). + /// + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)] + public string DaylightName; + /// + /// A SystemTime structure that contains a date and local time when the transition from standard time to daylight saving time occurs on this operating system. + /// + public SystemTime DaylightDate; + /// + /// The bias value to be used during local time translations that occur during daylight saving time. + /// + [MarshalAs(UnmanagedType.I4)] + public int DaylightBias; + } + + /// + /// PInvoke SetDynamicTimeZoneInformation API + /// + /// A DYNAMIC_TIME_ZONE_INFORMATION structure representing the desired local time zone. + /// + [DllImport(SetDynamicTimeZoneApiDllName, SetLastError = true)] + public static extern bool SetDynamicTimeZoneInformation([In] ref DYNAMIC_TIME_ZONE_INFORMATION lpTimeZoneInformation); + + [DllImport(GetTimeZoneInformationForYearApiDllName, SetLastError = true)] + public static extern bool GetTimeZoneInformationForYear([In] ushort wYear, [In] ref DYNAMIC_TIME_ZONE_INFORMATION pdtzi, ref TIME_ZONE_INFORMATION ptzi); + + #endregion Win32 SetDynamicTimeZoneInformation imports + + #region Win32 AdjustTokenPrivilege imports + + /// + /// Definition of TOKEN_QUERY constant from Win32 API + /// + public const int TOKEN_QUERY = 0x00000008; + + /// + /// Definition of TOKEN_ADJUST_PRIVILEGES constant from Win32 API + /// + public const int TOKEN_ADJUST_PRIVILEGES = 0x00000020; + + /// + /// Definition of SE_PRIVILEGE_ENABLED constant from Win32 API + /// + public const int SE_PRIVILEGE_ENABLED = 0x00000002; + + /// + /// Definition of SE_TIME_ZONE_NAME constant from Win32 API + /// + public const string SE_TIME_ZONE_NAME = "SeTimeZonePrivilege"; //http://msdn.microsoft.com/en-us/library/bb530716(VS.85).aspx + + /// + /// PInvoke GetCurrentProcess API + /// + /// + [DllImport(GetCurrentProcessApiDllName, ExactSpelling = true)] + public static extern IntPtr GetCurrentProcess(); + + /// + /// PInvoke OpenProcessToken API + /// + /// + /// + /// + /// + [DllImport(OpenProcessTokenApiDllName, SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool OpenProcessToken(IntPtr ProcessHandle, int DesiredAccess, ref IntPtr TokenHandle); + + /// + /// PInvoke LookupPrivilegeValue API + /// + /// + /// + /// + /// + [DllImport(LookupPrivilegeTokenApiDllName, SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, ref long lpLuid); + + /// + /// PInvoke PrivilegeCheck API + /// + /// + /// + /// + /// + [DllImport(PrivilegeCheckApiDllName, SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool PrivilegeCheck(IntPtr ClientToken, ref PRIVILEGE_SET RequiredPrivileges, ref bool pfResult); + + + /// + /// PInvoke AdjustTokenPrivilege API + /// + /// + /// + /// + /// + /// + /// + /// + [DllImport(AdjustTokenPrivilegesApiDllName, SetLastError = true, CharSet = CharSet.Unicode, BestFitMapping = false)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool AdjustTokenPrivileges(IntPtr TokenHandle, bool DisableAllPrivilegs, + ref TOKEN_PRIVILEGES NewState, int BufferLength, IntPtr PreviousState, IntPtr ReturnLength); + + /// + /// PInvoke CloseHandle API + /// + /// + /// + [DllImport(CloseHandleApiDllName, ExactSpelling = true, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CloseHandle(IntPtr hObject); + + /// + /// Used to marshal win32 PRIVILEGE_SET structure to managed code layer + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct PRIVILEGE_SET + { + public int PrivilegeCount; + public int Control; + public long Luid; + public int Attributes; + } + + /// + /// Used to marshal win32 TOKEN_PRIVILEGES structure to managed code layer + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct TOKEN_PRIVILEGES + { + public int PrivilegeCount; + public long Luid; + public int Attributes; + } + + #endregion Win32 AdjustTokenPrivilege imports + + #region Win32 SendMessage imports + + /// + /// Definition of WM_SETTINGCHANGE constant from Win32 API + /// + public const int WM_SETTINGCHANGE = 0x001A; + + /// + /// Definition of HWND_BROADCAST constant from Win32 API + /// + public const int HWND_BROADCAST = (-1); + + /// + /// Definition of SMTO_ABORTIFHUNG constant from Win32 API + /// + public const int SMTO_ABORTIFHUNG = 0x0002; + + /// + /// PInvoke SendMessageTimeout API + /// + /// + /// + /// + /// + /// + /// + /// + /// + [DllImport(SendMessageTimeoutApiDllName, SetLastError = true, CharSet = CharSet.Unicode)] + public static extern IntPtr SendMessageTimeout(IntPtr hWnd, int Msg, IntPtr wParam, string lParam, int fuFlags, int uTimeout, ref int lpdwResult); + + #endregion Win32 SendMessage imports + } + + #endregion Win32 interop helper + } + + + /// + /// static Helper class for working with system time zones. + /// + internal static class TimeZoneHelper + { + #region Error Ids + + internal const string TimeZoneNotFoundError = "TimeZoneNotFound"; + internal const string MultipleMatchingTimeZonesError = "MultipleMatchingTimeZones"; + internal const string InsufficientPermissionsError = "InsufficientPermissions"; + internal const string SetTimeZoneFailedError = "SetTimeZoneFailed"; + + #endregion Error Ids + + /// + /// Clear the cached TimeZoneInfo data. Note that since TimeZoneInfo.ClearCachedData + /// does not exist in .NET Core there is an alternative mechanism (which doesn't seem + /// to affect clearing out the cache under the full desktop version of .NET) + /// + internal static void ClearCachedData() + { +#if CORECLR + // NOTE: .NET Core does not expose the ClearCachedData method, but executing + // the specific ConvertTime request below should have the same effect. + TimeZoneInfo.ConvertTime(new DateTime(0), TimeZoneInfo.Local); +#else + TimeZoneInfo.ClearCachedData(); +#endif + } + + /// + /// Find the system time zone by checking first against StandardName and then, + /// if no matches were found, against the DaylightName. + /// + /// The name (or wildcard pattern) of the system time zone to find. + /// A TimeZoneInfo object array containing information about the specified system time zones. + internal static TimeZoneInfo[] LookupSystemTimeZoneInfoByName(string name) + { + WildcardPattern namePattern = new WildcardPattern(name, WildcardOptions.IgnoreCase); + List tzi = new List(); + + // get the available system time zones + ReadOnlyCollection zones = TimeZoneInfo.GetSystemTimeZones(); + + // check against the standard and daylight names for each TimeZoneInfo + foreach (TimeZoneInfo zone in zones) + { + if (namePattern.IsMatch(zone.StandardName) || namePattern.IsMatch(zone.DaylightName)) + { + tzi.Add(zone); + } + } + + return (tzi.ToArray()); + } + } +} diff --git a/src/Microsoft.PowerShell.Commands.Management/resources/TimeZoneResources.resx b/src/Microsoft.PowerShell.Commands.Management/resources/TimeZoneResources.resx new file mode 100644 index 0000000000..1118c6c567 --- /dev/null +++ b/src/Microsoft.PowerShell.Commands.Management/resources/TimeZoneResources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Cannot set the local time zone because the name '{0}' resolves to multiple entries. + + + The time zone name '{0}' was not found on the local computer. + + \ No newline at end of file diff --git a/src/Microsoft.PowerShell.Security/security/CatalogCommands.cs b/src/Microsoft.PowerShell.Security/security/CatalogCommands.cs new file mode 100644 index 0000000000..4ccdd4386e --- /dev/null +++ b/src/Microsoft.PowerShell.Security/security/CatalogCommands.cs @@ -0,0 +1,282 @@ +/********************************************************************++ +Copyright (c) Microsoft Corporation. All rights reserved. +--********************************************************************/ + +using System; +using System.Management.Automation; +using Dbg=System.Management.Automation.Diagnostics; +using System.Collections; +using System.IO; +using System.Management.Automation.Provider; +using System.Runtime.InteropServices; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.PowerShell.Commands +{ + /// + /// Defines the base class from which all catalog commands are derived. + /// + public abstract class CatalogCommandsBase : PSCmdlet + { + /// + /// Path of folder/file to generate or validate the catalog file + /// + [Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "ByPath")] + public string CatalogFilePath + { + get + { + return catalogFilePath; + } + set + { + catalogFilePath = value; + } + } + private string catalogFilePath; + + /// + /// Path of folder/file to generate or validate the catalog file + /// + [Parameter(Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "ByPath")] + public string[] Path + { + get + { + return path; + } + set + { + path = value; + } + } + private string[] path; + // + // name of this command + // + string commandName; + + /// + /// Initializes a new instance of the CatalogCommandsBase class, + /// using the given command name. + /// + /// + /// The name of the command. + /// + protected CatalogCommandsBase(string name) : base() + { + commandName = name; + } + + private CatalogCommandsBase() : base() {} + + /// + /// Processes records from the input pipeline. + /// For each input object, the command either generate the Catalog or + /// Validates the existing Catalog + /// + protected override void ProcessRecord() + { + // + // this cannot happen as we have specified the Path + // property to be mandatory parameter + // + Dbg.Assert((CatalogFilePath != null) && (CatalogFilePath.Length > 0), + "CatalogCommands: Param binder did not bind catalogFilePath"); + + Collection paths = new Collection(); + + if (Path != null) + { + foreach (string p in Path) + { + foreach (PathInfo tempPath in SessionState.Path.GetResolvedPSPathFromPSPath(p)) + { + paths.Add(tempPath.ProviderPath); + } + } + } + + string drive = null; + + // resolve catalog destination Path + if (!SessionState.Path.IsPSAbsolute(catalogFilePath, out drive) && !System.IO.Path.IsPathRooted(catalogFilePath)) + { + catalogFilePath = SessionState.Path.GetUnresolvedProviderPathFromPSPath(catalogFilePath); + } + + PerformAction(paths, catalogFilePath); + } + + /// + /// Performs the action i.e. Generate or Validate the Windows Catalog File + /// + /// + /// The name of the Folder or file on which to perform the action. + /// + /// + /// Path to Catalog + /// + protected abstract void PerformAction(Collection path, string catalogFilePath); + } + + /// + /// Defines the implementation of the 'New-FileCatalog' cmdlet. + /// This cmdlet generates the catalog for File or Folder + /// + [Cmdlet(VerbsCommon.New, "FileCatalog", SupportsShouldProcess = true, DefaultParameterSetName = "ByPath", + HelpUri = "http://go.microsoft.com/fwlink/?LinkId=786749")] + [OutputType(typeof(FileInfo))] + public sealed class NewFileCatalogCommand : CatalogCommandsBase + { + /// + /// Initializes a new instance of the New-FileCatalog class. + /// + public NewFileCatalogCommand() : base("New-FileCatalog") { } + + /// + /// Catalog version + /// + [Parameter()] + public int CatalogVersion + { + get + { + return catalogVersion; + } + set + { + catalogVersion = value; + } + } + + // Based on the Catalog version we will decide which hashing Algorithm to use + private int catalogVersion = 1; + + /// + /// Generate the Catalog for the Path + /// + /// + /// File or Folder Path + /// + /// + /// Path to Catalog + /// + /// + /// True if able to Create Catalog or else False + /// + protected override void PerformAction(Collection path,string catalogFilePath) + { + if (path.Count == 0) + { + // if user has not provided the path use current directory to generate catalog + path.Add(SessionState.Path.CurrentFileSystemLocation.Path); + } + + FileInfo catalogFileInfo = new FileInfo(catalogFilePath); + + // If Path points to the expected cat file make sure + // parent Directory exists other wise CryptoAPI fails to create a .cat file + if (catalogFileInfo.Extension.Equals(".cat", StringComparison.Ordinal)) + { + System.IO.Directory.CreateDirectory(catalogFileInfo.Directory.FullName); + } + else + { + // This only creates Directory if it does not exists, Append a default name + System.IO.Directory.CreateDirectory(catalogFilePath); + catalogFilePath = System.IO.Path.Combine(catalogFilePath, "catalog.cat"); + } + + FileInfo catalogFile = CatalogHelper.GenerateCatalog(this, path, catalogFilePath, catalogVersion); + + if (catalogFile != null) + { + WriteObject(catalogFile); + } + } + } + + /// + /// Defines the implementation of the 'Test-FileCatalog' cmdlet. + /// This cmdlet validaes the Intgerity of catalog + /// + [Cmdlet(VerbsDiagnostic.Test, "FileCatalog", SupportsShouldProcess = true, DefaultParameterSetName = "ByPath", + HelpUri = "http://go.microsoft.com/fwlink/?LinkId=786750")] + [OutputType(typeof(CatalogValidationStatus))] + [OutputType(typeof(CatalogInformation))] + public sealed class TestFileCatalogCommand : CatalogCommandsBase + { + /// + /// Initializes a new instance of the New-FileCatalog class. + /// + public TestFileCatalogCommand() : base("Test-FileCatalog") { } + + /// + /// + [Parameter()] + public SwitchParameter Detailed + { + get { return detailed; } + set { detailed = value; } + } + private bool detailed = false; + + /// + /// Patterns used to exclude files from DiskPaths and Catalog + /// + [Parameter()] + public string[] FilesToSkip + { + get + { + return filesToSkip; + } + set + { + filesToSkip = value; + this.excludedPatterns = new WildcardPattern[filesToSkip.Length]; + for (int i = 0; i < filesToSkip.Length; i++) + { + this.excludedPatterns[i] = WildcardPattern.Get(filesToSkip[i], WildcardOptions.IgnoreCase); + } + } + } + string[] filesToSkip = null; + internal WildcardPattern[] excludedPatterns = null; + + /// + /// Validate the Integrity of given Catalog + /// + /// + /// File or Folder Path + /// + /// + /// Path to Catalog + /// + /// + /// True if able to Validate the Catalog and its not tampered or else False + /// + protected override void PerformAction(Collection path, string catalogFilePath) + { + if (path.Count == 0) + { + // if user has not provided the path use the path of catalog file itself. + path.Add(new FileInfo(catalogFilePath).Directory.FullName); + } + + CatalogInformation catalogInfo = CatalogHelper.ValidateCatalog(this, path, catalogFilePath, excludedPatterns); + + if (detailed) + { + WriteObject(catalogInfo); + } + else + { + WriteObject(catalogInfo.Status); + } + } + } + +} \ No newline at end of file diff --git a/src/System.Management.Automation/resources/CatalogStrings.resx b/src/System.Management.Automation/resources/CatalogStrings.resx new file mode 100644 index 0000000000..a9d5706f5e --- /dev/null +++ b/src/System.Management.Automation/resources/CatalogStrings.resx @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Unable to generate catalog defintion file. + + + Adding file '{0}' to the catalog. Relative path of file in catalog is '{1}'. + + + Skipping validating file {0} from catalog. + + + Found file {0} in the catalog with hash of {1}. + + + Paths for catalog contains multiple files with same relative path {0}. + + + Found file {0} on disk with hash of {1}. + + + Skipping validating file {0} from path. + + + Unable to acquire a handle to a catalog administrator context for a given hash algorithm {0}. + + + Unable to create the hash for file {0}. + + + Unable to open catalog file {0}. + + + Catalog version is not valid. We only support catalog version {0} and version {1}. + + + Unable to open catalog defintion file. + + + Found multiple entries of file member {0} in the catalog. + + + Unable to find file name or path for catalog member {0}. + + + Unable to find file {0} to hash. + + + Unable to read file {0} to calculate its hash. + + \ No newline at end of file diff --git a/src/System.Management.Automation/security/CatalogHelper.cs b/src/System.Management.Automation/security/CatalogHelper.cs new file mode 100644 index 0000000000..f052cbe488 --- /dev/null +++ b/src/System.Management.Automation/security/CatalogHelper.cs @@ -0,0 +1,846 @@ + +/********************************************************************++ +Copyright (c) Microsoft Corporation. All rights reserved. +--********************************************************************/ + +using Dbg = System.Management.Automation; +using System; +using System.Text; +using System.Security.Cryptography; +using System.Collections.Generic; +using System.Collections; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Management.Automation.Internal; +using System.Management.Automation.Provider; +using System.Management.Automation.Security; +using System.Runtime.InteropServices; +using DWORD = System.UInt32; + +namespace System.Management.Automation +{ + + /// + /// Defines the possible status when validating integrity of catalog + /// + public enum CatalogValidationStatus + { + /// + /// Status when catalog is not tampered. + /// + Valid, + + /// + /// Status when catalog is tampered. + /// + ValidationFailed + } + + /// + /// Object returned by Catalog Cmdlets + /// + public class CatalogInformation + { + /// + /// status of catalog + /// + public CatalogValidationStatus Status { get; set; } + + /// + /// Hash Algorithm used to calculate the hashes of files in Catalog + /// + public string HashAlgorithm { get; set; } + + /// + /// Dictionary mapping files relative paths to their hash values found from Catalog + /// + public Dictionary CatalogItems { get; set;} + + /// + /// Dictionary mapping files relative paths to their hash values + /// + public Dictionary PathItems { get; set;} + + /// + /// Signature for the catalog + /// + public Signature Signature { get; set; } + } + + /// + /// Helper functions for Windows Catalog functionality + /// + internal static class CatalogHelper + { + + // Catalog Version is (0X100 = 256) for Catalog Version 1 + private static int catalogVersion1 = 256; + + // Catalog Version is (0X200 = 512) for Catalog Version 2 + private static int catalogVersion2 = 512; + + // Hash Algorithms supported by Windows Catalog + private static string HashAlgorithmSHA1 = "SHA1"; + private static string HashAlgorithmSHA256 = "SHA256"; + private static PSCmdlet _cmdlet = null; + + /// + /// Find out the Version of Catalog by reading its Meta data. We can have either version 1 or version 2 catalog + /// + /// Handle to open catalog file + /// Version of the catalog + private static int GetCatalogVersion(IntPtr catalogHandle) + { + int catalogVersion = -1; + + IntPtr catalogData = NativeMethods.CryptCATStoreFromHandle(catalogHandle); + NativeMethods.CRYPTCATSTORE catalogInfo = ClrFacade.PtrToStructure(catalogData); + + if (catalogInfo.dwPublicVersion == catalogVersion2) + { + catalogVersion = 2; + } + // One Windows 7 this API sent version information as decimal 1 not hex (0X100 = 256) + // so we are checking for that value as well. Reason we are not checking for version 2 above in + // this scenario because catalog verion 2 is not supported on win7. + else if ((catalogInfo.dwPublicVersion == catalogVersion1) || (catalogInfo.dwPublicVersion == 1)) + { + catalogVersion = 1; + } + else + { + // catalog version we don't understand + Exception exception = new InvalidOperationException(StringUtil.Format(CatalogStrings.UnKnownCatalogVersion, + catalogVersion1.ToString("X"), + catalogVersion2.ToString("X"))); + + ErrorRecord errorRecord = new ErrorRecord(exception, "UnKnownCatalogVersion", ErrorCategory.InvalidOperation, null); + _cmdlet.ThrowTerminatingError(errorRecord); + } + return catalogVersion; + } + + /// + /// HashAlgorithm used by the Catalog. It is based on the version of Catalog. + /// + /// Path of the output catalog file + /// Version of the catalog + private static string GetCatalogHashAlgorithm(int catalogVersion) + { + string hashAlgorithm = string.Empty; + + if (catalogVersion == 1) + { + hashAlgorithm = HashAlgorithmSHA1; + } + else if (catalogVersion == 2) + { + hashAlgorithm = HashAlgorithmSHA256; + } + else + { + // version we don't understand + Exception exception = new InvalidOperationException(StringUtil.Format(CatalogStrings.UnKnownCatalogVersion, + "1.0", + "2.0")); + + ErrorRecord errorRecord = new ErrorRecord(exception, "UnKnownCatalogVersion", ErrorCategory.InvalidOperation, null); + _cmdlet.ThrowTerminatingError(errorRecord); + } + return hashAlgorithm; + } + + /// + /// Generate the Catalog Defintion File representing files and folders + /// + /// + /// Path of expected output .cdf file + /// + /// Path of the output catalog file + /// + /// Path of the catalog definiton file + /// + /// Version of catalog + /// + /// hash method used to generate hashes for the Catalog + /// HashSet for the relative Path for files in Catalog + internal static string GenerateCDFFile(Collection Path, string catalogFilePath, string cdfFilePath, int catalogVersion, string hashAlgorithm) + { + HashSet relativePaths = new HashSet(); + + string cdfHeaderContent = string.Empty; + string cdfFilesContent = string.Empty; + int catAttributeCount = 0; + + // First create header and files section for the catalog then write in file + cdfHeaderContent += "[CatalogHeader]" + Environment.NewLine; + cdfHeaderContent += @"Name=" + catalogFilePath + Environment.NewLine; + cdfHeaderContent += "CatalogVersion=" + catalogVersion + Environment.NewLine; + cdfHeaderContent += "HashAlgorithms=" + hashAlgorithm + Environment.NewLine; + cdfFilesContent += "[CatalogFiles]" + Environment.NewLine; + + foreach (string catalogFile in Path) + { + if (System.IO.Directory.Exists(catalogFile)) + { + var directoryItems = Directory.EnumerateFiles(catalogFile, "*.*", SearchOption.AllDirectories); + foreach (string fileItem in directoryItems) + { + ProcessFileToBeAddedInCatalogDefinitionFile(new FileInfo(fileItem), new DirectoryInfo(catalogFile), ref relativePaths, ref cdfHeaderContent, ref cdfFilesContent, ref catAttributeCount); + } + } + else if (System.IO.File.Exists(catalogFile)) + { + ProcessFileToBeAddedInCatalogDefinitionFile(new FileInfo(catalogFile), null, ref relativePaths, ref cdfHeaderContent, ref cdfFilesContent, ref catAttributeCount); + } + } + + using (System.IO.StreamWriter fileWriter = new System.IO.StreamWriter(new FileStream(cdfFilePath, FileMode.Create))) + { + fileWriter.WriteLine(cdfHeaderContent); + fileWriter.WriteLine(); + fileWriter.WriteLine(cdfFilesContent); + } + return cdfFilePath; + } + + /// + /// Get file attribute (Relative path in our case) from catalog + /// + /// file to hash + /// directory information about file needed to calculate relative file path. + /// working set of relative paths of all files. + /// content to be added in CatalogHeader section of cdf File + /// content to be added in CatalogFiles section of cdf File + /// indictaing the current no of catalog header level attributes + /// void + internal static void ProcessFileToBeAddedInCatalogDefinitionFile(FileInfo fileToHash, DirectoryInfo dirInfo, ref HashSet relativePaths, ref string cdfHeaderContent, ref string cdfFilesContent, ref int catAttributeCount) + { + string relativePath = string.Empty; + + if (dirInfo != null) + { + //Relative path of the file is the path inside the containing folder excluding folder Name + relativePath = fileToHash.FullName.Substring(dirInfo.FullName.Length).TrimStart('\\'); + } + else + { + relativePath = fileToHash.Name; + } + + if (!relativePaths.Contains(relativePath)) + { + relativePaths.Add(relativePath); + if (fileToHash.Length != 0) + { + cdfFilesContent += "" + fileToHash.FullName + "=" + fileToHash.FullName + Environment.NewLine; + cdfFilesContent += "" + fileToHash.FullName + "ATTR1=0x10010001:FilePath:" + relativePath + Environment.NewLine; + } + else + { + // zero length files are added as catalog level attributes because they can not be hashed + cdfHeaderContent += "CATATTR" + (++catAttributeCount) + "=0x10010001:FilePath:" + relativePath + Environment.NewLine; + } + } + else + { + // If Files have same relative paths we can not distinguish them for + // Validation. So failing. + ErrorRecord errorRecord = new ErrorRecord(new InvalidOperationException(StringUtil.Format(CatalogStrings.FoundDuplicateFilesRelativePath, relativePath)), "FoundDuplicateFilesRelativePath", ErrorCategory.InvalidOperation, null); + _cmdlet.ThrowTerminatingError(errorRecord); + } + } + /// + /// Generate the Catalog file for Input Catalog Defintion File + /// + /// + /// Path to the Input .cdf file + internal static void GenerateCatalogFile(string cdfFilePath) + { + string pwszFilePath = cdfFilePath; + NativeMethods.CryptCATCDFOpenCallBack catOpenCallBack = new NativeMethods.CryptCATCDFOpenCallBack(ParseErrorCallback); + + // Open CDF File + IntPtr resultCDF = NativeMethods.CryptCATCDFOpen(pwszFilePath, catOpenCallBack); + + // navigate CDF header and files sections + if (resultCDF != IntPtr.Zero) + { + // First navigate all catalog level attributes entries first, they represent zero size files + IntPtr catalogAttr = IntPtr.Zero; + do + { + catalogAttr = NativeMethods.CryptCATCDFEnumCatAttributes(resultCDF, catalogAttr, catOpenCallBack); + + if (catalogAttr != IntPtr.Zero) + { + string filePath = ProcessFilePathAttributeInCatalog(catalogAttr); + _cmdlet.WriteVerbose(StringUtil.Format(CatalogStrings.AddFileToCatalog, filePath, filePath)); + } + } while (catalogAttr != IntPtr.Zero); + + // navigate all the files hash entries in the .cdf file + IntPtr memberInfo = IntPtr.Zero; + try + { + IntPtr memberFile = IntPtr.Zero; + NativeMethods.CryptCATCDFEnumMembersByCDFTagExErrorCallBack memberCallBack = new NativeMethods.CryptCATCDFEnumMembersByCDFTagExErrorCallBack(ParseErrorCallback); + string fileName = string.Empty; + do + { + memberFile = NativeMethods.CryptCATCDFEnumMembersByCDFTagEx(resultCDF, memberFile, memberCallBack, ref memberInfo, true, IntPtr.Zero); + fileName = Marshal.PtrToStringUni(memberFile); + + if (!String.IsNullOrEmpty(fileName)) + { + + IntPtr memberAttr = IntPtr.Zero; + string fileRelativePath = String.Empty; + do + { + memberAttr = NativeMethods.CryptCATCDFEnumAttributesWithCDFTag(resultCDF, memberFile, memberInfo, memberAttr, memberCallBack); + + if (memberAttr != IntPtr.Zero) + { + fileRelativePath = ProcessFilePathAttributeInCatalog(memberAttr); + if (!String.IsNullOrEmpty(fileRelativePath)) + { + // Found the attribute we are looking for + // Filename we read from the above API has appended to its name as per CDF file tags convention + // Truncating that Information from the string. + string itemName = fileName.Substring(6); + _cmdlet.WriteVerbose(StringUtil.Format(CatalogStrings.AddFileToCatalog, itemName, fileRelativePath)); + break; + } + } + } while (memberAttr != IntPtr.Zero); + } + } while (fileName != null); + } + finally + { + NativeMethods.CryptCATCDFClose(resultCDF); + } + } + else + { + // If we are not able to open CDF file we can not continue generating catalog + ErrorRecord errorRecord = new ErrorRecord(new InvalidOperationException(CatalogStrings.UnableToOpenCatalogDefinitionFile), "UnableToOpenCatalogDefinitionFile", ErrorCategory.InvalidOperation, null); + _cmdlet.ThrowTerminatingError(errorRecord); + } + } + + /// + /// To generate Catalog for the folder + /// + /// + /// Path to folder or File + /// Catalog File Path + /// Catalog File Path + /// Instance of cmdlet calling this method + /// true if able to generate .cat file or false + internal static FileInfo GenerateCatalog(PSCmdlet cmdlet, Collection Path, string catalogFilePath, int catalogVersion) + { + _cmdlet = cmdlet; + string hashAlgorithm = GetCatalogHashAlgorithm(catalogVersion); + + if (!String.IsNullOrEmpty(hashAlgorithm)) + { + // Generate Path for Catalog Defintion File + string cdfFilePath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), System.IO.Path.GetRandomFileName()); + cdfFilePath = cdfFilePath + ".cdf"; + try + { + cdfFilePath = GenerateCDFFile(Path, catalogFilePath, cdfFilePath, catalogVersion, hashAlgorithm); + + if (!File.Exists(cdfFilePath)) + { + // If we are not able to generate catalog defintion file we can not continue generating catalog + // throw PSTraceSource.NewInvalidOperationException("catalog", CatalogStrings.CatalogDefintionFileNotGenerated); + ErrorRecord errorRecord = new ErrorRecord(new InvalidOperationException(CatalogStrings.CatalogDefintionFileNotGenerated), "CatalogDefintionFileNotGenerated", ErrorCategory.InvalidOperation, null); + _cmdlet.ThrowTerminatingError(errorRecord); + } + + GenerateCatalogFile(cdfFilePath); + if (File.Exists(catalogFilePath)) + { + return new FileInfo(catalogFilePath); + } + } + finally + { + File.Delete(cdfFilePath); + } + } + return null; + } + + /// + /// Get file attribute (Relative path in our case) from catalog + /// + /// Pointer to current attribute of catalog member. + /// value of the attribute + internal static string ProcessFilePathAttributeInCatalog(IntPtr memberAttrInfo) + { + string relativePath = string.Empty; + + NativeMethods.CRYPTCATATTRIBUTE currentMemberAttr = ClrFacade.PtrToStructure(memberAttrInfo); + + // check if this is the attribute we are looking for + // catalog generated other way not using New-FileCatalog can have attributes we don't understand + if (currentMemberAttr.pwszReferenceTag.Equals("FilePath", StringComparison.OrdinalIgnoreCase)) + { + // find the size for the current attribute value and then allocate buffer and copy from byte array + int attrValueSize = (int)currentMemberAttr.cbValue; + byte[] attrValue = new byte[attrValueSize]; + Marshal.Copy(currentMemberAttr.pbValue, attrValue, 0, attrValueSize); + relativePath = System.Text.Encoding.Unicode.GetString(attrValue); + relativePath = relativePath.TrimEnd('\0'); + } + + return relativePath; + } + + /// + /// Make a hash for the file + /// + /// + /// Path of the file + /// Used to calculate Hash + /// HashValue for the file + internal static string CalculateFileHash(string filePath, string hashAlgorithm) + { + string hashValue = string.Empty; + IntPtr catAdmin = IntPtr.Zero; + + // To get handle to the hash algorithm to be used to calculate hashes + if (!NativeMethods.CryptCATAdminAcquireContext2(ref catAdmin, IntPtr.Zero, hashAlgorithm, IntPtr.Zero, 0)) + { + ErrorRecord errorRecord = new ErrorRecord(new InvalidOperationException(StringUtil.Format(CatalogStrings.UnableToAcquireHashAlgorithmContext, hashAlgorithm)), "UnableToAcquireHashAlgorithmContext", ErrorCategory.InvalidOperation, null); + _cmdlet.ThrowTerminatingError(errorRecord); + } + + DWORD GENERIC_READ = 0x80000000; + DWORD OPEN_EXISTING = 3; + IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1); + + // Open the file that is to be hashed for reading and get its handle + IntPtr fileHandle = NativeMethods.CreateFile(filePath, GENERIC_READ, 0, 0 , OPEN_EXISTING, 0, IntPtr.Zero); + if (fileHandle != INVALID_HANDLE_VALUE) + { + try + { + DWORD hashBufferSize = 0; + IntPtr hashBuffer = IntPtr.Zero; + + // Call first time to get the size of expected buffer to hold new hash value + if (!NativeMethods.CryptCATAdminCalcHashFromFileHandle2(catAdmin, fileHandle, ref hashBufferSize, hashBuffer, 0)) + { + ErrorRecord errorRecord = new ErrorRecord(new InvalidOperationException(StringUtil.Format(CatalogStrings.UnableToCreateFileHash, filePath)), "UnableToCreateFileHash", ErrorCategory.InvalidOperation, null); + _cmdlet.ThrowTerminatingError(errorRecord); + } + + int size = (int)hashBufferSize; + hashBuffer = Marshal.AllocHGlobal(size); + try + { + // Call second time to actually get the hash value + if (!NativeMethods.CryptCATAdminCalcHashFromFileHandle2(catAdmin, fileHandle, ref hashBufferSize, hashBuffer, 0)) + { + ErrorRecord errorRecord = new ErrorRecord(new InvalidOperationException(StringUtil.Format(CatalogStrings.UnableToCreateFileHash, filePath)), "UnableToCreateFileHash", ErrorCategory.InvalidOperation, null); + _cmdlet.ThrowTerminatingError(errorRecord); + } + + byte[] hashBytes = new byte[size]; + Marshal.Copy(hashBuffer, hashBytes, 0, size); + hashValue = BitConverter.ToString(hashBytes).Replace("-", ""); + + } + finally + { + if (hashBuffer != IntPtr.Zero) + { + Marshal.FreeHGlobal(hashBuffer); + } + } + } + finally + { + NativeMethods.CryptCATAdminReleaseContext(catAdmin, 0); + NativeMethods.CloseHandle(fileHandle); + } + } + else + { + // If we are not able to open file that is to be hashed we can not continue with catalog validation + ErrorRecord errorRecord = new ErrorRecord(new InvalidOperationException(StringUtil.Format(CatalogStrings.UnableToReadFileToHash, filePath)), "UnableToReadFileToHash", ErrorCategory.InvalidOperation, null); + _cmdlet.ThrowTerminatingError(errorRecord); + } + return hashValue; + } + + /// + /// Make list of hashes for given Catalog File + /// + /// Path to the folder having catalog file + /// + /// The version of input catalog we read from catalog meta data after opening it. + /// Dictionary mapping files relative paths to HashValues + internal static Dictionary GetHashesFromCatalog(string catalogFilePath, WildcardPattern[] excludedPatterns,out int catalogVersion) + { + IntPtr resultCatalog = NativeMethods.CryptCATOpen(catalogFilePath, 0, IntPtr.Zero, 1, 0); + IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1); + Dictionary catalogHashes = new Dictionary(StringComparer.CurrentCultureIgnoreCase); + catalogVersion = 0; + + if (resultCatalog != INVALID_HANDLE_VALUE) + { + try + { + IntPtr catAttrInfo = IntPtr.Zero; + + // First traverse all catalog level attributes to get information about zero size file. + do + { + catAttrInfo = NativeMethods.CryptCATEnumerateCatAttr(resultCatalog, catAttrInfo); + + // If we found attribute it is a file information retrieve its relative path + // and add it to catalog hash collection if its not in excluded files criteria + if (catAttrInfo != IntPtr.Zero) + { + string relativePath = ProcessFilePathAttributeInCatalog(catAttrInfo); + if (!String.IsNullOrEmpty(relativePath)) + { + ProcessCatalogFile(relativePath, string.Empty, excludedPatterns, ref catalogHashes); + } + } + } while (catAttrInfo != IntPtr.Zero); + + catalogVersion = GetCatalogVersion(resultCatalog); + + IntPtr memberInfo = IntPtr.Zero; + // Next Navigate all members in Catalog files and get their relative paths and hashes + do + { + memberInfo = NativeMethods.CryptCATEnumerateMember(resultCatalog, memberInfo); + if (memberInfo != IntPtr.Zero) + { + NativeMethods.CRYPTCATMEMBER currentMember = ClrFacade.PtrToStructure(memberInfo); + NativeMethods.SIP_INDIRECT_DATA pIndirectData = ClrFacade.PtrToStructure(currentMember.pIndirectData); + + // For Catalog version 2 CryptoAPI puts hashes of file attributes(relative path in our case) in Catalog as well + // We validate those along with file hashes so we are skipping duplicate entries + if (!((catalogVersion == 2) && (pIndirectData.DigestAlgorithm.pszObjId.Equals(new Oid("SHA1").Value, StringComparison.OrdinalIgnoreCase)))) + { + string relativePath = String.Empty; + IntPtr memberAttrInfo = IntPtr.Zero; + do + { + memberAttrInfo = NativeMethods.CryptCATEnumerateAttr(resultCatalog, memberInfo, memberAttrInfo); + + if (memberAttrInfo != IntPtr.Zero) + { + relativePath = ProcessFilePathAttributeInCatalog(memberAttrInfo); + if (!String.IsNullOrEmpty(relativePath)) + { + break; + } + } + } + while (memberAttrInfo != IntPtr.Zero); + + // If we did not find any Relative Path for the item in catalog we should quit + // This catalog must not be valid for our use as catalogs generated using New-FileCatalog + // always contains relative file Paths + if (String.IsNullOrEmpty(relativePath)) + { + ErrorRecord errorRecord = new ErrorRecord(new InvalidOperationException(StringUtil.Format(CatalogStrings.UnableToOpenCatalogFile, catalogFilePath)), "UnableToOpenCatalogFile", ErrorCategory.InvalidOperation, null); + _cmdlet.ThrowTerminatingError(errorRecord); + } + + ProcessCatalogFile(relativePath, currentMember.pwszReferenceTag, excludedPatterns, ref catalogHashes); + } + } + } while (memberInfo != IntPtr.Zero); + } + finally + { + NativeMethods.CryptCATClose(resultCatalog); + } + } + else + { + ErrorRecord errorRecord = new ErrorRecord(new InvalidOperationException(StringUtil.Format(CatalogStrings.UnableToOpenCatalogFile, catalogFilePath)), "UnableToOpenCatalogFile", ErrorCategory.InvalidOperation, null); + _cmdlet.ThrowTerminatingError(errorRecord); + } + return catalogHashes; + } + + /// + /// Process file in path for its relative paths + /// + /// relative path of file found in catalog + /// hash of file found in catalog. + /// skip file from validation if it matches these patterns + /// collection of hashes of catalog + /// void + internal static void ProcessCatalogFile(string relativePath, string fileHash, WildcardPattern[] excludedPatterns, ref Dictionary catalogHashes) + { + // Found the attribute we are looking for + _cmdlet.WriteVerbose(StringUtil.Format(CatalogStrings.FoundFileHashInCatlogItem, relativePath, fileHash)); + + // Only add the file for validation if it does not meet exclusion criteria + if (!CheckExcludedCriteria((new FileInfo(relativePath)).Name, excludedPatterns)) + { + // Add relativePath mapping to hashvalue for each file + catalogHashes.Add(relativePath, fileHash); + } + else + { + // Verbose about skipping file from catalog + _cmdlet.WriteVerbose(StringUtil.Format(CatalogStrings.SkipValidationOfCatalogFile, relativePath)); + } + } + /// + /// Process file in path for its relative paths + /// + /// file to hash + /// directory information about file needed to calculate relative file path. + /// Used to calculate Hash + /// skip file if it matches these patterns + /// collection of hashes of files + /// void + internal static void ProcessPathFile(FileInfo fileToHash, DirectoryInfo dirInfo, string hashAlgorithm, WildcardPattern[] excludedPatterns, ref Dictionary fileHashes) + { + string relativePath = string.Empty; + string exclude = string.Empty; + + if (dirInfo != null) + { + //Relative path of the file is the path inside the containing folder excluding folder Name + relativePath = fileToHash.FullName.Substring(dirInfo.FullName.Length).TrimStart('\\'); + exclude = fileToHash.Name; + } + else + { + relativePath = fileToHash.Name; + exclude = relativePath; + } + + if (!CheckExcludedCriteria(exclude, excludedPatterns)) + { + string fileHash = string.Empty; + + if(fileToHash.Length != 0) + { + fileHash = CalculateFileHash(fileToHash.FullName, hashAlgorithm); + } + + if (!fileHashes.ContainsKey(relativePath)) + { + fileHashes.Add(relativePath, fileHash); + _cmdlet.WriteVerbose(StringUtil.Format(CatalogStrings.FoundFileInPath, relativePath, fileHash)); + } + else + { + ErrorRecord errorRecord = new ErrorRecord(new InvalidOperationException(StringUtil.Format(CatalogStrings.FoundDuplicateFilesRelativePath, relativePath)), "FoundDuplicateFilesRelativePath", ErrorCategory.InvalidOperation, null); + _cmdlet.ThrowTerminatingError(errorRecord); + } + } + else + { + // Verbose about skipping file from path + _cmdlet.WriteVerbose(StringUtil.Format(CatalogStrings.SkipValidationOfPathFile, relativePath)); + } + } + + /// + /// Generate the hashes of all the files in given folder + /// + /// + /// Path to folder or File + /// catalog file path it should be skipped when calculating the hashes + /// Used to calculate Hash + /// + /// Dictionary mapping file relative paths to hashes. + internal static Dictionary CalculateHashesFromPath(Collection folderPaths, string catalogFilePath, string hashAlgorithm, WildcardPattern[] excludedPatterns) + { + // Create a HashTable of file Hashes + Dictionary fileHashes = new Dictionary(StringComparer.CurrentCultureIgnoreCase); + + foreach (string folderPath in folderPaths) + { + if (System.IO.Directory.Exists(folderPath)) + { + var directoryItems = Directory.EnumerateFiles(folderPath, "*.*", SearchOption.AllDirectories); + foreach (string fileItem in directoryItems) + { + // if its the catalog file we are validating we will skip it + if (String.Equals(fileItem, catalogFilePath, StringComparison.OrdinalIgnoreCase)) + continue; + + ProcessPathFile(new FileInfo(fileItem), new DirectoryInfo(folderPath), hashAlgorithm, excludedPatterns, ref fileHashes); + } + } + else if (System.IO.File.Exists(folderPath)) + { + ProcessPathFile(new FileInfo(folderPath), null , hashAlgorithm, excludedPatterns, ref fileHashes); + } + } + return fileHashes; + } + + /// + /// Compare Dictionary objects + /// + /// + /// Hashes extracted from Catalog + /// + /// Hashes created from folders path + /// + /// True if both collections are same + internal static bool CompareDictionaries(Dictionary catalogItems, Dictionary pathItems) + { + bool Status = true; + + List relativePathsFromFolder = pathItems.Keys.ToList(); + List relativePathsFromCatalog = catalogItems.Keys.ToList(); + + // Find entires those are not in both list lists. These should be empty lists for success + // Hashes in Catalog should be exact similar to the ones from folder + List relativePathsNotInFolder = relativePathsFromFolder.Except(relativePathsFromCatalog, StringComparer.CurrentCultureIgnoreCase).ToList(); + List relativePathsNotInCatalog = relativePathsFromCatalog.Except(relativePathsFromFolder, StringComparer.CurrentCultureIgnoreCase).ToList(); + + //Found extra hashes in Folder + if ((relativePathsNotInFolder.Count != 0) || (relativePathsNotInCatalog.Count != 0)) + { + Status = false; + } + + foreach (KeyValuePair item in catalogItems) + { + string catalogHashValue = (string)catalogItems[item.Key]; + if (pathItems.ContainsKey(item.Key)) + { + string folderHashValue = (string)pathItems[item.Key]; + if (folderHashValue.Equals(catalogHashValue)) + { + continue; + } + else + { + Status = false; + } + } + } + return Status; + } + /// + /// To Validate the Integrity of Catalog + /// + /// + /// Folder for which catalog is created + /// File Name of the Catalog + /// + /// Instance of cmdlet calling this method + /// Information about Catalog + internal static CatalogInformation ValidateCatalog(PSCmdlet cmdlet, Collection catalogFolders, String catalogFilePath, WildcardPattern[] excludedPatterns) + { + _cmdlet = cmdlet; + int catalogVersion = 0; + Dictionary catalogHashes = GetHashesFromCatalog(catalogFilePath, excludedPatterns, out catalogVersion); + string hashAlgorithm = GetCatalogHashAlgorithm(catalogVersion); + + if (!String.IsNullOrEmpty(hashAlgorithm)) + { + Dictionary fileHashes = CalculateHashesFromPath(catalogFolders, catalogFilePath, hashAlgorithm, excludedPatterns); + CatalogInformation catalog = new CatalogInformation(); + catalog.CatalogItems = catalogHashes; + catalog.PathItems = fileHashes; + bool status = CompareDictionaries(catalogHashes, fileHashes); + if (status == true) + { + catalog.Status = CatalogValidationStatus.Valid; + } + else + { + catalog.Status = CatalogValidationStatus.ValidationFailed; + } + catalog.HashAlgorithm = hashAlgorithm; + catalog.Signature = SignatureHelper.GetSignature(catalogFilePath, null); + return catalog; + } + return null; + } + + /// + /// Check if file meets the skip validation criteria + /// + /// + /// + /// True if match is found else false + internal static bool CheckExcludedCriteria(string filename, WildcardPattern[] excludedPatterns) + { + if (excludedPatterns != null) + { + foreach (WildcardPattern patternItem in excludedPatterns) + { + if (patternItem.IsMatch(filename)) + { + return true; + } + } + } + return false; + } + /// + /// Call back when error is thrown by catalog API's + /// + private static void ParseErrorCallback(DWORD dwErrorArea, DWORD dwLocalError, string pwszLine) + { + switch (dwErrorArea) + { + case NativeConstants.CRYPTCAT_E_AREA_HEADER: break; + case NativeConstants.CRYPTCAT_E_AREA_MEMBER: break; + case NativeConstants.CRYPTCAT_E_AREA_ATTRIBUTE: break; + default: break; + } + switch (dwLocalError) + { + case NativeConstants.CRYPTCAT_E_CDF_MEMBER_FILE_PATH: + { + ErrorRecord errorRecord = new ErrorRecord(new InvalidOperationException(StringUtil.Format(CatalogStrings.UnableToFindFileNameOrPathForCatalogMember, pwszLine)), "UnableToFindFileNameOrPathForCatalogMember", ErrorCategory.InvalidOperation, null); + _cmdlet.ThrowTerminatingError(errorRecord); + break; + } + case NativeConstants.CRYPTCAT_E_CDF_MEMBER_INDIRECTDATA: + { + ErrorRecord errorRecord = new ErrorRecord(new InvalidOperationException(StringUtil.Format(CatalogStrings.UnableToCreateFileHash, pwszLine)), "UnableToCreateFileHash", ErrorCategory.InvalidOperation, null); + _cmdlet.ThrowTerminatingError(errorRecord); + break; + } + case NativeConstants.CRYPTCAT_E_CDF_MEMBER_FILENOTFOUND: + { + ErrorRecord errorRecord = new ErrorRecord(new InvalidOperationException(StringUtil.Format(CatalogStrings.UnableToFindFileToHash, pwszLine)), "UnableToFindFileToHash", ErrorCategory.InvalidOperation, null); + _cmdlet.ThrowTerminatingError(errorRecord); + break; + } + case NativeConstants.CRYPTCAT_E_CDF_BAD_GUID_CONV: break; + case NativeConstants.CRYPTCAT_E_CDF_ATTR_TYPECOMBO: break; + case NativeConstants.CRYPTCAT_E_CDF_ATTR_TOOFEWVALUES: break; + case NativeConstants.CRYPTCAT_E_CDF_UNSUPPORTED: break; + case NativeConstants.CRYPTCAT_E_CDF_DUPLICATE: + { + ErrorRecord errorRecord = new ErrorRecord(new InvalidOperationException(StringUtil.Format(CatalogStrings.FoundDuplicateFileMemberInCatalog, pwszLine)), "FoundDuplicateFileMemberInCatalog", ErrorCategory.InvalidOperation, null); + _cmdlet.ThrowTerminatingError(errorRecord); + break; + } + case NativeConstants.CRYPTCAT_E_CDF_TAGNOTFOUND: break; + default: break; + } + } + } +} +