Rename the entire workspace:
- Crate packages: quicnprotochat-{core,proto,server,client,gui,p2p,mobile} -> quicproquo-*
- Binary names: quicnprotochat -> qpq, quicnprotochat-server -> qpq-server,
quicnprotochat-gui -> qpq-gui
- Default files: *-state.bin -> qpq-state.bin, *-server.toml -> qpq-server.toml,
*.db -> qpq.db
- Environment variable prefix: QUICNPROTOCHAT_* -> QPQ_*
- App identifier: chat.quicnproto.gui -> chat.quicproquo.gui
- Proto package: quicnprotochat.bench -> quicproquo.bench
- All documentation, Docker, CI, and script references updated
HKDF domain-separation strings and P2P ALPN remain unchanged for
backward compatibility with existing encrypted state and wire protocol.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
87 lines
2.7 KiB
Rust
87 lines
2.7 KiB
Rust
//! Cached session token stored next to the state file.
|
|
//!
|
|
//! File format (no password): two lines — username and hex-encoded session token.
|
|
//! File format (with password): QPCE-encrypted version of the above.
|
|
//! The token has a server-side 24h TTL; no client-side expiry tracking.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use anyhow::Context;
|
|
|
|
use super::state::{decrypt_state, encrypt_state, is_encrypted_state};
|
|
|
|
pub struct CachedSession {
|
|
pub username: String,
|
|
pub token_hex: String,
|
|
}
|
|
|
|
/// Derive the session cache path: `{state_path}.session`.
|
|
fn session_cache_path(state_path: &Path) -> PathBuf {
|
|
state_path.with_extension("session")
|
|
}
|
|
|
|
/// Parse the two-line format (username + token_hex) from plaintext bytes.
|
|
fn parse_session_lines(text: &str) -> Option<CachedSession> {
|
|
let mut lines = text.lines();
|
|
let username = lines.next()?.trim().to_string();
|
|
let token_hex = lines.next()?.trim().to_string();
|
|
if username.is_empty() || token_hex.is_empty() {
|
|
return None;
|
|
}
|
|
if hex::decode(&token_hex).is_err() {
|
|
return None;
|
|
}
|
|
Some(CachedSession { username, token_hex })
|
|
}
|
|
|
|
/// Load a cached session token. Returns None if file is missing or malformed.
|
|
/// Decrypts if the file is QPCE-encrypted (requires `password`).
|
|
pub fn load_cached_session(state_path: &Path, password: Option<&str>) -> Option<CachedSession> {
|
|
let path = session_cache_path(state_path);
|
|
let raw = std::fs::read(&path).ok()?;
|
|
|
|
if is_encrypted_state(&raw) {
|
|
let pw = password?;
|
|
let plaintext = decrypt_state(pw, &raw).ok()?;
|
|
let text = String::from_utf8(plaintext).ok()?;
|
|
parse_session_lines(&text)
|
|
} else {
|
|
let text = String::from_utf8(raw).ok()?;
|
|
parse_session_lines(&text)
|
|
}
|
|
}
|
|
|
|
/// Save a session token to the cache file (mode 0o600 on Unix).
|
|
/// Encrypts with QPCE if `password` is provided.
|
|
pub fn save_cached_session(
|
|
state_path: &Path,
|
|
username: &str,
|
|
token_hex: &str,
|
|
password: Option<&str>,
|
|
) -> anyhow::Result<()> {
|
|
let path = session_cache_path(state_path);
|
|
let contents = format!("{username}\n{token_hex}\n");
|
|
|
|
let bytes = match password {
|
|
Some(pw) => encrypt_state(pw, contents.as_bytes())?,
|
|
None => contents.into_bytes(),
|
|
};
|
|
|
|
std::fs::write(&path, bytes).with_context(|| format!("write session cache {path:?}"))?;
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let perms = std::fs::Permissions::from_mode(0o600);
|
|
std::fs::set_permissions(&path, perms).ok();
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Remove the cached session file.
|
|
pub fn clear_cached_session(state_path: &Path) {
|
|
let path = session_cache_path(state_path);
|
|
std::fs::remove_file(&path).ok();
|
|
}
|