Mejora la seguridad de tu app

Si aumentas la seguridad de tu app, ayudas a preservar la confianza de los usuarios y la integridad del dispositivo.

En esta página, se muestran varias prácticas recomendadas que tienen un impacto positivo en la seguridad de tu app.

Practica la comunicación segura

Cuando proteges los datos que intercambia tu app con otras apps o con un sitio web, mejoras la estabilidad de la app y proteges los datos que envías y recibes.

Protege la comunicación entre apps

Para comunicarte de forma más segura entre apps, usa intents implícitos con un selector de apps, permisos basados en firmas y proveedores de contenido no exportado.

Muestra un selector de apps

Si un intent implícito puede iniciar al menos dos apps posibles en el dispositivo de un usuario, muestra explícitamente un selector de apps. Esta estrategia de interacción permite a los usuarios transferir información sensible a una app de confianza.

Kotlin

val intent = Intent(Intent.ACTION_SEND)
val possibleActivitiesList: List<ResolveInfo> =
        packageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL)

// Verify that an activity in at least two apps on the user's device
// can handle the intent. Otherwise, start the intent only if an app
// on the user's device can handle the intent.
if (possibleActivitiesList.size > 1) {

    // Create intent to show chooser.
    // Title is something similar to "Share this photo with."

    val chooser = resources.getString(R.string.chooser_title).let { title ->
        Intent.createChooser(intent, title)
    }
    startActivity(chooser)
} else if (intent.resolveActivity(packageManager) != null) {
    startActivity(intent)
}

Java

Intent intent = new Intent(Intent.ACTION_SEND);
List<ResolveInfo> possibleActivitiesList = getPackageManager()
        .queryIntentActivities(intent, PackageManager.MATCH_ALL);

// Verify that an activity in at least two apps on the user's device
// can handle the intent. Otherwise, start the intent only if an app
// on the user's device can handle the intent.
if (possibleActivitiesList.size() > 1) {

    // Create intent to show chooser.
    // Title is something similar to "Share this photo with."

    String title = getResources().getString(R.string.chooser_title);
    Intent chooser = Intent.createChooser(intent, title);
    startActivity(chooser);
} else if (intent.resolveActivity(getPackageManager()) != null) {
    startActivity(intent);
}

Información relacionada:

Aplica permisos basados en firmas

Cuando compartes datos entre dos apps que controlas o posees, usa permisos basados en firmas. Estos permisos no requieren la confirmación del usuario y, en cambio, comprueban que la app que accede a los datos esté firmada con la misma clave. Por lo tanto, estos permisos ofrecen una experiencia del usuario más segura y simplificada.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp">
    <permission android:name="my_custom_permission_name"
                android:protectionLevel="signature" />

Información relacionada:

Inhabilita el acceso a los proveedores de contenido de tu app

A menos que pretendas enviar datos desde tu app a una diferente que no te pertenece, impide explícitamente que las apps de otros desarrolladores accedan a los objetos ContentProvider de tu app. Esta opción de configuración resulta particularmente importante si se puede instalar tu app en dispositivos que ejecutan Android 4.1.1 (nivel de API 16) o versiones anteriores, ya que el atributo android:exported del elemento <provider> es true de forma predeterminada en esas versiones de Android.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp">
    <application ... >
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.example.myapp.fileprovider"
            ...
            android:exported="false">
            <!-- Place child elements of <provider> here. -->
        </provider>
        ...
    </application>
</manifest>

Solicita credenciales antes de mostrar información sensible

Cuando solicitas credenciales a los usuarios para que puedan acceder a información sensible o contenido premium de tu app, puedes solicitar un PIN, una contraseña, un patrón o una credencial biométrica, como el reconocimiento facial o de huella dactilar.

Para obtener más información acerca de cómo solicitar credenciales biométricas, consulta la guía acerca de la autenticación biométrica.

Aplica medidas de seguridad de red

En las siguientes secciones, se describe cómo puedes mejorar la seguridad de red de tu app.

Usa tráfico TLS

Si tu app se comunica con un servidor web que tiene un certificado emitido por una autoridad certificadora (CA) reconocida y de confianza, usa una solicitud HTTPS similar a la siguiente:

Kotlin

val url = URL("https://www.google.com")
val urlConnection = url.openConnection() as HttpsURLConnection
urlConnection.connect()
urlConnection.inputStream.use {
    ...
}

Java

