echoのSHA-256が一致しない理由 改行とPowerShellの文字コード

Web のハッシュ計算ツールに abc と入れると ba7816bf... になるのに、ターミナルで echo abc | sha256sum と打つと edeaaff3... になる。計算が間違っているわけではない。ハッシュ関数に渡ったバイト列が違っている。

SHA-256 は文字列ではなくバイト列を受け取る。画面で同じ abc に見えても、末尾の改行や文字コードが違えば、まったく別の値になる。この記事では、シェルごとに実際に何バイトが渡るのかを測った。とくに Windows PowerShell 5.1 は、日本語を ? に置き換えてからパイプへ流す。最後に、一致しないハッシュ値から原因のバイト列を特定するスクリプトを示す。

検証環境は Windows 11(日本語)、Node.js v24.11.1、Python 3.11.9、Windows PowerShell 5.1、Git Bash(bash 5.2、GNU coreutils 8.32 の sha256sum)である。

同じ abc で SHA-256 は 6 種類になった

バイト列は、受け取る側の Python で sys.stdin.buffer.read() を表示するか、できたファイルの先頭を読んで確かめた。

打ったもの 渡ったバイト列(16進) SHA-256 の先頭 8 桁
bash の printf abc、echo -n abc 61 62 63 ba7816bf
bash の echo abc、sha256sum <<< abc 61 62 63 0a edeaaff3
cmd.exe の echo で、パイプの直前に空白なし 61 62 63 0d 0a 552bab68
cmd.exe の echo で、パイプの直前に空白あり 61 62 63 20 0d 0a fff01482
PowerShell 5.1 の "abc" をパイプ(コードページ 932) 61 62 63 0d 0a 552bab68
PowerShell 5.1 の "abc" をパイプ(コードページ 65001) ef bb bf 61 62 63 0d 0a c3199f17
PowerShell 5.1 の "abc" > file ff fe 61 00 62 00 63 00 0d 00 0a 00 abbc6233

正しいのは ba7816bf だけである、とは言えない。どの値も、渡ったバイト列に対しては正しい。Web ツールや API と比べたいなら、相手が受け取るのと同じバイト列を作る必要がある。

1. 末尾の改行が 1 バイト入る

echo は引数の後ろに改行を付ける。abc は 3 バイトだが、echo abc の出力は 4 バイトになる。ヒアストリング(<<<)も同じで、末尾に改行が付く。

エディタで保存したファイルも同様である。最終行に改行を入れる設定なら、abc と書いたつもりのファイルは 61 62 63 0a になる。sha256sum の結果は edeaaff3 で、printf abc とは合わない。

echo -n で改行を消せるように見えるが、これは移植性がない。POSIX の echo はオプションを定めておらず、最初の引数が -n のときの動作を規格の範囲内で実装に任せている。XSI 準拠のシステムでは文字列として出力する。bash で POSIX モードと xpg_echo を有効にすると、実際にそうなった。

1
2
$ bash -c 'set -o posix; shopt -s xpg_echo; echo -n abc | xxd -p'
2d6e206162630a

2d 6e 20 は -n である。バックスラッシュの扱いも実装で分かれる。echo "a\tb" は、この環境の bash では \t がそのまま残り(61 5c 74 62 0a)、dash ではタブになった(61 09 62 0a)。POSIX も、新しく書くものには printf を使うよう勧めている。

1
printf '%s' 'abc' | sha256sum

2. cmd.exe はパイプの前の空白も出力する

cmd.exe の echo は CRLF を付ける。さらに、| の前の空白まで出力に含める。

1
2
3
4
> cmd /c "echo abc| python -c ""import sys;print(sys.stdin.buffer.read())"""
b'abc\r\n'
> cmd /c "echo abc | python -c ""import sys;print(sys.stdin.buffer.read())"""
b'abc \r\n'

読みやすくしようとパイプの前に空白を入れただけで、ハッシュ値が変わる。

3. PowerShell 5.1 は日本語を ? に置き換えてパイプへ流す

ここがいちばん気づきにくい。Windows PowerShell 5.1 では、外部コマンドへパイプするときの文字コードを $OutputEncoding が決める。Microsoft のドキュメントでは、5.1 の既定値は ASCIIEncoding である。ASCII にない文字は ? になる。

1
2
> "こんにちは" | python -c "import sys;print(sys.stdin.buffer.read())"
b'?????\r\n'

