Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ The following app permissions need to be available in the manifest to use Approo
<uses-permission android:name="android.permission.INTERNET" />
```

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.

Expand All @@ -51,9 +51,11 @@ The `<enter-your-config-string-here>` 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.
Expand Down
4 changes: 3 additions & 1 deletion REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 3 additions & 3 deletions SECRETS-PROTECTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;

...

Expand Down
31 changes: 28 additions & 3 deletions SHAPES-EXAMPLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:

![Approov Initialization](readme-images/approov-init-code.png)

Expand All @@ -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.

Comment on lines +77 to 78
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation refers to HttpsUrlConnection, but the actual Java type is HttpsURLConnection (as linked). Using the correct class name here will avoid confusion for readers trying to follow the sample code.

Copilot uses AI. Check for mistakes.
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):

Expand Down Expand Up @@ -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):

<p>
<img src="readme-images/shapes-good.png" width="256" title="Shapes Good">
</p>

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:
Expand Down
4 changes: 2 additions & 2 deletions shapes-app/.project
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@
</natures>
<filteredResources>
<filter>
<id>1645701082319</id>
<id>1776687200928</id>
<name></name>
<type>30</type>
<matcher>
<id>org.eclipse.core.resources.regexFilterMatcher</id>
<arguments>node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
</matcher>
</filter>
</filteredResources>
Expand Down
4 changes: 2 additions & 2 deletions shapes-app/.settings/org.eclipse.buildship.core.prefs
Original file line number Diff line number Diff line change
@@ -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=
Comment on lines +1 to 9
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Buildship prefs file now embeds machine-specific temporary --init-script paths and a user-specific java.home. Committing these values makes the Eclipse/Buildship configuration non-portable and likely to break for other developers. Consider removing these entries (or the whole file) from version control and relying on workspace-local settings instead.

Copilot uses AI. Check for mistakes.
offline.mode=false
override.workspace.settings=true
Expand Down
2 changes: 1 addition & 1 deletion shapes-app/app/.classpath
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11/"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17/"/>
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Eclipse classpath is being switched to JavaSE-17. This repo’s Gradle wrapper is gradle-7.2, which typically requires running on an older JDK (and AGP 7.1.x is commonly run on JDK 11). Unless the wrapper/AGP are also being upgraded, keeping the project JRE at 11 avoids IDE/Buildship build failures.

Suggested change
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17/"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11/"/>

Copilot uses AI. Check for mistakes.
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>
4 changes: 2 additions & 2 deletions shapes-app/app/.project
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@
</natures>
<filteredResources>
<filter>
<id>1645701082300</id>
<id>1776687200918</id>
<name></name>
<type>30</type>
<matcher>
<id>org.eclipse.core.resources.regexFilterMatcher</id>
<arguments>node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
</matcher>
</filter>
</filteredResources>
Expand Down
4 changes: 3 additions & 1 deletion shapes-app/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ android {
compileSdkVersion 31
defaultConfig {
applicationId "io.approov.shapes"
minSdkVersion 21
minSdkVersion 23
targetSdkVersion 31
versionCode 3
versionName "3.0"
Expand Down Expand Up @@ -32,6 +32,8 @@ android {
}

dependencies {
// implementation 'androidx.annotation:annotation:1.8.2'
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a commented-out dependency left in the dependencies block. Since it has no effect and can be confusing when troubleshooting dependency resolution, consider removing it or adding a brief comment explaining when it is needed.

Suggested change
// implementation 'androidx.annotation:annotation:1.8.2'

Copilot uses AI. Check for mistakes.
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation project(':approov-service')
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implementation project(':approov-service') makes the app depend on a Gradle module that is not present in this repository (and is currently mapped via an absolute path in settings.gradle). This will fail for fresh checkouts and CI; switch back to the documented Maven dependency or vendor the module into the repo with a relative path.

Suggested change
implementation project(':approov-service')
implementation 'io.approov:service:+'

Copilot uses AI. Check for mistakes.
}
107 changes: 53 additions & 54 deletions shapes-app/app/src/main/java/io/approov/shapes/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
});
}
});

}
});
}
});
});
}
});
}
}
9 changes: 9 additions & 0 deletions shapes-app/app/src/main/java/io/approov/shapes/ShapesApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,5 +32,12 @@ public void onCreate() {

// *** UNCOMMENT THE LINE BELOW FOR APPROOV ***
//ApproovService.initialize(getApplicationContext(), "<enter-your-config-string-here>");

// *** UNCOMMENT THE LINES BELOW FOR APPROOV WITH INSTALLATION MESSAGE SIGNING ***
//ApproovService.setServiceMutator(
// new ApproovDefaultMessageSigning()
// .setDefaultFactory(ApproovDefaultMessageSigning.generateDefaultSignatureParametersFactory())
//);

}
}
4 changes: 3 additions & 1 deletion shapes-app/settings.gradle
Original file line number Diff line number Diff line change
@@ -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')
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

project(':approov-service').projectDir is set to an absolute path under /Users/..., which will break builds for anyone else (CI, other dev machines, different OS). Prefer a repository-relative path (e.g., based on settingsDir) or, better, consume the service via a published Maven dependency instead of a local project reference.

Suggested change
project(':approov-service').projectDir = new File('/Users/charlesoj/Developer/Quickstarts/approov-service-httpsurlconn/approov-service')
project(':approov-service').projectDir = new File(settingsDir, '../approov-service-httpsurlconn/approov-service')

Copilot uses AI. Check for mistakes.