コンテンツへスキップ
HexaTransfer
ブログへ戻る
暗号化とセキュリティ

Web Crypto APIガイド:開発者向けブラウザネイティブ暗号化

暗号化ファイル転送アプリ構築のためのWeb Crypto API完全ガイド。AES-GCM・RSA-OAEP・ブラウザでの鍵管理。

Web Crypto API(W3C の Web Cryptography API 勧告で仕様化され、window.crypto.subtle で公開)は、ネットワーク経由で暗号化ライブラリを配信することなくブラウザネイティブで暗号処理を行う方法です。AES-GCM・AES-CBC・AES-CTR・AES-KW・HMAC・RSA-OAEP・RSA-PSS・RSASSA-PKCS1-v1_5・ECDH・ECDSA・HKDF・PBKDF2 をすべての主要ブラウザ(Chrome 37+・Firefox 34+・Safari 10.1+・Edge 79+)でサポートします。ファイル転送アプリケーションでこれが重要なのは、暗号文のすべてのバイトをアップロード前にクライアントサイドで生成でき、ブラウザが定時間・監査済みの実装を提供するからです。このガイドでは暗号化ファイル転送に重要なプリミティブと、最初の実装で誰もがつまずく落とし穴を解説します。

SubtleCrypto はプロミスベースで非同期

crypto.subtle のすべてのメソッドは Promise を返します。これは意図的です。暗号化操作はハードウェアやバックグラウンドスレッドにオフロードできるため、非同期を強制することでメインスレッドをブロックする方法での誤用を防ぎます。コードの形:

const key = await crypto.subtle.generateKey(
  { name: "AES-GCM", length: 256 },
  true, // extractable
  ["encrypt", "decrypt"]
);

第2引数(true)は鍵を抽出可能としてマークします。これにより後で crypto.subtle.exportKey() でエクスポートできます。長期保存される鍵には false に設定して、生のバイトを JavaScript から到達不能にしてください。URL フラグメントにシリアライズする必要がある鍵(HexaTransfer パターン)には true に設定します。

第3引数は鍵の用途の配列です。["encrypt"] で生成された鍵は、AES-GCM が対称であっても復号に使用できません。この分離により侵害された暗号化フローが過去のデータを復号するために悪用されることを防ぎます。

対称ファイル暗号化のための AES-GCM

AES-GCM はファイルコンテンツの主力です。関連データ付き認証暗号化(AEAD)を提供します。暗号文・認証タグ・オプションで認証されるが暗号化されない関連データ(AAD)です。ファイル転送では NIST SP 800-38D の推奨に従い256ビット鍵と96ビットナンスを使用します。

const iv = crypto.getRandomValues(new Uint8Array(12)); // 96-bit nonce
const ciphertext = await crypto.subtle.encrypt(
  { name: "AES-GCM", iv },
  key,
  plaintext
);

出力には暗号文に付加された128ビット GCM 認証タグが含まれます。復号は自動的にタグを検証し、一致しない場合はスローします。同じ鍵でナンスを再利用してはいけません。GCM のセキュリティはナンス再利用で壊滅的に崩壊します(攻撃者が認証鍵を回復し任意の暗号文を偽造できます)。各ファイルが新しい鍵を受け取るファイル転送ではランダムナンスは安全です。長期保存される鍵にはカウンターを使用してください。

パスワード導出鍵のための PBKDF2

ユーザーがファイルを保護するパスワードを入力する場合、パスワードを直接 AES 鍵として使用できません。まず PBKDF2 を通してください。

const passwordKey = await crypto.subtle.importKey(
  "raw",
  new TextEncoder().encode(password),
  "PBKDF2",
  false,
  ["deriveKey"]
);

const aesKey = await crypto.subtle.deriveKey(
  {
    name: "PBKDF2",
    salt: crypto.getRandomValues(new Uint8Array(16)),
    iterations: 600000,
    hash: "SHA-256",
  },
  passwordKey,
  { name: "AES-GCM", length: 256 },
  false,
  ["encrypt", "decrypt"]
);

OWASP の2023年パスワードハッシング指針は PBKDF2-SHA-256 に600,000回のイテレーションを推奨します。310,000回未満は現在のベストプラクティス以下です。ソルトはランダムで暗号文と一緒に保存する必要があります(秘密ではなく、ただし一意でなければなりません)。

2026年の新しいコードでは PBKDF2 の代わりに Argon2id を検討してください。Argon2 はまだ Web Crypto API に含まれていませんが、argon2-browser@noble/hashes などのライブラリが JavaScript/WASM 実装を提供します。Argon2id は PBKDF2 よりはるかに優れた GPU 攻撃耐性を持ちます。

鍵ラッピングのための RSA-OAEP

受信者の公開鍵でファイルの AES 鍵を暗号化したいシナリオには RSA-OAEP を使用します。

const keyPair = await crypto.subtle.generateKey(
  {
    name: "RSA-OAEP",
    modulusLength: 4096,
    publicExponent: new Uint8Array([1, 0, 1]), // 65537
    hash: "SHA-256",
  },
  true,
  ["encrypt", "decrypt"]
);

新しい鍵には modulusLength 4096 を使用してください。2048は許容されますが、量子のタイムラインが固まるにつれて非推奨になり始めるでしょう。RSA-OAEP は小さなペイロードのみ暗号化します(最大 modulusLength/8 - 2*hashLength - 2 バイト)。ファイルコンテンツを直接暗号化するのではなく、256ビットの AES 鍵をラップしてください。

