Skip to content

Asset Encryption

This page describes the custom asset encryption format (known as the muast format) used to protect the game assets.

The encryption method does not apply a uniform algorithm to the entire file. Instead, it splits the file into multiple segments, applies different decryption techniques (standard AES-CBC, AES-CBC with a reduced key length, a mutated AES-CBC with altered key/IV arrays, and byte-level endian/bit inversions), and shuffles the data sequence (specifically, reversing the decrypted blocks).


Technical Concept Overview

To visualize the workflow of the decryption process, refer to the conceptual diagram below:

Decryption Flowchart

flowchart TD
    classDef plain fill:#2d3436,stroke:#b2bec3,stroke-width:2px,color:#fff;
    classDef encAes256 fill:#0984e3,stroke:#74b9ff,stroke-width:2px,color:#fff;
    classDef encAes128 fill:#00b894,stroke:#55efc4,stroke-width:2px,color:#fff;
    classDef encMutant fill:#6c5ce7,stroke:#a29bfe,stroke-width:2px,color:#fff;
    classDef reverse fill:#d63031,stroke:#fab1a0,stroke-width:2px,color:#fff;

    Start([muast File Input]) --> Split1{"Boundary Split"}

    Split1 -->|Unencrypted| PlainStart("[Plain Initial (1%)]"):::plain
    Split1 -->|Encrypted Segment| EncData{"Encrypted Blocks"}
    Split1 -->|Unencrypted| PlainEnd("[Plain Final]"):::plain

    EncData --> Half1["First Half Block"]
    EncData --> Half2["Second Half Block"]
    Half2 --> PDDEC["Extract Mid 32-Bytes\n(at final)"]

    Half2 -.Padding Shared.-> RJDEC256
    PDDEC -->|Endian Swap & NOT| P_Bytes["Puzzle Solved Parts (byteP1 / byteP2)"]

    Half1 --> RJDEC256["RJDEC\n[AES-256-CBC]"]:::encAes256
    Half2 --> RJDEC128["RJDEC\n[AES-128-CBC]"]:::encAes128

    RJDEC256 -.Parsing Info.-> HTDEC

    HTDEC_Target["HTDEC Target Extraction"] --> HTDEC["HTDEC\n[Mutated AES-256-CBC]"]:::encMutant

    RJDEC128 --> Join1{"Part Integration"}
    RJDEC256 --> Join1
    P_Bytes --> Join1
    HTDEC --> Join1

    Join1 --> Reverser["Block-Level Full Sequence Reversal"]:::reverse

    PlainStart --> FinalJoin{"Final Merge"}
    Reverser --> FinalJoin
    PlainEnd --> FinalJoin

    FinalJoin --> End([Decrypted Plaintext Output])

The core characteristic of this encryption format is the puzzle-like reconstruction and final sequence reversal (reverse). Instead of traditional sequential decryption, it decrypts different parts of the file using different keys and IVs, extracts necessary pad data dynamically, and finally reverses the decrypted block sequence before merging them with the unencrypted plain boundaries.


File Structure and Decryption Ranges

The file contains unencrypted plaintext areas at the very beginning and at the end. Only the middle section is subject to cryptographic transformation. The boundaries are calculated using predetermined percentages (typically rangeIni = 1 and rangeFin = 100).

flowchart LR
    classDef plain fill:#2d3436,stroke:#b2bec3,stroke-width:2px,color:#fff;
    classDef encrypted fill:#6c5ce7,stroke:#a29bfe,stroke-width:2px,color:#fff;
    classDef boundary fill:#ffeaa7,stroke:#fdcb6e,stroke-width:1px,color:#2d3436,font-weight:bold;

    Start([0]) --> P1["Plain Initial<br>(Unencrypted 1%)"]:::plain
    P1 --> init["initial"]:::boundary
    init --> EM["Encrypted Middle<br>(Subject to Decryption)"]:::encrypted
    EM --> fin["final"]:::boundary
    fin --> PF["Plain Final<br>(Unencrypted)"]:::plain
    PF --> End([EOF])

