181 lines
6.1 KiB
Markdown
181 lines
6.1 KiB
Markdown
# 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
|
|
|
|
1. Client computes `cmod = G^A mod P` and sends it in the `sfunc=r` request.
|
|
2. Server computes its own keypair and responds with `smod` (its public key).
|
|
3. Client computes the shared secret: `shared = smod^A mod P`
|
|
4. Client SHA-256 hashes the decimal string of the shared secret (uppercased hex).
|
|
5. Client converts that hex string to raw bytes, then base64-encodes it.
|
|
6. The result is the Blowfish key for the rest of the session.
|
|
|
|
```python
|
|
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:
|
|
|
|
```json
|
|
{
|
|
"sfunc": "<function code>",
|
|
"xxid": "<session token>",
|
|
"data": {
|
|
...
|
|
}
|
|
}
|
|
```
|
|
|
|
### Encryption steps
|
|
|
|
1. `JSON.stringify` the payload.
|
|
2. Use the raw UTF-8 bytes of the payload as plaintext.
|
|
3. Use the raw UTF-8 bytes of the key string as the Blowfish key.
|
|
4. Encrypt: Blowfish / ECB / PKCS5 padding.
|
|
5. Base64-encode the ciphertext.
|
|
6. URL-encode the base64 string.
|
|
7. Send as form field: `sfunc=<value>&data=<url-encoded-ciphertext>`
|
|
|
|
```python
|
|
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
|
|
|
|
1. Base64-decode the response body to get the ciphertext bytes.
|
|
2. Decrypt with Blowfish / ECB / PKCS5 padding using the appropriate key.
|
|
3. Parse the result as JSON.
|
|
|
|
```python
|
|
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):
|
|
```json
|
|
{
|
|
"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`
|
|
|
|
```python
|
|
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 |
|