背景
自分のサイトに、テキストの SHA-256 をブラウザで出すツールを置いています。
SHA-256 は、任意のデータから256ビット(16進数で64文字)の値を作るハッシュ関数だ。
入力が1バイトでも違うとまったく別の値になるので、ファイルが改ざんされていないかの確認などに使われます。
先日、ツールで出した値と、ターミナルで出した値を見比べたら合いませんでした。
ツールが壊れたかと思って、しばらく JavaScript 側を疑っていました(実際は自分のコマンドのほうでした…)。
同じ「abc」なのに値が何種類も出たので、その内訳を残しておきます。
結論
ハッシュは文字ではなくバイト列に対して計算されます。
echo "abc" は末尾に改行を1バイト足すので、abc のハッシュにはなりません。
Windows の PowerShell でファイルに書くと、改行が CRLF(Windows 流の、0d 0a の2バイトで表す改行)になったり、先頭に BOM が付いたり、文字コード自体が変わったりします。
値が合わないときは、ハッシュを疑う前に、入力のバイト数と中身を16進数で確認するのが近道です。
ツール側が計算しているもの
ツールは、入力欄の文字列を TextEncoder で UTF-8 のバイト列にしてから、Web Crypto API に渡しています。
UTF-8 は、ASCII の文字を1バイトで、日本語などを2〜4バイトで表す文字コードだ。
Web Crypto API はブラウザ標準の暗号機能で、crypto.subtle.digest がハッシュを計算する関数です。
下記は、ツールと同じ処理を Node.js v24.11.1 で動かしたものです。
crypto.subtle.digest('SHA-256', new TextEncoder().encode('abc'))
.then(b => console.log(Buffer.from(b).toString('hex')));
出力は下記でした。
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
これは abc の3バイト(61 62 63)に対する SHA-256 で、よく例に出てくる値と同じです。
入力欄に改行を入れなければ、ツールは余計なバイトを足しません。
echo は改行を足す
Git Bash(bash 5.2)で、同じ abc を3通りに渡してみました。
sha256sum は GNU coreutils の、標準入力やファイルの SHA-256 を出すコマンドだ。
$ echo "abc" | sha256sum
edeaaff3f1774ad2888673770c6d64097e391bc362d7d6fb34982ddf0efd18cb *-
$ printf 'abc' | sha256sum
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad *-
$ echo -n abc | sha256sum
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad *-
echo だけ値が違います。
echo は引数の後ろに改行(0a)を出力するので、ハッシュに渡っているのは 61 62 63 0a の4バイトだ。
printf 'abc' と echo -n は改行を足さないので、ツールの値と一致しました。
PowerShell でファイルに書くともっとずれる
Windows では、PowerShell で作ったファイルを Get-FileHash に掛けて比べることも多いと思います。
Get-FileHash は、ファイルのハッシュを出す PowerShell のコマンドだ。
下記のスクリプトを、Windows PowerShell 5.1 でプロファイルを読まずに(-NoProfile で)実行しました。
Out-File と Set-Content でそれぞれ "abc" を書き、バイト列とハッシュを並べています。
"abc" | Out-File h4.txt
$b = [IO.File]::ReadAllBytes("$PWD\h4.txt")
"$($b.Length) [$(($b | ForEach-Object { '{0:x2}' -f $_ }) -join ' ')] $((Get-FileHash h4.txt -Algorithm SHA256).Hash)"
"abc" | Set-Content h5.txt
$b = [IO.File]::ReadAllBytes("$PWD\h5.txt")
"$($b.Length) [$(($b | ForEach-Object { '{0:x2}' -f $_ }) -join ' ')] $((Get-FileHash h5.txt -Algorithm SHA256).Hash)"
出力は下記でした。
12 [ff fe 61 00 62 00 63 00 0d 00 0a 00] ABBC6233C875D9F75760B15CACC6B80D4B7D844B30A506971E2C8C9315755263
5 [61 62 63 0d 0a] 552BAB6864C7A7B69A502ED1854B9245C0E1A30F008AAA0B281DA62585FDB025
Out-File は12バイトになりました。
先頭の ff fe は BOM(バイトオーダーマーク)で、ファイルの文字コードを示すために頭に置く目印だ。
その後ろが UTF-16LE(1文字を2バイトで表し、小さい桁を先に置く形)の abc と、CRLF の改行です。
UTF-16 なので、改行も 0d 00 0a 00 の4バイトになっている。
Set-Content のほうは UTF-16 にはならず abc と CRLF の5バイトでしたが、それでも改行ぶん値が変わります。
ちなみに私の普段の環境では $PSDefaultParameterValues で Out-File の文字コードを utf8 にしていて、そのときは ef bb bf 61 62 63 0d 0a(UTF-8 の BOM 付き)の8バイトになりました。
同じコマンドでも、設定しだいでバイト列が変わるということです…
合わせるには
改行も BOM も入れずに書きたいときは、.NET の WriteAllText を使います。
Windows PowerShell 5.1 から呼ぶと、BOM なしの UTF-8 で、渡した文字列だけを書いてくれました。
[IO.File]::WriteAllText("$PWD\h3.txt", "abc")
(Get-FileHash h3.txt -Algorithm SHA256).Hash
出力は BA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD で、3バイトの abc と一致しました(大文字なのは Get-FileHash の表示の都合です)。
なぜツール側で末尾の改行を自動で削らないのかというと、改行込みのハッシュが欲しい人もいるからです。
入力をこっそり変えると、今度は「改行込みで計算したいのに合わない」という逆の事故になる。
なので、ツールは入力欄の中身をそのままバイト列にする作りのままにしています。
作ったツールはここに置いています。
https://hashitosystem.com/tools/hash/
ハッシュが合わないときは、まず printf で改行を足さずに計算し直し、それでも違えばファイルのバイト数と先頭・末尾の16進を見る。
ほとんどの場合、原因は末尾の改行か BOM か文字コードのどれかです。
本記事はAI補助で執筆した、個人開発の紹介記事です。