Skip to content

Commit d4181b0

Browse files
authored
Merge pull request #40 from 8JP8/fix-totp-key-persistence-10011267980899559604
Fix TOTP key invalidation by persisting encryption key
2 parents 757b5ae + 537aba3 commit d4181b0

1 file changed

Lines changed: 73 additions & 13 deletions

File tree

backend/models/user.py

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,50 +20,110 @@ def __init__(self, db):
2020
self.encryption_key = self._get_or_create_encryption_key()
2121

2222
def _get_or_create_encryption_key(self) -> bytes:
23-
"""Get or create encryption key for TOTP secrets. Prioritizes environment variables."""
23+
"""Get or create encryption key for TOTP secrets. Prioritizes environment variables, then Redis, then local file."""
2424
import os
2525
import logging
26+
from cryptography.fernet import Fernet
27+
2628
logger = logging.getLogger(__name__)
2729

30+
# 1. Try Environment Variables
2831
# Prioritize environment variable for Azure and production stability
29-
env_key = os.environ.get('ENCRYPTION_KEY')
32+
# Check multiple potential variable names
33+
env_keys = ['TOTP_ENCRYPTION_KEY', 'TOTP_SECRET', 'ENCRYPTION_KEY']
34+
env_key = None
35+
36+
for var_name in env_keys:
37+
val = os.environ.get(var_name)
38+
if val:
39+
env_key = val
40+
logger.info(f"Found encryption key in environment variable: {var_name}")
41+
break
42+
3043
if env_key:
3144
try:
3245
# Ensure it's a valid Fernet key
33-
from cryptography.fernet import Fernet
3446
Fernet(env_key.encode())
3547
logger.info("Using encryption key from environment variable")
3648
return env_key.encode()
3749
except Exception as e:
38-
logger.error(f"Invalid ENCRYPTION_KEY in environment: {str(e)}")
50+
logger.error(f"Invalid encryption key in environment variable: {str(e)}")
51+
# Fall through to other methods if env key is invalid
52+
53+
# 2. Try Redis (for persistence between deploys without env vars)
54+
redis_client = None
55+
try:
56+
from flask import current_app
57+
if current_app:
58+
redis_client = current_app.config.get('REDIS_CLIENT')
59+
if redis_client:
60+
try:
61+
stored_key = redis_client.get('totp_encryption_key')
62+
if stored_key:
63+
if isinstance(stored_key, bytes):
64+
stored_key = stored_key.decode('utf-8')
65+
66+
Fernet(stored_key.encode())
67+
logger.info("Using encryption key from Redis")
68+
return stored_key.encode()
69+
except Exception as e:
70+
logger.error(f"Invalid encryption key in Redis: {e}")
71+
except Exception as e:
72+
logger.warning(f"Failed to check Redis for encryption key: {e}")
3973

74+
# 3. Try Local File (Legacy/Fallback)
4075
# Use absolute path to ensure key is found regardless of working directory
4176
# Key file should be in the backend root directory (parent of models directory)
4277
current_dir = os.path.dirname(os.path.abspath(__file__))
4378
backend_dir = os.path.dirname(current_dir)
4479
key_file = os.path.join(backend_dir, 'totp_encryption.key')
4580

81+
final_key = None
82+
4683
if os.path.exists(key_file):
4784
with open(key_file, 'rb') as f:
4885
key = f.read()
4986
# Validate key to ensure it's not corrupt
5087
try:
51-
from cryptography.fernet import Fernet
5288
Fernet(key)
53-
return key
89+
final_key = key
90+
logger.info(f"Using encryption key from local file: {key_file}")
5491
except Exception as e:
5592
logger.error(f"Invalid encryption key in {key_file}: {e}")
56-
return key
57-
else:
58-
from cryptography.fernet import Fernet
59-
key = Fernet.generate_key()
93+
94+
# 4. Generate New Key if nothing found
95+
if not final_key:
96+
final_key = Fernet.generate_key()
97+
logger.info("Generated NEW encryption key")
98+
99+
# Save to file
60100
try:
61101
with open(key_file, 'wb') as f:
62-
f.write(key)
63-
logger.info(f"Generated new encryption key at {key_file}")
102+
f.write(final_key)
103+
logger.info(f"Saved new encryption key to {key_file}")
64104
except Exception as e:
65105
logger.error(f"Failed to write encryption key to {key_file}: {e}")
66-
return key
106+
107+
# 5. Persist to Redis if available (to prevent future loss)
108+
if final_key and redis_client:
109+
try:
110+
# Store indefinitely (no TTL) or with very long TTL
111+
redis_client.set('totp_encryption_key', final_key.decode('utf-8'))
112+
logger.info("Persisted encryption key to Redis for future deployments")
113+
except Exception as e:
114+
logger.error(f"Failed to persist encryption key to Redis: {e}")
115+
116+
# 6. Log the key for the user (as requested)
117+
try:
118+
key_str = final_key.decode('utf-8')
119+
logger.warning("="*60)
120+
logger.warning("TOTP ENCRYPTION KEY (SAVE THIS TO AZURE ENV VAR 'TOTP_ENCRYPTION_KEY'):")
121+
logger.warning(f"{key_str}")
122+
logger.warning("="*60)
123+
except:
124+
pass
125+
126+
return final_key
67127

68128
def _encrypt_totp_secret(self, secret: str) -> str:
69129
"""Encrypt TOTP secret before storing."""

0 commit comments

Comments
 (0)