Files
basedbank/docs/mibapi/ENCRYPTION.md

6.1 KiB

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.
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:

{
  "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>
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.
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):

{
  "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

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