//! Contains a function to generate a qr code ready to be scanned by an authenticator app. //! Tested and working on Google Authenticator and FreeOTP. //! OTP uris generated by this module (which are encoded in the QR itself) are done so //! using [this specification](https://github.com/google/google-authenticator/wiki/Key-Uri-Format). pub use qrcode::EcLevel; use super::ThotpError; use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use qrcode::{self, render::svg::Color, QrCode}; use std::fmt::Write; /// Generates a QR code SVG ready to be scanned by an authenticator app. /// The parameters for OTPs and the secret get stored in the otp_uri /// /// The `width` and `height` parameters are used to adjust the size of the /// generated SVG and if not provided default to 200. /// /// The `ec_level` indicates how much wrong blocks are allowed in the generated QR code. pub fn generate_code_svg( otp_uri: &str, width: Option, height: Option, ec_level: EcLevel, ) -> Result { let width = if let Some(width) = width { width } else { 200 }; let height = if let Some(height) = height { height } else { 200 }; let code = QrCode::with_error_correction_level(otp_uri, ec_level)?; Ok(code .render() .min_dimensions(width, height) .dark_color(Color("#000000")) .light_color(Color("#ffffff")) .build()) } /// Generates an otp uri following [this specification](https://github.com/google/google-authenticator/wiki/Key-Uri-Format) /// /// The `otp_type` must be either be `"totp"` or `"hotp"` or the function will return an error. /// /// The `secret` is what gets appended to the otp uri as described in the spec along with `label` and `issuer`. /// /// The `counter` can be used to set the initial counter value for HOTP passwords. If not provided /// when generating HOTPs it will default to 0, omitted when generating TOTps. pub fn otp_uri( otp_type: &str, secret: &str, label: &str, issuer: &str, counter: Option, ) -> Result { if otp_type != "totp" && otp_type != "hotp" { return Err(ThotpError::InvalidUri(String::from( "Invalid otp type provided, accepted values are \"hotp\" and \"totp\"", ))); } let label = utf8_percent_encode(label, NON_ALPHANUMERIC); let issuer = utf8_percent_encode(issuer, NON_ALPHANUMERIC); let mut uri = format!( "otpauth://{}/{}?secret={}&issuer={}", otp_type, label, secret, issuer ); if otp_type == "hotp" { if let Some(counter) = counter { write!(uri, "&counter={}", counter)?; } else { write!(uri, "&counter=0")?; } } Ok(uri) } #[cfg(feature = "custom")] /// Appends optional parameters to OTP uris. Mutates the string in place. The URI generated by /// this module's `otp_uri` function is usually sufficient, however if you need to fully /// customize your OTP uri you may choose to do so with this function. /// #### Warning! /// **Google authenticator ignores the algorithm, digits and time_step (period) parameters.** pub fn uri_append_params( otp_uri: &mut String, algorithm: Option<&str>, digits: Option, time_step: Option, ) -> Result<(), ThotpError> { if let Some(algorithm) = algorithm { if algorithm != "SHA1" && algorithm != "SHA256" && algorithm != "SHA512" { return Err(ThotpError::InvalidUri(String::from( "Invalid algorithm provided, accepted values are \"SHA1\", \"SHA256\" and \"SHA512\"", ))); } write!(otp_uri, "&algorithm={}", algorithm)?; } if let Some(digits) = digits { if !(6..=10).contains(&digits) { return Err(ThotpError::InvalidDigits); } write!(otp_uri, "&digits={}", digits)?; } if let Some(time_step) = time_step { write!(otp_uri, "&period={}", time_step)?; } Ok(()) } #[cfg(test)] mod tests { use super::super::encoding::{decode, encode}; use super::super::ThotpError; use super::super::{generate_secret, otp, TIME_STEP}; use super::*; use std::time::{SystemTime, UNIX_EPOCH}; #[test] fn uri() -> Result<(), ThotpError> { let mut uri = otp_uri("totp", "super_secret", "biblius", "bedgalopolis", None)?; assert_eq!( uri, "otpauth://totp/biblius?secret=super_secret&issuer=bedgalopolis" ); uri_append_params(&mut uri, Some("SHA256"), Some(7), None)?; assert_eq!(uri, "otpauth://totp/biblius?secret=super_secret&issuer=bedgalopolis&algorithm=SHA256&digits=7"); Ok(()) } // These tests don't assert anything, but are useful for debugging qr codes and codes from // the authenticator #[test] fn qr() -> Result<(), ThotpError> { let secret = generate_secret(160); let secret = &encode(&secret, data_encoding::BASE32); let uri = otp_uri("totp", secret, "biblius", "bedgalopolis", None)?; let _code = generate_code_svg(&uri, Some(400), Some(400), EcLevel::H).unwrap(); // std::fs::write("./temp_secret", secret).unwrap(); // std::fs::write("./qr.html", _code).unwrap(); Ok(()) } // Used to check the current time totp, useful to compare to https://totp.danhersam.com/ #[test] fn totp_now() -> Result<(), ThotpError> { let secret = "6RPFBC2M7HKNEAMQ435XFIEGBGW4ZLN2NT6DPYNVMV7R7REDIBGTXCKIN5S7BSNZTUBPXT6ZILNU6Q5LW6UZGUJZNJMWF55QE67PIZ3KRBWYS35FDKQ6I34XWQOHVR76NEFLZEBBEWGY2UK2KRMGI3BC7AXAV3OBO3J2DXXGVNBLHB5VFCFJF65CZCXUYP5LVAMHJSV4M6ZJ5FD3D5OU66NBE3M2FAXFBKQBHNMWCVZ7NZXAG3DVHMORDMJNDFWT"; let secret = decode(secret, data_encoding::BASE32)?; let nonce = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs() / TIME_STEP as u64; let _totp = otp(&secret, nonce)?; // println!("TOTP: {}", _totp); // let _res = verify_totp("789705", &secret, 0); Ok(()) } #[test] fn hotp_now() -> Result<(), ThotpError> { let secret = "ACTGTGXN6K5SIAWMTDPAUULYEZI2RFA3NFJC27U4EO4PNL6UEMUB3ZOD7BGOIRAFF54RDGBAKAZKTCX2CDRLPQ3GPW42AXVD4SEKLWNTBM56O4EXP7HUBBGKEEUHM4IF"; let secret = decode(secret, data_encoding::BASE32)?; let nonce = 3; let _totp = otp(&secret, nonce)?; // println!("HOTP: {}", _totp); // let _res = verify_totp("789705", &secret, 0); Ok(()) } }