Full codebase review by 4 independent agents (security, architecture,
code quality, correctness) identified ~80 findings. This commit fixes 40
of them across all workspace crates.
Critical fixes:
- Federation service: validate origin against mTLS cert CN/SAN (C1)
- WS bridge: add DM channel auth, size limits, rate limiting (C2)
- hpke_seal: panic on error instead of silent empty ciphertext (C3)
- hpke_setup_sender_and_export: error on parse fail, no PQ downgrade (C7)
Security fixes:
- Zeroize: seed_bytes() returns Zeroizing<[u8;32]>, private_to_bytes()
returns Zeroizing<Vec<u8>>, ClientAuth.access_token, SessionState.password,
conversation hex_key all wrapped in Zeroizing
- Keystore: 0o600 file permissions on Unix
- MeshIdentity: 0o600 file permissions on Unix
- Timing floors: resolveIdentity + WS bridge resolve_user get 5ms floor
- Mobile: TLS verification gated behind insecure-dev feature flag
- Proto: from_bytes default limit tightened from 64 MiB to 8 MiB
Correctness fixes:
- fetch_wait: register waiter before fetch to close TOCTOU window
- MeshEnvelope: exclude hop_count from signature (forwarding no longer
invalidates sender signature)
- BroadcastChannel: encrypt returns Result instead of panicking
- transcript: rename verify_transcript_chain → validate_transcript_structure
- group.rs: extract shared process_incoming() for receive_message variants
- auth_ops: remove spurious RegistrationRequest deserialization
- MeshStore.seen: bounded to 100K with FIFO eviction
Quality fixes:
- FFI error classification: typed downcast instead of string matching
- Plugin HookVTable: SAFETY documentation for unsafe Send+Sync
- clippy::unwrap_used: warn → deny workspace-wide
- Various .unwrap_or("") → proper error returns
Review report: docs/REVIEW-2026-03-04.md
152 tests passing (72 core + 35 server + 14 E2E + 1 doctest + 30 P2P)
94 lines
2.9 KiB
Rust
94 lines
2.9 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 => {
|
|
#[cfg(not(unix))]
|
|
tracing::warn!(
|
|
"storing session token as plaintext (no password set); \
|
|
file permissions cannot be restricted on this platform"
|
|
);
|
|
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();
|
|
}
|