Skip to content

MASTG-DEMO-0101: Local Storage for Input Validation with semgrep

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

Sample

The sample implements a small role based demo using SharedPreferences. On the first run, it initializes two entries, user_role_insecure and user_role_secure, both with the value user. The secure entry is stored together with an HMAC. On later runs, the app reads both values back through loadData(...) and uses them in a security-relevant decision.

The important detail is that loadData(...) has two modes. When called with useHmac = false, it returns the value loaded from SharedPreferences directly. When called with useHmac = true, it loads the companion HMAC, recomputes the HMAC for the stored value, and only returns the value if both match. The app then compares the loaded role with admin to demonstrate whether tampering succeeded.

App behavior:

  • A first click on Start triggers the setup and display the stored values being both user.
  • A second click triggers the check, which without any tampering shows:

    ✅ Insecure value unchanged.
    ✅ Secure value unchanged.
    
  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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
package org.owasp.mastestapp

import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import androidx.core.content.edit
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

class MastgTest(private val context: Context) {

    companion object {
        private const val PREFS_NAME = "app_settings"
        private const val HMAC_ALGORITHM = "HmacSHA256"
        private const val SECRET_KEY = "this-is-a-very-secret-key-for-the-demo"

        private const val KEY_SETUP_COMPLETE = "setup_complete"
        private const val KEY_ROLE_INSECURE = "user_role_insecure"
        private const val KEY_ROLE_SECURE = "user_role_secure"
        private const val KEY_ROLE_SECURE_HMAC = "user_role_secure_hmac"

        private const val DEFAULT_ROLE = "user"
        private const val TAMPERING_DETECTED = "tampering_detected"
        private const val TAG = "MASTG-TEST"
    }

    fun mastgTest(): String {
        val prefs = prefs()

        if (!prefs.contains(KEY_SETUP_COMPLETE)) {
            initializeDemoData()
            return """
SETUP DONE.

Stored values:
insecure = user
secure = user
secure_hmac = HMAC(user)

Run the app again after editing app_settings.xml.
""".trimIndent()
        }

        val insecureRole = loadPlain(KEY_ROLE_INSECURE, "error")
        val secureRole = loadProtected(KEY_ROLE_SECURE, TAMPERING_DETECTED)

        return """
Stored now:
insecure = ${prefs.getString(KEY_ROLE_INSECURE, null)}
secure = ${prefs.getString(KEY_ROLE_SECURE, null)}
secure_hmac = ${prefs.getString(KEY_ROLE_SECURE_HMAC, null)}

Loaded:
insecure = $insecureRole
secure = $secureRole

Result:
${describeInsecure(insecureRole)}
${describeSecure(secureRole)}
""".trimIndent()
    }

    private fun describeInsecure(value: String): String {
        return when (value) {
            "admin" -> "❌ Insecure check bypassed."
            "user" -> "✅ Insecure value unchanged."
            else -> "⚠️ Insecure value unexpected."
        }
    }

    private fun describeSecure(value: String): String {
        return when (value) {
            TAMPERING_DETECTED -> "✅ Secure check detected tampering."
            "admin" -> "⚠️ Secure check bypassed with forged HMAC."
            "user" -> "✅ Secure value unchanged."
            else -> "⚠️ Secure value unexpected."
        }
    }

    private fun initializeDemoData() {
        savePlain(KEY_ROLE_INSECURE, DEFAULT_ROLE)
        saveProtected(KEY_ROLE_SECURE, DEFAULT_ROLE)
        prefs().edit(commit = true) {
            putBoolean(KEY_SETUP_COMPLETE, true)
        }
    }

    private fun savePlain(key: String, value: String) {
        prefs().edit(commit = true) {
            putString(key, value)
        }
        Log.d(TAG, "Saved plain value, key=$key, value=$value")
    }

    private fun saveProtected(key: String, value: String) {
        val hmac = calculateHmac(value)
        prefs().edit(commit = true) {
            putString(key, value)
            putString("${key}_hmac", hmac)
        }
        Log.d(TAG, "Saved protected value, key=$key, value=$value, hmac=$hmac")
    }

    private fun loadPlain(key: String, defaultValue: String): String {
        return prefs().getString(key, defaultValue) ?: defaultValue
    }

