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 ¤t, 455 &context, 456 ¤t_material 457 ))), 458 Err(Error::SecretNotFound { .. }) 459 )); 460 provider 461 .provision(¤t, ¤t_material) 462 .expect("provision"); 463 let wrapped = futures_executor::block_on(provider.wrap(WrapRequest::new( 464 ¤t, 465 &context, 466 ¤t_material, 467 ))) 468 .expect("wrap"); 469 let opened = futures_executor::block_on( 470 provider.unwrap(UnwrapRequest::new(¤t, &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 ¤t, 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(¤t, &next, &next_material) 488 .expect("rotate"); 489 assert!(matches!( 490 provider.read_material(¤t), 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 }