8.7 KiB
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)
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_VALUEin source is the exponent (shorter number);P_VALUEis the prime modulus (longer number).
G (generator) = 2
A (client privkey) = 1563516802667282387226490351799736881442299778484610378722158765594241028592123324764949712696577
P (prime modulus) = 2410312426921032588552076022197566074856950548502459942654116941958108831682612228890093858261341614673227141477904012196503648957050582631942730706805009223062734745341073406696246014589361659774041027169249453200378729434170325843778659198143763193776859869524088940195577346119843545301547043747207749969763750084308926339295559968882457872412993810129130294592999947926365264059284647209730384947211681434464714438488520940127459844288859336526896320919633919
Steps
- Client sends
cmod = G^A mod P(as decimal string) in thesfunc=rrequest - Server responds with
smod(its DH public key, as decimal string) - Client computes shared secret:
shared = smod^A mod P - Client SHA-256 hashes
str(shared)→ uppercase hex - Client converts the hex string to raw bytes, then base64-encodes → session key
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:
- Extract the number from
token[0](e.g.M85→85) - Generate random
r = floor(random() * 99) + 1(range 1–99 inclusive) product = N * r→ zero-pad to 5 digitsdigitSum = sum of digits of paddedlastTwo = int(padded[-2:])(last two digits)- Accumulate
cumSum += digitSum
Phase 2 — nonce digits (tokens 1–7 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
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
nonceandsodiumare separate fields.sodiumis an independent random integer (~23–24 bit range).- The
nonceGeneratoris 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 |
Next → Login Flow