Skip to main content

authy/session/
mod.rs

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/// A session record stored in the vault (only the HMAC of the token is stored, not the token itself).
17#[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    /// When true, this token can only use `run` and `list` — not `get`, `env`, or `export`.
27    #[serde(default)]
28    pub run_only: bool,
29}
30
31/// Generate a session token and its HMAC.
32/// Returns (token_string, hmac_bytes).
33pub 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
44/// Compute the HMAC of a token.
45fn 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
52/// Validate a token against stored session records.
53/// Returns the matching session record if valid.
54pub 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        // Constant-time comparison
71        if session
72            .token_hmac
73            .ct_eq(&token_hmac)
74            .into()
75        {
76            // Check expiration
77            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/// Parse a duration string like "1h", "30m", "7d".
88#[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
96/// Generate a short unique session ID.
97pub fn generate_session_id() -> String {
98    let mut bytes = [0u8; 8];
99    rand::thread_rng().fill_bytes(&mut bytes);
100    hex::encode(bytes)
101}