Skip to content

API Encryption & Verification

This page describes the API payload encryption, decryption, and verification processes essential for communicating with game servers.


1. Technical Concept & Headers

To connect with the game server, use the following mapping tables. These headers control encryption flag states, idempotency, and cryptographic signatures.

Request Headers

Client Property (in ProjDepend.cs) Actual HTTP Header Key Role / Purpose
WHCRY PznxhHrYEDRrfARyBgRp Encryption flag (1 = encrypted, missing or other = non-encrypted)
WHERHK 2PcpGWb2EJGmnagcrVxY Request signature hash (SHA-256)
WHSIK nVJuf6NNHtyez3xTRKix User Session ID
WHRTID LGEwRMs6UzzbidE8raAg Request Identifier (UUID + Session ID) for idempotency

Response Headers

Client Property (in ProjDepend.cs) Actual HTTP Header Key Role / Purpose
WHRCRY kNNHMre7jsDEHFaURYBP Response encryption flag (1 = encrypted, 0 = non-encrypted)
WHRPK X-Star-CC Response signature hash (CRC32 Checksum in hexadecimal)

2. Cryptographic Parameters

The cryptographic parameters used for API payload encryption and decryption are derived dynamically within the game client.

AES Key (Static)

  • Specification: 128-bit (16-byte) raw binary array.
  • Source: Derived from <CRYPT_AES_CBC_KEY from ProjDepend.cs> (specifically mapped to the field m_ABKBinary in the client).

AES IV (Dynamic)

The IV is dynamically derived for each endpoint to prevent replay attacks and pattern leakage.

  • Specification: 128-bit (16-byte) block.
  • Generation Steps:
    1. Concatenate: Join the request API path (e.g., /api/training/get_all) and the static crypt salt <WHCRYSC from ProjDepend.cs> (derived via LogicCalcKey in the client).
    2. Encode: Convert the concatenated string to a UTF-8 byte array.
    3. Extract: Take the first 16 bytes of this encoded byte array to serve as the IV.

\(\text{IV} = \text{First16Bytes}(\text{UTF8}(\text{Path} + \text{Salt}))\)


3. Outbound Flow: Request Generation (Client to Server)

This section details the step-by-step process of constructing, signing, and encrypting an API request.

flowchart TD
    classDef plain fill:#2d3436,stroke:#b2bec3,stroke-width:2px,color:#fff;
    classDef process fill:#e74c3c,stroke:#c0392b,color:#fff,stroke-width:2px;
    classDef data fill:#f1c40f,stroke:#f39c12,color:#333,stroke-width:2px;
    classDef highlight fill:#0984e3,stroke:#74b9ff,stroke-width:2px,color:#fff;

    Start(["Plain JSON Request Body"]) --> Step1{"Is Endpoint Encrypted?"}

    %% Non-encrypted path
    Step1 -->|No| PlainBody["Plain JSON Payload"]:::plain
    PlainBody --> SignPlain["Step 1: Compute Request Signature"]:::process
    SignPlain --> SendPlain(["Send HTTP Request<br>PznxhHrYEDRrfARyBgRp = 0"])

    %% Encrypted path
    Step1 -->|Yes| EncryptBody["Plain JSON Payload"]:::data
    EncryptBody --> SignEnc["Step 1: Compute Request Signature"]:::process
    SignEnc --> Zlib["Step 2.1: Zlib Compression"]:::process
    Zlib --> AES["Step 2.2: AES-128-CBC Encryption"]:::process
    AES --> B64["Step 2.3: Base64 Encoding"]:::process
    B64 --> SendEnc(["Send HTTP Request<br>PznxhHrYEDRrfARyBgRp = 1"]):::highlight

Step 1: Signature Generation (2PcpGWb2EJGmnagcrVxY)

Every API request is validated for integrity and authenticity using a signature hash sent in the 2PcpGWb2EJGmnagcrVxY header.

  1. Select Validation Salt: The salt differs based on the active connection endpoint:

    • Production Connection (SERVER_URL): Uses <REQUESTHASH_SECRET from ProjDepend.cs>.
    • Store Review Connection (SERVER_APPLYING_URL): Uses <REQUESTHASH_SECRET_APPLYING from ProjDepend.cs> when APIUtility.ApplyingOn() is active.
  2. Construct Base String: Concatenate the following fields in order, joining them with a single space (" ") separator. Skip optional or empty fields entirely, including their corresponding separator:

    • Variables:
      • SessionID (if present and not empty)
      • Path (e.g., /api/training/get_all)
      • RequestBody (if present and not empty, containing the raw plain JSON body)
      • Salt
    • Formulas based on field presence:
      • Both SessionID and RequestBody present:
        • Hash = SHA256(SessionID + " " + Path + " " + RequestBody + " " + Salt)
      • Only SessionID present (RequestBody is empty):
        • Hash = SHA256(SessionID + " " + Path + " " + Salt)
      • Only RequestBody present (SessionID is empty):
        • Hash = SHA256(Path + " " + RequestBody + " " + Salt)
      • Both SessionID and RequestBody are empty:
        • Hash = SHA256(Path + " " + Salt)
  3. Compute Hash: Calculate the SHA-256 hash over the constructed Base String, format the resulting bytes as a lowercase 64-character hexadecimal string, and write it to the 2PcpGWb2EJGmnagcrVxY header.

