From 22553e3299b24bdeca91be100f587365e2d906c7 Mon Sep 17 00:00:00 2001 From: davidk Date: Tue, 30 Jul 2019 17:51:38 -0700 Subject: [PATCH] Add project files --- Cargo.toml | 11 ++ README.md | 51 ++++++++ src/lib.rs | 361 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 169 ++++++++++++++++++++++++ 4 files changed, 592 insertions(+) create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/lib.rs create mode 100644 src/main.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..21a8dfc --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "wifiqr" +version = "0.1.0" +authors = ["davidk "] +edition = "2018" + +[dependencies] +qrcodegen = "1.4.0" +image = "0.21.0" +imageproc = "0.18.0" +clap = "2.32.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..0b88237 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# WifiQr + +This Rust crate encodes Wifi credentials into a QR code. There's a command-line interface for testing and basic use. + +### Get it + +Download a built binary from the releases tab. + +### Usage + + WifiQR 0.01 + davidk + Encode your wi-fi credentials as a scannable QR code + + USAGE: + wifiqr --ssid [ssid] --password [password] --encr [encryption type (default:wpa2)] --imagefile [output_name.png] | --svg | --svgfile [output_name.svg] + + FLAGS: + --hidden Optional: Indicate whether or not the SSID is hidden + --svg Emit the QR code as an SVG (to standard output) + -d, --debug Display some extra debugging output + -a, --ask Ask for password instead of getting it through the command-line + -h, --help Prints help information + -V, --version Prints version information + + OPTIONS: + --ssid Sets the WiFi SSID + --password Sets the WiFi password [default: ] + --encr The WiFi's encryption type (wpa, wpa2, nopass) [default: wpa2] + --scale QR code scaling factor [default: 10] + --quietzone QR code: The size of the quiet zone/border to apply to the final QR code [default: + 2] + --imagefile The name of the file to save to (e.g. --imagefile qr.png). Formats: [png, jpg, bmp] + --svgfile Save the QR code to a file (SVG formatted) + + +#### Building + +This requires a complete Rust toolchain. [Link to installation instructions](https://www.rust-lang.org/tools/install). + +```bash + cargo build --release +``` + +### Information on QR codes as used in WI-FI authentication + +* [Format documentation, from zxing/zxing](https://github.com/zxing/zxing/wiki/Barcode-Contents) + +### Crates used + +* [qrcodegen, via project nayuki](https://docs.rs/crate/qrcodegen/1.4.0) diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..dae9ccb --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,361 @@ +// wifiqr +// things that this crate does not currently support: +// * hexadecimal input for the wifi password +// * direct to console output (give the program your password, etc and pop out a QR code for your +// friends to scan on a console) +extern crate image; +extern crate qrcodegen; + +// * HEX `S` / `P` : its possible that these could be interpreted as hex if ascii. add quotes unles +// unless an option is set to ignore it +macro_rules! wifi_auth { + // Derived from: + // https://github.com/zxing/zxing/wiki/Barcode-Contents#wifi-network-config-android + // + // T: authentication type (WEP, WPA, 'nopass'). Can be ommitted for no password. + // S: network SSID + // P: wifi password. Can be ommitted if T is 'nopass' + // H: Hidden SSID. Optional. + (hidden) => ("WIFI:T:{};S:{};P:{};H:{};;"); + (nopass) => ("WIFI:S:{};;"); + (nopass_hidden) => ("WIFI:S:{};H:{};;"); + () => { + "WIFI:T:{};S:{};P:{};;"; + }; +} + +#[cfg(test)] +mod tests { + use super::code::Credentials; + use super::code::{encode, make_svg, manual_encode}; + use qrcodegen::{QrCodeEcc, Version}; + + // Basic functionality test + #[test] + fn test_credentials() { + assert_eq!( + Credentials::new(Some("test"), Some("password"), Some("wpa2"), false).format().unwrap(), + "WIFI:T:wpa2;S:test;P:password;;" + ); + } + + // Test credential escaping; per Zxing guidelines on how to format a `WIFI:` string + #[test] + fn test_credentials_escapes() { + assert_eq!( + Credentials::new(Some(r###""foo;bar\baz""###), + Some("randompassword"), + Some("wpa2"), + false).format().unwrap(), + r###"WIFI:T:wpa2;S:\"foo\;bar\\baz\";P:randompassword;;"### + ); + } + + // Exercise the automatic qr encoder against the manual encoder + #[test] + fn test_qrcodes() { + let credentials = Credentials::new(Some("test"), Some("WPA"), Some("test"), false); + + assert_eq!( + make_svg(&encode(&credentials).unwrap()), + make_svg(&manual_encode( + &credentials, + QrCodeEcc::High, + Version::new(2), + Version::new(15), + None, + )) + ); + } + + // Ensure that the hidden flag is added if requested + #[test] + fn test_hidden_ssid() { + assert_eq!(Credentials::new(Some(r###""foo;bar\baz""###), + Some("randompassword"), + Some("wpa2"), true).format().unwrap(), + r###"WIFI:T:wpa2;S:\"foo\;bar\\baz\";P:randompassword;H:true;;"###); + } + + // If a ssid isn't hidden, it shouldn't be set in the formatted string + #[test] + fn test_normal_ssid() { + assert_eq!(Credentials::new(Some(r###""foo;bar\baz""###), + Some("randompassword"), + Some("wpa2"), false).format().unwrap(), + r###"WIFI:T:wpa2;S:\"foo\;bar\\baz\";P:randompassword;;"###); + } + + // requier a password when wpa/wpa2 is requested + #[test] + fn test_nopassword_with_wpa2() { + assert!(Credentials::new(Some(r###""foo;bar\baz""###), + Some(""), + Some("wpa"), + false).format().is_err(), "wpa2 requires a password"); + + assert!(Credentials::new(Some(r###""foo;bar\baz""###), + Some(""), + Some("wpa2"), + false).format().is_err(), "wpa2 requires a password"); + } + + // require a password when using wep + #[test] + fn test_nopassword_with_wep() { + assert!(Credentials::new(Some(r###""foo;bar\baz""###), + Some(""), + Some("wep"), + false).format().is_err(), "wep requires a password"); + } + + #[test] + fn test_nopassword_with_nopassword() { + assert!(Credentials::new(Some("bane"), + Some(""), + Some("nopass"), + false).format().is_ok(), "nopass specified with a blank password should work"); + } + + // Test various auth (T) types, like WPA/WPA2 + #[test] + fn test_auth_types() { + // wep + assert_eq!( + Credentials::new(Some("test"), Some("password"), Some("wep"), false).format().unwrap(), + "WIFI:T:wep;S:test;P:password;;" + ); + + // wpa + assert_eq!( + Credentials::new(Some("test"), Some("password"), Some("WPA"), false).format().unwrap(), + "WIFI:T:WPA;S:test;P:password;;" + ); + + // wpa2 + assert_eq!( + Credentials::new(Some("test"), Some("password"), Some("wpa2"), false).format().unwrap(), + "WIFI:T:wpa2;S:test;P:password;;" + ); + + // wpa3 + assert_eq!( + Credentials::new(Some("test"), Some("password"), Some("wpa3"), false).format().unwrap(), + "WIFI:T:wpa3;S:test;P:password;;" + ); + } + + #[test] + fn test_empty_passwords_with_nopass_encr() { + assert!(Credentials::new(Some(r###""foo;bar\baz""###), + Some("password"), + Some("nopass"), + false).format().is_err(), "nopass cannot be specified with a password"); + } + + // ensure that nopass is set along with an empty password when it is requested by the user + #[test] + fn test_encr_nopass_with_empty_password() { + assert_eq!( + Credentials::new(Some("test"), Some(""), Some("nopass"), false).format().unwrap(), + "WIFI:T:nopass;S:test;P:;;" + ); + } + + +} + +pub mod code { + + use image::{ImageBuffer, LumaA}; + use qrcodegen::{DataTooLong, Mask, QrCode, QrCodeEcc, QrSegment}; + + use imageproc::drawing::draw_filled_rect_mut; + use imageproc::rect::Rect; + + #[derive(Debug)] + pub struct Credentials { + pub ssid: String, + pub pass: String, + pub encr: String, + pub hidden: bool, + } + + impl Credentials { + pub fn new( + mut _ssid: Option<&str>, + mut _password: Option<&str>, + mut _encr: Option<&str>, + mut _hidden: bool, + ) -> Self { + return Credentials { + ssid: _ssid.unwrap().to_string(), + encr: _encr.unwrap().to_string(), + pass: _password.unwrap().to_string(), + hidden: _hidden + }; + } + + // escape characters as in: + // https://github.com/zxing/zxing/wiki/Barcode-Contents#wifi-network-config-android + // Special characters `\`, `;`, `,` and `:` should be escaped with a backslash + fn filter_credentials(&self, field: &str) -> String { + // N.B. If performance problems ever crop up, this might be more performant + // with regex replace_all + return field.to_string() + .replace(r#"\"#, r#"\\"#) + .replace(r#"""#, r#"\""#) + .replace(r#";"#, r#"\;"#) + .replace(r#":"#, r#"\:"#); + } + + // Call the wifi_auth! macro to generate a qr-string and/or return any errors that + // need to be raised to the caller. Note: format does not enforce an encryption type, it is + // up to the end user to use the right value if one is provided. + pub fn format(&self) -> Result { + // empty password -> + // * is password empty and ssid hidden? => set T:nopass and H: + // * is encryption type empty? => set nopass + // * hidden ssid? => add H: + // plain format + // unrecoverable errors: + // * ssid has no password, but sets a T type + // * sets a password, but sets T type to nopass + if self.pass.is_empty() { + // Error condition: Password is empty, and the T (encr) type is not "nopass" / not empty + if self.encr != "nopass" && !self.encr.is_empty() { + return Err("The encryption method requested requires a password.") + } + + if self.hidden { + return Ok(format!( + wifi_auth!(nopass_hidden), + self.filter_credentials(&self.ssid), + &self.hidden, + )); + } + + if self.encr.is_empty() { + return Ok(format!( + wifi_auth!(nopass), + self.filter_credentials(&self.ssid), + )) + } + } + + if self.encr == "nopass" || self.encr.is_empty() { + if !self.pass.is_empty() { + return Err("With nopass as the encryption type (or unset encryption type), the password field should be empty. (Encryption should probably be set to something like wpa2)") + } + } + + if self.hidden { + return Ok(format!( + wifi_auth!(hidden), + self.filter_credentials(&self.encr), + self.filter_credentials(&self.ssid), + self.filter_credentials(&self.pass), + &self.hidden, + )) + } else { + return Ok(format!( + wifi_auth!(), + self.filter_credentials(&self.encr), + self.filter_credentials(&self.ssid), + self.filter_credentials(&self.pass) + )) + } + } + + // Transform the QR Wifi connection string into a Vec for use with manual_encode() + pub fn format_vec(&self) -> Vec { + return Credentials::format(&self).unwrap().chars().collect(); + } + } + + // returns a new Credentials struct given Wifi credentials. This data is not validated, + // nor formatted into a QR code string. Use .format() to do this + pub fn auth(_ssid: Option<&str>, _password: Option<&str>, _encr: Option<&str>, _hidden: bool) -> Credentials { + return self::Credentials::new(_ssid, _password, _encr, _hidden); + } + + // generates a qrcode from a Credentials configuration + pub fn encode(config: &Credentials) -> Result { + let q = QrCode::encode_text(&config.format().unwrap(), QrCodeEcc::High)?; + Ok(q) + } + + // manual_encode isn't intended for use externally, but exists to compare between the + // automated encoder and this manual_encode version + // https://docs.rs/qrcodegen/latest/src/qrcodegen/lib.rs.html#151 + pub fn manual_encode(config: &Credentials, error_level: QrCodeEcc, lowest_version: qrcodegen::Version, + highest_version: qrcodegen::Version, mask_level: Option) -> QrCode { + + let wifi: Vec = config.format_vec(); + let segs: Vec = QrSegment::make_segments(&wifi); + + return QrCode::encode_segments_advanced( + &segs, + error_level, + lowest_version, + highest_version, + mask_level, + true, + ).unwrap(); + } + + pub fn make_svg(qrcode: &QrCode) -> String { + return qrcode.to_svg_string(4); + } + + // make_image + // qrcode: Is an encoded qrcode + // scale: The scaling factor to apply to the qrcode + // border_size: How large to make the quiet zone + // This returns an ImageBuffer<> that can be saved using save_image(), or passed on + // for further manipulation by the caller + pub fn make_image(qrcode: &QrCode, scale: i32, border_size: u32) -> ImageBuffer, Vec> { + let new_qr_size = qrcode.size() as i32 * scale; + + // --- Initialize to a white canvas with the alpha layer pre-set --- + let mut image = ImageBuffer::from_pixel( + new_qr_size as u32 + border_size * 2, + new_qr_size as u32 + border_size * 2, + LumaA([255, 255]), + ); + + // --- Draw QR w/scale --- + for y in 0..new_qr_size { + for x in 0..new_qr_size { + if qrcode.get_module(x, y) { + draw_filled_rect_mut( + &mut image, + Rect::at( + (x * scale) + border_size as i32, + (y * scale) + border_size as i32, + ).of_size(scale as u32, scale as u32), + LumaA([0, 255]), + ); + } else { + draw_filled_rect_mut( + &mut image, + Rect::at( + (x * scale) + border_size as i32, + (y * scale) + border_size as i32, + ).of_size(scale as u32, scale as u32), + LumaA([255, 255]), + ); + } + } + } + + return image; + } + + // save_image + // image: ImageBuffer + // save_file: file to save the image into + pub fn save_image(image: &ImageBuffer, Vec>, save_file: String) { + let _ = image.save(save_file).unwrap(); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..78232bf --- /dev/null +++ b/src/main.rs @@ -0,0 +1,169 @@ +extern crate clap; +extern crate wifiqr; + +use std::{fs, io}; +use std::io::Write; +use clap::{App, Arg, ArgGroup}; + +fn main() { + let options = App::new("WifiQR") + .version("0.01") + .about("Encode your wi-fi credentials as a scannable QR code") + .author("davidk") + .usage("wifiqr --ssid (ssid) [ --password (password) | --ask ] --encr [ encryption type (default:wpa2) ] [ --imagefile (output_name.png) | --svg | --svgfile (output_name.svg) ]") + .arg( + Arg::with_name("ssid") + .long("ssid") + .takes_value(true) + .required(true) + .display_order(1) + .help("Sets the WiFi SSID"), + ) + .arg( + Arg::with_name("password") + .long("password") + .takes_value(true) + .default_value("") + .display_order(2) + .help("Sets the WiFi password"), + ) + .arg( + Arg::with_name("encryption") + .long("encr") + .takes_value(true) + .default_value("wpa2") + .display_order(3) + .help("The WiFi's encryption type (wpa, wpa2, nopass)"), + ) + .arg( + Arg::with_name("hidden") + .long("hidden") + .display_order(4) + .takes_value(false) + .help("Optional: Indicate whether or not the SSID is hidden"), + ) + .arg( + Arg::with_name("scale") + .long("scale") + .takes_value(true) + .default_value("10") + .display_order(5) + .help("QR code scaling factor"), + ) + .arg( + Arg::with_name("quiet_zone") + .long("quietzone") + .takes_value(true) + .display_order(6) + .default_value("2") + .help("QR code: The size of the quiet zone/border to apply to the final QR code"), + ) + .arg( + Arg::with_name("image_file") + .long("imagefile") + .takes_value(true) + .display_order(7) + .help("The name of the file to save to (e.g. --imagefile qr.png). Formats: [png, jpg, bmp]"), + ) + .arg( + Arg::with_name("svg") + .long("svg") + .takes_value(false) + .display_order(8) + .help("Emit the QR code as an SVG (to standard output)") + ) + .arg( + Arg::with_name("svg_file") + .long("svgfile") + .takes_value(true) + .display_order(9) + .help("Save the QR code to a file (SVG formatted)") + ) + .group( + ArgGroup::with_name("output types") + .required(true) + .args(&["image_file","svg","svg_file"]) + ) + .arg( + Arg::with_name("debug") + .long("debug") + .short("d") + .takes_value(false) + .display_order(10) + .help("Display some extra debugging output") + ) + .arg( + Arg::with_name("ask") + .long("ask") + .short("a") + .takes_value(false) + .display_order(11) + .help("Ask for password instead of getting it through the command-line") + ) + .get_matches(); + + + let mut password = String::new(); + + if options.is_present("ask") { + print!("Enter password for network `{}` (will echo to screen): ", options.value_of("ssid").unwrap()); + io::stdout().flush().unwrap(); + io::stdin().read_line(&mut password).expect("Failed to read password"); + } else { + password = options.value_of("password").unwrap().to_string(); + } + + let config = wifiqr::code::auth( + options.value_of("ssid"), + Some(&password), + options.value_of("encryption"), + options.is_present("hidden"), + ); + + if options.is_present("debug") { + println!("SSID: {} | PASSWORD: {} | ENCRYPTION: {} | HIDDEN: {}", + options.value_of("ssid").unwrap(), + password, + options.value_of("encryption").unwrap(), + options.is_present("hidden")); + } + + let encoding = wifiqr::code::encode(&config).expect("There was a problem generating the QR code"); + + // Note: avoid turbofish/generic on parse() through upfront declaration + let scale: i32 = options.value_of("scale").unwrap_or("10").parse().unwrap(); + let quiet_zone: u32 = options.value_of("quiet_zone").unwrap_or("10").parse().unwrap(); + let image_file: String = options.value_of("image_file").unwrap_or("qr.png").parse().unwrap(); + + if options.is_present("svg_file") { + + println!("Generating QR code .."); + let file_name = options.value_of("svg_file").unwrap(); + + println!("Writing out to SVG file: {} ..", file_name); + let svg_data = wifiqr::code::make_svg(&encoding); + + fs::write(file_name, svg_data).expect("Unable to write file"); + + } else if options.is_present("image_file") { + + println!("Generating QR code .."); + + println!("Scale {} + Quiet Zone: {} ", quiet_zone, scale); + println!("Writing out to file .."); + + let image = wifiqr::code::make_image(&encoding, scale, quiet_zone); + wifiqr::code::save_image(&image, image_file.to_string()); + + println!("The QR code has been saved to {}", image_file); + + } else if options.is_present("svg") { + + println!("{}", wifiqr::code::make_svg(&encoding)); + + } else { + + println!("Please select an output format. For available formats, re-run with --help"); + + } +}