Codelab de configuración de seguridad de red de Android

1. Introducción

Es común que las apps intercambien datos a través de Internet. Dado que es posible que tu app se comunique con servidores que no sean los de confianza, debes tener cuidado cuando envíes y recibas información que pueda ser sensible y privada.

Qué compilarás

En este codelab, compilarás una app que muestra mensajes. Cada mensaje contendrá el nombre del remitente, el mensaje de texto y una URL que vincule a su "foto de perfil". La app mostrará estos mensajes de la siguiente manera:

  • Cargará un archivo JSON que contenga una lista de mensajes de texto de la red.
  • Cargará cada foto de perfil y la mostrará junto al mensaje apropiado.

Qué aprenderás

  • Por qué es importante la comunicación segura de la red
  • Cómo usar la biblioteca Volley para realizar solicitudes de red
  • Cómo usar una configuración de seguridad de red para ayudar a que la comunicación de la red sea más segura
  • Cómo modificar algunas opciones avanzadas de la configuración de seguridad de la red, que te ayudarán durante el desarrollo y las pruebas
  • Explorarás uno de los problemas de seguridad de red más comunes y descubrirás cómo puedes evitarlo con una configuración de seguridad de la red

Requisitos

  • La versión más reciente de Android Studio
  • Un dispositivo o emulador de Android que ejecute Android 7.0 (nivel de API 24) o una versión posterior
  • Node.js (o acceso a un servidor web configurable)

Si, a medida que avanzas con este codelab, encuentras algún problema (errores de código, errores gramaticales, texto poco claro, etc.), infórmalo mediante el vínculo Informa un error que se encuentra en la esquina inferior izquierda del codelab.

2. Cómo prepararte

Descarga el código

Haz clic en el siguiente vínculo a fin de descargar todo el código de este codelab:

Descomprime el archivo ZIP descargado. Se descomprimirá una carpeta raíz (android-network-secure-config), que contendrá el proyecto de Android Studio (SecureConfig/) y algunos archivos de datos que usaremos en una etapa más adelante (server/).

También puedes ver el código directamente desde GitHub: (comienza con la rama master).

También preparamos una rama con el código final después de cada paso. Si te bloqueas, revisa las ramas de GitHub o clona el repositorio completo: https://github.com/android/codelab-android-network-security-config/branches/all.

3. Cómo ejecutar la app

Después de hacer clic en el ícono "cargar", esta app accede a un servidor remoto para cargar una lista de mensajes, nombres y URL de fotos de perfil desde un archivo JSON. A continuación, los mensajes se mostrarán en una lista, y la app cargará las imágenes de las URL a las que se hace referencia.

Nota: El propósito de la app que usamos en este codelab es únicamente demostrativo. No se incluye tanto control de errores como se necesitaría en un entorno de producción.

d9e465c94b420ea1.png

Arquitectura de la app

La app sigue el patrón de MVP, separa el almacenamiento de datos y el acceso a la red (modelo) de la lógica (presentación) y la pantalla (vista).

La clase MainContract contiene el contrato que describe la interfaz entre la vista y la presentación:

MainContract.java

/*
 * Copyright 2017 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.networksecurity;

import com.example.networksecurity.model.Post;

/**
 * Contract defining the interface between the View and Presenter.
 */
public interface MainContract {

    interface View {
        /**
         * Sets the presenter for interaction from the View.
         *
         * @param presenter
         */
        void setPresenter(Presenter presenter);

        /**
         * Displays or hides a loading indicator.
         *
         * @param isLoading If true, display a loading indicator, hide it otherwise.
         */
        void setLoadingPosts(boolean isLoading);

        /**
         * Displays a list of posts on screen.
         *
         * @param posts The posts to display. If null or empty, the list should not be shown.
         */
        void setPosts(Post[] posts);

        /**
         * Displays an error message on screen and optionally prints out the error to logcat.
         */
        void showError(String title, String error);

        /**
         * Hides the error message.
         *
         * @see #showError(String, String)
         */
        void hideError();

        /**
         * Displays an empty message and icon.
         *
         * @param showMessage If true, the message is show. If false, the message is hidden
         */
        void showNoPostsMessage(boolean showMessage);
    }

    interface Presenter {
        /**
         * Call to start the application. Sets up initial state.
         */
        void start();

