15.
User Privacy
Written by Antonio Roa-Valverde
With so many data breaches and new privacy laws recently, your app’s credibility depends on how you manage your user’s data. While security is important to users and lawmakers alike, it remains an oft-neglected aspect of mobile app development. When you build an app, you need to think about security from the ground up.
To assist developers in keeping their user data secure, starting from Android 11 the OS offers new privacy features and device enhancements including scoped storage, hardened permissions, biometric authentication and hardware-backed key storage. Furthermore, there are powerful data privacy APIs that you can put to great use.
In this chapter, you’ll learn about:
- Privacy and security basics
- Permissions
- Locking down user data
If you missed the previous chapters, the sample app includes a list of pets and their medical data along with a section that lets you report issues anonymously:
In this chapter, you’ll focus on keeping that sensitive information secure.
Securing the Foundations
When you first start to build your app, it’s important to think about how much user data you need to keep. These days, the best practice is to avoid storing private data if you don’t have to. Pets, of course, are always concerned about their privacy rights. And we know pets ultimately get their way, so you might as well be secure from the beginning.
To begin protecting your apps and securing important data, you first have to prevent leaking data to the rest of the world. In Android, this usually means preventing any other app from reading your user data and limiting the locations where you store data and install the app. This will be your first step toward securing private information.
Using Permissions
Ever since Android 6.0, you set the files and SharedPreferences you save with the MODE_PRIVATE constant. That means only your app can access the data. Android 7 doesn’t allow any other option, so you’ll implement this next.
Open PetSavePreferences.kt in the core.data.preferences package. You’ll notice there are deprecation warnings for MODE_WORLD_READABLE and MODE_WORLD_WRITEABLE.
These allow public access to your files for earlier Android versions. If only there was a way to tell those users to update their devices! Well, technically there is, but instead, replace the code in Figure 15.1 with the following:
@Singleton
class PetSavePreferences @Inject constructor(
@ApplicationContext context: Context
) : Preferences {
// ...
private val preferences = context.getSharedPreferences(PREFERENCES_NAME,
Context.MODE_PRIVATE)
private val preferencesWrite = context.getSharedPreferences(PREFERENCES_NAME,
Context.MODE_PRIVATE)
// ...
}
Great, you’ve just made your preferences more private. Additionally, when you build and run the app now, those security violations won’t cause a crash on Android 7+ versions.
Another important point regarding private access: You should enforce a secure location for your app’s install directory.
Limiting Installation Directories
One of the larger problems Android has faced in the past few years was running out of memory to install the plethora of available apps due to the low storage capacity of many devices. Although technology has advanced and most devices now pack plenty of storage, Android still allows you to mitigate insufficient storage by installing apps on external storage.
This works well, but it opens security concerns. Installing apps on external SD cards is convenient, but also a security flaw. Anyone with access to the SD card also has access to the app’s data — and that data could hold sensitive information. This is why it’s a best practice to restrict your app to internal storage.
To do this, open AndroidManifest.xml and find the line that reads android:installLocation="auto", then replace it like this:
android:installLocation="internalOnly"
With this, you’ve limited the install location to the device, but you can still back up your app and its data. Users can access the contents of the app’s private data folder using ADB backup. To disallow backups, find the line that reads android:allowBackup="true" and replace the value with "false".
Following these best practices, you’ve hardened your app data from the outside. On the flip side, you’ll want to let the user decide if the app can access other parts of the device’s data like the camera or the user’s location.
Requesting User Permissions
As mentioned earlier, Android 11 debuted many new privacy features, which you can read about here: https://developer.android.com/about/versions/11/privacy.
For example, users can grant one-time access to location data, the microphone and the camera. The Settings section offers improved control over background access to the user’s location. Additionally, there’s a consistent place for Google account activity and autofill services and the OS resets permissions if you haven’t interacted with an app for a few months.
Because of these privacy features, you must ask for permission before your app can access the user’s external data. As such, the first question to consider is how much data your app needs to acquire. A good approach is to avoid gathering any information you don’t need.
APIs that access user data require you to declare that access in the manifest file beforehand. In AndroidManifest.xml, find the line that reads:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
In the past, this was enough. When the user installed the app, they’d see a list of permissions. But Marshmallow changed that with Runtime Permissions. Now, your app should request permissions at the moment when it needs them. This approach is more transparent because it shows exactly which features the permission covers. It helps weed out unnecessary permissions. To do this, go to ReportDetailFragment.kt and add the following declaration at the top of the file:
private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) {
selectImageFromGallery()
}
}
This code relies on the Activity Result API and registers a RequestPermission contract. The method accepts a callback that is executed when the activity gets the result. In this case, the callback receives a boolean indicating if the permission was granted. If that’s the case, then you have green light to select the image from the gallery.
Note: The Activity Result API offers other predefined contracts and it also allows you to define your custom ones. For more information about the different possibilities visit https://developer.android.com/training/basics/intents/result.
This code alone doesn’t do much. In order to execute it, you need to launch it first. You’ll do this next. Replace the contents of uploadPhotoPressed() like this:
@AndroidEntryPoint
class ReportDetailFragment : Fragment() {
// ...
private fun uploadPhotoPressed() {
context?.let {
if (ContextCompat.checkSelfPermission(it, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) { // 1
requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) // 2
} else {
selectImageFromGallery() // 3
}
}
}
// ...
}
Here, you implement runtime permissions by:
- Checking if the user has already granted permission for
READ_EXTERNAL_STORAGE. - In case the permission is not granted, you request it launching
requestPermissionLauncherthat you declared previously. - If already granted you directly invoke the method to select an image.
If the user grants permission, the image selection starts. Build and run the project after you’ve made the changes. When prompted for permission, tap Allow. You can now select an image. :]
Note: Android 11 enforces scoped access to app files and media. It requires that you use the Storage Access Framework https://developer.android.com/about/versions/11/privacy/storage to access folders on external storage the app doesn’t own. It’s best practice to access external media using the Media Store: https://developer.android.com/training/data-storage/shared/media. For apps where these APIs cannot be used effectively, it is possible to request the
MANAGE_EXTERNAL_STORAGEpermission. However, you should take into account that Google Play imposes very hard restrictions in the kind of apps that are allowed to use it: https://developer.android.com/training/data-storage/manage-all-files#all-files-access-google-play.
These aren’t the only ways you can pass data between apps. In the past, IPC has been a popular choice for developers.
Using IPC
Permissions cover most of what you need to access and pass data outside of the app. But sometimes you pass data via IPC to other apps that you build. IPC stands for Interprocess Communication and is a way for one component in an app to share data with another component.
There have been cases where developers have left shared files on the storage or have implemented sockets to exchange sensitive information. This is not secure. Instead, the best practice is to use Intents. You can send data using an Intent by providing the package name, like this:
val intent = Intent()
val packageName = "com.example.app" //1
val activityClass = "com.example.app.TheActivity" // 2
intent.component = ComponentName(packageName, activityClass)
intent.putExtra("UserInfo", "Example string") //3
startActivityForResult(intent) //4
Here you’re specifying:
- The package name of the app where you’ll send the intent.
- The qualified class name in the target app that receives the intent.
- Data sent with the intent.
- The intent, by starting the activity with it and then waiting for the result.
To broadcast data to more than one app, enforce that only apps signed with your signing key will get the data. Otherwise, any app that registers to receive the broadcast can read the sent information. Likewise, a malicious app could send a broadcast to your app if you’ve registered to receive its broadcast.
Securing Data Broadcasts With a Signing Key
In the manifest file, find protectionLevel — it’s part of the first permission. You’ll notice it’s set to normal. Change it to signature by replacing that line with the following:
android:protectionLevel="signature" />
Then replace the protectionLevel inside the <application tag with:
android:protectionLevel="signature"
Other apps access the permission by including the following code in the manifest file:
<uses-permission android:name="com.realworld.android.snitcher.permission.REPORT_DETAIL_FRAGMENT"/>
Apps typically send a broadcast like this:
val intent = Intent()
intent.putExtra("UserInfo", "Example string")
intent.action = "com.example.SOME_NOTIFICATION"
sendBroadcast(intent, "com.example.mypermission")
Alternatively, you can use setPackage(String) when sending a broadcast to restrict it to a set of apps that match the specified package. Also, setting android:exported to false in the manifest file will exclude broadcasts from outside your app. That setting tells the system whether other apps can invoke or interact with a particular activity or service.
Now, you’ve set permissions correctly and waited for the user to grant them. But what if the user wants to disallow access later?
Opting Out
Using permissions properly offers another benefit: It grants users the ability to revoke permissions in the system settings and opt out of data sharing if they change their minds later. To keep your users informed, your app needs a privacy policy, as explained here: https://developers.google.com/assistant/console/policies/privacy-policy-guide.
Privacy policies disclose the types of personally identifiable information (PII) apps collect, such as unique device identifiers. If you’re collecting such data intentionally, you must provide a place in your UI where the user can opt out. It’s also prudent to understand the laws in any jurisdiction where your app is available. EU member countries, for example, require explicit consent for data collection.
To learn more about privacy policies, visit the Android Privacy Section: https://play.google.com/about/privacy-security-deception and Android’s best practices for unique identifiers: https://developer.android.com/training/articles/user-data-ids.
When users opt out, you should delete the stored data you have for them. But during this process, be sure not to overlook temporary data files.
Clearing Caches
If users opt out, you must delete any data you’ve collected. This includes temporary files and caches! Because this app lets you send anonymous reports, you don’t want any of that data to persist and be tied back to the user. Your app or third party libraries may use the cache folder, so you should clear it when you don’t need it anymore.
To do this, add the following function to ReportDetailFragment.kt:
@AndroidEntryPoint
class ReportDetailFragment : Fragment() {
// ...
override fun onPause() {
context?.cacheDir?.deleteRecursively()
context?.externalCacheDir?.deleteRecursively()
super.onPause()
}
}
Here, you tell the OS to delete the cache directories when you pause the fragment.
Note: You can also delete your shared preferences by removing /data/data/com.your.package.name/shared_prefs/your_prefs_name.xml and your_prefs_name.bak and clearing the in-memory preferences with the following code:
context.getSharedPreferences("prefs", Context.MODE_PRIVATE).edit().clear().commit().
Disabling the Keyboard Cache
Your app also has a keyboard cache for text fields with autocorrect enabled. Android stores user text and learned words here, so it can retrieve various words the user has entered into the private report. To prevent leaking this information, you need to disable this cache.
To disable the keyboard cache, you need to turn off the autocorrect option. Open fragment_report_detail.xml and switch to the Code Editing Mode tab. Find the first EditText and replace the android:inputType="textMultiLine" line with the following:
android:inputType="textNoSuggestions|textVisiblePassword|textFilter|textMultiLine"
For the second EditText that doesn’t need the the multiline setting, replace it with this:
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 these flags.
Note: You should also mark password fields as
secureTextEntry. Secure text fields don’t display the password or use the keyboard cache.
Disabling Other Caches
There are a few other caches to consider. For example, Android caches data sent over the network to memory and on-device storage. You don’t want to leave that data behind, either. In provideOkHttpClient() inside APIModule.kt, replace //TODO: Disable cache here with:
.cache(null)
That disables the cache for OkHttp, but you might use a different implementation in your app. For example, this disables the cache for the native HttpsURLConnection session:
connection.setRequestProperty("Cache-Control", "no-cache")
connection.defaultUseCaches = false
connection.useCaches = false
For WebView, you can remove the cache at any time with this code:
webview.clearCache(true)
Check other third-party libraries you use for a way to disable or remove the cache. In this app, you’ve used the popular Glide image loading library. It allows you to cache photos in memory instead of in storage. Navigate to Extentions.kt and replace //TODO: Disable disk cache here with the following:
.diskCacheStrategy(DiskCacheStrategy.NONE)
Libraries may also leak other kinds of data. For example, check if there’s an option to disable logging. That’s what you’ll look at next.
Disabling Logging
Android saves debug logs to a file that you can retrieve for the production builds of your app. Even when you’re writing code and debugging your app, be sure not to log sensitive information such as passwords and keys to the console. You wouldn’t want to 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 log stuff...")
}
In theory, that’s good for non-sensitive logging; in practice, it’s dangerous to rely on. There have been bugs in the build system that set the flag to true for release builds. You can define your own constant, but then you’re back to the problem of developers remembering to change it before release.
The solution is to not log sensitive variables. Instead, use a breakpoint to view them.
For example, in AuthenticationInterceptor.kt, notice Log.d("Pet Save", "The auth token is: $token") outputs the real PetFinder authentication token to the console. Looks like someone was debugging and forgot to remove it! Select the line and delete it.
The anonymous report section is getting much safer to use. However, there are a couple more things you can do to be diligent about not leaking data.
Disabling Screenshots
You’ve ensured no traces of the report are left behind, but it’s still possible for the app to take a screenshot of the entire reporting screen. The OS takes screenshots of your app, too. It uses them for the animation it plays when it puts an app into the background or for the list of open apps in the task switcher. Those screenshots are stored on the device.
You should disable this feature for views revealing sensitive data. Back in MainActivity.kt, find onCreate(). Replace //TODO: Disable screenshots with:
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. This is especially important for private messaging or video streaming apps, deterring someone from taking a snapshot.
Keep in mind that it’s not foolproof. A user can still take a picture from another device, for example.
Build and run, then make a report:
Try to take a screenshot. You’ll notice that you can’t!
Now, users can make anonymous reports without accidentally leaving a screen-grabbed copy of their report behind.
You’ve taken care of most of the privacy-related points by either preventing or removing data. When it comes to removing data there’s a way to make sure it’s done securely.
Wiping Memory Securely
When an OS deletes a file, it only removes the reference, not the data. To completely remove that data, you must overwrite the file with random data:
fun wipeFile(file: File) {
if (file.exists()) {
val length = file.length()
val random = SecureRandom()
val randomAccessFile = RandomAccessFile(file, "rws")
randomAccessFile.seek(0)
randomAccessFile.filePointer
val data = ByteArray(64)
var position = 0
while (position < length) {
random.nextBytes(data)
randomAccessFile.write(data)
position += data.size
}
randomAccessFile.close()
file.delete()
}
}
The code above iterates over a File, replacing the bytes with random data generated from SecureRandom.
You’ll also notice most security functions work with ByteArray or CharArray instead of objects such as String. That’s because String is immutable and there’s no control over how the system copies or garbage collects it.
If you’re working with sensitive strings or data, it’s better — though not foolproof — to store the information in a mutable array, then overwrite the sensitive arrays when you’re done with them. For ByteArray that would be:
Arrays.fill(byteArray, 0.toByte())
and for CharArray, it’s:
Arrays.fill(charArray, '\u0000')
Depending on the platform, some types of solid-state storage devices, such as solid-state drives (SSD) in modern laptops, won’t write to the same area of memory each time. This preserves the longevity of the SSD. Depending on the platforms you port your code to, a secure erase method may not work.
A better solution for this type of scenario is to encrypt the stored data in the first place. As long as you discard the encryption key, you don’t need to securely erase the data. And that’s what the next chapter is about!
Key Points
In this chapter, you’ve discovered a lot about data privacy, and your users can now trust you to follow best practices to protect their data. Feel free to download the completed final project.
Here are a few points to remember:
- Only collect sensitive information when it’s necessary for your app.
- You can restrict access to internal app data with permissions.
- Request user consent to let the app access data outside the app.
- Clearing caches and wiping sensitive files helps protect the user’s data.
Where to Go From Here?
So you tightened access to the data at a high level. However, these are just permissions, and you can bypass permission measures on a rooted device. The solution? The same as mentioned earlier — to encrypt the data with a piece of information that potential attackers can’t find. So to learn the finer details of encryption, head on to the next chapter.
In the meantime, to learn more about some of the more recent privacy laws, check out these resources: