# 機能設計書 104-Server Actions暗号化

## 概要

本ドキュメントは、Next.jsのServer Actionsにおけるクロージャデータ（バウンド引数）の暗号化機能の設計を記述する。Server Actionsがクライアントに渡される際に、バインドされた引数をAES-GCMで暗号化し、サーバー側で復号する仕組みである。

### 本機能の処理概要

Server Actionsのバインドされた引数（クロージャに含まれる変数）をAES-GCM暗号化アルゴリズムで暗号化し、クライアントに安全に配信する機能である。React Server Components（RSC）のFlight protocolを使用して引数をシリアライズした後、暗号化を適用する。

**業務上の目的・背景**：Server ActionsはサーバーサイドのクロージャをクライアントからRPC形式で呼び出す仕組みであるが、バインドされた引数はクライアントに送信されるため、機密データの漏洩を防ぐ必要がある。暗号化により、クライアント側でバインドされた引数の内容を読み取ることができなくなり、セキュリティが確保される。

**機能の利用シーン**：Server Actionsがクライアントコンポーネントにpropsとして渡される際、または`use cache`ディレクティブのキャッシュキーとして使用される際に自動的に暗号化が適用される。開発者が明示的に暗号化APIを呼び出す必要はない。

**主要な処理内容**：
1. React Flight protocolによるバインド引数のシリアライズ（`renderToReadableStream`）
2. AES-GCM暗号化キーの取得（ビルドマニフェストまたは環境変数から）
3. ランダムIV（16バイト）の生成
4. actionIdをプレフィックスとしたペイロードのAES-GCM暗号化
5. Base64エンコードによる転送可能な文字列への変換
6. 復号時のactionIdプレフィックス検証（チェックサム的機能）
7. React Flight protocolによるデシリアライズ（`createFromReadableStream`）

**関連システム・外部連携**：React Server Components（Flight protocol）、Web Crypto API（AES-GCM）と連携する。

**権限による制御**：暗号化キーはビルド時に自動生成され、`NEXT_SERVER_ACTIONS_ENCRYPTION_KEY`環境変数でオーバーライド可能。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| - | Server Actions利用画面 | 主画面 | Server Actionsをpropsとして受け取るクライアントコンポーネント |

## 機能種別

セキュリティ / データ暗号化

## 入力仕様

### 入力パラメータ（暗号化）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| actionId | string | Yes | Server ActionのID（暗号化salt兼チェックサム） | - |
| args | any[] | Yes | バインドされた引数の配列 | Flight serializableであること |

### 入力パラメータ（復号）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| actionId | string | Yes | Server ActionのID | - |
| encryptedPromise | Promise\<string\> | Yes | 暗号化されたBase64文字列のPromise | - |

### 入力データソース

- Server Actionsの定義ファイル（`use server`ディレクティブ付き関数）
- ビルドマニフェスト（暗号化キー）
- 環境変数`NEXT_SERVER_ACTIONS_ENCRYPTION_KEY`

## 出力仕様

### 出力データ（暗号化）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| encrypted | string | Base64エンコードされた暗号化文字列（IV 16バイト + 暗号化ペイロード） |

### 出力データ（復号）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| deserialized | any[] | 復号・デシリアライズされた引数配列 |

### 出力先

- 暗号化結果：RSCレスポンスストリームに含まれてクライアントに配信
- 復号結果：Server Action実行時の引数として使用

## 処理フロー

### 処理シーケンス

```
1. encryptActionBoundArgs（暗号化）
   └─ React.cacheでメモ化された関数
2. Flight serializationでargs配列をストリーム化
   └─ renderToReadableStreamでRSCペイロードに変換
3. streamToStringでストリームを文字列に変換
   └─ hangingInputAbortSignalでタイムアウト制御
4. encodeActionBoundArg（内部暗号化）
   ├─ getActionEncryptionKeyで暗号化キーを取得
   ├─ 16バイトのランダムIVを生成
   ├─ actionId + serializedをAES-GCMで暗号化
   └─ IV + 暗号化データをBase64エンコード
5. decryptActionBoundArgs（復号）
   └─ encryptedPromiseをawaitして暗号化文字列を取得
6. decodeActionBoundArg（内部復号）
   ├─ Base64デコード
   ├─ IV（先頭16バイト）とペイロードを分離
   ├─ AES-GCM復号
   └─ actionIdプレフィックスの検証と除去
7. Flight deserialization
   └─ createFromReadableStreamで元のオブジェクトに復元
```

### フローチャート