        /**
         * Loads post for display.
         */
        void loadPosts();

        /**
         * An error was encountered during the loading of profile images.
         */
        void onLoadPostImageError(String error, Exception e);
    }

}

Configuración de la app

A modo de demostración, se inhabilitó el almacenamiento en caché de la red en esta app. Lo ideal sería que, en un entorno de producción, la app utilice una caché local para limitar la cantidad de solicitudes de red remota.

El archivo gradle.properties contiene la URL desde la que se carga la lista de mensajes:

gradle.properties

postsUrl="http://storage.googleapis.com/network-security-conf-codelab.appspot.com/v1/posts.json"

Cómo compilar y ejecutar la app

  1. Inicia Android Studio y abre el directorio SecureConfig como proyecto de Android.
  2. Haz clic en "run" para iniciar la app: e15973f44eed7cc2.png

La siguiente captura de pantalla de la app muestra cómo se verá en un dispositivo:

63300e7e262bd161.png

4. Configuración de seguridad de la red básica

En este paso, crearemos una configuración de seguridad de red básica y observaremos un error que se produce cuando se infringe una de las reglas de la configuración.

Descripción general

La configuración de seguridad de la red permite que las apps personalicen su configuración de seguridad de red mediante un archivo de configuración declarativo. Toda la configuración está contenida en este archivo en formato XML, y no necesitas modificar el código.

Permite la configuración de los siguientes elementos:

  • Inhabilitación de tráfico de texto simple: Inhabilita el tráfico de texto simple.
  • Anclas de confianza personalizadas: Especifican cuáles son las autoridades certificadoras y las fuentes en las que confiará la app.
  • Anulaciones exclusivas de depuración: Depuran de forma segura las conexiones protegidas sin afectar las compilaciones de lanzamiento.
  • Fijación de certificados: Restringe las conexiones protegidas a certificados específicos.

El archivo se puede organizar por dominios. Esto permite que se aplique la configuración de seguridad de la red a todas las URL o solo a dominios específicos.

La configuración de seguridad de la red está disponible en Android 7.0 (nivel de API 24) y versiones posteriores.

Cómo crear un archivo de configuración de seguridad de red en formato XML

Crea un nuevo archivo de recursos XML con el nombre network_security_config.xml.

En el Android Project Panel del lado izquierdo, haz clic con el botón derecho en res y, luego, selecciona New > Android Resource File.

35db6786b96a6980.png

Configura las siguientes opciones y haz clic en OK.

Nombre del archivo

network_security_config.xml

Tipo de recurso

XML

Elemento root

network-security-config

Nombre del directorio

xml

36ae9e950fe66f1c.png

Abre el archivo xml/network_security_config.xml (si no se abrió automáticamente).

Reemplaza su contenido con el siguiente fragmento:

res/xml/network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="false" >
    </base-config>
</network-security-config>

Esta configuración se aplica a la configuración base o a la configuración de seguridad predeterminada de la app, e inhabilita todo el tráfico de texto simple.

Cómo habilitar la configuración de seguridad de la red

A continuación, agrega una referencia a la configuración de la app en el archivo AndroidManifest.xml.

Abre el archivo AndroidManifest.xml y busca el elemento application.

Primero, quita la línea que configura la propiedad android:usesCleartextTraffic="true".

Luego, agrega la propiedad android:networkSecurityConfig al elemento application en AndroidManifest, y haz referencia al recurso network_security_config del archivo en formato XML: @xml/network_security_config.

Después de quitar y agregar las dos propiedades anteriores, la etiqueta de aplicación de apertura debería verse de la siguiente manera:

AndroidManifest.xml

<application
    android:networkSecurityConfig="@xml/network_security_config"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme"
    android:fullBackupContent="false"
    tools:ignore="GoogleAppIndexingWarning">
    ...

Cómo compilar y ejecutar la app

Compila y ejecuta la app.

Verás un error: la app está tratando de cargar datos a través de una conexión de texto simple.

98d8a173d5293742.png

En logcat, notarás este error:

java.io.IOException: Cleartext HTTP traffic to storage.googleapis.com not permitted

La app no está cargando datos porque todavía está configurada para cargar la lista de mensajes desde una conexión HTTP sin encriptar. La URL configurada en el archivo gradle.properties dirige a un servidor HTTP que no usa TLS.

