1use super::super::{
33 DeRecChannelStore, DeRecEvent, DeRecSecretStore, DeRecShareStore, DeRecStateStore,
34 DeRecTransport, DeRecUserSecretStore, SecretValue, UnpairAck,
35 types::{Channel, ChannelStatus, HelperInfo, Replicas, Secret, Share, UserSecrets},
36};
37use crate::{
38 Result,
39 types::{ChannelId, SharedKey},
40 utils::SenderKindExt as _,
41};
42use std::collections::HashSet;
43
44#[cfg(not(target_arch = "wasm32"))]
45use crate::utils::now_secs;
46#[cfg(target_arch = "wasm32")]
47use crate::wasm::now_secs;
48
49#[derive(Debug, thiserror::Error)]
55#[non_exhaustive]
56pub enum RestoreError {
57 #[error("user-secret snapshot already exists for secret_id")]
60 AlreadyRestored,
61
62 #[error("restore blocked by pre-existing channels at canonical ids")]
67 Conflict(Vec<ChannelId>),
68
69 #[error("recovered Secret is internally inconsistent: {0}")]
74 Invariant(&'static str),
75}
76
77#[cfg_attr(feature = "logging", tracing::instrument(skip_all))]
88#[allow(clippy::too_many_arguments)]
89pub(in crate::protocol) async fn restore<
90 Ch: DeRecChannelStore,
91 Sh: DeRecShareStore,
92 Ss: DeRecSecretStore,
93 Us: DeRecUserSecretStore,
94 T: DeRecTransport,
95 St: DeRecStateStore,
96>(
97 channel_store: &mut Ch,
98 share_store: &mut Sh,
99 secret_store: &mut Ss,
100 user_secret_store: &mut Us,
101 transport: &T,
102 state_store: &mut St,
103 local_replica_id: &mut Option<u64>,
104 secret_id: u64,
105 secret: &Secret,
106 recovered_version: u32,
107) -> Result<Vec<DeRecEvent>> {
108 let (canonical_ids, existing_channels) =
109 check_preconditions(user_secret_store, channel_store, secret_id, secret).await?;
110
111 write_helper_channels(
112 channel_store,
113 share_store,
114 secret_store,
115 secret_id,
116 &secret.helpers,
117 recovered_version,
118 )
119 .await?;
120
121 if let Some(group) = secret.replicas.as_ref().filter(|g| !g.replicas.is_empty()) {
122 write_replica_channels(channel_store, secret_store, secret_id, group).await?;
123 }
124
125 commit_snapshot(user_secret_store, secret_id, secret, recovered_version).await?;
126
127 adopt_owner_replica_id(local_replica_id, secret.owner_replica_id);
128
129 let events = unpair_recovery_channels(
130 channel_store,
131 share_store,
132 secret_store,
133 transport,
134 state_store,
135 secret_id,
136 &existing_channels,
137 &canonical_ids,
138 )
139 .await?;
140
141 #[cfg(feature = "logging")]
142 tracing::info!(
143 secret_id,
144 helpers_restored = secret.helpers.len(),
145 replicas_restored = secret.replicas.as_ref().map_or(0, |g| g.replicas.len()),
146 user_secrets_restored = secret.secrets.len(),
147 "DeRecProtocol restored from recovered Secret"
148 );
149
150 Ok(events)
151}
152
153async fn check_preconditions<Ch: DeRecChannelStore, Us: DeRecUserSecretStore>(
164 user_secret_store: &Us,
165 channel_store: &Ch,
166 secret_id: u64,
167 secret: &Secret,
168) -> Result<(HashSet<u64>, Vec<Channel>)> {
169 if user_secret_store.load_latest(secret_id).await?.is_some() {
170 return Err(RestoreError::AlreadyRestored.into());
171 }
172
173 if let Some(group) = &secret.replicas {
174 if !group.replicas.is_empty() && group.shared_key.len() != 32 {
175 return Err(RestoreError::Invariant(
176 "recovered Secret carries replicas but replicas.shared_key is missing or wrong size",
177 )
178 .into());
179 }
180 }
181
182 let canonical_ids: HashSet<u64> = secret
185 .helpers
186 .iter()
187 .map(|h| h.channel_id)
188 .chain(
189 secret
190 .replicas
191 .as_ref()
192 .into_iter()
193 .flat_map(|g| g.replicas.iter().map(|r| r.channel_id)),
194 )
195 .collect();
196 let existing_channels = channel_store.channels(secret_id).await?;
197 let collisions: Vec<ChannelId> = existing_channels
198 .iter()
199 .filter(|c| canonical_ids.contains(&c.id.0))
200 .map(|c| c.id)
201 .collect();
202 if !collisions.is_empty() {
203 return Err(RestoreError::Conflict(collisions).into());
204 }
205
206 Ok((canonical_ids, existing_channels))
207}
208
209async fn write_helper_channels<Ch: DeRecChannelStore, Sh: DeRecShareStore, Ss: DeRecSecretStore>(
212 channel_store: &mut Ch,
213 share_store: &mut Sh,
214 secret_store: &mut Ss,
215 secret_id: u64,
216 helpers: &[HelperInfo],
217 recovered_version: u32,
218) -> Result<()> {
219 for h in helpers {
220 let cid = ChannelId(h.channel_id);
221 let shared_key: SharedKey = h
222 .shared_key
223 .as_slice()
224 .try_into()
225 .map_err(|_| RestoreError::Invariant("helper.shared_key must be 32 bytes"))?;
226 channel_store
227 .save(
228 secret_id,
229 Channel {
230 id: cid,
231 transport: derec_proto::TransportProtocol {
232 uri: h.transport_uri.clone(),
233 protocol: derec_proto::Protocol::Https as i32,
234 },
235 communication_info: h.communication_info.clone(),
236 status: ChannelStatus::Paired,
237 created_at: now_secs(),
238 role: derec_proto::SenderKind::Owner,
239 replica_id: None,
240 },
241 )
242 .await?;
243 secret_store
244 .save(secret_id, cid, SecretValue::SharedKey(shared_key))
245 .await?;
246 share_store
247 .save(
248 secret_id,
249 cid,
250 Share {
251 secret_id,
252 version: recovered_version,
253 replica_id: None,
254 bytes: Vec::new(),
255 },
256 )
257 .await?;
258 }
259 Ok(())
260}
261
262async fn write_replica_channels<Ch: DeRecChannelStore, Ss: DeRecSecretStore>(
269 channel_store: &mut Ch,
270 secret_store: &mut Ss,
271 secret_id: u64,
272 group: &Replicas,
273) -> Result<()> {
274 let group_key: SharedKey = group.shared_key.as_slice().try_into().map_err(|_| {
275 RestoreError::Invariant("replicas.shared_key must be 32 bytes when replicas is non-empty")
276 })?;
277 for r in &group.replicas {
278 let peer_kind = derec_proto::SenderKind::try_from(r.sender_kind)
279 .map_err(|_| RestoreError::Invariant("replica.sender_kind invalid"))?;
280 let local_role = peer_kind.derive_peer();
281 let cid = ChannelId(r.channel_id);
282 channel_store
283 .save(
284 secret_id,
285 Channel {
286 id: cid,
287 transport: derec_proto::TransportProtocol {
288 uri: r.transport_uri.clone(),
289 protocol: derec_proto::Protocol::Https as i32,
290 },
291 communication_info: r.communication_info.clone(),
292 status: ChannelStatus::Paired,
293 created_at: now_secs(),
294 role: local_role,
295 replica_id: Some(r.replica_id),
296 },
297 )
298 .await?;
299 secret_store
300 .save(secret_id, cid, SecretValue::SharedKey(group_key))
301 .await?;
302 }
303 Ok(())
304}
305
306async fn commit_snapshot<Us: DeRecUserSecretStore>(
310 user_secret_store: &mut Us,
311 secret_id: u64,
312 secret: &Secret,
313 recovered_version: u32,
314) -> Result<()> {
315 user_secret_store
316 .save_latest(
317 secret_id,
318 UserSecrets {
319 version: recovered_version,
320 secrets: secret.secrets.clone(),
321 description: None,
322 replicas: secret.replicas.clone(),
323 },
324 )
325 .await?;
326 Ok(())
327}
328
329fn adopt_owner_replica_id(local_replica_id: &mut Option<u64>, owner_replica_id: u64) {
333 if local_replica_id.is_none() && owner_replica_id != 0 {
334 *local_replica_id = Some(owner_replica_id);
335 }
336}
337
338#[allow(clippy::too_many_arguments)]
344async fn unpair_recovery_channels<
345 Ch: DeRecChannelStore,
346 Sh: DeRecShareStore,
347 Ss: DeRecSecretStore,
348 T: DeRecTransport,
349 St: DeRecStateStore,
350>(
351 channel_store: &mut Ch,
352 share_store: &mut Sh,
353 secret_store: &mut Ss,
354 transport: &T,
355 state_store: &mut St,
356 secret_id: u64,
357 existing_channels: &[Channel],
358 canonical_ids: &HashSet<u64>,
359) -> Result<Vec<DeRecEvent>> {
360 let recovery_ids: Vec<ChannelId> = existing_channels
361 .iter()
362 .filter(|c| !canonical_ids.contains(&c.id.0))
363 .map(|c| c.id)
364 .collect();
365 if recovery_ids.is_empty() {
366 return Ok(Vec::new());
367 }
368 let now = now_secs();
369 let mut events = Vec::new();
370 for channel_id in recovery_ids {
371 let mut per_channel = super::unpairing::start(
372 channel_store,
373 share_store,
374 secret_store,
375 transport,
376 state_store,
377 secret_id,
378 channel_id,
379 None,
380 UnpairAck::NotRequired,
381 now,
382 None,
383 )
384 .await?;
385 events.append(&mut per_channel);
386 }
387 Ok(events)
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393 use crate::protocol::DeRecProtocolBuilder;
394 use crate::protocol::traits::{
395 ChannelStoreFuture, DeRecChannelStore, DeRecSecretStore, DeRecShareStore, DeRecTransport,
396 DeRecUserSecretStore, SecretStoreFuture, ShareStoreFuture, TransportFuture,
397 };
398 use crate::protocol::types::{
399 Channel, ChannelStatus, HelperInfo, MissingPolicy, ReplicaInfo, Replicas, Secret,
400 SecretKind, SecretValue, Share, UserSecret, UserSecrets,
401 };
402 use derec_proto::{SenderKind, TransportProtocol};
403 use std::collections::HashMap;
404 use std::sync::{Arc, Mutex};
405
406 #[derive(Default, Clone)]
413 struct InMemChannelStore {
414 data: Arc<Mutex<HashMap<(u64, u64), Channel>>>,
415 }
416 impl DeRecChannelStore for InMemChannelStore {
417 fn load(&self, sid: u64, cid: ChannelId) -> ChannelStoreFuture<'_, Option<Channel>> {
418 let v = self.data.lock().unwrap().get(&(sid, cid.0)).cloned();
419 Box::pin(std::future::ready(Ok(v)))
420 }
421 fn save(&mut self, sid: u64, c: Channel) -> ChannelStoreFuture<'_, ()> {
422 self.data.lock().unwrap().insert((sid, c.id.0), c);
423 Box::pin(std::future::ready(Ok(())))
424 }
425 fn remove(&mut self, sid: u64, cid: ChannelId) -> ChannelStoreFuture<'_, bool> {
426 let removed = self.data.lock().unwrap().remove(&(sid, cid.0)).is_some();
427 Box::pin(std::future::ready(Ok(removed)))
428 }
429 fn channels(&self, sid: u64) -> ChannelStoreFuture<'_, Vec<Channel>> {
430 let v: Vec<Channel> = self
431 .data
432 .lock()
433 .unwrap()
434 .iter()
435 .filter(|((s, _), _)| *s == sid)
436 .map(|(_, c)| c.clone())
437 .collect();
438 Box::pin(std::future::ready(Ok(v)))
439 }
440 fn link_channel(
441 &mut self,
442 _: u64,
443 _: ChannelId,
444 _: ChannelId,
445 ) -> ChannelStoreFuture<'_, ()> {
446 Box::pin(std::future::ready(Ok(())))
447 }
448 fn linked_channels(
449 &self,
450 _: u64,
451 cid: ChannelId,
452 ) -> ChannelStoreFuture<'_, Vec<ChannelId>> {
453 Box::pin(std::future::ready(Ok(vec![cid])))
454 }
455 }
456
457 #[derive(Default, Clone)]
458 struct InMemSecretStore {
459 #[allow(clippy::type_complexity)]
460 data: Arc<Mutex<HashMap<(u64, u64, u8), SecretValue>>>,
461 }
462 impl DeRecSecretStore for InMemSecretStore {
463 fn load(
464 &self,
465 sid: u64,
466 cid: ChannelId,
467 kind: SecretKind,
468 ) -> SecretStoreFuture<'_, Option<SecretValue>> {
469 let v = self
470 .data
471 .lock()
472 .unwrap()
473 .get(&(sid, cid.0, kind as u8))
474 .cloned();
475 Box::pin(std::future::ready(Ok(v)))
476 }
477 fn load_many(
478 &self,
479 sid: u64,
480 cids: &[ChannelId],
481 kind: SecretKind,
482 _: MissingPolicy,
483 ) -> SecretStoreFuture<'_, Vec<(ChannelId, SecretValue)>> {
484 let mut out = Vec::new();
485 for c in cids {
486 if let Some(v) = self.data.lock().unwrap().get(&(sid, c.0, kind as u8)) {
487 out.push((*c, v.clone()));
488 }
489 }
490 Box::pin(std::future::ready(Ok(out)))
491 }
492 fn save(
493 &mut self,
494 sid: u64,
495 cid: ChannelId,
496 value: SecretValue,
497 ) -> SecretStoreFuture<'_, ()> {
498 let k = match &value {
499 SecretValue::SharedKey(_) => SecretKind::SharedKey as u8,
500 SecretValue::PairingSecret(_) => SecretKind::PairingSecret as u8,
501 SecretValue::PairingContact(_) => SecretKind::PairingContact as u8,
502 };
503 self.data.lock().unwrap().insert((sid, cid.0, k), value);
504 Box::pin(std::future::ready(Ok(())))
505 }
506 fn remove(
507 &mut self,
508 sid: u64,
509 cid: ChannelId,
510 kind: SecretKind,
511 ) -> SecretStoreFuture<'_, ()> {
512 self.data.lock().unwrap().remove(&(sid, cid.0, kind as u8));
513 Box::pin(std::future::ready(Ok(())))
514 }
515 }
516
517 #[derive(Default, Clone)]
518 struct InMemShareStore {
519 #[allow(clippy::type_complexity)]
520 data: Arc<Mutex<HashMap<(u64, u64, u32), Share>>>,
521 }
522 impl DeRecShareStore for InMemShareStore {
523 fn load(
524 &self,
525 sid: u64,
526 cid: ChannelId,
527 versions: &[u32],
528 ) -> ShareStoreFuture<'_, Vec<Share>> {
529 let lock = self.data.lock().unwrap();
530 let out: Vec<Share> = lock
531 .iter()
532 .filter(|((s, c, v), _)| {
533 *s == sid && *c == cid.0 && (versions.is_empty() || versions.contains(v))
534 })
535 .map(|(_, s)| s.clone())
536 .collect();
537 Box::pin(std::future::ready(Ok(out)))
538 }
539 fn load_many(
540 &self,
541 _: u64,
542 _: &[ChannelId],
543 _: &[u32],
544 ) -> ShareStoreFuture<'_, Vec<Share>> {
545 Box::pin(std::future::ready(Ok(Vec::new())))
546 }
547 fn load_all(&self, _: u64, _: &[ChannelId]) -> ShareStoreFuture<'_, Vec<Share>> {
548 Box::pin(std::future::ready(Ok(Vec::new())))
549 }
550 fn latest_version(&self, _: u64) -> ShareStoreFuture<'_, Option<u32>> {
551 Box::pin(std::future::ready(Ok(None)))
552 }
553 fn save(&mut self, sid: u64, cid: ChannelId, share: Share) -> ShareStoreFuture<'_, ()> {
554 let v = share.version;
555 self.data.lock().unwrap().insert((sid, cid.0, v), share);
556 Box::pin(std::future::ready(Ok(())))
557 }
558 fn remove_channel(&mut self, _: u64, _: ChannelId) -> ShareStoreFuture<'_, ()> {
559 Box::pin(std::future::ready(Ok(())))
560 }
561 }
562
563 #[derive(Default, Clone)]
564 struct InMemUserSecretStore {
565 data: Arc<Mutex<HashMap<u64, UserSecrets>>>,
566 }
567 impl DeRecUserSecretStore for InMemUserSecretStore {
568 fn load_latest(&self, sid: u64) -> ShareStoreFuture<'_, Option<UserSecrets>> {
569 let v = self.data.lock().unwrap().get(&sid).cloned();
570 Box::pin(std::future::ready(Ok(v)))
571 }
572 fn save_latest(&mut self, sid: u64, value: UserSecrets) -> ShareStoreFuture<'_, ()> {
573 self.data.lock().unwrap().insert(sid, value);
574 Box::pin(std::future::ready(Ok(())))
575 }
576 fn remove(&mut self, sid: u64) -> ShareStoreFuture<'_, ()> {
577 self.data.lock().unwrap().remove(&sid);
578 Box::pin(std::future::ready(Ok(())))
579 }
580 }
581
582 #[derive(Default, Clone)]
583 struct NoopTransport;
584 impl DeRecTransport for NoopTransport {
585 fn send(&self, _: &TransportProtocol, _: Vec<u8>) -> TransportFuture<'_> {
586 Box::pin(std::future::ready(Ok(())))
587 }
588 }
589
590 type TestProto = crate::protocol::DeRecProtocol<
591 InMemChannelStore,
592 InMemShareStore,
593 InMemSecretStore,
594 InMemUserSecretStore,
595 RestoreTestStateStore,
596 NoopTransport,
597 >;
598
599 #[derive(Default)]
600 struct RestoreTestStateStore;
601 impl crate::protocol::DeRecStateStore for RestoreTestStateStore {
602 fn save(
603 &mut self,
604 _: u64,
605 _: crate::protocol::StateItem,
606 ) -> crate::protocol::StateStoreFuture<'_, ()> {
607 Box::pin(std::future::ready(Ok(())))
608 }
609 fn load(
610 &self,
611 _: u64,
612 _: crate::protocol::StateKey,
613 ) -> crate::protocol::StateStoreFuture<'_, Option<crate::protocol::StateItem>> {
614 Box::pin(std::future::ready(Ok(None)))
615 }
616 fn remove(
617 &mut self,
618 _: u64,
619 _: crate::protocol::StateKey,
620 ) -> crate::protocol::StateStoreFuture<'_, bool> {
621 Box::pin(std::future::ready(Ok(false)))
622 }
623 fn load_all(
624 &self,
625 _: u64,
626 _: crate::protocol::StateKind,
627 ) -> crate::protocol::StateStoreFuture<'_, Vec<crate::protocol::StateItem>> {
628 Box::pin(std::future::ready(Ok(Vec::new())))
629 }
630 }
631
632 struct TestRig {
636 protocol: TestProto,
637 channel_store: InMemChannelStore,
638 secret_store: InMemSecretStore,
639 share_store: InMemShareStore,
640 user_secret_store: InMemUserSecretStore,
641 }
642
643 fn build_rig(secret_id: u64) -> TestRig {
644 let channel_store = InMemChannelStore::default();
645 let secret_store = InMemSecretStore::default();
646 let share_store = InMemShareStore::default();
647 let user_secret_store = InMemUserSecretStore::default();
648 let protocol = DeRecProtocolBuilder::new(secret_id)
649 .with_channel_store(channel_store.clone())
650 .with_share_store(share_store.clone())
651 .with_secret_store(secret_store.clone())
652 .with_user_secret_store(user_secret_store.clone())
653 .with_transport(NoopTransport)
654 .with_state_store(RestoreTestStateStore)
655 .with_own_transport("https://owner.example.com")
656 .with_threshold(2)
657 .build()
658 .expect("test rig builds");
659 TestRig {
660 protocol,
661 channel_store,
662 secret_store,
663 share_store,
664 user_secret_store,
665 }
666 }
667
668 fn fixture_secret() -> Secret {
669 Secret {
670 helpers: vec![
671 HelperInfo {
672 channel_id: 11,
673 transport_uri: "https://helper-a.example".to_owned(),
674 shared_key: vec![0xAA; 32],
675 communication_info: HashMap::from([("name".to_owned(), "HelperA".to_owned())]),
676 },
677 HelperInfo {
678 channel_id: 12,
679 transport_uri: "https://helper-b.example".to_owned(),
680 shared_key: vec![0xBB; 32],
681 communication_info: HashMap::new(),
682 },
683 ],
684 secrets: vec![
685 UserSecret {
686 id: vec![0x01],
687 name: "wallet".to_owned(),
688 data: b"correct horse battery staple".to_vec(),
689 },
690 UserSecret {
691 id: vec![0x02],
692 name: "api token".to_owned(),
693 data: b"hunter2".to_vec(),
694 },
695 ],
696 replicas: Some(Replicas {
697 replicas: vec![ReplicaInfo {
698 channel_id: 21,
699 transport_uri: "https://replica.example".to_owned(),
700 communication_info: HashMap::new(),
701 replica_id: 0xCAFE,
702 sender_kind: SenderKind::ReplicaDestination as i32,
703 }],
704 shared_key: vec![0xCC; 32],
705 }),
706 owner_replica_id: 0xBEEF,
707 }
708 }
709
710 fn run_async<F: std::future::Future<Output = ()>>(f: F) {
711 tokio::runtime::Builder::new_current_thread()
712 .build()
713 .expect("test runtime")
714 .block_on(f)
715 }
716
717 #[test]
720 fn restore_happy_path_persists_canonical_state() {
721 run_async(async {
722 let secret_id: u64 = 0xDE_2EC;
723 let mut rig = build_rig(secret_id);
724
725 rig.protocol
726 .restore(&fixture_secret(), 7)
727 .await
728 .expect("happy path must succeed");
729
730 for hid in [11_u64, 12] {
732 let ch = rig
733 .channel_store
734 .load(secret_id, ChannelId(hid))
735 .await
736 .unwrap()
737 .expect("helper channel must be persisted");
738 assert_eq!(ch.status, ChannelStatus::Paired);
739 assert_eq!(ch.role, SenderKind::Owner);
740 assert!(ch.replica_id.is_none());
741 let sk = rig
742 .secret_store
743 .load(secret_id, ChannelId(hid), SecretKind::SharedKey)
744 .await
745 .unwrap()
746 .expect("helper SharedKey must be persisted");
747 assert!(matches!(sk, SecretValue::SharedKey(_)));
748 let shares = rig
749 .share_store
750 .load(secret_id, ChannelId(hid), &[])
751 .await
752 .unwrap();
753 assert_eq!(shares.len(), 1);
754 assert_eq!(shares[0].version, 7);
755 }
756
757 let rep = rig
760 .channel_store
761 .load(secret_id, ChannelId(21))
762 .await
763 .unwrap()
764 .expect("replica channel must be persisted");
765 assert_eq!(rep.status, ChannelStatus::Paired);
766 assert_eq!(rep.role, SenderKind::ReplicaSource);
767 assert_eq!(rep.replica_id, Some(0xCAFE));
768 let rep_sk = rig
769 .secret_store
770 .load(secret_id, ChannelId(21), SecretKind::SharedKey)
771 .await
772 .unwrap()
773 .expect("replica group key must be persisted");
774 match rep_sk {
775 SecretValue::SharedKey(k) => assert_eq!(k.to_vec(), vec![0xCC; 32]),
776 _ => panic!("expected SharedKey"),
777 }
778
779 let snapshot = rig
781 .user_secret_store
782 .load_latest(secret_id)
783 .await
784 .unwrap()
785 .expect("snapshot must exist");
786 assert_eq!(snapshot.version, 7);
787 assert_eq!(snapshot.secrets.len(), 2);
788 assert_eq!(snapshot.secrets[0].name, "wallet");
789 assert_eq!(snapshot.secrets[0].data, b"correct horse battery staple");
790 assert_eq!(snapshot.secrets[1].name, "api token");
791
792 assert_eq!(rig.protocol.replica_id(), Some(0xBEEF));
794 });
795 }
796
797 #[test]
800 fn restore_unpairs_pre_existing_recovery_channels() {
801 run_async(async {
802 let secret_id: u64 = 0xDE_2EC;
803 let mut rig = build_rig(secret_id);
804
805 for rcid in [99_u64, 100] {
810 rig.channel_store.data.lock().unwrap().insert(
811 (secret_id, rcid),
812 Channel {
813 id: ChannelId(rcid),
814 transport: TransportProtocol {
815 uri: format!("https://recovery-{rcid}.example"),
816 protocol: 0,
817 },
818 communication_info: HashMap::new(),
819 status: ChannelStatus::Paired,
820 created_at: 1,
821 role: SenderKind::Owner,
822 replica_id: None,
823 },
824 );
825 rig.secret_store.data.lock().unwrap().insert(
826 (secret_id, rcid, SecretKind::SharedKey as u8),
827 SecretValue::SharedKey([0x77; 32]),
828 );
829 }
830
831 rig.protocol
832 .restore(&fixture_secret(), 7)
833 .await
834 .expect("restore must succeed despite recovery channels");
835
836 for rcid in [99_u64, 100] {
838 assert!(
839 rig.channel_store
840 .load(secret_id, ChannelId(rcid))
841 .await
842 .unwrap()
843 .is_none(),
844 "recovery channel {rcid} must be unpaired"
845 );
846 assert!(
847 rig.secret_store
848 .load(secret_id, ChannelId(rcid), SecretKind::SharedKey)
849 .await
850 .unwrap()
851 .is_none()
852 );
853 }
854
855 assert!(
857 rig.channel_store
858 .load(secret_id, ChannelId(11))
859 .await
860 .unwrap()
861 .is_some()
862 );
863 });
864 }
865
866 #[test]
869 fn restore_returns_already_restored_when_snapshot_exists() {
870 run_async(async {
871 let secret_id: u64 = 0xDE_2EC;
872 let mut rig = build_rig(secret_id);
873 rig.user_secret_store.data.lock().unwrap().insert(
874 secret_id,
875 UserSecrets {
876 version: 1,
877 secrets: Vec::new(),
878 description: None,
879 replicas: None,
880 },
881 );
882
883 let err = rig
884 .protocol
885 .restore(&fixture_secret(), 7)
886 .await
887 .unwrap_err();
888 assert!(matches!(
889 err,
890 crate::Error::Restore(RestoreError::AlreadyRestored)
891 ));
892
893 assert!(
895 rig.channel_store
896 .load(secret_id, ChannelId(11))
897 .await
898 .unwrap()
899 .is_none()
900 );
901 });
902 }
903
904 #[test]
905 fn restore_returns_conflict_on_canonical_id_collision() {
906 run_async(async {
907 let secret_id: u64 = 0xDE_2EC;
908 let mut rig = build_rig(secret_id);
909 rig.channel_store.data.lock().unwrap().insert(
911 (secret_id, 11),
912 Channel {
913 id: ChannelId(11),
914 transport: TransportProtocol {
915 uri: "https://collision.example".to_owned(),
916 protocol: 0,
917 },
918 communication_info: HashMap::new(),
919 status: ChannelStatus::Paired,
920 created_at: 1,
921 role: SenderKind::Owner,
922 replica_id: None,
923 },
924 );
925
926 let err = rig
927 .protocol
928 .restore(&fixture_secret(), 7)
929 .await
930 .unwrap_err();
931 let crate::Error::Restore(RestoreError::Conflict(ids)) = err else {
932 panic!("expected Restore(Conflict), got {err:?}");
933 };
934 assert_eq!(ids, vec![ChannelId(11)]);
935
936 assert!(
938 rig.user_secret_store
939 .load_latest(secret_id)
940 .await
941 .unwrap()
942 .is_none()
943 );
944 });
945 }
946
947 #[test]
948 fn restore_invariant_error_when_replicas_present_but_group_key_missing() {
949 run_async(async {
950 let secret_id: u64 = 0xDE_2EC;
951 let mut rig = build_rig(secret_id);
952 let mut secret = fixture_secret();
953 if let Some(group) = secret.replicas.as_mut() {
954 group.shared_key = Vec::new();
955 }
956
957 let err = rig.protocol.restore(&secret, 7).await.unwrap_err();
958 assert!(matches!(
959 err,
960 crate::Error::Restore(RestoreError::Invariant(_))
961 ));
962
963 assert!(
965 rig.channel_store
966 .load(secret_id, ChannelId(11))
967 .await
968 .unwrap()
969 .is_none()
970 );
971 });
972 }
973
974 #[test]
977 fn restore_does_not_overwrite_explicit_replica_id() {
978 run_async(async {
979 let secret_id: u64 = 0xDE_2EC;
980 let mut protocol = DeRecProtocolBuilder::new(secret_id)
984 .with_channel_store(InMemChannelStore::default())
985 .with_share_store(InMemShareStore::default())
986 .with_secret_store(InMemSecretStore::default())
987 .with_user_secret_store(InMemUserSecretStore::default())
988 .with_transport(NoopTransport)
989 .with_state_store(RestoreTestStateStore)
990 .with_own_transport("https://owner.example.com")
991 .with_threshold(2)
992 .with_replica_id(0x1234)
993 .build()
994 .expect("build");
995
996 protocol.restore(&fixture_secret(), 7).await.unwrap();
997 assert_eq!(protocol.replica_id(), Some(0x1234));
998 });
999 }
1000}