locked
Playing Encrypted Video File with MediaElement

    Question

  • My Windows Store app has an encryption / decryption option that lets people store all their data files (which include images and movies) encrypted in their project folder for potential copying over the Internet, to cloud services, etc. (Trust me, this is necessary, and no, BitLocker, SSL, etc. are insufficient).

    To play videos, the app uses MediaElement. I'd like to be able to set the MediaElement source to a stream that decrypts the file on demand. With desktop apps, I would just use a CryptoStream, but Windows Store apps don't appear to support CryptoStream. 

    Can anyone suggest an efficient way to do this?

    Other options that come to mind (each with significant disadvantages)-

    - Decrypt the whole file to a temporary location, then source MediaElement with the temp file - obviously bad if the app or device crashes before the temp file is deleted though, and not particularly efficient.

    - Create a custom media stream source that decrypts the data frame by frame, sample by sample, and feeds it to the MediaElement; problem with this is that my encryption function would likewise have to scramble the video and audio data sample by sample, rather than encrypt the whole file once, meaning lots of C++/DirectX nastiness or dealing with media transcoding which I've not had good experiences with (maybe they've fixed it, but earlier this year I found the whole transcoding system to be riddled with memory leak problems and wound up having to write everything in C++ with DirectX)

    - Write a custom CrytpoStream that implements IRandomAccessStream? I honestly wouldn't know where to begin though.

    Any other ideas appreciated. Thanks!

    Peter

    Friday, November 21, 2014 4:37 PM

Answers

  • Success! It was tricky to debug, but it totally works.  I created my own CryptoStream class that derives from Stream and takes as a constructor parameter an encrypted Stream, the password, and the "salt." The overrides handle the decryption and seeking. I can use AsRandomAccessStream and pass that to a MediaElement, and voila, I'm playing password encrypted video.

    As noted, the encryption/decryption is in chunks, and reading anything less than or equal to one chunk size nonetheless requires reading and decrypting the whole chunk. For streaming video though I don't think this is terribly inefficient. I am more curious about Jtmtv's security concern though. Note this is only going to be used for video and audio encryption, where random access is necessary, not images and certainly not plain text, if that makes a difference. And I would also reiterate my earlier point - how else, other than through chunks, could an encrypted file system possibly work efficiently?

    Anyway I'll post the code if anyone's interested. It was a little tricky but nothing magical.


    • Marked as answer by peter_legistek Sunday, November 23, 2014 8:43 PM
    Saturday, November 22, 2014 9:11 PM