Modifiquemos esta URL para que use otro servidor y cargue los datos mediante una conexión HTTPS segura.

Cambia el archivo gradle.properties como se muestra a continuación:

gradle.properties

postsUrl="https://storage.googleapis.com/network-security-conf-codelab.appspot.com/v1/posts.json"

(Observa el protocolo https en la URL).

Es posible que debas volver a compilar el proyecto para que se detecte este cambio. En el menú, selecciona Build > Rebuild.

Vuelve a ejecutar la app. Ahora verás la carga de datos porque la solicitud de red usa una conexión HTTPS:

63300e7e262bd161.png

5. Problema común: Actualizaciones del servidor

La configuración de seguridad de red puede protegerte contra vulnerabilidades cuando una app realiza una solicitud a través de una conexión no segura.

Otro problema común del que se encarga la configuración de seguridad de la red son los cambios en el servidor que afectan a las URL cargadas en la app para Android. En el caso de nuestra app, por ejemplo, imagina que el servidor comienza a mostrar URL HTTP no seguras para las imágenes de perfil, en lugar de URL HTTPS seguras. Una configuración de seguridad de red que utiliza conexiones HTTPS generaría una excepción porque este requisito no se cumpliría en el tiempo de ejecución.

Cómo actualizar el backend de la app

Como recordarás, la app primero carga una lista de mensajes, cada uno de los cuales hace referencia a una URL de una foto de perfil.

Imagina que hubo un cambio en los datos que consume la app y hace que solicite URL de imágenes diferentes. Modifiquemos la URL de datos del backend para simular este cambio.

Cambia el archivo gradle.properties como se muestra a continuación:

gradle.properties

postsUrl="https://storage.googleapis.com/network-security-conf-codelab.appspot.com/v2/posts.json"

(Observa el "v2" en la ruta)

Es posible que debas volver a compilar el proyecto para que se detecte este cambio. En el menú, selecciona Build > Rebuild.

Puedes acceder al backend "nuevo" desde tu navegador para ver el archivo JSON modificado. Observa que todas las URL a las que se hace referencia usan HTTP y no HTTPS.

Cómo ejecutar la app y examinar el error

Compila y ejecuta la app.

La app carga los mensajes, pero no las imágenes. Examina el mensaje de error en la app y en logcat para ver por qué:

a2a98a842e99168d.png

java.io.IOException: Cleartext HTTP traffic to storage.googleapis.com not permitted

La app todavía usa HTTPS para acceder al archivo JSON. Sin embargo, los vínculos para crear imágenes de perfil dentro del archivo JSON usan direcciones HTTP, por lo que la app intenta cargar las imágenes en HTTP (no seguro).

Cómo proteger los datos

La configuración de seguridad de la red impidió correctamente una exposición accidental de datos. En lugar de intentar acceder a datos no seguros, la app bloquea el intento de conexión.

Imagina una situación como esta, donde un cambio en el backend no se probó satisfactoriamente antes del lanzamiento. Aplicar una configuración de seguridad de red a tu app para Android puede evitar que ocurran problemas similares, incluso después del lanzamiento de la app.

Cómo cambiar el backend para corregir la app

Cambia las URL del backend a una versión nueva que se "corrigió". En este ejemplo, se simula una corrección mediante la referencia a imágenes de perfil con URL HTTPS correctas.

Cambia la URL de backend en el archivo gradle.properties y actualiza el proyecto:

gradle.properties

postsUrl="https://storage.googleapis.com/network-security-conf-codelab.appspot.com/v3/posts.json"

(Observa el "v3" en la ruta)

Vuelve a ejecutar la app. Ahora, funciona como estaba previsto:

63300e7e262bd161.png

6. Configuración específica del dominio

Hasta ahora, especificamos la configuración de seguridad de red en base-config, que aplica la configuración a todas las conexiones que la app intenta realizar.

Puedes anular esta configuración para destinos específicos estableciendo un elemento domain-config. Una domain-config declara las opciones de configuración para un conjunto específico de dominios.

Actualicemos la configuración de seguridad de la red en nuestra app a la siguiente:

res/xml/network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="false" />
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">localhost</domain>
    </domain-config>
</network-security-config>

Esta configuración aplica base-config a todos los dominios, excepto "localhost" y sus subdominios, para los que se aplica una configuración diferente.

