use super::*;
use crate::sync::client::keys::{peer::Disclosure, rotation::Action};
use crate::sync::seed_claim::membership::{CancelStatus, MAX_CUTOFF};
#[derive(Debug, PartialEq, Eq)]
pub enum RemovalStatus {
Complete,
Pending,
SelfRevoked,
}
impl Client {
pub async fn remove_device(
&self,
store: &ProtectedLocalKeyStore,
db: &Database,
target: [u8; 32],
) -> Result<RemovalStatus> {
Box::pin(self.manage(store, db, Some(target), None)).await
}
pub async fn finish_pending_management(
&self,
store: &ProtectedLocalKeyStore,
db: &Database,
) -> Result<()> {
self.finish_pending_removal(store, db).await?;
Box::pin(self.withdraw_expired_disclosure(store, db)).await
}
pub async fn finish_pending_removal(
&self,
store: &ProtectedLocalKeyStore,
db: &Database,
) -> Result<()> {
Box::pin(self.manage(store, db, None, None)).await?;
Ok(())
}
pub(crate) async fn refresh_and_finish_pending_removal(
&self,
store: &ProtectedLocalKeyStore,
db: &Database,
) -> Result<()> {
let mut inputs = store.active_inputs(db, &self.locator).await?;
self.refresh_inputs(store, db, &mut inputs).await?;
Box::pin(self.manage_inputs(store, db, inputs, None, None)).await?;
Ok(())
}
async fn management_preparation(
&self,
store: &ProtectedLocalKeyStore,
db: &Database,
inputs: &mut ActiveInputs,
) -> Result<u64> {
let Reply::PreparedManagement(prepared) = self
.exchange(
Operation::PrepareManagement {
context: Context::active(inputs),
},
Some(inputs.bearer()),
)
.await?
else {
anyhow::bail!("error management-response");
};
let membership = prepared.evidence.verify()?;
ensure!(
membership.head() == inputs.membership.head()
&& prepared.high_water >= membership.current_generation().starts_after
&& prepared.high_water >= db.encrypted_tail_observed_rank().await?
&& prepared.high_water <= MAX_CUTOFF,
"error management-context"
);
store
.adopt_verified_refresh(db, inputs, prepared.evidence, membership)
.await?;
Ok(prepared.high_water)
}
pub async fn withdraw_expired_disclosure(
&self,
store: &ProtectedLocalKeyStore,
db: &Database,
) -> Result<()> {
let handle = {
let mut inputs = store.active_inputs(db, &self.locator).await?;
let Some(journal) = store.expired_disclosure(db, &inputs).await? else {
return Ok(());
};
self.refresh_inputs(store, db, &mut inputs).await?;
if store.reconcile_disclosure(db, &inputs, &journal).await? != Disclosure::Unresolved {
return Ok(());
}
store.mark_withdrawing(db, &inputs, &journal).await?;
let status = self.cancel(store, db, &mut inputs, journal.handle).await?;
self.refresh_inputs(store, db, &mut inputs).await?;
if store.reconcile_disclosure(db, &inputs, &journal).await? != Disclosure::Unresolved {
return Ok(());
}
ensure!(
status == CancelStatus::Cancelled,
"error withdrawal-admission-unverified"
);
store
.management_intent(db, &inputs, None, Some(journal.handle))
.await?;
journal.handle
};
Box::pin(self.manage(store, db, None, Some(handle))).await?;
let mut inputs = store.active_inputs(db, &self.locator).await?;
self.refresh_inputs(store, db, &mut inputs).await?;
if let Some(journal) = store.expired_disclosure(db, &inputs).await? {
store.reconcile_disclosure(db, &inputs, &journal).await?;
}
Ok(())
}
pub async fn cancel(
&self,
store: &ProtectedLocalKeyStore,
db: &Database,
inputs: &mut ActiveInputs,
handle: [u8; 32],
) -> Result<CancelStatus> {
let reply = retry_stale!(
self.exchange(
Operation::Cancel {
context: Context::active(inputs),
handle,
},
Some(inputs.bearer()),
)
.await,
self.refresh_inputs(store, db, inputs).await,
)?;
match reply {
Reply::Cancelled(status) => Ok(status),
_ => anyhow::bail!("error enrollment-response"),
}
}
pub async fn manage(
&self,
store: &ProtectedLocalKeyStore,
db: &Database,
target: Option<[u8; 32]>,
withdraw: Option<[u8; 32]>,
) -> Result<RemovalStatus> {
let mut inputs = store.active_inputs(db, &self.locator).await?;
if target.is_none()
&& withdraw.is_none()
&& store
.management_intent(db, &inputs, None, None)
.await?
.is_none()
{
return Ok(RemovalStatus::Complete);
}
self.refresh_inputs(store, db, &mut inputs).await?;
Box::pin(self.manage_inputs(store, db, inputs, target, withdraw)).await
}
async fn manage_inputs(
&self,
store: &ProtectedLocalKeyStore,
db: &Database,
mut inputs: ActiveInputs,
target: Option<[u8; 32]>,
withdraw: Option<[u8; 32]>,
) -> Result<RemovalStatus> {
let Some(intent) = store
.management_intent(db, &inputs, target, withdraw)
.await?
else {
return Ok(RemovalStatus::Complete);
};
let mut stale_retry = false;
let mut rotation_attempts = 0;
for _ in 0..3 {
let result = self.management_preparation(store, db, &mut inputs).await;
let high = match result {
Ok(high) => high,
Err(error) if is_stale(&error) && !stale_retry => {
stale_retry = true;
self.refresh_inputs(store, db, &mut inputs).await?;
continue;
}
Err(error) => return Err(error),
};
let Some(dispatch) = store
.management_dispatch(db, &inputs, &intent, high)
.await?
else {
store
.management_intent(db, &inputs, target, withdraw)
.await?;
return Ok(RemovalStatus::Complete);
};
if dispatch.action == Action::Rotate {
rotation_attempts += 1;
ensure!(rotation_attempts <= 2, "error management-budget");
}
match self
.exchange(
Operation::Manage {
context: Context::active(&inputs),
record: dispatch.record.clone(),
},
Some(inputs.bearer()),
)
.await
{
Ok(Reply::Managed(record)) => {
ensure!(
record == dispatch.record,
"error management-outcome-mismatch"
);
if intent.target() == Some(inputs.device()) && dispatch.action == Action::Revoke
{
let result = store
.adopt_verified_refresh(
db,
&mut inputs,
dispatch.evidence,
dispatch.membership,
)
.await;
ensure!(
result.as_ref().err().is_some_and(|e| {
crate::sync::client::errors::has_code(e, "enrollment-revoked")
}),
"error management-self-revoke"
);
return Ok(RemovalStatus::SelfRevoked);
}
self.refresh_inputs(store, db, &mut inputs).await?;
if store
.management_intent(db, &inputs, target, withdraw)
.await?
.is_none()
{
return Ok(RemovalStatus::Complete);
}
if dispatch.action == Action::Rotate {
return Ok(RemovalStatus::Pending);
}
}
Err(error) if is_stale(&error) && !stale_retry => {
stale_retry = true;
self.refresh_inputs(store, db, &mut inputs).await?;
}
Err(error) => return Err(error),
_ => anyhow::bail!("error management-response"),
}
}
Ok(RemovalStatus::Pending)
}
}