This project has moved. For the latest updates, please go here.

HLS Encryption

Jun 10, 2013 at 2:47 PM
Is there a way to handle encrypted HLS streams? The stream I'd like to connect to expects a cookie to be in the header of the request in the #EXT-X-KEY tag.

By the way, fantastic work!
Coordinator
Jun 10, 2013 at 5:55 PM
Thanks.

I looked at it briefly when I was working on the M3U8 parser, but I never went further than adding a layer of indirection for the segment reader (so one could add a decryption "decorator" or some-such). From what I can recall, it did not look like it would take more than a couple of hours to implement/test; the main stumbling block was more along the lines of locating some reliable test cases.
Jun 11, 2013 at 5:24 PM
Cool. If you're already parsing the key tag then I'll spend some time digging into code and see if I can figure out where to deal with decryption
Aug 3, 2013 at 6:52 AM
Can you share what you find on encryption? I have a similar requirement. I modified the example to pass my cookie container in on the httpWebrequestFactory but now I'm getting a number of exceptions which I think may be related.

This just keeps repeating in the output window and nothing ever plays.

A first chance exception of type 'System.Exception' occurred in SM.TsParser.DLL
Callback failed: Invalid packet
Coordinator
Aug 3, 2013 at 9:12 AM
Have you take a look with Fiddler? I saw something like this a while back when a server was returning an error page with a 200 result code (<sigh>) instead of a transport stream. The page said something about access denied and the TS parser/demux isn't designed to parse UTF-8 encoded HTML.

I would suggest debugging with the TsDump console app until the transport stream is sorted (if it is a live stream or you want to debug while offline, download a segment or two and read it from the local machine with a "file:" URL). If that works, try "SimulatedPlayer" with a playlist URL. The cross debugger for the emulator and WP hardware is nice, but running the debugger locally is nicer still. It is a bit more work if you are stuck with VS Express, but it can still be done (get the source and replace the references to the PCL projects with references to the prebuilt assemblies).
Aug 3, 2013 at 2:35 PM
The main problem I found was that the WorkBuffers are set at a fixed size of 32768 bytes. This becomes a problem when needing to decrypt that data, since the individual .ts chunks in the HLS playlist are almost always larger than that and are encrypted as one file and not in smaller buffer chunks. After the WorkBuffer chunk is downloaded, it gets appended into one giant array that the TsDecoder pulls from. That 32768 chunk either needs to be decrypted before thrown into the array, or as it's being pulled out. I wasn't successful in ever decrypting partial data chunks, I only ever got it to successfully decrypt the data if I had all of the .ts file isolated into one array.
Aug 3, 2013 at 8:35 PM
What I'm running into is in TSPacket.parse(). The first element in the buffer is not 0x47 so it is throwing an exception.
        // sync_byte
        if (0x47 != buffer[i++])
        {
            IsSkip = true;
            return false;
        }
In Fiddler it looks like the responses are correct HTTP 200, not error codes. I got the latest code from source control and modified it to login and then pass in the cookie.

I do think there might be a bug in HttpsClients. When passing in a cookieContainer I had to reverse the order of the code below to set the UseCookies to true before setting the CookieContainer.
        if (null != cookieContainer)
        {
            _httpClientHandler.UseCookies = true;
            _httpClientHandler.CookieContainer = cookieContainer;                
        }
Coordinator
Aug 3, 2013 at 11:11 PM
Edited Aug 3, 2013 at 11:20 PM
The idea with media stream decryption was to use SegmentReader's "streamFilter" to return a CryptoStream wrapping the underlying stream. The crypto initialization needs to be implemented outside of SM.Media since (IIRC) the PCL doesn't provide access to the required crypto APIs.

Note that the retry code in SegementReader.OpenStream() needs to be aware of "null != streamFilter", since it can no longer restart in the middle of a segment (unless, I suspect, the offset comes directly from the .m3u8 file).

The above is for METHOD=AES-128 decryption; I have no idea how to deal with SAMPLE-AES decryption without ripping apart SM.TsParser.
Coordinator
Aug 3, 2013 at 11:22 PM
@JDL440, does the Fiddler2 HexView show something that looks like a transport stream? What headers are you getting back from the server?

