Skip to main content

ati/security/
sealed_file.rs

1use std::path::Path;
2use thiserror::Error;
3
4#[derive(Error, Debug)]
5pub enum SealedFileError {
6    #[error("Key file not found at {0} — was it already consumed?")]
7    NotFound(String),
8    #[error("Permission denied reading key file: {0}")]
9    PermissionDenied(String),
10    #[error("Invalid base64 in key file: {0}")]
11    InvalidBase64(#[from] base64::DecodeError),
12    #[error("Invalid key length: expected 32 bytes, got {0}")]
13    InvalidKeyLength(usize),
14    #[error("IO error: {0}")]
15    Io(#[from] std::io::Error),
16}
17
18/// Default path for the session key sealed file
19pub const DEFAULT_KEY_PATH: &str = "/run/ati/.key";
20
21/// Read the session key from a sealed file, then immediately delete the file.
22///
23/// The key file contains a base64-encoded 256-bit (32 byte) session key.
24/// After reading, the file is unlink()'d so it can never be read again.
25///
26/// Also checks ATI_KEY_FILE env var as an override (for testing).
27pub fn read_and_delete_key() -> Result<[u8; 32], SealedFileError> {
28    let key_path = std::env::var("ATI_KEY_FILE").unwrap_or_else(|_| DEFAULT_KEY_PATH.to_string());
29
30    read_and_delete_key_from(Path::new(&key_path))
31}
32
33/// Read session key from a specific path, then delete the file.
34///
35/// Uses open-then-unlink-then-read pattern to eliminate the TOCTOU window:
36/// 1. Open the file (get fd)
37/// 2. Unlink the file from the filesystem (no other process can open it)
38/// 3. Read contents from the fd (still valid after unlink)
39pub fn read_and_delete_key_from(path: &Path) -> Result<[u8; 32], SealedFileError> {
40    use std::io::Read;
41
42    // Step 1: Open the file (get fd)
43    let mut file = match std::fs::File::open(path) {
44        Ok(f) => f,
45        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
46            return Err(SealedFileError::NotFound(path.display().to_string()));
47        }
48        Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
49            return Err(SealedFileError::PermissionDenied(
50                path.display().to_string(),
51            ));
52        }
53        Err(e) => return Err(SealedFileError::Io(e)),
54    };
55
56    // Step 2: Unlink the file BEFORE reading — closes the TOCTOU window.
57    // After unlink, the file is invisible to other processes but our fd is still valid.
58    if let Err(e) = std::fs::remove_file(path) {
59        tracing::warn!(path = %path.display(), error = %e, "could not delete key file");
60    }
61
62    // Step 3: Read contents from the fd (file data persists until last fd is closed)
63    let mut contents = String::new();
64    file.read_to_string(&mut contents)?;
65
66    // Decode base64
67    let decoded =
68        base64::Engine::decode(&base64::engine::general_purpose::STANDARD, contents.trim())?;
69
70    // Validate length
71    if decoded.len() != 32 {
72        return Err(SealedFileError::InvalidKeyLength(decoded.len()));
73    }
74
75    let mut key = [0u8; 32];
76    key.copy_from_slice(&decoded);
77    Ok(key)
78}