1use crate::{CloudHomeConfig, CloudHomeKeyState, SyncError};
2
3#[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#[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#[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#[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}