soundwave/backend/user/two_factor.py

159 lines
4.6 KiB
Python
Raw Normal View History

"""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(
'<b>⚠️ IMPORTANT:</b> 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