# 機能設計書 46-キーリング管理

## 概要

本ドキュメントは、複数暗号化キーのローテーション管理機能の設計について記載する。この機能は、暗号化キーの追加・削除・ローテーションを可能にし、セキュリティベストプラクティスに基づいたキー管理を実現する。

### 本機能の処理概要

**業務上の目的・背景**：セキュリティベストプラクティスでは、暗号化キーの定期的なローテーションが推奨されている。キーが漏洩した場合や、一定期間が経過した場合に新しいキーに切り替える必要がある。キーリング管理機能は、複数のキーを同時に保持し、新しいキーで暗号化しながら古いキーでの復号化もサポートすることで、ダウンタイムなしのキーローテーションを実現する。

**機能の利用シーン**：
- 定期的なキーローテーション実施時
- キー漏洩疑いがある場合の緊急キー更新
- 新しい暗号化キーの追加
- 古いキーの段階的な廃止

**主要な処理内容**：
1. 環境変数からのキーリングパース
2. キーID指定による特定キーの取得
3. 現在の暗号化キー（先頭キー）の返却
4. テスト用キーリングの設定
5. 複数キーによる復号化サポート

**関連システム・外部連携**：
- 環境変数 LIAM_GITHUB_OAUTH_KEYRING
- @liam-hq/security 暗号化機能（No.45）

**権限による制御**：サーバーサイドでのみ実行可能。キーリングはプロセス起動時に環境変数から読み込まれる。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| - | - | - | 本機能は画面に直接関連しないセキュリティ基盤 |

## 機能種別

セキュリティ処理 / キー管理

## 入力仕様

### 入力パラメータ

**環境変数**

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| LIAM_GITHUB_OAUTH_KEYRING | string | Yes | キーリング文字列 | 「kid:base64key」形式、カンマ区切り |

**parseKeyring関数**

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| envValue | string \| undefined | No | キーリング文字列 | - |

**setKeyring関数**

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| keys | Key[] | Yes | 設定するキー配列 | 各キーは32バイト |

### 入力データソース

- 環境変数 LIAM_GITHUB_OAUTH_KEYRING

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| Key | { id: string; key: Buffer } | キーオブジェクト |
| RING | Key[] | キーリング配列 |

### 出力先

モジュール内部変数およびResult型による返却

## 処理フロー

### 処理シーケンス

```
[キーリング初期化フロー]
1. プロセス起動時に環境変数を読み込み
   └─ process.env['LIAM_GITHUB_OAUTH_KEYRING']
2. parseKeyring関数でパース
   ├─ カンマで分割
   ├─ 各エントリを「:」で分割
   ├─ Base64デコード
   └─ 32バイトのキーのみ保持
3. RING変数に格納

[キー取得フロー]
1. currentKey()呼び出し
   └─ RING[0]を返却（先頭が現在のキー）

[キー検索フロー]
1. decryptAesGcm内でkeyIdを使用
   └─ RING.find(k => k.id === keyId)
```

### フローチャート

