Browse Source

multi-key setup

zehe 3 months ago
parent
commit
e9eb409331
8 changed files with 789 additions and 609 deletions
  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/*
 .idea/*
+config.json

+ 305 - 46
README.md

@@ -1,32 +1,36 @@
 # 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
 
+- **Multiple key support**: Configure multiple decryption keys with individual routes and notification backends
 - **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.)
 - **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
 - **Systemd integration**: Automatic startup and service management
 - **Comprehensive logging**: Detailed audit trail of all operations with live notifications
 
 ## 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
+   - 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
-   - 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
 
 **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**:
    ```bash
    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
    ```
 
@@ -96,7 +100,14 @@ sudo ./install.sh
 
 ### 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
 {
@@ -105,18 +116,45 @@ Edit `/etc/emergency-access/config.json`:
     "port": 1127
   },
   "routes": {
-    "key_route": "/emergency-key-a7f9d2e1",
-    "health_route": "/health-check-b8e3f4a2"
+    "health_route": "/health-check-f8d9e2a1b4c7f3e6d9a2b5c8e1f4d7a0b3c6e9f2a5d8b1c4e7f0a3d6b9c2e5f8"
   },
   "files": {
-    "key_file": "/etc/emergency-access/key-part.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": {
-    "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
@@ -131,18 +169,21 @@ Edit `/etc/emergency-access/config.json`:
 - `port`: Listen port (default: `1127`)
 
 #### 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
-- `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
-- `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
 - `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
 - `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)
@@ -155,11 +196,27 @@ Edit `/etc/emergency-access/config.json`:
 
 ### Key and Dummy Files
 
-1. **Create key file**:
+1. **Create key files for each configured key**:
    ```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**:
@@ -278,15 +335,40 @@ curl http://localhost:1127/health-check-b8e3f4a2
 ### Emergency Key Access
 
 ```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:
 {
   "success": true,
-  "key_part": "YOUR_KEY_PART_HERE",
+  "key_id": "backup_key",
+  "key_part": "YOUR_BACKUP_KEY_PART_HERE",
   "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
 # Regular health monitoring
-curl https://your-domain.com/health-check-b8e3f4a2
+curl https://your-domain.com/health-check-f8d9e2a1b4c7f3e6d9a2b5c8e1f4d7a0b3c6e9f2a5d8b1c4e7f0a3d6b9c2e5f8
 
 # Expected response (all systems operational):
 {
   "status": "ok",
   "timestamp": 1703123456.789,
   "health_backends_notified": ["matrix_health"],
-  "key_backends_tested": ["matrix_sec", "pushover_emergency"],
   "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
 }
 
@@ -311,11 +416,38 @@ curl https://your-domain.com/health-check-b8e3f4a2
 {
   "status": "error",
   "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,
-  "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
 
 ```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
 
 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`
 3. Test notification systems independently
 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 os
 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:
     def __init__(self, config_path: str = None):
@@ -10,6 +26,7 @@ class Config:
 
         self.config_path = config_path
         self.config = self._load_config()
+        self._keys = self._load_keys()
 
     def _load_config(self) -> Dict[str, Any]:
         """Load configuration from file"""
@@ -24,6 +41,21 @@ class Config:
         except Exception as 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
     def server_host(self) -> str:
         return self.config.get('server', {}).get('host', '127.0.0.1')
@@ -32,21 +64,10 @@ class Config:
     def server_port(self) -> int:
         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
     def health_route(self) -> str:
         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
     def dummy_file_path(self) -> str:
         dummy_file = self.config.get('files', {}).get('dummy_file')
@@ -55,11 +76,20 @@ class Config:
         return dummy_file
 
     @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
     def ntfy_backends_health(self) -> List[str]:
@@ -72,7 +102,6 @@ class Config:
     def ntfy_config_path(self) -> str:
         return self.config.get('notifications', {}).get('config_path', '/etc/emergency-access/ntfy.yml')
 
-
     @property
     def log_level(self) -> str:
         return self.config.get('notifications', {}).get('log_level', 'WARNING')
@@ -81,12 +110,6 @@ class Config:
     def send_all_logs(self) -> bool:
         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
     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
     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_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
         print_warning "Configuration file already exists, skipping copy"
     fi
@@ -221,10 +221,11 @@ print_final_instructions() {
     print_status "Installation complete!"
     echo
     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 "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
     print_status "Service management commands:"
     echo "  Start service:    sudo systemctl start emergency-access"
@@ -236,7 +237,10 @@ print_final_instructions() {
     print_status "Configuration files:"
     echo "  Service config:   $CONFIG_DIR/config.json"
     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 "  Log file:         $LOG_FILE"
     echo

+ 152 - 83
main.py

@@ -53,7 +53,7 @@ logger = logging.getLogger(__name__)
 
 app = Flask(__name__)
 
-# Global config instance
+# Global config instance will be initialized in main
 config = None
 
 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:
         try:
             # 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
             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)}")
         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({
-                '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({
-                'error': 'File access failure',
-                'message': 'Unable to retrieve key part'
+                'error': 'System error',
+                'message': 'Internal server error'
             }), 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():
-    """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")
 
     try:
@@ -180,34 +189,63 @@ def 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
         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
-        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:
             error_details = []
             if not health_notification_success:
                 error_details.append("health notifications failed")
-            if not key_notification_success:
-                error_details.append("key notifications failed")
             if not dummy_file_success:
                 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)}")
             return jsonify({
@@ -215,9 +253,9 @@ def health_check():
                 'message': 'System components failed',
                 'details': error_details,
                 'health_notifications': health_notification_success,
-                'key_notifications': key_notification_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
 
         logger.info("Health check completed successfully - all systems operational")
@@ -225,9 +263,10 @@ def health_check():
             'status': 'ok',
             'timestamp': time.time(),
             'health_backends_notified': health_backends,
-            'key_backends_tested': key_backends,
             '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
         })
 
@@ -239,6 +278,9 @@ def health_check():
             'error': str(e)
         }), 500
 
+
+
+
 @app.errorhandler(404)
 def not_found(error):
     """Handle 404 errors silently for security"""
@@ -255,46 +297,67 @@ def validate_setup():
     """Validate system setup before starting"""
     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):
         logger.error(f"Dummy file not found: {config.dummy_file_path}")
         return False
 
-    # Test file permissions
+    # Test dummy file permissions
     try:
-        with open(config.key_file_path, 'r') as f:
-            f.read(1)
         with open(config.dummy_file_path, 'r') as f:
             f.read(1)
     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
 
+    # 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
     logger.info("Testing notification system...")
     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(
             config.ntfy_backends_health[:1],  # Test only first backend
             "System startup test - health notifications",
             "Emergency Access Startup Test"
         )
 
-        if not key_success:
-            logger.error("Key notification system test failed")
-            return False
-
         if not health_success:
             logger.error("Health notification system test failed")
             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:
         logger.warning(f"Notification test failed, but continuing: {str(e)}")
 
@@ -320,13 +383,19 @@ if __name__ == '__main__':
             logger.error("System validation failed, exiting")
             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'])
 
         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"Configured {len(config.keys)} key(s)")
 
         # Run the server on local port for Caddy reverse proxy
         app.run(

+ 273 - 403
test.py

@@ -1,437 +1,307 @@
 #!/usr/bin/env python3
 
-"""
-Test script for Emergency Access Server
-Validates system configuration, file access, notifications, and endpoints
-"""
-
 import os
 import sys
 import json
-import time
-import yaml
 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:
-            # 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:
-            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:
-            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():
-    """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:
-        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)