Boundary Computation Details

Given the file input input (excluding the 4-byte header):

  1. Base Size Calculation: \(\text{lenWithoutPadding} = \text{len}(input) - 32\)

  2. Plain Initial Boundary (initial): \(\text{initial} = \frac{\text{lenWithoutPadding} \times \text{rangeIni}}{100}\) (Typically represents the first 1% of the file size)

  3. Encrypted Block Calculation: \(\text{cipherSize} = \text{lenWithoutPadding} - \text{initial}\) \(\text{numEncrypted} = \frac{\text{rangeFin} \times (\text{cipherSize} / 16)}{100}\)

  4. Split Coordinates:

    • \(\text{half} = \text{initial} + (\text{numEncrypted} / 2) \times 16\)
    • \(\text{final} = \text{initial} + \text{numEncrypted} \times 16\)

Decryption Process and Algorithms

The decryption logic consists of 5 distinct processing blocks, executed sequentially.

1. Key and IV Definitions (Conceptual)

The algorithm operates using two primary secrets defined in Decrypt.cs (Meige.Decrypt.Main):

  • key (32 bytes): <key from Decrypt.cs>
  • vec (16 bytes): <vec from Decrypt.cs>

From these, three derived keys and IVs are extracted:

  • Key-256: The full 32-byte key.
  • Key-128: The central 16 bytes of the key (key[8:24]).
  • Mutated Key/IV (HTDEC): Formed via specific permute operations.

2. PDDEC: Mid-Block Bit Inversion Puzzle

A 32-byte block is read from [final : final + 32]. The middle 16 bytes (index [8:24]) are processed as follows:

  • Read as 4-byte integers in little-endian format.
  • Invert the endianness and apply a bitwise NOT (XOR with 0xFFFFFFFF).

This results in a reconstructed 32-byte block, split into two 16-byte halves: byteP1 and byteP2. These serve as dynamic padding/keys in subsequent steps.


3. RJDEC (Second Half): AES-128-CBC Decryption

The segment [half : final] is merged with byteP2 to form the ciphertext payload.

  • Algorithm: AES-128-CBC
  • Key: Key-128 (key[8:24])
  • IV: vec

From the decrypted output, the first 16 bytes (leftPad) and the final 16 bytes (updated byteP2) are extracted, and the rest forms the plain second-half data segment.


4. RJDEC (First Half): AES-256-CBC Decryption

The segment [initial + 16 : half] is merged with the newly extracted leftPad and byteP1.

  • Algorithm: AES-256-CBC
  • Key: Key-256 (Full 32-byte key)
  • IV: vec

Similar to the second half, the resulting output is sliced to update leftPad and byteP1, leaving the plain first-half data segment.


5. HTDEC: Mutated AES-256-CBC Decryption

The segment [initial : initial + 16] is joined with leftPad (32 bytes total) and decrypted using a customized AES variant.

Mutated Key Generation

The 32-byte key is permuted by rearranging the 8-byte chunks of the key in the sequence [1, 3, 0, 2]:

  • HTDEC Key = key[8:16] + key[24:32] + key[0:8] + key[16:24]

Mutated IV Generation

The 16-byte vec is split into four 4-byte chunks. Each chunk is endian-flipped and bitwise negated (NOT).

These mutated parameters are used to decrypt the 32-byte payload via AES-256-CBC.


6. Sequence Reversal and Merging

The decrypted parts are joined in the following order: \(\text{decryptedBlock} = \text{HTDEC Output} + \text{First Half} + \text{byteP1} + \text{Second Half}\)

The entire joined array is reversed (first byte swapped with last byte, and so on). This step is crucial and highly unique to the muast format.

Finally, the complete plaintext file is reassembled: \(\text{Plain Output} = input[0:\text{initial}] + \text{reversedBlock} + \text{byteP2} + input[\text{final}+32:]\)