106 lines
3.8 KiB
Rust
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}"))
|
|
}
|
|
}
|