3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScriptだけでSHA-256ハッシュを生成する。crypto.subtle.digestの罠(MD5なし・HTTPS必須)

3
Posted at

ファイルの整合性確認やAPI署名の検証で、手元でサッとSHA-256を計算したいことがある。ブラウザには標準で Web Crypto APIcrypto.subtle)が載っているので、ライブラリを一切入れずにハッシュ生成ができる。

ぱんだツールズに作ったハッシュ値生成ツールは、テキストから SHA-256 / SHA-1 / SHA-512 を一括計算する。中身は crypto.subtle.digest を呼ぶだけだが、「16進数への変換」「非同期である理由」「MD5が無い」「HTTPS必須」あたりに、知らないと詰まる罠がいくつかある。実装と一緒にまとめておく。

crypto.subtle.digest でハッシュを計算する

コアはこれだけ。TextEncoder で文字列をバイト列にし、crypto.subtle.digest に渡す。

type Algorithm = 'SHA-256' | 'SHA-1' | 'SHA-512'

async function computeHash(text: string, algorithm: Algorithm): Promise<string> {
  const encoder = new TextEncoder()
  const data = encoder.encode(text)
  const hashBuffer = await crypto.subtle.digest(algorithm, data)
  const hashArray = Array.from(new Uint8Array(hashBuffer))
  return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
}

crypto.subtle.digest が返すのは ArrayBuffer(生のバイト列)。これをそのまま画面に出しても意味不明なので、16進数文字列に変換する。ここに地味な罠がある。

hex 変換の padStart(2, '0') を忘れると壊れる

ArrayBufferUint8Array にして、各バイトを toString(16) で16進数にする。問題は 1桁になるバイトだ。

たとえば値が 10(0x0a)のバイトを (10).toString(16) すると "a" になる。本来は "0a" の2文字でなければいけないのに1文字になってしまい、ハッシュ全体の桁がずれて別物の文字列になる015(0x00〜0x0f)のバイトが入るたびにこれが起きる。

なので必ず padStart(2, '0') で2桁に左ゼロ埋めする。

hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')

SHA-256 なら 32バイト → 64文字、SHA-512 なら 64バイト → 128文字、SHA-1 なら 20バイト → 40文字。padStart を入れておけば、どんなバイト値が来ても必ずこの長さの小文字16進数(lowercase hex)になる。他ツールやDBのハッシュ値と突き合わせる前提なら、この形式に揃えるのが無難。

crypto.subtle はなぜ非同期なのか

computeHashasync で、digestawait しているのに気づいたと思う。Web Crypto の subtle 系APIはすべて Promise を返す。同期版は無い。

btoaTextEncoder は同期なのに、ハッシュ計算だけ非同期なのは違和感があるかもしれない。これは Web Crypto の設計思想で、暗号処理は重くなりうる・ハードウェアアクセラレーションや別スレッド実装の余地を残したい、という理由で最初から非同期APIとして定義されている。実際の SHA 計算は一瞬で終わるが、APIの契約として await が必須。

なので「同期的にハッシュが欲しい」と思っても crypto.subtle.digest では叶わない。同期が要るならサードパーティの純JS実装(js-sha256 等)を使うことになるが、標準APIで済むならそちらが速くて安全。

複数アルゴリズムは Promise.all で並列計算

このツールは SHA-256 / SHA-1 / SHA-512 を一度に出す。3回 await を直列で書くと待ち時間が積み上がるので、Promise.all で並列にする。

const ALGORITHMS: Algorithm[] = ['SHA-256', 'SHA-1', 'SHA-512']

async function handleGenerate() {
  const entries = await Promise.all(
    ALGORITHMS.map(async (alg) => [alg, await computeHash(input, alg)] as const)
  )
  setResults(Object.fromEntries(entries) as Record<Algorithm, string>)
}

各アルゴリズムの計算は互いに独立なので、直列にする理由がない。map[アルゴリズム名, ハッシュ値] のタプル配列を作り、Object.fromEntries{ 'SHA-256': '...', ... } のオブジェクトに畳む。as const でタプル型を保ったまま渡すのがTypeScript的なポイント。

罠1:Web Crypto に MD5 は無い

crypto.subtle.digest が受け付けるアルゴリズムは SHA-1 / SHA-256 / SHA-384 / SHA-512 の4つだけ'MD5' を渡すとエラーになる。

MD5 は衝突攻撃が現実的で暗号学的に壊れているため、Web Crypto の仕様から意図的に除外されている。「レガシーなシステムとの照合でどうしてもMD5が要る」場合は、標準APIでは作れず、別途ライブラリを入れるしかない。逆に言うと、ブラウザ標準だけで完結させたいなら MD5 は選択肢に入らない。これは設計段階で知っておきたい制約。

このツールが SHA-1 を残しているのも「SHA-1 を新規に推奨するから」ではなく、既存システムとの互換性確認・比較用途のため。新規なら SHA-256 が無難。

罠2:crypto.subtle は secure context(HTTPS)でしか動かない

もう一つハマりやすいのが、crypto.subtle は secure context でしか使えないこと。具体的には HTTPS のページか localhost でのみ動作し、http:// の本番ページや、ファイルを直接開いた file:// では crypto.subtleundefined になる。

ローカルの localhost:3000 で開発しているときは動くのに、HTTP の検証環境にデプロイした途端「Cannot read properties of undefined」で落ちる、というのが典型的な踏み方。ハッシュ生成をフロントに組み込むなら、配信が HTTPS であることが前提になる。

ブラウザ完結ツールにとってはむしろ好都合で、Web Crypto を使う=テキストがサーバーに一切送られない。パスワードや機密文字列のハッシュを確認したいときでも、入力がネットワークに出ないので安心して使える。

まとめ

ブラウザだけでハッシュ生成するときの要点。

  • crypto.subtle.digest(algorithm, data) でライブラリ不要。入力は TextEncoder でバイト列に
  • 出力の hex 変換は padStart(2, '0') 必須。1桁バイトでハッシュが壊れる
  • crypto.subtle 系は すべて非同期(await 必須)。同期版は無い
  • 複数アルゴリズムは独立計算なので Promise.all で並列
  • MD5 は無い(仕様から除外)。SHA-1/256/384/512 のみ
  • secure context(HTTPS / localhost)必須。HTTP本番だと crypto.subtle が undefined

外部ライブラリゼロ・サーバー送信ゼロでハッシュが計算できるのが Web Crypto の良いところ。padStart と非同期だけ押さえておけば、自前ツールにも数行で組み込める。

ぱんだツールズ では他にもパスワード生成・JWTデコード・テキスト暗号化など、開発者向けのブラウザ完結ツールを多数公開中。全部無料・登録不要・入力はサーバーに送られない。
https://sakutto-panda.com


この記事は Zenn にも同じ内容を投稿しています。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?