Step 2: Payload Compression & Encryption

If the endpoint requires encryption, the raw JSON payload is processed as follows:

  1. Zlib Compression: Compress the plain JSON string using the standard Zlib deflate algorithm.
  2. AES Encryption: Encrypt the compressed byte array using AES-128-CBC with the static AES Key and the dynamically generated IV (see Section 2).
  3. Base64 Encoding: Encode the encrypted ciphertext bytes into a Base64 string.
  4. Header and Payload Injection:
    • Write the Base64 string to the HTTP POST Request body.
    • Set the PznxhHrYEDRrfARyBgRp header to 1.

Step 3: Exception Handling (Non-encrypted Endpoints)

The following subset of endpoints bypasses payload compression and encryption entirely:

  • /api/app/version/get — Get latest app information
  • /api/player/login — Log-in to account
  • /api/player/signup — Create an account
  • /api/player/move/set — Transfer an account
  • /api/player/reset — Delete an account

For these endpoints, skip Step 2 entirely:

  • Send the raw plain JSON body in the POST Request.
  • Set the PznxhHrYEDRrfARyBgRp header to 0 (or omit it).

4. Inbound Flow: Response Verification & Decryption (Server to Client)

This section details the step-by-step process of parsing, decrypting, and verifying a response received from the server.

flowchart TD
    classDef plain fill:#2d3436,stroke:#b2bec3,stroke-width:2px,color:#fff;
    classDef process fill:#e74c3c,stroke:#c0392b,color:#fff,stroke-width:2px;
    classDef data fill:#f1c40f,stroke:#f39c12,color:#333,stroke-width:2px;
    classDef highlight fill:#00b894,stroke:#55efc4,stroke-width:2px,color:#fff;

    Start(["HTTP Response Received"]) --> Step1{"Check kNNHMre7jsDEHFaURYBP Header"}

    %% Decrypted path
    Step1 -->|kNNHMre7jsDEHFaURYBP = 1| Decrypt["Step 2.1: Base64 Decoding"]:::process
    Decrypt --> AES["Step 2.2: AES-128-CBC Decryption"]:::process
    AES --> Zlib["Step 2.3: Zlib Decompression"]:::process
    Zlib --> JSON["Plain JSON Payload"]:::data

    %% Plain path
    Step1 -->|kNNHMre7jsDEHFaURYBP = 0| JSON

    JSON --> Verify["Step 3: Response Signature Verification"]:::process
    Verify --> End(["Decrypted and Verified Payload"]):::highlight

Step 1: Check Encryption Flag

Before performing any decryption, read the kNNHMre7jsDEHFaURYBP header from the response.

  • If kNNHMre7jsDEHFaURYBP is 0 or missing: Skip decryption and decompression (Step 2). The response body is raw plain JSON.
  • If kNNHMre7jsDEHFaURYBP is 1: Proceed to Step 2.

Step 2: Payload Decryption & Decompression

If encryption is active, perform the following cryptographic transformations:

  1. Base64 Decoding: Decode the HTTP response body string from Base64 into encrypted ciphertext bytes.
  2. AES Decryption: Decrypt the ciphertext bytes using AES-128-CBC with the static AES Key and the dynamic IV (derived using the request API path, as defined in Section 2).
  3. Zlib Decompression: Decompress the decrypted bytes using the standard Zlib inflate algorithm to yield the plain JSON payload string.

Step 3: Response Signature Verification (X-Star-CC)

