1use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
2use hmac::{Hmac, Mac};
3use rand::RngCore;
4use serde::{Deserialize, Serialize};
5use sha2::Sha256;
6use subtle::ConstantTimeEq;
7
8use crate::error::{AuthyError, Result};
9use crate::types::*;
10
11type HmacSha256 = Hmac<Sha256>;
12
13const TOKEN_PREFIX: &str = "authy_v1.";
14const TOKEN_BYTES: usize = 32;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct SessionRecord {
19 pub id: String,
20 pub scope: String,
21 pub token_hmac: Vec<u8>,
22 pub created_at: DateTime<Utc>,
23 pub expires_at: DateTime<Utc>,
24 pub revoked: bool,
25 pub label: Option<String>,
26 #[serde(default)]
28 pub run_only: bool,
29}
30
31pub fn generate_token(hmac_key: &[u8]) -> (String, Vec<u8>) {
34 let mut token_bytes = [0u8; TOKEN_BYTES];
35 rand::thread_rng().fill_bytes(&mut token_bytes);
36
37 let token_string = format!("{}{}", TOKEN_PREFIX, URL_SAFE_NO_PAD.encode(token_bytes));
38
39 let hmac_bytes = compute_token_hmac(&token_string, hmac_key);
40
41 (token_string, hmac_bytes)
42}
43
44fn compute_token_hmac(token: &str, hmac_key: &[u8]) -> Vec<u8> {
46 let mut mac =
47 HmacSha256::new_from_slice(hmac_key).expect("HMAC can take key of any size");
48 mac.update(token.as_bytes());
49 mac.finalize().into_bytes().to_vec()
50}
51
52pub fn validate_token<'a>(
55 token: &str,
56 sessions: &'a [SessionRecord],
57 hmac_key: &[u8],
58) -> Result<&'a SessionRecord> {
59 if !token.starts_with(TOKEN_PREFIX) {
60 return Err(AuthyError::InvalidToken);
61 }
62
63 let token_hmac = compute_token_hmac(token, hmac_key);
64
65 for session in sessions {
66 if session.revoked {
67 continue;
68 }
69
70 if session
72 .token_hmac
73 .ct_eq(&token_hmac)
74 .into()
75 {
76 if Utc::now() > session.expires_at {
78 return Err(AuthyError::TokenExpired);
79 }
80 return Ok(session);
81 }
82 }
83
84 Err(AuthyError::InvalidToken)
85}
86
87#[cfg(feature = "cli")]
89pub fn parse_ttl(s: &str) -> Result<chrono::Duration> {
90 let duration: std::time::Duration =
91 humantime::parse_duration(s).map_err(|e| AuthyError::Other(format!("Invalid TTL: {e}")))?;
92 chrono::Duration::from_std(duration)
93 .map_err(|e| AuthyError::Other(format!("Duration out of range: {e}")))
94}
95
96pub fn generate_session_id() -> String {
98 let mut bytes = [0u8; 8];
99 rand::thread_rng().fill_bytes(&mut bytes);
100 hex::encode(bytes)
101}