Browse Source

fix: calculate Get-FileHash2 hashes in PowerShell 7

Get-FileHash2 failed in PowerShell 7 for every algorithm, because the hash
method referenced RIPEMD160, which .NET Core and later lack. RIPEMD160 and
MACTripleDES are now created by name; requesting one where .NET lacks it
stops the cmdlet with an error that names the algorithm and points to
Windows PowerShell 5.1. MACTripleDES uses a random key, so its result
differs on every call; the value is deprecated, and the cmdlet warns when
it is used.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: AI Assistant <ai@example.com>
pull/104/head
Raimund Andree 7 days ago
parent
commit
3e53db04ef
  1. 7
      CHANGELOG.md
  2. 6
      Docs/Cmdlets/Get-FileHash2.md
  3. 16
      NTFSSecurity/MiscCmdlets/GetFileHash2.cs
  4. 8
      NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml
  5. 74
      Security2/FileSystem/FileInfo/Extensions.cs
  6. 43
      Tests/FileHash.Tests.ps1

7
CHANGELOG.md

@ -40,6 +40,9 @@ The format is based on
### Deprecated
- Deprecate the `-PassThur` alias of `Remove-Item2`; use `-PassThru`
- Deprecate the `MACTripleDES` value of `Get-FileHash2 -Algorithm`: it uses
a random key, so its result differs on every call; the cmdlet now warns
when you use it
### Fixed
@ -149,5 +152,9 @@ The format is based on
- Fix `-PassThru` of `Copy-Item2`, `Move-Item2`, and `Remove-Item2`, which
wrote the item also when `-WhatIf` or a declined confirmation skipped the
operation
- Fix `Get-FileHash2` in PowerShell 7, where it failed for every algorithm;
`RIPEMD160` and `MACTripleDES`, which .NET lacks there, now stop the
cmdlet with an error that names the algorithm and points to Windows
PowerShell 5.1
[Unreleased]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...HEAD

6
Docs/Cmdlets/Get-FileHash2.md

