DEK でAES-256-GCM暗号化し、DEK だけを各閲覧者の公開鍵でラップします。サーバーには暗号文とラップ済みDEKだけを保存します。ただし、管理者がWebアプリのJavaScriptを配信・改変できるなら、それ自体が重要な信頼境界です。目標は「サーバーが平文を持たない」保管
ここで目指すのは、サーバーを認証・アクセス制御・暗号文保管の担当に限定し、ファイルの中身を理解できないようにする構成です。S3、DB、バックアップが流出しても、秘密鍵がなければ復号できない状態を作ります。
これはE2EE(End-to-End Encryption)に近い設計です。ただし「誰から守れるか」は先に決める必要があります。少なくとも、保管基盤の漏えい、DBのコピー、バックアップの流出から平文を守る、という脅威モデルには非常に有効です。
ファイルは公開鍵で直接暗号化しない
PDFや画像をRSAなどの公開鍵暗号でそのまま暗号化する設計は避けます。公開鍵暗号は大容量データ向きではなく、処理も重く、暗号化できるデータ長にも制約があります。
代わりに、共通鍵暗号と公開鍵暗号を組み合わせるハイブリッド暗号を使います。ファイル本体を暗号化するための一時的な対称鍵を DEK(Data Encryption Key)と呼びます。
ファイル本体 → AES-256-GCM(DEK) → ciphertext
DEK → 閲覧者の公開鍵 → wrapped_DEK
この「鍵を別の鍵で包む」考え方は、エンベロープ暗号または鍵ラップと呼ばれます。Web Crypto APIにはAES-GCMとRSA-OAEPの操作が定義されているため、ブラウザ実装ではこの組み合わせが一つの分かりやすい選択肢になります。Web Cryptography Level 2
アップロード時の流れ
flowchart LR F[ファイルの平文] --> A[AES-256-GCMで暗号化] D[ランダムな DEK] --> A A --> C[暗号文 ciphertext] D --> W[閲覧者の公開鍵でラップ] W --> K[wrapped DEK] C --> S[Server / Object Storage] K --> S
ブラウザはアップロードごとに、暗号学的に安全な乱数から256 bitのDEKを作ります。次にファイルをAES-256-GCMで暗号化し、DEKを閲覧者の公開鍵で暗号化(ラップ)します。サーバーへ送るのは平文ではなく、次のデータです。
ciphertext # 暗号化されたファイル
wrapped_DEK # 閲覧者の公開鍵でラップしたDEK
iv # AES-GCM用のnonce / IV
algorithm # 例: AES-256-GCM, RSA-OAEP-SHA-256
version # フォーマット変更用
metadata # 最小限にする。必要なら暗号化またはAADで保護する
AES-GCMは暗号化と改ざん検知を同時に行うAEADです。復号時に認証タグの検証に失敗すれば、平文を信用してはいけません。DEKを使い回す場合は、同じ鍵との組でIVを二度と再利用しないことが必須です。NISTもGCMのIVの一意性を重要な要件として示しています。NIST SP 800-38D
閲覧時は端末側で復号する
flowchart LR S[Server / Object Storage] --> C[ciphertext と wrapped DEK] C --> B[Browser] P[閲覧者の秘密鍵] --> U[wrapped DEK を復号] C --> U U --> D[DEK] D --> R[AES-GCMで復号] C --> R R --> F[ファイルの平文]
サーバーは暗号文と、その利用者用の wrapped_DEK を返すだけです。ブラウザが秘密鍵でDEKを復元し、DEKでファイル本体を復号します。秘密鍵がサーバーに存在しないなら、サーバー管理者が保管データを取得しても、それだけでは内容を読めません。
複数人へ共有する場合
閲覧者が増えても、ファイル本体を人数分暗号化する必要はありません。暗号文は1つのままにし、同じDEKを閲覧者ごとの公開鍵でラップします。
flowchart TD D[DEK] --> A[User A の公開鍵でラップ] D --> B[User B の公開鍵でラップ] D --> C[User C の公開鍵でラップ] A --> KA[wrapped_DEK_A] B --> KB[wrapped_DEK_B] C --> KC[wrapped_DEK_C] F[ファイル暗号文は1つ] --> S[Server] KA --> S KB --> S KC --> S
file_123:
ciphertext: ...
recipients:
user-a: wrapped_DEK_A
user-b: wrapped_DEK_B
user-c: wrapped_DEK_C
たとえばUser Bには wrapped_DEK_B だけを渡します。User Bの秘密鍵でそれを復号できればDEKを得られ、ファイルを復号できます。共有の追加は、新たな閲覧者の公開鍵でDEKをラップしてレコードを1件増やす操作になります。
共有解除は「過去に取得した平文」を消せない
閲覧者の wrapped_DEK を削除すれば、その後サーバーから鍵を取得させないことはできます。しかし、その閲覧者が既にDEKや平文を取得していた場合、そこから先を技術的に取り消すことはできません。これは通常のファイル共有と同じです。
漏えいしたDEKを無効にしたい場合は、ファイルを新しいDEKで再暗号化し、残す閲覧者全員の公開鍵で新しいDEKを再ラップします。単に権限レコードを消すだけでは、すでにコピーされた暗号文とDEKの組を無効化できません。
秘密鍵の置き場所と復旧設計
秘密鍵をサーバーDBへ平文保存した時点で、この設計の意味は大きく失われます。理想は端末側で生成・保持し、OSのキーストア、Secure Enclave、TPM、または少なくともブラウザの非抽出可能な鍵として扱うことです。
ただし、端末だけに置くと端末紛失時の復旧が問題になります。復旧を許可するなら、復旧用の鍵を別途設計します。たとえば利用者の強いパスフレーズから適切なKDFで導出した鍵、または信頼する複数の復旧連絡先・組織鍵へDEKを追加でラップする方法があります。どの方法も復旧可能性と「誰が読めるか」のトレードオフです。
本当の信頼境界はJavaScriptの配信にもある
ブラウザで復号するからといって、Webアプリの管理者から常に守れるとは限りません。管理者がHTMLやJavaScriptを差し替えられるなら、利用者が次回開いたとき、秘密鍵の操作や復号後の平文を外部送信するコードを配信できるためです。
利用者の秘密鍵は端末にある
↓
ブラウザが管理者の配信したJavaScriptを実行する
↓
復号処理や平文を悪意あるコードが観測できる可能性がある
この方式は「静的な保管データの漏えい」には強い一方、「アプリ配信を支配する管理者が利用者を能動的に攻撃する」脅威まで自動的に解決するものではありません。後者まで対象にするなら、署名・更新検証を持つデスクトップアプリ、ブラウザ拡張、独立して監査・配布されるクライアントなど、復号処理の信頼根をWeb配信から分離する設計を検討します。
実装で外せないチェックリスト
| 項目 | 設計上のポイント |
|---|---|
| ファイル暗号 | AES-256-GCMなどのAEADを使う。暗号方式・IV・認証タグを復号に必要な形式で保存する。 |
| IV / nonce | 同一DEKとの組で絶対に再利用しない。ファイルを分割して暗号化するなら、各チャンクの一意性も設計する。 |
| 鍵ラップ | RSAを採るならRSA-OAEP-SHA-256のように用途を明示する。独自のRSA処理や独自暗号方式は作らない。 |
| メタデータ | ファイル名・サイズ・所有者・共有先も機密になり得る。平文で残すものを最小化し、必要に応じて暗号化またはAADで保護する。 |
| 公開鍵の正当性 | 共有時に取得した公開鍵が本当にその利用者のものかを保証する。鍵すり替えが起きると攻撃者へDEKを渡してしまう。 |
| 鍵更新・失効 | 鍵IDと暗号フォーマットのバージョンを持たせる。秘密鍵漏えい時の再暗号化手順をあらかじめ決める。 |
| 平文の扱い | 復号後にログ、エラー報告、プレビューのキャッシュ、解析サービスへ平文を渡さない。 |
まとめ
ブラウザでファイルをAES-GCM暗号化し、DEKだけを閲覧者の公開鍵でラップする構成は、機密ファイルの保管に適した基本形です。サーバーは暗号文とアクセス権を管理するだけで、秘密鍵を持たなければ内容を復号できません。
複数人共有では、ファイル暗号文を複製せず、利用者ごとにラップ済みDEKを追加します。一方で、鍵の紛失と復旧、共有解除の限界、公開鍵の正当性、そしてJavaScriptを誰が配信・変更できるかは、暗号アルゴリズムと同じくらい重要です。設計の強さは「どこまでを信頼するか」を明文化して初めて決まります。