MXF : Codes & Files

JS-MXF

Prototype of MXF analyzer without needs to upload the MXF file : everything is analyzed in your local machine.

You can test with this MXF file.

Get the MXF identifier (asdcplib)

$ asdcp-info -i "picture.mxf" | awk '/AssetUUID/ { print $2 }'
3bd3d849-117b-46b0-bc45-3d3228c987c6

Get the MXF identifier (Python)

The prototype is available here mxf-get-assetuid.py

$ mxf-get-assetuid.py "jp2k_video.mxf"
3bd3d849-117b-46b0-bc45-3d3228c987c6

Get the Cryptographic-KeyID of the MXF (asdcplib)

$ asdcp-info -i "picture.mxf" | awk '/CryptographicKeyID/ { print $2 }'
cf2ab7c6-c00f-4d52-aae2-3c3396a89b93

Get the Cryptographic-KeyID of the MXF (Python)

TODO

Show the SMPTE Universal Label - 16 octets (Python)

4x4 octets Format :

bytes = b'\x06\x0e\x2b\x34\x02\x05\x01\x01\x0d\x01\x02\x01\x01\x02\x01\x00'
print("{:02x}{:02x}{:02x}{:02x}.{:02x}{:02x}{:02x}{:02x}.{:02x}{:02x}{:02x}{:02x}.{:02x}{:02x}{:02x}{:02x}".format(*bytes))
060e2b34.02050101.0d010201.01020100

UUID Format :

bytes = b'\x06\x0e\x2b\x34\x02\x05\x01\x01\x0d\x01\x02\x01\x01\x02\x01\x00'
print("{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}".format(*bytes))
060e2b34-0205-0101-0d01-020101020100

Show the SMPTE Universal Label - 12 octets (Python)

3x4 octets Format:

bytes = b'\x06\x0e\x2b\x34\x02\x05\x01\x01\x0d\x01\x02\x01'
print("{:02x}{:02x}{:02x}{:02x}.{:02x}{:02x}{:02x}{:02x}.{:02x}{:02x}{:02x}{:02x}".format(*bytes))
060e2b34.02050101.0d010201

BER : Determinate if a BER is short-form or long-form (C/C++)

bool ber_get_form(uint8_t bercode) {
    return (bool)(bercode & 0x80);
}

// Exemple d'utilisation
ber_get_form(0x83);  /* long form */
>>> true

ber_get_form(0x10);  /* short form */
>>> false

BER : Get the BER size according to the header code (C/C++)

uint8_t ber_get_size(uint8_t bercode) {
    if (bercode >= 0x80)
        return (uint8_t)(bercode ^ 0x80);
    return 0;
}

// Usage Example - (0x83 == BER à 3)
ber_get_size(0x83);
>>> 3

BER : Calculate the size (C/C++)

Réecrire cette fonction

unsigned long int ber_get_datasize(char *berdata, unsigned int bersize) {
    unsigned long int size = 0;
    unsigned short int index, shift, i;
    for(i = 0; i < bersize; i++) {
        index = 1-(i-(bersize-1))-1;   // swap-index
        shift = 8 * i;
        size |= ((unsigned long int)berdata[index] & 0xFF) << shift;
    }
    return size;
}

// Usage Example - (0x000170 == 368)
ber_get_datasize("\x00\x01\x70", 3);
>>> 368   /* octets */

BER : Calculate the size from a BER 0x83 (Python)

Rewrite this function to handle all BER

def ber(bercode):
    return int.from_bytes(bercode, byteorder='big') ^ 0x83000000

is SMTPE Universal Label (C/C++)

Check if the MXF (or the KLV) is a SMPTE Universal Label (SMPTE UL) :

bool is_smpte_ul(char *buffer) {
    return (bool)(strncmp("\x06\x0e\x2b\x34", buffer, 4) == 0);
}

Show the SMPTE Universal Label - 16 octets (C/C++)

Change 4x4o => uuid format

int smpte_uuid(char *src, char *dst) {
    return snprintf(dst,
        36,
        "%02x%02x%02x%02x.%02x%02x%02x%02x.%02x%02x%02x%02x.%02x%02x%02x%02x",
        src[0],  src[1],  src[2],  src[3],
        src[4],  src[5],  src[6],  src[7],
        src[8],  src[9],  src[10], src[11],
        src[12], src[13], src[14], src[15]
    );
}