```mermaid
flowchart TD
    A[encryptActionBoundArgs] --> B[renderToReadableStream]
    B --> C[streamToString]
    C --> D{エラー発生?}
    D -->|Yes| E[エラースロー]
    D -->|No| F{workUnitStore存在?}
    F -->|No| G[encodeActionBoundArg]
    F -->|Yes| H{キャッシュヒット?}
    H -->|Yes| I[キャッシュ値返却]
    H -->|No| G
    G --> J[getActionEncryptionKey]
    J --> K[ランダムIV生成 16bytes]
    K --> L[AES-GCM encrypt]
    L --> M[Base64エンコード]
    M --> N[キャッシュ保存]
    N --> O[暗号化文字列返却]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-104-1 | AES-GCM暗号化 | すべてのバインド引数はAES-GCMアルゴリズムで暗号化 | Server Actionsがクライアントに渡される場合 |
| BR-104-2 | actionIdチェックサム | 暗号化前にactionIdをペイロードにプレフィックスし、復号時に検証 | すべての暗号化/復号操作 |
| BR-104-3 | IV一意性 | 各暗号化操作で16バイトのランダムIVを生成 | すべての暗号化操作 |
| BR-104-4 | React.cacheメモ化 | 同一actionIdと同一argsの組み合わせではキャッシュされた暗号化結果を再利用 | 同一レンダリングパス内 |
| BR-104-5 | キー取得優先順位 | 環境変数 > ビルドマニフェストの順で暗号化キーを取得 | 暗号化キー初期化時 |
| BR-104-6 | デバッグチャンネル無効化 | キャッシュコンポーネント内ではデバッグ情報をストリップ | prerenderResumeDataCacheまたはrenderResumeDataCache存在時 |

### 計算ロジック

暗号化フォーマット: `Base64(IV[16bytes] + AES-GCM(actionId + serializedArgs))`

## データベース操作仕様

### 操作別データベース影響一覧

該当なし。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | 暗号化キー未設定 | `NEXT_SERVER_ACTIONS_ENCRYPTION_KEY`とビルドマニフェストのキーが両方未定義 | InvariantError('Missing encryption key for Server Actions')をスロー |
| - | 復号検証失敗 | 復号後のプレフィックスがactionIdと一致しない | Error('Invalid Server Action payload: failed to decrypt.')をスロー |
| - | シリアライズエラー | バインド引数がFlight serializableでない場合 | エラーメッセージとスタックトレースを保持してスロー |

### リトライ仕様

リトライは不要。暗号化/復号の失敗はデータ整合性の問題を示すため、再試行では解決しない。

## トランザクション仕様

該当なし。

## パフォーマンス要件

- `React.cache`によるメモ化で同一レンダリングパス内の重複暗号化を回避
- `prerenderResumeDataCache`/`renderResumeDataCache`を活用した暗号化結果のキャッシュ
- `hangingInputAbortSignal`によるストリーム処理のタイムアウト制御
- V8の65535引数制限を考慮した`arrayBufferToString`の分岐処理

## セキュリティ考慮事項

- AES-GCM（Galois/Counter Mode）は認証付き暗号化であり、データの機密性と完全性の両方を保証
- 暗号化キーはビルドごとに生成され、`server-actions-manifest`に格納
- IVは暗号化ごとにランダム生成され、同一平文からの暗号文の一意性を保証
- actionIdプレフィックスによるチェックサムで、異なるアクション間の暗号文の流用を防止
- 開発環境ではシリアライズエラーを`console.error`でログ出力し、エラーオーバーレイでの表示を可能にする

## 備考

- Edge Runtimeでも動作可能（`process.env.NEXT_RUNTIME === 'edge'`での分岐あり）
- `workUnitAsyncStorage.exit()`を使用してランダムバイト生成をAsync Local Storageの外で実行している

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: 暗号化ユーティリティを理解する

低レベルの暗号化/復号関数を把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | encryption-utils.ts | `packages/next/src/server/app-render/encryption-utils.ts` | encrypt/decrypt関数、getActionEncryptionKey関数、バイナリ変換ユーティリティ |

**主要処理フロー**:
- **6-24行目**: `arrayBufferToString` - V8の65535引数制限を考慮したバイナリ変換
- **26-35行目**: `stringToUint8Array` - 文字列からUint8Arrayへの変換
- **37-50行目**: `encrypt` - Web Crypto APIのAES-GCM暗号化
- **52-65行目**: `decrypt` - Web Crypto APIのAES-GCM復号
- **67-91行目**: `getActionEncryptionKey` - 暗号化キーの取得とインポート（キャッシュ付き）

**読解のコツ**: `crypto.subtle.importKey`は非同期であるが、結果は`__next_loaded_action_key`にキャッシュされるため、2回目以降は即座に返却される。

#### Step 2: 暗号化処理のエントリーポイントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | encryption.ts | `packages/next/src/server/app-render/encryption.ts` | encryptActionBoundArgs関数（109-245行目） |

**主要処理フロー**:
1. **111行目**: `workUnitAsyncStorage.getStore()`でワークユニットストアを取得
2. **116行目**: `getClientReferenceManifest()`でクライアントモジュールマップを取得
3. **164-206行目**: `renderToReadableStream` + `streamToString`でFlight serializationを実行
4. **220-224行目**: ワークユニットストアなしの場合、直接暗号化して返却
5. **228-236行目**: キャッシュチェック（prerenderResumeDataCache/renderResumeDataCache）
6. **238行目**: `encodeActionBoundArg`で実際の暗号化を実行

#### Step 3: 復号処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | encryption.ts | `packages/next/src/server/app-render/encryption.ts` | decryptActionBoundArgs関数（248-334行目） |

**主要処理フロー**:
- **252行目**: `encryptedPromise`をawait
- **257-274行目**: キャッシュからの復号結果取得、なければ`decodeActionBoundArg`で復号
- **280-331行目**: `createFromReadableStream`でFlight deserialization

#### Step 4: 内部暗号化/復号関数を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | encryption.ts | `packages/next/src/server/app-render/encryption.ts` | encodeActionBoundArg/decodeActionBoundArg |

**主要処理フロー**:
- **77-97行目**: `encodeActionBoundArg` - IV生成、actionId+引数の暗号化、Base64エンコード
- **49-71行目**: `decodeActionBoundArg` - Base64デコード、IV分離、復号、actionId検証

### プログラム呼び出し階層図

```
RSC Renderer (app-render.tsx)
    │
    ├─ encryptActionBoundArgs(actionId, ...args)  [React.cache]
    │      ├─ getClientReferenceManifest()
    │      ├─ renderToReadableStream(args, clientModules)
    │      ├─ streamToString(stream, signal)
    │      ├─ getCacheSignal() / beginRead() / endRead()
    │      ├─ getPrerenderResumeDataCache() [キャッシュ検索]
    │      └─ encodeActionBoundArg(actionId, serialized)
    │             ├─ getActionEncryptionKey()
    │             │      └─ crypto.subtle.importKey()
    │             ├─ crypto.getRandomValues() [IV生成]
    │             └─ encrypt(key, iv, data)
    │                    └─ crypto.subtle.encrypt()
    │
    └─ decryptActionBoundArgs(actionId, encryptedPromise)
           ├─ decodeActionBoundArg(actionId, encrypted)
           │      ├─ getActionEncryptionKey()
           │      ├─ atob() [Base64デコード]
           │      └─ decrypt(key, iv, data)
           │             └─ crypto.subtle.decrypt()
           └─ createFromReadableStream(stream, options)
