diff --git a/README.md b/README.md index 7a9c967..f1f73db 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Secondly, add the dependency in your app's `build.gradle`: ``` dependencies { - implementation 'com.github.approov:approov-service-retrofit:2.9.0' + implementation 'com.github.approov:approov-service-retrofit:3.0.0' } ``` @@ -83,12 +83,14 @@ object ClientInstance { .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) } - return ShapesApp.approovService.getRetrofit(retrofitBuilder!!) + return YourApp.approovService.getRetrofit(retrofitBuilder!!) } } ``` -This obtains a retrofit instance includes an `OkHttp` interceptor to add the `Approov-Token` header and pins the connections. +This obtains a retrofit instance includes an `OkHttp` interceptor that protects channel integrity (with either pinning or managed trust roots). The interceptor may also add `Approov-Token` or substitute app secret values, depending upon your integration choices. You should thus use this client for all API calls you may wish to protect. + +Approov errors will generate an `ApproovException`, which is a type of `IOException`. This may be further specialized into an `ApproovNetworkException`, indicating an issue with networking that should provide an option for a user initiated retry (which must make the new request with a call to the `getRetrofit` to get the latest client). ## CUSTOM OKHTTP BUILDER By default, the Retrofit instance gets a default client constructed with a default `OkHttpClient`. However, your existing code may use a customized `OkHttpClient` with, for instance, different timeouts or other interceptors. For example, if you have existing code: @@ -112,5 +114,11 @@ Initially you won't have set which API domains to protect, so the interceptor wi Your Approov onboarding email should contain a link allowing you to access [Live Metrics Graphs](https://approov.io/docs/latest/approov-usage-documentation/#metrics-graphs). After you've run your app with Approov integration you should be able to see the results in the live metrics within a minute or so. At this stage you could even release your app to get details of your app population and the attributes of the devices they are running upon. -However, to actually protect your APIs there are some further steps you can learn about in [Next Steps](https://github.com/approov/quickstart-android-kotlin-retrofit/blob/master/NEXT-STEPS.md). +## NEXT STEPS +To actually protect your APIs there are some further steps. Approov provides two different options for protecting APIs: + +* [TOKEN PROTECTION](https://github.com/approov/quickstart-android-kotlin-retrofit/blob/master/TOKEN-PROTECTION.md): You should use this if you control the backend API(s) being protected and are able to modify them to ensure that a valid Approov token is being passed by the app. An [Approov Token](https://approov.io/docs/latest/approov-usage-documentation/#approov-tokens) is short lived crytographically signed JWT proving the authenticity of the call. + +* [SECRET PROTECTION](https://github.com/approov/quickstart-android-kotlin-retrofit/blob/master/SECRET-PROTECTION.md): If you do not control the backend API(s) being protected, and are therefore unable to modify it to check Approov tokens, you can use this approach instead. It allows app secrets, and API keys, to be protected so that they no longer need to be included in the built code and are only made available to passing apps at runtime. +Note that it is possible to use both approaches side-by-side in the same app, in case your app uses a mixture of 1st and 3rd party APIs. diff --git a/SECRET-PROTECTION.md b/SECRET-PROTECTION.md new file mode 100644 index 0000000..b583833 --- /dev/null +++ b/SECRET-PROTECTION.md @@ -0,0 +1,117 @@ +# Secret Protection +You should use this option if you wish to protect access to 3rd party or managed APIs where you are not able to add an Approov token check to the backend. This allows client secrets, or API keys, used for access to be protected with Approov. Rather than build secrets into an app where they might be reverse engineered, they are only provided at runtime by Approov for apps that are pass Approov attestation. This substantially improves your protection and prevents these secrets being abused by attackers. Where you are able to modify the backend we recommend you use Token Protection for further enchanced flexibility and security. + +This quickstart provides straigtforward implementation if the secret is currently supplied in a request header to the API. The quickstart interceptor is able to automatically rewrite headers as the requests are being made, to automatically substitute in the secret, but only if the app has passed the Approov attestation checks. If the app fails its checks then you can add a custom [rejection](#handling-rejections) handler. + +These additional steps require access to the [Approov CLI](https://approov.io/docs/latest/approov-cli-tool-reference/), please follow the [Installation](https://approov.io/docs/latest/approov-installation/) instructions. + +## ENABLING MANAGED TRUST ROOTS +Client secrets or API keys also need to be protected in transit. For 3rd party APIs you should not pin against their certificates since you are not in control of when they might changed. Instead the [Managed Trust Roots](https://approov.io/docs/latest/approov-usage-documentation/#managed-trust-roots) feature can be used to protect TLS. + +Ensure managed trust roots are enabled using: + +``` +approov pin -setManagedTrustRoots on +``` + +This ensures connections may only use official certificates, and blocks the use of self signed certificates that might be used by a Man-in-the-Middle (MitM) attacker. + +## ADDING API DOMAINS +In order for secrets to be protected for particular API domains it is necessary to inform Approov about them. Execute the following command: + +``` +approov api -add -noApproovToken +``` + +This informs Approov that it should be active for the domain, but does not need to send Approov tokens for it. Adding the domain ensures that the channel will be protected against Man-in-the-Middle (MitM) attacks. + +## MIGRATING THE SECRET INTO APPROOV +It is assumed that you already have some client secrets and/or API keys in your app that you would like to migrate for protection by Approov. To do this you first need to enable the [Secure Strings](https://approov.io/docs/latest/approov-usage-documentation/#secure-strings) feature: + +``` +approov secstrings -setEnabled +``` + +The quickstart integration works by allowing you to replace the secret in your app with a placeholder value instead, and then the placeholder value is mapped to the actual secret value on the fly by the interceptor (if the app passes Approov attestation). The shipped app code will only contain the placeholder values. + +If your app currently uses `` then replace it in your app with the value ``. Choose a suitable placeholder name to reflect the type of the secret. The placeholder value will be added to requests in the normal way, but you should be using the Approov enabled networking client to perfom the substituion. + +You must inform Approov that it should substitute `` for `` in requests as follows: + +``` +approov secstrings -addKey -predefinedValue +``` + +If the secret value is provided on the header `` then it is necessary to notify the `ApproovService` that the header is subject to substitution. You do this by making the call once, after initialization: + +```Java +YourApp.approovService.addSubstitutionHeader("", null); +``` + +With this in place the Approov interceptor should replace the `` with the `` as required when the app passes attestation. You can add up to 16 different secret values to be substituted in this way. Since the mapping lookup is performed on the placeholder value you have the flexibiluty of providing different secrets on different API calls, even if they passed with the same header name. + +Since earlier released versions of the app may have already leaked the ``, you may wish to refresh the secret at some later point when any older version of the app is no longer in use. You can of course do this update over-the-air using Approov without any need to modify the app. + +## REGISTERING APPS +In order for Approov to recognize the app as being valid it needs to be registered with the service. Change the directory to the top level of your app project and then register the app with Approov: + +``` +approov registration -add app/build/outputs/apk/debug/app-debug.apk +``` +Note, on Windows you need to substitute \ for / in the above command. + +> **IMPORTANT:** The registration takes up to 30 seconds to propagate across the Approov Cloud Infrastructure, therefore don't try to run the app again before this time as elapsed. During development of your app you can ensure it [always passes](https://approov.io/docs/latest/approov-usage-documentation/#adding-a-device-security-policy) on your device to not have to register the APK each time you modify it. + +[Managing Registrations](https://approov.io/docs/latest/approov-usage-documentation/#managing-registrations) provides more details for app registrations, especially for releases to the Play Store. Note that you may also need to apply specific [Android Obfuscation](https://approov.io/docs/latest/approov-usage-documentation/#android-obfuscation) rules for your app when releasing it. + +## HANDLING REJECTIONS +If the app is not recognized as being valid by Approov then an `ApproovRejectionException` is thrown on the request and the API call is not completed. The secret value will never be communicated to the app in this case. + +Your app should specifically catch this exception and provide some feedback to the user to explain why the app is not working. The `ApproovRejectionException` has a `geARC()` method which provides an [Attestation Response Code](https://approov.io/docs/latest/approov-usage-documentation/#attestation-response-code) which can provide more information about the status of the device, without revealing any details to the end user. + +If you wish to provide more direct feedback then enable the [Rejection Reasons](https://approov.io/docs/latest/approov-usage-documentation/#rejection-reasons) feature: + +``` +approov policy -setRejectionReasons on +``` + +You will then be able to use `getRejectionReasons()` on an `ApproovRejectionException` to obtain a comma separated list of [device properties](https://approov.io/docs/latest/approov-usage-documentation/#device-properties) responsible for causing the rejection. + +## FURTHER OPTIONS + +See [Getting Started With Approov](https://approov.io/docs/latest/approov-usage-documentation/#getting-started-with-approov) for information about additional Approov features you may wish to try. + +The quickstart also provides the following additional methods: + +### Header Prefixes +In some cases the value to be substituted on a header may be prefixed by some fixed string. A common case is the presence of `Bearer` included in an authorization header to indicate the use of a bearer token. In this case you can specify a prefix as follows: + +``` +YourApp.approovService.addSubstitutionHeader("Authorization", "Bearer "); +``` + +This causes the `Bearer` prefix to be stripped before doing the lookup for the substitution, and the `Bearer` prefix added to the actual secret value as part of the substitution. + +### App Instance Secure Strings +As shown, it is possible to set predefined secret strings that are only communicated to passing apps. It is also possible to get and set secure string values for each app instance. These are never communicated to the Approov cloud service, but are encrypted at rest using keys which can only be retrieved by passing apps. + +Use the the following method in `ApproovService`: + +``` +public static String fetchSecureString(String key, String newDef) throws ApproovException +``` + +to lookup a secure string with the given `key`, returning `null` if it is not defined. Note that you should never cache this value in your code. You may define a new value for the `key` by passing a new value in `newDef` rather than `null`. + +Note that this method may make networking calls so should never be called from the main UI thread. The call may also fail with an `ApproovException`. If this is of type `ApproovNetworkException` then a retry should be performed as the issue is temporary and network related. If `ApproovRejectionException` is thrown then the app has not passed Approov attestation and some user feedback should be provided. + +This method is also useful for providing runtime secrets protection when the values are not passed on headers. Secure strings set using this method may also be looked up using subsequent networking interceptor header substitutions. + +### Prefetching +If you wish to reduce the latency associated with substituting the first secret, then make this call immediately after creating `ApproovService`: + +```Java +YourApp.approovService.prefetch() +``` + +This initiates the process of fetching the required information as a background task, so that it is available immediately when subsequently needed. Note the information will automatically expire after approximately 5 minutes. diff --git a/SHAPES-EXAMPLE.md b/SHAPES-EXAMPLE.md index c40e3f7..668839c 100644 --- a/SHAPES-EXAMPLE.md +++ b/SHAPES-EXAMPLE.md @@ -51,7 +51,7 @@ The `approov-service-retrofit` dependency needs to be added as follows to the `a Note that in this case the dependency has been added with the tag `main-SNAPSHOT`. For your projects we recommend you add a dependency to a specific version. ``` -implementation 'com.github.approov:approov-service-retrofit:2.9.0' +implementation 'com.github.approov:approov-service-retrofit:3.0.0' ``` Make sure you do a Gradle sync (by selecting `Sync Now` in the banner at the top of the modified `.gradle` file) after making these changes. @@ -86,6 +86,10 @@ Instead of constructing the `Retrofit` object lazily here we construct the build The `ApproovService` constructs a custom `OkHttpClient` that adds the `Approov-Token` header and also applies pinning for the connections to ensure that no Man-in-the-Middle can eavesdrop on any communication being made. If the pins are changed then a new `Retrofit` instance is automatically created. +You should edit the `ShapesService.kt` file to change to using the shapes `https://shapes.approov.io/v3/shapes/` endpoint. This checks Approov tokens (as well as the API key built into the app): + +![Shapes V3 Endpoint](readme-images/shapes-v3-endpoint.png) + Run the app again to ensure that the `app-debug.apk` in the generated build outputs is up to date. ## REGISTER YOUR APP WITH APPROOV @@ -99,7 +103,7 @@ Note, on Windows you need to substitute \ for / in the above command. > **IMPORTANT:** The registration takes up to 30 seconds to propagate across the Approov Cloud Infrastructure, therefore don't try to run the app again before this time as elapsed. During development of your app you can ensure your device [always passes](https://approov.io/docs/latest/approov-usage-documentation/#adding-a-device-security-policy) so you do not have to register the APK each time you modify it. -## RUNNING THE SHAPES APP WITH APPROOV +## SHAPES APP WITH APPROOV TOKEN PROTECTION Run the app again without making any changes to the app and press the `Get Shape` button. You should now see this (or another shape): @@ -120,3 +124,43 @@ If you still don't get a valid shape then there are some things you can try. Rem * Consider using an [Annotation Policy](https://approov.io/docs/latest/approov-usage-documentation/#annotation-policies) during initial development to directly see why the device is not being issued with a valid token. * Use `approov metrics` to see [Live Metrics](https://approov.io/docs/latest/approov-usage-documentation/#live-metrics) of the cause of failure. * You can use a debugger or emulator and get valid Approov tokens on a specific device by ensuring it [always passes](https://approov.io/docs/latest/approov-usage-documentation/#adding-a-device-security-policy). As a shortcut, when you are first setting up, you can add a [device security policy](https://approov.io/docs/latest/approov-usage-documentation/#adding-a-device-security-policy) using the `latest` shortcut as discussed so that the `device ID` doesn't need to be extracted from the logs or an Approov token. + +## SHAPES APP WITH SECRET PROTECTION + +This section provides an illustration of an alternative option for Approov protection if you are not able to modify the backend to add an Approov Token check. Firstly, revert any previous change to `ShapesService.java` so that it uses `https://shapes.approov.io/v1/shapes/`. This endpoiint simply checks for an API key: + +![Shapes V1 Endpoint](readme-images/shapes-v1-endpoint.png) + +Next, the `res/values/strings.xml` file needs to be changed with the `shapes_api_key` entry modified to `shapes_api_key_placeholder`. Tis removss the actual API key out of the code: + +![Placeholder API Key](readme-images/placeholder-api-key.png) + +Next we enable the [Secure Strings](https://approov.io/docs/latest/approov-usage-documentation/#secure-strings) feature: + +``` +approov secstrings -setEnabled +``` + +You must inform Approov that it should map `shapes_api_key_placeholder` to `yXClypapWNHIifHUWmBIyPFAm` (the actual API key) in requests as follows: + +``` +approov secstrings -addKey shapes_api_key_placeholder -predefinedValue yXClypapWNHIifHUWmBIyPFAm +``` + +Next we need to inform Approov that it needs to substitute the placeholder value for the real API key on the `Api-Key` header. Only a single line of code needs to be changed at `io/approov/shapes/ShapesClientInstance.kt:52`: + +![Approov Substitute Header](readme-images/approov-subs-header.png) + +Build and run the app again to ensure that the `app-debug.apk` in the generated build outputs is up to date. You need to register the updated app with Approov. Change directory to the top level of the `shapes-app` project and then register the app with: + +``` +approov registration -add app/build/outputs/apk/debug/app-debug.apk +``` +Run the app again without making any changes to the app and press the `Get Shape` button. You should now see this (or another shape): + +

+ +

+ +This means that the registered app is able to access the API key, even though it is no longer embedded in the app configuration, and provide it to the shapes request. + diff --git a/NEXT-STEPS.md b/TOKEN-PROTECTION.md similarity index 54% rename from NEXT-STEPS.md rename to TOKEN-PROTECTION.md index b100d53..65c9cfc 100644 --- a/NEXT-STEPS.md +++ b/TOKEN-PROTECTION.md @@ -1,10 +1,10 @@ -# Next Steps -Once you have finished a basic integration of the SDK and are able to see metrics from it, there are some further steps to provide full Approov integration. +# Token Protection +You should use this option if you wish to protect access to your APIs using an Approov token. We recommend this approach where it is possible to modify the backend API implementation to perform the token verification. Various [Backend API Quickstarts](https://approov.io/docs/latest/approov-integration-examples/backend-api/) are available to suit your particular situation depending on the backend technology used. You will need to implement this in addition to the steps in this frontend guide. These steps require access to the [Approov CLI](https://approov.io/docs/latest/approov-cli-tool-reference/), please follow the [Installation](https://approov.io/docs/latest/approov-installation/) instructions. ## ADDING API DOMAINS -In order for Approov tokens to be added by the interceptor for particular API domains is necessary to inform Approov about them. Execute the following command: +In order for Approov tokens to be added by the interceptor for particular API domains it is necessary to inform Approov about them. Execute the following command: ``` approov api -add @@ -14,30 +14,22 @@ Approov tokens will then be added automatically to any requests to that domain ( Note that this will also add a public key certicate pin for connections to the domain to ensure that no Man-in-the-Middle attacks on your app's communication are possible. Please read [Managing Pins](https://approov.io/docs/latest/approov-usage-documentation/#public-key-pinning-configuration) to understand this in more detail. ## REGISTERING APPS -In order for Approov to recognize the app as being valid it needs to be registered with the service. Change directory to the top level of your app project and then register the app with Approov: +In order for Approov to recognize the app as being valid it needs to be registered with the service. Change the directory to the top level of your app project and then register the app with Approov: ``` approov registration -add app/build/outputs/apk/debug/app-debug.apk ``` Note, on Windows you need to substitute \ for / in the above command. -> **IMPORTANT:** The registration takes up to 30 seconds to propagate across the Approov Cloud Infrastructure, therefore don't try to run the app again before this time as elapsed. During development of your app you can ensure it [always passes](https://approov.io/docs/latest/approov-usage-documentation/#adding-a-device-security-policy) on your device so you do not have to register the APK each time you modify it. +> **IMPORTANT:** The registration takes up to 30 seconds to propagate across the Approov Cloud Infrastructure, therefore don't try to run the app again before this time as elapsed. During development of your app you can ensure it [always passes](https://approov.io/docs/latest/approov-usage-documentation/#adding-a-device-security-policy) on your device to not have to register the APK each time you modify it. [Managing Registrations](https://approov.io/docs/latest/approov-usage-documentation/#managing-registrations) provides more details for app registrations, especially for releases to the Play Store. Note that you may also need to apply specific [Android Obfuscation](https://approov.io/docs/latest/approov-usage-documentation/#android-obfuscation) rules for your app when releasing it. -## BACKEND INTEGRATION -In order to fully implement Approov you must verify the Approov token in your backend API. Various [Backend API Quickstarts](https://approov.io/docs/latest/approov-integration-examples/backend-api/) are availble to suit your particular situation. - -## OTHER FEATURES -There are various other Approov capabilities that you may wish to explore: +## FURTHER OPTIONS -* Update your [Security Policy](https://approov.io/docs/latest/approov-usage-documentation/#security-policies) that determines the conditions under which an app will be given a valid Approov token. -* Learn how to [Manage Devices](https://approov.io/docs/latest/approov-usage-documentation/#managing-devices) that allows you to change the policies on specific devices. -* Understand how to provide access for other [Users](https://approov.io/docs/latest/approov-usage-documentation/#user-management) of your Approov account. -* Learn about [Automated Approov CLI Usage](https://approov.io/docs/latest/approov-usage-documentation/#automated-approov-cli-usage). -* Investigate other advanced features, such as [Offline Security Mode](https://approov.io/docs/latest/approov-usage-documentation/#offline-security-mode), [SafetyNet Integration](https://approov.io/docs/latest/approov-usage-documentation/#google-safetynet-integration) and [Android Automated Launch Detection](https://approov.io/docs/latest/approov-usage-documentation/#android-automated-launch-detection). +See [Getting Started With Approov](https://approov.io/docs/latest/approov-usage-documentation/#getting-started-with-approov) for information about additional Approov features you may wish to try. -## FURTHER OPTIONS +The quickstart also provides the following additional methods: ### Changing Approov Token Header Name The default header name of `Approov-Token` can be changed as follows: @@ -55,16 +47,13 @@ If want to use [Token Binding](https://approov.io/docs/latest/approov-usage-docu YourApp.approovService.setBindingHeader("Authorization") ``` -In this case it means that the value of `Authorization` holds the token value to be bound. This only needs to be called once. On subsequent requests the value of the specified header is read and its value set as the token binding value. Note that you should only select a header that is normally always present and the value does not typically change from request to request, as each change requires a new Approov token to be fetched. +In this case it means that the value of `Authorization` holds the token value to be bound. This only needs to be called once. On subsequent requests the value of the specified header is read and its value set as the token binding value. Note that you should select a header whose value does not typically change from request to request, as each change requires a new Approov token to be fetched. -### Token Prefetching +### Prefetching If you wish to reduce the latency associated with fetching the first Approov token, then make this call immediately after creating `ApproovService`: ```Java -YourApp.approovService.prefetchApproovToken() +YourApp.approovService.prefetch() ``` This initiates the process of fetching an Approov token as a background task, so that a cached token is available immediately when subsequently needed, or at least the fetch time is reduced. Note that there is no point in performing a prefetch if you are using token binding. - -### Changing Configuration Persistence -An Approov app automatically downloads any new configurations of APIs and their pins that are available. These are stored in the [`SharedPreferences`](https://developer.android.com/reference/android/content/SharedPreferences) for the app in a preference file `approov-prefs` and key `approov-config`. You can store the preferences differently by modifying or overriding the methods `ApproovService.putApproovDynamicConfig` and `ApproovService.getApproovDynamicConfig`. diff --git a/readme-images/approov-fetch.png b/readme-images/approov-fetch.png index 7db0bad..93f5506 100644 Binary files a/readme-images/approov-fetch.png and b/readme-images/approov-fetch.png differ diff --git a/readme-images/approov-init-code.png b/readme-images/approov-init-code.png index b6869fb..235ab8d 100644 Binary files a/readme-images/approov-init-code.png and b/readme-images/approov-init-code.png differ diff --git a/readme-images/approov-subs-header.png b/readme-images/approov-subs-header.png new file mode 100644 index 0000000..a31158b Binary files /dev/null and b/readme-images/approov-subs-header.png differ diff --git a/readme-images/placeholder-api-key.png b/readme-images/placeholder-api-key.png new file mode 100644 index 0000000..9adc73d Binary files /dev/null and b/readme-images/placeholder-api-key.png differ diff --git a/readme-images/shapes-v1-endpoint.png b/readme-images/shapes-v1-endpoint.png new file mode 100644 index 0000000..67c7cac Binary files /dev/null and b/readme-images/shapes-v1-endpoint.png differ diff --git a/readme-images/shapes-v3-endpoint.png b/readme-images/shapes-v3-endpoint.png new file mode 100644 index 0000000..1fa10ed Binary files /dev/null and b/readme-images/shapes-v3-endpoint.png differ diff --git a/shapes-app/app/build.gradle b/shapes-app/app/build.gradle index e376f3f..a7217a9 100644 --- a/shapes-app/app/build.gradle +++ b/shapes-app/app/build.gradle @@ -3,11 +3,11 @@ apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android' android { - compileSdkVersion 28 + compileSdkVersion 31 defaultConfig { applicationId "io.approov.shapes" minSdkVersion 21 - targetSdkVersion 28 + targetSdkVersion 31 versionCode 3 versionName "3.0" } @@ -34,7 +34,7 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" diff --git a/shapes-app/app/src/main/java/io/approov/shapes/HelloModel.kt b/shapes-app/app/src/main/java/io/approov/shapes/HelloModel.kt index fcb0a2a..31fa6d8 100644 --- a/shapes-app/app/src/main/java/io/approov/shapes/HelloModel.kt +++ b/shapes-app/app/src/main/java/io/approov/shapes/HelloModel.kt @@ -1,4 +1,3 @@ -// Retrofit Model of Hello Text // // MIT License // diff --git a/shapes-app/app/src/main/java/io/approov/shapes/MainActivity.kt b/shapes-app/app/src/main/java/io/approov/shapes/MainActivity.kt index 8a27f2a..9495ae3 100644 --- a/shapes-app/app/src/main/java/io/approov/shapes/MainActivity.kt +++ b/shapes-app/app/src/main/java/io/approov/shapes/MainActivity.kt @@ -1,4 +1,3 @@ -// Main activity for Approov Shapes App Demo (using Retrofit) // // MIT License // @@ -30,12 +29,12 @@ import retrofit2.Callback import retrofit2.Response class MainActivity : Activity() { - private var activity: Activity? = null - private var statusView: View? = null - private var statusImageView: ImageView? = null - private var statusTextView: TextView? = null - private var connectivityCheckButton: Button? = null - private var shapesCheckButton: Button? = null + private lateinit var activity: Activity + private lateinit var statusView: View + private lateinit var statusImageView: ImageView + private lateinit var statusTextView: TextView + private lateinit var helloCheckButton: Button + private lateinit var shapesCheckButton: Button override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) @@ -45,57 +44,60 @@ class MainActivity : Activity() { statusView = findViewById(R.id.viewStatus) statusImageView = findViewById(R.id.imgStatus) as ImageView statusTextView = findViewById(R.id.txtStatus) - connectivityCheckButton = findViewById(R.id.btnConnectionCheck) + helloCheckButton = findViewById(R.id.btnConnectionCheck) shapesCheckButton = findViewById(R.id.btnShapesCheck) - // handle connection check - connectivityCheckButton!!.setOnClickListener(View.OnClickListener { + // handle hello connection check + helloCheckButton.setOnClickListener(View.OnClickListener { // hide status - activity!!.runOnUiThread(Runnable { statusView!!.setVisibility(View.INVISIBLE) }) + activity.runOnUiThread(Runnable { statusView.setVisibility(View.INVISIBLE) }) // Make a Retrofit request to get hello text val service = retrofitInstance!!.create(ShapesService::class.java) val call = service.hello - call!!.enqueue(object : Callback { + call.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { val imgId: Int var message = "Http status code " + response.code() if (response.isSuccessful) { - Log.d(TAG, "Connectivity call successful") + Log.d(TAG, "Hello call successful") imgId = R.drawable.hello message = response.body()!!.text } else { - Log.d(TAG, "Connectivity call unsuccessful") + Log.d(TAG, "Hello call unsuccessful") imgId = R.drawable.confused } val msg = message - activity!!.runOnUiThread(Runnable { - statusImageView!!.setImageResource(imgId) - statusTextView!!.setText(msg) - statusView!!.setVisibility(View.VISIBLE) + activity.runOnUiThread(Runnable { + statusImageView.setImageResource(imgId) + statusTextView.setText(msg) + statusView.setVisibility(View.VISIBLE) }) } override fun onFailure(call: Call, t: Throwable) { - Log.d(TAG, "Connectivity call failed") + Log.d(TAG, "Hello call failed: " + t.message) val imgId = R.drawable.confused val msg = "Request failed: " + t.message - activity!!.runOnUiThread(Runnable { - statusImageView!!.setImageResource(imgId) - statusTextView!!.setText(msg) - statusView!!.setVisibility(View.VISIBLE) + activity.runOnUiThread(Runnable { + statusImageView.setImageResource(imgId) + statusTextView.setText(msg) + statusView.setVisibility(View.VISIBLE) }) } }) }) // handle getting shapes - shapesCheckButton!!.setOnClickListener(View.OnClickListener { + shapesCheckButton.setOnClickListener(View.OnClickListener { // hide status - activity!!.runOnUiThread(Runnable { statusView!!.setVisibility(View.INVISIBLE) }) + activity.runOnUiThread(Runnable { statusView.setVisibility(View.INVISIBLE) }) // Make a Retrofit request to get a shape val service = retrofitInstance!!.create(ShapesService::class.java) - val call = service.shape - call!!.enqueue(object : Callback { + val headers: MutableMap = HashMap() + val apiKey = resources.getString(R.string.shapes_api_key) + headers["Api-Key"] = apiKey + val call = service.getShape(headers) + call.enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { var imgId = R.drawable.confused val msg = "Http status code " + response.code() @@ -115,21 +117,21 @@ class MainActivity : Activity() { Log.d(TAG, "Shapes call unsuccessful") } val finalImgId = imgId - activity!!.runOnUiThread(Runnable { - statusImageView!!.setImageResource(finalImgId) - statusTextView!!.setText(msg) - statusView!!.setVisibility(View.VISIBLE) + activity.runOnUiThread(Runnable { + statusImageView.setImageResource(finalImgId) + statusTextView.setText(msg) + statusView.setVisibility(View.VISIBLE) }) } override fun onFailure(call: Call, t: Throwable) { - Log.d(TAG, "Shapes call failed") + Log.d(TAG, "Shapes call failed: " + t.message) val imgId = R.drawable.confused val msg = "Request failed: " + t.message - activity!!.runOnUiThread(Runnable { - statusImageView!!.setImageResource(imgId) - statusTextView!!.setText(msg) - statusView!!.setVisibility(View.VISIBLE) + activity.runOnUiThread(Runnable { + statusImageView.setImageResource(imgId) + statusTextView.setText(msg) + statusView.setVisibility(View.VISIBLE) }) } }) diff --git a/shapes-app/app/src/main/java/io/approov/shapes/ShapeModel.kt b/shapes-app/app/src/main/java/io/approov/shapes/ShapeModel.kt index f944de6..10cde22 100644 --- a/shapes-app/app/src/main/java/io/approov/shapes/ShapeModel.kt +++ b/shapes-app/app/src/main/java/io/approov/shapes/ShapeModel.kt @@ -1,4 +1,3 @@ -// Retrofit Model of Shapes // // MIT License // diff --git a/shapes-app/app/src/main/java/io/approov/shapes/ShapesApp.kt b/shapes-app/app/src/main/java/io/approov/shapes/ShapesApp.kt index b0f7bc0..ef63140 100644 --- a/shapes-app/app/src/main/java/io/approov/shapes/ShapesApp.kt +++ b/shapes-app/app/src/main/java/io/approov/shapes/ShapesApp.kt @@ -1,4 +1,3 @@ -// Application class for Approov Shapes App Demo (using Retrofit) // // MIT License // @@ -22,7 +21,7 @@ import android.app.Application // *** UNCOMMENT THE LINE BELOW FOR APPROOV *** //import io.approov.service.retrofit.ApproovService -class ShapesApp : Application() { +class ShapesApp: Application() { override fun onCreate() { super.onCreate() @@ -32,6 +31,6 @@ class ShapesApp : Application() { companion object { // *** UNCOMMENT THE LINE BELOW FOR APPROOV *** - //var approovService: ApproovService? = null + //lateinit var approovService: ApproovService } } \ No newline at end of file diff --git a/shapes-app/app/src/main/java/io/approov/shapes/ShapesClientInstance.kt b/shapes-app/app/src/main/java/io/approov/shapes/ShapesClientInstance.kt index a96f5b5..6fb6f1c 100644 --- a/shapes-app/app/src/main/java/io/approov/shapes/ShapesClientInstance.kt +++ b/shapes-app/app/src/main/java/io/approov/shapes/ShapesClientInstance.kt @@ -1,4 +1,3 @@ -// Retrofit Client Instance // // MIT License // @@ -47,6 +46,30 @@ object ShapesClientInstance { .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) } - return ShapesApp.approovService!!.getRetrofit(retrofitBuilder!!); + + // *** UNCOMMENT THE LINE BELOW FOR APPROOV USING SECRET PROTECTION *** + //ShapesApp.approovService.addSubstitutionHeader("Api-Key", null) + + return ShapesApp.approovService.getRetrofit(retrofitBuilder!!); }*/ -} \ No newline at end of file +} + + + + + + + + + + + + + + + + + + + + diff --git a/shapes-app/app/src/main/java/io/approov/shapes/ShapesService.kt b/shapes-app/app/src/main/java/io/approov/shapes/ShapesService.kt index 2e9288a..4753512 100644 --- a/shapes-app/app/src/main/java/io/approov/shapes/ShapesService.kt +++ b/shapes-app/app/src/main/java/io/approov/shapes/ShapesService.kt @@ -1,4 +1,3 @@ -// Retrofit Service Interface for Shapes // // MIT License // @@ -19,11 +18,24 @@ package io.approov.shapes import retrofit2.Call import retrofit2.http.GET +import retrofit2.http.HeaderMap interface ShapesService { - @get:GET("/v2/shapes") - val shape: Call? + // *** COMMENT THE LINES BELOW WHEN USING APPROOV WITH TOKEN PROTECTION *** + @GET("/v1/shapes") + // *** UNCOMMENT THE LINES BELOW WHEN USING APPROOV WITH TOKEN PROTECTION *** + //@GET("/v3/shapes") + fun getShape(@HeaderMap headers: Map): Call @get:GET("/v1/hello") - val hello: Call? -} \ No newline at end of file + val hello: Call +} + + + + + + + + + diff --git a/shapes-app/app/src/main/res/values/strings.xml b/shapes-app/app/src/main/res/values/strings.xml index a22e575..1a049de 100644 --- a/shapes-app/app/src/main/res/values/strings.xml +++ b/shapes-app/app/src/main/res/values/strings.xml @@ -3,4 +3,5 @@ Demo Say Hello Get Shape + yXClypapWNHIifHUWmBIyPFAm