Logs seguros — lo más olvidado

Los logs son visibles con adb logcat en cualquier dispositivo conectado a una computadora. En builds de debug esto es útil — en producción es un vector de fuga de información.

// MAL — visible en producción con adb logcat
Log.d("Auth", "Token recibido: $token")
Log.d("User", "Email: ${usuario.email}, Password: ${usuario.password}")

// BIEN — usar un wrapper que deshabilita en release
object Timber {
    fun d(mensaje: String) {
        if (BuildConfig.DEBUG) Log.d("App", mensaje)
    }
}

// O usar Timber (la librería estándar para esto):
// En debug: planta DebugTree (logs visibles)
// En release: no plantar nada, o plantar CrashReportingTree solo para errores
class MiApp : Application() {
    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) {
            Timber.plant(Timber.DebugTree())
        } else {
            Timber.plant(CrashReportingTree())  // solo errores a Crashlytics
        }
    }
}

class CrashReportingTree : Timber.Tree() {
    override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
        if (priority == Log.VERBOSE || priority == Log.DEBUG) return
        Firebase.crashlytics.log("$tag: $message")
        t?.let { Firebase.crashlytics.recordException(it) }
    }
}

Detección de debugger adjunto

// Detectar si hay un debugger conectado
fun hayDebuggerConectado(): Boolean {
    return Debug.isDebuggerConnected() || Debug.waitingForDebugger()
}

// Detectar si la app fue firmada con un keystore de debug
fun esFirmadaConDebugKey(context: Context): Boolean {
    return try {
        val signature = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            context.packageManager.getPackageInfo(
                context.packageName,
                PackageManager.GET_SIGNING_CERTIFICATES
            ).signingInfo.apkContentsSigners[0]
        } else {
            @Suppress("DEPRECATION")
            context.packageManager.getPackageInfo(
                context.packageName,
                PackageManager.GET_SIGNATURES
            ).signatures[0]
        }
        // El debug keystore de Android Studio tiene un SHA-1 conocido
        val md = MessageDigest.getInstance("SHA-1")
        val hash = md.digest(signature.toByteArray())
            .joinToString("") { "%02X".format(it) }
        // "C8:A2:..."  ← el SHA-1 del debug keystore por defecto
        hash.contains("C8A2") // simplificado — verificar el hash completo
    } catch (e: Exception) { false }
}

// Verificación al iniciar la app (solo en release)
if (!BuildConfig.DEBUG) {
    if (hayDebuggerConectado()) {
        // Un debugger en una build de release es sospechoso
        finishAffinity()  // cerrar la app
        return
    }
}

Bloquear capturas de pantalla

En pantallas con información sensible (balance bancario, documentos privados), podés impedir que se tomen capturas de pantalla:

// Bloquear screenshots en una Activity específica
class PantallaSeguraActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // FLAG_SECURE impide:
        // - Capturas de pantalla del usuario
        // - Aparición en el selector de apps recientes (muestra pantalla en blanco)
        // - Grabación de pantalla
        window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
        setContentView(binding.root)
    }
}

// O habilitarlo/deshabilitarlo dinámicamente:
fun bloquearPantalla(bloquear: Boolean) {
    if (bloquear) {
        window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
    } else {
        window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
    }
}

// En Compose:
@Composable
fun PantallaSegura() {
    val activity = LocalContext.current as Activity
    DisposableEffect(Unit) {
        activity.window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
        onDispose {
            activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
        }
    }
    // contenido de la pantalla...
}

Portapapeles seguro

Los datos copiados al portapapeles son accesibles por otras apps (hasta Android 10) y pueden persistir. Para campos de contraseña o datos sensibles:

// Limpiar el portapapeles después de un tiempo
fun copiarDatoSensible(context: Context, texto: String) {
    val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
    clipboard.setPrimaryClip(ClipData.newPlainText("", texto))

    // Limpiar automáticamente después de 30 segundos
    Handler(Looper.getMainLooper()).postDelayed({
        clipboard.clearPrimaryClip()
    }, 30_000)
}

// Marcar el campo como sensitivo para que Android no lo sugiera
// en autofill ni lo muestre en la UI de portapapeles:
binding.etPassword.apply {
    setTextIsSelectable(false)
    importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO
    // En XML: android:importantForAutofill="no"
}

Componentes exported — superficie de ataque

<!-- Revisar TODOS los componentes del Manifest -->

<!-- MAL: Activity exportada sin restricción -->
<activity android:name=".AdminActivity" android:exported="true" />

<!-- BIEN: Solo accesible desde tu propia firma -->
<activity
    android:name=".AdminActivity"
    android:exported="true"
    android:permission="ar.pensa.miapp.ADMIN_ACCESS" />

<!-- BIEN: Solo accesible desde dentro de la propia app -->
<activity android:name=".AdminActivity" android:exported="false" />

<!-- Content Providers — especialmente peligrosos -->
<provider
    android:name=".MiProvider"
    android:exported="false"  <!-- nunca exportar a menos que sea necesario -->
    android:grantUriPermissions="false" />

<!-- Proteger BroadcastReceivers de broadcasts externos -->
<receiver
    android:name=".MiReceiver"
    android:exported="false" />

Checklist completo de seguridad Android

# ── ALMACENAMIENTO ──────────────────────────────────────────
# ✓ No hay datos sensibles en SharedPreferences sin cifrar
# ✓ Tokens, claves y contraseñas usan EncryptedSharedPreferences
# ✓ Archivos sensibles usan EncryptedFile o cifrado con Keystore
# ✓ No se usa getExternalStorageDirectory() para datos privados
# ✓ android:allowBackup configurado correctamente (datos sensibles excluidos)

# ── RED ─────────────────────────────────────────────────────
# ✓ cleartextTrafficPermitted="false" en Network Security Config
# ✓ Respuestas HTTP se validan antes de procesar
# ✓ Certificate pinning en dominios críticos (con pin de backup)
# ✓ Tokens de autenticación en headers, no en query params ni URLs

# ── CÓDIGO ──────────────────────────────────────────────────
# ✓ minifyEnabled = true en release
# ✓ No hay claves API hardcodeadas en el código fuente
# ✓ No hay URLs de staging/debug en el build de release
# ✓ Logs deshabilitados o controlados en release (Timber sin DebugTree)
# ✓ Strings sensibles en BuildConfig (ofuscados por R8)

# ── MANIFEST ─────────────────────────────────────────────────
# ✓ android:exported declarado explícitamente en todos los componentes
# ✓ Activities, Services, Receivers y Providers con el menor exported posible
# ✓ Permisos declarados son los mínimos necesarios
# ✓ android:debuggable="false" (no ponerlo, por default es false en release)

# ── RUNTIME ──────────────────────────────────────────────────
# ✓ FLAG_SECURE en pantallas con datos sensibles
# ✓ No se loggea información sensible (emails, tokens, contraseñas)
# ✓ Detección de root/emulador en apps enterprise
# ✓ Play Integrity API para verificación de dispositivo en backend
# ✓ Manejo seguro del portapapeles en campos sensibles

# ── SERVIDOR (complementario) ────────────────────────────────
# ✓ Tokens con expiración corta (access token) + refresh token
# ✓ Rate limiting en endpoints de autenticación
# ✓ Verificación de Play Integrity en operaciones críticas
# ✓ Logs de seguridad en el backend (intentos fallidos, IPs sospechosas)