wifiqr/
lib.rs

1/// wifiqr
2/// A crate to transform Wifi credentials into a scannable QR code
3extern crate image;
4extern crate qrcodegen;
5
6mod exporters;
7
8macro_rules! wifi_auth {
9    // Derived from:
10    // https://github.com/zxing/zxing/wiki/Barcode-Contents#wifi-network-config-android
11    //
12    // T: authentication type (WEP, WPA, 'nopass'). Can be ommitted for no password.
13    // S: network SSID
14    // P: wifi password. Can be ommitted if T is 'nopass'
15    // H: Hidden SSID. Optional.
16    (hidden) => {
17        "WIFI:T:{};S:{};P:{};H:{};;"
18    };
19    (nopass) => {
20        "WIFI:T:nopass;S:{};;"
21    };
22    (nopass_hidden) => {
23        "WIFI:T:nopass;S:{};H:{};;"
24    };
25    () => {
26        "WIFI:T:{};S:{};P:{};;"
27    };
28}
29
30#[cfg(test)]
31mod tests {
32    use super::code::Credentials;
33    use super::code::{encode, make_svg, manual_encode};
34    use qrcodegen::{QrCodeEcc, Version};
35
36    /// Basic functionality test
37    #[test]
38    fn test_credentials() {
39        assert_eq!(
40            Credentials::new(Some("test"), Some("password"), Some("WPA2"), false, false)
41                .format()
42                .unwrap(),
43            "WIFI:T:WPA2;S:test;P:password;;"
44        );
45    }
46
47    /// Test credential escaping; per Zxing guidelines on how to format a `WIFI:` string
48    #[test]
49    fn test_credentials_escapes() {
50        assert_eq!(
51            Credentials::new(
52                Some(r###""foo;bar\baz""###),
53                Some("randompassword"),
54                Some("wpa2"),
55                false,
56                false
57            )
58            .format()
59            .unwrap(),
60            r###"WIFI:T:WPA2;S:\"foo\;bar\\baz\";P:randompassword;;"###
61        );
62    }
63
64    /// Exercise the automatic qr encoder against the manual encoder
65    #[test]
66    fn test_qrcodes() {
67        let credentials = Credentials::new(Some("test"), Some("WPA"), Some("test"), false, false);
68
69        assert_eq!(
70            make_svg(&encode(&credentials).unwrap()),
71            make_svg(&manual_encode(
72                &credentials,
73                QrCodeEcc::High,
74                Version::new(2),
75                Version::new(15),
76                None,
77            ))
78        );
79    }
80
81    /// Ensure that the hidden flag is added if requested
82    #[test]
83    fn test_hidden_ssid() {
84        assert_eq!(
85            Credentials::new(
86                Some(r###""foo;bar\baz""###),
87                Some("randompassword"),
88                Some("WPA2"),
89                true,
90                false
91            )
92            .format()
93            .unwrap(),
94            r###"WIFI:T:WPA2;S:\"foo\;bar\\baz\";P:randompassword;H:true;;"###
95        );
96    }
97
98    /// If a ssid isn't hidden, it shouldn't be set in the formatted string
99    #[test]
100    fn test_normal_ssid() {
101        assert_eq!(
102            Credentials::new(
103                Some(r###""foo;bar\baz""###),
104                Some("randompassword"),
105                Some("WPA2"),
106                false,
107                false
108            )
109            .format()
110            .unwrap(),
111            r###"WIFI:T:WPA2;S:\"foo\;bar\\baz\";P:randompassword;;"###
112        );
113    }
114
115    /// Require a password when wpa/wpa2 is requested
116    #[test]
117    fn test_nopassword_with_wpa2() {
118        assert!(
119            Credentials::new(
120                Some(r###""foo;bar\baz""###),
121                Some(""),
122                Some("wpa"),
123                false,
124                false
125            )
126            .format()
127            .is_err(),
128            "wpa2 requires a password"
129        );
130
131        assert!(
132            Credentials::new(
133                Some(r###""foo;bar\baz""###),
134                Some(""),
135                Some("wpa2"),
136                false,
137                false
138            )
139            .format()
140            .is_err(),
141            "wpa2 requires a password"
142        );
143    }
144
145    /// Require a password when using wep
146    #[test]
147    fn test_nopassword_with_wep() {
148        assert!(
149            Credentials::new(
150                Some(r###""foo;bar\baz""###),
151                Some(""),
152                Some("wep"),
153                false,
154                false
155            )
156            .format()
157            .is_err(),
158            "wep requires a password"
159        );
160    }
161
162    #[test]
163    fn test_nopassword_with_nopassword() {
164        assert!(
165            Credentials::new(Some("bane"), Some(""), Some("nopass"), false, false)
166                .format()
167                .is_ok(),
168            "nopass specified with a blank password should work"
169        );
170    }
171
172    /// Test various auth (T) types, like WPA/WPA2
173    #[test]
174    fn test_auth_types() {
175        // wep
176        assert_eq!(
177            Credentials::new(Some("test"), Some("password"), Some("wep"), false, false)
178                .format()
179                .unwrap(),
180            "WIFI:T:WEP;S:test;P:password;;"
181        );
182
183        // wpa
184        assert_eq!(
185            Credentials::new(Some("test"), Some("password"), Some("WPA"), false, false)
186                .format()
187                .unwrap(),
188            "WIFI:T:WPA;S:test;P:password;;"
189        );
190
191        // wpa2 -- note that the wifi string has WPA2 in caps. it seems that iOS devices are sensitive
192        // to the T: parameter being lowercase (and will return 'no usable data found')
193        assert_eq!(
194            Credentials::new(Some("test"), Some("password"), Some("wpa2"), false, false)
195                .format()
196                .unwrap(),
197            "WIFI:T:WPA2;S:test;P:password;;"
198        );
199
200        // wpa3
201        assert_eq!(
202            Credentials::new(Some("test"), Some("password"), Some("wpa3"), false, false)
203                .format()
204                .unwrap(),
205            "WIFI:T:WPA3;S:test;P:password;;"
206        );
207
208        // nopass -- unlike wpa2/wpa3, etc, nopass is accepted by iOS devices uncapitalized
209        assert_eq!(
210            Credentials::new(Some("test"), Some(""), Some("nopass"), false, false)
211                .format()
212                .unwrap(),
213            "WIFI:T:nopass;S:test;;"
214        );
215    }
216
217    #[test]
218    fn test_empty_passwords_with_nopass_encr() {
219        assert!(
220            Credentials::new(
221                Some(r###""foo;bar\baz""###),
222                Some("password"),
223                Some("nopass"),
224                false,
225                false
226            )
227            .format()
228            .is_err(),
229            "nopass cannot be specified with a password"
230        );
231    }
232
233    /// ensure that nopass is set along with an empty password when it is requested by the user
234    #[test]
235    fn test_encr_nopass_with_empty_password() {
236        assert_eq!(
237            Credentials::new(Some("test"), Some(""), Some("nopass"), false, false)
238                .format()
239                .unwrap(),
240            "WIFI:T:nopass;S:test;;"
241        );
242    }
243
244    /// when quote is set, ensure that the result is quoted
245    #[test]
246    fn test_quoted_ssid_password() {
247        assert_eq!(
248            Credentials::new(Some("test"), Some("password"), Some("wpa2"), false, true)
249                .format()
250                .unwrap(),
251            "WIFI:T:WPA2;S:\"test\";P:\"password\";;"
252        );
253    }
254}
255
256/// Wifi QR code generator
257pub mod code {
258    use std::error;
259
260    use image::{ImageBuffer, LumaA};
261    use qrcodegen::{Mask, QrCode, QrCodeEcc, QrSegment};
262
263    use crate::exporters::methods::{
264        make_image as make_image_export, save_image as save_image_export,
265        to_svg_string as to_svg_string_export,
266    };
267
268    #[derive(Debug)]
269    pub struct Credentials {
270        pub ssid: String,
271        pub pass: String,
272        pub encr: String,
273        pub hidden: bool,
274        pub quote: bool,
275    }
276
277    impl Credentials {
278        pub fn new(
279            mut _ssid: Option<&str>,
280            mut _password: Option<&str>,
281            mut _encr: Option<&str>,
282            mut _hidden: bool,
283            mut _quote: bool,
284        ) -> Self {
285            Credentials {
286                ssid: _ssid.unwrap().to_string(),
287                encr: _encr.unwrap().to_string(),
288                pass: _password.unwrap().to_string(),
289                hidden: _hidden,
290                quote: _quote,
291            }
292        }
293
294        /// escape characters as in:
295        /// https://github.com/zxing/zxing/wiki/Barcode-Contents#wifi-network-config-android
296        /// Special characters `\`, `;`, `,` and `:` should be escaped with a backslash
297        fn filter_credentials(&self, field: &str) -> String {
298            // N.B. If performance problems ever crop up, this might be more performant
299            // with regex replace_all
300
301            let mut filtered = field
302                .to_string()
303                .replace('\\', r#"\\"#)
304                .replace('"', r#"\""#)
305                .replace(';', r#"\;"#)
306                .replace(r#"':'"#, r#"\:"#);
307
308            if (filtered == self.ssid || filtered == self.pass) && self.quote {
309                // println!("Adding quotes to SSID/Password -- quote is not set");
310                filtered = format!("\"{}\"", field);
311            }
312
313            filtered
314        }
315
316        /// the encryption field in the Wifi QR code fails on iOS devices if it is
317        /// not provided in an uppercase format. Android devices are case insensitive,
318        /// so the encryption field is passed through as uppercase now.
319        fn filter_encr(&self, field: &str) -> String {
320            if field != "nopass" && !self.encr.is_empty() {
321                return field.to_string().to_uppercase();
322            }
323            field.to_string()
324        }
325
326        /// Call the wifi_auth! macro to generate a qr-string and/or return any errors that
327        /// need to be raised to the caller. Note: format does not enforce an encryption type, it is
328        /// up to the end user to use the right value if one is provided.
329        pub fn format(&self) -> Result<String, FormatError> {
330            // empty password ->
331            //  * is password empty and ssid hidden? => set T:nopass and H:
332            //  * is encryption type empty? => set nopass
333            //  * hidden ssid? => add H:
334            // plain format
335            // unrecoverable errors:
336            // * ssid has no password, but sets a T type
337            // * sets a password, but sets T type to nopass
338            if (self.encr == "nopass" || self.encr.is_empty()) && !self.pass.is_empty() {
339                return Err(FormatError(
340                    "With nopass as the encryption type (or unset encryption type), 
341                    the password field should be empty. (Encryption should probably be set 
342                    to something like wpa2)".to_string(),
343                ));
344            }
345
346            if self.pass.is_empty() {
347                // Error condition: Password is empty, and the T (encr) type is not "nopass" / not empty
348                if self.encr != "nopass" && !self.encr.is_empty() {
349                    return Err(FormatError("The encryption method requested requires a password.".to_string()));
350                }
351
352                if self.encr.is_empty() || self.encr == "nopass" {
353                    if self.hidden {
354                        return Ok(format!(
355                            wifi_auth!(nopass_hidden),
356                            self.filter_credentials(&self.ssid),
357                            &self.hidden,
358                        ));
359                    } else if self.pass.is_empty() {
360                        return Ok(format!(
361                            wifi_auth!(nopass),
362                            self.filter_credentials(&self.ssid),
363                        ));
364                    }
365                }
366            }
367
368            if self.hidden {
369                return Ok(format!(
370                    wifi_auth!(hidden),
371                    self.filter_credentials(&self.filter_encr(&self.encr)),
372                    self.filter_credentials(&self.ssid),
373                    self.filter_credentials(&self.pass),
374                    &self.hidden,
375                ));
376            } else {
377                return Ok(format!(
378                    wifi_auth!(),
379                    self.filter_credentials(&self.filter_encr(&self.encr)),
380                    self.filter_credentials(&self.ssid),
381                    self.filter_credentials(&self.pass)
382                ));
383            }
384        }
385    }
386
387    /// returns a new Credentials struct given Wifi credentials. This data is not validated,
388    /// nor formatted into a QR code string. Call .format() on Credentials to do this.
389    pub fn auth(
390        _ssid: Option<&str>,
391        _password: Option<&str>,
392        _encr: Option<&str>,
393        _hidden: bool,
394        _quote: bool,
395    ) -> Credentials {
396        self::Credentials::new(_ssid, _password, _encr, _hidden, _quote)
397    }
398
399    /// generates a qrcode from a Credentials configuration
400    pub fn encode(config: &Credentials) -> Result<QrCode, Box<dyn error::Error>> {
401        let c = match config.format() {
402            Ok(c) => c,
403            Err(e) => return Err(e.into()),
404        };
405
406        match QrCode::encode_text(&c, QrCodeEcc::High) {
407            Ok(qr) => Ok(qr),
408            Err(e) => Err(e.into()),
409        }
410    }
411
412    /// manual_encode isn't intended for use externally, but exists to compare between the
413    /// automated encoder and this manual_encode version
414    /// https://docs.rs/qrcodegen/latest/src/qrcodegen/lib.rs.html#151
415    pub fn manual_encode(
416        config: &Credentials,
417        error_level: QrCodeEcc,
418        lowest_version: qrcodegen::Version,
419        highest_version: qrcodegen::Version,
420        mask_level: Option<Mask>,
421    ) -> QrCode {
422        let segs: Vec<QrSegment> = QrSegment::make_segments(&config.format().unwrap());
423
424        QrCode::encode_segments_advanced(
425            &segs,
426            error_level,
427            lowest_version,
428            highest_version,
429            mask_level,
430            true,
431        )
432        .unwrap()
433    }
434
435    /// generates a wifi qr code that is printed to a terminal/console for quick scanning
436    /// parameters:
437    /// - qrcode: encoded qrcode
438    /// - quiet_zone: the border size to apply to the QR code (created with ASCII_BL_BLOCK)
439    /// result:
440    /// - this prints a block of text directly to the console
441    pub fn console_qr(qrcode: &QrCode, quiet_zone: i32) {
442        const ASCII_BL_BLOCK: &str = "  ";
443        const ASCII_W_BLOCK: &str = "██";
444
445        let x_zone = quiet_zone;
446        let y_zone = quiet_zone;
447
448        // paint top border -- y axis
449        for _top_border in 0..y_zone {
450            print!("{}", ASCII_BL_BLOCK);
451            println!();
452        }
453
454        for y in 0..qrcode.size() {
455            // paint left border -- x axis
456            for _left_border in 0..x_zone {
457                print!("{}", ASCII_BL_BLOCK);
458            }
459
460            // paint qr
461            for x in 0..qrcode.size() {
462                if qrcode.get_module(x, y) {
463                    print!("{}", ASCII_W_BLOCK);
464                } else {
465                    print!("{}", ASCII_BL_BLOCK);
466                }
467            }
468
469            // paint right border -- x axis
470            for _right_border in 0..x_zone {
471                print!("{}", ASCII_BL_BLOCK);
472            }
473
474            println!();
475        }
476
477        // paint bottom border -- y axis
478        for _bottom_border in 0..y_zone {
479            print!("{}", ASCII_BL_BLOCK);
480            println!();
481        }
482    }
483
484    pub fn make_image(
485        qrcode: &QrCode,
486        scale: i32,
487        border_size: i32,
488    ) -> ImageBuffer<LumaA<u8>, Vec<u8>> {
489        make_image_export(qrcode, scale, border_size)
490    }
491
492    /// generates an svg string from a QrCode (output from the QR library)
493    ///
494    /// * qrcode: &QrCode
495    ///
496    pub fn make_svg(qrcode: &QrCode) -> String {
497        to_svg_string_export(qrcode, 4)
498    }
499
500    /// saves an image to a file
501    ///
502    /// * image: ImageBuffer<>
503    ///
504    /// * save_file: file path to save the image into
505    pub fn save_image(
506        image: &ImageBuffer<LumaA<u8>, Vec<u8>>,
507        save_file: String,
508    ) -> Result<(), image::ImageError> {
509        save_image_export(image, save_file)
510    }
511
512    /// this error is returned when a potentally invalid combination of choices are made in the process
513    /// of building a wifi connection string to embed as a QR code.
514    ///
515    /// a recommendation is returned to the caller as a string to provide corrective action
516    #[derive(Debug, Clone)]
517    pub struct FormatError(String);
518
519    impl std::error::Error for FormatError {
520        fn description(&self) -> &str {
521            &self.0
522        }
523    }
524
525    impl std::fmt::Display for FormatError {
526        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
527            f.write_str(&self.0)
528        }
529    }
530
531}