Skip to content

MASTG-DEMO-0058: Using KeyGenParameterSpec with a Broken ECB Block Mode

Download MASTG-DEMO-0058 APK Open MASTG-DEMO-0058 Folder Build MASTG-DEMO-0058 APK

Sample

This code demonstrates the risks of using AES in ECB mode (which is a broken mode of operation) using three scenarios:

  1. Importing a raw AES key into AndroidKeyStore with the purpose "decrypt" and mode "ECB"
  2. Importing a raw AES key into AndroidKeyStore with the purpose "encrypt" and mode "ECB"
  3. Generating an AES key in AndroidKeyStore with the purpose "encrypt" or "decrypt" and mode "ECB"

Current versions of Android prohibit the use of keys with ECB in some cases. For example, it is possible to use such a key for decryption but not to encrypt data by default, unless randomized encryption is explicitly disabled (bad practice).

MastgTest.kt
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package org.owasp.mastestapp

import android.content.Context
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.security.keystore.KeyProtection
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import android.util.Base64
import javax.crypto.spec.SecretKeySpec

class MastgTest(private val context: Context) {

    fun mastgTest(): String {

        val results = mutableListOf<String>()
        var rawKey: SecretKey? = null
        var encryptedData: ByteArray? = null
        var decryptedData: ByteArray? = null

        try {
            // Suppose we received the raw key from a secure source and we want to use it for decryption.
            val rawKeyString = "43ede5660e82123ee091d6b4c8f7d150"
            val keyBytes = rawKeyString.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
            rawKey = SecretKeySpec(keyBytes, KeyProperties.KEY_ALGORITHM_AES)

            // The cipher text is 'Hello from OWASP MASTG!' AES/ECB encrypted using CyberChef:
            // https://gchq.github.io/CyberChef/#recipe=AES_Encrypt(%7B'option':'Hex','string':'43ede5660e82123ee091d6b4c8f7d150'%7D,%7B'option':'Hex','string':''%7D,'ECB','Raw','Hex',%7B'option':'Hex','string':''%7D)&input=SGVsbG8gZnJvbSBPV0FTUCBNQVNURyE
            val encryptedDataString = "20b0eef4e5ad3d8984a4fb94f6001885f0ce25104cb8251f600624b46dcefb92"
            encryptedData = encryptedDataString.chunked(2).map { it.toInt(16).toByte() }.toByteArray()

            val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
            val alias = "importedAesKey"
            val entry = KeyStore.SecretKeyEntry(rawKey)
            val protection = KeyProtection.Builder(KeyProperties.PURPOSE_DECRYPT)
                .setBlockModes(KeyProperties.BLOCK_MODE_ECB)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
                .build()
            keyStore.setEntry(alias, entry, protection)
            val importedKey = keyStore.getKey(alias, null) as SecretKey
            val cipher2 = Cipher.getInstance("AES/ECB/PKCS7Padding").apply {
                init(Cipher.DECRYPT_MODE, importedKey)
            }
            decryptedData = cipher2.doFinal(encryptedData)
            val decryptedString = String(decryptedData)
            results.add("\n[*] Keystore-imported AES ECB key decryption (plaintext):\n\n$decryptedString")
        } catch (e: Exception) {
            results.add("\n[!] Keystore-imported AES ECB key decryption error:\n\n${e.message}")
        }

        // Import the raw key into AndroidKeyStore with the purpose "encrypt" and mode "ECB"
        // The import succeeds in this case because we explicitly disable randomized encryption (bad practice)
        try {
            if (rawKey == null || encryptedData == null) {
                throw IllegalStateException("Key or data missing for encryption")
            }
            val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
            val alias = "importedAesKey2"
            val entry = KeyStore.SecretKeyEntry(rawKey)
            val protection = KeyProtection.Builder(KeyProperties.PURPOSE_ENCRYPT)
                .setBlockModes(KeyProperties.BLOCK_MODE_ECB)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
                .setRandomizedEncryptionRequired(false) // For demonstration purposes, we disable randomized encryption
                .build()
            keyStore.setEntry(alias, entry, protection)
            val importedKey = keyStore.getKey(alias, null) as SecretKey
            val cipher3 = Cipher.getInstance("AES/ECB/PKCS7Padding").apply {
                init(Cipher.ENCRYPT_MODE, importedKey)
            }
            val encryptedBytes = cipher3.doFinal(decryptedData)
            val encrypted = Base64.encodeToString(encryptedBytes, Base64.DEFAULT)

            results.add("\n\n[*] Keystore-imported AES ECB key encryption (ciphertext):\n\n$encrypted")
        } catch (e: Exception) {
            results.add("\n\n[!] Keystore-imported AES ECB key encryption error:\n\n${e.message}")
        }

        // Generate a raw key in the AndroidKeyStore with the purpose "encrypt" or "decrypt" and and mode "ECB"
        // The generation fails in this case because we don't disable randomized encryption
        try {
            val keyAlias = "testKeyGenParameter"
            val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
            val spec = KeyGenParameterSpec.Builder(
                keyAlias,
                KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
            )
                .setBlockModes(KeyProperties.BLOCK_MODE_ECB)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
                // .setRandomizedEncryptionRequired(false) // Disabling randomized encryption would allow the key to be used in ECB mode.
                .build()
            KeyGenerator.getInstance(
                KeyProperties.KEY_ALGORITHM_AES,
                "AndroidKeyStore"
            ).apply {
                init(spec)
                generateKey()
            }

            val secretKey = keyStore.getKey(keyAlias, null) as SecretKey
            val cipher = Cipher.getInstance("AES/ECB/PKCS7Padding").apply {
                init(Cipher.ENCRYPT_MODE, secretKey)
            }
            val encrypted = Base64.encodeToString(cipher.doFinal(decryptedData), Base64.DEFAULT)
            results.add("\n[*] Keystore-generated AES ECB key encryption (ciphertext):\n\n$encrypted")
        } catch (e: Exception) {
            results.add("\n[!] Keystore-generated AES ECB error:\n\n${e.message}")
        }

        return results.joinToString("\n")
    }
}

