bitwarden_auth/registration/open_org_invite_crypto/
open_org_invite.rs1use bitwarden_core::key_management::KeySlotIds;
5use bitwarden_crypto::{
6 KeyStore,
7 safe::{
8 DataEnvelope, HighEntropySecret, SecretProtectedKeyEnvelope,
9 SecretProtectedKeyEnvelopeNamespace,
10 },
11};
12use serde::{Deserialize, Serialize};
13
14use super::{RegistrationOpenOrgInviteData, data_v1::RegistrationOpenOrgInviteDataV1};
15use crate::registration::registration_client::RegistrationError;
16
17pub(super) const OPEN_ORG_INVITE_SECRET_SIZE_BYTES: usize = 32;
19
20#[bitwarden_ffi::wasm_record]
26#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
27#[serde(rename_all = "camelCase")]
28pub struct OpenOrgInvite {
29 pub organization_id: String,
31 pub invite_link_code: String,
33 pub invite_secret: String,
35}
36
37#[derive(Debug, Clone)]
39pub struct SealedOpenOrgInviteData {
40 pub(super) data_envelope: DataEnvelope,
42 pub(super) key_envelope: SecretProtectedKeyEnvelope,
44}
45
46#[cfg(feature = "wasm")]
48#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
49const TS_CUSTOM_TYPES: &'static str = r#"
50export type SealedOpenOrgInviteData = Tagged<string, "SealedOpenOrgInviteData">;
51"#;
52
53impl SealedOpenOrgInviteData {
54 pub fn seal(input: OpenOrgInvite) -> Result<(Self, HighEntropySecret), RegistrationError> {
58 let key_store: KeyStore<KeySlotIds> = KeyStore::default();
60 let mut ctx = key_store.context_mut();
61
62 let high_entropy_secret = HighEntropySecret::make(OPEN_ORG_INVITE_SECRET_SIZE_BYTES)
63 .map_err(|_| RegistrationError::Crypto)?;
64
65 let versioned: RegistrationOpenOrgInviteData = RegistrationOpenOrgInviteDataV1 {
66 organization_id: input.organization_id,
67 invite_link_code: input.invite_link_code,
68 invite_secret: input.invite_secret,
69 }
70 .into();
71
72 let (data_envelope, cek_id) =
73 DataEnvelope::seal(versioned, &mut ctx).map_err(|_| RegistrationError::Crypto)?;
74
75 let key_envelope = SecretProtectedKeyEnvelope::seal(
76 cek_id,
77 &high_entropy_secret,
78 SecretProtectedKeyEnvelopeNamespace::RegistrationOpenOrgInvite,
79 &ctx,
80 )
81 .map_err(|_| RegistrationError::Crypto)?;
82
83 Ok((
84 SealedOpenOrgInviteData {
85 data_envelope,
86 key_envelope,
87 },
88 high_entropy_secret,
89 ))
90 }
91
92 pub fn unseal(&self, secret: &HighEntropySecret) -> Result<OpenOrgInvite, RegistrationError> {
96 let key_store: KeyStore<KeySlotIds> = KeyStore::default();
98 let mut ctx = key_store.context_mut();
99
100 let cek_id = self
101 .key_envelope
102 .unseal(
103 secret,
104 SecretProtectedKeyEnvelopeNamespace::RegistrationOpenOrgInvite,
105 &mut ctx,
106 )
107 .map_err(|_| RegistrationError::Crypto)?;
108
109 let versioned: RegistrationOpenOrgInviteData = self
110 .data_envelope
111 .unseal(cek_id, &mut ctx)
112 .map_err(|_| RegistrationError::Crypto)?;
113
114 let RegistrationOpenOrgInviteData::RegistrationOpenOrgInviteDataV1(v1) = versioned;
117 Ok(OpenOrgInvite {
118 organization_id: v1.organization_id,
119 invite_link_code: v1.invite_link_code,
120 invite_secret: v1.invite_secret,
121 })
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 fn sample_input() -> OpenOrgInvite {
130 OpenOrgInvite {
131 organization_id: "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".to_string(),
132 invite_link_code: "abcd1234efgh5678".to_string(),
133 invite_secret: "raw-invite-secret-material-base64url".to_string(),
134 }
135 }
136
137 #[test]
138 fn seal_produces_populated_sealed_data_and_high_entropy_secret() {
139 let (sealed_data, high_entropy_secret) =
140 SealedOpenOrgInviteData::seal(sample_input()).expect("seal should succeed");
141
142 let wire = String::from(&sealed_data);
143 assert!(!wire.is_empty());
144 let parsed: SealedOpenOrgInviteData = wire.parse().expect("wire form must round-trip");
145 let _ = parsed.data_envelope;
146 let _ = parsed.key_envelope;
147
148 let secret_wire = String::from(high_entropy_secret);
150 assert!(!secret_wire.is_empty());
151 secret_wire
152 .parse::<HighEntropySecret>()
153 .expect("high_entropy_secret must be a valid wire string");
154 }
155
156 #[test]
157 fn two_seals_produce_distinct_secrets_and_data() {
158 let (first_data, first_secret) =
159 SealedOpenOrgInviteData::seal(sample_input()).expect("first seal should succeed");
160 let (second_data, second_secret) =
161 SealedOpenOrgInviteData::seal(sample_input()).expect("second seal should succeed");
162
163 assert_ne!(String::from(first_secret), String::from(second_secret));
165 assert_ne!(String::from(&first_data), String::from(&second_data));
166 }
167
168 #[test]
169 fn seal_unseal_round_trip_recovers_original_fields() {
170 let input = sample_input();
171 let (sealed_data, high_entropy_secret) =
172 SealedOpenOrgInviteData::seal(input.clone()).expect("seal should succeed");
173
174 let unsealed = sealed_data
175 .unseal(&high_entropy_secret)
176 .expect("unseal should succeed");
177
178 assert_eq!(unsealed, input);
179 }
180
181 #[test]
182 fn unseal_fails_with_wrong_high_entropy_secret() {
183 let (sealed_data, _) =
184 SealedOpenOrgInviteData::seal(sample_input()).expect("seal should succeed");
185 let unrelated = HighEntropySecret::make(OPEN_ORG_INVITE_SECRET_SIZE_BYTES).unwrap();
186
187 let err = sealed_data
188 .unseal(&unrelated)
189 .expect_err("unseal must reject an unrelated secret");
190 assert!(matches!(err, RegistrationError::Crypto));
191 }
192}