In this chapter, I want to look at how you can understand the integrity of your app and the integrity of the device that your app runs on. Let’s take those two topics and drill down a bit further.
On the app side, when you write and publish an app, you want to ensure that the app that is running on the end user’s device is indeed your app. In cases of commercial apps or apps that have a commercial component to it within the app, for example, buying additional features, you will want to make sure that the code being run and the back end that it is talking to know that it was built by you or your team. The reason you would want this check in place is if an attacker reverse engineers and modifies your app. For instance, an attacker can try to “purchase” or obtain certain paid content or features from your app by reverse engineering and patching your app. For instance, he can bypass a trial period check by patching the check to see if the trial period has expired. To do this, he would have to reverse engineer the app, edit the code, rebuild the app, sign it with his own key, and install it on his own device. Your app, without having a means to verify where it was actually installed from, will continue to work. App Licensing aims to correct this attack vector by working together with Google Play’s licensing server to make sure the app was legitimately downloaded or purchased through the Play Store. Any modified, side-loaded apps can be filtered out and disabled by this mechanism. I cover how to get App Licensing work in the first edition of this book in Chapter 9 – “Publishing and Selling Your Apps.” The concept itself remains the same; however, for an updated set of documentation, please see here: https://developer.android.com/google/play/licensing.
- 1.
First, whenever a user of the app requests a feature or an action, the app will make a request to the back end to request a nonce.
- 2.
The server responds with the nonce. This nonce is important for the lifetime of the entire transaction, and it prevents replay attacks. The nonce should be completely random and used only once.
- 3.
The app calls the SafetyNet Attestation API in Google SafetyNet services on the device.
- 4.
The SafetyNet Attestation API will evaluate the device state and then send a request to the back-end server of the Google SafetyNet Attestation API to reply with a signed response giving back the results of the attestation.
- 5.
Google’s servers return a signed set of attestation results.
- 6.
The SafetyNet services respond with the results to your app.
- 7.
Using this set of results, your app calls your back end to fetch whatever user request there was.
- 8.
Your back end decides whether or not to honor the request for services based on the results of the attestation. If the results show that the device has been tampered or has root, then you can choose not to interact with the device.
It is important to note the involvement of the back-end server in this whole transaction flow. Ultimately, it is the back-end server that enjoys a higher level of security and should be more trusted than getting results from the device. The device itself could be compromised and return a “Yup! I’m all safe over here” response, but that could entirely be fabricated. Hence, the need for the final validation from the server. Prior studies that have been done by independent companies have shown a very small percentage of developers or apps that make use of Google’s SafetyNet. This is understandable when you consider the amount of extra work that you have to do as a developer to get this implemented. The ideal scenario is that you write this into some sort of helper library and have it available for all your in-house developers to use. You can choose to enforce this at build time by doing a static source code assessment and failing the build if you detect no use of SafetyNet in all critical requests to the back end. The requests to the SafetyNet API are expensive and will add overhead to your requests. Therefore, you may want to consider that as well. You could also choose to make a single recurring call for the duration of your app usage, but this may not always work out, especially if your app is not long running or gets used for a prolonged duration of time.
Now let’s see how all this looks like in an app. For this demo, we will build both our app and our back end. Let’s first define what we want to do. Let’s say that we are a delivery bike rider and that we want to set our status as ready to accept deliveries from the back end. Let’s build out our application first. Let’s install Android Studio first so that we can use it to build our app. Visit https://developer.android.com/studio and click the Download Android Studio button. I am on a Mac, so these instructions will be MacOS focused; where possible, I will try to give Windows equivalents. Read and agree to the terms of use for Android Studio and download the package. Once downloaded, double-click to open the disk image. Drag the Android Studio icon into the Applications folder to install. In Windows, when you are asked if you want to allow the app to make changes on your device, verify that it says Android Studio and click Yes. At the first screen, click Next. Then you’re asked to pick components to install; leave the default ones and click Next. Then you’re asked to pick an install location. For this, as well, leave the default and click Next. Lastly, click Install on the window that comes up and wait while the files are installed. When the installation is complete, click Next on the resulting window and then click Finish to start Android Studio.
I am including the full source code for you here, and you can also find both the client and the server source code at https://github.com/sheran/aas2-ch03-android_attest_client and https://github.com/sheran/aas2-ch03-go_attest_backend, respectively. I do, however, want to go over two of our classes which we use in the project and break down what they do:
The MainActivity file is not very busy as such. We have a simple switch widget that we have added to our Activity, and we check whether it is moved into the checked position or not in lines 28–29. If it is enabled, then we begin our process for attestation – lines 30–31.
In our Attest.kt file, we have three methods in lines 20, 32, and 46. The first method, getNonce() , does just that; it fetches a nonce from our back-end server. Once it gets a nonce, it calls the requestAttest() method and passes this nonce to that method. This is the method that then queries Android SafetyNet APIs. Behind the scenes, the Android SafetyNet library makes a call back to Google’s APIs. Since you end up accessing Google’s APIs for this, you will need to get your own API key in order to call this specific endpoint. I will show you the steps in getting your API key later on. After you receive the result of the call, you are then ready to decide whether to allow or disallow the specific request that was just made. The best way to do this is, once again, on the server side as opposed to the client side. The client side can easily be intercepted and altered, and that is why we should never fully trust the end user’s device that he runs our apps on. That is why we have the validate() method. This last method will send the entire response from Android SafetyNet back to your own back-end server where you can decide whether or not to honor the request. We will talk about this more, but let’s first do two things: build a back end and get an API key.
API Key
As mentioned before, Google requires you to register for an API key before you call the SafetyNet Attestation API. You will need to have a Gmail account or a G Suite account to do this, so have this handy. In your browser, visit this URL: https://console.developers.google.com/apis/library and sign in if required.
Building the Back End
Once again, the back-end source code will be provided, but I want to share with you the basics. The back end can be written in multiple languages; I have written mine in Go. I will give you a pseudocode interpretation of the endpoints so that you can effectively build it in any other language if required.
A requirement for using Volley in Android on an actual device is that you are forced to use HTTPS rather than a plaintext HTTP. Therefore, you will need to get a server certificate to run the back end. I cover how to create a server certificate in Chapter 9. In our preceding code, you will notice two endpoints – /nonce and /validate. These two generate and reply with a unique nonce and validate a returned SafetyNet result, respectively.
Our nonce generation code is on lines 78–93. We take the current timestamp of the server in seconds, concatenate a set of random bytes, and then generate a SHA256 hash with that data. We then Base64 encode the SHA256 hash that we generated. We then return this Base64 string as our nonce.
Pseudocode for the Back End
- 1.
Get server timestamp in seconds, convert to bytes, and concatenate 8 random bytes.
- 2.
SHA256 hash the number from 1 and Base64 encode it before returning it to the requester.
- 1.
Split the JWS Result into three parts with separator as the "." character.
- 2.
Base64 decode the first part, then take the first certificate in the array of the label "x5c".
- 3.
Extract the public key from 2 as pubkey.
- 4.
Base64 decode the second part from 1.
- 5.
Using pubkey and the algorithm from the label "alg" of point 1, verify the first two parts from point 1. They should match the third part from point 1.
- 6.
On correct verification from point 1, verify that the labels ctsProfileMatch and basicIntegrity are both true. If not, the device has been tampered with.
Validation
When Android SafetyNet responds to your attestation request, you get a long Base64 encoded string. I found that there was insufficient information on the Google website that details the structure of the response. Instead, there is a link that sends you to the RFC [https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-36] for JSON Web Signature (JWS) which is essentially the result that is given to us. A JSON Web Signature (JWS) is serialized with a URL-safe encoding called JSON Compact Serialization and is represented as three separate sections concatenated to each other and separated by the “ . ” character. The first section is the Protected Header, then next is the Payload, and the third is the Signature. Another alternative representation of JWS is the JWS JSON Serialization which has four parts. These are Protected Header, Unprotected Header, Payload, and Signature. In my experiments so far, the result has always been the JSON Compact Serialization with the three sections. I was unable to find out on Google’s documentation pages the type of response returned. In any case, it may make sense to check for the number of sections there are. I would expect, at minimum, some detail outlining the format that was returned, but as of writing this book, there is none.
If the signature can be successfully verified, then you can trust the payload that was received. In our back-end code, you will see the signature verification process beginning at line 43. We use the Go library called go-jose developed and open sourced by Square to do our verification. If at any point during the verification process we generate an error, then we mark the entire process as failing the verification process. This is a full-on fail closed situation. You may want to consider how you want to handle the failure as you may want to be less strict. Once we verify that we can trust our payload successfully, we move on to the actual response to our attestation request from Google.
The Payload
Line 2 has the original nonce that we generated at the beginning of this attestation process. In our example, we are not doing anything with our nonces, but they ideally should be generated, placed in a temporary database such as Redis or a file, and then marked as used when the request completes its full cycle. The purpose of the nonce is so that an attacker can’t send the same request to us to find out more information about how our payloads are structured. If we detect the same nonce being sent to us after we marked it as used, then we know not to take further action and to drop the request.
You can further enhance the effectiveness of the nonce by giving it a certain time to live. So the instant that we generate a nonce, we also generate an expiry time of, let’s say, two minutes. This means that we have a two-minute window in which to receive our attestation response. Anything arriving later than that is disregarded, and the client has to start a new attestation cycle once again.
If you look at our back-end code on lines 111–117, you will see where we decide whether to validate the request or not. We are using a strict approach here and only returning true if a device reports that both ctsProfileMatch and basicIntegrity are true. Further information regarding the parameters and their explanations can be found here: https://developer.android.com/training/safetynet/attestation#use-response-server.
Can This Be Bypassed?
So, Why Don’t Many People Use SafetyNet?
I think because it’s quite cumbersome and gives the developer yet another set of code to maintain outside of the normal set of app features. Also, I think for the longest time, it was possible to report false information about the device itself to SafetyNet if you used frameworks like Magisk. So, people most likely decided to skip the extra work that they would have had to do in getting SafetyNet up and running. I think differently, however, especially now with the more effective way of preventing manipulation even by frameworks such as Magisk. I think having SafetyNet Attestation as a complement to your app’s functionality is certainly a good thing and gives you a greater degree of control when it comes to protecting your app.