Skip to main content

xmtp_proto/types/
welcome_message.rs

1use crate::types::{Cursor, InstallationId};
2use crate::{
3    ConversionError,
4    xmtp::mls::message_contents::{
5        WelcomePointeeEncryptionAeadType, WelcomePointerWrapperAlgorithm, WelcomeWrapperAlgorithm,
6    },
7};
8use chrono::Utc;
9use derive_builder::Builder;
10use prost::Message;
11
12/// Welcome Message from the network
13#[derive(Clone, Builder, Debug)]
14#[builder(setter(into), build_fn(error = "ConversionError"))]
15pub struct WelcomeMessage {
16    /// cursor of this message
17    pub cursor: Cursor,
18    /// server timestamp indicating when this message was created
19    pub created_ns: chrono::DateTime<Utc>,
20    /// Variant of the welcome message
21    pub variant: WelcomeMessageType,
22}
23
24impl WelcomeMessage {
25    pub fn builder() -> WelcomeMessageBuilder {
26        WelcomeMessageBuilder::default()
27    }
28    pub fn as_v1(&self) -> Option<&WelcomeMessageV1> {
29        match &self.variant {
30            WelcomeMessageType::V1(v1) => Some(v1),
31            _ => None,
32        }
33    }
34}
35
36impl WelcomeMessage {
37    pub fn sequence_id(&self) -> u64 {
38        self.cursor.0
39    }
40
41    pub fn timestamp(&self) -> i64 {
42        self.created_ns
43            .timestamp_nanos_opt()
44            .expect("timestamp out of range for i64, are we in 2262 A.D?")
45    }
46}
47
48#[derive(Clone, Debug)]
49pub enum WelcomeMessageType {
50    V1(WelcomeMessageV1),
51    WelcomePointer(WelcomePointer),
52}
53
54impl From<WelcomeMessageV1> for WelcomeMessageType {
55    fn from(v1: WelcomeMessageV1) -> Self {
56        WelcomeMessageType::V1(v1)
57    }
58}
59
60impl From<WelcomePointer> for WelcomeMessageType {
61    fn from(pointer: WelcomePointer) -> Self {
62        WelcomeMessageType::WelcomePointer(pointer)
63    }
64}
65
66#[derive(Clone, Builder, Debug)]
67#[builder(build_fn(error = "ConversionError"))]
68pub struct WelcomeMessageV1 {
69    // Installation key the welcome was sent to
70    pub installation_key: InstallationId,
71    // HPKE public key used to encrypt the welcome
72    pub hpke_public_key: Vec<u8>,
73    // Wrapper algorithm used to encrypt the welcome
74    pub wrapper_algorithm: WelcomeWrapperAlgorithm,
75    // Encrypted welcome message payload
76    pub data: Vec<u8>,
77    // Encrypted welcome metadata
78    pub welcome_metadata: Vec<u8>,
79}
80
81impl WelcomeMessageV1 {
82    pub fn builder() -> WelcomeMessageV1Builder {
83        WelcomeMessageV1Builder::default()
84    }
85}
86
87#[derive(Clone, Builder, Debug)]
88#[builder(build_fn(error = "ConversionError"))]
89pub struct WelcomePointer {
90    // Installation key the welcome pointer was sent to
91    pub installation_key: InstallationId,
92    // HPKE public key used to encrypt the welcome pointer
93    pub hpke_public_key: Vec<u8>,
94    // Wrapper algorithm used to encrypt the welcome pointer (Only post quantum compatible algorithms are allowed)
95    pub wrapper_algorithm: WelcomePointerWrapperAlgorithm,
96    // Encrypted welcome pointer data
97    pub welcome_pointer: Vec<u8>,
98}
99
100impl WelcomePointer {
101    pub fn builder() -> WelcomePointerBuilder {
102        WelcomePointerBuilder::default()
103    }
104}
105
106#[derive(Clone, Builder, Debug)]
107#[builder(build_fn(error = "ConversionError"))]
108pub struct DecryptedWelcomePointer {
109    // Topic the welcome pointee was sent to
110    pub destination: InstallationId,
111    // AEAD type used to encrypt the welcome pointee
112    pub aead_type: WelcomePointeeEncryptionAeadType,
113    // Encryption key used to encrypt the welcome pointee. Length MUST match the aead_type.
114    pub encryption_key: Vec<u8>,
115    // Nonce used to encrypt the welcome pointee data. Length MUST match the aead_type.
116    pub data_nonce: Vec<u8>,
117    // Nonce used to encrypt the welcome pointee metadata. Length MUST match the aead_type.
118    pub welcome_metadata_nonce: Vec<u8>,
119}
120
121impl DecryptedWelcomePointer {
122    pub fn builder() -> DecryptedWelcomePointerBuilder {
123        DecryptedWelcomePointerBuilder::default()
124    }
125    pub fn decode(data: &[u8]) -> Result<Self, ConversionError> {
126        crate::xmtp::mls::message_contents::WelcomePointer::decode(data)?.try_into()
127    }
128    pub fn to_proto(self) -> crate::xmtp::mls::message_contents::WelcomePointer {
129        crate::xmtp::mls::message_contents::WelcomePointer {
130            version: Some(
131                crate::xmtp::mls::message_contents::welcome_pointer::Version::WelcomeV1Pointer(
132                    crate::xmtp::mls::message_contents::welcome_pointer::WelcomeV1Pointer {
133                        destination: self.destination.to_vec(),
134                        aead_type: self.aead_type.into(),
135                        encryption_key: self.encryption_key,
136                        data_nonce: self.data_nonce,
137                        welcome_metadata_nonce: self.welcome_metadata_nonce,
138                    },
139                ),
140            ),
141        }
142    }
143}
144
145impl TryFrom<crate::xmtp::mls::message_contents::WelcomePointer> for DecryptedWelcomePointer {
146    type Error = ConversionError;
147    fn try_from(
148        value: crate::xmtp::mls::message_contents::WelcomePointer,
149    ) -> Result<Self, Self::Error> {
150        let wp = match value.version {
151            Some(
152                crate::xmtp::mls::message_contents::welcome_pointer::Version::WelcomeV1Pointer(v1),
153            ) => v1,
154            None => {
155                return Err(ConversionError::InvalidValue {
156                    item: "WelcomePointer",
157                    expected: "WelcomeV1Pointer",
158                    got: "None".into(),
159                });
160            }
161        };
162        validate_v1(&wp)?;
163        Ok(Self {
164            destination: wp.destination.try_into()?,
165            aead_type: wp.aead_type.try_into()?,
166            encryption_key: wp.encryption_key,
167            data_nonce: wp.data_nonce,
168            welcome_metadata_nonce: wp.welcome_metadata_nonce,
169        })
170    }
171}
172
173/// Accept only a pointer that ChaCha20-Poly1305 can open: its `aead_type` is
174/// ChaCha20-Poly1305, its key is `WELCOME_POINTEE_KEY_BYTES`, and its two
175/// nonces are different `WELCOME_POINTEE_NONCE_BYTES` values. Any other pointer
176/// can never open a pointee, so it is invalid input, not a pointer to hold.
177// implements: JOIN-094
178fn validate_v1(
179    pointer: &crate::xmtp::mls::message_contents::welcome_pointer::WelcomeV1Pointer,
180) -> Result<(), ConversionError> {
181    use xmtp_configuration::{WELCOME_POINTEE_KEY_BYTES, WELCOME_POINTEE_NONCE_BYTES};
182
183    let aead_type = WelcomePointeeEncryptionAeadType::try_from(pointer.aead_type)?;
184    if aead_type != WelcomePointeeEncryptionAeadType::Chacha20Poly1305 {
185        return Err(ConversionError::InvalidValue {
186            item: "WelcomeV1Pointer.aead_type",
187            expected: "Chacha20Poly1305",
188            got: format!("{aead_type:?}"),
189        });
190    }
191    for (item, bytes, expected) in [
192        (
193            "WelcomeV1Pointer.encryption_key",
194            &pointer.encryption_key,
195            WELCOME_POINTEE_KEY_BYTES,
196        ),
197        (
198            "WelcomeV1Pointer.data_nonce",
199            &pointer.data_nonce,
200            WELCOME_POINTEE_NONCE_BYTES,
201        ),
202        (
203            "WelcomeV1Pointer.welcome_metadata_nonce",
204            &pointer.welcome_metadata_nonce,
205            WELCOME_POINTEE_NONCE_BYTES,
206        ),
207    ] {
208        if bytes.len() != expected {
209            return Err(ConversionError::InvalidLength {
210                item,
211                expected,
212                got: bytes.len(),
213            });
214        }
215    }
216    if pointer.data_nonce == pointer.welcome_metadata_nonce {
217        return Err(ConversionError::InvalidValue {
218            item: "WelcomeV1Pointer.welcome_metadata_nonce",
219            expected: "a nonce other than data_nonce",
220            got: "data_nonce".into(),
221        });
222    }
223    Ok(())
224}
225
226#[cfg(any(test, feature = "test-utils"))]
227impl xmtp_common::Generate for WelcomeMessage {
228    fn generate() -> Self {
229        Self {
230            cursor: Cursor::generate(),
231            created_ns: chrono::DateTime::from_timestamp_nanos(xmtp_common::rand_i64()),
232            variant: WelcomeMessageV1 {
233                installation_key: xmtp_common::rand_array::<32>().into(),
234                data: xmtp_common::rand_vec::<16>(),
235                hpke_public_key: xmtp_common::rand_vec::<16>(),
236                wrapper_algorithm: WelcomeWrapperAlgorithm::Curve25519,
237                welcome_metadata: xmtp_common::rand_vec::<16>(),
238            }
239            .into(),
240        }
241    }
242}
243
244#[cfg(test)]
245mod test {
246    use super::*;
247    use crate::xmtp::mls::message_contents::{
248        WelcomePointer as WelcomePointerProto, welcome_pointer,
249    };
250    use rstest::rstest;
251    use xmtp_common::Generate;
252    use xmtp_configuration::{WELCOME_POINTEE_KEY_BYTES, WELCOME_POINTEE_NONCE_BYTES};
253
254    #[rstest]
255    #[case(Cursor(123), 123)]
256    #[case(Cursor(0), 0)]
257    #[case(Cursor(u64::MAX), u64::MAX)]
258    #[xmtp_common::test]
259    async fn test_accessor_methods(#[case] cursor: Cursor, #[case] expected_seq: u64) {
260        use xmtp_common::Generate;
261
262        let mut welcome_message = WelcomeMessage::generate();
263        welcome_message.cursor = cursor;
264        assert_eq!(welcome_message.sequence_id(), expected_seq);
265    }
266
267    fn pointer_proto(
268        aead_type: WelcomePointeeEncryptionAeadType,
269        key_bytes: usize,
270        data_nonce: Vec<u8>,
271        welcome_metadata_nonce: Vec<u8>,
272    ) -> WelcomePointerProto {
273        WelcomePointerProto {
274            version: Some(welcome_pointer::Version::WelcomeV1Pointer(
275                welcome_pointer::WelcomeV1Pointer {
276                    destination: vec![1; 32],
277                    aead_type: aead_type.into(),
278                    encryption_key: vec![2; key_bytes],
279                    data_nonce,
280                    welcome_metadata_nonce,
281                },
282            )),
283        }
284    }
285
286    /// Only a pointer that ChaCha20-Poly1305 can open decodes, on both
287    /// construction paths.
288    // verifies: JOIN-094
289    #[xmtp_common::test]
290    fn decode_rejects_malformed_pointer_fields() {
291        use WelcomePointeeEncryptionAeadType::{Chacha20Poly1305, Unspecified};
292        let nonce = |byte| vec![byte; WELCOME_POINTEE_NONCE_BYTES];
293        let key = WELCOME_POINTEE_KEY_BYTES;
294
295        let valid = pointer_proto(Chacha20Poly1305, key, nonce(3), nonce(4));
296        assert!(DecryptedWelcomePointer::decode(&valid.encode_to_vec()).is_ok());
297        assert!(DecryptedWelcomePointer::try_from(valid).is_ok());
298
299        for malformed in [
300            pointer_proto(Chacha20Poly1305, 31, nonce(3), nonce(4)),
301            pointer_proto(Chacha20Poly1305, 33, nonce(3), nonce(4)),
302            pointer_proto(Chacha20Poly1305, 64 * 1024, nonce(3), nonce(4)),
303            pointer_proto(Chacha20Poly1305, key, vec![3; 11], nonce(4)),
304            pointer_proto(Chacha20Poly1305, key, nonce(3), vec![4; 13]),
305            pointer_proto(Chacha20Poly1305, key, nonce(3), nonce(3)),
306            pointer_proto(Unspecified, key, nonce(3), nonce(4)),
307        ] {
308            assert!(DecryptedWelcomePointer::decode(&malformed.encode_to_vec()).is_err());
309            assert!(DecryptedWelcomePointer::try_from(malformed).is_err());
310        }
311    }
312
313    #[xmtp_common::test]
314    async fn test_timestamp() {
315        let test_time = chrono::Utc::now();
316        let mut welcome_message = WelcomeMessage::generate();
317        welcome_message.created_ns = test_time;
318        assert_eq!(
319            welcome_message.timestamp(),
320            test_time.timestamp_nanos_opt().unwrap()
321        );
322    }
323}