This commit is contained in:
@@ -1,180 +0,0 @@
|
||||
# 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 |
|
||||
Reference in New Issue
Block a user