//! QR Code Generator for TOTP Enrollment //! //! Generates QR codes for TOTP secret enrollment in authenticator apps. use anyhow::Result; use qrcode::{render::svg, QrCode}; use thiserror::Error; /// QR code generation errors #[derive(Debug, Error)] pub enum QrCodeError { #[error("QR code generation failed: {0}")] GenerationFailed(String), #[error("Invalid URI format: {0}")] InvalidUri(String), #[error("Rendering failed: {0}")] RenderingFailed(String), } /// QR code generator for TOTP enrollment pub struct QrCodeGenerator { /// Image size in pixels image_size: u32, /// Error correction level error_correction: qrcode::EcLevel, } impl QrCodeGenerator { /// Create new QR code generator with default settings pub fn new() -> Self { Self { image_size: 256, error_correction: qrcode::EcLevel::M, // Medium error correction } } /// Create QR code generator with custom size pub fn with_size(size: u32) -> Self { Self { image_size: size, error_correction: qrcode::EcLevel::M, } } /// Generate PNG image from otpauth:// URI pub fn generate_png(&self, uri: &str) -> Result> { // Validate URI format if !uri.starts_with("otpauth://") { return Err( QrCodeError::InvalidUri("URI must start with otpauth://".to_string()).into(), ); } // Create QR code let code = QrCode::with_error_correction_level(uri, self.error_correction) .map_err(|e| QrCodeError::GenerationFailed(e.to_string()))?; // Render to PNG let image = code .render::>() .min_dimensions(self.image_size, self.image_size) .build(); // Convert to PNG bytes let mut png_bytes = Vec::new(); image .write_to( &mut std::io::Cursor::new(&mut png_bytes), image::ImageFormat::Png, ) .map_err(|e| QrCodeError::RenderingFailed(e.to_string()))?; Ok(png_bytes) } /// Generate SVG image from otpauth:// URI pub fn generate_svg(&self, uri: &str) -> Result { // Validate URI format if !uri.starts_with("otpauth://") { return Err( QrCodeError::InvalidUri("URI must start with otpauth://".to_string()).into(), ); } // Create QR code let code = QrCode::with_error_correction_level(uri, self.error_correction) .map_err(|e| QrCodeError::GenerationFailed(e.to_string()))?; // Render to SVG let svg = code .render() .min_dimensions(self.image_size, self.image_size) .dark_color(svg::Color("#000000")) .light_color(svg::Color("#ffffff")) .build(); Ok(svg) } /// Generate data URL for inline display (base64 encoded PNG) pub fn generate_data_url(&self, uri: &str) -> Result { let png_bytes = self.generate_png(uri)?; let base64_encoded = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &png_bytes); Ok(format!("data:image/png;base64,{}", base64_encoded)) } /// Set image size pub fn set_size(&mut self, size: u32) { self.image_size = size; } /// Set error correction level pub fn set_error_correction(&mut self, level: qrcode::EcLevel) { self.error_correction = level; } } impl Default for QrCodeGenerator { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_generate_png() { let generator = QrCodeGenerator::new(); let uri = "otpauth://totp/FoxhuntHFT:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=FoxhuntHFT"; let png_bytes = generator.generate_png(uri).unwrap(); // PNG should have magic bytes assert_eq!(&png_bytes[0..8], b"\x89PNG\r\n\x1a\n"); // Should be a valid size assert!(png_bytes.len() > 100); } #[test] fn test_generate_svg() { let generator = QrCodeGenerator::new(); let uri = "otpauth://totp/FoxhuntHFT:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=FoxhuntHFT"; let svg = generator.generate_svg(uri).unwrap(); // SVG should contain proper XML assert!(svg.contains("")); } #[test] fn test_generate_data_url() { let generator = QrCodeGenerator::new(); let uri = "otpauth://totp/FoxhuntHFT:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=FoxhuntHFT"; let data_url = generator.generate_data_url(uri).unwrap(); assert!(data_url.starts_with("data:image/png;base64,")); } #[test] fn test_invalid_uri() { let generator = QrCodeGenerator::new(); let invalid_uri = "https://example.com/invalid"; assert!(generator.generate_png(invalid_uri).is_err()); assert!(generator.generate_svg(invalid_uri).is_err()); } #[test] fn test_custom_size() { let generator = QrCodeGenerator::with_size(512); assert_eq!(generator.image_size, 512); } }