selection.rs (11075B)
1 use crate::backend::{RadrootsSecretBackend, RadrootsSecretBackendKind}; 2 use crate::error::RadrootsSecretVaultError; 3 use crate::policy::RadrootsHostVaultCapabilities; 4 5 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 6 pub struct RadrootsSecretBackendSelection { 7 pub primary: RadrootsSecretBackend, 8 pub fallback: Option<RadrootsSecretBackend>, 9 } 10 11 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 12 pub struct RadrootsSecretBackendAvailability { 13 pub host_vault: RadrootsHostVaultCapabilities, 14 pub encrypted_file: bool, 15 pub external_command: bool, 16 pub memory: bool, 17 } 18 19 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 20 pub struct RadrootsResolvedSecretBackend { 21 pub backend: RadrootsSecretBackend, 22 pub used_fallback: bool, 23 } 24 25 impl RadrootsSecretBackendSelection { 26 pub fn resolve( 27 self, 28 availability: RadrootsSecretBackendAvailability, 29 ) -> Result<RadrootsResolvedSecretBackend, RadrootsSecretVaultError> { 30 if availability.supports(self.primary).is_ok() { 31 return Ok(RadrootsResolvedSecretBackend { 32 backend: self.primary, 33 used_fallback: false, 34 }); 35 } 36 37 if let RadrootsSecretBackend::HostVault(policy) = self.primary 38 && availability.host_vault.available 39 { 40 availability.host_vault.validate(policy)?; 41 } 42 43 match self.fallback { 44 Some(fallback) => { 45 if !self.primary.allows_fallback_to(fallback.kind()) { 46 return Err(RadrootsSecretVaultError::FallbackDisallowed { 47 primary: self.primary.kind(), 48 fallback: fallback.kind(), 49 }); 50 } 51 52 availability.supports(fallback).map_err(|_| { 53 RadrootsSecretVaultError::FallbackUnavailable { 54 primary: self.primary.kind(), 55 fallback: fallback.kind(), 56 } 57 })?; 58 59 Ok(RadrootsResolvedSecretBackend { 60 backend: fallback, 61 used_fallback: true, 62 }) 63 } 64 None => Err(RadrootsSecretVaultError::BackendUnavailable { 65 backend: self.primary.kind(), 66 }), 67 } 68 } 69 } 70 71 impl RadrootsSecretBackendAvailability { 72 fn supports(self, backend: RadrootsSecretBackend) -> Result<(), RadrootsSecretVaultError> { 73 match backend { 74 RadrootsSecretBackend::HostVault(policy) => self.host_vault.validate(policy), 75 RadrootsSecretBackend::EncryptedFile if self.encrypted_file => Ok(()), 76 RadrootsSecretBackend::ExternalCommand if self.external_command => Ok(()), 77 RadrootsSecretBackend::Memory if self.memory => Ok(()), 78 _ => Err(RadrootsSecretVaultError::BackendUnavailable { 79 backend: backend.kind(), 80 }), 81 } 82 } 83 } 84 85 impl RadrootsSecretBackend { 86 const fn allows_fallback_to(self, fallback: RadrootsSecretBackendKind) -> bool { 87 matches!( 88 (self.kind(), fallback), 89 ( 90 RadrootsSecretBackendKind::HostVault, 91 RadrootsSecretBackendKind::EncryptedFile 92 ) 93 ) 94 } 95 } 96 97 #[cfg(test)] 98 mod tests { 99 use super::*; 100 use crate::error::RadrootsHostVaultRequirement; 101 use crate::policy::{ 102 RadrootsHostVaultHardwarePolicy, RadrootsHostVaultPolicy, RadrootsHostVaultResidency, 103 RadrootsHostVaultUserPresencePolicy, 104 }; 105 106 #[test] 107 fn host_vault_is_selected_when_available() { 108 let selection = RadrootsSecretBackendSelection { 109 primary: RadrootsSecretBackend::HostVault(RadrootsHostVaultPolicy::desktop()), 110 fallback: Some(RadrootsSecretBackend::EncryptedFile), 111 }; 112 113 let resolved = selection 114 .resolve(RadrootsSecretBackendAvailability { 115 host_vault: RadrootsHostVaultCapabilities::desktop_keyring(), 116 encrypted_file: true, 117 external_command: false, 118 memory: false, 119 }) 120 .expect("host vault resolves"); 121 122 assert_eq!( 123 resolved, 124 RadrootsResolvedSecretBackend { 125 backend: RadrootsSecretBackend::HostVault(RadrootsHostVaultPolicy::desktop()), 126 used_fallback: false, 127 } 128 ); 129 } 130 131 #[test] 132 fn host_vault_may_explicitly_fallback_to_encrypted_file() { 133 let selection = RadrootsSecretBackendSelection { 134 primary: RadrootsSecretBackend::HostVault(RadrootsHostVaultPolicy::desktop()), 135 fallback: Some(RadrootsSecretBackend::EncryptedFile), 136 }; 137 138 let resolved = selection 139 .resolve(RadrootsSecretBackendAvailability { 140 host_vault: RadrootsHostVaultCapabilities::unavailable(), 141 encrypted_file: true, 142 external_command: false, 143 memory: false, 144 }) 145 .expect("encrypted file fallback resolves"); 146 147 assert_eq!( 148 resolved, 149 RadrootsResolvedSecretBackend { 150 backend: RadrootsSecretBackend::EncryptedFile, 151 used_fallback: true, 152 } 153 ); 154 } 155 156 #[test] 157 fn host_vault_without_explicit_fallback_fails_closed() { 158 let selection = RadrootsSecretBackendSelection { 159 primary: RadrootsSecretBackend::HostVault(RadrootsHostVaultPolicy::desktop()), 160 fallback: None, 161 }; 162 163 let err = selection 164 .resolve(RadrootsSecretBackendAvailability { 165 host_vault: RadrootsHostVaultCapabilities::unavailable(), 166 encrypted_file: true, 167 external_command: false, 168 memory: false, 169 }) 170 .expect_err("missing fallback must fail"); 171 172 assert_eq!( 173 err, 174 RadrootsSecretVaultError::BackendUnavailable { 175 backend: RadrootsSecretBackendKind::HostVault, 176 } 177 ); 178 } 179 180 #[test] 181 fn unsupported_host_vault_policy_fails_before_any_downgrade() { 182 let selection = RadrootsSecretBackendSelection { 183 primary: RadrootsSecretBackend::HostVault(RadrootsHostVaultPolicy { 184 residency: RadrootsHostVaultResidency::DeviceLocalOnly, 185 user_presence: RadrootsHostVaultUserPresencePolicy::Required, 186 hardware: RadrootsHostVaultHardwarePolicy::RequireHardwareBacked, 187 }), 188 fallback: Some(RadrootsSecretBackend::EncryptedFile), 189 }; 190 191 let err = selection 192 .resolve(RadrootsSecretBackendAvailability { 193 host_vault: RadrootsHostVaultCapabilities::desktop_keyring(), 194 encrypted_file: true, 195 external_command: false, 196 memory: false, 197 }) 198 .expect_err("unsupported host policy must fail"); 199 200 assert_eq!( 201 err, 202 RadrootsSecretVaultError::HostVaultPolicyUnsupported { 203 requirement: RadrootsHostVaultRequirement::DeviceLocalOnly, 204 } 205 ); 206 } 207 208 #[test] 209 fn external_command_may_not_downgrade_to_encrypted_file() { 210 let selection = RadrootsSecretBackendSelection { 211 primary: RadrootsSecretBackend::ExternalCommand, 212 fallback: Some(RadrootsSecretBackend::EncryptedFile), 213 }; 214 215 let err = selection 216 .resolve(RadrootsSecretBackendAvailability { 217 host_vault: RadrootsHostVaultCapabilities::unavailable(), 218 encrypted_file: true, 219 external_command: false, 220 memory: false, 221 }) 222 .expect_err("external command downgrade must fail"); 223 224 assert_eq!( 225 err, 226 RadrootsSecretVaultError::FallbackDisallowed { 227 primary: RadrootsSecretBackendKind::ExternalCommand, 228 fallback: RadrootsSecretBackendKind::EncryptedFile, 229 } 230 ); 231 } 232 233 #[test] 234 fn external_command_resolves_when_available() { 235 let selection = RadrootsSecretBackendSelection { 236 primary: RadrootsSecretBackend::ExternalCommand, 237 fallback: None, 238 }; 239 240 let resolved = selection 241 .resolve(RadrootsSecretBackendAvailability { 242 host_vault: RadrootsHostVaultCapabilities::unavailable(), 243 encrypted_file: false, 244 external_command: true, 245 memory: false, 246 }) 247 .expect("external command resolves"); 248 249 assert_eq!( 250 resolved, 251 RadrootsResolvedSecretBackend { 252 backend: RadrootsSecretBackend::ExternalCommand, 253 used_fallback: false, 254 } 255 ); 256 } 257 258 #[test] 259 fn memory_backend_must_be_selected_explicitly() { 260 let selection = RadrootsSecretBackendSelection { 261 primary: RadrootsSecretBackend::Memory, 262 fallback: None, 263 }; 264 265 let resolved = selection 266 .resolve(RadrootsSecretBackendAvailability { 267 host_vault: RadrootsHostVaultCapabilities::unavailable(), 268 encrypted_file: false, 269 external_command: false, 270 memory: true, 271 }) 272 .expect("memory backend resolves"); 273 274 assert_eq!( 275 resolved, 276 RadrootsResolvedSecretBackend { 277 backend: RadrootsSecretBackend::Memory, 278 used_fallback: false, 279 } 280 ); 281 282 let err = selection 283 .resolve(RadrootsSecretBackendAvailability { 284 host_vault: RadrootsHostVaultCapabilities::unavailable(), 285 encrypted_file: false, 286 external_command: false, 287 memory: false, 288 }) 289 .expect_err("unavailable memory backend must fail"); 290 291 assert_eq!( 292 err, 293 RadrootsSecretVaultError::BackendUnavailable { 294 backend: RadrootsSecretBackendKind::Memory, 295 } 296 ); 297 } 298 299 #[test] 300 fn unavailable_explicit_fallback_reports_fallback_unavailable() { 301 let selection = RadrootsSecretBackendSelection { 302 primary: RadrootsSecretBackend::HostVault(RadrootsHostVaultPolicy::desktop()), 303 fallback: Some(RadrootsSecretBackend::EncryptedFile), 304 }; 305 306 let err = selection 307 .resolve(RadrootsSecretBackendAvailability { 308 host_vault: RadrootsHostVaultCapabilities::unavailable(), 309 encrypted_file: false, 310 external_command: false, 311 memory: false, 312 }) 313 .expect_err("unavailable fallback must fail"); 314 315 assert_eq!( 316 err, 317 RadrootsSecretVaultError::FallbackUnavailable { 318 primary: RadrootsSecretBackendKind::HostVault, 319 fallback: RadrootsSecretBackendKind::EncryptedFile, 320 } 321 ); 322 } 323 }