// Exemple d'utilisation
char uuid_src[16] = "\x06\x0e\x2b\x34\x01\x02\x03\x04\x05\x06\x07\x08\x09\x1A\x1B\x1C";
char uuid_dst[36];
smpte_uuid(uuid_src, uuid_dst);
>>> uuid_dst = "060e2b34.01020304.05060708.091a1b1c"

Read an MXF and get the Universal Label, the size, and part of the data (C/C++)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>

/* this functions are defined in another place in documentation */
extern int smpte_uuid(char *, char *); 
extern bool is_smpte_ul(char *);
extern uint8_t ber_get_datasize(char *, unsigned int);
extern uint8_t ber_get_size(uint8_t);
extern uint8_t ber_get_form(uint8_t);

/*
    This code is neither optimized nor adheres to best C programming practices.
    It's intended for learning purposes: for example, declarations and malloc calls
    are placed inside the while loop when they ideally shouldn’t be.
    Some parts have been merged to avoid unnecessary back-and-forth operations.
*/

int main(int argc, char **argv) {

    if (argc < 2) {
        printf("Usage: %s <mxf>\n", argv[0]);
        return 1;
    }

    FILE *fh = fopen(argv[1], "r");

    while(!feof(fh)) {

        // Getting the file position
        // (optional: just for the display)
        unsigned long long int offset;
        offset = ftell(fh);
        printf("%12llu │ ", offset);

        // --------------------------
        // ---------- Key -----------
        // --------------------------
        
        // Get SMPTE UUID
        char key[16];
        fread(key, 16, 1, fh);

        // Show the key (UUID format)
        char *uuid = (char *)malloc(sizeof(char *) * 36);
        smpte_uuid(key, uuid);
        printf("%s │ ", uuid);
        free(uuid);

        // Stop if the key isn't an UUID SMPTE (optional)
        if( !is_smpte_ul(key) ) {
            printf("\n**SMPTE UL not found**\n");
            break;
        }

        // --------------------------
        // -------- Length ----------
        // --------------------------

        // Get the first octet of the BER (ex. 0x83)
        uint8_t ber_code;
        fread(&ber_code, 1, 1, fh);

        // Calculate the real size of the data
        unsigned long long int data_size;
        if ( ber_get_form(ber_code) == 1 ) {      // Long form (>= 0x80)
            // get size of length
            unsigned int ber_size;
            ber_size = ber_get_size(ber_code);
            // read full length data
            char ber[ber_size];
            fread(ber, ber_size, 1, fh);
            // convert length data to size of data
            data_size = ber_get_datasize(ber, ber_size);
        } else {                                    // short BER (< 0x80)
            // get size of data
            data_size = ber_code;
        }
        printf("%10llu │ ", data_size);

        // --------------------------
        // --------- Value ----------
        // --------------------------
 
        char *data = (char *)malloc(sizeof(char *) * data_size);
        fread(data, data_size, 1, fh);
        
        // Show the 16 first octets
        for(int i=0; i<16; i++) {
            printf("%02x", data[i] & 0xFF);
        }
        printf("\n");
        
        free(data);
    }

    fclose(fh);
    return 0;
}

// Usage :
$ gcc mxf-reader.c -o mxf-reader

$ ./mxf-reader
$   Usage: ./mxf-reader <mxf>

$ ./mxf-reader video.mxf
           0 │ 060e2b34.02050101.0d010201.01020400 │        104 │ 00010003000002000000000000000000
         124 │ 060e2b34.01010102.03010210.01000000 │        368 │ 00000000000000000000000000000000
         512 │ 060e2b34.02050101.0d010201.01050100 │       1772 │ 00000062000000123c0a060e2b340101
        2303 │ 060e2b34.01010102.03010210.01000000 │        237 │ 00000000000000000000000000000000
        2560 │ 060e2b34.02530101.0d010101.01012f00 │        154 │ 3c0a0010adab44242f254dc792ff29bd
        2732 │ 060e2b34.02530101.0d010101.01013000 │        168 │ 3c0a0010adab44242f254dc792ff29bd
        2918 │ 060e2b34.02530101.0d010101.01011800 │         92 │ 3c0a0010adab44242f254dc792ff29bd
        3027 │ 060e2b34.02530101.0d010101.01013600 │        124 │ 3c0a0010adab44242f254dc792ff29bd
         ... │                 ...                 │        ... │              ...
    62565888 │ 060e2b34.02050101.0d010201.01040400 │        104 │ 00010003000002000000000003baae00
    62566012 │ 060e2b34.01010102.03010210.01000000 │        368 │ 00000000000000000000000000000000
    62566400 │ 060e2b34.02530101.0d010201.01100100 │        271 │ 3c0a0010adab44242f254dc792ff29bd
    62566691 │ 060e2b34.01010102.03010210.01000000 │        201 │ 00000000000000000000000000000000
    62566912 │ 060e2b34.02050101.0d010201.01110100 │        256 │ 00000000000000000000000000000001
    62567187 │ 060e2b34.02050101.0d010201.01110100 │        256 │ 00000000000000000000000000000001