    private fun loadProtected(key: String, defaultValue: String): String {
        val value = prefs().getString(key, null) ?: return defaultValue
        val storedHmac = prefs().getString("${key}_hmac", null) ?: return defaultValue
        val calculatedHmac = calculateHmac(value)
        return if (storedHmac == calculatedHmac) value else defaultValue
    }

    private fun calculateHmac(data: String): String {
        return try {
            val mac = Mac.getInstance(HMAC_ALGORITHM)
            val key = SecretKeySpec(SECRET_KEY.toByteArray(Charsets.UTF_8), HMAC_ALGORITHM)
            mac.init(key)
            bytesToHex(mac.doFinal(data.toByteArray(Charsets.UTF_8)))
        } catch (e: NoSuchAlgorithmException) {
            Log.e(TAG, "HMAC algorithm not found", e)
            ""
        } catch (e: InvalidKeyException) {
            Log.e(TAG, "Invalid HMAC key", e)
            ""
        }
    }

    private fun bytesToHex(bytes: ByteArray): String {
        val hexChars = "0123456789abcdef"
        val result = StringBuilder(bytes.size * 2)
        bytes.forEach { b ->
            val i = b.toInt() and 0xff
            result.append(hexChars[i ushr 4])
            result.append(hexChars[i and 0x0f])
        }
        return result.toString()
    }

    private fun prefs(): SharedPreferences {
        return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
    }
}
  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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
package org.owasp.mastestapp;

import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import kotlin.Metadata;
import kotlin.UByte;
import kotlin.jvm.internal.Intrinsics;
import kotlin.text.Charsets;
import kotlin.text.StringsKt;

