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
48impl SealedOpenOrgInviteData {
49 pub fn seal(input: OpenOrgInvite) -> Result<(Self, HighEntropySecret), RegistrationError> {
53 let key_store: KeyStore<KeySlotIds> = KeyStore::default();
55 let mut ctx = key_store.context_mut();
56
57 let high_entropy_secret = HighEntropySecret::make(OPEN_ORG_INVITE_SECRET_SIZE_BYTES)
58 .map_err(|_| RegistrationError::Crypto)?;
59
60 let versioned: RegistrationOpenOrgInviteData = RegistrationOpenOrgInviteDataV1 {
61 organization_id: input.organization_id,
62 invite_link_code: input.invite_link_code,
63 invite_secret: input.invite_secret,
64 }
65 .into();
66
67 let (data_envelope, cek_id) =
68 DataEnvelope::seal(versioned, &mut ctx).map_err(|_| RegistrationError::Crypto)?;
69
70 let key_envelope = SecretProtectedKeyEnvelope::seal(
71 cek_id,
72 &high_entropy_secret,
73 SecretProtectedKeyEnvelopeNamespace::RegistrationOpenOrgInvite,
74 &ctx,
75 )
76 .map_err(|_| RegistrationError::Crypto)?;
77
78 Ok((
79 SealedOpenOrgInviteData {
80 data_envelope,
81 key_envelope,
82 },
83 high_entropy_secret,
84 ))
85 }
86
87 pub fn unseal(&self, secret: &HighEntropySecret) -> Result<OpenOrgInvite, RegistrationError> {
91 let key_store: KeyStore<KeySlotIds> = KeyStore::default();
93 let mut ctx = key_store.context_mut();
94
95 let cek_id = self
96 .key_envelope
97 .unseal(
98 secret,
99 SecretProtectedKeyEnvelopeNamespace::RegistrationOpenOrgInvite,
100 &mut ctx,
101 )
102 .map_err(|_| RegistrationError::Crypto)?;
103
104 let versioned: RegistrationOpenOrgInviteData = self
105 .data_envelope
106 .unseal(cek_id, &mut ctx)
107 .map_err(|_| RegistrationError::Crypto)?;
108
109 let RegistrationOpenOrgInviteData::RegistrationOpenOrgInviteDataV1(v1) = versioned;
112 Ok(OpenOrgInvite {
113 organization_id: v1.organization_id,
114 invite_link_code: v1.invite_link_code,
115 invite_secret: v1.invite_secret,
116 })
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 fn sample_input() -> OpenOrgInvite {
125 OpenOrgInvite {
126 organization_id: "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".to_string(),
127 invite_link_code: "abcd1234efgh5678".to_string(),
128 invite_secret: "raw-invite-secret-material-base64url".to_string(),
129 }
130 }
131
132 #[test]
133 fn seal_produces_populated_sealed_data_and_high_entropy_secret() {
134 let (sealed_data, high_entropy_secret) =
135 SealedOpenOrgInviteData::seal(sample_input()).expect("seal should succeed");
136
137 let wire = String::from(&sealed_data);
138 assert!(!wire.is_empty());
139 let parsed: SealedOpenOrgInviteData = wire.parse().expect("wire form must round-trip");
140 let _ = parsed.data_envelope;
141 let _ = parsed.key_envelope;
142
143 let secret_wire = String::from(high_entropy_secret);
145 assert!(!secret_wire.is_empty());
146 secret_wire
147 .parse::<HighEntropySecret>()
148 .expect("high_entropy_secret must be a valid wire string");
149 }
150
151 #[test]
152 fn two_seals_produce_distinct_secrets_and_data() {
153 let (first_data, first_secret) =
154 SealedOpenOrgInviteData::seal(sample_input()).expect("first seal should succeed");
155 let (second_data, second_secret) =
156 SealedOpenOrgInviteData::seal(sample_input()).expect("second seal should succeed");
157
158 assert_ne!(String::from(first_secret), String::from(second_secret));
160 assert_ne!(String::from(&first_data), String::from(&second_data));
161 }
162
163 #[test]
164 fn seal_unseal_round_trip_recovers_original_fields() {
165 let input = sample_input();
166 let (sealed_data, high_entropy_secret) =
167 SealedOpenOrgInviteData::seal(input.clone()).expect("seal should succeed");
168
169 let unsealed = sealed_data
170 .unseal(&high_entropy_secret)
171 .expect("unseal should succeed");
172
173 assert_eq!(unsealed, input);
174 }
175
176 #[test]
177 fn unseal_fails_with_wrong_high_entropy_secret() {
178 let (sealed_data, _) =
179 SealedOpenOrgInviteData::seal(sample_input()).expect("seal should succeed");
180 let unrelated = HighEntropySecret::make(OPEN_ORG_INVITE_SECRET_SIZE_BYTES).unwrap();
181
182 let err = sealed_data
183 .unseal(&unrelated)
184 .expect_err("unseal must reject an unrelated secret");
185 assert!(matches!(err, RegistrationError::Crypto));
186 }
187}