1use std::collections::BTreeMap;
2use std::fmt;
3use std::sync::Arc;
4use std::time::Duration;
5
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use zeroize::{Zeroize, Zeroizing};
9
10mod env;
11mod keyring;
12
13pub use env::EnvSecretProvider;
14pub use keyring::KeyringSecretProvider;
15
16pub const DEFAULT_SECRET_PROVIDER_CHAIN: &str = "env,keyring";
17pub const SECRET_PROVIDER_CHAIN_ENV: &str = "HARN_SECRET_PROVIDERS";
18
19#[derive(Clone, Debug, Default, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
20pub enum SecretVersion {
21 #[default]
22 Latest,
23 Exact(u64),
24}
25
26#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
27pub struct SecretId {
28 pub namespace: String,
29 pub name: String,
30 #[serde(default)]
31 pub version: SecretVersion,
32}
33
34impl SecretId {
35 pub fn new(namespace: impl Into<String>, name: impl Into<String>) -> Self {
36 Self {
37 namespace: namespace.into(),
38 name: name.into(),
39 version: SecretVersion::Latest,
40 }
41 }
42
43 pub fn with_version(mut self, version: SecretVersion) -> Self {
44 self.version = version;
45 self
46 }
47}
48
49impl fmt::Display for SecretId {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 if self.namespace.is_empty() {
52 write!(f, "{}", self.name)?;
53 } else {
54 write!(f, "{}/{}", self.namespace, self.name)?;
55 }
56 match self.version {
57 SecretVersion::Latest => Ok(()),
58 SecretVersion::Exact(version) => write!(f, "@{version}"),
59 }
60 }
61}
62
63#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
64pub struct SecretMeta {
65 pub id: SecretId,
66 pub provider: String,
67}
68
69#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
70pub struct RotationHandle {
71 pub provider: String,
72 pub id: SecretId,
73 pub from_version: Option<u64>,
74 pub to_version: Option<u64>,
75}
76
77#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
78#[serde(rename_all = "snake_case")]
79pub enum SecretScope {
80 Tenant { id: Option<String> },
81 Workspace { id: String },
82 System,
83 Custom { kind: String, id: Option<String> },
84}
85
86impl Default for SecretScope {
87 fn default() -> Self {
88 Self::Tenant { id: None }
89 }
90}
91
92impl SecretScope {
93 pub fn tenant(id: Option<String>) -> Self {
94 Self::Tenant { id }
95 }
96
97 pub fn workspace(id: impl Into<String>) -> Self {
98 Self::Workspace { id: id.into() }
99 }
100
101 pub fn system() -> Self {
102 Self::System
103 }
104
105 pub fn custom(kind: impl Into<String>, id: Option<String>) -> Self {
106 Self::Custom {
107 kind: kind.into(),
108 id,
109 }
110 }
111
112 pub fn namespace(&self) -> String {
113 match self {
114 Self::Tenant { id: Some(id) } if !id.is_empty() => format!("harn.tenant.{id}"),
115 Self::Tenant { .. } => "harn.tenant".to_string(),
116 Self::Workspace { id } => format!("harn.workspace.{id}"),
117 Self::System => "harn.system".to_string(),
118 Self::Custom { kind, id: Some(id) } if !id.is_empty() => {
119 format!("harn.{kind}.{id}")
120 }
121 Self::Custom { kind, .. } => format!("harn.{kind}"),
122 }
123 }
124
125 pub fn kind(&self) -> &str {
126 match self {
127 Self::Tenant { .. } => "tenant",
128 Self::Workspace { .. } => "workspace",
129 Self::System => "system",
130 Self::Custom { kind, .. } => kind.as_str(),
131 }
132 }
133
134 pub fn id(&self) -> Option<&str> {
135 match self {
136 Self::Tenant { id } | Self::Custom { id, .. } => id.as_deref(),
137 Self::Workspace { id } => Some(id.as_str()),
138 Self::System => None,
139 }
140 }
141}
142
143#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
144pub struct SecretWriteOptions {
145 pub ttl: Option<Duration>,
146}
147
148#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
149pub struct SecretRotationOptions {
150 pub grace: Option<Duration>,
151 pub ttl: Option<Duration>,
152}
153
154#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
155pub struct SecretAuditContext {
156 pub request_id: Option<String>,
157 pub actor_subject: Option<String>,
158 pub actor_kind: Option<String>,
159}
160
161#[derive(Debug)]
162pub struct SecretReadRequest {
163 pub id: SecretId,
164 pub scope: SecretScope,
165 pub audit: SecretAuditContext,
166}
167
168#[derive(Debug)]
169pub struct SecretWriteRequest {
170 pub id: SecretId,
171 pub scope: SecretScope,
172 pub value: SecretBytes,
173 pub options: SecretWriteOptions,
174 pub audit: SecretAuditContext,
175}
176
177#[derive(Debug)]
178pub struct SecretRotateRequest {
179 pub id: SecretId,
180 pub scope: SecretScope,
181 pub value: SecretBytes,
182 pub options: SecretRotationOptions,
183 pub audit: SecretAuditContext,
184}
185
186#[derive(Debug)]
187pub struct SecretLeaseRequest {
188 pub id: SecretId,
189 pub scope: SecretScope,
190 pub duration: Duration,
191 pub audit: SecretAuditContext,
192}
193
194#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
195pub struct SecretWriteReceipt {
196 pub provider: String,
197 pub id: SecretId,
198 pub scope: SecretScope,
199 pub version: Option<u64>,
200 pub expires_at_unix_ms: Option<i64>,
201}
202
203#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
204pub struct SecretRotationReceipt {
205 pub provider: String,
206 pub id: SecretId,
207 pub scope: SecretScope,
208 pub from_version: Option<u64>,
209 pub to_version: Option<u64>,
210 pub grace_until_unix_ms: Option<i64>,
211 pub expires_at_unix_ms: Option<i64>,
212}
213
214#[derive(Debug)]
215pub struct SecretLeaseGrant {
216 pub provider: String,
217 pub id: SecretId,
218 pub scope: SecretScope,
219 pub lease_id: String,
220 pub value: SecretBytes,
221 pub expires_at_unix_ms: i64,
222}
223
224#[derive(Clone, Debug, Eq, PartialEq)]
225pub enum SecretError {
226 NotFound {
227 provider: String,
228 id: SecretId,
229 },
230 Unsupported {
231 provider: String,
232 operation: &'static str,
233 },
234 Backend {
235 provider: String,
236 message: String,
237 },
238 InvalidConfig(String),
239 InvalidInput(String),
240 NoProviders {
241 namespace: String,
242 },
243 All(Vec<SecretError>),
244}
245
246impl fmt::Display for SecretError {
247 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248 match self {
249 Self::NotFound { provider, id } => {
250 write!(f, "{provider}: secret '{id}' not found")
251 }
252 Self::Unsupported {
253 provider,
254 operation,
255 } => write!(f, "{provider}: operation '{operation}' is unsupported"),
256 Self::Backend { provider, message } => write!(f, "{provider}: {message}"),
257 Self::InvalidConfig(message) => write!(f, "{message}"),
258 Self::InvalidInput(message) => write!(f, "{message}"),
259 Self::NoProviders { namespace } => {
260 write!(
261 f,
262 "no secret providers configured for namespace '{namespace}'"
263 )
264 }
265 Self::All(errors) => {
266 let rendered = errors
267 .iter()
268 .map(ToString::to_string)
269 .collect::<Vec<_>>()
270 .join("; ");
271 write!(f, "all secret providers failed: {rendered}")
272 }
273 }
274 }
275}
276
277impl std::error::Error for SecretError {}
278
279#[derive(Default)]
280struct SecretBuffer {
281 bytes: Vec<u8>,
282 #[cfg(test)]
283 drop_probe: Option<std::sync::Arc<std::sync::Mutex<Option<Vec<u8>>>>>,
284}
285
286impl SecretBuffer {
287 fn new(bytes: Vec<u8>) -> Self {
288 Self {
289 bytes,
290 #[cfg(test)]
291 drop_probe: None,
292 }
293 }
294
295 fn as_slice(&self) -> &[u8] {
296 &self.bytes
297 }
298
299 #[cfg(test)]
300 fn attach_drop_probe(&mut self, probe: std::sync::Arc<std::sync::Mutex<Option<Vec<u8>>>>) {
301 self.drop_probe = Some(probe);
302 }
303}
304
305impl std::ops::Deref for SecretBuffer {
306 type Target = [u8];
307
308 fn deref(&self) -> &Self::Target {
309 self.as_slice()
310 }
311}
312
313impl Zeroize for SecretBuffer {
314 fn zeroize(&mut self) {
315 self.bytes.zeroize();
316 }
317}
318
319impl Drop for SecretBuffer {
320 fn drop(&mut self) {
321 #[cfg(test)]
322 if let Some(probe) = &self.drop_probe {
323 *probe.lock().expect("drop probe poisoned") = Some(self.bytes.clone());
324 }
325 }
326}
327
328pub struct SecretBytes(Zeroizing<SecretBuffer>);
329
330impl SecretBytes {
331 pub fn new(bytes: Vec<u8>) -> Self {
332 Self(Zeroizing::new(SecretBuffer::new(bytes)))
333 }
334
335 pub fn len(&self) -> usize {
336 self.0.as_slice().len()
337 }
338
339 pub fn is_empty(&self) -> bool {
340 self.0.as_slice().is_empty()
341 }
342
343 pub fn with_exposed<R>(&self, f: impl FnOnce(&[u8]) -> R) -> R {
344 f(self.0.as_slice())
345 }
346
347 pub fn reborrow(&self) -> Self {
348 self.with_exposed(|bytes| Self::new(bytes.to_vec()))
349 }
350
351 #[cfg(test)]
352 pub(crate) fn attach_drop_probe(
353 &mut self,
354 probe: std::sync::Arc<std::sync::Mutex<Option<Vec<u8>>>>,
355 ) {
356 self.0.attach_drop_probe(probe);
357 }
358}
359
360impl fmt::Debug for SecretBytes {
361 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362 write!(f, "SecretBytes {{ redacted: {} bytes }}", self.len())
363 }
364}
365
366impl Serialize for SecretBytes {
367 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
368 where
369 S: serde::Serializer,
370 {
371 serializer.serialize_str(&format!("<redacted:{} bytes>", self.len()))
372 }
373}
374
375impl From<Vec<u8>> for SecretBytes {
376 fn from(value: Vec<u8>) -> Self {
377 Self::new(value)
378 }
379}
380
381impl From<String> for SecretBytes {
382 fn from(value: String) -> Self {
383 Self::new(value.into_bytes())
384 }
385}
386
387impl From<&str> for SecretBytes {
388 fn from(value: &str) -> Self {
389 Self::new(value.as_bytes().to_vec())
390 }
391}
392
393impl From<&[u8]> for SecretBytes {
394 fn from(value: &[u8]) -> Self {
395 Self::new(value.to_vec())
396 }
397}
398
399#[async_trait]
400pub trait SecretProvider: Send + Sync {
401 async fn get(&self, id: &SecretId) -> Result<SecretBytes, SecretError>;
402 async fn put(&self, id: &SecretId, value: SecretBytes) -> Result<(), SecretError>;
403 async fn rotate(&self, id: &SecretId) -> Result<RotationHandle, SecretError>;
404 async fn list(&self, prefix: &SecretId) -> Result<Vec<SecretMeta>, SecretError>;
405
406 async fn read_scoped(&self, request: SecretReadRequest) -> Result<SecretBytes, SecretError> {
407 self.get(&request.id).await
408 }
409
410 async fn write_scoped(
411 &self,
412 request: SecretWriteRequest,
413 ) -> Result<SecretWriteReceipt, SecretError> {
414 if request.options.ttl.is_some() {
415 return Err(SecretError::Unsupported {
416 provider: self.namespace().to_string(),
417 operation: "write_ttl",
418 });
419 }
420 self.put(&request.id, request.value).await?;
421 Ok(SecretWriteReceipt {
422 provider: self.namespace().to_string(),
423 id: request.id,
424 scope: request.scope,
425 version: None,
426 expires_at_unix_ms: None,
427 })
428 }
429
430 async fn rotate_scoped(
431 &self,
432 request: SecretRotateRequest,
433 ) -> Result<SecretRotationReceipt, SecretError> {
434 let _ = request;
435 Err(SecretError::Unsupported {
436 provider: self.namespace().to_string(),
437 operation: "rotate_to",
438 })
439 }
440
441 async fn lease_scoped(
442 &self,
443 request: SecretLeaseRequest,
444 ) -> Result<SecretLeaseGrant, SecretError> {
445 let _ = request;
446 Err(SecretError::Unsupported {
447 provider: self.namespace().to_string(),
448 operation: "lease",
449 })
450 }
451
452 fn namespace(&self) -> &str;
453 fn supports_versions(&self) -> bool;
454}
455
456pub struct ChainSecretProvider {
457 namespace: String,
458 providers: Vec<Arc<dyn SecretProvider>>,
459}
460
461impl ChainSecretProvider {
462 pub fn new(namespace: impl Into<String>, providers: Vec<Arc<dyn SecretProvider>>) -> Self {
463 Self {
464 namespace: namespace.into(),
465 providers,
466 }
467 }
468
469 pub fn providers(&self) -> &[Arc<dyn SecretProvider>] {
470 &self.providers
471 }
472}
473
474#[async_trait]
475impl SecretProvider for ChainSecretProvider {
476 async fn get(&self, id: &SecretId) -> Result<SecretBytes, SecretError> {
477 if self.providers.is_empty() {
478 return Err(SecretError::NoProviders {
479 namespace: self.namespace.clone(),
480 });
481 }
482
483 let mut errors = Vec::new();
484 for provider in &self.providers {
485 match provider.get(id).await {
486 Ok(secret) => return Ok(secret),
487 Err(error) => errors.push(error),
488 }
489 }
490
491 Err(SecretError::All(errors))
492 }
493
494 async fn put(&self, id: &SecretId, value: SecretBytes) -> Result<(), SecretError> {
495 if self.providers.is_empty() {
496 return Err(SecretError::NoProviders {
497 namespace: self.namespace.clone(),
498 });
499 }
500
501 let mut last_value = Some(value);
502 let mut errors = Vec::new();
503 for (index, provider) in self.providers.iter().enumerate() {
504 let attempt_value = if index + 1 == self.providers.len() {
505 last_value
506 .take()
507 .expect("final secret write attempt missing value")
508 } else {
509 last_value
510 .as_ref()
511 .expect("intermediate secret write attempt missing value")
512 .reborrow()
513 };
514 match provider.put(id, attempt_value).await {
515 Ok(()) => return Ok(()),
516 Err(error) => errors.push(error),
517 }
518 }
519
520 Err(SecretError::All(errors))
521 }
522
523 async fn rotate(&self, id: &SecretId) -> Result<RotationHandle, SecretError> {
524 if self.providers.is_empty() {
525 return Err(SecretError::NoProviders {
526 namespace: self.namespace.clone(),
527 });
528 }
529
530 let mut errors = Vec::new();
531 for provider in &self.providers {
532 match provider.rotate(id).await {
533 Ok(handle) => return Ok(handle),
534 Err(error) => errors.push(error),
535 }
536 }
537
538 Err(SecretError::All(errors))
539 }
540
541 async fn list(&self, prefix: &SecretId) -> Result<Vec<SecretMeta>, SecretError> {
542 if self.providers.is_empty() {
543 return Err(SecretError::NoProviders {
544 namespace: self.namespace.clone(),
545 });
546 }
547
548 let mut errors = Vec::new();
549 let mut merged = BTreeMap::<SecretId, SecretMeta>::new();
550 for provider in &self.providers {
551 match provider.list(prefix).await {
552 Ok(items) => {
553 for item in items {
554 merged.entry(item.id.clone()).or_insert(item);
555 }
556 }
557 Err(error) => errors.push(error),
558 }
559 }
560
561 if merged.is_empty() && !errors.is_empty() {
562 return Err(SecretError::All(errors));
563 }
564
565 Ok(merged.into_values().collect())
566 }
567
568 fn namespace(&self) -> &str {
569 &self.namespace
570 }
571
572 fn supports_versions(&self) -> bool {
573 self.providers
574 .iter()
575 .any(|provider| provider.supports_versions())
576 }
577}
578
579pub fn configured_default_chain(
580 namespace: impl Into<String>,
581) -> Result<ChainSecretProvider, SecretError> {
582 let namespace = namespace.into();
583 let configured = std::env::var(SECRET_PROVIDER_CHAIN_ENV)
584 .unwrap_or_else(|_| DEFAULT_SECRET_PROVIDER_CHAIN.to_string());
585 let mut providers: Vec<Arc<dyn SecretProvider>> = Vec::new();
586
587 for raw_name in configured.split(',') {
588 let provider_name = raw_name.trim();
589 if provider_name.is_empty() {
590 continue;
591 }
592 match provider_name {
593 "env" => providers.push(Arc::new(EnvSecretProvider::new(namespace.clone()))),
594 "keyring" => providers.push(Arc::new(KeyringSecretProvider::new(namespace.clone()))),
595 other => {
596 return Err(SecretError::InvalidConfig(format!(
597 "unsupported secret provider '{other}' in {SECRET_PROVIDER_CHAIN_ENV}; expected a comma-separated list of env,keyring"
598 )))
599 }
600 }
601 }
602
603 Ok(ChainSecretProvider::new(namespace, providers))
604}
605
606pub(crate) fn emit_secret_access_event(provider: &str, id: &SecretId) {
607 #[derive(Serialize)]
608 struct SecretAccessEvent<'a> {
609 topic: &'a str,
610 provider: &'a str,
611 id: &'a SecretId,
612 caller_span_id: Option<u64>,
613 mutation_session_id: Option<String>,
614 timestamp: String,
615 }
616
617 let event = SecretAccessEvent {
618 topic: "audit.secret_access",
619 provider,
620 id,
621 caller_span_id: crate::tracing::current_span_id(),
622 mutation_session_id: crate::orchestration::current_mutation_session()
623 .map(|session| session.session_id),
624 timestamp: crate::orchestration::now_rfc3339(),
625 };
626 let metadata = serde_json::to_value(event)
627 .ok()
628 .and_then(|value| value.as_object().cloned())
629 .map(|object| object.into_iter().collect::<BTreeMap<_, _>>())
630 .unwrap_or_default();
631 crate::events::log_info_meta("secret.audit", "secret accessed", metadata);
632}
633
634#[cfg(test)]
635mod tests {
636 use std::sync::{Arc, Mutex, Once};
637
638 use async_trait::async_trait;
639
640 use super::*;
641
642 fn install_mock_keyring() {
643 static INIT: Once = Once::new();
644 INIT.call_once(|| {
645 ::keyring::set_default_credential_builder(::keyring::mock::default_credential_builder());
646 });
647 }
648
649 struct FakeProvider {
650 namespace: String,
651 result: Mutex<Vec<Result<SecretBytes, SecretError>>>,
652 }
653
654 impl FakeProvider {
655 fn new(
656 namespace: impl Into<String>,
657 result: Vec<Result<SecretBytes, SecretError>>,
658 ) -> Self {
659 Self {
660 namespace: namespace.into(),
661 result: Mutex::new(result),
662 }
663 }
664 }
665
666 #[async_trait]
667 impl SecretProvider for FakeProvider {
668 async fn get(&self, _id: &SecretId) -> Result<SecretBytes, SecretError> {
669 self.result
670 .lock()
671 .expect("fake provider poisoned")
672 .remove(0)
673 }
674
675 async fn put(&self, _id: &SecretId, _value: SecretBytes) -> Result<(), SecretError> {
676 Err(SecretError::Unsupported {
677 provider: self.namespace.clone(),
678 operation: "put",
679 })
680 }
681
682 async fn rotate(&self, _id: &SecretId) -> Result<RotationHandle, SecretError> {
683 Err(SecretError::Unsupported {
684 provider: self.namespace.clone(),
685 operation: "rotate",
686 })
687 }
688
689 async fn list(&self, _prefix: &SecretId) -> Result<Vec<SecretMeta>, SecretError> {
690 Err(SecretError::Unsupported {
691 provider: self.namespace.clone(),
692 operation: "list",
693 })
694 }
695
696 fn namespace(&self) -> &str {
697 &self.namespace
698 }
699
700 fn supports_versions(&self) -> bool {
701 false
702 }
703 }
704
705 #[test]
706 fn secret_bytes_debug_is_redacted() {
707 let secret = SecretBytes::from("abcd");
708 assert_eq!(format!("{secret:?}"), "SecretBytes { redacted: 4 bytes }");
709 }
710
711 #[test]
712 fn secret_bytes_zeroes_on_drop() {
713 let probe = Arc::new(Mutex::new(None));
714 let mut secret = SecretBytes::from("super-secret");
715 secret.attach_drop_probe(probe.clone());
716 drop(secret);
717
718 let dropped = probe
719 .lock()
720 .expect("drop probe poisoned")
721 .clone()
722 .expect("probe should capture bytes");
723 assert!(dropped.iter().all(|byte| *byte == 0));
724 }
725
726 #[tokio::test]
727 async fn chain_secret_provider_falls_through_to_next_hit() {
728 let id = SecretId::new("harn.test", "api-key");
729 let first = Arc::new(FakeProvider::new(
730 "first",
731 vec![Err(SecretError::NotFound {
732 provider: "first".to_string(),
733 id: id.clone(),
734 })],
735 ));
736 let second = Arc::new(FakeProvider::new(
737 "second",
738 vec![Ok(SecretBytes::from("value"))],
739 ));
740 let chain = ChainSecretProvider::new("harn/test", vec![first, second]);
741
742 let secret = chain.get(&id).await.expect("chain should resolve");
743 let exposed = secret.with_exposed(|bytes| bytes.to_vec());
744 assert_eq!(exposed, b"value");
745 }
746
747 #[tokio::test]
748 async fn chain_secret_provider_returns_all_errors_when_everything_fails() {
749 let id = SecretId::new("harn.test", "missing");
750 let first = Arc::new(FakeProvider::new(
751 "first",
752 vec![Err(SecretError::NotFound {
753 provider: "first".to_string(),
754 id: id.clone(),
755 })],
756 ));
757 let second = Arc::new(FakeProvider::new(
758 "second",
759 vec![Err(SecretError::Backend {
760 provider: "second".to_string(),
761 message: "boom".to_string(),
762 })],
763 ));
764 let chain = ChainSecretProvider::new("harn/test", vec![first, second]);
765
766 let error = chain.get(&id).await.expect_err("chain should fail");
767 match error {
768 SecretError::All(errors) => {
769 assert_eq!(errors.len(), 2);
770 assert!(matches!(errors[0], SecretError::NotFound { .. }));
771 assert!(matches!(errors[1], SecretError::Backend { .. }));
772 }
773 other => panic!("expected aggregated errors, got {other:?}"),
774 }
775 }
776
777 #[tokio::test]
778 async fn keyring_provider_round_trips_and_zeroes_on_drop() {
779 install_mock_keyring();
780
781 let provider = KeyringSecretProvider::new("harn.test");
782 let id = SecretId::new("", format!("mock-{}", uuid::Uuid::now_v7()));
783 provider
784 .put(&id, SecretBytes::from("round-trip-secret"))
785 .await
786 .expect("mock keyring write should succeed");
787
788 let probe = Arc::new(Mutex::new(None));
789 let mut secret = provider
790 .get(&id)
791 .await
792 .expect("mock keyring read should succeed");
793 assert_eq!(
794 secret.with_exposed(|bytes| bytes.to_vec()),
795 b"round-trip-secret"
796 );
797 secret.attach_drop_probe(probe.clone());
798 drop(secret);
799
800 let dropped = probe
801 .lock()
802 .expect("drop probe poisoned")
803 .clone()
804 .expect("probe should capture bytes");
805 assert!(dropped.iter().all(|byte| *byte == 0));
806
807 provider
808 .delete(&id)
809 .await
810 .expect("mock keyring delete should succeed");
811 }
812}