diff --git a/README.md b/README.md
index e97a643..54d1ff6 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ The following app permissions need to be available in the manifest to use Approo
```
-Note that the minimum SDK version you can use with the Approov package is 21 (Android 5.0).
+Note that the minimum SDK version you can use with the Approov package is 23 (Android 6.0).
Please [read this](https://approov.io/docs/latest/approov-usage-documentation/#targeting-android-11-and-above) section of the reference documentation if targeting Android 11 (API level 30) or above.
@@ -51,9 +51,11 @@ The `` is a custom string that configures your Ap
You can then make Approov enabled `HttpsUrlConnection` API calls using the following call on any `HttpsUrlConnection` connection, just before the connection is made:
```Java
-ApproovService.addApproov(connection);
+connection = ApproovService.addApproov(connection);
```
+Always continue to use the returned `connection` instance afterwards, because the service layer may wrap the original connection when it needs to apply additional request mutations such as URL rewriting.
+
> **NOTE:** It is important that this call is made just prior to the connection being made and thus within any retry loop, to ensure that an updated Approov token is always made available on the connection request.
For API domains that are configured to be protected with an Approov token, this adds the `Approov-Token` header and pins the connection. This may also substitute header values when using secrets protection.
diff --git a/REFERENCE.md b/REFERENCE.md
index 6d2eac1..2140a29 100644
--- a/REFERENCE.md
+++ b/REFERENCE.md
@@ -26,9 +26,11 @@ It is possible to pass an empty `config` string to indicate that no initializati
Adds Approov to the given `connection`. The Approov token is added in a header and this also overrides the HostnameVerifier with something that pins the connections. If a binding header has been specified then its hash will be set if it is present. This function may also substitute header values to hold secure string secrets. If it is not possible to fetch an Approov token due to networking issues, or header substitution fails due to attestation rejection, then `ApproovException` is thrown.
```Java
-void addApproov(HttpsURLConnection connection) throws ApproovException
+HttpsURLConnection addApproov(HttpsURLConnection connection) throws ApproovException
```
+The returned `HttpsURLConnection` should always be used for subsequent calls such as `connect()`, reading the response body, and `disconnect()`. In many cases this will be the same instance that was passed in, but a wrapped connection may be returned when additional request mutation is required.
+
## SetProceedOnNetworkFail
If the provided `proceed` value is `true` then this indicates that the networking should proceed anyway if it is not possible to obtain an Approov token due to a networking failure. If this is called then the backend API can receive calls without the expected Approov token header being added, or without header/query parameter substitutions being made. This should only ever be used if there is some particular reason, perhaps due to local network conditions, that you believe that traffic to the Approov cloud service will be particularly problematic.
diff --git a/SECRETS-PROTECTION.md b/SECRETS-PROTECTION.md
index ac6c792..b6fcdbd 100644
--- a/SECRETS-PROTECTION.md
+++ b/SECRETS-PROTECTION.md
@@ -54,9 +54,9 @@ In some cases it might not be possible to automatically substitute a secret in a
In this case it is possible to make an explicit call at runtime to obtain the secret value, for apps passing attestation. Here is an example for using the required method in `ApproovService`:
```Java
-import io.approov.service.okhttp.ApproovException;
-import io.approov.service.okhttp.ApproovNetworkException;
-import io.approov.service.okhttp.ApproovRejectionException;
+import io.approov.service.httpsurlconn.ApproovException;
+import io.approov.service.httpsurlconn.ApproovNetworkException;
+import io.approov.service.httpsurlconn.ApproovRejectionException;
...
diff --git a/SHAPES-EXAMPLE.md b/SHAPES-EXAMPLE.md
index ead6c8a..52e00f6 100644
--- a/SHAPES-EXAMPLE.md
+++ b/SHAPES-EXAMPLE.md
@@ -62,7 +62,7 @@ Tokens for this domain will be automatically signed with the specific secret for
## MODIFY THE APP TO USE APPROOV
-Uncomment the three lines of Approov initialization code in `io/approov/shapes/ShapesApp.java`:
+Uncomment the Approov initialization code in `io/approov/shapes/ShapesApp.java`:

@@ -74,9 +74,11 @@ Next we need to use Approov when we make request for the shapes. Change the mark
Note you will also need to uncomment the `ApproovService` import near the start of the file.
-We pass the `HttpsUrlConnection` to the `ApproovService.addApproov` method and this automatically fetches an Approov token and adds it as a header to the request. It also pins the connection to the endpoint to ensure that no Man-in-the-Middle can eavesdrop on any communication being made.
+We pass the `HttpsUrlConnection` to the `ApproovService.addApproov` method and continue with the returned `HttpsURLConnection`. This automatically fetches an Approov token and adds it as a header to the request. It also pins the connection to the endpoint to ensure that no Man-in-the-Middle can eavesdrop on any communication being made.
-Note that this method may throw an `ApproovException` (derived from `IOException`) if it is unable to fetch an Approov token due to no or poor Internet connectivity then `ApproovNetworkException` is thrown. In this case the user should be able to initiate a retry. Therefore the call should be in a`try-catch` block, possibly the same one as [`connect`](https://developer.android.com/reference/java/net/URLConnection.html#connect()) or reads of the body for a `GET`.
+Note that this method may return a wrapped connection when it needs to apply additional request mutations, such as URL rewriting. For that reason you should always keep using the returned `connection` reference afterwards.
+
+Note that this method may throw an `ApproovException` (derived from `IOException`) if it is unable to fetch an Approov token due to no or poor Internet connectivity then `ApproovNetworkException` is thrown. In this case the user should be able to initiate a retry. Therefore the call should be in a `try-catch` block, possibly the same one as [`connect`](https://developer.android.com/reference/java/net/URLConnection.html#connect()) or reads of the body for a `GET`.
You should also edit the `res/values/strings.xml` file to change to using the shapes `https://shapes.approov.io/v3/shapes/` endpoint that checks Approov tokens (as well as the API key built into the app):
@@ -115,6 +117,29 @@ If you still don't get a valid shape then there are some things you can try. Rem
* Use `approov metrics` to see [Live Metrics](https://approov.io/docs/latest/approov-usage-documentation/#metrics-graphs) of the cause of failure.
* You can use a debugger or emulator and get valid Approov tokens on a specific device by ensuring you are [forcing a device ID to pass](https://approov.io/docs/latest/approov-usage-documentation/#forcing-a-device-id-to-pass). As a shortcut, you can use the `latest` as discussed so that the `device ID` doesn't need to be extracted from the logs or an Approov token.
* Also, you can use a debugger or Android emulator and get valid Approov tokens on any device if you [mark the signing certificate as being for development](https://approov.io/docs/latest/approov-usage-documentation/#development-app-signing-certificates).
+
+## SHAPES APP WITH INSTALLATION MESSAGE SIGNING
+
+This section shows how to add message signing as an additional layer of protection in addition to an Approov token.
+
+1. Edit the `res/values/strings.xml` file to use the shapes `https://shapes.approov.io/v5/shapes/` endpoint. The v5 endpoint performs a message signature check in addition to the Approov token check.
+
+2. Uncomment the message signing setup code in `io/approov/shapes/ShapesApp.java`. This installs an `ApproovService` mutator that adds the message signature to the request automatically.
+
+3. Configure Approov to add the public message signing key to the Approov token. This key is used by the v5 endpoint to perform its message signature check.
+
+```
+approov policy -setInstallPubKey on
+```
+
+4. Build and run the app again and press the `Get Shape` button. You should see this (or another shape):
+
+
+
+
+
+This indicates that in addition to the app obtaining a validly signed Approov token, the message also has a valid signature.
+
## SHAPES APP WITH SECRETS 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 `res/values/strings.xml` to using `https://shapes.approov.io/v1/shapes/` that simply checks for an API key. The `shapes_api_key` should also be changed to `shapes_api_key_placeholder`, removing the actual API key out of the code:
diff --git a/shapes-app/.project b/shapes-app/.project
index 7633412..cc8e375 100644
--- a/shapes-app/.project
+++ b/shapes-app/.project
@@ -16,12 +16,12 @@
- 1645701082319
+ 1776687200928
30
org.eclipse.core.resources.regexFilterMatcher
- node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__
+ node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__
diff --git a/shapes-app/.settings/org.eclipse.buildship.core.prefs b/shapes-app/.settings/org.eclipse.buildship.core.prefs
index 67c1136..ae13e80 100644
--- a/shapes-app/.settings/org.eclipse.buildship.core.prefs
+++ b/shapes-app/.settings/org.eclipse.buildship.core.prefs
@@ -1,11 +1,11 @@
-arguments=--init-script /home/richardt/.config/Code/User/globalStorage/redhat.java/1.13.0/config_linux/org.eclipse.osgi/51/0/.cp/gradle/init/init.gradle --init-script /home/richardt/.config/Code/User/globalStorage/redhat.java/1.13.0/config_linux/org.eclipse.osgi/51/0/.cp/gradle/protobuf/init.gradle
+arguments=--init-script /var/folders/d1/7dc4qrgd51v_5zzdcgsm3k0m0000gn/T/db3b08fc4a9ef609cb16b96b200fa13e563f396e9bb1ed0905fdab7bc3bc513b.gradle --init-script /var/folders/d1/7dc4qrgd51v_5zzdcgsm3k0m0000gn/T/52cde0cfcf3e28b8b7510e992210d9614505e0911af0c190bd590d7158574963.gradle --init-script /var/folders/d1/7dc4qrgd51v_5zzdcgsm3k0m0000gn/T/861a75667e10803d304a058d833cb7404195ca44013d0d61d3b653eb084379b8.gradle --init-script /var/folders/d1/7dc4qrgd51v_5zzdcgsm3k0m0000gn/T/68eb1b6516fe21c6fbba58e63c99c3207ccfc918360613709367eecde56fa77f.gradle --init-script /var/folders/d1/7dc4qrgd51v_5zzdcgsm3k0m0000gn/T/da64152279c70a8b4f3de4ca9ea66fd3b3405b7aca4e1f20f2d08e5593aa1ce1.gradle
auto.sync=false
build.scans.enabled=false
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
connection.project.dir=
eclipse.preferences.version=1
gradle.user.home=
-java.home=/usr/lib/jvm/java-11-openjdk-amd64
+java.home=/Users/charlesoj/Library/Java/JavaVirtualMachines/jbr-17.0.14/Contents/Home
jvm.arguments=
offline.mode=false
override.workspace.settings=true
diff --git a/shapes-app/app/.classpath b/shapes-app/app/.classpath
index 4a04201..bbe97e5 100644
--- a/shapes-app/app/.classpath
+++ b/shapes-app/app/.classpath
@@ -1,6 +1,6 @@
-
+
diff --git a/shapes-app/app/.project b/shapes-app/app/.project
index bce9cd5..de8121d 100644
--- a/shapes-app/app/.project
+++ b/shapes-app/app/.project
@@ -22,12 +22,12 @@
- 1645701082300
+ 1776687200918
30
org.eclipse.core.resources.regexFilterMatcher
- node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__
+ node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__
diff --git a/shapes-app/app/build.gradle b/shapes-app/app/build.gradle
index 1f496e7..589da6d 100644
--- a/shapes-app/app/build.gradle
+++ b/shapes-app/app/build.gradle
@@ -4,7 +4,7 @@ android {
compileSdkVersion 31
defaultConfig {
applicationId "io.approov.shapes"
- minSdkVersion 21
+ minSdkVersion 23
targetSdkVersion 31
versionCode 3
versionName "3.0"
@@ -32,6 +32,8 @@ android {
}
dependencies {
+// implementation 'androidx.annotation:annotation:1.8.2'
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.4.1'
+ implementation project(':approov-service')
}
diff --git a/shapes-app/app/src/main/java/io/approov/shapes/MainActivity.java b/shapes-app/app/src/main/java/io/approov/shapes/MainActivity.java
index 6ff231c..8d2a72c 100644
--- a/shapes-app/app/src/main/java/io/approov/shapes/MainActivity.java
+++ b/shapes-app/app/src/main/java/io/approov/shapes/MainActivity.java
@@ -128,8 +128,8 @@ protected void onCreate(Bundle savedInstanceState) {
helloCheckButton = findViewById(R.id.btnConnectionCheck);
shapesCheckButton = findViewById(R.id.btnShapesCheck);
- // handle hello connection check
- helloCheckButton.setOnClickListener(new View.OnClickListener() {
+ // handle hello connection check
+ helloCheckButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// hide status
@@ -183,62 +183,61 @@ public void run() {
// handle getting shapes
shapesCheckButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- // hide status
- activity.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- statusView.setVisibility(View.INVISIBLE);
- }
- });
-
- // run our HTTP request in a background thread to avoid blocking the UI thread
- AsyncTask.execute(new Runnable() {
- @Override
- public void run() {
- // fetch from the endpoint
- int imgId = R.drawable.confused;
- String msg;
- HttpsURLConnection connection = null;
- try {
- URL url = new URL(getResources().getString(R.string.shapes_url));
- connection = (HttpsURLConnection) url.openConnection();
- connection.setRequestMethod("GET");
- connection.addRequestProperty("Api-Key", getResources().getString(R.string.shapes_api_key));
-
- // *** UNCOMMENT THE LINE BELOW FOR APPROOV USING SECRETS PROTECTION ***
- //ApproovService.addSubstitutionHeader("Api-Key", null);
-
- // *** UNCOMMENT THE LINE BELOW FOR APPROOV ***
- //ApproovService.addApproov(connection);
-
- connection.connect();
- msg = "Http status code " + connection.getResponseCode();
- if (connection.getResponseCode() == 200)
- imgId = readShapesResponse(connection);
- } catch (IOException e) {
- Log.d(TAG, "Shapes call failed: " + e.toString());
- msg = "Shapes call failed: " + e.toString();
+ @Override
+ public void onClick(View view) {
+ // hide status
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ statusView.setVisibility(View.INVISIBLE);
}
- if (connection != null)
- connection.disconnect();
+ });
- // display the result
- final int finalImgId = imgId;
- final String finalMsg = msg;
- activity.runOnUiThread(new Runnable() {
+ // run our HTTP request in a background thread to avoid blocking the UI thread
+ AsyncTask.execute(new Runnable() {
@Override
public void run() {
- statusImageView.setImageResource(finalImgId);
- statusTextView.setText(finalMsg);
- statusView.setVisibility(View.VISIBLE);
+ // fetch from the endpoint
+ int imgId = R.drawable.confused;
+ String msg;
+ HttpsURLConnection connection = null;
+ try {
+ URL url = new URL(getResources().getString(R.string.shapes_url));
+ connection = (HttpsURLConnection) url.openConnection();
+ connection.setRequestMethod("GET");
+ connection.addRequestProperty("Api-Key", getResources().getString(R.string.shapes_api_key));
+
+ // *** UNCOMMENT THE LINE BELOW FOR APPROOV USING SECRETS PROTECTION ***
+ //ApproovService.addSubstitutionHeader("Api-Key", null);
+
+ // *** UNCOMMENT THE LINE BELOW FOR APPROOV ***
+ //connection = ApproovService.addApproov(connection);
+
+ connection.connect();
+ msg = "Http status code " + connection.getResponseCode();
+ if (connection.getResponseCode() == 200)
+ imgId = readShapesResponse(connection);
+ } catch (IOException e) {
+ Log.d(TAG, "Shapes call failed: " + e.toString());
+ msg = "Shapes call failed: " + e.toString();
+ }
+ if (connection != null)
+ connection.disconnect();
+
+ // display the result
+ final int finalImgId = imgId;
+ final String finalMsg = msg;
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ statusImageView.setImageResource(finalImgId);
+ statusTextView.setText(finalMsg);
+ statusView.setVisibility(View.VISIBLE);
+ }
+ });
}
- });
-
- }
- });
- }
- });
+ });
+ }
+ });
}
}
diff --git a/shapes-app/app/src/main/java/io/approov/shapes/ShapesApp.java b/shapes-app/app/src/main/java/io/approov/shapes/ShapesApp.java
index 08dd385..aea9e8e 100644
--- a/shapes-app/app/src/main/java/io/approov/shapes/ShapesApp.java
+++ b/shapes-app/app/src/main/java/io/approov/shapes/ShapesApp.java
@@ -22,6 +22,8 @@
// *** UNCOMMENT THE LINE BELOW FOR APPROOV ***
//import io.approov.service.httpsurlconn.ApproovService;
+// *** UNCOMMENT THE LINE BELOW FOR APPROOV WITH INSTALLATION MESSAGE SIGNING ***
+//import io.approov.service.httpsurlconn.ApproovDefaultMessageSigning;
public class ShapesApp extends Application {
@Override
@@ -30,5 +32,12 @@ public void onCreate() {
// *** UNCOMMENT THE LINE BELOW FOR APPROOV ***
//ApproovService.initialize(getApplicationContext(), "");
+
+ // *** UNCOMMENT THE LINES BELOW FOR APPROOV WITH INSTALLATION MESSAGE SIGNING ***
+ //ApproovService.setServiceMutator(
+ // new ApproovDefaultMessageSigning()
+ // .setDefaultFactory(ApproovDefaultMessageSigning.generateDefaultSignatureParametersFactory())
+ //);
+
}
}
diff --git a/shapes-app/settings.gradle b/shapes-app/settings.gradle
index b46b48c..f0171b1 100644
--- a/shapes-app/settings.gradle
+++ b/shapes-app/settings.gradle
@@ -1,2 +1,4 @@
-include ':approov-sdk'
+//include ':approov-sdk'
include ':app'
+include ':approov-service'
+project(':approov-service').projectDir = new File('/Users/charlesoj/Developer/Quickstarts/approov-service-httpsurlconn/approov-service')