All replies

  • i had the same issue when i wrote a encryption app for wp8. You would have to create a buffer class of some sorts using memorystreams to hold decrypted data in because encrypted streams dont support seek. This isnt as easy as it sounds because there is a reason that encryption streams dont support seek. Depending on the encyption algorthim the bytes of data may have to be fed through the algorithm mutltiple times (or whatever the algorithm dictates), making the issue of holding decrypted data particularly hard. You would have to research the encryption algorthim and its specific details in order to figure out how to implement a seek class. Depending on the size of the video you could try decrypting the whole file in memory and holding it, but thats not very effiecent for large files. The other option you have is to create a decrypted file, and then read that file into your media element. the disadvantage to that is a big on when it comes to security. You have a decrypted copy of your video now on the Harddrive on the host computer, if the program crashes or exits before you can delete this file then your encryption is bypassed. Also even if you do delete the unencrypted file, it isnt actually deleted unless you implement a secure delete algorithm.

    If security is the most important factor here i would recommend looking to a programmer/developer that specialized in encryption/security, and also has working code you can use. Not a amature programmer and/or developer that specializes in other areas of computer science.

    Sorry im not much help.

    Jesse

    Friday, November 21, 2014 7:17 PM
  • You say encryption doesn't support seek, but couldn't I encrypt in chunks of, say, 1024 bytes?

    I actually started trying to implement IRandomAccessStream in this fashion and am currently debugging. Will let y'all know if it works and if so post it.

    Friday, November 21, 2014 7:34 PM
  • Encypting data in multiple chunks isnt a good idea either, in some cases. say you have a file that is multiple megabytes, if you encrypt chunks of 1024 bytes you now have a repetition in the bytes that are encrypted. Im terrible at explaining stuff so i apologize. But cryptanalysis, IE code breakers, look for repetion among other things, as a source of weakness. The best and most secure way is to encrypt all the bytes in one shot through the encryption algorthim, not in chunks. I error on the cautious side, Im not the best programmer, nor and i a 'hacker', but i operate on the idea of 'when not if' if a adversary of security wants access to X, i try to stop the access but operate on the idea they they Will get in, so i make sure what they get is of as little value as possible. Same idea with basic file security. Again i would stick the tried and true information from security experts. My general thought has always been repetition is bad, and to stick to how the algorithm is meant to be used, if repetition doesnt matter to the aglorthim then great. The false feeling of strong security is worse then having no security at all. Encyption is a can of worms, a lot of responsibility, and i dont like it the more i sit here and think about it, but i also love the challenge of it lol. I would continue doing what you are doing with the chunk idea, and wait for a person with more knowledge comes by. Im interested to see the outcome with your seek class though. I decided not to go that route with my program just because i felt responsible for providing security i didnt feel that i could truly offer, and decided not to offer it at all.

    jesse

    Friday, November 21, 2014 8:00 PM
  • Ah I see what you're saying. But still, encrypted file systems must operate this way because you can, for example, watch movies off of a BitLockered hard drive and I'm reasonably confident the OS is not decrypting the entire thing before playback. Maybe it's not as good as encrypting the whole file in one shot but it's still better than nothing, like you said, unless/until someone else has a better idea at least.
    Friday, November 21, 2014 8:35 PM
  • Success! It was tricky to debug, but it totally works.  I created my own CryptoStream class that derives from Stream and takes as a constructor parameter an encrypted Stream, the password, and the "salt." The overrides handle the decryption and seeking. I can use AsRandomAccessStream and pass that to a MediaElement, and voila, I'm playing password encrypted video.

    As noted, the encryption/decryption is in chunks, and reading anything less than or equal to one chunk size nonetheless requires reading and decrypting the whole chunk. For streaming video though I don't think this is terribly inefficient. I am more curious about Jtmtv's security concern though. Note this is only going to be used for video and audio encryption, where random access is necessary, not images and certainly not plain text, if that makes a difference. And I would also reiterate my earlier point - how else, other than through chunks, could an encrypted file system possibly work efficiently?

    Anyway I'll post the code if anyone's interested. It was a little tricky but nothing magical.


    • Marked as answer by peter_legistek Sunday, November 23, 2014 8:43 PM
    Saturday, November 22, 2014 9:11 PM
  • Hi, I see you have found a solution but there is another way that is common for any type of content. This is a virtualization: a set of low level hooks that comprise a "file system layer", for example you (of someone that will have similar task) may read this article that describes the way: http://boxedapp.com/encrypted_video_streaming.html


    • Edited by Nick__G Sunday, November 23, 2014 10:06 PM
    Sunday, November 23, 2014 10:06 PM
  • Hey Peter. 

    You said you would post your code, but it seems like you never did. Are there any chances you would provide the code, I just had a similar issue, found a workaround though, but for curiosity I would really like to see your code :). 

    Tuesday, February 17, 2015 10:06 AM
  • Ah, well I said if anyone is interested. :) You're the first one. It's a little long to post here though and it's been heavily integrated into my application so it wouldn't make much sense out of context. I'll try to start a Github project when I get a chance for a more general purpose utility. Which would be a good idea since it does need some additional work - namely, you can only write (encrypt) an entire file at once now. Efficient random-access encryption writing is a lot harder than random-access reading.

    Are you interested in contributing? If so let me know how I can contact you offline.

    Wednesday, February 18, 2015 2:41 PM
  • ill contribute also, i dont know any programers personally so it will be fun to work on something with someone. Feel free to contact me codingcoupdetat@outlook.com
    Wednesday, February 25, 2015 3:09 AM
  • Hi Peter,

    I would like to contribute too. I am working on similar projects and using the temp file mechanism. I have even tried to use the httplistener mechanism but no luck. The BoxApp method is quite costly and hence used the standard one. Though I am not personally satisfied with the solution. So was still searching and found this one. If you can share the same to my email id y d h o n g a d e AT g m a i l DOT c o m then it would be great.

    Looking forward !!! Cheers'

    Saturday, April 04, 2015 2:48 PM
  • Hey guys -

    I'll try to put something together on Github and let you know when it's up. Sorry it's been hard to find the time but I definitely plan to do it sooner or later.

    What I can tell you is how it works at a high level, which based on this should not be too hard to reproduce:

    (1) There is a reader and a writer. Currently the writer does not allow seeking or derive from Stream; it just writes an entire file encrypted in a single operation. Random access encrypted writing is a lot harder than random access encrypted reading, and I don't have a need for random access writing, so I haven't tried to do it. The reader on the other hand is a full blown Stream implementation.

    (2) The writer encrypts a source stream into nnn-sized blocks; the best block size depends on the application. I find that Media Foundation often tends to read in 1k-multiple chunk sizes, so as long as the block size is a reasonable multiple of 1024, you should be fine. In my application I use 8192. The larger the block sizes, the less efficient smaller read ops will be; the smaller they are, the less efficient the write op will be. So it's a trade-off. 

    For each block, I write (a) a randomly generated 16-byte initialization vector (I am operating under the assumption that it is safe to make this public, which is usually true) and (b) 8192 bytes worth of 256-bit AES encrypted data. So each encrypted block size is 9024 bytes (8192 + IV + padding). The separate IV for each block is necessary because, as was pointed out above, it is possible to discern patterns even from encrypted data if data is written in chunks all with the same IV. By changing the IV randomly, the encrypted data appears completely random, but no security is lost by making the IV public since the data is still useless without the key.

    A more generalized solution would include a header block at the beginning of the encrypted file which specifies the size of the unencrypted file, the size of each data chunk and the number of encryption bits / size of the IV for each block. 

    (3) The reader class - DecryptionStream - derives from the abstract Stream class, and includes a private "Stream _internalStream" member which is the stream representing the file on disk. The constructor takes a Stream representing the encrypted file, and the 256-bit AES key. The other members of DecryptionStream virtualize the stream so that consumers of the DecryptionStream see the Stream as though it is unencrypted. 

    Dispose should dispose of the _internalStream. 

    There is a private DecryptChunk(int chunk) member:

    bool DecryptChunk(int chunk, byte[] outputBuffer, int outputOffset, int outputCount)

    This seeks the _internalStream to the right position (chunk * 9024), reads the IV (the first 16 bytes of the chunk), and then decrypts the chunk. It outputs the decrypted data to outputBuffer. outputOffset and outputCount allow you to write only a subset of the decrypted data to the outputBuffer, at a desired offset, which comes in handy in the Read implementation, so that you can write the decrypted data directly into the buffer provided to you in the Read method. This method should also cache the last decrypted chunk for future use because there's a high probability successive read ops will be in the same chunk and you don't want to have to decrypt the entire 8192-byte chunk 64 times if there are 64 successive read operations of 128 bytes!

    The Length member needs to return the unencrypted length of the file. Calculating the unencrypted length requires taking the number of encrypted complete data blocks, multiplied by the size of an unencrypted block (8192), plus the size of the final block, which can be variable. Getting that size requires decrypting that final block. You must properly calculate the file size because MediaElement (and most any other consumer of your stream) needs this to be accurate, and you also need this to be able to properly Seek to an offset based on the end of the stream.

    Naturally, seeking doesn't actually change the position of the _internalStream. Rather you need to maintain your own internal seek counter, "long _position". 

    The Read method:

    public override int Read (byte[] buffer, int offset, int count)

    has two basic steps:

    - Determine the blocks of the encrypted file that need to be decrypted to satisfy the read op (this is a function of _position and count). 

    - Decrypt each block into the appropriate location in buffer

    It's a little complicated because the data the consumer wants to read will often not line up with your 8192 (or whatever you choose) blocks, so you have to account for the fact that you might be providing back to the caller only a subset of the first and/or last block. The logic is not complex but it's just tricky keeping track of it all (and tedious to debug). That's why including offset and count parameters in DecryptChunk is helpful.

    Also I don't know if this isn't entirely unnecessary, but I override ReadAsync also, wrapping Read in a Task. I assume the default implementation of Stream just does that (it's not abstract like Read is) but it can make debugging easier to be able to break inside ReadAsync. As far as I can tell, Media Foundation will alwasy use ReadAsync rather than call Read directly.

    (4) Debug your writer and DecryptionStream class thoroughly by feeding the writer known data and decrypting random sections of it with DecryptionStream. MediaElement is going to seek around your file a fair amount so you need to be absolutely certain all seeks and read ops result in accurate data. I know this sounds obvious, but it's easy to overlook certain failure modes. If your DecryptionStream class doesn't work right, the only indication you will get from MediaElement is simply that it can't play the file. Good luck finding where the problem actually was. 

    (5) Once your code is working flawlessly, it's ridiculously simple to play your encrypted file with MediaElement:

    StorageFile file = await RetrieveMyEncryptedMediaFileAsync();
    byte[] key = GetMyEncryptionKey();
    var stream = await file.OpenReadAsync();
    var decryptedStream = new DecryptionStream(stream, key);
    
    _myMediaElement.SetSource(decryptedStream.AsRandomAccessStream(), "");
    

    I was pretty psyched the first time I saw an encrypted media file playing perfectly in the MediaElement, yet Windows Media Player and everything else had no chance whatsoever. 

    The great thing about this is that it works with ANY XAML control or anything else that allows you to set an IRandomAccessStream as a source, e.g., BitmapImage. Also, if your DecryptionStream class implements IStream, you can use it with COM functions also. And it's easily extended to .NET. You can use it with MemoryStream or any other stream that is seekable. Very versatile and useful any time you want to read encrypted content without decrypting the entire file first.

    Hope this helps. 


    Saturday, April 04, 2015 7:37 PM