# Nigig-Pay Threat Model ## 1. System Overview Nigig-pay is a Makepad-based mobile wallet app that automates M-Pesa USSD payments on Android. The Rust code drives an AccessibilityService that types into the USSD dialog, reads confirmation SMS via an SMS inbox scraper, and persists transaction records locally. ### Trust Boundary | Zone | Description | |---|---| | **Device** | User's Android phone. Makepad UI, Rust business logic, local PSV files. | | **USSD Gateway** | Safaricom's USSD channel. Untrusted input — cannot prove payment. | | **SMS Inbox** | Device SMS app. Forgeable, spoofable, replayable. | | **Server (future)** | Backend for reconciliation. Not yet implemented. | --- ## 2. Assets | Asset | Sensitivity | Storage | |---|---|---| | M-Pesa PIN | **Critical** | SharedPreferences (Android) — now scrubbed on session end | | Phone number | High | SharedPreferences, PSV file | | Transaction code | Medium | PSV file (unencrypted) | | Amount, party, balance | Medium | PSV file (unencrypted) | | Raw SMS text | **High** | In-memory only. Omitted from the payment PSV (Phase 0) and from `offline_store` SMS cache (Phase E1). Sensitivity raised from Medium: SMS carries OTPs and banking codes, not just payment confirmations. | | Biometric auth state | Low | In-memory only | --- ## 3. Threat Actors | Actor | Capability | Goal | |---|---|---| | **Malicious app** | Same-device, same UID | Read SharedPreferences, SMS, PSV files | | **Compromised SMS** | Spoofed sender/address | Inject fake M-Pesa confirmation to steal goods | | **Malicious insider** | Physical access | Extract PIN from SharedPreferences | | **Network MITM** | USSD intercept | Replay or modify USSD session | --- ## 4. Threats (STRIDE) ### S — Spoofing | ID | Threat | Mitigation | Status | |---|---|---|---| | T-S1 | Spoofed M-Pesa SMS causes false "Verified" | `ObservedEvidence` renamed to clarify it's not proof. Server confirmation required. | ✅ Phase 0 | | T-S2 | SMS sender spoofing (not from Safaricom) | No sender validation in SMS scraper. Requires server-side confirmation. | ⚠️ Phase 1 | ### T — Tampering | ID | Threat | Mitigation | Status | |---|---|---|---| | T-T1 | PSV file modified on disk | No integrity check. Needs HMAC or encrypted store. | ⚠️ Phase 1 | | T-T2 | SharedPreferences modified to inject PIN | Now scrubbed on session end; but no encryption-at-rest. | ⚠️ Phase 1 | ### R — Repudiation | ID | Threat | Mitigation | Status | |---|---|---|---| | T-R1 | User denies initiating USSD | No server-side audit trail. Biometric gate helps for demo. | ⚠️ Phase 2 | ### I — Information Disclosure | ID | Threat | Mitigation | Status | |---|---|---|---| | T-I1 | PIN persists in SharedPreferences indefinitely | `session.rs` `clear()` now removes `KEY_PIN` + all keys. Java `finishPin()`/`fail()`/`closeUssdAfterResult()` also scrub PIN. | ✅ Phase 0 | | T-I2 | Raw SMS persisted to PSV file | `raw_message` now omitted from `save_to_disk()`. Lives in memory only. | ✅ Phase 0 | | T-I2b | Raw SMS re-persisted by the SMS app | The same defect reappeared at larger scale: `offline_store::upsert_sms_messages` wrote **every inbox message body** to `app_data_dir/offline_store/sms_messages.json` as pretty-printed plaintext, uncapped. Reachable by the same "malicious app, same UID" adversary already in this model, and by any backup extraction. `OfflineSmsMessage.body` is now `#[serde(skip)]`, so only metadata is written; retention is capped at 5,000 rows. The device provider remains the system of record, so nothing is lost. | ✅ Phase E1 | | T-I4 | Scheduled SMS bodies in SharedPreferences | `robius-sms` stores recipient and body in `MODE_PRIVATE` SharedPreferences for pending schedules. UID-scoped, which is the right primitive, but unencrypted at rest and included in cloud backup by default. Not yet addressed. | ⚠️ Phase E3 | | T-I3 | PIN leaked in logs | No evidence in current code, but no policy prevents it. | ⚠️ Phase 1 | ### D — Denial of Service | ID | Threat | Mitigation | Status | |---|---|---|---| | T-D1 | Auto-retry floods Safaricom | 5 retries with exponential backoff. No idempotency key. | ⚠️ Phase 1 | ### E — Elevation of Privilege | ID | Threat | Mitigation | Status | |---|---|---|---| | T-E1 | USSD dispatched without biometric auth | Biometric error now fails closed. Previously dispatched anyway. | ✅ Phase 0 | | T-E2 | USSD dispatched in production without demo flag | `dispatch_ussd`, `retry_dispatch`, `start_bulk_payments` gated behind `#[cfg(feature = "demo")]`. | ✅ Phase 0 | | T-E3 | Bulk payments in production | Gated behind `demo` feature. No authorized provider rail. | ✅ Phase 0 | --- ## 5. Open Risks | Risk | Severity | Notes | |---|---|---| | `f64` used for money throughout | **Critical** | `MpesaTransaction.amount: f64`, `totals()` sums f64. IEEE 754 rounding = financial loss. | | No server-side confirmation | **Critical** | Local SMS observation cannot prove payment. | | PSV escaping lossy (`|` ↔ `~`) | Medium | Literal `~` in SMS corrupts the record on round-trip. | | Classification lost on reload | Medium | `load_from_disk()` hardcodes `classification: Default::default()`. | | Hard-coded 2024 fee table | Low | Fee schedules are static since Jan 2024. | | No encrypted transport | High | When server integration exists, TLS is required. | | No sender validation on SMS read | High | T-S2 remains open. Nothing verifies an SMS actually came from the claimed sender, so a spoofed M-Pesa confirmation is indistinguishable from a real one to the parser. Mitigation requires server-side confirmation; documented rather than fixed. | | Delivery is not confirmed | Medium | `send_sms` returning `Ok` means the send was handed to the platform, not delivered — both PendingIntents are null. UI must not present `Ok` as "delivered". Documented in the crate (Phase A8). | --- ## 6. References - [NIGIG_PAY_CONSOLIDATED_REVIEW.md](./NIGIG_PAY_CONSOLIDATED_REVIEW.md) — Full audit findings - [PAYMENT_RISK_REGISTER.md](./PAYMENT_RISK_REGISTER.md) — Per-feature risk register