update docs

This commit is contained in:
2026-05-30 19:00:57 +05:00
parent 1d2cd40b3c
commit a5124096d7
6 changed files with 284 additions and 1 deletions
+247
View File
@@ -0,0 +1,247 @@
# Transfer Flows
The transfer screen (`TransferFragment`) handles all outgoing payments across MIB, BML, and Fahipay. This document covers how the UI routes transfers, how recipients are looked up, which combinations are allowed, and which are rejected.
---
## Entry Points
`TransferFragment` can be launched in several modes depending on context:
| Factory method | Behaviour |
|---|---|
| `newInstance(account, name, ...)` | Pre-fills the "To" card from a contact or recents pick |
| `newInstanceFrom(account)` | Pre-selects the given account in the "From" dropdown |
| `newInstanceFromQr(account, name, amount, remarks)` | Pre-fills recipient + optional amount/remarks from a PayMV QR scan |
| `newInstanceFromBmlQr(qrUrl, fromAccountNumber?)` | BML card/gateway QR merchant payment mode — locks recipient, may pre-fill amount |
| `newInstanceWithAutoScan()` | Opens the QR scanner immediately on load |
---
## Account Input Detection
The raw "To" field input is normalised first (spaces stripped, `+960`/`960` country prefix removed if the result is 7 digits), then classified:
| Pattern | Type |
|---|---|
| Starts with `9`, exactly 17 digits | `MIB_ACCOUNT` |
| Starts with `7`, exactly 13 digits | `BML_ACCOUNT` |
| Starts with `7` or `9`, exactly 7 digits | `PHONE` |
| Starts with `A` followed by 6 digits | `NATIONAL_ID` |
| Contains `@` | `EMAIL` |
| Anything else | `UNKNOWN` |
---
## Recipient Lookup
Lookup behaviour depends on the **source account's bank**.
### Fahipay source
Only `PHONE` input is accepted. Any other type is rejected immediately with an error on the "To" field.
Phone lookup hits both Dhiraagu and Ooredoo in parallel (order depends on the first digit):
- Numbers starting with `7`: Dhiraagu first, Ooredoo fallback
- Numbers starting with `9`: Ooredoo first, Dhiraagu fallback
The result maps to one or more Fahipay services:
| Carrier result | Service shown |
|---|---|
| Dhiraagu `RELOAD` | Dhiraagu Reload |
| Dhiraagu `BILL_PAY` | Dhiraagu Bill Pay |
| Ooredoo `PRE` or `HYBRID` | Raastas (prepaid top-up) |
| Ooredoo `POST` or `HYBRID` | Ooredoo Bill Pay |
If exactly one service matches, it is auto-selected. If multiple match (Ooredoo `HYBRID` gives two), a chip group is shown for the user to choose.
### BML source
1. If the input type is `MIB_ACCOUNT`, calls `BmlValidateClient.verifyMibAccount()`.
2. Otherwise calls `BmlValidateClient.validateAccount()`.
3. If either BML call fails and a MIB session is available, falls back to `MibTransferClient.lookup()`.
4. If both fail, shows the error from the MIB lookup (or a generic "account not found").
There is also a short-circuit: if the input matches a saved contact whose `transferCyDesc` is not `MVR`, the contact is used directly without a network lookup.
### MIB source
Calls `MibTransferClient.lookup()` directly. Errors from `MibLookupException` are shown verbatim to the user.
### BML-only session (no MIB session)
Falls back to `BmlValidateClient.validateAccount()` only.
---
## Transfer Type Routing
Once the source and destination are resolved, the transfer type is determined as follows. This applies for both BML personal (`doBmlTransfer`) and BML business (`startBmlBusinessOtpFlow`) — the routing logic is identical.
```
Source: BML
├── isSrcCard (BML_PREPAID / BML_CREDIT / BML_DEBIT)
│ └── type = CAD creditAccount = dest BML CASA internalId (or dest account number)
├── isDestMyCard (destination is user's own BML card)
│ └── type = CPA creditAccount = card internalId
├── isDestMib && currency == MVR
│ └── type = DOT creditAccount = MIB account number bank = "MIB"
├── isDestMib && currency == USD
│ └── Requires a saved BML contact for that MIB account (see Rejections)
│ type = DOT creditAccount = contact.benefNo (numeric) bank = null
└── everything else (BML → BML CASA, BML → other local bank)
└── type = IAT creditAccount = dest account number
```
```
Source: MIB
├── isDestMib (17-digit 9… account)
│ └── bankNo = 2 endpoint = transferInternal
└── everything else (BML or other local bank)
└── bankNo = 3 endpoint = transferLocal
```
```
Source: Fahipay
└── Routed to the selected service:
FAHIPAY_TRANSFER, RAASTAS, OOREDOO_BILL, DHIRAAGU_RELOAD, DHIRAAGU_BILL
```
---
## Rejected Combinations
These combinations are blocked before a transfer is attempted.
### BML USD → MIB (no saved contact)
**Condition:** source is BML, currency is USD, destination is a MIB account, and no BML contact exists for that account number.
**Result:** dialog shown — "Contact required". The user must first add the MIB account as a BML contact before a USD cross-bank transfer can proceed.
> This is enforced in `initiateTransfer()` before reaching `doBmlTransfer`.
---
### BML QR payment — non-card source
**Condition:** in BML QR merchant payment mode and the user selects a non-card account (i.e. not `BML_PREPAID`, `BML_CREDIT`, or `BML_DEBIT`) from the "From" dropdown.
**Result:** selection is rejected with a toast: "Unsupported for BML QR — select a card". The dropdown resets.
---
### Fahipay — non-phone destination
**Condition:** source is Fahipay and the input type is anything other than `PHONE`.
**Result:** inline error on the "To" field: "Only phone numbers are supported for Fahipay transfers."
---
### No source account selected
**Condition:** user taps the lookup button or the transfer button without selecting a "From" account.
**Result:** toast: "Please select a source account first."
---
### Inactive BML card as source
**Condition:** a BML card (`BML_PREPAID`, `BML_CREDIT`, `BML_DEBIT`) with `statusDesc != "Active"` appears in the dropdown but is not selectable — `getAccount()` returns `null` for it and `isEnabled()` returns `false`.
**Result:** the row is shown at 40% opacity and cannot be tapped.
---
### Missing internalId
**Condition:** a BML source account has a blank `internalId` (needed as the `debitAccount` in BML API calls).
**Result:** transfer is aborted with a toast: "Missing internal account ID — please refresh your accounts."
---
## Warnings (allowed but flagged)
These combinations proceed after user confirmation but show a prominent red warning in the confirm dialog.
### USD source → MVR destination
> "You are transferring from a USD account to an MVR account. The currency will be converted at the bank's rate and this cannot be reversed!"
**Condition:** `src.currencyName == "USD"` and the resolved destination account's currency is `MVR`.
---
### BML credit card as source
> "Transferring from a credit card is treated as a cash advance. Cash advance fees will be charged on the 10th of the month."
**Condition:** `src.profileType == "BML_CREDIT"`.
---
## BML Business Profile OTP Flow
Business profiles use a manual OTP delivered via email or SMS rather than a TOTP seed. The flow replaces the standard single-step confirm:
1. **Initiate**`startBmlBusinessOtpFlow()` calls `BmlAccountClient.fetchTransferChannels()` to list available channels (email, SMS).
2. **Channel selection** — a channel picker is shown inline. Transfer fields are locked (dimmed, disabled).
3. **Initiate with channel**`BmlTransferClient.initiateTransfer()` is called with the chosen channel, which triggers the OTP dispatch.
4. **OTP entry** — an OTP input field appears. The transfer button label changes to "Verify Payment".
5. **Confirm**`BmlTransferClient.confirmTransfer()` is called with the entered OTP (not a generated TOTP).
If channel fetch fails or returns empty, the flow is aborted and the form is re-enabled.
**Profile detection:** `isBusinessProfile()` checks `bmlProfilesMap[loginId]` for a profile entry matching `src.profileId` with `profileType == "business"`.
---
## BML QR Merchant Payment Flow
Triggered when the transfer screen is opened via `newInstanceFromBmlQr()` or when a BML ebanking/pay.bml URL is scanned from the QR scanner.
Two sub-modes:
| Mode | Trigger | Extra step |
|---|---|---|
| Static card QR | URL starts with `https://ebanking.bankofmaldives.com.mv/qrpay/` | None |
| Gateway QR | URL starts with `https://pay.bml.com.mv/app/` | `BmlQrPayClient.preInitiatePayment()` required before initiate |
Flow:
1. `lookupBmlQrMerchant()` — fetches merchant info via `BmlQrPayClient.lookupPayRequest()`. Locks the "To" row.
2. For dynamic QRs (`info.amount > 0`), pre-fills the amount and locks the amount field.
3. Remarks field is locked (not applicable for merchant payments).
4. On confirm: TOTP is generated, then `initiatePayment()` → (for gateway QR: `preInitiatePayment()` first) → `confirmPayment()` with a fresh TOTP.
5. On success: a success dialog is shown (no receipt saved). Back-press returns to previous screen.
---
## Transfer Button Enable Conditions
The transfer button is only enabled when all of the following are true:
- A source account is selected
- A recipient is resolved (`resolvedAccountNumber` not blank, or `bmlQrInfo` is set)
- Amount is greater than `0`
- No connectivity error for `NO_INTERNET` or for the source bank
---
 
