back

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!
Hello