ファイルの改ざんチェック、APIの署名確認、文字列の指紋づくり——SHA-256のハッシュ値がサッと欲しい場面は地味に多いです。でも、そのためだけにライブラリを入れたり、入力内容をどこかのオンラインツールに貼る(=送信する)のは、内容によっては気が引けます。
実は、ハッシュ計算はブラウザ標準の Web Crypto API だけで完結します。外部ライブラリも通信も不要です。この記事では、crypto.subtle.digest を使って SHA-256(および SHA-1 / SHA-384 / SHA-512)をブラウザ内で生成する方法と、ハマりやすい注意点をまとめます。
ゴール:文字列やファイルから、ライブラリなし・送信なしで SHA-256 ハッシュを求められるようになること。
最小コード
Web Crypto の crypto.subtle.digest(アルゴリズム, データ) にバイト列を渡すと、ハッシュを ArrayBuffer で返してくれます。文字列はそのまま渡せないので、TextEncoder で UTF-8 のバイト列にしてから渡します。
async function sha256(text) {
const data = new TextEncoder().encode(text); // 文字列 → UTF-8 バイト列
const buf = await crypto.subtle.digest("SHA-256", data); // ArrayBuffer が返る
return toHex(buf);
}
digest は Promise を返す非同期APIなので、await(または .then)が必要です。ここを忘れて同期的に使おうとすると、Promise { <pending> } が返ってきて「値が取れない」と混乱しがちなので注意。
ArrayBuffer を16進文字列に変換する
digest が返すのは生のバイト列(ArrayBuffer)です。私たちが見慣れた e3b0c4... のような16進文字列にするには、1バイトずつ2桁の16進に変換して連結します。
function toHex(buf) {
const bytes = new Uint8Array(buf);
let s = "";
for (const b of bytes) {
s += b.toString(16).padStart(2, "0"); // 1桁の値も必ず2桁に(0f など)
}
return s;
}
ポイントは padStart(2, "0")。b.toString(16) は 15 → "f" のように1桁になることがあり、これを放置するとハッシュがずれてしまいます。必ず2桁ゼロ埋めします。
ちなみに空文字列の SHA-256 は e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 です。実装が正しいかの確認に使えます。
落とし穴①:crypto.subtle は「セキュアコンテキスト」でないと使えない
これがいちばん多いハマりどころです。crypto.subtle は HTTPS もしくは localhost でないと undefined になります(セキュアコンテキスト限定のAPIのため)。file:// で直接HTMLを開いたり、http:// のローカルIPで開いたりすると動きません。
事前にガードを入れておくと親切です。
if (!("crypto" in window) || !crypto.subtle) {
// https もしくは localhost で開く必要がある
alert("この環境では暗号機能が使えません(https または localhost が必要です)");
return;
}
開発中は localhost、公開時は HTTPS。ここさえ押さえれば大丈夫です。
複数アルゴリズムをまとめて計算する
アルゴリズム名を変えるだけで SHA-1 / SHA-384 / SHA-512 も同じコードで計算できます。Promise.all で並行処理するとスッキリします。
async function digestAll(text, algos = ["SHA-256", "SHA-384", "SHA-512"]) {
const data = new TextEncoder().encode(text);
const results = await Promise.all(
algos.map(async (algo) => {
const buf = await crypto.subtle.digest(algo, data);
return [algo, toHex(buf)];
})
);
return Object.fromEntries(results);
}
落とし穴②:MD5 と SHA-1 の扱い
- MD5 は Web Crypto に含まれません。 ブラウザ標準で使えるのは SHA 系(SHA-1 / SHA-256 / SHA-384 / SHA-512)だけです。MD5 が必要なら別実装が要りますが、そもそも MD5 は壊れているので新規採用は避けるべきです。
- SHA-1 も非推奨。 衝突攻撃が現実的になっており、セキュリティ用途では使わないでください。レガシー互換の確認くらいに留めます。
- 新規なら SHA-256 が無難。より強度が欲しければ SHA-384 / SHA-512。
ファイルのハッシュも取れる
文字列だけでなく、ファイルの中身(ArrayBuffer)をそのまま digest に渡せます。ダウンロードしたファイルの整合性チェックなどに便利です。
async function sha256OfFile(file) {
const buf = await file.arrayBuffer(); // File / Blob → ArrayBuffer
const hash = await crypto.subtle.digest("SHA-256", buf);
return toHex(hash);
}
大きなファイルでも、メモリに乗る範囲ならこれで一発です(巨大ファイルをストリーミングで分割ハッシュするのはWeb Cryptoだけでは難しく、別の工夫が要ります)。
重要:パスワードの保存にこのまま使わない
「パスワードを SHA-256 にして保存すれば安全」と思いがちですが、それだけでは不十分です。単純なハッシュは高速なので、総当たりやレインボーテーブルに弱いからです。パスワードを扱うなら、
- ソルト(ランダムな追加データ)を付ける
- わざと遅いアルゴリズム(PBKDF2 / bcrypt / scrypt / Argon2)で何万回も繰り返す
のが定石です。Web Crypto なら PBKDF2 が使えます(crypto.subtle.deriveBits + importKey)。SHA-256 の digest 単体は、改ざん検知・指紋づくり・署名検証の部品としては最適ですが、パスワード保存そのものには使わないと覚えておくと安全です。
まとめ
- ハッシュは Web Crypto(
crypto.subtle.digest)だけでブラウザ内生成でき、ライブラリも送信も不要 - 文字列は
TextEncoderで UTF-8 バイト列にしてから渡す - 返り値は
ArrayBuffer。Uint8Array→toString(16)+padStart(2,"0")で16進化 -
digestは Promise(await必須) -
crypto.subtleは https / localhost 限定(file:// やhttpでは undefined) - MD5は非対応・SHA-1は非推奨、新規は SHA-256 を
- ファイルも
arrayBuffer()で同じように計算できる - パスワード保存にはソルト+PBKDF2等を使う(単純ハッシュは不可)
「ハッシュがちょっと欲しい」に、Web Crypto × ブラウザ完結はとてもよく効きます。参考になれば嬉しいです。