auths_cli/core/
pubkey_cache.rs1use anyhow::{Context, Result, anyhow};
9use std::fs;
10use std::path::PathBuf;
11
12use super::fs::{create_restricted_dir, write_sensitive_file};
13
14fn get_pubkey_cache_dir() -> Result<PathBuf> {
16 Ok(auths_core::paths::auths_home()
17 .map_err(|e| anyhow!(e))?
18 .join("pubkeys"))
19}
20
21fn get_cache_path(alias: &str) -> Result<PathBuf> {
23 let dir = get_pubkey_cache_dir()?;
24 let safe_alias = alias.replace(['/', '\\', '\0'], "_");
26 Ok(dir.join(format!("{}.pub", safe_alias)))
27}
28
29pub fn cache_pubkey(alias: &str, pubkey: &[u8]) -> Result<()> {
41 if pubkey.len() != 32 {
42 return Err(anyhow!(
43 "Invalid public key length: expected 32 bytes, got {}",
44 pubkey.len()
45 ));
46 }
47
48 let cache_dir = get_pubkey_cache_dir()?;
49 create_restricted_dir(&cache_dir)
50 .with_context(|| format!("Failed to create pubkey cache directory: {:?}", cache_dir))?;
51
52 let cache_path = get_cache_path(alias)?;
53 let hex_pubkey = hex::encode(pubkey);
54
55 write_sensitive_file(&cache_path, &hex_pubkey)
56 .with_context(|| format!("Failed to write pubkey cache file: {:?}", cache_path))?;
57
58 Ok(())
59}
60
61pub fn get_cached_pubkey(alias: &str) -> Result<Option<Vec<u8>>> {
71 let cache_path = get_cache_path(alias)?;
72
73 if !cache_path.exists() {
74 return Ok(None);
75 }
76
77 let hex_pubkey = fs::read_to_string(&cache_path)
78 .with_context(|| format!("Failed to read pubkey cache file: {:?}", cache_path))?;
79
80 let pubkey = hex::decode(hex_pubkey.trim())
81 .with_context(|| format!("Invalid hex in pubkey cache file: {:?}", cache_path))?;
82
83 if pubkey.len() != 32 {
84 return Err(anyhow!(
85 "Invalid cached public key length in {:?}: expected 32 bytes, got {}",
86 cache_path,
87 pubkey.len()
88 ));
89 }
90
91 Ok(Some(pubkey))
92}
93
94pub fn clear_cached_pubkey(alias: &str) -> Result<bool> {
106 let cache_path = get_cache_path(alias)?;
107
108 if !cache_path.exists() {
109 return Ok(false);
110 }
111
112 fs::remove_file(&cache_path)
113 .with_context(|| format!("Failed to remove pubkey cache file: {:?}", cache_path))?;
114
115 Ok(true)
116}
117
118pub fn clear_all_cached_pubkeys() -> Result<usize> {
126 let cache_dir = get_pubkey_cache_dir()?;
127
128 if !cache_dir.exists() {
129 return Ok(0);
130 }
131
132 let mut count = 0;
133 for entry in fs::read_dir(&cache_dir)
134 .with_context(|| format!("Failed to read pubkey cache directory: {:?}", cache_dir))?
135 {
136 let entry = entry?;
137 let path = entry.path();
138 if path.extension().is_some_and(|ext| ext == "pub") {
139 fs::remove_file(&path)
140 .with_context(|| format!("Failed to remove cache file: {:?}", path))?;
141 count += 1;
142 }
143 }
144
145 Ok(count)
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
156 fn test_get_cache_path_sanitizes_alias() {
157 let path = get_cache_path("test/alias").unwrap();
158 assert!(path.to_string_lossy().contains("test_alias.pub"));
159 }
160
161 #[test]
162 fn test_cache_pubkey_validates_length() {
163 let result = cache_pubkey("test", &[0u8; 16]);
164 assert!(result.is_err());
165 assert!(
166 result
167 .unwrap_err()
168 .to_string()
169 .contains("expected 32 bytes")
170 );
171 }
172}