Skip to main content

bitwarden_organization_crypto/
invite.rs

1//! Cryptographic organization invites
2//!
3//! An invite is built around an XAES-256-GCM **invite key** that acts as the hub tying every sealed
4//! object together. Two independent secrets can recover the invite key (the invite secret from the
5//! link, or the organization key held by admins), and from the invite key everything else — the
6//! invite data and, when confirmation is enabled, the organization key — can be unsealed:
7//!
8//! ```text
9//! InviteSecret -> SecretProtectedKeyEnvelope -> InviteKey -> SymmetricKeyEnvelope -> InviteDataCEK
10//!                                                ^     |                                   |
11//!                                                |     |               DataEnvelope --------+
12//!                                                |     |                    +-> InviteSecret
13//!                                                |     |                    +-> OrgPubKeyPrint
14//! OrganizationKey -> EncString ------------------+     +-> SymmetricKeyEnvelope -> OrganizationKey
15//! ```
16//!
17//! - `InviteSecret -> SecretProtectedKeyEnvelope -> InviteKey` (`invite_secret_sealed_invite_key`):
18//!   the invite key sealed with the high-entropy invite secret, so an invitee holding only the
19//!   secret from the link recovers the invite key.
20//! - `OrganizationKey -> EncString -> InviteKey` (`organization_key_sealed_invite_key`): the invite
21//!   key sealed with the organization key, so anyone holding the organization key recovers the
22//!   invite key (and thus the invite data).
23//! - `InviteKey -> SymmetricKeyEnvelope -> InviteDataCEK -> DataEnvelope -> InviteSecret +
24//!   OrgPubKeyPrint` (`invite_key_sealed_invite_data_cek` + `sealed_invite_data`): the
25//!   `InviteDataV1` is sealed under its own content-encryption key (CEK), and that CEK is sealed
26//!   with the invite key. The data binds the organization public-key thumbprint and a copy of the
27//!   invite secret (so the invite link can be reconstructed from the invite key).
28//! - `InviteKey -> SymmetricKeyEnvelope -> OrganizationKey` (`invite_key_sealed_organization_key`):
29//!   the organization key sealed with the invite key, so a redeeming invitee can recover the
30//!   organization key. It is present if and exactly if confirmation is enabled for the invite.
31
32use std::str::FromStr;
33
34use bitwarden_crypto::{
35    CoseKeyThumbprint, CoseKeyThumbprintExt, EncString, KeySlotIds, KeyStoreContext,
36    generate_versioned_sealable,
37    safe::{
38        DataEnvelope, DataEnvelopeNamespace, HighEntropySecret, HighEntropySecretSource,
39        KeyEncryptionKey, SealableData, SealableVersionedData, SecretProtectedKeyEnvelope,
40        SecretProtectedKeyEnvelopeNamespace, SymmetricKeyEnvelope, SymmetricKeyEnvelopeNamespace,
41    },
42};
43use bitwarden_encoding::{B64Url, FromStrVisitor};
44use bitwarden_sensitive_value::{Sensitive, SensitiveSlice};
45use rand::Rng;
46use serde::{Deserialize, Serialize};
47use subtle::{Choice, ConstantTimeEq};
48use thiserror::Error;
49use zeroize::Zeroizing;
50
51use crate::organization_private_key::OrganizationPrivateKey;
52
53/// Length, in bytes, of the raw invite secret. 32 bytes provides 256 bits of entropy, which is why
54/// the invite secret is safe to use directly as a [`HighEntropySecret`].
55const INVITE_SECRET_LEN: usize = 32;
56
57/// Errors that can occur when creating or opening an invite.
58#[derive(Debug, Error)]
59pub enum InviteKeyBundleError {
60    /// Decoding the encrypted invite failed
61    #[error("Decoding failed")]
62    DecodingFailed,
63    /// Sealing one of the invite's envelopes failed
64    #[error("Unable to seal invite")]
65    KeySealingFailed,
66    /// Opening one of the invite's envelopes failed
67    #[error("Unable to unseal invite")]
68    KeyUnsealingFailed,
69    /// A required key was not found in the key store context
70    #[error("Missing Key for Id: {0}")]
71    MissingKeyId(String),
72    /// The organization private key could not be unwrapped, or its public-key thumbprint could not
73    /// be derived
74    #[error("Invalid organization private key")]
75    InvalidPrivateKey,
76    /// The organization key cannot be recovered from the invite because confirmation is disabled
77    #[error("Confirmation is not enabled on this invite")]
78    ConfirmationNotEnabled,
79}
80
81#[cfg(feature = "wasm")]
82#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
83const TS_INVITE_SECRET: &'static str = r#"
84export type InviteSecret = Tagged<string, "InviteSecret">;
85"#;
86
87/// The invite secret: 32 random, high-entropy bytes carried in the invite link.
88///
89/// CRITICAL: This must never be sent to the server.
90#[derive(Clone)]
91pub struct InviteSecret(Zeroizing<[u8; INVITE_SECRET_LEN]>);
92
93impl InviteSecret {
94    /// Generates a fresh invite secret: 32 random, high-entropy bytes drawn from the SDK CSPRNG.
95    fn make() -> Self {
96        let mut bytes = Zeroizing::new([0u8; INVITE_SECRET_LEN]);
97        bitwarden_random::rng().fill_bytes(bytes.as_mut_slice());
98        InviteSecret(bytes)
99    }
100}
101
102impl ConstantTimeEq for InviteSecret {
103    fn ct_eq(&self, other: &InviteSecret) -> Choice {
104        self.0.as_slice().ct_eq(other.0.as_slice())
105    }
106}
107
108impl PartialEq for InviteSecret {
109    fn eq(&self, other: &Self) -> bool {
110        self.ct_eq(other).into()
111    }
112}
113
114// Manually implemented so the raw invite secret bytes are never printed.
115impl std::fmt::Debug for InviteSecret {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        f.debug_struct("InviteSecret").finish()
118    }
119}
120
121/// Exposes the invite secret as a [`HighEntropySecret`]. This is sound because the invite secret is
122/// 32 bytes sampled from a CSPRNG.
123impl HighEntropySecretSource for InviteSecret {
124    fn provide_high_entropy_bytes(&self) -> SensitiveSlice<'_> {
125        Sensitive::from(self.0.as_slice())
126    }
127}
128
129impl From<&InviteSecret> for String {
130    fn from(secret: &InviteSecret) -> Self {
131        B64Url::from(secret.0.as_slice()).to_string()
132    }
133}
134
135impl FromStr for InviteSecret {
136    type Err = InviteKeyBundleError;
137
138    fn from_str(s: &str) -> Result<Self, Self::Err> {
139        let data = B64Url::try_from(s).map_err(|_| InviteKeyBundleError::DecodingFailed)?;
140        let bytes: [u8; INVITE_SECRET_LEN] = data
141            .as_bytes()
142            .try_into()
143            .map_err(|_| InviteKeyBundleError::DecodingFailed)?;
144        Ok(InviteSecret(Zeroizing::new(bytes)))
145    }
146}
147
148impl<'de> Deserialize<'de> for InviteSecret {
149    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
150    where
151        D: serde::Deserializer<'de>,
152    {
153        deserializer.deserialize_str(FromStrVisitor::new())
154    }
155}
156
157impl Serialize for InviteSecret {
158    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
159    where
160        S: serde::Serializer,
161    {
162        serializer.serialize_str(&String::from(self))
163    }
164}
165
166/// The plaintext data sealed inside the invite's [`DataEnvelope`] with the invite key.
167///
168/// It binds the organization public-key thumbprint (so account-recovery enrollment can verify the
169/// organization key belongs to the expected identity) and a copy of the invite secret (so a holder
170/// of the invite key can reconstruct the invite link).
171#[derive(Serialize, Deserialize, Debug, PartialEq)]
172struct InviteDataV1 {
173    /// RFC 9679 COSE Key Thumbprint (SHA-256) of the organization public key.
174    public_key_thumbprint: [u8; 32],
175    /// The raw invite secret bytes.
176    invite_secret: [u8; INVITE_SECRET_LEN],
177}
178impl SealableData for InviteDataV1 {}
179
180generate_versioned_sealable!(
181    InviteData,
182    DataEnvelopeNamespace::OrganizationInvite,
183    [
184        InviteDataV1 => "1",
185    ]
186);
187
188/// Cryptographic invite for an organization, built around an XAES-256-GCM invite key that acts as
189/// the hub tying everything together. See the module-level docs for the overall diagram.
190#[derive(Clone)]
191pub struct Invite {
192    /// The `InviteData` (org public-key thumbprint + invite secret) sealed with its own
193    /// content-encryption key (CEK).
194    sealed_invite_data: DataEnvelope,
195    /// The invite-data content-encryption key sealed with the invite key, so a holder of the
196    /// invite key can recover the CEK and open [`Self::sealed_invite_data`].
197    invite_key_sealed_invite_data_cek: SymmetricKeyEnvelope,
198    /// The invite key sealed with the high-entropy invite secret, letting an invitee (who holds
199    /// only the invite secret from the link) recover the invite key.
200    invite_secret_sealed_invite_key: SecretProtectedKeyEnvelope,
201    /// The organization key sealed with the invite key, letting a redeeming invitee recover the
202    /// organization key once they hold the invite key. This is **optional**: its presence is what
203    /// "confirmation" means. When confirmation is enabled the invitee can self-confirm by
204    /// recovering the organization key; when disabled, this field is absent and an admin must
205    /// confirm the invitee out of band.
206    invite_key_sealed_organization_key: Option<SymmetricKeyEnvelope>,
207    /// The invite key sealed with the organization key, letting anyone holding the organization
208    /// key recover the invite key (and thus the invite data).
209    organization_key_sealed_invite_key: EncString,
210}
211
212/// Wire format for [`Invite`]. This is what's serialized by serde.
213///
214/// [`Invite`] itself serializes as its [`String`] form, so this mirror struct carries the actual
215/// JSON field layout. Keeping the two apart means an invite is a single opaque string wherever it
216/// is embedded — on the wire and in storage.
217#[derive(Serialize, Deserialize)]
218struct SerializedInvite {
219    sealed_invite_data: DataEnvelope,
220    invite_key_sealed_invite_data_cek: SymmetricKeyEnvelope,
221    invite_secret_sealed_invite_key: SecretProtectedKeyEnvelope,
222    invite_key_sealed_organization_key: Option<SymmetricKeyEnvelope>,
223    organization_key_sealed_invite_key: EncString,
224}
225
226impl From<&Invite> for SerializedInvite {
227    fn from(invite: &Invite) -> Self {
228        SerializedInvite {
229            sealed_invite_data: invite.sealed_invite_data.clone(),
230            invite_key_sealed_invite_data_cek: invite.invite_key_sealed_invite_data_cek.clone(),
231            invite_secret_sealed_invite_key: invite.invite_secret_sealed_invite_key.clone(),
232            invite_key_sealed_organization_key: invite.invite_key_sealed_organization_key.clone(),
233            organization_key_sealed_invite_key: invite.organization_key_sealed_invite_key.clone(),
234        }
235    }
236}
237
238impl From<&Invite> for String {
239    fn from(invite: &Invite) -> Self {
240        serde_json::to_string(&SerializedInvite::from(invite))
241            .expect("JSON serialization of Invite never fails")
242    }
243}
244
245impl FromStr for Invite {
246    type Err = InviteKeyBundleError;
247
248    fn from_str(s: &str) -> Result<Self, Self::Err> {
249        let data: SerializedInvite =
250            serde_json::from_str(s).map_err(|_| InviteKeyBundleError::DecodingFailed)?;
251        Ok(Invite {
252            sealed_invite_data: data.sealed_invite_data,
253            invite_key_sealed_invite_data_cek: data.invite_key_sealed_invite_data_cek,
254            invite_secret_sealed_invite_key: data.invite_secret_sealed_invite_key,
255            invite_key_sealed_organization_key: data.invite_key_sealed_organization_key,
256            organization_key_sealed_invite_key: data.organization_key_sealed_invite_key,
257        })
258    }
259}
260
261impl<'de> Deserialize<'de> for Invite {
262    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
263    where
264        D: serde::Deserializer<'de>,
265    {
266        deserializer.deserialize_str(FromStrVisitor::new())
267    }
268}
269
270impl Serialize for Invite {
271    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
272    where
273        S: serde::Serializer,
274    {
275        serializer.serialize_str(&String::from(self))
276    }
277}
278
279// Manually implemented to mirror the safe key-envelope primitives: it surfaces the sealed fields
280// without ever printing key material (each field's own `Debug` is key-material-safe).
281impl std::fmt::Debug for Invite {
282    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283        f.debug_struct("Invite")
284            .field("sealed_invite_data", &self.sealed_invite_data)
285            .field(
286                "invite_key_sealed_invite_data_cek",
287                &self.invite_key_sealed_invite_data_cek,
288            )
289            .field(
290                "invite_secret_sealed_invite_key",
291                &self.invite_secret_sealed_invite_key,
292            )
293            .field(
294                "invite_key_sealed_organization_key",
295                &self.invite_key_sealed_organization_key,
296            )
297            .field(
298                "organization_key_sealed_invite_key",
299                &self.organization_key_sealed_invite_key,
300            )
301            .finish()
302    }
303}
304
305impl Invite {
306    /// Recovers the invite key using the organization key
307    pub fn unseal_invite_key_with_organization_key<Ids: KeySlotIds>(
308        &self,
309        organization_key: Ids::Symmetric,
310        ctx: &mut KeyStoreContext<Ids>,
311    ) -> Result<Ids::Symmetric, InviteKeyBundleError> {
312        ctx.unwrap_symmetric_key(organization_key, &self.organization_key_sealed_invite_key)
313            .map_err(|_| InviteKeyBundleError::KeyUnsealingFailed)
314    }
315
316    /// Recovers the invite key from the invite secret
317    pub fn unseal_invite_key_with_invite_secret<Ids: KeySlotIds>(
318        &self,
319        invite_secret: &InviteSecret,
320        ctx: &mut KeyStoreContext<Ids>,
321    ) -> Result<Ids::Symmetric, InviteKeyBundleError> {
322        let secret = HighEntropySecret::from(invite_secret.clone());
323        self.invite_secret_sealed_invite_key
324            .unseal(
325                &secret,
326                SecretProtectedKeyEnvelopeNamespace::OrganizationInvite,
327                ctx,
328            )
329            .map_err(|_| InviteKeyBundleError::KeyUnsealingFailed)
330    }
331
332    /// Unseals the `InviteDataV1` using an invite key
333    fn unseal_invite_data<Ids: KeySlotIds>(
334        &self,
335        invite_key: Ids::Symmetric,
336        ctx: &mut KeyStoreContext<Ids>,
337    ) -> Result<InviteDataV1, InviteKeyBundleError> {
338        // Recover the invite-data CEK by unsealing it with the invite key, then open the data.
339        let cek = self
340            .invite_key_sealed_invite_data_cek
341            .unseal(
342                invite_key,
343                SymmetricKeyEnvelopeNamespace::OrganizationInvite,
344                ctx,
345            )
346            .map_err(|_| InviteKeyBundleError::KeyUnsealingFailed)?;
347        let data: InviteData = self
348            .sealed_invite_data
349            .unseal(cek, ctx)
350            .map_err(|_| InviteKeyBundleError::KeyUnsealingFailed)?;
351        let InviteData::InviteDataV1(data) = data;
352        Ok(data)
353    }
354
355    /// Recovers the [`InviteSecret`] using an invite key
356    pub fn get_invite_secret<Ids: KeySlotIds>(
357        &self,
358        invite_key: Ids::Symmetric,
359        ctx: &mut KeyStoreContext<Ids>,
360    ) -> Result<InviteSecret, InviteKeyBundleError> {
361        let data = self.unseal_invite_data(invite_key, ctx)?;
362        Ok(InviteSecret(Zeroizing::new(data.invite_secret)))
363    }
364
365    /// Recovers the organization public-key thumbprint bound into the invite using an invite key
366    pub fn get_public_key_thumbprint<Ids: KeySlotIds>(
367        &self,
368        invite_key: Ids::Symmetric,
369        ctx: &mut KeyStoreContext<Ids>,
370    ) -> Result<CoseKeyThumbprint, InviteKeyBundleError> {
371        let data = self.unseal_invite_data(invite_key, ctx)?;
372        Ok(CoseKeyThumbprint::from_bytes(data.public_key_thumbprint))
373    }
374
375    /// Whether confirmation is enabled on this invite, i.e. whether the organization key can be
376    /// recovered from the invite key.
377    pub fn supports_confirmation(&self) -> bool {
378        self.invite_key_sealed_organization_key.is_some()
379    }
380
381    /// Unseals the organization key using an invite key, storing it in the key store context and
382    /// returning its id.
383    pub fn unseal_organization_key<Ids: KeySlotIds>(
384        &self,
385        invite_key: Ids::Symmetric,
386        ctx: &mut KeyStoreContext<Ids>,
387    ) -> Result<Ids::Symmetric, InviteKeyBundleError> {
388        self.invite_key_sealed_organization_key
389            .as_ref()
390            .ok_or(InviteKeyBundleError::ConfirmationNotEnabled)?
391            .unseal(
392                invite_key,
393                SymmetricKeyEnvelopeNamespace::OrganizationInvite,
394                ctx,
395            )
396            .map_err(|_| InviteKeyBundleError::KeyUnsealingFailed)
397    }
398
399    /// Enables confirmation
400    pub fn enable_confirmation<Ids: KeySlotIds>(
401        &mut self,
402        organization_key: Ids::Symmetric,
403        ctx: &mut KeyStoreContext<Ids>,
404    ) -> Result<(), InviteKeyBundleError> {
405        let invite_key = self.unseal_invite_key_with_organization_key(organization_key, ctx)?;
406        let envelope = SymmetricKeyEnvelope::seal(
407            organization_key,
408            invite_key,
409            SymmetricKeyEnvelopeNamespace::OrganizationInvite,
410            ctx,
411        )
412        .map_err(|_| InviteKeyBundleError::KeySealingFailed)?;
413        self.invite_key_sealed_organization_key = Some(envelope);
414        Ok(())
415    }
416
417    /// Disables confirmation
418    pub fn disable_confirmation(&mut self) {
419        self.invite_key_sealed_organization_key = None;
420    }
421
422    /// Generates a brand new invite around a new invite key. The invite is sealed for the
423    /// provided organization key and bound to the organization's public-key thumbprint (see
424    /// [`Invite`]).
425    ///
426    /// `wrapped_organization_private_key` is the organization's private key wrapped with
427    /// `organization_key`; the public-key thumbprint bound into the invite is derived from it.
428    ///
429    /// Returns the raw [`InviteSecret`] (which MUST NOT be sent to the server) together with the
430    /// server-safe [`Invite`].
431    pub fn make_for_private_key<Ids: KeySlotIds>(
432        organization_key: Ids::Symmetric,
433        wrapped_organization_private_key: &EncString,
434        ctx: &mut KeyStoreContext<Ids>,
435    ) -> Result<(InviteSecret, Invite), InviteKeyBundleError> {
436        // Derive the organization public-key thumbprint from the wrapped private key.
437        let organization_private_key = OrganizationPrivateKey::unwrap_with_organization_key(
438            organization_key,
439            wrapped_organization_private_key,
440            ctx,
441        )
442        .map_err(|_| InviteKeyBundleError::InvalidPrivateKey)?;
443        let thumbprint = organization_private_key
444            .public_key(ctx)
445            .map_err(|_| InviteKeyBundleError::InvalidPrivateKey)?
446            .thumbprint()
447            .map_err(|_| InviteKeyBundleError::InvalidPrivateKey)?;
448
449        let invite_secret = InviteSecret::make();
450        let invite_key = KeyEncryptionKey::make(ctx);
451
452        // Seal the invite data (thumbprint + a copy of the invite secret) under a fresh
453        // content-encryption key (CEK), then seal that CEK with the invite key so a holder of the
454        // invite key can open the data.
455        let invite_data: InviteData = InviteDataV1 {
456            public_key_thumbprint: *thumbprint.as_bytes(),
457            invite_secret: *invite_secret.0,
458        }
459        .into();
460        let (sealed_invite_data, invite_data_cek) = DataEnvelope::seal(invite_data, ctx)
461            .map_err(|_| InviteKeyBundleError::KeySealingFailed)?;
462        let invite_key_sealed_invite_data_cek = SymmetricKeyEnvelope::seal(
463            invite_data_cek,
464            invite_key,
465            SymmetricKeyEnvelopeNamespace::OrganizationInvite,
466            ctx,
467        )
468        .map_err(|_| InviteKeyBundleError::KeySealingFailed)?;
469
470        // Seal the invite key with the invite secret (invitee -> invite key direction).
471        let secret = HighEntropySecret::from(invite_secret.clone());
472        let invite_secret_sealed_invite_key = SecretProtectedKeyEnvelope::seal(
473            invite_key,
474            &secret,
475            SecretProtectedKeyEnvelopeNamespace::OrganizationInvite,
476            ctx,
477        )
478        .map_err(|_| InviteKeyBundleError::KeySealingFailed)?;
479
480        // Seal the organization key with the invite key (invite key -> organization key direction).
481        // New invites are created with confirmation enabled; callers can disable it afterwards via
482        // `Invite::disable_confirmation`.
483        let invite_key_sealed_organization_key = Some(
484            SymmetricKeyEnvelope::seal(
485                organization_key,
486                invite_key,
487                SymmetricKeyEnvelopeNamespace::OrganizationInvite,
488                ctx,
489            )
490            .map_err(|_| InviteKeyBundleError::KeySealingFailed)?,
491        );
492
493        // Seal the invite key with the organization key (organization key -> invite key direction).
494        let organization_key_sealed_invite_key = ctx
495            .wrap_symmetric_key(organization_key, invite_key)
496            .map_err(|_| InviteKeyBundleError::KeySealingFailed)?;
497
498        Ok((
499            invite_secret,
500            Invite {
501                sealed_invite_data,
502                invite_key_sealed_invite_data_cek,
503                invite_secret_sealed_invite_key,
504                invite_key_sealed_organization_key,
505                organization_key_sealed_invite_key,
506            },
507        ))
508    }
509}
510
511#[cfg(test)]
512mod tests {
513    use bitwarden_crypto::{CoseKeyThumbprintExt, KeyStore, PublicKeyEncryptionAlgorithm};
514    use bitwarden_encoding::B64Url;
515
516    use crate::{
517        invite::{Invite, InviteKeyBundleError, InviteSecret},
518        test_support::{
519            TEST_VECTOR_V1_ORG_KEY, TestIds, TestSymmKey, add_symmetric_key, load_v1_test_vectors,
520        },
521    };
522
523    const TEST_VECTOR_INVITE: &str = r#"{"sealed_invite_data":"g1hHpQEDA3gjYXBwbGljYXRpb24veC5iaXR3YXJkZW4uY2Jvci1wYWRkZWQEUPw60HtcnAwO6kRKd7MnQz86AAE4gQI6AAE4gAKhBUz+fzObmLDLKRDeBJJYxX9qAxhXIe1Ri1CJw3ojv7WUEwBpVWeMEGTK7HHbp9WnTDjK849psMx6EOpQ7B/BYGiO28Zn0tjtQ85zwShii4FRK39mtJFE8XxV46hxW9+LlH6EPt4yfzHVkTNZ3vgOiSXcs6zyKXIhQaz6yh8eyA653m4DJEHV00JjdQ/jSfTEfyDf6LiflGntDZ7gSYlQMRxuS71yVP9Mgm79yFY4aIw703G/HUjvIEt4XlWCLzvaLDmegpDz9z4eQCVk8v8JM1fS7BsA","invite_key_sealed_invite_data_cek":"g1g+pgE6AAEReQMYZQRQ7raDdes1HnAgiuiZWXyS9ToAARVcUPw60HtcnAwO6kRKd7MnQz86AAE4gQM6AAE4gAKhBVgYvxSL4XPZLwEXHPnEWweqeeKjyH9r85/ZWE3rsZy1MLmzkFOCJa8a0o20b1P3IH6c5NYtT3v1a50+X0GmgDKiMf/omjhwRRQ7ua6wK0O2JBlhM4JIE8jmtsyMd46mFDbURK1kGRvl7Q==","invite_secret_sealed_invite_key":"hFgopQEDAxhlOgABFVxQ7raDdes1HnAgiuiZWXyS9ToAATiBBjoAATiAAaEFTJvCToVTNSe1uWbD5FhUf061kAAj1sMGWjJ8IWPV2e2xk5Z+ownZ1RqUXG2jv9h2vnsEFZ2yglvKaSCGTsOuvOXv0ESwQk6eUtFAaZjxV/Rajyuu6YgEa712Tfb9Jz13QLGngYNAogEpM1gg1MsuVokUsK8WkqggHRdJ4jvzFsbN/bP4g0l+j6f97qb2","invite_key_sealed_organization_key":"g1hKpQE6AAEReQN4ImFwcGxpY2F0aW9uL3guYml0d2FyZGVuLmxlZ2FjeS1rZXkEUO62g3XrNR5wIIromVl8kvU6AAE4gQM6AAE4gAKhBVgYoth3hg+yUTLXF4ksaeT42IWGKuTv27B4WFAKV8Z7uKGZNHOONGgZQLbQMozgYX9tseuet413M314W0sV3cBKZIRSEZfj3NvHU8tE/6b2oGxPQIPKP3Tyhl84zhI4uG+Mo5WKkvtdPonD0A==","organization_key_sealed_invite_key":"2.XjZXnAwXK/cXCmHNgCPQzw==|WEGa37JPdNWVnMPWlfinyvZdXpMW8kpTBypXPWf/abbG4/+6vp/WJQmVlkVEflEkuZwjKWSkMWJPcACGBoabeBHgzqpkSYf3kyQb7VhePHQ=|kZLDCLGm1nCJhlVuNahFiZOecy5tKG2fXGCNZZzbDjk="}"#;
524    const TEST_VECTOR_INVITE_SECRET: &str = "Ttas45_CvZi1yoFJ3bMCHx0DAAGGxDi-1BhHCutwDjI";
525
526    #[test]
527    fn test_basic_invitation_bundle() {
528        let key_store = KeyStore::<TestIds>::default();
529        let mut ctx = key_store.context_mut();
530        let wrapped_private_key = load_v1_test_vectors(&mut ctx);
531
532        let (secret1, _) =
533            Invite::make_for_private_key(TestSymmKey::Organization, &wrapped_private_key, &mut ctx)
534                .unwrap();
535        let (secret2, _) =
536            Invite::make_for_private_key(TestSymmKey::Organization, &wrapped_private_key, &mut ctx)
537                .unwrap();
538
539        assert_ne!(secret1, secret2);
540    }
541
542    #[test]
543    fn test_admin_recovers_invite_secret() {
544        let key_store = KeyStore::<TestIds>::default();
545        let mut ctx = key_store.context_mut();
546        let wrapped_private_key = load_v1_test_vectors(&mut ctx);
547
548        let (invite_secret, invite) =
549            Invite::make_for_private_key(TestSymmKey::Organization, &wrapped_private_key, &mut ctx)
550                .unwrap();
551
552        let invite_key = invite
553            .unseal_invite_key_with_organization_key(TestSymmKey::Organization, &mut ctx)
554            .unwrap();
555        let recovered = invite.get_invite_secret(invite_key, &mut ctx).unwrap();
556
557        assert_eq!(invite_secret, recovered);
558    }
559
560    #[test]
561    fn test_admin_recovers_thumbprint() {
562        let key_store = KeyStore::<TestIds>::default();
563        let mut ctx = key_store.context_mut();
564        let wrapped_private_key = load_v1_test_vectors(&mut ctx);
565
566        // The expected thumbprint is derived from the same wrapped private key that
567        // `make_for_private_key` binds into the invite.
568        let private_key_id = ctx
569            .unwrap_private_key(TestSymmKey::Organization, &wrapped_private_key)
570            .unwrap();
571        let expected = ctx
572            .get_public_key(private_key_id)
573            .unwrap()
574            .thumbprint()
575            .unwrap();
576
577        let (_, invite) =
578            Invite::make_for_private_key(TestSymmKey::Organization, &wrapped_private_key, &mut ctx)
579                .unwrap();
580
581        let invite_key = invite
582            .unseal_invite_key_with_organization_key(TestSymmKey::Organization, &mut ctx)
583            .unwrap();
584        let recovered = invite
585            .get_public_key_thumbprint(invite_key, &mut ctx)
586            .unwrap();
587
588        assert_eq!(recovered, expected);
589    }
590
591    #[test]
592    fn test_invitee_recovers_organization_key() {
593        let key_store = KeyStore::<TestIds>::default();
594        let mut ctx = key_store.context_mut();
595        let wrapped_private_key = load_v1_test_vectors(&mut ctx);
596
597        let (invite_secret, invite) =
598            Invite::make_for_private_key(TestSymmKey::Organization, &wrapped_private_key, &mut ctx)
599                .unwrap();
600
601        // Using only the raw invite secret, an invitee can recover the invite key and then the
602        // organization key.
603        let invite_key = invite
604            .unseal_invite_key_with_invite_secret(&invite_secret, &mut ctx)
605            .unwrap();
606        let recovered_org_key_id = invite
607            .unseal_organization_key(invite_key, &mut ctx)
608            .unwrap();
609
610        ctx.assert_symmetric_keys_equal(recovered_org_key_id, TestSymmKey::Organization);
611    }
612
613    #[test]
614    fn test_confirmation_toggle() {
615        let key_store = KeyStore::<TestIds>::default();
616        let mut ctx = key_store.context_mut();
617        let wrapped_private_key = load_v1_test_vectors(&mut ctx);
618
619        let (invite_secret, mut invite) =
620            Invite::make_for_private_key(TestSymmKey::Organization, &wrapped_private_key, &mut ctx)
621                .unwrap();
622
623        // New invites are created with confirmation enabled.
624        assert!(invite.supports_confirmation());
625
626        // Disabling confirmation removes the org-key envelope, so an invitee can no longer recover
627        // the organization key.
628        invite.disable_confirmation();
629        assert!(!invite.supports_confirmation());
630        let invite_key = invite
631            .unseal_invite_key_with_invite_secret(&invite_secret, &mut ctx)
632            .unwrap();
633        assert!(matches!(
634            invite.unseal_organization_key(invite_key, &mut ctx),
635            Err(InviteKeyBundleError::ConfirmationNotEnabled)
636        ));
637
638        // Re-enabling confirmation restores the invitee's ability to recover the organization key.
639        invite
640            .enable_confirmation(TestSymmKey::Organization, &mut ctx)
641            .unwrap();
642        assert!(invite.supports_confirmation());
643        let invite_key = invite
644            .unseal_invite_key_with_invite_secret(&invite_secret, &mut ctx)
645            .unwrap();
646        let recovered_org_key_id = invite
647            .unseal_organization_key(invite_key, &mut ctx)
648            .unwrap();
649        ctx.assert_symmetric_keys_equal(recovered_org_key_id, TestSymmKey::Organization);
650    }
651
652    #[test]
653    fn test_invite_string_round_trip() {
654        let key_store = KeyStore::<TestIds>::default();
655        let mut ctx = key_store.context_mut();
656        let wrapped_private_key = load_v1_test_vectors(&mut ctx);
657
658        let (invite_secret, invite) =
659            Invite::make_for_private_key(TestSymmKey::Organization, &wrapped_private_key, &mut ctx)
660                .unwrap();
661
662        let encoded = String::from(&invite);
663        let decoded: Invite = encoded.parse().unwrap();
664        assert_eq!(String::from(&decoded), encoded);
665
666        // The decoded invite still recovers the invite secret.
667        let invite_key = decoded
668            .unseal_invite_key_with_organization_key(TestSymmKey::Organization, &mut ctx)
669            .unwrap();
670        let recovered = decoded.get_invite_secret(invite_key, &mut ctx).unwrap();
671        assert_eq!(invite_secret, recovered);
672
673        // The custom serde impls delegate to the string round-trip: an invite must serialize as a
674        // single string, not as a struct, matching how the server API carries it.
675        let json = serde_json::to_string(&invite).unwrap();
676        assert_eq!(json, serde_json::to_string(&encoded).unwrap());
677        let from_json: Invite = serde_json::from_str(&json).unwrap();
678        assert_eq!(String::from(&from_json), encoded);
679    }
680
681    #[test]
682    fn test_wrong_invite_secret_fails() {
683        let key_store = KeyStore::<TestIds>::default();
684        let mut ctx = key_store.context_mut();
685        let wrapped_private_key = load_v1_test_vectors(&mut ctx);
686
687        let (_, invite) =
688            Invite::make_for_private_key(TestSymmKey::Organization, &wrapped_private_key, &mut ctx)
689                .unwrap();
690
691        // A different invite secret cannot unseal the invite key.
692        let wrong_secret = InviteSecret(zeroize::Zeroizing::new([7u8; 32]));
693        assert!(matches!(
694            invite.unseal_invite_key_with_invite_secret(&wrong_secret, &mut ctx),
695            Err(InviteKeyBundleError::KeyUnsealingFailed)
696        ));
697    }
698
699    #[test]
700    fn test_wrong_organization_key_fails() {
701        let key_store = KeyStore::<TestIds>::default();
702        let mut ctx = key_store.context_mut();
703        let wrapped_private_key = load_v1_test_vectors(&mut ctx);
704
705        let (_, invite) =
706            Invite::make_for_private_key(TestSymmKey::Organization, &wrapped_private_key, &mut ctx)
707                .unwrap();
708
709        // A different organization key cannot unwrap the invite key.
710        let wrong_org_key = ctx.generate_symmetric_key();
711        assert!(matches!(
712            invite.unseal_invite_key_with_organization_key(wrong_org_key, &mut ctx),
713            Err(InviteKeyBundleError::KeyUnsealingFailed)
714        ));
715    }
716
717    #[test]
718    fn test_invitee_recovers_thumbprint() {
719        let key_store = KeyStore::<TestIds>::default();
720        let mut ctx = key_store.context_mut();
721        let wrapped_private_key = load_v1_test_vectors(&mut ctx);
722
723        let private_key_id = ctx
724            .unwrap_private_key(TestSymmKey::Organization, &wrapped_private_key)
725            .unwrap();
726        let expected = ctx
727            .get_public_key(private_key_id)
728            .unwrap()
729            .thumbprint()
730            .unwrap();
731
732        let (invite_secret, invite) =
733            Invite::make_for_private_key(TestSymmKey::Organization, &wrapped_private_key, &mut ctx)
734                .unwrap();
735
736        // The invitee reaches the invite key via the invite secret and reads the same bound
737        // thumbprint the admin would.
738        let invite_key = invite
739            .unseal_invite_key_with_invite_secret(&invite_secret, &mut ctx)
740            .unwrap();
741        let recovered = invite
742            .get_public_key_thumbprint(invite_key, &mut ctx)
743            .unwrap();
744
745        assert_eq!(recovered, expected);
746    }
747
748    #[test]
749    fn test_malformed_invite_string_fails() {
750        // Not JSON at all.
751        assert!(matches!(
752            "not valid json !!!".parse::<Invite>(),
753            Err(InviteKeyBundleError::DecodingFailed)
754        ));
755
756        // Valid JSON, but missing the required invite fields.
757        assert!(matches!(
758            "{}".parse::<Invite>(),
759            Err(InviteKeyBundleError::DecodingFailed)
760        ));
761    }
762
763    #[test]
764    fn test_invalid_wrapped_private_key_fails() {
765        let key_store = KeyStore::<TestIds>::default();
766        let mut ctx = key_store.context_mut();
767        let wrapped_private_key = load_v1_test_vectors(&mut ctx);
768
769        // The wrapped private key can only be unwrapped with the organization key it was wrapped
770        // with; a different key makes `make_for_private_key` fail before building the invite.
771        let wrong_org_key = ctx.generate_symmetric_key();
772        assert!(matches!(
773            Invite::make_for_private_key(wrong_org_key, &wrapped_private_key, &mut ctx),
774            Err(InviteKeyBundleError::InvalidPrivateKey)
775        ));
776    }
777
778    #[test]
779    fn test_invite_secret_into_base64_url() {
780        let data: [u8; 32] = *b"+/=Hello, World!AAAAAAAAAAAAAAAA";
781        let secret = InviteSecret(zeroize::Zeroizing::new(data));
782
783        let encoded = String::from(&secret);
784
785        assert_eq!(encoded, "Ky89SGVsbG8sIFdvcmxkIUFBQUFBQUFBQUFBQUFBQUE");
786        assert!(!encoded.contains('+'));
787        assert!(!encoded.contains('/'));
788        assert!(!encoded.contains('='));
789
790        let decoded = B64Url::try_from(encoded.as_str()).unwrap();
791        assert_eq!(decoded.as_bytes(), data);
792
793        // Round-trips back to the same invite secret.
794        let reparsed: InviteSecret = encoded.parse().unwrap();
795        assert_eq!(reparsed, secret);
796    }
797
798    #[test]
799    #[ignore = "Manual test to generate test vectors"]
800    fn generate_test_vectors() {
801        let key_store = KeyStore::<TestIds>::default();
802        let mut ctx = key_store.context_mut();
803
804        let org_key_id = add_symmetric_key(TEST_VECTOR_V1_ORG_KEY, &mut ctx);
805
806        // Make and wrap a fresh organization private key so it can be recorded as a fixed vector.
807        let private_key_id = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
808        let wrapped_private_key = ctx.wrap_private_key(org_key_id, private_key_id).unwrap();
809
810        let (invite_secret, invite) =
811            Invite::make_for_private_key(org_key_id, &wrapped_private_key, &mut ctx).unwrap();
812
813        println!(
814            "const TEST_VECTOR_V1_WRAPPED_PRIVATE_KEY: &str = \"{}\";",
815            wrapped_private_key.to_string()
816        );
817        println!(
818            "const TEST_VECTOR_INVITE: &str = r#\"{}\"#;",
819            String::from(&invite)
820        );
821        println!(
822            "const TEST_VECTOR_INVITE_SECRET: &str = \"{}\";",
823            String::from(&invite_secret)
824        );
825    }
826
827    #[test]
828    fn test_invite_test_vector() {
829        let key_store = KeyStore::<TestIds>::default();
830        let mut ctx = key_store.context_mut();
831
832        let org_key_id = add_symmetric_key(TEST_VECTOR_V1_ORG_KEY, &mut ctx);
833
834        let invite: Invite = TEST_VECTOR_INVITE.parse().unwrap();
835        let invite_key = invite
836            .unseal_invite_key_with_organization_key(org_key_id, &mut ctx)
837            .unwrap();
838        let recovered = invite.get_invite_secret(invite_key, &mut ctx).unwrap();
839
840        assert_eq!(String::from(&recovered), TEST_VECTOR_INVITE_SECRET);
841    }
842
843    #[test]
844    #[ignore = "Manual test to verify debug format"]
845    fn test_debug() {
846        let key_store = KeyStore::<TestIds>::default();
847        let mut ctx = key_store.context_mut();
848        let wrapped_private_key = load_v1_test_vectors(&mut ctx);
849
850        let (invite_secret, invite) =
851            Invite::make_for_private_key(TestSymmKey::Organization, &wrapped_private_key, &mut ctx)
852                .unwrap();
853        println!("{invite_secret:?}");
854        println!("{invite:?}");
855    }
856}