Recomendaciones sobre seguridad de apps

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 su estabilidad y proteges los datos que envías y recibes.

Usa intents implícitos y proveedores de contenido no exportado

Muestra un selector de apps

Si un intent implícito puede lanzar 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(ACTION_SEND)
    val possibleActivitiesList: List<ResolveInfo> =
            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 =
            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, deberías impedir explícitamente que las apps de otros desarrolladores accedan a los objetos ContentProvider que contiene la tuya. Esta opción de configuración resulta particularmente importante si se puede instalar tu app en dispositivos que ejecutan Android 4.1.1 (API nivel 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

Si los usuarios deben proporcionar credenciales para 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 digital.

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 SSL

Si tu app se comunica con un servidor web que tiene un certificado emitido por un CA reconocido y de confianza, la solicitud HTTPS es muy simple.

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 explícitamente 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 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 SSL no debería aceptar todos los certificados. Es posible que tengas que configurar un administrador de confianza y manejar todas las advertencias de SSL que se produzcan si alguna de las siguientes condiciones se aplica a tu caso práctico:

  • 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

Siempre que sea posible, carga solo contenido de la lista blanca en objetos WebView. En otras palabras, los objetos WebView de tu app no deberían permitir que los usuarios naveguen en sitios que se encuentran fuera de tu control.

Además, nunca deberías habilitar la compatibilidad con la interfaz de JavaScript, a menos que controles por completo el contenido de los objetos WebView de tu app.

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)

    // messagePorts[0] and messagePorts[1] represent the two ports.
    // They are already tangled to 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);

    // messagePorts[0] and messagePorts[1] represent the two ports.
    // They are already tangled to 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

Tu app solo debería pedir la cantidad mínima de permisos necesarios para funcionar adecuadamente. Cuando sea posible, tu app debería renunciar a algunos de estos permisos cuando ya no sean necesarios.

Usa intents para diferir permisos

Siempre que sea posible, no agregues un permiso a tu app para completar una acción que podría completarse en otra app. En cambio, usa un intent para 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: Las apps no confiables que se orientan a Android 10 (API nivel 29) y versiones posteriores no pueden invocar a exec() en archivos dentro del directorio principal de la app. Esta ejecución de archivos desde el directorio principal de la app que admite escritura es una infracción de W^X. Las apps deben cargar solo el 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.

Nota: Si los datos que almacenas son confidenciales o privados, procura trabajar con objetos EncryptedFile, que están disponibles en la Biblioteca de seguridad, en lugar de con objetos File.

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

Kotlin

    // Although you can define your own key generation parameter specification, it's
    // recommended that you use the value specified here.
    val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
    val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)

    // 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 fileToWrite = "my_sensitive_data.txt"
    val encryptedFile = EncryptedFile.Builder(
        File(directory, fileToWrite),
        context,
        masterKeyAlias,
        EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
    ).build()

    encryptedFile.openFileOutput().bufferedWriter().use {
        it.write("MY SUPER-SECRET INFORMATION")
    }
    

Java

    // Although you can define your own key generation parameter specification, it's
    // recommended that you use the value specified here.
    KeyGenParameterSpec keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC;
    String masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec);

    // 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.
    String fileToWrite = "my_sensitive_data.txt";
    try {
        EncryptedFile encryptedFile = new EncryptedFile.Builder(
                new File(directory, fileToWrite),
                context,
                masterKeyAlias,
                EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
        ).build();

        // Write to a file.
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
                encryptedFile.openFileOutput()));
        writer.write("MY SUPER-SECRET INFORMATION");
    } catch (GeneralSecurityException gse) {
        // Error occurred getting or creating keyset.
    } catch (IOException ex) {
        // Error occurred opening file for writing.
    }
    

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

Kotlin

    // Although you can define your own key generation parameter specification, it's
    // recommended that you use the value specified here.
    val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
    val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)

    val fileToRead = "my_sensitive_data.txt"
    val encryptedFile = EncryptedFile.Builder(
        File(directory, fileToRead),
        context,
        masterKeyAlias,
        EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
    ).build()

    val contents = encryptedFile.bufferedReader().useLines { lines ->
        lines.fold("") { working, line ->
            "$working\n$line"
        }
    }
    

Java

    // Although you can define your own key generation parameter specification, it's
    // recommended that you use the value specified here.
    KeyGenParameterSpec keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC;
    String masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec);

    String fileToRead = "my_sensitive_data.txt";
    EncryptedFile encryptedFile = new EncryptedFile.Builder(
            new File(directory, fileToRead),
            context,
            masterKeyAlias,
            EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
    ).build();

    StringBuffer stringBuffer = new StringBuffer();
    try (BufferedReader reader =
                 new BufferedReader(new FileReader(encryptedFile))) {

        String line = reader.readLine();
        while (line != null) {
            stringBuffer.append(line).append('\n');
            line = reader.readLine();
        }
    } catch (IOException e) {
        // Error occurred opening raw file for reading.
    } finally {
        String contents = stringBuffer.toString();
    }
    

Información relacionada:

Guarda datos en un almacenamiento externo según el caso práctico

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 API 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.

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.

Accede a archivos específicos de la app

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.

Accede a archivos compartidos

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

  • Archivos multimedia: para almacenar y acceder a imágenes, archivos de audio y videos compartidos entre apps, usa la API de Media Store.
  • Otros archivos: para almacenar y usar otros tipos de archivos compartidos, incluidos los descargados, use Storage Access Framework.

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. Tu app también debería incluir lógica para procesar archivos que ya no se encuentren en un formato estable.

En el siguiente fragmento de código, se muestra 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(). De lo contrario, utiliza getCacheDir(). Cada método te proporciona 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. Deberías incluir 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, cualquier app que tenga el permiso WRITE_EXTERNAL_STORAGE podrá acceder al contenido de esta caché.

Información relacionada: Cómo guardar archivos de caché

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 necesarios para compartir datos de manera segura entre apps.

Información relacionada: Cómo utilizar las preferencias compartidas

Mantén los servicios y las dependencias actualizados

La mayoría de las apps usan información del sistema del dispositivo y bibliotecas externas para completar tareas especializadas. Al mantener 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 la app. Esta comprobación debería hacerse de manera asíncrona, por fuera del subproceso de IU. Si no está actualizado el dispositivo, tu app debería activar un error de autorización.

Para 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:

Recursos adicionales

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

Codelabs

Blogs