パフォーマンスが重要なアプリには、RSA の代替として P-256 または P-384 の ECDH が優れています。鍵生成は一桁速く、鍵サイズははるかに小さいです。

大きなファイルのストリーミング

2 GB のファイルはブラウザの ArrayBuffer に快適に収まりません。Chrome・Firefox・Safari はすべて File.stream() で ReadableStream を返してファイルを読み、チャンクで処理できます。Web Crypto API 自体にはまだストリーミング暗号化/復号メソッドがないため(これはスペックのギャップです)、2つの回避策があります。

  1. チャンクに分割(64 KB または1 MB)し、それぞれ固有のナンスで暗号化します。受信者は順番に連結します。ファイル全体の真の AEAD は失われますが、ほとんどのケースで機能します。
  2. WASM 暗号ライブラリ(libsodium.js・WASM バックエンドを持つ @noble/ciphers)を使用します。XChaCha20-Poly1305 や AES-GCM-SIV のようなストリーミング AEAD モードをサポートします。

数百メガバイト未満の転送では、バッファ化された AES-GCM は完全に機能し、はるかにシンプルです。それ以上ではストリーミングがメモリ圧を避けるために必要になります。

鍵のエクスポート・インポートと URL フラグメント

鍵が URL フラグメントを通る HexaTransfer スタイルのフローの場合:

const rawKey = await crypto.subtle.exportKey("raw", aesKey);
const keyBase64 = btoa(String.fromCharCode(...new Uint8Array(rawKey)));
// Share URL like https://example.com/file/abc123#key=keyBase64

URL フラグメントは HTTP リクエストではサーバーに送信されません(ブラウザがそれらを取り除きます)。これにより鍵はユーザーがリンクを共有しても クライアントサイドに保たれます。受信者側では:

const keyBase64 = window.location.hash.slice(5); // strip "#key="
const rawKey = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
const key = await crypto.subtle.importKey(
  "raw", rawKey, "AES-GCM", false, ["decrypt"]
);

URL エンコードの問題を避けるため base64url エンコーディング(+- に、/_ に置換し、パディングを除去)を使用してください。

よくある落とし穴

ソルトや IV に Math.random() を使用する。 Math.random() は暗号的に安全ではありません。常に crypto.getRandomValues() を使用してください。

同じ鍵で IV を再利用する。 GCM のセキュリティ特性はナンス再利用で完全に崩壊します。96ビットのランダムナンスは同じ鍵での約2^48回の暗号化後に衝突します(誕生日の境界)。各ファイルが自分の鍵を持つファイル転送では安全です。長期保存される鍵にはカウンターを使用してください。

HTTPS を忘れる。 crypto.subtle はセキュアなコンテキスト(HTTPS またはローカルホスト)でのみ利用可能です。セキュアでないオリジンでは crypto.subtle は undefined です。

IndexedDB に保護されていない抽出可能な鍵を保存する。 鍵を永続化する必要がある場合、保存前にラップしてください。localStorage に生の AES 鍵を保存しないでください。オリジン上の任意のスクリプトからアクセス可能です。

PBKDF2 なしでユーザー提供のパスワードを信頼する。 UTF-8 バイトに変換された生のパスワードは256ビット鍵ではありません。常に導出してください。

認証タグを検証しない。 crypto.subtle.decrypt() は AES-GCM に対してこれを自動的に行いますが、その上にカスタムプロトコルを実装する場合は、チェックを省略しないでください。

ブラウザのサポートの微妙な点

すべての主要ブラウザは HTTPS で Web Crypto をサポートします。いくつかの注意点:

  • Safari の PBKDF2 は長年 Chrome/Firefox より遅かった。Safari 15 でギャップが縮まりました。
  • Firefox はより厳格な入力検証を適用します。Chrome で実行されるコードが Firefox で OperationError をスローすることがあります。両方でテストしてください。
  • Service Workers での Web Crypto は機能しますが、登録スコープが HTTPS である必要があります。
  • Node.js は Node 15 以降 require("crypto").webcrypto で互換性のある API を提供します。アイソモーフィックな暗号コードに便利です。

ライブラリを使う場合

Web Crypto は基本をよくカバーしますが、ChaCha20-Poly1305・Argon2・X25519・Ed25519 などの最新のプリミティブが不足しています(Ed25519 は実装が進んでいます)。それらには libsodium.js(WASM 経由)または @noble/ciphers / @noble/curves(純粋な JavaScript、監査済み)が主要な選択肢です。 HexaTransfer は依存関係なしにファイル転送パスをカバーするため、AES-GCM と PBKDF2 に Web Crypto プリミティブを直接使用します。

完全な暗号化転送フローには Web Crypto で100行以下のコードで実装できます。AES 鍵を生成し、導出またはランダムに、ファイルを暗号化し、暗号文をアップロードし、フラグメントに鍵を含むリンクを共有し、受信者が鍵をインポートして復号します。それがすべてです。

hexatransfer.comでお試しください — 無料、登録不要、最大10GB。

エンドツーエンド暗号化で大容量ファイルを安全に送信

エンドツーエンド暗号化で最大10GBのファイルを無料で転送。アカウント不要。ファイルはアップロード前にブラウザで暗号化されるため、他の誰にも読まれません。

ファイルを送信