コードページ 932 で実行した結果である。5 文字すべてが 3f(?)になり、CRLF が付いた。このまま openssl などへ流すと、こんにちは ではなく ?????\r\n のハッシュ(488fdf9c)を計算することになる。別の日本語 5 文字でも同じ値が出るので、エラーにならず、気づきにくい。

$OutputEncoding を UTF-8 に変えると、文字は正しく渡る。ただし CRLF は残る。

1
2
3
$OutputEncoding = New-Object Text.UTF8Encoding $false
"こんにちは" | python -c "import sys;print(sys.stdin.buffer.read())"
# b'\xe3\x81\x93\xe3\x82\x93\xe3\x81\xab\xe3\x81\xa1\xe3\x81\xaf\r\n'

コンソールのコードページを 65001(chcp 65001)にして同じことをすると、先頭に ef bb bf(BOM)まで付いた。上の表の c3199f17 がこれである。$OutputEncoding を BOM なしの UTF-8 にしても消えなかった。コードページの設定によって結果が変わるので、パイプで文字列を渡す方法は避けたほうがよい。

なお、PowerShell 7 の $OutputEncoding の既定値は、ドキュメントでは UTF8Encoding となっている。今回 PowerShell 7 では測っていない。

4. PowerShell 5.1 の > は UTF-16 のファイルを作る

"abc" > file で作ったファイルの中身は、BOM(ff fe)付きの UTF-16LE に CRLF が付いたものだった。Out-File の 5.1 のドキュメントでも、-Encoding の既定値は UTF-16LE を表す unicode である。5.1 の about_Redirection には > は UTF8NoBOM を使うと書かれているが、この環境の実測は UTF-16LE で、Out-File の説明と一致した。

Out-File -Encoding utf8 は BOM 付きの UTF-8 になった(ef bb bf 61 62 63 0d 0a)。Set-Content -NoNewline -Encoding ascii なら 61 62 63 の 3 バイトになり、Get-FileHash の結果は BA7816BF... と一致した。

5. 文字コードと正規化で変わる

日本語は、文字コードが違えば別のバイト列になる。

文字列 形式 SHA-256 の先頭 8 桁
こんにちは UTF-8 125aeadf
こんにちは Shift_JIS 58f4daa9
こんにちは UTF-16LE(BOM なし) 9e9eefc6
が NFC(e3 81 8c) e3b21956
が NFD(e3 81 8b e3 82 99) f62b28f7

最後の 2 行は、見た目も文字コードも同じで、Unicode の正規化形だけが違う。macOS のファイル名などから NFD の文字列が来ると、NFC の文字列と同じに見えてもハッシュは一致しない。

6. ファイルをテキストとして読むと変わる

CRLF のファイルを Python の open() で普通に開くと、universal newlines によって \r\n が \n に変換される。

1
2
3
4
import hashlib
# crlf.txt の中身は 61 62 63 0d 0a
print(hashlib.sha256(open('crlf.txt', 'rb').read()).hexdigest()[:8]) # 552bab68
print(hashlib.sha256(open('crlf.txt', encoding='utf-8').read().encode()).hexdigest()[:8]) # edeaaff3

sha256sum crlf.txt と合うのは 'rb' で開いたほうである。BOM の扱いも読み方で変わる。Python の encoding='utf-8' は BOM を  として文字列に残し、utf-8-sig なら取り除く。Node.js の readFileSync(p, 'utf8') は、CRLF も BOM も残した。ファイルのハッシュは、必ずバイト列のまま計算する。

Webhook の署名検証で生ボディを使う必要があるのも、同じ理由である。詳しくは Webhook 署名の HMAC-SHA256 検証の記事 に書いた。Git の設定で作業ツリーの改行が変わる話は、改行コードの記事 にまとめてある。

正しい渡し方

余計なバイトを入れない書き方を、環境別にまとめる。いずれも今回の環境で 125AEADF...(こんにちは の UTF-8)または ba7816bf...(abc)になることを確かめた。

1
2
# bash
printf '%s' 'こんにちは' | sha256sum
1
2
3
4
# Windows PowerShell 5.1(パイプを通さず、バイト列を直接渡す)
$b = [Text.Encoding]::UTF8.GetBytes("こんにちは")
$m = [IO.MemoryStream]::new($b)
(Get-FileHash -InputStream $m -Algorithm SHA256).Hash
1
2
3
# Python
import hashlib
print(hashlib.sha256('こんにちは'.encode('utf-8')).hexdigest())
1
2
3
// Node.js
const { createHash } = require('node:crypto');
console.log(createHash('sha256').update('こんにちは', 'utf8').digest('hex'));

