aven_core/sync/client/enrollment/
management.rs1use super::*;
4use crate::sync::client::keys::{peer::Disclosure, rotation::Action};
5use crate::sync::seed_claim::membership::{CancelStatus, MAX_CUTOFF};
6
7#[derive(Debug, PartialEq, Eq)]
8pub enum RemovalStatus {
9 Complete,
10 Pending,
11 SelfRevoked,
13}
14impl Client {
15 pub async fn remove_device(
19 &self,
20 store: &ProtectedLocalKeyStore,
21 db: &Database,
22 target: [u8; 32],
23 ) -> Result<RemovalStatus> {
24 Box::pin(self.manage(store, db, Some(target), None)).await
25 }
26 pub async fn finish_pending_management(
27 &self,
28 store: &ProtectedLocalKeyStore,
29 db: &Database,
30 ) -> Result<()> {
31 self.finish_pending_removal(store, db).await?;
32 Box::pin(self.withdraw_expired_disclosure(store, db)).await
33 }
34 pub async fn finish_pending_removal(
36 &self,
37 store: &ProtectedLocalKeyStore,
38 db: &Database,
39 ) -> Result<()> {
40 Box::pin(self.manage(store, db, None, None)).await?;
41 Ok(())
42 }
43 pub(crate) async fn refresh_and_finish_pending_removal(
44 &self,
45 store: &ProtectedLocalKeyStore,
46 db: &Database,
47 ) -> Result<()> {
48 let mut inputs = store.active_inputs(db, &self.locator).await?;
49 self.refresh_inputs(store, db, &mut inputs).await?;
50 Box::pin(self.manage_inputs(store, db, inputs, None, None)).await?;
51 Ok(())
52 }
53 async fn management_preparation(
54 &self,
55 store: &ProtectedLocalKeyStore,
56 db: &Database,
57 inputs: &mut ActiveInputs,
58 ) -> Result<u64> {
59 let Reply::PreparedManagement(prepared) = self
60 .exchange(
61 Operation::PrepareManagement {
62 context: Context::active(inputs),
63 },
64 Some(inputs.bearer()),
65 )
66 .await?
67 else {
68 anyhow::bail!("error management-response");
69 };
70 let membership = prepared.evidence.verify()?;
71 ensure!(
72 membership.head() == inputs.membership.head()
73 && prepared.high_water >= membership.current_generation().starts_after
74 && prepared.high_water >= db.encrypted_tail_observed_rank().await?
75 && prepared.high_water <= MAX_CUTOFF,
76 "error management-context"
77 );
78 store
79 .adopt_verified_refresh(db, inputs, prepared.evidence, membership)
80 .await?;
81 Ok(prepared.high_water)
82 }
83 pub async fn withdraw_expired_disclosure(
89 &self,
90 store: &ProtectedLocalKeyStore,
91 db: &Database,
92 ) -> Result<()> {
93 let handle = {
94 let mut inputs = store.active_inputs(db, &self.locator).await?;
95 let Some(journal) = store.expired_disclosure(db, &inputs).await? else {
96 return Ok(());
97 };
98 self.refresh_inputs(store, db, &mut inputs).await?;
99 if store.reconcile_disclosure(db, &inputs, &journal).await? != Disclosure::Unresolved {
100 return Ok(());
101 }
102 store.mark_withdrawing(db, &inputs, &journal).await?;
103 let status = self.cancel(store, db, &mut inputs, journal.handle).await?;
104 self.refresh_inputs(store, db, &mut inputs).await?;
105 if store.reconcile_disclosure(db, &inputs, &journal).await? != Disclosure::Unresolved {
106 return Ok(());
107 }
108 ensure!(
109 status == CancelStatus::Cancelled,
110 "error withdrawal-admission-unverified"
111 );
112 store
113 .management_intent(db, &inputs, None, Some(journal.handle))
114 .await?;
115 journal.handle
116 };
117 Box::pin(self.manage(store, db, None, Some(handle))).await?;
118 let mut inputs = store.active_inputs(db, &self.locator).await?;
119 self.refresh_inputs(store, db, &mut inputs).await?;
120 if let Some(journal) = store.expired_disclosure(db, &inputs).await? {
121 store.reconcile_disclosure(db, &inputs, &journal).await?;
122 }
123 Ok(())
124 }
125 pub async fn cancel(
126 &self,
127 store: &ProtectedLocalKeyStore,
128 db: &Database,
129 inputs: &mut ActiveInputs,
130 handle: [u8; 32],
131 ) -> Result<CancelStatus> {
132 let reply = retry_stale!(
133 self.exchange(
134 Operation::Cancel {
135 context: Context::active(inputs),
136 handle,
137 },
138 Some(inputs.bearer()),
139 )
140 .await,
141 self.refresh_inputs(store, db, inputs).await,
142 )?;
143 match reply {
144 Reply::Cancelled(status) => Ok(status),
145 _ => anyhow::bail!("error enrollment-response"),
146 }
147 }
148 pub async fn manage(
149 &self,
150 store: &ProtectedLocalKeyStore,
151 db: &Database,
152 target: Option<[u8; 32]>,
153 withdraw: Option<[u8; 32]>,
154 ) -> Result<RemovalStatus> {
155 let mut inputs = store.active_inputs(db, &self.locator).await?;
156 if target.is_none()
157 && withdraw.is_none()
158 && store
159 .management_intent(db, &inputs, None, None)
160 .await?
161 .is_none()
162 {
163 return Ok(RemovalStatus::Complete);
164 }
165 self.refresh_inputs(store, db, &mut inputs).await?;
166 Box::pin(self.manage_inputs(store, db, inputs, target, withdraw)).await
167 }
168 async fn manage_inputs(
169 &self,
170 store: &ProtectedLocalKeyStore,
171 db: &Database,
172 mut inputs: ActiveInputs,
173 target: Option<[u8; 32]>,
174 withdraw: Option<[u8; 32]>,
175 ) -> Result<RemovalStatus> {
176 let Some(intent) = store
177 .management_intent(db, &inputs, target, withdraw)
178 .await?
179 else {
180 return Ok(RemovalStatus::Complete);
181 };
182 let mut stale_retry = false;
183 let mut rotation_attempts = 0;
184 for _ in 0..3 {
186 let result = self.management_preparation(store, db, &mut inputs).await;
187 let high = match result {
188 Ok(high) => high,
189 Err(error) if is_stale(&error) && !stale_retry => {
190 stale_retry = true;
191 self.refresh_inputs(store, db, &mut inputs).await?;
192 continue;
193 }
194 Err(error) => return Err(error),
195 };
196 let Some(dispatch) = store
197 .management_dispatch(db, &inputs, &intent, high)
198 .await?
199 else {
200 store
201 .management_intent(db, &inputs, target, withdraw)
202 .await?;
203 return Ok(RemovalStatus::Complete);
204 };
205 if dispatch.action == Action::Rotate {
206 rotation_attempts += 1;
207 ensure!(rotation_attempts <= 2, "error management-budget");
208 }
209 match self
210 .exchange(
211 Operation::Manage {
212 context: Context::active(&inputs),
213 record: dispatch.record.clone(),
214 },
215 Some(inputs.bearer()),
216 )
217 .await
218 {
219 Ok(Reply::Managed(record)) => {
220 ensure!(
221 record == dispatch.record,
222 "error management-outcome-mismatch"
223 );
224 if intent.target() == Some(inputs.device()) && dispatch.action == Action::Revoke
225 {
226 let result = store
227 .adopt_verified_refresh(
228 db,
229 &mut inputs,
230 dispatch.evidence,
231 dispatch.membership,
232 )
233 .await;
234 ensure!(
235 result.as_ref().err().is_some_and(|e| {
236 crate::sync::client::errors::has_code(e, "enrollment-revoked")
237 }),
238 "error management-self-revoke"
239 );
240 return Ok(RemovalStatus::SelfRevoked);
241 }
242 self.refresh_inputs(store, db, &mut inputs).await?;
243 if store
244 .management_intent(db, &inputs, target, withdraw)
245 .await?
246 .is_none()
247 {
248 return Ok(RemovalStatus::Complete);
249 }
250 if dispatch.action == Action::Rotate {
251 return Ok(RemovalStatus::Pending);
252 }
253 }
254 Err(error) if is_stale(&error) && !stale_retry => {
255 stale_retry = true;
256 self.refresh_inputs(store, db, &mut inputs).await?;
257 }
258 Err(error) => return Err(error),
259 _ => anyhow::bail!("error management-response"),
260 }
261 }
262 Ok(RemovalStatus::Pending)
263 }
264}