Skip to main content

bitwarden_auth/registration/open_org_invite_crypto/
client.rs

1//! FFI-facing seal/unseal. The [`RegistrationClient`] methods are thin wrappers over
2//! [`SealedOpenOrgInviteData::seal`] / [`SealedOpenOrgInviteData::unseal`];
3//! [`SealedOpenOrgInvite`] bundles both halves of the seal output as one WASM return type.
4
5use bitwarden_crypto::safe::HighEntropySecret;
6use serde::{Deserialize, Serialize};
7
8use super::{OpenOrgInvite, SealedOpenOrgInviteData};
9use crate::registration::registration_client::{RegistrationClient, RegistrationError};
10
11/// Sealed open-organization-invite payload. Produced by
12/// [`RegistrationClient::seal_open_org_invite_data`] and consumed by
13/// [`RegistrationClient::unseal_open_org_invite_data`]. Both fields are required to unseal;
14/// neither half is useful on its own.
15#[bitwarden_ffi::wasm_record]
16#[derive(Serialize, Deserialize, Debug, Clone)]
17#[serde(rename_all = "camelCase")]
18pub struct SealedOpenOrgInvite {
19    /// URL-safe opaque payload; place on the verification-email link.
20    pub sealed_data: SealedOpenOrgInviteData,
21    /// Paired secret; keep client-side (e.g. `localStorage`) and never send to the server.
22    pub high_entropy_secret: HighEntropySecret,
23}
24
25#[bitwarden_ffi::wasm_export]
26impl RegistrationClient {
27    /// Seals an [`OpenOrgInvite`] into a [`SealedOpenOrgInvite`]. The returned
28    /// `sealed_data` is safe to place on the verification-email link; the returned
29    /// `high_entropy_secret` must stay client-side.
30    pub fn seal_open_org_invite_data(
31        &self,
32        input: OpenOrgInvite,
33    ) -> Result<SealedOpenOrgInvite, RegistrationError> {
34        let (sealed_data, high_entropy_secret) = SealedOpenOrgInviteData::seal(input)?;
35        Ok(SealedOpenOrgInvite {
36            sealed_data,
37            high_entropy_secret,
38        })
39    }
40
41    /// Unseals a [`SealedOpenOrgInvite`] back into an [`OpenOrgInvite`]. Returns
42    /// [`RegistrationError::Crypto`] if the paired secret does not match the sealed payload or
43    /// the payload has been tampered with.
44    pub fn unseal_open_org_invite_data(
45        &self,
46        sealed: SealedOpenOrgInvite,
47    ) -> Result<OpenOrgInvite, RegistrationError> {
48        sealed.sealed_data.unseal(&sealed.high_entropy_secret)
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use bitwarden_core::Client;
55
56    use super::*;
57
58    fn sample_input() -> OpenOrgInvite {
59        OpenOrgInvite {
60            organization_id: "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".to_string(),
61            invite_link_code: "abcd1234efgh5678".to_string(),
62            invite_secret: "raw-invite-secret-material-base64url".to_string(),
63        }
64    }
65
66    #[test]
67    fn sealed_open_org_invite_json_wire_shape_is_stable() {
68        // Locks the JSON wire: two-key camelCase object, both values as strings.
69        let client = Client::new(None);
70        let registration_client = RegistrationClient::new(client);
71        let sealed = registration_client
72            .seal_open_org_invite_data(sample_input())
73            .expect("seal should succeed");
74
75        let json = serde_json::to_value(&sealed).expect("serialize");
76        let obj = json.as_object().expect("must be a JSON object");
77        assert_eq!(obj.len(), 2, "no extra or missing fields");
78        assert!(
79            obj.get("sealedData")
80                .expect("sealedData key must be present")
81                .is_string(),
82            "sealedData must serialize as a JSON string"
83        );
84        assert!(
85            obj.get("highEntropySecret")
86                .expect("highEntropySecret key must be present")
87                .is_string(),
88            "highEntropySecret must serialize as a JSON string"
89        );
90    }
91}