できない.dev

文字列の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 hashing

hashlib.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 で止まる。

できないときNode.js「error:0308010C:digital envelope routines::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"));
// 5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03

Web 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)); // 2CF24DBA5FB0A30E26E83B2AC5B9E29E1B161E5C1FA7425E73043362938B9824

SHA256.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…938b9824
  • hello\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 は高速に計算できるため、パスワードの保存にそのまま使う用途には向かない。