浏览代码

multi-key setup

zehe 3 月之前
父节点
当前提交
e9eb409331
共有 8 个文件被更改,包括 789 次插入609 次删除
  1. 1 0
      .gitignore
  2. 305 46
      README.md
  3. 0 23
      config-production.json
  4. 0 23
      config.json
  5. 48 25
      config.py
  6. 10 6
      install.sh
  7. 152 83
      main.py
  8. 273 403
      test.py

+ 1 - 0
.gitignore

@@ -1,2 +1,3 @@
 venv/*
 venv/*
 .idea/*
 .idea/*
+config.json

+ 305 - 46
README.md

@@ -1,32 +1,36 @@
 # Emergency Access Server
 # Emergency Access Server
 
 
-A fail-safe webserver that provides secure access to decryption key parts with mandatory notification system integration. Designed for emergency scenarios where key retrieval must be monitored and logged.
+A fail-safe webserver that provides secure access to multiple decryption key parts with mandatory notification system integration. Designed for emergency scenarios where key retrieval must be monitored and logged.
 
 
 ## Features
 ## Features
 
 
+- **Multiple key support**: Configure multiple decryption keys with individual routes and notification backends
 - **Fail-safe design**: All operations require successful notification delivery
 - **Fail-safe design**: All operations require successful notification delivery
-- **Dual endpoint system**: Emergency key access and health monitoring
+- **Dynamic endpoint system**: Emergency key access and health monitoring with configurable routes
 - **dschep/ntfy integration**: Real-time notifications via multiple backends (Pushover, Pushbullet, Slack, etc.)
 - **dschep/ntfy integration**: Real-time notifications via multiple backends (Pushover, Pushbullet, Slack, etc.)
 - **Real-time log monitoring**: All application logs automatically sent to notification backends
 - **Real-time log monitoring**: All application logs automatically sent to notification backends
-- **Configurable security**: Random endpoint paths and file locations
+- **Configurable security**: Extremely long random endpoint paths (100+ characters) and file locations per key
 - **Caddy reverse proxy ready**: Runs on localhost for secure proxy setup
 - **Caddy reverse proxy ready**: Runs on localhost for secure proxy setup
 - **Systemd integration**: Automatic startup and service management
 - **Systemd integration**: Automatic startup and service management
 - **Comprehensive logging**: Detailed audit trail of all operations with live notifications
 - **Comprehensive logging**: Detailed audit trail of all operations with live notifications
 
 
 ## Architecture
 ## Architecture
 
 
-The system consists of two main endpoints:
+The system consists of multiple key endpoints and a health monitoring endpoint:
 
 
-1. **Emergency Key Endpoint** (`/emergency-key-xyz123`): 
-   - Serves the actual decryption key part
-   - Sends critical alerts via dschep/ntfy to configured backends
+1. **Emergency Key Endpoints** (e.g., `/emergency-key-backup-a7f9d2e1b4c7f3e6d9a2b5c8e1f4d7a0b3c6e9f2a5d8b1c4e7f0a3d6b9c2e5f8a1d4b7c0e3f6d9a2b5c8e1f4a7b0d3`): 
+   - Each serves a specific decryption key part
+   - Extremely long paths (100+ characters) to prevent accidental access
+   - Individual notification backends per key
+   - Custom messages per key type
    - Fails closed if notifications cannot be sent
    - Fails closed if notifications cannot be sent
+   - Supports unlimited number of keys
 
 
-2. **Health Check Endpoint** (`/health-check-abc456`):
+2. **Health Check Endpoint** (`/health-check-f8d9e2a1b4c7f3e6d9a2b5c8e1f4d7a0b3c6e9f2a5d8b1c4e7f0a3d6b9c2e5f8`):
    - Verifies all system components required for emergency access
    - Verifies all system components required for emergency access
-   - Tests both health and emergency notification backends
-   - Validates file system access for both dummy and key files
-   - Ensures complete emergency system readiness
+   - Tests both health and all key notification backends
+   - Validates file system access for dummy and all key files
+   - Ensures complete emergency system readiness for all configured keys
    - Used for regular system verification
    - Used for regular system verification
 
 
 **Log Monitoring**: All application logs (WARNING level and above by default) are automatically sent to the health backends for real-time monitoring and alerting.
 **Log Monitoring**: All application logs (WARNING level and above by default) are automatically sent to the health backends for real-time monitoring and alerting.
@@ -76,7 +80,7 @@ sudo ./install.sh
 4. **Install application**:
 4. **Install application**:
    ```bash
    ```bash
    sudo cp *.py requirements.txt /opt/emergency-access/
    sudo cp *.py requirements.txt /opt/emergency-access/
-   sudo cp config.json /etc/emergency-access/
+   sudo cp config.json.example /etc/emergency-access/config.json
    sudo chown -R emergency-access:emergency-access /opt/emergency-access
    sudo chown -R emergency-access:emergency-access /opt/emergency-access
    ```
    ```
 
 
@@ -96,7 +100,14 @@ sudo ./install.sh
 
 
 ### Main Configuration File
 ### Main Configuration File
 
 
-Edit `/etc/emergency-access/config.json`:
+Copy and edit the example configuration to `/etc/emergency-access/config.json`:
+
+```bash
+sudo cp config.json.example /etc/emergency-access/config.json
+sudo nano /etc/emergency-access/config.json
+```
+
+Configuration structure:
 
 
 ```json
 ```json
 {
 {
@@ -105,18 +116,45 @@ Edit `/etc/emergency-access/config.json`:
     "port": 1127
     "port": 1127
   },
   },
   "routes": {
   "routes": {
-    "key_route": "/emergency-key-a7f9d2e1",
-    "health_route": "/health-check-b8e3f4a2"
+    "health_route": "/health-check-f8d9e2a1b4c7f3e6d9a2b5c8e1f4d7a0b3c6e9f2a5d8b1c4e7f0a3d6b9c2e5f8"
   },
   },
   "files": {
   "files": {
-    "key_file": "/etc/emergency-access/key-part.txt",
     "dummy_file": "/etc/emergency-access/dummy.txt"
     "dummy_file": "/etc/emergency-access/dummy.txt"
   },
   },
+  "keys": {
+    "backup_key": {
+      "route": "/emergency-key-backup-a7f9d2e1b4c7f3e6d9a2b5c8e1f4d7a0b3c6e9f2a5d8b1c4e7f0a3d6b9c2e5f8a1d4b7c0e3f6d9a2b5c8e1f4a7b0d3",
+      "file": "/etc/emergency-access/backup-key.txt",
+      "backends": ["matrix_sec", "pushover_emergency"],
+      "message": "🚨 EMERGENCY: Backup decryption key accessed from server"
+    },
+    "master_key": {
+      "route": "/emergency-key-master-x3k8m9n2p5q7r1t4u6v9w2y5z8b1c4d7e0f3g6h9j2k5l8m1n4o7p0q3r6s9t2u5v8w1x4y7z0a3b6c9d2e5f8",
+      "file": "/etc/emergency-access/master-key.txt",
+      "backends": ["matrix_sec", "pushover_critical", "slack_emergency"],
+      "message": "🚨 CRITICAL: Master decryption key accessed from server"
+    },
+    "recovery_key": {
+      "route": "/emergency-key-recovery-q5w7r8t1u3i6o9p0a2s4d6f8g0h2j4k7l9z1x3c5v7b9n1m3q6w8e0r2t4y6u8i0o2p4a6s8d0f2g4h6j8k0l2z4x6c8v0",
+      "file": "/etc/emergency-access/recovery-key.txt",
+      "backends": ["matrix_sec", "email_emergency"],
+      "message": "🚨 EMERGENCY: Recovery decryption key accessed from server"
+    },
+    "admin_key": {
+      "route": "/emergency-key-admin-z9x7c5v3b1n8m6k4j2h0g9f7d5s3a1p0o9i8u7y6t5r4e3w2q1z0x9c8v7b6n5m4k3j2h1g0f9d8s7a6p5o4i3u2y1t0r9",
+      "file": "/etc/emergency-access/admin-key.txt",
+      "backends": [
+        "matrix_sec",
+        "pushover_critical",
+        "slack_emergency",
+        "email_critical"
+      ],
+      "message": "🚨 CRITICAL ALERT: Administrator master key accessed from server"
+    }
+  },
   "notifications": {
   "notifications": {
-    "key_backends": ["matrix_sec", "pushover_emergency"],
     "health_backends": ["matrix_health"],
     "health_backends": ["matrix_health"],
     "config_path": "/etc/emergency-access/ntfy.yml",
     "config_path": "/etc/emergency-access/ntfy.yml",
-    "key_message": "🚨 EMERGENCY: Decryption key accessed from server",
     "health_message": "✅ Emergency access server health check completed",
     "health_message": "✅ Emergency access server health check completed",
     "log_level": "WARNING",
     "log_level": "WARNING",
     "send_all_logs": true
     "send_all_logs": true
@@ -131,18 +169,21 @@ Edit `/etc/emergency-access/config.json`:
 - `port`: Listen port (default: `1127`)
 - `port`: Listen port (default: `1127`)
 
 
 #### Route Settings
 #### Route Settings
-- `key_route`: Random path for key access (e.g., `/emergency-key-a7f9d2e1`)
-- `health_route`: Path for health checks (e.g., `/health-check-b8e3f4a2`)
+- `health_route`: Path for health checks - must be 100+ characters long to prevent accidental access
+
+#### Keys Configuration
+Each key in the `keys` object supports:
+- `route`: Unique random path for this specific key - must be 100+ characters long to prevent accidental access
+- `file`: Path to the key file for this specific key
+- `backends`: List of notification backend names for this specific key
+- `message`: Custom message sent when this specific key is accessed
 
 
 #### File Settings
 #### File Settings
-- `key_file`: Path to the actual key part file
-- `dummy_file`: Path to dummy content for health checks (health check also validates key file accessibility)
+- `dummy_file`: Path to dummy content for health checks
 
 
 #### Notification Settings
 #### Notification Settings
-- `key_backends`: List of backend names from `/etc/emergency-access/ntfy.yml` for key access alerts
 - `health_backends`: List of backend names from `/etc/emergency-access/ntfy.yml` for health check notifications and all application logs
 - `health_backends`: List of backend names from `/etc/emergency-access/ntfy.yml` for health check notifications and all application logs
 - `config_path`: Path to the ntfy configuration file (default: `/etc/emergency-access/ntfy.yml`)
 - `config_path`: Path to the ntfy configuration file (default: `/etc/emergency-access/ntfy.yml`)
-- `key_message`: Message sent when key is accessed
 - `health_message`: Message sent for health checks
 - `health_message`: Message sent for health checks
 - `log_level`: Minimum log level to send to health backends ("INFO", "WARNING", "ERROR")
 - `log_level`: Minimum log level to send to health backends ("INFO", "WARNING", "ERROR")
 - `send_all_logs`: Whether to send application logs to health backends (true/false)
 - `send_all_logs`: Whether to send application logs to health backends (true/false)
@@ -155,11 +196,27 @@ Edit `/etc/emergency-access/config.json`:
 
 
 ### Key and Dummy Files
 ### Key and Dummy Files
 
 
-1. **Create key file**:
+1. **Create key files for each configured key**:
    ```bash
    ```bash
-   echo "YOUR_ACTUAL_KEY_PART_HERE" | sudo tee /etc/emergency-access/key-part.txt
-   sudo chown emergency-access:emergency-access /etc/emergency-access/key-part.txt
-   sudo chmod 600 /etc/emergency-access/key-part.txt
+   # Backup key
+   echo "YOUR_BACKUP_KEY_PART_HERE" | sudo tee /etc/emergency-access/backup-key.txt
+   sudo chown emergency-access:emergency-access /etc/emergency-access/backup-key.txt
+   sudo chmod 600 /etc/emergency-access/backup-key.txt
+
+   # Master key
+   echo "YOUR_MASTER_KEY_PART_HERE" | sudo tee /etc/emergency-access/master-key.txt
+   sudo chown emergency-access:emergency-access /etc/emergency-access/master-key.txt
+   sudo chmod 600 /etc/emergency-access/master-key.txt
+
+   # Recovery key
+   echo "YOUR_RECOVERY_KEY_PART_HERE" | sudo tee /etc/emergency-access/recovery-key.txt
+   sudo chown emergency-access:emergency-access /etc/emergency-access/recovery-key.txt
+   sudo chmod 600 /etc/emergency-access/recovery-key.txt
+
+   # Admin key
+   echo "YOUR_ADMIN_KEY_PART_HERE" | sudo tee /etc/emergency-access/admin-key.txt
+   sudo chown emergency-access:emergency-access /etc/emergency-access/admin-key.txt
+   sudo chmod 600 /etc/emergency-access/admin-key.txt
    ```
    ```
 
 
 2. **Create dummy file**:
 2. **Create dummy file**:
@@ -278,15 +335,40 @@ curl http://localhost:1127/health-check-b8e3f4a2
 ### Emergency Key Access
 ### Emergency Key Access
 
 
 ```bash
 ```bash
-# Access the key (replace with your actual route)
-curl https://your-domain.com/emergency-key-a7f9d2e1
+# Access backup key
+curl https://your-domain.com/emergency-key-backup-a7f9d2e1b4c7f3e6d9a2b5c8e1f4d7a0b3c6e9f2a5d8b1c4e7f0a3d6b9c2e5f8a1d4b7c0e3f6d9a2b5c8e1f4a7b0d3
 
 
 # Expected response:
 # Expected response:
 {
 {
   "success": true,
   "success": true,
-  "key_part": "YOUR_KEY_PART_HERE",
+  "key_id": "backup_key",
+  "key_part": "YOUR_BACKUP_KEY_PART_HERE",
   "timestamp": 1703123456.789,
   "timestamp": 1703123456.789,
-  "notified_backends": ["matrix_sec"]
+  "notified_backends": ["matrix_sec", "pushover_emergency"]
+}
+
+# Access master key
+curl https://your-domain.com/emergency-key-master-x3k8m9n2p5q7r1t4u6v9w2y5z8b1c4d7e0f3g6h9j2k5l8m1n4o7p0q3r6s9t2u5v8w1x4y7z0a3b6c9d2e5f8
+
+# Expected response:
+{
+  "success": true,
+  "key_id": "master_key",
+  "key_part": "YOUR_MASTER_KEY_PART_HERE",
+  "timestamp": 1703123456.789,
+  "notified_backends": ["matrix_sec", "pushover_critical", "slack_emergency"]
+}
+
+# Access admin key
+curl https://your-domain.com/emergency-key-admin-z9x7c5v3b1n8m6k4j2h0g9f7d5s3a1p0o9i8u7y6t5r4e3w2q1z0x9c8v7b6n5m4k3j2h1g0f9d8s7a6p5o4i3u2y1t0r9
+
+# Expected response:
+{
+  "success": true,
+  "key_id": "admin_key",
+  "key_part": "YOUR_ADMIN_KEY_PART_HERE",
+  "timestamp": 1703123456.789,
+  "notified_backends": ["matrix_sec", "pushover_critical", "slack_emergency", "email_critical"]
 }
 }
 ```
 ```
 
 
@@ -294,16 +376,39 @@ curl https://your-domain.com/emergency-key-a7f9d2e1
 
 
 ```bash
 ```bash
 # Regular health monitoring
 # Regular health monitoring
-curl https://your-domain.com/health-check-b8e3f4a2
+curl https://your-domain.com/health-check-f8d9e2a1b4c7f3e6d9a2b5c8e1f4d7a0b3c6e9f2a5d8b1c4e7f0a3d6b9c2e5f8
 
 
 # Expected response (all systems operational):
 # Expected response (all systems operational):
 {
 {
   "status": "ok",
   "status": "ok",
   "timestamp": 1703123456.789,
   "timestamp": 1703123456.789,
   "health_backends_notified": ["matrix_health"],
   "health_backends_notified": ["matrix_health"],
-  "key_backends_tested": ["matrix_sec", "pushover_emergency"],
   "dummy_content_length": 14,
   "dummy_content_length": 14,
-  "key_file_accessible": true,
+  "keys_accessible": 4,
+  "key_files_status": {
+    "backup_key": true,
+    "master_key": true,
+    "recovery_key": true,
+    "admin_key": true
+  },
+  "key_backends_status": {
+    "backup_key": {
+      "backends": ["matrix_sec", "pushover_emergency"],
+      "success": true
+    },
+    "master_key": {
+      "backends": ["matrix_sec", "pushover_critical", "slack_emergency"],
+      "success": true
+    },
+    "recovery_key": {
+      "backends": ["matrix_sec", "email_emergency"],
+      "success": true
+    },
+    "admin_key": {
+      "backends": ["matrix_sec", "pushover_critical", "slack_emergency", "email_critical"],
+      "success": true
+    }
+  },
   "emergency_system_ready": true
   "emergency_system_ready": true
 }
 }
 
 
@@ -311,11 +416,38 @@ curl https://your-domain.com/health-check-b8e3f4a2
 {
 {
   "status": "error",
   "status": "error",
   "message": "System components failed",
   "message": "System components failed",
-  "details": ["key notifications failed", "key file access failed: Permission denied"],
+  "details": [
+    "dummy file access failed: Permission denied",
+    "key file access failed for 'master_key'",
+    "key backends failed for 'recovery_key': Configuration error"
+  ],
   "health_notifications": true,
   "health_notifications": true,
-  "key_notifications": false,
-  "dummy_file_access": true,
-  "key_file_access": false
+  "dummy_file_access": false,
+  "key_files_status": {
+    "backup_key": true,
+    "master_key": false,
+    "recovery_key": true,
+    "admin_key": true
+  },
+  "key_backends_status": {
+    "backup_key": {
+      "backends": ["matrix_sec", "pushover_emergency"],
+      "success": true
+    },
+    "master_key": {
+      "backends": ["matrix_sec", "pushover_critical"],
+      "success": true
+    },
+    "recovery_key": {
+      "backends": ["matrix_sec", "email_emergency"],
+      "success": false,
+      "error": "Configuration error"
+    },
+    "admin_key": {
+      "backends": ["matrix_sec", "pushover_critical", "slack_emergency", "email_critical"],
+      "success": true
+    }
+  }
 }
 }
 ```
 ```
 
 
@@ -456,17 +588,128 @@ EMERGENCY_CONFIG=config.json python main.py
 ### Testing
 ### Testing
 
 
 ```bash
 ```bash
-# Test key endpoint (direct to service)
-curl http://localhost:1127/emergency-key-xyz123
+# Run comprehensive test suite
+python test.py
+
+# Run quick configuration tests only
+python test.py --quick
+
+# Test specific endpoints (replace with your actual paths)
+curl http://localhost:1127/emergency-key-backup-[YOUR_SECURE_PATH]
+curl http://localhost:1127/emergency-key-master-[YOUR_SECURE_PATH]
+curl http://localhost:1127/health-check-[YOUR_SECURE_PATH]
+
+# Generate new configuration with secure paths
+python generate_secure_paths.py --keys 3 --output config
+```
+
+## Multiple Key Implementation Details
+
+### Path Security
+
+#### Path Length Requirements
+- **Minimum**: 100 characters
+- **Recommended**: 120+ characters
+- **Example length**: `/emergency-key-backup-a7f9d2e1b4c7f3e6d9a2b5c8e1f4d7a0b3c6e9f2a5d8b1c4e7f0a3d6b9c2e5f8a1d4b7c0e3f6d9a2b5c8e1f4a7b0d3` (130 chars)
+
+#### Why Ultra-Long Paths?
+1. **Prevent accidental access**: Impossible to guess or stumble upon
+2. **Reduce false positives**: No accidental emergency notifications
+3. **Security through obscurity**: Additional layer of protection
+4. **URL safety**: Only lowercase letters and numbers
+
+### Dynamic Route Registration
+The application dynamically registers routes for each configured key:
+```python
+for key_id, key_config in config.keys.items():
+    handler = create_key_handler(key_config)
+    endpoint_name = f'get_key_part_{key_id}'
+    app.add_url_rule(key_config.route, endpoint_name, handler, methods=['GET'])
+```
+
+### Individual Key Handlers
+Each key gets its own handler function with specific configuration:
+```python
+def create_key_handler(key_config):
+    def get_key_part():
+        # Send notifications to key-specific backends
+        notification_success, successful_backends = send_ntfy_notification(
+            key_config.backends,
+            key_config.message,
+            "EMERGENCY ACCESS ALERT"
+        )
+        # Return key with key ID
+        return jsonify({
+            'success': True,
+            'key_id': key_config.key_id,
+            'key_part': content,
+            'timestamp': time.time(),
+            'notified_backends': successful_backends
+        })
+    return get_key_part
+```
+
+### Health Check Validation
+Health checks now validate all configured keys:
+```python
+# Test all key files
+for key_id, key_config in config.keys.items():
+    key_file_success, key_content = read_file_safely(key_config.file_path)
+    key_files_status[key_id] = key_file_success
+```
+
+### Generate Secure Configuration
+
+Use the included path generator for cryptographically secure paths:
+
+```bash
+# Generate new configuration with secure paths
+python generate_secure_paths.py --keys 3 --output config
 
 
-# Test health endpoint (direct to service)
-curl http://localhost:1127/health-check
+# Generate complete setup including Caddy config and setup scripts
+python generate_secure_paths.py --keys 4 --domain your-domain.com --output all
 
 
-# Test through Caddy proxy
-curl https://yourdomain.com/emergency-key-xyz123
-curl https://yourdomain.com/health-check
+# Generate just secure paths for existing configuration
+python generate_secure_paths.py --keys 2 --output paths
 ```
 ```
 
 
+The path generator creates:
+- **Configuration file**: Complete JSON config with secure paths
+- **Caddy configuration**: Reverse proxy setup with path restrictions
+- **Setup script**: Automated key file creation with proper permissions
+- **Security validation**: Ensures all paths meet minimum length requirements
+
+### Security Best Practices
+
+#### Path Storage
+- Store paths in secure password manager
+- Document which path corresponds to which key
+- Never store paths in plain text logs
+- Use the path generator for cryptographically secure random paths
+
+#### Access Monitoring
+- Monitor reverse proxy logs for path access
+- Set up alerts for any key access
+- Regularly audit notification delivery
+- Implement rate limiting to prevent brute force attempts
+
+#### Emergency Procedures
+1. **Key Compromise**: Change the path, not the key content
+2. **False Positive**: Investigate immediately, check logs
+3. **Path Leak**: Generate new paths and update configuration
+4. **Notification Failure**: Validate all backends before deployment
+
+### Migration Considerations
+
+This implementation removes legacy single-key support for enhanced security:
+
+#### Clean Architecture
+- Removed all legacy single-key configuration support
+- Simplified configuration structure with only `keys` section
+- All paths must be 100+ characters for security
+- Each key requires explicit configuration
+- No backward compatibility to ensure security by default
+
 ## License
 ## License
 
 
 This project is designed for emergency access scenarios. Use responsibly and ensure proper security measures are in place.
 This project is designed for emergency access scenarios. Use responsibly and ensure proper security measures are in place.
@@ -478,3 +721,19 @@ For issues and questions:
 2. Verify configuration: `/etc/emergency-access/config.json`
 2. Verify configuration: `/etc/emergency-access/config.json`
 3. Test notification systems independently
 3. Test notification systems independently
 4. Monitor service status: `systemctl status emergency-access`
 4. Monitor service status: `systemctl status emergency-access`
+5. Use path generator: `python generate_secure_paths.py --help`
+
+### Validation Commands
+```bash
+# Validate configuration
+python -c "from config import Config; Config('/etc/emergency-access/config.json')"
+
+# Test file access
+sudo -u emergency-access cat /etc/emergency-access/backup-key.txt
+
+# Test notifications
+ntfy -c /etc/emergency-access/ntfy.yml -b matrix_sec send "Test message"
+
+# Generate new secure paths
+python generate_secure_paths.py --keys 3 --path-length 120
+```

+ 0 - 23
config-production.json

@@ -1,23 +0,0 @@
-{
-  "server": {
-    "host": "127.0.0.1",
-    "port": 1127
-  },
-  "routes": {
-    "key_route": "/emergency-key-7a8b9c2d3e4f5g6h",
-    "health_route": "/health-check-9x8y7z6w5v4u3t2s"
-  },
-  "files": {
-    "key_file": "/etc/emergency-access/production-key-part.txt",
-    "dummy_file": "/etc/emergency-access/production-dummy.txt"
-  },
-  "notifications": {
-    "key_backends": ["matrix_sec"],
-    "health_backends": ["matrix_ntf"],
-    "config_path": "/etc/emergency-access/ntfy.yml",
-    "key_message": "🚨 CRITICAL ALERT: Emergency decryption key accessed in PRODUCTION environment",
-    "health_message": "✅ Emergency access system health check - all systems operational",
-    "log_level": "WARNING",
-    "send_all_logs": true
-  }
-}

+ 0 - 23
config.json

@@ -1,23 +0,0 @@
-{
-  "server": {
-    "host": "127.0.0.1",
-    "port": 1127
-  },
-  "routes": {
-    "key_route": "/emergency-key-a7f9d2e1",
-    "health_route": "/health-check-b8e3f4a2"
-  },
-  "files": {
-    "key_file": "/etc/emergency-access/key-part.txt",
-    "dummy_file": "/etc/emergency-access/dummy.txt"
-  },
-  "notifications": {
-    "key_backends": ["matrix_sec", "pushover_emergency"],
-    "health_backends": ["matrix_health"],
-    "config_path": "/etc/emergency-access/ntfy.yml",
-    "key_message": "🚨 EMERGENCY: Decryption key accessed from server",
-    "health_message": "✅ Emergency access server health check completed",
-    "log_level": "WARNING",
-    "send_all_logs": true
-  }
-}

+ 48 - 25
config.py

@@ -1,7 +1,23 @@
 import json
 import json
 import os
 import os
 import yaml
 import yaml
-from typing import Dict, List, Any
+from typing import Dict, List, Any, Optional
+
+class KeyConfig:
+    """Configuration for a single key"""
+    def __init__(self, key_id: str, config_data: Dict[str, Any], global_config: Dict[str, Any]):
+        self.key_id = key_id
+        self.route = config_data.get('route', '')
+        self.file_path = config_data.get('file', '')
+        self.backends = config_data.get('backends', [])
+        self.message = config_data.get('message', f'🚨 EMERGENCY: Key {key_id} accessed from server')
+
+        if not self.route:
+            raise Exception(f"Route not configured for key '{key_id}'")
+        if not self.file_path:
+            raise Exception(f"File path not configured for key '{key_id}'")
+        if not self.backends:
+            raise Exception(f"No notification backends configured for key '{key_id}'")
 
 
 class Config:
 class Config:
     def __init__(self, config_path: str = None):
     def __init__(self, config_path: str = None):
@@ -10,6 +26,7 @@ class Config:
 
 
         self.config_path = config_path
         self.config_path = config_path
         self.config = self._load_config()
         self.config = self._load_config()
+        self._keys = self._load_keys()
 
 
     def _load_config(self) -> Dict[str, Any]:
     def _load_config(self) -> Dict[str, Any]:
         """Load configuration from file"""
         """Load configuration from file"""
@@ -24,6 +41,21 @@ class Config:
         except Exception as e:
         except Exception as e:
             raise Exception(f"Failed to load configuration: {str(e)}")
             raise Exception(f"Failed to load configuration: {str(e)}")
 
 
+    def _load_keys(self) -> Dict[str, KeyConfig]:
+        """Load key configurations"""
+        keys = {}
+
+        if 'keys' not in self.config:
+            raise Exception("No keys configured. Configuration must include a 'keys' section")
+
+        for key_id, key_config in self.config['keys'].items():
+            keys[key_id] = KeyConfig(key_id, key_config, self.config)
+
+        if not keys:
+            raise Exception("No valid keys found in configuration")
+
+        return keys
+
     @property
     @property
     def server_host(self) -> str:
     def server_host(self) -> str:
         return self.config.get('server', {}).get('host', '127.0.0.1')
         return self.config.get('server', {}).get('host', '127.0.0.1')
@@ -32,21 +64,10 @@ class Config:
     def server_port(self) -> int:
     def server_port(self) -> int:
         return self.config.get('server', {}).get('port', 1127)
         return self.config.get('server', {}).get('port', 1127)
 
 
-    @property
-    def key_route(self) -> str:
-        return self.config.get('routes', {}).get('key_route', '/emergency-key-xyz123')
-
     @property
     @property
     def health_route(self) -> str:
     def health_route(self) -> str:
         return self.config.get('routes', {}).get('health_route', '/health-check')
         return self.config.get('routes', {}).get('health_route', '/health-check')
 
 
-    @property
-    def key_file_path(self) -> str:
-        key_file = self.config.get('files', {}).get('key_file')
-        if not key_file:
-            raise Exception("key_file not configured")
-        return key_file
-
     @property
     @property
     def dummy_file_path(self) -> str:
     def dummy_file_path(self) -> str:
         dummy_file = self.config.get('files', {}).get('dummy_file')
         dummy_file = self.config.get('files', {}).get('dummy_file')
@@ -55,11 +76,20 @@ class Config:
         return dummy_file
         return dummy_file
 
 
     @property
     @property
-    def ntfy_backends_key(self) -> List[str]:
-        backends = self.config.get('notifications', {}).get('key_backends', [])
-        if not backends:
-            raise Exception("No notification backends configured for key access")
-        return backends
+    def keys(self) -> Dict[str, KeyConfig]:
+        """Get all configured keys"""
+        return self._keys
+
+    def get_key_by_route(self, route: str) -> Optional[KeyConfig]:
+        """Find a key configuration by its route"""
+        for key_config in self._keys.values():
+            if key_config.route == route:
+                return key_config
+        return None
+
+    def get_key_by_id(self, key_id: str) -> Optional[KeyConfig]:
+        """Get a key configuration by its ID"""
+        return self._keys.get(key_id)
 
 
     @property
     @property
     def ntfy_backends_health(self) -> List[str]:
     def ntfy_backends_health(self) -> List[str]:
@@ -72,7 +102,6 @@ class Config:
     def ntfy_config_path(self) -> str:
     def ntfy_config_path(self) -> str:
         return self.config.get('notifications', {}).get('config_path', '/etc/emergency-access/ntfy.yml')
         return self.config.get('notifications', {}).get('config_path', '/etc/emergency-access/ntfy.yml')
 
 
-
     @property
     @property
     def log_level(self) -> str:
     def log_level(self) -> str:
         return self.config.get('notifications', {}).get('log_level', 'WARNING')
         return self.config.get('notifications', {}).get('log_level', 'WARNING')
@@ -81,12 +110,6 @@ class Config:
     def send_all_logs(self) -> bool:
     def send_all_logs(self) -> bool:
         return self.config.get('notifications', {}).get('send_all_logs', True)
         return self.config.get('notifications', {}).get('send_all_logs', True)
 
 
-
-
-    @property
-    def ntfy_key_message(self) -> str:
-        return self.config.get('notifications', {}).get('key_message', 'EMERGENCY: Decryption key accessed')
-
     @property
     @property
     def ntfy_health_message(self) -> str:
     def ntfy_health_message(self) -> str:
-        return self.config.get('notifications', {}).get('health_message', 'Health check performed')
+        return self.config.get('notifications', {}).get('health_message', '✅ Emergency access server health check completed')

+ 10 - 6
install.sh

@@ -107,9 +107,9 @@ install_application() {
 
 
     # Copy example config if config doesn't exist
     # Copy example config if config doesn't exist
     if [[ ! -f "$CONFIG_DIR/config.json" ]]; then
     if [[ ! -f "$CONFIG_DIR/config.json" ]]; then
-        cp config.json "$CONFIG_DIR/"
+        cp config.json.example "$CONFIG_DIR/config.json"
         print_status "Copied example configuration to $CONFIG_DIR/config.json"
         print_status "Copied example configuration to $CONFIG_DIR/config.json"
-        print_warning "Please edit $CONFIG_DIR/config.json with your backend names"
+        print_warning "Please edit $CONFIG_DIR/config.json with your backend names and secure paths"
     else
     else
         print_warning "Configuration file already exists, skipping copy"
         print_warning "Configuration file already exists, skipping copy"
     fi
     fi
@@ -221,10 +221,11 @@ print_final_instructions() {
     print_status "Installation complete!"
     print_status "Installation complete!"
     echo
     echo
     print_warning "IMPORTANT: Before starting the service:"
     print_warning "IMPORTANT: Before starting the service:"
-    echo "1. Edit $CONFIG_DIR/config.json with your backend names"
+    echo "1. Edit $CONFIG_DIR/config.json with your backend names and secure paths"
     echo "2. Edit $CONFIG_DIR/ntfy.yml with your notification backend configurations"
     echo "2. Edit $CONFIG_DIR/ntfy.yml with your notification backend configurations"
-    echo "3. Replace $CONFIG_DIR/key-part.txt with your actual key part"
-    echo "4. Test the configuration"
+    echo "3. Create key files for each configured key (e.g., backup-key.txt, master-key.txt)"
+    echo "4. Use generate_secure_paths.py to create ultra-secure access paths"
+    echo "5. Test the configuration"
     echo
     echo
     print_status "Service management commands:"
     print_status "Service management commands:"
     echo "  Start service:    sudo systemctl start emergency-access"
     echo "  Start service:    sudo systemctl start emergency-access"
@@ -236,7 +237,10 @@ print_final_instructions() {
     print_status "Configuration files:"
     print_status "Configuration files:"
     echo "  Service config:   $CONFIG_DIR/config.json"
     echo "  Service config:   $CONFIG_DIR/config.json"
     echo "  ntfy config:      $CONFIG_DIR/ntfy.yml"
     echo "  ntfy config:      $CONFIG_DIR/ntfy.yml"
-    echo "  Key file:         $CONFIG_DIR/key-part.txt"
+    echo "  Key files:        $CONFIG_DIR/[key-name]-key.txt (create as configured)"
+    echo ""
+    print_status "Path generator:"
+    echo "  Generate paths:   python generate_secure_paths.py --keys 3"
     echo "  Dummy file:       $CONFIG_DIR/dummy.txt"
     echo "  Dummy file:       $CONFIG_DIR/dummy.txt"
     echo "  Log file:         $LOG_FILE"
     echo "  Log file:         $LOG_FILE"
     echo
     echo

+ 152 - 83
main.py

@@ -53,7 +53,7 @@ logger = logging.getLogger(__name__)
 
 
 app = Flask(__name__)
 app = Flask(__name__)
 
 
-# Global config instance
+# Global config instance will be initialized in main
 config = None
 config = None
 
 
 def send_ntfy_notification(backends: List[str], message: str, title: str = None) -> Tuple[bool, List[str]]:
 def send_ntfy_notification(backends: List[str], message: str, title: str = None) -> Tuple[bool, List[str]]:
@@ -66,8 +66,11 @@ def send_ntfy_notification(backends: List[str], message: str, title: str = None)
     for backend in backends:
     for backend in backends:
         try:
         try:
             # Import ntfy here to avoid import issues during startup
             # Import ntfy here to avoid import issues during startup
-            import ntfy
-            from ntfy.config import load_config
+            try:
+                import ntfy
+                from ntfy.config import load_config
+            except ImportError:
+                raise Exception("ntfy package not available. Please install with: pip install ntfy")
 
 
             # Load the ntfy config file
             # Load the ntfy config file
             ntfy_config = load_config(config.ntfy_config_path)
             ntfy_config = load_config(config.ntfy_config_path)
@@ -122,54 +125,60 @@ def read_file_safely(file_path: str) -> Tuple[bool, str]:
         logger.error(f"Failed to read file {file_path}: {str(e)}")
         logger.error(f"Failed to read file {file_path}: {str(e)}")
         return False, f"Failed to read file: {str(e)}"
         return False, f"Failed to read file: {str(e)}"
 
 
-def get_key_part():
-    """Emergency key access endpoint"""
-    logger.warning("EMERGENCY: Key access attempt detected")
-
-    try:
-        # Send notification first - fail-safe approach
-        notification_success, successful_backends = send_ntfy_notification(
-            config.ntfy_backends_key,
-            config.ntfy_key_message,
-            "EMERGENCY ACCESS ALERT"
-        )
+def create_key_handler(key_config):
+    """Create a key access handler for a specific key configuration"""
+    def get_key_part():
+        """Emergency key access endpoint"""
+        logger.warning(f"EMERGENCY: Key access attempt detected for key '{key_config.key_id}'")
 
 
-        if not notification_success:
-            logger.error("CRITICAL: Failed to send notifications to any backend")
+        try:
+            # Send notification first - fail-safe approach
+            notification_success, successful_backends = send_ntfy_notification(
+                key_config.backends,
+                key_config.message,
+                "EMERGENCY ACCESS ALERT"
+            )
+
+            if not notification_success:
+                logger.error(f"CRITICAL: Failed to send notifications to any backend for key '{key_config.key_id}'")
+                return jsonify({
+                    'error': 'Notification system failure',
+                    'message': 'Access denied for security reasons'
+                }), 500
+
+            logger.info(f"Notifications sent successfully to: {successful_backends} for key '{key_config.key_id}'")
+
+            # Read key file
+            file_success, content = read_file_safely(key_config.file_path)
+
+            if not file_success:
+                logger.error(f"CRITICAL: Failed to read key file for '{key_config.key_id}': {content}")
+                return jsonify({
+                    'error': 'File access failure',
+                    'message': 'Unable to retrieve key part'
+                }), 500
+
+            logger.warning(f"EMERGENCY: Key part successfully retrieved and sent for key '{key_config.key_id}'")
             return jsonify({
             return jsonify({
-                'error': 'Notification system failure',
-                'message': 'Access denied for security reasons'
-            }), 500
+                'success': True,
+                'key_id': key_config.key_id,
+                'key_part': content,
+                'timestamp': time.time(),
+                'notified_backends': successful_backends
+            })
 
 
-        logger.info(f"Notifications sent successfully to: {successful_backends}")
-
-        # Read key file
-        file_success, content = read_file_safely(config.key_file_path)
-
-        if not file_success:
-            logger.error(f"CRITICAL: Failed to read key file: {content}")
+        except Exception as e:
+            logger.error(f"CRITICAL: Unexpected error in key access for '{key_config.key_id}': {str(e)}")
             return jsonify({
             return jsonify({
-                'error': 'File access failure',
-                'message': 'Unable to retrieve key part'
+                'error': 'System error',
+                'message': 'Internal server error'
             }), 500
             }), 500
 
 
-        logger.warning("EMERGENCY: Key part successfully retrieved and sent")
-        return jsonify({
-            'success': True,
-            'key_part': content,
-            'timestamp': time.time(),
-            'notified_backends': successful_backends
-        })
+    return get_key_part
 
 
-    except Exception as e:
-        logger.error(f"CRITICAL: Unexpected error in key access: {str(e)}")
-        return jsonify({
-            'error': 'System error',
-            'message': 'Internal server error'
-        }), 500
 
 
 def health_check():
 def health_check():
-    """Health check endpoint that verifies both health monitoring and key request functionality"""
+    """Health check endpoint that verifies both health monitoring and all key request functionality"""
     logger.info("Health check requested")
     logger.info("Health check requested")
 
 
     try:
     try:
@@ -180,34 +189,63 @@ def health_check():
             "Health Check"
             "Health Check"
         )
         )
 
 
-        # Test key notification system (without triggering emergency alert)
-        key_test_message = "🔧 Emergency access system health verification - key notification test"
-        key_notification_success, key_backends = send_ntfy_notification(
-            config.ntfy_backends_key,
-            key_test_message,
-            "System Health Check"
-        )
-
         # Test dummy file access
         # Test dummy file access
         dummy_file_success, dummy_content = read_file_safely(config.dummy_file_path)
         dummy_file_success, dummy_content = read_file_safely(config.dummy_file_path)
 
 
-        # Test actual key file access (without exposing content)
-        key_file_success, key_content = read_file_safely(config.key_file_path)
+        # Test all key files access (without exposing content)
+        key_files_status = {}
+        all_key_files_ok = True
+        for key_id, key_config in config.keys.items():
+            key_file_success, key_content = read_file_safely(key_config.file_path)
+            key_files_status[key_id] = key_file_success
+            if not key_file_success:
+                all_key_files_ok = False
+
+        # Test all key notification backends
+        key_backends_status = {}
+        all_key_backends_ok = True
+        for key_id, key_config in config.keys.items():
+            try:
+                # Test notification without actually sending
+                backend_test_success = len(key_config.backends) > 0
+                key_backends_status[key_id] = {
+                    'backends': key_config.backends,
+                    'success': backend_test_success
+                }
+                if not backend_test_success:
+                    all_key_backends_ok = False
+            except Exception as e:
+                key_backends_status[key_id] = {
+                    'backends': key_config.backends,
+                    'success': False,
+                    'error': str(e)
+                }
+                all_key_backends_ok = False
 
 
         # Determine overall health status
         # Determine overall health status
-        all_systems_ok = (health_notification_success and key_notification_success and
-                         dummy_file_success and key_file_success)
+        all_systems_ok = (health_notification_success and
+                         dummy_file_success and
+                         all_key_files_ok and
+                         all_key_backends_ok)
 
 
         if not all_systems_ok:
         if not all_systems_ok:
             error_details = []
             error_details = []
             if not health_notification_success:
             if not health_notification_success:
                 error_details.append("health notifications failed")
                 error_details.append("health notifications failed")
-            if not key_notification_success:
-                error_details.append("key notifications failed")
             if not dummy_file_success:
             if not dummy_file_success:
                 error_details.append(f"dummy file access failed: {dummy_content}")
                 error_details.append(f"dummy file access failed: {dummy_content}")
-            if not key_file_success:
-                error_details.append(f"key file access failed: {key_content}")
+
+            # Add key-specific errors
+            for key_id, status in key_files_status.items():
+                if not status:
+                    error_details.append(f"key file access failed for '{key_id}'")
+
+            for key_id, status in key_backends_status.items():
+                if not status['success']:
+                    error_msg = f"key backends failed for '{key_id}'"
+                    if 'error' in status:
+                        error_msg += f": {status['error']}"
+                    error_details.append(error_msg)
 
 
             logger.error(f"Health check failed: {', '.join(error_details)}")
             logger.error(f"Health check failed: {', '.join(error_details)}")
             return jsonify({
             return jsonify({
@@ -215,9 +253,9 @@ def health_check():
                 'message': 'System components failed',
                 'message': 'System components failed',
                 'details': error_details,
                 'details': error_details,
                 'health_notifications': health_notification_success,
                 'health_notifications': health_notification_success,
-                'key_notifications': key_notification_success,
                 'dummy_file_access': dummy_file_success,
                 'dummy_file_access': dummy_file_success,
-                'key_file_access': key_file_success
+                'key_files_status': key_files_status,
+                'key_backends_status': key_backends_status
             }), 500
             }), 500
 
 
         logger.info("Health check completed successfully - all systems operational")
         logger.info("Health check completed successfully - all systems operational")
@@ -225,9 +263,10 @@ def health_check():
             'status': 'ok',
             'status': 'ok',
             'timestamp': time.time(),
             'timestamp': time.time(),
             'health_backends_notified': health_backends,
             'health_backends_notified': health_backends,
-            'key_backends_tested': key_backends,
             'dummy_content_length': len(dummy_content),
             'dummy_content_length': len(dummy_content),
-            'key_file_accessible': True,
+            'keys_accessible': len(key_files_status),
+            'key_files_status': key_files_status,
+            'key_backends_status': key_backends_status,
             'emergency_system_ready': True
             'emergency_system_ready': True
         })
         })
 
 
@@ -239,6 +278,9 @@ def health_check():
             'error': str(e)
             'error': str(e)
         }), 500
         }), 500
 
 
+
+
+
 @app.errorhandler(404)
 @app.errorhandler(404)
 def not_found(error):
 def not_found(error):
     """Handle 404 errors silently for security"""
     """Handle 404 errors silently for security"""
@@ -255,46 +297,67 @@ def validate_setup():
     """Validate system setup before starting"""
     """Validate system setup before starting"""
     logger.info("Validating system setup...")
     logger.info("Validating system setup...")
 
 
-    # Check config files exist
-    if not os.path.exists(config.key_file_path):
-        logger.error(f"Key file not found: {config.key_file_path}")
-        return False
-
+    # Check dummy file exists
     if not os.path.exists(config.dummy_file_path):
     if not os.path.exists(config.dummy_file_path):
         logger.error(f"Dummy file not found: {config.dummy_file_path}")
         logger.error(f"Dummy file not found: {config.dummy_file_path}")
         return False
         return False
 
 
-    # Test file permissions
+    # Test dummy file permissions
     try:
     try:
-        with open(config.key_file_path, 'r') as f:
-            f.read(1)
         with open(config.dummy_file_path, 'r') as f:
         with open(config.dummy_file_path, 'r') as f:
             f.read(1)
             f.read(1)
     except Exception as e:
     except Exception as e:
-        logger.error(f"File permission test failed: {str(e)}")
+        logger.error(f"Dummy file permission test failed: {str(e)}")
         return False
         return False
 
 
+    # Validate all key configurations
+    for key_id, key_config in config.keys.items():
+        logger.info(f"Validating key '{key_id}'...")
+
+        # Check key file exists
+        if not os.path.exists(key_config.file_path):
+            logger.error(f"Key file not found for '{key_id}': {key_config.file_path}")
+            return False
+
+        # Test key file permissions
+        try:
+            with open(key_config.file_path, 'r') as f:
+                f.read(1)
+        except Exception as e:
+            logger.error(f"Key file permission test failed for '{key_id}': {str(e)}")
+            return False
+
+        # Validate backends are configured
+        if not key_config.backends:
+            logger.error(f"No notification backends configured for key '{key_id}'")
+            return False
+
     # Test notification system
     # Test notification system
     logger.info("Testing notification system...")
     logger.info("Testing notification system...")
     try:
     try:
-        key_success, _ = send_ntfy_notification(
-            config.ntfy_backends_key[:1],  # Test only first backend
-            "System startup test - key notifications",
-            "Emergency Access Startup Test"
-        )
+        # Test health notification system
         health_success, _ = send_ntfy_notification(
         health_success, _ = send_ntfy_notification(
             config.ntfy_backends_health[:1],  # Test only first backend
             config.ntfy_backends_health[:1],  # Test only first backend
             "System startup test - health notifications",
             "System startup test - health notifications",
             "Emergency Access Startup Test"
             "Emergency Access Startup Test"
         )
         )
 
 
-        if not key_success:
-            logger.error("Key notification system test failed")
-            return False
-
         if not health_success:
         if not health_success:
             logger.error("Health notification system test failed")
             logger.error("Health notification system test failed")
             return False
             return False
+
+        # Test each key's notification backends
+        for key_id, key_config in config.keys.items():
+            key_success, _ = send_ntfy_notification(
+                key_config.backends[:1],  # Test only first backend
+                f"System startup test - key '{key_id}' notifications",
+                "Emergency Access Startup Test"
+            )
+
+            if not key_success:
+                logger.error(f"Key notification system test failed for '{key_id}'")
+                return False
+
     except Exception as e:
     except Exception as e:
         logger.warning(f"Notification test failed, but continuing: {str(e)}")
         logger.warning(f"Notification test failed, but continuing: {str(e)}")
 
 
@@ -320,13 +383,19 @@ if __name__ == '__main__':
             logger.error("System validation failed, exiting")
             logger.error("System validation failed, exiting")
             sys.exit(1)
             sys.exit(1)
 
 
-        # Add Flask routes with config values
-        app.add_url_rule(config.key_route, 'get_key_part', get_key_part, methods=['GET'])
+        # Add Flask routes dynamically for each key
+        for key_id, key_config in config.keys.items():
+            handler = create_key_handler(key_config)
+            endpoint_name = f'get_key_part_{key_id}'
+            app.add_url_rule(key_config.route, endpoint_name, handler, methods=['GET'])
+            logger.info(f"Registered key route for '{key_id}': {key_config.route}")
+
+        # Add health check route
         app.add_url_rule(config.health_route, 'health_check', health_check, methods=['GET'])
         app.add_url_rule(config.health_route, 'health_check', health_check, methods=['GET'])
 
 
         logger.info(f"Starting emergency access server on {config.server_host}:{config.server_port}")
         logger.info(f"Starting emergency access server on {config.server_host}:{config.server_port}")
-        logger.info(f"Key route: {config.key_route}")
         logger.info(f"Health route: {config.health_route}")
         logger.info(f"Health route: {config.health_route}")
+        logger.info(f"Configured {len(config.keys)} key(s)")
 
 
         # Run the server on local port for Caddy reverse proxy
         # Run the server on local port for Caddy reverse proxy
         app.run(
         app.run(

+ 273 - 403
test.py

@@ -1,437 +1,307 @@
 #!/usr/bin/env python3
 #!/usr/bin/env python3
 
 
-"""
-Test script for Emergency Access Server
-Validates system configuration, file access, notifications, and endpoints
-"""
-
 import os
 import os
 import sys
 import sys
 import json
 import json
-import time
-import yaml
 import tempfile
 import tempfile
-import subprocess
-from typing import Dict, List, Tuple, Any
-from config import Config
-
-class EmergencyAccessTester:
-    def __init__(self, config_path: str = None):
-        """Initialize tester with configuration"""
-        self.config = Config(config_path)
-        self.test_results = []
-        self.server_process = None
-
-    def log_test(self, test_name: str, success: bool, message: str = ""):
-        """Log test result"""
-        status = "PASS" if success else "FAIL"
-        result = {
-            'test': test_name,
-            'status': status,
-            'message': message,
-            'timestamp': time.time()
+import shutil
+from pathlib import Path
+
+# Add the current directory to Python path to import our modules
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+
+from config import Config, KeyConfig
+
+def test_multikey_config():
+    """Test multi-key configuration loading"""
+    print("Testing multi-key configuration...")
+
+    # Create temporary config file
+    config_data = {
+        "server": {
+            "host": "127.0.0.1",
+            "port": 1127
+        },
+        "routes": {
+            "health_route": "/health-check-test"
+        },
+        "files": {
+            "dummy_file": "/tmp/dummy.txt"
+        },
+        "keys": {
+            "backup_key": {
+                "route": "/emergency-key-backup-a7f9d2e1b4c7f3e6d9a2b5c8e1f4d7a0b3c6e9f2a5d8b1c4e7f0a3d6b9c2e5f8a1d4b7c0e3f6d9a2b5c8e1f4a7b0d3",
+                "file": "/tmp/backup-key.txt",
+                "backends": ["test_backend1", "test_backend2"],
+                "message": "Backup key accessed"
+            },
+            "master_key": {
+                "route": "/emergency-key-master-x3k8m9n2p5q7r1t4u6v9w2y5z8b1c4d7e0f3g6h9j2k5l8m1n4o7p0q3r6s9t2u5v8w1x4y7z0a3b6c9d2e5f8",
+                "file": "/tmp/master-key.txt",
+                "backends": ["test_backend1", "test_backend3"],
+                "message": "Master key accessed"
+            }
+        },
+        "notifications": {
+            "health_backends": ["health_backend"],
+            "config_path": "/tmp/ntfy.yml",
+            "health_message": "Health check",
+            "log_level": "WARNING",
+            "send_all_logs": true
         }
         }
-        self.test_results.append(result)
+    }
 
 
-        color = '\033[92m' if success else '\033[91m'  # Green or Red
-        reset = '\033[0m'
-        print(f"{color}[{status}]{reset} {test_name}: {message}")
+    # Write config to temporary file
+    with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+        json.dump(config_data, f)
+        config_file = f.name
 
 
-        return success
+    try:
+        # Load config
+        config = Config(config_file)
+
+        # Test basic properties
+        assert config.server_host == "127.0.0.1"
+        assert config.server_port == 1127
+        assert config.health_route == "/health-check-test"
+
+        # Test keys
+        keys = config.keys
+        assert len(keys) == 2
+        assert "backup_key" in keys
+        assert "master_key" in keys
+
+        # Test backup key
+        backup_key = keys["backup_key"]
+        assert backup_key.route == "/emergency-key-backup-a7f9d2e1b4c7f3e6d9a2b5c8e1f4d7a0b3c6e9f2a5d8b1c4e7f0a3d6b9c2e5f8a1d4b7c0e3f6d9a2b5c8e1f4a7b0d3"
+        assert backup_key.file_path == "/tmp/backup-key.txt"
+        assert backup_key.backends == ["test_backend1", "test_backend2"]
+        assert backup_key.message == "Backup key accessed"
+
+        # Test master key
+        master_key = keys["master_key"]
+        assert master_key.route == "/emergency-key-master-x3k8m9n2p5q7r1t4u6v9w2y5z8b1c4d7e0f3g6h9j2k5l8m1n4o7p0q3r6s9t2u5v8w1x4y7z0a3b6c9d2e5f8"
+        assert master_key.file_path == "/tmp/master-key.txt"
+        assert master_key.backends == ["test_backend1", "test_backend3"]
+        assert master_key.message == "Master key accessed"
+
+        # Test key lookup methods
+        found_key = config.get_key_by_route("/emergency-key-backup-a7f9d2e1b4c7f3e6d9a2b5c8e1f4d7a0b3c6e9f2a5d8b1c4e7f0a3d6b9c2e5f8a1d4b7c0e3f6d9a2b5c8e1f4a7b0d3")
+        assert found_key is not None
+        assert found_key.key_id == "backup_key"
+
+        found_key = config.get_key_by_id("master_key")
+        assert found_key is not None
+        assert found_key.route == "/emergency-key-master-x3k8m9n2p5q7r1t4u6v9w2y5z8b1c4d7e0f3g6h9j2k5l8m1n4o7p0q3r6s9t2u5v8w1x4y7z0a3b6c9d2e5f8"
+
+        print("✅ Multi-key configuration test passed")
+
+    finally:
+        os.unlink(config_file)
+
+def test_invalid_config():
+    """Test invalid configuration handling"""
+    print("Testing invalid configuration handling...")
+
+    # Test config without keys section
+    config_data = {
+        "server": {
+            "host": "127.0.0.1",
+            "port": 1127
+        },
+        "routes": {
+            "health_route": "/health-check-test"
+        },
+        "files": {
+            "dummy_file": "/tmp/dummy.txt"
+        },
+        "notifications": {
+            "health_backends": ["health_backend"],
+            "config_path": "/tmp/ntfy.yml",
+            "health_message": "Health check",
+            "log_level": "WARNING",
+            "send_all_logs": true
+        }
+    }
 
 
-    def test_config_loading(self) -> bool:
-        """Test configuration file loading"""
+    # Write config to temporary file
+    with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+        json.dump(config_data, f)
+        config_file = f.name
+
+    try:
+        # Should raise exception for missing keys
         try:
         try:
-            # Test all required config properties
-            self.config.server_host
-            self.config.server_port
-            self.config.key_route
-            self.config.health_route
-            self.config.key_file_path
-            self.config.dummy_file_path
-            self.config.ntfy_backends_key
-            self.config.ntfy_backends_health
-
-            return self.log_test("Config Loading", True, "All config properties accessible")
+            config = Config(config_file)
+            assert False, "Should have raised exception for missing keys section"
         except Exception as e:
         except Exception as e:
-            return self.log_test("Config Loading", False, f"Error: {str(e)}")
+            assert "No keys configured" in str(e)
 
 
-    def test_file_access(self) -> bool:
-        """Test access to key and dummy files"""
-        success = True
+        print("✅ Invalid configuration test passed")
 
 
-        # Test key file
-        try:
-            if not os.path.exists(self.config.key_file_path):
-                self.log_test("Key File Exists", False, f"File not found: {self.config.key_file_path}")
-                success = False
-            else:
-                with open(self.config.key_file_path, 'r') as f:
-                    content = f.read().strip()
-                if not content:
-                    self.log_test("Key File Content", False, "Key file is empty")
-                    success = False
-                else:
-                    self.log_test("Key File Access", True, f"Key file readable, length: {len(content)}")
-        except Exception as e:
-            self.log_test("Key File Access", False, f"Error: {str(e)}")
-            success = False
+    finally:
+        os.unlink(config_file)
 
 
-        # Test dummy file
-        try:
-            if not os.path.exists(self.config.dummy_file_path):
-                self.log_test("Dummy File Exists", False, f"File not found: {self.config.dummy_file_path}")
-                success = False
-            else:
-                with open(self.config.dummy_file_path, 'r') as f:
-                    content = f.read().strip()
-                if not content:
-                    self.log_test("Dummy File Content", False, "Dummy file is empty")
-                    success = False
-                else:
-                    self.log_test("Dummy File Access", True, f"Dummy file readable, length: {len(content)}")
-        except Exception as e:
-            self.log_test("Dummy File Access", False, f"Error: {str(e)}")
-            success = False
+def test_key_config_validation():
+    """Test key configuration validation"""
+    print("Testing key configuration validation...")
 
 
-        return success
+    # Test missing route
+    try:
+        KeyConfig("test", {"file": "/tmp/test.txt"}, {})
+        assert False, "Should have raised exception for missing route"
+    except Exception as e:
+        assert "Route not configured" in str(e)
 
 
-    def test_ntfy_connectivity(self) -> bool:
-        """Test ntfy backend connectivity"""
-        success = True
+    # Test missing file
+    try:
+        KeyConfig("test", {"route": "/test"}, {})
+        assert False, "Should have raised exception for missing file"
+    except Exception as e:
+        assert "File path not configured" in str(e)
 
 
-        # Test importing ntfy and loading config
-        try:
-            import ntfy
-            self.log_test("ntfy Import", True, "dschep/ntfy package available")
-
-            # Test loading the ntfy config file
-            ntfy_config = ntfy.load_config(self.config.ntfy_config_path)
-            self.log_test("ntfy Config Load", True, f"Config loaded from {self.config.ntfy_config_path}")
-        except ImportError:
-            self.log_test("ntfy Import", False, "dschep/ntfy package not installed")
-            return False
-        except Exception as e:
-            self.log_test("ntfy Config Load", False, f"Failed to load config: {str(e)}")
-            return False
-
-        # Test key notification backends
-        for backend in self.config.ntfy_backends_key:
-            try:
-                # Just verify the backend name is valid (assumes global ntfy config is set up)
-                # We don't actually send notifications during testing
-                if backend and isinstance(backend, str) and len(backend.strip()) > 0:
-                    self.log_test(f"Key Backend: {backend}", True, "Backend name valid (using global ntfy config)")
-                else:
-                    self.log_test(f"Key Backend: {backend}", False, "Invalid backend name")
-                    success = False
-
-            except Exception as e:
-                self.log_test(f"Key Backend: {backend}", False, f"Error: {str(e)}")
-                success = False
-
-        # Test health notification backends
-        for backend in self.config.ntfy_backends_health:
-            try:
-                # Similar validation for health backends
-                if backend and isinstance(backend, str) and len(backend.strip()) > 0:
-                    self.log_test(f"Health Backend: {backend}", True, "Backend name valid (using global ntfy config)")
-                else:
-                    self.log_test(f"Health Backend: {backend}", False, "Invalid backend name")
-                    success = False
-
-            except Exception as e:
-                self.log_test(f"Health Backend: {backend}", False, f"Error: {str(e)}")
-                success = False
-
-        return success
-
-    def start_test_server(self) -> bool:
-        """Start the server for endpoint testing"""
-        try:
-            import subprocess
-            import time
-
-            # Start server in background
-            cmd = [sys.executable, "main.py"]
-            env = os.environ.copy()
-            env['EMERGENCY_CONFIG'] = self.config.config_path
-
-            self.server_process = subprocess.Popen(
-                cmd,
-                stdout=subprocess.PIPE,
-                stderr=subprocess.PIPE,
-                env=env
-            )
-
-            # Wait for server to start
-            time.sleep(3)
-
-            # Check if server is running
-            if self.server_process.poll() is None:
-                return self.log_test("Server Start", True, "Server started successfully")
-            else:
-                stdout, stderr = self.server_process.communicate()
-                error_msg = stderr.decode() if stderr else "Unknown error"
-                return self.log_test("Server Start", False, f"Server failed to start: {error_msg}")
+    # Test missing backends
+    try:
+        KeyConfig("test", {"route": "/test", "file": "/tmp/test.txt"}, {})
+        assert False, "Should have raised exception for missing backends"
+    except Exception as e:
+        assert "No notification backends configured" in str(e)
+
+    # Test valid config
+    key_config = KeyConfig(
+        "test",
+        {
+            "route": "/test",
+            "file": "/tmp/test.txt",
+            "backends": ["backend1"],
+            "message": "Test message"
+        },
+        {}
+    )
+
+    assert key_config.key_id == "test"
+    assert key_config.route == "/test"
+    assert key_config.file_path == "/tmp/test.txt"
+    assert key_config.backends == ["backend1"]
+    assert key_config.message == "Test message"
+
+    print("✅ Key configuration validation test passed")
+
+def test_app_integration():
+    """Test Flask app integration with multiple keys"""
+    print("Testing Flask app integration...")
+
+    # Create test files
+    temp_dir = tempfile.mkdtemp()
+    try:
+        # Create test key files
+        backup_key_file = os.path.join(temp_dir, "backup-key.txt")
+        master_key_file = os.path.join(temp_dir, "master-key.txt")
+        dummy_file = os.path.join(temp_dir, "dummy.txt")
+
+        with open(backup_key_file, 'w') as f:
+            f.write("backup_key_content_123")
+        with open(master_key_file, 'w') as f:
+            f.write("master_key_content_456")
+        with open(dummy_file, 'w') as f:
+            f.write("system_healthy")
+
+        # Create config
+        config_data = {
+            "server": {
+                "host": "127.0.0.1",
+                "port": 1127
+            },
+            "routes": {
+                "health_route": "/health-check-test"
+            },
+            "files": {
+                "dummy_file": dummy_file
+            },
+            "keys": {
+                "backup_key": {
+                    "route": "/emergency-key-backup-a7f9d2e1b4c7f3e6d9a2b5c8e1f4d7a0b3c6e9f2a5d8b1c4e7f0a3d6b9c2e5f8a1d4b7c0e3f6d9a2b5c8e1f4a7b0d3",
+                    "file": backup_key_file,
+                    "backends": ["test_backend1"],
+                    "message": "Backup key accessed"
+                },
+                "master_key": {
+                    "route": "/emergency-key-master-x3k8m9n2p5q7r1t4u6v9w2y5z8b1c4d7e0f3g6h9j2k5l8m1n4o7p0q3r6s9t2u5v8w1x4y7z0a3b6c9d2e5f8",
+                    "file": master_key_file,
+                    "backends": ["test_backend2"],
+                    "message": "Master key accessed"
+                }
+            },
+            "notifications": {
+                "health_backends": ["health_backend"],
+                "config_path": "/tmp/ntfy.yml",
+                "health_message": "Health check",
+                "log_level": "WARNING",
+                "send_all_logs": false
+            }
+        }
+
+        # Write config
+        with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
+            json.dump(config_data, f)
+            config_file = f.name
 
 
-        except Exception as e:
-            return self.log_test("Server Start", False, f"Error: {str(e)}")
-
-    def stop_test_server(self):
-        """Stop the test server"""
-        if self.server_process and self.server_process.poll() is None:
-            self.server_process.terminate()
-            try:
-                self.server_process.wait(timeout=5)
-            except subprocess.TimeoutExpired:
-                self.server_process.kill()
-                self.server_process.wait()
-            self.log_test("Server Stop", True, "Server stopped successfully")
-
-    def test_endpoints(self) -> bool:
-        """Test server endpoints"""
-        success = True
-        base_url = f"http://{self.config.server_host}:{self.config.server_port}"
-
-        # Test health endpoint
         try:
         try:
-            import requests
-            response = requests.get(
-                f"{base_url}{self.config.health_route}",
-                timeout=30
-            )
-
-            if response.status_code == 200:
-                data = response.json()
-                if data.get('status') == 'ok':
-                    self.log_test("Health Endpoint", True, f"Response: {response.status_code}")
-                else:
-                    self.log_test("Health Endpoint", False, f"Invalid response: {data}")
-                    success = False
-            else:
-                self.log_test("Health Endpoint", False, f"HTTP {response.status_code}")
-                success = False
+            # Set environment variable
+            os.environ['EMERGENCY_CONFIG'] = config_file
 
 
-        except Exception as e:
-            self.log_test("Health Endpoint", False, f"Error: {str(e)}")
-            success = False
+            # Import main module (this tests configuration loading)
+            import main
 
 
-        # Test key endpoint
-        try:
-            import requests
-            response = requests.get(
-                f"{base_url}{self.config.key_route}",
-                timeout=30
-            )
-
-            if response.status_code == 200:
-                data = response.json()
-                if data.get('success') and 'key_part' in data:
-                    self.log_test("Key Endpoint", True, f"Key retrieved successfully")
-                else:
-                    self.log_test("Key Endpoint", False, f"Invalid response: {data}")
-                    success = False
-            else:
-                self.log_test("Key Endpoint", False, f"HTTP {response.status_code}")
-                success = False
+            # Test config loading
+            main.config = Config(config_file)
 
 
-        except Exception as e:
-            self.log_test("Key Endpoint", False, f"Error: {str(e)}")
-            success = False
+            # Test file reading
+            success, content = main.read_file_safely(backup_key_file)
+            assert success == True
+            assert content == "backup_key_content_123"
 
 
-        # Test 404 handling
-        try:
-            import requests
-            response = requests.get(f"{base_url}/nonexistent-path", timeout=10)
-            if response.status_code == 404:
-                self.log_test("404 Handling", True, "Correctly returns 404 for invalid paths")
-            else:
-                self.log_test("404 Handling", False, f"Expected 404, got {response.status_code}")
-                success = False
-        except Exception as e:
-            self.log_test("404 Handling", False, f"Error: {str(e)}")
-            success = False
+            success, content = main.read_file_safely(master_key_file)
+            assert success == True
+            assert content == "master_key_content_456"
 
 
-        return success
+            # Test key handler creation
+            backup_key_config = main.config.get_key_by_id("backup_key")
+            handler = main.create_key_handler(backup_key_config)
+            assert handler is not None
 
 
-    def test_fail_safe_behavior(self) -> bool:
-        """Test fail-safe behavior with invalid backends"""
-        success = True
+            print("✅ Flask app integration test passed")
 
 
-        # Create temporary config with invalid backends
-        try:
-            with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
-                invalid_config = {
-                    "server": {
-                        "host": self.config.server_host,
-                        "port": self.config.server_port + 1  # Different port
-                    },
-                    "routes": {
-                        "key_route": "/test-key",
-                        "health_route": "/test-health"
-                    },
-                    "files": {
-                        "key_file": self.config.key_file_path,
-                        "dummy_file": self.config.dummy_file_path
-                    },
-                    "notifications": {
-                        "key_backends": ["invalid_backend_test"],
-                        "health_backends": ["invalid_backend_test"],
-                        "key_message": "Test message",
-                        "health_message": "Test health message"
-                    }
-                }
-                json.dump(invalid_config, f)
-                temp_config_path = f.name
-
-            # Test with invalid config
-            test_config = Config(temp_config_path)
-
-            # Start server with invalid config
-            cmd = [sys.executable, "main.py"]
-            env = os.environ.copy()
-            env['EMERGENCY_CONFIG'] = temp_config_path
-
-            test_process = subprocess.Popen(
-                cmd,
-                stdout=subprocess.PIPE,
-                stderr=subprocess.PIPE,
-                env=env
-            )
-
-            time.sleep(3)
-
-            if test_process.poll() is None:
-                # Try to access endpoints - should fail due to notification failures
-                base_url = f"http://{test_config.server_host}:{test_config.server_port}"
-
-                try:
-                    import requests
-                    response = requests.get(f"{base_url}/test-key", timeout=15)
-                    if response.status_code == 500:
-                        self.log_test("Fail-Safe Key", True, "Correctly blocks access when notifications fail")
-                    else:
-                        self.log_test("Fail-Safe Key", False, f"Expected 500, got {response.status_code}")
-                        success = False
-                except Exception as e:
-                    self.log_test("Fail-Safe Key", False, f"Error testing fail-safe: {str(e)}")
-                    success = False
-
-                # Clean up
-                test_process.terminate()
-                try:
-                    test_process.wait(timeout=5)
-                except subprocess.TimeoutExpired:
-                    test_process.kill()
-            else:
-                self.log_test("Fail-Safe Test", False, "Test server failed to start")
-                success = False
-
-            # Clean up temp file
-            os.unlink(temp_config_path)
+        finally:
+            os.unlink(config_file)
+            if 'EMERGENCY_CONFIG' in os.environ:
+                del os.environ['EMERGENCY_CONFIG']
 
 
-        except Exception as e:
-            self.log_test("Fail-Safe Test", False, f"Error: {str(e)}")
-            success = False
-
-        return success
-
-    def run_all_tests(self) -> bool:
-        """Run all tests and return overall success"""
-        print("=" * 60)
-        print("Emergency Access Server Test Suite")
-        print("=" * 60)
-
-        overall_success = True
-
-        # Configuration tests
-        print("\n--- Configuration Tests ---")
-        overall_success &= self.test_config_loading()
-
-        # File system tests
-        print("\n--- File System Tests ---")
-        overall_success &= self.test_file_access()
-
-        # Network tests
-        print("\n--- Network Tests ---")
-        overall_success &= self.test_ntfy_connectivity()
-
-        # Server tests
-        print("\n--- Server Tests ---")
-        if self.start_test_server():
-            time.sleep(2)  # Give server time to fully start
-            overall_success &= self.test_endpoints()
-            self.stop_test_server()
-        else:
-            overall_success = False
-
-        # Fail-safe tests
-        print("\n--- Fail-Safe Tests ---")
-        overall_success &= self.test_fail_safe_behavior()
-
-        # Print summary
-        self.print_summary()
-
-        return overall_success
-
-    def print_summary(self):
-        """Print test summary"""
-        print("\n" + "=" * 60)
-        print("TEST SUMMARY")
-        print("=" * 60)
-
-        passed = sum(1 for r in self.test_results if r['status'] == 'PASS')
-        failed = sum(1 for r in self.test_results if r['status'] == 'FAIL')
-        total = len(self.test_results)
-
-        print(f"Total Tests: {total}")
-        print(f"Passed: {passed}")
-        print(f"Failed: {failed}")
-
-        if failed > 0:
-            print("\nFAILED TESTS:")
-            for result in self.test_results:
-                if result['status'] == 'FAIL':
-                    print(f"  - {result['test']}: {result['message']}")
-
-        overall_status = "PASS" if failed == 0 else "FAIL"
-        color = '\033[92m' if failed == 0 else '\033[91m'
-        reset = '\033[0m'
-        print(f"\n{color}Overall Status: {overall_status}{reset}")
+    finally:
+        shutil.rmtree(temp_dir)
 
 
 def main():
 def main():
-    """Main function"""
-    import argparse
+    """Run all tests"""
+    print("Running multi-key functionality tests...\n")
 
 
-    parser = argparse.ArgumentParser(description='Test Emergency Access Server')
-    parser.add_argument('--config', help='Configuration file path')
-    parser.add_argument('--quick', action='store_true', help='Run quick tests only (skip server startup)')
+    try:
+        test_multikey_config()
+        test_invalid_config()
+        test_key_config_validation()
+        test_app_integration()
 
 
-    args = parser.parse_args()
+        print("\n🎉 All tests passed! Multi-key functionality is working correctly.")
+        return True
 
 
-    try:
-        tester = EmergencyAccessTester(args.config)
-
-        if args.quick:
-            # Quick tests only
-            success = True
-            success &= tester.test_config_loading()
-            success &= tester.test_file_access()
-            success &= tester.test_ntfy_connectivity()
-            tester.print_summary()
-        else:
-            # Full test suite
-            success = tester.run_all_tests()
-
-        sys.exit(0 if success else 1)
-
-    except KeyboardInterrupt:
-        print("\nTest interrupted by user")
-        sys.exit(1)
+    except AssertionError as e:
+        print(f"\n❌ Test failed: {e}")
+        return False
     except Exception as e:
     except Exception as e:
-        print(f"Test suite failed: {str(e)}")
-        sys.exit(1)
-
-if __name__ == '__main__':
-    main()
+        print(f"\n❌ Unexpected error: {e}")
+        import traceback
+        traceback.print_exc()
+        return False
+
+if __name__ == "__main__":
+    success = main()
+    sys.exit(0 if success else 1)