Bug on the last line displayed - to be fixed soon

Cryptographie

XOR

def xor(v1, v2):
    return bytes(a ^ b for a, b in zip(v1, v2))
    
print(xor(
    b'\x01\x23\x45\x67\x89\xAB\xCD\xEF',
    b'\x45\xA9\x1E\x06\xB4\xCD\x87\x1A'
).hex().upper())

AES-128-CBC Encryption

from cryptography.hazmat.primitives.ciphers import ( Cipher, algorithms, modes )
from cryptography.hazmat.backends import default_backend

# The Encryption Key : 'UNE CLEF SECRETE'
key       = b'\x55\x4E\x45\x20\x43\x4C\x45\x46\x20\x53\x45\x43\x52\x45\x54\x45'

# IV not really random ;-)
iv        = b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16'

# The plaintext : 'BONJOUR LE MONDE' (sorry, it's in French: 'HELLO WORLD' ;)
plaintext = b'\x42\x4F\x4E\x4A\x4F\x55\x52\x20\x4C\x45\x20\x4D\x4F\x4E\x44\x45'

# Cryptographic Engine Settings
cipher = Cipher(
    algorithms.AES(key),
    modes.CBC(iv),
    backend=default_backend()
)
encryptor = cipher.encryptor()

# Encryption of the plaintext
ciphertext = encryptor.update(plaintext)

# Give :
#   0xEB 0x36 0xD4 0x79
#   0x25 0x6C 0x9E 0x6D
#   0x1B 0xB4 0x11 0x52
#   0x6f 0x00 0x9C 0x1B
print(ciphertext, ciphertext.hex())

AES-128-CBC Decryption

from cryptography.hazmat.primitives.ciphers import ( Cipher, algorithms, modes )
from cryptography.hazmat.backends import default_backend

# The Decryption Key : 'UNE CLEF SECRETE'
key       = b'\x55\x4E\x45\x20\x43\x4C\x45\x46\x20\x53\x45\x43\x52\x45\x54\x45'

# IV not really random ;-)
iv        = b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16'

# The ciphertext (encrypted content)
ciphertext = b'\xeb\x36\xd4\x79\x25\x6c\x9e\x6d\x1b\xb4\x11\x52\x6f\x00\x9c\x1b'

# Cryptographic Engine Settings
cipher = Cipher(
    algorithms.AES(key),
    modes.CBC(iv),
    backend=default_backend()
)
decryptor = cipher.decryptor()

# Decryption of the ciphertext (encrypted content)
plaintext = decryptor.update(ciphertext)

# Give 'BONJOUR LE MONDE'
print(plaintext, plaintext.hex())

Decryption of the Value from a KLV Encrypted Essence Container

This code is an example of a decryption of the 16 first octets of the encrypted content following the "SMPTE-429-6 - Essence Encryption" (KLV JPEG2000, KLV WAV PCM, KLV Dolby Atmos, KLV Subtitles, etc...) standard.

#!/usr/bin/env python3

from cryptography.hazmat.primitives.ciphers import ( Cipher, algorithms, modes )
from cryptography.hazmat.backends import default_backend

# Decryption key used to decrypt the MXF content.
key = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

# Initialization Vector (IV)
# Block of 16 octets used to initiate the cryptographic engine
iv  = b'\xc7\x51\x65\x82\xd2\x74\x77\x9e\x09\xf8\x44\x01\x77\x0a\x61\x60'

