tonhowtf/omniget · error · anyhow::Error
AES decrypt: {:?}
Error message
AES decrypt: {:?} What it means
This error is raised in write_segments_ordered when the HLS segment decryptor fails at the decryption step (AES-128-CBC with PKCS7 padding via decrypt_padded_mut). Initialization succeeded (key/IV slices were valid lengths), but the ciphertext did not decrypt cleanly — most often because the padded data length is not a multiple of 16 bytes or the PKCS7 padding is invalid. It wraps the underlying cipher error with anyhow to give context that the failure happened during AES segment decryption.
Solutions
- Verify the segment download completed fully (check Content-Length / resp bytes length) before decrypting; re-download truncated segments.
- Confirm the correct EXT-X-KEY URI is fetched and the IV matches the playlist (use the IV attribute or media sequence number exactly as specified).
- Ensure payload_start trimming keeps the ciphertext block-aligned (multiple of 16 bytes for AES-128-CBC).
- Log the segment URL, key URI, and IV alongside this error to identify which segment/key pairing is wrong.
Example fix
// before
let decrypted = decryptor
.decrypt_padded_mut::<Pkcs7>(&mut buf)
.map_err(|e| anyhow::anyhow!("AES decrypt: {:?}", e))?;
// after
if buf.len() == 0 || buf.len() % 16 != 0 {
anyhow::bail!(
"segment {} ciphertext not block-aligned ({} bytes); re-downloading",
seg_url, buf.len()
);
}
let decrypted = decryptor
.decrypt_padded_mut::<Pkcs7>(&mut buf)
.with_context(|| format!("AES decrypt failed for segment {} (len {})", seg_url, buf.len()))?; Defensive patterns
Strategy: try-catch
Validate before calling
// before decrypting
if enc.key_bytes.len() != 16 {
return Err(anyhow::anyhow!("AES-128 key must be 16 bytes, got {}", enc.key_bytes.len()));
}
if iv.len() != 16 {
return Err(anyhow::anyhow!("AES-128 IV must be 16 bytes, got {}", iv.len()));
}
if buf.len() == 0 || buf.len() % 16 != 0 {
return Err(anyhow::anyhow!("ciphertext not block-aligned: {} bytes (truncated segment?)", buf.len()));
} Type guard
fn is_block_aligned(buf: &[u8]) -> bool { !buf.is_empty() && buf.len() % 16 == 0 } Try / catch
match decryptor.decrypt_padded_mut::<Pkcs7>(&mut buf) {
Ok(decrypted) => file.write_all(decrypted)?,
Err(e) => {
log::warn!("decrypt failed ({}), re-fetching segment once", e);
// re-download the segment, then retry decrypt; bail if it fails again
}
} Prevention
- Always validate segment payload length is a nonzero multiple of 16 before AES-CBC decrypt.
- Verify key fetch (EXT-X-KEY URI) succeeded and returned exactly 16 bytes.
- Honor the playlist's IV attribute or derive it from media sequence exactly per the HLS spec.
- Check the segment body isn't an HTML/JSON error page before treating it as ciphertext.
When it happens
Trigger: A media playlist segment has EXT-X-KEY with METHOD=AES-128; the segment bytes were truncated (not a multiple of 16 bytes), or the wrong IV/key was used, or the payload_start offset cut the buffer mid-block, so PKCS7 unpadding fails.
Common situations: Live/HLS streams where segments are downloaded partially or the playlist rotated and segment URLs return an error page instead of encrypted media; encrypting key rotated between playlist fetches; custom IV (EXT-X-KEY IV attribute) missing so a wrong default IV is derived from the media sequence number; corrupt or HTML error body written into buf instead of ciphertext.
Understand the failure class
Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.
Related errors
- HTTP {} fetching AES key
- Failed to fetch AES key after {} attempts
- AES init: {:?}
- Extension playlist is {} bytes, over the {} byte limit
- Download cancelled by user
AI-assisted analysis of tonhowtf/omniget@8600b91f42 (2026-09-12).
Data as JSON: /api/errors/6bf80abcdc727a6a.
Report an issue: GitHub.
Appendix: source
Thrown at src-tauri/omniget-core/src/core/hls_downloader.rs:680
while let Some(segment_data) = pending.remove(&next_expected) {
// The image wrapper, when present, sits outside the encryption:
// it has to come off before the AES-128 block decryption runs.
let payload_start = image_wrapper_offset(&segment_data);
if let Some(enc) = encryption {
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};
type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
let iv = compute_iv(enc, next_expected, media_sequence);
let mut buf = segment_data;
if payload_start > 0 {
buf.drain(..payload_start);
}
let decryptor = Aes128CbcDec::new_from_slices(&enc.key_bytes, &iv)
.map_err(|e| anyhow::anyhow!("AES init: {:?}", e))?;
let decrypted = decryptor
.decrypt_padded_mut::<Pkcs7>(&mut buf)
.map_err(|e| anyhow::anyhow!("AES decrypt: {:?}", e))?;
file.write_all(decrypted)?;
} else {
file.write_all(&segment_data[payload_start..])?;
}
next_expected += 1;
}
}
file.flush()?;
if next_expected < total_segments {
anyhow::bail!(
"Only {} of {} segments were written",
next_expected,
total_segments
);
}
View on GitHub (pinned to 8600b91f42)