Decrypt M3U8 Playlist encrypted with AES-128 without IV

13,158

Solution 1

The HLS spec states [1]:

An encryption method of AES-128 signals that Media Segments are completely encrypted using the Advanced Encryption Standard (AES) [AES_128] with a 128-bit key, Cipher Block Chaining (CBC), and Public-Key Cryptography Standards #7 (PKCS7) padding [RFC5652]. CBC is restarted on each segment boundary, using either the Initialization Vector (IV) attribute value or the Media Sequence Number as the IV; see Section 5.2.

So you have to use the value of the EXT-X-MEDIA-SEQUENCE tag in the variant playlist. Be sure to extrapolate, i.e. increment it for each segment.

[1] https://www.rfc-editor.org/rfc/rfc8216#section-4.3.2.4

Solution 2

I know this has already been answered correctly by bzier, but I figured I'd mention this for future readers:

Parsing/decrypt m3u8 files can be handled automatically by ffmpeg. By taking a look at the source code, we can understand how the IV is established when it's not provided.

This is also documented in RFC 8216.

If you feel the need to do this yourself in C#, here is a fully working example:

string m3u8_url = "https://example.com/file.m3u8";

WebClient web = new WebClient();
Stream m3u8_data = web.OpenRead(m3u8_url);
web.Dispose();

M3u8Content content = new M3u8Content();
M3uPlaylist playlist = content.GetFromStream(m3u8_data);
int media_sequence = 0;

// 16 chars - steal this from the key file.
byte[] key = Encoding.ASCII.GetBytes("0123456701234567");

string path = Path.GetFullPath("output.mp4");
FileStream fs = File.Create(path);

foreach(M3uPlaylistEntry entry in playlist.PlaylistEntries) {

    // establish initialization vector
    // note: iv must be 16 bytes (AES-128)
    byte[] iv = media_sequence.ToBigEndianBytes(); // 8 bytes
    iv = new byte[8].Concat(iv).ToArray(); // add 8 empty bytes to beginning

    // https://www.rfc-editor.org/rfc/rfc8216#section-4.3.2.4
    // HLS uses AES-128 w/ CBC & PKCS7
    RijndaelManaged algorithm = new RijndaelManaged() {
        Padding = PaddingMode.PKCS7,
        Mode = CipherMode.CBC,
        KeySize = 128,
        BlockSize = 128
    };

    // key = from uri in m3u8 file
    // iv = derived from sequence number
    algorithm.Key = key;
    algorithm.IV = iv;

    web = new WebClient();
    byte[] data = web.DownloadData(entry.Path);

    // create memorystream to store bytes & cryptostream to decrypt
    MemoryStream ms = new MemoryStream();
    CryptoStream cs = new CryptoStream(ms, algorithm.CreateDecryptor(), CryptoStreamMode.Write);

    // decrypt data to memorystream
    cs.Write(data, 0, data.Length);

    // write decrypted bytes to our mp4 file
    byte[] bytes = ms.ToArray();
    fs.Write(bytes, 0, bytes.Length);

    // close/dispose those streams
    cs.Close();
    ms.Close();
    cs.Dispose();
    ms.Dispose();

    // increment media sequence to update initialization vector
    media_sequence++;

}

// close the file stream & dispose of it
fs.Close();
fs.Dispose();

Here's the ToBigEndianBytes extension func, which I borrowed from bzier's response.

public static byte[] ToBigEndianBytes(this int i) {
    byte[] bytes = BitConverter.GetBytes(Convert.ToUInt64(i));
    if (BitConverter.IsLittleEndian)
        Array.Reverse(bytes);
    return bytes;
}

This code uses PlaylistsNET to parse playlist entries, and you'll have to set the key/starting media sequence manually - but it shows off the encryption and how it works.

I still highly recommend using ffmpeg though.

string cmd = string.Format("ffmpeg -i \"{0}\" -c copy -bsf:a aac_adtstoasc \"{1}\"", m3u8_url, local_mp4_path);
Execute(cmd);

