Session Management

Deep dive into session handling, encryption, and security in DesQTA

Session Management Overview

DesQTA implements a secure session management system that stores SEQTA authentication credentials locally with encryption. Sessions are managed entirely by the Rust backend for security.

Session Structure

Session Data Model

// src-tauri/src/utils/session.rs

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct Session {
    pub base_url: String,              // SEQTA base URL
    pub jsessionid: String,             // JSESSIONID cookie or JWT token
    pub additional_cookies: Vec<Cookie>, // Other cookies from SEQTA
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Cookie {
    pub name: String,
    pub value: String,
    pub domain: Option<String>,
    pub path: Option<String>,
    pub secure: Option<bool>,
    pub http_only: Option<bool>,
}

Session Storage

File Location

Sessions are stored per profile:

$DATA_DIR/DesQTA/profiles/{profile_id}/session.enc  (Desktop - encrypted)
$DATA_DIR/DesQTA/profiles/{profile_id}/session.json (Mobile - plain)

Encryption (Desktop)

Desktop platforms use AES-256-GCM encryption:

// Encryption key stored in OS keychain
struct SessionEncryption;

impl SessionEncryption {
    fn get_or_create_key() -> Result<Vec<u8>, String> {
        let entry = keyring::Entry::new("DesQTA", "session_encryption_key")?;
        
        match entry.get_password() {
            Ok(key_b64) => {
                // Decode existing key
                general_purpose::STANDARD.decode(&key_b64)
                    .map_err(|e| format!("Failed to decode key: {}", e))
            }
            Err(_) => {
                // Generate new key
                let rng = SystemRandom::new();
                let mut key = vec![0u8; 32]; // 256 bits
                rng.fill(&mut key)?;
                
                // Store in keychain
                let key_b64 = general_purpose::STANDARD.encode(&key);
                entry.set_password(&key_b64)?;
                
                Ok(key)
            }
        }
    }
    
    fn encrypt(data: &[u8]) -> Result<Vec<u8>, String> {
        let mut key_bytes = Self::get_or_create_key()?;
        let unbound_key = UnboundKey::new(&AES_256_GCM, &key_bytes)?;
        let nonce_sequence = CounterNonceSequence(0);
        let mut sealing_key = SealingKey::new(unbound_key, nonce_sequence);
        
        let mut in_out = data.to_vec();
        sealing_key.seal_in_place_append_tag(Aad::empty(), &mut in_out)?;
        
        // Clear key from memory
        key_bytes.zeroize();
        
        Ok(in_out)
    }
    
    fn decrypt(encrypted_data: &[u8]) -> Result<Vec<u8>, String> {
        let mut key_bytes = Self::get_or_create_key()?;
        let unbound_key = UnboundKey::new(&AES_256_GCM, &key_bytes)?;
        let nonce_sequence = CounterNonceSequence(0);
        let mut opening_key = OpeningKey::new(unbound_key, nonce_sequence);
        
        let mut in_out = encrypted_data.to_vec();
        let decrypted = opening_key.open_in_place(Aad::empty(), &mut in_out)?;
        
        key_bytes.zeroize();
        
        Ok(decrypted.to_vec())
    }
}

Session Operations

Loading Session

impl Session {
    pub fn load() -> Self {
        let path = session_file();
        
        #[cfg(any(target_os = "android", target_os = "ios"))]
        {
            // Mobile: Plain JSON
            if let Ok(mut file) = fs::File::open(&path) {
                let mut contents = String::new();
                if file.read_to_string(&mut contents).is_ok() {
                    if let Ok(session) = serde_json::from_str::<Session>(&contents) {
                        return session;
                    }
                }
            }
        }
        
        #[cfg(not(any(target_os = "android", target_os = "ios")))]
        {
            // Desktop: Encrypted
            if let Ok(encrypted_data) = fs::read(&path) {
                if let Ok(decrypted) = SessionEncryption::decrypt(&encrypted_data) {
                    if let Ok(session) = serde_json::from_slice::<Session>(&decrypted) {
                        return session;
                    }
                }
            }
        }
        
        Session::default()
    }
}

Saving Session

impl Session {
    pub fn save(&self) -> io::Result<()> {
        let path = session_file();
        
        // Create parent directory if needed
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }
        
        let json_data = serde_json::to_string(self)
            .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
        
        #[cfg(any(target_os = "android", target_os = "ios"))]
        {
            // Mobile: Plain JSON
            fs::write(&path, json_data)
        }
        
        #[cfg(not(any(target_os = "android", target_os = "ios")))]
        {
            // Desktop: Encrypted
            let encrypted = SessionEncryption::encrypt(json_data.as_bytes())
                .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
            fs::write(&path, encrypted)
        }
    }
}

Checking Session Existence

impl Session {
    pub fn exists() -> bool {
        let path = session_file();
        if !path.exists() {
            return false;
        }
        
        // Try to load and validate
        let session = Self::load();
        !session.base_url.is_empty() && !session.jsessionid.is_empty()
    }
}

Tauri Commands

Check Session

#[tauri::command]
pub fn check_session_exists() -> bool {
    Session::exists()
}

Get Session

#[tauri::command]
pub fn get_session() -> Result<Session, String> {
    let session = Session::load();
    if session.base_url.is_empty() {
        Err("No session found".to_string())
    } else {
        Ok(session)
    }
}

Save Session

#[tauri::command]
pub fn save_session(session: Session) -> Result<(), String> {
    session.save()
        .map_err(|e| format!("Failed to save session: {}", e))
}

Clear Session

