wifi_qr_code_generator/
lib.rs

1use std::fmt::Debug;
2use std::fmt::Display;
3use std::path::Path;
4
5use arqoii::types::QoiHeader;
6use base64::Engine;
7
8#[cfg(feature = "cli")]
9use clap::{builder::PossibleValue, ValueEnum};
10
11use image::ImageBuffer;
12use image::Luma;
13use qrcode::render::Pixel;
14use qrcode::QrCode;
15
16#[derive(Clone)]
17#[non_exhaustive]
18pub enum ImageFormat {
19    #[non_exhaustive]
20    ImageFormat(image::ImageFormat),
21    #[cfg(feature = "qoi")]
22    #[non_exhaustive]
23    Qoi,
24}
25
26impl Debug for ImageFormat {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        match self {
29            ImageFormat::ImageFormat(format) => write!(f, "{format:?}"),
30            ImageFormat::Qoi => write!(f, "Qoi"),
31        }
32    }
33}
34
35impl Default for ImageFormat {
36    fn default() -> Self {
37        Self::ImageFormat(image::ImageFormat::Png)
38    }
39}
40
41#[cfg(feature = "cli")]
42impl ValueEnum for ImageFormat {
43    fn value_variants<'a>() -> &'a [Self] {
44        &[
45            Self::Qoi,
46            Self::ImageFormat(image::ImageFormat::Png),
47            Self::ImageFormat(image::ImageFormat::Jpeg),
48        ]
49    }
50
51    fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
52        let name = format!("{self:?}").to_lowercase();
53        Some(PossibleValue::new(name))
54    }
55}
56
57impl ImageFormat {
58    pub fn png() -> Self {
59        Self::ImageFormat(image::ImageFormat::Png)
60    }
61
62    #[cfg(feature = "qoi")]
63    pub fn qoi() -> Self {
64        Self::Qoi
65    }
66}
67
68struct Image {
69    buffer: ImageBuffer<Luma<u8>, Vec<u8>>,
70}
71
72impl Image {
73    pub fn save(&self, format: ImageFormat, file_path: &Path) -> Result<(), GenerationError> {
74        match format {
75            ImageFormat::ImageFormat(format) => {
76                self.buffer.save_with_format(file_path, format)?;
77            }
78            ImageFormat::Qoi => {
79                let data = arqoii::encode::QoiEncoder::new(
80                    QoiHeader::new(
81                        self.buffer.width(),
82                        self.buffer.height(),
83                        arqoii::types::QoiChannels::Rgb,
84                        arqoii::types::QoiColorSpace::SRgbWithLinearAlpha,
85                    ),
86                    self.buffer.pixels().map(|px| arqoii::types::Pixel {
87                        r: px.0[0],
88                        g: px.0[0],
89                        b: px.0[0],
90                        a: 255,
91                    }),
92                )
93                .collect::<Vec<_>>();
94                std::fs::write(file_path, data)?;
95            }
96        }
97        Ok(())
98    }
99    pub fn save_guess_format(&self, file_path: &Path) -> Result<(), GenerationError> {
100        if cfg!(feature = "qoi") && file_path.extension().is_some_and(|ext| ext == "qoi") {
101            self.save(ImageFormat::Qoi, file_path)
102        } else {
103            self.buffer.save(file_path)?;
104            Ok(())
105        }
106    }
107}
108
109#[derive(Debug, Clone, Copy)]
110struct Px(Luma<u8>);
111
112struct Canvas(Px, Image);
113
114impl Pixel for Px {
115    type Image = Image;
116
117    type Canvas = Canvas;
118
119    fn default_color(color: qrcode::Color) -> Self {
120        Self(Luma([color.select(0, 255)]))
121    }
122}
123
124impl qrcode::render::Canvas for Canvas {
125    type Pixel = Px;
126
127    type Image = <Px as Pixel>::Image;
128
129    fn new(width: u32, height: u32, dark_pixel: Self::Pixel, light_pixel: Self::Pixel) -> Self {
130        Self(
131            dark_pixel,
132            Image {
133                buffer: ImageBuffer::from_pixel(width, height, light_pixel.0),
134            },
135        )
136    }
137
138    fn draw_dark_pixel(&mut self, x: u32, y: u32) {
139        self.1.buffer.put_pixel(x, y, self.0 .0)
140    }
141
142    fn into_image(self) -> Self::Image {
143        self.1
144    }
145}
146
147#[derive(Debug, thiserror::Error)]
148pub enum GenerationError {
149    #[error("{0}")]
150    QrError(#[from] qrcode::types::QrError),
151    #[error("{0}")]
152    ImageError(#[from] image::error::ImageError),
153    #[error("{0}")]
154    Io(#[from] std::io::Error),
155}
156
157#[derive(Debug, Clone)]
158pub struct Wifi {
159    ssid: String,
160    kind: Option<WifiMethod>,
161    hidden: bool,
162    eap_method: Option<EapMethod>,
163    phase2: Option<Phase2>,
164    anonymous_identity: Option<String>,
165    identity: Option<String>,
166    password: Option<String>,
167    public_key: Option<Vec<u8>>,
168}
169
170impl Wifi {
171    pub fn new(ssid: String) -> Self {
172        Self {
173            ssid,
174            kind: None,
175            hidden: false,
176            eap_method: None,
177            phase2: None,
178            anonymous_identity: None,
179            identity: None,
180            password: None,
181            public_key: None,
182        }
183    }
184
185    pub fn with_method(mut self, wifi_method: Option<WifiMethod>) -> Self {
186        self.kind = wifi_method;
187        self
188    }
189
190    pub fn with_hidden(mut self, hidden: bool) -> Self {
191        self.hidden = hidden;
192        self
193    }
194
195    pub fn with_eap_method(mut self, eap: Option<EapMethod>) -> Self {
196        self.eap_method = eap;
197        self
198    }
199
200    pub fn with_phase2(mut self, ph2: Option<Phase2>) -> Self {
201        self.phase2 = ph2;
202        self
203    }
204
205    pub fn with_anonymous_identity(mut self, anon: Option<String>) -> Self {
206        self.anonymous_identity = anon;
207        self
208    }
209
210    pub fn with_identity(mut self, id: Option<String>) -> Self {
211        self.identity = id;
212        self
213    }
214
215    pub fn with_password(mut self, pw: Option<String>) -> Self {
216        self.password = pw;
217        self
218    }
219
220    pub fn with_public_key(mut self, pk: Option<Vec<u8>>) -> Self {
221        self.public_key = pk;
222        self
223    }
224
225    pub fn generate_image_file(
226        &self,
227        format: Option<ImageFormat>,
228        file_path: &Path,
229    ) -> Result<(), GenerationError> {
230        let code = QrCode::new(self.to_string())?;
231
232        let image = code.render::<Px>().build();
233
234        match format {
235            Some(format) => image.save(format, file_path)?,
236            None => image.save_guess_format(file_path)?,
237        }
238
239        Ok(())
240    }
241
242    fn expected_field_count(&self) -> usize {
243        self.kind.as_ref().map_or(0, |method|if let WifiMethod::Wpa3 = method {
244            2
245        } else {
246            1
247        })
248         + 1 // ssid is required
249            + self.hidden as usize
250            + self.eap_method.is_some() as usize
251            + self.phase2.is_some() as usize
252            + self.anonymous_identity.is_some() as usize
253            + self.identity.is_some() as usize
254            + self.password.is_some() as usize
255            + self.public_key.is_some() as usize
256    }
257
258    fn fields(&self) -> Vec<Field> {
259        let expected_fields = self.expected_field_count();
260
261        let mut fields = Vec::with_capacity(expected_fields);
262
263        if let Some(kind) = &self.kind {
264            kind.add_fields(&mut fields);
265        }
266
267        fields.push(Field::new_string("S", &self.ssid));
268
269        if self.hidden {
270            fields.push(Field::new_string("H", "true"))
271        }
272
273        if let Some(eap) = &self.eap_method {
274            eap.add_fields(&mut fields);
275        }
276
277        if let Some(ph2) = &self.phase2 {
278            ph2.add_fields(&mut fields)
279        }
280
281        if let Some(anon) = &self.anonymous_identity {
282            fields.push(Field::new_string("A", anon));
283        }
284
285        if let Some(ident) = &self.identity {
286            fields.push(Field::new_string("I", ident));
287        }
288
289        if let Some(password) = &self.password {
290            fields.push(Field::new_string("P", password));
291        }
292
293        if let Some(pk) = &self.public_key {
294            fields.push(Field::new_base64("K", pk));
295        }
296
297        fields
298    }
299}
300
301impl ToString for Wifi {
302    fn to_string(&self) -> String {
303        let content: String = self.fields().into_iter().map(|f| f.to_string()).collect();
304        format!("WIFI:{content};")
305    }
306}
307
308pub struct Field {
309    name: String,
310    value: String,
311}
312
313impl Field {
314    fn new_string(name: impl AsRef<str>, value: impl AsRef<str>) -> Self {
315        Self {
316            name: name.as_ref().to_string(),
317            value: Self::escape_field_value(value.as_ref()),
318        }
319    }
320
321    fn new_base64(name: impl AsRef<str>, value: impl AsRef<[u8]>) -> Self {
322        Self {
323            name: name.as_ref().to_string(),
324            value: base64::engine::general_purpose::STANDARD.encode(value),
325        }
326    }
327
328    fn new_hex(name: impl AsRef<str>, value: impl AsRef<[u8]>) -> Self {
329        Self {
330            name: name.as_ref().to_string(),
331            value: value.as_ref().iter().map(|b| format!("{b:x}")).collect(),
332        }
333    }
334
335    fn escape_field_value(value: &str) -> String {
336        // escape \ first so we don't escape the escape sequences
337        let value = value
338            .replace('\\', "\\\\")
339            .replace(';', "\\;")
340            .replace(',', "\\,")
341            .replace('"', "\\\"")
342            .replace(':', "\\:");
343
344        if Self::could_be_ascii_hex(&value) {
345            format!("\"{value}\"")
346        } else {
347            value
348        }
349    }
350
351    fn could_be_ascii_hex(value: &str) -> bool {
352        for c in value.chars() {
353            if !"0123456789abcdef".contains(c) {
354                return false;
355            }
356        }
357        true
358    }
359}
360
361impl Display for Field {
362    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363        write!(f, "{}:{};", self.name, self.value)
364    }
365}
366
367#[derive(Debug, Clone)]
368#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
369#[non_exhaustive]
370pub enum WifiMethod {
371    NoPass,
372    Wep,
373    /// WPA is also used for WPA2 and WPA3
374    Wpa,
375    Wpa2Enterprise,
376    /// Same as WPA, but for devices that support it includes a flag for WPA2/WPA3 transition mode disabled, to prevent downgrade attacks
377    Wpa3,
378}
379
380impl WifiMethod {
381    pub fn add_fields(&self, fields: &mut Vec<Field>) {
382        let kind = match self {
383            WifiMethod::NoPass => "nopass",
384            WifiMethod::Wep => "WEP",
385            WifiMethod::Wpa
386            // https://superuser.com/a/1752085
387            | WifiMethod::Wpa3 => "WPA",
388            WifiMethod::Wpa2Enterprise => "WPA2-EAP",
389        };
390
391        fields.push(Field::new_string("T", kind));
392
393        if let WifiMethod::Wpa3 = self {
394            // https://superuser.com/a/1752085
395            // https://www.wi-fi.org/file/wpa3tm-specification
396            // https://www.wi-fi.org/system/files/WPA3%20Specification%20v3.1.pdf
397            fields.push(Field::new_hex("R", [1]))
398        }
399    }
400}
401
402#[derive(Debug, Clone)]
403#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
404#[non_exhaustive]
405pub enum EapMethod {
406    Peap,
407    Tls,
408    Ttls,
409    Pwd,
410    Sim,
411    Aka,
412    AkaPrime,
413}
414
415impl EapMethod {
416    pub fn add_fields(&self, fields: &mut Vec<Field>) {
417        let eap_name = match self {
418            EapMethod::Peap => "PEAP",
419            EapMethod::Tls => "TLS",
420            EapMethod::Ttls => "TTLS",
421            EapMethod::Pwd => "PWD",
422            EapMethod::Sim => "SIM",
423            EapMethod::Aka => "AKA",
424            EapMethod::AkaPrime => "AKA_PRIME",
425        };
426        fields.push(Field::new_string("E", eap_name));
427    }
428}
429
430#[derive(Debug, Clone)]
431#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
432#[non_exhaustive]
433pub enum Phase2 {
434    MsChap,
435    MsChapV2,
436    Pap,
437    Gtc,
438    Sim,
439    Aka,
440    AkaPrime,
441}
442
443impl Phase2 {
444    pub fn add_fields(&self, fields: &mut Vec<Field>) {
445        let ph2_name = match self {
446            Phase2::MsChap => "MSCHAP",
447            Phase2::MsChapV2 => "MSCHAPV2",
448            Phase2::Gtc => "GTC",
449            Phase2::Sim => "SIM",
450            Phase2::Aka => "AKA",
451            Phase2::AkaPrime => "AKA_PRIME",
452            Phase2::Pap => "PAP",
453        };
454        fields.push(Field::new_string("PH2", ph2_name));
455    }
456}