1use async_trait::async_trait;
8use std::path::Path;
9
10use crate::storage::cloud::ObjectSlot;
11use crate::sync::store_commit::ObjectHash;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub(crate) enum ProtectedObjectDomain {
17 StoreProtocolRoot,
18 StoreCommit,
19 StoreHead,
20 StoreAck,
21 StoreDeviceRegistration,
22 DeviceJoinAttempt,
23 DeviceJoinOutcome,
24 DeviceJoinAbandonment,
25 DeviceJoinCleanupReceipt,
26 DeviceJoinTransport,
27 StoreDeviceExclusionProposal,
28 StoreDeviceExclusionOutcome,
29 StoreReclaimEvidence,
30 StoreReclaimAuthorization,
31 StoreReclaimReceipt,
32 ProviderAccessGrant,
33 ProviderAccessWithdrawal,
34 OwnerRecoveryNode,
35 StoreSnapshotMeta,
36 StoreSnapshotImage,
37 StoreMembershipEntry,
38 StoreMembershipHead,
39 StoreMembershipResolution,
40 StoreWrappedKey,
41 StorePackage,
42 CircleControl,
43 CircleRoster,
44 CircleRosterResolution,
45 CircleMetadata,
46 CirclePackage,
47 CircleBootstrapImage,
48 CircleEpochCloseIntent,
49 CircleEpochCloseOutcome,
50 CircleEpochCloseResponse,
51 CircleAccessLeaf,
52 CircleAccessEnvelope,
53 CircleAcknowledgement,
54 CircleSnapshotMeta,
55 CircleSnapshotImage,
56}
57
58#[derive(Clone, Copy)]
59struct ProtocolObjectMetadata {
60 aad_label: &'static [u8],
61 path: ProtocolPathRule,
62 extension: &'static str,
63}
64
65#[derive(Clone, Copy)]
66enum ProtocolPathRule {
67 Exact(&'static [ExactPathShape]),
68 StoreDeviceRegistration,
69 StoreMembershipHead,
70 StoreCandidate {
71 kind: &'static str,
72 component_count: usize,
73 },
74 CircleCandidate {
75 kind: &'static str,
76 component_count: usize,
77 },
78}
79
80#[derive(Clone, Copy)]
81struct ExactPathShape {
82 component_count: usize,
83 fixed_components: &'static [(usize, &'static str)],
84}
85
86impl ProtocolPathRule {
87 fn accepts(self, semantic_prefix: &str) -> bool {
88 match self {
89 Self::Exact(shapes) => shapes
90 .iter()
91 .any(|shape| accepts_path_shape(semantic_prefix, *shape)),
92 Self::StoreDeviceRegistration => {
93 (accepts_path_shape(
94 semantic_prefix,
95 ExactPathShape {
96 component_count: 3,
97 fixed_components: &[(0, "store-v1"), (1, "devices")],
98 },
99 ) && semantic_prefix.split('/').nth(2) != Some("founder"))
100 || accepts_path_shape(
101 semantic_prefix,
102 ExactPathShape {
103 component_count: 5,
104 fixed_components: &[
105 (0, "store-v1"),
106 (1, "devices"),
107 (2, "founder"),
108 (4, "registration"),
109 ],
110 },
111 )
112 }
113 Self::StoreMembershipHead => {
114 (accepts_path_shape(
115 semantic_prefix,
116 ExactPathShape {
117 component_count: 7,
118 fixed_components: &[(0, "store-v1"), (1, "membership"), (2, "heads")],
119 },
120 ) && semantic_prefix.split('/').nth(3) != Some("founder"))
121 || accepts_path_shape(
122 semantic_prefix,
123 ExactPathShape {
124 component_count: 6,
125 fixed_components: &[
126 (0, "store-v1"),
127 (1, "membership"),
128 (2, "heads"),
129 (3, "founder"),
130 (5, "1"),
131 ],
132 },
133 )
134 }
135 Self::StoreCandidate {
136 kind,
137 component_count,
138 } => accepts_candidate_path(
139 semantic_prefix,
140 component_count,
141 &[(0, "store-v1"), (1, "candidates"), (3, kind)],
142 ),
143 Self::CircleCandidate {
144 kind,
145 component_count,
146 } => accepts_candidate_path(
147 semantic_prefix,
148 component_count,
149 &[(0, "circles"), (2, "candidates"), (4, kind)],
150 ),
151 }
152 }
153}
154
155fn accepts_path_shape(semantic_prefix: &str, shape: ExactPathShape) -> bool {
156 let components = semantic_prefix.split('/').collect::<Vec<_>>();
157 components.len() == shape.component_count
158 && components.iter().all(|component| !component.is_empty())
159 && shape
160 .fixed_components
161 .iter()
162 .all(|(index, expected)| components[*index] == *expected)
163}
164
165fn accepts_candidate_path(
166 semantic_prefix: &str,
167 component_count: usize,
168 fixed_components: &[(usize, &str)],
169) -> bool {
170 let components = semantic_prefix.split('/').collect::<Vec<_>>();
171 components.len() == component_count
172 && components.iter().all(|component| !component.is_empty())
173 && fixed_components.iter().all(|(index, expected)| {
174 components[*index] == *expected
175 && components
176 .iter()
177 .filter(|component| **component == *expected)
178 .count()
179 == 1
180 })
181}
182
183impl ProtectedObjectDomain {
184 fn metadata(self) -> ProtocolObjectMetadata {
185 match self {
186 Self::StoreProtocolRoot => ProtocolObjectMetadata {
187 aad_label: b"store-protocol-root",
188 path: ProtocolPathRule::Exact(&[ExactPathShape {
189 component_count: 2,
190 fixed_components: &[(0, "store-v1"), (1, "store-protocol-root")],
191 }]),
192 extension: ".json",
193 },
194 Self::StoreCommit => ProtocolObjectMetadata {
195 aad_label: b"store-commit",
196 path: ProtocolPathRule::StoreCandidate {
197 kind: "commits",
198 component_count: 7,
199 },
200 extension: ".json",
201 },
202 Self::StoreHead => ProtocolObjectMetadata {
203 aad_label: b"store-head",
204 path: ProtocolPathRule::Exact(&[ExactPathShape {
205 component_count: 4,
206 fixed_components: &[(0, "store-v1"), (1, "heads")],
207 }]),
208 extension: ".json",
209 },
210 Self::StoreAck => ProtocolObjectMetadata {
211 aad_label: b"store-ack",
212 path: ProtocolPathRule::Exact(&[ExactPathShape {
213 component_count: 4,
214 fixed_components: &[(0, "store-v1"), (1, "acks")],
215 }]),
216 extension: ".json",
217 },
218 Self::StoreDeviceRegistration => ProtocolObjectMetadata {
219 aad_label: b"store-device-registration",
220 path: ProtocolPathRule::StoreDeviceRegistration,
221 extension: ".json",
222 },
223 Self::DeviceJoinAttempt => ProtocolObjectMetadata {
224 aad_label: b"device-join-attempt",
225 path: ProtocolPathRule::Exact(&[ExactPathShape {
226 component_count: 3,
227 fixed_components: &[(0, "store-v1"), (1, "device-join-attempts")],
228 }]),
229 extension: ".json",
230 },
231 Self::DeviceJoinOutcome => ProtocolObjectMetadata {
232 aad_label: b"device-join-outcome",
233 path: ProtocolPathRule::Exact(&[ExactPathShape {
234 component_count: 3,
235 fixed_components: &[(0, "store-v1"), (1, "device-join-outcomes")],
236 }]),
237 extension: ".json",
238 },
239 Self::DeviceJoinAbandonment => ProtocolObjectMetadata {
240 aad_label: b"device-join-abandonment",
241 path: ProtocolPathRule::Exact(&[ExactPathShape {
242 component_count: 3,
243 fixed_components: &[(0, "store-v1"), (1, "device-join-attempts")],
244 }]),
245 extension: ".json",
246 },
247 Self::DeviceJoinCleanupReceipt => ProtocolObjectMetadata {
248 aad_label: b"device-join-cleanup-receipt",
249 path: ProtocolPathRule::Exact(&[ExactPathShape {
250 component_count: 3,
251 fixed_components: &[(0, "store-v1"), (1, "device-join-cleanup-receipts")],
252 }]),
253 extension: ".json",
254 },
255 Self::DeviceJoinTransport => ProtocolObjectMetadata {
256 aad_label: b"device-join-transport",
257 path: ProtocolPathRule::Exact(&[ExactPathShape {
258 component_count: 4,
259 fixed_components: &[(0, "store-v1"), (1, "device-join-transport")],
260 }]),
261 extension: ".json",
262 },
263 Self::StoreDeviceExclusionProposal => ProtocolObjectMetadata {
264 aad_label: b"store-device-exclusion-proposal",
265 path: ProtocolPathRule::Exact(&[ExactPathShape {
266 component_count: 5,
267 fixed_components: &[(0, "store-v1"), (1, "device-exclusion-proposals")],
268 }]),
269 extension: ".json",
270 },
271 Self::StoreDeviceExclusionOutcome => ProtocolObjectMetadata {
272 aad_label: b"store-device-exclusion-outcome",
273 path: ProtocolPathRule::Exact(&[ExactPathShape {
274 component_count: 4,
275 fixed_components: &[(0, "store-v1"), (1, "device-exclusion-outcomes")],
276 }]),
277 extension: ".json",
278 },
279 Self::StoreReclaimEvidence => ProtocolObjectMetadata {
280 aad_label: b"store-reclaim-evidence",
281 path: ProtocolPathRule::Exact(&[ExactPathShape {
282 component_count: 4,
283 fixed_components: &[(0, "store-v1"), (1, "reclaim"), (2, "evidence")],
284 }]),
285 extension: ".json",
286 },
287 Self::StoreReclaimAuthorization => ProtocolObjectMetadata {
288 aad_label: b"store-reclaim-authorization",
289 path: ProtocolPathRule::Exact(&[ExactPathShape {
290 component_count: 4,
291 fixed_components: &[(0, "store-v1"), (1, "reclaim"), (2, "authorizations")],
292 }]),
293 extension: ".json",
294 },
295 Self::StoreReclaimReceipt => ProtocolObjectMetadata {
296 aad_label: b"store-reclaim-receipt",
297 path: ProtocolPathRule::Exact(&[ExactPathShape {
298 component_count: 4,
299 fixed_components: &[(0, "store-v1"), (1, "reclaim"), (2, "receipts")],
300 }]),
301 extension: ".json",
302 },
303 Self::ProviderAccessGrant => ProtocolObjectMetadata {
304 aad_label: b"provider-access-grant",
305 path: ProtocolPathRule::Exact(&[ExactPathShape {
306 component_count: 4,
307 fixed_components: &[(0, "store-v1"), (1, "provider-access"), (2, "grants")],
308 }]),
309 extension: ".json",
310 },
311 Self::ProviderAccessWithdrawal => ProtocolObjectMetadata {
312 aad_label: b"provider-access-withdrawal",
313 path: ProtocolPathRule::Exact(&[ExactPathShape {
314 component_count: 4,
315 fixed_components: &[
316 (0, "store-v1"),
317 (1, "provider-access"),
318 (2, "withdrawals"),
319 ],
320 }]),
321 extension: ".json",
322 },
323 Self::OwnerRecoveryNode => ProtocolObjectMetadata {
324 aad_label: b"owner-recovery-node",
325 path: ProtocolPathRule::Exact(&[ExactPathShape {
326 component_count: 5,
327 fixed_components: &[(0, "store-v1"), (1, "recovery")],
328 }]),
329 extension: ".json",
330 },
331 Self::StoreSnapshotMeta => ProtocolObjectMetadata {
332 aad_label: b"store-snapshot-meta",
333 path: ProtocolPathRule::Exact(&[ExactPathShape {
334 component_count: 4,
335 fixed_components: &[(0, "store-v1"), (1, "snapshots")],
336 }]),
337 extension: ".json",
338 },
339 Self::StoreSnapshotImage => ProtocolObjectMetadata {
340 aad_label: b"store-snapshot-image",
341 path: ProtocolPathRule::Exact(&[ExactPathShape {
342 component_count: 4,
343 fixed_components: &[(0, "store-v1"), (1, "snapshot-images")],
344 }]),
345 extension: ".db",
346 },
347 Self::StoreMembershipEntry => ProtocolObjectMetadata {
348 aad_label: b"store-membership-entry",
349 path: ProtocolPathRule::Exact(&[ExactPathShape {
350 component_count: 8,
351 fixed_components: &[(0, "store-v1"), (1, "membership"), (2, "entries")],
352 }]),
353 extension: ".json",
354 },
355 Self::StoreMembershipHead => ProtocolObjectMetadata {
356 aad_label: b"store-membership-head",
357 path: ProtocolPathRule::StoreMembershipHead,
358 extension: ".json",
359 },
360 Self::StoreMembershipResolution => ProtocolObjectMetadata {
361 aad_label: b"store-membership-resolution",
362 path: ProtocolPathRule::Exact(&[ExactPathShape {
363 component_count: 6,
364 fixed_components: &[(0, "store-v1"), (1, "membership"), (2, "resolutions")],
365 }]),
366 extension: ".json",
367 },
368 Self::StoreWrappedKey => ProtocolObjectMetadata {
369 aad_label: b"store-wrapped-key",
370 path: ProtocolPathRule::Exact(&[ExactPathShape {
371 component_count: 5,
372 fixed_components: &[(0, "keys")],
373 }]),
374 extension: ".json",
375 },
376 Self::StorePackage => ProtocolObjectMetadata {
377 aad_label: b"store-package",
378 path: ProtocolPathRule::StoreCandidate {
379 kind: "packages",
380 component_count: 7,
381 },
382 extension: ".pkg",
383 },
384 Self::CircleControl => ProtocolObjectMetadata {
385 aad_label: b"circle-control",
386 path: ProtocolPathRule::Exact(&[
387 ExactPathShape {
388 component_count: 10,
389 fixed_components: &[(0, "circle-control"), (2, "merge"), (3, "entries")],
390 },
391 ExactPathShape {
392 component_count: 9,
393 fixed_components: &[(0, "circle-control"), (2, "merge"), (3, "heads")],
394 },
395 ]),
396 extension: ".json",
397 },
398 Self::CircleRoster => ProtocolObjectMetadata {
399 aad_label: b"circle-roster",
400 path: ProtocolPathRule::Exact(&[
401 ExactPathShape {
402 component_count: 10,
403 fixed_components: &[(0, "circles"), (2, "roster"), (3, "entries")],
404 },
405 ExactPathShape {
406 component_count: 9,
407 fixed_components: &[(0, "circles"), (2, "roster"), (3, "heads")],
408 },
409 ]),
410 extension: ".json",
411 },
412 Self::CircleRosterResolution => ProtocolObjectMetadata {
413 aad_label: b"circle-roster-resolution",
414 path: ProtocolPathRule::Exact(&[ExactPathShape {
415 component_count: 7,
416 fixed_components: &[(0, "circles"), (2, "roster"), (3, "resolutions")],
417 }]),
418 extension: ".json",
419 },
420 Self::CircleMetadata => ProtocolObjectMetadata {
421 aad_label: b"circle-metadata",
422 path: ProtocolPathRule::Exact(&[
423 ExactPathShape {
424 component_count: 10,
425 fixed_components: &[(0, "circles"), (2, "metadata"), (3, "entries")],
426 },
427 ExactPathShape {
428 component_count: 9,
429 fixed_components: &[(0, "circles"), (2, "metadata"), (3, "heads")],
430 },
431 ]),
432 extension: ".json",
433 },
434 Self::CirclePackage => ProtocolObjectMetadata {
435 aad_label: b"circle-package",
436 path: ProtocolPathRule::CircleCandidate {
437 kind: "packages",
438 component_count: 8,
439 },
440 extension: ".pkg",
441 },
442 Self::CircleBootstrapImage => ProtocolObjectMetadata {
443 aad_label: b"circle-bootstrap-image",
444 path: ProtocolPathRule::CircleCandidate {
445 kind: "bootstraps",
446 component_count: 9,
447 },
448 extension: ".db",
449 },
450 Self::CircleEpochCloseIntent => ProtocolObjectMetadata {
451 aad_label: b"circle-epoch-close-intent",
452 path: ProtocolPathRule::Exact(&[ExactPathShape {
453 component_count: 6,
454 fixed_components: &[(0, "circles"), (2, "epoch-close"), (4, "intent")],
455 }]),
456 extension: ".json",
457 },
458 Self::CircleEpochCloseOutcome => ProtocolObjectMetadata {
459 aad_label: b"circle-epoch-close-outcome",
460 path: ProtocolPathRule::Exact(&[ExactPathShape {
461 component_count: 5,
462 fixed_components: &[(0, "circles"), (2, "epoch-close"), (4, "outcome")],
463 }]),
464 extension: ".json",
465 },
466 Self::CircleEpochCloseResponse => ProtocolObjectMetadata {
467 aad_label: b"circle-epoch-close-response",
468 path: ProtocolPathRule::Exact(&[ExactPathShape {
469 component_count: 6,
470 fixed_components: &[(0, "circles"), (2, "epoch-close"), (4, "responses")],
471 }]),
472 extension: ".json",
473 },
474 Self::CircleAccessLeaf => ProtocolObjectMetadata {
475 aad_label: b"circle-access-leaf",
476 path: ProtocolPathRule::CircleCandidate {
477 kind: "access-leaves",
478 component_count: 9,
479 },
480 extension: "",
481 },
482 Self::CircleAccessEnvelope => ProtocolObjectMetadata {
483 aad_label: b"circle-access-envelope",
484 path: ProtocolPathRule::CircleCandidate {
485 kind: "access-envelopes",
486 component_count: 8,
487 },
488 extension: ".json",
489 },
490 Self::CircleAcknowledgement => ProtocolObjectMetadata {
491 aad_label: b"circle-acknowledgement",
492 path: ProtocolPathRule::Exact(&[ExactPathShape {
493 component_count: 5,
494 fixed_components: &[(0, "circles"), (2, "acks")],
495 }]),
496 extension: ".json",
497 },
498 Self::CircleSnapshotMeta => ProtocolObjectMetadata {
499 aad_label: b"circle-snapshot-meta",
500 path: ProtocolPathRule::Exact(&[ExactPathShape {
501 component_count: 5,
502 fixed_components: &[(0, "circles"), (2, "snapshots")],
503 }]),
504 extension: ".json",
505 },
506 Self::CircleSnapshotImage => ProtocolObjectMetadata {
507 aad_label: b"circle-snapshot-image",
508 path: ProtocolPathRule::Exact(&[ExactPathShape {
509 component_count: 5,
510 fixed_components: &[(0, "circles"), (2, "snapshot-images")],
511 }]),
512 extension: ".db",
513 },
514 }
515 }
516
517 pub(crate) fn aad_label(self) -> &'static [u8] {
518 self.metadata().aad_label
519 }
520
521 pub(crate) fn extension(self) -> &'static str {
522 self.metadata().extension
523 }
524}
525
526#[derive(Clone, Copy, Debug, PartialEq, Eq)]
528pub struct StoreEncryptedProtocolObjectDomain(ProtectedObjectDomain);
529
530#[derive(Clone, Copy, Debug, PartialEq, Eq)]
533pub struct SignedStoreProtocolObjectDomain(ProtectedObjectDomain);
534
535#[derive(Clone, Copy, Debug, PartialEq, Eq)]
537pub struct CircleProtocolObjectDomain(ProtectedObjectDomain);
538
539#[derive(Clone, Copy, Debug, PartialEq, Eq)]
541pub struct RecipientSealedProtocolObjectDomain(ProtectedObjectDomain);
542
543pub struct ProtocolObjectDomain;
546
547#[allow(non_upper_case_globals)]
548impl ProtocolObjectDomain {
549 pub const StoreProtocolRoot: SignedStoreProtocolObjectDomain =
550 SignedStoreProtocolObjectDomain(ProtectedObjectDomain::StoreProtocolRoot);
551 pub const StoreCommit: SignedStoreProtocolObjectDomain =
552 SignedStoreProtocolObjectDomain(ProtectedObjectDomain::StoreCommit);
553 pub const StoreHead: SignedStoreProtocolObjectDomain =
554 SignedStoreProtocolObjectDomain(ProtectedObjectDomain::StoreHead);
555 pub const StoreAck: SignedStoreProtocolObjectDomain =
556 SignedStoreProtocolObjectDomain(ProtectedObjectDomain::StoreAck);
557 pub const StoreDeviceRegistration: SignedStoreProtocolObjectDomain =
558 SignedStoreProtocolObjectDomain(ProtectedObjectDomain::StoreDeviceRegistration);
559 pub const DeviceJoinAttempt: SignedStoreProtocolObjectDomain =
560 SignedStoreProtocolObjectDomain(ProtectedObjectDomain::DeviceJoinAttempt);
561 pub const DeviceJoinOutcome: SignedStoreProtocolObjectDomain =
562 SignedStoreProtocolObjectDomain(ProtectedObjectDomain::DeviceJoinOutcome);
563 pub const DeviceJoinAbandonment: SignedStoreProtocolObjectDomain =
564 SignedStoreProtocolObjectDomain(ProtectedObjectDomain::DeviceJoinAbandonment);
565 pub const DeviceJoinCleanupReceipt: SignedStoreProtocolObjectDomain =
566 SignedStoreProtocolObjectDomain(ProtectedObjectDomain::DeviceJoinCleanupReceipt);
567 pub const DeviceJoinTransport: RecipientSealedProtocolObjectDomain =
570 RecipientSealedProtocolObjectDomain(ProtectedObjectDomain::DeviceJoinTransport);
571 pub const StoreDeviceExclusionProposal: SignedStoreProtocolObjectDomain =
572 SignedStoreProtocolObjectDomain(ProtectedObjectDomain::StoreDeviceExclusionProposal);
573 pub const StoreDeviceExclusionOutcome: SignedStoreProtocolObjectDomain =
574 SignedStoreProtocolObjectDomain(ProtectedObjectDomain::StoreDeviceExclusionOutcome);
575 pub const StoreReclaimEvidence: StoreEncryptedProtocolObjectDomain =
576 StoreEncryptedProtocolObjectDomain(ProtectedObjectDomain::StoreReclaimEvidence);
577 pub const StoreReclaimAuthorization: SignedStoreProtocolObjectDomain =
578 SignedStoreProtocolObjectDomain(ProtectedObjectDomain::StoreReclaimAuthorization);
579 pub const StoreReclaimReceipt: SignedStoreProtocolObjectDomain =
580 SignedStoreProtocolObjectDomain(ProtectedObjectDomain::StoreReclaimReceipt);
581 pub const ProviderAccessGrant: SignedStoreProtocolObjectDomain =
582 SignedStoreProtocolObjectDomain(ProtectedObjectDomain::ProviderAccessGrant);
583 pub const ProviderAccessWithdrawal: SignedStoreProtocolObjectDomain =
584 SignedStoreProtocolObjectDomain(ProtectedObjectDomain::ProviderAccessWithdrawal);
585 pub const OwnerRecoveryNode: SignedStoreProtocolObjectDomain =
586 SignedStoreProtocolObjectDomain(ProtectedObjectDomain::OwnerRecoveryNode);
587 pub const StoreSnapshotMeta: SignedStoreProtocolObjectDomain =
588 SignedStoreProtocolObjectDomain(ProtectedObjectDomain::StoreSnapshotMeta);
589 pub const StoreSnapshotImage: StoreEncryptedProtocolObjectDomain =
590 StoreEncryptedProtocolObjectDomain(ProtectedObjectDomain::StoreSnapshotImage);
591 pub const StoreMembershipEntry: SignedStoreProtocolObjectDomain =
592 SignedStoreProtocolObjectDomain(ProtectedObjectDomain::StoreMembershipEntry);
593 pub const StoreMembershipHead: SignedStoreProtocolObjectDomain =
594 SignedStoreProtocolObjectDomain(ProtectedObjectDomain::StoreMembershipHead);
595 pub const StoreMembershipResolution: SignedStoreProtocolObjectDomain =
596 SignedStoreProtocolObjectDomain(ProtectedObjectDomain::StoreMembershipResolution);
597 pub const StoreWrappedKey: RecipientSealedProtocolObjectDomain =
598 RecipientSealedProtocolObjectDomain(ProtectedObjectDomain::StoreWrappedKey);
599 pub const CircleAccessLeaf: RecipientSealedProtocolObjectDomain =
600 RecipientSealedProtocolObjectDomain(ProtectedObjectDomain::CircleAccessLeaf);
601 pub const StorePackage: StoreEncryptedProtocolObjectDomain =
602 StoreEncryptedProtocolObjectDomain(ProtectedObjectDomain::StorePackage);
603 pub const CircleControl: StoreEncryptedProtocolObjectDomain =
604 StoreEncryptedProtocolObjectDomain(ProtectedObjectDomain::CircleControl);
605 pub const CircleAccessEnvelope: StoreEncryptedProtocolObjectDomain =
606 StoreEncryptedProtocolObjectDomain(ProtectedObjectDomain::CircleAccessEnvelope);
607 pub const CircleRoster: CircleProtocolObjectDomain =
608 CircleProtocolObjectDomain(ProtectedObjectDomain::CircleRoster);
609 pub const CircleRosterResolution: CircleProtocolObjectDomain =
610 CircleProtocolObjectDomain(ProtectedObjectDomain::CircleRosterResolution);
611 pub const CircleMetadata: CircleProtocolObjectDomain =
612 CircleProtocolObjectDomain(ProtectedObjectDomain::CircleMetadata);
613 pub const CirclePackage: CircleProtocolObjectDomain =
614 CircleProtocolObjectDomain(ProtectedObjectDomain::CirclePackage);
615 pub const CircleBootstrapImage: CircleProtocolObjectDomain =
616 CircleProtocolObjectDomain(ProtectedObjectDomain::CircleBootstrapImage);
617 pub const CircleEpochCloseIntent: CircleProtocolObjectDomain =
618 CircleProtocolObjectDomain(ProtectedObjectDomain::CircleEpochCloseIntent);
619 pub const CircleEpochCloseOutcome: StoreEncryptedProtocolObjectDomain =
620 StoreEncryptedProtocolObjectDomain(ProtectedObjectDomain::CircleEpochCloseOutcome);
621 pub const CircleEpochCloseResponse: StoreEncryptedProtocolObjectDomain =
622 StoreEncryptedProtocolObjectDomain(ProtectedObjectDomain::CircleEpochCloseResponse);
623 pub const CircleAcknowledgement: CircleProtocolObjectDomain =
624 CircleProtocolObjectDomain(ProtectedObjectDomain::CircleAcknowledgement);
625 pub const CircleSnapshotMeta: CircleProtocolObjectDomain =
626 CircleProtocolObjectDomain(ProtectedObjectDomain::CircleSnapshotMeta);
627 pub const CircleSnapshotImage: CircleProtocolObjectDomain =
628 CircleProtocolObjectDomain(ProtectedObjectDomain::CircleSnapshotImage);
629}
630
631pub struct ProtocolObjectContext {
658 store_root_hash: ObjectHash,
659 domain: ProtectedObjectDomain,
660 protection: ProtocolObjectProtection,
661}
662
663#[derive(Clone)]
664pub(crate) enum ProtocolObjectProtection {
665 StoreEncrypted,
666 SignedPlaintext,
667 Circle(crate::encryption::EncryptionService),
668 RecipientSealed,
669}
670
671impl ProtocolObjectContext {
672 pub fn store_encrypted(
673 store_root_hash: ObjectHash,
674 domain: StoreEncryptedProtocolObjectDomain,
675 ) -> Self {
676 Self {
677 store_root_hash,
678 domain: domain.0,
679 protection: ProtocolObjectProtection::StoreEncrypted,
680 }
681 }
682
683 pub fn signed_plaintext(
684 store_root_hash: ObjectHash,
685 domain: SignedStoreProtocolObjectDomain,
686 ) -> Self {
687 Self {
688 store_root_hash,
689 domain: domain.0,
690 protection: ProtocolObjectProtection::SignedPlaintext,
691 }
692 }
693
694 pub fn circle(
695 store_root_hash: ObjectHash,
696 domain: CircleProtocolObjectDomain,
697 encryption: crate::encryption::EncryptionService,
698 ) -> Self {
699 Self {
700 store_root_hash,
701 domain: domain.0,
702 protection: ProtocolObjectProtection::Circle(encryption),
703 }
704 }
705
706 pub fn recipient_sealed(
707 store_root_hash: ObjectHash,
708 domain: RecipientSealedProtocolObjectDomain,
709 ) -> Self {
710 Self {
711 store_root_hash,
712 domain: domain.0,
713 protection: ProtocolObjectProtection::RecipientSealed,
714 }
715 }
716
717 pub fn store_root_hash(&self) -> ObjectHash {
718 self.store_root_hash
719 }
720
721 pub(crate) fn domain(&self) -> ProtectedObjectDomain {
722 self.domain
723 }
724
725 pub(crate) fn protection(&self) -> &ProtocolObjectProtection {
726 &self.protection
727 }
728
729 pub fn validate_path(&self, semantic_prefix: &str) -> Result<(), StorageError> {
730 let metadata = self.domain.metadata();
731 if semantic_prefix.contains("/copies/") || !metadata.path.accepts(semantic_prefix) {
732 return Err(StorageError::Parse(format!(
733 "object domain {:?} does not accept semantic path {semantic_prefix:?}",
734 self.domain
735 )));
736 }
737 Ok(())
738 }
739
740 pub fn validate_extension(&self, extension: &str) -> Result<(), StorageError> {
741 if extension != self.domain.extension() {
742 return Err(StorageError::Parse(format!(
743 "object domain {:?} does not accept extension {extension:?}",
744 self.domain
745 )));
746 }
747 Ok(())
748 }
749
750 pub fn validate_reference(
751 &self,
752 object: &ExactObjectRef,
753 semantic_prefix: &str,
754 ) -> Result<(), StorageError> {
755 self.validate_slot(object.slot(), semantic_prefix)
756 }
757
758 pub fn validate_slot(
759 &self,
760 slot: &ObjectSlot,
761 semantic_prefix: &str,
762 ) -> Result<(), StorageError> {
763 self.validate_path(semantic_prefix)?;
764 let expected = format!("{semantic_prefix}{}", self.domain.extension());
765 if slot.logical_key() != expected {
766 return Err(StorageError::Parse(format!(
767 "protocol object {:?} does not match semantic path {semantic_prefix:?}",
768 slot.logical_key()
769 )));
770 }
771 Ok(())
772 }
773}
774
775#[derive(Clone)]
777pub enum BlobSpoolProtection {
778 Opaque(crate::encryption::EncryptionService),
779 Browsable,
780}
781
782#[derive(Clone, Copy, Debug, PartialEq, Eq)]
783pub enum BlobSpoolWrite {
784 Created,
785 Reused,
786}
787
788#[derive(Clone, Copy)]
789pub struct BlobWriteAuthority<'a> {
790 pub reference: &'a crate::sync::store_commit::StoreDeviceRegistrationRef,
791 pub registration: &'a crate::sync::store_commit::StoreDeviceRegistration,
792}
793
794impl<'a> BlobWriteAuthority<'a> {
795 pub fn new(
796 reference: &'a crate::sync::store_commit::StoreDeviceRegistrationRef,
797 registration: &'a crate::sync::store_commit::StoreDeviceRegistration,
798 ) -> Result<Self, StorageError> {
799 reference
800 .verify_registration(registration)
801 .map_err(|error| StorageError::InvalidContent(error.to_string()))?;
802 Ok(Self {
803 reference,
804 registration,
805 })
806 }
807}
808
809#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
811#[serde(tag = "provider", rename_all = "snake_case", deny_unknown_fields)]
812pub enum StoreProviderBinding {
813 S3 {
814 endpoint: S3EndpointBinding,
815 region: String,
816 bucket: String,
817 key_prefix: Option<String>,
818 },
819 GoogleDrive {
820 corpus: GoogleDriveCorpus,
821 },
822 Dropbox {
823 namespace_id: String,
824 },
825 OneDrive {
826 drive_id: String,
827 folder_id: String,
828 },
829 CloudKit {
830 container_id: String,
831 environment: CloudKitEnvironment,
832 owner_name: String,
833 zone_name: String,
834 },
835}
836
837#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
838#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
839pub enum S3EndpointBinding {
840 Aws { partition: String },
841 Custom { origin: String },
842}
843
844#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
845#[serde(tag = "corpus", rename_all = "snake_case", deny_unknown_fields)]
846pub enum GoogleDriveCorpus {
847 MyDrive { folder_id: String },
848 SharedDrive { drive_id: String, folder_id: String },
849}
850
851#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
852#[serde(rename_all = "snake_case")]
853pub enum CloudKitEnvironment {
854 Development,
855 Production,
856}
857
858#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
859#[serde(tag = "provider", rename_all = "snake_case", deny_unknown_fields)]
860pub enum ProviderPrincipalId {
861 Aws {
862 account_id: String,
863 principal: AwsPrincipal,
864 },
865 CustomS3Credential {
866 access_key_id_hash: ObjectHash,
867 },
868 GoogleDrive {
869 permission_id: String,
870 },
871 Dropbox {
872 account_id: String,
873 },
874 OneDrive {
875 user_id: String,
876 },
877 CloudKitPrivateZoneOwner {
878 record_name: String,
879 },
880 CloudKitSharedZoneParticipant {
881 record_name: String,
882 },
883}
884
885#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
886#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
887pub enum AwsPrincipal {
888 Root,
889 User { arn: String, user_id: String },
890 Role { role_id: String },
891}
892
893#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
894#[serde(deny_unknown_fields)]
895pub struct ProviderDeviceBinding {
896 pub principal: ProviderPrincipalId,
897}
898
899#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
900#[serde(deny_unknown_fields)]
901pub struct ResolvedProviderBinding {
902 pub store: StoreProviderBinding,
903 pub device: ProviderDeviceBinding,
904}
905
906impl StoreProviderBinding {
907 pub fn validate(&self) -> Result<(), StorageError> {
908 fn present(label: &str, value: &str) -> Result<(), StorageError> {
909 if value.is_empty() {
910 Err(StorageError::Configuration(format!("{label} is empty")))
911 } else {
912 Ok(())
913 }
914 }
915
916 match self {
917 Self::S3 {
918 endpoint,
919 region,
920 bucket,
921 key_prefix,
922 } => {
923 present("S3 region", region)?;
924 present("S3 bucket", bucket)?;
925 if key_prefix.as_deref().is_some_and(str::is_empty) {
926 return Err(StorageError::Configuration(
927 "S3 key prefix is empty instead of absent".to_string(),
928 ));
929 }
930 match endpoint {
931 S3EndpointBinding::Aws { partition } => present("AWS partition", partition),
932 S3EndpointBinding::Custom { origin } => {
933 let canonical = super::provider::canonical_custom_s3_origin(origin)?;
934 if canonical != *origin {
935 return Err(StorageError::Configuration(
936 "custom S3 origin is not canonical".to_string(),
937 ));
938 }
939 Ok(())
940 }
941 }
942 }
943 Self::GoogleDrive { corpus } => match corpus {
944 GoogleDriveCorpus::MyDrive { folder_id } => {
945 present("Google Drive folder id", folder_id)
946 }
947 GoogleDriveCorpus::SharedDrive {
948 drive_id,
949 folder_id,
950 } => {
951 present("Google Drive id", drive_id)?;
952 present("Google Drive folder id", folder_id)
953 }
954 },
955 Self::Dropbox { namespace_id } => present("Dropbox namespace id", namespace_id),
956 Self::OneDrive {
957 drive_id,
958 folder_id,
959 } => {
960 present("OneDrive drive id", drive_id)?;
961 present("OneDrive folder id", folder_id)
962 }
963 Self::CloudKit {
964 container_id,
965 owner_name,
966 zone_name,
967 ..
968 } => {
969 present("CloudKit container id", container_id)?;
970 present("CloudKit owner name", owner_name)?;
971 present("CloudKit zone name", zone_name)
972 }
973 }
974 }
975}
976
977impl ProviderDeviceBinding {
978 pub fn validate_for(&self, store: &StoreProviderBinding) -> Result<(), StorageError> {
979 fn present(label: &str, value: &str) -> Result<(), StorageError> {
980 if value.is_empty() {
981 Err(StorageError::Configuration(format!("{label} is empty")))
982 } else {
983 Ok(())
984 }
985 }
986
987 let compatible = matches!(
988 (store, &self.principal),
989 (
990 StoreProviderBinding::S3 {
991 endpoint: S3EndpointBinding::Aws { .. },
992 ..
993 },
994 ProviderPrincipalId::Aws { .. }
995 ) | (
996 StoreProviderBinding::S3 {
997 endpoint: S3EndpointBinding::Custom { .. },
998 ..
999 },
1000 ProviderPrincipalId::CustomS3Credential { .. }
1001 ) | (
1002 StoreProviderBinding::GoogleDrive { .. },
1003 ProviderPrincipalId::GoogleDrive { .. }
1004 ) | (
1005 StoreProviderBinding::Dropbox { .. },
1006 ProviderPrincipalId::Dropbox { .. }
1007 ) | (
1008 StoreProviderBinding::OneDrive { .. },
1009 ProviderPrincipalId::OneDrive { .. }
1010 ) | (
1011 StoreProviderBinding::CloudKit { .. },
1012 ProviderPrincipalId::CloudKitPrivateZoneOwner { .. }
1013 | ProviderPrincipalId::CloudKitSharedZoneParticipant { .. }
1014 )
1015 );
1016 if !compatible {
1017 return Err(StorageError::Configuration(
1018 "provider principal is incompatible with the Store provider binding".to_string(),
1019 ));
1020 }
1021 match &self.principal {
1022 ProviderPrincipalId::Aws {
1023 account_id,
1024 principal,
1025 } => {
1026 if account_id.len() != 12 || !account_id.bytes().all(|byte| byte.is_ascii_digit()) {
1027 return Err(StorageError::Configuration(
1028 "AWS account id must contain exactly 12 decimal digits".to_string(),
1029 ));
1030 }
1031 match principal {
1032 AwsPrincipal::Root => Ok(()),
1033 AwsPrincipal::User { arn, user_id } => {
1034 present("AWS user id", user_id)?;
1035 let fields: Vec<_> = arn.splitn(6, ':').collect();
1036 let StoreProviderBinding::S3 {
1037 endpoint: S3EndpointBinding::Aws { partition },
1038 ..
1039 } = store
1040 else {
1041 return Err(StorageError::Configuration(
1042 "AWS user principal is bound to non-AWS S3".to_string(),
1043 ));
1044 };
1045 if fields.len() != 6
1046 || fields[0] != "arn"
1047 || fields[1] != partition
1048 || fields[2] != "iam"
1049 || !fields[3].is_empty()
1050 || fields[4] != account_id
1051 || !fields[5].starts_with("user/")
1052 || fields[5].len() == "user/".len()
1053 {
1054 return Err(StorageError::Configuration(
1055 "AWS IAM user ARN is malformed or differs from its Store binding"
1056 .to_string(),
1057 ));
1058 }
1059 Ok(())
1060 }
1061 AwsPrincipal::Role { role_id } => {
1062 present("AWS role id", role_id)?;
1063 if role_id.contains(':') {
1064 return Err(StorageError::Configuration(
1065 "AWS role id must be the stable prefix before the session separator"
1066 .to_string(),
1067 ));
1068 }
1069 Ok(())
1070 }
1071 }
1072 }
1073 ProviderPrincipalId::CustomS3Credential { .. } => Ok(()),
1074 ProviderPrincipalId::GoogleDrive { permission_id } => {
1075 present("Google Drive permission id", permission_id)
1076 }
1077 ProviderPrincipalId::Dropbox { account_id } => {
1078 present("Dropbox account id", account_id)
1079 }
1080 ProviderPrincipalId::OneDrive { user_id } => present("OneDrive user id", user_id),
1081 ProviderPrincipalId::CloudKitPrivateZoneOwner { record_name } => {
1082 present("CloudKit private-zone owner record name", record_name)
1083 }
1084 ProviderPrincipalId::CloudKitSharedZoneParticipant { record_name } => {
1085 present("CloudKit shared-zone participant record name", record_name)
1086 }
1087 }
1088 }
1089}
1090
1091impl ResolvedProviderBinding {
1092 pub fn validate(&self) -> Result<(), StorageError> {
1093 self.store.validate()?;
1094 self.device.validate_for(&self.store)
1095 }
1096}
1097
1098#[derive(
1100 Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
1101)]
1102#[serde(deny_unknown_fields)]
1103pub struct ExactObjectRef {
1104 slot: ObjectSlot,
1105 stored_size: u64,
1106 stored_hash: ObjectHash,
1107}
1108
1109impl ExactObjectRef {
1110 pub fn new(slot: ObjectSlot, stored_size: u64, stored_hash: ObjectHash) -> Self {
1111 Self {
1112 slot,
1113 stored_size,
1114 stored_hash,
1115 }
1116 }
1117
1118 pub fn slot(&self) -> &ObjectSlot {
1119 &self.slot
1120 }
1121
1122 pub fn stored_size(&self) -> u64 {
1123 self.stored_size
1124 }
1125
1126 pub fn stored_hash(&self) -> ObjectHash {
1127 self.stored_hash
1128 }
1129
1130 pub fn verify(&self, bytes: &[u8]) -> Result<(), StorageError> {
1131 if bytes.len() as u64 != self.stored_size || ObjectHash::digest(bytes) != self.stored_hash {
1132 return Err(StorageError::InvalidContent(format!(
1133 "exact object {} does not match stored size/hash",
1134 self.slot.logical_key()
1135 )));
1136 }
1137 Ok(())
1138 }
1139}
1140
1141#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
1143#[serde(deny_unknown_fields)]
1144pub struct PreparedExactObject {
1145 reference: ExactObjectRef,
1146 stored_bytes: Vec<u8>,
1147}
1148
1149impl<'de> serde::Deserialize<'de> for PreparedExactObject {
1150 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1151 where
1152 D: serde::Deserializer<'de>,
1153 {
1154 #[derive(serde::Deserialize)]
1155 #[serde(deny_unknown_fields)]
1156 struct Fields {
1157 reference: ExactObjectRef,
1158 stored_bytes: Vec<u8>,
1159 }
1160
1161 let fields = Fields::deserialize(deserializer)?;
1162 Self::new(fields.reference, fields.stored_bytes).map_err(serde::de::Error::custom)
1163 }
1164}
1165
1166impl PreparedExactObject {
1167 pub fn new(reference: ExactObjectRef, stored_bytes: Vec<u8>) -> Result<Self, StorageError> {
1168 reference.verify(&stored_bytes)?;
1169 Ok(Self {
1170 reference,
1171 stored_bytes,
1172 })
1173 }
1174
1175 pub fn reference(&self) -> &ExactObjectRef {
1176 &self.reference
1177 }
1178
1179 pub fn stored_bytes(&self) -> &[u8] {
1180 &self.stored_bytes
1181 }
1182}
1183
1184#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
1186pub enum StorageError {
1187 #[error("storage operation failed: {0}")]
1188 Storage(String),
1189 #[error("{operation}; storage cleanup failed: {cleanup}")]
1190 CleanupFailed {
1191 #[source]
1192 operation: Box<StorageError>,
1193 cleanup: Box<StorageError>,
1194 },
1195 #[error("{operation}; exact response-loss readback failed: {readback}")]
1196 UnresolvedOutcome {
1197 #[source]
1198 operation: Box<StorageError>,
1199 readback: Box<StorageError>,
1200 },
1201 #[error("storage configuration is invalid: {0}")]
1202 Configuration(String),
1203 #[error("storage object parse failed: {0}")]
1204 Parse(String),
1205 #[error("object not found: {0}")]
1206 NotFound(String),
1207 #[error("storage object already exists: {0}")]
1208 AlreadyExists(String),
1209 #[error("reserved storage slot contains different bytes: {0}")]
1210 SlotCollision(String),
1211 #[error("decryption failed: {0}")]
1212 Decryption(String),
1213 #[error("remote blob content is invalid: {0}")]
1214 InvalidContent(String),
1215 #[error("local blob filesystem failed: {0}")]
1216 LocalFilesystem(String),
1217 #[error("{0}")]
1220 RotationPending(#[from] crate::sync::cloud_storage::RotationPending),
1221}
1222
1223impl From<crate::storage::cloud::CloudHomeError> for StorageError {
1224 fn from(e: crate::storage::cloud::CloudHomeError) -> Self {
1225 match e {
1226 crate::storage::cloud::CloudHomeError::NotFound(key) => StorageError::NotFound(key),
1227 crate::storage::cloud::CloudHomeError::AlreadyExists(key) => {
1228 StorageError::AlreadyExists(key)
1229 }
1230 crate::storage::cloud::CloudHomeError::Configuration(msg) => {
1231 StorageError::Configuration(msg)
1232 }
1233 crate::storage::cloud::CloudHomeError::Transport(msg) => StorageError::Storage(msg),
1234 crate::storage::cloud::CloudHomeError::CleanupFailed { operation, cleanup } => {
1235 StorageError::CleanupFailed {
1236 operation: Box::new(StorageError::from(*operation)),
1237 cleanup: Box::new(StorageError::from(*cleanup)),
1238 }
1239 }
1240 crate::storage::cloud::CloudHomeError::UnresolvedOutcome {
1241 operation,
1242 readback,
1243 } => StorageError::UnresolvedOutcome {
1244 operation: Box::new(StorageError::from(*operation)),
1245 readback: Box::new(StorageError::from(*readback)),
1246 },
1247 crate::storage::cloud::CloudHomeError::Io(io_err) => {
1248 StorageError::Storage(format!("I/O error: {io_err}"))
1249 }
1250 }
1251 }
1252}
1253
1254impl StorageError {
1255 pub fn is_transport(&self) -> bool {
1256 match self {
1257 Self::Storage(_) => true,
1258 Self::CleanupFailed { operation, .. } | Self::UnresolvedOutcome { operation, .. } => {
1259 operation.is_transport()
1260 }
1261 _ => false,
1262 }
1263 }
1264
1265 pub fn cleanup_causes(&self) -> Option<(&StorageError, &StorageError)> {
1266 match self {
1267 Self::CleanupFailed { operation, cleanup } => Some((operation, cleanup)),
1268 _ => None,
1269 }
1270 }
1271}
1272
1273impl From<crate::store_dir::PathTokenError> for StorageError {
1274 fn from(e: crate::store_dir::PathTokenError) -> Self {
1278 StorageError::Parse(format!("unsafe blob path: {e}"))
1279 }
1280}
1281
1282#[async_trait]
1283pub trait SyncStorage: Send + Sync {
1284 fn store_blob_protection(&self) -> Result<BlobSpoolProtection, StorageError>;
1287
1288 async fn provider_binding(&self) -> Result<ResolvedProviderBinding, StorageError>;
1291
1292 async fn allocate_protocol_slot(
1294 &self,
1295 context: &ProtocolObjectContext,
1296 semantic_prefix: &str,
1297 extension: &str,
1298 ) -> Result<ObjectSlot, StorageError>;
1299
1300 fn prepare_protocol_object(
1302 &self,
1303 context: &ProtocolObjectContext,
1304 slot: ObjectSlot,
1305 semantic_prefix: &str,
1306 data: Vec<u8>,
1307 ) -> Result<PreparedExactObject, StorageError>;
1308
1309 async fn create_protocol_object(
1312 &self,
1313 prepared: &PreparedExactObject,
1314 ) -> Result<(), StorageError>;
1315
1316 async fn read_protocol_object(
1319 &self,
1320 context: &ProtocolObjectContext,
1321 object: &ExactObjectRef,
1322 semantic_prefix: &str,
1323 ) -> Result<Vec<u8>, StorageError>;
1324
1325 async fn read_protocol_slot(
1328 &self,
1329 context: &ProtocolObjectContext,
1330 slot: &ObjectSlot,
1331 semantic_prefix: &str,
1332 ) -> Result<(Vec<u8>, ExactObjectRef), StorageError>;
1333
1334 async fn read_prepared_protocol_slot(
1337 &self,
1338 context: &ProtocolObjectContext,
1339 slot: &ObjectSlot,
1340 semantic_prefix: &str,
1341 ) -> Result<(Vec<u8>, PreparedExactObject), StorageError>;
1342
1343 async fn delete_protocol_object(&self, object: &ExactObjectRef) -> Result<(), StorageError>;
1345
1346 async fn allocate_blob_slot(
1348 &self,
1349 locator: &crate::blob::locator::BlobLocator,
1350 authority: &BlobWriteAuthority<'_>,
1351 ) -> Result<ObjectSlot, StorageError>;
1352
1353 async fn seal_blob_to_spool(
1356 &self,
1357 locator: &crate::blob::locator::BlobLocator,
1358 authority: &BlobWriteAuthority<'_>,
1359 protection: BlobSpoolProtection,
1360 plaintext_file: &Path,
1361 spool_file: &Path,
1362 ) -> Result<BlobSpoolWrite, StorageError>;
1363
1364 async fn prepare_blob_object(
1366 &self,
1367 locator: &crate::blob::locator::BlobLocator,
1368 authority: &BlobWriteAuthority<'_>,
1369 slot: ObjectSlot,
1370 stored_file: &Path,
1371 ) -> Result<crate::blob::locator::StoredBlobRef, StorageError>;
1372
1373 async fn create_blob_object_from_file(
1375 &self,
1376 blob: &crate::blob::locator::StoredBlobRef,
1377 authority: &BlobWriteAuthority<'_>,
1378 stored_file: &Path,
1379 progress: &crate::storage::cloud::UploadProgress<'_>,
1380 ) -> Result<(), StorageError>;
1381
1382 async fn verify_blob_object(
1384 &self,
1385 blob: &crate::blob::locator::StoredBlobRef,
1386 ) -> Result<(), StorageError>;
1387
1388 async fn stage_exact_blob_download(
1392 &self,
1393 blob: &crate::blob::locator::StoredBlobRef,
1394 dest: &Path,
1395 ) -> Result<crate::local_blob::AtomicStagedFile, StorageError>;
1396
1397 async fn stage_verified_blob_plaintext(
1401 &self,
1402 blob: &crate::blob::locator::StoredBlobRef,
1403 protection: BlobSpoolProtection,
1404 dest: &Path,
1405 ) -> Result<crate::local_blob::AtomicStagedFile, StorageError>;
1406
1407 async fn delete_blob_object(
1409 &self,
1410 blob: &crate::blob::locator::StoredBlobRef,
1411 ) -> Result<(), StorageError>;
1412}
1413
1414#[async_trait]
1415impl<T> SyncStorage for std::sync::Arc<T>
1416where
1417 T: SyncStorage + ?Sized,
1418{
1419 fn store_blob_protection(&self) -> Result<BlobSpoolProtection, StorageError> {
1420 (**self).store_blob_protection()
1421 }
1422
1423 async fn provider_binding(&self) -> Result<ResolvedProviderBinding, StorageError> {
1424 (**self).provider_binding().await
1425 }
1426
1427 async fn allocate_protocol_slot(
1428 &self,
1429 context: &ProtocolObjectContext,
1430 semantic_prefix: &str,
1431 extension: &str,
1432 ) -> Result<ObjectSlot, StorageError> {
1433 (**self)
1434 .allocate_protocol_slot(context, semantic_prefix, extension)
1435 .await
1436 }
1437
1438 fn prepare_protocol_object(
1439 &self,
1440 context: &ProtocolObjectContext,
1441 slot: ObjectSlot,
1442 semantic_prefix: &str,
1443 data: Vec<u8>,
1444 ) -> Result<PreparedExactObject, StorageError> {
1445 (**self).prepare_protocol_object(context, slot, semantic_prefix, data)
1446 }
1447
1448 async fn create_protocol_object(
1449 &self,
1450 prepared: &PreparedExactObject,
1451 ) -> Result<(), StorageError> {
1452 (**self).create_protocol_object(prepared).await
1453 }
1454
1455 async fn read_protocol_object(
1456 &self,
1457 context: &ProtocolObjectContext,
1458 object: &ExactObjectRef,
1459 semantic_prefix: &str,
1460 ) -> Result<Vec<u8>, StorageError> {
1461 (**self)
1462 .read_protocol_object(context, object, semantic_prefix)
1463 .await
1464 }
1465
1466 async fn read_protocol_slot(
1467 &self,
1468 context: &ProtocolObjectContext,
1469 slot: &ObjectSlot,
1470 semantic_prefix: &str,
1471 ) -> Result<(Vec<u8>, ExactObjectRef), StorageError> {
1472 (**self)
1473 .read_protocol_slot(context, slot, semantic_prefix)
1474 .await
1475 }
1476
1477 async fn read_prepared_protocol_slot(
1478 &self,
1479 context: &ProtocolObjectContext,
1480 slot: &ObjectSlot,
1481 semantic_prefix: &str,
1482 ) -> Result<(Vec<u8>, PreparedExactObject), StorageError> {
1483 (**self)
1484 .read_prepared_protocol_slot(context, slot, semantic_prefix)
1485 .await
1486 }
1487
1488 async fn delete_protocol_object(&self, object: &ExactObjectRef) -> Result<(), StorageError> {
1489 (**self).delete_protocol_object(object).await
1490 }
1491
1492 async fn allocate_blob_slot(
1493 &self,
1494 locator: &crate::blob::locator::BlobLocator,
1495 authority: &BlobWriteAuthority<'_>,
1496 ) -> Result<ObjectSlot, StorageError> {
1497 (**self).allocate_blob_slot(locator, authority).await
1498 }
1499
1500 async fn seal_blob_to_spool(
1501 &self,
1502 locator: &crate::blob::locator::BlobLocator,
1503 authority: &BlobWriteAuthority<'_>,
1504 protection: BlobSpoolProtection,
1505 plaintext_file: &Path,
1506 spool_file: &Path,
1507 ) -> Result<BlobSpoolWrite, StorageError> {
1508 (**self)
1509 .seal_blob_to_spool(locator, authority, protection, plaintext_file, spool_file)
1510 .await
1511 }
1512
1513 async fn prepare_blob_object(
1514 &self,
1515 locator: &crate::blob::locator::BlobLocator,
1516 authority: &BlobWriteAuthority<'_>,
1517 slot: ObjectSlot,
1518 stored_file: &Path,
1519 ) -> Result<crate::blob::locator::StoredBlobRef, StorageError> {
1520 (**self)
1521 .prepare_blob_object(locator, authority, slot, stored_file)
1522 .await
1523 }
1524
1525 async fn create_blob_object_from_file(
1526 &self,
1527 blob: &crate::blob::locator::StoredBlobRef,
1528 authority: &BlobWriteAuthority<'_>,
1529 stored_file: &Path,
1530 progress: &crate::storage::cloud::UploadProgress<'_>,
1531 ) -> Result<(), StorageError> {
1532 (**self)
1533 .create_blob_object_from_file(blob, authority, stored_file, progress)
1534 .await
1535 }
1536
1537 async fn verify_blob_object(
1538 &self,
1539 blob: &crate::blob::locator::StoredBlobRef,
1540 ) -> Result<(), StorageError> {
1541 (**self).verify_blob_object(blob).await
1542 }
1543
1544 async fn stage_exact_blob_download(
1545 &self,
1546 blob: &crate::blob::locator::StoredBlobRef,
1547 dest: &Path,
1548 ) -> Result<crate::local_blob::AtomicStagedFile, StorageError> {
1549 (**self).stage_exact_blob_download(blob, dest).await
1550 }
1551
1552 async fn stage_verified_blob_plaintext(
1553 &self,
1554 blob: &crate::blob::locator::StoredBlobRef,
1555 protection: BlobSpoolProtection,
1556 dest: &Path,
1557 ) -> Result<crate::local_blob::AtomicStagedFile, StorageError> {
1558 (**self)
1559 .stage_verified_blob_plaintext(blob, protection, dest)
1560 .await
1561 }
1562
1563 async fn delete_blob_object(
1564 &self,
1565 blob: &crate::blob::locator::StoredBlobRef,
1566 ) -> Result<(), StorageError> {
1567 (**self).delete_blob_object(blob).await
1568 }
1569}
1570
1571#[cfg(test)]
1572mod tests {
1573 use super::*;
1574
1575 struct DomainPathCase {
1576 domain: ProtectedObjectDomain,
1577 valid: &'static [&'static str],
1578 cross_domain: &'static str,
1579 }
1580
1581 fn validates(domain: ProtectedObjectDomain, path: &str) -> bool {
1582 ProtocolObjectContext {
1583 store_root_hash: ObjectHash::digest(b"protocol path grammar"),
1584 domain,
1585 protection: ProtocolObjectProtection::StoreEncrypted,
1586 }
1587 .validate_path(path)
1588 .is_ok()
1589 }
1590
1591 #[test]
1592 fn every_protocol_domain_requires_its_exact_path_grammar() {
1593 let cases = [
1594 DomainPathCase {
1595 domain: ProtectedObjectDomain::StoreProtocolRoot,
1596 valid: &["store-v1/store-protocol-root"],
1597 cross_domain: "store-v1/heads/device/1",
1598 },
1599 DomainPathCase {
1600 domain: ProtectedObjectDomain::StoreCommit,
1601 valid: &["store-v1/candidates/family/commits/device/1/hash"],
1602 cross_domain: "store-v1/candidates/family/packages/device/1/hash",
1603 },
1604 DomainPathCase {
1605 domain: ProtectedObjectDomain::StoreHead,
1606 valid: &["store-v1/heads/device/1"],
1607 cross_domain: "store-v1/acks/device/1",
1608 },
1609 DomainPathCase {
1610 domain: ProtectedObjectDomain::StoreAck,
1611 valid: &["store-v1/acks/device/1"],
1612 cross_domain: "store-v1/heads/device/1",
1613 },
1614 DomainPathCase {
1615 domain: ProtectedObjectDomain::StoreDeviceRegistration,
1616 valid: &[
1617 "store-v1/devices/device",
1618 "store-v1/devices/founder/creation/registration",
1619 ],
1620 cross_domain: "store-v1/device-join-attempts/attempt",
1621 },
1622 DomainPathCase {
1623 domain: ProtectedObjectDomain::DeviceJoinAttempt,
1624 valid: &["store-v1/device-join-attempts/attempt"],
1625 cross_domain: "store-v1/device-join-outcomes/attempt",
1626 },
1627 DomainPathCase {
1628 domain: ProtectedObjectDomain::DeviceJoinOutcome,
1629 valid: &["store-v1/device-join-outcomes/attempt"],
1630 cross_domain: "store-v1/device-join-attempts/attempt",
1631 },
1632 DomainPathCase {
1633 domain: ProtectedObjectDomain::DeviceJoinAbandonment,
1634 valid: &["store-v1/device-join-attempts/attempt"],
1635 cross_domain: "store-v1/device-join-outcomes/attempt",
1636 },
1637 DomainPathCase {
1638 domain: ProtectedObjectDomain::DeviceJoinCleanupReceipt,
1639 valid: &["store-v1/device-join-cleanup-receipts/attempt"],
1640 cross_domain: "store-v1/device-join-attempts/attempt",
1641 },
1642 DomainPathCase {
1643 domain: ProtectedObjectDomain::StoreDeviceExclusionProposal,
1644 valid: &["store-v1/device-exclusion-proposals/device/proposal/hash"],
1645 cross_domain: "store-v1/device-exclusion-outcomes/device/proposal",
1646 },
1647 DomainPathCase {
1648 domain: ProtectedObjectDomain::StoreDeviceExclusionOutcome,
1649 valid: &["store-v1/device-exclusion-outcomes/device/proposal"],
1650 cross_domain: "store-v1/device-exclusion-proposals/device/proposal/hash",
1651 },
1652 DomainPathCase {
1653 domain: ProtectedObjectDomain::StoreReclaimEvidence,
1654 valid: &["store-v1/reclaim/evidence/hash"],
1655 cross_domain: "store-v1/reclaim/authorizations/hash",
1656 },
1657 DomainPathCase {
1658 domain: ProtectedObjectDomain::StoreReclaimAuthorization,
1659 valid: &["store-v1/reclaim/authorizations/hash"],
1660 cross_domain: "store-v1/reclaim/evidence/hash",
1661 },
1662 DomainPathCase {
1663 domain: ProtectedObjectDomain::StoreReclaimReceipt,
1664 valid: &["store-v1/reclaim/receipts/hash"],
1665 cross_domain: "store-v1/reclaim/authorizations/hash",
1666 },
1667 DomainPathCase {
1668 domain: ProtectedObjectDomain::ProviderAccessGrant,
1669 valid: &["store-v1/provider-access/grants/grant"],
1670 cross_domain: "store-v1/provider-access/withdrawals/grant",
1671 },
1672 DomainPathCase {
1673 domain: ProtectedObjectDomain::ProviderAccessWithdrawal,
1674 valid: &["store-v1/provider-access/withdrawals/grant"],
1675 cross_domain: "store-v1/provider-access/grants/grant",
1676 },
1677 DomainPathCase {
1678 domain: ProtectedObjectDomain::OwnerRecoveryNode,
1679 valid: &["store-v1/recovery/owner/grant/1"],
1680 cross_domain: "store-v1/snapshots/owner/hash",
1681 },
1682 DomainPathCase {
1683 domain: ProtectedObjectDomain::StoreSnapshotMeta,
1684 valid: &["store-v1/snapshots/owner/hash"],
1685 cross_domain: "store-v1/snapshot-images/owner/hash",
1686 },
1687 DomainPathCase {
1688 domain: ProtectedObjectDomain::StoreSnapshotImage,
1689 valid: &["store-v1/snapshot-images/owner/hash"],
1690 cross_domain: "store-v1/snapshots/owner/hash",
1691 },
1692 DomainPathCase {
1693 domain: ProtectedObjectDomain::StoreMembershipEntry,
1694 valid: &["store-v1/membership/entries/owner/grant/stream/1/hash"],
1695 cross_domain: "store-v1/membership/heads/owner/grant/stream/1/hash",
1696 },
1697 DomainPathCase {
1698 domain: ProtectedObjectDomain::StoreMembershipHead,
1699 valid: &[
1700 "store-v1/membership/heads/owner/grant/stream/1",
1701 "store-v1/membership/heads/founder/creation/1",
1702 ],
1703 cross_domain: "store-v1/membership/entries/owner/grant/stream/1/hash",
1704 },
1705 DomainPathCase {
1706 domain: ProtectedObjectDomain::StoreMembershipResolution,
1707 valid: &["store-v1/membership/resolutions/conflict/resolver/hash"],
1708 cross_domain: "store-v1/membership/entries/owner/grant/stream/1/hash",
1709 },
1710 DomainPathCase {
1711 domain: ProtectedObjectDomain::StoreWrappedKey,
1712 valid: &["keys/owner/recipient/1/hash"],
1713 cross_domain: "store-v1/membership/entries/owner/grant/stream/1/hash",
1714 },
1715 DomainPathCase {
1716 domain: ProtectedObjectDomain::StorePackage,
1717 valid: &["store-v1/candidates/family/packages/device/1/hash"],
1718 cross_domain: "store-v1/candidates/family/commits/device/1/hash",
1719 },
1720 DomainPathCase {
1721 domain: ProtectedObjectDomain::CircleControl,
1722 valid: &[
1723 "circle-control/circle/merge/entries/owner/device/grant/stream/1/hash",
1724 "circle-control/circle/merge/heads/owner/device/grant/stream/1",
1725 ],
1726 cross_domain: "circles/circle/roster/entries/owner/device/grant/stream/1/hash",
1727 },
1728 DomainPathCase {
1729 domain: ProtectedObjectDomain::CircleRoster,
1730 valid: &[
1731 "circles/circle/roster/entries/owner/device/grant/stream/1/hash",
1732 "circles/circle/roster/heads/owner/device/grant/stream/1",
1733 ],
1734 cross_domain: "circles/circle/roster/resolutions/conflict/resolver/hash",
1735 },
1736 DomainPathCase {
1737 domain: ProtectedObjectDomain::CircleRosterResolution,
1738 valid: &["circles/circle/roster/resolutions/conflict/resolver/hash"],
1739 cross_domain: "circles/circle/roster/entries/owner/device/grant/stream/1/hash",
1740 },
1741 DomainPathCase {
1742 domain: ProtectedObjectDomain::CircleMetadata,
1743 valid: &[
1744 "circles/circle/metadata/entries/owner/device/grant/stream/1/hash",
1745 "circles/circle/metadata/heads/owner/device/grant/stream/1",
1746 ],
1747 cross_domain: "circles/circle/roster/entries/owner/device/grant/stream/1/hash",
1748 },
1749 DomainPathCase {
1750 domain: ProtectedObjectDomain::CirclePackage,
1751 valid: &["circles/circle/candidates/family/packages/device/1/hash"],
1752 cross_domain:
1753 "circles/circle/candidates/family/access-envelopes/owner/recipient/hash",
1754 },
1755 DomainPathCase {
1756 domain: ProtectedObjectDomain::CircleBootstrapImage,
1757 valid: &["circles/circle/candidates/family/bootstraps/owner/epoch/recipient/hash"],
1758 cross_domain: "circles/circle/candidates/family/packages/device/1/hash",
1759 },
1760 DomainPathCase {
1761 domain: ProtectedObjectDomain::CircleEpochCloseIntent,
1762 valid: &["circles/circle/epoch-close/close/intent/hash"],
1763 cross_domain: "circles/circle/epoch-close/close/outcome",
1764 },
1765 DomainPathCase {
1766 domain: ProtectedObjectDomain::CircleEpochCloseOutcome,
1767 valid: &["circles/circle/epoch-close/close/outcome"],
1768 cross_domain: "circles/circle/epoch-close/close/responses/device",
1769 },
1770 DomainPathCase {
1771 domain: ProtectedObjectDomain::CircleEpochCloseResponse,
1772 valid: &["circles/circle/epoch-close/close/responses/device"],
1773 cross_domain: "circles/circle/epoch-close/close/outcome",
1774 },
1775 DomainPathCase {
1776 domain: ProtectedObjectDomain::CircleAccessLeaf,
1777 valid: &[
1778 "circles/circle/candidates/family/access-leaves/owner/epoch/recipient/leaf",
1779 ],
1780 cross_domain:
1781 "circles/circle/candidates/family/access-envelopes/owner/recipient/hash",
1782 },
1783 DomainPathCase {
1784 domain: ProtectedObjectDomain::CircleAccessEnvelope,
1785 valid: &["circles/circle/candidates/family/access-envelopes/owner/recipient/hash"],
1786 cross_domain:
1787 "circles/circle/candidates/family/access-leaves/owner/epoch/recipient/leaf",
1788 },
1789 ];
1790
1791 for case in cases {
1792 for valid in case.valid {
1793 assert!(
1794 validates(case.domain, valid),
1795 "{:?} rejected {valid}",
1796 case.domain
1797 );
1798 assert!(
1799 !validates(case.domain, &format!("{valid}/extra")),
1800 "{:?} accepted an extra component after {valid}",
1801 case.domain,
1802 );
1803 let (missing, _) = valid
1804 .rsplit_once('/')
1805 .expect("protocol paths have more than one component");
1806 assert!(
1807 !validates(case.domain, missing),
1808 "{:?} accepted a missing component in {valid}",
1809 case.domain,
1810 );
1811 }
1812 assert!(
1813 !validates(case.domain, case.cross_domain),
1814 "{:?} accepted cross-domain path {}",
1815 case.domain,
1816 case.cross_domain,
1817 );
1818 }
1819 assert!(!validates(
1820 ProtectedObjectDomain::StoreDeviceRegistration,
1821 "store-v1/devices/founder",
1822 ));
1823 assert!(!validates(
1824 ProtectedObjectDomain::StoreMembershipHead,
1825 "store-v1/membership/heads/founder/creation/1/extra",
1826 ));
1827 }
1828
1829 #[test]
1830 fn candidate_protocol_domains_reject_reordered_and_nested_components() {
1831 let cases = [
1832 (
1833 ProtectedObjectDomain::StoreCommit,
1834 "store-v1/candidates/family/commits/device/1/hash",
1835 1,
1836 3,
1837 ),
1838 (
1839 ProtectedObjectDomain::StorePackage,
1840 "store-v1/candidates/family/packages/device/1/hash",
1841 1,
1842 3,
1843 ),
1844 (
1845 ProtectedObjectDomain::CirclePackage,
1846 "circles/circle/candidates/family/packages/device/1/hash",
1847 2,
1848 4,
1849 ),
1850 (
1851 ProtectedObjectDomain::CircleBootstrapImage,
1852 "circles/circle/candidates/family/bootstraps/owner/epoch/recipient/hash",
1853 2,
1854 4,
1855 ),
1856 (
1857 ProtectedObjectDomain::CircleAccessLeaf,
1858 "circles/circle/candidates/family/access-leaves/owner/epoch/recipient/leaf",
1859 2,
1860 4,
1861 ),
1862 (
1863 ProtectedObjectDomain::CircleAccessEnvelope,
1864 "circles/circle/candidates/family/access-envelopes/owner/recipient/hash",
1865 2,
1866 4,
1867 ),
1868 ];
1869
1870 for (domain, valid, candidates_index, kind_index) in cases {
1871 let components = valid.split('/').collect::<Vec<_>>();
1872 let mut reordered = components.clone();
1873 reordered.swap(candidates_index, candidates_index + 1);
1874 let mut nested = components.clone();
1875 nested[kind_index] = "nested";
1876 nested[kind_index + 1] = components[kind_index];
1877 let mut repeated_kind = components.clone();
1878 repeated_kind[kind_index + 1] = components[kind_index];
1879 let mut empty_family = components.clone();
1880 empty_family[candidates_index + 1] = "";
1881 for malformed in [reordered, nested, repeated_kind, empty_family] {
1882 let malformed = malformed.join("/");
1883 assert!(
1884 !validates(domain, &malformed),
1885 "{domain:?} accepted malformed path {malformed}",
1886 );
1887 }
1888 }
1889 }
1890}