1 parent
0421994fe6
commit
425ea7449b
138 files changed
+5705
-1012
No files matched your search
@@ -0,0 +1,105 @@
|
||||
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}"))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user