I'll take a look at the UseCookie/CookieContainer thing (thanks).
Aug 4, 2013 at 12:57 AM
I'm not super familiar with what the transport stream should look like, but I think the answer is yes, these look right.


Here is a header from one of the responses:


HTTP/1.1 200 OK
Server: Sun-ONE-Web-Server/6.1
Last-Modified: Sun, 28 Jul 2013 20:00:05 GMT
ETag: "82430-4e297d26adb40"
Accept-Ranges: bytes
Content-Length: 533552
Content-Type: video/mpeg
Cache-Control: max-age=2919
Expires: Sat, 03 Aug 2013 21:10:22 GMT
Date: Sat, 03 Aug 2013 20:21:43 GMT
Connection: keep-alive

I'm happy to share more information and help repro the issue if it will help. I believe this is AES-128 by the way.
Coordinator
Aug 6, 2013 at 1:28 AM
It looks reasonable. I was mostly looking for an odd "Content-Type" or a suspiciously small "Content-Length".
Aug 7, 2013 at 5:23 AM
I'm looking at the StreamFilter code and trying to figure out how I would plug something like BouncyCastle decryption in here on the stream level. Can you expand on this more? Do you know of any decryption libraries that work on async streams like this?
Coordinator
Aug 7, 2013 at 9:27 AM
Edited Aug 7, 2013 at 10:41 AM
In general, one could always derive a class from Stream and implement the async read so that it first awaits the base stream, and then applies the decryption synchronously (there isn't much to gain by throwing a CPU bound task on a background thread when all one is going to do is to wait for the background work to complete anyway).

That said, are you sure you need Bouncy Castle? While the native AES only supports CBC with PKCS7 padding, that is what HLS uses for "AES-128" (IIRC). Something like this should work (it needs to live in a native assembly [as in, "a managed assembly targeting the underlying platform instead of a PCL," not, "native machine code"] to have access to the crypto stuff; SM.Media.Platform.* probably makes the most sense, perhaps as part of "PlatformServices" plus the corresponding change in "IPlatformService"):
public Stream Aes128DecryptionFilter(Stream stream, byte[] key, byte[] iv)
{
    var aes = new AesManaged
                {
                    Key = key,
                    IV = iv
                };

    return new CryptoStream(stream, aes.CreateDecryptor(), CryptoStreamMode.Read);
}
The tricky part is still getting the key and IV from the playlist to the decryptor. Assuming IPlatformServices does add something along the lines of "DecryptionFilter", then one could perhaps add a "DecryptionFIlter(Stream stream)" to SM.Media.Playlists.SubStreamSegment. PlaylistSubProgramBase.GetPlaylist would then need to extract the key/IV information from the M3U8 playlist (it already grabs the byte range and x-inf). That DecryptionFilter would then look something like:
public Stream DecryptionStream(Stream stream)
{
    var key = [ grab key from some property? ];
    var iv =  [ grab iv from some property? ];

    return GlobalPlatformServices.Aes128DecryptionFilter(stream, key, iv);
}
It also looks like the streamFilter argument needs to change from "Func<Stream, Stream>" to "Func<ISegment, Stream, Stream>" since knowing which segment the filter is to operate on is critically important (since both the IV and key are per-segment).

This is all off the top of my head and from skimming the code, so I apologize if I'm missing some crucial point...
Aug 7, 2013 at 5:58 PM
Thanks, this was really helpful. I went a little further, changed the response to a string and used bouncy castle to decrypt the string instead of a stream. That's when I realized the IV was different and in the playlist and didn't get much further. Your information above will really help.

In my playlist it looks like the iv is on EXT-X-KEY, but the key isn't there. There is a kid on the uri, which I'm wondering if is a key id, but I can do some more research on this to find out.

EXT-X-KEY:METHOD=AES-128,URI="https://xxxx.xxxxxx.com/xxxxxxxx/xxxxxxxxx/xxxxxxxxx/xxxxxxxxxxx/v-x.x?productionId=XXXXXXXXXXXXXXXXXXX_XXXXXXXX&kid=72960",IV=0xc7f72425d9e33c7789ea7f6d96852c4e

As for the native assembly, will that work on WP7 also? I'm using WP8 so it's probably not a big deal, just wondering.

It sounds like you have a good understanding of what changes need to be made. Do you plan on doing more work in this area soon?
Coordinator
Aug 7, 2013 at 11:24 PM
The URI in EXT-X-KEY (see also section 6.3.6) is a link to a binary file containing the AES 128 key. It applies to the current segment as well as all future segments (until another EXT-X-KEY is found).

Add the code to SM.Media.Platform.WP7, then share it with SM.Media.Platform.WP8. That way it will be available on both platforms (that's how most of the other code is shared). I'm pretty sure the code is the same...

I have limited time to look at this stuff and there is apparently a race condition in starting playback I need to dig into. After that, I have another project with a bunch of code changes related to the "Audio isnt synced with video?" discussion that has been pending since May that I really need to sort out (before I completely forget what I was doing).
Aug 8, 2013 at 6:03 AM
Ok, I can see the ext_x_key tag in this method and in the debugger I can see the attributes for METHOD, URI, and IV, but I'm having a hard time getting them. Any pointer?

Then when I get it... it looks like I'll need to add it to segment. From here I can use this in SegmentReader.OpenStream to read the response, decrypt it, then pass it on. Thoughts?
    public static IEnumerable<SubStreamSegment> GetPlaylist(M3U8Parser parser)
    {
        var lastOffset = 0L;

        foreach (var p in parser.Playlist)
        {
            var url = parser.ResolveUrl(p.Uri);

            var segment = new SubStreamSegment(url);

            if (null != p.Tags && 0 != p.Tags.Length)
            {
                M3U8TagInstance ext_x_key = M3U8Tags.ExtXKey.Find(p.Tags);

                if (null != ext_x_key)
                {
                    M3U8Attribute ext_x_key_uri;
                    ext_x_key.Tag.Attributes().TryGetValue("URI", out ext_x_key_uri);

                    M3U8Attribute ext_x_key_iv;
                    ext_x_key.Tag.Attributes().TryGetValue("IV", out ext_x_key_iv);
                }
I had to add a find method to ExtXKeyTag. Also, I think scope this might need to be changed to segment instead of shared?

public static readonly M3U8ExtKeyTag ExtXKey = new M3U8ExtKeyTag("#EXT-X-KEY", M3U8TagScope.Segment);
Coordinator
Aug 8, 2013 at 12:47 PM
Yes, I think you need to add the relevant gunk to the segment. (I'm not sure if it makes more sense to add them to ISegment or to add separate ISegmentDecoder interface or some-such...)

Yes, ExtXKey should probably have a Find() that does something along the lines of "return tags.Tag<M3U8ExtKeyTag, ExtKeyTagInstance>(this);".

The code in GetPlaylist() should be able to do something like this and have the types work out correctly (i.e., method is string and iv is byte[]):
var extKey = M3U8Tags.ExtXKey.Find(p.Tags);

if (null != extKey)
{
    var method = extKey.AttributeObject(ExtKeySupport.AttrMethod);
    var uri = extKey.AttributeObject(ExtKeySupport.AttrUri);
    var iv = extKey.AttributeObject(ExtKeySupport.AttrIv);
}
(And yes, the M3U8 parser API is a good contender for a, "too clever by half," award.)

IIRC, the standard says that segments keep the previous segment's key/iv unless a new one is provided; M3U8TagScope.Segment would mean that each segment was independent. I'm not positive that M3U8TagScope.Shared works properly, but the intent was for it to mean that the tag sticks around until changed.
Coordinator
Aug 31, 2013 at 1:05 AM
Changeset e04f52fa8c7c adds support for AES-128 encryption. It hasn't been tested all that much and it may have broken other things, but it is a start...

If anyone has a chance to try playing back some encrypted streams, any feedback would be appreciated.

Thanks.