
Download and decrypt AES-128 .m3u8 playlists
Download and decrypt AES-128 .m3u8 playlists
If you’ve ever tried to download a video using ffmpeg and been met with a wall of 403 Forbidden errors or "Invalid Data" messages, you know how frustrating it can be. Most tutorials suggest a simple ffmpeg -i URL -c copy output.mp4 command, but modern CDNs like BunnyCDN (MediaDelivery) have security layers that make this impossible.
In this post, I’ll walk through how I bypassed these restrictions, extracted the decryption key, and merged over 1,700 encrypted segments into a single 1080p video.
1. The Problem: The Triple Threat
When attempting to download the stream, I hit three major roadblocks:
- 403 Forbidden: The server rejected any request for the key or video segments made outside the official video player.
- Encrypted Chunks: The video wasn’t a single file; it was broken into thousands of .dts segments, each encrypted with AES-128.
- FFmpeg Gatekeeping: FFmpeg refused to process .dts files within an HLS manifest, labeling them as a security risk.
2. Capturing the “Unlock Code” (key.bin)
AES-128 streams require a 16-byte key to decrypt. The .m3u8 manifest pointed to a URI, but visiting it directly resulted in a 403 Forbidden error because the CDN checks for a specific Referer header.
The Solution: Use the Browser Console. Since the browser is already authorized to view the video, I used the fetch() API in the DevTools console to grab the key and force a download.
// Run this in the console while on the video page
fetch("https://your-cdn-link.com/path-to-key")
.then(response => response.blob())
.then(blob => {
const a = document.createElement('a');
a.href = window.URL.createObjectURL(blob);
a.download = 'key.bin';
a.click();
});Note: I verified the file was exactly 16 bytes. If it’s not 16 bytes, it’s not the real key.
3. Mass Downloading 1,700+ Segments
With over 1,700 segments, downloading them one by one was impossible. However, the CDN’s security meant standard tools like wget or curl failed.
The Solution: A Batch-Zipping Script. I wrote a JavaScript snippet to fetch the segments in batches of 200, package them into a .zip file using the JSZip library, and download them. This bypassed the 403 error because the requests originated from the authorized browser session.
I also took this opportunity to rename the files from .dts to .ts during the zipping process. Why? Because FFmpeg’s HLS demuxer has a hardcoded whitelist of allowed extensions, and .dts isn't on it.
4. Reconstructing the Manifest
After extracting all the .ts files into one folder, I had to "lie" to the manifest file (video.m3u8) so it would look at my local files instead of the web.
I edited the .m3u8 file to:
- Change the Key URI: URI="key.bin"
- Change Segment Extensions: Replaced all instances of .dts with .ts.
5. The Grand Finale: Merging with FFmpeg
With the local key, the local segments, and the modified manifest all in one folder, the final command was simple:
ffmpeg -allowed_extensions ALL -i video.m3u8 -c copy final_video.mp4
Why this works:
-allowed_extensions ALL: This is critical. It tells FFmpeg to trust the local .bin key file.-c copy: Since the segments are already encoded, we don't waste time re-rendering. We are simply decrypting and "gluing" them together.
Conclusion
Downloading protected content isn’t always about “cracking” security; it’s often about understanding how the browser interacts with a CDN. By using the browser’s own authorized context to fetch the data and cleaning up the files for FFmpeg, you can save high-quality streams that would otherwise be inaccessible.
Tips for success:
- Always check your file sizes — if a segment is only 4KB, you’ve downloaded an error page, not video data.
- If the video is scrambled, double-check the IV (Initialization Vector) in your manifest.
- Be patient with the browser’s memory when zipping large 1080p batches!