Skip to main content

coven_keys/keys/
credential_custody.rs

1use std::sync::{Arc, Mutex};
2
3use super::{CloudHomeCredentials, KeyError, StoreKeys};
4
5/// Retained access to one store's cloud-provider credentials.
6///
7/// Provider implementations read and refresh credentials through this
8/// capability rather than reaching into the platform keyring. Cloud setup uses
9/// [`StagedCloudHomeCredentials`] so provider construction and token refreshes
10/// remain in memory until the complete cloud connection is ready to install.
11pub trait CloudHomeCredentialCustody: Send + Sync {
12    fn unlock(&self) -> Result<Option<CloudHomeCredentials>, KeyError>;
13
14    fn persist(&self, credentials: &CloudHomeCredentials) -> Result<(), KeyError>;
15}
16
17/// The durable owner that issues one credential capability per provider
18/// connection and rejects writes from providers replaced by a later setup.
19#[derive(Clone)]
20pub struct CloudHomeCredentialsOwner {
21    destination: StoreKeys,
22    epoch: Arc<Mutex<u64>>,
23}
24
25impl CloudHomeCredentialsOwner {
26    pub fn new(destination: StoreKeys) -> Self {
27        Self {
28            destination,
29            epoch: Arc::new(Mutex::new(0)),
30        }
31    }
32
33    pub fn current(&self) -> Arc<dyn CloudHomeCredentialCustody> {
34        let epoch = *self.epoch.lock().expect("lock cloud credential epoch");
35        Arc::new(CloudHomeCredentialLease {
36            owner: self.clone(),
37            epoch,
38        })
39    }
40
41    pub fn stage(&self, proposed: Option<CloudHomeCredentials>) -> Arc<StagedCloudHomeCredentials> {
42        let base_epoch = *self.epoch.lock().expect("lock cloud credential epoch");
43        Arc::new(StagedCloudHomeCredentials {
44            owner: self.clone(),
45            base_epoch,
46            state: Mutex::new(StagedCredentialState::Proposed(proposed)),
47        })
48    }
49
50    fn read_at(&self, expected_epoch: u64) -> Result<Option<CloudHomeCredentials>, KeyError> {
51        let epoch = self.epoch.lock().expect("lock cloud credential epoch");
52        if *epoch != expected_epoch {
53            return Err(KeyError::CloudCredentialsSuperseded);
54        }
55        self.destination.get_cloud_home_credentials()
56    }
57
58    fn write_at(
59        &self,
60        expected_epoch: u64,
61        credentials: &CloudHomeCredentials,
62    ) -> Result<(), KeyError> {
63        let epoch = self.epoch.lock().expect("lock cloud credential epoch");
64        if *epoch != expected_epoch {
65            return Err(KeyError::CloudCredentialsSuperseded);
66        }
67        self.destination.set_cloud_home_credentials(credentials)
68    }
69}
70
71struct CloudHomeCredentialLease {
72    owner: CloudHomeCredentialsOwner,
73    epoch: u64,
74}
75
76impl CloudHomeCredentialCustody for CloudHomeCredentialLease {
77    fn unlock(&self) -> Result<Option<CloudHomeCredentials>, KeyError> {
78        self.owner.read_at(self.epoch)
79    }
80
81    fn persist(&self, credentials: &CloudHomeCredentials) -> Result<(), KeyError> {
82        self.owner.write_at(self.epoch, credentials)
83    }
84}
85
86enum StagedCredentialState {
87    Proposed(Option<CloudHomeCredentials>),
88    Committed {
89        previous: Option<CloudHomeCredentials>,
90    },
91    RolledBack,
92}
93
94/// One proposed cloud credential value and the durable value it would replace.
95///
96/// Clones of this object are the credential capability retained by a proposed
97/// provider. Before [`commit`](Self::commit), provider refreshes update only the
98/// proposed value. Commit writes the latest proposal to the keyring and makes
99/// later refreshes durable. Rollback restores the exact value commit replaced.
100pub struct StagedCloudHomeCredentials {
101    owner: CloudHomeCredentialsOwner,
102    base_epoch: u64,
103    state: Mutex<StagedCredentialState>,
104}
105
106impl StagedCloudHomeCredentials {
107    fn committed_epoch(&self) -> u64 {
108        self.base_epoch
109            .checked_add(1)
110            .expect("cloud credential epoch overflow")
111    }
112
113    /// Persist the latest proposed value. Repeating commit after it succeeded is
114    /// idempotent.
115    pub fn commit(&self) -> Result<(), KeyError> {
116        let mut state = self.state.lock().expect("lock staged cloud credentials");
117        match &*state {
118            StagedCredentialState::Proposed(credentials) => {
119                let credentials = credentials.clone();
120                let mut epoch = self
121                    .owner
122                    .epoch
123                    .lock()
124                    .expect("lock cloud credential epoch");
125                if *epoch != self.base_epoch {
126                    return Err(KeyError::CloudCredentialsSuperseded);
127                }
128                let previous = self.owner.destination.get_cloud_home_credentials()?;
129                match &credentials {
130                    Some(credentials) => self
131                        .owner
132                        .destination
133                        .set_cloud_home_credentials(credentials)?,
134                    None => self.owner.destination.delete_cloud_home_credentials()?,
135                }
136                *epoch = self.committed_epoch();
137                *state = StagedCredentialState::Committed { previous };
138                Ok(())
139            }
140            StagedCredentialState::Committed { .. } => Ok(()),
141            StagedCredentialState::RolledBack => Err(KeyError::CloudCredentialsRolledBack {
142                operation: "commit",
143            }),
144        }
145    }
146
147    /// Restore the durable value this proposal replaced. Repeating rollback is
148    /// idempotent so every failing setup path can invoke it unconditionally.
149    pub fn rollback(&self) -> Result<(), KeyError> {
150        let mut state = self.state.lock().expect("lock staged cloud credentials");
151        match &*state {
152            StagedCredentialState::Proposed(_) => {
153                let epoch = self
154                    .owner
155                    .epoch
156                    .lock()
157                    .expect("lock cloud credential epoch");
158                if *epoch != self.base_epoch {
159                    return Err(KeyError::CloudCredentialsSuperseded);
160                }
161                *state = StagedCredentialState::RolledBack;
162                Ok(())
163            }
164            StagedCredentialState::Committed { previous } => {
165                let mut epoch = self
166                    .owner
167                    .epoch
168                    .lock()
169                    .expect("lock cloud credential epoch");
170                if *epoch != self.committed_epoch() {
171                    return Err(KeyError::CloudCredentialsSuperseded);
172                }
173                match previous {
174                    Some(previous) => self
175                        .owner
176                        .destination
177                        .set_cloud_home_credentials(previous)?,
178                    None => self.owner.destination.delete_cloud_home_credentials()?,
179                }
180                *epoch = self.base_epoch;
181                *state = StagedCredentialState::RolledBack;
182                Ok(())
183            }
184            StagedCredentialState::RolledBack => Ok(()),
185        }
186    }
187}
188
189impl CloudHomeCredentialCustody for StagedCloudHomeCredentials {
190    fn unlock(&self) -> Result<Option<CloudHomeCredentials>, KeyError> {
191        let state = self.state.lock().expect("lock staged cloud credentials");
192        match &*state {
193            StagedCredentialState::Proposed(credentials) => Ok(credentials.clone()),
194            StagedCredentialState::Committed { .. } => self.owner.read_at(self.committed_epoch()),
195            StagedCredentialState::RolledBack => Err(KeyError::CloudCredentialsRolledBack {
196                operation: "unlock",
197            }),
198        }
199    }
200
201    fn persist(&self, credentials: &CloudHomeCredentials) -> Result<(), KeyError> {
202        let mut state = self.state.lock().expect("lock staged cloud credentials");
203        match &mut *state {
204            StagedCredentialState::Proposed(proposed) => {
205                *proposed = Some(credentials.clone());
206                Ok(())
207            }
208            StagedCredentialState::Committed { .. } => {
209                self.owner.write_at(self.committed_epoch(), credentials)
210            }
211            StagedCredentialState::RolledBack => Err(KeyError::CloudCredentialsRolledBack {
212                operation: "persist refreshed",
213            }),
214        }
215    }
216}
217
218#[cfg(test)]
219#[path = "credential_custody_tests.rs"]
220mod tests;