Although it's commonplace for apps to exchange data over the Internet, often with servers other than those you trust, you need to exercise caution when sending and receiving information that could be sensitive and private.

What you will build

In this codelab, you're going to build an app that displays messages. Each message will contain the sender's name, the text message, and a URL to their "profile picture". The app will show these messages by doing the following:

  • Load a JSON file that contains a list of text messages from the network.
  • Load each profile picture and displays it next to the appropriate message.

What you'll learn

What you'll need

Download the Code

Click the following link to download all the code for this codelab:

Download source code

Unpack the downloaded zip file. This will unpack a root folder (android-network-secure-config), which contains the Android Studio project (SecureConfig/) and some data files we will use in a later stage (server/).

You can also check out the code directly from Github: (Start with the master branch.)

Github repository

We have also prepared a branch with the final code after each step. If you get stuck, take a look at the branches on github or clone the entire repository: https://github.com/googlecodelabs/android-network-security-config/branches/all

After clicking on the "load" icon, this app accesses a remote server to load a list of messages, names, and URLs to their profile picture from a JSON file. Next, the messages are displayed in a list, and the app loads the images from the referenced URLs.

Note: The app we are using in this codelab is only for demo purposes. It doesn't include as much error handling as you would need in a production environment.

App Architecture

The app follows the MVP pattern, separating the data storage and network access (model) from the logic (presenter) and display (view).

This is the contract that describes the interface between the View and Presenter:

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);
    }

}

App Configuration

For demonstration purposes, all network caching has been disabled for this app. Ideally, in a production environment, the app would utilize a local cache to limit the number of remote network requests.

The URL from which the list of messages is loaded is configured in the gradle.properties file:

gradle.properties

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

Building and running the app

  1. Start Android Studio and open the SecureConfig directory as an Android project.
  2. Click on "run" to start the app:

The following screenshot from the app shows how it looks on a device:

In this step, we'll set up a basic network security configuration and observe an error that occurs when it's violated.

Overview

The Network Security Configuration lets apps customize their network security settings through a declarative configuration file. The entire configuration is contained within this XML file, and no code changes are required.

It allows for the configuration of the following:

The file can be organized by domains, allowing network security settings to be applied to the all URLs or only to specific domains.

The Network Security Configuration is available on Android 7.0 (API level 24) and higher.

Create a Network Security Configuration XML file

Create a new xml resource file with the name network_security_config.xml.

In the Android Project Panel on the left hand side, right click on res, then select New > Android resource file.

Set the following options and click OK.

File name

network_security_config.xml

Resource type

XML

Root element

network-security-config

Directory name

xml

