11 KiB
MIB Faisanet — Login Flow
Fully reverse engineered from a captured HAR trace of a first-time device registration followed immediately by a regular login.
Key Corrections
The DH parameter names in the app's source are misleading:
| App variable | DH role | Value |
|---|---|---|
A_VALUE |
Exponent / client private key | 15635168026... (shorter) |
P_VALUE |
Prime modulus | 24103124269... (longer) |
G_VALUE |
Generator | 2 |
The session key derivation is:
shared = pow(smod, A_VALUE, P_VALUE) # NOT pow(smod, P_VALUE, A_VALUE)
sha256_hex = SHA256(str(shared)).upper()
session_key = base64(bytes.fromhex(sha256_hex))
Overview
The full login sequence consists of two phases:
| Phase | Purpose | Key used |
|---|---|---|
| Phase 1 — Device registration | First time this device+account pair is seen | DH session key from sfunc=r |
| Phase 2 — Regular login | Every subsequent login | key1/key2 (from phase 1) → second DH → new session key |
Full Flow Diagram
Client Server
| |
| [0] sfunc=r (DEFAULT_KEY) |
| { cmod, appId, routePath:S40, ... } |
|--------------------------------------------->|
| { smod, nonceGenerator, xxid, ... } |
|<---------------------------------------------|
| derive session_key_1 = DH(smod) |
| |
| [1] sfunc=n routePath:A44 (session_key_1)|
| { uname } |
|--------------------------------------------->|
| { loginType, userSalt } |
|<---------------------------------------------|
| |
| [2] sfunc=n routePath:C41 (session_key_1)| ← device registration init
| { uname, pgf03, clientSalt } |
|--------------------------------------------->|
| { key1, key2, otpTypes, fullName, ... } |
|<---------------------------------------------|
| |
| [3] sfunc=n routePath:C42 (session_key_1)| ← OTP verify (registration)
| { otp, uname, otpType } |
|--------------------------------------------->|
| { key1, key2, encryptionMethod:2, ... } |
|<---------------------------------------------|
| store key1, key2 on device |
| |
| [4] sfunc=i (key1) | ← second DH key exchange
| { cmod, appId, routePath:S40, key2 } |
|--------------------------------------------->|
| { smod, nonceGenerator, xxid, ... } |
|<---------------------------------------------|
| derive session_key_2 = DH(smod) |
| |
| [5] sfunc=n routePath:A44 (session_key_2)|
| { uname } |
|--------------------------------------------->|
| { loginType, userSalt } |
|<---------------------------------------------|
| |
| [6] sfunc=n routePath:A41 (session_key_2)| ← regular login init
| { uname, pgf03, clientSalt, requireBankData:1 }|
|--------------------------------------------->|
| { primaryOTPType, otpTypes, email, uuid, uuid2, ... }|
|<---------------------------------------------|
| |
| [7] sfunc=n routePath:P41 (session_key_2)| ← fetch profile image
| { imageHash } |
|--------------------------------------------->|
| { profileImage (base64 JPEG) } |
|<---------------------------------------------|
| |
| [8] sfunc=n routePath:A42 (session_key_2)| ← OTP verify (regular login)
| { otp, uname, otpType } |
|--------------------------------------------->|
| { ... session established ... } |
|<---------------------------------------------|
Step-by-Step Reference
[0] Initial Key Exchange — sfunc=r
Key: DEFAULT_KEY = 8M3L9SBF1AC4FRE56788M3L9SBF1AC4FRE5678
Request body (inner data field):
{
"cmod": "<G^A_VALUE mod P_VALUE as decimal string>",
"appId": "IOS17.2-<15 random chars>",
"routePath": "S40",
"sodium": "<random 20-bit int>",
"xxid": "<random 40-bit int>"
}
Full request (outer wrapper, encrypted together):
{ "sfunc": "r", "data": { ...above... } }
Response (decrypted with DEFAULT_KEY):
{
"success": true,
"reasonCode": "201",
"reasonText": "Key generated successfully.",
"smod": "<server DH public key>",
"nonceGenerator": "<instruction string>",
"xxid": "<session token — carry for all subsequent calls>",
"sodium": "<server random>",
"encMethod": 2
}
After this step:
- Derive
session_key_1 = derive_session_key(smod) - Save
xxidandnonceGenerator
[1] Get Auth Type — sfunc=n, routePath: A44
Key: session_key_1
Request (encrypted):
{
"sfunc": "n",
"xxid": "<session xxid>",
"data": {
"uname": "<username>",
"nonce": "<computed from nonceGenerator>",
"appId": "<appId>",
"sodium": "<random 20-bit int>",
"routePath": "A44",
"xxid": "<session xxid>"
}
}
Response:
{
"success": true,
"reasonCode": "108",
"reasonText": "Auth type retrieved!",
"data": [
{
"loginType": "1",
"userSalt": "<server salt for password hashing>"
}
]
}
[2] Device Registration Init — sfunc=n, routePath: C41
First-time only. Registers this device+account pair.
Key: session_key_1
Request:
{
"sfunc": "n",
"xxid": "<session xxid>",
"data": {
"uname": "<username>",
"pgf03": "<salted password hash — see below>",
"clientSalt": "<random 32-char string>",
"nonce": "<nonce>",
"appId": "<appId>",
"sodium": "<random>",
"routePath": "C41",
"xxid": "<session xxid>"
}
}
Response:
{
"success": true,
"reasonCode": "201",
"reasonText": "Registration Initialization Successfully.",
"primaryOTPType": "3",
"roleName": "Consumer Premium",
"otpTypes": [2, 3],
"fullName": "<user's full name>",
"lastLoginTime": "<datetime>",
"customerImgHash": "<hash>"
}
[3] OTP Verification (Registration) — sfunc=n, routePath: C42
Key: session_key_1
Request:
{
"sfunc": "n",
"xxid": "<session xxid>",
"data": {
"otp": "<6-digit OTP>",
"uname": "<username>",
"otpType": "3",
"nonce": "<nonce>",
"appId": "<appId>",
"sodium": "<random>",
"routePath": "C42",
"xxid": "<session xxid>"
}
}
Response:
{
"success": true,
"reasonCode": "101",
"reasonText": "registration success",
"data": [
{
"appId": "<appId>",
"createdDate": "<datetime>",
"key1": "<device credential 1 — store securely>",
"key2": "<device credential 2 — store securely>",
"encryptionMethod": "2",
"appAgent": "android/1.0"
}
]
}
key1 and key2 are long-lived device credentials. key1 is the Blowfish key
for the next sfunc=i call. key2 is sent as plaintext in the outer wrapper of
that call.
[4] Authenticated Key Exchange — sfunc=i
Second DH exchange, authenticated with the device credentials.
Key: key1
Request (outer wrapper includes key2):
{
"sfunc": "i",
"key2": "<key2 from registration>",
"data": {
"cmod": "<G^A_VALUE mod P_VALUE>",
"appId": "<appId>",
"routePath": "S40",
"sodium": "<random 20-bit int>",
"xxid": "<random 40-bit int>"
}
}
Response (decrypted with key1):
{
"success": true,
"reasonCode": "201",
"reasonText": "Key generated successfully.",
"smod": "<new server DH public key>",
"nonceGenerator": "<new instruction string>",
"xxid": "<new session token>",
"encMethod": 2
}
After this step:
- Derive
session_key_2 = derive_session_key(smod) - Replace
xxidandnonceGeneratorwith new values
[5] Get Auth Type — sfunc=n, routePath: A44
Same as step [1] but with session_key_2. Fetches userSalt for password hashing.
[6] Regular Login Init — sfunc=n, routePath: A41
Key: session_key_2
Request:
{
"sfunc": "n",
"xxid": "<session xxid>",
"data": {
"uname": "<username>",
"pgf03": "<salted password hash>",
"clientSalt": "<random 32-char string>",
"pmodTime": 0,
"requireBankData": 1,
"nonce": "<nonce>",
"appId": "<appId>",
"sodium": "<random>",
"routePath": "A41",
"xxid": "<session xxid>"
}
}
Response:
{
"success": true,
"reasonCode": "104",
"reasonText": "Initialization Successful",
"primaryOTPType": "3",
"roleName": "Consumer Premium",
"otpTypes": [2, 3],
"email": "<masked email>",
"uuid": "<uuid1>",
"uuid2": "<uuid2>",
"xxid": "<xxid>"
}
[7] Get Profile Image — sfunc=n, routePath: P41
Key: session_key_2
Request:
{
"sfunc": "n",
"xxid": "<session xxid>",
"data": {
"imageHash": "<customerImgHash from step 2/6>",
"nonce": "<nonce>",
"appId": "<appId>",
"sodium": "<random>",
"routePath": "P41",
"xxid": "<session xxid>"
}
}
Response:
{
"success": true,
"reasonCode": "201",
"reasonText": "Image Found",
"profileImage": "<base64 JPEG>"
}
[8] OTP Verification (Login) — sfunc=n, routePath: A42
Key: session_key_2
Request:
{
"sfunc": "n",
"xxid": "<session xxid>",
"data": {
"otp": "<6-digit OTP>",
"uname": "<username>",
"otpType": "3",
"nonce": "<nonce>",
"appId": "<appId>",
"sodium": "<random>",
"routePath": "A42",
"xxid": "<session xxid>"
}
}
Password Hashing (pgf03)
The password is never sent in plaintext. The scheme prevents replay attacks by mixing in a server-provided salt and a client-generated random salt.
pgf03 = SHA256( clientSalt + SHA256( userSalt + SHA256( password ) ) )
All SHA256 values are uppercase hex strings.
import hashlib
def compute_pgf03(password: str, user_salt: str, client_salt: str) -> str:
h1 = hashlib.sha256(password.encode()).hexdigest().upper()
h2 = hashlib.sha256((user_salt + h1).encode()).hexdigest().upper()
h3 = hashlib.sha256((client_salt + h2).encode()).hexdigest().upper()
return h3
Route Paths Summary
| routePath | sfunc | Description |
|---|---|---|
S40 |
r or i |
DH key exchange |
A44 |
n |
Get auth type / userSalt |
A41 |
n |
Regular login initialization |
A42 |
n |
OTP verification (regular login) |
C41 |
n |
Device registration initialization |
C42 |
n |
OTP verification (registration) |
P41 |
n |
Get profile image |
P40 |
n |
Update profile image |
P42 |
n |
Delete profile image |