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