```

### データフロー図

```
[暗号化フロー]
args[] ──> renderToReadableStream ──> streamToString ──> actionId+serialized
                                                              │
                                                         AES-GCM encrypt
                                                              │
                                                         Base64(IV+encrypted) ──> クライアントへ

[復号フロー]
クライアントから ──> Base64デコード ──> IV分離 ──> AES-GCM decrypt
                                                      │
                                               actionId検証 + 除去
                                                      │
                                               createFromReadableStream ──> args[]
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| encryption.ts | `packages/next/src/server/app-render/encryption.ts` | ソース | 暗号化/復号のメイン処理（Flight serialization含む） |
| encryption-utils.ts | `packages/next/src/server/app-render/encryption-utils.ts` | ソース | 低レベル暗号化ユーティリティ（AES-GCM、キー管理） |
| manifests-singleton.ts | `packages/next/src/server/app-render/manifests-singleton.ts` | ソース | クライアントリファレンスマニフェスト、Server Actionsマニフェストの取得 |
| work-unit-async-storage.external.ts | `packages/next/src/server/app-render/work-unit-async-storage.external.ts` | ソース | ワークユニットストア、キャッシュシグナル管理 |
| dynamic-rendering.ts | `packages/next/src/server/app-render/dynamic-rendering.ts` | ソース | createHangingInputAbortSignal関数 |
| stream-utils/node-web-streams-helper.ts | `packages/next/src/server/stream-utils/node-web-streams-helper.ts` | ソース | streamToString関数 |