# "CheckValue" is the 16 first octets that include a fixed-value.
# Decrypted, the content is '43:48:55:4B:43:48:55:4B:43:48:55:4B:43:48:55:4B' or 'CHUKCHUKCHUKCHUK'
checkvalue = b'\x44\x8d\xf3\xbc\x12\x60\x6b\xb2\xe3\xcc\x4a\xed\xec\x31\xbe\x78'

# The 16 first octets of the "Value" field of the KLV (this is the encrypted JPEG2000 header)
data = b'\xbb\x07\x4c\x4f\x11\x37\x30\x6a\x64\x45\xad\x3d\xaf\x0c\xc3\x05'

# Init Cryptographic Engine
cipher = Cipher(
    algorithms.AES(key),
    modes.CBC(iv),
    backend=default_backend()
)
decryptor = cipher.decryptor()

# We add the 16 first octets which is the encrypted "CheckValue"
decryptor.update(checkvalue)

# We add the 16 first octets of our encrypted data
plaintext = decryptor.update(data)

# The decrypted data must be "ff4fff51002f00030000080000000438"
# This is a JPEG2000 Header
print(plaintext.hex())

To get the complete (and simplified) version of this tool which allows to read an encrypted MXF and to extract encrypted essences : mxf-encrypted-decryption.py

$ mxf-encrypted-decryption.py
Usage: ./parse-klv-encrypted-essence.py <mxf> <aeskey>

$ mxf-encrypted-decryption.py encrypted-key-00000000000000000000000000000000.mxf 00000000000000000000000000000000
060e2b34020401010d010301027e0100 -  40300 - 8300001067bec4fc40de4996aac7fa42...
CryptographicContextLink Length         : 83000010
CryptographicContextLink Value          : 67bec4fc40de4996aac7fa42a6b0ed5e
PlaintextOffset Length                  : 83000008
PlaintextOffset Value                   : 0000000000000000 (0 bytes)
SourceKey Length                        : 83000010
SourceKey Value                         : 060e2b34010201010d01030115010801
SourceLength Length                     : 83000008
SourceLength Value                      : 0000000000009cc8 (40136 bytes)
Encrypted Source Length                 : 83009cf0 (40176 bytes)
Encrypted Source Value - IV             : 765a067b36dfd2e89da94a9c6af0902f
Encrypted Source Value - CheckValue     : 7d95b3c594116732ed0b2d9b13ac5283
Encrypted Source Value - Plaintext Data :
Encrypted Source Value - Encrypted Data : 9c52432ad90a1bba64fd0ac5c604a1c9...
TrackFile ID Length                     : 83000010
TrackFile ID Value                      : 89af85f04a1545ec8a769008829b2029
Sequence Number Length                  : 83000008
Sequence Number Value                   : 0000000000000001
Message Integrity Code (MIC) Length     : 83000014
Message Integrity Code (MIC) Value      : 5b594d66d09cf6ddfda8f6e691e4291ea7097bc8
Plaintext Source Value                  : 40144 bytes
Padding                                 : 8 bytes

Note that this tool is focused on JPEG2000 frame extraction, but could work with another essences (some exceptions, see the different chapters for each essence type)

Encrypted MXF samples : See Annex

Create an encrypted MXF (asdcplib)

asdcp-test \
    -L \
    -j deadbeefdeadbeefdeadbeefdeadbeef \
    -k 00000000000000000000000000000000 \
    -c output.mxf \
    ./

Expaination:

-L : create an SMPTE MXF (not Interop)
-j <keyId>  : defined manually the "Cryptographic Key Id"
-k <aeskey> : the AES key for the encryption
-c <outputfile>

Create an encrypted MXF with a Plaintext Offset

asdcp-test \
    -L \
    -E \
    -j deadbeefdeadbeefdeadbeefdeadbeef \
    -k 00000000000000000000000000000000 \
    -c output.mxf \
    ./

Explication :

-L : create an SMPTE MXF (not Interop)
-E : no headers encryption (36 first octets)
-j <keyId>  : defined manually the "Cryptographic Key Id"
-k <aeskey> : the AES key for the encryption
-c <outputfile>

Essence Extraction from an encrypted MXF (asdcplib)

asdcp-unwrap \
    -v \
    -k 00000000000000000000000000000000 \
    encrypted.mxf \
    extract/

Explication :

-v : Verbose
-k <aeskey> : the AES key for the decryption

