Data Privacy for Android

In this data privacy tutorial for Android with Kotlin, you’ll learn how to protect users’ data. By Kolin Stürt.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Clearing the Cache

If users opt out, you must delete any data you’ve collected. These include temporary files and caches!

Your app or third party libraries may use the cache folder, so it should be cleared when no longer needed. In ReportDetailActivity.kt, add the following function at the end:

override fun onPause() {
  cacheDir.deleteRecursively()
  externalCacheDir?.deleteRecursively()
 
  super.onPause()
}

Here, you told the OS to delete the cache directories when you pause the activity.

Note: You can also delete your shared preferences by removing the /data/data/com.your.package.name/shared_prefs/your_prefs_name.xml and your_prefs_name.bak files and clearing the in-memory preferences with the following code:
context.getSharedPreferences("prefs", Context.MODE_PRIVATE).edit().clear().commit()
context.getSharedPreferences("prefs", Context.MODE_PRIVATE).edit().clear().commit()

Your app also has a keyboard cache for text fields with auto-correct enabled. Android stores user text and learned words here, making it possible to retrieve various words the user has entered in your app. To prevent leaking this information, you need to disable this cache.

To disable the keyboard cache, you’ll need to turn off the auto-correct option. Open activity_report_detail.xml and switch to the Text editing mode tab. Find EditText and replace the android:inputType="textMultiLine" line with the following:

android:inputType="textNoSuggestions|textVisiblePassword|textFilter"

Various devices and OS versions have some bugs where some of these flags do nothing on their own. That means it’s a good idea to implement all of the flags.

Note: Mark password fields as secureTextEntry. Secure text fields don’t display the password or use the keyboard cache.

There are a few other caches to consider. For example, Android caches data sent over the network in memory and on-device storage. You don’t want to leave that data behind.

In sendReportPressed() of the ReportDetailActivity.kt file, replace //TODO: Disable cache here with the code below:

connection.setRequestProperty("Cache-Control", "no-cache")
connection.defaultUseCaches = false
connection.useCaches = false

This disables the cache for the HttpsURLConnection session.

For WebView, you can remove the cache at any time with this code:

webview.clearCache(true)

Check any third-party libraries you use for a way to disable or remove the cache. For example, the popular Glide image loading library allows you to cache photos in memory instead of on storage:

Glide.with(context)
    .load(theURL)
    ...
    .diskCacheStrategy(DiskCacheStrategy.NONE)
    ...
    .into(holder.imageView)

Libraries may leak other data. For example, you’ll want to check if there’s an option to disable logging. Head over to the next section to learn about that.

Disabling Logging of Sensitive Data

Android saves debug logs to a file that you can retrieve for the production builds of your app. Even when you are writing code and debugging your app, be sure not to log sensitive information such as passwords and keys to the console. You may forget to remove the logs before releasing your app.

There’s a class called BuildConfig that contains a flag called DEBUG. It’s set to true when you’re debugging and automatically set to false when you export a release build. Here’s an example:

if (BuildConfig.DEBUG) {
  Log.v(TAG, "Some harmless log...")
}

In theory, that’s good for non-sensitive logging; in practice, it’s dangerous to rely on it. There have been bugs in the build system that caused the flag to be true for release builds. You can define your own constant but then you’re back to the same problem of developers remembering to change it before release.

The solution is not to log sensitive variables. Instead, use a breakpoint to view sensitive variables.

In the same sendReportPressed(), notice Log.d("MY_APP_TAG", "Sanitized report is: $reportString") outputs the entire report to the console. It shouldn’t be there. Select the line and delete it.

Disabling ability to Screenshot

You’ve ensured no traces of the report are left behind, yet it’s still possible for a user to take a screenshot of the entire reporting screen. The OS takes screenshots of your app too. It uses them for the animation that happens when putting an app into the background or for the list of open apps on the task switcher. Those screenshots are stored on the device.

You should disable this feature for views revealing sensitive data. Back in ReportDetailActivity.kt, find onCreate(). Replace //TODO: Disable screenshots with below:

window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)

Here, you’ve told the window to have FLAG_SECURE, which prevents explicit and implicit capturing of the screen.

Build and run. Make a report:

Filled out report

Try to take a screenshot, and you’ll notice that you can’t!

Screenshot error notice

Now, users can make anonymous reports without accidentally leaving behind data.

But what about the reporting itself? Is it secure? To find out, first a little theory…

Exploring Hardware Security Modules

Security chip