URL url = new URL("https://www.google.com");
HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
urlConnection.connect();
InputStream in = urlConnection.getInputStream();

Agrega una configuración de seguridad de red

Si tu app usa CA nuevos o personalizados, puedes declarar la configuración de seguridad de tu red en un archivo de configuración. Este proceso te permite crear una configuración sin modificar ningún código de app.

Para agregar un archivo de configuración de seguridad de red a tu app, sigue estos pasos:

  1. Declara la configuración en el manifiesto de tu app:
  2. <manifest ... >
        <application
            android:networkSecurityConfig="@xml/network_security_config"
            ... >
            <!-- Place child elements of <application> element here. -->
        </application>
    </manifest>
  3. Agrega un archivo de recursos XML ubicado en res/xml/network_security_config.xml.

    Especifica que todo el tráfico hacia dominios en particular debería usar HTTPS. Para ello, inhabilita el borrado de texto:

    <network-security-config>
        <domain-config cleartextTrafficPermitted="false">
            <domain includeSubdomains="true">secure.example.com</domain>
            ...
        </domain-config>
    </network-security-config>

    Durante el proceso de desarrollo, puedes usar el elemento <debug-overrides> para permitir, de forma explícita, los certificados instalados por el usuario. Este elemento anula las opciones importantes de seguridad de tu app durante la depuración y las pruebas sin afectar la configuración de lanzamiento de la app. En el siguiente fragmento, se muestra cómo definir este elemento en tu archivo en formato XML de configuración de seguridad de red de tu app:

    <network-security-config>
        <debug-overrides>
            <trust-anchors>
                <certificates src="user" />
            </trust-anchors>
        </debug-overrides>
    </network-security-config>

Información relacionada: Configuración de seguridad de red

Crea tu propio administrador de confianza

Tu verificador de TLS no debería aceptar todos los certificados. Es posible que tengas que configurar un administrador de confianza y manejar todas las advertencias de TLS que se produzcan si alguna de las siguientes condiciones se aplica a tu caso de uso:

  • Te comunicas con un servidor web que tiene un certificado firmado por un CA nuevo o personalizado.
  • No todos los dispositivos que usas confían en el CA.
  • No puedes usar una configuración de seguridad de red.

Para obtener más información acerca de cómo completar estos pasos, consulta la explicación de cómo procesar una autoridad de certificados desconocida.

Información relacionada:

Usa objetos WebView cuidadosamente

Los objetos WebView de tu app no deberían permitir que los usuarios naveguen en sitios que se encuentran fuera de tu control. Siempre que sea posible, usa una lista de entidades permitidas para restringir el contenido que cargan los objetos WebView de tu app.

Además, nunca habilites la compatibilidad con la interfaz de JavaScript, a menos que controles por completo el contenido de los objetos WebView de tu app y confíes en este.

Usa canales de mensajes HTML

Si tu app usa compatibilidad con la interfaz de JavaScript en dispositivos que ejecutan Android 6.0 (API nivel 23) y versiones posteriores, usa los canales de mensajes HTML en lugar de establecer una comunicación entre un sitio web y tu app, como se muestra en el siguiente fragmento de código:

Kotlin

val myWebView: WebView = findViewById(R.id.webview)

// channel[0] and channel[1] represent the two ports.
// They are already entangled with each other and have been started.
val channel: Array<out WebMessagePort> = myWebView.createWebMessageChannel()

// Create handler for channel[0] to receive messages.
channel[0].setWebMessageCallback(object : WebMessagePort.WebMessageCallback() {

    override fun onMessage(port: WebMessagePort, message: WebMessage) {
        Log.d(TAG, "On port $port, received this message: $message")
    }
})

// Send a message from channel[1] to channel[0].
channel[1].postMessage(WebMessage("My secure message"))

Java

WebView myWebView = (WebView) findViewById(R.id.webview);

// channel[0] and channel[1] represent the two ports.
// They are already entangled with each other and have been started.
WebMessagePort[] channel = myWebView.createWebMessageChannel();

// Create handler for channel[0] to receive messages.
channel[0].setWebMessageCallback(new WebMessagePort.WebMessageCallback() {
    @Override
    public void onMessage(WebMessagePort port, WebMessage message) {
         Log.d(TAG, "On port " + port + ", received this message: " + message);
    }
});

// Send a message from channel[1] to channel[0].
channel[1].postMessage(new WebMessage("My secure message"));

Información relacionada:

