google_authenticator/
authenticator.rs

1// MIT License
2//
3// Copyright (c) 2018 hanskorg
4//
5// Permission is hereby granted, free of charge, to any person obtaining a copy
6// of this software and associated documentation files (the "Software"), to deal
7// in the Software without restriction, including without limitation the rights
8// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9// copies of the Software, and to permit persons to whom the Software is
10// furnished to do so, subject to the following conditions:
11//
12// The above copyright notice and this permission notice shall be included in all
13// copies or substantial portions of the Software.
14//
15// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21// SOFTWARE.
22
23use hmacsha1::hmac_sha1;
24use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
25use std::time::{SystemTime, UNIX_EPOCH};
26use std::{error, fmt, result};
27
28#[cfg(feature = "with-qrcode")]
29use qrcode::render::svg;
30#[cfg(feature = "with-qrcode")]
31use qrcode::{EcLevel, QrCode};
32
33#[cfg(any(feature = "with-qrcode"))]
34use qrcode::types::QrError;
35/// cbindgen:ignore
36const SECRET_MAX_LEN: usize = 128;
37/// cbindgen:ignore
38const SECRET_MIN_LEN: usize = 16;
39
40/// Controls the amount of fault tolerance that the QR code should accept. Require the feature
41/// flag `with-qrcode`.
42// This is a new enum to use in our public interface instead of rqcode::EcLevel.
43#[derive(Copy, Clone)]
44#[repr(C)]
45pub enum ErrorCorrectionLevel {
46    /// 7% of data bytes can be restored.
47    Low,
48    /// 15% of data bytes can be restored.
49    Medium,
50    /// 25% of data bytes can be restored.
51    Quartile,
52    /// 30% of data bytes can be restored.
53    High,
54}
55
56use self::ErrorCorrectionLevel::*;
57
58impl fmt::Display for ErrorCorrectionLevel {
59    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
60        let result = match self {
61            Low => 'L',
62            Medium => 'M',
63            Quartile => 'Q',
64            High => 'H',
65        };
66        write!(f, "{}", result)
67    }
68}
69
70#[cfg(feature = "with-qrcode")]
71impl From<ErrorCorrectionLevel> for qrcode::EcLevel {
72    fn from(level: ErrorCorrectionLevel) -> Self {
73        match level {
74            ErrorCorrectionLevel::High => EcLevel::H,
75            ErrorCorrectionLevel::Medium => EcLevel::M,
76            ErrorCorrectionLevel::Quartile => EcLevel::Q,
77            ErrorCorrectionLevel::Low => EcLevel::L,
78        }
79    }
80}
81
82/// cbindgen:ignore
83/// A list of all usable characters in base32.
84const ALPHABET: [char; 33] = [
85    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
86    'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '2', '3', '4', '5', '6', '7', '=',
87];
88
89/// The main interface of this library. It exports several function that are necessary to interface
90/// with google authenticator.
91pub struct GoogleAuthenticator {
92    code_len: usize,
93}
94
95impl Default for GoogleAuthenticator {
96    fn default() -> Self {
97        Self { code_len: 6 }
98    }
99}
100
101impl GoogleAuthenticator {
102    /// Create a new `GoogleAuthenticator` using the default implementation. This means that the
103    /// codes generated have a length of 6 and the secret will be chosen from allowed base32
104    /// characters.
105    ///
106    /// ### Example
107    /// ```
108    /// use google_authenticator::GoogleAuthenticator;
109    ///
110    /// let auth = GoogleAuthenticator::new();
111    /// ```
112    pub fn new() -> Self {
113        Self::default()
114    }
115
116    /// Use this method to configure the length of the generated code.
117    ///
118    /// ### Example
119    /// ```rust
120    /// use google_authenticator::GoogleAuthenticator;
121    ///
122    /// let auth = GoogleAuthenticator::new()
123    ///     .with_code_length(8);
124    /// ```
125    pub fn with_code_length(mut self, code_length: usize) -> Self {
126        self.code_len = code_length;
127        self
128    }
129
130    /// Create new secret.
131    ///
132    /// Example:
133    /// ```rust
134    /// use google_authenticator::GoogleAuthenticator;
135    ///
136    /// let google_authenticator = GoogleAuthenticator::new();
137    /// google_authenticator.create_secret(32);
138    /// ```
139    pub fn create_secret(&self, length: u8) -> String {
140        let mut secret = Vec::<char>::new();
141        let mut index: usize;
142        for _ in 0..length {
143            index = (rand::random::<u8>() & 0x1F) as usize;
144            secret.push(ALPHABET[index]);
145        }
146        secret.into_iter().collect()
147    }
148
149    /// Calculate the code, with given secret and point in time. The `secret` parameter is the
150    /// secret configured for this user. The `times_slice` parameter is the unix timestamp divided
151    /// by 30 at which the code should expire.
152    ///
153    /// ### Example
154    /// ```rust
155    /// use google_authenticator::GoogleAuthenticator;
156    ///
157    /// let authenticator = GoogleAuthenticator::new();
158    /// authenticator.get_code("I3VFM3JKMNDJCDH5BMBEEQAW6KJ6NOE3", 1523610659 / 30).unwrap();
159    /// ```
160    pub fn get_code(&self, secret: &str, times_slice: u64) -> Result<String> {
161        if secret.len() < SECRET_MIN_LEN || secret.len() > SECRET_MAX_LEN {
162            return Err(GAError::Error(
163                "bad secret length. must be less than 128 and more than 16, recommend 32",
164            ));
165        }
166
167        let message = if times_slice == 0 {
168            SystemTime::now()
169                .duration_since(UNIX_EPOCH)
170                .unwrap()
171                .as_secs()
172                / 30
173        } else {
174            times_slice
175        };
176        let key = Self::base32_decode(secret)?;
177        let msg_bytes = message.to_be_bytes();
178        let hash = hmac_sha1(&key, &msg_bytes);
179        let offset = hash[hash.len() - 1] & 0x0F;
180        let mut truncated_hash: [u8; 4] = Default::default();
181        truncated_hash.copy_from_slice(&hash[offset as usize..(offset + 4) as usize]);
182        let mut code = i32::from_be_bytes(truncated_hash);
183        code &= 0x7FFFFFFF;
184        code %= 1_000_000;
185        let mut code_str = code.to_string();
186        for i in 0..(self.code_len - code_str.len()) {
187            code_str.insert(i, '0');
188        }
189        Ok(code_str)
190    }
191
192    /// This function verifies that a provided code is correct. The parameter `secret` is used to
193    /// verify the user. `code` is the code that will be verified. The parameter `discrepancy`
194    /// indicates number of seconds ago that a code may be generated. `time_slice` is used to modify
195    /// what the current time is, as a unix timestamp. If 0 is provided here, the current time will
196    /// be used.
197    ///
198    /// ### Example
199    /// ```rust
200    /// use google_authenticator::GoogleAuthenticator;
201    ///
202    /// let authenticator = GoogleAuthenticator::new();
203    /// authenticator.verify_code("I3VFM3JKMNDJCDH5BMBEEQAW6KJ6NOE3", "224124", 3, 1523610659 / 30);
204    /// ```
205    pub fn verify_code(&self, secret: &str, code: &str, discrepancy: u64, time_slice: u64) -> bool {
206        if code.len() != self.code_len {
207            return false;
208        }
209        let curr_time_slice = if time_slice == 0 {
210            SystemTime::now()
211                .duration_since(UNIX_EPOCH)
212                .unwrap()
213                .as_secs()
214                / 30
215        } else {
216            time_slice
217        };
218        let start_time = curr_time_slice.saturating_sub(discrepancy);
219        let end_time = curr_time_slice.saturating_add(discrepancy + 1);
220        for _time_slice in start_time..end_time {
221            if let Ok(c) = self.get_code(secret, _time_slice) {
222                if code == c {
223                    return true;
224                }
225            }
226        }
227        false
228    }
229
230    /// Get QR-Code URL for image, from google charts. For the height and width, if a value of 0 is
231    /// provided, the default of `200px` is used. Level is the amount of fault tolerance that the
232    /// QR code should accept, see
233    /// [this page](https://en.wikipedia.org/wiki/QR_code#Error_correction) for more information.
234    ///
235    /// ### Example
236    /// ```rust
237    /// use google_authenticator::{GoogleAuthenticator, ErrorCorrectionLevel};
238    ///
239    /// let authenticator = GoogleAuthenticator::new();
240    /// authenticator.qr_code_url(
241    ///     "I3VFM3JKMNDJCDH5BMBEEQAW6KJ6NOE3",
242    ///     "your company name",
243    ///     "hello",
244    ///     0,
245    ///     0,
246    ///     ErrorCorrectionLevel::Medium,
247    /// );
248    /// ```
249    pub fn qr_code_url(
250        &self,
251        secret: &str,
252        name: &str,
253        title: &str,
254        width: u32,
255        height: u32,
256        level: ErrorCorrectionLevel,
257    ) -> String {
258        let width = if width == 0 { 200 } else { width };
259        let height = if height == 0 { 200 } else { height };
260
261        // Scheme will be a url to the totp we will use. Since it is a query parameter of the final
262        // url, it must be percent encoded. In turn, this means that `name` and `title` must be
263        // percent encoded twice for the final url to work if they contain characters that are not
264        // allowed in urls.
265        let scheme = Self::create_scheme(name, secret, title);
266        let scheme = utf8_percent_encode(&scheme, NON_ALPHANUMERIC);
267        format!(
268            "https://chart.googleapis.com/chart?chs={}x{}&chld={}|0&cht=qr&chl={}",
269            width, height, level, scheme
270        )
271    }
272
273    /// Creates an in-memory SVG file that can be used to perform 2fa with Google Authenticator. The
274    /// `height` and `width` parameters are the minimun dimensions of the generated svg. When 0 is
275    /// supplied here, these values default to `200px`.
276    ///
277    /// ### Example
278    /// ```rust
279    /// use google_authenticator::{GoogleAuthenticator, ErrorCorrectionLevel};
280    ///
281    /// let authenticator = GoogleAuthenticator::new();
282    /// authenticator.qr_code(
283    ///     "I3VFM3JKMNDJCDH5BMBEEQAW6KJ6NOE3",
284    ///     "your company name",
285    ///     "hello",
286    ///     0,
287    ///     0,
288    ///     ErrorCorrectionLevel::Medium,
289    /// );
290    /// ```
291    #[cfg(feature = "with-qrcode")]
292    pub fn qr_code(
293        &self,
294        secret: &str,
295        name: &str,
296        title: &str,
297        width: u32,
298        height: u32,
299        level: ErrorCorrectionLevel,
300    ) -> Result<String> {
301        let width = if width == 0 { 200 } else { width };
302        let height = if height == 0 { 200 } else { height };
303        let scheme = Self::create_scheme(name, secret, title);
304        let code = QrCode::with_error_correction_level(scheme.as_bytes(), level.into())?;
305        Ok(code
306            .render()
307            .min_dimensions(width, height)
308            .dark_color(svg::Color("#000000"))
309            .light_color(svg::Color("#ffffff"))
310            .build())
311    }
312
313    /// Creates a totp url.
314    fn create_scheme(name: &str, secret: &str, title: &str) -> String {
315        let name = utf8_percent_encode(name, NON_ALPHANUMERIC);
316        let title = utf8_percent_encode(title, NON_ALPHANUMERIC);
317        format!("otpauth://totp/{}?secret={}&issuer={}", name, secret, title)
318    }
319
320    fn base32_decode(secret: &str) -> Result<Vec<u8>> {
321        match base32::decode(base32::Alphabet::RFC4648 { padding: true }, secret) {
322            Some(_decode_str) => Ok(_decode_str),
323            _ => Err(GAError::Error("secret must be base32 decodeable.")),
324        }
325    }
326}
327
328/// Represents any of the reasons why using 2fa with Google Authenenticator can fail.
329#[derive(Debug)]
330pub enum GAError {
331    /// An error in the logic of the QR code. Contains a static string with the error message.
332    Error(&'static str),
333    /// An error related to the QR code. This variant is only available with the feature flag
334    /// `with-qrcode`.
335    #[cfg(any(feature = "with-qrcode"))]
336    QrError(QrError),
337}
338
339impl error::Error for GAError {
340    fn description(&self) -> &str {
341        match *self {
342            GAError::Error(description) => description,
343            #[cfg(any(feature = "with-qrcode"))]
344            GAError::QrError(ref _err) => "",
345        }
346    }
347
348    fn cause(&self) -> Option<&dyn error::Error> {
349        match *self {
350            #[cfg(any(feature = "with-qrcode"))]
351            GAError::QrError(ref _err) => None,
352            GAError::Error(_) => None,
353        }
354    }
355}
356
357#[cfg(any(feature = "with-qrcode"))]
358impl From<QrError> for GAError {
359    fn from(err: QrError) -> GAError {
360        GAError::QrError(err)
361    }
362}
363
364impl fmt::Display for GAError {
365    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
366        match *self {
367            GAError::Error(desc) => f.write_str(desc),
368            #[cfg(any(feature = "with-qrcode"))]
369            GAError::QrError(ref err) => fmt::Display::fmt(err, f),
370        }
371    }
372}
373
374/// A type alias that can be used for fallible functions with `google_authenticator`.
375pub type Result<T> = result::Result<T, GAError>;