```mermaid
flowchart TD
    subgraph Init["初期化フロー"]
        I1[プロセス起動] --> I2[環境変数読み込み]
        I2 --> I3[parseKeyring]
        I3 --> I4[カンマ分割]
        I4 --> I5{各エントリをパース}
        I5 --> I6[kid:base64で分割]
        I6 --> I7[Base64デコード]
        I7 --> I8{32バイト?}
        I8 -->|Yes| I9[キー配列に追加]
        I8 -->|No| I10[スキップ]
        I9 --> I11[RING変数に格納]
    end

    subgraph Current["現在キー取得"]
        C1[currentKey呼び出し] --> C2{RING空?}
        C2 -->|Yes| C3[エラー返却]
        C2 -->|No| C4[RING 0 返却]
    end

    subgraph Search["キー検索"]
        S1[keyIdで検索] --> S2{キー存在?}
        S2 -->|Yes| S3[キー返却]
        S2 -->|No| S4[エラー返却]
    end
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-46-01 | 先頭キー優先 | RING[0]が現在の暗号化キー | 暗号化時 |
| BR-46-02 | 複数キー復号 | キーIDで適切なキーを選択して復号 | 復号化時 |
| BR-46-03 | キー形式 | 「kid:base64key」形式でカンマ区切り | 環境変数設定時 |
| BR-46-04 | キー長検証 | 32バイト以外のキーは無視 | パース時 |
| BR-46-05 | 空キーリング不可 | キーリングが空の場合はエラー | currentKey呼び出し時 |

### 計算ロジック

**キーリングパース**：
```typescript
function parseKeyring(envValue: string | undefined): Key[] {
  const keys: Key[] = []
  const parts = (envValue ?? '')
    .split(',')
    .map((s) => s.trim())
    .filter(Boolean)
  for (const entry of parts) {
    const [id, b64] = entry.split(':')
    if (!id || !b64) continue
    const keyBuf = Buffer.from(b64, 'base64')
    if (keyBuf.length !== 32) continue
    keys.push({ id, key: keyBuf })
  }
  return keys
}
```

**キーリング形式例**：
```
kid1:base64encodedkey1,kid0:base64encodedkey0
```
- 先頭のkid1が現在のキー（暗号化に使用）
- kid0は古いキー（復号化のみ）

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| - | - | - | 本機能はデータベースを直接操作しない |

### テーブル別操作詳細

本機能はメモリ内でのキー管理であり、データベースを直接操作しない。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | No keys configured | キーリングが空 | 環境変数LIAM_GITHUB_OAUTH_KEYRINGを設定 |
| - | Unknown key id | 指定キーIDが存在しない | 古いキーが削除された可能性。再暗号化が必要 |
| - | 無効なキー形式 | kid:base64形式でない | 環境変数の形式を確認 |
| - | キー長不正 | 32バイトでないキー | Base64エンコード後に32バイトになるキーを使用 |

### リトライ仕様

キーリング操作は決定論的であり、リトライは不要。

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

本機能はメモリ内処理のみであり、トランザクション管理は不要。

## パフォーマンス要件

- キーリング初期化: 起動時1回のみ
- キー取得: O(1)（currentKey）またはO(n)（キーID検索、nはキー数）
- 通常のキー数は2-3個程度のため、性能影響は軽微

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

- **キーローテーション**: 新しいキーを先頭に追加し、古いキーを末尾に移動
- **段階的廃止**: 古いキーで暗号化されたデータが存在する間は古いキーを保持
- **環境変数管理**: キーリングは環境変数で管理し、ソースコードには含めない
- **メモリ保護**: キーはBufferとして保持し、文字列としてログ出力しない
- **最小権限**: サーバーサイドでのみアクセス可能

## 備考

- キーローテーション手順：
  1. 新しいキー（kid2）を生成
  2. 環境変数を「kid2:newkey,kid1:currentkey,kid0:oldkey」に更新
  3. アプリケーションを再起動
  4. 新しいデータはkid2で暗号化される
  5. 古いデータはkid1またはkid0で復号化可能
  6. 十分な時間経過後、最も古いキーを削除

---

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

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

### 推奨読解順序

#### Step 1: データ構造を理解する

まず、キーリングで使用される型定義を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | cryptoBox.ts | `frontend/internal-packages/security/src/cryptoBox.ts` | Key型の定義 |

**読解のコツ**: Key型は`{ id: string; key: Buffer }`の単純な構造。idはキーの識別子、keyは32バイトのAES-256キー。

#### Step 2: キーリングパースを理解する

環境変数からキーリングを構築するロジックを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | cryptoBox.ts | `frontend/internal-packages/security/src/cryptoBox.ts` | parseKeyring関数 |

**主要処理フロー**:
1. **12行目**: 関数シグネチャ `parseKeyring(envValue: string | undefined): Key[]`
2. **13行目**: 空のKey配列を初期化
3. **14-17行目**: 環境変数をカンマで分割、トリム、空文字除去
4. **18行目**: 各エントリをループ処理
5. **19行目**: `entry.split(':')` - kid:base64で分割
6. **20行目**: idまたはb64がない場合はスキップ
7. **21行目**: Base64デコード
8. **22行目**: 32バイトでない場合はスキップ
9. **23行目**: キー配列に追加
10. **25行目**: キー配列を返却

#### Step 3: キーリング初期化を理解する

プロセス起動時のキーリング初期化を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | cryptoBox.ts | `frontend/internal-packages/security/src/cryptoBox.ts` | RING変数の初期化 |

**主要処理フロー**:
- **28-29行目**: コメントとRING変数の初期化
  - `let RING: Key[] = parseKeyring(process.env['LIAM_GITHUB_OAUTH_KEYRING'])`

#### Step 4: キー取得関数を理解する

currentKeyとsetKeyring関数を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | cryptoBox.ts | `frontend/internal-packages/security/src/cryptoBox.ts` | currentKey, setKeyring関数 |

**主要処理フロー**:
- **31-33行目**: setKeyring関数（テスト用）
  - キーリングを外部から設定可能
- **35-38行目**: currentKey関数
  - RING[0]を返却（先頭が現在のキー）
  - キーリングが空の場合はエラー

#### Step 5: キー検索（復号化時）を理解する

復号化時のキー検索ロジックを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | cryptoBox.ts | `frontend/internal-packages/security/src/cryptoBox.ts` | decryptAesGcm内のキー検索 |

**主要処理フロー**:
- **79行目**: `const entry = RING.find((k) => k.id === keyId)`
- **80行目**: キーが見つからない場合はエラー

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

```
[初期化]
プロセス起動
    │
    └─ parseKeyring(process.env['LIAM_GITHUB_OAUTH_KEYRING'])
           │
           ├─ split(',')  ← カンマ分割
           │
           ├─ map/filter  ← トリム・空文字除去
           │
           └─ for each entry
                  │
                  ├─ split(':')  ← kid:base64分割
                  │
                  ├─ Buffer.from(b64, 'base64')  ← デコード
                  │
                  └─ 32バイト検証 → RING配列に追加