Proporciona los permisos adecuados

Solo solicita la cantidad mínima de permisos necesarios para que tu app funcione adecuadamente. Cuando sea posible, renuncia a los permisos cuando tu app ya no los necesite.

Usa intents para diferir permisos

Siempre que sea posible, no agregues un permiso a tu app para completar una acción que puede completarse en otra app. En cambio, usa un intent a fin de diferir la solicitud a una app diferente que ya tiene el permiso necesario.

En el siguiente ejemplo, se muestra cómo usar un intent para dirigir a los usuarios a una app de contactos, en lugar de solicitar los permisos READ_CONTACTS y WRITE_CONTACTS:

Kotlin

// Delegates the responsibility of creating the contact to a contacts app,
// which has already been granted the appropriate WRITE_CONTACTS permission.
Intent(Intent.ACTION_INSERT).apply {
    type = ContactsContract.Contacts.CONTENT_TYPE
}.also { intent ->
    // Make sure that the user has a contacts app installed on their device.
    intent.resolveActivity(packageManager)?.run {
        startActivity(intent)
    }
}

Java

// Delegates the responsibility of creating the contact to a contacts app,
// which has already been granted the appropriate WRITE_CONTACTS permission.
Intent insertContactIntent = new Intent(Intent.ACTION_INSERT);
insertContactIntent.setType(ContactsContract.Contacts.CONTENT_TYPE);

// Make sure that the user has a contacts app installed on their device.
if (insertContactIntent.resolveActivity(getPackageManager()) != null) {
    startActivity(insertContactIntent);
}

Además, si tu app necesita realizar procesos de I/O basados en archivos, como acceder al almacenamiento o elegir un archivo, no se necesita un permiso especial, ya que el sistema puede completar las operaciones en nombre de tu app. Mejor aún, después de que el usuario selecciona el contenido en un URI en particular, la app que realiza la llamada obtiene el permiso para el recurso seleccionado.

Información relacionada:

Comparte datos de manera segura entre apps

Sigue estas prácticas recomendadas para compartir el contenido de tu app con otras apps de manera más segura:

En el siguiente fragmento de código, se muestra cómo usar las marcas para otorgar permisos de URI y permisos de proveedores de contenido a fin de mostrar el archivo PDF de la app en una app de visualización de PDF independiente:

Kotlin

// Create an Intent to launch a PDF viewer for a file owned by this app.
Intent(Intent.ACTION_VIEW).apply {
    data = Uri.parse("content://com.example/personal-info.pdf")

    // This flag gives the started app read access to the file.
    addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}.also { intent ->
    // Make sure that the user has a PDF viewer app installed on their device.
    intent.resolveActivity(packageManager)?.run {
        startActivity(intent)
    }
}

Java

// Create an Intent to launch a PDF viewer for a file owned by this app.
Intent viewPdfIntent = new Intent(Intent.ACTION_VIEW);
viewPdfIntent.setData(Uri.parse("content://com.example/personal-info.pdf"));

// This flag gives the started app read access to the file.
viewPdfIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

// Make sure that the user has a PDF viewer app installed on their device.
if (viewPdfIntent.resolveActivity(getPackageManager()) != null) {
    startActivity(viewPdfIntent);
}

Nota: Ejecutar archivos desde el directorio principal de la app que admite escritura es una infracción de W^X. Por este motivo, las apps no confiables que se orientan a Android 10 (nivel de API 29) y versiones posteriores no pueden invocar a exec() en archivos dentro del directorio principal de la app, solo al código binario incorporado en el archivo APK de una app. Además, las apps que se orientan a Android 10 y versiones posteriores no pueden modificar el código ejecutable en la memoria de los archivos que se abrieron con dlopen(), incluido cualquier archivo de objeto (.so) compartido con reubicaciones de texto.

Información relacionada: android:grantUriPermissions

Almacena datos de manera segura

Si bien tu app podría requerir acceso a información sensible del usuario, tus usuarios solo le otorgarán a tu app acceso a sus datos si confían en que los protegerás adecuadamente.

Guarda datos privados en el almacenamiento interno

Almacena todos los datos del usuario privados dentro del almacenamiento interno del dispositivo, que aísla la información de cada app. No es necesario que tu app solicite permiso para ver esos archivos, y otras apps no podrán acceder a ellos. Como medida de seguridad adicional, cuando el usuario desinstala una app, el dispositivo borra todos los archivos que la app guardó en el almacenamiento interno.

