Security

Secrets management

How sessions/tokens are protected

OTP abuse prevention

Enforcement lives in app/services/otp.py, ahead of any call to Termii:

PIN handling (offline-transfer transaction PIN — separate from the OTP login PIN)

Webhook authenticity

Inbound Termii webhooks are verified via X-Termii-Signature (HMAC-SHA512 of the raw request body, keyed with TERMII_WEBHOOK_SECRET) before the body is parsed at all. A forged webhook can't reach the decryption or balance-mutation code paths.

Known risks and mitigations

Risk Mitigation Status
PIN travels over SMS (store-and-forward, not a live session) AES-256-GCM authenticated encryption end-to-end; flagged in the original architecture doc as a reason to consider USSD for production Accepted for POC, open question for production
Stolen refresh token Hashed at rest, rotated on every use, immediately revocable Mitigated
Forged webhook triggering a fake debit HMAC-SHA512 signature check before any parsing Mitigated
OTP brute force Cooldown + hourly cap + attempt limit, enforced locally (backed by Termii's own server-side limit too) Mitigated
Offline transfer PIN brute force 5-attempt lockout, 30 minutes (previously no limit at all) Mitigated
Sender ID spoofing / phishing via SMS Out of scope for this POC — no different from any SMS-based OTP system's baseline exposure Not addressed
Duplicate/replayed webhook causing a double debit Idempotency check (unique provider_message_id constraint) happens before any balance mutation, proven under retry in tests Mitigated
Key rotation for PAYLOAD_ENCRYPTION_KEY / JWT_SECRET_KEY Not implemented — out of scope for POC, flagged as a required follow-up before production Not addressed
PAYLOAD_ENCRYPTION_KEY is a single static key shipped inside every app install Inherent to an app-embedded symmetric key with no connectivity for a handshake; anyone extracting it from the APK can craft valid payloads Not addressed — architectural tradeoff, flagged for production review