When executing the code, you will see the following results for each of the three scenarios:

  1. Decryption succeeds because that's always allowed.
  2. Encryption succeeds. The import succeeds in this case because we explicitly disable randomized encryption (bad practice). Otherwise, KeyStore.setEntry would fail with an error similar to the one for scenario 3.
  3. Encryption cannot even happen because the generation fails (KeyGenerator.init specifically) due to randomized encryption not being disabled. The error says "Randomized encryption (IND-CPA) required but may be violated by block mode: ECB. See android.security.keystore.KeyGenParameterSpec documentation".

Steps

  1. Install the app on a device ( Installing Apps)
  2. Make sure you have Frida for Android installed on your machine and the frida-server running on the device
  3. Run run.sh to spawn the app with Frida
  4. Click the Start button
  5. Stop the script by pressing Ctrl+C and/or q to quit the Frida CLI

These are the relevant methods we are hooking to detect the use of ECB and whether randomized encryption is disabled:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
var target = {
    category: "CRYPTO",
    demo: "0058",
    hooks: [
    {
      class: "android.security.keystore.KeyGenParameterSpec$Builder",
      methods: [
        "setBlockModes",
        "setRandomizedEncryptionRequired"
      ]
    },
    {
      class: "android.security.keystore.KeyProtection$Builder",
      methods: [
        "setBlockModes",
        "setRandomizedEncryptionRequired"
      ]
    }
  ]
}
1
2
#!/bin/bash
../../../../utils/frida/android/run.sh ./hooks.js

Observation

The output shows all instances of block modes that were found at runtime. A backtrace is also provided to help identify the location in the code. If randomized encryption is disabled, that is also indicated in the output.