En el siguiente fragmento de código, se muestra una manera de escribir datos en el almacenamiento interno:

Kotlin

// Creates a file with this name, or replaces an existing file
// that has the same name. Note that the file name cannot contain
// path separators.
val FILE_NAME = "sensitive_info.txt"
val fileContents = "This is some top-secret information!"
File(filesDir, FILE_NAME).bufferedWriter().use { writer ->
    writer.write(fileContents)
}

Java

// Creates a file with this name, or replaces an existing file
// that has the same name. Note that the file name cannot contain
// path separators.
final String FILE_NAME = "sensitive_info.txt";
String fileContents = "This is some top-secret information!";
try (BufferedWriter writer =
             new BufferedWriter(new FileWriter(new File(getFilesDir(), FILE_NAME)))) {
    writer.write(fileContents);
} catch (IOException e) {
    // Handle exception.
}

En el siguiente fragmento de código, se muestra la operación inversa, la lectura de datos desde el almacenamiento interno:

Kotlin

val FILE_NAME = "sensitive_info.txt"
val contents = File(filesDir, FILE_NAME).bufferedReader().useLines { lines ->
    lines.fold("") { working, line ->
        "$working\n$line"
    }
}

Java

final String FILE_NAME = "sensitive_info.txt";
StringBuffer stringBuffer = new StringBuffer();
try (BufferedReader reader =
             new BufferedReader(new FileReader(new File(getFilesDir(), FILE_NAME)))) {

    String line = reader.readLine();
    while (line != null) {
        stringBuffer.append(line).append('\n');
        line = reader.readLine();
    }
} catch (IOException e) {
    // Handle exception.
}

Información relacionada:

Guarda datos en un almacenamiento externo según el caso de uso

Usa el almacenamiento externo para archivos grandes y no sensibles que sean específicos de tu app, además de archivos que comparta con otras. Las APIs específicas que uses dependerán de si tu app está diseñada para acceder a archivos específicos de la app o acceder a archivos compartidos.

Si un archivo no contiene información privada o sensible, pero proporciona valor al usuario solo en tu app, almacena el archivo en un directorio específico de la app en el almacenamiento externo.

Si tu app necesita usar o almacenar un archivo que proporciona valor a otras apps, utiliza una de las siguientes APIs según el caso de uso:

Comprueba la disponibilidad del volumen de almacenamiento

Si tu app interactúa con un dispositivo de almacenamiento externo extraíble, ten en cuenta que el usuario puede quitar el dispositivo de almacenamiento mientras tu app intenta acceder a él. Incluye lógica para verificar que el dispositivo de almacenamiento esté disponible.

Comprueba la validez de los datos

Si tu app usa datos de un almacenamiento externo, asegúrate de que el contenido de los datos no esté dañado ni haya sido modificado. Incluye lógica para procesar archivos que ya no se encuentren en un formato estable.

En el siguiente fragmento de código, se incluye un ejemplo de un verificador de hash:

Kotlin

val hash = calculateHash(stream)
// Store "expectedHash" in a secure location.
if (hash == expectedHash) {
    // Work with the content.
}

// Calculating the hash code can take quite a bit of time, so it shouldn't
// be done on the main thread.
suspend fun calculateHash(stream: InputStream): String {
    return withContext(Dispatchers.IO) {
        val digest = MessageDigest.getInstance("SHA-512")
        val digestStream = DigestInputStream(stream, digest)
        while (digestStream.read() != -1) {
            // The DigestInputStream does the work; nothing for us to do.
        }
        digest.digest().joinToString(":") { "%02x".format(it) }
    }
}

Java

Executor threadPoolExecutor = Executors.newFixedThreadPool(4);
private interface HashCallback {
    void onHashCalculated(@Nullable String hash);
}

boolean hashRunning = calculateHash(inputStream, threadPoolExecutor, hash -> {
    if (Objects.equals(hash, expectedHash)) {
        // Work with the content.
    }
});

if (!hashRunning) {
    // There was an error setting up the hash function.
}