/* compiled from: MastgTest.kt */
@Metadata(d1 = {"\u00000\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u000e\n\u0002\b\u0004\n\u0002\u0010\u0002\n\u0002\b\n\n\u0002\u0010\u0012\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0002\b\u0007\u0018\u0000 \u001a2\u00020\u0001:\u0001\u001aB\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\u0006\u0010\u0006\u001a\u00020\u0007J\u0010\u0010\b\u001a\u00020\u00072\u0006\u0010\t\u001a\u00020\u0007H\u0002J\u0010\u0010\n\u001a\u00020\u00072\u0006\u0010\t\u001a\u00020\u0007H\u0002J\b\u0010\u000b\u001a\u00020\fH\u0002J\u0018\u0010\r\u001a\u00020\f2\u0006\u0010\u000e\u001a\u00020\u00072\u0006\u0010\t\u001a\u00020\u0007H\u0002J\u0018\u0010\u000f\u001a\u00020\f2\u0006\u0010\u000e\u001a\u00020\u00072\u0006\u0010\t\u001a\u00020\u0007H\u0002J\u0018\u0010\u0010\u001a\u00020\u00072\u0006\u0010\u000e\u001a\u00020\u00072\u0006\u0010\u0011\u001a\u00020\u0007H\u0002J\u0018\u0010\u0012\u001a\u00020\u00072\u0006\u0010\u000e\u001a\u00020\u00072\u0006\u0010\u0011\u001a\u00020\u0007H\u0002J\u0010\u0010\u0013\u001a\u00020\u00072\u0006\u0010\u0014\u001a\u00020\u0007H\u0002J\u0010\u0010\u0015\u001a\u00020\u00072\u0006\u0010\u0016\u001a\u00020\u0017H\u0002J\b\u0010\u0018\u001a\u00020\u0019H\u0002R\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000¨\u0006\u001b"}, d2 = {"Lorg/owasp/mastestapp/MastgTest;", "", "context", "Landroid/content/Context;", "<init>", "(Landroid/content/Context;)V", "mastgTest", "", "describeInsecure", "value", "describeSecure", "initializeDemoData", "", "savePlain", "key", "saveProtected", "loadPlain", "defaultValue", "loadProtected", "calculateHmac", "data", "bytesToHex", "bytes", "", "prefs", "Landroid/content/SharedPreferences;", "Companion", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48)
/* loaded from: classes3.dex */
public final class MastgTest {
    private static final String DEFAULT_ROLE = "user";
    private static final String HMAC_ALGORITHM = "HmacSHA256";
    private static final String KEY_ROLE_INSECURE = "user_role_insecure";
    private static final String KEY_ROLE_SECURE = "user_role_secure";
    private static final String KEY_ROLE_SECURE_HMAC = "user_role_secure_hmac";
    private static final String KEY_SETUP_COMPLETE = "setup_complete";
    private static final String PREFS_NAME = "app_settings";
    private static final String SECRET_KEY = "this-is-a-very-secret-key-for-the-demo";
    private static final String TAG = "MASTG-TEST";
    private static final String TAMPERING_DETECTED = "tampering_detected";
    private final Context context;
    public static final int $stable = 8;

    public MastgTest(Context context) {
        Intrinsics.checkNotNullParameter(context, "context");
        this.context = context;
    }

    public final String mastgTest() throws IllegalStateException, NoSuchAlgorithmException, InvalidKeyException {
        SharedPreferences prefs = prefs();
        if (!prefs.contains(KEY_SETUP_COMPLETE)) {
            initializeDemoData();
            return "SETUP DONE.\n\nStored values:\ninsecure = user\nsecure = user\nsecure_hmac = HMAC(user)\n\nRun the app again after editing app_settings.xml.";
        }
        String insecureRole = loadPlain(KEY_ROLE_INSECURE, "error");
        String secureRole = loadProtected(KEY_ROLE_SECURE, TAMPERING_DETECTED);
        return StringsKt.trimIndent("\nStored now:\ninsecure = " + prefs.getString(KEY_ROLE_INSECURE, null) + "\nsecure = " + prefs.getString(KEY_ROLE_SECURE, null) + "\nsecure_hmac = " + prefs.getString(KEY_ROLE_SECURE_HMAC, null) + "\n\nLoaded:\ninsecure = " + insecureRole + "\nsecure = " + secureRole + "\n\nResult:\n" + describeInsecure(insecureRole) + "\n" + describeSecure(secureRole) + "\n");
    }

    private final String describeInsecure(String value) {
        return Intrinsics.areEqual(value, "admin") ? "❌ Insecure check bypassed." : Intrinsics.areEqual(value, DEFAULT_ROLE) ? "✅ Insecure value unchanged." : "⚠️ Insecure value unexpected.";
    }

    /* JADX WARN: Can't fix incorrect switch cases order, some code will duplicate */
    /* JADX WARN: Failed to restore switch over string. Please report as a decompilation issue
    java.lang.NullPointerException: Cannot invoke "java.util.List.iterator()" because the return value of "jadx.core.dex.visitors.regions.SwitchOverStringVisitor$SwitchData.getNewCases()" is null
        at jadx.core.dex.visitors.regions.SwitchOverStringVisitor.restoreSwitchOverString(SwitchOverStringVisitor.java:109)
        at jadx.core.dex.visitors.regions.SwitchOverStringVisitor.visitRegion(SwitchOverStringVisitor.java:66)
        at jadx.core.dex.visitors.regions.DepthRegionTraversal.traverseIterativeStepInternal(DepthRegionTraversal.java:77)
        at jadx.core.dex.visitors.regions.DepthRegionTraversal.traverseIterativeStepInternal(DepthRegionTraversal.java:82)
        at jadx.core.dex.visitors.regions.DepthRegionTraversal.traverseIterative(DepthRegionTraversal.java:31)
        at jadx.core.dex.visitors.regions.SwitchOverStringVisitor.visit(SwitchOverStringVisitor.java:60)
     */
    /* JADX WARN: Removed duplicated region for block: B:17:0x002c A[ORIG_RETURN, RETURN] */
    /*
        Code decompiled incorrectly, please refer to instructions dump.
        To view partially-correct add '--show-bad-code' argument
    */
    private final java.lang.String describeSecure(java.lang.String r2) {
        /*
            r1 = this;
            int r0 = r2.hashCode()
            switch(r0) {
                case -432068: goto L20;
                case 3599307: goto L14;
                case 92668751: goto L8;
                default: goto L7;
            }
        L7:
            goto L2c
        L8:
            java.lang.String r0 = "admin"
            boolean r0 = r2.equals(r0)
            if (r0 != 0) goto L11
            goto L7
        L11:
            java.lang.String r0 = "⚠️ Secure check bypassed with forged HMAC."
            goto L2e
        L14:
            java.lang.String r0 = "user"
            boolean r0 = r2.equals(r0)
            if (r0 != 0) goto L1d
            goto L7
        L1d:
            java.lang.String r0 = "✅ Secure value unchanged."
            goto L2e
        L20:
            java.lang.String r0 = "tampering_detected"
            boolean r0 = r2.equals(r0)
            if (r0 != 0) goto L29
            goto L7
        L29:
            java.lang.String r0 = "✅ Secure check detected tampering."
            goto L2e
        L2c:
            java.lang.String r0 = "⚠️ Secure value unexpected."
        L2e:
            return r0
        */
        throw new UnsupportedOperationException("Method not decompiled: org.owasp.mastestapp.MastgTest.describeSecure(java.lang.String):java.lang.String");
    }

    private final void initializeDemoData() throws IllegalStateException, NoSuchAlgorithmException, InvalidKeyException {
        savePlain(KEY_ROLE_INSECURE, DEFAULT_ROLE);
        saveProtected(KEY_ROLE_SECURE, DEFAULT_ROLE);
        SharedPreferences $this$edit$iv = prefs();
        SharedPreferences.Editor editor$iv = $this$edit$iv.edit();
        editor$iv.putBoolean(KEY_SETUP_COMPLETE, true);
        editor$iv.commit();
    }

    private final void savePlain(String key, String value) {
        SharedPreferences $this$edit$iv = prefs();
        SharedPreferences.Editor editor$iv = $this$edit$iv.edit();
        editor$iv.putString(key, value);
        editor$iv.commit();
        Log.d(TAG, "Saved plain value, key=" + key + ", value=" + value);
    }

    private final void saveProtected(String key, String value) throws IllegalStateException, NoSuchAlgorithmException, InvalidKeyException {
        String hmac = calculateHmac(value);
        SharedPreferences $this$edit$iv = prefs();
        SharedPreferences.Editor editor$iv = $this$edit$iv.edit();
        editor$iv.putString(key, value);
        editor$iv.putString(key + "_hmac", hmac);
        editor$iv.commit();
        Log.d(TAG, "Saved protected value, key=" + key + ", value=" + value + ", hmac=" + hmac);
    }

    private final String loadPlain(String key, String defaultValue) {
        String string = prefs().getString(key, defaultValue);
        return string == null ? defaultValue : string;
    }

    private final String loadProtected(String key, String defaultValue) throws IllegalStateException, NoSuchAlgorithmException, InvalidKeyException {
        String storedHmac;
        String value = prefs().getString(key, null);
        if (value == null || (storedHmac = prefs().getString(key + "_hmac", null)) == null) {
            return defaultValue;
        }
        String calculatedHmac = calculateHmac(value);
        return Intrinsics.areEqual(storedHmac, calculatedHmac) ? value : defaultValue;
    }

    private final String calculateHmac(String data) throws IllegalStateException, NoSuchAlgorithmException, InvalidKeyException {
        try {
            Mac mac = Mac.getInstance(HMAC_ALGORITHM);
            byte[] bytes = SECRET_KEY.getBytes(Charsets.UTF_8);
            Intrinsics.checkNotNullExpressionValue(bytes, "getBytes(...)");
            SecretKeySpec key = new SecretKeySpec(bytes, HMAC_ALGORITHM);
            mac.init(key);
            byte[] bytes2 = data.getBytes(Charsets.UTF_8);
            Intrinsics.checkNotNullExpressionValue(bytes2, "getBytes(...)");
            byte[] bArrDoFinal = mac.doFinal(bytes2);
            Intrinsics.checkNotNullExpressionValue(bArrDoFinal, "doFinal(...)");
            return bytesToHex(bArrDoFinal);
        } catch (InvalidKeyException e) {
            Log.e(TAG, "Invalid HMAC key", e);
            return "";
        } catch (NoSuchAlgorithmException e2) {
            Log.e(TAG, "HMAC algorithm not found", e2);
            return "";
        }
    }

    private final String bytesToHex(byte[] bytes) {
        StringBuilder result = new StringBuilder(bytes.length * 2);
        for (byte element$iv : bytes) {
            int i = element$iv & UByte.MAX_VALUE;
            result.append("0123456789abcdef".charAt(i >>> 4));
            result.append("0123456789abcdef".charAt(i & 15));
        }
        String string = result.toString();
        Intrinsics.checkNotNullExpressionValue(string, "toString(...)");
        return string;
    }

    private final SharedPreferences prefs() {
        SharedPreferences sharedPreferences = this.context.getSharedPreferences(PREFS_NAME, 0);
        Intrinsics.checkNotNullExpressionValue(sharedPreferences, "getSharedPreferences(...)");
        return sharedPreferences;
    }
}

Steps

Let's run semgrep rules against the sample code.

../../../../rules/mastg-android-local-storage-input-validation.yml
 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
rules:
  - id: mastg-android-local-storage-input-validation
    severity: WARNING
    languages:
      - java
    metadata:
      summary: SharedPreferences read that requires manual review.
    message: "[MASVS-CODE-4] SharedPreferences value loaded. Review whether integrity and authenticity are validated before the value is trusted."
    pattern-either:
      - patterns:
          - pattern-inside: |
              ...
              private final String loadPlain(...) {
                ...
              }
          - pattern-either:
              - pattern: prefs().getString(...)
              - pattern: prefs().getStringSet(...)
              - pattern: prefs().getInt(...)
              - pattern: prefs().getLong(...)
              - pattern: prefs().getFloat(...)
              - pattern: prefs().getBoolean(...)
      - patterns:
          - pattern-inside: |
              ...
              private final String loadProtected(...) {
                ...
              }
          - pattern-either:
              - pattern: prefs().getString(...)
              - pattern: prefs().getStringSet(...)
              - pattern: prefs().getInt(...)
              - pattern: prefs().getLong(...)
              - pattern: prefs().getFloat(...)
              - pattern: prefs().getBoolean(...)

  - id: mastg-android-hmac-validation-present
    severity: INFO
    languages:
      - java
    metadata:
      summary: HMAC or MAC based integrity logic present near local storage handling.
    message: "[MASVS-CODE-4] HMAC or MAC related integrity logic detected. Review whether it protects the reported local storage read and whether the secret is attacker accessible."
    pattern-either:
      - pattern: Mac.getInstance(...)
      - pattern: new SecretKeySpec(...)
      - pattern: $MAC.init(...)
      - pattern: $MAC.doFinal(...)
run.sh
1
2
#!/bin/bash
NO_COLOR=true semgrep -c ../../../../rules/mastg-android-local-storage-input-validation.yml ./MastgTest_reversed.java > output.txt

Observation

The rule reports two SharedPreferences.getString(...) reads inside loadData(...) and other matches related to nearby operations, which are useful as context.

output.txt
 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
┌─────────────────┐
 7 Code Findings 
└─────────────────┘

    MastgTest_reversed.java
    ❯❱ mastg-android-local-storage-input-validation
          [MASVS-CODE-4] SharedPreferences value loaded. Review whether integrity and authenticity are
          validated before the value is trusted.                                                      

          140 String string = prefs().getString(key, defaultValue);
            ⋮┆----------------------------------------
          146 String value = prefs().getString(key, null);
            ⋮┆----------------------------------------
          147 if (value == null || (storedHmac = prefs().getString(key + "_hmac", null)) == null) {

      mastg-android-hmac-validation-present
          [MASVS-CODE-4] HMAC or MAC related integrity logic detected. Review whether it protects the reported
          local storage read and whether the secret is attacker accessible.                                   

          156 Mac mac = Mac.getInstance(HMAC_ALGORITHM);
            ⋮┆----------------------------------------
          159 SecretKeySpec key = new SecretKeySpec(bytes, HMAC_ALGORITHM);
            ⋮┆----------------------------------------
          160 mac.init(key);
            ⋮┆----------------------------------------
          163 byte[] bArrDoFinal = mac.doFinal(bytes2);

Evaluation

The test case fails because at least one security relevant local storage path loads and trusts data without validating its integrity and authenticity.

The output is a good starting point for analysis, not the conclusion. To determine whether the test fails, we must inspect how the loaded values are handled after they are read.

Failing case: data loaded and trusted without integrity validation

Reverse engineering mastgTest() shows that one role is loaded through:

String insecureRole = loadPlain(KEY_ROLE_INSECURE, "error");

The implementation of loadPlain(...) reads from SharedPreferences and returns the value directly:

private final String loadPlain(String key, String defaultValue) {
    String string = prefs().getString(key, defaultValue);
    return string == null ? defaultValue : string;
}

This means that the role stored under user_role_insecure is accepted without any integrity or authenticity validation. Because the returned role is then used in the demo's security relevant result logic, this path fails the test.

You can demo this by editing the shared preferences manually and re-launching the app:

adb shell "am force-stop org.owasp.mastestapp"
adb shell "sed -i 's#>user</#>admin</#g' /data/data/org.owasp.mastestapp/shared_prefs/app_settings.xml"
adb shell "cat /data/data/org.owasp.mastestapp/shared_prefs/app_settings.xml"
adb shell monkey -p org.owasp.mastestapp -c android.intent.category.LAUNCHER 1

Click Start and you'll see:

❌ Insecure check bypassed.

Passing case: data validated with HMAC before use

The second role is loaded through:

String secureRole = loadProtected(KEY_ROLE_SECURE, TAMPERING_DETECTED);

Here, the app loads the stored HMAC, recomputes the HMAC over the stored value, and compares both before returning the value:

private final String loadProtected(String key, String defaultValue) throws IllegalStateException, NoSuchAlgorithmException, InvalidKeyException {
    String storedHmac;
    String value = prefs().getString(key, null);
    if (value == null || (storedHmac = prefs().getString(key + "_hmac", null)) == null) {
        return defaultValue;
    }
    String calculatedHmac = calculateHmac(value);
    return Intrinsics.areEqual(storedHmac, calculatedHmac) ? value : defaultValue;
}

So for this path, the value is not trusted just because it was read from local storage. It is first checked for integrity, and if the check fails the method returns the default value instead. Under the scope of this test, this path passes because the app does perform an integrity check before using the loaded value.

You can demo this by editing the shared preferences manually and re-launching the app:

adb shell "am force-stop org.owasp.mastestapp"
adb shell "sed -i 's#>user</#>admin</#g' /data/data/org.owasp.mastestapp/shared_prefs/app_settings.xml"
adb shell "cat /data/data/org.owasp.mastestapp/shared_prefs/app_settings.xml"
adb shell monkey -p org.owasp.mastestapp -c android.intent.category.LAUNCHER 1

Click Start and you'll see:

✅ Secure check detected tampering.

Final Note

Even though this demo includes a second issue about the HMAC key being hardcoded in the app, that issue is covered by a different test. Still, it is useful to understand how the protected path could be exploited in practice. An attacker who reverse engineers the app can recover the hardcoded key, compute a valid HMAC for a forged value (such as admin), and update the stored role and HMAC to make the integrity check succeed.

The hardcoded key is visible here:

private static final String SECRET_KEY = "this-is-a-very-secret-key-for-the-demo";

The forged HMAC for admin can be computed with:

python3 -c 'import hmac,hashlib; print(hmac.new(b"this-is-a-very-secret-key-for-the-demo", b"admin", hashlib.sha256).hexdigest())'

And then run:

adb shell "am force-stop org.owasp.mastestapp"
adb shell "sed -i \"s#<string name=\\\"user_role_secure\\\">user</string>#<string name=\\\"user_role_secure\\\">admin</string>#g\" /data/data/org.owasp.mastestapp/shared_prefs/app_settings.xml"
adb shell "sed -i \"s#<string name=\\\"user_role_secure_hmac\\\">[0-9a-f]*</string>#<string name=\\\"user_role_secure_hmac\\\">3e578c851ac37cb66033471a49585bedbb4dd5a3b2b2240f7ff6c8c2da993635</string>#g\" /data/data/org.owasp.mastestapp/shared_prefs/app_settings.xml"
adb shell monkey -p org.owasp.mastestapp -c android.intent.category.LAUNCHER 1

Click Start and you'll see:

⚠️ Secure check bypassed with forged HMAC.