A Trusted Execution Environment (TEE) is software separate from the OS. It safely sandboxes security operations, and while inside the main processor, it’s cordoned off from the main OS. Security keys that are isolated this way are hardware-backed. You can find out if a key is hardware-backed using KeyInfo.isInsideSecureHardware().

An example of a TEE is the ARM processor that has the TrustZone secure enclave, available in modern Samsung phones.

A Secure Element (SE) takes this a step further by putting the environment on a segregated chip. It has its own CPU, storage and encryption, and random-number generator methods. Security chips that exist outside of the main processor make it harder to attack. Google’s devices contain the Titan M security chip, which is a SE.

In both cases, security operations happen at the hardware level in a separate environment that is less susceptible to software exploits.

Android 9 and above provide the StrongBox Keymaster API for these features. To ensure the key exists inside a segregated secure element, you can call KeyGenParameterSpec.Builder.setIsStrongBoxBacked(true).

Time to put this information to practical use!

Implementing Biometrics

If hackers guess your account password, they’ll be able to see your reports. To ensure you are you, modern devices have some form of biometric readers. Face, retina and fingerprint scanners are all examples. You’ll implement a biometric prompt to log in to the app so that only you can report crimes on your device.

To prevent crashes and give the user a chance for an alternative, first check that the device can use biometrics. In MainActivity.kt, replace the contents of loginPressed() with the below code block:

val email = login_email.text.toString()
if ( !isSignedUp && !isValidEmailString(email)) {
  toast("Please enter a valid email.")
} else {
  val biometricManager = BiometricManager.from(this)
  when (biometricManager.canAuthenticate()) {
    BiometricManager.BIOMETRIC_SUCCESS ->
      displayLogin(view,false)
    BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE ->
      displayLogin(view,true)
    BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE ->
      toast("Biometric features are currently unavailable.")
    BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED ->
      toast("Please associate a biometric credential with your account.")
    else ->
      toast("An unknown error occurred. Please check your Biometric settings")
  }
}

The app calls displayLogin() if the device can perform biometric authentication with BIOMETRIC_SUCCESS. Otherwise, the fallback flag is set to true, allowing for password or pin authentication.

Add the following variables to the class:

private lateinit var biometricPrompt: BiometricPrompt
private lateinit var promptInfo: BiometricPrompt.PromptInfo

where BiometricPrompt is a class from AndroidX.

Next, replace the contents of displayLogin() with the following:

val executor = Executors.newSingleThreadExecutor()
biometricPrompt = BiometricPrompt(this, executor, // 1
    object : BiometricPrompt.AuthenticationCallback() {
      override fun onAuthenticationError(errorCode: Int,
                                         errString: CharSequence) {
        super.onAuthenticationError(errorCode, errString)
        runOnUiThread {
          toast("Authentication error: $errString")
        }
      }

      override fun onAuthenticationFailed() {
        super.onAuthenticationFailed()
        runOnUiThread {
          toast("Authentication failed")
        }
      }

      override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {// 2
        super.onAuthenticationSucceeded(result)

        runOnUiThread {
          toast("Authentication succeeded!")
          if (!isSignedUp) {
            generateSecretKey() // 3
          }
          performLoginOperation(view)
        }
      }
    })

if (fallback) {
  promptInfo = BiometricPrompt.PromptInfo.Builder()
      .setTitle("Biometric login for my app")
      .setSubtitle("Log in using your biometric credential")
      // Cannot call setNegativeButtonText() and
      // setDeviceCredentialAllowed() at the same time.
      // .setNegativeButtonText("Use account password")
      .setDeviceCredentialAllowed(true) // 4
      .build()
} else {
  promptInfo = BiometricPrompt.PromptInfo.Builder()
      .setTitle("Biometric login for my app")
      .setSubtitle("Log in using your biometric credential")
      .setNegativeButtonText("Use account password")
      .build()
}
biometricPrompt.authenticate(promptInfo)

Here’s what’s happening:

  1. You create an object, BiometricPrompt, for authentication.
  2. You override onAuthenticationSucceeded to determine a successful authentication.
  3. You create a secret key that’s tied to the authentication for first-time users.
  4. You allow a fallback to password authentication by calling .setDeviceCredentialAllowed(true), if desired.

Be sure you have a face, fingerprint or similar biometric scanner on your device. Build and run. You should be able to log in with your credential:

Biometric login prompt

On successful authentication, you’ll see the report list:

Report list

Congrats! You’ve secured access to the app with biometric security!

Lock screen

Even though access is limited now, the data isn’t encrypted. That is not good. You will fix that next!