Skip to main content

coven/
cloud_home_setup.rs

1use crate::{CloudHomeConfig, CloudHomeKeyState, SyncError};
2
3/// A cloud-home setup that Coven has connected and committed.
4#[derive(Clone, Debug, PartialEq)]
5pub struct ConnectedCloudHome {
6    pub cloud_home: CloudHomeConfig,
7    pub key_state: CloudHomeKeyState,
8}
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum CloudHomeSetupFailure {
12    Authentication,
13    PermissionDenied,
14    ContainerNotFound,
15    RegionMismatch,
16    QuotaExceeded,
17    InvalidConfiguration,
18    LocationOccupied,
19    Network,
20    DeviceIdentityMissing,
21    SecureStorage,
22    Internal,
23}
24
25/// Which durable key material could not be restored after setup failed.
26#[derive(Debug, thiserror::Error)]
27pub enum CloudHomeRollbackError {
28    #[error("restore cloud-home credentials: {0}")]
29    Credentials(#[source] coven_keys::keys::KeyError),
30    #[error("restore cloud-home master key: {0}")]
31    MasterKey(#[source] coven_keys::keys::KeyError),
32    #[error("restore cloud-home credentials: {credentials}; restore master key: {master_key}")]
33    Both {
34        #[source]
35        credentials: coven_keys::keys::KeyError,
36        master_key: coven_keys::keys::KeyError,
37    },
38}
39
40/// Why a proposed cloud home was not installed.
41#[derive(Debug, thiserror::Error)]
42pub enum CloudHomeSetupError {
43    #[error("prepare cloud-home master key: {0}")]
44    MasterKey(#[source] Box<coven_keys::keys::MasterKeyError>),
45    #[error("prepare cloud-home connection: {0}")]
46    Connection(#[source] Box<SyncError>),
47    #[error("commit cloud-home {subject}: {source}")]
48    Commit {
49        subject: &'static str,
50        #[source]
51        source: Box<coven_keys::keys::KeyError>,
52    },
53    #[error("{failure}; rollback also failed: {rollback}")]
54    Rollback {
55        failure: Box<CloudHomeSetupError>,
56        #[source]
57        rollback: Box<CloudHomeRollbackError>,
58    },
59}
60
61impl CloudHomeSetupError {
62    pub fn failure(&self) -> CloudHomeSetupFailure {
63        match self {
64            Self::MasterKey(_) | Self::Commit { .. } => CloudHomeSetupFailure::SecureStorage,
65            Self::Connection(error) => classify_setup_error(error.as_ref()),
66            Self::Rollback { failure, .. } => failure.failure(),
67        }
68    }
69
70    pub(crate) fn with_rollback(self, rollback: Result<(), CloudHomeRollbackError>) -> Self {
71        match rollback {
72            Ok(()) => self,
73            Err(rollback) => Self::Rollback {
74                failure: Box::new(self),
75                rollback: Box::new(rollback),
76            },
77        }
78    }
79}
80
81fn classify_backend_failure(
82    failure: coven_protocol::objects::StorageBackendFailure,
83) -> CloudHomeSetupFailure {
84    use coven_protocol::objects::StorageBackendFailure;
85    match failure {
86        StorageBackendFailure::Authentication => CloudHomeSetupFailure::Authentication,
87        StorageBackendFailure::PermissionDenied => CloudHomeSetupFailure::PermissionDenied,
88        StorageBackendFailure::ContainerNotFound => CloudHomeSetupFailure::ContainerNotFound,
89        StorageBackendFailure::RegionMismatch => CloudHomeSetupFailure::RegionMismatch,
90        StorageBackendFailure::QuotaExceeded => CloudHomeSetupFailure::QuotaExceeded,
91        StorageBackendFailure::Configuration => CloudHomeSetupFailure::InvalidConfiguration,
92        StorageBackendFailure::Transport => CloudHomeSetupFailure::Network,
93        StorageBackendFailure::Internal => CloudHomeSetupFailure::Internal,
94    }
95}
96
97fn classify_setup_error(error: &(dyn std::error::Error + 'static)) -> CloudHomeSetupFailure {
98    let mut current = Some(error);
99    while let Some(source) = current {
100        if let Some(error) = source.downcast_ref::<coven_keys::keys::KeyError>() {
101            return match error {
102                coven_keys::keys::KeyError::NoDeviceIdentity => {
103                    CloudHomeSetupFailure::DeviceIdentityMissing
104                }
105                _ => CloudHomeSetupFailure::SecureStorage,
106            };
107        }
108        if let Some(error) = source.downcast_ref::<coven_storage::cloud::CloudHomeError>() {
109            match error {
110                coven_storage::cloud::CloudHomeError::AlreadyExists(_)
111                | coven_storage::cloud::CloudHomeError::SlotCollision(_) => {
112                    return CloudHomeSetupFailure::LocationOccupied;
113                }
114                coven_storage::cloud::CloudHomeError::Configuration(_) => {
115                    return CloudHomeSetupFailure::InvalidConfiguration;
116                }
117                coven_storage::cloud::CloudHomeError::Transport(_)
118                | coven_storage::cloud::CloudHomeError::Io(_) => {
119                    return CloudHomeSetupFailure::Network;
120                }
121                _ => {}
122            }
123            if let Some(failure) = error.backend_failure() {
124                return classify_backend_failure(failure);
125            }
126        }
127        if let Some(error) = source.downcast_ref::<coven_protocol::objects::StorageError>() {
128            match error {
129                coven_protocol::objects::StorageError::AlreadyExists(_)
130                | coven_protocol::objects::StorageError::SlotCollision(_) => {
131                    return CloudHomeSetupFailure::LocationOccupied;
132                }
133                coven_protocol::objects::StorageError::Configuration(_) => {
134                    return CloudHomeSetupFailure::InvalidConfiguration;
135                }
136                coven_protocol::objects::StorageError::Key(_) => {
137                    return CloudHomeSetupFailure::SecureStorage;
138                }
139                _ => {}
140            }
141            if let Some(failure) = error.backend_failure() {
142                return classify_backend_failure(failure);
143            }
144        }
145        current = source.source();
146    }
147    CloudHomeSetupFailure::Internal
148}
149
150/// Why a returning opaque cloud home could not be unlocked and connected.
151#[derive(Debug, thiserror::Error)]
152pub enum CloudHomeUnlockError {
153    #[error("this cloud home does not require a master key")]
154    KeyNotRequired,
155    #[error("prepare imported cloud-home master key: {0}")]
156    MasterKey(#[source] Box<coven_keys::keys::MasterKeyError>),
157    #[error("prepare cloud-home connection: {0}")]
158    Connection(#[source] Box<SyncError>),
159    #[error("commit cloud-home master key: {0}")]
160    Commit(#[source] Box<coven_keys::keys::KeyError>),
161    #[error("{failure}; rollback also failed: {rollback}")]
162    Rollback {
163        failure: Box<CloudHomeUnlockError>,
164        #[source]
165        rollback: Box<coven_keys::keys::KeyError>,
166    },
167}
168
169impl CloudHomeUnlockError {
170    pub(crate) fn with_rollback(self, rollback: Result<(), coven_keys::keys::KeyError>) -> Self {
171        match rollback {
172            Ok(()) => self,
173            Err(rollback) => Self::Rollback {
174                failure: Box::new(self),
175                rollback: Box::new(rollback),
176            },
177        }
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use coven_protocol::objects::StorageBackendFailure;
185
186    fn backend_setup_error(kind: StorageBackendFailure) -> CloudHomeSetupError {
187        CloudHomeSetupError::Connection(Box::new(SyncError::CloudHome(
188            coven_storage::cloud::CloudHomeError::backend(
189                kind,
190                "test cloud setup",
191                std::io::Error::other("test backend failure"),
192            ),
193        )))
194    }
195
196    #[test]
197    fn cloud_home_setup_preserves_every_actionable_provider_failure() {
198        let cases = [
199            (
200                StorageBackendFailure::Authentication,
201                CloudHomeSetupFailure::Authentication,
202            ),
203            (
204                StorageBackendFailure::PermissionDenied,
205                CloudHomeSetupFailure::PermissionDenied,
206            ),
207            (
208                StorageBackendFailure::ContainerNotFound,
209                CloudHomeSetupFailure::ContainerNotFound,
210            ),
211            (
212                StorageBackendFailure::RegionMismatch,
213                CloudHomeSetupFailure::RegionMismatch,
214            ),
215            (
216                StorageBackendFailure::QuotaExceeded,
217                CloudHomeSetupFailure::QuotaExceeded,
218            ),
219            (
220                StorageBackendFailure::Configuration,
221                CloudHomeSetupFailure::InvalidConfiguration,
222            ),
223            (
224                StorageBackendFailure::Transport,
225                CloudHomeSetupFailure::Network,
226            ),
227            (
228                StorageBackendFailure::Internal,
229                CloudHomeSetupFailure::Internal,
230            ),
231        ];
232
233        for (backend, expected) in cases {
234            assert_eq!(backend_setup_error(backend).failure(), expected);
235        }
236    }
237
238    #[test]
239    fn occupied_cloud_location_is_distinct_from_credentials_and_network() {
240        let error = CloudHomeSetupError::Connection(Box::new(SyncError::CloudHome(
241            coven_storage::cloud::CloudHomeError::SlotCollision(
242                "store/protocol-root.json".to_string(),
243            ),
244        )));
245
246        assert_eq!(error.failure(), CloudHomeSetupFailure::LocationOccupied);
247    }
248
249    #[test]
250    fn missing_device_identity_is_distinct_from_secure_storage() {
251        let error = CloudHomeSetupError::Connection(Box::new(SyncError::Key(
252            coven_keys::keys::KeyError::NoDeviceIdentity,
253        )));
254
255        assert_eq!(
256            error.failure(),
257            CloudHomeSetupFailure::DeviceIdentityMissing
258        );
259    }
260}