file.rs (17796B)
1 //! Explicit file-backed secret adapters. 2 3 use crate::SecretRef; 4 use crate::envelope::Nonce; 5 use crate::error::{Error, Operation}; 6 use crate::id::BackendKind; 7 use crate::provider::{CapabilitySupport, ResidencySupport, SecretCapabilities, SecretProvider}; 8 use crate::wrapping::{ 9 BoxFuture, KeyWrapping, LegacyV1UnwrapRequest, SecretMaterial, UnwrapRequest, WrapRequest, 10 WrappedSecret, 11 }; 12 use alloc::string::String; 13 use alloc::vec::Vec; 14 use chacha20poly1305::aead::{Aead, KeyInit, Payload}; 15 use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce}; 16 use core::fmt; 17 use std::fs::{self, File}; 18 use std::io::{Read, Write}; 19 use std::path::{Component, Path, PathBuf}; 20 use tempfile::NamedTempFile; 21 22 const FILE_MAGIC: &[u8; 4] = b"RRK1"; 23 const FILE_VERSION: u8 = 1; 24 const DATA_KEY_BYTES: usize = 32; 25 const NONCE_BYTES: usize = 24; 26 const MAX_FILE_BYTES: usize = 128 * 1024; 27 28 /// Explicit root handling for a file provider. 29 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 30 #[non_exhaustive] 31 pub enum FileOpenMode { 32 /// Create a new root and fail if it already exists. 33 CreateNew, 34 /// Open an existing secure root without creating it. 35 OpenExisting, 36 } 37 38 /// File-backed provider using a caller-supplied in-memory master key. 39 pub struct FileProvider { 40 root: PathBuf, 41 master_key: SecretMaterial, 42 } 43 44 impl FileProvider { 45 /// Creates or opens an absolute normalized provider root. 46 pub fn open( 47 root: impl AsRef<Path>, 48 mode: FileOpenMode, 49 master_key: SecretMaterial, 50 ) -> Result<Self, Error> { 51 validate_master_key(&master_key)?; 52 let root = root.as_ref(); 53 validate_requested_root(root)?; 54 match mode { 55 FileOpenMode::CreateNew => { 56 fs::create_dir(root).map_err(|_| backend_failure(Operation::Open))?; 57 set_directory_permissions(root)?; 58 } 59 FileOpenMode::OpenExisting => validate_existing_root(root)?, 60 } 61 let root = fs::canonicalize(root).map_err(|_| backend_failure(Operation::Open))?; 62 validate_existing_root(&root)?; 63 Ok(Self { root, master_key }) 64 } 65 66 /// Persists caller-owned material with an explicit unique nonce. 67 pub fn provision( 68 &self, 69 reference: &SecretRef, 70 material: &SecretMaterial, 71 nonce: Nonce, 72 ) -> Result<(), Error> { 73 validate_file_reference(reference)?; 74 let path = self.entry_path(reference); 75 reject_existing_path(&path)?; 76 let encoded = self.seal_entry(reference, material, nonce)?; 77 self.persist_noclobber(path.as_path(), encoded.as_slice()) 78 } 79 80 /// Resumes safely if the next version reached disk before old-version removal. 81 pub fn rotate( 82 &self, 83 current: &SecretRef, 84 next: &SecretRef, 85 material: &SecretMaterial, 86 nonce: Nonce, 87 ) -> Result<(), Error> { 88 validate_file_reference(current)?; 89 validate_file_reference(next)?; 90 if current.id().as_str() != next.id().as_str() 91 || next.key_version() <= current.key_version() 92 { 93 return Err(Error::InvalidRotation); 94 } 95 if !self.contains(current)? { 96 return Err(not_found(current)); 97 } 98 99 if self.contains(next)? { 100 let persisted = self.read_entry(next)?; 101 let matches = persisted 102 .expose_secret(|expected| material.expose_secret(|actual| expected == actual)); 103 if !matches { 104 return Err(Error::SecretAlreadyExists { 105 backend: BackendKind::File, 106 key_version: next.key_version().get(), 107 }); 108 } 109 } else { 110 self.provision(next, material, nonce)?; 111 } 112 self.remove(current)?; 113 Ok(()) 114 } 115 116 /// Removes one exact key revision when present. 117 pub fn remove(&self, reference: &SecretRef) -> Result<bool, Error> { 118 validate_file_reference(reference)?; 119 let path = self.entry_path(reference); 120 match fs::symlink_metadata(&path) { 121 Ok(metadata) => { 122 validate_entry_metadata(&metadata)?; 123 validate_file_permissions(&metadata)?; 124 fs::remove_file(path).map_err(|_| backend_failure(Operation::Remove))?; 125 sync_directory(&self.root)?; 126 Ok(true) 127 } 128 Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(false), 129 Err(_) => Err(backend_failure(Operation::Remove)), 130 } 131 } 132 133 /// Reports whether one exact regular-file entry exists. 134 pub fn contains(&self, reference: &SecretRef) -> Result<bool, Error> { 135 validate_file_reference(reference)?; 136 match fs::symlink_metadata(self.entry_path(reference)) { 137 Ok(metadata) => { 138 validate_entry_metadata(&metadata)?; 139 validate_file_permissions(&metadata)?; 140 Ok(true) 141 } 142 Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(false), 143 Err(_) => Err(backend_failure(Operation::Read)), 144 } 145 } 146 147 fn entry_path(&self, reference: &SecretRef) -> PathBuf { 148 self.root.join(entry_name(reference)) 149 } 150 151 fn seal_entry( 152 &self, 153 reference: &SecretRef, 154 material: &SecretMaterial, 155 nonce: Nonce, 156 ) -> Result<Vec<u8>, Error> { 157 let aad = entry_aad(reference); 158 let ciphertext = self.master_key.expose_secret(|master_key| { 159 material.expose_secret(|plaintext| { 160 let cipher = XChaCha20Poly1305::new(Key::from_slice(master_key)); 161 cipher 162 .encrypt( 163 XNonce::from_slice(nonce.as_bytes()), 164 Payload { 165 msg: plaintext, 166 aad: aad.as_slice(), 167 }, 168 ) 169 .map_err(|_| Error::EncryptFailed) 170 }) 171 })?; 172 let ciphertext_len = 173 u32::try_from(ciphertext.len()).map_err(|_| backend_failure(Operation::Write))?; 174 let mut encoded = Vec::with_capacity(4 + 1 + NONCE_BYTES + 4 + ciphertext.len()); 175 encoded.extend_from_slice(FILE_MAGIC); 176 encoded.push(FILE_VERSION); 177 encoded.extend_from_slice(nonce.as_bytes()); 178 encoded.extend_from_slice(&ciphertext_len.to_be_bytes()); 179 encoded.extend_from_slice(ciphertext.as_slice()); 180 if encoded.len() > MAX_FILE_BYTES { 181 return Err(Error::EnvelopeTooLarge { 182 actual_bytes: encoded.len(), 183 max_bytes: MAX_FILE_BYTES, 184 }); 185 } 186 Ok(encoded) 187 } 188 189 fn read_entry(&self, reference: &SecretRef) -> Result<SecretMaterial, Error> { 190 let path = self.entry_path(reference); 191 let metadata = fs::symlink_metadata(&path).map_err(|source| { 192 if source.kind() == std::io::ErrorKind::NotFound { 193 not_found(reference) 194 } else { 195 backend_failure(Operation::Read) 196 } 197 })?; 198 validate_entry_metadata(&metadata)?; 199 validate_file_permissions(&metadata)?; 200 let file = File::open(path).map_err(|_| backend_failure(Operation::Read))?; 201 let mut encoded = Vec::new(); 202 file.take((MAX_FILE_BYTES + 1) as u64) 203 .read_to_end(&mut encoded) 204 .map_err(|_| backend_failure(Operation::Read))?; 205 if encoded.len() > MAX_FILE_BYTES { 206 return Err(Error::EnvelopeTooLarge { 207 actual_bytes: encoded.len(), 208 max_bytes: MAX_FILE_BYTES, 209 }); 210 } 211 self.open_entry(reference, encoded.as_slice()) 212 } 213 214 fn open_entry(&self, reference: &SecretRef, encoded: &[u8]) -> Result<SecretMaterial, Error> { 215 let minimum = 4 + 1 + NONCE_BYTES + 4 + 16; 216 if encoded.len() < minimum || &encoded[..4] != FILE_MAGIC { 217 return Err(Error::EnvelopeMalformed); 218 } 219 if encoded[4] != FILE_VERSION { 220 return Err(Error::UnsupportedEnvelopeVersion { 221 version: u16::from(encoded[4]), 222 }); 223 } 224 let nonce: [u8; NONCE_BYTES] = encoded[5..5 + NONCE_BYTES] 225 .try_into() 226 .map_err(|_| Error::EnvelopeMalformed)?; 227 let length_offset = 5 + NONCE_BYTES; 228 let ciphertext_len = u32::from_be_bytes( 229 encoded[length_offset..length_offset + 4] 230 .try_into() 231 .map_err(|_| Error::EnvelopeMalformed)?, 232 ) as usize; 233 let ciphertext = &encoded[length_offset + 4..]; 234 if ciphertext.len() != ciphertext_len { 235 return Err(Error::EnvelopeMalformed); 236 } 237 let aad = entry_aad(reference); 238 let plaintext = self.master_key.expose_secret(|master_key| { 239 let cipher = XChaCha20Poly1305::new(Key::from_slice(master_key)); 240 cipher 241 .decrypt( 242 XNonce::from_slice(&nonce), 243 Payload { 244 msg: ciphertext, 245 aad: aad.as_slice(), 246 }, 247 ) 248 .map_err(|_| Error::DecryptFailed) 249 })?; 250 SecretMaterial::from_owned(plaintext) 251 } 252 253 fn persist_noclobber(&self, path: &Path, encoded: &[u8]) -> Result<(), Error> { 254 let mut temporary = 255 NamedTempFile::new_in(&self.root).map_err(|_| backend_failure(Operation::Write))?; 256 set_file_permissions(temporary.as_file())?; 257 temporary 258 .write_all(encoded) 259 .map_err(|_| backend_failure(Operation::Write))?; 260 temporary 261 .as_file() 262 .sync_all() 263 .map_err(|_| backend_failure(Operation::Write))?; 264 temporary.persist_noclobber(path).map_err(|error| { 265 if error.error.kind() == std::io::ErrorKind::AlreadyExists { 266 Error::SecretAlreadyExists { 267 backend: BackendKind::File, 268 key_version: key_version_from_path(path).unwrap_or(0), 269 } 270 } else { 271 backend_failure(Operation::Write) 272 } 273 })?; 274 sync_directory(&self.root) 275 } 276 } 277 278 impl fmt::Debug for FileProvider { 279 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 280 formatter.write_str("FileProvider(<redacted>)") 281 } 282 } 283 284 impl KeyWrapping for FileProvider { 285 fn wrap<'a>(&'a self, request: WrapRequest<'a>) -> BoxFuture<'a, Result<WrappedSecret, Error>> { 286 Box::pin(async move { 287 validate_file_reference(request.reference())?; 288 let persisted = self.read_entry(request.reference())?; 289 let matches = persisted.expose_secret(|expected| { 290 request 291 .plaintext() 292 .expose_secret(|actual| expected == actual) 293 }); 294 if !matches { 295 return Err(backend_failure(Operation::Wrap)); 296 } 297 WrappedSecret::from_bytes(wrapping_token(request.reference(), request.context())) 298 }) 299 } 300 301 fn unwrap<'a>( 302 &'a self, 303 request: UnwrapRequest<'a>, 304 ) -> BoxFuture<'a, Result<SecretMaterial, Error>> { 305 Box::pin(async move { 306 validate_file_reference(request.reference())?; 307 if request.wrapped().as_bytes() 308 != wrapping_token(request.reference(), request.context()).as_slice() 309 { 310 return Err(backend_failure(Operation::Unwrap)); 311 } 312 self.read_entry(request.reference()) 313 }) 314 } 315 316 fn unwrap_legacy_v1<'a>( 317 &'a self, 318 request: LegacyV1UnwrapRequest<'a>, 319 ) -> BoxFuture<'a, Result<SecretMaterial, Error>> { 320 Box::pin(async move { 321 validate_file_reference(request.reference())?; 322 if request.wrapped().as_bytes() != entry_token(request.reference()).as_slice() { 323 return Err(backend_failure(Operation::Unwrap)); 324 } 325 self.read_entry(request.reference()) 326 }) 327 } 328 } 329 330 impl SecretProvider for FileProvider { 331 fn backend_kind(&self) -> BackendKind { 332 BackendKind::File 333 } 334 335 fn capabilities(&self) -> SecretCapabilities { 336 SecretCapabilities::available( 337 ResidencySupport::UserProfile, 338 CapabilitySupport::Unavailable, 339 CapabilitySupport::Unavailable, 340 ) 341 } 342 } 343 344 fn validate_requested_root(root: &Path) -> Result<(), Error> { 345 if !root.is_absolute() 346 || root 347 .components() 348 .any(|component| matches!(component, Component::CurDir | Component::ParentDir)) 349 { 350 return Err(Error::UnsafePath); 351 } 352 if let Ok(metadata) = fs::symlink_metadata(root) 353 && metadata.file_type().is_symlink() 354 { 355 return Err(Error::UnsafePath); 356 } 357 Ok(()) 358 } 359 360 fn validate_existing_root(root: &Path) -> Result<(), Error> { 361 let metadata = fs::symlink_metadata(root).map_err(|_| backend_failure(Operation::Open))?; 362 if metadata.file_type().is_symlink() || !metadata.is_dir() { 363 return Err(Error::UnsafePath); 364 } 365 validate_directory_permissions(&metadata) 366 } 367 368 fn validate_entry_metadata(metadata: &fs::Metadata) -> Result<(), Error> { 369 if metadata.file_type().is_symlink() || !metadata.is_file() { 370 return Err(Error::UnsafePath); 371 } 372 Ok(()) 373 } 374 375 fn validate_master_key(master_key: &SecretMaterial) -> Result<(), Error> { 376 if master_key.len() != DATA_KEY_BYTES { 377 return Err(Error::InvalidDataKeyLength { 378 actual_bytes: master_key.len(), 379 }); 380 } 381 Ok(()) 382 } 383 384 fn validate_file_reference(reference: &SecretRef) -> Result<(), Error> { 385 if reference.backend() != BackendKind::File { 386 return Err(Error::BackendMismatch { 387 provider: BackendKind::File, 388 reference: reference.backend(), 389 }); 390 } 391 Ok(()) 392 } 393 394 fn reject_existing_path(path: &Path) -> Result<(), Error> { 395 match fs::symlink_metadata(path) { 396 Ok(metadata) => { 397 validate_entry_metadata(&metadata)?; 398 Err(Error::SecretAlreadyExists { 399 backend: BackendKind::File, 400 key_version: key_version_from_path(path).unwrap_or(0), 401 }) 402 } 403 Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(()), 404 Err(_) => Err(backend_failure(Operation::Write)), 405 } 406 } 407 408 fn entry_name(reference: &SecretRef) -> String { 409 alloc::format!( 410 "{}.v{}.rrk", 411 hex_identifier(reference.id().as_str()), 412 reference.key_version().get() 413 ) 414 } 415 416 fn hex_identifier(identifier: &str) -> String { 417 const HEX: &[u8; 16] = b"0123456789abcdef"; 418 let mut encoded = String::with_capacity(identifier.len() * 2); 419 for byte in identifier.as_bytes() { 420 encoded.push(char::from(HEX[usize::from(byte >> 4)])); 421 encoded.push(char::from(HEX[usize::from(byte & 0x0f)])); 422 } 423 encoded 424 } 425 426 fn entry_token(reference: &SecretRef) -> Vec<u8> { 427 let mut token = Vec::from(b"radroots-file-key-v1\0".as_slice()); 428 token.extend_from_slice(&reference.key_version().get().to_be_bytes()); 429 token.extend_from_slice(reference.id().as_str().as_bytes()); 430 token 431 } 432 433 fn wrapping_token(reference: &SecretRef, context: &crate::context::EnvelopeContext) -> Vec<u8> { 434 let mut token = entry_token(reference); 435 token.extend_from_slice(&context.authentication_digest()); 436 token 437 } 438 439 fn entry_aad(reference: &SecretRef) -> Vec<u8> { 440 entry_token(reference) 441 } 442 443 fn key_version_from_path(path: &Path) -> Option<u32> { 444 let name = path.file_name()?.to_str()?; 445 let (_, suffix) = name.rsplit_once(".v")?; 446 suffix.strip_suffix(".rrk")?.parse().ok() 447 } 448 449 fn not_found(reference: &SecretRef) -> Error { 450 Error::SecretNotFound { 451 backend: BackendKind::File, 452 key_version: reference.key_version().get(), 453 } 454 } 455 456 const fn backend_failure(operation: Operation) -> Error { 457 Error::BackendFailure { 458 backend: BackendKind::File, 459 operation, 460 } 461 } 462 463 #[cfg(unix)] 464 fn set_directory_permissions(path: &Path) -> Result<(), Error> { 465 use std::os::unix::fs::PermissionsExt; 466 fs::set_permissions(path, fs::Permissions::from_mode(0o700)) 467 .map_err(|_| backend_failure(Operation::Open)) 468 } 469 470 #[cfg(not(unix))] 471 fn set_directory_permissions(_path: &Path) -> Result<(), Error> { 472 Ok(()) 473 } 474 475 #[cfg(unix)] 476 fn validate_directory_permissions(metadata: &fs::Metadata) -> Result<(), Error> { 477 use std::os::unix::fs::PermissionsExt; 478 if metadata.permissions().mode() & 0o077 != 0 { 479 return Err(Error::InsecurePermissions); 480 } 481 Ok(()) 482 } 483 484 #[cfg(not(unix))] 485 fn validate_directory_permissions(_metadata: &fs::Metadata) -> Result<(), Error> { 486 Ok(()) 487 } 488 489 #[cfg(unix)] 490 fn set_file_permissions(file: &File) -> Result<(), Error> { 491 use std::os::unix::fs::PermissionsExt; 492 file.set_permissions(fs::Permissions::from_mode(0o600)) 493 .map_err(|_| backend_failure(Operation::Write)) 494 } 495 496 #[cfg(not(unix))] 497 fn set_file_permissions(_file: &File) -> Result<(), Error> { 498 Ok(()) 499 } 500 501 #[cfg(unix)] 502 fn validate_file_permissions(metadata: &fs::Metadata) -> Result<(), Error> { 503 use std::os::unix::fs::PermissionsExt; 504 if metadata.permissions().mode() & 0o077 != 0 { 505 return Err(Error::InsecurePermissions); 506 } 507 Ok(()) 508 } 509 510 #[cfg(not(unix))] 511 fn validate_file_permissions(_metadata: &fs::Metadata) -> Result<(), Error> { 512 Ok(()) 513 } 514 515 #[cfg(unix)] 516 fn sync_directory(root: &Path) -> Result<(), Error> { 517 File::open(root) 518 .and_then(|directory| directory.sync_all()) 519 .map_err(|_| backend_failure(Operation::Write)) 520 } 521 522 #[cfg(not(unix))] 523 fn sync_directory(_root: &Path) -> Result<(), Error> { 524 Ok(()) 525 }