Skip to main content

im_core/secure/
service.rs

1pub struct SecureService<'a> {
2    client: &'a crate::core::ImClient,
3}
4
5impl<'a> SecureService<'a> {
6    pub(crate) fn new(client: &'a crate::core::ImClient) -> Self {
7        Self { client }
8    }
9
10    pub fn direct(&self, peer: crate::ids::PeerRef) -> DirectSecureConversation<'a> {
11        DirectSecureConversation {
12            client: self.client,
13            peer,
14        }
15    }
16
17    pub fn group(&self, group: crate::ids::GroupRef) -> GroupSecureConversation<'a> {
18        GroupSecureConversation {
19            client: self.client,
20            group,
21        }
22    }
23
24    pub fn outbox(&self) -> SecureOutboxService<'a> {
25        SecureOutboxService {
26            client: self.client,
27        }
28    }
29}
30
31pub struct DirectSecureConversation<'a> {
32    client: &'a crate::core::ImClient,
33    peer: crate::ids::PeerRef,
34}
35
36impl DirectSecureConversation<'_> {
37    pub fn status(&self) -> crate::ImResult<super::DirectSecureStatus> {
38        #[cfg(feature = "blocking")]
39        {
40            direct_status_to_dto(
41                crate::internal::secure_direct::status::direct_status_for_client(
42                    self.client,
43                    self.peer.clone(),
44                )?,
45            )
46        }
47        #[cfg(not(feature = "blocking"))]
48        {
49            let _ = self.client.current_identity();
50            Err(crate::ImError::unsupported("sync-direct-secure-status"))
51        }
52    }
53
54    pub async fn status_async(&self) -> crate::ImResult<super::DirectSecureStatus> {
55        direct_status_to_dto(
56            crate::internal::secure_direct::status::direct_status_for_client_async(
57                self.client,
58                self.peer.clone(),
59            )
60            .await?,
61        )
62    }
63
64    pub fn prepare(&self) -> crate::ImResult<super::DirectSecurePrepareResult> {
65        #[cfg(feature = "blocking")]
66        {
67            let plan = crate::internal::secure_direct::prepare::prepare_direct_for_client(
68                self.client,
69                self.peer.clone(),
70            )?;
71            direct_prepare_to_dto(plan)
72        }
73        #[cfg(not(feature = "blocking"))]
74        {
75            let _ = self.client.current_identity();
76            Err(crate::ImError::unsupported("sync-direct-secure-prepare"))
77        }
78    }
79
80    pub async fn prepare_async(&self) -> crate::ImResult<super::DirectSecurePrepareResult> {
81        let plan = crate::internal::secure_direct::prepare::prepare_direct_for_client_async(
82            self.client,
83            self.peer.clone(),
84        )
85        .await?;
86        direct_prepare_to_dto(plan)
87    }
88
89    pub fn repair(&self) -> crate::ImResult<super::DirectSecureRepairResult> {
90        #[cfg(feature = "blocking")]
91        {
92            direct_repair_for_client(self.client, self.peer.clone())
93        }
94        #[cfg(not(feature = "blocking"))]
95        {
96            let _ = self.client.current_identity();
97            Err(crate::ImError::unsupported("sync-direct-secure-repair"))
98        }
99    }
100
101    pub async fn repair_async(&self) -> crate::ImResult<super::DirectSecureRepairResult> {
102        let client = self.client.clone();
103        let peer = self.peer.clone();
104        let plan =
105            crate::internal::secure_direct::status::repair_direct_for_client_async(&client, peer)
106                .await?;
107        direct_repair_plan_to_dto_with_prepare(self.client, self.peer.clone(), plan).await
108    }
109}
110
111fn direct_prepare_to_dto(
112    plan: crate::internal::secure_direct::prepare::DirectSecurePreparePlan,
113) -> crate::ImResult<super::DirectSecurePrepareResult> {
114    let mut warnings = plan.status.warnings;
115    if plan.prepared_local_send_state {
116        warnings.push("direct E2EE local send state prepared".to_owned());
117    }
118    Ok(super::DirectSecurePrepareResult {
119        peer: plan.status.peer,
120        state: plan.status.state,
121        can_send_secure: plan.status.can_send_secure,
122        warnings,
123    })
124}
125
126fn direct_repair_for_client(
127    client: &crate::core::ImClient,
128    peer: crate::ids::PeerRef,
129) -> crate::ImResult<super::DirectSecureRepairResult> {
130    #[cfg(feature = "blocking")]
131    {
132        let plan =
133            crate::internal::secure_direct::status::repair_direct_for_client(client, peer.clone())?;
134        direct_repair_plan_to_dto(client, peer, plan)
135    }
136    #[cfg(not(feature = "blocking"))]
137    {
138        let _ = (client, peer);
139        Err(crate::ImError::unsupported("sync-direct-secure-repair"))
140    }
141}
142
143async fn direct_repair_plan_to_dto_with_prepare(
144    client: &crate::core::ImClient,
145    peer: crate::ids::PeerRef,
146    plan: crate::internal::secure_direct::status::DirectSecureRepairPlan,
147) -> crate::ImResult<super::DirectSecureRepairResult> {
148    let prepare_result =
149        crate::internal::secure_direct::prepare::prepare_direct_for_client_async(client, peer)
150            .await;
151    Ok(direct_repair_plan_to_dto_with_prepare_result(
152        plan,
153        prepare_result,
154    ))
155}
156
157fn direct_repair_plan_to_dto(
158    client: &crate::core::ImClient,
159    peer: crate::ids::PeerRef,
160    plan: crate::internal::secure_direct::status::DirectSecureRepairPlan,
161) -> crate::ImResult<super::DirectSecureRepairResult> {
162    Ok(direct_repair_plan_to_dto_with_prepare_result(
163        plan,
164        crate::internal::secure_direct::prepare::prepare_direct_for_client(client, peer),
165    ))
166}
167
168fn direct_repair_plan_to_dto_with_prepare_result(
169    plan: crate::internal::secure_direct::status::DirectSecureRepairPlan,
170    prepare_result: crate::ImResult<
171        crate::internal::secure_direct::prepare::DirectSecurePreparePlan,
172    >,
173) -> super::DirectSecureRepairResult {
174    let mut warnings = plan.status.warnings;
175    let mut state = plan.status.state;
176    let mut problem = plan.status.problem;
177    let mut prepared_local_send_state = false;
178    match prepare_result {
179        Ok(prepare_plan) => {
180            warnings.extend(prepare_plan.status.warnings);
181            prepared_local_send_state = prepare_plan.prepared_local_send_state;
182            if prepared_local_send_state {
183                state = prepare_plan.status.state;
184                problem = prepare_plan.status.problem;
185                warnings.push("direct E2EE local send state prepared".to_owned());
186            }
187        }
188        Err(err) => {
189            warnings.push(format!("direct E2EE prekey preparation failed: {err}"));
190        }
191    }
192    if plan.requeued_outbox_count > 0 {
193        warnings.push(format!(
194            "{} failed secure outbox item(s) were moved back to queued",
195            plan.requeued_outbox_count
196        ));
197    }
198    super::DirectSecureRepairResult {
199        peer: plan.status.peer,
200        state,
201        repaired: plan.removed_session
202            || plan.requeued_outbox_count > 0
203            || prepared_local_send_state,
204        problem,
205        prepared_local_send_state,
206        warnings,
207    }
208}
209
210fn direct_status_to_dto(
211    status: crate::internal::secure_direct::status::DirectSecureLocalStatus,
212) -> crate::ImResult<super::DirectSecureStatus> {
213    Ok(super::DirectSecureStatus {
214        peer: status.peer,
215        resolved_peer: status.resolved_peer,
216        state: status.state,
217        can_send_secure: status.can_send_secure,
218        pending_outbox_count: status.pending_outbox_count,
219        problem: status.problem,
220        warnings: status.warnings,
221    })
222}
223
224pub struct GroupSecureConversation<'a> {
225    client: &'a crate::core::ImClient,
226    group: crate::ids::GroupRef,
227}
228
229impl GroupSecureConversation<'_> {
230    pub fn status(&self) -> crate::ImResult<super::GroupSecureStatus> {
231        #[cfg(all(feature = "group-e2ee", feature = "blocking"))]
232        {
233            let provider =
234                match crate::internal::group_e2ee::storage::native_provider_for_client(self.client)
235                {
236                    Ok(provider) => provider,
237                    Err(err) => return Ok(group_status_unavailable(self.group.clone(), err)),
238                };
239            let status = crate::internal::group_e2ee::status::GroupE2eeStatusRuntime::new(
240                self.client,
241                crate::internal::auth::session::FileSessionProvider::new(self.client),
242                crate::internal::transport::CoreHttpTransport::new(self.client),
243                provider,
244            )
245            .status(crate::internal::group_e2ee::status::GroupE2eeStatusInput {
246                group: self.group.clone(),
247                credentials: None,
248                notice_limit: 50,
249            });
250            match status {
251                Ok(status) => group_status_to_dto(status),
252                Err(err) if group_status_can_downgrade_error(&err) => {
253                    Ok(group_status_unavailable(self.group.clone(), err))
254                }
255                Err(err) => Err(err),
256            }
257        }
258        #[cfg(all(feature = "group-e2ee", not(feature = "blocking")))]
259        {
260            let _ = self.client.current_identity();
261            Err(crate::ImError::unsupported("sync-group-secure-status"))
262        }
263        #[cfg(not(feature = "group-e2ee"))]
264        {
265            let _ = self.client.current_identity();
266            Ok(super::GroupSecureStatus {
267                group: self.group.clone(),
268                state: super::GroupSecureState::Unavailable,
269                can_send_secure: false,
270                local_readiness: super::GroupSecureLocalReadiness {
271                    has_local_state: false,
272                    has_active_membership: false,
273                },
274                pending_work: super::GroupSecurePendingWork::default(),
275                problem: Some(super::SecureProblem {
276                    code: super::SecureProblemCode::Unsupported,
277                    message: "group-e2ee-status is not available yet".to_owned(),
278                    retryable: false,
279                }),
280                warnings: Vec::new(),
281            })
282        }
283    }
284
285    pub fn prepare(&self) -> crate::ImResult<super::GroupSecurePrepareResult> {
286        let status = self.status()?;
287        Ok(super::GroupSecurePrepareResult {
288            group: status.group,
289            state: status.state,
290            can_send_secure: status.can_send_secure,
291            warnings: status.warnings,
292        })
293    }
294
295    pub async fn status_async(&self) -> crate::ImResult<super::GroupSecureStatus> {
296        #[cfg(feature = "group-e2ee")]
297        {
298            let provider =
299                match crate::internal::group_e2ee::storage::native_provider_for_client(self.client)
300                {
301                    Ok(provider) => provider,
302                    Err(err) => return Ok(group_status_unavailable(self.group.clone(), err)),
303                };
304            let status = crate::internal::group_e2ee::status::GroupE2eeStatusRuntime::new(
305                self.client,
306                crate::internal::auth::session::FileSessionProvider::new(self.client),
307                crate::internal::transport::CoreHttpTransport::new(self.client),
308                provider,
309            )
310            .status_async(crate::internal::group_e2ee::status::GroupE2eeStatusInput {
311                group: self.group.clone(),
312                credentials: None,
313                notice_limit: 50,
314            })
315            .await;
316            match status {
317                Ok(status) => group_status_to_dto(status),
318                Err(err) if group_status_can_downgrade_error(&err) => {
319                    Ok(group_status_unavailable(self.group.clone(), err))
320                }
321                Err(err) => Err(err),
322            }
323        }
324        #[cfg(not(feature = "group-e2ee"))]
325        {
326            let _ = self.client.current_identity();
327            Ok(super::GroupSecureStatus {
328                group: self.group.clone(),
329                state: super::GroupSecureState::Unavailable,
330                can_send_secure: false,
331                local_readiness: super::GroupSecureLocalReadiness {
332                    has_local_state: false,
333                    has_active_membership: false,
334                },
335                pending_work: super::GroupSecurePendingWork::default(),
336                problem: Some(super::SecureProblem {
337                    code: super::SecureProblemCode::Unsupported,
338                    message: "group-e2ee-status is not available yet".to_owned(),
339                    retryable: false,
340                }),
341                warnings: Vec::new(),
342            })
343        }
344    }
345
346    pub async fn prepare_async(&self) -> crate::ImResult<super::GroupSecurePrepareResult> {
347        let status = self.status_async().await?;
348        Ok(super::GroupSecurePrepareResult {
349            group: status.group,
350            state: status.state,
351            can_send_secure: status.can_send_secure,
352            warnings: status.warnings,
353        })
354    }
355
356    pub fn repair(&self) -> crate::ImResult<super::GroupSecureRepairResult> {
357        #[cfg(all(feature = "group-e2ee", feature = "blocking"))]
358        {
359            let provider =
360                crate::internal::group_e2ee::storage::native_provider_for_client(self.client)?;
361            group_repair_to_dto(
362                crate::internal::group_e2ee::repair::GroupE2eeRepairRuntime::new(
363                    self.client,
364                    crate::internal::auth::session::FileSessionProvider::new(self.client),
365                    crate::internal::transport::CoreHttpTransport::new(self.client),
366                    provider,
367                )
368                .repair(
369                    crate::internal::group_e2ee::repair::GroupE2eeRepairInput {
370                        group: self.group.clone(),
371                        credentials: None,
372                        notice_limit: 50,
373                    },
374                )?,
375            )
376        }
377        #[cfg(all(feature = "group-e2ee", not(feature = "blocking")))]
378        {
379            let _ = self.client.current_identity();
380            Err(crate::ImError::unsupported("sync-group-secure-repair"))
381        }
382        #[cfg(not(feature = "group-e2ee"))]
383        {
384            Err(crate::ImError::unsupported("group-e2ee"))
385        }
386    }
387
388    pub async fn repair_async(&self) -> crate::ImResult<super::GroupSecureRepairResult> {
389        #[cfg(feature = "group-e2ee")]
390        {
391            let provider =
392                crate::internal::group_e2ee::storage::native_provider_for_client(self.client)?;
393            group_repair_to_dto(
394                crate::internal::group_e2ee::repair::GroupE2eeRepairRuntime::new(
395                    self.client,
396                    crate::internal::auth::session::FileSessionProvider::new(self.client),
397                    crate::internal::transport::CoreHttpTransport::new(self.client),
398                    provider,
399                )
400                .repair_async(crate::internal::group_e2ee::repair::GroupE2eeRepairInput {
401                    group: self.group.clone(),
402                    credentials: None,
403                    notice_limit: 50,
404                })
405                .await?,
406            )
407        }
408        #[cfg(not(feature = "group-e2ee"))]
409        {
410            Err(crate::ImError::unsupported("group-e2ee"))
411        }
412    }
413}
414
415#[cfg(feature = "group-e2ee")]
416fn group_status_unavailable(
417    group: crate::ids::GroupRef,
418    err: crate::ImError,
419) -> super::GroupSecureStatus {
420    let problem_code = group_status_problem_code(&err);
421    super::GroupSecureStatus {
422        group,
423        state: super::GroupSecureState::Unavailable,
424        can_send_secure: false,
425        local_readiness: super::GroupSecureLocalReadiness {
426            has_local_state: false,
427            has_active_membership: false,
428        },
429        pending_work: super::GroupSecurePendingWork::default(),
430        problem: Some(super::SecureProblem {
431            code: problem_code,
432            message: format!("group E2EE status is unavailable: {err}"),
433            retryable: true,
434        }),
435        warnings: vec!["group E2EE status is unavailable".to_owned()],
436    }
437}
438
439#[cfg(feature = "group-e2ee")]
440fn group_status_can_downgrade_error(err: &crate::ImError) -> bool {
441    matches!(
442        err,
443        crate::ImError::IdentityRequired
444            | crate::ImError::IdentityNotFound { .. }
445            | crate::ImError::DefaultIdentityMissing
446            | crate::ImError::IdentityNotReady { .. }
447            | crate::ImError::AuthRequired
448            | crate::ImError::SessionExpired
449            | crate::ImError::TransportUnavailable { .. }
450            | crate::ImError::LocalStateUnavailable { .. }
451            | crate::ImError::PathUnavailable { .. }
452            | crate::ImError::CredentialFileUnreadable { .. }
453    )
454}
455
456#[cfg(feature = "group-e2ee")]
457fn group_status_problem_code(err: &crate::ImError) -> super::SecureProblemCode {
458    match err {
459        crate::ImError::TransportUnavailable { .. } => {
460            super::SecureProblemCode::TransportUnavailable
461        }
462        crate::ImError::LocalStateUnavailable { .. } => {
463            super::SecureProblemCode::LocalStateUnavailable
464        }
465        _ => super::SecureProblemCode::IdentityNotReady,
466    }
467}
468
469#[cfg(feature = "group-e2ee")]
470fn group_status_to_dto(
471    status: crate::internal::group_e2ee::status::GroupE2eeStatusResult,
472) -> crate::ImResult<super::GroupSecureStatus> {
473    Ok(super::GroupSecureStatus {
474        group: status.group,
475        state: status.state,
476        can_send_secure: status.can_send_secure,
477        local_readiness: status.local_readiness,
478        pending_work: status.pending_work,
479        problem: status.problem,
480        warnings: status.warnings,
481    })
482}
483
484#[cfg(feature = "group-e2ee")]
485fn group_repair_to_dto(
486    result: crate::internal::group_e2ee::repair::GroupE2eeRepairResult,
487) -> crate::ImResult<super::GroupSecureRepairResult> {
488    Ok(super::GroupSecureRepairResult {
489        group: result.group,
490        state: result.state,
491        repaired: result.repaired,
492        problem: result.problem,
493        warnings: result.warnings,
494    })
495}
496
497pub struct SecureOutboxService<'a> {
498    client: &'a crate::core::ImClient,
499}
500
501impl SecureOutboxService<'_> {
502    pub fn list_failed(&self) -> crate::ImResult<Vec<super::SecureOutboxEntry>> {
503        #[cfg(all(feature = "sqlite", feature = "blocking"))]
504        {
505            let connection = crate::internal::local_state::open_writable(
506                &self.client.core_inner().sdk_paths().local_state.sqlite_path,
507            )?;
508            let scope =
509                crate::internal::store::e2ee_outbox::E2eeOutboxOwnerScope::for_client(self.client);
510            let entries = crate::internal::store::e2ee_outbox::list_e2ee_outbox(
511                &connection,
512                &scope,
513                Some("failed"),
514            )?
515            .iter()
516            .map(crate::internal::store::e2ee_outbox::secure_outbox_entry_from_record)
517            .collect::<crate::ImResult<Vec<_>>>()?;
518            Ok(entries)
519        }
520        #[cfg(all(feature = "sqlite", not(feature = "blocking")))]
521        {
522            let _ = self.client.current_identity();
523            Err(crate::ImError::unsupported("sync-secure-outbox"))
524        }
525        #[cfg(not(feature = "sqlite"))]
526        {
527            let _ = self.client.current_identity();
528            Ok(Vec::new())
529        }
530    }
531
532    pub async fn list_failed_async(&self) -> crate::ImResult<Vec<super::SecureOutboxEntry>> {
533        #[cfg(feature = "sqlite")]
534        {
535            let db = self.client.core_inner().local_state_db().await?;
536            let scope =
537                crate::internal::store::e2ee_outbox::E2eeOutboxOwnerScope::for_client(self.client);
538            db.list_e2ee_outbox(scope, Some("failed".to_owned()))
539                .await?
540                .iter()
541                .map(crate::internal::store::e2ee_outbox::secure_outbox_entry_from_record)
542                .collect::<crate::ImResult<Vec<_>>>()
543        }
544        #[cfg(not(feature = "sqlite"))]
545        {
546            let _ = self.client.current_identity();
547            Ok(Vec::new())
548        }
549    }
550
551    pub fn retry(
552        &self,
553        outbox_id: super::SecureOutboxId,
554    ) -> crate::ImResult<super::SecureOutboxResult> {
555        #[cfg(all(feature = "sqlite", feature = "blocking"))]
556        {
557            let connection = crate::internal::local_state::open_writable(
558                &self.client.core_inner().sdk_paths().local_state.sqlite_path,
559            )?;
560            let scope =
561                crate::internal::store::e2ee_outbox::E2eeOutboxOwnerScope::for_client(self.client);
562            let outbox_id_value = outbox_id.as_str().to_owned();
563            outbox_result_from_record(
564                outbox_id,
565                crate::internal::store::e2ee_outbox::retry_e2ee_outbox(
566                    &connection,
567                    &scope,
568                    &outbox_id_value,
569                )?,
570            )
571        }
572        #[cfg(all(feature = "sqlite", not(feature = "blocking")))]
573        {
574            let _ = outbox_id;
575            Err(crate::ImError::unsupported("sync-secure-outbox"))
576        }
577        #[cfg(not(feature = "sqlite"))]
578        {
579            let _ = outbox_id;
580            Err(crate::ImError::unsupported("secure-outbox"))
581        }
582    }
583
584    pub async fn retry_async(
585        &self,
586        outbox_id: super::SecureOutboxId,
587    ) -> crate::ImResult<super::SecureOutboxResult> {
588        #[cfg(feature = "sqlite")]
589        {
590            let db = self.client.core_inner().local_state_db().await?;
591            let scope =
592                crate::internal::store::e2ee_outbox::E2eeOutboxOwnerScope::for_client(self.client);
593            let outbox_id_value = outbox_id.as_str().to_owned();
594            outbox_result_from_record(
595                outbox_id,
596                db.retry_e2ee_outbox(scope, outbox_id_value).await?,
597            )
598        }
599        #[cfg(not(feature = "sqlite"))]
600        {
601            let _ = outbox_id;
602            Err(crate::ImError::unsupported("secure-outbox"))
603        }
604    }
605
606    pub fn drop(
607        &self,
608        outbox_id: super::SecureOutboxId,
609    ) -> crate::ImResult<super::SecureOutboxResult> {
610        #[cfg(all(feature = "sqlite", feature = "blocking"))]
611        {
612            let connection = crate::internal::local_state::open_writable(
613                &self.client.core_inner().sdk_paths().local_state.sqlite_path,
614            )?;
615            let scope =
616                crate::internal::store::e2ee_outbox::E2eeOutboxOwnerScope::for_client(self.client);
617            let outbox_id_value = outbox_id.as_str().to_owned();
618            outbox_result_from_record(
619                outbox_id,
620                crate::internal::store::e2ee_outbox::drop_e2ee_outbox(
621                    &connection,
622                    &scope,
623                    &outbox_id_value,
624                )?,
625            )
626        }
627        #[cfg(all(feature = "sqlite", not(feature = "blocking")))]
628        {
629            let _ = outbox_id;
630            Err(crate::ImError::unsupported("sync-secure-outbox"))
631        }
632        #[cfg(not(feature = "sqlite"))]
633        {
634            let _ = outbox_id;
635            Err(crate::ImError::unsupported("secure-outbox"))
636        }
637    }
638
639    pub async fn drop_async(
640        &self,
641        outbox_id: super::SecureOutboxId,
642    ) -> crate::ImResult<super::SecureOutboxResult> {
643        #[cfg(feature = "sqlite")]
644        {
645            let db = self.client.core_inner().local_state_db().await?;
646            let scope =
647                crate::internal::store::e2ee_outbox::E2eeOutboxOwnerScope::for_client(self.client);
648            let outbox_id_value = outbox_id.as_str().to_owned();
649            outbox_result_from_record(
650                outbox_id,
651                db.drop_e2ee_outbox(scope, outbox_id_value).await?,
652            )
653        }
654        #[cfg(not(feature = "sqlite"))]
655        {
656            let _ = outbox_id;
657            Err(crate::ImError::unsupported("secure-outbox"))
658        }
659    }
660}
661
662#[cfg(feature = "sqlite")]
663fn outbox_result_from_record(
664    id: super::SecureOutboxId,
665    record: Option<crate::internal::store::e2ee_outbox::E2eeOutboxRecord>,
666) -> crate::ImResult<super::SecureOutboxResult> {
667    let Some(record) = record else {
668        return Err(crate::ImError::MessageNotFound {
669            message_id: id.as_str().to_owned(),
670        });
671    };
672    Ok(super::SecureOutboxResult {
673        id,
674        status: match record.local_status.trim() {
675            "sending" => super::SecureOutboxStatus::Sending,
676            "failed" => super::SecureOutboxStatus::Failed,
677            "sent" => super::SecureOutboxStatus::Sent,
678            "dropped" => super::SecureOutboxStatus::Dropped,
679            _ => super::SecureOutboxStatus::Queued,
680        },
681        delivery: None,
682        warnings: Vec::new(),
683    })
684}