#!/usr/bin/env python """ Re-encrypt existing plaintext TOTP secrets with AES-256-GCM. Run ONCE after applying migration a1b2c3d4e5f6 and before restarting the app: python scripts/reencrypt_totp_secrets.py Requirements: - TOTP_ENCRYPTION_KEY must be set in .env (64-char hex string) - Run from the project root directory This script is idempotent: it skips users who already have a totp_iv set, so it is safe to re-run if interrupted. """ import os import sys # Allow running from project root sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from dotenv import load_dotenv load_dotenv() from app import create_app, db from app.models.user import User from app.services.auth_service import encrypt_totp_secret app = create_app(os.environ.get('FLASK_ENV', 'production')) with app.app_context(): users = User.query.filter( User.totp_enabled == True, User.totp_secret != None, User.totp_iv == None, # skip already-encrypted rows ).all() if not users: print('No plaintext TOTP secrets found. Nothing to do.') sys.exit(0) print(f'Found {len(users)} user(s) with plaintext TOTP secrets. Re-encrypting...') errors = 0 for user in users: try: plaintext = user.totp_secret ciphertext_b64, iv_b64 = encrypt_totp_secret(plaintext) user.totp_secret = ciphertext_b64 user.totp_iv = iv_b64 print(f' [OK] user_id={user.id} ({user.email})') except Exception as e: print(f' [ERROR] user_id={user.id} ({user.email}): {e}', file=sys.stderr) errors += 1 if errors: db.session.rollback() print(f'\nAborted — {errors} error(s) encountered. No changes committed.', file=sys.stderr) sys.exit(1) db.session.commit() print(f'\nDone. {len(users)} TOTP secret(s) re-encrypted successfully.')