@ -65,7 +65,7 @@ This command groups the files below `C:\Data` by hash value and returns the grou
### -Algorithm
Specifies the hash algorithm to use. The accepted values are `SHA1`, `SHA256`, `SHA384`, `SHA512`, `MACTripleDES`, `MD5`, and `RIPEMD160`. When you omit this parameter, the cmdlet uses `SHA256`.
Specifies the hash algorithm to use. The accepted values are `SHA1`, `SHA256`, `SHA384`, `SHA512`, `MACTripleDES`, `MD5`, and `RIPEMD160`. When you omit this parameter, the cmdlet uses `SHA256`. `RIPEMD160` and `MACTripleDES` are available only in Windows PowerShell 5.1, and `MACTripleDES` is deprecated.
```yaml
Type: HashAlgorithms
@ -117,13 +117,13 @@ For every hashed file, the cmdlet writes the file object of that file, decorated
## NOTES
The cmdlet works only in Windows PowerShell. In PowerShell 7, it fails for every algorithm with the error `Could not load type 'System.Security.Cryptography.RIPEMD160'`, because .NET no longer includes the RIPEMD-160 implementation that the cmdlet references. In PowerShell 7, use the built-in `Get-FileHash` cmdlet instead.
In PowerShell 7, the cmdlet supports `SHA1`, `SHA256`, `SHA384`, `SHA512`, and `MD5`. .NET no longer includes `RIPEMD160` and `MACTripleDES`, so requesting one of them there stops the cmdlet with the error `HashAlgorithmNotAvailable`, which names the algorithm; use Windows PowerShell 5.1 to calculate those hashes. Before 5.0.0, the cmdlet failed in PowerShell 7 for every algorithm with the error `Could not load type 'System.Security.Cryptography.RIPEMD160'`.
If the file cannot be opened because access is denied, the cmdlet takes ownership of the file with the account that runs it, calculates the hash, and restores the previous owner afterward. That fallback fails with a `GetHashError` when the account is not allowed to change the owner of the file. A file that cannot be read produces a `GetHashError` and no result.
The hash is returned as an uppercase hexadecimal string without separators, which differs from the lowercase output of some other hashing tools. Compare hash values case-insensitively.
`MACTripleDES` is a keyed message authentication code that is created with a key that is generated for each call, so its result is not reproducible across invocations and is not suitable for comparing files.
`MACTripleDES` is a keyed message authentication code that is created with a key that is generated for each call, so its result is not reproducible across invocations and is not suitable for comparing files. The value is deprecated: the cmdlet writes a warning when you use it, and a future version will remove it.
Before 5.0.0, a folder in a `-Path` array stopped the processing of that array, so the files that followed the folder were not hashed, and a file that could not be read got a result with the hash of the previous file.

16
NTFSSecurity/MiscCmdlets/GetFileHash2.cs

@ -11,6 +11,7 @@ namespace NTFSSecurity
public class GetFileHash2 : BaseCmdlet
{
private HashAlgorithms algorithm = HashAlgorithms.SHA256;
private bool deprecationWarningWritten = false;
[Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
@ -39,6 +40,21 @@ namespace NTFSSecurity
protected override void ProcessRecord()
{
try
{
Security2.FileSystem.FileInfo.Extensions.CreateHashAlgorithm(algorithm).Dispose();
}
catch (PlatformNotSupportedException ex)
{
ThrowTerminatingError(new ErrorRecord(ex, "HashAlgorithmNotAvailable", ErrorCategory.NotImplemented, algorithm));
}
if (algorithm == HashAlgorithms.MACTripleDES && !deprecationWarningWritten)
{
WriteWarning("The MACTripleDES algorithm uses a random key, so its result differs on every call. The value is deprecated and will be removed in a future version.");
deprecationWarningWritten = true;
}
foreach (var path in paths)
{
string hash = null;

8
NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml

@ -4082,7 +4082,7 @@ PS C:\&gt; Disable-Privileges</dev:code>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName)" position="2" aliases="none">
<maml:name>Algorithm</maml:name>
<maml:description>
<maml:para>Specifies the hash algorithm to use. The accepted values are `SHA1`, `SHA256`, `SHA384`, `SHA512`, `MACTripleDES`, `MD5`, and `RIPEMD160`. When you omit this parameter, the cmdlet uses `SHA256`.</maml:para>
<maml:para>Specifies the hash algorithm to use. The accepted values are `SHA1`, `SHA256`, `SHA384`, `SHA512`, `MACTripleDES`, `MD5`, and `RIPEMD160`. When you omit this parameter, the cmdlet uses `SHA256`. `RIPEMD160` and `MACTripleDES` are available only in Windows PowerShell 5.1, and `MACTripleDES` is deprecated.</maml:para>
</maml:description>
<command:parameterValueGroup>
<command:parameterValue required="false" command:variableLength="false">SHA1</command:parameterValue>
@ -4106,7 +4106,7 @@ PS C:\&gt; Disable-Privileges</dev:code>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="True (ByPropertyName)" position="2" aliases="none">
<maml:name>Algorithm</maml:name>
<maml:description>
<maml:para>Specifies the hash algorithm to use. The accepted values are `SHA1`, `SHA256`, `SHA384`, `SHA512`, `MACTripleDES`, `MD5`, and `RIPEMD160`. When you omit this parameter, the cmdlet uses `SHA256`.</maml:para>
<maml:para>Specifies the hash algorithm to use. The accepted values are `SHA1`, `SHA256`, `SHA384`, `SHA512`, `MACTripleDES`, `MD5`, and `RIPEMD160`. When you omit this parameter, the cmdlet uses `SHA256`. `RIPEMD160` and `MACTripleDES` are available only in Windows PowerShell 5.1, and `MACTripleDES` is deprecated.</maml:para>
</maml:description>
<command:parameterValue required="true" variableLength="false">HashAlgorithms</command:parameterValue>
<dev:type>
@ -4158,10 +4158,10 @@ PS C:\&gt; Disable-Privileges</dev:code>
</command:returnValues>
<maml:alertSet>
<maml:alert>
<maml:para>The cmdlet works only in Windows PowerShell. In PowerShell 7, it fails for every algorithm with the error `Could not load type 'System.Security.Cryptography.RIPEMD160'`, because .NET no longer includes the RIPEMD-160 implementation that the cmdlet references. In PowerShell 7, use the built-in `Get-FileHash` cmdlet instead.</maml:para>
<maml:para>In PowerShell 7, the cmdlet supports `SHA1`, `SHA256`, `SHA384`, `SHA512`, and `MD5`. .NET no longer includes `RIPEMD160` and `MACTripleDES`, so requesting one of them there stops the cmdlet with the error `HashAlgorithmNotAvailable`, which names the algorithm; use Windows PowerShell 5.1 to calculate those hashes. Before 5.0.0, the cmdlet failed in PowerShell 7 for every algorithm with the error `Could not load type 'System.Security.Cryptography.RIPEMD160'`.</maml:para>
<maml:para>If the file cannot be opened because access is denied, the cmdlet takes ownership of the file with the account that runs it, calculates the hash, and restores the previous owner afterward. That fallback fails with a `GetHashError` when the account is not allowed to change the owner of the file. A file that cannot be read produces a `GetHashError` and no result.</maml:para>
<maml:para>The hash is returned as an uppercase hexadecimal string without separators, which differs from the lowercase output of some other hashing tools. Compare hash values case-insensitively.</maml:para>
<maml:para>`MACTripleDES` is a keyed message authentication code that is created with a key that is generated for each call, so its result is not reproducible across invocations and is not suitable for comparing files.</maml:para>
<maml:para>`MACTripleDES` is a keyed message authentication code that is created with a key that is generated for each call, so its result is not reproducible across invocations and is not suitable for comparing files. The value is deprecated: the cmdlet writes a warning when you use it, and a future version will remove it.</maml:para>
<maml:para>Before 5.0.0, a folder in a `-Path` array stopped the processing of that array, so the files that followed the folder were not hashed, and a file that could not be read got a result with the hash of the previous file.</maml:para>
</maml:alert>
</maml:alertSet>

74
Security2/FileSystem/FileInfo/Extensions.cs

@ -1,4 +1,6 @@
using System.Text;
using System;
using System.Security.Cryptography;
using System.Text;
namespace Security2.FileSystem.FileInfo
{
@ -15,48 +17,60 @@ namespace Security2.FileSystem.FileInfo
public static class Extensions
{
public static string GetHash(this Alphaleonis.Win32.Filesystem.FileInfo file, HashAlgorithms algorithm)
{
byte[] hash = null;
using (var hashAlgorithm = CreateHashAlgorithm(algorithm))
using (var fileStream = file.OpenRead())
{
switch (algorithm)
{
case HashAlgorithms.MD5:
hash = System.Security.Cryptography.MD5.Create().ComputeHash(fileStream);
break;
case HashAlgorithms.SHA1:
hash = System.Security.Cryptography.SHA1.Create().ComputeHash(fileStream);
break;
case HashAlgorithms.SHA256:
hash = System.Security.Cryptography.SHA256.Create().ComputeHash(fileStream);
break;
case HashAlgorithms.SHA384:
hash = System.Security.Cryptography.SHA384.Create().ComputeHash(fileStream);
break;
case HashAlgorithms.SHA512:
hash = System.Security.Cryptography.SHA512.Create().ComputeHash(fileStream);
break;
case HashAlgorithms.MACTripleDES:
hash = System.Security.Cryptography.MACTripleDES.Create().ComputeHash(fileStream);
break;
case HashAlgorithms.RIPEMD160:
hash = System.Security.Cryptography.RIPEMD160.Create().ComputeHash(fileStream);
break;
}
fileStream.Close();
hash = hashAlgorithm.ComputeHash(fileStream);
}
var sb = new StringBuilder(hash.Length);
var sb = new StringBuilder(hash.Length * 2);
for (var i = 0; i < hash.Length; i++)
{
sb.Append(hash[i].ToString("X2"));
}
return sb.ToString();
}
/// <summary>
/// Creates the hash algorithm. Throws a PlatformNotSupportedException that names the algorithm when the
/// .NET runtime lacks it, as .NET Core and later lack RIPEMD160 and MACTripleDES.
/// </summary>
/// <param name="algorithm">The hash algorithm to create.</param>
/// <returns>A new instance of the hash algorithm.</returns>
public static HashAlgorithm CreateHashAlgorithm(HashAlgorithms algorithm)
{
switch (algorithm)
{
case HashAlgorithms.MD5:
return MD5.Create();
case HashAlgorithms.SHA1:
return SHA1.Create();
case HashAlgorithms.SHA256:
return SHA256.Create();
case HashAlgorithms.SHA384:
return SHA384.Create();
case HashAlgorithms.SHA512:
return SHA512.Create();
case HashAlgorithms.MACTripleDES:
case HashAlgorithms.RIPEMD160:
// Created by name: a reference to the type would make every hash fail where the type is missing.
var hashAlgorithm = CryptoConfig.CreateFromName(algorithm.ToString()) as HashAlgorithm;
if (hashAlgorithm == null)
{
throw new PlatformNotSupportedException(string.Format(
"The hash algorithm '{0}' is not available in this version of .NET. Use Windows PowerShell 5.1 to calculate it.",
algorithm));
}
return hashAlgorithm;
default:
throw new ArgumentOutOfRangeException("algorithm", algorithm, "Unknown hash algorithm.");
}
}
}
}

43
Tests/FileHash.Tests.ps1

@ -9,6 +9,7 @@ param ()
BeforeDiscovery {
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force
$isElevated = Test-IsElevated
$isCore = $PSVersionTable.PSEdition -eq 'Core'
}
BeforeAll {
@ -34,12 +35,40 @@ AfterAll {
}
Describe 'Get-FileHash2' {
Context 'Algorithms' {
# Before 5.0.0, the cmdlet failed in PowerShell 7 for every algorithm, because it referenced RIPEMD160.
It 'Should return the hash of Get-FileHash for <_>' -ForEach @('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5') {
$result = Get-FileHash2 -Path $first -Algorithm $_
$result.Hash | Should -BeExactly (Get-FileHash -LiteralPath $first -Algorithm $_).Hash
$result.Algorithm | Should -Be $_
}
It 'Should calculate RIPEMD160 in Windows PowerShell' -Skip:$isCore {
$expected = [BitConverter]::ToString(
[System.Security.Cryptography.RIPEMD160]::Create().ComputeHash([IO.File]::ReadAllBytes($first))
).Replace('-', '')
(Get-FileHash2 -Path $first -Algorithm RIPEMD160).Hash | Should -BeExactly $expected
}
It 'Should stop with an error that names <_> in PowerShell 7' -Skip:(-not $isCore) -ForEach @('RIPEMD160', 'MACTripleDES') {
$algorithm = $_
{ Get-FileHash2 -Path $first -Algorithm $algorithm -ErrorAction Stop } |
Should -Throw -ExpectedMessage "*'$algorithm'*Windows PowerShell 5.1*"
}
It 'Should warn that MACTripleDES is deprecated' -Skip:$isCore {
$result = Get-FileHash2 -Path $first -Algorithm MACTripleDES -WarningVariable hashWarnings -WarningAction SilentlyContinue
$result.Hash | Should -Not -BeNullOrEmpty
$hashWarnings | Should -HaveCount 1
$hashWarnings[0].Message | Should -BeLike '*MACTripleDES*random key*deprecated*'
}
}
Context 'When -Path contains a folder' {
It 'Should skip the folder and hash the files that follow it' {
if ($PSVersionTable.PSEdition -eq 'Core') {
Set-ItResult -Skipped -Because 'Get-FileHash2 fails in PowerShell 7 until it no longer references RIPEMD160'
}
$results = @(Get-FileHash2 -Path $first, $folder, $second -ErrorVariable hashErrors -ErrorAction SilentlyContinue)
$hashErrors | Should -BeNullOrEmpty
@ -51,9 +80,6 @@ Describe 'Get-FileHash2' {
Context 'When a file cannot be read' {
# Before 5.0.0, the cmdlet wrote a result for the file anyway, with the hash of the previous file.
It 'Should write an error and no result for the file' {
if ($PSVersionTable.PSEdition -eq 'Core') {
Set-ItResult -Skipped -Because 'Get-FileHash2 fails in PowerShell 7 until it no longer references RIPEMD160'
}
$locked = New-TestSandboxItem -Sandbox $sandbox -Name 'Locked'
$stream = [IO.File]::Open($locked, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::None)
try {
@ -72,9 +98,6 @@ Describe 'Get-FileHash2' {
# Before 5.0.0, the account that ran the cmdlet stayed the owner when the second attempt failed. Only an
# elevated process can make another account the owner first, so the test runs in CI.
It 'Should restore the previous owner' -Skip:(-not $isElevated) {
if ($PSVersionTable.PSEdition -eq 'Core') {
Set-ItResult -Skipped -Because 'Get-FileHash2 fails in PowerShell 7 until it no longer references RIPEMD160'
}
$denied = New-TestSandboxItem -Sandbox $sandbox -Name 'Denied'
Assert-TestSandboxPath -Sandbox $sandbox -Path $denied
Set-NTFSOwner -Path $denied -Account 'S-1-5-32-544'

Loading…
Cancel
Save