Files
gh-christianlouis-inboxconv…/backend/app/core/security.py
T
2026-02-01 12:44:22 +00:00

114 lines
3.8 KiB
Python

"""
Security utilities for encryption, hashing, and token generation.
"""
import secrets
from datetime import datetime, timedelta
from typing import Optional, Dict, Any
from jose import JWTError, jwt
from passlib.context import CryptContext
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2
import base64
from app.core.config import settings
# Password hashing context
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a password against its hash"""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
"""Generate password hash"""
return pwd_context.hash(password)
def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
"""Create JWT access token"""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire, "type": "access"})
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt
def create_refresh_token(data: Dict[str, Any]) -> str:
"""Create JWT refresh token"""
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
to_encode.update({"exp": expire, "type": "refresh"})
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt
def decode_token(token: str) -> Optional[Dict[str, Any]]:
"""Decode and validate JWT token"""
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
return payload
except JWTError:
return None
def generate_random_token(length: int = 32) -> str:
"""Generate a secure random token"""
return secrets.token_urlsafe(length)
class CredentialEncryption:
"""Handles encryption/decryption of sensitive credentials (POP3/IMAP passwords)"""
def __init__(self, key: Optional[str] = None):
"""
Initialize encryption with a key.
If no key provided, uses the one from settings.
"""
if key is None:
key = settings.ENCRYPTION_KEY
# Derive a proper Fernet key from the provided key
kdf = PBKDF2(
algorithm=hashes.SHA256(),
length=32,
salt=b'pop3_forwarder_salt', # In production, use unique salt per user
iterations=100000,
)
key_bytes = key.encode('utf-8')
derived_key = base64.urlsafe_b64encode(kdf.derive(key_bytes))
self.fernet = Fernet(derived_key)
def encrypt(self, plain_text: str) -> str:
"""Encrypt a string and return base64-encoded ciphertext"""
encrypted = self.fernet.encrypt(plain_text.encode('utf-8'))
return base64.b64encode(encrypted).decode('utf-8')
def decrypt(self, encrypted_text: str) -> str:
"""Decrypt a base64-encoded ciphertext"""
encrypted_bytes = base64.b64decode(encrypted_text.encode('utf-8'))
decrypted = self.fernet.decrypt(encrypted_bytes)
return decrypted.decode('utf-8')
# Global encryption instance
credential_encryptor = CredentialEncryption()
def encrypt_credential(credential: str) -> str:
"""Convenience function to encrypt a credential"""
return credential_encryptor.encrypt(credential)
def decrypt_credential(encrypted_credential: str) -> str:
"""Convenience function to decrypt a credential"""
return credential_encryptor.decrypt(encrypted_credential)