Module 16 — Cryptography Misuse & Secure Implementation in Android

Short reminder: this module shows how to detect, test, and fix crypto misuse. It does not provide offensive exploit recipes for attacking real systems. Use all examples only in authorized labs or on your own test apps.

16.0 Learning objectives

After this module you will be able to:

  • Identify all common cryptographic misuses in Android apps (static + runtime).
  • Verify cryptographic primitives and their correct usage (algorithms, modes, IVs, nonces, padding).
  • Detect hardcoded secrets, poor randomness, reuse of IVs/nonces, wrong padding modes, and key/credential leakage.
  • Instrument apps safely in a test lab to observe crypto operations (inputs/outputs) and verify correctness.
  • Implement secure patterns: Keystore-backed keys, AES-GCM/ChaCha20-Poly1305, RSA-OAEP, ECDH, correct KDFs (Argon2/PBKDF2), and safe key lifecycles.
  • Encode tests and CI checks (Semgrep, MobSF, unit tests) that prevent crypto regressions.
  • Produce remediation tickets with code diffs and unit test evidence.

16.1 Cryptography fundamentals (precise checklist)

When you see a crypto usage, check each of the following explicitly:

  1. Algorithm — Is it modern and appropriate?
    • Good: AES-GCM, AES-CTR+HMAC (rare), ChaCha20-Poly1305, RSA-OAEP (for wrapping), ECDH (X25519 / P-256).
    • Bad: AES-ECB, RC4, DES/3DES, MD5/SHA1 for security-sensitive purposes.
  2. Mode & Auth — Is the mode authenticated (AEAD)?
    • Prefer AEAD (GCM, ChaCha20-Poly1305). If using non-AEAD, ensure separate integrity (HMAC) with correct order (Encrypt-then-MAC).
  3. IV / Nonce — Correct usage:
    • GCM/CTR: unique per key (never reuse IV with same key). Use SecureRandom or counter-mode with proper nonce management.
    • For GCM, 96-bit random IV is recommended; for certain systems counters are allowed if uniqueness guaranteed.
  4. Key management — Are keys generated/stored safely?
    • Keys must not be hardcoded or derivable from static data.
    • Use Android Keystore (hardware-backed / StrongBox when available). Mark keys non-exportable.
  5. Randomness — Use SecureRandom (no fixed seeds).
  6. Padding — Use AEAD to avoid padding oracle attack classes. For legacy modes, use safe padding verifications (but migrating to AEAD is better).
  7. KDF / Password hashing — Passwords must be hashed with a memory-hard KDF: Argon2 (preferred), scrypt, or PBKDF2 with sufficiently large iterations and salt.
  8. Key rotation & lifecycle — There must be a policy for rotation, revocation, and key destruction.
  9. Use of platform TLS — Don’t roll your own TLS; use platform TLS libs, and check for correct TLS configs (see Module 10 for TLS specifics).
  10. Attestation & Keystore binding — For higher assurance bind keys to TEE/StrongBox and validate attestations server-side.

16.2 Common misuse patterns — with detection signatures

Below are patterns you’ll encounter, what they mean, and how to detect them (static + runtime).

16.2.1 Hardcoded keys / secrets

  • What: AES keys, HMAC keys or RSA private keys embedded as string/byte arrays.
  • Detection (static):
    • Grep for long base64 string literals, e.g. "[A-Za-z0-9+/]{40,}".
    • Look for SecretKeySpec constructed from string constants: new SecretKeySpec("...".getBytes(), "AES").
    • Search decompiled code for private final static String KEY = "...".
  • Detection (runtime):
    • Frida hook on javax.crypto.SecretKeySpec constructor to log when keys created (lab-only).
  • Why bad: Anyone with the APK can extract key and decrypt all ciphertext.
  • Remediation: Generate keys at runtime and store in Keystore; never store raw keys in code or resources.

16.2.2 AES-ECB mode

  • What: Cipher.getInstance("AES/ECB/PKCS5Padding")
  • Detection (static):
    • Search for string AES/ECB or ECB in Cipher.getInstance calls.
  • Why bad: Deterministic block-by-block encryption leaks patterns.
  • Remediation: Use AES-GCM or ChaCha20-Poly1305.

16.2.3 Static IVs and nonce reuse

  • What: IV constant reuse like byte[] iv = new byte[]{0,0,0,...} or deriving IV deterministically and reusing across messages.
  • Detection:
    • Look for cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(<constant>)).
    • Look in storage for IVs identical across multiple records.
  • Why bad: Reuse in GCM/CTR breaks confidentiality and/or integrity.
  • Remediation: Use securely generated random IVs (96-bit for GCM) and store IV alongside ciphertext; ensure uniqueness per key.

