1use crate::StringEnum;
23use crate::libsignal::protocol::{KeyPair, PublicKey};
24use aes::cipher::{KeyIvInit, StreamCipher};
25use aes_gcm::Aes256Gcm;
26use aes_gcm::aead::{Aead, KeyInit};
27use ctr::Ctr128BE;
28use hkdf::Hkdf;
29use rand::RngExt;
30use sha2::Sha256;
31use wacore_binary::builder::NodeBuilder;
32use wacore_binary::jid::SERVER_JID;
33use wacore_binary::node::{Node, NodeContent};
34
35type Aes256Ctr = Ctr128BE<aes::Aes256>;
37
38const PAIR_CODE_PBKDF2_ITERATIONS: u32 = 131_072;
41
42const PAIR_CODE_SALT_SIZE: usize = 32;
44
45const PAIR_CODE_IV_SIZE: usize = 16;
47
48const CROCKFORD_ALPHABET: &[u8; 32] = b"123456789ABCDEFGHJKLMNPQRSTVWXYZ";
51
52const PAIR_CODE_VALIDITY_SECS: u64 = 180;
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, StringEnum)]
58#[repr(u8)]
59pub enum PlatformId {
60 #[str = "0"]
61 Unknown = 0,
62 #[string_default]
63 #[str = "1"]
64 Chrome = 1,
65 #[str = "2"]
66 Firefox = 2,
67 #[str = "3"]
68 InternetExplorer = 3,
69 #[str = "4"]
70 Opera = 4,
71 #[str = "5"]
72 Safari = 5,
73 #[str = "6"]
74 Edge = 6,
75 #[str = "7"]
76 Electron = 7,
77 #[str = "8"]
78 Uwp = 8,
79 #[str = "9"]
80 OtherWebClient = 9,
81}
82
83#[derive(Debug, Clone)]
85pub struct PairCodeOptions {
86 pub phone_number: String,
88 pub show_push_notification: bool,
90 pub custom_code: Option<String>,
92 pub platform_id: PlatformId,
94 pub platform_display: String,
96}
97
98impl Default for PairCodeOptions {
99 fn default() -> Self {
100 Self {
101 phone_number: String::new(),
102 show_push_notification: true,
103 custom_code: None,
104 platform_id: PlatformId::Chrome,
105 platform_display: "Chrome (Linux)".to_string(),
106 }
107 }
108}
109
110#[derive(Default)]
112pub enum PairCodeState {
113 #[default]
115 Idle,
116 WaitingForPhoneConfirmation {
118 pairing_ref: Vec<u8>,
120 phone_jid: String,
122 pair_code: String,
124 ephemeral_keypair: Box<KeyPair>,
126 },
127 Completed,
129}
130
131impl std::fmt::Debug for PairCodeState {
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 match self {
134 Self::Idle => write!(f, "Idle"),
135 Self::WaitingForPhoneConfirmation { phone_jid, .. } => f
136 .debug_struct("WaitingForPhoneConfirmation")
137 .field("phone_jid", phone_jid)
138 .finish_non_exhaustive(),
139 Self::Completed => write!(f, "Completed"),
140 }
141 }
142}
143
144pub struct PairCodeUtils;
148
149impl PairCodeUtils {
150 pub fn generate_code() -> String {
155 let mut bytes = [0u8; 5];
156 rand::make_rng::<rand::rngs::StdRng>().fill(&mut bytes);
157 Self::encode_crockford(&bytes)
158 }
159
160 pub fn validate_code(code: &str) -> bool {
165 code.len() == 8
166 && code
167 .bytes()
168 .all(|b| CROCKFORD_ALPHABET.contains(&b.to_ascii_uppercase()))
169 }
170
171 fn encode_crockford(bytes: &[u8; 5]) -> String {
175 let mut accumulator: u64 = 0;
177 for &byte in bytes {
178 accumulator = (accumulator << 8) | u64::from(byte);
179 }
180
181 let mut result = String::with_capacity(8);
183 for i in (0..8).rev() {
184 let index = ((accumulator >> (i * 5)) & 0x1F) as usize;
185 result.push(CROCKFORD_ALPHABET[index] as char);
186 }
187 result
188 }
189
190 pub fn derive_key(code: &str, salt: &[u8; PAIR_CODE_SALT_SIZE]) -> [u8; 32] {
195 let mut key = [0u8; 32];
196 pbkdf2::pbkdf2_hmac::<Sha256>(code.as_bytes(), salt, PAIR_CODE_PBKDF2_ITERATIONS, &mut key);
197 key
198 }
199
200 pub fn encrypt_ephemeral_pub(ephemeral_pub: &[u8; 32], code: &str) -> [u8; 80] {
204 let mut salt = [0u8; PAIR_CODE_SALT_SIZE];
206 let mut iv = [0u8; PAIR_CODE_IV_SIZE];
207 rand::make_rng::<rand::rngs::StdRng>().fill(&mut salt);
208 rand::make_rng::<rand::rngs::StdRng>().fill(&mut iv);
209
210 let key = Self::derive_key(code, &salt);
212 let mut cipher = Aes256Ctr::new(&key.into(), &iv.into());
213 let mut ciphertext = *ephemeral_pub;
214 cipher.apply_keystream(&mut ciphertext);
215
216 let mut result = [0u8; 80];
218 result[..32].copy_from_slice(&salt);
219 result[32..48].copy_from_slice(&iv);
220 result[48..80].copy_from_slice(&ciphertext);
221
222 result
223 }
224
225 pub fn decrypt_primary_ephemeral_pub(
235 wrapped: &[u8],
236 pair_code: &str,
237 ) -> Result<[u8; 32], PairCodeError> {
238 if wrapped.len() != 80 {
239 return Err(PairCodeError::InvalidWrappedData {
240 expected: 80,
241 got: wrapped.len(),
242 });
243 }
244
245 let salt: [u8; PAIR_CODE_SALT_SIZE] = wrapped[0..32]
247 .try_into()
248 .expect("salt slice is exactly 32 bytes");
249 let iv: [u8; PAIR_CODE_IV_SIZE] = wrapped[32..48]
250 .try_into()
251 .expect("iv slice is exactly 16 bytes");
252 let mut plaintext: [u8; 32] = wrapped[48..80]
253 .try_into()
254 .expect("ciphertext slice is exactly 32 bytes");
255
256 let derived_key = Self::derive_key(pair_code, &salt);
258
259 let mut cipher = Aes256Ctr::new((&derived_key).into(), &iv.into());
261 cipher.apply_keystream(&mut plaintext);
262
263 Ok(plaintext)
264 }
265
266 pub fn build_companion_hello_iq(
268 phone_number: &str,
269 noise_static_pub: &[u8; 32],
270 wrapped_ephemeral: &[u8; 80],
271 platform_id: PlatformId,
272 platform_display: &str,
273 show_push_notification: bool,
274 req_id: String,
275 ) -> Node {
276 let link_code_reg = NodeBuilder::new("link_code_companion_reg")
277 .attrs([
278 ("jid", format!("{}@s.whatsapp.net", phone_number)),
279 ("stage", "companion_hello".to_string()),
280 (
281 "should_show_push_notification",
282 show_push_notification.to_string(),
283 ),
284 ])
285 .children([
286 NodeBuilder::new("link_code_pairing_wrapped_companion_ephemeral_pub")
287 .bytes(wrapped_ephemeral.to_vec())
288 .build(),
289 NodeBuilder::new("companion_server_auth_key_pub")
290 .bytes(noise_static_pub.to_vec())
291 .build(),
292 NodeBuilder::new("companion_platform_id")
293 .bytes(platform_id.as_str().as_bytes().to_vec())
294 .build(),
295 NodeBuilder::new("companion_platform_display")
296 .bytes(platform_display.as_bytes().to_vec())
297 .build(),
298 NodeBuilder::new("link_code_pairing_nonce")
300 .bytes(b"0".to_vec())
301 .build(),
302 ])
303 .build();
304
305 NodeBuilder::new("iq")
306 .attrs([
307 ("xmlns", "md".to_string()),
308 ("type", "set".to_string()),
309 ("to", SERVER_JID.to_string()),
310 ("id", req_id),
311 ])
312 .children([link_code_reg])
313 .build()
314 }
315
316 pub fn parse_companion_hello_response(node: &Node) -> Option<Vec<u8>> {
318 node.get_optional_child_by_tag(&["link_code_companion_reg"])
319 .and_then(|n| n.get_optional_child_by_tag(&["link_code_pairing_ref"]))
320 .and_then(|n| n.content.as_ref())
321 .and_then(|c| match c {
322 NodeContent::Bytes(b) => Some(b.clone()),
323 _ => None,
324 })
325 }
326
327 pub fn build_companion_finish_iq(
329 phone_number: &str,
330 wrapped_key_bundle: Vec<u8>,
331 identity_pub: &[u8; 32],
332 pairing_ref: &[u8],
333 req_id: String,
334 ) -> Node {
335 let link_code_reg = NodeBuilder::new("link_code_companion_reg")
336 .attrs([
337 ("jid", format!("{}@s.whatsapp.net", phone_number)),
338 ("stage", "companion_finish".to_string()),
339 ])
340 .children([
341 NodeBuilder::new("link_code_pairing_wrapped_key_bundle")
342 .bytes(wrapped_key_bundle)
343 .build(),
344 NodeBuilder::new("companion_identity_public")
345 .bytes(identity_pub.to_vec())
346 .build(),
347 NodeBuilder::new("link_code_pairing_ref")
348 .bytes(pairing_ref.to_vec())
349 .build(),
350 ])
351 .build();
352
353 NodeBuilder::new("iq")
354 .attrs([
355 ("xmlns", "md".to_string()),
356 ("type", "set".to_string()),
357 ("to", SERVER_JID.to_string()),
358 ("id", req_id),
359 ])
360 .children([link_code_reg])
361 .build()
362 }
363
364 pub fn prepare_key_bundle(
375 ephemeral_keypair: &KeyPair,
376 primary_ephemeral_pub: &[u8; 32],
377 primary_identity_pub: &[u8; 32],
378 identity_key: &KeyPair,
379 ) -> Result<(Vec<u8>, [u8; 32]), PairCodeError> {
380 let primary_eph_pub =
382 PublicKey::from_djb_public_key_bytes(primary_ephemeral_pub).map_err(|e| {
383 PairCodeError::CryptoError(format!("Invalid primary ephemeral key: {e}"))
384 })?;
385
386 let primary_id_pub =
388 PublicKey::from_djb_public_key_bytes(primary_identity_pub).map_err(|e| {
389 PairCodeError::CryptoError(format!("Invalid primary identity key: {e}"))
390 })?;
391
392 let ephemeral_shared = ephemeral_keypair
394 .private_key
395 .calculate_agreement(&primary_eph_pub)
396 .map_err(|e| PairCodeError::CryptoError(format!("Ephemeral DH failed: {e}")))?;
397
398 let identity_shared = identity_key
400 .private_key
401 .calculate_agreement(&primary_id_pub)
402 .map_err(|e| PairCodeError::CryptoError(format!("Identity DH failed: {e}")))?;
403
404 let mut random_bytes = [0u8; 32];
406 rand::make_rng::<rand::rngs::StdRng>().fill(&mut random_bytes);
407
408 let mut combined_secret = Vec::with_capacity(96);
411 combined_secret.extend_from_slice(&ephemeral_shared);
412 combined_secret.extend_from_slice(&identity_shared);
413 combined_secret.extend_from_slice(&random_bytes);
414
415 let hk_adv = Hkdf::<Sha256>::new(None, &combined_secret);
416 let mut new_adv_secret = [0u8; 32];
417 hk_adv
418 .expand(b"adv_secret", &mut new_adv_secret)
419 .map_err(|_| PairCodeError::CryptoError("HKDF expand for adv_secret failed".into()))?;
420
421 let mut bundle = Vec::with_capacity(96);
423 bundle.extend_from_slice(identity_key.public_key.public_key_bytes());
424 bundle.extend_from_slice(primary_identity_pub);
425 bundle.extend_from_slice(&random_bytes);
426
427 let mut key_bundle_salt = [0u8; 32];
429 rand::make_rng::<rand::rngs::StdRng>().fill(&mut key_bundle_salt);
430
431 let hk_bundle = Hkdf::<Sha256>::new(Some(&key_bundle_salt), &ephemeral_shared);
434 let mut enc_key = [0u8; 32];
435 hk_bundle
436 .expand(b"link_code_pairing_key_bundle_encryption_key", &mut enc_key)
437 .map_err(|_| {
438 PairCodeError::CryptoError("HKDF expand for bundle encryption key failed".into())
439 })?;
440
441 let mut iv = [0u8; 12];
443 rand::make_rng::<rand::rngs::StdRng>().fill(&mut iv);
444
445 let cipher = Aes256Gcm::new_from_slice(&enc_key)
447 .map_err(|e| PairCodeError::CryptoError(format!("AES-GCM init failed: {e}")))?;
448 let nonce = aes_gcm::Nonce::from_slice(&iv);
449 let encrypted_bundle = cipher
450 .encrypt(nonce, bundle.as_slice())
451 .map_err(|e| PairCodeError::CryptoError(format!("AES-GCM encryption failed: {e}")))?;
452
453 let mut wrapped_bundle = Vec::with_capacity(32 + 12 + encrypted_bundle.len());
455 wrapped_bundle.extend_from_slice(&key_bundle_salt);
456 wrapped_bundle.extend_from_slice(&iv);
457 wrapped_bundle.extend_from_slice(&encrypted_bundle);
458
459 Ok((wrapped_bundle, new_adv_secret))
460 }
461
462 pub fn code_validity() -> std::time::Duration {
464 std::time::Duration::from_secs(PAIR_CODE_VALIDITY_SECS)
465 }
466}
467
468#[derive(Debug, thiserror::Error)]
470pub enum PairCodeError {
471 #[error("Phone number is required")]
472 PhoneNumberRequired,
473
474 #[error("Phone number is too short (must be at least 7 digits)")]
475 PhoneNumberTooShort,
476
477 #[error("Phone number must not start with 0 (use international format)")]
478 PhoneNumberNotInternational,
479
480 #[error("Invalid custom code: must be 8 characters from Crockford Base32 alphabet")]
481 InvalidCustomCode,
482
483 #[error("Invalid wrapped data: expected {expected} bytes, got {got}")]
484 InvalidWrappedData { expected: usize, got: usize },
485
486 #[error("Cryptographic operation failed: {0}")]
487 CryptoError(String),
488
489 #[error("Not in waiting state for pair code notification")]
490 NotWaiting,
491
492 #[error("Server response missing pairing ref")]
493 MissingPairingRef,
494
495 #[error("Request failed: {0}")]
496 RequestFailed(String),
497}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502
503 #[test]
504 fn test_generate_code() {
505 let code = PairCodeUtils::generate_code();
506 assert_eq!(code.len(), 8);
507 assert!(PairCodeUtils::validate_code(&code));
508 }
509
510 #[test]
511 fn test_validate_code_valid() {
512 assert!(PairCodeUtils::validate_code("ABCD1234"));
513 assert!(PairCodeUtils::validate_code("12345678"));
514 assert!(PairCodeUtils::validate_code("VWXYZ123"));
515 }
516
517 #[test]
518 fn test_validate_code_invalid() {
519 assert!(!PairCodeUtils::validate_code("ABC1234"));
521 assert!(!PairCodeUtils::validate_code("ABCD12345"));
523 assert!(!PairCodeUtils::validate_code("ABCD0123")); assert!(!PairCodeUtils::validate_code("ABCDOIJK")); assert!(!PairCodeUtils::validate_code("ABCDIJKL")); }
528
529 #[test]
530 fn test_encode_crockford() {
531 let zeros = [0u8; 5];
533 let encoded = PairCodeUtils::encode_crockford(&zeros);
534 assert_eq!(encoded, "11111111");
535
536 let ones = [0xFFu8; 5];
538 let encoded = PairCodeUtils::encode_crockford(&ones);
539 assert_eq!(encoded, "ZZZZZZZZ");
540 }
541
542 #[test]
543 fn test_derive_key_deterministic() {
544 let salt = [0u8; 32];
545 let key1 = PairCodeUtils::derive_key("ABCD1234", &salt);
546 let key2 = PairCodeUtils::derive_key("ABCD1234", &salt);
547 assert_eq!(key1, key2);
548
549 let key3 = PairCodeUtils::derive_key("WXYZ5678", &salt);
551 assert_ne!(key1, key3);
552 }
553
554 #[test]
555 fn test_encrypt_ephemeral_output_size() {
556 let ephemeral_pub = [0x42u8; 32];
557 let wrapped = PairCodeUtils::encrypt_ephemeral_pub(&ephemeral_pub, "ABCD1234");
558 assert_eq!(wrapped.len(), 80);
559
560 assert_eq!(wrapped[0..32].len(), 32); assert_eq!(wrapped[32..48].len(), 16); assert_eq!(wrapped[48..80].len(), 32); }
565
566 #[test]
567 fn test_encrypt_decrypt_roundtrip() {
568 let ephemeral_pub = [0x42u8; 32];
569 let code = "ABCD1234";
570
571 let wrapped = PairCodeUtils::encrypt_ephemeral_pub(&ephemeral_pub, code);
572
573 let decrypted = PairCodeUtils::decrypt_primary_ephemeral_pub(&wrapped, code)
575 .expect("Decryption should succeed");
576
577 assert_eq!(decrypted, ephemeral_pub);
578 }
579
580 #[test]
581 fn test_decrypt_invalid_length() {
582 let code = "ABCD1234";
583
584 let result = PairCodeUtils::decrypt_primary_ephemeral_pub(&[0u8; 79], code);
586 assert!(matches!(
587 result,
588 Err(PairCodeError::InvalidWrappedData { .. })
589 ));
590
591 let result = PairCodeUtils::decrypt_primary_ephemeral_pub(&[0u8; 81], code);
593 assert!(matches!(
594 result,
595 Err(PairCodeError::InvalidWrappedData { .. })
596 ));
597 }
598
599 #[test]
600 fn test_platform_id_string_enum() {
601 assert_eq!(PlatformId::Chrome.as_str(), "1");
603 assert_eq!(PlatformId::Firefox.to_string(), "2");
604 assert_eq!(PlatformId::default(), PlatformId::Chrome);
605 assert_eq!(PlatformId::Chrome as u8, 1);
607 }
608
609 #[test]
610 fn test_code_validity_duration() {
611 let duration = PairCodeUtils::code_validity();
612 assert_eq!(duration.as_secs(), 180);
613 }
614
615 #[test]
616 fn test_validate_code_case_insensitive() {
617 assert!(PairCodeUtils::validate_code("abcd1234"));
619 assert!(PairCodeUtils::validate_code("AbCd1234"));
620 assert!(PairCodeUtils::validate_code("vwxyz123"));
621 }
622
623 #[test]
624 fn test_validate_code_all_crockford_chars() {
625 assert!(PairCodeUtils::validate_code("12345678"));
627 assert!(PairCodeUtils::validate_code("9ABCDEFG"));
628 assert!(PairCodeUtils::validate_code("HJKLMNPQ"));
629 assert!(PairCodeUtils::validate_code("RSTVWXYZ"));
630 }
631
632 #[test]
633 fn test_generate_code_uniqueness() {
634 let codes: Vec<String> = (0..100).map(|_| PairCodeUtils::generate_code()).collect();
636 let unique_codes: std::collections::HashSet<_> = codes.iter().collect();
637 assert!(unique_codes.len() > 95);
639 }
640
641 #[test]
642 fn test_encrypt_produces_different_output_each_time() {
643 let ephemeral_pub = [0x42u8; 32];
645 let code = "ABCD1234";
646
647 let wrapped1 = PairCodeUtils::encrypt_ephemeral_pub(&ephemeral_pub, code);
648 let wrapped2 = PairCodeUtils::encrypt_ephemeral_pub(&ephemeral_pub, code);
649
650 assert_ne!(&wrapped1[0..32], &wrapped2[0..32]); assert_ne!(&wrapped1[32..48], &wrapped2[32..48]); }
654
655 #[test]
656 fn test_decrypt_with_wrong_code_produces_garbage() {
657 let ephemeral_pub = [0x42u8; 32];
658 let correct_code = "ABCD1234";
659 let wrong_code = "WXYZ5678";
660
661 let wrapped = PairCodeUtils::encrypt_ephemeral_pub(&ephemeral_pub, correct_code);
662
663 let decrypted = PairCodeUtils::decrypt_primary_ephemeral_pub(&wrapped, wrong_code)
665 .expect("Decryption should succeed structurally");
666
667 assert_ne!(decrypted, ephemeral_pub);
669 }
670
671 #[test]
672 fn test_derive_key_with_different_salts() {
673 let code = "ABCD1234";
674 let salt1 = [0u8; 32];
675 let salt2 = [1u8; 32];
676
677 let key1 = PairCodeUtils::derive_key(code, &salt1);
678 let key2 = PairCodeUtils::derive_key(code, &salt2);
679
680 assert_ne!(key1, key2);
682 }
683
684 #[test]
685 fn test_pair_code_options_default() {
686 let options = PairCodeOptions::default();
687 assert!(options.phone_number.is_empty());
688 assert!(options.show_push_notification);
689 assert!(options.custom_code.is_none());
690 assert_eq!(options.platform_id, PlatformId::Chrome);
691 assert_eq!(options.platform_display, "Chrome (Linux)");
692 }
693
694 #[test]
695 fn test_pair_code_options_with_custom_code() {
696 let options = PairCodeOptions {
697 phone_number: "15551234567".to_string(),
698 custom_code: Some("MYCODE12".to_string()),
699 ..Default::default()
700 };
701 assert_eq!(options.phone_number, "15551234567");
702 assert_eq!(options.custom_code, Some("MYCODE12".to_string()));
703 }
704
705 #[test]
706 fn test_pair_code_state_debug() {
707 let idle = PairCodeState::Idle;
708 assert_eq!(format!("{:?}", idle), "Idle");
709
710 let completed = PairCodeState::Completed;
711 assert_eq!(format!("{:?}", completed), "Completed");
712 }
713
714 #[test]
715 fn test_pair_code_error_display() {
716 let err = PairCodeError::PhoneNumberRequired;
717 assert_eq!(err.to_string(), "Phone number is required");
718
719 let err = PairCodeError::PhoneNumberTooShort;
720 assert_eq!(
721 err.to_string(),
722 "Phone number is too short (must be at least 7 digits)"
723 );
724
725 let err = PairCodeError::InvalidCustomCode;
726 assert_eq!(
727 err.to_string(),
728 "Invalid custom code: must be 8 characters from Crockford Base32 alphabet"
729 );
730
731 let err = PairCodeError::InvalidWrappedData {
732 expected: 80,
733 got: 50,
734 };
735 assert_eq!(
736 err.to_string(),
737 "Invalid wrapped data: expected 80 bytes, got 50"
738 );
739 }
740
741 #[test]
742 fn test_crockford_encoding_boundary_values() {
743 let bytes = [0x00, 0x00, 0x00, 0x00, 0x1F]; let encoded = PairCodeUtils::encode_crockford(&bytes);
746 assert_eq!(encoded.chars().last().unwrap(), 'Z');
747
748 let bytes = [0x00, 0x00, 0x00, 0x00, 0x01]; let encoded = PairCodeUtils::encode_crockford(&bytes);
750 assert_eq!(encoded.chars().last().unwrap(), '2');
751 }
752}