找回密码
 新建账号

windows openssl dgst 不能正确计算字符串哈希值

[复制链接]
php 发表于 2024/9/21 23:47 | 显示全部楼层 |阅读模式
windows 版本的 openssl 计算从管道(pipeline)传递的字符串的哈希值时会得到错误的结果
  1. openssl dgst -md5 -r <file> ...
  2. openssl md5 -r <file> ...
  3. "string" | openssl dgst -md5 -r
  4. "string" | openssl md5 -r
复制代码
其中 -r 改变输出格式,可以省略,md5 是算法的一种,可以换成 sha1 sha256 sha512 等其它算法,不区分大小写,openssl 支持的算法很多。语法规则是,可以将这些算法当成子命令使用,也可以使用 dgst 作为子命令,将算法作为子命令 dgst 的参数,算法作为参数时,需要在前面加一个英文状态的中横线-,如 -md5,<file> 是要使用 openssl 计算哈希值的文件,可以有多个文件,用空格分开,而使用管道传递字符串计算字符串的哈希值时,openssl 对管道传递的字符串计算出的哈希值并不在普通人预料以内。
  1. "foo" | openssl.exe md5 -r
  2. 2145971cf82058b108229a3a2e3bff35 *stdin
复制代码
而 foo 这个字符串正确的 MD5 哈希值是
  1. acbd18db4cc2f85cedef654fccc4a4d8
复制代码
这是一个非常经典的错误,造成这个错误的原因是,openssl 计算哈希值的字符串不是 foo,而是 foo 后面追加了一个 windows 的行结束符,也就是 \r\n,也就是说,如果你创建一个文本文件,输入foo,按一次回车键,文件格式是 CR LF(CR 是 \r,LF 是 \n),文件编码是 ASCII,这个文件的 MD5 哈希值是 2145971cf82058b108229a3a2e3bff35。
但,如果写代码测试管道,好像接收到的字符串好像没有多余的 \r\n。
在 Powershell 控制台内运行以下代码
  1. PS C:\Users\WuXiancheng> "foo" | & { [String]$Input.Length }
  2. 3
复制代码
在 cmd.exe 中运行以下代码
  1. C:\Users\Wuxiancheng\Downloads>echo foo|  powershell -Command "[String]$Input.Length"
  2. 3
复制代码
得到的结果是一样的。
在 Powershell 中运行以下代码
  1. "foo" | Out-File -NoNewline foo.txt
复制代码
文件的大小是 3 个字节,也就是说只有 foo,没有 \r\n。
在 cmd.exe 中运行以下代码
  1. echo foo>foo.txt
复制代码
文件 foo.txt 的大小是 5 个字节,也就是说被写入文件的内容确实是 foo\r\n
建一个 1.bat 内容如下
  1. @echo off
  2. set /p foo=
  3. echo "%foo%"
复制代码
然后在 cmd.exe 中运行它
  1. C:\Users\orange\Downloads>echo foo|1.bat
  2. "foo"
复制代码
似乎得不出什么结论,Powershell、cmd.exe、openssl 三足鼎立,各自都有诡异之处,它们结合之后就始终是错。
解决方法之一
  1. [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 计算文件内容的哈希值 完成计算后删除这个文件
  1. $String = "吴先成"
  2. $TemporaryFileToHash = New-TemporaryFile | Select-Object -ExpandProperty FullName
  3. [System.IO.File]::WriteAllText($TemporaryFileToHash, $String, [System.Text.UTF8Encoding]::New($False))
  4. openssl "dgst" "-md5" "-r" $TemporaryFileToHash
  5. 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 中执行
  1. echo|set /p="foo" | openssl dgst -md5
复制代码
解决方法之四 使用 Get-FileHash 加 -InputStream 参数
  1. Using namespace System.IO
  2. $MemoryStream = [MemoryStream]::New()
  3. $StreamWriter = [StreamWriter]::New($MemoryStream)
  4. $StreamWriter.Write("foo")
  5. $StreamWriter.Flush()
  6. $MemoryStream.Position = 0
  7. Get-FileHash -Algorithm MD5 -InputStream $MemoryStream | Select-Object -ExpandProperty Hash
复制代码
这种方法实际上在内存中创建了一个临时文件,然后用计算文件哈希值的方法计算字符串哈希值。
如果将以上代码保存为 hash.ps1,将写死的 "foo" 改为 [String]$Input,然后从管道传递一个字符串给它。
  1. PS C:\Users\WuXiancheng> "foo" | .\hash.ps1
  2. ACBD18DB4CC2F85CEDEF654FCCC4A4D8
复制代码
得到的哈希值是正确的,证明 Powershell 并没有给管道传递的值添加佐料,而是 openssl 对接收到的字符串做了加工,加上了 \r\n。

解决方法之五 自己写一个函数
  1. <#
  2.     Powershell计算字符串的MD5 SHA1 SHA256 SHA384 SHA512哈希值
  3.     @Author 吴先成
  4.     @E-mail ohcc@163.com
  5.     @Param String $String 要计算哈希值的字符串 值可以是数组或单个字符串 参数名可以省略
  6.     @Param String $Algorithm 哈希值算法 MD5 SHA1 SHA256 SHA384 SHA512之一 默认值SHA256
  7.     @Notes 文件需要保存为UTF-8或UTF-16编码 否则会乱码或得到错误的哈希值
  8. #>
  9. Function Hash-String{
  10.     Param(
  11.         [String[]][Parameter(Position=0, ValueFromRemainingArguments)]$String,
  12.         [String][ValidateSet("MD5", "SHA1", "SHA256", "SHA384", "SHA512")]$Algorithm="SHA256"
  13.     )
  14.     If($String.Count -ge 1){
  15.         $HashProvider = New-Object -TypeName "System.Security.Cryptography.${Algorithm}CryptoServiceProvider"
  16.         $UTF8Encoding = New-Object -TypeName System.Text.UTF8Encoding
  17.         Write-Output "算法 哈希值 字符串"
  18.         $String | ForEach-Object {
  19.             $StringHash = [System.BitConverter]::ToString($HashProvider.ComputeHash($UTF8Encoding.GetBytes($_))).ToLower().Replace("-", "")
  20.             Write-Output "$Algorithm $StringHash $_"
  21.         }
  22.     }
  23. }
复制代码
函数用法举例
  1. Hash-String "foo" "bar" "你好"
  2. Hash-String -Algorithm MD5 -String "foo","bar","你好"
复制代码
最后两个办法不依赖 openssl,局限是支持的算法没有 openssl 那么多,但平常使用完全够用。
当然,每一种方法都可以封装成函数,Powershell 也是可以执行 cmd 代码的,给它套一个 cmd /c 就行了嘛。
以上各个示例中,调用 openssl 时没有使用路径和 .exe 扩展名,是因为 openss.exe 在 PATH 环境变量配置的目录之中,.exe 在 PATHEXT 配置之中。

手机版|轻松E站

GMT+8, 2024/12/21 20:41

快速回复 返回顶部 返回列表