Save the date! Android Dev Summit is coming to Mountain View, CA on November 7-8, 2018.

App security best practices

By making your app more secure, you help preserve user trust and device integrity.

This page presents several best practices that have a significant, positive impact on your app's security.

Enforce secure communication

When you safeguard the data that you exchange between your app and other apps, or between your app and a website, you improve your app's stability and protect the data that you send and receive.

Use implicit intents and non-exported content providers

Show an app chooser

If an implicit intent can launch at least two possible apps on a user's device, explicitly show an app chooser. This interaction strategy allows users to transfer sensitive information to an app that they trust.

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

Related info:

Apply signature-based permissions

When sharing data between two apps that you control or own, use signature-based permissions. These permissions don't require user confirmation and instead check that the apps accessing the data are signed using the same signing key. Therefore, these permissions offer a more streamlined, secure user experience.

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

Related info:

Disallow access to your app's content providers

Unless you intend to send data from your app to a different app that you don't own, you should explicitly disallow other developers' apps from accessing the ContentProvider objects that your app contains. This setting is particularly important if your app can be installed on devices running Android 4.1.1 (API level 16) or lower, as the android:exported attribute of the <provider> element is true by default on those versions of 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>

Apply network security measures

Use SSL traffic

If your app communicates with a web server that has a certificate issued by a well-known, trusted CA, the HTTPS request is very simple:

Kotlin

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

Java

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

Add a network security configuration

If your app uses new or custom CAs, you can declare your network's security settings in a configuration file. This process allows you to create the configuration without modifying any app code.

To add a network security configuration file to your app, follow these steps:

  1. Declare the configuration in your app's manifest:
  2. <manifest ... >
        <application
            android:networkSecurityConfig="@xml/network_security_config"
            ... >
            <!-- Place child elements of <application> element here. -->
        </application>
    </manifest>
    
  3. Add an XML resource file, located at res/xml/network_security_config.xml.

    Specify that all traffic to particular domains should use HTTPS by disabling clear-text:

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

    During the development process, you can use the <debug-overrides> element to expliticly allow user-installed certificates. This element overrides your app's security-critical options during debugging and testing without affecting the app's release configuration. The following snippet shows how to define this element in your app's network security configuration XML file:

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

Related info: Network Security Configuration

Create your own trust manager

Your SSL checker shouldn't accept every certificate. You may need to set up a trust manager and handle all SSL warnings that occur if one of the following conditions applies to your use case:

  • You're communicating with a web server that has a certificate signed by a new or custom CA.
  • That CA isn't trusted by the device you're using.
  • You cannot use a network security configuration.

To learn more about how to complete these steps, see the discussion about handling an unknown cerificate authority.

Related info:

Use WebView objects carefully

Whenever possible, load only whitelisted content in WebView objects. In other words, the WebView objects in your app shouldn't allow users to navigate to sites that are outside of your control.

In addition, you should never enable JavaScript interface support unless you completely control and trust the content in your app's WebView objects.

Use HTML message channels

If your app must use JavaScript interface support on devices running Android 6.0 (API level 23) and higher, use HTML message channels instead of evaluateJavascript() to communicate between a website and your app, as shown in the following code snippet:

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

Related info:

Provide the right permissions

Your app should request only the minimum number of permissions necessary to function properly. When possible, your app should relinquish some of these permissions when they're no longer needed.

Use intents to defer permissions

Whenever possible, don't add a permission to your app to complete an action that could be completed in another app. Instead, use an intent to defer the request to a different app that already has the necessary permission.

The following example shows how to use an intent to direct users to a contacts app instead of requesting the READ_CONTACTS and WRITE_CONTACTS permissions:

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

In addition, if your app needs to perform file-based I/O—such as accessing storage or choosing a file—it doesn't need special permissions because the system can complete the operations on your app's behalf. Better still, after a user selects content at a particular URI, the calling app gets granted permission to the selected resource.

Related info:

Share data securely across apps

Follow these best practices in order to share your app's content with other apps in a more secure manner:

The following code snippet shows how to use URI permission grant flags and content provider permissions to display an app's PDF file in a separate PDF Viewer app:

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

Related info: android:grantUriPermissions

Store data safely

Although your app might require access to sensitive user information, your users will grant your app access to their data only if they trust that you'll safeguard it properly.

Store private data within internal storage

Store all private user data within the device's internal storage, which is sandboxed per app. Your app doesn't need to request permission to view these files, and other apps cannot access the files. As an added security measure, when the user uninstalls an app, the device deletes all files that the app saved within internal storage.

The following code snippet demonstrates one way to write data to internal storage:

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

openFileOutput(FILE_NAME, Context.MODE_PRIVATE).use { fos ->
    fos.write(fileContents.toByteArray())
}

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!";

