The other day I had the need to calculate MD5 file hashes in order to compare files. The PowerShell Community Extensions has a nifty cmdlet, Get-Hash, that does exactly that. Unfortunately, sometimes even free tools like this aren't an option, as in the case of my client. Or they don't go far enough. So with a little work I wrote my own function to compute a file hash using either MD5, SHA1, or SHA256.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
My function, Get-FileHash, takes file names as input and writes a custom object to the pipeline.
[cc lang="PowerShell"]
PS E:\temp> get-filehash winlogon.xml
Date : 3/31/2011 10:04:46 AM
FullName : E:\temp\winlogon.xml
FileHash : 817511618E6ECCF41DC3CA93B5677EDE
HashType : MD5
Filename : winlogon.xml
Size : 17700
[/cc]
The Date property is not the file date but the date of the hash. I can even audit entire folders and use the data later.
[cc lang="PowerShell"]
C:\work> dir c:\scripts\*.ps* | Get-FileHash | Export-CLIXml PSScriptHashes.xml
[/cc]
Here's the function minus the comment based help, which I generated by the way with New-CommentHelp.
[cc lang="PowerShell"]
Function Get-FileHash {
[cmdletbinding(SupportsShouldProcess=$True,ConfirmImpact="Low")]
Param (
[Parameter(Position=0,Mandatory=$True,HelpMessage="Enter file name and path.",
ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
[ValidateNotNullorEmpty()]
[Alias("name","file","path")]
[string[]]$PSPath,
[Parameter(Position=1,Mandatory=$False)]
[Alias("hash")]
[ValidateSet("MD5","SHA1","SHA256")]
[string]$Type = "MD5",
[switch]$Force
)
Begin
{
#what time are we starting?
$cmdStart=Get-Date
Write-Verbose "$(Get-Date) Starting $($myinvocation.mycommand)"
if ($force)
{
Write-Verbose "$(Get-Date) Using -Force to find hidden files"
}
#create the hash provider
Switch ($Type) {
"sha1" {
$provider = New-Object System.Security.Cryptography.SHA1CryptoServiceProvider
}
"sha256" {
$provider = New-Object System.Security.Cryptography.SHA256CryptoServiceProvider
}
"md5" {
$provider = New-Object System.Security.Cryptography.MD5CryptoServiceProvider
}
}
Write-Verbose "$(Get-Date) Calculating $type hash"
} #begin
Process
{
Foreach ($name in $PSPath) {
Write-Verbose "$(Get-Date) Verifying $name"
#verify file exists
if (Test-Path -Path $name)
{
Write-Verbose "$(Get-Date) Path verified"
Try
{
#get the file item
if ($force)
{
$file=Get-Item -Path $name -force -ErrorAction "Stop"
}
else
{
$file=Get-Item -Path $name -ErrorAction "Stop"
}
}
Catch
{
Write-Warning "Cannot get item $name. Verify it is not a hidden file (did you use -Force?) and that you have correct permissions."
}
#only process if we were able to get the item
if ($file)
{
#only process if file size is greater than 0
#this will also fail if the object is not from the
#filesystem provider
if ($file.length -gt 0)
{
Write-Verbose "$(Get-Date) Opening file stream"
if ($pscmdlet.ShouldProcess($file.fullname))
{
Try
{
$inStream = $file.OpenRead()
Write-Verbose "$(Get-Date) Computing hash"
$start=Get-Date
$hashBytes = $provider.ComputeHash($inStream)
$end=Get-Date
Write-Verbose "$(Get-Date) hash computed in $(($end-$start).ToString())"
Write-Verbose "$(Get-Date) Closing file stream"
$inStream.Close() | Out-Null
#define a hash string variable
$hashString=""
Write-Verbose "$(Get-Date) hashing file bytes"
foreach ($byte in $hashBytes)
{
#calculate the hash
$hashString+=$byte.ToString("X2")
}
#write the hash object to the pipeline
New-Object -TypeName PSObject -Property @{
Filename=$file.name
FullName=$file.Fullname
FileHash=$hashString
HashType=$Type
Size=$file.length
Date=Get-Date
}
} #try
Catch
{
Write-Warning "Failed to get file contents for $($file.name)."
Write-Warning $_.Exception.Message
}
} #should process
} #if $file.length
else
{
Write-Warning "$(Get-Date) File size for $name is 0 or is not from the filesystem provider."
}
}#if $file
} #if Test-Path
else
{
Write-Warning "$(Get-Date) Failed to find $name."
}
} #foreach $file
} #process
End
{
Write-Verbose "$(Get-Date) Ending $($myinvocation.mycommand)"
#what time did we finish?
$cmdEnd=Get-Date
Write-Verbose "$(Get-Date) Total processing time $(($cmdEnd-$cmdStart).ToString())"
}
} #end function
[/cc]
The function takes file names as a parameter. You can also specify the hash algorithm. The default is MD5. If you will be calculating hash signatures for hidden items you will need to use -Force. Although I'm not changing the file, I included -WhatIf support because calculating a hash for a 30GB VHD takes some time and I often just wanted to test the rest of the function.
Much of the function is comprised of error handling and testing. For example if the item is not in the filesystem or has a 0 size, there is nothing to calculate so I check for those things. Same thing goes for permissions. You must have READ permission otherwise there's no way to calculate a hash. If you don't, then an exception is caught.
The core functionality is derived from creating an appropriate crypto provider.
[cc lang="PowerShell"]
Switch ($Type) {
"sha1" {
$provider = New-Object System.Security.Cryptography.SHA1CryptoServiceProvider
}
"sha256" {
$provider = New-Object System.Security.Cryptography.SHA256CryptoServiceProvider
}
"md5" {
$provider = New-Object System.Security.Cryptography.MD5CryptoServiceProvider
}
}
[/cc]
Once I've confirmed the file, it's contents are read as a stream and the provider computes a hash.
[cc lang="PowerShell"]
$inStream = $file.OpenRead()
Write-Verbose "$(Get-Date) Computing hash"
$start=Get-Date
$hashBytes = $provider.ComputeHash($inStream)
$end=Get-Date
Write-Verbose "$(Get-Date) hash computed in $(($end-$start).ToString())"
Write-Verbose "$(Get-Date) Closing file stream"
$inStream.Close() | Out-Null
[/cc]
This is where disk speed really comes into play. It took over a minute to calculate a hash for a 3GB file on an external USB drive and 17 seconds on an SSD. In any event, the hash is collection of bytes which is converted into a friendly string format.
[cc lang="PowerShell"]
foreach ($byte in $hashBytes) {
#calculate the hash
$hashString+=$byte.ToString("X2")
}
[/cc]
At this point all that remains is to create a custom object with the information I want.
[cc lang="PowerShell"]
#write the hash object to the pipeline
New-Object -TypeName PSObject -Property @{
Filename=$file.name
FullName=$file.Fullname
FileHash=$hashString
HashType=$Type
Size=$file.length
Date=Get-Date
}
[/cc]
I hope you'll let me know what works, and doesn't. I'm also open to enhancement suggestions.
Download Get-FileHash.
I was working on a similar script, mainly to practise scripting and give myself something new to engage the mind with through PowerShell.
A script out on the web called Get-MD5 which I was re-writing, and your’s use a stream – is this needed? I ask because I shortened by:
$x = get-item file.ps1
$provider = New-Object System.Security.Cryptography.MD5CryptoServiceProvider
$hashBytes = $provider.ComputeHash($x.openread())
[string]$hashBytes
Thanks for any help.
Kind regards,
Pritesh Patel
The way I understand hashing you have to read the contents to verify they haven’t changed. Now, whether you read as a stream or some other technique is up for debate and testing I suppose. Can you give me a link to the original Get-MD5 script you found?
Hi Jeff,
Thanks for the reply, here is a link to the script I found many moons ago:
http://blogs.msdn.com/powershell/archive/2006/04/25/583225.aspx
Kind regards,
Pritesh Patel
I see now. We’re doing the same thing in terms of reading the file. The old example is combining steps. Mine are separate more for clarity. But there’s no real difference other than the way the hashbytes are formatted.