Skip to main content

ssh_vault/
cache.rs

1use crate::tools::get_home;
2use anyhow::{Result, anyhow};
3use std::{
4    fs,
5    path::{Path, PathBuf},
6    time::{Duration, SystemTime},
7};
8
9// Load the response from a cache file ~/.ssh/vault/keys/`<key>`
10/// # Errors
11/// Return an error if the cache is older than 30 days
12pub fn get(key: &str) -> Result<String> {
13    let cache = get_cache_path(key)?;
14    if cache.exists() {
15        let metadata = fs::metadata(&cache);
16        let last_modified = metadata.map_or_else(
17            |_| SystemTime::now(),
18            |meta| meta.modified().unwrap_or_else(|_| SystemTime::now()),
19        );
20
21        // Calculate the duration since the file was last modified
22        let duration_since_modified = SystemTime::now()
23            .duration_since(last_modified)
24            .unwrap_or(Duration::from_secs(0));
25
26        // Return an error if the cache is older than 30 days
27        if duration_since_modified > Duration::from_hours(30 * 24) {
28            Err(anyhow!("cache expired"))
29        } else {
30            Ok(fs::read_to_string(cache)?)
31        }
32    } else {
33        Err(anyhow!("cache not found"))
34    }
35}
36
37/// Save the response to a cache file ~/.ssh/vault/keys/`<key>`
38/// # Errors
39/// Return an error if the cache file can't be created
40pub fn put(key: &str, response: &str) -> Result<()> {
41    let cache = get_cache_path(key)?;
42
43    // Create parent directories if they don't exist. The cache path is
44    // predictable (md5 of the URL), so on a shared host a lax-permission
45    // directory would let another user read or overwrite cached keys (cache
46    // poisoning) — and `find` may even cache a fetched private key here.
47    // Restrict to the owner.
48    if let Some(parent_dir) = cache.parent() {
49        fs::create_dir_all(parent_dir)?;
50        #[cfg(unix)]
51        {
52            use std::os::unix::fs::PermissionsExt;
53            // Enforce 0700 on ssh-vault's own cache dirs even if they already
54            // existed with looser permissions. Only these two — never ~/.ssh.
55            let vault_dir = get_ssh_vault_path()?;
56            for dir in [vault_dir.join("keys"), vault_dir] {
57                if dir.is_dir() {
58                    fs::set_permissions(&dir, fs::Permissions::from_mode(0o700))?;
59                }
60            }
61        }
62    }
63
64    #[cfg(unix)]
65    {
66        use std::io::Write;
67        use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
68        let mut file = fs::OpenOptions::new()
69            .write(true)
70            .create(true)
71            .truncate(true)
72            .mode(0o600)
73            .open(&cache)?;
74        file.write_all(response.as_bytes())?;
75        // Enforce 0600 even if the file already existed with looser permissions.
76        fs::set_permissions(&cache, fs::Permissions::from_mode(0o600))?;
77    }
78    #[cfg(not(unix))]
79    fs::write(&cache, response)?;
80
81    Ok(())
82}
83
84/// Get the path to the cache file ~/.ssh/vault/keys/`<key>`
85/// # Errors
86/// Return an error if we can't get the path to the cache file
87fn get_cache_path(key: &str) -> Result<PathBuf> {
88    let ssh_vault = get_ssh_vault_path()?;
89    Ok(ssh_vault.join("keys").join(key))
90}
91
92/// Get the path to the ssh-vault directory ~/.ssh/vault
93/// # Errors
94/// Return an error if we can't get the path to the ssh-vault directory
95fn get_ssh_vault_path() -> Result<PathBuf> {
96    let home = get_home()?;
97    Ok(Path::new(&home).join(".ssh").join("vault"))
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use std::fs;
104
105    #[test]
106    fn test_get_cache_path() -> Result<(), Box<dyn std::error::Error>> {
107        let cache = get_cache_path("test")?;
108        assert!(!cache.is_dir());
109        assert_eq!(
110            cache.to_str(),
111            get_home()?
112                .join(".ssh")
113                .join("vault")
114                .join("keys")
115                .join("test")
116                .to_str()
117        );
118        Ok(())
119    }
120
121    #[test]
122    fn test_get_ssh_vault_path() -> Result<(), Box<dyn std::error::Error>> {
123        let ssh_vault = get_ssh_vault_path()?;
124        assert!(!ssh_vault.is_file());
125        assert_eq!(
126            ssh_vault.to_str(),
127            get_home()?.join(".ssh").join("vault").to_str()
128        );
129        Ok(())
130    }
131
132    #[test]
133    fn test_put() -> Result<(), Box<dyn std::error::Error>> {
134        let cache = get_cache_path("test-2")?;
135        put("test-2", "test")?;
136
137        assert!(cache.is_file());
138        assert!(!cache.is_dir());
139        assert!(cache.exists());
140        assert_eq!(
141            cache.to_str(),
142            get_home()?
143                .join(".ssh")
144                .join("vault")
145                .join("keys")
146                .join("test-2")
147                .to_str()
148        );
149        fs::remove_file(cache)?;
150        Ok(())
151    }
152
153    #[test]
154    fn test_get() -> Result<(), Box<dyn std::error::Error>> {
155        let cache = get_cache_path("test-3")?;
156        put("test-3", "test")?;
157        let response = get("test-3")?;
158        assert_eq!(response, "test");
159        fs::remove_file(cache)?;
160        Ok(())
161    }
162
163    #[cfg(unix)]
164    #[test]
165    fn test_put_permissions() -> Result<(), Box<dyn std::error::Error>> {
166        use std::os::unix::fs::PermissionsExt;
167
168        let cache = get_cache_path("test-perms")?;
169        put("test-perms", "test")?;
170
171        // Cache file must be owner-only (0600).
172        let file_mode = fs::metadata(&cache)?.permissions().mode() & 0o777;
173        assert_eq!(file_mode, 0o600);
174
175        // Parent directory must be owner-only (0700).
176        if let Some(parent) = cache.parent() {
177            let dir_mode = fs::metadata(parent)?.permissions().mode() & 0o777;
178            assert_eq!(dir_mode, 0o700);
179        }
180
181        fs::remove_file(cache)?;
182        Ok(())
183    }
184}