文字列のSHA-256ハッシュ値を求めるには
文字列を UTF-8 のバイト列にして SHA-256 のハッシュ値を求め、64 文字の 16 進数の文字列として取り出す方法を各言語で示す。
文字コードや末尾の改行で値が変わる点と、分けて渡す書き方までを扱う。
公開:
選んだ言語は、ほかのサンプルコードを開いたときも最初に表示する。
Python
import hashlib
text = "hello"
digest = hashlib.sha256(text.encode("utf-8")).hexdigest()
print(digest) # 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
print(len(digest)) # 64
# 末尾の改行が 1 文字あるだけで別の値になる
print(hashlib.sha256(b"hello\n").hexdigest())
# 5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03
# 大きなデータは update で分けて渡しても同じ値になる
h = hashlib.sha256()
h.update(b"hel")
h.update(b"lo")
print(h.hexdigest() == digest) # True
try:
hashlib.sha256("hello")
except TypeError as e:
print(e) # Strings must be encoded before hashinghashlib.sha256 にバイト列を渡して hexdigest() を呼ぶと、64 文字の 16 進数の文字列が得られる。
文字列をそのまま渡すと TypeError になるため、先に encode("utf-8") でバイト列にする。
大きなデータは update に分けて渡しても結果が変わらない。
JavaScript
const { createHash } = require("node:crypto");
const text = "hello";
const digest = createHash("sha256").update(text, "utf8").digest("hex");
console.log(digest); // 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
console.log(digest.length); // 64
// 末尾の改行が 1 文字あるだけで別の値になる
console.log(createHash("sha256").update("hello\n").digest("hex"));
// 5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03
// 大きなデータは update を重ねて渡しても同じ値になる
const h = createHash("sha256");
h.update("hel");
h.update("lo");
console.log(h.digest("hex") === digest); // true
try {
createHash("sha257"); // 存在しないアルゴリズム名
} catch (e) {
console.log(e.message); // Digest method not supported
}
try {
createHash("md4"); // OpenSSL 3 が既定で無効にしている古いアルゴリズム
} catch (e) {
console.log(e.code); // ERR_OSSL_EVP_UNSUPPORTED
}Node.js では crypto の createHash("sha256") に update でデータを渡し、digest("hex") で 16 進数の文字列を受け取る。update に文字列を渡すと UTF-8 として扱われる。md4 のように OpenSSL 3 が既定で無効にしているアルゴリズムを指定すると、ERR_OSSL_EVP_UNSUPPORTED で止まる。
TypeScript
async function sha256Hex(text: string): Promise<string> {
const bytes: Uint8Array = new TextEncoder().encode(text);
const buffer: ArrayBuffer = await crypto.subtle.digest("SHA-256", bytes);
return Array.from(new Uint8Array(buffer), (b) => b.toString(16).padStart(2, "0")).join("");
}
const digest: string = await sha256Hex("hello");
console.log(digest); // 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
console.log(digest.length); // 64
// 末尾の改行が 1 文字あるだけで別の値になる
console.log(await sha256Hex("hello\n"));
// 5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03Web Crypto API の crypto.subtle.digest は Promise を返すので、await で結果を待つ。
戻り値は ArrayBuffer なので、1 バイトずつ padStart(2, "0") で 2 桁の 16 進数にそろえて連結する。Buffer を使わないため、ブラウザでも Node.js でも同じ書き方で動く。
Go
package main
import (
"crypto/sha256"
"encoding/hex"
"fmt"
)
func main() {
sum := sha256.Sum256([]byte("hello"))
digest := hex.EncodeToString(sum[:])
fmt.Println(digest) // 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
fmt.Println(len(digest)) // 64
// 末尾の改行が 1 文字あるだけで別の値になる
sum2 := sha256.Sum256([]byte("hello\n"))
fmt.Println(hex.EncodeToString(sum2[:]))
// 5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03
// 大きなデータは Write を重ねて渡しても同じ値になる
h := sha256.New()
h.Write([]byte("hel"))
h.Write([]byte("lo"))
fmt.Println(hex.EncodeToString(h.Sum(nil)) == digest) // true
// %x でも 16 進数の文字列にできる
fmt.Printf("%x\n", sum) // 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
}標準ライブラリの crypto/sha256 の Sum256 は、32 バイトの配列([32]byte)を返す。
16 進数の文字列にするには hex.EncodeToString(sum[:]) か %x の書式を使う。
複数回に分けて渡すときは sha256.New() で作ったハッシュに Write を重ね、最後に Sum(nil) で取り出す。
Rust
// Cargo.toml: sha2 = "0.10"
use sha2::{Digest, Sha256};
fn main() {
let digest = Sha256::digest("hello".as_bytes());
println!("{:x}", digest); // 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
println!("{}", digest.len()); // 32(バイト数。16 進数にすると 64 文字)
// 末尾の改行が 1 文字あるだけで別の値になる
println!("{:x}", Sha256::digest(b"hello\n"));
// 5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03
// 大きなデータは update を重ねて渡しても同じ値になる
let mut hasher = Sha256::new();
hasher.update(b"hel");
hasher.update(b"lo");
println!("{}", hasher.finalize() == digest); // true
}標準ライブラリに SHA-256 は無いため、sha2 クレートを Cargo.toml に追加して使う。Sha256::digest は 32 バイトの結果を返し、{:x} の書式でそのまま 16 進数の文字列として出力できる。
分けて渡すときは Sha256::new() に update を重ね、finalize で取り出す。
Java
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
public class Main {
public static void main(String[] args) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest("hello".getBytes(StandardCharsets.UTF_8));
String digest = HexFormat.of().formatHex(hash);
System.out.println(digest); // 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
System.out.println(digest.length()); // 64
// 末尾の改行が 1 文字あるだけで別の値になる
System.out.println(HexFormat.of().formatHex(md.digest("hello\n".getBytes(StandardCharsets.UTF_8))));
// 5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03
// 大きなデータは update を重ねて渡しても同じ値になる
md.update("hel".getBytes(StandardCharsets.UTF_8));
md.update("lo".getBytes(StandardCharsets.UTF_8));
System.out.println(HexFormat.of().formatHex(md.digest()).equals(digest)); // true
try {
MessageDigest.getInstance("SHA-2");
} catch (java.security.NoSuchAlgorithmException e) {
System.out.println(e.getMessage()); // SHA-2 MessageDigest not available
}
}
}MessageDigest.getInstance("SHA-256") で作ったオブジェクトの digest に、文字コードを明示したバイト列を渡す。
結果は byte[] なので、Java 17 以降の HexFormat.of().formatHex で 16 進数の文字列にする。
アルゴリズム名を SHA-2 のように誤ると NoSuchAlgorithmException になる。
C#
using System;
using System.Security.Cryptography;
using System.Text;
var hash = SHA256.HashData(Encoding.UTF8.GetBytes("hello"));
var digest = Convert.ToHexString(hash).ToLowerInvariant();
Console.WriteLine(digest); // 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
Console.WriteLine(digest.Length); // 64
// 末尾の改行が 1 文字あるだけで別の値になる
Console.WriteLine(Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes("hello\n"))).ToLowerInvariant());
// 5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03
// ToHexString は大文字で返す
Console.WriteLine(Convert.ToHexString(hash)); // 2CF24DBA5FB0A30E26E83B2AC5B9E29E1B161E5C1FA7425E73043362938B9824SHA256.HashData にバイト配列を渡すと、32 バイトのハッシュ値が返る。Convert.ToHexString で 16 進数の文字列にできるが、結果は大文字になるため、他の言語や sha256sum の出力と比べるときは ToLowerInvariant で小文字にそろえる。
つまずき
ハッシュ関数が受け取るのは文字列ではなくバイト列である。
Python の hashlib.sha256("hello") のように文字列をそのまま渡すと、TypeError(Strings must be encoded before hashing)で止まる。
Go・Rust・Java・C# も、文字列をバイト列にしてから関数に渡す。
hello の SHA-256 は、どの言語でも次の 64 文字になった。
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
この 64 文字は 32 バイトを 16 進数で書いたもので、Rust の例では結果の長さが 32 バイトと出る。
文字コードで値が変わる
同じ文字列でも、バイト列にするときの文字コードが違えば、ハッシュ値は別物になる。こんにちは を Python で求めると、次の結果になった。
- UTF-8:
125aeadf27b0459b8760c13a3d80912dfa8a81a68261906f60d87f4a0268646c - Shift_JIS(
cp932):58f4daa97f82ff3d1e2ddf6f1b750f5c4511fa1e7aea6340c4990ae0eac3974e - UTF-16(リトルエンディアン):
9e9eefc64149707e676053033bf0b1329f3565efb55ca77d356eac65d690595d
Node.js の update("こんにちは") と sha256sum の出力は、UTF-8 の値と一致した。
他のシステムが出した値と突き合わせるときは、まず UTF-8 でそろえる。
末尾の改行で値が変わる
hello と hello に改行 1 文字を足した hello\n は、別の値になる。
hello:2cf24dba…938b9824hello\n:5891b5b5…f6be03
シェルで echo hello | sha256sum を実行すると、改行付きの 5891b5b5… が出る。
コードで求めた値とシェルの出力が合わないときは、末尾の改行を疑う。
改行を付けない printf hello | sha256sum なら 2cf24dba… になった。
出力の形式と使えない名前
ハッシュ値の取り出し方は言語ごとに違った。
- Python・JavaScript:
hexdigest()やdigest("hex")で 16 進数の文字列がそのまま得られる - Go・Rust・Java:32 バイトのバイト列が返るので、
%xやHexFormatで 16 進数にする - C#:
Convert.ToHexStringは大文字で返す
アルゴリズム名の指定にも癖がある。
Java の getInstance は SHA-256 と SHA256 のどちらも通ったが、SHA-2 は NoSuchAlgorithmException になった。
Node.js は存在しない名前を Digest method not supported で拒否する。
なお SHA-256 は高速に計算できるため、パスワードの保存にそのまま使う用途には向かない。