Commit b8011207 authored by Your Name's avatar Your Name

Add ENCRYPTION_KEY management UI in admin dashboard

- Added encryption key configuration section in admin payment settings page
- Encryption key can now be set/viewed in admin UI instead of only env var
- Key is stored in admin_settings table with get/save methods in database.py
- Startup loads key from: 1) database, 2) environment, 3) generates temporary
- Added API endpoints: GET/POST /api/admin/settings/encryption-key
- UI shows key status (database/environment/temporary) and allows generation
- Includes security warnings about not changing key after master keys generated
- Fixes issue where temporary key was regenerated on every restart
parent 51d67426
...@@ -2503,6 +2503,45 @@ class DatabaseManager: ...@@ -2503,6 +2503,45 @@ class DatabaseManager:
return default_gateways return default_gateways
def get_encryption_key(self) -> Optional[str]:
"""Get encryption key from admin_settings table."""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
try:
cursor.execute(f'''
SELECT setting_value
FROM admin_settings
WHERE setting_key = {placeholder}
''', ('encryption_key',))
row = cursor.fetchone()
if row and row[0]:
return row[0]
except Exception as e:
logger.warning(f"Error loading encryption key: {e}")
return None
def save_encryption_key(self, encryption_key: str) -> bool:
"""Save encryption key to admin_settings table."""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
try:
insert_syntax = 'INSERT OR REPLACE' if self.db_type == 'sqlite' else 'REPLACE'
cursor.execute(f'''
{insert_syntax} INTO admin_settings (setting_key, setting_value, updated_at)
VALUES ({placeholder}, {placeholder}, CURRENT_TIMESTAMP)
''', ('encryption_key', encryption_key))
conn.commit()
return True
except Exception as e:
logger.error(f"Error saving encryption key: {e}")
conn.rollback()
return False
def save_payment_gateway_settings(self, settings: Dict) -> bool: def save_payment_gateway_settings(self, settings: Dict) -> bool:
"""Save payment gateway settings to admin_settings table.""" """Save payment gateway settings to admin_settings table."""
with self._get_connection() as conn: with self._get_connection() as conn:
......
...@@ -1160,14 +1160,25 @@ async def startup_event(): ...@@ -1160,14 +1160,25 @@ async def startup_event():
# Initialize payment service # Initialize payment service
global payment_service global payment_service
try: try:
# Generate or load encryption key # Load encryption key from database, fallback to env var, then generate temporary
db_manager = DatabaseRegistry.get_config_database()
encryption_key = db_manager.get_encryption_key()
encryption_key_source = 'database'
if not encryption_key:
encryption_key = os.getenv('ENCRYPTION_KEY') encryption_key = os.getenv('ENCRYPTION_KEY')
encryption_key_source = 'environment'
if not encryption_key: if not encryption_key:
encryption_key = Fernet.generate_key().decode() encryption_key = Fernet.generate_key().decode()
logger.warning("No ENCRYPTION_KEY set, generated temporary key") encryption_key_source = 'temporary'
logger.warning("No ENCRYPTION_KEY set in database or environment, generated temporary key")
else:
logger.info(f"Loaded ENCRYPTION_KEY from {encryption_key_source}")
payment_config = { payment_config = {
'encryption_key': encryption_key, 'encryption_key': encryption_key,
'encryption_key_source': encryption_key_source,
'base_url': os.getenv('BASE_URL', 'http://localhost:17765'), 'base_url': os.getenv('BASE_URL', 'http://localhost:17765'),
'currency_code': 'USD', 'currency_code': 'USD',
'btc_confirmations': 3, 'btc_confirmations': 3,
...@@ -6206,6 +6217,67 @@ async def api_save_payment_gateways(request: Request): ...@@ -6206,6 +6217,67 @@ async def api_save_payment_gateways(request: Request):
logger.error(f"Error saving payment gateway settings: {e}") logger.error(f"Error saving payment gateway settings: {e}")
return JSONResponse({"error": str(e)}, status_code=500) return JSONResponse({"error": str(e)}, status_code=500)
@app.get("/api/admin/settings/encryption-key")
async def api_get_encryption_key_status(request: Request):
"""Get encryption key status - API endpoint"""
auth_check = require_admin(request)
if auth_check:
return auth_check
try:
db = DatabaseRegistry.get_config_database()
encryption_key = db.get_encryption_key()
# Check if key is set in database or environment
env_key = os.getenv('ENCRYPTION_KEY')
if encryption_key:
source = 'database'
is_set = True
elif env_key:
source = 'environment'
is_set = True
else:
source = 'temporary'
is_set = False
return JSONResponse({
"is_set": is_set,
"source": source
})
except Exception as e:
logger.error(f"Error getting encryption key status: {e}")
return JSONResponse({"error": str(e)}, status_code=500)
@app.post("/api/admin/settings/encryption-key")
async def api_save_encryption_key(request: Request):
"""Save encryption key - API endpoint"""
auth_check = require_admin(request)
if auth_check:
return auth_check
try:
body = await request.json()
encryption_key = body.get('encryption_key', '').strip()
if not encryption_key:
return JSONResponse({"success": False, "error": "Encryption key is required"}, status_code=400)
if len(encryption_key) != 44:
return JSONResponse({"success": False, "error": "Encryption key must be 44 characters (base64 encoded)"}, status_code=400)
db = DatabaseRegistry.get_config_database()
success = db.save_encryption_key(encryption_key)
if success:
logger.info("Encryption key saved to database by admin")
return JSONResponse({"success": True, "message": "Encryption key saved successfully. Restart server to apply."})
else:
return JSONResponse({"success": False, "error": "Failed to save encryption key"}, status_code=500)
except Exception as e:
logger.error(f"Error saving encryption key: {e}")
return JSONResponse({"success": False, "error": str(e)}, status_code=500)
# Admin configuration API endpoints # Admin configuration API endpoints
@app.get("/api/admin/config/price-sources") @app.get("/api/admin/config/price-sources")
......
...@@ -9,6 +9,51 @@ ...@@ -9,6 +9,51 @@
<i class="fas fa-cog me-2"></i>Payment System Settings <i class="fas fa-cog me-2"></i>Payment System Settings
</h2> </h2>
<!-- Encryption Key Configuration -->
<div style="background: #16213e; border: 2px solid #e74c3c; border-radius: 8px; padding: 20px; margin-bottom: 20px;">
<h3 style="margin: 0 0 20px 0; color: #e74c3c;">
<i class="fas fa-key me-2"></i>Encryption Key Configuration
</h3>
<div style="margin-bottom: 20px; padding: 15px; background: #1a1a2e; border-radius: 8px; border-left: 4px solid #e74c3c;">
<div style="color: #e74c3c; font-weight: bold; margin-bottom: 5px;">
<i class="fas fa-exclamation-triangle me-2"></i>Critical Security Setting
</div>
<div style="color: #e0e0e0; font-size: 14px;">
The encryption key is used to encrypt HD wallet master keys. Once set, DO NOT change it or you will lose access to all crypto wallets.
If not set, a temporary key is generated on each restart (not recommended for production).
</div>
</div>
<div style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 10px; color: #e0e0e0; font-weight: 500;">
Encryption Key Status
</label>
<div id="encryptionKeyStatus" style="padding: 15px; background: #1a1a2e; border: 1px solid #0f3460; border-radius: 8px; color: #e0e0e0;">
<i class="fas fa-spinner fa-spin"></i> Loading...
</div>
</div>
<div style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 10px; color: #e0e0e0; font-weight: 500;">
Set New Encryption Key (44 characters, base64)
</label>
<input type="text" id="encryptionKey" placeholder="Leave empty to auto-generate" style="width: 100%; padding: 10px; border: 1px solid #0f3460; border-radius: 5px; background: #1a1a2e; color: #e0e0e0; font-family: monospace;">
<div style="margin-top: 10px; color: #888; font-size: 14px;">
<i class="fas fa-info-circle me-2"></i>Leave empty to auto-generate a secure key. Copy and save it securely - you cannot retrieve it later.
</div>
</div>
<div style="display: flex; gap: 10px;">
<button type="button" class="btn" onclick="generateEncryptionKey()" style="background: #9b59b6; color: white;">
<i class="fas fa-random me-2"></i>Generate Random Key
</button>
<button type="button" class="btn" onclick="saveEncryptionKey()" style="background: #e74c3c; color: white;">
<i class="fas fa-save me-2"></i>Save Encryption Key
</button>
</div>
</div>
<!-- System Status --> <!-- System Status -->
<div style="background: #16213e; border: 2px solid #4a9eff; border-radius: 8px; padding: 20px; margin-bottom: 20px;"> <div style="background: #16213e; border: 2px solid #4a9eff; border-radius: 8px; padding: 20px; margin-bottom: 20px;">
<h3 style="margin: 0 0 20px 0; color: #4a9eff;"> <h3 style="margin: 0 0 20px 0; color: #4a9eff;">
...@@ -395,6 +440,73 @@ ...@@ -395,6 +440,73 @@
</div> </div>
<script> <script>
// Load encryption key status
async function loadEncryptionKeyStatus() {
try {
const response = await fetch('{{ url_for(request, "/api/admin/settings/encryption-key") }}');
const data = await response.json();
const statusEl = document.getElementById('encryptionKeyStatus');
if (data.is_set) {
statusEl.innerHTML = '<i class="fas fa-check-circle" style="color: #27ae60;"></i> Encryption key is configured (Source: ' + data.source + ')';
} else {
statusEl.innerHTML = '<i class="fas fa-exclamation-triangle" style="color: #e74c3c;"></i> No encryption key set - using temporary key (will change on restart)';
}
} catch (error) {
console.error('Error loading encryption key status:', error);
document.getElementById('encryptionKeyStatus').innerHTML = '<i class="fas fa-times-circle" style="color: #e74c3c;"></i> Error loading status';
}
}
// Generate random encryption key
function generateEncryptionKey() {
// Generate 32 random bytes and encode as base64 (44 characters)
const array = new Uint8Array(32);
crypto.getRandomValues(array);
const base64 = btoa(String.fromCharCode.apply(null, array));
document.getElementById('encryptionKey').value = base64;
showToast('Random encryption key generated. Save it securely!', 'warning');
}
// Save encryption key
async function saveEncryptionKey() {
const key = document.getElementById('encryptionKey').value.trim();
if (!key) {
showToast('Please enter or generate an encryption key', 'danger');
return;
}
if (key.length !== 44) {
showToast('Encryption key must be 44 characters (base64 encoded)', 'danger');
return;
}
if (!confirm('WARNING: Setting or changing the encryption key will affect all encrypted data. Are you sure?')) {
return;
}
try {
const response = await fetch('{{ url_for(request, "/api/admin/settings/encryption-key") }}', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({encryption_key: key})
});
const result = await response.json();
if (response.ok && result.success) {
showToast('Encryption key saved successfully. Server restart required to take effect.', 'success');
document.getElementById('encryptionKey').value = '';
loadEncryptionKeyStatus();
} else {
showToast(result.error || 'Failed to save encryption key', 'danger');
}
} catch (error) {
showToast('Error saving encryption key: ' + error.message, 'danger');
}
}
// Load system status // Load system status
async function loadSystemStatus() { async function loadSystemStatus() {
try { try {
...@@ -707,6 +819,7 @@ function savePaymentGateways() { ...@@ -707,6 +819,7 @@ function savePaymentGateways() {
// Load data on page load - update to include payment gateways // Load data on page load - update to include payment gateways
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
loadEncryptionKeyStatus();
loadSystemStatus(); loadSystemStatus();
loadConfiguration(); loadConfiguration();
loadPaymentGateways(); loadPaymentGateways();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment