Skip to main content

download

Download and decrypt media from a message.
Only use download when you need the plaintext bytes (processing, transcoding, re-upload). To forward existing media unchanged, reuse the original message’s CDN fields directly — no download required. See media forwarding via CDN reuse.
pub async fn download(
    &self,
    downloadable: &dyn Downloadable
) -> Result<Vec<u8>, anyhow::Error>
downloadable
&dyn Downloadable
required
Any message type that implements the Downloadable trait. Includes:
  • ImageMessage
  • VideoMessage
  • AudioMessage
  • DocumentMessage
  • StickerMessage
  • ExternalBlobReference (app state)
  • HistorySyncNotification
bytes
Vec<u8>
Decrypted media bytes. For encrypted media (E2EE), automatically decrypts using AES-256-CBC and verifies HMAC-SHA256. For plaintext media (newsletters/channels), validates SHA-256 hash.

Example: Download Image

use waproto::whatsapp as wa;

// From a received message
if let Some(image_msg) = message.image_message {
    let image_bytes = client.download(image_msg.as_ref()).await?;
    std::fs::write("downloaded_image.jpg", image_bytes)?;
}

Example: Download with Error Handling

match client.download(downloadable).await {
    Ok(data) => {
        println!("Downloaded {} bytes", data.len());
        // Process data...
    }
    Err(e) => {
        eprintln!("Download failed: {}", e);
        // Fallback logic...
    }
}

Automatic retry and URL re-derivation

All download methods handle three categories of CDN errors automatically:
  • Auth errors (401/403): The client invalidates the cached media connection, fetches fresh credentials, and retries the download once.
  • Media not found (404/410): When a media URL has expired or the file has been relocated, the CDN returns 404 or 410. The client treats this the same as an auth error — it invalidates the cached connection, re-derives download URLs with fresh credentials and hosts, and retries once. This matches WhatsApp Web’s MediaNotFoundError handling.
  • Other errors (e.g., 500): The client tries the next available CDN host without refreshing credentials. Hosts are tried in priority order (primary first, then fallback).
For streaming downloads, the writer is seeked back to position 0 before retrying so partial writes are overwritten.

download_to_file

Download media and write directly to a file.
pub async fn download_to_file<W: Write + Seek + Send + Unpin>(
    &self,
    downloadable: &dyn Downloadable,
    writer: W,
) -> Result<(), anyhow::Error>
downloadable
&dyn Downloadable
required
Message containing downloadable media
writer
W: Write + Seek + Send + Unpin
required
Writer to output the decrypted data. Typically a File or BufWriter<File>.

Example: Download to File

use std::fs::File;
use std::io::BufWriter;

let file = File::create("video.mp4")?;
let writer = BufWriter::new(file);

client.download_to_file(video_msg.as_ref(), writer).await?;
println!("Video saved to video.mp4");

download_to_writer

Download media using streaming when available, with automatic buffered fallback. When the HTTP client supports streaming (supports_streaming() returns true), the entire HTTP download, decryption, and file write happen in a single blocking thread with ~40KB memory usage regardless of file size. When streaming is not available, the method automatically falls back to a buffered download — fetching the full response into memory, then decrypting and writing to the writer. This ensures download_to_writer works with any HttpClient implementation.
pub async fn download_to_writer<W: Write + Seek + Send + 'static>(
    &self,
    downloadable: &dyn Downloadable,
    writer: W,
) -> Result<W, anyhow::Error>
downloadable
&dyn Downloadable
required
Message containing downloadable media
writer
W: Write + Seek + Send + 'static
required
Writer for streaming output. Must be Send + ‘static for use in blocking task.
writer
W
Returns the writer after successful download, seeked back to position 0.

Example: Streaming Download

use std::fs::File;

let file = File::create("large_video.mp4")?;
let file = client.download_to_writer(video_msg.as_ref(), file).await?;
// File is seeked back to start and can be reused
When using an HTTP client that supports streaming (like the default UreqHttpClient), memory usage is constant ~40KB (8KB read buffer + decryption state). HTTP clients that don’t support streaming fall back to buffered downloads, which load the full file into memory before writing.

download_from_params

Download and decrypt media from raw parameters without the original message.
pub async fn download_from_params(
    &self,
    direct_path: &str,
    media_key: &[u8],
    file_sha256: &[u8],
    file_enc_sha256: &[u8],
    file_length: u64,
    media_type: MediaType,
) -> Result<Vec<u8>, anyhow::Error>
direct_path
&str
required
WhatsApp CDN path (e.g., /v/t62.7118-24/12345_67890)
media_key
&[u8]
required
32-byte media encryption key from message
file_sha256
&[u8]
required
SHA-256 hash of decrypted file
file_enc_sha256
&[u8]
required
SHA-256 hash of encrypted file
file_length
u64
required
Original file size in bytes
media_type
MediaType
required
Type of media: Image, Video, Audio, Document, Sticker, etc.
bytes
Vec<u8>
Decrypted media bytes

Example: Download from Stored Metadata

use wacore::download::MediaType;

// If you stored media metadata separately
let image_bytes = client.download_from_params(
    "/v/t62.7118-24/12345_67890",
    &media_key,
    &file_sha256,
    &file_enc_sha256,
    file_length,
    MediaType::Image
).await?;

download_from_params_to_writer

Streaming variant of download_from_params that writes to a writer.
pub async fn download_from_params_to_writer<W: Write + Seek + Send + 'static>(
    &self,
    direct_path: &str,
    media_key: &[u8],
    file_sha256: &[u8],
    file_enc_sha256: &[u8],
    file_length: u64,
    media_type: MediaType,
    writer: W,
) -> Result<W, anyhow::Error>
direct_path
&str
required
WhatsApp CDN path
media_key
&[u8]
required
32-byte media encryption key
file_sha256
&[u8]
required
SHA-256 hash of decrypted file
file_enc_sha256
&[u8]
required
SHA-256 hash of encrypted file
file_length
u64
required
Original file size in bytes
media_type
MediaType
required
Type of media
writer
W
required
Writer for streaming output
writer
W
Returns the writer after successful download

Downloadable Trait

The Downloadable trait provides a generic interface for downloading media from any message type.
pub trait Downloadable: Sync + Send {
    fn direct_path(&self) -> Option<&str>;
    fn media_key(&self) -> Option<&[u8]>;
    fn file_enc_sha256(&self) -> Option<&[u8]>;
    fn file_sha256(&self) -> Option<&[u8]>;
    fn file_length(&self) -> Option<u64>;
    fn app_info(&self) -> MediaType;
    fn static_url(&self) -> Option<&str> { None }
    fn is_encrypted(&self) -> bool { self.media_key().is_some() }
}
direct_path
fn() -> Option<&str>
WhatsApp CDN path for the media file
media_key
fn() -> Option<&[u8]>
32-byte encryption key. Present for E2EE media, None for plaintext (newsletter/channel) media.
file_enc_sha256
fn() -> Option<&[u8]>
SHA-256 hash of the encrypted file. Used for encrypted media validation.
file_sha256
fn() -> Option<&[u8]>
SHA-256 hash of the decrypted file. Used for plaintext media validation.
file_length
fn() -> Option<u64>
Original file size in bytes
app_info
fn() -> MediaType
Media type for HKDF key derivation (Image, Video, Audio, Document, etc.)
static_url
fn() -> Option<&str>
default:"None"
Static CDN URL for direct download. Present on newsletter/channel media, bypasses host construction.
is_encrypted
fn() -> bool
default:"media_key().is_some()"
Returns true if media is encrypted (has media_key), false for plaintext media

Built-in Implementations

The Downloadable trait is automatically implemented for:
  • wa::message::ImageMessage
  • wa::message::VideoMessage
  • wa::message::AudioMessage
  • wa::message::DocumentMessage
  • wa::message::StickerMessage
  • wa::ExternalBlobReference (app state)
  • wa::message::HistorySyncNotification

MediaType

Media type enum for encryption/decryption.
pub enum MediaType {
    Image,
    Video,
    Audio,
    Document,
    History,
    AppState,
    Sticker,
    StickerPack,
    StickerPackThumbnail,
    LinkThumbnail,
    ProductCatalogImage,
}
Each media type has specific HKDF info strings used for key derivation:
  • Image / Sticker"WhatsApp Image Keys"
  • Video"WhatsApp Video Keys"
  • Audio"WhatsApp Audio Keys"
  • Document"WhatsApp Document Keys"
  • History"WhatsApp History Keys"
  • AppState"WhatsApp App State Keys"
  • StickerPack"WhatsApp Sticker Pack Keys"
  • StickerPackThumbnail"WhatsApp Sticker Pack Thumbnail Keys"
  • LinkThumbnail"WhatsApp Link Thumbnail Keys"

MediaType methods

MethodReturn typeDescription
app_info()&'static strHKDF info string for key derivation
mms_type()&'static strMedia type string for MMS path construction
upload_path()&'static strURL path prefix for upload/download
is_encrypted()boolWhether this media type uses E2E encryption

Upload paths

Media typeUpload path
Image / Sticker/mms/image
Video/mms/video
Audio/mms/audio
Document/mms/document
History/mms/md-msg-hist
AppState/mms/md-app-state
StickerPack/mms/sticker-pack
StickerPackThumbnail/mms/thumbnail-sticker-pack
LinkThumbnail/mms/thumbnail-link
ProductCatalogImage/product/image
ProductCatalogImage is unencryptedis_encrypted() returns false. This matches WhatsApp Web’s behavior where CreateMediaKeys.js skips encryption for product catalog images. Its upload path is /product/image (not under the /mms/ prefix like other media types).

Media Decryption

WhatsApp uses different handling for encrypted (E2EE) and plaintext media:

Encrypted Media (E2EE)

  1. Download encrypted bytes from CDN
  2. Verify HMAC-SHA256 (last 10 bytes)
  3. Decrypt using AES-256-CBC with keys derived from media_key via HKDF
  4. Return decrypted plaintext
The media_key is expanded using HKDF-SHA256 to derive:
  • 16-byte IV
  • 32-byte cipher key
  • 32-byte MAC key

Plaintext Media (Newsletter/Channel)

  1. Download plaintext bytes from CDN (often via static_url)
  2. Verify SHA-256 hash matches file_sha256
  3. Return plaintext (no decryption needed)
Newsletter and channel media is not encrypted. The library automatically detects this when media_key is absent and switches to plaintext validation.

Example: Detect Media Type

if downloadable.is_encrypted() {
    println!("E2EE media - will decrypt");
} else {
    println!("Plaintext media - no decryption needed");
}

DownloadUtils

Low-level static methods for media decryption and validation. These are re-exported from wacore::download and useful when you need fine-grained control over the download pipeline.
use whatsapp_rust::download::DownloadUtils;

Key methods

MethodDescription
verify_and_decrypt(encrypted_payload, media_key, media_type)Verifies HMAC and decrypts AES-256-CBC in one call
decrypt_stream(reader, media_key, media_type)Streaming decryption from a reader (convenience wrapper around decrypt_stream_to_writer)
decrypt_stream_to_writer(reader, media_key, media_type, writer)Streaming decryption directly into a writer with constant memory usage
validate_plaintext_sha256(data, expected_sha256)Validates SHA-256 hash of plaintext media (in-memory)
copy_and_validate_plaintext_to_writer(reader, expected_sha256, writer)Streams plaintext media to a writer while validating SHA-256 hash
prepare_download_requests(downloadable, media_conn)Builds CDN request URLs with host failover
get_media_keys(media_key, app_info)Derives IV, cipher key, and MAC key via HKDF