Aquí, la configuración básica impide el tráfico de texto simple para todos los dominios. Sin embargo, la configuración del dominio anula la regla y permite que la app acceda a localhost con texto simple.

Cómo realizar pruebas con un servidor HTTP local

Ahora que la app puede acceder a localhost con texto simple, iniciemos un servidor web local y probemos este protocolo de acceso.

Existen varias herramientas que pueden usarse para alojar un servidor web muy básico, como Node.js, Python y PERL. En este codelab usaremos el módulo http-server de Node.js para entregar los datos de nuestra app.

  1. Abre una terminal e instala http-server:
npm install http-server -g
  1. Navega al directorio donde revisaste el código y, luego, al directorio server/:
cd server/
  1. Inicia el servidor web y entrega los archivos ubicados en los datos o el directorio de datos:
http-server ./data -p 8080
  1. Abre un navegador web y ve a http://localhost:8080 para verificar que puedas acceder a los archivos y ver el archivo "posts.json":

934e48553bcc48e7.png

  1. A continuación, reenvía el puerto 8080 del dispositivo a la máquina local. En otra ventana de terminal, ejecuta el siguiente comando:
adb reverse tcp:8080 tcp:8080

Ahora tu app puede acceder a "localhost:8080" desde el dispositivo Android.

  1. Cambia la URL que usaste a fin de cargar datos en la app para que dirija al servidor nuevo en localhost. Modifica el archivo gradle.properties de la siguiente manera (recuerda que posiblemente debas sincronizar un proyecto de Gradle después de hacerlo):

gradle.properties

postsUrl="http://localhost:8080/posts.json"
  1. Ejecuta la app y verifica que los datos se carguen desde la máquina local. Puedes intentar modificar el archivo data/posts.json y actualizar la app para confirmar que la configuración nueva funcione según lo previsto.

63300e7e262bd161.png

Apartado: Configuración del dominio

Las opciones de configuración que se aplican a dominios específicos se definen en un elemento domain-config que puede contener varias entradas domain que especifican dónde se deberían aplicar las reglas domain-config. Si varios elementos domain-config contienen entradas domain similares, la configuración de seguridad de la red eligirá una configuración que se aplicará a una URL determinada en función de la cantidad de caracteres coincidentes. Se usará la configuración que contenga la entrada domain que coincida con la mayor cantidad de caracteres (consecutivos) de la URL.

Una configuración de dominio puede aplicarse a varios dominios y también puede incluir subdominios.

En el siguiente ejemplo, se muestra una configuración de seguridad de red que contiene varios dominios. (No cambiará nuestra app, solo es un ejemplo).

<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">secure.example.com</domain>
        <domain includeSubdomains="true">cdn.example.com</domain>
        <trust-anchors>
            <certificates src="@raw/trusted_roots"/>
        </trust-anchors>
    </domain-config>
</network-security-config>

Para conocer más detalles, consulta la definición del formato de archivo de configuración.

7. Anulaciones de depuración

Cuando desarrolles y pruebes una app diseñada para realizar solicitudes a través de HTTPS, es posible que debas conectarla a un servidor web local o a un entorno de prueba, tal como lo hicimos en el paso anterior.

En lugar de agregar un uso general para permitir el tráfico de texto simple en este caso de uso o modificar el código, la opción debug-override de la configuración de seguridad de la red te permitirá establecer opciones de seguridad que se implementen solo cuando la aplicación se ejecute en modo de depuración; es decir, cuando android:debuggable sea verdadero. Esto es mucho más seguro que usar un código condicional debido a su definición explícita de solo depuración. Play Store también evita que se suban apps depurables, lo que hace que esta opción sea aún más segura.

Cómo habilitar SSL en el servidor web local

Anteriormente, iniciamos un servidor web local que entregaba datos a través de HTTP en el puerto 8080. Ahora, generaremos un certificado SSL autofirmado y lo usaremos para entregar datos mediante HTTPS:

  1. Cambia al directorio server/ en una ventana de terminal y, luego, ejecuta los siguientes comandos para generar un certificado (si aún estás ejecutando el servidor HTTP, puedes presionar [CTRL] + [C] y detenerlo ahora).
# Run these commands from inside the server/ directory!

