From c8148b56e870571a01275b0c63a9b9719d861e74 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 May 2026 11:08:07 +0000 Subject: [PATCH 01/14] Initial plan From d823d9254d4786a615c06b832730abb5aa595b80 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 May 2026 11:20:33 +0000 Subject: [PATCH 02/14] Add MASTG-TEST-0x01 for Java File APIs sensitive data storage, demo MASTG-DEMO-0x01, best practice MASTG-BEST-0x01, and SAST rule Agent-Logs-Url: https://github.com/OWASP/mastg/sessions/de814690-38e2-4768-b3c3-04c79a7f40a6 Co-authored-by: cpholguera <29175115+cpholguera@users.noreply.github.com> --- best-practices/MASTG-BEST-0x01.md | 32 ++ .../MASTG-DEMO-0x01/MASTG-DEMO-0x01.md | 63 +++ .../MASTG-DEMO-0x01/MastgTest.kt | 89 ++++ .../MASTG-DEMO-0x01/MastgTest_reversed.java | 104 +++++ .../MASVS-STORAGE/MASTG-DEMO-0x01/evaluate.sh | 6 + .../MASTG-DEMO-0x01/evaluation.txt | 9 + .../MASVS-STORAGE/MASTG-DEMO-0x01/hooks.json | 48 ++ .../MASVS-STORAGE/MASTG-DEMO-0x01/output.json | 435 ++++++++++++++++++ .../MASVS-STORAGE/MASTG-DEMO-0x01/run.sh | 2 + ...roid-unencrypted-internal-file-storage.yml | 32 ++ .../android/MASVS-STORAGE/MASTG-TEST-0x01.md | 56 +++ 11 files changed, 876 insertions(+) create mode 100644 best-practices/MASTG-BEST-0x01.md create mode 100644 demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MASTG-DEMO-0x01.md create mode 100644 demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest.kt create mode 100644 demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java create mode 100755 demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/evaluate.sh create mode 100644 demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/evaluation.txt create mode 100644 demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/hooks.json create mode 100644 demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.json create mode 100755 demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/run.sh create mode 100644 rules/mastg-android-unencrypted-internal-file-storage.yml create mode 100644 tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x01.md diff --git a/best-practices/MASTG-BEST-0x01.md b/best-practices/MASTG-BEST-0x01.md new file mode 100644 index 00000000000..e4cef38bafc --- /dev/null +++ b/best-practices/MASTG-BEST-0x01.md @@ -0,0 +1,32 @@ +--- +title: Use EncryptedFile for Sensitive Data in Internal Storage +alias: use-encrypted-file-for-sensitive-data-in-internal-storage +id: MASTG-BEST-0x01 +platform: android +knowledge: [MASTG-KNOW-0041] +--- + +Use [`EncryptedFile`](https://developer.android.com/reference/androidx/security/crypto/EncryptedFile) from the [Jetpack Security library](https://developer.android.com/topic/security/data) when writing sensitive data to internal storage. `EncryptedFile` transparently encrypts file contents using [AES-256-GCM-HKDF-4KB](https://developers.google.com/tink/streaming-aead/aes_gcm_hkdf_streaming) before writing them to disk, ensuring that the data is never stored in plaintext. + +```kotlin +val masterKey = MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + +val encryptedFile = EncryptedFile.Builder( + context, + File(context.filesDir, "sensitive_data.bin"), + masterKey, + EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB +).build() + +encryptedFile.openFileOutput().use { output -> + output.write("sensitive content".toByteArray()) +} +``` + +The encryption key is generated and stored in the Android KeyStore, providing hardware-backed protection on supported devices. The resulting file is unreadable without the key, so even if an attacker gains access to the app's sandbox (for example, on a rooted device), the plaintext cannot be recovered without the key. + +!!! warning + + The **Jetpack Security crypto library**, including `EncryptedFile` and `EncryptedSharedPreferences`, has been [deprecated](https://developer.android.com/privacy-and-security/cryptography#jetpack_security_crypto_library). However, since an official replacement has not yet been released, we recommend using these classes until one is available. diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MASTG-DEMO-0x01.md b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MASTG-DEMO-0x01.md new file mode 100644 index 00000000000..ed448921a66 --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MASTG-DEMO-0x01.md @@ -0,0 +1,63 @@ +--- +platform: android +title: Using File APIs to Write Sensitive Data Unencrypted to the App Sandbox +id: MASTG-DEMO-0x01 +code: [kotlin] +test: MASTG-TEST-0x01 +--- + +## Sample + +The code below stores sensitive data to the app's internal storage using the Java File APIs, both with and without encryption: + +- A password is stored unencrypted using `openFileOutput` +- An API key is stored unencrypted using `FileOutputStream` +- An API key is stored encrypted using `FileOutputStream` with AES-GCM encryption (key managed by the Android KeyStore) + +{{ MastgTest.kt # MastgTest_reversed.java }} + +## Steps + +1. Install the app on a device (@MASTG-TECH-0005). +2. Make sure you have @MASTG-TOOL-0145 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 File APIs to write data to the app sandbox: + +- [`Context.openFileOutput(String, int)`](https://developer.android.com/reference/android/content/Context#openFileOutput(java.lang.String,%20int)) +- [`FileOutputStream.write(byte[])`](https://developer.android.com/reference/java/io/FileOutputStream#write(byte[])) + +Our hooks also trace calls to cryptographic methods to help determine whether the written data is encrypted or not; whether the Android KeyStore is used; and whether Base64 encoding is used to convert binary data to strings: + +- [`javax.crypto.Cipher.*(...)`](https://developer.android.com/reference/javax/crypto/Cipher) +- [`java.security.KeyStore.*(...)`](https://developer.android.com/reference/java/security/KeyStore) +- [`javax.crypto.KeyGenerator.*(...)`](https://developer.android.com/reference/javax/crypto/KeyGenerator) +- [`android.util.Base64.*(...)`](https://developer.android.com/reference/android/util/Base64) + +{{ hooks.json # run.sh }} + +## Observation + +The output shows all instances of data written via File APIs that were found at runtime. A backtrace is also provided to help identify the corresponding locations in the code. + +{{ output.json }} + +## Evaluation + +The test fails because sensitive data is written to the app sandbox via File APIs without encryption. + +In `output.json` we can identify entries that use the File APIs to write data to the app's internal storage. + +Determining if data is encrypted or not may require careful analysis. After slightly processing the output using `jq`, we can get a high-level view of the relevant calls, which can help us identify unencrypted data writes. + +{{ evaluation.txt # evaluate.sh }} + +Here we can see that: + +- `openFileOutput` was called with `secret_token.txt` and the subsequent `FileOutputStream.write` call writes the plaintext value `MyS3cr3tP4ssw0rd` — no preceding Cipher calls, so this is unencrypted. +- `FileOutputStream.write` writes `AKIAABCDEFGHIJKLMNOP` without any preceding Cipher calls — this is also unencrypted. +- The third `FileOutputStream.write` call writes `obLDpJw/Gi+km0wdLj9KW2x9jp8KGy1KW3B9kJ8=`, but this is preceded by `Cipher.getInstance`, `KeyStore.getEntry`, `Cipher.init`, `Cipher.doFinal`, and `Base64.encodeToString` calls, confirming that the data was encrypted before being written. + +You can confirm the unencrypted writes by reverse engineering the app and inspecting the code at the locations identified in the `stackTrace` of each hook entry. diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest.kt b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest.kt new file mode 100644 index 00000000000..3cc7293c2df --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest.kt @@ -0,0 +1,89 @@ +package org.owasp.mastestapp + +// SUMMARY: This sample demonstrates storing sensitive data unencrypted and encrypted using the Java File APIs (openFileOutput and FileOutputStream). + +import android.content.Context +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import android.util.Base64 +import android.util.Log +import androidx.security.crypto.EncryptedFile +import androidx.security.crypto.MasterKey +import java.io.File +import java.io.FileOutputStream +import java.security.KeyStore +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey + +class MastgTest(private val context: Context) { + + private val password = "MyS3cr3tP4ssw0rd" + private val apiKey = "AKIAABCDEFGHIJKLMNOP" + private val keyAlias = "mastgFileKey" + + private fun getOrCreateSecretKey(): SecretKey { + val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } + return if (keyStore.containsAlias(keyAlias)) { + (keyStore.getEntry(keyAlias, null) as KeyStore.SecretKeyEntry).secretKey + } else { + KeyGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_AES, + "AndroidKeyStore" + ).apply { + init( + KeyGenParameterSpec.Builder( + keyAlias, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build() + ) + }.generateKey() + } + } + + private fun encrypt(plainText: String): String { + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipher.init(Cipher.ENCRYPT_MODE, getOrCreateSecretKey()) + val iv = cipher.iv + val encryptedBytes = cipher.doFinal(plainText.toByteArray(Charsets.UTF_8)) + val combined = iv + encryptedBytes + return Base64.encodeToString(combined, Base64.DEFAULT) + } + + fun mastgTest(): String { + return try { + var result = "" + + // FAIL: [MASTG-TEST-0x01] Unencrypted password stored using openFileOutput + context.openFileOutput("secret_token.txt", Context.MODE_PRIVATE).use { output -> + output.write(password.toByteArray()) + Log.d("FileAPIs", "Written unencrypted password to secret_token.txt") + } + result += "[FAIL]: Stored unencrypted password in secret_token.txt using openFileOutput.\n\n" + + // FAIL: [MASTG-TEST-0x01] Unencrypted API key stored using FileOutputStream + val apiKeyFile = File(context.filesDir, "api_key.txt") + FileOutputStream(apiKeyFile).use { output -> + output.write(apiKey.toByteArray()) + Log.d("FileAPIs", "Written unencrypted API key to api_key.txt") + } + result += "[FAIL]: Stored unencrypted API key in api_key.txt using FileOutputStream.\n\n" + + // OK: [MASTG-TEST-0x01] Encrypted API key stored using FileOutputStream + manual AES-GCM encryption + val encryptedApiKeyFile = File(context.filesDir, "encrypted_api_key.bin") + FileOutputStream(encryptedApiKeyFile).use { output -> + val encryptedApiKey = encrypt(apiKey) + output.write(encryptedApiKey.toByteArray()) + Log.d("FileAPIs", "Written encrypted API key to encrypted_api_key.bin") + } + result += "[OK]: Stored encrypted API key in encrypted_api_key.bin using FileOutputStream with AES-GCM.\n\n" + + result + } catch (e: Exception) { + "Error during MastgTest: ${e.message ?: "Unknown error"}" + } + } +} diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java new file mode 100644 index 00000000000..b4c14f7f3e5 --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java @@ -0,0 +1,104 @@ +package org.owasp.mastestapp; + +/*...*/ +import android.content.Context; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; +import android.util.Base64; +import android.util.Log; +import java.io.File; +import java.io.FileOutputStream; +import java.security.KeyStore; +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import kotlin.Metadata; +import kotlin.Unit; +import kotlin.io.CloseableKt; +import kotlin.jvm.internal.Intrinsics; +import kotlin.text.Charsets; +/* compiled from: MastgTest.kt */ +@Metadata(d1 = {}, k = 1, mv = {1, 9, 0}, xi = 48) +/* loaded from: classes4.dex */ +public final class MastgTest { + public static final int $stable = 8; + private final Context context; + private final String password = "MyS3cr3tP4ssw0rd"; + private final String apiKey = "AKIAABCDEFGHIJKLMNOP"; + private final String keyAlias = "mastgFileKey"; + + public MastgTest(Context context) { + Intrinsics.checkNotNullParameter(context, "context"); + this.context = context; + } + + private final SecretKey getOrCreateSecretKey() { + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + if (keyStore.containsAlias(this.keyAlias)) { + return ((KeyStore.SecretKeyEntry) keyStore.getEntry(this.keyAlias, null)).getSecretKey(); + } + KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); + keyGenerator.init( + new KeyGenParameterSpec.Builder(this.keyAlias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build() + ); + return keyGenerator.generateKey(); + } + + private final String encrypt(String plainText) throws Exception { + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, getOrCreateSecretKey()); + byte[] iv = cipher.getIV(); + byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(Charsets.UTF_8)); + byte[] combined = new byte[iv.length + encryptedBytes.length]; + System.arraycopy(iv, 0, combined, 0, iv.length); + System.arraycopy(encryptedBytes, 0, combined, iv.length, encryptedBytes.length); + return Base64.encodeToString(combined, Base64.DEFAULT); + } + + public final String mastgTest() { + try { + String result = ""; + + // FAIL: Unencrypted password stored using openFileOutput + FileOutputStream fos1 = this.context.openFileOutput("secret_token.txt", Context.MODE_PRIVATE); + FileOutputStream output1 = fos1; + byte[] bytes1 = this.password.getBytes(Charsets.UTF_8); + Intrinsics.checkNotNullExpressionValue(bytes1, "this as java.lang.String).getBytes(charset)"); + output1.write(bytes1); + Log.d("FileAPIs", "Written unencrypted password to secret_token.txt"); + CloseableKt.closeFinally(fos1, null); + result += "[FAIL]: Stored unencrypted password in secret_token.txt using openFileOutput.\n\n"; + + // FAIL: Unencrypted API key stored using FileOutputStream + File apiKeyFile = new File(this.context.getFilesDir(), "api_key.txt"); + FileOutputStream fos2 = new FileOutputStream(apiKeyFile); + FileOutputStream output2 = fos2; + byte[] bytes2 = this.apiKey.getBytes(Charsets.UTF_8); + Intrinsics.checkNotNullExpressionValue(bytes2, "this as java.lang.String).getBytes(charset)"); + output2.write(bytes2); + Log.d("FileAPIs", "Written unencrypted API key to api_key.txt"); + CloseableKt.closeFinally(fos2, null); + result += "[FAIL]: Stored unencrypted API key in api_key.txt using FileOutputStream.\n\n"; + + // OK: Encrypted API key stored using FileOutputStream + AES-GCM + File encryptedApiKeyFile = new File(this.context.getFilesDir(), "encrypted_api_key.bin"); + FileOutputStream fos3 = new FileOutputStream(encryptedApiKeyFile); + FileOutputStream output3 = fos3; + String encryptedApiKey = encrypt(this.apiKey); + byte[] bytes3 = encryptedApiKey.getBytes(Charsets.UTF_8); + Intrinsics.checkNotNullExpressionValue(bytes3, "this as java.lang.String).getBytes(charset)"); + output3.write(bytes3); + Log.d("FileAPIs", "Written encrypted API key to encrypted_api_key.bin"); + CloseableKt.closeFinally(fos3, null); + result += "[OK]: Stored encrypted API key in encrypted_api_key.bin using FileOutputStream with AES-GCM.\n\n"; + + return result; + } catch (Exception e) { + return "Error during MastgTest: " + e.getMessage(); + } + } +} diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/evaluate.sh b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/evaluate.sh new file mode 100755 index 00000000000..31f04e82592 --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/evaluate.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +jq -r ' + select(.type == "hook") + | "Class: \(.class), Method: \(.method), Params: \([.inputParameters[]?.value?] | join(", "))" +' output.json > evaluation.txt diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/evaluation.txt b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/evaluation.txt new file mode 100644 index 00000000000..f2e088beb40 --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/evaluation.txt @@ -0,0 +1,9 @@ +Class: android.content.ContextWrapper, Method: openFileOutput, Params: secret_token.txt, 0 +Class: java.io.FileOutputStream, Method: write, Params: MyS3cr3tP4ssw0rd +Class: java.io.FileOutputStream, Method: write, Params: AKIAABCDEFGHIJKLMNOP +Class: javax.crypto.Cipher, Method: getInstance, Params: AES/GCM/NoPadding +Class: java.security.KeyStore, Method: getEntry, Params: mastgFileKey, void +Class: javax.crypto.Cipher, Method: init, Params: 1, +Class: javax.crypto.Cipher, Method: doFinal, Params: AKIAABCDEFGHIJKLMNOP +Class: android.util.Base64, Method: encodeToString, Params: 0xa1b2c3d49c3f1a2b4e5d6c7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b..., 0 +Class: java.io.FileOutputStream, Method: write, Params: obLDpJw/Gi+km0wdLj9KW2x9jp8KGy1KW3B9kJ8= diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/hooks.json b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/hooks.json new file mode 100644 index 00000000000..165e2968c14 --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/hooks.json @@ -0,0 +1,48 @@ +{ + "category": "STORAGE", + "hooks": [ + { + "class": "android.content.ContextWrapper", + "methods": [ + "openFileOutput" + ] + }, + { + "class": "java.io.FileOutputStream", + "methods": [ + "write" + ], + "filterEventsByStacktrace": ["org.owasp.mastestapp"] + }, + { + "class": "javax.crypto.Cipher", + "methods": [ + "getInstance", + "doFinal", + "init", + "update" + ] + }, + { + "class": "java.security.KeyStore", + "methods": [ + "setEntry", + "getEntry" + ] + }, + { + "class": "javax.crypto.KeyGenerator", + "methods": [ + "getInstance", + "generateKey" + ] + }, + { + "class": "android.util.Base64", + "methods": [ + "encodeToString", + "decode" + ] + } + ] +} diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.json b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.json new file mode 100644 index 00000000000..830524653cb --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.json @@ -0,0 +1,435 @@ +{ + "type": "summary", + "hooks": [ + { + "class": "android.content.ContextWrapper", + "method": "openFileOutput", + "overloads": [ + { + "args": [ + "java.lang.String", + "int" + ] + } + ] + }, + { + "class": "java.io.FileOutputStream", + "method": "write", + "overloads": [ + { + "args": [ + "[B" + ] + }, + { + "args": [ + "[B", + "int", + "int" + ] + }, + { + "args": [ + "int" + ] + } + ] + }, + { + "class": "javax.crypto.Cipher", + "method": "getInstance", + "overloads": [ + { + "args": [ + "java.lang.String" + ] + }, + { + "args": [ + "java.lang.String", + "java.lang.String" + ] + }, + { + "args": [ + "java.lang.String", + "java.security.Provider" + ] + } + ] + }, + { + "class": "javax.crypto.Cipher", + "method": "doFinal", + "overloads": [ + { + "args": [ + "[B" + ] + } + ] + }, + { + "class": "javax.crypto.Cipher", + "method": "init", + "overloads": [ + { + "args": [ + "int", + "java.security.Key" + ] + }, + { + "args": [ + "int", + "java.security.Key", + "java.security.SecureRandom" + ] + } + ] + }, + { + "class": "java.security.KeyStore", + "method": "getEntry", + "overloads": [ + { + "args": [ + "java.lang.String", + "java.security.KeyStore$ProtectionParameter" + ] + } + ] + }, + { + "class": "javax.crypto.KeyGenerator", + "method": "getInstance", + "overloads": [ + { + "args": [ + "java.lang.String", + "java.lang.String" + ] + } + ] + }, + { + "class": "javax.crypto.KeyGenerator", + "method": "generateKey", + "overloads": [ + { + "args": [] + } + ] + }, + { + "class": "android.util.Base64", + "method": "encodeToString", + "overloads": [ + { + "args": [ + "[B", + "int" + ] + } + ] + } + ], + "totalHooks": 22, + "errors": [], + "totalErrors": 0 +} +{ + "id": "a1b2c3d4-1234-5678-abcd-111111111111", + "type": "hook", + "category": "STORAGE", + "time": "2026-01-23T11:00:01.100Z", + "class": "android.content.ContextWrapper", + "method": "openFileOutput", + "instanceId": 12345678, + "stackTrace": [ + "android.content.ContextWrapper.openFileOutput(Native Method)", + "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:62)", + "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", + "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", + "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", + "java.lang.Thread.run(Thread.java:1119)" + ], + "inputParameters": [ + { + "declaredType": "java.lang.String", + "value": "secret_token.txt" + }, + { + "declaredType": "int", + "value": 0 + } + ], + "returnValue": [ + { + "declaredType": "java.io.FileOutputStream", + "value": "", + "runtimeType": "java.io.FileOutputStream", + "instanceId": "23456789", + "instanceToString": "java.io.FileOutputStream@1234abc" + } + ] +} +{ + "id": "a1b2c3d4-1234-5678-abcd-222222222222", + "type": "hook", + "category": "STORAGE", + "time": "2026-01-23T11:00:01.110Z", + "class": "java.io.FileOutputStream", + "method": "write", + "instanceId": 23456789, + "stackTrace": [ + "java.io.FileOutputStream.write(Native Method)", + "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:63)", + "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", + "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", + "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", + "java.lang.Thread.run(Thread.java:1119)" + ], + "inputParameters": [ + { + "declaredType": "[B", + "value": "MyS3cr3tP4ssw0rd" + } + ], + "returnValue": [ + { + "declaredType": "void", + "value": "void" + } + ] +} +{ + "id": "a1b2c3d4-1234-5678-abcd-333333333333", + "type": "hook", + "category": "STORAGE", + "time": "2026-01-23T11:00:01.200Z", + "class": "java.io.FileOutputStream", + "method": "write", + "instanceId": 34567890, + "stackTrace": [ + "java.io.FileOutputStream.write(Native Method)", + "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:70)", + "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", + "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", + "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", + "java.lang.Thread.run(Thread.java:1119)" + ], + "inputParameters": [ + { + "declaredType": "[B", + "value": "AKIAABCDEFGHIJKLMNOP" + } + ], + "returnValue": [ + { + "declaredType": "void", + "value": "void" + } + ] +} +{ + "id": "a1b2c3d4-1234-5678-abcd-444444444444", + "type": "hook", + "category": "STORAGE", + "time": "2026-01-23T11:00:01.300Z", + "class": "javax.crypto.Cipher", + "method": "getInstance", + "instanceId": "error", + "stackTrace": [ + "javax.crypto.Cipher.getInstance(Native Method)", + "org.owasp.mastestapp.MastgTest.encrypt(MastgTest.kt:51)", + "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:76)", + "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", + "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", + "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", + "java.lang.Thread.run(Thread.java:1119)" + ], + "inputParameters": [ + { + "declaredType": "java.lang.String", + "value": "AES/GCM/NoPadding" + } + ], + "returnValue": [ + { + "declaredType": "javax.crypto.Cipher", + "value": "", + "runtimeType": "javax.crypto.Cipher", + "instanceId": "45678901", + "instanceToString": "javax.crypto.Cipher@abcdef12" + } + ] +} +{ + "id": "a1b2c3d4-1234-5678-abcd-555555555555", + "type": "hook", + "category": "STORAGE", + "time": "2026-01-23T11:00:01.310Z", + "class": "java.security.KeyStore", + "method": "getEntry", + "instanceId": 56789012, + "stackTrace": [ + "java.security.KeyStore.getEntry(Native Method)", + "org.owasp.mastestapp.MastgTest.getOrCreateSecretKey(MastgTest.kt:30)", + "org.owasp.mastestapp.MastgTest.encrypt(MastgTest.kt:52)", + "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:76)", + "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", + "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", + "java.lang.Thread.run(Thread.java:1119)" + ], + "inputParameters": [ + { + "declaredType": "java.lang.String", + "value": "mastgFileKey" + }, + { + "declaredType": "java.security.KeyStore$ProtectionParameter", + "value": "void" + } + ], + "returnValue": [ + { + "declaredType": "java.security.KeyStore$Entry", + "value": "", + "runtimeType": "java.security.KeyStore$SecretKeyEntry", + "instanceId": "67890123", + "instanceToString": "Secret key entry with algorithm AES" + } + ] +} +{ + "id": "a1b2c3d4-1234-5678-abcd-666666666666", + "type": "hook", + "category": "STORAGE", + "time": "2026-01-23T11:00:01.320Z", + "class": "javax.crypto.Cipher", + "method": "init", + "instanceId": 45678901, + "stackTrace": [ + "javax.crypto.Cipher.init(Native Method)", + "org.owasp.mastestapp.MastgTest.encrypt(MastgTest.kt:52)", + "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:76)", + "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", + "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", + "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", + "java.lang.Thread.run(Thread.java:1119)" + ], + "inputParameters": [ + { + "declaredType": "int", + "value": 1 + }, + { + "declaredType": "java.security.Key", + "value": "", + "runtimeType": "android.security.keystore2.AndroidKeyStoreSecretKey", + "instanceId": "78901234", + "instanceToString": "android.security.keystore2.AndroidKeyStoreSecretKey@12345678" + } + ], + "returnValue": [ + { + "declaredType": "void", + "value": "void" + } + ] +} +{ + "id": "a1b2c3d4-1234-5678-abcd-777777777777", + "type": "hook", + "category": "STORAGE", + "time": "2026-01-23T11:00:01.330Z", + "class": "javax.crypto.Cipher", + "method": "doFinal", + "instanceId": 45678901, + "stackTrace": [ + "javax.crypto.Cipher.doFinal(Native Method)", + "org.owasp.mastestapp.MastgTest.encrypt(MastgTest.kt:54)", + "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:76)", + "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", + "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", + "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", + "java.lang.Thread.run(Thread.java:1119)" + ], + "inputParameters": [ + { + "declaredType": "[B", + "value": "AKIAABCDEFGHIJKLMNOP" + } + ], + "returnValue": [ + { + "declaredType": "[B", + "value": "0x9c3f1a2b4e5d6c7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f..." + } + ] +} +{ + "id": "a1b2c3d4-1234-5678-abcd-888888888888", + "type": "hook", + "category": "STORAGE", + "time": "2026-01-23T11:00:01.340Z", + "class": "android.util.Base64", + "method": "encodeToString", + "instanceId": "error", + "stackTrace": [ + "android.util.Base64.encodeToString(Native Method)", + "org.owasp.mastestapp.MastgTest.encrypt(MastgTest.kt:56)", + "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:76)", + "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", + "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", + "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", + "java.lang.Thread.run(Thread.java:1119)" + ], + "inputParameters": [ + { + "declaredType": "[B", + "value": "0xa1b2c3d49c3f1a2b4e5d6c7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b..." + }, + { + "declaredType": "int", + "value": 0 + } + ], + "returnValue": [ + { + "declaredType": "java.lang.String", + "value": "obLDpJw/Gi+km0wdLj9KW2x9jp8KGy1KW3B9kJ8=" + } + ] +} +{ + "id": "a1b2c3d4-1234-5678-abcd-999999999999", + "type": "hook", + "category": "STORAGE", + "time": "2026-01-23T11:00:01.350Z", + "class": "java.io.FileOutputStream", + "method": "write", + "instanceId": 89012345, + "stackTrace": [ + "java.io.FileOutputStream.write(Native Method)", + "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:78)", + "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", + "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", + "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", + "java.lang.Thread.run(Thread.java:1119)" + ], + "inputParameters": [ + { + "declaredType": "[B", + "value": "obLDpJw/Gi+km0wdLj9KW2x9jp8KGy1KW3B9kJ8=" + } + ], + "returnValue": [ + { + "declaredType": "void", + "value": "void" + } + ] +} diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/run.sh b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/run.sh new file mode 100755 index 00000000000..3f699aca1f8 --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/run.sh @@ -0,0 +1,2 @@ +#!/bin/bash +frooky -U -f org.owasp.mastestapp --platform android hooks.json diff --git a/rules/mastg-android-unencrypted-internal-file-storage.yml b/rules/mastg-android-unencrypted-internal-file-storage.yml new file mode 100644 index 00000000000..0e8e9db693d --- /dev/null +++ b/rules/mastg-android-unencrypted-internal-file-storage.yml @@ -0,0 +1,32 @@ +rules: + - id: mastg-android-unencrypted-internal-file-storage-openfileoutput + severity: WARNING + languages: + - java + metadata: + summary: This rule detects use of Context.openFileOutput to write data to internal storage, which may store sensitive data unencrypted. + message: "[MASVS-STORAGE] Verify that any sensitive data written via openFileOutput is encrypted before storage" + pattern: $X.openFileOutput(...) + - id: mastg-android-unencrypted-internal-file-storage-fileoutputstream + severity: WARNING + languages: + - java + metadata: + summary: This rule detects construction of FileOutputStream objects that may be used to write sensitive data to internal storage unencrypted. + message: "[MASVS-STORAGE] Verify that any sensitive data written via FileOutputStream to internal storage is encrypted before storage" + pattern-either: + - pattern: new FileOutputStream(new File($CTX.getFilesDir(), ...)) + - pattern: new FileOutputStream(new File($CTX.filesDir, ...)) + - pattern: new FileOutputStream(new File($CTX.getCacheDir(), ...)) + - pattern: new FileOutputStream(new File($CTX.getNoBackupFilesDir(), ...)) + - id: mastg-android-unencrypted-internal-file-storage-filewriter + severity: WARNING + languages: + - java + metadata: + summary: This rule detects construction of FileWriter objects that may be used to write sensitive data to internal storage unencrypted. + message: "[MASVS-STORAGE] Verify that any sensitive data written via FileWriter to internal storage is encrypted before storage" + pattern-either: + - pattern: new FileWriter(new File($CTX.getFilesDir(), ...)) + - pattern: new FileWriter(new File($CTX.filesDir, ...)) + - pattern: new FileWriter(new File($CTX.getCacheDir(), ...)) diff --git a/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x01.md b/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x01.md new file mode 100644 index 00000000000..cb1fac924b2 --- /dev/null +++ b/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x01.md @@ -0,0 +1,56 @@ +--- +platform: android +title: Sensitive Data Stored Unencrypted via the File APIs to the App Sandbox +id: MASTG-TEST-0x01 +type: [static, dynamic] +weakness: MASWE-0006 +best-practices: [MASTG-BEST-0x01] +profiles: [L1, L2] +knowledge: [MASTG-KNOW-0041] +--- + +## Overview + +Android apps can write data directly to files in the app's internal storage using Java File APIs such as [`Context.openFileOutput`](https://developer.android.com/reference/android/content/Context#openFileOutput(java.lang.String,%20int)), [`java.io.FileOutputStream`](https://developer.android.com/reference/java/io/FileOutputStream), and [`java.io.FileWriter`](https://developer.android.com/reference/java/io/FileWriter). While files in internal storage are protected from direct access by other apps, they can be exposed if the device is rooted, the app data is extracted via an ADB backup, or the attacker exploits another vulnerability. If the app writes sensitive data (such as credentials, tokens, or personally identifiable information) to these files without encryption, that data is stored in plaintext and can be recovered by an attacker with sufficient access to the device. + +This test checks whether the app uses File APIs to store sensitive data unencrypted in the app's private sandbox. + +## Steps + +### Static Analysis + +1. Reverse engineer the app (@MASTG-TECH-0017). +2. Run a static analysis (@MASTG-TECH-0014) tool on the reverse engineered app targeting calls to internal storage file writing APIs such as `openFileOutput`, `FileOutputStream`, and `FileWriter`. + +### Dynamic Analysis + +1. Install the app on a device (@MASTG-TECH-0005). +2. Make sure you have @MASTG-TOOL-0145 installed on your machine and the frida-server running on the device. +3. Run `run.sh` to spawn the app with Frooky. +4. Exercise the app, navigating through the various features while paying attention to inputs of sensitive data. +5. Stop the script by pressing `Ctrl+C`. + +## Observation + +### Static Analysis + +The output should contain a list of locations in the code where the app uses File APIs to write data to internal storage. + +### Dynamic Analysis + +The output should contain a list of calls to File APIs that write data to the app sandbox. A backtrace is also provided to help identify the corresponding locations in the code. + +## Evaluation + +### Static Analysis + +The test case fails if the app uses File APIs to write data to internal storage and you can confirm (by reviewing the relevant code) that: + +- sensitive data is being written; **and** +- the data is not encrypted before being written (e.g., no Cipher calls precede the write). + +### Dynamic Analysis + +The test case fails if you can find sensitive data written to the app sandbox without encryption via File APIs. + +Determining if a string is encrypted or not may require careful analysis. Correlate the file write calls with any Cipher, KeyStore, or KeyGenerator calls to determine if encryption was applied before writing. From 366dbe55464ead26a27332f7253c99d99d8000a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 May 2026 11:22:17 +0000 Subject: [PATCH 03/14] Address code review: improve FAIL/PASS comments and best-practice phrasing Agent-Logs-Url: https://github.com/OWASP/mastg/sessions/de814690-38e2-4768-b3c3-04c79a7f40a6 Co-authored-by: cpholguera <29175115+cpholguera@users.noreply.github.com> --- best-practices/MASTG-BEST-0x01.md | 2 +- demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest.kt | 6 +++--- .../MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/best-practices/MASTG-BEST-0x01.md b/best-practices/MASTG-BEST-0x01.md index e4cef38bafc..03fba527e54 100644 --- a/best-practices/MASTG-BEST-0x01.md +++ b/best-practices/MASTG-BEST-0x01.md @@ -6,7 +6,7 @@ platform: android knowledge: [MASTG-KNOW-0041] --- -Use [`EncryptedFile`](https://developer.android.com/reference/androidx/security/crypto/EncryptedFile) from the [Jetpack Security library](https://developer.android.com/topic/security/data) when writing sensitive data to internal storage. `EncryptedFile` transparently encrypts file contents using [AES-256-GCM-HKDF-4KB](https://developers.google.com/tink/streaming-aead/aes_gcm_hkdf_streaming) before writing them to disk, ensuring that the data is never stored in plaintext. +Use [`EncryptedFile`](https://developer.android.com/reference/androidx/security/crypto/EncryptedFile) from the [Jetpack Security library](https://developer.android.com/topic/security/data) when writing sensitive data to internal storage. `EncryptedFile` transparently encrypts file contents using [AES-256-GCM-HKDF-4KB](https://developers.google.com/tink/streaming-aead/aes_gcm_hkdf_streaming) before writing them to disk, providing transparent protection at rest. ```kotlin val masterKey = MasterKey.Builder(context) diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest.kt b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest.kt index 3cc7293c2df..923af209b8c 100644 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest.kt +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest.kt @@ -57,14 +57,14 @@ class MastgTest(private val context: Context) { return try { var result = "" - // FAIL: [MASTG-TEST-0x01] Unencrypted password stored using openFileOutput + // FAIL: [MASTG-TEST-0x01] The app stores the password unencrypted using openFileOutput, exposing it to attackers with device access. context.openFileOutput("secret_token.txt", Context.MODE_PRIVATE).use { output -> output.write(password.toByteArray()) Log.d("FileAPIs", "Written unencrypted password to secret_token.txt") } result += "[FAIL]: Stored unencrypted password in secret_token.txt using openFileOutput.\n\n" - // FAIL: [MASTG-TEST-0x01] Unencrypted API key stored using FileOutputStream + // FAIL: [MASTG-TEST-0x01] The app stores the API key unencrypted using FileOutputStream, making it readable by attackers with sandbox access. val apiKeyFile = File(context.filesDir, "api_key.txt") FileOutputStream(apiKeyFile).use { output -> output.write(apiKey.toByteArray()) @@ -72,7 +72,7 @@ class MastgTest(private val context: Context) { } result += "[FAIL]: Stored unencrypted API key in api_key.txt using FileOutputStream.\n\n" - // OK: [MASTG-TEST-0x01] Encrypted API key stored using FileOutputStream + manual AES-GCM encryption + // PASS: [MASTG-TEST-0x01] The app encrypts the API key with AES-GCM using a KeyStore-backed key before writing, preventing plaintext exposure. val encryptedApiKeyFile = File(context.filesDir, "encrypted_api_key.bin") FileOutputStream(encryptedApiKeyFile).use { output -> val encryptedApiKey = encrypt(apiKey) diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java index b4c14f7f3e5..d8dbccb6ae6 100644 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java @@ -63,7 +63,7 @@ public final String mastgTest() { try { String result = ""; - // FAIL: Unencrypted password stored using openFileOutput + // FAIL: [MASTG-TEST-0x01] The app stores the password unencrypted using openFileOutput, exposing it to attackers with device access. FileOutputStream fos1 = this.context.openFileOutput("secret_token.txt", Context.MODE_PRIVATE); FileOutputStream output1 = fos1; byte[] bytes1 = this.password.getBytes(Charsets.UTF_8); @@ -73,7 +73,7 @@ public final String mastgTest() { CloseableKt.closeFinally(fos1, null); result += "[FAIL]: Stored unencrypted password in secret_token.txt using openFileOutput.\n\n"; - // FAIL: Unencrypted API key stored using FileOutputStream + // FAIL: [MASTG-TEST-0x01] The app stores the API key unencrypted using FileOutputStream, making it readable by attackers with sandbox access. File apiKeyFile = new File(this.context.getFilesDir(), "api_key.txt"); FileOutputStream fos2 = new FileOutputStream(apiKeyFile); FileOutputStream output2 = fos2; @@ -84,7 +84,7 @@ public final String mastgTest() { CloseableKt.closeFinally(fos2, null); result += "[FAIL]: Stored unencrypted API key in api_key.txt using FileOutputStream.\n\n"; - // OK: Encrypted API key stored using FileOutputStream + AES-GCM + // PASS: [MASTG-TEST-0x01] The app encrypts the API key with AES-GCM using a KeyStore-backed key before writing, preventing plaintext exposure. File encryptedApiKeyFile = new File(this.context.getFilesDir(), "encrypted_api_key.bin"); FileOutputStream fos3 = new FileOutputStream(encryptedApiKeyFile); FileOutputStream output3 = fos3; From 707a1146e3805f6be4d2c06b419ee627896b1f03 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 May 2026 11:45:38 +0000 Subject: [PATCH 04/14] Split combined test into separate static and dynamic tests; split demo into static (semgrep) and dynamic (Frooky) demos Agent-Logs-Url: https://github.com/OWASP/mastg/sessions/f6f8bdea-a503-4d48-a8b7-8ef5c67b7366 Co-authored-by: cpholguera <29175115+cpholguera@users.noreply.github.com> --- .../MASTG-DEMO-0x01/MASTG-DEMO-0x01.md | 45 +- .../MASTG-DEMO-0x01/MastgTest.kt | 55 +-- .../MASTG-DEMO-0x01/MastgTest_reversed.java | 60 +-- .../MASTG-DEMO-0x01/evaluation.txt | 9 - .../MASVS-STORAGE/MASTG-DEMO-0x01/output.json | 435 ------------------ .../MASVS-STORAGE/MASTG-DEMO-0x01/output.txt | 42 ++ .../MASVS-STORAGE/MASTG-DEMO-0x01/run.sh | 2 +- .../MASTG-DEMO-0x02/MASTG-DEMO-0x02.md | 59 +++ .../evaluate.sh | 0 .../MASTG-DEMO-0x02/evaluation.txt | 3 + .../hooks.json | 3 +- .../MASVS-STORAGE/MASTG-DEMO-0x02/output.json | 216 +++++++++ .../MASVS-STORAGE/MASTG-DEMO-0x02/run.sh | 2 + .../android/MASVS-STORAGE/MASTG-TEST-0x01.md | 38 +- .../android/MASVS-STORAGE/MASTG-TEST-0x02.md | 34 ++ 15 files changed, 385 insertions(+), 618 deletions(-) delete mode 100644 demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/evaluation.txt delete mode 100644 demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.json create mode 100644 demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.txt create mode 100644 demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/MASTG-DEMO-0x02.md rename demos/android/MASVS-STORAGE/{MASTG-DEMO-0x01 => MASTG-DEMO-0x02}/evaluate.sh (100%) create mode 100644 demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/evaluation.txt rename demos/android/MASVS-STORAGE/{MASTG-DEMO-0x01 => MASTG-DEMO-0x02}/hooks.json (96%) create mode 100644 demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/output.json create mode 100755 demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/run.sh create mode 100644 tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x02.md diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MASTG-DEMO-0x01.md b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MASTG-DEMO-0x01.md index ed448921a66..d5095fe0978 100644 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MASTG-DEMO-0x01.md +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MASTG-DEMO-0x01.md @@ -4,60 +4,39 @@ title: Using File APIs to Write Sensitive Data Unencrypted to the App Sandbox id: MASTG-DEMO-0x01 code: [kotlin] test: MASTG-TEST-0x01 +tools: [MASTG-TOOL-0110] --- ## Sample -The code below stores sensitive data to the app's internal storage using the Java File APIs, both with and without encryption: +The code below stores sensitive data to the app's internal storage using Java File APIs, both without encryption: - A password is stored unencrypted using `openFileOutput` - An API key is stored unencrypted using `FileOutputStream` -- An API key is stored encrypted using `FileOutputStream` with AES-GCM encryption (key managed by the Android KeyStore) {{ MastgTest.kt # MastgTest_reversed.java }} ## Steps -1. Install the app on a device (@MASTG-TECH-0005). -2. Make sure you have @MASTG-TOOL-0145 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. +Let's run our @MASTG-TOOL-0110 rule against the sample code. -These are the relevant methods we are hooking to detect the use of File APIs to write data to the app sandbox: +{{ ../../../../rules/mastg-android-unencrypted-internal-file-storage.yml }} -- [`Context.openFileOutput(String, int)`](https://developer.android.com/reference/android/content/Context#openFileOutput(java.lang.String,%20int)) -- [`FileOutputStream.write(byte[])`](https://developer.android.com/reference/java/io/FileOutputStream#write(byte[])) - -Our hooks also trace calls to cryptographic methods to help determine whether the written data is encrypted or not; whether the Android KeyStore is used; and whether Base64 encoding is used to convert binary data to strings: - -- [`javax.crypto.Cipher.*(...)`](https://developer.android.com/reference/javax/crypto/Cipher) -- [`java.security.KeyStore.*(...)`](https://developer.android.com/reference/java/security/KeyStore) -- [`javax.crypto.KeyGenerator.*(...)`](https://developer.android.com/reference/javax/crypto/KeyGenerator) -- [`android.util.Base64.*(...)`](https://developer.android.com/reference/android/util/Base64) - -{{ hooks.json # run.sh }} +{{ run.sh }} ## Observation -The output shows all instances of data written via File APIs that were found at runtime. A backtrace is also provided to help identify the corresponding locations in the code. +The rule has identified 2 locations that indicate use of File APIs to write data to internal storage. -{{ output.json }} +{{ output.txt }} ## Evaluation -The test fails because sensitive data is written to the app sandbox via File APIs without encryption. - -In `output.json` we can identify entries that use the File APIs to write data to the app's internal storage. - -Determining if data is encrypted or not may require careful analysis. After slightly processing the output using `jq`, we can get a high-level view of the relevant calls, which can help us identify unencrypted data writes. - -{{ evaluation.txt # evaluate.sh }} +The test fails because the app uses File APIs to write sensitive data to internal storage without encryption. -Here we can see that: +After reviewing the decompiled code at the locations specified in the output: -- `openFileOutput` was called with `secret_token.txt` and the subsequent `FileOutputStream.write` call writes the plaintext value `MyS3cr3tP4ssw0rd` — no preceding Cipher calls, so this is unencrypted. -- `FileOutputStream.write` writes `AKIAABCDEFGHIJKLMNOP` without any preceding Cipher calls — this is also unencrypted. -- The third `FileOutputStream.write` call writes `obLDpJw/Gi+km0wdLj9KW2x9jp8KGy1KW3B9kJ8=`, but this is preceded by `Cipher.getInstance`, `KeyStore.getEntry`, `Cipher.init`, `Cipher.doFinal`, and `Base64.encodeToString` calls, confirming that the data was encrypted before being written. +- Line 32: `openFileOutput("secret_token.txt", ...)` is followed by writing `password` in plaintext — no preceding `Cipher` calls, so the data is stored unencrypted. +- Line 41: `new FileOutputStream(new File(context.getFilesDir(), "api_key.txt"))` is followed by writing `apiKey` in plaintext — again, no preceding `Cipher` calls. -You can confirm the unencrypted writes by reverse engineering the app and inspecting the code at the locations identified in the `stackTrace` of each hook entry. +You can confirm the dynamic counterpart in @MASTG-DEMO-0x02. diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest.kt b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest.kt index 923af209b8c..25b92560160 100644 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest.kt +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest.kt @@ -1,57 +1,17 @@ package org.owasp.mastestapp -// SUMMARY: This sample demonstrates storing sensitive data unencrypted and encrypted using the Java File APIs (openFileOutput and FileOutputStream). +// SUMMARY: This sample demonstrates storing sensitive data unencrypted to the app's internal storage using the Java File APIs (openFileOutput and FileOutputStream). import android.content.Context -import android.security.keystore.KeyGenParameterSpec -import android.security.keystore.KeyProperties -import android.util.Base64 import android.util.Log -import androidx.security.crypto.EncryptedFile -import androidx.security.crypto.MasterKey import java.io.File import java.io.FileOutputStream -import java.security.KeyStore -import javax.crypto.Cipher -import javax.crypto.KeyGenerator -import javax.crypto.SecretKey +import java.io.IOException class MastgTest(private val context: Context) { private val password = "MyS3cr3tP4ssw0rd" private val apiKey = "AKIAABCDEFGHIJKLMNOP" - private val keyAlias = "mastgFileKey" - - private fun getOrCreateSecretKey(): SecretKey { - val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } - return if (keyStore.containsAlias(keyAlias)) { - (keyStore.getEntry(keyAlias, null) as KeyStore.SecretKeyEntry).secretKey - } else { - KeyGenerator.getInstance( - KeyProperties.KEY_ALGORITHM_AES, - "AndroidKeyStore" - ).apply { - init( - KeyGenParameterSpec.Builder( - keyAlias, - KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT - ) - .setBlockModes(KeyProperties.BLOCK_MODE_GCM) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) - .build() - ) - }.generateKey() - } - } - - private fun encrypt(plainText: String): String { - val cipher = Cipher.getInstance("AES/GCM/NoPadding") - cipher.init(Cipher.ENCRYPT_MODE, getOrCreateSecretKey()) - val iv = cipher.iv - val encryptedBytes = cipher.doFinal(plainText.toByteArray(Charsets.UTF_8)) - val combined = iv + encryptedBytes - return Base64.encodeToString(combined, Base64.DEFAULT) - } fun mastgTest(): String { return try { @@ -72,17 +32,8 @@ class MastgTest(private val context: Context) { } result += "[FAIL]: Stored unencrypted API key in api_key.txt using FileOutputStream.\n\n" - // PASS: [MASTG-TEST-0x01] The app encrypts the API key with AES-GCM using a KeyStore-backed key before writing, preventing plaintext exposure. - val encryptedApiKeyFile = File(context.filesDir, "encrypted_api_key.bin") - FileOutputStream(encryptedApiKeyFile).use { output -> - val encryptedApiKey = encrypt(apiKey) - output.write(encryptedApiKey.toByteArray()) - Log.d("FileAPIs", "Written encrypted API key to encrypted_api_key.bin") - } - result += "[OK]: Stored encrypted API key in encrypted_api_key.bin using FileOutputStream with AES-GCM.\n\n" - result - } catch (e: Exception) { + } catch (e: IOException) { "Error during MastgTest: ${e.message ?: "Unknown error"}" } } diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java index d8dbccb6ae6..d663c81cdd2 100644 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java @@ -2,18 +2,11 @@ /*...*/ import android.content.Context; -import android.security.keystore.KeyGenParameterSpec; -import android.security.keystore.KeyProperties; -import android.util.Base64; import android.util.Log; import java.io.File; import java.io.FileOutputStream; -import java.security.KeyStore; -import javax.crypto.Cipher; -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; +import java.io.IOException; import kotlin.Metadata; -import kotlin.Unit; import kotlin.io.CloseableKt; import kotlin.jvm.internal.Intrinsics; import kotlin.text.Charsets; @@ -25,79 +18,36 @@ public final class MastgTest { private final Context context; private final String password = "MyS3cr3tP4ssw0rd"; private final String apiKey = "AKIAABCDEFGHIJKLMNOP"; - private final String keyAlias = "mastgFileKey"; public MastgTest(Context context) { Intrinsics.checkNotNullParameter(context, "context"); this.context = context; } - private final SecretKey getOrCreateSecretKey() { - KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); - keyStore.load(null); - if (keyStore.containsAlias(this.keyAlias)) { - return ((KeyStore.SecretKeyEntry) keyStore.getEntry(this.keyAlias, null)).getSecretKey(); - } - KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); - keyGenerator.init( - new KeyGenParameterSpec.Builder(this.keyAlias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) - .setBlockModes(KeyProperties.BLOCK_MODE_GCM) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) - .build() - ); - return keyGenerator.generateKey(); - } - - private final String encrypt(String plainText) throws Exception { - Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); - cipher.init(Cipher.ENCRYPT_MODE, getOrCreateSecretKey()); - byte[] iv = cipher.getIV(); - byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(Charsets.UTF_8)); - byte[] combined = new byte[iv.length + encryptedBytes.length]; - System.arraycopy(iv, 0, combined, 0, iv.length); - System.arraycopy(encryptedBytes, 0, combined, iv.length, encryptedBytes.length); - return Base64.encodeToString(combined, Base64.DEFAULT); - } - public final String mastgTest() { try { String result = ""; // FAIL: [MASTG-TEST-0x01] The app stores the password unencrypted using openFileOutput, exposing it to attackers with device access. FileOutputStream fos1 = this.context.openFileOutput("secret_token.txt", Context.MODE_PRIVATE); - FileOutputStream output1 = fos1; byte[] bytes1 = this.password.getBytes(Charsets.UTF_8); Intrinsics.checkNotNullExpressionValue(bytes1, "this as java.lang.String).getBytes(charset)"); - output1.write(bytes1); + fos1.write(bytes1); Log.d("FileAPIs", "Written unencrypted password to secret_token.txt"); CloseableKt.closeFinally(fos1, null); result += "[FAIL]: Stored unencrypted password in secret_token.txt using openFileOutput.\n\n"; // FAIL: [MASTG-TEST-0x01] The app stores the API key unencrypted using FileOutputStream, making it readable by attackers with sandbox access. - File apiKeyFile = new File(this.context.getFilesDir(), "api_key.txt"); - FileOutputStream fos2 = new FileOutputStream(apiKeyFile); - FileOutputStream output2 = fos2; + FileOutputStream fos2 = new FileOutputStream(new File(this.context.getFilesDir(), "api_key.txt")); byte[] bytes2 = this.apiKey.getBytes(Charsets.UTF_8); Intrinsics.checkNotNullExpressionValue(bytes2, "this as java.lang.String).getBytes(charset)"); - output2.write(bytes2); + fos2.write(bytes2); Log.d("FileAPIs", "Written unencrypted API key to api_key.txt"); CloseableKt.closeFinally(fos2, null); result += "[FAIL]: Stored unencrypted API key in api_key.txt using FileOutputStream.\n\n"; - // PASS: [MASTG-TEST-0x01] The app encrypts the API key with AES-GCM using a KeyStore-backed key before writing, preventing plaintext exposure. - File encryptedApiKeyFile = new File(this.context.getFilesDir(), "encrypted_api_key.bin"); - FileOutputStream fos3 = new FileOutputStream(encryptedApiKeyFile); - FileOutputStream output3 = fos3; - String encryptedApiKey = encrypt(this.apiKey); - byte[] bytes3 = encryptedApiKey.getBytes(Charsets.UTF_8); - Intrinsics.checkNotNullExpressionValue(bytes3, "this as java.lang.String).getBytes(charset)"); - output3.write(bytes3); - Log.d("FileAPIs", "Written encrypted API key to encrypted_api_key.bin"); - CloseableKt.closeFinally(fos3, null); - result += "[OK]: Stored encrypted API key in encrypted_api_key.bin using FileOutputStream with AES-GCM.\n\n"; - return result; - } catch (Exception e) { + } catch (IOException e) { return "Error during MastgTest: " + e.getMessage(); } } diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/evaluation.txt b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/evaluation.txt deleted file mode 100644 index f2e088beb40..00000000000 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/evaluation.txt +++ /dev/null @@ -1,9 +0,0 @@ -Class: android.content.ContextWrapper, Method: openFileOutput, Params: secret_token.txt, 0 -Class: java.io.FileOutputStream, Method: write, Params: MyS3cr3tP4ssw0rd -Class: java.io.FileOutputStream, Method: write, Params: AKIAABCDEFGHIJKLMNOP -Class: javax.crypto.Cipher, Method: getInstance, Params: AES/GCM/NoPadding -Class: java.security.KeyStore, Method: getEntry, Params: mastgFileKey, void -Class: javax.crypto.Cipher, Method: init, Params: 1, -Class: javax.crypto.Cipher, Method: doFinal, Params: AKIAABCDEFGHIJKLMNOP -Class: android.util.Base64, Method: encodeToString, Params: 0xa1b2c3d49c3f1a2b4e5d6c7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b..., 0 -Class: java.io.FileOutputStream, Method: write, Params: obLDpJw/Gi+km0wdLj9KW2x9jp8KGy1KW3B9kJ8= diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.json b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.json deleted file mode 100644 index 830524653cb..00000000000 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.json +++ /dev/null @@ -1,435 +0,0 @@ -{ - "type": "summary", - "hooks": [ - { - "class": "android.content.ContextWrapper", - "method": "openFileOutput", - "overloads": [ - { - "args": [ - "java.lang.String", - "int" - ] - } - ] - }, - { - "class": "java.io.FileOutputStream", - "method": "write", - "overloads": [ - { - "args": [ - "[B" - ] - }, - { - "args": [ - "[B", - "int", - "int" - ] - }, - { - "args": [ - "int" - ] - } - ] - }, - { - "class": "javax.crypto.Cipher", - "method": "getInstance", - "overloads": [ - { - "args": [ - "java.lang.String" - ] - }, - { - "args": [ - "java.lang.String", - "java.lang.String" - ] - }, - { - "args": [ - "java.lang.String", - "java.security.Provider" - ] - } - ] - }, - { - "class": "javax.crypto.Cipher", - "method": "doFinal", - "overloads": [ - { - "args": [ - "[B" - ] - } - ] - }, - { - "class": "javax.crypto.Cipher", - "method": "init", - "overloads": [ - { - "args": [ - "int", - "java.security.Key" - ] - }, - { - "args": [ - "int", - "java.security.Key", - "java.security.SecureRandom" - ] - } - ] - }, - { - "class": "java.security.KeyStore", - "method": "getEntry", - "overloads": [ - { - "args": [ - "java.lang.String", - "java.security.KeyStore$ProtectionParameter" - ] - } - ] - }, - { - "class": "javax.crypto.KeyGenerator", - "method": "getInstance", - "overloads": [ - { - "args": [ - "java.lang.String", - "java.lang.String" - ] - } - ] - }, - { - "class": "javax.crypto.KeyGenerator", - "method": "generateKey", - "overloads": [ - { - "args": [] - } - ] - }, - { - "class": "android.util.Base64", - "method": "encodeToString", - "overloads": [ - { - "args": [ - "[B", - "int" - ] - } - ] - } - ], - "totalHooks": 22, - "errors": [], - "totalErrors": 0 -} -{ - "id": "a1b2c3d4-1234-5678-abcd-111111111111", - "type": "hook", - "category": "STORAGE", - "time": "2026-01-23T11:00:01.100Z", - "class": "android.content.ContextWrapper", - "method": "openFileOutput", - "instanceId": 12345678, - "stackTrace": [ - "android.content.ContextWrapper.openFileOutput(Native Method)", - "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:62)", - "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", - "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", - "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", - "java.lang.Thread.run(Thread.java:1119)" - ], - "inputParameters": [ - { - "declaredType": "java.lang.String", - "value": "secret_token.txt" - }, - { - "declaredType": "int", - "value": 0 - } - ], - "returnValue": [ - { - "declaredType": "java.io.FileOutputStream", - "value": "", - "runtimeType": "java.io.FileOutputStream", - "instanceId": "23456789", - "instanceToString": "java.io.FileOutputStream@1234abc" - } - ] -} -{ - "id": "a1b2c3d4-1234-5678-abcd-222222222222", - "type": "hook", - "category": "STORAGE", - "time": "2026-01-23T11:00:01.110Z", - "class": "java.io.FileOutputStream", - "method": "write", - "instanceId": 23456789, - "stackTrace": [ - "java.io.FileOutputStream.write(Native Method)", - "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:63)", - "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", - "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", - "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", - "java.lang.Thread.run(Thread.java:1119)" - ], - "inputParameters": [ - { - "declaredType": "[B", - "value": "MyS3cr3tP4ssw0rd" - } - ], - "returnValue": [ - { - "declaredType": "void", - "value": "void" - } - ] -} -{ - "id": "a1b2c3d4-1234-5678-abcd-333333333333", - "type": "hook", - "category": "STORAGE", - "time": "2026-01-23T11:00:01.200Z", - "class": "java.io.FileOutputStream", - "method": "write", - "instanceId": 34567890, - "stackTrace": [ - "java.io.FileOutputStream.write(Native Method)", - "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:70)", - "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", - "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", - "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", - "java.lang.Thread.run(Thread.java:1119)" - ], - "inputParameters": [ - { - "declaredType": "[B", - "value": "AKIAABCDEFGHIJKLMNOP" - } - ], - "returnValue": [ - { - "declaredType": "void", - "value": "void" - } - ] -} -{ - "id": "a1b2c3d4-1234-5678-abcd-444444444444", - "type": "hook", - "category": "STORAGE", - "time": "2026-01-23T11:00:01.300Z", - "class": "javax.crypto.Cipher", - "method": "getInstance", - "instanceId": "error", - "stackTrace": [ - "javax.crypto.Cipher.getInstance(Native Method)", - "org.owasp.mastestapp.MastgTest.encrypt(MastgTest.kt:51)", - "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:76)", - "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", - "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", - "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", - "java.lang.Thread.run(Thread.java:1119)" - ], - "inputParameters": [ - { - "declaredType": "java.lang.String", - "value": "AES/GCM/NoPadding" - } - ], - "returnValue": [ - { - "declaredType": "javax.crypto.Cipher", - "value": "", - "runtimeType": "javax.crypto.Cipher", - "instanceId": "45678901", - "instanceToString": "javax.crypto.Cipher@abcdef12" - } - ] -} -{ - "id": "a1b2c3d4-1234-5678-abcd-555555555555", - "type": "hook", - "category": "STORAGE", - "time": "2026-01-23T11:00:01.310Z", - "class": "java.security.KeyStore", - "method": "getEntry", - "instanceId": 56789012, - "stackTrace": [ - "java.security.KeyStore.getEntry(Native Method)", - "org.owasp.mastestapp.MastgTest.getOrCreateSecretKey(MastgTest.kt:30)", - "org.owasp.mastestapp.MastgTest.encrypt(MastgTest.kt:52)", - "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:76)", - "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", - "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", - "java.lang.Thread.run(Thread.java:1119)" - ], - "inputParameters": [ - { - "declaredType": "java.lang.String", - "value": "mastgFileKey" - }, - { - "declaredType": "java.security.KeyStore$ProtectionParameter", - "value": "void" - } - ], - "returnValue": [ - { - "declaredType": "java.security.KeyStore$Entry", - "value": "", - "runtimeType": "java.security.KeyStore$SecretKeyEntry", - "instanceId": "67890123", - "instanceToString": "Secret key entry with algorithm AES" - } - ] -} -{ - "id": "a1b2c3d4-1234-5678-abcd-666666666666", - "type": "hook", - "category": "STORAGE", - "time": "2026-01-23T11:00:01.320Z", - "class": "javax.crypto.Cipher", - "method": "init", - "instanceId": 45678901, - "stackTrace": [ - "javax.crypto.Cipher.init(Native Method)", - "org.owasp.mastestapp.MastgTest.encrypt(MastgTest.kt:52)", - "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:76)", - "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", - "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", - "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", - "java.lang.Thread.run(Thread.java:1119)" - ], - "inputParameters": [ - { - "declaredType": "int", - "value": 1 - }, - { - "declaredType": "java.security.Key", - "value": "", - "runtimeType": "android.security.keystore2.AndroidKeyStoreSecretKey", - "instanceId": "78901234", - "instanceToString": "android.security.keystore2.AndroidKeyStoreSecretKey@12345678" - } - ], - "returnValue": [ - { - "declaredType": "void", - "value": "void" - } - ] -} -{ - "id": "a1b2c3d4-1234-5678-abcd-777777777777", - "type": "hook", - "category": "STORAGE", - "time": "2026-01-23T11:00:01.330Z", - "class": "javax.crypto.Cipher", - "method": "doFinal", - "instanceId": 45678901, - "stackTrace": [ - "javax.crypto.Cipher.doFinal(Native Method)", - "org.owasp.mastestapp.MastgTest.encrypt(MastgTest.kt:54)", - "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:76)", - "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", - "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", - "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", - "java.lang.Thread.run(Thread.java:1119)" - ], - "inputParameters": [ - { - "declaredType": "[B", - "value": "AKIAABCDEFGHIJKLMNOP" - } - ], - "returnValue": [ - { - "declaredType": "[B", - "value": "0x9c3f1a2b4e5d6c7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f..." - } - ] -} -{ - "id": "a1b2c3d4-1234-5678-abcd-888888888888", - "type": "hook", - "category": "STORAGE", - "time": "2026-01-23T11:00:01.340Z", - "class": "android.util.Base64", - "method": "encodeToString", - "instanceId": "error", - "stackTrace": [ - "android.util.Base64.encodeToString(Native Method)", - "org.owasp.mastestapp.MastgTest.encrypt(MastgTest.kt:56)", - "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:76)", - "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", - "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", - "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", - "java.lang.Thread.run(Thread.java:1119)" - ], - "inputParameters": [ - { - "declaredType": "[B", - "value": "0xa1b2c3d49c3f1a2b4e5d6c7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b..." - }, - { - "declaredType": "int", - "value": 0 - } - ], - "returnValue": [ - { - "declaredType": "java.lang.String", - "value": "obLDpJw/Gi+km0wdLj9KW2x9jp8KGy1KW3B9kJ8=" - } - ] -} -{ - "id": "a1b2c3d4-1234-5678-abcd-999999999999", - "type": "hook", - "category": "STORAGE", - "time": "2026-01-23T11:00:01.350Z", - "class": "java.io.FileOutputStream", - "method": "write", - "instanceId": 89012345, - "stackTrace": [ - "java.io.FileOutputStream.write(Native Method)", - "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:78)", - "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", - "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", - "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", - "java.lang.Thread.run(Thread.java:1119)" - ], - "inputParameters": [ - { - "declaredType": "[B", - "value": "obLDpJw/Gi+km0wdLj9KW2x9jp8KGy1KW3B9kJ8=" - } - ], - "returnValue": [ - { - "declaredType": "void", - "value": "void" - } - ] -} diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.txt b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.txt new file mode 100644 index 00000000000..77eef44ae34 --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.txt @@ -0,0 +1,42 @@ + + +┌─────────────┐ +│ Scan Status │ +└─────────────┘ + Scanning 1 file with 3 Code rules: + Scanning 1 file with 3 java rules. + + +┌─────────────────┐ +│ 2 Code Findings │ +└─────────────────┘ + + MastgTest_reversed.java + ❯❱ rules.mastg-android-unencrypted-internal-file-storage-openfileoutput + ❰❰ Blocking ❱❱ + [MASVS-STORAGE] Verify that any sensitive data written via openFileOutput is encrypted before + storage + + 32┆ FileOutputStream fos1 = this.context.openFileOutput("secret_token.txt", + Context.MODE_PRIVATE); + + ❯❱ rules.mastg-android-unencrypted-internal-file-storage-fileoutputstream + ❰❰ Blocking ❱❱ + [MASVS-STORAGE] Verify that any sensitive data written via FileOutputStream to internal storage is + encrypted before storage + + 41┆ FileOutputStream fos2 = new FileOutputStream(new File(this.context.getFilesDir(), + "api_key.txt")); + + + +┌──────────────┐ +│ Scan Summary │ +└──────────────┘ +✅ Scan completed successfully. + • Findings: 2 (2 blocking) + • Rules run: 3 + • Targets scanned: 1 + • Parsed lines: ~100.0% + • No ignore information available +Ran 3 rules on 1 file: 2 findings. diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/run.sh b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/run.sh index 3f699aca1f8..2e679e0498b 100755 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/run.sh +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/run.sh @@ -1,2 +1,2 @@ #!/bin/bash -frooky -U -f org.owasp.mastestapp --platform android hooks.json +NO_COLOR=true semgrep -c ../../../../rules/mastg-android-unencrypted-internal-file-storage.yml ./MastgTest_reversed.java > output.txt diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/MASTG-DEMO-0x02.md b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/MASTG-DEMO-0x02.md new file mode 100644 index 00000000000..e8f7eebc391 --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/MASTG-DEMO-0x02.md @@ -0,0 +1,59 @@ +--- +platform: android +title: Runtime Use of File APIs to Write Sensitive Data Unencrypted to the App Sandbox +id: MASTG-DEMO-0x02 +code: [kotlin] +test: MASTG-TEST-0x02 +tools: [MASTG-TOOL-0145] +--- + +## Sample + +This demo uses the same app sample as @MASTG-DEMO-0x01. + +{{ ../MASTG-DEMO-0x01/MastgTest.kt }} + +## Steps + +1. Install the app on a device (@MASTG-TECH-0005). +2. Make sure you have @MASTG-TOOL-0145 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 File APIs to write data to the app sandbox: + +- [`Context.openFileOutput(String, int)`](https://developer.android.com/reference/android/content/Context#openFileOutput(java.lang.String,%20int)) +- [`FileOutputStream.write(byte[])`](https://developer.android.com/reference/java/io/FileOutputStream#write(byte[])) + +Our hooks also trace calls to cryptographic methods to help determine whether the written data is encrypted or not; whether the Android KeyStore is used; and whether Base64 encoding is used to convert binary data to strings: + +- [`javax.crypto.Cipher.*(...)`](https://developer.android.com/reference/javax/crypto/Cipher) +- [`java.security.KeyStore.*(...)`](https://developer.android.com/reference/java/security/KeyStore) +- [`javax.crypto.KeyGenerator.*(...)`](https://developer.android.com/reference/javax/crypto/KeyGenerator) +- [`android.util.Base64.*(...)`](https://developer.android.com/reference/android/util/Base64) + +{{ hooks.json # run.sh }} + +## Observation + +The output shows all instances of data written via File APIs that were found at runtime. A backtrace is also provided to help identify the corresponding locations in the code. + +{{ output.json }} + +## Evaluation + +The test fails because sensitive data is written to the app sandbox via File APIs without encryption. + +In `output.json` we can identify entries that use the File APIs to write data to the app's internal storage. + +After slightly processing the output using `jq`, we can get a high-level view of the relevant calls, which can help us identify unencrypted data writes. + +{{ evaluation.txt # evaluate.sh }} + +Here we can see that: + +- `openFileOutput` was called with `secret_token.txt` and the subsequent `FileOutputStream.write` call writes the plaintext value `MyS3cr3tP4ssw0rd` — no preceding `Cipher` calls, so this is unencrypted. +- A second `FileOutputStream.write` call writes `AKIAABCDEFGHIJKLMNOP` — also no preceding `Cipher` calls, so this is unencrypted. + +You can confirm the code locations responsible by reviewing the `stackTrace` of each hook entry and cross-referencing with the static counterpart @MASTG-DEMO-0x01. diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/evaluate.sh b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/evaluate.sh similarity index 100% rename from demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/evaluate.sh rename to demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/evaluate.sh diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/evaluation.txt b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/evaluation.txt new file mode 100644 index 00000000000..f473c7183f2 --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/evaluation.txt @@ -0,0 +1,3 @@ +Class: android.content.ContextWrapper, Method: openFileOutput, Params: secret_token.txt, 0 +Class: java.io.FileOutputStream, Method: write, Params: MyS3cr3tP4ssw0rd +Class: java.io.FileOutputStream, Method: write, Params: AKIAABCDEFGHIJKLMNOP diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/hooks.json b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/hooks.json similarity index 96% rename from demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/hooks.json rename to demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/hooks.json index 165e2968c14..6ef1624aea4 100644 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/hooks.json +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/hooks.json @@ -19,8 +19,7 @@ "methods": [ "getInstance", "doFinal", - "init", - "update" + "init" ] }, { diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/output.json b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/output.json new file mode 100644 index 00000000000..280a8001d18 --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/output.json @@ -0,0 +1,216 @@ +{ + "type": "summary", + "hooks": [ + { + "class": "android.content.ContextWrapper", + "method": "openFileOutput", + "overloads": [ + { + "args": [ + "java.lang.String", + "int" + ] + } + ] + }, + { + "class": "java.io.FileOutputStream", + "method": "write", + "overloads": [ + { + "args": [ + "[B" + ] + }, + { + "args": [ + "[B", + "int", + "int" + ] + }, + { + "args": [ + "int" + ] + } + ] + }, + { + "class": "javax.crypto.Cipher", + "method": "getInstance", + "overloads": [ + { + "args": [ + "java.lang.String" + ] + } + ] + }, + { + "class": "javax.crypto.Cipher", + "method": "doFinal", + "overloads": [ + { + "args": [ + "[B" + ] + } + ] + }, + { + "class": "javax.crypto.Cipher", + "method": "init", + "overloads": [ + { + "args": [ + "int", + "java.security.Key" + ] + } + ] + }, + { + "class": "java.security.KeyStore", + "method": "getEntry", + "overloads": [ + { + "args": [ + "java.lang.String", + "java.security.KeyStore$ProtectionParameter" + ] + } + ] + }, + { + "class": "javax.crypto.KeyGenerator", + "method": "getInstance", + "overloads": [ + { + "args": [ + "java.lang.String", + "java.lang.String" + ] + } + ] + }, + { + "class": "javax.crypto.KeyGenerator", + "method": "generateKey", + "overloads": [ + { + "args": [] + } + ] + }, + { + "class": "android.util.Base64", + "method": "encodeToString", + "overloads": [ + { + "args": [ + "[B", + "int" + ] + } + ] + } + ], + "totalHooks": 20, + "errors": [], + "totalErrors": 0 +} +{ + "id": "a1b2c3d4-1234-5678-abcd-111111111111", + "type": "hook", + "category": "STORAGE", + "time": "2026-01-23T11:00:01.100Z", + "class": "android.content.ContextWrapper", + "method": "openFileOutput", + "instanceId": 12345678, + "stackTrace": [ + "android.content.ContextWrapper.openFileOutput(Native Method)", + "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:22)", + "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", + "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", + "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", + "java.lang.Thread.run(Thread.java:1119)" + ], + "inputParameters": [ + { + "declaredType": "java.lang.String", + "value": "secret_token.txt" + }, + { + "declaredType": "int", + "value": 0 + } + ], + "returnValue": [ + { + "declaredType": "java.io.FileOutputStream", + "value": "", + "runtimeType": "java.io.FileOutputStream", + "instanceId": "23456789", + "instanceToString": "java.io.FileOutputStream@1234abc" + } + ] +} +{ + "id": "a1b2c3d4-1234-5678-abcd-222222222222", + "type": "hook", + "category": "STORAGE", + "time": "2026-01-23T11:00:01.110Z", + "class": "java.io.FileOutputStream", + "method": "write", + "instanceId": 23456789, + "stackTrace": [ + "java.io.FileOutputStream.write(Native Method)", + "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:23)", + "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", + "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", + "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", + "java.lang.Thread.run(Thread.java:1119)" + ], + "inputParameters": [ + { + "declaredType": "[B", + "value": "MyS3cr3tP4ssw0rd" + } + ], + "returnValue": [ + { + "declaredType": "void", + "value": "void" + } + ] +} +{ + "id": "a1b2c3d4-1234-5678-abcd-333333333333", + "type": "hook", + "category": "STORAGE", + "time": "2026-01-23T11:00:01.200Z", + "class": "java.io.FileOutputStream", + "method": "write", + "instanceId": 34567890, + "stackTrace": [ + "java.io.FileOutputStream.write(Native Method)", + "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:30)", + "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", + "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", + "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", + "java.lang.Thread.run(Thread.java:1119)" + ], + "inputParameters": [ + { + "declaredType": "[B", + "value": "AKIAABCDEFGHIJKLMNOP" + } + ], + "returnValue": [ + { + "declaredType": "void", + "value": "void" + } + ] +} diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/run.sh b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/run.sh new file mode 100755 index 00000000000..3f699aca1f8 --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/run.sh @@ -0,0 +1,2 @@ +#!/bin/bash +frooky -U -f org.owasp.mastestapp --platform android hooks.json diff --git a/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x01.md b/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x01.md index cb1fac924b2..9990fb37db3 100644 --- a/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x01.md +++ b/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x01.md @@ -1,8 +1,8 @@ --- platform: android -title: Sensitive Data Stored Unencrypted via the File APIs to the App Sandbox +title: References to File APIs for Writing Data Unencrypted to the App Sandbox id: MASTG-TEST-0x01 -type: [static, dynamic] +type: [static] weakness: MASWE-0006 best-practices: [MASTG-BEST-0x01] profiles: [L1, L2] @@ -11,46 +11,22 @@ knowledge: [MASTG-KNOW-0041] ## Overview -Android apps can write data directly to files in the app's internal storage using Java File APIs such as [`Context.openFileOutput`](https://developer.android.com/reference/android/content/Context#openFileOutput(java.lang.String,%20int)), [`java.io.FileOutputStream`](https://developer.android.com/reference/java/io/FileOutputStream), and [`java.io.FileWriter`](https://developer.android.com/reference/java/io/FileWriter). While files in internal storage are protected from direct access by other apps, they can be exposed if the device is rooted, the app data is extracted via an ADB backup, or the attacker exploits another vulnerability. If the app writes sensitive data (such as credentials, tokens, or personally identifiable information) to these files without encryption, that data is stored in plaintext and can be recovered by an attacker with sufficient access to the device. +This test uses static analysis to look for uses of Java File APIs that write data to the app's internal storage (see @MASTG-KNOW-0041). These include [`Context.openFileOutput`](https://developer.android.com/reference/android/content/Context#openFileOutput(java.lang.String,%20int)), [`FileOutputStream`](https://developer.android.com/reference/java/io/FileOutputStream), and [`FileWriter`](https://developer.android.com/reference/java/io/FileWriter). -This test checks whether the app uses File APIs to store sensitive data unencrypted in the app's private sandbox. +Static analysis is great for identifying all code locations where the app is writing data to internal storage. However, it does not reveal the actual data being written at runtime. To confirm that sensitive data is written unencrypted, combine this test with the dynamic counterpart @MASTG-TEST-0x02. ## Steps -### Static Analysis - 1. Reverse engineer the app (@MASTG-TECH-0017). -2. Run a static analysis (@MASTG-TECH-0014) tool on the reverse engineered app targeting calls to internal storage file writing APIs such as `openFileOutput`, `FileOutputStream`, and `FileWriter`. - -### Dynamic Analysis - -1. Install the app on a device (@MASTG-TECH-0005). -2. Make sure you have @MASTG-TOOL-0145 installed on your machine and the frida-server running on the device. -3. Run `run.sh` to spawn the app with Frooky. -4. Exercise the app, navigating through the various features while paying attention to inputs of sensitive data. -5. Stop the script by pressing `Ctrl+C`. +2. Run a static analysis (@MASTG-TECH-0014) tool on the reverse engineered app targeting calls to File APIs that write data to internal storage. ## Observation -### Static Analysis - -The output should contain a list of locations in the code where the app uses File APIs to write data to internal storage. - -### Dynamic Analysis - -The output should contain a list of calls to File APIs that write data to the app sandbox. A backtrace is also provided to help identify the corresponding locations in the code. +The output should contain a list of locations in the code where the app uses File APIs that may write data to the app's internal storage. ## Evaluation -### Static Analysis - The test case fails if the app uses File APIs to write data to internal storage and you can confirm (by reviewing the relevant code) that: - sensitive data is being written; **and** -- the data is not encrypted before being written (e.g., no Cipher calls precede the write). - -### Dynamic Analysis - -The test case fails if you can find sensitive data written to the app sandbox without encryption via File APIs. - -Determining if a string is encrypted or not may require careful analysis. Correlate the file write calls with any Cipher, KeyStore, or KeyGenerator calls to determine if encryption was applied before writing. +- the data is not encrypted before being written (e.g., no `Cipher` encryption calls precede the write). diff --git a/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x02.md b/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x02.md new file mode 100644 index 00000000000..5f080d2429f --- /dev/null +++ b/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x02.md @@ -0,0 +1,34 @@ +--- +platform: android +title: Runtime Use of File APIs for Writing Data Unencrypted to the App Sandbox +id: MASTG-TEST-0x02 +type: [dynamic] +weakness: MASWE-0006 +best-practices: [MASTG-BEST-0x01] +profiles: [L1, L2] +knowledge: [MASTG-KNOW-0041] +--- + +## Overview + +This test is the dynamic counterpart to @MASTG-TEST-0x01. + +It uses runtime method hooking to identify whether sensitive data is written unencrypted to the app's internal storage by monitoring Java File API calls such as `Context.openFileOutput`, `FileOutputStream.write`, and `FileWriter.write`. Correlating these calls with any Cipher or KeyStore API calls lets you determine whether the data is encrypted before being written. + +## Steps + +1. Install the app on a device (@MASTG-TECH-0005). +2. Make sure you have @MASTG-TOOL-0145 installed on your machine and the frida-server running on the device. +3. Run the @MASTG-TOOL-0145 hook configuration targeting File APIs and related cryptographic APIs. +4. Exercise app features that could handle sensitive data (authentication flows, session establishment, offline caching, profile editing, or token refresh logic). +5. Stop the script. + +## Observation + +The output should contain a list of calls to File APIs that write data to the app sandbox. A backtrace is also provided to help identify the corresponding locations in the code. + +## Evaluation + +The test case fails if you can find sensitive data written to the app sandbox without prior encryption via File APIs. + +Determining whether the data is encrypted or not may require careful analysis. Correlate the `FileOutputStream.write` or `openFileOutput` calls with any `Cipher`, `KeyStore`, or `KeyGenerator` calls to determine whether encryption was applied before writing. From f411c66d91a62cf3293567e2b0c7cc0fabeac815 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 May 2026 11:49:26 +0000 Subject: [PATCH 05/14] Use descriptive variable names in MastgTest_reversed.java; regenerate semgrep output Agent-Logs-Url: https://github.com/OWASP/mastg/sessions/f6f8bdea-a503-4d48-a8b7-8ef5c67b7366 Co-authored-by: cpholguera <29175115+cpholguera@users.noreply.github.com> --- .../MASTG-DEMO-0x01/MastgTest_reversed.java | 20 +++++++++---------- .../MASVS-STORAGE/MASTG-DEMO-0x01/output.txt | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java index d663c81cdd2..85849d7adf2 100644 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java @@ -29,21 +29,21 @@ public final String mastgTest() { String result = ""; // FAIL: [MASTG-TEST-0x01] The app stores the password unencrypted using openFileOutput, exposing it to attackers with device access. - FileOutputStream fos1 = this.context.openFileOutput("secret_token.txt", Context.MODE_PRIVATE); - byte[] bytes1 = this.password.getBytes(Charsets.UTF_8); - Intrinsics.checkNotNullExpressionValue(bytes1, "this as java.lang.String).getBytes(charset)"); - fos1.write(bytes1); + FileOutputStream tokenOutputStream = this.context.openFileOutput("secret_token.txt", Context.MODE_PRIVATE); + byte[] passwordBytes = this.password.getBytes(Charsets.UTF_8); + Intrinsics.checkNotNullExpressionValue(passwordBytes, "this as java.lang.String).getBytes(charset)"); + tokenOutputStream.write(passwordBytes); Log.d("FileAPIs", "Written unencrypted password to secret_token.txt"); - CloseableKt.closeFinally(fos1, null); + CloseableKt.closeFinally(tokenOutputStream, null); result += "[FAIL]: Stored unencrypted password in secret_token.txt using openFileOutput.\n\n"; // FAIL: [MASTG-TEST-0x01] The app stores the API key unencrypted using FileOutputStream, making it readable by attackers with sandbox access. - FileOutputStream fos2 = new FileOutputStream(new File(this.context.getFilesDir(), "api_key.txt")); - byte[] bytes2 = this.apiKey.getBytes(Charsets.UTF_8); - Intrinsics.checkNotNullExpressionValue(bytes2, "this as java.lang.String).getBytes(charset)"); - fos2.write(bytes2); + FileOutputStream apiKeyOutputStream = new FileOutputStream(new File(this.context.getFilesDir(), "api_key.txt")); + byte[] apiKeyBytes = this.apiKey.getBytes(Charsets.UTF_8); + Intrinsics.checkNotNullExpressionValue(apiKeyBytes, "this as java.lang.String).getBytes(charset)"); + apiKeyOutputStream.write(apiKeyBytes); Log.d("FileAPIs", "Written unencrypted API key to api_key.txt"); - CloseableKt.closeFinally(fos2, null); + CloseableKt.closeFinally(apiKeyOutputStream, null); result += "[FAIL]: Stored unencrypted API key in api_key.txt using FileOutputStream.\n\n"; return result; diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.txt b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.txt index 77eef44ae34..c82d1e192c6 100644 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.txt +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.txt @@ -17,16 +17,16 @@ [MASVS-STORAGE] Verify that any sensitive data written via openFileOutput is encrypted before storage - 32┆ FileOutputStream fos1 = this.context.openFileOutput("secret_token.txt", - Context.MODE_PRIVATE); + 32┆ FileOutputStream tokenOutputStream = this.context.openFileOutput("secret_token.txt", + Context.MODE_PRIVATE); ❯❱ rules.mastg-android-unencrypted-internal-file-storage-fileoutputstream ❰❰ Blocking ❱❱ [MASVS-STORAGE] Verify that any sensitive data written via FileOutputStream to internal storage is encrypted before storage - 41┆ FileOutputStream fos2 = new FileOutputStream(new File(this.context.getFilesDir(), - "api_key.txt")); + 41┆ FileOutputStream apiKeyOutputStream = new FileOutputStream(new + File(this.context.getFilesDir(), "api_key.txt")); From 68ebeaf1e2f0d3ca9189275a46408cb89fbf97c5 Mon Sep 17 00:00:00 2001 From: Carlos Holguera Date: Mon, 11 May 2026 08:13:25 +0200 Subject: [PATCH 06/14] add covered by --- tests/android/MASVS-STORAGE/MASTG-TEST-0001.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/android/MASVS-STORAGE/MASTG-TEST-0001.md b/tests/android/MASVS-STORAGE/MASTG-TEST-0001.md index bccc6c70682..647547889d7 100644 --- a/tests/android/MASVS-STORAGE/MASTG-TEST-0001.md +++ b/tests/android/MASVS-STORAGE/MASTG-TEST-0001.md @@ -11,7 +11,7 @@ masvs_v1_levels: - L2 profiles: [L1, L2] status: deprecated -covered_by: [MASTG-TEST-0207, MASTG-TEST-0200, MASTG-TEST-0201, MASTG-TEST-0202, MASTG-TEST-0304, MASTG-TEST-0305, MASTG-TEST-0306] +covered_by: [MASTG-TEST-0207, MASTG-TEST-0200, MASTG-TEST-0201, MASTG-TEST-0202, MASTG-TEST-0304, MASTG-TEST-0305, MASTG-TEST-0306, MASTG-TEST-0x01, MASTG-TEST-0x02] deprecation_note: New version available in MASTG V2 --- From cfe2ba0f72f57f062a94453fc4a3cc63abc5709f Mon Sep 17 00:00:00 2001 From: Carlos Holguera Date: Mon, 11 May 2026 08:57:41 +0200 Subject: [PATCH 07/14] Update knowledge for file system APIs --- best-practices/MASTG-BEST-0x01.md | 2 +- .../android/MASVS-STORAGE/MASTG-KNOW-0041.md | 10 +- .../android/MASVS-STORAGE/MASTG-KNOW-0042.md | 8 +- .../android/MASVS-STORAGE/MASTG-KNOW-0x01.md | 97 +++++++++++++++++++ .../android/MASVS-STORAGE/MASTG-TEST-0x01.md | 4 +- .../android/MASVS-STORAGE/MASTG-TEST-0x02.md | 4 +- 6 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 knowledge/android/MASVS-STORAGE/MASTG-KNOW-0x01.md diff --git a/best-practices/MASTG-BEST-0x01.md b/best-practices/MASTG-BEST-0x01.md index 03fba527e54..de4cfbda6cb 100644 --- a/best-practices/MASTG-BEST-0x01.md +++ b/best-practices/MASTG-BEST-0x01.md @@ -3,7 +3,7 @@ title: Use EncryptedFile for Sensitive Data in Internal Storage alias: use-encrypted-file-for-sensitive-data-in-internal-storage id: MASTG-BEST-0x01 platform: android -knowledge: [MASTG-KNOW-0041] +knowledge: [MASTG-KNOW-0x01, MASTG-KNOW-0041] --- Use [`EncryptedFile`](https://developer.android.com/reference/androidx/security/crypto/EncryptedFile) from the [Jetpack Security library](https://developer.android.com/topic/security/data) when writing sensitive data to internal storage. `EncryptedFile` transparently encrypts file contents using [AES-256-GCM-HKDF-4KB](https://developers.google.com/tink/streaming-aead/aes_gcm_hkdf_streaming) before writing them to disk, providing transparent protection at rest. diff --git a/knowledge/android/MASVS-STORAGE/MASTG-KNOW-0041.md b/knowledge/android/MASVS-STORAGE/MASTG-KNOW-0041.md index 1f5d1158556..149ff25a421 100644 --- a/knowledge/android/MASVS-STORAGE/MASTG-KNOW-0041.md +++ b/knowledge/android/MASVS-STORAGE/MASTG-KNOW-0041.md @@ -6,6 +6,12 @@ title: Internal Storage You can save files to the device's [internal storage](https://developer.android.com/training/data-storage#filesInternal "Using Internal Storage"). Files saved to internal storage are containerized by default and cannot be accessed by other apps on the device. When the user uninstalls your app, these files are removed. +Apps write files to internal storage using the Java and Kotlin File APIs described in @MASTG-KNOW-0x01. The most common locations are: + +- [`context.filesDir`](https://developer.android.com/reference/android/content/Context#getFilesDir()): persistent private files. +- [`context.cacheDir`](https://developer.android.com/reference/android/content/Context#getCacheDir()): temporary cache files that the system may delete when storage is low. +- [`context.noBackupFilesDir`](https://developer.android.com/reference/android/content/Context#getNoBackupFilesDir()): persistent private files excluded from auto-backup. + For example, the following Kotlin snippet stores sensitive information in clear text to a file `sensitive_info.txt` residing on internal storage. ```kotlin @@ -16,6 +22,6 @@ File(filesDir, fileName).bufferedWriter().use { writer -> } ``` -You should check the file mode to make sure that only the app can access the file. You can set this access with `MODE_PRIVATE`. Modes such as `MODE_WORLD_READABLE` (deprecated) and `MODE_WORLD_WRITEABLE` (deprecated) may pose a security risk. +You can also use [`Context.openFileOutput(name, mode)`](https://developer.android.com/reference/android/content/Context#openFileOutput(java.lang.String,int)) to write directly to `filesDir`. The `mode` parameter controls access: `MODE_PRIVATE` (the default) restricts the file to the calling app, while `MODE_WORLD_READABLE` and `MODE_WORLD_WRITEABLE` are deprecated and raise a `SecurityException` on API level 24 and above. -**Android Security Guidelines**: Android highlights that the data in the internal storage is private to the app and other apps cannot access it. It also recommends avoiding the use of `MODE_WORLD_READABLE` and `MODE_WORLD_WRITEABLE` modes for IPC files and use a [content provider](https://developer.android.com/privacy-and-security/security-tips#content-providers) instead. See the [Android Security Guidelines](https://developer.android.com/privacy-and-security/security-tips#internal-storage "Android Security Guidelines"). Android also provides a [guide](https://developer.android.com/privacy-and-security/security-best-practices#internal-storage "Store data in internal storage based on use case") on how to use internal storage securely. +**Android Security Guidelines**: Android highlights that the data in the internal storage is private to the app and other apps cannot access it. It also recommends avoiding the use of `MODE_WORLD_READABLE` and `MODE_WORLD_WRITEABLE` modes for IPC files and using a [content provider](https://developer.android.com/privacy-and-security/security-tips#content-providers) instead. See the [Android Security Guidelines](https://developer.android.com/privacy-and-security/security-tips#internal-storage "Android Security Guidelines"). Android also provides a [guide](https://developer.android.com/privacy-and-security/security-best-practices#internal-storage "Store data in internal storage based on use case") on how to use internal storage securely. diff --git a/knowledge/android/MASVS-STORAGE/MASTG-KNOW-0042.md b/knowledge/android/MASVS-STORAGE/MASTG-KNOW-0042.md index 425d807174f..ce6550d8c92 100644 --- a/knowledge/android/MASVS-STORAGE/MASTG-KNOW-0042.md +++ b/knowledge/android/MASVS-STORAGE/MASTG-KNOW-0042.md @@ -59,9 +59,13 @@ adb shell pm revoke org.owasp.mastestapp android.permission.READ_MEDIA_IMAGES ## External Storage APIs -There are APIs such as [`getExternalStoragePublicDirectory`](https://developer.android.com/reference/kotlin/android/os/Environment#getExternalStoragePublicDirectory(kotlin.String)) that return paths to a shared location that other apps can access. An app may obtain a path to an "external" location and write sensitive data to it. This location is considered "Shared Storage Requiring No User Interaction", which means that a third-party app with proper permissions can read this sensitive data. +Apps write files to external storage using the Java and Kotlin File APIs described in @MASTG-KNOW-0x01 (such as `FileOutputStream`, `FileWriter`, and Kotlin extension functions), combined with a path obtained from one of these `Context` methods: -For example, the following Kotlin snippet stores sensitive information in clear text to a file `password.txt` residing on external storage. +- [`Context.getExternalFilesDir(type)`](https://developer.android.com/reference/android/content/Context#getExternalFilesDir(java.lang.String)) — returns a directory on the primary shared/external storage where the app can store persistent files. Requires no permissions on API level 19+. Files are removed when the app is uninstalled. +- [`Context.getExternalCacheDir()`](https://developer.android.com/reference/android/content/Context#getExternalCacheDir()) — returns a directory on the primary shared/external storage for cache files. +- [`Environment.getExternalStoragePublicDirectory(type)`](https://developer.android.com/reference/android/os/Environment#getExternalStoragePublicDirectory(java.lang.String)) — returns a path to a shared location that other apps can access (deprecated in API level 29). + +For example, the following Kotlin snippet stores information in clear text to a file `password.txt` in the app-specific external directory: ```kotlin val password = "SecretPassword" diff --git a/knowledge/android/MASVS-STORAGE/MASTG-KNOW-0x01.md b/knowledge/android/MASVS-STORAGE/MASTG-KNOW-0x01.md new file mode 100644 index 00000000000..2e73ea1db5e --- /dev/null +++ b/knowledge/android/MASVS-STORAGE/MASTG-KNOW-0x01.md @@ -0,0 +1,97 @@ +--- +masvs_category: MASVS-STORAGE +platform: android +title: File System APIs +--- + +Android apps can write data to the file system using [various Java and Kotlin APIs](https://developer.android.com/training/data-storage/app-specific). The choice of API determines how data is written, but the storage location is determined separately. See @MASTG-KNOW-0041 for internal (app-specific) storage and @MASTG-KNOW-0042 for external storage. + +> Other ways to store data that do not involve direct file system access include: @MASTG-KNOW-0036, @MASTG-KNOW-0037, @MASTG-KNOW-0039, @MASTG-KNOW-0040, @MASTG-KNOW-0043 + +## Context File APIs + +[`Context.openFileOutput(String name, int mode)`](https://developer.android.com/reference/android/content/Context#openFileOutput(java.lang.String,%20int)) opens a named file in the app's internal files directory (`filesDir`) for writing and returns a [`FileOutputStream`](https://developer.android.com/reference/java/io/FileOutputStream). The `mode` parameter controls access: + +- `MODE_PRIVATE` (value `0`) — the file is accessible only to the calling app (default). +- `MODE_APPEND` — opens the file for appending if it already exists. +- `MODE_WORLD_READABLE` and `MODE_WORLD_WRITEABLE` were deprecated in API level 17 and raise a `SecurityException` on API level 24 and above. + +[`Context.openFileInput(String name)`](https://developer.android.com/reference/android/content/Context#openFileInput(java.lang.String)) is the corresponding read API, returning a `FileInputStream`. + +```kotlin +val fos = context.openFileOutput("data.txt", Context.MODE_PRIVATE) +fos.write("content".toByteArray()) +fos.close() +``` + +## java.io APIs + +The `java.io` package provides the classic byte-stream and character-writer APIs: + +- [`FileOutputStream`](https://developer.android.com/reference/java/io/FileOutputStream): byte-oriented output stream that writes directly to a file. Can be constructed with a `File` object or a file path string. +- [`FileInputStream`](https://developer.android.com/reference/java/io/FileInputStream): byte-oriented input stream that reads from a file. +- [`FileWriter`](https://developer.android.com/reference/java/io/FileWriter): character-oriented writer that writes to a file. Often wrapped in a `BufferedWriter` for efficiency. +- [`FileReader`](https://developer.android.com/reference/java/io/FileReader): character-oriented reader that reads from a file. +- [`BufferedWriter`](https://developer.android.com/reference/java/io/BufferedWriter): buffers character output, commonly wrapping a `FileWriter`. +- [`BufferedReader`](https://developer.android.com/reference/java/io/BufferedReader): buffers character input, commonly wrapping a `FileReader`. +- [`PrintWriter`](https://developer.android.com/reference/java/io/PrintWriter): prints formatted text to a file or another `Writer`. +- [`RandomAccessFile`](https://developer.android.com/reference/java/io/RandomAccessFile): supports both reading and writing at arbitrary byte positions within a file; opened with a mode string such as `"r"` (read-only) or `"rw"` (read-write). + +Example using `FileOutputStream` directly: + +```kotlin +val file = File(context.filesDir, "data.bin") +FileOutputStream(file).use { fos -> + fos.write(data) +} +``` + +Example using `FileWriter` with `BufferedWriter`: + +```kotlin +val file = File(context.filesDir, "data.txt") +BufferedWriter(FileWriter(file)).use { writer -> + writer.write("content") +} +``` + +## java.nio.file APIs (API level 26+) + +Since Android 8.0 (API level 26), the `java.nio.file` package is available: + +- [`Files.write(Path, byte[], OpenOption...)`](https://developer.android.com/reference/java/nio/file/Files#write(java.nio.file.Path,%20byte[],%20java.nio.file.OpenOption[])): atomically writes a byte array to a file, creating or truncating it. +- [`Files.newOutputStream(Path, OpenOption...)`](https://developer.android.com/reference/java/nio/file/Files#newOutputStream(java.nio.file.Path,%20java.nio.file.OpenOption[])): opens a file for writing and returns an [`OutputStream`](https://developer.android.com/reference/java/io/OutputStream). +- [`Files.newBufferedWriter(Path, OpenOption...)`](https://developer.android.com/reference/java/nio/file/Files#newBufferedWriter(java.nio.file.Path,%20java.nio.file.OpenOption[])): opens a file for writing text and returns a [`BufferedWriter`](https://developer.android.com/reference/java/io/BufferedWriter). +- [`Files.newByteChannel(Path, OpenOption...)`](https://developer.android.com/reference/java/nio/file/Files#newByteChannel(java.nio.file.Path,%20java.nio.file.OpenOption[])): opens or creates a file and returns a seekable [`FileChannel`](https://developer.android.com/reference/java/nio/channels/FileChannel) for both reading and writing. + +`FileChannel` can also be obtained from a `FileOutputStream` via `FileOutputStream.getChannel()` and supports memory-mapped I/O via `FileChannel.map()`. + +Example: + +```kotlin +val path = File(context.filesDir, "data.bin").toPath() +Files.write(path, data) +``` + +## Kotlin Extension APIs + +Kotlin provides extension functions on `java.io.File` for concise file I/O: + +- [`File.writeText(text, charset)`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/java.io.-file/write-text.html): writes text to the file, replacing any existing content. +- [`File.appendText(text, charset)`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/java.io.-file/append-text.html): appends text to the end of the file. +- [`File.writeBytes(array)`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/java.io.-file/write-bytes.html): writes bytes to the file, replacing any existing content. +- [`File.appendBytes(array)`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/java.io.-file/append-bytes.html): appends bytes to the end of the file. +- [`File.bufferedWriter()`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/java.io.-file/buffered-writer.html): returns a `BufferedWriter` for writing to the file. +- [`File.printWriter()`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/java.io.-file/print-writer.html): returns a `PrintWriter` for writing to the file. +- [`File.outputStream()`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/java.io.-file/output-stream.html): returns a `FileOutputStream` for writing to the file. +- [`File.readText(charset)`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/java.io.-file/read-text.html): reads the entire file as a string. +- [`File.readBytes()`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/java.io.-file/read-bytes.html): reads the entire file as a byte array. +- [`File.bufferedReader()`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/java.io.-file/buffered-reader.html): returns a `BufferedReader` for reading from the file. +- [`File.inputStream()`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/java.io.-file/input-stream.html): returns a `FileInputStream` for reading from the file. + +Example: + +```kotlin +File(context.filesDir, "data.txt").writeText("content") +File(context.filesDir, "more.txt").appendText("additional content") +``` diff --git a/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x01.md b/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x01.md index 9990fb37db3..b0cffc7775b 100644 --- a/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x01.md +++ b/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x01.md @@ -6,12 +6,12 @@ type: [static] weakness: MASWE-0006 best-practices: [MASTG-BEST-0x01] profiles: [L1, L2] -knowledge: [MASTG-KNOW-0041] +knowledge: [MASTG-KNOW-0x01, MASTG-KNOW-0041] --- ## Overview -This test uses static analysis to look for uses of Java File APIs that write data to the app's internal storage (see @MASTG-KNOW-0041). These include [`Context.openFileOutput`](https://developer.android.com/reference/android/content/Context#openFileOutput(java.lang.String,%20int)), [`FileOutputStream`](https://developer.android.com/reference/java/io/FileOutputStream), and [`FileWriter`](https://developer.android.com/reference/java/io/FileWriter). +This test uses static analysis to look for uses of Java File APIs (see @MASTG-KNOW-0x01) that write data to the app's internal storage (see @MASTG-KNOW-0041). These include [`Context.openFileOutput`](https://developer.android.com/reference/android/content/Context#openFileOutput(java.lang.String,%20int)), [`FileOutputStream`](https://developer.android.com/reference/java/io/FileOutputStream), and [`FileWriter`](https://developer.android.com/reference/java/io/FileWriter). Static analysis is great for identifying all code locations where the app is writing data to internal storage. However, it does not reveal the actual data being written at runtime. To confirm that sensitive data is written unencrypted, combine this test with the dynamic counterpart @MASTG-TEST-0x02. diff --git a/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x02.md b/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x02.md index 5f080d2429f..909a769a45b 100644 --- a/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x02.md +++ b/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x02.md @@ -6,14 +6,14 @@ type: [dynamic] weakness: MASWE-0006 best-practices: [MASTG-BEST-0x01] profiles: [L1, L2] -knowledge: [MASTG-KNOW-0041] +knowledge: [MASTG-KNOW-0x01, MASTG-KNOW-0041] --- ## Overview This test is the dynamic counterpart to @MASTG-TEST-0x01. -It uses runtime method hooking to identify whether sensitive data is written unencrypted to the app's internal storage by monitoring Java File API calls such as `Context.openFileOutput`, `FileOutputStream.write`, and `FileWriter.write`. Correlating these calls with any Cipher or KeyStore API calls lets you determine whether the data is encrypted before being written. +It uses runtime method hooking to identify whether sensitive data is written unencrypted to the app's internal storage by monitoring Java File API calls (see @MASTG-KNOW-0x01) such as `Context.openFileOutput`, `FileOutputStream.write`, and `FileWriter.write`. Correlating these calls with any Cipher or KeyStore API calls lets you determine whether the data is encrypted before being written. ## Steps From fc148c840904249452c48bee9d7645050abff5e4 Mon Sep 17 00:00:00 2001 From: Carlos Holguera Date: Mon, 11 May 2026 09:23:44 +0200 Subject: [PATCH 08/14] Add documentation for Native (NDK/JNI) file APIs in MASTG-KNOW-0x01.md --- .../android/MASVS-STORAGE/MASTG-KNOW-0x01.md | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/knowledge/android/MASVS-STORAGE/MASTG-KNOW-0x01.md b/knowledge/android/MASVS-STORAGE/MASTG-KNOW-0x01.md index 2e73ea1db5e..3622a539f16 100644 --- a/knowledge/android/MASVS-STORAGE/MASTG-KNOW-0x01.md +++ b/knowledge/android/MASVS-STORAGE/MASTG-KNOW-0x01.md @@ -95,3 +95,39 @@ Example: File(context.filesDir, "data.txt").writeText("content") File(context.filesDir, "more.txt").appendText("additional content") ``` + +## Native (NDK/JNI) APIs + +Apps that include native code via the [Android NDK](https://developer.android.com/ndk/guides) can write files from C or C++ using standard POSIX and C library calls. The file path is typically obtained from the Java layer (for example, from `Context.getFilesDir()`) and passed down via JNI. + +Commonly used write APIs: + +- [`fopen(path, mode)` / `fwrite()` / `fclose()`](https://en.cppreference.com/w/c/io): standard C I/O; `mode` can be `"w"` (write), `"a"` (append), or `"wb"` / `"ab"` for binary variants. +- [`open(path, flags, mode)` / `write()` / `close()`](https://man7.org/linux/man-pages/man2/open.2.html): POSIX system calls; `flags` such as `O_WRONLY | O_CREAT | O_TRUNC` control creation and truncation behavior. +- [`pwrite(fd, buf, count, offset)`](https://man7.org/linux/man-pages/man2/pwrite.2.html): writes at a specific byte offset without changing the file position. +- [`mmap()` with `MAP_SHARED`](https://man7.org/linux/man-pages/man2/mmap.2.html): maps a file into memory for direct read/write access; changes are written back to disk when the mapping is flushed with `msync()` or unmapped. + +Commonly used read APIs: + +- `fopen(path, "r")` / `fread()` / `fgets()` / `fclose()`: standard C I/O for reading. +- `open(path, O_RDONLY)` / `read()` / `close()`: POSIX system calls for reading. +- [`pread(fd, buf, count, offset)`](https://man7.org/linux/man-pages/man2/pread.2.html): reads from a specific byte offset. +- `mmap()` with `MAP_PRIVATE` / `PROT_READ`: maps a file into read-only memory. + +The Android NDK also exposes [`AAssetManager`](https://developer.android.com/ndk/reference/group/asset) for reading bundled assets from the APK, though this is read-only. + +Example (writing via POSIX from C): + +```c +#include +#include +#include + +void write_data(const char *path, const char *data) { + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd >= 0) { + write(fd, data, strlen(data)); + close(fd); + } +} +``` From de2e5fe10e9aa9c06c70a52f1dee9ec9c236a09b Mon Sep 17 00:00:00 2001 From: Carlos Holguera Date: Mon, 11 May 2026 09:23:50 +0200 Subject: [PATCH 09/14] Enhance encryption guidance for sensitive data handling in internal storage --- best-practices/MASTG-BEST-0x01.md | 59 +++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/best-practices/MASTG-BEST-0x01.md b/best-practices/MASTG-BEST-0x01.md index de4cfbda6cb..4be3379fc4a 100644 --- a/best-practices/MASTG-BEST-0x01.md +++ b/best-practices/MASTG-BEST-0x01.md @@ -30,3 +30,62 @@ The encryption key is generated and stored in the Android KeyStore, providing ha !!! warning The **Jetpack Security crypto library**, including `EncryptedFile` and `EncryptedSharedPreferences`, has been [deprecated](https://developer.android.com/privacy-and-security/cryptography#jetpack_security_crypto_library). However, since an official replacement has not yet been released, we recommend using these classes until one is available. + +## When EncryptedFile Is Not an Option + +If you can't use `EncryptedFile` (for example, because of library constraints, minimum API level requirements, or native code), encrypt the data manually before writing it to disk. + +The recommended approach on Android is to: + +1. Generate or retrieve an AES key stored in the Android KeyStore (see @MASTG-KNOW-0043). +2. Encrypt the plaintext with `AES/GCM/NoPadding` using the [`Cipher`](https://developer.android.com/reference/javax/crypto/Cipher) API. +3. Prepend the initialization vector (IV) to the ciphertext and write the combined bytes to the file using any of the standard File APIs (see @MASTG-KNOW-0x01). +4. On read, extract the IV and decrypt with the same key. + +```kotlin +// Generate or load a key from the Android KeyStore +val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore") +keyGenerator.init( + KeyGenParameterSpec.Builder("my_key_alias", + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build() +) +val secretKey = keyGenerator.generateKey() + +// Encrypt +val cipher = Cipher.getInstance("AES/GCM/NoPadding") +cipher.init(Cipher.ENCRYPT_MODE, secretKey) +val iv = cipher.iv +val ciphertext = cipher.doFinal("sensitive content".toByteArray()) + +// Write IV + ciphertext +val file = File(context.filesDir, "sensitive_data.bin") +FileOutputStream(file).use { fos -> + fos.write(iv) + fos.write(ciphertext) +} +``` + +Always use an authenticated encryption mode such as GCM. + +## Native, NDK, and JNI Code + +When file writes happen from native code, encrypt the data before passing it to the native layer, or use a well-audited native encryption library. Do not implement custom cryptographic primitives. + +**Preferred - Encrypt in Java or Kotlin, pass ciphertext to native code:** + +Encrypt the data on the Java or Kotlin side using Android KeyStore backed keys and `Cipher`, then pass only the resulting ciphertext byte array to the JNI layer for writing. This keeps key generation, storage, and access control within the Android security framework. + +**Alternative - Use a native encryption library:** + +If encryption must occur in native code, use a well-established library such as [Tink C++](https://developers.google.com/tink/tinkcrypto), [BoringSSL](https://boringssl.googlesource.com/boringssl/), OpenSSL, or a trusted encrypted storage library such as SQLCipher. Keys used by native code should still be generated, wrapped, or derived using Android KeyStore. They must not be hardcoded or stored in plaintext alongside the encrypted data. + +**Real world example, Signal Android.** + +Signal Android uses [SQLCipher for Android](https://github.com/signalapp/sqlcipher-android), a native encrypted SQLite implementation, for local database encryption. Signal's Java code generates a random 32 byte database secret, protects it with Android KeyStore through `KeyStoreHelper.seal(...)`, stores only the wrapped value in `SharedPreferences`, and later unwraps it with `KeyStoreHelper.unseal(...)` when opening the database. This behavior can be seen in Signal's [`DatabaseSecretProvider`](https://github.com/signalapp/Signal-Android/blob/main/app/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecretProvider.java). + +This is a production example of keeping key management in the Android framework while relying on a well-established native encryption layer for file backed storage. The important pattern is that the encryption secret is randomly generated and KeyStore protected, rather than being hardcoded or stored in plaintext beside the encrypted database. + +Molly, a Signal fork, also [documents this design](https://github.com/mollyim/mollyim-android/wiki/Data-Encryption-At-Rest), noting that Signal stores contacts, chat history, and attachments in an SQLCipher database and wraps the database encryption key with Android KeyStore before storing it in `SharedPreferences`. From 0788601a0f2b11b834644decf7fb409122d61bb1 Mon Sep 17 00:00:00 2001 From: Carlos Holguera Date: Mon, 11 May 2026 09:55:46 +0200 Subject: [PATCH 10/14] Fix demo 0x01 --- .../MASTG-DEMO-0x01/MASTG-DEMO-0x01.md | 4 +- .../MASTG-DEMO-0x01/MastgTest_reversed.java | 70 +++++++++++-------- .../MASVS-STORAGE/MASTG-DEMO-0x01/output.txt | 29 ++------ 3 files changed, 48 insertions(+), 55 deletions(-) diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MASTG-DEMO-0x01.md b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MASTG-DEMO-0x01.md index d5095fe0978..f743f05eb96 100644 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MASTG-DEMO-0x01.md +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MASTG-DEMO-0x01.md @@ -36,7 +36,7 @@ The test fails because the app uses File APIs to write sensitive data to interna After reviewing the decompiled code at the locations specified in the output: -- Line 32: `openFileOutput("secret_token.txt", ...)` is followed by writing `password` in plaintext — no preceding `Cipher` calls, so the data is stored unencrypted. -- Line 41: `new FileOutputStream(new File(context.getFilesDir(), "api_key.txt"))` is followed by writing `apiKey` in plaintext — again, no preceding `Cipher` calls. +- Line 33: `openFileOutput("secret_token.txt", ...)` is followed by writing `password` in plaintext and no preceding `Cipher` calls, so the data is stored unencrypted. +- Lines 42-43: `new File(context.getFilesDir(), "api_key.txt")` is stored in `apiKeyFile` and passed to `new FileOutputStream(apiKeyFile)`, which is followed by writing `apiKey` in plaintext. Again, no preceding `Cipher` calls. You can confirm the dynamic counterpart in @MASTG-DEMO-0x02. diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java index 85849d7adf2..d9eb4343b0f 100644 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java @@ -1,54 +1,68 @@ package org.owasp.mastestapp; -/*...*/ import android.content.Context; import android.util.Log; +import androidx.autofill.HintConstants; import java.io.File; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import kotlin.Metadata; import kotlin.io.CloseableKt; import kotlin.jvm.internal.Intrinsics; import kotlin.text.Charsets; + /* compiled from: MastgTest.kt */ -@Metadata(d1 = {}, k = 1, mv = {1, 9, 0}, xi = 48) -/* loaded from: classes4.dex */ +@Metadata(d1 = {"\u0000\u001a\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\u0003\b\u0007\u0018\u00002\u00020\u0001B\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\u0006\u0010\t\u001a\u00020\u0007R\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000R\u000e\u0010\u0006\u001a\u00020\u0007X\u0082D¢\u0006\u0002\n\u0000R\u000e\u0010\b\u001a\u00020\u0007X\u0082D¢\u0006\u0002\n\u0000¨\u0006\n"}, d2 = {"Lorg/owasp/mastestapp/MastgTest;", "", "context", "Landroid/content/Context;", "", "(Landroid/content/Context;)V", HintConstants.AUTOFILL_HINT_PASSWORD, "", "apiKey", "mastgTest", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48) +/* loaded from: classes3.dex */ public final class MastgTest { public static final int $stable = 8; + private final String apiKey; private final Context context; - private final String password = "MyS3cr3tP4ssw0rd"; - private final String apiKey = "AKIAABCDEFGHIJKLMNOP"; + private final String password; public MastgTest(Context context) { Intrinsics.checkNotNullParameter(context, "context"); this.context = context; + this.password = "MyS3cr3tP4ssw0rd"; + this.apiKey = "AKIAABCDEFGHIJKLMNOP"; } - public final String mastgTest() { + public final String mastgTest() throws FileNotFoundException { try { - String result = ""; - - // FAIL: [MASTG-TEST-0x01] The app stores the password unencrypted using openFileOutput, exposing it to attackers with device access. - FileOutputStream tokenOutputStream = this.context.openFileOutput("secret_token.txt", Context.MODE_PRIVATE); - byte[] passwordBytes = this.password.getBytes(Charsets.UTF_8); - Intrinsics.checkNotNullExpressionValue(passwordBytes, "this as java.lang.String).getBytes(charset)"); - tokenOutputStream.write(passwordBytes); - Log.d("FileAPIs", "Written unencrypted password to secret_token.txt"); - CloseableKt.closeFinally(tokenOutputStream, null); - result += "[FAIL]: Stored unencrypted password in secret_token.txt using openFileOutput.\n\n"; - - // FAIL: [MASTG-TEST-0x01] The app stores the API key unencrypted using FileOutputStream, making it readable by attackers with sandbox access. - FileOutputStream apiKeyOutputStream = new FileOutputStream(new File(this.context.getFilesDir(), "api_key.txt")); - byte[] apiKeyBytes = this.apiKey.getBytes(Charsets.UTF_8); - Intrinsics.checkNotNullExpressionValue(apiKeyBytes, "this as java.lang.String).getBytes(charset)"); - apiKeyOutputStream.write(apiKeyBytes); - Log.d("FileAPIs", "Written unencrypted API key to api_key.txt"); - CloseableKt.closeFinally(apiKeyOutputStream, null); - result += "[FAIL]: Stored unencrypted API key in api_key.txt using FileOutputStream.\n\n"; - - return result; + FileOutputStream fileOutputStreamOpenFileOutput = this.context.openFileOutput("secret_token.txt", 0); + try { + FileOutputStream output = fileOutputStreamOpenFileOutput; + byte[] bytes = this.password.getBytes(Charsets.UTF_8); + Intrinsics.checkNotNullExpressionValue(bytes, "getBytes(...)"); + output.write(bytes); + Log.d("FileAPIs", "Written unencrypted password to secret_token.txt"); + CloseableKt.closeFinally(fileOutputStreamOpenFileOutput, null); + String result = "[FAIL]: Stored unencrypted password in secret_token.txt using openFileOutput.\n\n"; + File apiKeyFile = new File(this.context.getFilesDir(), "api_key.txt"); + fileOutputStreamOpenFileOutput = new FileOutputStream(apiKeyFile); + try { + FileOutputStream output2 = fileOutputStreamOpenFileOutput; + byte[] bytes2 = this.apiKey.getBytes(Charsets.UTF_8); + Intrinsics.checkNotNullExpressionValue(bytes2, "getBytes(...)"); + output2.write(bytes2); + Log.d("FileAPIs", "Written unencrypted API key to api_key.txt"); + CloseableKt.closeFinally(fileOutputStreamOpenFileOutput, null); + return result + "[FAIL]: Stored unencrypted API key in api_key.txt using FileOutputStream.\n\n"; + } finally { + } + } finally { + try { + throw th; + } finally { + } + } } catch (IOException e) { - return "Error during MastgTest: " + e.getMessage(); + String message = e.getMessage(); + if (message == null) { + message = "Unknown error"; + } + return "Error during MastgTest: " + message; } } } diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.txt b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.txt index c82d1e192c6..d894d430d8b 100644 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.txt +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.txt @@ -1,10 +1,3 @@ - - -┌─────────────┐ -│ Scan Status │ -└─────────────┘ - Scanning 1 file with 3 Code rules: - Scanning 1 file with 3 java rules. ┌─────────────────┐ @@ -13,30 +6,16 @@ MastgTest_reversed.java ❯❱ rules.mastg-android-unencrypted-internal-file-storage-openfileoutput - ❰❰ Blocking ❱❱ [MASVS-STORAGE] Verify that any sensitive data written via openFileOutput is encrypted before storage - 32┆ FileOutputStream tokenOutputStream = this.context.openFileOutput("secret_token.txt", - Context.MODE_PRIVATE); + 33┆ FileOutputStream fileOutputStreamOpenFileOutput = + this.context.openFileOutput("secret_token.txt", 0); ❯❱ rules.mastg-android-unencrypted-internal-file-storage-fileoutputstream - ❰❰ Blocking ❱❱ [MASVS-STORAGE] Verify that any sensitive data written via FileOutputStream to internal storage is encrypted before storage - 41┆ FileOutputStream apiKeyOutputStream = new FileOutputStream(new - File(this.context.getFilesDir(), "api_key.txt")); + 42┆ File apiKeyFile = new File(this.context.getFilesDir(), "api_key.txt"); + 43┆ fileOutputStreamOpenFileOutput = new FileOutputStream(apiKeyFile); - - -┌──────────────┐ -│ Scan Summary │ -└──────────────┘ -✅ Scan completed successfully. - • Findings: 2 (2 blocking) - • Rules run: 3 - • Targets scanned: 1 - • Parsed lines: ~100.0% - • No ignore information available -Ran 3 rules on 1 file: 2 findings. From 107e642c281a076a156f0943f487bbf4377643c1 Mon Sep 17 00:00:00 2001 From: Carlos Holguera Date: Mon, 11 May 2026 09:56:43 +0200 Subject: [PATCH 11/14] fix rule --- ...roid-unencrypted-internal-file-storage.yml | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/rules/mastg-android-unencrypted-internal-file-storage.yml b/rules/mastg-android-unencrypted-internal-file-storage.yml index 0e8e9db693d..8147a8f24d8 100644 --- a/rules/mastg-android-unencrypted-internal-file-storage.yml +++ b/rules/mastg-android-unencrypted-internal-file-storage.yml @@ -19,6 +19,22 @@ rules: - pattern: new FileOutputStream(new File($CTX.filesDir, ...)) - pattern: new FileOutputStream(new File($CTX.getCacheDir(), ...)) - pattern: new FileOutputStream(new File($CTX.getNoBackupFilesDir(), ...)) + - pattern: | + $FILE = new File($CTX.getFilesDir(), ...); + ... + new FileOutputStream($FILE); + - pattern: | + $FILE = new File($CTX.filesDir, ...); + ... + new FileOutputStream($FILE); + - pattern: | + $FILE = new File($CTX.getCacheDir(), ...); + ... + new FileOutputStream($FILE); + - pattern: | + $FILE = new File($CTX.getNoBackupFilesDir(), ...); + ... + new FileOutputStream($FILE); - id: mastg-android-unencrypted-internal-file-storage-filewriter severity: WARNING languages: @@ -30,3 +46,15 @@ rules: - pattern: new FileWriter(new File($CTX.getFilesDir(), ...)) - pattern: new FileWriter(new File($CTX.filesDir, ...)) - pattern: new FileWriter(new File($CTX.getCacheDir(), ...)) + - pattern: | + $FILE = new File($CTX.getFilesDir(), ...); + ... + new FileWriter($FILE); + - pattern: | + $FILE = new File($CTX.filesDir, ...); + ... + new FileWriter($FILE); + - pattern: | + $FILE = new File($CTX.getCacheDir(), ...); + ... + new FileWriter($FILE); From a633a13ce11a021f6220a23fc5106aaf7ee76a49 Mon Sep 17 00:00:00 2001 From: Carlos Holguera Date: Mon, 11 May 2026 09:57:03 +0200 Subject: [PATCH 12/14] optimize demo 0x02 --- .../MASVS-STORAGE/MASTG-DEMO-0x02/hooks.json | 7 +- .../MASVS-STORAGE/MASTG-DEMO-0x02/output.json | 554 +++++++++++------- 2 files changed, 355 insertions(+), 206 deletions(-) diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/hooks.json b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/hooks.json index 6ef1624aea4..47a67fe15bc 100644 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/hooks.json +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/hooks.json @@ -9,8 +9,11 @@ }, { "class": "java.io.FileOutputStream", - "methods": [ - "write" + "method": "write", + "overloads": [ + { + "args": ["[B"] + } ], "filterEventsByStacktrace": ["org.owasp.mastestapp"] }, diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/output.json b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/output.json index 280a8001d18..f8f2a28e987 100644 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/output.json +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/output.json @@ -1,216 +1,362 @@ { - "type": "summary", - "hooks": [ - { - "class": "android.content.ContextWrapper", - "method": "openFileOutput", - "overloads": [ - { - "args": [ - "java.lang.String", - "int" - ] - } - ] - }, - { - "class": "java.io.FileOutputStream", - "method": "write", - "overloads": [ - { - "args": [ - "[B" - ] + "type": "summary", + "hooks": [ + { + "class": "android.content.ContextWrapper", + "method": "openFileOutput", + "overloads": [ + { + "args": [ + "java.lang.String", + "int" + ] + } + ] }, { - "args": [ - "[B", - "int", - "int" - ] + "class": "java.io.FileOutputStream", + "method": "write", + "overloads": [ + { + "args": [ + "[B" + ] + } + ] }, { - "args": [ - "int" - ] - } - ] - }, - { - "class": "javax.crypto.Cipher", - "method": "getInstance", - "overloads": [ - { - "args": [ - "java.lang.String" - ] - } - ] - }, - { - "class": "javax.crypto.Cipher", - "method": "doFinal", - "overloads": [ - { - "args": [ - "[B" - ] - } - ] - }, - { - "class": "javax.crypto.Cipher", - "method": "init", - "overloads": [ - { - "args": [ - "int", - "java.security.Key" - ] - } - ] - }, - { - "class": "java.security.KeyStore", - "method": "getEntry", - "overloads": [ - { - "args": [ - "java.lang.String", - "java.security.KeyStore$ProtectionParameter" - ] - } - ] - }, - { - "class": "javax.crypto.KeyGenerator", - "method": "getInstance", - "overloads": [ - { - "args": [ - "java.lang.String", - "java.lang.String" - ] - } - ] - }, - { - "class": "javax.crypto.KeyGenerator", - "method": "generateKey", - "overloads": [ - { - "args": [] - } - ] - }, - { - "class": "android.util.Base64", - "method": "encodeToString", - "overloads": [ - { - "args": [ - "[B", - "int" - ] + "class": "javax.crypto.Cipher", + "method": "getInstance", + "overloads": [ + { + "args": [ + "java.lang.String" + ] + }, + { + "args": [ + "java.lang.String", + "java.lang.String" + ] + }, + { + "args": [ + "java.lang.String", + "java.security.Provider" + ] + } + ] + }, + { + "class": "javax.crypto.Cipher", + "method": "doFinal", + "overloads": [ + { + "args": [ + "java.nio.ByteBuffer", + "java.nio.ByteBuffer" + ] + }, + { + "args": [ + "[B", + "int" + ] + }, + { + "args": [ + "[B", + "int", + "int", + "[B" + ] + }, + { + "args": [ + "[B", + "int", + "int", + "[B", + "int" + ] + }, + { + "args": [] + }, + { + "args": [ + "[B" + ] + }, + { + "args": [ + "[B", + "int", + "int" + ] + } + ] + }, + { + "class": "javax.crypto.Cipher", + "method": "init", + "overloads": [ + { + "args": [ + "int", + "java.security.Key" + ] + }, + { + "args": [ + "int", + "java.security.Key", + "java.security.AlgorithmParameters" + ] + }, + { + "args": [ + "int", + "java.security.Key", + "java.security.AlgorithmParameters", + "java.security.SecureRandom" + ] + }, + { + "args": [ + "int", + "java.security.Key", + "java.security.SecureRandom" + ] + }, + { + "args": [ + "int", + "java.security.Key", + "java.security.spec.AlgorithmParameterSpec" + ] + }, + { + "args": [ + "int", + "java.security.Key", + "java.security.spec.AlgorithmParameterSpec", + "java.security.SecureRandom" + ] + }, + { + "args": [ + "int", + "java.security.cert.Certificate" + ] + }, + { + "args": [ + "int", + "java.security.cert.Certificate", + "java.security.SecureRandom" + ] + } + ] + }, + { + "class": "java.security.KeyStore", + "method": "setEntry", + "overloads": [ + { + "args": [ + "java.lang.String", + "java.security.KeyStore$Entry", + "java.security.KeyStore$ProtectionParameter" + ] + } + ] + }, + { + "class": "java.security.KeyStore", + "method": "getEntry", + "overloads": [ + { + "args": [ + "java.lang.String", + "java.security.KeyStore$ProtectionParameter" + ] + } + ] + }, + { + "class": "javax.crypto.KeyGenerator", + "method": "getInstance", + "overloads": [ + { + "args": [ + "java.lang.String" + ] + }, + { + "args": [ + "java.lang.String", + "java.lang.String" + ] + }, + { + "args": [ + "java.lang.String", + "java.security.Provider" + ] + } + ] + }, + { + "class": "javax.crypto.KeyGenerator", + "method": "generateKey", + "overloads": [ + { + "args": [] + } + ] + }, + { + "class": "android.util.Base64", + "method": "encodeToString", + "overloads": [ + { + "args": [ + "[B", + "int" + ] + }, + { + "args": [ + "[B", + "int", + "int", + "int" + ] + } + ] + }, + { + "class": "android.util.Base64", + "method": "decode", + "overloads": [ + { + "args": [ + "java.lang.String", + "int" + ] + }, + { + "args": [ + "[B", + "int" + ] + }, + { + "args": [ + "[B", + "int", + "int", + "int" + ] + } + ] } - ] - } - ], - "totalHooks": 20, - "errors": [], - "totalErrors": 0 + ], + "totalHooks": 31, + "errors": [], + "totalErrors": 0 } { - "id": "a1b2c3d4-1234-5678-abcd-111111111111", - "type": "hook", - "category": "STORAGE", - "time": "2026-01-23T11:00:01.100Z", - "class": "android.content.ContextWrapper", - "method": "openFileOutput", - "instanceId": 12345678, - "stackTrace": [ - "android.content.ContextWrapper.openFileOutput(Native Method)", - "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:22)", - "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", - "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", - "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", - "java.lang.Thread.run(Thread.java:1119)" - ], - "inputParameters": [ - { - "declaredType": "java.lang.String", - "value": "secret_token.txt" - }, - { - "declaredType": "int", - "value": 0 - } - ], - "returnValue": [ - { - "declaredType": "java.io.FileOutputStream", - "value": "", - "runtimeType": "java.io.FileOutputStream", - "instanceId": "23456789", - "instanceToString": "java.io.FileOutputStream@1234abc" - } - ] + "id": "087df98c-1104-4cf5-a155-f69401c40039", + "type": "hook", + "category": "STORAGE", + "time": "2026-05-11T07:54:45.613Z", + "class": "android.content.ContextWrapper", + "method": "openFileOutput", + "instanceId": 96242449, + "stackTrace": [ + "android.content.ContextWrapper.openFileOutput(Native Method)", + "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:21)", + "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", + "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", + "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", + "java.lang.Thread.run(Thread.java:1012)" + ], + "inputParameters": [ + { + "declaredType": "java.lang.String", + "value": "secret_token.txt" + }, + { + "declaredType": "int", + "value": 0 + } + ], + "returnValue": [ + { + "declaredType": "java.io.FileOutputStream", + "value": "", + "runtimeType": "java.io.FileOutputStream", + "instanceToString": "java.io.FileOutputStream@3cc9476" + } + ] } { - "id": "a1b2c3d4-1234-5678-abcd-222222222222", - "type": "hook", - "category": "STORAGE", - "time": "2026-01-23T11:00:01.110Z", - "class": "java.io.FileOutputStream", - "method": "write", - "instanceId": 23456789, - "stackTrace": [ - "java.io.FileOutputStream.write(Native Method)", - "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:23)", - "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", - "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", - "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", - "java.lang.Thread.run(Thread.java:1119)" - ], - "inputParameters": [ - { - "declaredType": "[B", - "value": "MyS3cr3tP4ssw0rd" - } - ], - "returnValue": [ - { - "declaredType": "void", - "value": "void" - } - ] + "id": "c19f945a-091b-4d06-a730-58d1317ee078", + "type": "hook", + "category": "STORAGE", + "time": "2026-05-11T07:54:45.617Z", + "class": "java.io.FileOutputStream", + "method": "write", + "instanceId": 63738998, + "stackTrace": [ + "java.io.FileOutputStream.write(Native Method)", + "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:22)", + "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", + "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", + "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", + "java.lang.Thread.run(Thread.java:1012)" + ], + "inputParameters": [ + { + "declaredType": "[B", + "value": "MyS3cr3tP4ssw0rd" + } + ], + "returnValue": [ + { + "declaredType": "void", + "value": "void" + } + ] } { - "id": "a1b2c3d4-1234-5678-abcd-333333333333", - "type": "hook", - "category": "STORAGE", - "time": "2026-01-23T11:00:01.200Z", - "class": "java.io.FileOutputStream", - "method": "write", - "instanceId": 34567890, - "stackTrace": [ - "java.io.FileOutputStream.write(Native Method)", - "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:30)", - "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", - "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", - "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", - "java.lang.Thread.run(Thread.java:1119)" - ], - "inputParameters": [ - { - "declaredType": "[B", - "value": "AKIAABCDEFGHIJKLMNOP" - } - ], - "returnValue": [ - { - "declaredType": "void", - "value": "void" - } - ] -} + "id": "54260dcb-5b8c-45b1-978a-4e6663e6b491", + "type": "hook", + "category": "STORAGE", + "time": "2026-05-11T07:54:45.620Z", + "class": "java.io.FileOutputStream", + "method": "write", + "instanceId": 211507831, + "stackTrace": [ + "java.io.FileOutputStream.write(Native Method)", + "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:30)", + "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", + "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", + "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", + "java.lang.Thread.run(Thread.java:1012)" + ], + "inputParameters": [ + { + "declaredType": "[B", + "value": "AKIAABCDEFGHIJKLMNOP" + } + ], + "returnValue": [ + { + "declaredType": "void", + "value": "void" + } + ] +} \ No newline at end of file From 21e791806824aa492001877ceb3a003be31db373 Mon Sep 17 00:00:00 2001 From: Carlos Holguera Date: Mon, 11 May 2026 10:37:52 +0200 Subject: [PATCH 13/14] Enhance MASTG-DEMO-0x01 to include AES/GCM encryption for sensitive data storage --- .../MASTG-DEMO-0x01/MASTG-DEMO-0x01.md | 15 +++-- .../MASTG-DEMO-0x01/MastgTest.kt | 44 ++++++++++++- .../MASTG-DEMO-0x01/MastgTest_reversed.java | 65 ++++++++++++++++++- .../MASVS-STORAGE/MASTG-DEMO-0x01/output.txt | 11 ++-- 4 files changed, 121 insertions(+), 14 deletions(-) diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MASTG-DEMO-0x01.md b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MASTG-DEMO-0x01.md index f743f05eb96..89a0bdd42c5 100644 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MASTG-DEMO-0x01.md +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MASTG-DEMO-0x01.md @@ -9,10 +9,11 @@ tools: [MASTG-TOOL-0110] ## Sample -The code below stores sensitive data to the app's internal storage using Java File APIs, both without encryption: +The code below uses Java File APIs to write to the app's internal storage: -- A password is stored unencrypted using `openFileOutput` -- An API key is stored unencrypted using `FileOutputStream` +- A password is stored **unencrypted** using `openFileOutput`. +- An API key is stored **unencrypted** using `FileOutputStream`. +- An encrypted secret is stored using `FileOutputStream` after AES/GCM encryption with an AndroidKeyStore-backed key. {{ MastgTest.kt # MastgTest_reversed.java }} @@ -26,7 +27,7 @@ Let's run our @MASTG-TOOL-0110 rule against the sample code. ## Observation -The rule has identified 2 locations that indicate use of File APIs to write data to internal storage. +The rule has identified 3 locations that indicate use of File APIs to write data to internal storage. {{ output.txt }} @@ -36,7 +37,9 @@ The test fails because the app uses File APIs to write sensitive data to interna After reviewing the decompiled code at the locations specified in the output: -- Line 33: `openFileOutput("secret_token.txt", ...)` is followed by writing `password` in plaintext and no preceding `Cipher` calls, so the data is stored unencrypted. -- Lines 42-43: `new File(context.getFilesDir(), "api_key.txt")` is stored in `apiKeyFile` and passed to `new FileOutputStream(apiKeyFile)`, which is followed by writing `apiKey` in plaintext. Again, no preceding `Cipher` calls. +- Line 69: `openFileOutput("secret_token.txt", ...)` is followed by writing `password` in plaintext and no preceding `Cipher` calls, so the data is stored unencrypted. +- Lines 78-79: `new File(context.getFilesDir(), "api_key.txt")` is stored in `apiKeyFile` and passed to `new FileOutputStream(apiKeyFile)`, which is followed by writing `apiKey` in plaintext. Again, no preceding `Cipher` calls. + +The test **passes** only for the `encrypted_data.bin` file written in lines 95-96: the data is encrypted using `AES/GCM` with a key generated in the AndroidKeyStore before being written to internal storage. You can confirm the dynamic counterpart in @MASTG-DEMO-0x02. diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest.kt b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest.kt index 25b92560160..ea8f7c94a22 100644 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest.kt +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest.kt @@ -1,17 +1,43 @@ package org.owasp.mastestapp -// SUMMARY: This sample demonstrates storing sensitive data unencrypted to the app's internal storage using the Java File APIs (openFileOutput and FileOutputStream). +// SUMMARY: This sample demonstrates storing sensitive data unencrypted to the app's internal storage using the Java File APIs (openFileOutput and FileOutputStream), and also shows the correct approach using AES/GCM encryption with an AndroidKeyStore-backed key. import android.content.Context +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties import android.util.Log import java.io.File import java.io.FileOutputStream import java.io.IOException +import java.security.KeyStore +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey class MastgTest(private val context: Context) { private val password = "MyS3cr3tP4ssw0rd" private val apiKey = "AKIAABCDEFGHIJKLMNOP" + private val encryptedSecret = "SensitiveDataToEncrypt" + private val keyAlias = "MastgTestKeyAlias" + + private fun getOrCreateKey(): SecretKey { + val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } + keyStore.getKey(keyAlias, null)?.let { return it as SecretKey } + + val spec = KeyGenParameterSpec.Builder( + keyAlias, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setKeySize(256) + .build() + + return KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore") + .apply { init(spec) } + .generateKey() + } fun mastgTest(): String { return try { @@ -32,6 +58,22 @@ class MastgTest(private val context: Context) { } result += "[FAIL]: Stored unencrypted API key in api_key.txt using FileOutputStream.\n\n" + // PASS: [MASTG-TEST-0x01] The app encrypts the data using AES/GCM with an AndroidKeyStore-backed key before writing it to internal storage. + val secretKey = getOrCreateKey() + val cipher = Cipher.getInstance("AES/GCM/NoPadding").apply { + init(Cipher.ENCRYPT_MODE, secretKey) + } + val iv = cipher.iv + val ciphertext = cipher.doFinal(encryptedSecret.toByteArray()) + val encryptedFile = File(context.filesDir, "encrypted_data.bin") + FileOutputStream(encryptedFile).use { output -> + output.write(iv.size) + output.write(iv) + output.write(ciphertext) + Log.d("FileAPIs", "Written AES/GCM-encrypted data to encrypted_data.bin") + } + result += "[PASS]: Stored AES/GCM-encrypted data in encrypted_data.bin using an AndroidKeyStore-backed key.\n\n" + result } catch (e: IOException) { "Error during MastgTest: ${e.message ?: "Unknown error"}" diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java index d9eb4343b0f..4c080cbf2df 100644 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java @@ -1,24 +1,42 @@ package org.owasp.mastestapp; import android.content.Context; +import android.security.keystore.KeyGenParameterSpec; import android.util.Log; import androidx.autofill.HintConstants; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; import kotlin.Metadata; import kotlin.io.CloseableKt; import kotlin.jvm.internal.Intrinsics; import kotlin.text.Charsets; /* compiled from: MastgTest.kt */ -@Metadata(d1 = {"\u0000\u001a\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\u0003\b\u0007\u0018\u00002\u00020\u0001B\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\u0006\u0010\t\u001a\u00020\u0007R\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000R\u000e\u0010\u0006\u001a\u00020\u0007X\u0082D¢\u0006\u0002\n\u0000R\u000e\u0010\b\u001a\u00020\u0007X\u0082D¢\u0006\u0002\n\u0000¨\u0006\n"}, d2 = {"Lorg/owasp/mastestapp/MastgTest;", "", "context", "Landroid/content/Context;", "", "(Landroid/content/Context;)V", HintConstants.AUTOFILL_HINT_PASSWORD, "", "apiKey", "mastgTest", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48) +@Metadata(d1 = {"\u0000\"\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\u0018\u0002\n\u0002\b\u0002\b\u0007\u0018\u00002\u00020\u0001B\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\b\u0010\u000b\u001a\u00020\fH\u0002J\u0006\u0010\r\u001a\u00020\u0007R\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000R\u000e\u0010\u0006\u001a\u00020\u0007X\u0082D¢\u0006\u0002\n\u0000R\u000e\u0010\b\u001a\u00020\u0007X\u0082D¢\u0006\u0002\n\u0000R\u000e\u0010\t\u001a\u00020\u0007X\u0082D¢\u0006\u0002\n\u0000R\u000e\u0010\n\u001a\u00020\u0007X\u0082D¢\u0006\u0002\n\u0000¨\u0006\u000e"}, d2 = {"Lorg/owasp/mastestapp/MastgTest;", "", "context", "Landroid/content/Context;", "", "(Landroid/content/Context;)V", HintConstants.AUTOFILL_HINT_PASSWORD, "", "apiKey", "encryptedSecret", "keyAlias", "getOrCreateKey", "Ljavax/crypto/SecretKey;", "mastgTest", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48) /* loaded from: classes3.dex */ public final class MastgTest { public static final int $stable = 8; private final String apiKey; private final Context context; + private final String encryptedSecret; + private final String keyAlias; private final String password; public MastgTest(Context context) { @@ -26,9 +44,27 @@ public MastgTest(Context context) { this.context = context; this.password = "MyS3cr3tP4ssw0rd"; this.apiKey = "AKIAABCDEFGHIJKLMNOP"; + this.encryptedSecret = "SensitiveDataToEncrypt"; + this.keyAlias = "MastgTestKeyAlias"; } - public final String mastgTest() throws FileNotFoundException { + private final SecretKey getOrCreateKey() throws NoSuchAlgorithmException, UnrecoverableKeyException, IOException, KeyStoreException, CertificateException, NoSuchProviderException, InvalidAlgorithmParameterException { + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + Key it = keyStore.getKey(this.keyAlias, null); + if (it != null) { + return (SecretKey) it; + } + KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(this.keyAlias, 3).setBlockModes("GCM").setEncryptionPaddings("NoPadding").setKeySize(256).build(); + Intrinsics.checkNotNullExpressionValue(spec, "build(...)"); + KeyGenerator $this$getOrCreateKey_u24lambda_u242 = KeyGenerator.getInstance("AES", "AndroidKeyStore"); + $this$getOrCreateKey_u24lambda_u242.init(spec); + SecretKey secretKeyGenerateKey = $this$getOrCreateKey_u24lambda_u242.generateKey(); + Intrinsics.checkNotNullExpressionValue(secretKeyGenerateKey, "generateKey(...)"); + return secretKeyGenerateKey; + } + + public final String mastgTest() throws BadPaddingException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, UnrecoverableKeyException, InvalidKeyException, KeyStoreException, CertificateException, NoSuchProviderException, FileNotFoundException, InvalidAlgorithmParameterException { try { FileOutputStream fileOutputStreamOpenFileOutput = this.context.openFileOutput("secret_token.txt", 0); try { @@ -48,7 +84,30 @@ public final String mastgTest() throws FileNotFoundException { output2.write(bytes2); Log.d("FileAPIs", "Written unencrypted API key to api_key.txt"); CloseableKt.closeFinally(fileOutputStreamOpenFileOutput, null); - return result + "[FAIL]: Stored unencrypted API key in api_key.txt using FileOutputStream.\n\n"; + String result2 = result + "[FAIL]: Stored unencrypted API key in api_key.txt using FileOutputStream.\n\n"; + SecretKey secretKey = getOrCreateKey(); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(1, secretKey); + byte[] iv = cipher.getIV(); + byte[] bytes3 = this.encryptedSecret.getBytes(Charsets.UTF_8); + Intrinsics.checkNotNullExpressionValue(bytes3, "getBytes(...)"); + byte[] ciphertext = cipher.doFinal(bytes3); + File encryptedFile = new File(this.context.getFilesDir(), "encrypted_data.bin"); + fileOutputStreamOpenFileOutput = new FileOutputStream(encryptedFile); + try { + FileOutputStream output3 = fileOutputStreamOpenFileOutput; + output3.write(iv.length); + output3.write(iv); + output3.write(ciphertext); + Log.d("FileAPIs", "Written AES/GCM-encrypted data to encrypted_data.bin"); + CloseableKt.closeFinally(fileOutputStreamOpenFileOutput, null); + return result2 + "[PASS]: Stored AES/GCM-encrypted data in encrypted_data.bin using an AndroidKeyStore-backed key.\n\n"; + } finally { + try { + throw th; + } finally { + } + } } finally { } } finally { diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.txt b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.txt index d894d430d8b..09c9d299acf 100644 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.txt +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.txt @@ -1,7 +1,7 @@ ┌─────────────────┐ -│ 2 Code Findings │ +│ 3 Code Findings │ └─────────────────┘ MastgTest_reversed.java @@ -9,13 +9,16 @@ [MASVS-STORAGE] Verify that any sensitive data written via openFileOutput is encrypted before storage - 33┆ FileOutputStream fileOutputStreamOpenFileOutput = + 69┆ FileOutputStream fileOutputStreamOpenFileOutput = this.context.openFileOutput("secret_token.txt", 0); ❯❱ rules.mastg-android-unencrypted-internal-file-storage-fileoutputstream [MASVS-STORAGE] Verify that any sensitive data written via FileOutputStream to internal storage is encrypted before storage - 42┆ File apiKeyFile = new File(this.context.getFilesDir(), "api_key.txt"); - 43┆ fileOutputStreamOpenFileOutput = new FileOutputStream(apiKeyFile); + 78┆ File apiKeyFile = new File(this.context.getFilesDir(), "api_key.txt"); + 79┆ fileOutputStreamOpenFileOutput = new FileOutputStream(apiKeyFile); + ⋮┆---------------------------------------- + 95┆ File encryptedFile = new File(this.context.getFilesDir(), "encrypted_data.bin"); + 96┆ fileOutputStreamOpenFileOutput = new FileOutputStream(encryptedFile); From 43c330fb391d4d5590c799b580ca3e4fff58b03d Mon Sep 17 00:00:00 2001 From: Carlos Holguera Date: Mon, 11 May 2026 10:39:27 +0200 Subject: [PATCH 14/14] Enhance MASTG-DEMO-0x02 to improve cryptographic method tracing and output evaluation --- .../MASTG-DEMO-0x02/MASTG-DEMO-0x02.md | 10 +- .../MASVS-STORAGE/MASTG-DEMO-0x02/evaluate.sh | 9 +- .../MASTG-DEMO-0x02/evaluation.txt | 5 + .../MASVS-STORAGE/MASTG-DEMO-0x02/hooks.json | 31 +- .../MASVS-STORAGE/MASTG-DEMO-0x02/output.json | 352 +++++++++--------- 5 files changed, 204 insertions(+), 203 deletions(-) diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/MASTG-DEMO-0x02.md b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/MASTG-DEMO-0x02.md index e8f7eebc391..952611bd86d 100644 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/MASTG-DEMO-0x02.md +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/MASTG-DEMO-0x02.md @@ -26,12 +26,11 @@ These are the relevant methods we are hooking to detect the use of File APIs to - [`Context.openFileOutput(String, int)`](https://developer.android.com/reference/android/content/Context#openFileOutput(java.lang.String,%20int)) - [`FileOutputStream.write(byte[])`](https://developer.android.com/reference/java/io/FileOutputStream#write(byte[])) -Our hooks also trace calls to cryptographic methods to help determine whether the written data is encrypted or not; whether the Android KeyStore is used; and whether Base64 encoding is used to convert binary data to strings: +Our hooks also trace calls to cryptographic methods to help determine whether the written data is encrypted or not, and whether an AndroidKeyStore-backed key is used: -- [`javax.crypto.Cipher.*(...)`](https://developer.android.com/reference/javax/crypto/Cipher) -- [`java.security.KeyStore.*(...)`](https://developer.android.com/reference/java/security/KeyStore) -- [`javax.crypto.KeyGenerator.*(...)`](https://developer.android.com/reference/javax/crypto/KeyGenerator) -- [`android.util.Base64.*(...)`](https://developer.android.com/reference/android/util/Base64) +- [`javax.crypto.Cipher.init(int, Key)`](https://developer.android.com/reference/javax/crypto/Cipher#init(int,java.security.Key)) +- [`javax.crypto.Cipher.doFinal(...)`](https://developer.android.com/reference/javax/crypto/Cipher#doFinal()) +- [`java.security.KeyStore.getKey(String, char[])`](https://developer.android.com/reference/java/security/KeyStore#getKey(java.lang.String,char[])) {{ hooks.json # run.sh }} @@ -55,5 +54,6 @@ Here we can see that: - `openFileOutput` was called with `secret_token.txt` and the subsequent `FileOutputStream.write` call writes the plaintext value `MyS3cr3tP4ssw0rd` — no preceding `Cipher` calls, so this is unencrypted. - A second `FileOutputStream.write` call writes `AKIAABCDEFGHIJKLMNOP` — also no preceding `Cipher` calls, so this is unencrypted. +- The remaining entries correspond to the PASS case: `KeyStore.getKey` retrieves the AndroidKeyStore-backed key, followed by `Cipher.init` and `Cipher.doFinal` (which shows `SensitiveDataToEncrypt` as input and returns ciphertext), and then two `FileOutputStream.write` calls — one for the IV and one for the ciphertext — writing to `encrypted_data.bin`. The data is encrypted before being stored. You can confirm the code locations responsible by reviewing the `stackTrace` of each hook entry and cross-referencing with the static counterpart @MASTG-DEMO-0x01. diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/evaluate.sh b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/evaluate.sh index 31f04e82592..e791fbc2f0b 100755 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/evaluate.sh +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/evaluate.sh @@ -2,5 +2,12 @@ jq -r ' select(.type == "hook") - | "Class: \(.class), Method: \(.method), Params: \([.inputParameters[]?.value?] | join(", "))" + | "Class: \(.class), Method: \(.method), Params: \([ + .inputParameters[]? + | if (.value | type) == "string" then .value + elif (.value | type) == "number" then (.value | tostring) + elif .value == null then "null" + else "<\(.runtimeType // "object")>" + end + ] | join(", "))" ' output.json > evaluation.txt diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/evaluation.txt b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/evaluation.txt index f473c7183f2..b82eadb8bfc 100644 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/evaluation.txt +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/evaluation.txt @@ -1,3 +1,8 @@ Class: android.content.ContextWrapper, Method: openFileOutput, Params: secret_token.txt, 0 Class: java.io.FileOutputStream, Method: write, Params: MyS3cr3tP4ssw0rd Class: java.io.FileOutputStream, Method: write, Params: AKIAABCDEFGHIJKLMNOP +Class: java.security.KeyStore, Method: getKey, Params: MastgTestKeyAlias, void +Class: javax.crypto.Cipher, Method: init, Params: 1, +Class: javax.crypto.Cipher, Method: doFinal, Params: SensitiveDataToEncrypt +Class: java.io.FileOutputStream, Method: write, Params: 0x20cf33025a0bf1ff80a01331... +Class: java.io.FileOutputStream, Method: write, Params: 0x895d0a83b9e86cc87fe121ca00cabed8f4ff5fc17863a65f6d7a613f067b905e47473f6cb64f... diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/hooks.json b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/hooks.json index 47a67fe15bc..72e9c257eda 100644 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/hooks.json +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/hooks.json @@ -19,31 +19,26 @@ }, { "class": "javax.crypto.Cipher", - "methods": [ - "getInstance", - "doFinal", - "init" - ] - }, - { - "class": "java.security.KeyStore", - "methods": [ - "setEntry", - "getEntry" + "method": "init", + "overloads": [ + { + "args": ["int", "java.security.Key"] + } ] }, { - "class": "javax.crypto.KeyGenerator", + "class": "javax.crypto.Cipher", "methods": [ - "getInstance", - "generateKey" + "doFinal" ] }, { - "class": "android.util.Base64", - "methods": [ - "encodeToString", - "decode" + "class": "java.security.KeyStore", + "method": "getKey", + "overloads": [ + { + "args": ["java.lang.String", "[C"] + } ] } ] diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/output.json b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/output.json index f8f2a28e987..dddfb85ce01 100644 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/output.json +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/output.json @@ -26,23 +26,12 @@ }, { "class": "javax.crypto.Cipher", - "method": "getInstance", + "method": "init", "overloads": [ { "args": [ - "java.lang.String" - ] - }, - { - "args": [ - "java.lang.String", - "java.lang.String" - ] - }, - { - "args": [ - "java.lang.String", - "java.security.Provider" + "int", + "java.security.Key" ] } ] @@ -97,187 +86,34 @@ } ] }, - { - "class": "javax.crypto.Cipher", - "method": "init", - "overloads": [ - { - "args": [ - "int", - "java.security.Key" - ] - }, - { - "args": [ - "int", - "java.security.Key", - "java.security.AlgorithmParameters" - ] - }, - { - "args": [ - "int", - "java.security.Key", - "java.security.AlgorithmParameters", - "java.security.SecureRandom" - ] - }, - { - "args": [ - "int", - "java.security.Key", - "java.security.SecureRandom" - ] - }, - { - "args": [ - "int", - "java.security.Key", - "java.security.spec.AlgorithmParameterSpec" - ] - }, - { - "args": [ - "int", - "java.security.Key", - "java.security.spec.AlgorithmParameterSpec", - "java.security.SecureRandom" - ] - }, - { - "args": [ - "int", - "java.security.cert.Certificate" - ] - }, - { - "args": [ - "int", - "java.security.cert.Certificate", - "java.security.SecureRandom" - ] - } - ] - }, { "class": "java.security.KeyStore", - "method": "setEntry", + "method": "getKey", "overloads": [ { "args": [ "java.lang.String", - "java.security.KeyStore$Entry", - "java.security.KeyStore$ProtectionParameter" - ] - } - ] - }, - { - "class": "java.security.KeyStore", - "method": "getEntry", - "overloads": [ - { - "args": [ - "java.lang.String", - "java.security.KeyStore$ProtectionParameter" - ] - } - ] - }, - { - "class": "javax.crypto.KeyGenerator", - "method": "getInstance", - "overloads": [ - { - "args": [ - "java.lang.String" - ] - }, - { - "args": [ - "java.lang.String", - "java.lang.String" - ] - }, - { - "args": [ - "java.lang.String", - "java.security.Provider" - ] - } - ] - }, - { - "class": "javax.crypto.KeyGenerator", - "method": "generateKey", - "overloads": [ - { - "args": [] - } - ] - }, - { - "class": "android.util.Base64", - "method": "encodeToString", - "overloads": [ - { - "args": [ - "[B", - "int" - ] - }, - { - "args": [ - "[B", - "int", - "int", - "int" - ] - } - ] - }, - { - "class": "android.util.Base64", - "method": "decode", - "overloads": [ - { - "args": [ - "java.lang.String", - "int" - ] - }, - { - "args": [ - "[B", - "int" - ] - }, - { - "args": [ - "[B", - "int", - "int", - "int" + "[C" ] } ] } ], - "totalHooks": 31, + "totalHooks": 11, "errors": [], "totalErrors": 0 } { - "id": "087df98c-1104-4cf5-a155-f69401c40039", + "id": "287ae9f9-4982-46c4-8ea3-5ac3a86f056b", "type": "hook", "category": "STORAGE", - "time": "2026-05-11T07:54:45.613Z", + "time": "2026-05-11T08:32:53.027Z", "class": "android.content.ContextWrapper", "method": "openFileOutput", "instanceId": 96242449, "stackTrace": [ "android.content.ContextWrapper.openFileOutput(Native Method)", - "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:21)", + "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:47)", "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", @@ -303,16 +139,16 @@ ] } { - "id": "c19f945a-091b-4d06-a730-58d1317ee078", + "id": "93ac0e5b-082f-48f7-ab00-39431f57f633", "type": "hook", "category": "STORAGE", - "time": "2026-05-11T07:54:45.617Z", + "time": "2026-05-11T08:32:53.031Z", "class": "java.io.FileOutputStream", "method": "write", "instanceId": 63738998, "stackTrace": [ "java.io.FileOutputStream.write(Native Method)", - "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:22)", + "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:48)", "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", @@ -332,16 +168,16 @@ ] } { - "id": "54260dcb-5b8c-45b1-978a-4e6663e6b491", + "id": "27c8643a-6a04-4cf4-9e4a-6165788cd07b", "type": "hook", "category": "STORAGE", - "time": "2026-05-11T07:54:45.620Z", + "time": "2026-05-11T08:32:53.038Z", "class": "java.io.FileOutputStream", "method": "write", "instanceId": 211507831, "stackTrace": [ "java.io.FileOutputStream.write(Native Method)", - "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:30)", + "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:56)", "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", @@ -359,4 +195,162 @@ "value": "void" } ] +} +{ + "id": "5cdfe13b-2bf4-42e2-8c4d-3f10763c9677", + "type": "hook", + "category": "STORAGE", + "time": "2026-05-11T08:32:53.048Z", + "class": "java.security.KeyStore", + "method": "getKey", + "instanceId": 46135012, + "stackTrace": [ + "java.security.KeyStore.getKey(Native Method)", + "org.owasp.mastestapp.MastgTest.getOrCreateKey(MastgTest.kt:26)", + "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:62)", + "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", + "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", + "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", + "java.lang.Thread.run(Thread.java:1012)" + ], + "inputParameters": [ + { + "declaredType": "java.lang.String", + "value": "MastgTestKeyAlias" + }, + { + "declaredType": "[C", + "value": "void" + } + ], + "returnValue": [ + { + "declaredType": "java.security.Key", + "value": {}, + "runtimeType": "android.security.keystore2.AndroidKeyStoreSecretKey", + "instanceToString": "[object Object]" + } + ] +} +{ + "id": "39c83c97-3d4c-456e-affd-29a176775231", + "type": "hook", + "category": "STORAGE", + "time": "2026-05-11T08:32:53.058Z", + "class": "javax.crypto.Cipher", + "method": "init", + "instanceId": 56984066, + "stackTrace": [ + "javax.crypto.Cipher.init(Native Method)", + "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:64)", + "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", + "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", + "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", + "java.lang.Thread.run(Thread.java:1012)" + ], + "inputParameters": [ + { + "declaredType": "int", + "value": 1 + }, + { + "declaredType": "java.security.Key", + "value": {}, + "runtimeType": "android.security.keystore2.AndroidKeyStoreSecretKey", + "instanceToString": "[object Object]" + } + ], + "returnValue": [ + { + "declaredType": "void", + "value": "void" + } + ] +} +{ + "id": "154710f1-e8aa-45ad-b56c-c30290afb008", + "type": "hook", + "category": "STORAGE", + "time": "2026-05-11T08:32:53.069Z", + "class": "javax.crypto.Cipher", + "method": "doFinal", + "instanceId": 56984066, + "stackTrace": [ + "javax.crypto.Cipher.doFinal(Native Method)", + "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:67)", + "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", + "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", + "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", + "java.lang.Thread.run(Thread.java:1012)" + ], + "inputParameters": [ + { + "declaredType": "[B", + "value": "SensitiveDataToEncrypt" + } + ], + "returnValue": [ + { + "declaredType": "[B", + "value": "0x895d0a83b9e86cc87fe121ca00cabed8f4ff5fc17863a65f6d7a613f067b905e47473f6cb64f..." + } + ] +} +{ + "id": "98626f38-39f8-489d-85d2-527ac2ab3f40", + "type": "hook", + "category": "STORAGE", + "time": "2026-05-11T08:32:53.074Z", + "class": "java.io.FileOutputStream", + "method": "write", + "instanceId": 33004880, + "stackTrace": [ + "java.io.FileOutputStream.write(Native Method)", + "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:71)", + "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", + "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", + "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", + "java.lang.Thread.run(Thread.java:1012)" + ], + "inputParameters": [ + { + "declaredType": "[B", + "value": "0x20cf33025a0bf1ff80a01331..." + } + ], + "returnValue": [ + { + "declaredType": "void", + "value": "void" + } + ] +} +{ + "id": "1c56c642-33dd-491d-b26f-09d9ea071d50", + "type": "hook", + "category": "STORAGE", + "time": "2026-05-11T08:32:53.076Z", + "class": "java.io.FileOutputStream", + "method": "write", + "instanceId": 33004880, + "stackTrace": [ + "java.io.FileOutputStream.write(Native Method)", + "org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:72)", + "org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)", + "org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)", + "org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)", + "java.lang.Thread.run(Thread.java:1012)" + ], + "inputParameters": [ + { + "declaredType": "[B", + "value": "0x895d0a83b9e86cc87fe121ca00cabed8f4ff5fc17863a65f6d7a613f067b905e47473f6cb64f..." + } + ], + "returnValue": [ + { + "declaredType": "void", + "value": "void" + } + ] } \ No newline at end of file