diff --git a/best-practices/MASTG-BEST-0025.md b/best-practices/MASTG-BEST-0025.md new file mode 100644 index 00000000000..fa840978090 --- /dev/null +++ b/best-practices/MASTG-BEST-0025.md @@ -0,0 +1,99 @@ +--- +title: Encrypt Sensitive Data in Private Storage Locations +alias: encrypt-sensitive-data +id: MASTG-BEST-0025 +platform: android +knowledge: [MASTG-KNOW-0036, MASTG-KNOW-0037, MASTG-KNOW-0038, MASTG-KNOW-0041] +--- + +Limit local storage of sensitive data to cases where it is strictly required. Android's app-private directories provide isolation but do not protect data on rooted devices, compromised devices, or during physical extraction. Therefore, sensitive data stored locally must always be **encrypted at rest**. + +## App Private Storage + +The following sandboxed locations (`/data/data//`) are private but not secure against privileged attackers: + +- `filesDir` – Internal app files. +- `noBackupDir` – Internal files excluded from Auto Backup. +- `databases` – SQLite databases. +- `shared_prefs` – SharedPreferences. + +Because sandbox isolation does not mitigate elevated access, **plaintext sensitive data must not be stored** in these locations. + +## Use Platform Cryptography + +Prefer platform-backed cryptography for local storage encryption. + +### Deprecated Jetpack Security Components + +**`EncryptedFile`** and **`EncryptedSharedPreferences`** (from `androidx.security:security-crypto`) are **deprecated** as of 1.1.0. + +#### `EncryptedFile` + +- **Deprecated:** API documentation states: "Use `java.io.File` instead." +- Uses a `MasterKey` stored in Android Keystore and enforces **AES256\_GCM\_HKDF\_4KB**. +- Must be **excluded from Auto Backup** (the restored file will not decrypt because the key will differ). +- A hidden keyset preferences file (`.../shared_prefs/__androidx_security_crypto_encrypted_file_pref__`) is auto-created and must also be **excluded from backups**. +- Decryption requires the identical master key and keyset; mismatch causes `GeneralSecurityException`. + +#### `EncryptedSharedPreferences` + +- **Deprecated:** API documentation states: "Use `android.content.SharedPreferences` instead." +- Encrypts keys (AES-SIV) and values (AES-GCM). +- Must be **excluded from Auto Backup.** +- Requires the same master key and keyset; mismatch leads to decryption failure. + +### When Using Deprecated Classes + +- Exclude encrypted files and all keyset preference files from Auto Backup. +- Reuse a single, consistent **`MasterKey`**. +- Prefer **hardware-backed Keystore keys** (TEE/StrongBox) when available. + +### For New Codebases + +Use maintained and recommended alternatives: + +- Android Keystore + manual **AES-GCM** implementation. +- **Tink**, **SQLCipher**, or equivalent well-maintained cryptographic library. + +## Envelope Encryption (Recommended) + +For databases or large files, use **envelope encryption**: + +- **DEK (Data Encryption Key)** — symmetric key (AES-GCM) used to encrypt data. +- **KEK (Key Encryption Key)** — stored securely in Android Keystore; used to encrypt the DEK. + +This approach provides efficient symmetric encryption while protecting the key material using hardware-backed Keystore, when available. + +### SQLite + +- Use **SQLCipher** for full-database AES-256 encryption. +- Protect the SQLCipher passphrase using **envelope encryption**. +- Never store database keys or passphrases in plaintext `SharedPreferences`. + +### Key–Value Data + +- Use `EncryptedSharedPreferences` only for legacy/maintenance code. +- Prefer **DataStore** + manual **AES-GCM** or **Tink** for new implementations. +- Ensure **AES-GCM** operations use unique nonces. + +## Storage Location Guidance + +| Location | Recommendation | +| :--- | :--- | +| `filesDir` | Allowed only for **encrypted data**. Private, but unsecured on rooted devices or after physical extraction. | +| `noBackupFilesDir` | Allowed for **encrypted data that must not appear in backups**. Prevents key mismatch issues after system restore. | + +### Locations to Avoid + +- **External / Shared Storage:** Prohibited. No protection against access by other apps. +- **Plaintext `SharedPreferences`:** Prohibited. Data is easily accessible if the device is compromised. +- **Plaintext Cache:** Prohibited. Lacks strong protection for sensitive data. + +> **Summary:** Use only `filesDir` and `noBackupFilesDir` for sensitive data, always encrypting it using platform cryptography or a well-maintained library. + +--- + +## Resources + +- [`EncryptedFile` (Android API Reference)](https://developer.android.com/reference/androidx/security/crypto/EncryptedFile) +- [`EncryptedSharedPreferences` (Android API Reference)](https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences) diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0068/MASTG-DEMO-0068.md b/demos/android/MASVS-STORAGE/MASTG-DEMO-0068/MASTG-DEMO-0068.md index b142f535be6..ea53f524a6f 100644 --- a/demos/android/MASVS-STORAGE/MASTG-DEMO-0068/MASTG-DEMO-0068.md +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0068/MASTG-DEMO-0068.md @@ -4,6 +4,32 @@ title: Sensitive Data in Unencrypted SQLite id: MASTG-DEMO-0068 code: [kotlin] test: MASTG-TEST-0304 -status: placeholder -note: This placeholder reserves the ID for a potential alternative or expanded demo related to unencrypted SQLite storage, linked to the MASTG-TEST-0304 test case. +status: new --- + +### Sample + +The snippet below shows sample code that uses the default SQLite API (`context.openOrCreateDatabase`) to store sensitive data, including PII and an access token, without any encryption. + +{{ MastgTest.kt }} + +### Steps + +1. Install the app on a device (@MASTG-TECH-0005) +2. Make sure you have @MASTG-TOOL-0004 installed on your machine +3. Click the **Start** button +4. Execute `run.sh`. + +The script `run.sh` pulls the database file (`PrivateUnencryptedData.db`) and queries its content: + +{{ run.sh }} + +### Observation + +The output contains the extracted content from the `Users` table, showing the sensitive PII (email address) and the access token stored in **plaintext**. + +{{ output.txt }} + +### Evaluation + +This test fails because the app uses the standard SQLite API to store sensitive data, specifically an access token (secret) and the user's email address (PII), in its sandbox without additional encryption. diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0068/MastgTest.kt b/demos/android/MASVS-STORAGE/MASTG-DEMO-0068/MastgTest.kt new file mode 100644 index 00000000000..47471746005 --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0068/MastgTest.kt @@ -0,0 +1,42 @@ +package org.owasp.mastestapp + +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.util.Log + +class MastgTest (private val context: Context){ + + + fun writeSensitiveDataToUnencryptedSQLite() { + val username = "demoUser_sqlite" + val userEmail = "john.doe@maswe-0006.com" + val accessToken = "ghp_1234567890abcdefghijklmnopqrstuvABCD" + val dbName = "PrivateUnencryptedData" + + try { + val db: SQLiteDatabase = context.openOrCreateDatabase( + dbName, + Context.MODE_PRIVATE, + null + ) + + db.execSQL("CREATE TABLE IF NOT EXISTS Users(Username VARCHAR, Email VARCHAR, Token VARCHAR);") + + + db.execSQL("DELETE FROM Users;") + + db.execSQL("INSERT INTO Users VALUES('$username', '$userEmail', '$accessToken');") + + db.close() + Log.i("MASTG-SQLITE", "Data written to unencrypted database: $dbName. Ready for ADB extraction.") + + } catch (e: Exception) { + Log.e("MASTG-SQLITE", "Error writing data to SQLite: ${e.message}") + } + } + + fun mastgTest(): String { + writeSensitiveDataToUnencryptedSQLite() + return "SQLite Demo Complete. Database 'PrivateUnencryptedData' created with PII and Token." + } +} diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0068/MastgTest_reversed.java b/demos/android/MASVS-STORAGE/MASTG-DEMO-0068/MastgTest_reversed.java new file mode 100644 index 00000000000..38c87895352 --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0068/MastgTest_reversed.java @@ -0,0 +1,32 @@ +package org.owasp.mastestapp; + +import android.content.Context; +import android.util.Log; +import kotlin.Metadata; +import kotlin.jvm.internal.Intrinsics; + +/* compiled from: MastgTest.kt */ +@Metadata(d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u000e\n\u0000\b\u0007\u0018\u00002\u00020\u0001B\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\u0006\u0010\u0006\u001a\u00020\u0007R\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000¨\u0006\b"}, d2 = {"Lorg/owasp/mastestapp/MastgTest;", "", "context", "Landroid/content/Context;", "", "(Landroid/content/Context;)V", "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 Context context; + + public MastgTest(Context context) { + Intrinsics.checkNotNullParameter(context, "context"); + this.context = context; + } + + public final String mastgTest() throws Exception { + DemoResults r = new DemoResults("0000"); + try { + Log.d("MASTG-TEST", "Hello from the OWASP MASTG Test app."); + r.add(Status.PASS, "The app implemented a demo which passed the test with the following value: 'Hello from the OWASP MASTG Test app.'"); + r.add(Status.FAIL, "The app implemented a demo which failed the test."); + throw new Exception("Example exception: Method not implemented."); + } catch (Exception e) { + r.add(Status.ERROR, e.toString()); + return r.toJson(); + } + } +} diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0068/output.txt b/demos/android/MASVS-STORAGE/MASTG-DEMO-0068/output.txt new file mode 100644 index 00000000000..ba189e00a40 --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0068/output.txt @@ -0,0 +1,2 @@ +--- Users Table Content --- +demoUser_sqlite|john.doe@maswe-0006.com|MASTG_ACCESS_TOKEN_B4C8D2F0E7A diff --git a/demos/android/MASVS-STORAGE/MASTG-DEMO-0068/run.sh b/demos/android/MASVS-STORAGE/MASTG-DEMO-0068/run.sh new file mode 100755 index 00000000000..f0eb56604fb --- /dev/null +++ b/demos/android/MASVS-STORAGE/MASTG-DEMO-0068/run.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +PACKAGE="org.owasp.mastestapp" +DB_NAME="PrivateUnencryptedData" +DB_PATH="/data/data/$PACKAGE/databases/$DB_NAME" +SDCARD_PULL_PATH="/sdcard/$DB_NAME.db" +OUTPUT_DB_FILE="${DB_NAME}.db" +OUTPUT_TXT_FILE="output.txt" + +# Copy Database to the sdcard +if ! adb shell "run-as $PACKAGE cp databases/$DB_NAME $SDCARD_PULL_PATH"; then + echo "run-as failed, trying su" + adb shell "su 0 sh -c 'cp $DB_PATH $SDCARD_PULL_PATH'" +fi + +# Pull the file to the host machine +adb pull "$SDCARD_PULL_PATH" . + +echo "--- Users Table Content ---" > $OUTPUT_TXT_FILE +sqlite3 "$OUTPUT_DB_FILE" "SELECT * FROM Users;" >> $OUTPUT_TXT_FILE + +adb shell "rm $SDCARD_PULL_PATH" 2>/dev/null +rm $OUTPUT_DB_FILE 2>/dev/null diff --git a/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0304.md b/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0304.md index b4c4a9c6352..d8c3c2b58a1 100644 --- a/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0304.md +++ b/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0304.md @@ -1,11 +1,37 @@ --- -title: Sensitive Data Stored Unencrypted via SQLite platform: android +title: Static Analysis for Unencrypted Sensitive Data in SQLite id: MASTG-TEST-0304 -type: [static, dynamic] +type: [static] weakness: MASWE-0006 -best-practices: [] -profiles: [L1, L2] -status: placeholder -note: This test checks if the app uses the default SQLite API (e.g., `SQLiteOpenHelper`, `context.openOrCreateDatabase`) to store sensitive data (e.g., tokens, PII) in an unencrypted database file within the app's sandbox. It confirms the absence of secure alternatives like SQLCipher or encrypted databases. +best-practices: [MASTG-BEST-0025] +profiles: [L2] +status: new --- + +## Overview + +This test verifies whether the app's code uses SQLite APIs to store sensitive data — such as tokens, credentials, or PII — without encryption. By default, SQLite databases created using `SQLiteOpenHelper` or `context.openOrCreateDatabase` are not encrypted. + +## Steps + +1. Obtain the application package (APK) using @MASTG-TECH-0003. + +2. Use static analysis (@MASTG-TECH-0014) to review code for calls to SQLite APIs, including: + - `SQLiteOpenHelper` + - `context.openOrCreateDatabase` + - `SQLiteDatabase#insert`, `query`, `execSQL`, etc. + +3. Check database creation and insertion logic to determine if: + - the database is created using default SQLite, + - no encryption mechanism (SQLCipher or equivalent) is applied, + - sensitive fields (tokens, secrets, PII) are inserted in plaintext. + +## Observation + +- Identify SQLite databases created in the code. +- Determine whether sensitive data is inserted without encryption. + +## Evaluation + +The test fails if the app references SQLite APIs and stores sensitive data without any encryption. diff --git a/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0307.md b/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0307.md new file mode 100644 index 00000000000..df971de65bc --- /dev/null +++ b/tests-beta/android/MASVS-STORAGE/MASTG-TEST-0307.md @@ -0,0 +1,35 @@ +--- +platform: android +title: Runtime Verification of Sensitive Data Stored Unencrypted in SQLite +id: MASTG-TEST-0307 +type: [dynamic, filesystem] +weakness: MASWE-0006 +best-practices: [MASTG-BEST-0025] +profiles: [L2] +status: new +--- + +## Overview + +This test checks at runtime whether sensitive data — tokens, credentials, or PII — is stored in SQLite databases in plaintext. The goal is to verify that data stored in the app's private storage is not left unencrypted. + +## Steps + +1. Install and run the app on a rooted or emulated device (@MASTG-TECH-0005). + +2. Trigger app functionality that processes or stores sensitive data. + +3. Access the app's private storage to locate SQLite database files (e.g., `.db`, `.sqlite`, `.sqlite3`) (@MASTG-TECH-0008). + +4. Extract the database files to the host machine using @MASTG-TECH-0003. + +5. Inspect the database content using dynamic analysis techniques (@MASTG-TECH-0015) to determine if sensitive data is stored in plaintext. + +## Observation + +- Which SQLite databases exist on the device. +- Whether sensitive data (tokens, secrets, PII) is present in plaintext. + +## Evaluation + +The test fails if sensitive data is stored in SQLite without encryption and can be read in plaintext through static or dynamic analysis.