diff --git a/Cargo.toml b/Cargo.toml index c44f914..0e621a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ features = [ "qr", "serde_support" ] [features] default = [] -qr = ["qrcode", "image", "base64"] +qr = ["qrcodegen", "image", "base64"] serde_support = ["serde"] [dependencies] @@ -26,6 +26,6 @@ sha-1 = "~0.10.0" hmac = "~0.12.1" base32 = "~0.4" constant_time_eq = "~0.2.1" -qrcode = { version = "~0.12", optional = true } -image = { version = "~0.23.14", optional = true} +qrcodegen = { version = "~1.8", optional = true } +image = { version = "~0.24.2", optional = true} base64 = { version = "~0.13", optional = true } diff --git a/src/lib.rs b/src/lib.rs index 0476204..ca28739 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,7 +52,7 @@ use serde::{Deserialize, Serialize}; use core::fmt; #[cfg(feature = "qr")] -use {base64, image::Luma, qrcode::QrCode}; +use {base64, image::Luma, qrcodegen}; use hmac::Mac; @@ -206,19 +206,78 @@ impl> TOTP { /// /// # Errors /// - /// This will return an error in case the URL gets too long to encode into a QR code + /// This will return an error in case the URL gets too long to encode into a QR code. + /// This would require the get_url method to generate an url bigger than 2000 characters, + /// Which would be too long for some browsers anyway. /// /// It will also return an error in case it can't encode the qr into a png. This shouldn't happen unless either the qrcode library returns malformed data, or the image library doesn't encode the data correctly #[cfg(feature = "qr")] pub fn get_qr(&self, label: &str, issuer: &str) -> Result> { + use image::ImageEncoder; + let url = self.get_url(label, issuer); - let code = QrCode::new(&url)?; let mut vec = Vec::new(); - let encoder = image::png::PngEncoder::new(&mut vec); - encoder.encode( - code.render::>().build().as_ref(), - ((code.width() + 8) * 8) as u32, - ((code.width() + 8) * 8) as u32, + let qr = qrcodegen::QrCode::encode_text(&url, qrcodegen::QrCodeEcc::Medium)?; + let size = qr.size() as u32; + + // "+ 8 * 8" is here to add padding (the white border around the QRCode) + // As some QRCode readers don't work without padding + let image_size = size * 8 + 8 * 8; + let mut canvas = image::GrayImage::new(image_size, image_size); + + // Draw the border + for x in 0..image_size { + for y in 0..image_size { + if y < 8*4 || y >= image_size - 8*4 { + canvas.put_pixel( + x, + y, + Luma([255]), + ); + continue; + } + if x < 8*4 || x >= image_size - 8*4 { + canvas.put_pixel( + x, + y, + Luma([255]), + ); + } + } + } + + // The QR inside the white border + for x_qr in 0..size { + for y_qr in 0..size { + // The canvas is a grayscale image without alpha. Hence it's only one 8-bits byte longs + // This clever trick to one-line the value was achieved with advanced mathematics + // And deep understanding of Boolean algebra. + let val = !qr.get_module(x_qr as i32, y_qr as i32) as u8 * 255; + + // Multiply coordinates by width of pixels + // And take into account the 8*4 padding on top and left side + let x_start = x_qr * 8 + 8*4; + let y_start = y_qr * 8 + 8*4; + + // Draw a 8-pixels-wide square + for x_img in x_start..x_start + 8 { + for y_img in y_start..y_start + 8 { + canvas.put_pixel( + x_img, + y_img, + Luma([val]), + ); + } + } + } + } + + // Encode the canvas into a PNG + let encoder = image::codecs::png::PngEncoder::new(&mut vec); + encoder.write_image( + &image::ImageBuffer::from(canvas).into_raw(), + image_size, + image_size, image::ColorType::L8, )?; Ok(base64::encode(vec)) @@ -345,7 +404,7 @@ mod tests { let hash_digest = Sha1::digest(qr.as_bytes()); assert_eq!( format!("{:x}", hash_digest).as_str(), - "3abc0127e7a2b1013fb25c97ef14422c1fe9e878" + "f671a5a553227a9565c6132024808123f2c9e5e3" ); } }