diff --git a/src/features/status.rs b/src/features/status.rs index 33b8faf93..c8ac7f747 100644 --- a/src/features/status.rs +++ b/src/features/status.rs @@ -73,7 +73,7 @@ impl<'a> Status<'a> { /// the `UploadResponse`, JPEG thumbnail bytes, and optional caption. pub async fn send_image( &self, - upload: &UploadResponse, + upload: UploadResponse, thumbnail: Vec, caption: Option<&str>, recipients: &[Jid], @@ -81,11 +81,11 @@ impl<'a> Status<'a> { ) -> Result { let message = wa::Message { image_message: Some(Box::new(wa::message::ImageMessage { - url: Some(upload.url.clone()), - direct_path: Some(upload.direct_path.clone()), - media_key: Some(upload.media_key.clone()), - file_sha256: Some(upload.file_sha256.clone()), - file_enc_sha256: Some(upload.file_enc_sha256.clone()), + url: Some(upload.url), + direct_path: Some(upload.direct_path), + media_key: Some(upload.media_key.to_vec()), + file_sha256: Some(upload.file_sha256.to_vec()), + file_enc_sha256: Some(upload.file_enc_sha256.to_vec()), file_length: Some(upload.file_length), mimetype: Some("image/jpeg".to_string()), jpeg_thumbnail: Some(thumbnail), @@ -106,7 +106,7 @@ impl<'a> Status<'a> { /// the `UploadResponse`, JPEG thumbnail bytes, duration in seconds, and optional caption. pub async fn send_video( &self, - upload: &UploadResponse, + upload: UploadResponse, thumbnail: Vec, duration_seconds: u32, caption: Option<&str>, @@ -115,11 +115,11 @@ impl<'a> Status<'a> { ) -> Result { let message = wa::Message { video_message: Some(Box::new(wa::message::VideoMessage { - url: Some(upload.url.clone()), - direct_path: Some(upload.direct_path.clone()), - media_key: Some(upload.media_key.clone()), - file_sha256: Some(upload.file_sha256.clone()), - file_enc_sha256: Some(upload.file_enc_sha256.clone()), + url: Some(upload.url), + direct_path: Some(upload.direct_path), + media_key: Some(upload.media_key.to_vec()), + file_sha256: Some(upload.file_sha256.to_vec()), + file_enc_sha256: Some(upload.file_enc_sha256.to_vec()), file_length: Some(upload.file_length), mimetype: Some("video/mp4".to_string()), jpeg_thumbnail: Some(thumbnail), diff --git a/src/upload.rs b/src/upload.rs index 4be11c9d3..23dcf1904 100644 --- a/src/upload.rs +++ b/src/upload.rs @@ -156,9 +156,9 @@ where return Ok(UploadResponse { url, direct_path, - media_key: enc.media_key.to_vec(), - file_enc_sha256: enc.file_enc_sha256.to_vec(), - file_sha256: enc.file_sha256.to_vec(), + media_key: enc.media_key, + file_enc_sha256: enc.file_enc_sha256, + file_sha256: enc.file_sha256, file_length, media_key_timestamp, }); @@ -196,9 +196,9 @@ where return Ok(UploadResponse { url: raw.url, direct_path: raw.direct_path, - media_key: enc.media_key.to_vec(), - file_enc_sha256: enc.file_enc_sha256.to_vec(), - file_sha256: enc.file_sha256.to_vec(), + media_key: enc.media_key, + file_enc_sha256: enc.file_enc_sha256, + file_sha256: enc.file_sha256, file_length, media_key_timestamp, }); @@ -233,9 +233,9 @@ where pub struct UploadResponse { pub url: String, pub direct_path: String, - pub media_key: Vec, - pub file_enc_sha256: Vec, - pub file_sha256: Vec, + pub media_key: [u8; 32], + pub file_enc_sha256: [u8; 32], + pub file_sha256: [u8; 32], pub file_length: u64, /// Unix timestamp (seconds) when the media key was generated. pub media_key_timestamp: i64, @@ -254,6 +254,19 @@ impl From for wacore::sticker_pack::MediaUploadInfo { } } +impl UploadResponse { + /// Convert crypto fields to `Vec` for protobuf message construction. + pub fn media_key_vec(&self) -> Vec { + self.media_key.to_vec() + } + pub fn file_sha256_vec(&self) -> Vec { + self.file_sha256.to_vec() + } + pub fn file_enc_sha256_vec(&self) -> Vec { + self.file_enc_sha256.to_vec() + } +} + #[derive(Deserialize)] struct RawUploadResponse { url: String, @@ -264,7 +277,7 @@ struct RawUploadResponse { #[derive(Default, Clone)] pub struct UploadOptions { /// Reuse an existing media key instead of generating a fresh one. - pub media_key: Option>, + pub media_key: Option<[u8; 32]>, } impl std::fmt::Debug for UploadOptions { @@ -280,7 +293,7 @@ impl UploadOptions { Self::default() } - pub fn with_media_key(mut self, key: Vec) -> Self { + pub fn with_media_key(mut self, key: [u8; 32]) -> Self { self.media_key = Some(key); self } @@ -299,14 +312,7 @@ impl Client { ) -> Result { let file_length = data.len() as u64; let enc = wacore::runtime::blocking(&*self.runtime, move || { - let key_ref = match &options.media_key { - Some(k) => Some( - <&[u8; 32]>::try_from(k.as_slice()) - .map_err(|_| anyhow!("media_key must be exactly 32 bytes"))?, - ), - None => None, - }; - wacore::upload::encrypt_media_with_key(&data, media_type, key_ref) + wacore::upload::encrypt_media_with_key(&data, media_type, options.media_key.as_ref()) }) .await?; diff --git a/tests/e2e/tests/media.rs b/tests/e2e/tests/media.rs index e69bd3eec..59e858233 100644 --- a/tests/e2e/tests/media.rs +++ b/tests/e2e/tests/media.rs @@ -11,9 +11,9 @@ fn build_image_message(upload: &UploadResponse, caption: Option<&str>) -> wa::Me image_message: Some(Box::new(wa::message::ImageMessage { url: Some(upload.url.clone()), direct_path: Some(upload.direct_path.clone()), - media_key: Some(upload.media_key.clone()), - file_sha256: Some(upload.file_sha256.clone()), - file_enc_sha256: Some(upload.file_enc_sha256.clone()), + media_key: Some(upload.media_key.to_vec()), + file_sha256: Some(upload.file_sha256.to_vec()), + file_enc_sha256: Some(upload.file_enc_sha256.to_vec()), file_length: Some(upload.file_length), mimetype: Some("image/jpeg".to_string()), caption: caption.map(|c| c.to_string()), @@ -33,9 +33,9 @@ fn build_video_message( video_message: Some(Box::new(wa::message::VideoMessage { url: Some(upload.url.clone()), direct_path: Some(upload.direct_path.clone()), - media_key: Some(upload.media_key.clone()), - file_sha256: Some(upload.file_sha256.clone()), - file_enc_sha256: Some(upload.file_enc_sha256.clone()), + media_key: Some(upload.media_key.to_vec()), + file_sha256: Some(upload.file_sha256.to_vec()), + file_enc_sha256: Some(upload.file_enc_sha256.to_vec()), file_length: Some(upload.file_length), mimetype: Some("video/mp4".to_string()), seconds: Some(seconds), @@ -52,9 +52,9 @@ fn build_document_message(upload: &UploadResponse, filename: &str, mimetype: &st document_message: Some(Box::new(wa::message::DocumentMessage { url: Some(upload.url.clone()), direct_path: Some(upload.direct_path.clone()), - media_key: Some(upload.media_key.clone()), - file_sha256: Some(upload.file_sha256.clone()), - file_enc_sha256: Some(upload.file_enc_sha256.clone()), + media_key: Some(upload.media_key.to_vec()), + file_sha256: Some(upload.file_sha256.to_vec()), + file_enc_sha256: Some(upload.file_enc_sha256.to_vec()), file_length: Some(upload.file_length), mimetype: Some(mimetype.to_string()), file_name: Some(filename.to_string()), @@ -70,9 +70,9 @@ fn build_audio_message(upload: &UploadResponse, ptt: bool, seconds: u32) -> wa:: audio_message: Some(Box::new(wa::message::AudioMessage { url: Some(upload.url.clone()), direct_path: Some(upload.direct_path.clone()), - media_key: Some(upload.media_key.clone()), - file_sha256: Some(upload.file_sha256.clone()), - file_enc_sha256: Some(upload.file_enc_sha256.clone()), + media_key: Some(upload.media_key.to_vec()), + file_sha256: Some(upload.file_sha256.to_vec()), + file_enc_sha256: Some(upload.file_enc_sha256.to_vec()), file_length: Some(upload.file_length), mimetype: Some(if ptt { "audio/ogg; codecs=opus".to_string() @@ -301,9 +301,9 @@ async fn test_upload_then_download_via_downloadable_trait() -> anyhow::Result<() let img_msg = wa::message::ImageMessage { url: Some(upload.url.clone()), direct_path: Some(upload.direct_path.clone()), - media_key: Some(upload.media_key.clone()), - file_sha256: Some(upload.file_sha256.clone()), - file_enc_sha256: Some(upload.file_enc_sha256.clone()), + media_key: Some(upload.media_key.to_vec()), + file_sha256: Some(upload.file_sha256.to_vec()), + file_enc_sha256: Some(upload.file_enc_sha256.to_vec()), file_length: Some(upload.file_length), mimetype: Some("image/jpeg".to_string()), ..Default::default() diff --git a/tests/e2e/tests/newsletter.rs b/tests/e2e/tests/newsletter.rs index e4bbeb694..f95980e00 100644 --- a/tests/e2e/tests/newsletter.rs +++ b/tests/e2e/tests/newsletter.rs @@ -277,9 +277,9 @@ async fn test_newsletter_send_media_message() -> anyhow::Result<()> { image_message: Some(Box::new(wa::message::ImageMessage { url: Some(upload.url.clone()), direct_path: Some(upload.direct_path.clone()), - media_key: Some(upload.media_key.clone()), - file_sha256: Some(upload.file_sha256.clone()), - file_enc_sha256: Some(upload.file_enc_sha256.clone()), + media_key: Some(upload.media_key.to_vec()), + file_sha256: Some(upload.file_sha256.to_vec()), + file_enc_sha256: Some(upload.file_enc_sha256.to_vec()), file_length: Some(upload.file_length), mimetype: Some("image/jpeg".to_string()), caption: Some("Newsletter image test".to_string()), diff --git a/wacore/binary/src/jid.rs b/wacore/binary/src/jid.rs index 5811321e8..8ce9a780a 100644 --- a/wacore/binary/src/jid.rs +++ b/wacore/binary/src/jid.rs @@ -313,9 +313,9 @@ impl JidExt for Jid { } impl Jid { - pub fn new(user: &str, server: &str) -> Self { + pub fn new(user: impl Into, server: &str) -> Self { Self { - user: user.to_string(), + user: user.into(), server: cow_server_from_str(server), ..Default::default() } diff --git a/wacore/src/send.rs b/wacore/src/send.rs index c9a4fc284..962e731ec 100644 --- a/wacore/src/send.rs +++ b/wacore/src/send.rs @@ -709,7 +709,7 @@ pub async fn prepare_dm_stanza< let dsm = wa::Message { device_sent_message: Some(Box::new(DeviceSentMessage { destination_jid: Some(to_jid.to_string()), - message: Some(Box::new(message_for_encryption.clone())), + message: Some(Box::new(message_for_encryption)), phash: Some("".to_string()), })), ..Default::default() @@ -977,7 +977,7 @@ pub async fn prepare_group_stanza< let mut message_children: Vec = Vec::new(); let mut includes_prekey_message = false; - let mut resolved_devices_for_phash: Option> = None; + let mut phash_for_stanza: Option = None; let mut skdm_encrypted_devices: Vec = Vec::new(); // Determine if we need to distribute SKDM and to which devices @@ -1107,7 +1107,10 @@ pub async fn prepare_group_stanza< if let Some(ref distribution_list) = distribution_list { // WA Web computes phash from the full distribution list (target set at // send time), not the actual encrypted outcome - resolved_devices_for_phash = Some(distribution_list.clone()); + match MessageUtils::participant_list_hash(distribution_list) { + Ok(phash) => phash_for_stanza = Some(phash), + Err(e) => log::warn!("Failed to compute phash for group {}: {:?}", to_jid, e), + } let axolotl_skdm_bytes = create_sender_key_distribution_message_for_group( stores.sender_key_store, &to_jid, @@ -1227,15 +1230,8 @@ pub async fn prepare_group_stanza< } // Add phash if we distributed keys in this message - if let Some(devices) = &resolved_devices_for_phash { - match MessageUtils::participant_list_hash(devices) { - Ok(phash) => { - stanza_builder = stanza_builder.attr("phash", phash); - } - Err(e) => { - log::warn!("Failed to compute phash for group {}: {:?}", to_jid, e); - } - } + if let Some(phash) = phash_for_stanza { + stanza_builder = stanza_builder.attr("phash", phash); } // Add any extra stanza nodes provided by the caller diff --git a/wacore/src/sticker_pack.rs b/wacore/src/sticker_pack.rs index b8cc152ee..034ef9605 100644 --- a/wacore/src/sticker_pack.rs +++ b/wacore/src/sticker_pack.rs @@ -12,11 +12,11 @@ //! let zip_upload = client.upload(zip_result.zip_bytes.clone(), MediaType::StickerPack, Default::default()).await?; //! let thumb_upload = client.upload( //! thumbnail_jpeg, MediaType::StickerPackThumbnail, -//! UploadOptions::new().with_media_key(zip_upload.media_key.clone()), +//! UploadOptions::new().with_media_key(zip_upload.media_key), //! ).await?; //! //! let metadata = StickerPackMetadata::new(pack_id, "My Pack".into(), "Me".into()); -//! let msg = build_sticker_pack_message(&zip_result, &zip_upload.into(), &thumb_upload.into(), metadata); +//! let msg = build_sticker_pack_message(&zip_result, &zip_upload.into(), &thumb_upload.into(), metadata)?; //! client.send_message(jid, msg).await?; //! ``` //! @@ -101,9 +101,9 @@ impl StickerPackMetadata { #[non_exhaustive] pub struct MediaUploadInfo { pub direct_path: String, - pub media_key: Vec, - pub file_sha256: Vec, - pub file_enc_sha256: Vec, + pub media_key: [u8; 32], + pub file_sha256: [u8; 32], + pub file_enc_sha256: [u8; 32], pub file_length: u64, pub media_key_timestamp: i64, } @@ -111,9 +111,9 @@ pub struct MediaUploadInfo { impl MediaUploadInfo { pub fn new( direct_path: String, - media_key: Vec, - file_sha256: Vec, - file_enc_sha256: Vec, + media_key: [u8; 32], + file_sha256: [u8; 32], + file_enc_sha256: [u8; 32], file_length: u64, media_key_timestamp: i64, ) -> Self { @@ -196,37 +196,47 @@ pub fn create_sticker_pack_zip( /// Builds a `wa::Message` with `StickerPackMessage` from upload results. /// /// Proto fields match WA Web's `GenerateStickerPackMessageProto.js` exactly. -/// Caller must supply a JPEG thumbnail uploaded separately. +/// Caller must supply a JPEG thumbnail uploaded with the same `media_key` as +/// the zip (via `UploadOptions::with_media_key(zip_upload.media_key)`). +/// +/// # Errors +/// +/// Returns an error if the thumbnail was uploaded with a different media key +/// than the zip, since the proto only carries one `media_key` for both. pub fn build_sticker_pack_message( zip_result: &StickerPackZipResult, zip_upload: &MediaUploadInfo, thumb_upload: &MediaUploadInfo, metadata: StickerPackMetadata, -) -> wa::Message { +) -> Result { + if zip_upload.media_key != thumb_upload.media_key { + bail!("thumbnail must be uploaded with the same media_key as the zip"); + } + let pack_msg = wa::message::StickerPackMessage { sticker_pack_id: Some(metadata.pack_id), name: Some(metadata.name), publisher: Some(metadata.publisher), stickers: zip_result.stickers.clone(), file_length: Some(zip_upload.file_length), - file_sha256: Some(zip_upload.file_sha256.clone()), - file_enc_sha256: Some(zip_upload.file_enc_sha256.clone()), - media_key: Some(zip_upload.media_key.clone()), + file_sha256: Some(zip_upload.file_sha256.to_vec()), + file_enc_sha256: Some(zip_upload.file_enc_sha256.to_vec()), + media_key: Some(zip_upload.media_key.to_vec()), direct_path: Some(zip_upload.direct_path.clone()), caption: metadata.caption, pack_description: metadata.description, - thumbnail_sha256: Some(thumb_upload.file_sha256.clone()), - thumbnail_enc_sha256: Some(thumb_upload.file_enc_sha256.clone()), + thumbnail_sha256: Some(thumb_upload.file_sha256.to_vec()), + thumbnail_enc_sha256: Some(thumb_upload.file_enc_sha256.to_vec()), thumbnail_direct_path: Some(thumb_upload.direct_path.clone()), sticker_pack_size: Some(zip_result.zip_bytes.len() as u64), tray_icon_file_name: Some(zip_result.tray_icon_file_name.clone()), ..Default::default() }; - wa::Message { + Ok(wa::Message { sticker_pack_message: Some(Box::new(pack_msg)), ..Default::default() - } + }) } fn base64url_encode(data: &[u8]) -> String { @@ -359,17 +369,17 @@ mod tests { let zip_upload = MediaUploadInfo::new( "/mms/sticker-pack/abc".into(), - vec![0u8; 32], - vec![1u8; 32], - vec![2u8; 32], + [0u8; 32], + [1u8; 32], + [2u8; 32], zip_result.zip_bytes.len() as u64, 1234567890, ); let thumb_upload = MediaUploadInfo::new( "/mms/thumbnail-sticker-pack/def".into(), - vec![0u8; 32], - vec![3u8; 32], - vec![4u8; 32], + [0u8; 32], + [3u8; 32], + [4u8; 32], 1000, 1234567890, ); @@ -381,7 +391,8 @@ mod tests { ) .with_description("A test pack".into()); - let msg = build_sticker_pack_message(&zip_result, &zip_upload, &thumb_upload, metadata); + let msg = + build_sticker_pack_message(&zip_result, &zip_upload, &thumb_upload, metadata).unwrap(); let pack = msg.sticker_pack_message.unwrap(); assert_eq!(pack.sticker_pack_id.as_deref(), Some("msg-test"));