QR Codes in Rust


Rapid, Fun Development with Crates

/images/2024/05-17/header.jpg

Vatican Museum's Bramante Staircase in Sep. 2017.

Continuing from my previous article, we’re taking a closer look at generating QR codes for OTP apps. The purpose is not to dive into the specifications of QR or the encoding technologies that it employs, such as Reed-Solomon codes for error correction. Instead, we’d like to wrap our heads around the process of capturing QR codes with apps like Authenticator and verifying TOTPs within the app.

Quick Response

I’ve just gathered a few facts about QR codes and listed them below. QR codes were initially invented in 1994 by DENSO WAVE that essentially encode various data, including numbers, letters, kanji, and more, into 2D images composed of cells that machines can read.

Primarily designed for factory use, these codes possess error correction capabilities that enable them to remain readable even if they get dirty from something like oil or are missing as little as 7% or as much as 30%.

Nowadays, these codes are widely used for electronic payments in retail settings. According to Japan’s Ministry of Internal Affairs and Communications in 2021, the usage rate of QR code payments is about 51% of the total in Japan.

There are various types of QR codes, including the small, rectangular rMQR intended for electronic components and the medical field, and SQRC, which requires a special reader equipped with a secret key for scanning.

Implementation

The following code is intended to be used with the actix-web framework, and some parts have been omitted. Rather than the QR code generation process from scratch, it utilizes a pre-existing crate. It also reuses the source code from a previous article.

use {
    base32::{encode, Alphabet::RFC4648},
    percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS},
    qrcode::{
        render::svg,
        {EcLevel, QrCode, Version},
    },
};

const APP_NAME: &str = "APP NAME";
const ALGORITHM: &str = "algorithm=SHA1";
const DIGITS: &str = "digits=6";
const PERIOD: &str = "period=30";

#[derive(Deserialize)]
pub struct QrQuery {
    email: String,
    size: Option<String>,
}

pub async fn get_qr(query: web::Query<QrQuery>) -> HttpResponse {
    let (w, h) = width_height(&query.size);

    /// An email will be passed as a GET parameter to the endpoint.
    let email: &String = &query.email;

    /// Change the secret.
    let secret = "secret".to_string();
    let secret_base32 = encode_as_base32(&secret);
    let in_qr = format!(
        "otpauth://totp/{}:{}?secret={}&{}&{}&{}",
        percent_encode(APP_NAME),
        percent_encode(email),
        secret_base32,
        ALGORITHM,
        DIGITS,
        PERIOD
    );

    let qr: QrCode = qr_code(in_qr);
    let image = build_qr_code(qr, w, h);

    HttpResponse::Ok().content_type("image/svg+xml").body(image)
}

fn percent_encode(input: &str) -> String {
    /// SEE: https://url.spec.whatwg.org/#fragment-percent-encode-set
    const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');

    utf8_percent_encode(input, FRAGMENT).to_string()
}

fn encode_as_base32(string: &String) -> String {
    let encoded = encode(RFC4648 { padding: true }, string.as_bytes());

    encoded
}

fn width_height(size: &Option<String>) -> (u32, u32) {
    // SEE: https://stackoverflow.com/questions/48034119/how-can-i-pattern-match-against-an-optionstring
    match size.as_ref().map(|s| &s[..]) {
        Some("small") => (250, 250),
        Some("medium") => (450, 450),
        Some("large") => (650, 650),
        _ => (450, 450),
    }
}

fn qr_code(qr: String) -> QrCode {
    let qr: &[u8] = qr.as_bytes();

    QrCode::with_version(qr, Version::Normal(10), EcLevel::L).unwrap()
}

fn build_qr_code(qr: QrCode, w: u32, h: u32) -> String {
    qr.render()
        .min_dimensions(w, h)
        .dark_color(svg::Color("#636363"))
        .light_color(svg::Color("#fefefe"))
        .build()
}

Cargo.toml:

[dependencies]
base32 = "0.4.0"
percent-encoding = "2.3"
qrcode = "0.14.0"

Render QR

Go ahead and run cargo run, then navigate to http://localhost:8080/v1/qr?email=dummy@example.com, you can open an SVG as shown below. The details of what this QR code represents will be discussed later.

/images/2024/05-17/qr.png

Now, let’s try scanning this QR code with the Microsoft Authenticator App or any other similar app. You can see for yourself that the TOTP has been added, just as shown below.

/images/2024/05-17/authenticator.jpg

Scheme

What does the QR code we generated contain? For a machine, it’s interpreted as the following string:

otpauth://totp/APP_NAME:EMAIL?secret=SECRET&algorithm=SHA1&digits=6&period=30

The otpauth:// URI scheme, established by Google as the Key Uri Format in 2018, has not been standardized so far. So its behavior varies depending on the app. However, using SHA1, a 6-digit code, and a 30-second lifespan has become the de facto standard.

Now, one last remark. The actix-web is an awesome framework!


Looking for more posts? The Acompany Engineering Blog Hub is finally published. Check out my colleagues’ blogs as well.

2fa  otp  qr  rust 

See also