6.1 KiB
MIB Faisanet API — Encryption & Decryption
Overview
All API traffic is encrypted using Blowfish in ECB mode with PKCS5 padding. Every request and response body is a single base64-encoded Blowfish ciphertext.
There are two keys in play:
| Key | Used for |
|---|---|
DEFAULT_KEY (hardcoded) |
The initial key exchange request and response (sfunc=r) |
| Session key (DH-derived) | Every request and response after the key exchange |
The DEFAULT_KEY
8M3L9SBF1AC4FRE56788M3L9SBF1AC4FRE5678
This key is hardcoded in the app's JavaScript bundle. It is only used for the
first call (sfunc=r) which establishes a session key via Diffie-Hellman.
Session Key Derivation (Diffie-Hellman)
The app uses a custom DH key exchange to derive a per-session Blowfish key. All three DH parameters are hardcoded in the app:
G = 2
P = 1563516802667282387226490351799736881442299778484610378722158765594241028592123324764949712696577
A = 2410312426921032588552076022197566074856950548502459942654116941958108831682612228890093858261341614673227141477904012196503648957050582631942730706805009223062734745341073406696246014589361659774041027169249453200378729434170325843778659198143763193776859869524088940195577346119843545301547043747207749969763750084308926339295559968882457872412993810129130294592999947926365264059284647209730384947211681434464714438488520940127459844288859336526896320919633919
A is the client's private key. Because it is hardcoded and never rotates,
anyone with the APK can derive the session key from a captured smod.
Step-by-step
- Client computes
cmod = G^A mod Pand sends it in thesfunc=rrequest. - Server computes its own keypair and responds with
smod(its public key). - Client computes the shared secret:
shared = smod^A mod P - Client SHA-256 hashes the decimal string of the shared secret (uppercased hex).
- Client converts that hex string to raw bytes, then base64-encodes it.
- The result is the Blowfish key for the rest of the session.
import hashlib, base64
def derive_session_key(smod: int) -> str:
# A_VALUE in app = exponent (shorter), P_VALUE in app = modulus (longer)
A = 1563516802667282387226490351799736881442299778484610378722158765594241028592123324764949712696577
P = 2410312426921032588552076022197566074856950548502459942654116941958108831682612228890093858261341614673227141477904012196503648957050582631942730706805009223062734745341073406696246014589361659774041027169249453200378729434170325843778659198143763193776859869524088940195577346119843545301547043747207749969763750084308926339295559968882457872412993810129130294592999947926365264059284647209730384947211681434464714438488520940127459844288859336526896320919633919
shared = pow(smod, A, P)
sha256_hex = hashlib.sha256(str(shared).encode()).hexdigest().upper()
return base64.b64encode(bytes.fromhex(sha256_hex)).decode()
Encrypting a Request
All request payloads follow this JSON structure before encryption:
{
"sfunc": "<function code>",
"xxid": "<session token>",
"data": {
...
}
}
Encryption steps
JSON.stringifythe payload.- Use the raw UTF-8 bytes of the payload as plaintext.
- Use the raw UTF-8 bytes of the key string as the Blowfish key.
- Encrypt: Blowfish / ECB / PKCS5 padding.
- Base64-encode the ciphertext.
- URL-encode the base64 string.
- Send as form field:
sfunc=<value>&data=<url-encoded-ciphertext>
import json, base64
from urllib.parse import quote
from Crypto.Cipher import Blowfish
from Crypto.Util.Padding import pad
def encrypt(payload: dict, key: str) -> str:
plaintext = json.dumps(payload, separators=(',', ':')).encode('utf-8')
key_bytes = key.encode('latin-1')
cipher = Blowfish.new(key_bytes, Blowfish.MODE_ECB)
ct = cipher.encrypt(pad(plaintext, Blowfish.block_size))
return base64.b64encode(ct).decode()
def build_request_body(payload: dict, key: str) -> str:
sfunc = payload.get('sfunc', '')
encrypted = encrypt(payload, key)
return f"sfunc={sfunc}&data={quote(encrypted)}"
Decrypting a Response
The response body is a raw base64-encoded Blowfish ciphertext (no form encoding).
Decryption steps
- Base64-decode the response body to get the ciphertext bytes.
- Decrypt with Blowfish / ECB / PKCS5 padding using the appropriate key.
- Parse the result as JSON.
import json, base64
from Crypto.Cipher import Blowfish
from Crypto.Util.Padding import unpad
def decrypt(ciphertext_b64: str, key: str) -> dict:
key_bytes = key.encode('latin-1')
ct = base64.b64decode(ciphertext_b64)
cipher = Blowfish.new(key_bytes, Blowfish.MODE_ECB)
plaintext = unpad(cipher.decrypt(ct), Blowfish.block_size)
return json.loads(plaintext.decode('utf-8'))
Full Session Example
1. Send key exchange (sfunc=r) — use DEFAULT_KEY
Request form body:
sfunc=r&data=<base64 of Blowfish(DEFAULT_KEY, {"sfunc":"r","data":{"cmod":"...","appId":"...","routePath":"S40","sodium":"...","xxid":"..."}})>
Response (decrypted with DEFAULT_KEY):
{
"success": true,
"smod": "<large decimal integer — server DH public key>",
"nonceGenerator": "<instruction string, e.g. 'M26 C16 C4 C5 M64 ...'>",
"xxid": "<session token>",
"sodium": "<server random hex string>",
"encMethod": 2
}
2. Derive the session key from smod
session_key = derive_session_key(int(response['smod']))
# → a 44-character base64 string, e.g. "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX="
# The key is 32 bytes (256-bit SHA-256 output) encoded as base64.
3. All subsequent requests — use session key
Encrypt with session_key, decrypt responses with session_key.
Quick reference
| What | How |
|---|---|
| Cipher | Blowfish, ECB mode, PKCS5 padding |
Key for sfunc=r |
8M3L9SBF1AC4FRE56788M3L9SBF1AC4FRE5678 |
| Key for everything else | derive_session_key(smod) |
| Request encoding | JSON → encrypt → base64 → URL-encode → form field data= |
| Response encoding | base64 → decrypt → JSON |
| Key input | raw UTF-8 bytes of key string |
| Plaintext input | raw UTF-8 bytes of JSON string |