Scope & ethics reminder: This module is for authorized, lab-only testing and for building defensible implementations in banking apps. It explains how to design and validate auth and crypto correctly. It does not provide offensive instructions for breaking real systems.
This module ties together identity, sessions, device security, and cryptography on Android: how to design login and session flows, how to store secrets safely, how to use the Android Keystore (TEE/StrongBox), how (and where) to use hardware-backed attestation, and how to manage keys and tokens across client and server with rotation and revocation. It includes checklists, code patterns, and lab exercises using intentionally vulnerable apps or mock backends.
11.0 Learning objectives
After this module you will be able to:
- Design secure authentication and session flows for mobile banking apps.
- Use Android Keystore (TEE/StrongBox) correctly for symmetric and asymmetric keys.
- Understand Key Attestation and Play Integrity roles and how to validate results server-side.
- Store and handle tokens (access/refresh) securely with short lifetimes, rotation, and device binding.
- Apply modern crypto (AEAD, KDFs, RNG) and avoid common pitfalls.
- Build a key lifecycle (generation → use → rotation → revocation → retirement) with auditing.
- Produce an auth & crypto hardening plan and test it in a lab.
11.1 Threat model (what we’re defending against)
- Network attackers (MitM): attempt to steal credentials/tokens → defend with TLS 1.2/1.3, pinning, HSTS.
- Compromised client: rooted device, runtime hooks; can read memory and modify flows → minimize client trust, use server validation and telemetry.
- Stolen device: physical attacker tries to unlock app or extract keys → biometrics, device credential gating, hardware-backed keys (TEE/StrongBox), remote revocation.
- Backend attacker: misconfigurations or weak server validation → strict server verification, logging, alerting, rotation.
- Supply chain: leaked signing keys/dependencies → CI controls, reproducible builds, signing key protection.
11.2 Authentication & session design (mobile-first)
11.2.1 Principles
- Short-lived access tokens, longer-lived refresh tokens stored carefully.
- Proof-of-possession (bind sessions to device keys/attestation where feasible).
- Step-up auth (biometric or OTP) for high-risk transactions.
- Least privilege scopes; rotate tokens frequently.
- All final decisions server-side (never trust client claims like “device not rooted”).
11.2.2 Recommended flow (high level)
- Primary login (password + optional OTP/biometric for step-up).
- Server issues short-lived access token (e.g., 5–10 minutes) and refresh token (longer).
- Client stores refresh token with hardware-backed protection (see §11.5).
- For sensitive operations, server requests attestation (nonce → token) and/or biometric confirmation.
- Regular rotation: access token refresh bound to device state (attestation flags + device public key).
11.3 Token models & binding
- Bearer tokens: simplest; must be short-lived and transmitted only over TLS in
Authorization: Bearer. - Bound tokens: include device binding (e.g., JWT claim with device key thumbprint) or DPoP/mTLS concepts for proof-of-possession.
- Refresh token constraints: one device → one refresh token; revoke on device change; store server-side metadata (issued_at, device_id, app_ver, attestation_result).
Server checks at every refresh:
- Token not revoked; device/app identifiers match; attestation recent and valid; client app signature digest matches expected.
11.4 Cryptographic foundations (what to use; what to avoid)
11.4.1 Primitives (modern, recommended)
- Symmetric encryption: AES-GCM (AEAD) with 128/256-bit keys.
- Hashing: SHA-256/512 (for integrity, not passwords).
- KDFs: PBKDF2 (>=100k iterations), Argon2 or scrypt where available (server-side) for password hashing.
- Asymmetric: ECDSA P-256 or Ed25519 for signatures; ECDH for key agreement.
- RNG:
SecureRandom()(on Android 6+), never roll your own RNG.
11.4.2 Don’ts
- ❌ AES-ECB; ❌ static IVs for CBC/GCM; ❌ MD5/SHA-1 for security; ❌ hardcoded keys; ❌ home-grown crypto; ❌ “alg”:”none” JWT.
11.5 Android Keystore, TEE & StrongBox (client-side key use)
Android Keystore provides key generation and use inside hardware or trusted execution (when available). Keys marked non-exportable cannot be read into app memory; crypto operations happen in TEE/StrongBox.
11.5.1 Key creation (Kotlin example — symmetric AES-GCM)
val keyGen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val spec = KeyGenParameterSpec.Builder(
"bank_refresh_token_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
).setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.setIsStrongBoxBacked(true) // falls back if not available
.setUserAuthenticationRequired(true) // gate with biometrics/device credential
.setUserAuthenticationParameters(60, KeyProperties.AUTH_BIOMETRIC_STRONG)
.build()
keyGen.init(spec)
val secretKey = keyGen.generateKey()
Notes
setIsStrongBoxBacked(true)uses a dedicated secure element when available.setUserAuthenticationRequired(true)forces biometric/device credential before use (optional tuning: per-use vs timeout).
11.5.2 Encrypt & store a refresh token (AES-GCM)
fun encryptRefreshToken(plaintext: ByteArray): EncryptedPayload {
val ks = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val key = (ks.getEntry("bank_refresh_token_key", null) as KeyStore.SecretKeyEntry).secretKey
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, key)
val iv = cipher.iv
val ciphertext = cipher.doFinal(plaintext)
return EncryptedPayload(iv, ciphertext) // persist iv+ciphertext in private storage
}
Store
iv+ciphertextin app private storage (e.g., EncryptedSharedPreferences or your own file) — never on external storage.
11.5.3 Asymmetric key for device binding (ECDSA P-256)
val kpg = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore"
)
val spec = KeyGenParameterSpec.Builder(
"bank_device_binding_key",
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
).setDigests(KeyProperties.DIGEST_SHA256)
.setUserAuthenticationRequired(false) // or true for step-up
.setAttestationChallenge(nonceFromServer()) // see §11.6
.build()
kpg.initialize(spec)
val kp = kpg.generateKeyPair()
- The public key can be uploaded to the server as the device binding identifier.
- Private key never leaves the keystore.
11.6 Key Attestation & Play Integrity in auth
Key Attestation proves a key was generated in secure hardware and provides a certificate chain + claims (e.g., app identity, OS version) bound to a challenge (nonce). Play Integrity is a broader integrity signal. Both must be validated server-side.
11.6.1 Flow (recommended)
- Server issues nonce (short TTL).
- App generates/uses a key with
setAttestationChallenge(nonce)(for key attestation) or calls Play Integrity with nonce. - App sends attestation token + public key (if device binding) to server.
- Server validates signature chain, nonce, timestamps, and claims; records result and binds device key to the account/session.
11.6.2 Server validation checklist (summary)
- Verify token signature chain (Google/Keymaster CA) → trusted root.
- Nonce matches issued value and within TTL.
- Package name & signing cert digest match release manifest.
- Device integrity flags (ctsProfileMatch, basicIntegrity) meet policy.
- Timestamp reasonable (skew window).
- Persist results with trace ID (for audits) and associate device public key if used.
11.7 Biometrics & user authentication gating
Use BiometricPrompt for UX-consistent, secure biometric flow. Two patterns:
- Gate key use with
setUserAuthenticationRequired(true): the keystore enforces biometric before decryption/sign. - Credential-protected operations for step-up (e.g., high-value transfers).
Best practice: tie only the minimal secret (e.g., refresh token or device private key operation) to biometric gating; do not roll your own biometric crypto.
11.8 Secure storage of secrets & local data
- Never store plaintext tokens, passwords, or keys in SharedPreferences or files.
- Use EncryptedSharedPreferences (Jetpack Security) which leverages Keystore.
- Sensitive caches (statements, PII) should be encrypted at rest with an app-unique key; clear on logout.
- Set
android:allowBackup="false"or ensure backup excludes sensitive paths.
11.9 JWT, JWS, JWE — safe usage pattern (server & client)
- Access tokens: signed JWS (never accept
alg":"none"). Use stable key IDs (kid) + rotation. - Refresh tokens: opaque (preferably) or strongly signed JWT stored securely; treat as bearer secrets.
- JWE (encrypted JWT) when payload contains sensitive data that must not be visible to clients/intermediaries—often unnecessary for mobile if TLS + short-lived JWS suffice.
Server checks:
- Verify
iss,aud,exp,nbf,iat; reject stale or too-long lifetimes. - Reject tokens signed with unexpected algorithms or stale keys; fetch by
kid. - Couple with device binding claim or DPoP to mitigate token theft.
11.10 Key lifecycle & rotation (client & server)
11.10.1 Client keys (Keystore)
- Generation: on first run or when server demands rotation.
- Use: only via Keystore operations (sign/decrypt); avoid exporting private keys.
- Rotation triggers: app update, attestation policy change, suspected compromise, server-initiated rotate.
- Revocation: server marks device key as untrusted; client must re-enroll.
11.10.2 Server keys
- Keep signing/encryption keys in HSM/KMS; rotate regularly.
- Maintain JWKS endpoint with kid for clients/servers to select right key.
- Audit every issuance/verification; log trace IDs.
- Incident runbook: immediate key revocation, token invalidation, forced re-login.
11.11 Defensive code patterns (client)
11.11.1 EncryptedSharedPreferences quickstart
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val prefs = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
prefs.edit().putString("refresh_token_enc", Base64.encodeToString(ciphertext, Base64.NO_WRAP)).apply()
11.11.2 Nonce fetch + attestation (outline)
// 1) fetch nonce from server
val nonce = api.getAttestationNonce()
// 2) perform attestation (Play Integrity or Key Attestation) with nonce
val attestationToken = integrityClient.attest(nonce)
// 3) send token for server validation
api.verifyAttestation(attestationToken)
(Exact APIs vary; keep the logic & telemetry in place.)
11.12 Telemetry & anomaly detection (fraud & abuse)
Collect privacy-aware telemetry for every sensitive operation:
- App version, OS version, device model (non-PII).
- Server verification outcome (attestation trace ID, token
kid). - Integrity signals (root/emulator flags) as hints only; decisions happen server-side.
- Unusual patterns: many refreshes, multiple devices per account, geo anomalies.
Pipeline to SIEM with alerts (e.g., excessive failed attestations, large token replay).
11.13 Validation & test strategy (what to verify in lab)
- Token storage: confirm no plaintext tokens in app private storage; verify AES-GCM use and key non-exportability.
- Biometric gating: test that keystore key refuses use without biometric when required.
- Attestation round-trip: server rejects invalid nonce and mismatched package/cert digest.
- Rotation: rotate server signing key (kid change) and verify smooth client behavior.
- Revocation: revoke device key or refresh token and ensure app requires re-auth.
- Timeouts: verify access token short lifetime; refresh on schedule; server denies expired tokens.
11.14 Lab exercises (authorized, reproducible)
Use a mock banking backend and a lab app. Do not test against production.
Lab 11-A — Secure token storage
- Generate AES-GCM key in Keystore; encrypt and store a fake refresh token.
- Verify ciphertext presence; confirm plaintext absent; test decryption gated by biometric.
Deliverables: code snippet, screenshots, logcat entries, and a short memo explaining key parameters.
Lab 11-B — Device binding with ECDSA
- Generate ECDSA key pair in Keystore; upload public key to mock server.
- Sign a server-provided challenge; server verifies signature and records device key thumbprint.
- Attempt operation with wrong device → server must reject.
Deliverables: server logs, challenge/response transcript, pass/fail matrix.
Lab 11-C — Attestation validation path
- Implement server nonce endpoint and validation handler (Node/Java/Python skeletons).
- App requests nonce, gets attestation token, submits to server.
- Test cases: valid token; wrong nonce; expired token; wrong package digest.
Deliverables: server verification logs with reasons for accept/deny, trace IDs.
Lab 11-D — Key rotation & revocation
- Rotate server signing key (new
kid); verify token issuance and acceptance. - Revoke refresh token/device key; app should receive 401/403 and re-login.
Deliverables: rotation plan, screenshots/logs of 401/403 and recovery flow.
11.15 Checklists
11.15.1 Developer “done-done” checklist
- No secrets hardcoded in code/resources.
- All tokens encrypted at rest; access token lifetime ≤ 10–15 min.
- Refresh token bound to device and revocable; one device → one token.
- Keystore keys are non-exportable; StrongBox where available.
- BiometricPrompt used for step-up; no custom crypto overlays.
- Attestation validated server-side (nonce, package, cert digest, timestamp).
- TLS 1.2/1.3; SPKI pinning with backup keys.
- Logging: verification results (trace IDs), token
kid, app version. - Incident runbook for key/token compromise and rotation.
11.15.2 Pentest review checklist
- Confirm token storage safe; attempt to locate plaintext tokens.
- Verify biometric gating actually enforced by Keystore (not just UI).
- Inspect attestation token in traffic; request server validation evidence.
- Confirm short-lived access tokens and proper refresh flow.
- Validate server rejects mismatched package/cert digest.
- Ensure revocation works (refresh token and device key).
11.16 Common mistakes & how to fix them
- Long-lived access tokens → shorten lifetimes; use refresh flow + device binding.
- Plaintext refresh tokens → encrypt with Keystore AES-GCM; gate with biometric.
- Client-only attestation decisions → move checks server-side; keep client as telemetry source.
- Weak RNG/IV reuse → use
SecureRandom+ GCM which generates IV per call; store IV with ciphertext. - Hardcoded keys → remove; derive per-device keys via keystore; server issues per-session tokens.
- Missing rotation → implement
kidwith JWKS; staged rotation; automated alarms.
11.17 Deliverables for Module 11
- Auth & Token Architecture Spec: flows, lifetimes, scopes, rotation, revocation.
- Keystore Usage Guide: key aliases, parameters, gating rules, and code snippets.
- Attestation Validation Playbook: server endpoints, fields, verification rules, logging schema.
- Incident Runbook: lost/stolen device, suspected token theft, rotation steps.
- Pentest Evidence Templates: artifacts to request/produce (tokens, nonces, logs, hashes).
11.18 “At a glance” cheat sheet
- Encrypt at rest: AES-GCM via Android Keystore; keys non-exportable.
- Bind to device: ECDSA keypair in Keystore + server challenge/verify.
- Attest freshness: server-issued nonce → validate token, store trace.
- Short-lived access; revocable refresh (device-scoped).
- Rotate everything: server signing keys (
kid), device keys, pins. - Never trust client: all critical checks happen server-side.
