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