# Create a certificate authority
openssl genrsa -out root-ca.privkey.pem 2048
# Sign the certificate authority
openssl req -x509 -new -nodes -days 100 -key root-ca.privkey.pem -out root-ca.cert.pem -subj "/C=US/O=Debug certificate/CN=localhost" -extensions v3_ca -config openssl_config.txt
# create DER format crt for Android
openssl x509 -outform der -in root-ca.cert.pem -out debug_certificate.crt

Esto genera una autoridad certificadora, la firma y produce un certificado en el formato DER requerido para Android.

  1. Inicia el servidor web con HTTPS con los certificados que acabas de generar:
http-server ./data --ssl --cert root-ca.cert.pem --key root-ca.privkey.pem

Cómo actualizar la URL del backend

Modifica la app para que acceda al servidor localhost mediante HTTPS.

Cambia el archivo gradle.properties:

gradle.properties

postsUrl="https://localhost:8080/posts.json"

Compila y ejecuta la app.

La app fallará y mostrará un error porque el certificado del servidor no es válido:

3bcce1390e354724.png

java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

La app no puede acceder al servidor web porque este utiliza un certificado autofirmado que no es de confianza como parte del sistema. En lugar de inhabilitar HTTPS, en el próximo paso, agregaremos este certificado autofirmado para el dominio localhost.

Cómo hacer referencia a una autoridad certificadora personalizada

El servidor web ahora entrega los datos usando una autoridad certificadora (CA) autofirmada que ningún dispositivo acepta de forma predeterminada. Si accedes al servidor desde tu navegador, verás una advertencia de seguridad: https://localhost:8080.

898b69ea4fe9bc21.png

A continuación, usaremos la opción debug-overrides en la configuración de seguridad de la red a fin de permitir esta autoridad certificadora personalizada solo en el dominio localhost:

  1. Modifica el archivo xml/network_security_config.xml para que contenga lo siguiente:

res/xml/network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="false" />
    <debug-overrides>
        <trust-anchors>
            <!-- Trust a debug certificate in addition to the system certificates -->
            <certificates src="system" />
            <certificates src="@raw/debug_certificate" />
        </trust-anchors>
    </debug-overrides>
</network-security-config>

Esta configuración inhabilita el tráfico de red de texto simple y, para compilaciones de depuración*,* habilita a la autoridad certificadora proporcionada por el sistema, así como un archivo de certificado almacenado en el directorio res/raw.

Nota: La configuración de depuración agrega <certificates src="system" /> de forma implícita, por lo que la app funcionará incluso sin eso. Lo incluimos para mostrar cómo la agregarías en una configuración más avanzada.

  1. A continuación, copia el archivo "debug_certificate.crt" del directorio server/ al directorio de recursos res/raw de la app en Android Studio. También puedes arrastrar y soltar el archivo en la ubicación correcta dentro de Android Studio.

Es posible que primero debas crear este directorio si no existe.

Para hacerlo, puedes ejecutar los siguientes comandos desde el servidor o directorio; o bien, puedes usar un administrador de archivos o Android Studio para crear la carpeta y copiar el archivo en la ubicación correcta:

mkdir  ../SecureConfig/app/src/main/res/raw/
cp debug_certificate.crt ../SecureConfig/app/src/main/res/raw/

Android Studio agregará el archivo debug_certificate.crt a la lista de archivos debajo de app/res/raw:

c3111ae17558e167.png

Cómo ejecutar la app

Compila y ejecuta la app, que ahora accede a nuestro servidor web local mediante HTTPS con un certificado de depuración autofirmado.

Si encuentras un error, revisa con atención los resultados del logcat y asegúrate de haber reiniciado el http-server con las nuevas opciones de línea de comandos. También verifica que el archivo debug_certificate.crt esté en la ubicación correcta (res/raw/debug_certificate.crt).

63300e7e262bd161.png

8. Más información

La configuración de seguridad de la red es compatible con muchas funciones más avanzadas, incluidas las siguientes:

Cuando uses estas funciones, revisa la documentación para obtener detalles sobre las prácticas recomendadas y las limitaciones.

Cómo hacer que tu app sea más segura

Como parte de este codelab, aprendiste a usar la configuración de seguridad de red con el propósito de que una app para Android sea más segura. Piensa en cómo tu propia app puede usar estas funciones y cómo podrías beneficiarte de una configuración de depuración más sólida para pruebas y desarrollo.

Más información