lib

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

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;