lib

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

keyring.rs (17924B)


      1 //! Explicit operating-system keyring adapters.
      2 
      3 use crate::SecretRef;
      4 use crate::error::{Error, Operation};
      5 use crate::id::BackendKind;
      6 use crate::provider::{CapabilitySupport, ResidencySupport, SecretCapabilities, SecretProvider};
      7 use crate::wrapping::{
      8     BoxFuture, KeyWrapping, LegacyV1UnwrapRequest, SecretMaterial, UnwrapRequest, WrapRequest,
      9     WrappedSecret,
     10 };
     11 use alloc::boxed::Box;
     12 use alloc::string::{String, ToString};
     13 use alloc::vec::Vec;
     14 use core::fmt;
     15 use std::sync::Mutex;
     16 use zeroize::Zeroizing;
     17 
     18 const SERVICE_NAME_MAX_BYTES: usize = 128;
     19 const VALUE_PREFIX: &str = "radroots-keyring-v1:";
     20 const TOKEN_PREFIX: &[u8] = b"radroots-keyring-key-v1\0";
     21 
     22 /// Explicit OS keyring provider with lazy credential access.
     23 pub struct KeyringProvider {
     24     service_name: String,
     25     store: Box<dyn CredentialStore>,
     26     operation_lock: Mutex<()>,
     27 }
     28 
     29 impl KeyringProvider {
     30     /// Creates a provider without reading or writing the operating-system keyring.
     31     pub fn new(service_name: impl AsRef<str>) -> Result<Self, Error> {
     32         let service_name = validate_service_name(service_name.as_ref())?;
     33         Ok(Self {
     34             service_name,
     35             store: Box::new(OsCredentialStore),
     36             operation_lock: Mutex::new(()),
     37         })
     38     }
     39 
     40     /// Stores caller-owned material at one exact key version.
     41     pub fn provision(&self, reference: &SecretRef, material: &SecretMaterial) -> Result<(), Error> {
     42         validate_keyring_reference(reference)?;
     43         let _guard = self
     44             .operation_lock
     45             .lock()
     46             .map_err(|_| backend_failure(Operation::Provision))?;
     47         let account = account_name(reference);
     48         match self.store.get(&self.service_name, account.as_str()) {
     49             Ok(_) => {
     50                 return Err(Error::SecretAlreadyExists {
     51                     backend: BackendKind::Keyring,
     52                     key_version: reference.key_version().get(),
     53                 });
     54             }
     55             Err(StoreError::Missing) => {}
     56             Err(StoreError::Failure) => return Err(backend_failure(Operation::Provision)),
     57         }
     58         let encoded = encode_material(material);
     59         self.store
     60             .set(&self.service_name, account.as_str(), encoded.as_str())
     61             .map_err(|_| backend_failure(Operation::Provision))
     62     }
     63 
     64     /// Resumes safely when the new version exists before old-version removal.
     65     pub fn rotate(
     66         &self,
     67         current: &SecretRef,
     68         next: &SecretRef,
     69         material: &SecretMaterial,
     70     ) -> Result<(), Error> {
     71         validate_keyring_reference(current)?;
     72         validate_keyring_reference(next)?;
     73         if current.id().as_str() != next.id().as_str()
     74             || next.key_version() <= current.key_version()
     75         {
     76             return Err(Error::InvalidRotation);
     77         }
     78         self.read_material(current)?;
     79         match self.read_material(next) {
     80             Ok(persisted) => {
     81                 let matches = persisted
     82                     .expose_secret(|expected| material.expose_secret(|actual| expected == actual));
     83                 if !matches {
     84                     return Err(Error::SecretAlreadyExists {
     85                         backend: BackendKind::Keyring,
     86                         key_version: next.key_version().get(),
     87                     });
     88                 }
     89             }
     90             Err(Error::SecretNotFound { .. }) => self.provision(next, material)?,
     91             Err(error) => return Err(error),
     92         }
     93         self.remove(current)?;
     94         Ok(())
     95     }
     96 
     97     /// Deletes one exact key revision when present.
     98     pub fn remove(&self, reference: &SecretRef) -> Result<bool, Error> {
     99         validate_keyring_reference(reference)?;
    100         let _guard = self
    101             .operation_lock
    102             .lock()
    103             .map_err(|_| backend_failure(Operation::Remove))?;
    104         let account = account_name(reference);
    105         match self.store.delete(&self.service_name, account.as_str()) {
    106             Ok(()) => Ok(true),
    107             Err(StoreError::Missing) => Ok(false),
    108             Err(StoreError::Failure) => Err(backend_failure(Operation::Remove)),
    109         }
    110     }
    111 
    112     fn read_material(&self, reference: &SecretRef) -> Result<SecretMaterial, Error> {
    113         let account = account_name(reference);
    114         let encoded = match self.store.get(&self.service_name, account.as_str()) {
    115             Ok(value) => Zeroizing::new(value),
    116             Err(StoreError::Missing) => return Err(not_found(reference)),
    117             Err(StoreError::Failure) => return Err(backend_failure(Operation::Read)),
    118         };
    119         decode_material(encoded.as_str())
    120     }
    121 
    122     #[cfg(test)]
    123     fn with_store(service_name: &str, store: Box<dyn CredentialStore>) -> Result<Self, Error> {
    124         Ok(Self {
    125             service_name: validate_service_name(service_name)?,
    126             store,
    127             operation_lock: Mutex::new(()),
    128         })
    129     }
    130 }
    131 
    132 impl fmt::Debug for KeyringProvider {
    133     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    134         formatter.write_str("KeyringProvider(<redacted>)")
    135     }
    136 }
    137 
    138 impl KeyWrapping for KeyringProvider {
    139     fn wrap<'a>(&'a self, request: WrapRequest<'a>) -> BoxFuture<'a, Result<WrappedSecret, Error>> {
    140         Box::pin(async move {
    141             validate_keyring_reference(request.reference())?;
    142             let persisted = self.read_material(request.reference())?;
    143             let matches = persisted.expose_secret(|expected| {
    144                 request
    145                     .plaintext()
    146                     .expose_secret(|actual| expected == actual)
    147             });
    148             if !matches {
    149                 return Err(backend_failure(Operation::Wrap));
    150             }
    151             WrappedSecret::from_bytes(wrapping_token(request.reference(), request.context()))
    152         })
    153     }
    154 
    155     fn unwrap<'a>(
    156         &'a self,
    157         request: UnwrapRequest<'a>,
    158     ) -> BoxFuture<'a, Result<SecretMaterial, Error>> {
    159         Box::pin(async move {
    160             validate_keyring_reference(request.reference())?;
    161             if request.wrapped().as_bytes()
    162                 != wrapping_token(request.reference(), request.context()).as_slice()
    163             {
    164                 return Err(backend_failure(Operation::Unwrap));
    165             }
    166             self.read_material(request.reference())
    167         })
    168     }
    169 
    170     fn unwrap_legacy_v1<'a>(
    171         &'a self,
    172         request: LegacyV1UnwrapRequest<'a>,
    173     ) -> BoxFuture<'a, Result<SecretMaterial, Error>> {
    174         Box::pin(async move {
    175             validate_keyring_reference(request.reference())?;
    176             if request.wrapped().as_bytes() != reference_token(request.reference()).as_slice() {
    177                 return Err(backend_failure(Operation::Unwrap));
    178             }
    179             self.read_material(request.reference())
    180         })
    181     }
    182 }
    183 
    184 fn wrapping_token(reference: &SecretRef, context: &crate::context::EnvelopeContext) -> Vec<u8> {
    185     let mut token = reference_token(reference);
    186     token.extend_from_slice(&context.authentication_digest());
    187     token
    188 }
    189 
    190 impl SecretProvider for KeyringProvider {
    191     fn backend_kind(&self) -> BackendKind {
    192         BackendKind::Keyring
    193     }
    194 
    195     fn capabilities(&self) -> SecretCapabilities {
    196         SecretCapabilities::available(
    197             ResidencySupport::UserProfile,
    198             CapabilitySupport::Unavailable,
    199             CapabilitySupport::Unavailable,
    200         )
    201     }
    202 }
    203 
    204 trait CredentialStore: Send + Sync {
    205     fn set(&self, service: &str, account: &str, value: &str) -> Result<(), StoreError>;
    206     fn get(&self, service: &str, account: &str) -> Result<String, StoreError>;
    207     fn delete(&self, service: &str, account: &str) -> Result<(), StoreError>;
    208 }
    209 
    210 struct OsCredentialStore;
    211 
    212 impl CredentialStore for OsCredentialStore {
    213     fn set(&self, service: &str, account: &str, value: &str) -> Result<(), StoreError> {
    214         let entry = keyring::Entry::new(service, account).map_err(|_| StoreError::Failure)?;
    215         entry.set_password(value).map_err(|_| StoreError::Failure)
    216     }
    217 
    218     fn get(&self, service: &str, account: &str) -> Result<String, StoreError> {
    219         let entry = keyring::Entry::new(service, account).map_err(|_| StoreError::Failure)?;
    220         match entry.get_password() {
    221             Ok(value) => Ok(value),
    222             Err(keyring::Error::NoEntry) => Err(StoreError::Missing),
    223             Err(_) => Err(StoreError::Failure),
    224         }
    225     }
    226 
    227     fn delete(&self, service: &str, account: &str) -> Result<(), StoreError> {
    228         let entry = keyring::Entry::new(service, account).map_err(|_| StoreError::Failure)?;
    229         match entry.delete_credential() {
    230             Ok(()) => Ok(()),
    231             Err(keyring::Error::NoEntry) => Err(StoreError::Missing),
    232             Err(_) => Err(StoreError::Failure),
    233         }
    234     }
    235 }
    236 
    237 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    238 enum StoreError {
    239     Missing,
    240     Failure,
    241 }
    242 
    243 fn validate_service_name(value: &str) -> Result<String, Error> {
    244     if value.is_empty()
    245         || value.len() > SERVICE_NAME_MAX_BYTES
    246         || !value.chars().enumerate().all(|(index, character)| {
    247             character.is_ascii_alphanumeric() || (index > 0 && matches!(character, '.' | '_' | '-'))
    248         })
    249     {
    250         return Err(Error::InvalidServiceName);
    251     }
    252     Ok(value.to_string())
    253 }
    254 
    255 fn validate_keyring_reference(reference: &SecretRef) -> Result<(), Error> {
    256     if reference.backend() != BackendKind::Keyring {
    257         return Err(Error::BackendMismatch {
    258             provider: BackendKind::Keyring,
    259             reference: reference.backend(),
    260         });
    261     }
    262     Ok(())
    263 }
    264 
    265 fn account_name(reference: &SecretRef) -> String {
    266     alloc::format!(
    267         "{}.v{}",
    268         hex_encode(reference.id().as_str().as_bytes()),
    269         reference.key_version().get()
    270     )
    271 }
    272 
    273 fn reference_token(reference: &SecretRef) -> Vec<u8> {
    274     let mut token = Vec::from(TOKEN_PREFIX);
    275     token.extend_from_slice(&reference.key_version().get().to_be_bytes());
    276     token.extend_from_slice(reference.id().as_str().as_bytes());
    277     token
    278 }
    279 
    280 fn encode_material(material: &SecretMaterial) -> Zeroizing<String> {
    281     material.expose_secret(|bytes| {
    282         let mut encoded = String::with_capacity(VALUE_PREFIX.len() + bytes.len() * 2);
    283         encoded.push_str(VALUE_PREFIX);
    284         encoded.push_str(hex_encode(bytes).as_str());
    285         Zeroizing::new(encoded)
    286     })
    287 }
    288 
    289 fn decode_material(encoded: &str) -> Result<SecretMaterial, Error> {
    290     let hex = encoded
    291         .strip_prefix(VALUE_PREFIX)
    292         .ok_or_else(|| backend_failure(Operation::Read))?;
    293     let decoded = Zeroizing::new(hex_decode(hex)?);
    294     SecretMaterial::from_slice(decoded.as_slice())
    295 }
    296 
    297 fn hex_encode(bytes: &[u8]) -> String {
    298     const HEX: &[u8; 16] = b"0123456789abcdef";
    299     let mut encoded = String::with_capacity(bytes.len() * 2);
    300     for byte in bytes {
    301         encoded.push(char::from(HEX[usize::from(byte >> 4)]));
    302         encoded.push(char::from(HEX[usize::from(byte & 0x0f)]));
    303     }
    304     encoded
    305 }
    306 
    307 fn hex_decode(value: &str) -> Result<Vec<u8>, Error> {
    308     if value.is_empty() || !value.len().is_multiple_of(2) {
    309         return Err(backend_failure(Operation::Read));
    310     }
    311     value
    312         .as_bytes()
    313         .chunks_exact(2)
    314         .map(|pair| {
    315             let high = hex_nibble(pair[0])?;
    316             let low = hex_nibble(pair[1])?;
    317             Ok((high << 4) | low)
    318         })
    319         .collect()
    320 }
    321 
    322 const fn hex_nibble(value: u8) -> Result<u8, Error> {
    323     match value {
    324         b'0'..=b'9' => Ok(value - b'0'),
    325         b'a'..=b'f' => Ok(value - b'a' + 10),
    326         _ => Err(backend_failure(Operation::Read)),
    327     }
    328 }
    329 
    330 fn not_found(reference: &SecretRef) -> Error {
    331     Error::SecretNotFound {
    332         backend: BackendKind::Keyring,
    333         key_version: reference.key_version().get(),
    334     }
    335 }
    336 
    337 const fn backend_failure(operation: Operation) -> Error {
    338     Error::BackendFailure {
    339         backend: BackendKind::Keyring,
    340         operation,
    341     }
    342 }
    343 
    344 #[cfg(test)]
    345 mod tests {
    346     use super::*;
    347     use crate::SecretId;
    348     use crate::context::{EnvelopeContext, EnvelopePurpose, EnvelopeSubject, PayloadSchemaId};
    349     use crate::id::KeyVersion;
    350     use alloc::collections::BTreeMap;
    351     use std::sync::Arc;
    352 
    353     #[derive(Default)]
    354     struct MockState {
    355         values: BTreeMap<(String, String), String>,
    356         calls: usize,
    357         fail: bool,
    358     }
    359 
    360     #[derive(Clone, Default)]
    361     struct MockStore {
    362         state: Arc<Mutex<MockState>>,
    363     }
    364 
    365     impl CredentialStore for MockStore {
    366         fn set(&self, service: &str, account: &str, value: &str) -> Result<(), StoreError> {
    367             let mut state = self.state.lock().map_err(|_| StoreError::Failure)?;
    368             state.calls += 1;
    369             if state.fail {
    370                 return Err(StoreError::Failure);
    371             }
    372             state.values.insert(
    373                 (service.to_string(), account.to_string()),
    374                 value.to_string(),
    375             );
    376             Ok(())
    377         }
    378 
    379         fn get(&self, service: &str, account: &str) -> Result<String, StoreError> {
    380             let mut state = self.state.lock().map_err(|_| StoreError::Failure)?;
    381             state.calls += 1;
    382             if state.fail {
    383                 return Err(StoreError::Failure);
    384             }
    385             state
    386                 .values
    387                 .get(&(service.to_string(), account.to_string()))
    388                 .cloned()
    389                 .ok_or(StoreError::Missing)
    390         }
    391 
    392         fn delete(&self, service: &str, account: &str) -> Result<(), StoreError> {
    393             let mut state = self.state.lock().map_err(|_| StoreError::Failure)?;
    394             state.calls += 1;
    395             if state.fail {
    396                 return Err(StoreError::Failure);
    397             }
    398             state
    399                 .values
    400                 .remove(&(service.to_string(), account.to_string()))
    401                 .map(|_| ())
    402                 .ok_or(StoreError::Missing)
    403         }
    404     }
    405 
    406     fn reference(id: &str, version: u32) -> SecretRef {
    407         SecretRef::new(
    408             SecretId::parse(id).expect("valid id"),
    409             BackendKind::Keyring,
    410             KeyVersion::new(version).expect("version"),
    411         )
    412     }
    413 
    414     fn context() -> EnvelopeContext {
    415         EnvelopeContext::new(
    416             EnvelopePurpose::parse("radroots.keyring_test").expect("purpose"),
    417             EnvelopeSubject::parse("keyring_test", "fixture").expect("subject"),
    418             PayloadSchemaId::parse("radroots.keyring_test.v1").expect("schema"),
    419         )
    420     }
    421 
    422     fn different_context() -> EnvelopeContext {
    423         EnvelopeContext::new(
    424             EnvelopePurpose::parse("radroots.keyring_test").expect("purpose"),
    425             EnvelopeSubject::parse("keyring_test", "different").expect("subject"),
    426             PayloadSchemaId::parse("radroots.keyring_test.v1").expect("schema"),
    427         )
    428     }
    429 
    430     #[test]
    431     fn construction_has_no_store_side_effect() {
    432         let store = MockStore::default();
    433         let state = Arc::clone(&store.state);
    434         let provider =
    435             KeyringProvider::with_store("org.radroots.test", Box::new(store)).expect("provider");
    436         assert_eq!(state.lock().expect("state").calls, 0);
    437         assert_eq!(provider.backend_kind(), BackendKind::Keyring);
    438         assert_eq!(format!("{provider:?}"), "KeyringProvider(<redacted>)");
    439     }
    440 
    441     #[test]
    442     fn mock_store_round_trip_missing_rotation_and_removal() {
    443         let store = MockStore::default();
    444         let provider =
    445             KeyringProvider::with_store("org.radroots.test", Box::new(store)).expect("provider");
    446         let current = reference("keyring-key", 1);
    447         let next = reference("keyring-key", 2);
    448         let current_material = SecretMaterial::from_slice(&[0x11; 32]).expect("material");
    449         let next_material = SecretMaterial::from_slice(&[0x22; 32]).expect("material");
    450         let context = context();
    451 
    452         assert!(matches!(
    453             futures_executor::block_on(provider.wrap(WrapRequest::new(
    454                 &current,
    455                 &context,
    456                 &current_material
    457             ))),
    458             Err(Error::SecretNotFound { .. })
    459         ));
    460         provider
    461             .provision(&current, &current_material)
    462             .expect("provision");
    463         let wrapped = futures_executor::block_on(provider.wrap(WrapRequest::new(
    464             &current,
    465             &context,
    466             &current_material,
    467         )))
    468         .expect("wrap");
    469         let opened = futures_executor::block_on(
    470             provider.unwrap(UnwrapRequest::new(&current, &context, &wrapped)),
    471         )
    472         .expect("unwrap");
    473         opened.expose_secret(|bytes| assert_eq!(bytes, &[0x11; 32]));
    474         assert!(matches!(
    475             futures_executor::block_on(provider.unwrap(UnwrapRequest::new(
    476                 &current,
    477                 &different_context(),
    478                 &wrapped,
    479             ))),
    480             Err(Error::BackendFailure {
    481                 backend: BackendKind::Keyring,
    482                 operation: Operation::Unwrap,
    483             })
    484         ));
    485 
    486         provider
    487             .rotate(&current, &next, &next_material)
    488             .expect("rotate");
    489         assert!(matches!(
    490             provider.read_material(&current),
    491             Err(Error::SecretNotFound { .. })
    492         ));
    493         assert!(provider.read_material(&next).is_ok());
    494         assert!(provider.remove(&next).expect("remove"));
    495         assert!(!provider.remove(&next).expect("idempotent remove"));
    496     }
    497 
    498     #[test]
    499     fn native_store_failures_are_normalized() {
    500         let store = MockStore::default();
    501         store.state.lock().expect("state").fail = true;
    502         let provider =
    503             KeyringProvider::with_store("org.radroots.test", Box::new(store)).expect("provider");
    504         let reference = reference("keyring-key", 1);
    505         let material = SecretMaterial::from_slice(&[0x11; 32]).expect("material");
    506         assert_eq!(
    507             provider.provision(&reference, &material),
    508             Err(backend_failure(Operation::Provision))
    509         );
    510     }
    511 }