[暗号化時]
encryptAesGcm(plaintext)
    │
    └─ currentKey()
           └─ RING[0]  ← 先頭キーを返却

[復号化時]
decryptAesGcm(keyId, ...)
    │
    └─ RING.find(k => k.id === keyId)
           └─ 該当キーを返却
```

### データフロー図

```
[環境変数]                  [パース処理]                    [キーリング]

LIAM_GITHUB_OAUTH_KEYRING ─▶ parseKeyring() ─────────────▶ RING: Key[]
"kid1:b64key1,kid0:b64key0"        │                          │
                                   │                          │
                            ┌──────▼──────┐                   │
                            │ split(',')  │                   │
                            └──────┬──────┘                   │
                                   │                          │
                            ┌──────▼──────┐                   │
                            │ split(':')  │                   │
                            │ per entry   │                   │
                            └──────┬──────┘                   │
                                   │                          │
                            ┌──────▼──────┐                   │
                            │ Base64      │                   │
                            │ decode      │                   │
                            └──────┬──────┘                   │
                                   │                          │
                            ┌──────▼──────┐                   │
                            │ 32バイト    │───────────────────▶│
                            │ 検証        │                    │
                            └─────────────┘                    ▼

                                                    ┌─────────────────┐
                                                    │ RING[0]         │
                                                    │ (現在のキー)     │
                                                    ├─────────────────┤
                                                    │ RING[1]         │
                                                    │ (古いキー)       │
                                                    └─────────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| cryptoBox.ts | `frontend/internal-packages/security/src/cryptoBox.ts` | ソース | キーリング管理のメイン実装 |
