Skip to main content

bitwarden_auth/registration/open_org_invite_crypto/
serialization.rs

1//! Wire encoding for [`SealedOpenOrgInviteData`]. Its two internal envelopes are packed with
2//! CBOR (compact binary format) then base64url-wrapped so it crosses every boundary as one
3//! opaque string — via serde on the Rust side and the WASM ABI (wasm-bindgen's Rust↔JS
4//! conversion layer) into TypeScript.
5
6use std::str::FromStr;
7
8use bitwarden_crypto::safe::{DataEnvelope, SecretProtectedKeyEnvelope};
9use bitwarden_encoding::{B64Url, FromStrVisitor};
10use serde::{Deserialize, Serialize};
11use thiserror::Error;
12
13use super::SealedOpenOrgInviteData;
14
15/// Intermediate shape used to (de)serialize [`SealedOpenOrgInviteData`] to and from its wire
16/// bytes. Each envelope is carried as raw bytes under a short field name for compactness.
17#[derive(Serialize, Deserialize)]
18struct SealedOpenOrgInviteDataWire {
19    // Without serde_bytes, Vec<u8> encodes as a CBOR array of integers (~2x the size).
20    /// Bytes of the data envelope (OpenOrgInvite plaintext encrypted under a fresh CEK).
21    #[serde(rename = "d", with = "serde_bytes")]
22    data_envelope: Vec<u8>,
23    /// Bytes of the key envelope (the CEK encrypted under the caller's HighEntropySecret).
24    #[serde(rename = "k", with = "serde_bytes")]
25    key_envelope: Vec<u8>,
26}
27
28/// Errors returned when parsing a [`SealedOpenOrgInviteData`] from its wire form.
29#[derive(Debug, Error)]
30pub enum SealedOpenOrgInviteDataError {
31    /// The wire string could not be decoded.
32    #[error("Sealed open org invite data is malformed")]
33    Malformed,
34}
35
36impl FromStr for SealedOpenOrgInviteData {
37    type Err = SealedOpenOrgInviteDataError;
38
39    fn from_str(s: &str) -> Result<Self, Self::Err> {
40        let outer = B64Url::try_from(s).map_err(|_| SealedOpenOrgInviteDataError::Malformed)?;
41        let wire: SealedOpenOrgInviteDataWire = ciborium::de::from_reader(outer.as_bytes())
42            .map_err(|_| SealedOpenOrgInviteDataError::Malformed)?;
43        let data_envelope = DataEnvelope::from(wire.data_envelope);
44        let key_envelope = SecretProtectedKeyEnvelope::try_from(&wire.key_envelope)
45            .map_err(|_| SealedOpenOrgInviteDataError::Malformed)?;
46        Ok(SealedOpenOrgInviteData {
47            data_envelope,
48            key_envelope,
49        })
50    }
51}
52
53impl From<&SealedOpenOrgInviteData> for String {
54    fn from(val: &SealedOpenOrgInviteData) -> Self {
55        let data_bytes: Vec<u8> = (&val.data_envelope).into();
56        let key_bytes: Vec<u8> = (&val.key_envelope).into();
57        let wire = SealedOpenOrgInviteDataWire {
58            data_envelope: data_bytes,
59            key_envelope: key_bytes,
60        };
61        let mut buf = Vec::new();
62        ciborium::ser::into_writer(&wire, &mut buf)
63            .expect("CBOR encoding of two byte fields cannot fail");
64        B64Url::from(buf).to_string()
65    }
66}
67
68impl From<SealedOpenOrgInviteData> for String {
69    fn from(val: SealedOpenOrgInviteData) -> Self {
70        (&val).into()
71    }
72}
73
74impl Serialize for SealedOpenOrgInviteData {
75    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
76    where
77        S: serde::Serializer,
78    {
79        serializer.serialize_str(&String::from(self))
80    }
81}
82
83impl<'de> Deserialize<'de> for SealedOpenOrgInviteData {
84    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
85    where
86        D: serde::Deserializer<'de>,
87    {
88        deserializer.deserialize_str(FromStrVisitor::new())
89    }
90}
91
92bitwarden_ffi::impl_wire_string!(
93    SealedOpenOrgInviteData,
94    parse = |wire: String| wire.parse(),
95    format = String::from,
96);
97
98#[cfg(test)]
99mod tests {
100    use bitwarden_encoding::B64Url;
101
102    use super::*;
103    use crate::registration::open_org_invite_crypto::{OpenOrgInvite, SealedOpenOrgInviteData};
104
105    fn sample_input() -> OpenOrgInvite {
106        OpenOrgInvite {
107            organization_id: "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".to_string(),
108            invite_link_code: "abcd1234efgh5678".to_string(),
109            invite_secret: "raw-invite-secret-material-base64url".to_string(),
110        }
111    }
112
113    #[test]
114    fn sealed_data_wire_is_valid_base64url() {
115        let (sealed_data, _) =
116            SealedOpenOrgInviteData::seal(sample_input()).expect("seal should succeed");
117
118        let wire = String::from(&sealed_data);
119        let decoded =
120            B64Url::try_from(wire.as_str()).expect("sealed_data wire must be valid base64url");
121        assert_eq!(B64Url::from(decoded.as_bytes()).to_string(), wire);
122    }
123
124    #[test]
125    fn parse_rejects_truncated_wire() {
126        let (sealed_data, _) =
127            SealedOpenOrgInviteData::seal(sample_input()).expect("seal should succeed");
128
129        let mut wire = String::from(&sealed_data);
130        wire.truncate(wire.len() / 2);
131
132        let err = wire
133            .parse::<SealedOpenOrgInviteData>()
134            .expect_err("truncated wire must be rejected at parse time");
135        assert!(matches!(err, SealedOpenOrgInviteDataError::Malformed));
136    }
137
138    #[test]
139    fn parse_rejects_malformed_base64url() {
140        let err = "not-valid-base64url!"
141            .parse::<SealedOpenOrgInviteData>()
142            .expect_err("malformed base64url must be rejected at parse time");
143        assert!(matches!(err, SealedOpenOrgInviteDataError::Malformed));
144
145        assert!(B64Url::try_from("not-valid-base64url!").is_err());
146    }
147
148    #[test]
149    fn parse_rejects_valid_cbor_with_bad_key_envelope_bytes() {
150        // Well-formed base64url + CBOR wire whose `k` field bytes don't parse as a
151        // SecretProtectedKeyEnvelope — exercises the parse-envelope failure path.
152        #[derive(serde::Serialize)]
153        struct FakeWire<'a> {
154            #[serde(rename = "d", with = "serde_bytes")]
155            d: &'a [u8],
156            #[serde(rename = "k", with = "serde_bytes")]
157            k: &'a [u8],
158        }
159        let fake = FakeWire {
160            d: &[1, 2, 3, 4],       // DataEnvelope::from is an infallible byte-wrap; that's fine
161            k: &[0xff, 0xff, 0xff], // won't parse as SecretProtectedKeyEnvelope
162        };
163        let mut buf = Vec::new();
164        ciborium::ser::into_writer(&fake, &mut buf).unwrap();
165        let wire = B64Url::from(buf).to_string();
166
167        let err = wire
168            .parse::<SealedOpenOrgInviteData>()
169            .expect_err("bad key-envelope bytes must be rejected at parse time");
170        assert!(matches!(err, SealedOpenOrgInviteDataError::Malformed));
171    }
172}