MUX

From Mania Tech Wiki
Revision as of 14:12, 12 July 2020 by Askuri (talk | contribs) (Add link to a library for dealing with mux files)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

.mux files are encrypted .ogg files and are typically used for custom track music. They use a simple, custom stream cipher based on a 16-byte key (an MD5 hash): a very long key (the key stream) is generated from the 16-byte key, and then every byte of the input file is xor'd with the corresponding byte in the key stream. Details can be found at Wikipedia.

File layout

byte magic[9] = "NadeoFile"
byte version = 1
int32 keySalt
byte data[]

Decryption and Encryption

First, the 16-byte decryption key is determined as follows:

key = md5(pack(keySalt) + "Hello,hack3r!")    // Hello, d3veloper :)

where pack() returns the bytes of its argument in little endian order, and the string (Hello,hack3r!) is encoded as ASCII. Note that on little endian systems you won't need to implement pack() as the bytes will already be in correct order.

Pseudo Code

// keySalt and data variables are as specified under File Layout

void mux2ogg() {
    byte[] key = generateKey(keySalt);
    dataXORkey(key, data);
    // decrypted data is now in data (pass by reference)
}

void ogg2mux() {
    byte[] keySalt = ...; // generate random 4 bytes
    byte[] key = generateKey(keySalt);
    dataXORkey(key, data); // data is the full ogg content here
    // mux file can now be created by building the mux header as described in File Layout
}

byte[] generateKey(byte[] keySalt) {
    return = md5(pack(keySalt) + "Hello,hack3r!");
}

void dataXORkey(byte[] key, byte[] data) {
    for (int i = 0; i < data.Length; i++)
    {
        data[i] ^= GetKeyStreamByte(key, i);
    }
}

byte GetKeyStreamByte(byte[] key, int pos) {
    return rol(key[pos % 16], (pos / 17) % 8);
}

byte rol(byte input, int amount) {
    return (byte)((input << amount) | (input >> (8 - amount)));
}

PHP Implementation

<?php
/**
 * Left rotate the bits of a 1 byte integer
 * 
 * @param int $input integer to rotate
 * @param int $amount how many steps to rotate
 * @return int the rotated integer
 */
function rol(int $input, int $amount): int {
    return bytify(($input << $amount) | ($input >> (8 - $amount)));
}

/**
 * Get the byte of the key in the key stream (very long key).
 * 
 * @param int $pos Byte position in the data
 * @return string Corresponding byte from key stream
 */
function GetKeyStreamByte(string $key, int $pos): string {
    // Get the corresponding byte from the key and transform it into an integer.
    // Wrap around on the key, which is only 16 bytes long -> do modulo.
    // This would also work, but it's slower:
    //      unpack('C', $key[$pos % 16])[1],
    $input = ord($key[$pos % 16]);

    // What does this do and why is it necessary? legacy code by arc_
    $amount = ($pos / 17) % 8;

    // alternatively, one could use pack('C', ...) but it' slower
    // rol(...): why is this necessary? legacy code by arc_
    $rotated = rol($input, $amount);

    // Convert integer back to char/byte.
    // Could use pack('C', $rotated) here, but it's slower.
    return chr($rotated);
}

/**
 * De- or encrypt the payload -> do XOR with the key.
 * 
 * @param string $key 16 byte key as returned by md5()
 * @param string &$data Input data, overwritten with output (pass by reference!)
 * @return void
 */
function keyXORdata(string $key, string &$data): void {
    $length = strlen($data);

    for ($i = 0; $i < $length; $i++) {
        // XOR every byte of the input with the corresponding byte of the key
        $data[$i] = $data[$i] ^ GetKeyStreamByte($key, $i);
    }
}

/**
 * Make any integer fit into 1 byte by chopping off the bits above bit 7.
 * This is a workaround because we don't have a "byte" type in PHP where
 * every bit exceeding 7 bit would be dropped automatically during left shift.
 * 
 * @param int $nobyte Int that we want to be cut down to 8 bit
 * @return int
 */
function bytify(int $nobyte): int {
    return 0x000000FF & $nobyte;
}

/**
 * Convert a .mux version 01 file to .ogg
 * 
 * @param type $inputFile .mux file
 * @param type $outputFile .ogg file
 */
function mux2ogg($inputFile, $outputFile) {
    // open file and read its parts
    $handle = fopen($inputFile, "rb");
    $magicNumber = fread($handle, 9); // always "NadeoFile"
    $version = fread($handle, 1); // should be 0x01
    $keySalt = fread($handle, 4);
    $data = fread($handle, 33554432); // read max 32MB (less if end of file comes first)
    // Generate key. Note that we need the binary representation.
    // By default, md5() returns the hexadecimal representation.
    $key = md5($keySalt . "Hello,hack3r!", true);

    // decrypt mux to ogg
    keyXORdata($key, $data);

    file_put_contents($outputFile, $data);

    fclose($handle);
}

/**
 * Convert a .ogg to a .mux file
 * 
 * @param type $inputFile .ogg file
 * @param type $outputFile .mux file
 */
function ogg2mux($inputFile, $outputFile) {
    $magicNumber = "NadeoFile";
    $version = "\x01";
    $keySalt = "test"; // in reality, generate some random 4 bytes here...
    $data = file_get_contents($inputFile);

    // Generate key. Note that we need the binary representation.
    // By default, md5() returns the hexadecimal representation.
    $key = md5($keySalt . "Hello,hack3r!", true);

    // encrypt ogg to mux
    keyXORdata($key, $data);

    // now concatenate the header of the mux together with the encrypted ogg
    $mux = $magicNumber . $version . $keySalt . $data;

    file_put_contents($outputFile, $mux);
}

Tools

Libraries