public static void ExecuteCommand(string command) {
    Process process = new Process();
    ProcessStartInfo startInfo = new ProcessStartInfo();
    startInfo.WindowStyle = ProcessWindowStyle.Hidden;
    startInfo.FileName = "cmd.exe";
    startInfo.Arguments = "/C " + command;
    process.StartInfo = startInfo;
    process.Start();
    process.WaitForExit();
}

It'll accomplish the same thing with less code and you wont have to convert that resulting .ts file to a .mp4 because ffmpeg can do it for you.

Solution 3

I implemented this in the following way (where seqNum is the media sequence number for this segment):

readonly byte[] blank8Bytes = new byte[8];

// [...]
    AES.IV = blank8Bytes.Concat(IntToBigEndianBytes(seqNum)).ToArray();
// [...]    

// https://stackoverflow.com/a/1318948/9526448
private static byte[] IntToBigEndianBytes(ulong intValue)
{
    byte[] intBytes = BitConverter.GetBytes(intValue);
    if (BitConverter.IsLittleEndian)
        Array.Reverse(intBytes);
    byte[] result = intBytes;
    return result;
}

FYI, since you said you are also parsing the playlists, I'll mention that I forked the iHeartRadio open-m3u8 playlist parser and translated it into C#. If you're interested, the C# library is here: https://github.com/bzier/open-m3u8

Share:
13,158

Related videos on Youtube

Tobias Tengler
Author by

Tobias Tengler

Senior Software developer from Germany with an interest in anything web related. I'm particularly passionate about GraphQL, React and Typescript. Disclaimer: All questions, answers, comments and code are from me personally, and in no way represent the opinions of my employer or affiliates.

Updated on June 04, 2022

Comments

  • Tobias Tengler
    Tobias Tengler almost 2 years

    I'm currently building an Application for downloading M3U8 Playlists, but i've run into an issue: If the Playlist is encrypted with AES-128, e.g. has a line like this:

    #EXT-X-KEY:METHOD=AES-128,URI="https://website.com/link.key",IV=0xblablabla

    I have to decrypt the segments before writing them to the output file, and if an IV is present the below code does work for me, but if the IV property doesn't exist the decryption produces a wrong result:

    var iv = "parsed iv"; // empty if not present
    var key_url = "parsed keyurl";
    
    var AES = new AesManaged()
    {
        Mode = CipherMode.CBC,
        Key = await Client.GetByteArrayAsync(key_url)
    };
    
    if (!string.IsNullOrEmpty(iv))
        AES.IV = Functions.HexToByte(iv.StartsWith("0x") ? iv.Remove(0, 2) : iv);
    else
        AES.IV = new byte[16];
    
    //...
    
    using (FileStream fs = new FileStream("file.ts", FileMode.Create, FileAccess.Write, FileShare.Read))
    {
        var data = DownloadSegment(...); // Downloads segment as byte array (encrypted)
    
        byte[] temp = new byte[data.Length];
    
        ICryptoTransform transform = AES.CreateDecryptor();
        using (MemoryStream memoryStream = new MemoryStream(data))
        {
            using (CryptoStream cryptoStream = new CryptoStream(memoryStream, transform, CryptoStreamMode.Read))
            {
                cryptoStream.Read(temp, 0, data.Length);
            }
        }
    
        await fs.WriteAsync(temp, 0, temp.Length);
    }
    

    (This is obviously just a code snippet, containing the decryption part, since all the parsing and downloading does work fine).

    Does anyone of you know how to decrypt an AES-128 encrypted segment in a M3U8 Playlist file if there is no IV present, e.g. just

    #EXT-X-KEY:METHOD=AES-128,URI="https://website.com/link.key"?

    Any help is greatly appreciated. Thanks in advance!

  • bzier
    bzier almost 6 years
    Note that section 5.2 goes on with more detail: > An EXT-X-KEY tag with a KEYFORMAT of "identity" that does not have an IV attribute indicates that the Media Sequence Number is to be used as the IV when decrypting a Media Segment, by putting its big-endian binary representation into a 16-octet (128-bit) buffer and padding (on the left) with zeros.
  • Zhou XF
    Zhou XF over 4 years
    Mind if I asked, what is a big-endian binary for a segment, say the segment is filesequence0000016.ts,is big-endian binary of this ts file is 0000016?@bzier