内容简介:Security is paramount. The flurry of new privacy laws, such as theTo assist developers, Android 10 offers new privacy advancements and device enhancements, including biometric authentication and hardware-backed key storage.In this tutorial, you’ll learn ab
Security is paramount. The flurry of new privacy laws, such as the CCPA , PIPEDA and GDPR , shows how important security is to users and lawmakers alike. Yet, it remains an often neglected aspect of mobile app development.
To assist developers, Android 10 offers new privacy advancements and device enhancements, including biometric authentication and hardware-backed key storage.
In this tutorial, you’ll learn about:
- Privacy permissions.
- Locking down user data.
- Clearing the cache.
Note : This tutorial assumes your familiarity with the basics of Android development and Android Studio. If Android development is new to you, first read through the Beginning Android Development and Kotlin for Android: An Introduction tutorials.
Getting Started
Download and unzip the materials for this tutorial using the Download Materials button at the top or bottom of this page. Open and run the starter project in Android Studio 3.5.0 or higher. You’ll see a simple sign-up screen. Once you enter an email and select Sign Up , a list of various topics will populate.
If you missed theprevious tutorial, this app lets users send anonymous tips about crimes against animals to law enforcement. OK, it doesn’t send the information to law enforcement, so feel free to test it out. :]
Requesting Permissions
As mentioned earlier, Android 10 brought many new privacy features . For example, security updates now occur in the background so users don’t need to reboot their phones.
The settings section offers improved control over a service’s access to user location. Additionally, there’s a consistent place for Google account activity and AutoFill services.
Android 10’s new privacy features also require you to ask for permission before your app can store user’s private data externally. As such, the first question to consider is how much data your app needs to access. A good approach is to avoid accessing data you don’t need.
APIs that access user data require you to declare that access in the manifest file beforehand. In AndroidManifest.xml , you can find the line that reads:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
In the past, this was enough. Users would see a list of permissions when installing the app. But Marshmallow changed that with Runtime Permissions . Now, your app requests permissions at the time of need. This approach is more transparent because it shows exactly what features the permission is for. It’s also beneficial since it weeds out unnecessary permissions.
Select one of the report categories from the app list and choose Upload Photo . Pick an image, and you should encounter the following:
This crash occurs because you’re required to ask for permission at runtime. In ReportDetailActivity.kt
, replace the contents of uploadPhotoPressed()
with the following code:
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) // 1 != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, // 2 arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE), PIC_FROM_GALLERY) } else { val galleryIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) // 3 startActivityForResult(galleryIntent, PIC_FROM_GALLERY) }
Here, you make use of runtime permissions by:
READ_EXTERNAL_STORAGE
When the user first grants permission, Android calls onRequestPermissionResult()
. Override that method by adding the following code to end of ReportDetailActivity.kt
:
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { when (requestCode) { PIC_FROM_GALLERY -> { // If request is cancelled, the result arrays are empty. if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) { // Permission was granted val galleryIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) startActivityForResult(galleryIntent, PIC_FROM_GALLERY) } return } else -> { // Ignore all other requests. } } }
If permission is granted, the media intent starts. To try it out build and run the project after you have made changes. When prompted for permission, hit Allow . You can now select a JPEG image without a crash. :]
Android 10 adds scoped access to app files and media. It requires you to use the Storage Access Framework to access folders on external storage the app doesn’t own. It’s also recommended to access external media using MediaStore .
Using IPC
Permissions cover the most ground with accessing and passing data outside of the app. But sometimes data is passed via IPC to other apps that you build.
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 awaiting 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 can send a broadcast to your app if you have registered to receive its broadcast.
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" />
Other apps access the permission by including the following code in the manifest file:
<uses-permission android:name="com.raywenderlich.android.snitcher.permission.REPORT_DETAIL_ACTIVITY"/>
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 matching the specified package. Also, setting android:exported
to false
in the manifest file will exclude broadcasts from outside your app.
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 will need a privacy policy .
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 UX 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 and Android’s best practices for unique identifiers 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.
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:
Try to take a screenshot, and you’ll notice that you can’t!
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
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:
BiometricPrompt onAuthenticationSucceeded .setDeviceCredentialAllowed(true)
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:
On successful authentication, you’ll see the report list:
Congrats! You’ve secured access to the app with biometric security!
Even though access is limited now, the data isn’t encrypted. That is not good. You will fix that next!
Hardening User Data
In theprevious tutorial, you discovered that the app stores sensitive reports in the clear. You’ll change that now by using MasterKeys
to generate a key in the KeyStore. This will encrypt the reports.
As you learned above, the benefit of storing a key in the KeyStore is that it allows the OS to operate on it without exposing the secret contents of that key. Key data do not enter the app space.
For devices that don’t have a security chip, permissions for private keys only allow for your app to access the keys — and only after user authorization. This means that a lock screen must be set up on the device before you can make use of the credential storage. This makes it more difficult to extract keys from a device, called extraction prevention .
The security library contains two new classes, EncryptedFile
and EncryptedSharedPreferences
. In Encryption.kt
, replace the entire encryptFile()
with this:
fun encryptFile(context: Context, file: File) : EncryptedFile { val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec) // 1 return EncryptedFile.Builder( file, context, masterKeyAlias, EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB // 2 ).build() }
Here’s what’s happening:
- You either create a new master key or retrieve one already created.
- You encrypt the file using the popular secure AES encryption algorithm.
In ReportDetailActivity.kt
, find sendReportPressed()
. Replace the two lines right after //TODO: Replace below for encrypting file
with the below code block:
val file = File(filesDir.absolutePath, "$reportID.txt") //1 val encryptedFile = encryptFile(baseContext, file) // 2 encryptedFile.openFileOutput().bufferedWriter().use { it.write(reportString) //3 }
Here’s what’s happening:
"$reportID.txt" EncryptedFile EncryptedFile
Note : Biometrics do come with a few concerns. People can use biometrics maliciously. An example of that is when someone steals and holds your phone up to your face while you’re unconscious, or when law enforcement holds your device to your finger after they handcuff you. Or, if someone cuts off your hand when you’re distracted and sends it to the Mob. Always check to make sure your hand is there!
Awesome! You’ve hardened the data stored on the device. To make the app more secure, you’ll next authenticate your biometric credentials with a server.
Authenticating With Biometrics
You can auto-generate a key in KeyStore that is protected by your biometric credential. The key will encrypt a password for server authentication, and if the device becomes compromised, the password will be encrypted.
In Encryption.kt
, add the following to generateSecretKey()
:
val keyGenParameterSpec = KeyGenParameterSpec.Builder( KEYSTORE_ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) // 1 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .setUserAuthenticationRequired(true) // 2 .setUserAuthenticationValidityDurationSeconds(120) // 3 .build() val keyGenerator = KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_AES, PROVIDER) // 4 keyGenerator.init(keyGenParameterSpec) keyGenerator.generateKey()
Here’s what’s happening:
- You chose GCM, a popular and safe-block mode that the encryption uses.
-
You require a lock screen to be set up and the key locked until the user authenticates by passing in
.setUserAuthenticationRequired(true)
. Enabling the requirement for authentication also revokes the key when the user removes or changes the lock screen. -
You made the key available for 120 seconds from password authentication with
.setUserAuthenticationValidityDurationSeconds(120)
. Passing in-1
requires fingerprint authentication every time you want to access the key. -
You create
KeyGenerator
with the above settings and set it theAndroidKeyStore
PROVDER
.
There are a few more options worth mentioning:
setRandomizedEncryptionRequired(true) .setUserAuthenticationValidWhileOnBody(boolean remainsValid)
Because you use the same key and cipher in different parts of the app, add the following helper functions to Encryption.kt
, under the companion
code block:
private fun getSecretKey(): SecretKey { val keyStore = KeyStore.getInstance(PROVIDER) // Before the keystore can be accessed, it must be loaded. keyStore.load(null) return keyStore.getKey(KEYSTORE_ALIAS, null) as SecretKey } private fun getCipher(): Cipher { return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_GCM + "/" + KeyProperties.ENCRYPTION_PADDING_NONE) }
The first function returns the secret key from the keystore. The second one returns a pre-configured Cipher
.
Encrypting Data
You’ve stored the key in the KeyStore. Next, you’ll update the login method to encrypt the user’s generated password using the Cipher
object, given the SecretKey
. In the Encryption
class, replace the contents of createLoginPassword()
with the following:
val cipher = getCipher() val secretKey = getSecretKey() val random = SecureRandom() // 1 val passwordBytes = ByteArray(256) random.nextBytes(passwordBytes) cipher.init(Cipher.ENCRYPT_MODE, secretKey) val ivParameters = cipher.parameters.getParameterSpec(GCMParameterSpec::class.java) // 2 val iv = ivParameters.iv PreferencesHelper.saveIV(context, iv) return cipher.doFinal(passwordBytes) // 3
Here’s what’s happening:
-
You create a random password using
SecureRandom
. - You gather a randomized initialization vector (IV) required to decrypt the data and save it into the shared preferences.
-
Your return a
ByteArray
containing the encrypted data.
Decrypting to a Byte Array
You’ve encrypted the password, so now you’ll need to decrypt it when the user authenticates with a server. Replace the contents of decryptPassword()
with below:
val cipher = getCipher() val secretKey = getSecretKey() val iv = PreferencesHelper.iv(context) // 1 val ivParameters = GCMParameterSpec(128, iv) cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameters) // 2 return cipher.doFinal(password) // 3
Here’s what’s happening:
- You retrieve the IV required to decrypt the data.
-
You initialize
Cipher
usingDECRYPT_MODE
. -
You return a decrypted
ByteArray
.
Back in MainActivity.kt
, find performLoginOperation()
. Replace the call to createDataSource
where it says //TODO: Replace with encrypted data source below
:
val encryptedInfo = createLoginPassword(this) createDataSource(it, encryptedInfo)
On sign up, you create a password for the account. Right after the //TODO: Replace below with implementation that decrypts password
, replace success = true
with the following:
val password = decryptPassword(this, Base64.decode(firstUser.password, Base64.NO_WRAP)) if (password.isNotEmpty()) { //Send password to authenticate with server etc success = true }
On log in, you retrieve the password to authenticate with a server. The app shouldn’t work without the key. Build and run. Then try to log in. You should encounter the following exception:
That’s because no key was created on the previous sign up.
Delete the app to remove the old saved state. Then rebuild and run the app. You should now be able to log in. :]
You’ll notice most security functions work with ByteArray
or CharArray
, instead of objects such as String
. That’s because String
is immutable. 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 them in a mutable array. Overwrite sensitive arrays when you’re done with them like this:
Arrays.fill(array, 0.toByte())
You’ve created an encrypted password that will only be available once you’ve authenticated with your credentials. Your data is safely guarded.
Where to Go From Here?
Congratulations! You’ve discovered a lot about data privacy. Your users can now trust you to protect their data and following the best practices you’ve learned will help you repay that confidence.
Feel free to download the completed final project using the Download Materials button at the top or bottom of this tutorial.
In this tutorial, you used encryption at a high level to protect data. To learn the finer details of advanced encryption, see the Encryption Tutorial for Android tutorial.
You also tightened the user’s data at rest. To protect it during transit, see the Securing Network Data tutorial.
Check out SafetyNet API for some cool features. These include device integrity checking, a Safe Browsing API to check for malicious URLs, and a reCAPTCHA API to protect your app from spammers and other malicious traffic.
Finally, 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 beforehand. Explore theNull Safety tutorial for sample code that wipes over files with data.
Last but not least, feel free to comment in the discussion below!
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
像程序员一样思考
V. Anton Spraul / 徐波 / 人民邮电出版社 / 2013-6 / 49.00元
编程的真正挑战不是学习一种语言的语法,而是学习创造性地解决问题,从而构建美妙的应用。《像程序员一样思考》分析了程序员解决问题的方法,并且教授你其他图书所忽略的一种能力,即如何像程序员一样思考。全书分为8章。第1章通对几个经典的算法问题切入,概括了问题解决的基本技巧和步骤。第2章通过实际编写C++代码来解决几个简单的问题,从而让读者进一步体会到问题解决的思路和应用。第3到7章是书中的主体部分,分别探......一起来看看 《像程序员一样思考》 这本书的介绍吧!