from django.db import models from django.utils import timezone from labs.utils.database_fields import EncryptedTextField class AdminOTPDevice(models.Model): """ Stores TOTP device configuration for admin sudo authentication. The secret and recovery codes are encrypted at rest using Fernet encryption (AES-128-CBC with HMAC verification). """ user = models.OneToOneField( "labs.User", on_delete=models.CASCADE, related_name="admin_otp_device", ) # TOTP secret key (base32 encoded, ~32 chars), encrypted at rest secret_encrypted = EncryptedTextField() # Whether the device has been confirmed by verifying an OTP code is_confirmed = models.BooleanField(default=False) # JSON array of one-time recovery codes, encrypted at rest # Format: ["CODE1", "CODE2", ...] - each code is a 10-char alphanumeric string recovery_codes_encrypted = EncryptedTextField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) last_used_at = models.DateTimeField(null=True, blank=True) # Timestamp of last successful TOTP verification, used to prevent replay attacks. # A TOTP code is only valid if verified after this timestamp plus the TOTP period. last_totp_verified_at = models.DateTimeField(null=True, blank=True) class Meta: db_table = "admin_otp_device" verbose_name = "Admin OTP Device" verbose_name_plural = "Admin OTP Devices" def __str__(self) -> str: status = "confirmed" if self.is_confirmed else "pending" return f"OTP Device for {self.user.email} ({status})" def mark_used(self) -> None: """Update the last_used_at timestamp.""" self.last_used_at = timezone.now() self.save(update_fields=["last_used_at", "updated_at"])