Search code examples
androidpythonmp3android-mediaplayerm3u8

Malformed encryption mp3 to m3u8


I have a python function encoding a mp3 to m3u8. This function allows me to generate a m3u8 file along with its ts chunks.

I can read this "playlist" using the native player on iOs. Unfortunately I can't achieve to do the using the android-mediaplayer (I got the error MEDIA_ERROR_MALFORMED).

The catch is, if in python, I use the openssl via subprocess, it works. But spawning a new process is too expensive and I want to avoid this :

 cmd = ["openssl",
            "aes-128-cbc",
            "-e",
            "-in", path,
            "-out", dest_path+".openssl.ts",
            "-iv", ("%d" % iv_counter).zfill(32),
            "-K", keyHex]
        subprocess.check_call(cmd)

Using openssl or my implementation produces the same m3u8 file, the same numbers of ts files and these ts files have exactly the same weight.

The only explanation I could find is that my implementation is wrong. I know this may be hard to debug, but maybe something will jump at you at first reading. Here is the function doing the encryption :

from Crypto import Random
from Crypto.Cipher import AES
def encrypt(manifest, chunks, enc_dir):
    os.makedirs(enc_dir)

    # Get a random key
    key = Random.new().read(16)
    keyHex = key.encode('hex')

    # Encrypt each chunk
    for iv_counter, (_, path) in enumerate(chunks):
        with open(path, "rb") as chunk:
            chunk_data = chunk.read()

        # PKCS#7 padding
        pad = 16 - (len(chunk_data) % 16)
        chunk_data += chr(pad) * pad

        print "crypting using %s" % ("%d" % iv_counter).zfill(32)

        # AES encryption
        aes = AES.new(key, AES.MODE_CBC, "%16X" % iv_counter)
        chunk_data = aes.encrypt(chunk_data)

        dest_path = os.path.join(enc_dir, os.path.basename(path))
        #cmd = ["openssl",
        #    "aes-128-cbc",
        #    "-e",
        #    "-in", path,
        #    "-out", dest_path+".openssl.ts",
        #    "-iv", ("%d" % iv_counter).zfill(32),
        #    "-K", keyHex]
        #subprocess.check_call(cmd)
        with open(dest_path, "wb") as chunk:
            chunk.write(chunk_data)

        # Write the key to a file
        key_file = os.path.join(enc_dir, os.path.splitext(os.path.basename(manifest))[0] + ".key")
        with open(key_file, "w") as keyf:
            keyf.write(key)
        key_url = os.path.basename(key_file) #"file://" + os.path.abspath(key_file)

        # Write the new manifest
        dest_manifest = os.path.join(enc_dir, os.path.basename(manifest))
        with open(dest_manifest, "w") as manifest:
            manifest.write("#EXTM3U\n")
            manifest.write("#EXT-X-VERSION:3\n")
            manifest.write("#EXT-X-MEDIA-SEQUENCE:0\n")
            manifest.write("#EXT-X-ALLOW-CACHE:YES\n")
            manifest.write("#EXT-X-TARGETDURATION:6\n")
            manifest.write("#EXT-X-KEY:METHOD=AES-128,URI=\"%s\"\n" % key_url)
            for extinf, path in chunks:
                manifest.write("%s\n%s\n" % (extinf, os.path.basename(path)))
            manifest.write("#EXT-X-ENDLIST\n")

EDIT, if it can helps : we have coded a small JAVA function to decrypt a ts file produced by openssl and our homemade code. The file produced by openssl is fine but we got a bad padding exception for the one produced by our python code.


Solution

  • The problem is coming from your initial vector (iv). OpenSSL is waiting for a number parameter (in hexadecimal), but in a string format of 16 characters.

    Your code just returns the number in hexadecimal, but in ASCII format:

        >>> iv_counter = 11111111
        >>> print("%16X" % iv_counter)
        '          A98AC7'
    

    However, the expected value is:

        '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa9\x8a\xc7'
    

    To achieve it, you must replace that with:

        >>> print(("%032X" % iv_counter).decode("hex"))
        '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa9\x8a\xc7'