# B40_セキュリティ基礎・脆弱性対策

## 🎯 概要

情報システム開発におけるセキュリティの基礎概念、一般的な脆弱性とその対策、セキュアコーディングの実践方法を解説します。
OWASP Top 10を基準とした包括的なセキュリティ対策を目指します。

---

## 📋 目次

1. [セキュリティ基礎概念](#セキュリティ基礎概念)
2. [OWASP Top 10・主要脆弱性](#owasp-top-10主要脆弱性)
3. [認証・認可・セッション管理](#認証認可セッション管理)
4. [入力値検証・インジェクション対策](#入力値検証インジェクション対策)
5. [暗号化・HTTPS・ハッシュ化](#暗号化httpsハッシュ化)
6. [セキュアコーディング実践](#セキュアコーディング実践)

---

## 🔐 セキュリティ基礎概念

### セキュリティの3要素（CIA）

#### **機密性（Confidentiality）**
- **定義**: 許可された者のみが情報にアクセスできること
- **脅威**: 情報漏洩、不正アクセス、盗聴
- **対策**: 暗号化、アクセス制御、認証

#### **完全性（Integrity）**
- **定義**: 情報が改ざんされていないことを保証すること
- **脅威**: データ改ざん、不正な変更、破損
- **対策**: デジタル署名、ハッシュ値、チェックサム

#### **可用性（Availability）**
- **定義**: 必要な時に情報システムが利用可能であること
- **脅威**: DoS攻撃、システム障害、サービス停止
- **対策**: 冗長化、負荷分散、バックアップ

### セキュリティの基本原則

#### 1. 多層防御（Defense in Depth）
```
   外部ファイアウォール
          ↓
      DMZ（非武装地帯）
          ↓
    内部ファイアウォール
          ↓
    アプリケーション認証
          ↓
     データベースアクセス制御
```

#### 2. 最小権限の原則（Principle of Least Privilege）
```python
# ✅ 良い例：必要最小限の権限
class EpisodeEditor:
    def __init__(self):
        self.permissions = [
            "read_own_episodes",
            "write_own_episodes",
            "read_episode_templates"
        ]

# ❌ 悪い例：過剰な権限
class AdminUser:
    def __init__(self):
        self.permissions = [
            "read_all", "write_all", "delete_all",
            "admin_all", "system_config"
        ]
```

#### 3. 明示的拒否（Deny by Default）
```python
class AccessController:
    def __init__(self):
        self._allowed_actions = set()  # デフォルトは空＝全て拒否

    def is_allowed(self, user: User, action: str) -> bool:
        # 明示的に許可されたもののみTrue
        return f"{user.role}:{action}" in self._allowed_actions

    def grant_permission(self, role: str, action: str):
        self._allowed_actions.add(f"{role}:{action}")
```

#### 4. セキュリティ・バイ・デザイン
- 設計段階からセキュリティを考慮
- 後付けではなく、基盤として組み込む
- セキュリティ要件の明確化

---

## 🚨 OWASP Top 10・主要脆弱性

### 1. A01: アクセス制御の不備

#### 脆弱性の例
```python
# ✅ 安全な実装
@app.route('/episode/<int:episode_id>')
@require_login
def get_episode(episode_id):
    episode = Episode.get_by_id(episode_id)

    # 所有者チェック
    if episode.author_id != current_user.id:
        abort(403, "Access denied")

    return jsonify(episode.to_dict())

# ❌ 脆弱な実装
@app.route('/episode/<int:episode_id>')
def get_episode(episode_id):
    episode = Episode.get_by_id(episode_id)
    return jsonify(episode.to_dict())  # 所有者チェックなし
```

#### 対策実装
```python
class EpisodeAccessController:
    def __init__(self, episode_repository: EpisodeRepository):
        self._repository = episode_repository

    def can_read(self, user: User, episode_id: str) -> bool:
        episode = self._repository.get_by_id(episode_id)
        if not episode:
            return False

        # 所有者または公開済み
        return (episode.author_id == user.id or
                episode.status == EpisodeStatus.PUBLISHED)

    def can_write(self, user: User, episode_id: str) -> bool:
        episode = self._repository.get_by_id(episode_id)
        if not episode:
            return False

        # 所有者のみ
        return episode.author_id == user.id

    def can_delete(self, user: User, episode_id: str) -> bool:
        return (self.can_write(user, episode_id) and
                user.has_permission("delete_episodes"))
```

### 2. A02: 暗号化の不備

#### 脆弱性の例
```python
# ❌ 脆弱な実装
import hashlib

def hash_password(password: str) -> str:
    return hashlib.md5(password.encode()).hexdigest()  # MD5は脆弱

# ❌ 平文保存
def save_user_password(user_id: str, password: str):
    database.save({
        "user_id": user_id,
        "password": password  # 平文保存
    })
```

#### 安全な実装
```python
import bcrypt
import secrets
from cryptography.fernet import Fernet

class SecurePasswordManager:
    def __init__(self):
        self._salt_rounds = 12

    def hash_password(self, password: str) -> str:
        """bcryptを使用した安全なパスワードハッシュ化"""
        salt = bcrypt.gensalt(rounds=self._salt_rounds)
        hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
        return hashed.decode('utf-8')

    def verify_password(self, password: str, hashed: str) -> bool:
        """パスワード検証"""
        return bcrypt.checkpw(
            password.encode('utf-8'),
            hashed.encode('utf-8')
        )

    def generate_secure_token(self, length: int = 32) -> str:
        """暗号学的に安全なトークン生成"""
        return secrets.token_urlsafe(length)

class DataEncryption:
    def __init__(self, encryption_key: bytes):
        self._fernet = Fernet(encryption_key)

    def encrypt_sensitive_data(self, data: str) -> str:
        """機密データの暗号化"""
        encrypted = self._fernet.encrypt(data.encode('utf-8'))
        return encrypted.decode('utf-8')

    def decrypt_sensitive_data(self, encrypted_data: str) -> str:
        """暗号化データの復号"""
        decrypted = self._fernet.decrypt(encrypted_data.encode('utf-8'))
        return decrypted.decode('utf-8')
```

### 3. A03: インジェクション攻撃

#### SQLインジェクション対策
```python
# ✅ 安全な実装（パラメータ化クエリ）
def get_user_episodes(user_id: str):
    query = "SELECT * FROM episodes WHERE author_id = %s"
    return database.execute(query, (user_id,))

# ✅ ORMを使用した安全な実装
class EpisodeRepository:
    def get_by_author(self, author_id: str) -> List[Episode]:
        return (session.query(Episode)
                .filter(Episode.author_id == author_id)
                .all())

# ❌ 脆弱な実装
def get_user_episodes(user_id: str):
    query = f"SELECT * FROM episodes WHERE author_id = '{user_id}'"
    return database.execute(query)  # SQLインジェクション脆弱性

```

#### コマンドインジェクション対策
```python
import subprocess
import shlex

# ✅ 安全な実装
def process_file(filename: str):
    # 入力値検証
    if not filename.replace('_', '').replace('-', '').replace('.', '').isalnum():
        raise ValueError("Invalid filename")

    # パラメータ配列で実行
    subprocess.run([
        "python", "process.py", filename
    ], check=True)

# ✅ より安全な実装（ホワイトリスト方式）
class SecureFileProcessor:
    ALLOWED_EXTENSIONS = {'.txt', '.md', '.yaml'}
    MAX_FILENAME_LENGTH = 100

    def process_file(self, filename: str):
        self._validate_filename(filename)

        # 安全なパスに制限
        safe_path = self._get_safe_path(filename)

        subprocess.run([
            "python", "scripts/tools/process_file.py", safe_path
        ], check=True, timeout=30)

    def _validate_filename(self, filename: str):
        if len(filename) > self.MAX_FILENAME_LENGTH:
            raise ValueError("Filename too long")

        path = Path(filename)
        if path.suffix not in self.ALLOWED_EXTENSIONS:
            raise ValueError("File extension not allowed")

        # パストラバーサル対策
        if '..' in filename or filename.startswith('/'):
            raise ValueError("Invalid path")

    def _get_safe_path(self, filename: str) -> str:
        # 安全なディレクトリに制限
        safe_dir = Path("./temp/processing")
        safe_dir.mkdir(exist_ok=True)
        return str(safe_dir / Path(filename).name)

# ❌ 脆弱な実装
def process_file(filename: str):
    command = f"python process.py {filename}"
    subprocess.run(command, shell=True)  # コマンドインジェクション脆弱性
```

### 4. A04: 不安全な設計

#### セキュアな設計原則
```python
class SecureEpisodeService:
    def __init__(self,
                 repository: EpisodeRepository,
                 access_controller: AccessController,
                 audit_logger: AuditLogger):
        self._repository = repository
        self._access_controller = access_controller
        self._audit_logger = audit_logger

    def create_episode(self, user: User, episode_data: dict) -> Episode:
        # 1. 入力値検証
        validated_data = self._validate_episode_data(episode_data)

        # 2. 権限チェック
        if not self._access_controller.can_create_episode(user):
            self._audit_logger.log_unauthorized_attempt(
                user.id, "create_episode", episode_data
            )
            raise UnauthorizedError("Cannot create episode")

        # 3. ビジネスルールチェック
        if self._exceeds_episode_limit(user):
            raise BusinessRuleViolationError("Episode limit exceeded")

        # 4. 安全な作成処理
        episode = Episode.create(validated_data)
        saved_episode = self._repository.save(episode)

        # 5. 監査ログ
        self._audit_logger.log_episode_created(user.id, saved_episode.id)

        return saved_episode

    def _validate_episode_data(self, data: dict) -> dict:
        """入力値検証とサニタイゼーション"""
        validator = EpisodeDataValidator()
        return validator.validate_and_sanitize(data)

    def _exceeds_episode_limit(self, user: User) -> bool:
        """ユーザーのエピソード数制限チェック"""
        current_count = self._repository.count_by_author(user.id)
        return current_count >= user.max_episodes
```

### 5. A05: セキュリティ設定ミス

#### 安全な設定管理
```python
import os
from dataclasses import dataclass
from typing import Optional

@dataclass
class SecurityConfig:
    secret_key: str
    database_url: str
    encryption_key: str
    session_timeout: int = 3600
    max_login_attempts: int = 5
    password_min_length: int = 8

    @classmethod
    def from_environment(cls) -> 'SecurityConfig':
        # 必須環境変数のチェック
        required_vars = ['SECRET_KEY', 'DATABASE_URL', 'ENCRYPTION_KEY']
        missing_vars = [var for var in required_vars if not os.getenv(var)]

        if missing_vars:
            raise ValueError(f"Missing required environment variables: {missing_vars}")

        return cls(
            secret_key=os.getenv('SECRET_KEY'),
            database_url=os.getenv('DATABASE_URL'),
            encryption_key=os.getenv('ENCRYPTION_KEY'),
            session_timeout=int(os.getenv('SESSION_TIMEOUT', '3600')),
            max_login_attempts=int(os.getenv('MAX_LOGIN_ATTEMPTS', '5')),
            password_min_length=int(os.getenv('PASSWORD_MIN_LENGTH', '8'))
        )

    def validate(self):
        """設定値の妥当性検証"""
        if len(self.secret_key) < 32:
            raise ValueError("SECRET_KEY must be at least 32 characters")

        if self.session_timeout < 300:  # 5分未満は危険
            raise ValueError("SESSION_TIMEOUT too short")

        if self.password_min_length < 8:
            raise ValueError("PASSWORD_MIN_LENGTH too short")

# 設定の読み込みと検証
config = SecurityConfig.from_environment()
config.validate()
```

#### セキュアヘッダーの設定
```python
# CLIアプリケーションでのセキュリティ例
from typer import Typer
from pathlib import Path
import os

app = Typer()

# セキュリティヘッダーの設定
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://trusted-domain.com"],
    allow_credentials=True,
    allow_methods=["GET", "POST"],
    allow_headers=["*"],
)
```

---

## 🔑 認証・認可・セッション管理

### 認証（Authentication）の実装

#### 多要素認証（MFA）
```python
import pyotp
import qrcode
from io import BytesIO

class MultiFactorAuth:
    def __init__(self, user_repository: UserRepository):
        self._user_repo = user_repository

    def setup_totp(self, user: User) -> tuple[str, bytes]:
        """TOTP（時間ベースワンタイムパスワード）設定"""
        secret = pyotp.random_base32()

        # QRコード生成
        totp_uri = pyotp.totp.TOTP(secret).provisioning_uri(
            name=user.email,
            issuer_name="Novel Writing System"
        )

        qr = qrcode.QRCode(version=1, box_size=10, border=5)
        qr.add_data(totp_uri)
        qr.make(fit=True)

        qr_image = qr.make_image(fill_color="black", back_color="white")
        buffer = BytesIO()
        qr_image.save(buffer, format='PNG')

        # ユーザーにTOTP秘密鍵を保存
        user.totp_secret = secret
        self._user_repo.save(user)

        return secret, buffer.getvalue()

    def verify_totp(self, user: User, token: str) -> bool:
        """TOTPトークン検証"""
        if not user.totp_secret:
            return False

        totp = pyotp.TOTP(user.totp_secret)
        return totp.verify(token, valid_window=1)  # 30秒の誤差許容

    def require_mfa(self, user: User, password: str, totp_token: str) -> bool:
        """多要素認証の実装"""
        # 1. パスワード認証
        if not self._verify_password(user, password):
            return False

        # 2. TOTP認証
        if not self.verify_totp(user, totp_token):
            return False

        return True
```

#### セッション管理
```python
import secrets
import time
from datetime import datetime, timedelta

class SecureSessionManager:
    def __init__(self, redis_client, session_timeout: int = 3600):
        self._redis = redis_client
        self._timeout = session_timeout

    def create_session(self, user: User) -> str:
        """セキュアなセッション作成"""
        # 暗号学的に安全なセッションID生成
        session_id = secrets.token_urlsafe(32)

        session_data = {
            'user_id': user.id,
            'user_role': user.role,
            'created_at': datetime.now().isoformat(),
            'last_activity': datetime.now().isoformat(),
            'ip_address': self._get_client_ip(),
            'user_agent': self._get_user_agent()
        }

        # Redisに保存（TTL付き）
        self._redis.setex(
            f"session:{session_id}",
            self._timeout,
            json.dumps(session_data)
        )

        return session_id

    def validate_session(self, session_id: str) -> Optional[dict]:
        """セッション検証"""
        if not session_id:
            return None

        session_data = self._redis.get(f"session:{session_id}")
        if not session_data:
            return None

        data = json.loads(session_data)

        # セッションハイジャック対策：IP・User-Agentチェック
        if (data.get('ip_address') != self._get_client_ip() or
            data.get('user_agent') != self._get_user_agent()):
            self.invalidate_session(session_id)
            return None

        # 最終活動時間更新
        data['last_activity'] = datetime.now().isoformat()
        self._redis.setex(
            f"session:{session_id}",
            self._timeout,
            json.dumps(data)
        )

        return data

    def invalidate_session(self, session_id: str):
        """セッション無効化"""
        self._redis.delete(f"session:{session_id}")

    def cleanup_expired_sessions(self):
        """期限切れセッションのクリーンアップ"""
        # Redisの期限切れ自動削除を利用
        pass
```

### 認可（Authorization）の実装

#### ロールベースアクセス制御（RBAC）
```python
from enum import Enum
from dataclasses import dataclass
from typing import Set

class Permission(Enum):
    READ_EPISODE = "read_episode"
    WRITE_EPISODE = "write_episode"
    DELETE_EPISODE = "delete_episode"
    PUBLISH_EPISODE = "publish_episode"
    ADMIN_USERS = "admin_users"
    SYSTEM_CONFIG = "system_config"

class Role(Enum):
    READER = "reader"
    AUTHOR = "author"
    EDITOR = "editor"
    ADMIN = "admin"

@dataclass
class RolePermissions:
    ROLE_PERMISSIONS = {
        Role.READER: {
            Permission.READ_EPISODE
        },
        Role.AUTHOR: {
            Permission.READ_EPISODE,
            Permission.WRITE_EPISODE,
            Permission.DELETE_EPISODE,
            Permission.PUBLISH_EPISODE
        },
        Role.EDITOR: {
            Permission.READ_EPISODE,
            Permission.WRITE_EPISODE,
            Permission.DELETE_EPISODE,
            Permission.PUBLISH_EPISODE
        },
        Role.ADMIN: {
            Permission.READ_EPISODE,
            Permission.WRITE_EPISODE,
            Permission.DELETE_EPISODE,
            Permission.PUBLISH_EPISODE,
            Permission.ADMIN_USERS,
            Permission.SYSTEM_CONFIG
        }
    }

class AuthorizationService:
    def __init__(self):
        self._role_permissions = RolePermissions.ROLE_PERMISSIONS

    def has_permission(self, user: User, permission: Permission) -> bool:
        """ユーザーが特定の権限を持つかチェック"""
        user_permissions = self._role_permissions.get(user.role, set())
        return permission in user_permissions

    def can_access_episode(self, user: User, episode: Episode) -> bool:
        """エピソードアクセス可能性チェック"""
        # 自分のエピソードなら常にアクセス可能
        if episode.author_id == user.id:
            return True

        # 公開済みエピソードは読み取り権限があれば可能
        if (episode.status == EpisodeStatus.PUBLISHED and
            self.has_permission(user, Permission.READ_EPISODE)):
            return True

        # エディター・管理者は全エピソードにアクセス可能
        return user.role in [Role.EDITOR, Role.ADMIN]

    def require_permission(self, permission: Permission):
        """デコレータ：権限チェック"""
        def decorator(func):
            def wrapper(*args, **kwargs):
                current_user = get_current_user()
                if not self.has_permission(current_user, permission):
                    raise UnauthorizedError(f"Permission {permission.value} required")
                return func(*args, **kwargs)
            return wrapper
        return decorator

# 使用例
auth_service = AuthorizationService()

@auth_service.require_permission(Permission.WRITE_EPISODE)
def create_episode(episode_data: dict) -> Episode:
    # エピソード作成処理
    pass
```

---

## 🛡️ 入力値検証・インジェクション対策

### 包括的入力値検証

#### バリデーションフレームワーク
```python
from pydantic import BaseModel, validator, Field
from typing import Optional
import re
import html

class EpisodeCreateRequest(BaseModel):
    title: str = Field(..., min_length=1, max_length=100)
    content: str = Field(..., min_length=1, max_length=50000)
    episode_number: int = Field(..., ge=1, le=9999)
    tags: Optional[List[str]] = Field(default=[], max_items=10)

    @validator('title')
    def validate_title(cls, v):
        # HTMLタグ除去
        clean_title = html.escape(v.strip())

        # 不正文字チェック
        if re.search(r'[<>\"\'&]', clean_title):
            raise ValueError('Title contains invalid characters')

        return clean_title

    @validator('content')
    def validate_content(cls, v):
        # 基本的なHTMLサニタイゼーション
        clean_content = html.escape(v)

        # 過度に長い行のチェック
        lines = clean_content.split('\n')
        for line in lines:
            if len(line) > 1000:
                raise ValueError('Content line too long')

        return clean_content

    @validator('tags')
    def validate_tags(cls, v):
        if not v:
            return []

        clean_tags = []
        for tag in v:
            # タグのサニタイゼーション
            clean_tag = html.escape(tag.strip().lower())

            # タグ形式チェック
            if not re.match(r'^[a-zA-Z0-9_-]+$', clean_tag):
                raise ValueError(f'Invalid tag format: {tag}')

            if len(clean_tag) > 20:
                raise ValueError(f'Tag too long: {tag}')

            clean_tags.append(clean_tag)

        return clean_tags

class SecureInputValidator:
    def __init__(self):
        self._max_string_length = 10000
        self._allowed_file_extensions = {'.md', '.txt', '.yaml'}
        self._dangerous_patterns = [
            r'<script.*?>.*?</script>',
            r'javascript:',
            r'vbscript:',
            r'onload=',
            r'onerror=',
            r'eval\(',
            r'exec\('
        ]

    def validate_string(self, value: str, field_name: str) -> str:
        """文字列の安全性検証"""
        if not isinstance(value, str):
            raise ValueError(f"{field_name} must be a string")

        if len(value) > self._max_string_length:
            raise ValueError(f"{field_name} too long")

        # 危険なパターンチェック
        for pattern in self._dangerous_patterns:
            if re.search(pattern, value, re.IGNORECASE):
                raise ValueError(f"{field_name} contains dangerous content")

        return html.escape(value.strip())

    def validate_filename(self, filename: str) -> str:
        """ファイル名の安全性検証"""
        if not filename:
            raise ValueError("Filename cannot be empty")

        # パストラバーサル対策
        if '..' in filename or filename.startswith('/'):
            raise ValueError("Invalid filename path")

        # 拡張子チェック
        file_path = Path(filename)
        if file_path.suffix not in self._allowed_file_extensions:
            raise ValueError("File extension not allowed")

        # ファイル名文字チェック
        if not re.match(r'^[a-zA-Z0-9._-]+$', file_path.name):
            raise ValueError("Filename contains invalid characters")

        return filename

    def validate_episode_number(self, number: int) -> int:
        """エピソード番号の検証"""
        if not isinstance(number, int):
            raise ValueError("Episode number must be an integer")

        if number < 1 or number > 9999:
            raise ValueError("Episode number out of range")

        return number
```

### XSS対策

#### 出力エスケープ
```python
import html
from markupsafe import Markup, escape
from jinja2 import Environment, select_autoescape

class XSSProtection:
    def __init__(self):
        # Jinja2テンプレートエンジンの安全な設定
        self._jinja_env = Environment(
            autoescape=select_autoescape(['html', 'xml'])
        )

    def escape_html(self, text: str) -> str:
        """HTML特殊文字のエスケープ"""
        return html.escape(text, quote=True)

    def escape_javascript(self, text: str) -> str:
        """JavaScript用エスケープ"""
        # JavaScript内で使用する際の特殊文字エスケープ
        replacements = {
            '\\': '\\\\',
            '"': '\\"',
            "'": "\\'",
            '\n': '\\n',
            '\r': '\\r',
            '\t': '\\t',
            '<': '\\x3C',
            '>': '\\x3E',
            '&': '\\x26'
        }

        escaped = text
        for char, replacement in replacements.items():
            escaped = escaped.replace(char, replacement)

        return escaped

    def sanitize_markdown(self, markdown_text: str) -> str:
        """Markdown安全化"""
        import bleach

        # 許可するHTMLタグとアトリビュート
        allowed_tags = [
            'p', 'br', 'strong', 'em', 'u', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
            'ul', 'ol', 'li', 'blockquote', 'code', 'pre'
        ]

        allowed_attributes = {
            '*': ['class'],
            'a': ['href', 'title'],
            'img': ['src', 'alt', 'title']
        }

        return bleach.clean(
            markdown_text,
            tags=allowed_tags,
            attributes=allowed_attributes,
            strip=True
        )

# テンプレート使用例
class EpisodeRenderer:
    def __init__(self):
        self._xss_protection = XSSProtection()

    def render_episode(self, episode: Episode) -> str:
        """エピソードの安全なレンダリング"""
        template = """
        <div class="episode">
            <h1>{{ title|e }}</h1>
            <div class="content">{{ content|safe }}</div>
            <div class="tags">
                {% for tag in tags %}
                    <span class="tag">{{ tag|e }}</span>
                {% endfor %}
            </div>
        </div>
        """

        # Markdownコンテンツのサニタイゼーション
        safe_content = self._xss_protection.sanitize_markdown(episode.content)

        return self._xss_protection._jinja_env.from_string(template).render(
            title=episode.title,
            content=Markup(safe_content),  # 既にサニタイズ済みなのでsafe
            tags=episode.tags
        )
```

---

## 🔐 暗号化・HTTPS・ハッシュ化

### データ暗号化

#### 対称暗号化（AES）
```python
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import base64
import os

class DataEncryption:
    def __init__(self, password: str):
        # パスワードから暗号化キーを導出
        salt = os.urandom(16)
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=salt,
            iterations=100000,
        )
        key = base64.urlsafe_b64encode(kdf.derive(password.encode()))
        self._cipher = Fernet(key)
        self._salt = salt

    def encrypt(self, data: str) -> str:
        """データの暗号化"""
        encrypted = self._cipher.encrypt(data.encode('utf-8'))
        # salt + encrypted data
        return base64.urlsafe_b64encode(self._salt + encrypted).decode('utf-8')

    def decrypt(self, encrypted_data: str) -> str:
        """データの復号"""
        data = base64.urlsafe_b64decode(encrypted_data.encode('utf-8'))
        salt = data[:16]
        encrypted = data[16:]

        # 同じsaltで鍵を再生成
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=salt,
            iterations=100000,
        )
        key = base64.urlsafe_b64encode(kdf.derive(password.encode()))
        cipher = Fernet(key)

        decrypted = cipher.decrypt(encrypted)
        return decrypted.decode('utf-8')

class EpisodeDraftEncryption:
    """エピソード下書きの暗号化保存"""

    def __init__(self, encryption_service: DataEncryption):
        self._encryption = encryption_service

    def save_draft(self, episode_id: str, content: str, author_key: str):
        """下書きの暗号化保存"""
        # コンテンツを暗号化
        encrypted_content = self._encryption.encrypt(content)

        # メタデータ（暗号化しない）
        metadata = {
            'episode_id': episode_id,
            'created_at': datetime.now().isoformat(),
            'encrypted': True
        }

        # 保存
        draft_data = {
            'metadata': metadata,
            'encrypted_content': encrypted_content
        }

        with open(f"drafts/{episode_id}.json", 'w') as f:
            json.dump(draft_data, f)

    def load_draft(self, episode_id: str, author_key: str) -> str:
        """下書きの復号読み込み"""
        with open(f"drafts/{episode_id}.json", 'r') as f:
            draft_data = json.load(f)

        encrypted_content = draft_data['encrypted_content']
        return self._encryption.decrypt(encrypted_content)
```

#### 非対称暗号化（RSA）
```python
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes, serialization

class AsymmetricEncryption:
    def __init__(self):
        self._private_key = None
        self._public_key = None

    def generate_key_pair(self) -> tuple[bytes, bytes]:
        """RSAキーペア生成"""
        self._private_key = rsa.generate_private_key(
            public_exponent=65537,
            key_size=2048,
        )
        self._public_key = self._private_key.public_key()

        # PEM形式でシリアライズ
        private_pem = self._private_key.private_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PrivateFormat.PKCS8,
            encryption_algorithm=serialization.NoEncryption()
        )

        public_pem = self._public_key.public_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PublicFormat.SubjectPublicKeyInfo
        )

        return private_pem, public_pem

    def encrypt_with_public_key(self, data: str, public_key_pem: bytes) -> bytes:
        """公開鍵による暗号化"""
        public_key = serialization.load_pem_public_key(public_key_pem)

        encrypted = public_key.encrypt(
            data.encode('utf-8'),
            padding.OAEP(
                mgf=padding.MGF1(algorithm=hashes.SHA256()),
                algorithm=hashes.SHA256(),
                label=None
            )
        )
        return encrypted

    def decrypt_with_private_key(self, encrypted_data: bytes, private_key_pem: bytes) -> str:
        """秘密鍵による復号"""
        private_key = serialization.load_pem_private_key(
            private_key_pem,
            password=None,
        )

        decrypted = private_key.decrypt(
            encrypted_data,
            padding.OAEP(
                mgf=padding.MGF1(algorithm=hashes.SHA256()),
                algorithm=hashes.SHA256(),
                label=None
            )
        )
        return decrypted.decode('utf-8')
```

### HTTPS実装

#### CLIアプリケーションでのセキュリティ設定
```python
# 本プロジェクトはCLIツールのため、Webサーバーは使用していません
# 代わりに、CLIツール（Typer）でのセキュリティ実装例を示します
from typer import Typer
from pathlib import Path
import os

app = Typer()

# CLIツールでのセキュア通信
# APIアクセス時のHTTPS利用例
import requests

def secure_api_call(endpoint: str, data: dict):
    """外部APIへのセキュアな通信"""
    response = requests.post(
        endpoint,
        json=data,
        verify=True,  # SSL証明書検証を有効化
        timeout=30
    )
    return response

# CLIコマンドでのセキュリティ実装
@app.command()
def process_secure(
    file_path: Path = typer.Argument(..., help="処理対象ファイル"),
    confirm: bool = typer.Option(False, "--confirm", help="実行確認")
):
    """セキュアなファイル処理コマンド"""
    # パス検証
    if not file_path.exists():
        typer.echo(f"Error: ファイルが存在しません: {file_path}")
        raise typer.Exit(1)

    # 確認プロンプト
    if not confirm:
        typer.echo("--confirm オプションで実行を確認してください")
        raise typer.Exit(1)

    # セキュアな処理実行
    typer.echo(f"処理中: {file_path}")
```

#### 証明書管理
```python
import ssl
import socket
from datetime import datetime
from cryptography import x509
from cryptography.x509.oid import NameOID

class CertificateManager:
    def check_certificate_expiry(self, hostname: str, port: int = 443) -> dict:
        """SSL証明書の有効期限チェック"""
        try:
            # SSL証明書取得
            context = ssl.create_default_context()
            with socket.create_connection((hostname, port), timeout=10) as sock:
                with context.wrap_socket(sock, server_hostname=hostname) as ssock:
                    cert_der = ssock.getpeercert(binary_form=True)

            # 証明書解析
            cert = x509.load_der_x509_certificate(cert_der)

            # 有効期限情報
            not_after = cert.not_valid_after
            days_until_expiry = (not_after - datetime.now()).days

            # サブジェクト情報
            subject = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value

            return {
                'hostname': hostname,
                'subject': subject,
                'not_after': not_after.isoformat(),
                'days_until_expiry': days_until_expiry,
                'is_valid': days_until_expiry > 0,
                'needs_renewal': days_until_expiry < 30  # 30日以内に更新
            }

        except Exception as e:
            return {
                'hostname': hostname,
                'error': str(e),
                'is_valid': False
            }

    def validate_certificate_chain(self, cert_file: str) -> bool:
        """証明書チェーンの検証"""
        try:
            with open(cert_file, 'rb') as f:
                cert_data = f.read()

            cert = x509.load_pem_x509_certificate(cert_data)

            # 基本的な検証
            now = datetime.now()
            if cert.not_valid_after < now:
                return False

            if cert.not_valid_before > now:
                return False

            return True

        except Exception:
            return False
```

---

## 🛡️ セキュアコーディング実践

### セキュアコーディングガイドライン

#### 1. 入力値は信頼しない
```python
# ✅ 安全な例
def process_user_input(user_input: str):
    # 入力値検証
    validator = InputValidator()
    safe_input = validator.validate_and_sanitize(user_input)

    # 安全な処理
    return process_safe_data(safe_input)

# ❌ 危険な例
def process_user_input(user_input: str):
    # 入力値をそのまま使用
    eval(user_input)  # 極めて危険
    exec(f"result = {user_input}")  # 危険
```

#### 2. 最小権限の原則
```python
class EpisodeFileManager:
    def __init__(self, base_path: str):
        self._base_path = Path(base_path)
        self._allowed_extensions = {'.md', '.txt', '.yaml'}

    def read_episode_file(self, filename: str, user: User) -> str:
        # ファイル名検証
        safe_path = self._validate_file_path(filename)

        # 権限チェック
        if not self._can_read_file(safe_path, user):
            raise PermissionError("Access denied")

        # 安全な読み込み
        return self._read_file_safely(safe_path)

    def _validate_file_path(self, filename: str) -> Path:
        """ファイルパスの安全性検証"""
        # パストラバーサル対策
        if '..' in filename or filename.startswith('/'):
            raise ValueError("Invalid file path")

        # ベースディレクトリ内に制限
        full_path = self._base_path / filename
        if not str(full_path).startswith(str(self._base_path)):
            raise ValueError("Path outside allowed directory")

        # 拡張子チェック
        if full_path.suffix not in self._allowed_extensions:
            raise ValueError("File type not allowed")

        return full_path

    def _can_read_file(self, file_path: Path, user: User) -> bool:
        """ファイル読み取り権限チェック"""
        # ファイル所有者チェック
        file_metadata = self._get_file_metadata(file_path)
        if file_metadata.get('owner_id') == user.id:
            return True

        # 公開ファイルチェック
        if file_metadata.get('is_public', False):
            return True

        # 管理者権限チェック
        return user.role == Role.ADMIN
```

#### 3. エラー情報の適切な処理
```python
from typing import Optional
from noveler.infrastructure.logging.unified_logger import (
    get_logger, configure_logging, LogLevel
)

class SecureErrorHandler:
    def __init__(self):
        # 統一ロガーを使用（ファイル出力/ローテーションは unified 設定側で制御）
        configure_logging(file_level=LogLevel.WARNING)
        self._logger = get_logger(__name__)

    def handle_error(self, err: Exception, user_id: Optional[str] = None) -> None:
        masked_user = user_id[:4] + '***' if user_id else 'anonymous'
        self._logger.warning("security: error detected", extra={
            "extra_data": {"user": masked_user, "error": str(err)[:200]}
        })
```

#### 4. セキュアな設定管理
```python
from cryptography.fernet import Fernet
import json
import os

class SecureConfigManager:
    def __init__(self, config_file: str, encryption_key: str):
        self._config_file = config_file
        self._cipher = Fernet(encryption_key.encode())
        self._config = {}
        self._load_config()

    def _load_config(self):
        """設定ファイルの安全な読み込み"""
        try:
            if os.path.exists(self._config_file):
                with open(self._config_file, 'rb') as f:
                    encrypted_data = f.read()

                decrypted_data = self._cipher.decrypt(encrypted_data)
                self._config = json.loads(decrypted_data.decode('utf-8'))
            else:
                self._config = self._get_default_config()
                self._save_config()

        except Exception as e:
            # 設定ファイル破損時はデフォルト設定を使用
            self._config = self._get_default_config()

    def _save_config(self):
        """設定ファイルの暗号化保存"""
        config_json = json.dumps(self._config, indent=2)
        encrypted_data = self._cipher.encrypt(config_json.encode('utf-8'))

        # 原子的書き込み
        temp_file = f"{self._config_file}.tmp"
        with open(temp_file, 'wb') as f:
            f.write(encrypted_data)

        os.rename(temp_file, self._config_file)

        # ファイル権限設定（所有者のみ読み書き可能）
        os.chmod(self._config_file, 0o600)

    def get_secret(self, key: str) -> Optional[str]:
        """機密情報の安全な取得"""
        return self._config.get('secrets', {}).get(key)

    def set_secret(self, key: str, value: str):
        """機密情報の安全な保存"""
        if 'secrets' not in self._config:
            self._config['secrets'] = {}

        self._config['secrets'][key] = value
        self._save_config()

    def _get_default_config(self) -> dict:
        """デフォルト設定"""
        return {
            'app': {
                'name': 'Novel Writing System',
                'version': '1.0.0',
                'debug': False
            },
            'security': {
                'session_timeout': 3600,
                'max_login_attempts': 5,
                'password_min_length': 8,
                'require_mfa': False
            },
            'secrets': {}
        }
```

### セキュリティテストの実装

#### 自動化セキュリティテスト
```python
import pytest
import requests
from unittest.mock import patch

class SecurityTestSuite:
    def test_sql_injection_protection(self, app_client):
        """SQLインジェクション対策テスト"""
        malicious_inputs = [
            "'; DROP TABLE episodes; --",
            "1' OR '1'='1",
            "admin'; UPDATE users SET role='admin' WHERE id=1; --"
        ]

        for payload in malicious_inputs:
            response = app_client.get(f'/episodes?author_id={payload}')

            # SQLインジェクションが防がれていることを確認
            assert response.status_code != 500
            assert 'error' not in response.json.get('data', {})

    def test_xss_protection(self, app_client):
        """XSS対策テスト"""
        xss_payloads = [
            "<script>alert('xss')</script>",
            "javascript:alert('xss')",
            "<img src=x onerror=alert('xss')>",
            "'><script>alert('xss')</script>"
        ]

        for payload in xss_payloads:
            response = app_client.post('/episodes', json={
                'title': payload,
                'content': f'Content with {payload}'
            })

            # XSSペイロードがエスケープされていることを確認
            if response.status_code == 200:
                html_content = response.data.decode('utf-8')
                assert '<script>' not in html_content
                assert 'javascript:' not in html_content

    def test_authentication_bypass(self, app_client):
        """認証バイパス対策テスト"""
        protected_endpoints = [
            '/admin/users',
            '/episodes/create',
            '/settings',
            '/api/admin'
        ]

        for endpoint in protected_endpoints:
            # 認証なしでアクセス
            response = app_client.get(endpoint)

            # 適切に認証が要求されることを確認
            assert response.status_code in [401, 403, 302]

    def test_session_security(self, app_client):
        """セッションセキュリティテスト"""
        # 正常ログイン
        login_response = app_client.post('/login', json={
            'username': 'testuser',
            'password': 'testpass'
        })

        assert login_response.status_code == 200

        # セッションクッキーの属性確認
        set_cookie = login_response.headers.get('Set-Cookie', '')
        assert 'HttpOnly' in set_cookie
        assert 'Secure' in set_cookie
        assert 'SameSite' in set_cookie

    def test_rate_limiting(self, app_client):
        """レート制限テスト"""
        # 大量リクエスト送信
        for i in range(100):
            response = app_client.post('/login', json={
                'username': 'testuser',
                'password': 'wrongpass'
            })

        # レート制限が機能することを確認
        assert response.status_code == 429  # Too Many Requests
```

---

## 📚 次のステップ

このガイドでセキュリティ基礎を理解した後は、以下のガイドで深化させてください：

### セキュリティの深化
- **B42: セキュリティ設計・実装** - 高度なセキュリティ実装
- **B43: データ保護・プライバシー** - GDPR対応とプライバシー保護

### 関連領域
- **B33: コード品質・静的解析** - セキュリティ品質の測定
- **B52: 監視・ログ管理・可観測性** - セキュリティ監視
- **B72: Infrastructure as Code** - インフラセキュリティ

---

**更新履歴**
- 2025-08-04: 初版作成
- 作成者: Claude Code
- レビュー状況: 策定完了