PowerShell の例は .ps1 を日本語を含むファイルとして保存するなら、BOM 付き UTF-8 にしておく。5.1 は BOM のないスクリプトを ANSI コードページとして読むためで、今回の検証も BOM 付きで保存して実行した。

実際に試す

前提: Node.js(確認は v24.11.1)。外部パッケージは不要で、ネットワークにも接続しない。元の文字列と一致させたいハッシュ値を渡すと、文字コード 7 種類、末尾 4 種類、正規化 2 種類の組み合わせを試し、一致したバイト列を表示する。ハッシュの種類は長さから判定する(MD5、SHA-1、SHA-256、SHA-512)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// which-bytes.mjs(Node.js v24.11.1 で確認)
// 使い方: node which-bytes.mjs <元の文字列> <一致させたいハッシュ値>
import { createHash } from 'node:crypto';

const [text, expected] = process.argv.slice(2);
const algo = { 32: 'md5', 40: 'sha1', 64: 'sha256', 128: 'sha512' }[expected?.length];
if (text === undefined || !algo) {
console.error('usage: node which-bytes.mjs <text> <md5|sha1|sha256|sha512 hex>');
process.exit(2);
}

// Shift_JIS は TextDecoder の逆引きで作る(外部パッケージなし)
const sjisTable = new Map();
const decoder = new TextDecoder('shift_jis');
for (let lead = 0x81; lead <= 0xfc; lead++) {
for (let trail = 0x40; trail <= 0xfc; trail++) {
const bytes = Uint8Array.of(lead, trail);
const ch = decoder.decode(bytes);
if ([...ch].length === 1 && ch !== '�' && !sjisTable.has(ch)) sjisTable.set(ch, bytes);
}
}
function toSjis(s) {
const out = [];
for (const ch of s) {
if (ch.codePointAt(0) < 0x80) out.push(ch.codePointAt(0));
else if (sjisTable.has(ch)) out.push(...sjisTable.get(ch));
else return null; // Shift_JIS にない文字
}
return Buffer.from(out);
}

const encodings = {
'UTF-8': (s) => Buffer.from(s, 'utf8'),
'UTF-8 BOM付き': (s) => Buffer.concat([Buffer.from('efbbbf', 'hex'), Buffer.from(s, 'utf8')]),
'UTF-16LE': (s) => Buffer.from(s, 'utf16le'),
'UTF-16LE BOM付き': (s) => Buffer.concat([Buffer.from('fffe', 'hex'), Buffer.from(s, 'utf16le')]),
'Shift_JIS': toSjis,
'ASCII(非ASCIIは?)': (s) => Buffer.from(s.replace(/[^\x00-\x7f]/gu, '?'), 'latin1'),
'ASCII(非ASCIIは?) BOM付き': (s) =>
Buffer.concat([Buffer.from('efbbbf', 'hex'), Buffer.from(s.replace(/[^\x00-\x7f]/gu, '?'), 'latin1')]),
};
const endings = { なし: '', LF: '\n', CRLF: '\r\n', '空白+CRLF': ' \r\n' };
const forms = ['NFC', 'NFD'];

const want = expected.toLowerCase();
const seen = new Set();
let tried = 0;
for (const form of forms) {
for (const [endName, end] of Object.entries(endings)) {
for (const [encName, encode] of Object.entries(encodings)) {
const buf = encode(text.normalize(form) + end);
if (!buf) continue;
tried++;
if (createHash(algo).update(buf).digest('hex') !== want) continue;
const hex = buf.toString('hex');
if (seen.has(hex)) continue;
seen.add(hex);
console.log(`一致: ${encName} / 末尾 ${endName} / ${form} -> ${hex}`);
}
}
}
if (seen.size === 0) console.log(`一致なし(${tried} 通りを試した)`);

実行結果は次のとおりである。ハッシュ値は上の表の完全な値を渡している。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ node which-bytes.mjs abc edeaaff3f1774ad2888673770c6d64097e391bc362d7d6fb34982ddf0efd18cb
一致: UTF-8 / 末尾 LF / NFC -> 6162630a