FileOutputStream fos = openFileOutput(FILE_NAME, Context.MODE_PRIVATE);
fos.write(fileContents.getBytes());
fos.close();

The following code snippet shows the inverse operation, reading data from internal storage:

Kotlin

// The file name cannot contain path separators.
val FILE_NAME = "sensitive_info.txt"
val fis = openFileInput(FILE_NAME)

// available() determines the approximate number of bytes that can be
// read without blocking.
val bytesAvailable = fis.available()
val fileBuffer = ByteArray(bytesAvailable)
val topSecretFileContents = StringBuilder(bytesAvailable).apply {
    // Make sure that read() returns a number of bytes that is equal to the
    // file's size.
    while (fis.read(fileBuffer) != -1) {
        append(fileBuffer)
    }
}

Java

// The file name cannot contain path separators.
final String FILE_NAME = "sensitive_info.txt";
FileInputStream fis = openFileInput(FILE_NAME);

// available() determines the approximate number of bytes that can be
// read without blocking.
int bytesAvailable = fis.available();
StringBuilder topSecretFileContents = new StringBuilder(bytesAvailable);

// Make sure that read() returns a number of bytes that is equal to the
// file's size.
byte[] fileBuffer = new byte[bytesAvailable];
while (fis.read(fileBuffer) != -1) {
    topSecretFileContents.append(fileBuffer);
}

Related info:

Use external storage cautiously

By default, the Android system doesn't enforce security restrictions on data that resides within external storage, and the storage medium itself isn't guaranteed to stay connected to the device. Therefore, you should apply the following security measures to provide safe access to information within external storage.

Use scoped directory access

If your app needs to access only a specific directory within the device's external storage, you can use scoped directory access to limit your app's access to a device's external storage accordingly. As a convenience to users, your app should save the directory access URI so that users don't need to approve access to the directory every time your app attempts to access it.

Note: If you use scoped directory access with a particular directory in external storage, know that the user might eject the media containing this storage while your app is running. You should include logic to gracefully handle the change to the Environment.getExternalStorageState() return value that this user behavior causes.

The following code snippet uses scoped directory access with the pictures directory within a device's primary shared storage:

Kotlin

private const val PICTURES_DIR_ACCESS_REQUEST_CODE = 42

...

private fun accessExternalPicturesDirectory() {
    val intent: Intent = (getSystemService(Context.STORAGE_SERVICE) as StorageManager)
            .primaryStorageVolume.createAccessIntent(Environment.DIRECTORY_PICTURES)
    startActivityForResult(intent, PICTURES_DIR_ACCESS_REQUEST_CODE)
}

...

override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
    if (requestCode == PICTURES_DIR_ACCESS_REQUEST_CODE && resultCode == Activity.RESULT_OK) {

        // User approved access to scoped directory.
        if (resultData != null) {
            val picturesDirUri: Uri = resultData.data

            // Save user's approval for accessing this directory
            // in your app.
            contentResolver.takePersistableUriPermission(
                    picturesDirUri,
                    Intent.FLAG_GRANT_READ_URI_PERMISSION
            )
        }
    }
}

Java

private static final int PICTURES_DIR_ACCESS_REQUEST_CODE = 42;

private void accessExternalPicturesDirectory() {
  StorageManager sm =
          (StorageManager) getSystemService(Context.STORAGE_SERVICE);
  StorageVolume volume = sm.getPrimaryStorageVolume();
  Intent intent =
          volume.createAccessIntent(Environment.DIRECTORY_PICTURES);
  startActivityForResult(intent, PICTURES_DIR_ACCESS_REQUEST_CODE);
}

...

@Override
public void onActivityResult(int requestCode, int resultCode,
        Intent resultData) {
    if (requestCode == PICTURES_DIR_ACCESS_REQUEST_CODE &&
            resultCode == Activity.RESULT_OK) {

        // User approved access to scoped directory.
        if (resultData != null) {
            Uri picturesDirUri = resultData.getData();

            // Save user's approval for accessing this directory
            // in your app.
            ContentResolver myContentResolver = getContentResolver();
            myContentResolver.takePersistableUriPermission(picturesDirUri,
                    Intent.FLAG_GRANT_READ_URI_PERMISSION);
        }
    }
}

Warning: Don't pass null into createAccessIntent() unnecessarily because this grants your app access to the entire volume that StorageManager finds for your app.

Related info:

Check validity of data

If your app uses data from external storage, make sure that the contents of the data haven't been corrupted or modified. Your app should also include logic to handle files that are no longer in a stable format.

The following example shows the permission and logic that check a file's validity:

AndroidManifest.xml

