450 lines
11 KiB
Markdown
450 lines
11 KiB
Markdown
# 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:
|
|
```python
|
|
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):
|
|
```json
|
|
{
|
|
"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):
|
|
```json
|
|
{ "sfunc": "r", "data": { ...above... } }
|
|
```
|
|
|
|
**Response** (decrypted with DEFAULT_KEY):
|
|
```json
|
|
{
|
|
"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 `xxid` and `nonceGenerator`
|
|
|
|
---
|
|
|
|
### [1] Get Auth Type — `sfunc=n`, `routePath: A44`
|
|
|
|
**Key**: `session_key_1`
|
|
|
|
**Request** (encrypted):
|
|
```json
|
|
{
|
|
"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**:
|
|
```json
|
|
{
|
|
"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**:
|
|
```json
|
|
{
|
|
"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**:
|
|
```json
|
|
{
|
|
"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**:
|
|
```json
|
|
{
|
|
"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**:
|
|
```json
|
|
{
|
|
"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`):
|
|
```json
|
|
{
|
|
"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`):
|
|
```json
|
|
{
|
|
"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 `xxid` and `nonceGenerator` with 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**:
|
|
```json
|
|
{
|
|
"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**:
|
|
```json
|
|
{
|
|
"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**:
|
|
```json
|
|
{
|
|
"sfunc": "n",
|
|
"xxid": "<session xxid>",
|
|
"data": {
|
|
"imageHash": "<customerImgHash from step 2/6>",
|
|
"nonce": "<nonce>",
|
|
"appId": "<appId>",
|
|
"sodium": "<random>",
|
|
"routePath": "P41",
|
|
"xxid": "<session xxid>"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response**:
|
|
```json
|
|
{
|
|
"success": true,
|
|
"reasonCode": "201",
|
|
"reasonText": "Image Found",
|
|
"profileImage": "<base64 JPEG>"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### [8] OTP Verification (Login) — `sfunc=n`, `routePath: A42`
|
|
|
|
**Key**: `session_key_2`
|
|
|
|
**Request**:
|
|
```json
|
|
{
|
|
"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.
|
|
|
|
```python
|
|
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 |
|