Files
thijooree/docs/mibapi/01-encryption.md
Shihaam Abdul Rahman 256f216da4
All checks were successful
Auto Tag on Version Change / check-version (push) Successful in 4s
update docs
2026-05-23 23:46:00 +05:00

235 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Encryption, Key Exchange & Nonce
All traffic to the encrypted API (`faisanet.mib.com.mv`) uses Blowfish encryption. This document covers the cipher, the DH key exchange that derives the session key, and the nonce algorithm required by every request.
---
## Cipher
- **Algorithm**: Blowfish, ECB mode, PKCS5 padding
- **Input**: raw UTF-8 bytes of the JSON payload string
- **Key**: raw UTF-8 bytes of the key string
- **Output**: base64-encoded ciphertext (URL-encoded when sent as form data)
```python
from Crypto.Cipher import Blowfish
from Crypto.Util.Padding import pad, unpad
import base64, json
from urllib.parse import quote
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 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'))
def build_request_body(payload: dict, key: str, extra_fields: dict = {}) -> str:
sfunc = payload.get('sfunc', '')
encrypted = encrypt(payload, key)
body = '&'.join(f"{k}={v}" for k, v in extra_fields.items())
if body:
return f"{body}&sfunc={sfunc}&data={quote(encrypted)}"
return f"sfunc={sfunc}&data={quote(encrypted)}"
```
---
## Request / Response Transport
**Request** — form field `data`:
```
sfunc=<value>&data=<url-encoded base64 Blowfish ciphertext>
```
For `sfunc=n` calls, `xxid` must be the **first** field:
```
xxid=<session_xxid>&sfunc=n&data=<encrypted>
```
For `sfunc=i` calls, `key2` is a separate unencrypted field:
```
key2=<key2>&sfunc=i&data=<encrypted>
```
**Response** — body is raw base64 Blowfish ciphertext (no form encoding); base64-decode then decrypt directly.
---
## Keys
| Key | Value | Used for |
|---|---|---|
| `DEFAULT_KEY` | `8M3L9SBF1AC4FRE56788M3L9SBF1AC4FRE5678` | `sfunc=r` key exchange request/response |
| Session key | DH-derived (44-char base64, 32 bytes) | All subsequent requests |
---
## Diffie-Hellman Session Key Derivation
The session key is derived via a custom DH exchange. All three parameters are hardcoded in the app — this provides no real security since the private key `A` never rotates.
> **Note**: The variable names in the app source are **swapped** from their DH role. `A_VALUE` in source is the exponent (shorter number); `P_VALUE` is the prime modulus (longer number).
```
G (generator) = 2
A (client privkey) = 1563516802667282387226490351799736881442299778484610378722158765594241028592123324764949712696577
P (prime modulus) = 2410312426921032588552076022197566074856950548502459942654116941958108831682612228890093858261341614673227141477904012196503648957050582631942730706805009223062734745341073406696246014589361659774041027169249453200378729434170325843778659198143763193776859869524088940195577346119843545301547043747207749969763750084308926339295559968882457872412993810129130294592999947926365264059284647209730384947211681434464714438488520940127459844288859336526896320919633919
```
### Steps
1. Client sends `cmod = G^A mod P` (as decimal string) in the `sfunc=r` request
2. Server responds with `smod` (its DH public key, as decimal string)
3. Client computes shared secret: `shared = smod^A mod P`
4. Client SHA-256 hashes `str(shared)` → uppercase hex
5. Client converts the hex string to raw bytes, then base64-encodes → session key
```python
import hashlib, base64
def derive_session_key(smod: int) -> str:
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()
```
The result is always a **44-character base64 string** (32 bytes). It changes every session.
---
## Nonce Computation
Every request after key exchange includes a `nonce` field. It is computed from the `nonceGenerator` string returned by the key exchange response.
### `nonceGenerator` format
A string of 4 groups separated by `-`. Each group contains 8 space-separated tokens. Each token is a letter followed by a number (e.g. `M85`, `A37`, `C95`, `X2`).
```
M85 A87 A82 M82 M60 M31 A46 C95-M14 X83 A37 X2 C4 X22 X46 C95-M57 X29 C51 C34 S91 X60 S1 A15-M54 A89 S13 S18 C81 A70 X92 X59
```
### Nonce output format
4 groups separated by `-`. Each group: a zero-padded 5-digit seed followed by 7 two-digit numbers separated by spaces.
```
08160 19 73 45 17 89 07 10-00924 64 73 18 08 48 80 67-01026 20 17 13 26 26 43 24-00648 12 32 17 69 14 63 92
```
### Algorithm
**Phase 1 — seed values (one per group):**
For each of the 4 groups:
1. Extract the number from `token[0]` (e.g. `M85``85`)
2. Generate random `r = floor(random() * 99) + 1` (range 199 inclusive)
3. `product = N * r` → zero-pad to 5 digits
4. `digitSum = sum of digits of padded`
5. `lastTwo = int(padded[-2:])` (last two digits)
6. Accumulate `cumSum += digitSum`
**Phase 2 — nonce digits (tokens 17 of each group):**
For each group, start with `carry = lastTwo[i]`:
| op letter | Formula |
|---|---|
| `M` | `(carry % num) + digitSum + cumSum` |
| `A` | `carry + num + digitSum + cumSum` |
| `S` | `(carry * carry) + num + digitSum + cumSum` |
| `X` | `(carry * num) + digitSum + cumSum` |
| `C` | `(carry * carry * carry) + num + digitSum + cumSum` |
Nonce digit = last two digits of the result. Update `carry = nonceDigit` for the next token.
**Output**: join padded seed + 7 two-digit nonce digits per group, join 4 groups with `-`.
### Python implementation
```python
import math, random
def generate_nonce(nonce_generator: str) -> str:
groups = nonce_generator.split('-')
padded_list, last_two, digit_sum = [], [], []
cum_sum = 0
for group in groups:
tokens = group.split(' ')
n = int(''.join(c for c in tokens[0] if c.isdigit()))
r = math.floor(random.random() * 99) + 1
product = n * r
padded = str(product).zfill(5)
ds = sum(int(d) for d in padded)
lt = int(padded[-2:])
padded_list.append(padded)
last_two.append(lt)
digit_sum.append(ds)
cum_sum += ds
result_groups = []
for i, group in enumerate(groups):
tokens = group.split(' ')
carry = last_two[i]
ds = digit_sum[i]
nonce_digits = []
for token in tokens[1:]:
op = ''.join(c for c in token if c.isalpha())
num = int(''.join(c for c in token if c.isdigit()))
if op == 'M': val = (carry % num) + ds + cum_sum
elif op == 'A': val = carry + num + ds + cum_sum
elif op == 'S': val = (carry * carry) + num + ds + cum_sum
elif op == 'X': val = (carry * num) + ds + cum_sum
elif op == 'C': val = (carry * carry * carry) + num + ds + cum_sum
else: val = 0
digit = int(str(val)[-2:])
nonce_digits.append(digit)
carry = digit
group_str = padded_list[i] + ' ' + ' '.join(str(d).zfill(2) for d in nonce_digits)
result_groups.append(group_str)
return '-'.join(result_groups)
```
### Notes
- `nonce` and `sodium` are separate fields. `sodium` is an independent random integer (~2324 bit range).
- The `nonceGenerator` is returned once by the key exchange response and reused for the entire session.
---
## Known `sfunc` / `routePath` Values
| `sfunc` | `routePath` | Description |
|---|---|---|
| `r` | `S40` | Initial DH key exchange (DEFAULT_KEY) |
| `i` | `S40` | Authenticated DH key exchange (key1/key2) |
| `n` | `A44` | Get auth type / `userSalt` |
| `n` | `A41` | Regular login initialization |
| `n` | `A42` | OTP verification (regular login) |
| `n` | `C41` | Device registration initialization |
| `n` | `C42` | OTP verification (registration) |
| `n` | `P41` | Get profile image (by hash) |
| `n` | `P40` | Update profile image |
| `n` | `P42` | Delete profile image |
| `n` | `P47` | Select profile / fetch account balances |
---
&nbsp;
---
**Next →** [Login Flow](02-login.md)