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 fieldm_ABKBinaryin 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:
- Concatenate: Join the request API path (e.g.,
/api/training/get_all) and the static crypt salt<WHCRYSC from ProjDepend.cs>(derived viaLogicCalcKeyin the client). - Encode: Convert the concatenated string to a UTF-8 byte array.
- Extract: Take the first 16 bytes of this encoded byte array to serve as the IV.
- Concatenate: Join the request API path (e.g.,
\(\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.
-
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>whenAPIUtility.ApplyingOn()is active.
- Production Connection (
-
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
SessionIDandRequestBodypresent:Hash = SHA256(SessionID + " " + Path + " " + RequestBody + " " + Salt)
- Only
SessionIDpresent (RequestBodyis empty):Hash = SHA256(SessionID + " " + Path + " " + Salt)
- Only
RequestBodypresent (SessionIDis empty):Hash = SHA256(Path + " " + RequestBody + " " + Salt)
- Both
SessionIDandRequestBodyare empty:Hash = SHA256(Path + " " + Salt)
- Both
- Variables:
-
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
2PcpGWb2EJGmnagcrVxYheader.
Step 2: Payload Compression & Encryption¶
If the endpoint requires encryption, the raw JSON payload is processed as follows:
- Zlib Compression: Compress the plain JSON string using the standard Zlib deflate algorithm.
- AES Encryption: Encrypt the compressed byte array using AES-128-CBC with the static AES Key and the dynamically generated IV (see Section 2).
- Base64 Encoding: Encode the encrypted ciphertext bytes into a Base64 string.
- Header and Payload Injection:
- Write the Base64 string to the HTTP POST Request body.
- Set the
PznxhHrYEDRrfARyBgRpheader to1.
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
PznxhHrYEDRrfARyBgRpheader to0(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
kNNHMre7jsDEHFaURYBPis0or missing: Skip decryption and decompression (Step 2). The response body is raw plain JSON. - If
kNNHMre7jsDEHFaURYBPis1: Proceed to Step 2.
Step 2: Payload Decryption & Decompression¶
If encryption is active, perform the following cryptographic transformations:
- Base64 Decoding: Decode the HTTP response body string from Base64 into encrypted ciphertext bytes.
- 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).
- 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.
-
Extract
serverTime: Locate the value ofserverTimewithin the response JSON payload. The client extracts this string using the following regular expression inMeigeWWW.cs:Example value:
2023-01-22T20:34:47 -
Construct Verification String: Concatenate the raw decrypted response JSON string and the extracted
serverTimestring directly with no space or separator. Example: -
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) -
Verify Hexadecimal Representation: Convert the computed CRC32 checksum into a lowercase hexadecimal string (since
RH_CRC_IS_HEXADECIMALis configured totrueinProjDepend.cs). Compare this hexadecimal value with the value sent in theX-Star-CCheader.- If they match, the response payload is successfully verified!
Note: The suffix
CCin the headerX-Star-CCstands 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
2PcpGWb2EJGmnagcrVxYheader value — the server does not need to validate it. - Ignore
LGEwRMs6UzzbidE8raAgon the server side entirely. - Always return plain JSON responses with
kNNHMre7jsDEHFaURYBP: 0. The only response header you must include isX-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)
}