$ node which-bytes.mjs abc ABBC6233C875D9F75760B15CACC6B80D4B7D844B30A506971E2C8C9315755263
一致: UTF-16LE BOM付き / 末尾 CRLF / NFC -> fffe6100620063000d000a00

$ node which-bytes.mjs abc fff0148214f5513e57cdeea0c3debb262f0a8fdc481fdcd480449716efb03491
一致: UTF-8 / 末尾 空白+CRLF / NFC -> 616263200d0a

$ node which-bytes.mjs こんにちは 58f4daa97f82ff3d1e2ddf6f1b750f5c4511fa1e7aea6340c4990ae0eac3974e
一致: Shift_JIS / 末尾 なし / NFC -> 82b182f182c982bf82cd

$ node which-bytes.mjs こんにちは 488fdf9c85244f22f7beca27eaa9e4abbcaaf8111a5680f78cbb898b5887111c
一致: ASCII(非ASCIIは?) / 末尾 CRLF / NFC -> 3f3f3f3f3f0d0a

$ node which-bytes.mjs が f62b28f746dd75126c7aa8a11b16c2f6e18ecdfc6d62b8488b8e8637d3a2178d
一致: UTF-8 / 末尾 なし / NFD -> e3818be38299

$ node which-bytes.mjs abc 0000000000000000000000000000000000000000000000000000000000000000
一致なし(56 通りを試した)

一致なし の場合は、元の文字列そのものが違うか、ここにない変換(ソルト、HMAC、JSON の整形など)が入っている。このスクリプトが調べるのは、同じ文字列の表現の違いだけである。

ハシトシステムのハッシュ生成では

ハッシュ生成 は、入力欄の文字列を TextEncoder で UTF-8 にして、crypto.subtle.digest に渡す。2026-11-08 時点のコードに前後の空白を取り除く処理はないので、入力欄の末尾に改行があれば、その改行もハッシュに入る。printf の結果と比べるときは、貼り付けた文字列の末尾に改行が残っていないか確かめるとよい。

一方で、入力欄は <textarea> なので、CRLF はハッシュに入らない。HTML Standard では、textarea の value は改行を LF に正規化した API value を返す。本番のツールで abc\r\n を値として設定すると、value の中身は abc\n になり、SHA-256 は edeaaff3 だった。CRLF を含むテキストのハッシュは、このツールでは再現できない。CRLF のファイルと比べるなら、ファイルのまま sha256sum や Get-FileHash にかける。

値が合わないときは、同じ文字列を 文字コードダンプ で UTF-8 のバイト列にして、相手のバイト列と見比べられる。改行コードは 改行コード変換、濁点の合成と分解は Unicode 正規化フォーム比較 で確かめられる。いずれもブラウザ内で処理され、入力はサーバーに送られない。

まとめ

  • ハッシュが合わないときは、計算ではなくバイト列を疑う。同じ abc が、シェルと書き方によって 6 種類の SHA-256 になった。
  • echo は改行を付ける。echo -n は移植性がないので、printf '%s' を使う。
  • cmd.exe の echo は CRLF を付け、| の前の空白も出力する。
  • Windows PowerShell 5.1 の既定では、パイプで日本語が ? になり、CRLF が付く。コードページ 65001 では BOM も付いた。> は UTF-16LE のファイルを作る。文字列のハッシュは Get-FileHash -InputStream にバイト列を渡して計算する。
  • ファイルはテキストモードで読まず、バイト列のままハッシュする。
  • <textarea> の値は改行が LF に正規化されるので、Web ツールでは CRLF のハッシュを再現できない。

結果は 2026-11-08 時点の上記の環境で得たものである。シェルの版やコードページの設定で変わるので、手元では xxd などでバイト列を確かめてほしい。

参考

  • POSIX.1-2024(The Open Group Base Specifications Issue 8)— echo
  • Microsoft Learn — about_Preference_Variables($OutputEncoding、PowerShell 5.1 / 7.5)
  • Microsoft Learn — Out-File(PowerShell 5.1、-Encoding)、about_Redirection
  • Python ドキュメント — open()(newline、universal newlines)
  • WHATWG HTML Standard — 4.10.11 The textarea element(raw value、API value)

echoのSHA-256が一致しない理由 改行とPowerShellの文字コード
https://blog.hashito.biz/2026/11/08/sha256-echo-trailing-newline-powershell-output-encoding-mismatch/
著者
hashito
作成日
2026年11月8日
著作権