Skip to content

ROFL Container Notes

Boreeas edited this page May 1, 2014 · 4 revisions

The ROFL container is documented in the repository as C structs.

Notes

  • "ubyte" - unsigned byte - 1 byte
  • "ushort" - unsigned short - 2 bytes
  • "uint" - unsigned int - 4 bytes
  • "ulong" - unsigned long - 4 bytes
  • "ulonglong" - unsigned long long - 8 bytes
  • All values are little-endian unless mentioned otherwise

Format

Header (288 bytes)

Magic (6 bytes)

ROFL files begin with 6 bytes: "RIOT\0\0" (that's 'RIOT' followed by two null bytes).

Signature (256 bytes)

Next up is the file's signature, which is 256 bytes long.

Length fields (26 bytes)

Now we have:

  • Header length (ushort)
  • File length (uint)
  • Metadata offset (uint)
  • Metadata length (uint)
  • Payload header offset (uint)
  • Payload header length (uint)
  • Payload offset (uint)

Metadata

This is the JSON encoded metadata. It's "Header>Length fields>Metadata length" bytes long.

Payload header (34 + key bytes)

  • Game ID (ulonglong)
  • Game length (uint)
  • Keyframe count (uint)
  • Chunk count (uint)
  • End startup chunk ID (uint)
  • Start game chunk ID (uint)
  • Keyframe interval (uint)
  • Encryption key length (ushort)
  • Encryption key (string, see encryption key length, base64 encoded)

Chunk headers (Payload header>Chunk count * 17 bytes)

Repeated chunk headers:

Chunk header (17 bytes)

  • Chunk ID (uint)
  • Chunk type (ubyte - 0 indicates keyframe, 1 indicates chunk)
  • Chunk length (uint)
  • Next chunk ID (uint)
  • Offset (uint)

Payload headers (Payload header>Keyframe count * 17 bytes)

Payload header format is same as chunk header - see above.

Chunks

These are in an as-yet unknown format. For more information, refer to ROFL/Payload.

Encryption

To decrypt the files, you need to do the following (in pseudocode):

function decrypt(key, data) {
    decrypted = blowfish_ecb_mode.decrypt(key, data)
    return pkcs5padding.remove(decrypted)
}

function decompress(data) {
    return gzip.decompress(data)
}

function make_encryption_key(file_data) {
    raw_stored_encryption_key = base64_decode(file_data.payload_header.encryption_key)
    game_id_str = str(file_data.payload_header.game_id)
    return decrypt(game_id_str, raw_stored_encryption_key)
}

decrypted_chunks = []
encryption_key = make_encryption_key(file_data)
for (chunk in chunks) {
    decrypted_chunks.add(decompress(decrypt(encryption_key, chunk)))
}

For examples of this, see the references section.

In summary:

  • Chunks are encrypted with a "Chunk Encryption Key"
  • The "Chunk Encryption Key" is a decrypted version of the encryption key stored in the payload header
  • The encryption method used for both chunk encryption and encryption key encryption is Blowfish, in ECB mode, with PKCS#5 padding
  • Chunks are also compressed, and should decrypt to have a valid GZip header (NB: not plain zlib/deflate, full gzip)
  • The encryption key is encrypted using the Game ID, in base 10, as a string, as the key (e.g. "123457890")

References

  • Ruby unpacker script [does not handle decrypting - decrypt.rb script in repo is designed for a slightly different header format and will not work with a "plain" Riot header]
  • Python unpacker script [unpacks ROFL file, decrypts and decompresses chunks]