use std::path::Path;
use crate::error::NativeError;
use crate::paths::AppPaths;
const AUTH_SECRET_ENV: &str = "AUTH_SECRET";
const AUTH_COOKIE_ENV: &str = "AUTH_COOKIE_NAME";
pub const SECRET_FILE: &str = "session.key";
const SECRET_BYTES: usize = 32;
pub fn session_secret(paths: &AppPaths) -> Result<String, NativeError> {
let file = paths.secret_file();
if let Some(existing) = read_secret(&file)? {
return Ok(existing);
}
let secret = generate()?;
write_secret(&file, &secret)?;
Ok(secret)
}
pub fn install_session_secret(
paths: &AppPaths,
cookie_name: Option<&str>,
) -> Result<(), NativeError> {
let secret = session_secret(paths)?;
unsafe {
std::env::set_var(AUTH_SECRET_ENV, secret);
if let Some(name) = cookie_name {
std::env::set_var(AUTH_COOKIE_ENV, name);
}
}
Ok(())
}
fn generate() -> Result<String, NativeError> {
let mut bytes = [0u8; SECRET_BYTES];
getrandom::fill(&mut bytes).map_err(|e| {
NativeError::new(
"secret",
format!("the operating system would not provide randomness for a session key: {e}"),
)
})?;
Ok(bytes.iter().map(|b| format!("{b:02x}")).collect())
}
fn read_secret(file: &Path) -> Result<Option<String>, NativeError> {
let stored = match std::fs::read(file) {
Ok(bytes) => bytes,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(NativeError::io("secret", file, e)),
};
let Some(plain) = unprotect(&stored) else {
return Ok(None);
};
let secret = String::from_utf8(plain).ok().filter(|s| s.len() >= 32);
Ok(secret)
}
fn write_secret(file: &Path, secret: &str) -> Result<(), NativeError> {
if let Some(parent) = file.parent() {
std::fs::create_dir_all(parent).map_err(|e| NativeError::io("secret", parent, e))?;
}
let protected = protect(secret.as_bytes())?;
std::fs::write(file, protected).map_err(|e| NativeError::io("secret", file, e))?;
restrict(file)?;
Ok(())
}
#[cfg(unix)]
fn restrict(file: &Path) -> Result<(), NativeError> {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(file, std::fs::Permissions::from_mode(0o600))
.map_err(|e| NativeError::io("secret", file, e))
}
#[cfg(not(unix))]
fn restrict(_file: &Path) -> Result<(), NativeError> {
Ok(())
}
#[cfg(windows)]
mod dpapi {
use windows_sys::Win32::Foundation::LocalFree;
use windows_sys::Win32::Security::Cryptography::{
CRYPT_INTEGER_BLOB, CryptProtectData, CryptUnprotectData,
};
pub fn protect(plain: &[u8]) -> Option<Vec<u8>> {
let input = blob(plain);
let mut output = CRYPT_INTEGER_BLOB {
cbData: 0,
pbData: std::ptr::null_mut(),
};
let ok = unsafe {
CryptProtectData(
&input,
std::ptr::null(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
0,
&mut output,
)
};
take(ok, output)
}
pub fn unprotect(sealed: &[u8]) -> Option<Vec<u8>> {
let input = blob(sealed);
let mut output = CRYPT_INTEGER_BLOB {
cbData: 0,
pbData: std::ptr::null_mut(),
};
let ok = unsafe {
CryptUnprotectData(
&input,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
0,
&mut output,
)
};
take(ok, output)
}
fn blob(bytes: &[u8]) -> CRYPT_INTEGER_BLOB {
CRYPT_INTEGER_BLOB {
cbData: bytes.len() as u32,
pbData: bytes.as_ptr() as *mut u8,
}
}
fn take(ok: i32, output: CRYPT_INTEGER_BLOB) -> Option<Vec<u8>> {
if ok == 0 || output.pbData.is_null() {
return None;
}
let copied =
unsafe { std::slice::from_raw_parts(output.pbData, output.cbData as usize).to_vec() };
unsafe {
LocalFree(output.pbData as _);
}
Some(copied)
}
}
#[cfg(windows)]
fn protect(plain: &[u8]) -> Result<Vec<u8>, NativeError> {
dpapi::protect(plain).ok_or_else(|| {
NativeError::new(
"secret",
"Windows would not encrypt the session key for this user account (DPAPI).",
)
})
}
#[cfg(windows)]
fn unprotect(sealed: &[u8]) -> Option<Vec<u8>> {
dpapi::unprotect(sealed)
}
#[cfg(not(windows))]
fn protect(plain: &[u8]) -> Result<Vec<u8>, NativeError> {
Ok(plain.to_vec())
}
#[cfg(not(windows))]
fn unprotect(sealed: &[u8]) -> Option<Vec<u8>> {
Some(sealed.to_vec())
}