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

8.7 KiB
Raw Blame History

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_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
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. M8585)
  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

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

 


Next → Login Flow