1use crate::types::events::DecryptFailMode;
8use wacore_binary::Node;
9use waproto::whatsapp as wa;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum EncType {
18 PreKeyMessage,
20 Message,
22 SenderKey,
24 MessageSecret,
27}
28
29impl EncType {
30 pub fn from_wire(s: &str) -> Option<Self> {
32 match s {
33 "pkmsg" => Some(Self::PreKeyMessage),
34 "msg" => Some(Self::Message),
35 "skmsg" => Some(Self::SenderKey),
36 "msmsg" => Some(Self::MessageSecret),
37 _ => None,
38 }
39 }
40
41 pub fn as_wire_str(&self) -> &'static str {
43 match self {
44 Self::PreKeyMessage => "pkmsg",
45 Self::Message => "msg",
46 Self::SenderKey => "skmsg",
47 Self::MessageSecret => "msmsg",
48 }
49 }
50
51 pub fn is_session(&self) -> bool {
54 matches!(self, Self::PreKeyMessage | Self::Message)
55 }
56
57 pub fn is_bot_secret(&self) -> bool {
60 matches!(self, Self::MessageSecret)
61 }
62}
63
64#[derive(Debug, Clone)]
66pub struct EncNodeInfo<'a> {
67 pub ciphertext: &'a [u8],
69 pub enc_type: EncType,
71 pub padding_version: u8,
73 pub retry_count: u8,
75}
76
77#[derive(Debug)]
79pub struct CategorizedEncNodes<'a> {
80 pub session_enc: Vec<EncNodeInfo<'a>>,
82 pub group_enc: Vec<EncNodeInfo<'a>>,
84 pub bot_enc: Vec<EncNodeInfo<'a>>,
87 pub max_retry_count: u8,
89 pub decrypt_fail_mode: DecryptFailMode,
91 pub unknown_enc_types: Vec<String>,
94 pub has_ordering_violation: bool,
97}
98
99use crate::protocol::retry::MAX_RETRY_COUNT as MAX_DECRYPT_RETRIES;
100
101pub fn categorize_enc_nodes<'a>(enc_nodes: &[&'a Node]) -> CategorizedEncNodes<'a> {
110 let mut session_enc = Vec::with_capacity(enc_nodes.len());
111 let mut group_enc = Vec::with_capacity(enc_nodes.len());
112 let mut bot_enc = Vec::with_capacity(enc_nodes.len());
113 let mut unknown_enc_types = Vec::new();
114 let mut max_retry_count: u8 = 0;
115 let mut has_hide_fail = false;
116
117 for &enc_node in enc_nodes {
118 let retry_count = enc_node
121 .attrs()
122 .optional_u64("count")
123 .map(|c| c.min(MAX_DECRYPT_RETRIES as u64) as u8)
124 .unwrap_or(0);
125 max_retry_count = max_retry_count.max(retry_count);
126
127 if enc_node
129 .attrs
130 .get("decrypt-fail")
131 .is_some_and(|v| v == "hide")
132 {
133 has_hide_fail = true;
134 }
135
136 let enc_type_str = match enc_node.attrs().optional_string("type") {
137 Some(t) => t,
138 None => {
139 log::warn!("Enc node missing 'type' attribute, skipping");
140 continue;
141 }
142 };
143
144 let ciphertext: &[u8] = match &enc_node.content {
145 Some(wacore_binary::NodeContent::Bytes(b)) => b,
146 _ => {
147 log::warn!("Enc node has no byte content, skipping");
148 continue;
149 }
150 };
151
152 let padding_version = enc_node.attrs().optional_u64("v").unwrap_or(2) as u8;
153
154 match EncType::from_wire(enc_type_str.as_ref()) {
155 Some(et @ (EncType::PreKeyMessage | EncType::Message)) => {
156 session_enc.push(EncNodeInfo {
157 ciphertext,
158 enc_type: et,
159 padding_version,
160 retry_count,
161 });
162 }
163 Some(EncType::SenderKey) => {
164 group_enc.push(EncNodeInfo {
165 ciphertext,
166 enc_type: EncType::SenderKey,
167 padding_version,
168 retry_count,
169 });
170 }
171 Some(EncType::MessageSecret) => {
172 bot_enc.push(EncNodeInfo {
173 ciphertext,
174 enc_type: EncType::MessageSecret,
175 padding_version,
176 retry_count,
177 });
178 }
179 None => {
180 unknown_enc_types.push(enc_type_str.to_string());
181 }
182 }
183 }
184
185 let has_ordering_violation = !session_enc.is_empty()
188 && !group_enc.is_empty()
189 && enc_nodes
190 .first()
191 .is_some_and(|n| n.attrs.get("type").is_some_and(|v| v == "skmsg"));
192
193 let decrypt_fail_mode = if has_hide_fail {
194 DecryptFailMode::Hide
195 } else {
196 DecryptFailMode::Show
197 };
198
199 CategorizedEncNodes {
200 session_enc,
201 group_enc,
202 bot_enc,
203 max_retry_count,
204 decrypt_fail_mode,
205 unknown_enc_types,
206 has_ordering_violation,
207 }
208}
209
210#[derive(Debug, Clone, Default)]
216pub struct ProtocolMessageInfo {
217 pub history_sync_notification: Option<wa::message::HistorySyncNotification>,
219 pub app_state_sync_key_share: Option<wa::message::AppStateSyncKeyShare>,
221 pub peer_data_operation_request_response:
223 Option<wa::message::PeerDataOperationRequestResponseMessage>,
224}
225
226#[derive(Debug, Clone)]
232pub struct DecryptedMessageResult {
233 pub message: wa::Message,
235 pub skdm: Option<wa::message::SenderKeyDistributionMessage>,
238 pub protocol_message: Option<ProtocolMessageInfo>,
240 pub is_skdm_only: bool,
243 pub has_invalid_dsm: bool,
246}
247
248pub fn process_decrypted_plaintext(
261 padded_plaintext: &[u8],
262 padding_version: u8,
263 is_from_me: bool,
264) -> Result<DecryptedMessageResult, anyhow::Error> {
265 let original_msg = crate::messages::decode_plaintext(padded_plaintext, padding_version)?;
266
267 let has_invalid_dsm = original_msg.device_sent_message.is_set() && !is_from_me;
269
270 let mut msg = crate::messages::unwrap_device_sent(original_msg);
272
273 let skdm = msg.sender_key_distribution_message.as_option().cloned();
275
276 let is_skdm_only = crate::messages::is_sender_key_distribution_only(&mut msg);
278
279 let protocol_message = msg
281 .protocol_message
282 .as_option()
283 .map(|pm| ProtocolMessageInfo {
284 history_sync_notification: pm.history_sync_notification.as_option().cloned(),
285 app_state_sync_key_share: pm.app_state_sync_key_share.as_option().cloned(),
286 peer_data_operation_request_response: pm
287 .peer_data_operation_request_response_message
288 .as_option()
289 .cloned(),
290 });
291
292 Ok(DecryptedMessageResult {
293 message: msg,
294 skdm,
295 protocol_message,
296 is_skdm_only,
297 has_invalid_dsm,
298 })
299}
300
301#[cfg(test)]
302#[allow(clippy::disallowed_methods)]
303mod tests {
304 use super::*;
305 use wacore_binary::{Attrs, Node, NodeContent, NodeValue};
306
307 fn make_enc_node(enc_type: &str, content: &[u8]) -> Node {
308 let mut attrs = Attrs::new();
309 attrs.insert("type", NodeValue::from(enc_type));
310 Node::new("enc", attrs, Some(NodeContent::Bytes(content.to_vec())))
311 }
312
313 fn make_enc_node_with_attrs(
314 enc_type: &str,
315 content: &[u8],
316 count: Option<u64>,
317 decrypt_fail: Option<&str>,
318 v: Option<u64>,
319 ) -> Node {
320 let mut attrs = Attrs::new();
321 attrs.insert("type", NodeValue::from(enc_type));
322 if let Some(c) = count {
323 attrs.insert("count", NodeValue::from(c.to_string()));
324 }
325 if let Some(df) = decrypt_fail {
326 attrs.insert("decrypt-fail", NodeValue::from(df));
327 }
328 if let Some(ver) = v {
329 attrs.insert("v", NodeValue::from(ver.to_string()));
330 }
331 Node::new("enc", attrs, Some(NodeContent::Bytes(content.to_vec())))
332 }
333
334 #[test]
335 fn test_categorize_empty() {
336 let result = categorize_enc_nodes(&[]);
337 assert!(result.session_enc.is_empty());
338 assert!(result.group_enc.is_empty());
339 assert!(result.bot_enc.is_empty());
340 assert_eq!(result.max_retry_count, 0);
341 assert_eq!(result.decrypt_fail_mode, DecryptFailMode::Show);
342 assert!(!result.has_ordering_violation);
343 }
344
345 #[test]
346 fn test_categorize_msmsg_goes_into_bot_bucket() {
347 let msmsg = make_enc_node("msmsg", b"bot_cipher");
348 let nodes: Vec<&Node> = vec![&msmsg];
349
350 let result = categorize_enc_nodes(&nodes);
351 assert!(result.session_enc.is_empty());
352 assert!(result.group_enc.is_empty());
353 assert_eq!(result.bot_enc.len(), 1);
354 assert_eq!(result.bot_enc[0].enc_type, EncType::MessageSecret);
355 assert_eq!(result.bot_enc[0].ciphertext, b"bot_cipher");
356 assert!(result.unknown_enc_types.is_empty());
357 }
358
359 #[test]
360 fn test_enc_type_msmsg_round_trip() {
361 assert_eq!(
362 EncType::from_wire("msmsg"),
363 Some(EncType::MessageSecret),
364 "msmsg must parse"
365 );
366 assert_eq!(EncType::MessageSecret.as_wire_str(), "msmsg");
367 assert!(
368 !EncType::MessageSecret.is_session(),
369 "msmsg is NOT a Signal session type"
370 );
371 assert!(
372 EncType::MessageSecret.is_bot_secret(),
373 "msmsg IS the bot-secret envelope"
374 );
375 for t in [EncType::PreKeyMessage, EncType::Message, EncType::SenderKey] {
376 assert!(!t.is_bot_secret(), "{t:?} must not be a bot-secret type");
377 }
378 }
379
380 #[test]
381 fn test_categorize_session_types() {
382 let pkmsg = make_enc_node("pkmsg", b"cipher1");
383 let msg = make_enc_node("msg", b"cipher2");
384 let nodes: Vec<&Node> = vec![&pkmsg, &msg];
385
386 let result = categorize_enc_nodes(&nodes);
387 assert_eq!(result.session_enc.len(), 2);
388 assert!(result.group_enc.is_empty());
389 assert_eq!(result.session_enc[0].enc_type, EncType::PreKeyMessage);
390 assert_eq!(result.session_enc[1].enc_type, EncType::Message);
391 assert_eq!(result.session_enc[0].ciphertext, b"cipher1");
392 assert_eq!(result.session_enc[1].ciphertext, b"cipher2");
393 }
394
395 #[test]
396 fn test_categorize_group_type() {
397 let skmsg = make_enc_node("skmsg", b"group_cipher");
398 let nodes: Vec<&Node> = vec![&skmsg];
399
400 let result = categorize_enc_nodes(&nodes);
401 assert!(result.session_enc.is_empty());
402 assert_eq!(result.group_enc.len(), 1);
403 assert_eq!(result.group_enc[0].enc_type, EncType::SenderKey);
404 }
405
406 #[test]
407 fn test_categorize_mixed_correct_order() {
408 let pkmsg = make_enc_node("pkmsg", b"session");
409 let skmsg = make_enc_node("skmsg", b"group");
410 let nodes: Vec<&Node> = vec![&pkmsg, &skmsg];
411
412 let result = categorize_enc_nodes(&nodes);
413 assert_eq!(result.session_enc.len(), 1);
414 assert_eq!(result.group_enc.len(), 1);
415 assert!(!result.has_ordering_violation);
416 }
417
418 #[test]
419 fn test_categorize_ordering_violation() {
420 let skmsg = make_enc_node("skmsg", b"group");
421 let pkmsg = make_enc_node("pkmsg", b"session");
422 let nodes: Vec<&Node> = vec![&skmsg, &pkmsg];
423
424 let result = categorize_enc_nodes(&nodes);
425 assert!(result.has_ordering_violation);
426 }
427
428 #[test]
429 fn test_categorize_retry_count() {
430 let node1 = make_enc_node_with_attrs("msg", b"c1", Some(2), None, None);
431 let node2 = make_enc_node_with_attrs("msg", b"c2", Some(4), None, None);
432 let nodes: Vec<&Node> = vec![&node1, &node2];
433
434 let result = categorize_enc_nodes(&nodes);
435 assert_eq!(result.max_retry_count, 4);
436 }
437
438 #[test]
439 fn test_categorize_retry_count_clamped() {
440 let node = make_enc_node_with_attrs("msg", b"c", Some(100), None, None);
441 let nodes: Vec<&Node> = vec![&node];
442
443 let result = categorize_enc_nodes(&nodes);
444 assert_eq!(result.max_retry_count, MAX_DECRYPT_RETRIES);
445 }
446
447 #[test]
448 fn test_categorize_decrypt_fail_hide() {
449 let node = make_enc_node_with_attrs("msg", b"c", None, Some("hide"), None);
450 let nodes: Vec<&Node> = vec![&node];
451
452 let result = categorize_enc_nodes(&nodes);
453 assert_eq!(result.decrypt_fail_mode, DecryptFailMode::Hide);
454 }
455
456 #[test]
457 fn test_categorize_decrypt_fail_show_default() {
458 let node = make_enc_node_with_attrs("msg", b"c", None, Some("show"), None);
459 let nodes: Vec<&Node> = vec![&node];
460
461 let result = categorize_enc_nodes(&nodes);
462 assert_eq!(result.decrypt_fail_mode, DecryptFailMode::Show);
463 }
464
465 #[test]
466 fn test_categorize_padding_version() {
467 let node = make_enc_node_with_attrs("msg", b"c", None, None, Some(3));
468 let nodes: Vec<&Node> = vec![&node];
469
470 let result = categorize_enc_nodes(&nodes);
471 assert_eq!(result.session_enc[0].padding_version, 3);
472 }
473
474 #[test]
475 fn test_categorize_padding_version_default() {
476 let node = make_enc_node("msg", b"c");
477 let nodes: Vec<&Node> = vec![&node];
478
479 let result = categorize_enc_nodes(&nodes);
480 assert_eq!(result.session_enc[0].padding_version, 2);
481 }
482
483 #[test]
484 fn test_categorize_unknown_type() {
485 let node = make_enc_node("frskmsg", b"custom");
486 let nodes: Vec<&Node> = vec![&node];
487
488 let result = categorize_enc_nodes(&nodes);
489 assert!(result.session_enc.is_empty());
490 assert!(result.group_enc.is_empty());
491 assert_eq!(result.unknown_enc_types, vec!["frskmsg"]);
492 }
493
494 #[test]
495 fn test_categorize_missing_content() {
496 let mut attrs = Attrs::new();
497 attrs.insert("type", NodeValue::from("msg"));
498 let node = Node::new("enc", attrs, None);
499 let nodes: Vec<&Node> = vec![&node];
500
501 let result = categorize_enc_nodes(&nodes);
502 assert!(result.session_enc.is_empty());
503 }
504
505 #[test]
506 fn test_enc_type_wire_roundtrip() {
507 for wire in &["pkmsg", "msg", "skmsg"] {
508 let et = EncType::from_wire(wire).unwrap();
509 assert_eq!(et.as_wire_str(), *wire);
510 }
511 assert!(EncType::from_wire("unknown").is_none());
512 }
513
514 #[test]
515 fn test_enc_type_is_session() {
516 assert!(EncType::PreKeyMessage.is_session());
517 assert!(EncType::Message.is_session());
518 assert!(!EncType::SenderKey.is_session());
519 }
520
521 #[test]
522 fn test_process_decrypted_plaintext_simple() {
523 use buffa::Message as ProtoMessage;
524
525 let msg = wa::Message {
527 conversation: Some("hello".to_string()),
528 ..Default::default()
529 };
530 let plaintext = msg.encode_to_vec();
531 let padded = crate::messages::MessageUtils::pad_message_v2(plaintext);
532
533 let result = process_decrypted_plaintext(&padded, 2, false).unwrap();
534 assert_eq!(result.message.conversation.as_deref(), Some("hello"));
535 assert!(result.skdm.is_none());
536 assert!(result.protocol_message.is_none());
537 assert!(!result.is_skdm_only);
538 assert!(!result.has_invalid_dsm);
539 }
540
541 #[test]
542 fn test_process_decrypted_plaintext_with_skdm() {
543 use buffa::Message as ProtoMessage;
544
545 let msg = wa::Message {
546 conversation: Some("hello".to_string()),
547 sender_key_distribution_message: buffa::MessageField::some(
548 wa::message::SenderKeyDistributionMessage {
549 group_id: Some("group@g.us".to_string()),
550 axolotl_sender_key_distribution_message: Some(vec![1, 2, 3]),
551 },
552 ),
553 ..Default::default()
554 };
555 let plaintext = msg.encode_to_vec();
556 let padded = crate::messages::MessageUtils::pad_message_v2(plaintext);
557
558 let result = process_decrypted_plaintext(&padded, 2, false).unwrap();
559 assert!(result.skdm.is_some());
560 assert!(!result.is_skdm_only); }
562
563 #[test]
564 fn test_process_decrypted_plaintext_skdm_only() {
565 use buffa::Message as ProtoMessage;
566
567 let msg = wa::Message {
568 sender_key_distribution_message: buffa::MessageField::some(
569 wa::message::SenderKeyDistributionMessage {
570 group_id: Some("group@g.us".to_string()),
571 axolotl_sender_key_distribution_message: Some(vec![1, 2, 3]),
572 },
573 ),
574 ..Default::default()
575 };
576 let plaintext = msg.encode_to_vec();
577 let padded = crate::messages::MessageUtils::pad_message_v2(plaintext);
578
579 let result = process_decrypted_plaintext(&padded, 2, false).unwrap();
580 assert!(result.skdm.is_some());
581 assert!(result.is_skdm_only);
582 }
583
584 #[test]
585 fn test_process_decrypted_plaintext_invalid_dsm() {
586 use buffa::Message as ProtoMessage;
587
588 let msg = wa::Message {
589 device_sent_message: buffa::MessageField::some(wa::message::DeviceSentMessage {
590 message: buffa::MessageField::some(wa::Message {
591 conversation: Some("inner".to_string()),
592 ..Default::default()
593 }),
594 ..Default::default()
595 }),
596 ..Default::default()
597 };
598 let plaintext = msg.encode_to_vec();
599 let padded = crate::messages::MessageUtils::pad_message_v2(plaintext);
600
601 let result = process_decrypted_plaintext(&padded, 2, false).unwrap();
603 assert!(result.has_invalid_dsm);
604 assert_eq!(result.message.conversation.as_deref(), Some("inner"));
606 }
607
608 #[test]
609 fn test_process_decrypted_plaintext_valid_dsm() {
610 use buffa::Message as ProtoMessage;
611
612 let msg = wa::Message {
613 device_sent_message: buffa::MessageField::some(wa::message::DeviceSentMessage {
614 message: buffa::MessageField::some(wa::Message {
615 conversation: Some("self-sent".to_string()),
616 ..Default::default()
617 }),
618 ..Default::default()
619 }),
620 ..Default::default()
621 };
622 let plaintext = msg.encode_to_vec();
623 let padded = crate::messages::MessageUtils::pad_message_v2(plaintext);
624
625 let result = process_decrypted_plaintext(&padded, 2, true).unwrap();
627 assert!(!result.has_invalid_dsm);
628 assert_eq!(result.message.conversation.as_deref(), Some("self-sent"));
629 }
630}