diff --git a/best-practices/MASTG-BEST-0x01.md b/best-practices/MASTG-BEST-0x01.md new file mode 100644 index 00000000000..4be3379fc4a --- /dev/null +++ b/best-practices/MASTG-BEST-0x01.md @@ -0,0 +1,91 @@ +--- +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-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. + +```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. + +## 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`. 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..89a0bdd42c5 --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MASTG-DEMO-0x01.md @@ -0,0 +1,45 @@ +--- +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 +tools: [MASTG-TOOL-0110] +--- + +## Sample + +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`. +- An encrypted secret is stored using `FileOutputStream` after AES/GCM encryption with an AndroidKeyStore-backed key. + +{{ MastgTest.kt # MastgTest_reversed.java }} + +## Steps + +Let's run our @MASTG-TOOL-0110 rule against the sample code. + +{{ ../../../../rules/mastg-android-unencrypted-internal-file-storage.yml }} + +{{ run.sh }} + +## Observation + +The rule has identified 3 locations that indicate use of File APIs to write data to internal storage. + +{{ output.txt }} + +## Evaluation + +The test fails because the app uses File APIs to write sensitive data to internal storage without encryption. + +After reviewing the decompiled code at the locations specified in the output: + +- 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 new file mode 100644 index 00000000000..ea8f7c94a22 --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest.kt @@ -0,0 +1,82 @@ +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), 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 { + var result = "" + + // 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] 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()) + 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" + + // 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 new file mode 100644 index 00000000000..4c080cbf2df --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/MastgTest_reversed.java @@ -0,0 +1,127 @@ +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\"\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) { + Intrinsics.checkNotNullParameter(context, "context"); + this.context = context; + this.password = "MyS3cr3tP4ssw0rd"; + this.apiKey = "AKIAABCDEFGHIJKLMNOP"; + this.encryptedSecret = "SensitiveDataToEncrypt"; + this.keyAlias = "MastgTestKeyAlias"; + } + + 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 { + 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); + 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 { + try { + throw th; + } finally { + } + } + } catch (IOException e) { + 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 new file mode 100644 index 00000000000..09c9d299acf --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/output.txt @@ -0,0 +1,24 @@ + + +┌─────────────────┐ +│ 3 Code Findings │ +└─────────────────┘ + + MastgTest_reversed.java + ❯❱ rules.mastg-android-unencrypted-internal-file-storage-openfileoutput + [MASVS-STORAGE] Verify that any sensitive data written via openFileOutput is encrypted before + storage + + 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 + + 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); + 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..2e679e0498b --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x01/run.sh @@ -0,0 +1,2 @@ +#!/bin/bash +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..952611bd86d --- /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, and whether an AndroidKeyStore-backed key is used: + +- [`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 }} + +## 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. +- 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 new file mode 100755 index 00000000000..e791fbc2f0b --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/evaluate.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +jq -r ' + select(.type == "hook") + | "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 new file mode 100644 index 00000000000..b82eadb8bfc --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/evaluation.txt @@ -0,0 +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 new file mode 100644 index 00000000000..72e9c257eda --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/hooks.json @@ -0,0 +1,45 @@ +{ + "category": "STORAGE", + "hooks": [ + { + "class": "android.content.ContextWrapper", + "methods": [ + "openFileOutput" + ] + }, + { + "class": "java.io.FileOutputStream", + "method": "write", + "overloads": [ + { + "args": ["[B"] + } + ], + "filterEventsByStacktrace": ["org.owasp.mastestapp"] + }, + { + "class": "javax.crypto.Cipher", + "method": "init", + "overloads": [ + { + "args": ["int", "java.security.Key"] + } + ] + }, + { + "class": "javax.crypto.Cipher", + "methods": [ + "doFinal" + ] + }, + { + "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 new file mode 100644 index 00000000000..dddfb85ce01 --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0x02/output.json @@ -0,0 +1,356 @@ +{ + "type": "summary", + "hooks": [ + { + "class": "android.content.ContextWrapper", + "method": "openFileOutput", + "overloads": [ + { + "args": [ + "java.lang.String", + "int" + ] + } + ] + }, + { + "class": "java.io.FileOutputStream", + "method": "write", + "overloads": [ + { + "args": [ + "[B" + ] + } + ] + }, + { + "class": "javax.crypto.Cipher", + "method": "init", + "overloads": [ + { + "args": [ + "int", + "java.security.Key" + ] + } + ] + }, + { + "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": "java.security.KeyStore", + "method": "getKey", + "overloads": [ + { + "args": [ + "java.lang.String", + "[C" + ] + } + ] + } + ], + "totalHooks": 11, + "errors": [], + "totalErrors": 0 +} +{ + "id": "287ae9f9-4982-46c4-8ea3-5ac3a86f056b", + "type": "hook", + "category": "STORAGE", + "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: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)", + "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": "93ac0e5b-082f-48f7-ab00-39431f57f633", + "type": "hook", + "category": "STORAGE", + "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: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)", + "java.lang.Thread.run(Thread.java:1012)" + ], + "inputParameters": [ + { + "declaredType": "[B", + "value": "MyS3cr3tP4ssw0rd" + } + ], + "returnValue": [ + { + "declaredType": "void", + "value": "void" + } + ] +} +{ + "id": "27c8643a-6a04-4cf4-9e4a-6165788cd07b", + "type": "hook", + "category": "STORAGE", + "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: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)", + "java.lang.Thread.run(Thread.java:1012)" + ], + "inputParameters": [ + { + "declaredType": "[B", + "value": "AKIAABCDEFGHIJKLMNOP" + } + ], + "returnValue": [ + { + "declaredType": "void", + "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 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/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..3622a539f16 --- /dev/null +++ b/knowledge/android/MASVS-STORAGE/MASTG-KNOW-0x01.md @@ -0,0 +1,133 @@ +--- +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") +``` + +## 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); + } +} +``` 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..8147a8f24d8 --- /dev/null +++ b/rules/mastg-android-unencrypted-internal-file-storage.yml @@ -0,0 +1,60 @@ +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(), ...)) + - 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: + - 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(), ...)) + - 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); 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..b0cffc7775b --- /dev/null +++ b/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0x01.md @@ -0,0 +1,32 @@ +--- +platform: android +title: References to File APIs for Writing Data Unencrypted to the App Sandbox +id: MASTG-TEST-0x01 +type: [static] +weakness: MASWE-0006 +best-practices: [MASTG-BEST-0x01] +profiles: [L1, L2] +knowledge: [MASTG-KNOW-0x01, MASTG-KNOW-0041] +--- + +## Overview + +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. + +## Steps + +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 File APIs that write data to internal storage. + +## Observation + +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 + +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` 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..909a769a45b --- /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-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 (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 + +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. 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 ---