<manifest ... >
    <!-- Apps on devices running Android 4.4 (API level 19) or higher cannot
         access external storage outside their own "sandboxed" directory, so
         the READ_EXTERNAL_STORAGE (and WRITE_EXTERNAL_STORAGE) permissions
         aren't necessary. -->
    <uses-permission
          android:name="android.permission.READ_EXTERNAL_STORAGE"
          android:maxSdkVersion="18" />
    ...
</manifest>

MyFileValidityChecker.java

Kotlin

private val UNAVAILABLE_STORAGE_STATES: Set<String> =
        setOf(MEDIA_REMOVED, MEDIA_UNMOUNTED, MEDIA_BAD_REMOVAL, MEDIA_UNMOUNTABLE)
...
val ringtone = File(getExternalFilesDir(DIRECTORY_RINGTONES), "my_awesome_new_ringtone.m4a")
when {
    isExternalStorageEmulated(ringtone) -> {
        Log.e(TAG, "External storage is not present")
    }
    UNAVAILABLE_STORAGE_STATES.contains(getExternalStorageState(ringtone)) -> {
        Log.e(TAG, "External storage is not available")
    }
    else -> {
        val fis = FileInputStream(ringtone)

        // available() determines the approximate number of bytes that
        // can be read without blocking.
        val bytesAvailable: Int = fis.available()
        val fileBuffer = ByteArray(bytesAvailable)
        StringBuilder(bytesAvailable).apply {
            while (fis.read(fileBuffer) != -1) {
                append(fileBuffer)
            }
            // Implement appropriate logic for checking a file's validity.
            checkFileValidity(this)
        }
    }
}

Java

File ringtone = new File(getExternalFilesDir(DIRECTORY_RINGTONES,
        "my_awesome_new_ringtone.m4a"));
if (isExternalStorageEmulated(ringtone)) {
    Logger.e(TAG, "External storage is not present");
} else if (getExternalStorageState(ringtone) == MEDIA_REMOVED
        | MEDIA_UNMOUNTED | MEDIA_BAD_REMOVAL | MEDIA_UNMOUNTABLE) {
    Logger.e(TAG, "External storage is not available");
} else {
    FileInputStream fis = new FileInputStream(ringtone);

    // available() determines the approximate number of bytes that
    // can be read without blocking.
    int bytesAvailable = fis.available();
    StringBuilder fileContents = new StringBuilder(bytesAvailable);
    byte[] fileBuffer = new byte[bytesAvailable];
    while (fis.read(fileBuffer) != -1) {
        fileContents.append(fileBuffer);
    }

    // Implement appropriate logic for checking a file's validity.
    checkFileValidity(fileContents);
}

Related info:

Store only non-sensitive data in cache files

To provide quicker access to non-sensitive app data, store it in the device's cache. For caches larger than 1 MB in size, use getExternalCacheDir(); otherwise, use getCacheDir(). Each method provides you with the File object that contains your app's cached data.

The following code snippet shows how to cache a file that your app recently downloaded:

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

Note: If you use getExternalCacheDir() to place your app's cache within shared storage, the user might eject the media containing this storage while your app is running. You should include logic to gracefully handle the cache miss that this user behavior causes.

Caution: There is no security enforced on these files. Therefore, any app that has the WRITE_EXTERNAL_STORAGE permission can access the contents of this cache.

Related info: Saving cache files

Use SharedPreferences in private mode

When using getSharedPreferences() to create or access your app's SharedPreferences objects, use MODE_PRIVATE. That way, only your app can access the information within the shared preferences file.

If you want to share data across apps, don't use SharedPreferences objects. Instead, you should follow the necessary steps to share data securely across apps.

Related info: Using Shared Preferences

Keep services and dependencies up-to-date

Most apps use external libraries and device system information to complete specialized tasks. By keeping your app's dependencies up to date, you make these points of communication more secure.

Check the Google Play services security provider

Note: This section applies only to apps targeting devices that have Google Play services installed.

If your app uses Google Play services, make sure that it's updated on the device where your app is installed. This check should be done asynchronously, off of the UI thread. If the device isn't up-to-date, your app should trigger an authorization error.

To determine whether Google Play services is up to date on the device where your app is installed, follow the steps in the guide for Updating Your Security Provider to Protect Against SSL Exploits.

Related info:

Update all app dependencies

Before deploying your app, make sure that all libraries, SDKs, and other dependencies are up to date:

  • For first-party dependencies, such as the Android SDK, use the updating tools found in Android Studio, such as the SDK Manager.
  • For third-party dependencies, check the websites of the libraries that your app uses, and install any available updates and security patches.

Related info: Add Build Dependencies

Additional resources

To learn more about how to make your app more secure, view the following resources: