Unprotected Storage of Credentials

ID

kotlin.android_unprotected_storage_of_credentials

Severity

low

Resource

Misconfiguration

Language

Kotlin

Tags

CWE:256, CWE:312, CWE:522, MASVS:MSTG-STORAGE-1, MASVS:MSTG-STORAGE-2, OWASP:2021:A02, OWASP:2024:M9, PCI-DSS:6.5.3, android, credentials, plaintext-storage

Description

Storing sensitive credentials (passwords, API keys, tokens, secrets) in plaintext exposes them to attackers through physical device access, malicious applications, device backups, system logs, and forensic analysis. Android provides secure storage mechanisms that encrypt data using hardware-backed keys.

Rationale

Unencrypted credential storage, such as plaintext storage of a password enables:

  • Account compromise through extracted passwords

  • API abuse via stolen access tokens

  • Session hijacking using captured session IDs

  • Cryptographic key theft allowing data decryption

Storing sensitive data without adequate protection is also checked. Sensitive data may persist in multiple locations:

  • SharedPreferences XML files (/data/data/<package>/shared_prefs/)

  • SQLite databases (/data/data/<package>/databases/)

  • Internal/external file storage

  • Device backups (cloud or local)

Even with MODE_PRIVATE, standard Android storage is vulnerable on:

  • Rooted devices (root access bypasses permission model)

  • Devices with unlocked bootloaders

  • Malicious apps exploiting vulnerabilities

  • Physical device access (forensic tools)

This detector identifies plaintext credential storage across multiple mechanisms. Some patterns of vulnerable code follow:

Unencrypted SharedPreferences

Standard SharedPreferences stores data in plaintext XML, at /data/data/{app package}/shared_prefs/user_prefs.xml:

// VULNERABLE: Plaintext SharedPreferences
val sharedPrefs = getSharedPreferences("user_prefs", Context.MODE_PRIVATE)
sharedPrefs.edit()
    .putString("password", userPassword)// FLAW
    .putString("api_token", apiToken)// FLAW
    .apply()

which ends up in cleartext:

<map>
    <string name="password">MySecretPass123</string>
    <string name="api_token">sk_live_1234567890</string>
</map>

Insecure File Storage

Writing credentials to files without encryption:

// VULNERABLE: FileOutputStream with plaintext credentials
val fos = openFileOutput("credentials.txt", Context.MODE_PRIVATE)
fos.write("password=$password".toByteArray())// FLAW
fos.close()

// VULNERABLE: External storage (accessible by all apps)
val file = File(Environment.getExternalStorageDirectory(), "config.txt")
file.writeText("apiKey=$apiKey")// FLAW

Plaintext Database Storage

SQLite databases without encryption:

// VULNERABLE: Unencrypted SQLite
val db = openOrCreateDatabase("user.db", MODE_PRIVATE, null)
db.execSQL(// FLAW
    "INSERT INTO users VALUES ('$username', '$password', '$apiToken')"
)

Database stored at: /data/data/com.example.app/databases/user.db (plaintext)

Remediation

  1. Use EncryptedSharedPreferences

    Encrypt SharedPreferences with AES256-GCM:

    // SECURE: EncryptedSharedPreferences
    import androidx.security.crypto.EncryptedSharedPreferences
    import androidx.security.crypto.MasterKey
    
    val masterKey = MasterKey.Builder(context)
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build()
    
    val encryptedPrefs = EncryptedSharedPreferences.create(
        context,
        "secret_prefs",
        masterKey,
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
    )
    
    // Keys and values are encrypted automatically
    encryptedPrefs.edit()
        .putString("password", userPassword)
        .putString("api_token", apiToken)
        .apply()

    Library: androidx.security:security-crypto:1.1.0-alpha06

  2. Use EncryptedFile for File Storage

    // SECURE: EncryptedFile
    import androidx.security.crypto.EncryptedFile
    
    val encryptedFile = EncryptedFile.Builder(
        context,
        File(context.filesDir, "credentials.enc"),
        masterKey,
        EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
    ).build()
    
    // Write encrypted data
    encryptedFile.openFileOutput().use {
        it.write(sensitiveData.toByteArray())
    }
  3. Use SQLCipher for Database Encryption

    // SECURE: SQLCipher
    import net.sqlcipher.database.SQLiteDatabase
    
    SQLiteDatabase.loadLibs(context)
    val password = "strong_db_password"
    val db = SQLiteDatabase.openOrCreateDatabase(
        databaseFile,
        password,
        null
    )
    
    // All database content is encrypted
    db.execSQL("INSERT INTO users VALUES (?, ?, ?)", arrayOf(username, password, token))

    Library: net.zetetic:android-database-sqlcipher:4.5.4

  4. Use Android Keystore for Cryptographic Keys

    // SECURE: Android Keystore (hardware-backed)
    val keyStore = KeyStore.getInstance("AndroidKeyStore")
    keyStore.load(null)
    
    val keyGenerator = KeyGenerator.getInstance(
        KeyProperties.KEY_ALGORITHM_AES,
        "AndroidKeyStore"
    )
    
    keyGenerator.init(
        KeyGenParameterSpec.Builder(
            "myKey",
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
        )
        .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
        .setUserAuthenticationRequired(true)  // Requires biometric/PIN
        .build()
    )
    
    val secretKey = keyGenerator.generateKey()
  5. Alternative: Hash Passwords (for local verification)

    // SECURE: Store only password hashes
    import java.security.MessageDigest
    
    fun hashPassword(password: String, salt: ByteArray): String {
        val digest = MessageDigest.getInstance("SHA-256")
        val saltedPassword = password.toByteArray() + salt
        return digest.digest(saltedPassword).joinToString("") { "%02x".format(it) }
    }
    
    // Store hash instead of plaintext
    val salt = generateSecureRandomSalt()
    val passwordHash = hashPassword(userPassword, salt)
    sharedPrefs.edit()
        .putString("password_hash", passwordHash)
        .putString("salt", salt.toBase64())
        .apply()

References