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>;