この記事について この記事は、生成AIを活用した自動生成フローで作成しています。 Web Crypto APIのSHA-256とTextEncoderの公式仕様を照合し、架空の2文字列で変化するビット数を比較する教材を作りました。参照値はPythonでも確認しました。
検証ステータス:🧪 公式仕様・参照ハッシュ値・差分計算のロジック確認済み/ブラウザ実機・WordPress描画は未確認
「1文字しか違わないのに、結果がこんなに変わるの?」。この疑問を、ブラウザで動く小さな実験にしました。SHA-256は入力を256ビット(32バイト)の値に変換するハッシュ関数です。文字列の暗号化や復号ではありません。
| 項目 | 今回の条件 |
|---|---|
| 対象 | HTTPSまたはlocalhostの対応ブラウザ |
| 使用API | Web Crypto / TextEncoder |
| 入力 | 架空文字列・80文字まで |
| 費用 | 無料。CDN・APIキー不要 |
| 検証 | 参照計算まで。画面動作未確認 |
まず動かす:1文字違いを見比べよう
SHA-256のライブデモを開く(ソースはGitHub保存済み。Pages到達と実ブラウザ動作は未確認)
左を papanda、右を papandb にして「比較する」を押します。
異なるビット数と32個のマスを見ます。赤いマスは、その位置のハッシュ1バイトが違うことを示します。
右を papanda に戻して、もう一度比較します。差分がどうなるか予想してください。
画面では赤・青だけでなく、数値と64桁の16進文字列も確認できます。スマートフォンでも入力と比較ができるように構成していますが、実機表示はまだ検証していません。
操作後はここを見る
次の数値はPython標準ライブラリのSHA-256で別途計算した参照値です。デモ画面の実測結果ではありません。
| 比較 | 期待する差 |
|---|---|
| papanda / papandb | 124 / 256ビット |
| papanda / papanda | 0 / 256ビット |
最初の比較では、入力は最後の1文字しか違いません。それでも出力は256ビット中124ビットが異なります。これは「入力の小さな変化が出力全体へ広がる」というハッシュ関数の性質を観察する一例です。
ただしどの1文字変更でも必ず124ビット、あるいは半分の128ビットが変わるわけではありません。結果は入力の組み合わせによって異なります。違う入力でも理論上は同じハッシュ値になる可能性があり、SHA-256が衝突を数学的に不可能にするわけでもありません。
なぜ64桁なの? 入力からハッシュまで
TextEncoderは文字列をUTF-8のバイト列に変えます。crypto.subtle.digest("SHA-256", bytes)は、そのバイト列から32バイトのハッシュ値を返します。1バイトを16進数2桁で表すので、32×2=64桁です。
flowchart LR A["文字列"] --> B["UTF-8バイト列"] B --> C["SHA-256"] C --> D["32バイト"] D --> E["64桁の16進数"]
核心部分はこれだけです。
const bytes = new TextEncoder().encode(text);
const buffer = await crypto.subtle.digest("SHA-256", bytes);
const digest = new Uint8Array(buffer);
digestは復号できる暗号文ではなく、固定長の値です。完成版では2つの32バイト配列を排他的論理和(XOR)で比較し、異なるビットを数えています。色付きマスは各バイトの一致・不一致、数値はビット単位の差を表します。
ここで「ハッシュ値が大きく変わった」ことと「入力が安全に保護された」ことは別です。SHA-256は入力の秘密を守る暗号化ではありません。辞書にある短い文字列なら、候補を同じ方法でハッシュ化して推測できるため、パスワードをSHA-256だけで保存してはいけません。
1か所変えて比べよう
右側の最後の1文字だけを変えます。papandbをpapandaへ戻したとき、同じ入力なら必ず同じハッシュになるため差分は0です。その後、右側に「ぱぱんだ」と入力してみると、見た目の文字数だけではUTF-8のバイト数を予測できないことにも気づけます。
| 変えるもの | 何を観察する? |
|---|---|
| 最後の英字 | 差分ビット数 |
| 同じ文字列に戻す | 差分0 |
| 日本語へ変更 | UTF-8バイト数 |
日本語では、見た目が似ている文字でも別のUnicode表現を持つ場合があります。比較は「見た目」ではなく、実際にエンコードしたバイト列に基づきます。文字列の正規化をしていない点も教材の前提です。
うまく動かないとき
| 症状 | まず確認 |
|---|---|
| 計算できない | HTTPSかlocalhostか |
| ボタンで変化なし | JavaScriptが有効か |
| 文字数とバイト数が違う | UTF-8の仕様 |
| 差分が0のまま | 2つの入力が同じか |
Web CryptoのSubtleCrypto.digest()は安全なコンテキストで提供されます。ブラウザや開き方によっては、ローカルのfile://で使える場合もありますが、この教材ではHTTPSまたはlocalhostを推奨します。デモが開けないときは、GitHubのソースを確認し、ローカルの開発用サーバーで試してください。
計算は非同期なので、連打したときに古い結果が新しい入力へ上書きされないよう、サンプルでは要求番号を比較しています。入力を外部へ送信する処理は書いていません。ただし、ホスティング側の通常のアクセスログまで制御する教材ではありません。
仕事・学習で使うなら
たとえば配布ファイルのチェックサムを理解する際に、「1バイト違うファイルは別のハッシュになりやすい」という考え方が役立ちます。しかしファイルの真正性まで確認するには、比較元のハッシュ値を信頼できる経路で受け取る必要があります。攻撃者がファイルとハッシュ値の両方を書き換えられるなら、単純な一致確認では防げません。
送信者を確かめる目的なら、HMACや電子署名など、別の仕組みを検討します。パスワード保存には専用のパスワードハッシュ方式を使います。本教材は文字列比較の学習用であり、署名検証、パスワード管理、ファイル全体のストリーム処理は実装していません。
また、MDNによるとdigest()は入力全体をメモリに置いて処理し、ストリーム入力には対応しません。大きなファイルをそのまま読み込ませる教材に拡張する前に、サイズ上限とメモリ使用量を検討してください。
追加実験:同じ文字と同じバイト列を区別する
SHA-256は文字の見た目を直接比較するのではなく、入力されたバイト列に対して計算を行います。JavaScriptで文字列を扱う場合、Unicodeの文字をどのバイト列へ変換するかが重要です。TextEncoderでUTF-8へ変換する方式なら、日本語1文字も内部では複数バイトになることがあります。文字数とバイト数を混同すると、別の実装と結果を比較したときに一致しない理由を見失います。
SHA-256の結果は256ビットで、16進数では64文字として表せます。1文字の違いで大きく異なる結果になることはハッシュ関数の特徴を理解する例ですが、たった数例の観察だけで安全性そのものを証明できるわけではありません。実験では入力した文字列、文字コード、得られたハッシュの並びを記録し、どの条件を固定した比較なのかを明確にします。改行や全角・半角の違いも入力データを変えます。
Web Cryptoのdigest()は非同期のAPIなので、結果を得る前に文字列を画面へ書き込もうとすると未完成の値を扱うことがあります。ボタンを押しても結果が変わらない場合は、Promiseの処理が完了しているか、Consoleに例外が出ていないかを調べてください。またcrypto.subtleは安全なコンテキストなどの利用条件があります。ローカルHTMLを開いたときとHTTPSで開いたときで機能が異なる場合は、APIの利用可否を確認します。
ハッシュ値は暗号化された文章ではありません。入力を後から元へ戻す用途とは違い、同一データかを比較するための指標などに利用されます。ただし平易なパスワードをそのままSHA-256に通して保存する方式は、パスワード保管の安全な代替にはなりません。実務では目的に合った専用のパスワードハッシュ方式や認証の仕組みを選びます。教材の架空文字列だけを使い、実際の秘密の入力は避けてください。
公式資料
確認日:2026年11月2日。APIの基本仕様と安全なコンテキスト要件を照合しました。ブラウザの実画面、GitHub Pages到達、WordPressでの青い引用枠、モバイル、印刷/PDFの実表示は未確認です。
GitHubサンプル
本文は仕組みと観察に絞り、実装全体はDaily Codeへ分離しました。WordPress本番への投稿はこの原稿保存とは別工程です。
