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#[derive(Clone, Builder, Debug)]
14#[builder(setter(into), build_fn(error = "ConversionError"))]
15pub struct WelcomeMessage {
16 pub cursor: Cursor,
18 pub created_ns: chrono::DateTime<Utc>,
20 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 pub installation_key: InstallationId,
71 pub hpke_public_key: Vec<u8>,
73 pub wrapper_algorithm: WelcomeWrapperAlgorithm,
75 pub data: Vec<u8>,
77 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 pub installation_key: InstallationId,
92 pub hpke_public_key: Vec<u8>,
94 pub wrapper_algorithm: WelcomePointerWrapperAlgorithm,
96 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 pub destination: InstallationId,
111 pub aead_type: WelcomePointeeEncryptionAeadType,
113 pub encryption_key: Vec<u8>,
115 pub data_nonce: Vec<u8>,
117 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
173fn 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 #[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}