secret_file.rs (10499B)
1 use std::ffi::OsString; 2 use std::fs; 3 use std::path::{Path, PathBuf}; 4 5 use chacha20poly1305::aead::{Aead, KeyInit, Payload}; 6 use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce}; 7 use getrandom::getrandom; 8 use radroots_protected_store::{ 9 RADROOTS_PROTECTED_STORE_KEY_LENGTH, RADROOTS_PROTECTED_STORE_NONCE_LENGTH, 10 RadrootsProtectedStoreEnvelope, 11 }; 12 use radroots_secret_vault::{RadrootsSecretKeyWrapping, RadrootsSecretVaultAccessError}; 13 use zeroize::Zeroize; 14 15 use crate::error::RuntimeProtectedFileError; 16 17 const LOCAL_WRAPPING_KEY_SUFFIX: &str = ".key"; 18 const WRAPPED_KEY_VERSION: u8 = 1; 19 20 #[derive(Debug, Clone)] 21 struct LocalWrappedKeySource { 22 key_path: PathBuf, 23 } 24 25 impl LocalWrappedKeySource { 26 fn new(path: &Path) -> Self { 27 Self { 28 key_path: local_wrapping_key_path(path), 29 } 30 } 31 32 fn load_or_create_wrapping_key( 33 &self, 34 ) -> Result<[u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH], RadrootsSecretVaultAccessError> { 35 if self.key_path.exists() { 36 return self.load_wrapping_key(); 37 } 38 39 if let Some(parent) = self.key_path.parent().filter(|p| !p.as_os_str().is_empty()) { 40 fs::create_dir_all(parent).map_err(io_backend_error)?; 41 } 42 43 let mut key = [0_u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH]; 44 getrandom(&mut key).map_err(entropy_unavailable_error)?; 45 fs::write(&self.key_path, key.as_slice()).map_err(io_backend_error)?; 46 set_secret_permissions(&self.key_path)?; 47 Ok(key) 48 } 49 50 fn load_wrapping_key( 51 &self, 52 ) -> Result<[u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH], RadrootsSecretVaultAccessError> { 53 let raw = fs::read(&self.key_path).map_err(io_backend_error)?; 54 if raw.len() != RADROOTS_PROTECTED_STORE_KEY_LENGTH { 55 return Err(RadrootsSecretVaultAccessError::Backend(format!( 56 "wrapping key {} has invalid length {}", 57 self.key_path.display(), 58 raw.len() 59 ))); 60 } 61 62 let mut key = [0_u8; RADROOTS_PROTECTED_STORE_KEY_LENGTH]; 63 key.copy_from_slice(&raw); 64 Ok(key) 65 } 66 } 67 68 impl RadrootsSecretKeyWrapping for LocalWrappedKeySource { 69 type Error = RadrootsSecretVaultAccessError; 70 71 fn wrap_data_key(&self, key_slot: &str, plaintext_key: &[u8]) -> Result<Vec<u8>, Self::Error> { 72 let mut master_key = self.load_or_create_wrapping_key()?; 73 let mut nonce = [0_u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH]; 74 getrandom(&mut nonce).map_err(entropy_unavailable_error)?; 75 let cipher = XChaCha20Poly1305::new(Key::from_slice(&master_key)); 76 let ciphertext = cipher 77 .encrypt( 78 XNonce::from_slice(&nonce), 79 Payload { 80 msg: plaintext_key, 81 aad: key_slot.as_bytes(), 82 }, 83 ) 84 .map_err(wrap_data_key_error)?; 85 master_key.zeroize(); 86 87 let mut encoded = Vec::with_capacity(1 + nonce.len() + ciphertext.len()); 88 encoded.push(WRAPPED_KEY_VERSION); 89 encoded.extend_from_slice(&nonce); 90 encoded.extend_from_slice(ciphertext.as_slice()); 91 Ok(encoded) 92 } 93 94 fn unwrap_data_key(&self, key_slot: &str, wrapped_key: &[u8]) -> Result<Vec<u8>, Self::Error> { 95 if wrapped_key.len() <= 1 + RADROOTS_PROTECTED_STORE_NONCE_LENGTH { 96 return Err(RadrootsSecretVaultAccessError::Backend( 97 "wrapped protected secret data key is truncated".into(), 98 )); 99 } 100 if wrapped_key[0] != WRAPPED_KEY_VERSION { 101 return Err(RadrootsSecretVaultAccessError::Backend(format!( 102 "unsupported wrapped protected secret data key version {}", 103 wrapped_key[0] 104 ))); 105 } 106 107 let mut master_key = self.load_wrapping_key()?; 108 let nonce_offset = 1; 109 let ciphertext_offset = nonce_offset + RADROOTS_PROTECTED_STORE_NONCE_LENGTH; 110 let cipher = XChaCha20Poly1305::new(Key::from_slice(&master_key)); 111 let plaintext = cipher 112 .decrypt( 113 XNonce::from_slice(&wrapped_key[nonce_offset..ciphertext_offset]), 114 Payload { 115 msg: &wrapped_key[ciphertext_offset..], 116 aad: key_slot.as_bytes(), 117 }, 118 ) 119 .map_err(|_| { 120 RadrootsSecretVaultAccessError::Backend( 121 "failed to unwrap protected secret data key".into(), 122 ) 123 })?; 124 master_key.zeroize(); 125 Ok(plaintext) 126 } 127 } 128 129 pub fn local_wrapping_key_path(path: impl AsRef<Path>) -> PathBuf { 130 let path = path.as_ref(); 131 let mut value = OsString::from(path.as_os_str()); 132 value.push(LOCAL_WRAPPING_KEY_SUFFIX); 133 PathBuf::from(value) 134 } 135 136 pub fn seal_local_secret_file( 137 path: impl AsRef<Path>, 138 key_slot: &str, 139 payload: &[u8], 140 ) -> Result<(), RuntimeProtectedFileError> { 141 let path = path.as_ref(); 142 if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) { 143 fs::create_dir_all(parent).map_err(|source| RuntimeProtectedFileError::CreateDir { 144 path: parent.to_path_buf(), 145 source, 146 })?; 147 } 148 149 let key_source = LocalWrappedKeySource::new(path); 150 let envelope = 151 RadrootsProtectedStoreEnvelope::seal_with_wrapped_key(&key_source, key_slot, payload) 152 .map_err(|error| seal_error(path, error.to_string()))?; 153 let encoded = match encode_secret_envelope(&envelope) { 154 Ok(encoded) => encoded, 155 Err(error) => return Err(seal_error(path, error.to_string())), 156 }; 157 fs::write(path, encoded).map_err(|source| RuntimeProtectedFileError::Io { 158 path: path.to_path_buf(), 159 source, 160 })?; 161 match set_secret_permissions(path) { 162 Ok(()) => {} 163 Err(error) => return Err(permissions_error(path, error.to_string())), 164 } 165 Ok(()) 166 } 167 168 pub fn open_local_secret_file( 169 path: impl AsRef<Path>, 170 key_slot: &str, 171 ) -> Result<Vec<u8>, RuntimeProtectedFileError> { 172 let path = path.as_ref(); 173 let encoded = fs::read(path).map_err(|source| RuntimeProtectedFileError::Io { 174 path: path.to_path_buf(), 175 source, 176 })?; 177 let key_source = LocalWrappedKeySource::new(path); 178 let envelope = RadrootsProtectedStoreEnvelope::decode_json(&encoded).map_err(|error| { 179 RuntimeProtectedFileError::Decode { 180 path: path.to_path_buf(), 181 message: error.to_string(), 182 } 183 })?; 184 if envelope.header.key_slot != key_slot { 185 return Err(RuntimeProtectedFileError::Open { 186 path: path.to_path_buf(), 187 message: format!( 188 "expected key slot {key_slot}, found {}", 189 envelope.header.key_slot 190 ), 191 }); 192 } 193 envelope 194 .open_with_wrapped_key(&key_source) 195 .map_err(|error| RuntimeProtectedFileError::Open { 196 path: path.to_path_buf(), 197 message: error.to_string(), 198 }) 199 } 200 201 fn io_backend_error(source: std::io::Error) -> RadrootsSecretVaultAccessError { 202 RadrootsSecretVaultAccessError::Backend(source.to_string()) 203 } 204 205 fn entropy_unavailable_error(_: getrandom::Error) -> RadrootsSecretVaultAccessError { 206 RadrootsSecretVaultAccessError::Backend("entropy unavailable".into()) 207 } 208 209 fn wrap_data_key_error(_: chacha20poly1305::Error) -> RadrootsSecretVaultAccessError { 210 RadrootsSecretVaultAccessError::Backend("failed to wrap protected secret data key".into()) 211 } 212 213 fn seal_error(path: &Path, message: String) -> RuntimeProtectedFileError { 214 RuntimeProtectedFileError::Seal { 215 path: path.to_path_buf(), 216 message, 217 } 218 } 219 220 fn permissions_error(path: &Path, message: String) -> RuntimeProtectedFileError { 221 RuntimeProtectedFileError::Permissions { 222 path: path.to_path_buf(), 223 message, 224 } 225 } 226 227 fn encode_secret_envelope( 228 envelope: &RadrootsProtectedStoreEnvelope, 229 ) -> Result<Vec<u8>, radroots_protected_store::error::RadrootsProtectedStoreError> { 230 #[cfg(test)] 231 if test_hooks::take_encode() { 232 return Err( 233 radroots_protected_store::error::RadrootsProtectedStoreError::EnvelopeEncodeFailed, 234 ); 235 } 236 237 envelope.encode_json() 238 } 239 240 #[cfg(test)] 241 mod test_hooks { 242 use std::collections::HashMap; 243 use std::sync::{Mutex, OnceLock}; 244 use std::thread::{self, ThreadId}; 245 246 const FAIL_ENCODE: u8 = 1; 247 const FAIL_PERMS: u8 = 2; 248 249 static FAIL_POINTS: OnceLock<Mutex<HashMap<ThreadId, u8>>> = OnceLock::new(); 250 251 pub struct FailGuard { 252 thread_id: ThreadId, 253 } 254 255 impl Drop for FailGuard { 256 fn drop(&mut self) { 257 clear(self.thread_id); 258 } 259 } 260 261 pub fn fail_encode() -> FailGuard { 262 set(FAIL_ENCODE) 263 } 264 265 pub fn fail_perms() -> FailGuard { 266 set(FAIL_PERMS) 267 } 268 269 pub fn take_encode() -> bool { 270 take(FAIL_ENCODE) 271 } 272 273 pub fn take_perms() -> bool { 274 take(FAIL_PERMS) 275 } 276 277 fn set(point: u8) -> FailGuard { 278 let thread_id = thread::current().id(); 279 fail_map() 280 .lock() 281 .expect("lock fail hooks") 282 .insert(thread_id, point); 283 FailGuard { thread_id } 284 } 285 286 fn clear(thread_id: ThreadId) { 287 fail_map() 288 .lock() 289 .expect("lock clear hooks") 290 .remove(&thread_id); 291 } 292 293 fn take(point: u8) -> bool { 294 let thread_id = thread::current().id(); 295 let mut map = fail_map().lock().expect("lock take hooks"); 296 match map.get(&thread_id).copied() { 297 Some(current_point) if current_point == point => { 298 map.remove(&thread_id); 299 true 300 } 301 _ => false, 302 } 303 } 304 305 fn fail_map() -> &'static Mutex<HashMap<ThreadId, u8>> { 306 FAIL_POINTS.get_or_init(|| Mutex::new(HashMap::new())) 307 } 308 } 309 310 #[cfg(unix)] 311 fn set_secret_permissions(path: &Path) -> Result<(), RadrootsSecretVaultAccessError> { 312 use std::os::unix::fs::PermissionsExt; 313 314 #[cfg(test)] 315 if test_hooks::take_perms() { 316 return Err(io_backend_error(std::io::Error::other( 317 "forced permissions failure", 318 ))); 319 } 320 321 let permissions = std::fs::Permissions::from_mode(0o600); 322 fs::set_permissions(path, permissions).map_err(io_backend_error) 323 } 324 325 #[cfg(not(unix))] 326 fn set_secret_permissions(_path: &Path) -> Result<(), RadrootsSecretVaultAccessError> { 327 Ok(()) 328 } 329 330 #[cfg(test)] 331 mod tests;