---
[Back to app docs index](README.md)
+375
View File
@@ -0,0 +1,375 @@
# BasedBank — AI Security Audit Report
> **Generated by:** Claude Sonnet 4.6
> **Date:** 2026-05-17
> **Scope:** Full source audit — credential storage, lock screen, network layer, all outbound connections, data privacy, manifest, dependencies, backup rules.
> **Context:** This app is built from reverse-engineered protocol implementations of the official MIB (Faisanet), BML, and Fahipay Android apps. Any cryptographic weaknesses that are part of those upstream protocols are inherited constraints, not design flaws in this codebase.
---
## Summary
| Category | Rating | Notes |
|---|---|---|
| Credential storage at rest | **Excellent** | AES-256-GCM, Android Keystore |
| Lock screen hashing | **Excellent** | PBKDF2-HMAC-SHA256, 100k iterations |
| Screen capture protection | **Good** | FLAG_SECURE on by default |
| Backup exclusion | **Excellent** | All sensitive prefs excluded |
| Manifest attack surface | **Excellent** | Only MainActivity exported |
| Third-party data leakage | **Low** | No analytics SDKs; phone numbers sent to Ooredoo/Dhiraagu for validation |
| Certificate pinning | **Missing** | Biggest actionable gap |
| Brute-force resistance | **Weak** | 5 attempts / 30s — too short |
| Lockout bypass (rooted device) | **Possible** | Counter stored in plain prefs |
| Merchant icon URL fetch | **Medium** | Arbitrary URLs from server — no domain validation |
| MIB transport crypto | **Upstream constraint** | Blowfish/ECB is the MIB protocol |
| Code obfuscation | **N/A** | Open-source app, not applicable |
---
## 1. All Outbound Connections
The following is the complete list of every server the app may contact. There is zero telemetry, analytics, or crash reporting.
### MIB (Maldives Islamic Bank — Faisanet)
| URL | Purpose |
|---|---|
| `https://faisanet.mib.com.mv/faisamobilex_smvc/` | All MIB API calls (login, accounts, transfer, contacts) |
| `https://faisamobilex-wv.mib.com.mv/ajaxAccounts/trxHistory` | MIB transaction history (WebView cookie auth) |
| `https://faisamobilex-wv.mib.com.mv/personalProfile` | MIB profile page (HTML scrape) |
### BML (Bank of Maldives)
| URL | Purpose |
|---|---|
| `https://www.bankofmaldives.com.mv/internetbanking/web/login` | BML web login (session cookie acquisition) |
| `https://www.bankofmaldives.com.mv/internetbanking/web/login/2fa` | BML 2FA / OTP submission |
| `https://www.bankofmaldives.com.mv/internetbanking/web/profile` | BML profile (sets blaze_identity cookie) |
| `https://www.bankofmaldives.com.mv/internetbanking/oauth/authorize` | PKCE OAuth authorization |
| `https://www.bankofmaldives.com.mv/internetbanking/oauth/token` | OAuth token exchange |
| `https://www.bankofmaldives.com.mv/internetbanking/api/mobile/dashboard` | BML accounts & balances |
| `https://www.bankofmaldives.com.mv/internetbanking/api/mobile/contacts` | BML contacts (read / add / delete) |
| `https://www.bankofmaldives.com.mv/internetbanking/api/mobile/transfer` | BML funds transfer |
| `https://www.bankofmaldives.com.mv/internetbanking/api/mobile/account/{id}/history/{page}` | BML CASA history |
| `https://www.bankofmaldives.com.mv/internetbanking/api/mobile/card/statement` | BML prepaid card statement |
| `https://www.bankofmaldives.com.mv/internetbanking/api/mobile/validate/account/{input}` | BML account validation |
| `https://www.bankofmaldives.com.mv/internetbanking/api/mobile/favara/account-verification/{acct}/MIB` | BML→MIB cross-bank verification |
| `https://www.bankofmaldives.com.mv/internetbanking/api/mobile/userinfo` | BML user info |
| `https://app.bankofmaldives.com.mv/api/v2/foreign-limits` | BML foreign spending limits |
### Fahipay
| URL | Purpose |
|---|---|
| `https://fahipay.mv/api/app/lang/data/` | Session init (seeds `__Secure-sess` cookie) |
| `https://fahipay.mv/api/app/login/` | Fahipay login |
| `https://fahipay.mv/api/app/otp/` | Fahipay TOTP verification |
| `https://fahipay.mv/actions/getprofile/?lang=en` | Fahipay user profile |
| `https://fahipay.mv/actions/getbalance/?lang=en` | Fahipay wallet balance |
| `https://fahipay.mv/actions/activity/?s={offset}&l=15&lang=en` | Fahipay transaction history |
| `https://fahipay.mv/api/app/favs/?page={group}&lang=en` | Fahipay saved favourites (contacts) |
| **Arbitrary merchant icon URLs** | Transaction icons returned in Fahipay activity response (see §5) |
### Telecom Operators (transfer helper validation)
| URL | Purpose | Data sent |
|---|---|---|
| `https://www.ooredoo.mv/ooredoo-prod/QuickPayPackage/v1/numberTypeValidation?action=cust_details&msisdn=960{number}` | Validates Ooredoo number type (pre/post/hybrid) | 7-digit phone number |
| `https://www.dhiraagu.com.mv/services/easy-pay` | Fetches Dhiraagu nonce (HTML) | None |
| `https://www.dhiraagu.com.mv/api/sdk-dhr-webapi.ashx?website_id=CA2BB809-...&sub=dhiraaguIO&act=infoUnlisted` | Validates Dhiraagu number, returns owner name | 7-digit phone number |
---
## 2. Credential Storage
**File:** `app/src/main/java/sh/sar/basedbank/util/CredentialStore.kt`
All credentials, session tokens, OTP seeds, and PII are encrypted before being written to SharedPreferences using **AES-256-GCM** with a key generated and stored inside the **Android Keystore** (hardware-backed on supported devices). The key never leaves the secure enclave.
Encrypted data:
- MIB username, password hash, OTP seed, session keys (key1/key2), app ID
- BML username, password, OTP seed, access token, device ID
- Fahipay ID card, password, authId, session cookie (`__Secure-sess`), device UUID
- PIN/pattern PBKDF2 hash + salt
- All user profile PII (full name, email, mobile, NID, customer ID, birthdate)
The IV is randomly generated per encryption call and stored alongside the ciphertext (`iv:ciphertext` Base64 format), so each encrypted value is unique even with the same plaintext.
**Cache encryption** (`CacheEncryption.kt`) reuses the same AndroidKeyStore key for all cache SharedPreferences — consistent and correct.
---
## 3. Lock Screen
**File:** `app/src/main/java/sh/sar/basedbank/LockActivity.kt`
**File:** `app/src/main/java/sh/sar/basedbank/ui/onboarding/SecuritySetupFragment.kt`
### PIN / Pattern Hashing
- Algorithm: **PBKDF2WithHmacSHA256**
- Iterations: **100,000**
- Salt: **16 bytes**, `SecureRandom`-generated, unique per setup
- Output: 256-bit key stored encrypted in `CredentialStore` (AndroidKeyStore)
- Password array is cleared via `PBEKeySpec.clearPassword()` after use
### Legacy Migration
Old installs used raw `SHA-256(salt + input)` stored in plaintext SharedPreferences. On the first successful unlock, the app transparently migrates to PBKDF2 + CredentialStore and removes the legacy plaintext fields.
### Biometrics
- Uses `androidx.biometric` with `BIOMETRIC_WEAK` authenticators
- Falls back to PIN/pattern if biometrics unavailable or cancelled
### Brute-Force Lockout
- Max attempts: **5**
- Lockout duration: **30 seconds**
- Attempt counter stored in `lock_attempts` SharedPreferences
**Weakness:** The lockout is weaker than ideal for two reasons:
1. 30 seconds is very short — an attacker gets ~10 guesses/minute, ~600/hour.
2. The `lock_attempts.xml` SharedPreferences file is **not encrypted**. On a rooted device, deleting this file resets the counter entirely, bypassing brute-force protection.
A stronger policy would use progressive delays (1 min, 5 min, 15 min, 1 hour) and store the counter inside `CredentialStore`.
---
## 4. Autolock
**File:** `app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt`
- Autolock timer resets on every user interaction (`onUserInteraction`)
- Warns the user 10 seconds before locking with a countdown dialog
- On resume, checks elapsed pause time and locks immediately if the timeout was exceeded while the app was backgrounded
- Default timeout: 60 seconds (configurable in Settings)
---
## 5. Screen Capture Protection
**File:** `app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt:80`
`FLAG_SECURE` is applied on `HomeActivity` creation, blocking screenshots and hiding app content in the recents/app-switcher. This is **enabled by default** and can be toggled off by the user in Settings.
---
## 6. Network Layer
### MIB Transport Cryptography (Upstream Protocol Constraint)
**File:** `app/src/main/java/sh/sar/basedbank/api/mib/MibCrypto.kt`
The MIB Faisanet API uses **Blowfish/ECB/PKCS5Padding** for payload encryption. ECB mode does not use an IV, meaning identical plaintext blocks produce identical ciphertext blocks, and block patterns can be analysed. This is the protocol defined by MIB's own servers and official app — it is an **upstream constraint inherited from the reverse-engineered protocol**, not a choice made in this codebase.
The session key itself is derived via a **Diffie-Hellman exchange** (2048-bit+ modulus) with SHA-256 key derivation, which provides forward secrecy per session. The hardcoded `DEFAULT_KEY` is only used as the bootstrap key for the initial DH handshake; all subsequent requests use the DH-derived session key.
### Certificate Pinning
**File:** `app/src/main/res/xml/network_security_config.xml`
The network security config is empty (`<network-security-config/>`), meaning the app relies on Android's default system CA trust store. There is no certificate pinning for any backend.
**Risk:** A device with a malicious or corporate CA certificate installed (e.g. MDM, proxy, or a compromised CA) could perform a man-in-the-middle attack against all API traffic. This is the most significant actionable security gap in the app.
**Recommendation:** Add `<pin-set>` entries for each banking domain's leaf or intermediate certificate in `network_security_config.xml`.
### Merchant Icon Fetch — Arbitrary URL
**Files:** `AccountHistoryFragment.kt:274295`, `TransferHistoryFragment.kt:298315`
Fahipay transaction history responses include an `iconUrl` field containing a merchant logo URL. The app fetches these images directly using a bare `OkHttpClient()` with **no domain restriction, no URL validation, and no TLS pinning**. The URL comes from Fahipay's server.
**Risks:**
1. **Server-directed requests to third parties** — If Fahipay's server is compromised or MITM'd, it can cause the app to make GET requests to arbitrary URLs. This is effectively a server-side request forgery at the client level.
2. **Merchant tracking by Fahipay** — Fahipay (or any CDN serving the icons) can observe exactly which merchant URLs are fetched, and cross-reference this with user session data to build a detailed picture of spending patterns, timing, and even infer location from merchants.
3. **No HTTPS enforcement** — A `http://` icon URL would be fetched without the user's knowledge, bypassing TLS for that image fetch. Android's default `usesCleartextTraffic` is still `true` for these unconstrained clients.
The icon cache correctly writes to `context.cacheDir` which is excluded from backup, so cached icons don't leak into cloud storage.
### User-Agent & Device Identity Impersonation
The app presents different identities to different backends, which is intentional for protocol compatibility:
| Backend | User-Agent sent |
|---|---|
| MIB API | `android/1.0` |
| MIB WebView | `Mozilla/5.0 (Linux; Android {version}; wv) AppleWebKit/537.36 ... Mobile Safari/537.36` |
| BML web steps | `Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0` |
| BML API calls | `bml-mobile-banking/345 (POCO; Android {version}; {model})` |
| Fahipay login/OTP | WebView UA with actual `Build.MODEL` |
| Fahipay API calls | `okhttp/4.12.0` |
| Ooredoo | No custom UA |
| Dhiraagu | `Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0` |
The BML API User-Agent hardcodes a specific device model (`POCO; Android {version}; {model}`) to mimic the official BML mobile app. This is required for the API to accept requests.
The Fahipay login includes `Build.MODEL` and `Build.MANUFACTURER` from the actual device, sent as `device[model]` and `device[manufacturer]` form fields. This is a **device fingerprint** sent to Fahipay on every login.
---
## 7. Privacy — Phone Number Disclosure to Telecom Operators
**Files:** `OoredooClient.kt`, `DhiraaguClient.kt`
When a user enters a recipient phone number in the transfer flow, the app validates it by calling the respective telecom's public API:
- The 7-digit number is sent to `ooredoo.mv` (as `msisdn=960XXXXXXX`)
- The number is sent to `dhiraagu.com.mv` (in a JSON POST body)
These are unauthenticated public endpoints, but the requests disclose to each operator which numbers are being queried and when. This also means:
- Ooredoo knows a BasedBank user is about to transact with a given Ooredoo number at a specific time.
- Dhiraagu additionally returns the **account owner's name** (`accountOwnerInfo.name`), which is displayed in the UI to help the user confirm the recipient.
This is inherent to the feature (the Dhiraagu name lookup is useful for transfer safety), but users should be aware their query patterns are visible to the telecom.
---
## 8. Third-Party Data Leakage
**No user data is sent to any advertiser, analytics service, or the developer.**
### Dependency Audit
| Library | Purpose | Phones Home? |
|---|---|---|
| `androidx.*` | UI / Lifecycle / Navigation | No |
| `com.google.android.material` | Material Design UI components | No |
| `com.squareup.okhttp3:okhttp` | HTTP client | No |
| `androidx.camera.*` | QR scanner camera | No |
| `com.github.markusfisch:zxing-cpp` | QR barcode decoding | No |
| `androidx.biometric` | Biometric authentication | No |
| `org.jetbrains.kotlinx:kotlinx-coroutines-android` | Coroutines | No |
No Firebase, no Crashlytics, no analytics SDKs, no ad networks.
### Logging
There are **zero** `android.util.Log` calls anywhere in the codebase. Nothing is written to logcat. The `okhttp3:logging-interceptor` dependency has been removed.
### TOTP
`Totp.kt` is a self-contained RFC 6238 implementation using only `javax.crypto` from the Android standard library. No third-party OTP SDK is used.
---
## 9. Backup & Data Extraction
**Files:** `app/src/main/res/xml/backup_rules.xml`, `app/src/main/res/xml/data_extraction_rules.xml`
Both the legacy backup rules (API ≤ 30) and the modern data extraction rules (API 31+) exclude all sensitive data from cloud backup **and** device-to-device transfer:
- `credential_store.xml`
- `mib_prefs.xml`
- `prefs.xml`
- `account_cache.xml`
- `contacts_cache.xml`
- `financing_cache.xml`
- `foreign_limits_cache.xml`
- `recents_cache.xml`
- `lock_attempts.xml`
- `cache/` directory (covers merchant icons and contact image cache)
Note: even if backup were to occur, the AndroidKeyStore encryption key is hardware-bound and **does not leave the device**, so the encrypted data would be unreadable without the original device's secure hardware.
The manifest has `android:allowBackup="true"`, but the exclusion rules above make the effective backup content empty of anything sensitive.
---
## 10. Manifest & Attack Surface
**File:** `app/src/main/AndroidManifest.xml`
Only `MainActivity` is `android:exported="true"` (required as the launcher entry point). All other activities (`LockActivity`, `OnboardingActivity`, `LoginActivity`, `HomeActivity`, `QrScannerActivity`) are `exported="false"` and cannot be launched by external apps.
The `FileProvider` is exported but with `android:grantUriPermissions="true"` scoped correctly — it cannot be accessed without an explicit URI grant.
### Permissions
| Permission | Reason |
|---|---|
| `INTERNET` | Banking API calls |
| `CAMERA` | QR scanner |
| `USE_BIOMETRIC` | Biometric unlock |
| `ACCESS_NETWORK_STATE` | Network availability checks |
| `WRITE_EXTERNAL_STORAGE` | Receipt saving, limited to API ≤ 28 |
No `READ_CONTACTS`, no `READ_SMS`, no `RECEIVE_SMS`, no location permissions.
---
## 11. Code Obfuscation
`isMinifyEnabled = false` in the release build config. This is **not a security concern** because BasedBank is an open-source project — the source code is already publicly available. Obfuscating the APK would provide no additional protection when the source is readable. The only benefit of enabling R8 would be APK size reduction via dead-code elimination.
---
## 12. Minor Code Issues
### DhiraaguClient JSON String Interpolation
**File:** `app/src/main/java/sh/sar/basedbank/api/dhiraagu/DhiraaguClient.kt:41`
```kotlin
val body = """{"number":"$number"}""".toRequestBody(...)
```
The phone number is embedded directly into a JSON string via string interpolation rather than using a JSON builder. If `number` contained quotes or backslashes, it would produce malformed JSON. In practice, `number` is validated upstream to be a 7-digit numeric string, so the actual injection risk is negligible — but it is worth noting as a code hygiene issue.
### MIB App ID Impersonates iOS
**File:** `app/src/main/java/sh/sar/basedbank/api/mib/MibLoginFlow.kt:395`
```kotlin
id = "IOS17.2-" + (1..15).map { ... }.joinToString("")
```
The app ID sent to MIB's servers is prefixed with `IOS17.2-` to match the format expected by the server. This is a protocol compatibility measure.
---
## 13. Hardcoded Values
All hardcoded values in this codebase are protocol constants extracted from reverse-engineering the official banking apps. There are **no developer secrets, API keys owned by this project, or user credentials** baked into the build.
| Value | File | What it is | Risk |
|---|---|---|---|
| `DEFAULT_KEY = "8M3L9SBF1AC4FRE56788M3L9SBF1AC4FRE5678"` | `MibCrypto.kt:12` | Blowfish bootstrap key for MIB's initial DH handshake. Same key is in the official Faisanet APK. | Not secret — server also knows it; only used for the key exchange handshake, not session traffic. |
| `A = BigInteger("1563516802667...")` | `MibCrypto.kt:14` | MIB Diffie-Hellman **private exponent** (client side). Hardcoded, not generated fresh per session. | **See below.** |
| `P = BigInteger("2410312426921...")` | `MibCrypto.kt:17` | MIB DH prime modulus. Same value as in the official app. | Public parameter — no risk. |
| `CLIENT_ID = "98C83590-513F-4716-B02B-EC68B7D9E7E7"` | `BmlLoginFlow.kt:30` | BML OAuth client ID, extracted from the official BML mobile app APK. | Not a personal secret — it is the same value for all BML mobile clients. |
| `REDIRECT_URI = "https://app.bankofmaldives.com.mv/oauth/mobile-callback"` | `BmlLoginFlow.kt:31` | BML OAuth redirect URI, must match what BML's server expects. | Fixed protocol value. |
| `APP_USER_AGENT = "bml-mobile-banking/345 (POCO; Android {version}; {model})"` | `BmlLoginFlow.kt:32` | Impersonates the official BML app and a specific POCO device model. | Intentional compatibility measure; no personal data. |
| `APP_VERSION = "2.1.43.345"` | `BmlLoginFlow.kt:33` | BML app version string being impersonated. | Fixed protocol value. |
| `website_id = "CA2BB809-3A22-485B-A518-DA6B6DE653A5"` | `DhiraaguClient.kt:45` | Dhiraagu SDK identifier embedded in the lookup URL. Extracted from Dhiraagu's public Easy Pay page. | Public value embedded in their web page; not a secret. |
| `MIB_SWIFT_ON_BML = "F4E79935-3E73-E611-80DD-00155D020F0A"` | `AddContactSheetFragment.kt:457` | BML's internal bank code (SWIFT/FinInstnId) used to identify MIB as the counterpart bank during BML transfers. | Protocol constant, not a secret. |
### MIB Hardcoded DH Private Key — Analysis
**File:** `MibCrypto.kt:14-29`
The Diffie-Hellman private exponent `A` is hardcoded as a constant rather than being generated fresh for each session. In a standard DH exchange:
- The client generates a random private value `a`
- Sends `g^a mod P` to the server
- Receives `g^b mod P` from the server
- Computes shared secret `(g^b)^a mod P`
With `A` hardcoded and published in source code:
1. Anyone who captures network traffic containing `smod` (the server's `g^b mod P` value) can compute `shared = BigInteger(smod).modPow(A, P)` and fully derive the session key.
2. This eliminates **forward secrecy** — captured past sessions remain decryptable by anyone with the hardcoded `A`.
However, this is again an **upstream protocol constraint**: the official Faisanet APK uses the same hardcoded DH exponent. The MIB server requires a specific `cmod` (`g^A mod P`) and would reject a freshly-generated one. Changing `A` would break compatibility with the server unless MIB updates their backend.
**Net effect:** The MIB DH exchange provides session isolation (each session gets a unique `smod` from the server), but **not true forward secrecy** because `A` is fixed and public. An attacker capturing traffic can decrypt any MIB session if they also capture the server's `smod` response from the handshake.
---
## Findings Summary
### Critical
_None._
### High
- **No certificate pinning** — MITM possible on devices with untrusted CA certs installed. Affects all backends: MIB, BML, Fahipay, Ooredoo, Dhiraagu, and merchant icon URLs.
### Medium
- **Merchant icon fetch — arbitrary server URLs** — Fahipay can direct the app to fetch images from any URL. No domain restriction. Fahipay CDN can observe spending patterns via icon request timing.
- **Weak lockout policy** — 30-second lockout after 5 attempts; no progressive backoff.
- **Lockout counter not encrypted** — `lock_attempts.xml` is plaintext; deletable on rooted devices.
### Low / Informational
- **MIB DH private key hardcoded** — forward secrecy is not achieved; captured sessions can be decrypted with the known `A`. Upstream protocol constraint.
- **Phone number disclosure to Ooredoo/Dhiraagu** — Numbers queried for transfer validation are visible to each telecom operator.
- **Device fingerprint in Fahipay login** — `Build.MODEL` and `Build.MANUFACTURER` sent on every login.
- **`BIOMETRIC_WEAK`** allows 2D face unlock on some devices, which may be spoofable with a photo.
- **MIB Blowfish/ECB** — inherited upstream protocol weakness, not actionable without server-side changes.
- **DhiraaguClient JSON string interpolation** — low real-world risk given numeric-only input validation upstream.
- **`android:allowBackup="true"`** — flagged by automated scanners but effectively mitigated by the exclusion rules.
+85
View File
@@ -0,0 +1,85 @@
# Account Display Parser Architecture
## Overview
Each bank's API returns account data in different formats and uses different field names for balances, product types, and status. To keep screens bank-agnostic, each bank has a dedicated parser that translates raw `MibAccount` model data into a standard `AccountListDisplay` object. Screens consume only `AccountListDisplay` — they never inspect `bank` or `profileType` or apply bank-specific logic.
## Bank Discriminator — `MibAccount.bank`
All dispatchers route by `account.bank`, a string set explicitly by each login flow at account creation time:
| `bank` value | Set by |
|--------------|-------------------|
| `"MIB"` | `MibLoginFlow` |
| `"BML"` | `BmlLoginFlow` |
| `"FAHIPAY"` | `FahipayLoginFlow`|
`profileType` is a bank-internal value (e.g. MIB's numeric profile ID, or BML's `"BML_PREPAID"`) and is **never** used for bank routing. Card-type checks within BML still use `profileType` (`"BML_PREPAID"` / `"BML_CREDIT"`).
`cifType` (MIB only) is the human-readable profile category name returned by the `operatingProfiles` API (e.g. `"Individual"`, `"Sole Propr"`). It is stored on `MibAccount` and surfaced in the accounts list section header and settings. It is **never hardcoded** in the app.
## Standard Output Model
```kotlin
// util/AccountListDisplay.kt
data class AccountListDisplay(
val name: String, // account or card display name
val number: String, // account/card number
val typeLabel: String, // human-friendly product type (e.g. "Savings", "Visa Platinum")
val balance: String, // formatted balance string (e.g. "MVR 1,234.56")
val isCard: Boolean, // true → use card layout; false → use account layout
val cardBrandIcon: Int, // drawable res for card brand logo (Visa / Mastercard / Amex)
val statusLabel: String? // null = active; non-null = shown as status label (e.g. "Inactive")
)
```
## Dispatcher
```kotlin
// util/AccountListParser.kt
AccountListParser.from(account: MibAccount): AccountListDisplay?
```
Routes to the correct parser based on `account.bank`. Returns `null` for unknown banks — never falls back to a specific bank.
| `account.bank` | Parser |
|----------------|-------------------------|
| `"BML"` | `BmlDashboardParser` |
| `"FAHIPAY"` | `FahipayAccountParser` |
| `"MIB"` | `MibAccountParser` |
| anything else | `null` |
## Bank Parsers
### BML — `util/bmlapi/BmlDashboardParser`
Handles both CASA accounts and prepaid/credit cards.
- **CASA balance**: uses `ledgerBalance` (working balance) — mapped to `account.currentBalance`
- **Card balance**: uses `availableBalance` (available limit from `cardBalance.AvailableLimit`)
- **Card brand**: resolved from product name (`VISA` / `MASTERCARD` / `AMEX`)
- **Status**: cards with `statusDesc != "Active"` surface `statusLabel`; active cards return `null`
### MIB — `util/mibapi/MibAccountParser`
- **Balance**: `availableBalance` from the MIB API directly
- Known product names (`SAVING ACCOUNT`, `CURRENT ACCOUNT`) mapped to short labels
- `cifType` (e.g. `"Individual"`, `"Sole Propr"`) comes from `MibProfile.cifType`, stored on `MibAccount`, displayed in section headers
### Fahipay — `util/fahipayapi/FahipayAccountParser`
- Single wallet account per user; `accountTypeName` is always `"Digital Wallet"`
- **Balance**: `availableBalance`
## Adding a New Bank
1. Create `util/<bankname>api/<Bank>AccountParser.kt` with a `displayData(account: MibAccount): AccountListDisplay` function
2. Set `bank = "<BANKNAME>"` in the new login flow when creating `MibAccount` objects
3. Add a `when` branch in `AccountListParser.from()` (and other dispatchers) for the new bank value
4. No changes needed in any screen or adapter
## Usage in Screens
`AccountsAdapter` calls `AccountListParser.from(account)` once per item (skipping `null` results) and binds the resulting `AccountListDisplay` directly. The adapter has zero bank-specific logic.
The transfer screen dropdown (`TransferFragment`) also uses `AccountListParser.from(acc)?.balance` for the source account balance display.
+11
View File
@@ -0,0 +1,11 @@
# App Internals
Documentation for app-specific logic — UI flows, routing decisions, and business rules implemented in the Android client.
---
| Document | Description |
|---|---|
| [01 — Transfer Flows](01-transfer-flows.md) | TransferFragment entry points, recipient lookup, transfer type routing, rejected combinations, BML business OTP flow, BML QR merchant payments |
| [Parsers](PARSERS.md) | Account display parser architecture — how raw bank API data is normalised into a unified `AccountListDisplay` model |
| [AI Security Audit](AI_SECURITY_CHECK.md) | Full source security audit — credential storage, network layer, manifest, data privacy |