Essence Extraction from an encrypted MXF (mxf-analyzer)

Disponible ici : assets/MXF/mxf-analyzer/

mxf-analyzer.py \
    -f encrypted.mxf \
    -x extract_directory/ \
    -k 00000000000000000000000000000000

Create an encrypted MXF with ASDCPlib (C++)

This code is a proof-of-concept, it create an MXF with a single encrypted frame : (the code is over-simplified, you have no check/verify)

#include <AS_DCP.h>
#include <KM_prng.h>  /* FortunaRNG */
#include <Metadata.h> /* MXF:: */

using namespace ASDCP;

int main(void) {

    WriterInfo Info;
    JP2K::MXFWriter Writer;
    JP2K::FrameBuffer FrameBuffer(1024 * 1024);
    JP2K::PictureDescriptor PDesc;
    JP2K::SequenceParser Parser;
    // AES
    AESEncContext* Context = 0;
    const byte_t aes_key[16] = {
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    };
    Kumu::FortunaRNG RNG;
    byte_t IV_buf[CBC_BLOCK_SIZE];
    // HMAC
    HMACContext* HMAC = 0;

    // Set Header
    Info.LabelSetType = LS_MXF_SMPTE;
    Kumu::GenRandomUUID(Info.ContextID);
    // AssetUUID == TrackFile ID (MIC)
    Kumu::GenRandomUUID(Info.AssetUUID);

    // Set Cryptographic
    Kumu::GenRandomUUID(Info.CryptographicKeyID);
    Info.EncryptedEssence = true;
    Context = new AESEncContext;
    Context->InitKey(aes_key);
    Context->SetIVec(RNG.FillRandom(IV_buf, CBC_BLOCK_SIZE));

    // Set HMAC
    Info.UsesHMAC = true;
    HMAC = new HMACContext;
    HMAC->InitKey(aes_key, Info.LabelSetType);

    // Set Parser from files
    Parser.OpenRead("essences/");
    Parser.FillPictureDescriptor(PDesc);

    // Go to the first file
    Parser.Reset();

    // Open MXF
    Writer.OpenWrite("dump.mxf", Info, PDesc);

    // --- foreach frame -------------------------------------- 
    // Read each frame (only one here)
    // Each call of ReadFrame() shift to the next frame
    // ReadFrame() returns a zero if no new frame
    Parser.ReadFrame(FrameBuffer);
    FrameBuffer.PlaintextOffset(0);  // force no plaintext
    // Write each frame into MXF (only one here)
    Writer.WriteFrame(FrameBuffer, Context, HMAC);
    // --------------------------------------------------------

    // Close MXF
    Writer.Finalize();

    // Show 256 bytes from JPEG2000
    FrameBuffer.Dump(stderr, 256);
    // Show all metadatas from JPEG2000
    JP2K::PictureDescriptorDump(PDesc);

    return 0;
}

To compile it :

# You must compile asdcplib first 
# to have the libasdcp et libkumu librairies
ASDCPLIB="/chemin/asdcplib/"

g++ -g -O2 \
    -lpthread \
    -Wl,-bind_at_load \
    -DHAVE_OPENSSL=1 \
    -I$(ASDCPLIB)/src/ \
    $(ASDCPLIB)/src/.libs/libasdcp.so \    # Linux 
    $(ASDCPLIB)/src/.libs/libkumu.so \     # Linux
    $(ASDCPLIB)/src/.libs/libasdcp.dylib \ # MacOS
    $(ASDCPLIB)/src/.libs/libkumu.dylib \  # MacOS
    `pkg-config openssl --cflags` \
    `pkg-config openssl --libs` \
    asdcplib-create-encrypted-mxf.cpp \
    -o asdcplib-create-encrypted-mxf

To run :

# You must compile asdcplib first 
# to have the libasdcp et libkumu librairies
ASDCPLIB="/chemin/asdcplib/"

# Linux
LD_LIBRARY_PATH="$(ASDCPLIB)/src/.libs:${LD_LIBRARY_PATH}" ./asdcplib-create-encrypted-mxf

# MacOS
DYLD_LIBRARY_PATH="$(ASDCPLIB)/src/.libs:${DYLD_LIBRARY_PATH}" ./asdcplib-create-encrypted-mxf

You can find the source code here : asdcplib-create-encrypted-mxf.cpp + Makefile

Files