Files
RustBox/src/session_store/crypto.rs
T
Mateusz Gruszczyński 425ea7449b
RustBox CI / rustbox (push) Canceled after 0s
v0.1.0
2026-10-30 17:22:17 +01:00

106 lines
3.8 KiB
Rust

use std::fs;
use std::path::Path;
use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead};
use base64::{Engine, engine::general_purpose::STANDARD};
use rand::RngCore;
use crate::config::AppConfig;
#[derive(Clone)]
pub struct SessionCrypto {
key: [u8; 32],
}
impl SessionCrypto {
pub fn load_or_create(config: &AppConfig) -> Result<Self, String> {
if let Some(raw) = config
.session_encryption_key
.as_deref()
.map(str::trim)
.filter(|v| !v.is_empty())
{
let decoded = STANDARD
.decode(raw)
.map_err(|e| format!("SESSION_ENCRYPTION_KEY must be base64: {e}"))?;
if decoded.len() != 32 {
return Err("SESSION_ENCRYPTION_KEY must decode to exactly 32 bytes".to_string());
}
let mut key = [0u8; 32];
key.copy_from_slice(&decoded);
return Ok(Self { key });
}
if config.cluster_mode
|| config.database_backend != "sqlite"
|| config.session_backend == "redis"
{
return Err(
"SESSION_ENCRYPTION_KEY is required for shared/cluster session storage".to_string(),
);
}
fs::create_dir_all(&config.data_dir)
.map_err(|e| format!("failed to create data_dir: {e}"))?;
let path = Path::new(&config.data_dir).join("session.key");
if path.is_file() {
let raw = fs::read(&path).map_err(|e| format!("failed to read session key: {e}"))?;
if raw.len() != 32 {
return Err(format!("invalid session key length in {}", path.display()));
}
let mut key = [0u8; 32];
key.copy_from_slice(&raw);
return Ok(Self { key });
}
let mut key = [0u8; 32];
rand::rng().fill_bytes(&mut key);
#[cfg(unix)]
{
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let mut file = fs::OpenOptions::new()
.create_new(true)
.write(true)
.mode(0o600)
.open(&path)
.map_err(|e| format!("failed to create session key: {e}"))?;
file.write_all(&key)
.map_err(|e| format!("failed to write session key: {e}"))?;
}
#[cfg(not(unix))]
fs::write(&path, key).map_err(|e| format!("failed to write session key: {e}"))?;
Ok(Self { key })
}
pub fn encrypt(&self, plaintext: &str) -> Result<String, String> {
let cipher =
Aes256Gcm::new_from_slice(&self.key).map_err(|_| "invalid session key".to_string())?;
let mut nonce_bytes = [0u8; 12];
rand::rng().fill_bytes(&mut nonce_bytes);
let ciphertext = cipher
.encrypt(Nonce::from_slice(&nonce_bytes), plaintext.as_bytes())
.map_err(|_| "failed to encrypt session credential".to_string())?;
let mut combined = nonce_bytes.to_vec();
combined.extend_from_slice(&ciphertext);
Ok(STANDARD.encode(combined))
}
pub fn decrypt(&self, encoded: &str) -> Result<String, String> {
let raw = STANDARD
.decode(encoded)
.map_err(|e| format!("invalid encrypted session credential: {e}"))?;
if raw.len() < 13 {
return Err("invalid encrypted session credential".to_string());
}
let (nonce, ciphertext) = raw.split_at(12);
let cipher =
Aes256Gcm::new_from_slice(&self.key).map_err(|_| "invalid session key".to_string())?;
let plaintext = cipher
.decrypt(Nonce::from_slice(nonce), ciphertext)
.map_err(|_| "failed to decrypt session credential".to_string())?;
String::from_utf8(plaintext)
.map_err(|e| format!("invalid UTF-8 in session credential: {e}"))
}
}