To prevent tampering of game data, verify the integrity of the response against the signature checksum sent in the X-Star-CC header.

  1. Extract serverTime: Locate the value of serverTime within the response JSON payload. The client extracts this string using the following regular expression in MeigeWWW.cs:

    "serverTime":"([-\+\d:T]+?)"
    

    Example value: 2023-01-22T20:34:47

  2. Construct Verification String: Concatenate the raw decrypted response JSON string and the extracted serverTime string directly with no space or separator. Example:

    {"message": "ok", "serverTime": "2023-01-22T20:34:47"}2023-01-22T20:34:47
    
  3. Compute Checksum: Convert the verification string into a UTF-8 byte array, and compute the standard CRC32 checksum over the bytes: Checksum = CRC32(responseJson + serverTime)

  4. Verify Hexadecimal Representation: Convert the computed CRC32 checksum into a lowercase hexadecimal string (since RH_CRC_IS_HEXADECIMAL is configured to true in ProjDepend.cs). Compare this hexadecimal value with the value sent in the X-Star-CC header.

    • If they match, the response payload is successfully verified!

Note: The suffix CC in the header X-Star-CC stands for Checksum Code.


Extra: Minimal API Server Implementation Reference

This section provides a practical reference for implementing a minimal, fully functional API server that can communicate with the game client, based on the implementation patterns found in sparkle-server.

In a production-ready server (like sparkle-server), these cryptographic operations are typically implemented as HTTP Middleware to decouple the core API logic from the encryption and verification layers.

Minimal vs. Full Implementation

Depending on your goals, you can choose how strictly to implement the full protocol. The table below summarizes which components are required and which can be deferred.

Component Header Minimal Implementation Full Implementation
Request Signature Verification 2PcpGWb2EJGmnagcrVxY Ignore — The server can process requests without reading or validating this header at all. The client always sends it, but the server is not required to act on it. Compute SHA-256 over the canonical base string and reject requests with an invalid or missing signature.
Request Idempotency Key LGEwRMs6UzzbidE8raAg Ignore — The client always sends this header, but the server is not required to track it. Simply discard it. Store seen keys per session and return a cached response for any duplicate key, preventing double-execution of mutating operations.
Response Encryption kNNHMre7jsDEHFaURYBP Always return 0 — The server can always respond with plain JSON and set kNNHMre7jsDEHFaURYBP: 0. The client reads this flag on every response and handles both 0 and 1 correctly (see Section 4). Encrypt the response body (Zlib → AES-128-CBC → raw binary) and set kNNHMre7jsDEHFaURYBP: 1 for all non-exempted endpoints.

If you want to get something running as quickly as possible, you can:

  • Send requests without caring about the 2PcpGWb2EJGmnagcrVxY header value — the server does not need to validate it.
  • Ignore LGEwRMs6UzzbidE8raAg on the server side entirely.
  • Always return plain JSON responses with kNNHMre7jsDEHFaURYBP: 0. The only response header you must include is X-Star-CC.

Important

Idempotency via LGEwRMs6UzzbidE8raAg should be implemented in any production-grade server. Without it, network retries or client-side reconnects can cause mutating operations (e.g., gacha draws, purchases) to execute multiple times, leading to inconsistent game state.

Request & Response Pipeline

The standard lifecycle for an incoming API request and its outgoing response follows this pipeline:

sequenceDiagram
    autonumber
    participant Client as Game Client
    participant DecryptMW as Decryption Middleware
    participant AuthMW as Authentication Middleware
    participant Handler as API Endpoint Handler
    participant EncryptMW as Encryption Middleware

    Client->>DecryptMW: HTTP POST (Encrypted Base64 Payload)
    Note over DecryptMW: "Check header: PznxhHrYEDRrfARyBgRp == 1"
    DecryptMW->>DecryptMW: "Base64 Decode -> AES Decrypt -> Zlib Decompress"
    DecryptMW->>AuthMW: "Plain JSON Request Body"

    Note over AuthMW: "Verify signature hash in: 2PcpGWb2EJGmnagcrVxY"
    AuthMW->>AuthMW: "Compute SHA-256(BaseString) & Compare"
    AuthMW->>Handler: "Verified & Plain Request"

    Note over Handler: "Process Game Logic & Generate JSON Response"
    Handler->>EncryptMW: "Plain JSON Response + serverTime"

    Note over EncryptMW: "Compute Checksum (X-Star-CC): CRC32(rawPlainJson + serverTime)"
    EncryptMW->>EncryptMW: "Generate X-Star-CC Header"
    Note over EncryptMW: "Zlib Compress -> AES Encrypt (Raw Binary)"
    EncryptMW->>Client: "HTTP 200 (Raw Binary Response) - Headers: kNNHMre7jsDEHFaURYBP=1, X-Star-CC=[hash]"

Core Cryptographic Helpers

Below are conceptual, clean implementations (written in Go-like syntax inspired by sparkle_cipher.go) demonstrating how to implement the core functions while keeping keys and salts parameterized.