private boolean calculateHash(@NonNull InputStream stream,
                              @NonNull Executor executor,
                              @NonNull HashCallback hashCallback) {
    final MessageDigest digest;
    try {
        digest = MessageDigest.getInstance("SHA-512");
    } catch (NoSuchAlgorithmException nsa) {
        return false;
    }

    // Calculating the hash code can take quite a bit of time, so it shouldn't
    // be done on the main thread.
    executor.execute(() -> {
        String hash;
        try (DigestInputStream digestStream =
                new DigestInputStream(stream, digest)) {
            while (digestStream.read() != -1) {
                // The DigestInputStream does the work; nothing for us to do.
            }
            StringBuilder builder = new StringBuilder();
            for (byte aByte : digest.digest()) {
                builder.append(String.format("%02x", aByte)).append(':');
            }
            hash = builder.substring(0, builder.length() - 1);
        } catch (IOException e) {
            hash = null;
        }

        final String calculatedHash = hash;
        runOnUiThread(() -> hashCallback.onHashCalculated(calculatedHash));
    });
    return true;
}

Almacena solo datos no sensibles en archivos de caché

Para proporcionar un acceso más rápido a los datos no sensibles de la app, almacénalos en la memoria caché del dispositivo. Para cachés de más de 1 MB, usa getExternalCacheDir(). Para cachés de 1 MB o menos, usa getCacheDir(). Ambos métodos te proporcionan el objeto File que contiene los datos en caché de tu app.

En el siguiente fragmento de código, se muestra cómo almacenar en caché un archivo que tu app descargó recientemente:

Kotlin

val cacheFile = File(myDownloadedFileUri).let { fileToCache ->
    File(cacheDir.path, fileToCache.name)
}

Java

File cacheDir = getCacheDir();
File fileToCache = new File(myDownloadedFileUri);
String fileToCacheName = fileToCache.getName();
File cacheFile = new File(cacheDir.getPath(), fileToCacheName);

Nota: Si usas getExternalCacheDir() para colocar la caché de tu app dentro del almacenamiento compartido, el usuario podría expulsar el medio que contiene este almacenamiento mientras se está ejecutando tu app. Incluye lógica para procesar correctamente la falta de caché que causa este comportamiento del usuario.

Precaución: No se aplica seguridad a estos archivos. Por lo tanto, las apps orientadas a Android 10 (nivel de API 29) o versiones anteriores que tengan el permiso WRITE_EXTERNAL_STORAGE, pueden acceder al contenido de esta caché.

Información relacionada: Descripción general del almacenamiento de datos y archivos

Usa SharedPreferences en el modo privado

Cuando uses getSharedPreferences() para crear o acceder a los objetos SharedPreferences de tu app, usa MODE_PRIVATE. De esa manera, solo tu app podrá acceder a la información que se encuentre dentro del archivo de preferencias compartido.

Si quieres compartir datos entre apps, no uses objetos SharedPreferences. En vez de eso, sigue los pasos para compartir datos de manera segura entre apps.

La biblioteca de Security también proporciona la clase EncryptedSharedPreferences, que une la clase SharedPreferences y encripta automáticamente claves y valores.

Información relacionada:

Mantén las dependencias y los servicios actualizados

La mayoría de las apps usan información del sistema del dispositivo y bibliotecas externas para completar tareas especializadas. Si mantienes actualizadas las dependencias de tu app, puedes hacer que estos puntos de comunicación sean más seguros.

Revisa el proveedor de seguridad de los Servicios de Google Play

Nota: Esta sección solo se aplica a las apps que se orientan a dispositivos que tienen los Servicios de Google Play instalados.

Si tu app usa los Servicios de Google Play, asegúrate de que estén actualizados en el dispositivo en el que esté instalada. Realiza la comprobación de manera asíncrona, por fuera del subproceso de IU. Si no está actualizado el dispositivo, activa un error de autorización.

A fin de determinar si están actualizados los Servicios de Google Play en el dispositivo en el que está instalada tu app, sigue los pasos que se indican en la guía Cómo actualizar tu proveedor de seguridad para protegerte contra vulnerabilidades de SSL.

Información relacionada:

Actualiza todas las dependencias de la app

Antes de implementar tu app, asegúrate de que se hayan actualizado todas las bibliotecas, los SDK y otras dependencias de la siguiente manera:

  • Para dependencias propias, como el SDK de Android, usa las herramientas de actualización que se encuentran en Android Studio, como SDK Manager.
  • Para dependencias de terceros, comprueba los sitios web de las bibliotecas que usa tu app y, luego, instala las actualizaciones y los parches de seguridad disponibles.

Información relacionada: Agrega dependencias de compilación

Más información

Para obtener más información acerca de cómo hacer que tu app sea más segura, consulta los siguientes recursos: