Skip to main content

openmoat_proxy/broker/
value.rs

1//! A brokered secret's value as it changes while the proxy runs (ADR-087).
2//!
3//! A value read once (an `env`, `file` or `keychain` source) never expires. A
4//! value from a `command` source has a deadline: past it the value is not put
5//! into any request, and the request is refused ([`super::RULE_SECRET_EXPIRED`]).
6//! The deadline is kept on two clocks and is past when either says so: the
7//! monotonic clock does not count a machine's sleep on every system, and the
8//! wall clock can be set back.
9//!
10//! The values a slot held before stay known to the leak and echo checks: a
11//! replaced value may still be accepted by its server until its own expiry.
12
13use std::collections::VecDeque;
14use std::sync::{Arc, PoisonError, RwLock, RwLockReadGuard};
15use std::time::{Duration, Instant, SystemTime};
16
17use zeroize::Zeroizing;
18
19use super::BrokerError;
20
21/// How many replaced values each slot keeps for the leak and echo checks.
22const KEPT: usize = 4;
23
24/// The current value, its deadline and the values it replaced.
25pub(super) struct Slot {
26    current: Zeroizing<String>,
27    until: Option<(Instant, SystemTime)>,
28    replaced: VecDeque<Zeroizing<String>>,
29}
30
31impl Slot {
32    pub(super) fn fixed(value: Zeroizing<String>) -> Arc<RwLock<Self>> {
33        Arc::new(RwLock::new(Self {
34            current: value,
35            until: None,
36            replaced: VecDeque::new(),
37        }))
38    }
39
40    /// The value to put into a request now, `None` past its deadline.
41    pub(super) fn live(&self) -> Option<&Zeroizing<String>> {
42        match self.until {
43            Some((mono, wall)) if Instant::now() >= mono || SystemTime::now() >= wall => None,
44            _ => Some(&self.current),
45        }
46    }
47
48    /// Every value the leak and echo checks look for: the current one, live or
49    /// not, and those it replaced.
50    pub(super) fn known(&self) -> impl Iterator<Item = &Zeroizing<String>> {
51        std::iter::once(&self.current).chain(&self.replaced)
52    }
53}
54
55/// Read `slot`; a writer that panicked left a whole value or the one before.
56pub(super) fn read(slot: &RwLock<Slot>) -> RwLockReadGuard<'_, Slot> {
57    slot.read().unwrap_or_else(PoisonError::into_inner)
58}
59
60/// The handle that renews one secret's value ([`super::Broker::rotation`]).
61/// `Debug` names the secret only.
62#[derive(Clone)]
63pub struct Rotation {
64    id: String,
65    slot: Arc<RwLock<Slot>>,
66}
67
68impl std::fmt::Debug for Rotation {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        f.debug_struct("Rotation")
71            .field("id", &self.id)
72            .finish_non_exhaustive()
73    }
74}
75
76impl Rotation {
77    pub(super) fn new(id: &str, slot: &Arc<RwLock<Slot>>) -> Self {
78        Self {
79            id: id.to_owned(),
80            slot: Arc::clone(slot),
81        }
82    }
83
84    /// Use `value` for the next `lifetime`, and from then on never again unless
85    /// renewed. The value it replaces stays known to the leak and echo checks.
86    /// Refused, and the slot left as it was, for a value the broker cannot use.
87    pub fn renew(&self, value: Zeroizing<String>, lifetime: Duration) -> Result<(), BrokerError> {
88        super::usable(&self.id, &value)?;
89        let until = (
90            Instant::now() + lifetime,
91            SystemTime::now()
92                .checked_add(lifetime)
93                .unwrap_or(SystemTime::UNIX_EPOCH),
94        );
95        let mut slot = self.slot.write().unwrap_or_else(PoisonError::into_inner);
96        if *slot.current != *value {
97            let old = std::mem::replace(&mut slot.current, value);
98            slot.replaced.push_front(old);
99            slot.replaced.truncate(KEPT);
100        }
101        slot.until = Some(until);
102        Ok(())
103    }
104
105    /// How long the current value stays usable: zero once past its deadline
106    /// on either clock, `None` for a value that never expires.
107    #[must_use]
108    pub fn remaining(&self) -> Option<Duration> {
109        let slot = read(&self.slot);
110        let (mono, wall) = slot.until?;
111        let by_wall = wall
112            .duration_since(SystemTime::now())
113            .unwrap_or(Duration::ZERO);
114        Some(mono.saturating_duration_since(Instant::now()).min(by_wall))
115    }
116}
117
118#[cfg(test)]
119mod tests;