Open the file xml/network_security_config.xml (if it didn't automatically open).

Replace its contents with the following snippet:

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>

This configuration applies to the base configuration, or the default security configuration, of the app and disables all clear text traffic.

Enable the Network Security Configuration

Next, add an entry into the app configuration in the AndroidManifest.xml file and reference the newly-created network security configuration file.

Add the android:networkSecurityConfig property to the application element in the AndroidManifest, referencing the network_security_config XML file resource:

AndroidManifest.xml

 <application
... 
 android:networkSecurityConfig="@xml/network_security_config"
... >

Compile and run the app

Compile and run the app.

You will see an error - the app is trying to load data over a cleartext connection!

In logcat, you will notice this error:

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

The app isn't loading data because it's still configured to load the list of messages from an unencrypted, HTTP connection. The URL configured in the gradle.properties file points to an HTTP server that doesn't use TLS!

Let's change this URL to use different server and load the data over a secured HTTPS connection.

Change the gradle.properties file as follows:

gradle.properties

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

(Note the https protocol in the URL.)

You may need to rebuild the project for this change to be picked up. From the menu, select Build > Rebuild.

Run the app again. You'll see data load now because the network request uses an HTTPS connection:

A Network Security Configuration can protect against vulnerabilities when an app makes a request over an unsecured connection.

A common problem that a Network Security Configuration addresses is server-side changes that affect the URLs loaded into an Android app. For example, in our app, imagine that the server started to return unsecured HTTP URLs for profile images instead of secured HTTPS URLs. A Network Security Configuration that enforces HTTPS connections would then raise an exception because this requirement wouldn't be met at runtime.

Update the app backend

As you may recall, the app first loads a list of messages, each referencing a URL to a profile picture.

Imagine that there was a change to the data that the app consumed, causing the app to request different image URLs. Let's simulate this change by modifying the backend data URL.

Change the gradle.properties file as follows:

gradle.properties

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

(Note the "v2" in the path!)

You may need to rebuild the project for this change to be picked up. From the menu, select Build > Rebuild.

You can access the "new" backend from your browser, too. Use this link to see the modified JSON file in your browser. Note how all referenced URLs use HTTP instead of HTTPS.

Run the app and examine the error

Compile and run the app.

The app loads messages, but the images aren't being loaded. Examine the error message in the app and in logcat to see why:

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

The app still uses HTTPS to access the JSON file. However, the links to profile images within the JSON file use HTTP addresses, so the app tries to load the images over (insecure) HTTP.

Protecting data

The Network Security Configuration has successfully prevented an accidental data exposure. instead of attempting to access unsecured data, the app blocks the connection attempt.

Imagine a scenario like this where a change on the backend wasn't sufficiently tested before rollout. Applying a Network Security Configuration to your Android app can catch similar issues from occurring, even after the app's release.

Change the backend to fix the app

Change the backend URLs to a new version that has been "fixed". This example simulates a fix by referencing profile images using correct HTTPS URLs.

Change the backend URL in the gradle.properties file and refresh the project:

gradle.properties

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

(Note the v3 in the path!)

Run the app again. It now works as expected:

So far, we've specified the network security configuration in the base-config, which applies the configuration to all connections that the app attempts to make.

You can override this configuration for specific destinations by specifying a domain-config element. A domain-config declares configuration options for a specific set of domains.

Let's update the network security configuration in our app to the following:

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>

This configuration applies the base-config to all domains, except for the domain "localhost" and its subdomains, for which a different configuration is applied.

Here, the base configuration prevents cleartext traffic for all domains, but the domain configuration overrides that rule, allowing the app to access localhost using cleartext.

Test using a local HTTP server

Now that the app can access localhost using cleartext, let's start a local web server and test this access protocol.

In this codelab, we'll use the http-server Node.JS module to set up a very basic web server and serve the data for our app:

  1. Open a terminal and install http-server:
npm install http-server -g
  1. Navigate to the directory where you have checked out the code, then go to the server/ directory:
cd server/
  1. Start the web server and serve the files located in the data/ directory:
http-server ./data -p 8080
  1. Open a web browser and navigate to http://localhost:8080 to verify that you can access the files and see the "posts.json" file:

  1. Next, forward the port 8080 from the device to the local machine. Run the following command in another terminal window:
adb reverse tcp:8080 tcp:8080

Your app can now access "localhost:8080" from the Android device.

  1. Change the URL used to load data in the app to point to the new server on localhost. Change the gradle.properties file as follows:
    (Remember, you may need to a gradle project sync after changing this file.)

gradle.properties

postsUrl="http://localhost:8080/posts.json"
  1. Run the app and verify that data is loaded from the local machine.
    You can try modifying the data/posts.json file and refreshing the app to confirm that the new configuration is working as intended.

Aside - domain configuration

Configuration options that apply to specific domains are defined in a domain-config element. This element can contain multiple domain entries that specify where the domain-config rules should apply. If multiple domain-config elements contain similar domain entries, the Network Security Configuration chooses a configuration to apply to a given URL based on the number of matching characters. The configuration containing the domain entry that matches the most characters (consecutively) with the URL is used.

A domain configuration can apply to multiple domains and may also include subdomains.

The following example shows a Network Security Configuration that contains multiple domains.
(We are not changing our app, this is just an example!)

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

For more details, refer to the configuration file format definition.

As you develop and test an app that's designed to make requests over HTTPS, you may need to connect it to a local web server or test environment, just as we did in the previous step.

Instead of adding a blanket whitelist for this use case or modifying the code, the debug-override option in the Network Security Configuration allows you to set security options that only apply when the application is run in debug mode; that is, when android:debuggable is true. This is significantly safer than using conditional code because of its explicit debug-only definition. The Play Store also rejects debuggable apps from being uploaded, making this option even safer

Enable SSL on the local web server

Earlier, we started a local web server that served data over HTTP on port 8080. We'll now generate a self-signed SSL certificate and use it to serve data over HTTPS:

  1. Generate a certificate by changing to the server/ directory in a terminal window, then by executing the following commands:
    (If you are still running the http-server, you can stop it now by pressing [CTRL] + [C].)
# 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 -key root-ca.privkey.pem -days 100 -out root-ca.cert.pem -subj "/C=US/O=Debug Cert/CN=localhost"
# create DER format crt for Android
openssl x509 -outform der -in root-ca.cert.pem -out debug_certificate.crt

This generates a certificate authority, signs it, and generates a certificate in the DER format that Android requires.

  1. Start the web server with HTTPS, using the newly-generated certificates:
http-server ./data --ssl --cert root-ca.cert.pem --key root-ca.privkey.pem

Update the backend URL

Change the app to access the localhost server via HTTPS.

Change the gradle.properties file:

gradle.properties

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

Compile and run the app.

The app will fail with an error because the server's certificate is invalid:

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

The app can't access the web server because the server is using a self-signed certificate that is not trusted as part of the system. Instead of disabling HTTPS, we'll add this self-signed certificate for the localhost domain in the next step.

Reference a custom certificate authority

The web server is now serving the data using a self-signed certificate authority (CA) that is not accepted by any devices by default. If you access the server from your browser, you will notice a security warning: https://localhost:8080

Next, we will use a debug-override option in the network security configuration to whitelist this custom certificate authority only for the localhost domain:

  1. Change the xml/network_security_config.xml file as follows:

res/xml/network_security_config.xml

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

    <domain-config>
        <domain includeSubdomains="true">localhost</domain>
        <trust-anchors>
            <!-- Trust a debug certificate in addition to the system certificates -->
            <certificates src="system" />
            <certificates src="@raw/debug_certificate" />
        </trust-anchors>
    </domain-config>

</network-security-config>

This configuration disables cleartext network traffic and, for the localhost domain, enables the system-provided certificate authority, as well as a certificate file stored in the res/raw directory.

  1. Next, copy the file "debug_certificate.crt" from the server/ directory into the res/raw resources directory of the app in Android Studio. You can also drag and drop the file into the correct location in Android Studio.

    You may have to create this directory first if it does not exist.

    From the server/ directory, you can run the following commands to do this, otherwise use a file manager or Android studio to create the folder and copy the file into the correct location:
mkdir  ../SecureConfig/app/src/main/res/raw/
cp debug_certificate.crt ../SecureConfig/app/src/main/res/raw/

Android studio will now list the file debug_certificate.crt file under app/res/raw:

Run the app

Compile and run the app. The app is now accessing our local web server over HTTPS using a self-signed debug certificate.

If you encounter an error, carefully check the logcat output and ensure that you've restarted the http-server with the new command line options. Also check that the debug_certificate.crt file is in the correct location (res/raw/debug_certificate.crt).

The Network Security Configuration supports many more advanced features, including the following:

When using these features, check the documentation for details on best practices and limitations.

Make your app more secure!

As part of this codelab, you've learned how to use the Network Security Configuration to secure an Android app. Think about how your own app can utilize these features and how you could benefit from a more robust debug configuration for testing and development.

Learn More