windows 版本的 openssl 计算从管道(pipeline)传递的字符串的哈希值时会得到错误的结果- openssl dgst -md5 -r <file> ...
- openssl md5 -r <file> ...
- "string" | openssl dgst -md5 -r
- "string" | openssl md5 -r
复制代码 其中 -r 改变输出格式,可以省略,md5 是算法的一种,可以换成 sha1 sha256 sha512 等其它算法,不区分大小写,openssl 支持的算法很多。语法规则是,可以将这些算法当成子命令使用,也可以使用 dgst 作为子命令,将算法作为子命令 dgst 的参数,算法作为参数时,需要在前面加一个英文状态的中横线-,如 -md5,<file> 是要使用 openssl 计算哈希值的文件,可以有多个文件,用空格分开,而使用管道传递字符串计算字符串的哈希值时,openssl 对管道传递的字符串计算出的哈希值并不在普通人预料以内。- "foo" | openssl.exe md5 -r
- 2145971cf82058b108229a3a2e3bff35 *stdin
复制代码 而 foo 这个字符串正确的 MD5 哈希值是- acbd18db4cc2f85cedef654fccc4a4d8
复制代码 这是一个非常经典的错误,造成这个错误的原因是,openssl 计算哈希值的字符串不是 foo,而是 foo 后面追加了一个 windows 的行结束符,也就是 \r\n,也就是说,如果你创建一个文本文件,输入foo,按一次回车键,文件格式是 CR LF(CR 是 \r,LF 是 \n),文件编码是 ASCII,这个文件的 MD5 哈希值是 2145971cf82058b108229a3a2e3bff35。
但,如果写代码测试管道,好像接收到的字符串好像没有多余的 \r\n。
在 Powershell 控制台内运行以下代码
- PS C:\Users\WuXiancheng> "foo" | & { [String]$Input.Length }
- 3
复制代码 在 cmd.exe 中运行以下代码
- C:\Users\Wuxiancheng\Downloads>echo foo| powershell -Command "[String]$Input.Length"
- 3
复制代码 得到的结果是一样的。
在 Powershell 中运行以下代码- "foo" | Out-File -NoNewline foo.txt
复制代码 文件的大小是 3 个字节,也就是说只有 foo,没有 \r\n。
在 cmd.exe 中运行以下代码文件 foo.txt 的大小是 5 个字节,也就是说被写入文件的内容确实是 foo\r\n
建一个 1.bat 内容如下- @echo off
- set /p foo=
- echo "%foo%"
复制代码 然后在 cmd.exe 中运行它
- C:\Users\orange\Downloads>echo foo|1.bat
- "foo"
复制代码 似乎得不出什么结论,Powershell、cmd.exe、openssl 三足鼎立,各自都有诡异之处,它们结合之后就始终是错。
解决方法之一- [System.Text.Encoding]::UTF8.GetBytes("foo") | openssl md5 -r
复制代码 将 "foo" 替换为需要计算哈希值的字符串,也可以使用单引号。
这种方法只适用于 Powershell 7.4 及更高版本。以下内容引用自官方文档。PowerShell 7.4 added the PSNativeCommandPreserveBytePipe experimental feature that preserves byte-stream data when redirecting the stdout stream of a native command to a file or when piping byte-stream data to the stdin stream of a native command.
解决方法之二 将字符串保存到文件中再使用 openssl 计算文件内容的哈希值 完成计算后删除这个文件
- $String = "吴先成"
- $TemporaryFileToHash = New-TemporaryFile | Select-Object -ExpandProperty FullName
- [System.IO.File]::WriteAllText($TemporaryFileToHash, $String, [System.Text.UTF8Encoding]::New($False))
- openssl "dgst" "-md5" "-r" $TemporaryFileToHash
- Remove-Item -Force -Path $TemporaryFileToHash
复制代码 需要注意两点。
一是,也可以直接使用 Powershell 的 Set-Content 保存内容到文件中,但它会添油加醋,最终写进文件的内容不见得和最初想要写进去的内容完全相同,比如,它会在行末加换行符,使用 -NoNewline 可以不加换行符,保存后文件最终使用的编码也有可能不是预期的编码,使用 -Encoding 可以指定文件编码,但在不同版本的 Powershell 中 -Encoding 支持的值也不同,即使都支持某一个值,它们代表的意义也可能不同,比如 Powershell 5.1 中 utf8 是带 BOM 头的 UTF-8 编码,Powershell 6.0+ 除了使用 utf8 还可以使用 utf8BOM 或 utf8NoBOM 分别将文件保存为有或没有 BOM 头的 UTF-8 编码,而且 utf8 等效于 utf8NoBOM,也就是不加 BOM 头,和 Powershell 5.1 刚好相反。BOM 头是加在文件正文内容前加的 0xEF 0xBB 0xBF 三个字节,这三个字节不会显示出来,但加了 BOM 头会改变文件内容,从而改变文件的哈希值,使用 .Net System.IO.File 的 WriteAllText() 方法可以保证写进文件的内容就是你想要写进入的内容,[System.Text.UTF8Encoding]::New($False) 的参数 $False 代表不加 BOM 头,换成 $True 会加上 BOM 头。
二是,如果将这段代码保存在文件中,文件的编码必须是 UTF-8 或 UTF-16,最好带上 BOM 头,否则有可能无法正常被 Powershell 引擎解析运行。
解决方法之三 这属于奇技淫巧 而且只可以在 cmd.exe 或 .bat .cmd 中执行- echo|set /p="foo" | openssl dgst -md5
复制代码 解决方法之四 使用 Get-FileHash 加 -InputStream 参数
- Using namespace System.IO
- $MemoryStream = [MemoryStream]::New()
- $StreamWriter = [StreamWriter]::New($MemoryStream)
- $StreamWriter.Write("foo")
- $StreamWriter.Flush()
- $MemoryStream.Position = 0
- Get-FileHash -Algorithm MD5 -InputStream $MemoryStream | Select-Object -ExpandProperty Hash
复制代码 这种方法实际上在内存中创建了一个临时文件,然后用计算文件哈希值的方法计算字符串哈希值。
如果将以上代码保存为 hash.ps1,将写死的 "foo" 改为 [String]$Input,然后从管道传递一个字符串给它。- PS C:\Users\WuXiancheng> "foo" | .\hash.ps1
- ACBD18DB4CC2F85CEDEF654FCCC4A4D8
复制代码 得到的哈希值是正确的,证明 Powershell 并没有给管道传递的值添加佐料,而是 openssl 对接收到的字符串做了加工,加上了 \r\n。
解决方法之五 自己写一个函数- <#
- Powershell计算字符串的MD5 SHA1 SHA256 SHA384 SHA512哈希值
- @Author 吴先成
- @E-mail ohcc@163.com
- @Param String $String 要计算哈希值的字符串 值可以是数组或单个字符串 参数名可以省略
- @Param String $Algorithm 哈希值算法 MD5 SHA1 SHA256 SHA384 SHA512之一 默认值SHA256
- @Notes 文件需要保存为UTF-8或UTF-16编码 否则会乱码或得到错误的哈希值
- #>
- Function Hash-String{
- Param(
- [String[]][Parameter(Position=0, ValueFromRemainingArguments)]$String,
- [String][ValidateSet("MD5", "SHA1", "SHA256", "SHA384", "SHA512")]$Algorithm="SHA256"
- )
- If($String.Count -ge 1){
- $HashProvider = New-Object -TypeName "System.Security.Cryptography.${Algorithm}CryptoServiceProvider"
- $UTF8Encoding = New-Object -TypeName System.Text.UTF8Encoding
- Write-Output "算法 哈希值 字符串"
- $String | ForEach-Object {
- $StringHash = [System.BitConverter]::ToString($HashProvider.ComputeHash($UTF8Encoding.GetBytes($_))).ToLower().Replace("-", "")
- Write-Output "$Algorithm $StringHash $_"
- }
- }
- }
复制代码 函数用法举例- Hash-String "foo" "bar" "你好"
- Hash-String -Algorithm MD5 -String "foo","bar","你好"
复制代码 最后两个办法不依赖 openssl,局限是支持的算法没有 openssl 那么多,但平常使用完全够用。
当然,每一种方法都可以封装成函数,Powershell 也是可以执行 cmd 代码的,给它套一个 cmd /c 就行了嘛。
以上各个示例中,调用 openssl 时没有使用路径和 .exe 扩展名,是因为 openss.exe 在 PATH 环境变量配置的目录之中,.exe 在 PATHEXT 配置之中。 |
|