Important: To comply with security guidelines, all cryptographic keys, dynamic salts, and secrets must be loaded securely (e.g., via environment variables) and should never be hardcoded into the source code.

Dynamic Initialization & IV Derivation

The Initialization Vector (IV) must be dynamically derived for each endpoint path using a static salt.

package encrypt

import (
    "bytes"
    "compress/zlib"
    "crypto/aes"
    "crypto/cipher"
    "encoding/base64"
    "encoding/hex"
    "fmt"
    "hash/crc32"
    "io"
)

type ApiCipher struct {
    aesKey []byte
    ivSalt []byte
}

func NewApiCipher(hexKey string, saltStr string) *ApiCipher {
    keyBytes, _ := hex.DecodeString(hexKey)
    return &ApiCipher{
        aesKey: keyBytes,
        ivSalt: []byte(saltStr),
    }
}

func (c *ApiCipher) DeriveIV(path string) []byte {
    iv := append([]byte(path), c.ivSalt...)
    if len(iv) > 16 {
        return iv[:16]
    }
    padded := make([]byte, 16)
    copy(padded, iv)
    return padded
}

Request Decryption Pipeline

Decryption takes the Base64-encoded encrypted request body and restores the plain JSON string.

func (c *ApiCipher) DecryptRequest(path string, encryptedBase64 string) ([]byte, error) {
    ciphertext, err := base64.StdEncoding.DecodeString(encryptedBase64)
    if !(err == nil) {
        return nil, err
    }

    iv := c.DeriveIV(path)

    block, err := aes.NewCipher(c.aesKey)
    if !(err == nil) {
        return nil, err
    }
    mode := cipher.NewCBCDecrypter(block, iv)

    decrypted := make([]byte, len(ciphertext))
    mode.CryptBlocks(decrypted, ciphertext)

    unpadded, err := pkcs7Unpad(decrypted)
    if !(err == nil) {
        return nil, err
    }

    return zlibDecompress(unpadded)
}

Outbound Response Encryption

Unlike the request payload, the encrypted response payload is sent as raw binary bytes (not Base64 encoded) to the client.

func (c *ApiCipher) EncryptResponse(path string, rawPlainJson []byte) ([]byte, error) {
    compressed, err := zlibCompress(rawPlainJson)
    if !(err == nil) {
        return nil, err
    }

    padded := pkcs7Pad(compressed, 16)

    iv := c.DeriveIV(path)

    block, err := aes.NewCipher(c.aesKey)
    if !(err == nil) {
        return nil, err
    }
    mode := cipher.NewCBCEncrypter(block, iv)

    ciphertext := make([]byte, len(padded))
    mode.CryptBlocks(ciphertext, padded)

    return ciphertext, nil
}

Response Checksum (X-Star-CC) Code Generation

The response checksum ensures data integrity. It is computed as a lowercase hexadecimal representation of a CRC32 checksum.

func ComputeResponseChecksum(rawPlainJson []byte, serverTime string) string {
    hashBytes := append(rawPlainJson, []byte(serverTime)...)
    checksum := crc32.ChecksumIEEE(hashBytes)
    return fmt.Sprintf("%08x", checksum)
}

Supporting Helper Functions

Since Go's standard library does not provide built-in PKCS#7 padding or ready-to-use inline zlib helpers, you should include the following code in your package:

func pkcs7Pad(data []byte, blockSize int) []byte {
    padding := blockSize - (len(data) % blockSize)
    padText := bytes.Repeat([]byte{byte(padding)}, padding)
    return append(data, padText...)
}

func pkcs7Unpad(data []byte) ([]byte, error) {
    length := len(data)
    if length == 0 {
        return nil, fmt.Errorf("empty data")
    }
    padding := int(data[length-1])
    if padding < 1 || padding > 16 {
        return nil, fmt.Errorf("invalid padding range")
    }
    for i := length - padding; i < length; i++ {
        if !(int(data[i]) == padding) {
            return nil, fmt.Errorf("invalid padding character")
        }
    }
    return data[:length-padding], nil
}

func zlibCompress(data []byte) ([]byte, error) {
    var buf bytes.Buffer
    writer := zlib.NewWriter(&buf)
    _, err := writer.Write(data)
    if !(err == nil) {
        return nil, err
    }
    writer.Close()
    return buf.Bytes(), nil
}

func zlibDecompress(data []byte) ([]byte, error) {
    reader, err := zlib.NewReader(bytes.NewReader(data))
    if !(err == nil) {
        return nil, err
    }
    defer reader.Close()
    return io.ReadAll(reader)
}