"""2FA utility functions""" import pyotp import qrcode import io import base64 import secrets from reportlab.lib.pagesizes import letter from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.units import inch from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle from reportlab.lib import colors from datetime import datetime def generate_totp_secret(): """Generate a new TOTP secret""" return pyotp.random_base32() def get_totp_uri(secret, username, issuer='SoundWave'): """Generate TOTP URI for QR code""" totp = pyotp.TOTP(secret) return totp.provisioning_uri(name=username, issuer_name=issuer) def generate_qr_code(uri): """Generate QR code image as base64 string""" qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=4, ) qr.add_data(uri) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") # Convert to base64 buffer = io.BytesIO() img.save(buffer, format='PNG') buffer.seek(0) img_base64 = base64.b64encode(buffer.getvalue()).decode() return f"data:image/png;base64,{img_base64}" def verify_totp(secret, token): """Verify a TOTP token""" totp = pyotp.TOTP(secret) return totp.verify(token, valid_window=1) def generate_backup_codes(count=10): """Generate backup codes""" codes = [] for _ in range(count): code = '-'.join([ secrets.token_hex(2).upper(), secrets.token_hex(2).upper(), secrets.token_hex(2).upper() ]) codes.append(code) return codes def generate_backup_codes_pdf(username, codes): """Generate PDF with backup codes""" buffer = io.BytesIO() # Create PDF doc = SimpleDocTemplate(buffer, pagesize=letter) story = [] styles = getSampleStyleSheet() # Custom styles title_style = ParagraphStyle( 'CustomTitle', parent=styles['Heading1'], fontSize=24, textColor=colors.HexColor('#1D3557'), spaceAfter=30, ) subtitle_style = ParagraphStyle( 'CustomSubtitle', parent=styles['Normal'], fontSize=12, textColor=colors.HexColor('#718096'), spaceAfter=20, ) # Title story.append(Paragraph('SoundWave Backup Codes', title_style)) story.append(Paragraph(f'User: {username}', subtitle_style)) story.append(Paragraph(f'Generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}', subtitle_style)) story.append(Spacer(1, 0.3 * inch)) # Warning warning_style = ParagraphStyle( 'Warning', parent=styles['Normal'], fontSize=10, textColor=colors.HexColor('#E53E3E'), spaceAfter=20, leftIndent=20, rightIndent=20, ) story.append(Paragraph( '⚠️ IMPORTANT: Store these codes securely. Each code can only be used once. ' 'If you lose access to your 2FA device, you can use these codes to log in.', warning_style )) story.append(Spacer(1, 0.3 * inch)) # Codes table data = [['#', 'Backup Code']] for i, code in enumerate(codes, 1): data.append([str(i), code]) table = Table(data, colWidths=[0.5 * inch, 3 * inch]) table.setStyle(TableStyle([ ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#4ECDC4')), ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor('#1D3557')), ('ALIGN', (0, 0), (-1, -1), 'LEFT'), ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, 0), 12), ('BOTTOMPADDING', (0, 0), (-1, 0), 12), ('BACKGROUND', (0, 1), (-1, -1), colors.white), ('TEXTCOLOR', (0, 1), (-1, -1), colors.HexColor('#2D3748')), ('FONTNAME', (0, 1), (-1, -1), 'Courier'), ('FONTSIZE', (0, 1), (-1, -1), 11), ('GRID', (0, 0), (-1, -1), 1, colors.HexColor('#E2E8F0')), ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), ('LEFTPADDING', (0, 0), (-1, -1), 10), ('RIGHTPADDING', (0, 0), (-1, -1), 10), ('TOPPADDING', (0, 1), (-1, -1), 8), ('BOTTOMPADDING', (0, 1), (-1, -1), 8), ])) story.append(table) # Footer story.append(Spacer(1, 0.5 * inch)) footer_style = ParagraphStyle( 'Footer', parent=styles['Normal'], fontSize=9, textColor=colors.HexColor('#A0AEC0'), alignment=1, # Center ) story.append(Paragraph('Keep this document in a safe place', footer_style)) # Build PDF doc.build(story) buffer.seek(0) return buffer