output.json
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
{
  "id": "a0a0815c-fa2c-43a5-ad35-10c2a3d097cd",
  "category": "CRYPTO",
  "time": "2025-09-12T16:45:55.639Z",
  "class": "android.security.keystore.KeyProtection$Builder",
  "method": "setBlockModes",
  "stackTrace": [
    "android.security.keystore.KeyProtection$Builder.setBlockModes(Native Method)",
    "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:41)",
    "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$6$lambda$5(MainActivity.kt:55)",
    "org.owasp.mastestapp.MainActivityKt.$r8$lambda$PtKdgqcXvbS9cMNZVWq3K3GGQKQ(Unknown Source:0)",
    "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)",
    "java.lang.Thread.run(Thread.java:1012)"
  ],
  "inputParameters": [
    {
      "type": "[Ljava.lang.String;",
      "value": [
        "ECB"
      ]
    }
  ],
  "returnValue": [
    {
      "type": "android.security.keystore.KeyProtection$Builder",
      "value": "<instance: android.security.keystore.KeyProtection$Builder>"
    }
  ]
}
{
  "id": "1ade0c02-74c8-4629-bbd4-5f51c9616b68",
  "category": "CRYPTO",
  "time": "2025-09-12T16:45:55.671Z",
  "class": "android.security.keystore.KeyProtection$Builder",
  "method": "setBlockModes",
  "stackTrace": [
    "android.security.keystore.KeyProtection$Builder.setBlockModes(Native Method)",
    "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:65)",
    "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$6$lambda$5(MainActivity.kt:55)",
    "org.owasp.mastestapp.MainActivityKt.$r8$lambda$PtKdgqcXvbS9cMNZVWq3K3GGQKQ(Unknown Source:0)",
    "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)",
    "java.lang.Thread.run(Thread.java:1012)"
  ],
  "inputParameters": [
    {
      "type": "[Ljava.lang.String;",
      "value": [
        "ECB"
      ]
    }
  ],
  "returnValue": [
    {
      "type": "android.security.keystore.KeyProtection$Builder",
      "value": "<instance: android.security.keystore.KeyProtection$Builder>"
    }
  ]
}
{
  "id": "48a8aa46-4ada-470e-bb5e-47ceefb53e1b",
  "category": "CRYPTO",
  "time": "2025-09-12T16:45:55.673Z",
  "class": "android.security.keystore.KeyProtection$Builder",
  "method": "setRandomizedEncryptionRequired",
  "stackTrace": [
    "android.security.keystore.KeyProtection$Builder.setRandomizedEncryptionRequired(Native Method)",
    "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:67)",
    "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$6$lambda$5(MainActivity.kt:55)",
    "org.owasp.mastestapp.MainActivityKt.$r8$lambda$PtKdgqcXvbS9cMNZVWq3K3GGQKQ(Unknown Source:0)",
    "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)",
    "java.lang.Thread.run(Thread.java:1012)"
  ],
  "inputParameters": [
    {
      "type": "boolean",
      "value": false
    }
  ],
  "returnValue": [
    {
      "type": "android.security.keystore.KeyProtection$Builder",
      "value": "<instance: android.security.keystore.KeyProtection$Builder>"
    }
  ]
}
{
  "id": "fe6e69b6-2764-473e-80d2-5a242b56947e",
  "category": "CRYPTO",
  "time": "2025-09-12T16:45:55.683Z",
  "class": "android.security.keystore.KeyGenParameterSpec$Builder",
  "method": "setBlockModes",
  "stackTrace": [
    "android.security.keystore.KeyGenParameterSpec$Builder.setBlockModes(Native Method)",
    "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:90)",
    "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$6$lambda$5(MainActivity.kt:55)",
    "org.owasp.mastestapp.MainActivityKt.$r8$lambda$PtKdgqcXvbS9cMNZVWq3K3GGQKQ(Unknown Source:0)",
    "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)",
    "java.lang.Thread.run(Thread.java:1012)"
  ],
  "inputParameters": [
    {
      "type": "[Ljava.lang.String;",
      "value": [
        "ECB"
      ]
    }
  ],
  "returnValue": [
    {
      "type": "android.security.keystore.KeyGenParameterSpec$Builder",
      "value": "<instance: android.security.keystore.KeyGenParameterSpec$Builder>"
    }
  ]
}

Evaluation

The test fails because the KeyGenParameterSpec.Builder#setBlockModes(...) and KeyProtection.Builder#setBlockModes(...) methods have been called with ECB.

1
2
3
Class: android.security.keystore.KeyProtection$Builder, Method: setBlockModes, Block modes: ECB
Class: android.security.keystore.KeyProtection$Builder, Method: setBlockModes, Block modes: ECB
Class: android.security.keystore.KeyGenParameterSpec$Builder, Method: setBlockModes, Block modes: ECB
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#!/bin/bash

jq -r -s '
  flatten
  | .[]
  | select(
      .method=="setBlockModes"
      and any(.inputParameters[]?.value[]?; . == "ECB")
    )
  | "Class: \(.class), Method: \(.method), Block modes: \([.inputParameters[]?.value[]?] | join(", "))"
' output.json > evaluation.txt

Regardless of whether the encryption succeeds or not, ECB should never be used in security-sensitive apps. Also, being present in the app may indicate issues in other parts of the app ecosystem (e.g., backend services).