#[tauri::command]
pub fn clear_session() -> Result<(), String> {
    let path = session_file();
    if path.exists() {
        fs::remove_file(&path)
            .map_err(|e| format!("Failed to delete session: {}", e))?;
    }
    
    // Clear encryption key (optional - for logout)
    SessionEncryption::clear_key()
        .unwrap_or_else(|e| {
            logger::warn(&format!("Failed to clear encryption key: {}", e));
        });
    
    Ok(())
}

Authentication Methods

// JSESSIONID is a cookie value
session.jsessionid = "ABC123DEF456...";
session.additional_cookies = vec![
    Cookie {
        name: "SESSIONID".to_string(),
        value: "xyz789".to_string(),
        // ...
    }
];

JWT Token Auth (QR Code)

// JSESSIONID contains JWT token (starts with "eyJ")
session.jsessionid = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
// Additional cookies may include JSESSIONID cookie from responses

Session Usage in Network Requests

Building Request Headers

// src-tauri/src/utils/netgrab.rs

async fn append_default_headers(req: RequestBuilder) -> RequestBuilder {
    let session = Session::load();
    let mut headers = HeaderMap::new();
    
    // Check if using JWT authentication
    if session.jsessionid.starts_with("eyJ") {
        // JWT Bearer token
        headers.insert(
            header::AUTHORIZATION,
            format!("Bearer {}", session.jsessionid).parse().unwrap(),
        );
        
        // Also include JSESSIONID cookie if present
        let mut cookie_parts = Vec::new();
        for cookie in &session.additional_cookies {
            if cookie.name == "JSESSIONID" {
                cookie_parts.push(format!("JSESSIONID={}", cookie.value));
            }
        }
        
        if !cookie_parts.is_empty() {
            headers.insert(
                header::COOKIE,
                cookie_parts.join("; ").parse().unwrap(),
            );
        }
    } else {
        // Traditional cookie-based authentication
        let mut cookie_parts = Vec::new();
        
        if !session.jsessionid.is_empty() {
            cookie_parts.push(format!("JSESSIONID={}", session.jsessionid));
        }
        
        for cookie in session.additional_cookies {
            cookie_parts.push(format!("{}={}", cookie.name, cookie.value));
        }
        
        if !cookie_parts.is_empty() {
            headers.insert(
                header::COOKIE,
                cookie_parts.join("; ").parse().unwrap(),
            );
        }
    }
    
    // Add origin and referer
    if !session.base_url.is_empty() {
        headers.insert(header::ORIGIN, session.base_url.parse().unwrap());
        headers.insert(header::REFERER, session.base_url.parse().unwrap());
    }
    
    req.headers(headers)
}

Session Security

Encryption Details

  • Algorithm: AES-256-GCM
  • Key Storage: OS keychain (Windows Credential Manager, macOS Keychain, Linux Secret Service)
  • Key Generation: Cryptographically secure random (32 bytes)
  • Nonce: Counter-based nonce sequence
  • Memory Safety: Keys zeroized after use

Security Best Practices

  1. Never Log Sessions: Session data never appears in logs
  2. Encrypted Storage: Desktop sessions encrypted at rest
  3. Keychain Storage: Encryption keys in OS keychain
  4. Memory Zeroization: Sensitive data cleared from memory
  5. Profile Isolation: Each profile has separate session

Session Lifecycle

1. Login Flow

// User logs in via browser window
// Backend captures cookies from browser
// Session created and encrypted
session.save()?;

2. Session Validation

// On app startup
if Session::exists() {
    let session = Session::load();
    // Validate session with SEQTA
    if validate_session(&session).await? {
        // Session valid, continue
    } else {
        // Session expired, require re-login
        Session::clear()?;
    }
}

3. Session Refresh

// During API calls, cookies may be updated
// Update session with new cookies
session.additional_cookies.push(new_cookie);
session.save()?;

4. Logout Flow

// Clear session file
Session::clear()?;
// Optionally clear encryption key
SessionEncryption::clear_key()?;

Profile-Based Sessions

Multi-Profile Support

// Sessions are stored per profile
pub fn session_file() -> PathBuf {
    let profile_id = ProfileManager::get_current_profile()
        .map(|p| p.id)
        .unwrap_or_else(|| "default".to_string());
    
    let mut dir = get_profile_dir(&profile_id);
    dir.push("session.enc");
    dir
}

Profile Switching

// When switching profiles
fn switch_profile(new_profile_id: String) {
    ProfileManager::set_current_profile(new_profile_id);
    // Session file path changes automatically
    // Load session for new profile
    let session = Session::load();
}

Frontend Integration

Checking Session

import { invoke } from '@tauri-apps/api/core';

const sessionExists = await invoke<boolean>('check_session_exists');
if (!sessionExists) {
  // Show login screen
}

Loading Session Info

const session = await invoke<Session>('get_session');
console.log('Base URL:', session.base_url);

Session Events

import { listen } from '@tauri-apps/api/event';

// Listen for session expiration
await listen('session-expired', () => {
  // Redirect to login
  showLoginScreen();
});

Troubleshooting

Session Not Loading

Check:

  • File exists at expected path
  • File permissions are correct
  • Encryption key exists in keychain
  • Profile is correct

Solutions:

  • Verify file path
  • Check keychain access
  • Try clearing and re-logging in

Encryption Errors

Check:

  • Keychain access permissions
  • Encryption key exists
  • File is not corrupted

Solutions:

  • Grant keychain access
  • Clear session and re-login
  • Check file integrity

Next Steps