lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

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 }