16.2.4 No authenticated encryption or bad MAC placement

  • What: Use of AES-CBC without HMAC or HMAC implemented incorrectly (MAC-then-Encrypt).
  • Detection:
    • Cipher.getInstance("AES/CBC/...") without associated HMAC usage.
    • If HMAC used, check order — must be Encrypt-then-MAC.
  • Remediation: Use AEAD, or if impossible, implement MAC as Encrypt-then-MAC with independent key.

16.2.5 Weak KDF / password hashing

  • What: storing passwords hashed with MD5/SHA1 or unsalted hashes, or PBKDF2 with tiny iteration counts.
  • Detection:
    • Search for MessageDigest.getInstance("MD5") or PBKDF2WithHmacSHA1 with low iteration constant (e.g., <10000).
  • Remediation: Use Argon2 (server-side preferred) or PBKDF2-HMAC-SHA256 with high iteration counts. Use salt and peppering as appropriate.

16.2.6 Insecure RNG

  • What: Use of new java.util.Random() or Random seeded from predictable values.
  • Detection:
    • Search for new Random() or Random r = new Random( seed ).
  • Remediation: Use SecureRandom (instantiate without seed), prefer SecureRandom.getInstanceStrong() if available.

16.2.7 Improper use of RSA (padding or key sizes)

  • What: RSA without OAEP (PKCS#1 v1.5) used for encryption; small key sizes (<=1024).
  • Detection:
    • Cipher.getInstance("RSA/ECB/PKCS1Padding") or RSA with key length <2048.
  • Remediation: Use RSA-OAEP with SHA-256 or prefer ECDH for key agreement and ECDSA for signatures.

16.2.8 Rolling your own crypto primitives or protocols

  • What: Custom encryption schemes or home-rolled MACs.
  • Detection: Unusual byte manipulations, XOR loops, custom block chaining.
  • Remediation: Replace with vetted, standard primitives and libraries.

16.3 Static analysis techniques

16.3.1 Decompile & scan

  • Tools: JADX, CFR, apktool.
  • Look for:
    • Cipher.getInstance(...) patterns.
    • SecretKeySpec, KeyGenerator, Mac.getInstance, MessageDigest.
    • Keystore usage: KeyStore.getInstance("AndroidKeyStore"), KeyGenParameterSpec.
    • PBKDF2: SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") (note iterations).
  • Example grep pipeline:
jadx -d src app.apk
grep -RIn --exclude-dir=src/res -E "Cipher.getInstance|SecretKeySpec|KeyGenerator|getInstance\\(|PBKDF2|MessageDigest|Mac.getInstance" src/

16.3.2 Look for suspicious constants

  • Long base64 strings, hex arrays, 16/32/64 byte constants.
  • Example regex to find base64-like literals in decompiled code:
grep -RInE "\"([A-Za-z0-9+/]{40,}={0,2})\"" src/ | head

16.3.3 Manifest & resources

  • Search res/raw or assets for keys/config with secrets.
  • Check for android:debuggable="true" in manifest (dangerous for exposing internals).

16.4 Dynamic analysis techniques

Dynamic tests allow you to see real parameter values at runtime. Use only in controlled lab builds and with test accounts.

16.4.1 Frida interception patterns (lab-only)

  • Hook key APIs to inspect runtime parameters (do not modify in production).
  • Example: log Cipher.init calls to see algorithm/mode/IV.

Frida snippet (lab-only):

Java.perform(function() {
  var Cipher = Java.use("javax.crypto.Cipher");
  Cipher.getInstance.overload('java.lang.String').implementation = function(transformation) {
    console.log("[FRIDA] Cipher.getInstance -> " + transformation);
    return this.getInstance(transformation);
  };

  var Cipher_init = Cipher.init.overload('int','java.security.Key','java.security.spec.AlgorithmParameterSpec');
  Cipher_init.implementation = function(opmode, key, params) {
    try {
      console.log("[FRIDA] init op=" + opmode + " key=" + key + " params=" + params);
    } catch (e) {}
    return this.init(opmode, key, params);
  };
});
  • Use Frida to capture IV values, modes used, and occasionally keys (only for test builds where keys are exposed).

Cautions:

  • Frida may be detected by app anti-instrumentation; use only lab builds.
  • Do not leak live credentials in shared reports — redact.

16.4.2 Runtime verification of IV uniqueness

  • Instrument the app to log IVs used for each encryption and check for duplicates across ciphertexts.
  • If IV reused with same key → critical finding.

16.4.3 Observing stored ciphertexts

  • In storage forensics (Module 15), locate ciphertext blobs and check if IV is stored alongside ciphertext.
  • If ciphertext sizes align to block boundaries without IV or tag, likely insecure.

16.5 Secure implementation patterns — code & rationale

Below are recommended constructs, with practical Kotlin/Java snippets and explanations.

16.5.1 AES-GCM with Keystore-backed key (Kotlin)

Key generation in Keystore:

fun createAesKey(alias: String) {
  val keyGen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
  val spec = KeyGenParameterSpec.Builder(
      alias,
      KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
    )
    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
    .setKeySize(256)
    .setUserAuthenticationRequired(false) // or true for gating
    .build()
  keyGen.init(spec)
  keyGen.generateKey()
}

Encrypt:

fun encrypt(alias: String, plaintext: ByteArray): Pair<ByteArray, ByteArray> {
  val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
  val secretKey = (keyStore.getEntry(alias, null) as KeyStore.SecretKeyEntry).secretKey
  val cipher = Cipher.getInstance("AES/GCM/NoPadding")
  cipher.init(Cipher.ENCRYPT_MODE, secretKey)
  val iv = cipher.iv // 12 bytes recommended
  val ciphertext = cipher.doFinal(plaintext)
  return Pair(iv, ciphertext) // persist iv + ciphertext
}

Decrypt:

fun decrypt(alias: String, iv: ByteArray, ciphertext: ByteArray): ByteArray {
  val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
  val secretKey = (keyStore.getEntry(alias, null) as KeyStore.SecretKeyEntry).secretKey
  val cipher = Cipher.getInstance("AES/GCM/NoPadding")
  val spec = GCMParameterSpec(128, iv) // 128-bit auth tag
  cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
  return cipher.doFinal(ciphertext)
}

Notes & pitfalls:

  • Store IVs per ciphertext; never reuse IV for the same key.
  • Keep ciphertext format: [version||iv||ciphertext] for forward compatibility.
  • Handle AEADBadTagException gracefully (don’t leak tag differences).

16.5.2 ChaCha20-Poly1305 (when AEAD via Boring/Conscrypt)

  • Use when AES hardware not available or when you prefer crypto agility. APIs similar to GCM via Cipher.getInstance("ChaCha20-Poly1305") or libraries (conscrypt).

16.5.3 RSA-OAEP for key wrap (Java)

  • Use RSA only for wrapping (encrypting small symmetric keys), not for bulk data.
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
cipher.init(Cipher.WRAP_MODE, publicKey);
byte[] wrappedKey = cipher.wrap(aesKey);

Notes: Keep RSA key sizes >= 2048; prefer ECC for signatures (ECDSA) and X25519/P-256 for key agreement.

16.5.4 Password hashing

  • Client should never handle password hashing for storage — that’s the server’s job.
  • If client needs local password-derived keys, use PBKDF2 with high iterations or Argon2 (if library available) to derive per-device keys.
  • Example (PBKDF2):
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
val spec = PBEKeySpec(password.toCharArray(), salt, iterations, keyLength)
val key = factory.generateSecret(spec).encoded
  • Set iterations high on server CI and benchmark (e.g., >=100k or per OWASP guidance), or use Argon2.

16.5.5 Key derivation & HKDF

  • For deriving multiple keys from one root, use HKDF (HMAC-based KDF) rather than ad-hoc slicing.
  • Use a well-tested HKDF library.

16.5.6 Key rotation & cryptographic erase

  • Use key wrapping: store data encrypted by DataKey, which is wrapped by MasterKey in Keystore. To “erase” all data, delete/unregister MasterKey (cryptographic erase). This is better than trying to overwrite all files.

16.6 Testing strategies & unit/integration tests

16.6.1 Unit tests

  • For each crypto wrapper add tests:
    • encrypt -> decrypt returns original.
    • different IVs produce different ciphertexts.
    • decrypt with wrong IV fails (AEADBadTagException).
    • uniqueness test: generate 1000 IVs and assert no duplicates (statistical).
  • Example test (JUnit/Kotlin):
@Test fun testRoundTrip() {
  createAesKey("test-key")
  val (iv, ct) = encrypt("test-key", "hello".toByteArray())
  val pt = decrypt("test-key", iv, ct)
  assertEquals("hello", String(pt))
}

16.6.2 Fuzzing & negative tests

  • Pass malformed data to decryption and assert safe failure (no crashes).
  • Stress-test KDF parameters and ensure performance within acceptable bounds.

16.6.3 Integration tests (CI)

  • Add a matrix in CI that:
    • Builds debug + release variants.
    • Runs MobSF/semgrep for crypto misuses.
    • Runs unit crypto tests and fails on regressions.

16.6.4 Continuous monitoring

  • Integrate tests that periodically check Keystore availability and attestations in pre-prod environments.

16.7 CI & Static checks

16.7.1 Semgrep rules (examples)

  • Flag Cipher.getInstance("AES/ECB").
  • Flag SecretKeySpec built from literal.
  • Flag new Random() usage.

16.7.2 MobSF or custom APK scanners

  • Run as part of PR pipeline; block builds with hardcoded secrets or high-severity crypto findings.

16.7.3 Git pre-commit hooks

  • Reject commits containing base64 strings longer than 40 characters in non-test directories.

16.8 Forensic evidence & reporting

When you find crypto misuse, collect and present:

  1. Artifact location — file/class/method, APK path, line numbers (decompiled).
  2. Evidence — copied code snippet, binary string matches, hashed artifact, screenshots of console capture, trace of Frida output (lab-only) showing algorithm and IV. Redact secrets in public reports.
  3. Reproduction steps — exact build, device image, commands used.
  4. Risk assessment — what an attacker could do (e.g., decrypt stored tokens if key is hardcoded). Always tie to impact (session hijack, PII exposure).
  5. Remediation suggestion — code changes + tests to validate fix.

16.9 Lab exercises (authorized)

Lab 16-A — Find & fix hardcoded AES key

  • Deploy intentionally vulnerable app with new SecretKeySpec("0123456789abcdef".getBytes(), "AES").
  • Steps:
    1. Decompile and locate SecretKeySpec creation.
    2. Replace with Keystore key generation + wrapper code.
    3. Validate with unit tests.

Lab 16-B — Detect nonce reuse

  • Instrument encryption calls to extract IVs and confirm uniqueness.
  • If reuse found, implement per-message random IVs and store them.

Lab 16-C — Replace AES-CBC + HMAC (insecure patterns) with AES-GCM

  • Show before/after performance and size differences; update unit tests.

16.10 Secure migration patterns

When replacing insecure storage/crypto in a live service:

  1. Introduce new scheme/versioning: store a version tag with ciphertext v1||iv||cipher.
  2. Dual-read logic: on read, try new scheme first; if no, try legacy and re-encrypt with new scheme.
  3. Rotate keys gradually: wrap data keys with new master key; background migration jobs re-wrap records.
  4. Telemetry: count remaining legacy items and monitor until phase-out.

Document migration and include rollback plan.

16.11 Common pitfalls & how to avoid them

  • Assuming obfuscation protects keys — it doesn’t. Keys in binaries are recoverable. Use Keystore.
  • Using AES-GCM incorrectly (tag/IV mismanagement) — always use authenticated exceptions as indicators of tampering.
  • Relying on client-side hashing for auth — password hashing & auth should be server-side.
  • Leaving debug crypto code in production — remove weak fallbacks before release.
  • Not testing Keystore failures — simulate hardware unavailability and ensure graceful degradation.

16.12 Appendix: helper scripts & regexes

A. Find suspicious constants (bash)

# Find long base64-like string literals in decompiled code
grep -RInE "\"([A-Za-z0-9+/]{40,}={0,2})\"" src/ | sed -n '1,200p'

B. Semgrep rule sample (YAML pseudo)

rules:
  - id: insecure-aes-ecb
    pattern: Cipher.getInstance("AES/ECB/%any")
    message: "Using AES/ECB is insecure. Use AES/GCM or ChaCha20-Poly1305."
    severity: ERROR

C. Frida script to watch Cipher (lab-only, safe)

(see 16.4.1 earlier for full script)

16.13 Deliverables & templates

  • Crypto Audit Checklist (one-page): algorithm, mode, IV, key storage, RNG, KDF, rotation.
  • Remediation PR template: code change, unit tests added, performance/perf regression notes, migration plan.
  • CI pipeline YAML: run semgrep + mobSF + unit crypto tests; fail on critical findings.
  • Report template: finding id, description, evidence, reproduction steps, impact, recommended fix, owner, due date.

16.14 Final recommendations

  • Prefer AEAD (AES-GCM / ChaCha20-Poly1305).
  • Store keys in Keystore: non-exportable, hardware-backed when possible.
  • Never hardcode secrets in code or resources.
  • Use secure RNG and per-message IV/nonce uniqueness.
  • Test thoroughly: unit, integration, CI, and controlled lab dynamic checks.
  • Plan migrations for legacy data with versioning and dual-read strategies.
  • Document key lifecycle, rotation, and incident runbook.