Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
685de70
Hardening and Zero-Config R8 Rules for Retrofit
May 19, 2026
3334b09
fix/test: align initialize comment and null config params in Retrofit…
May 22, 2026
468962b
Document initialize comment options and reinitialization in REFERENCE.md
May 22, 2026
90bc6b7
Simplify initialization: delegate re-init decisions to platform SDK
May 27, 2026
6ef4eef
Reject null config explicitly with IllegalArgumentException
May 27, 2026
9c03c1b
Update docs to reflect simplified initialization changes
May 27, 2026
f8b7ddd
fix: preserve service layer state on SDK initialization failure
May 29, 2026
2e72c2e
Revise SECURITY.md for clarity on support and reporting
naynovi Jun 1, 2026
dc731d8
Merge branch 'pr-20' into feature/3.6.0
charlesoj6205 Jun 2, 2026
0927d8d
Relocate BouncyCastle to io.approov.internal.retrofit.bouncycastle
Jun 2, 2026
f7dd1c6
Remove hardcoded worker URL fallbacks from CI workflow
Jun 2, 2026
138b399
Update copyright year in LICENSE file
naynovi Jun 3, 2026
95f5c20
fix: ignore empty config reinitialization when already initialized wi…
Jun 3, 2026
45348ad
refactor: simplify empty config reinitialization check using isApproo…
Jun 3, 2026
e735d6d
Merge PR #22 (Update copyright year)
Jun 3, 2026
0bca1f5
docs: integrate quickstart content into README.md
Jun 3, 2026
8650bc7
fix: preserve placeholder on null/empty secure string substitution; d…
Jun 10, 2026
c7896db
merge: incorporate SECURITY.md from feature/3.6.0 (PR #21)
Jun 10, 2026
30463ad
docs: fix typos, SECURITY.md version table, CHANGELOG BC package name…
Jun 10, 2026
73edb01
docs: address Copilot PR #23 comments — CHANGELOG, REFERENCE, Javadoc…
Jun 10, 2026
f2980a0
docs: document initialize reset contract + warn on re-init state discard
adriantuk Jun 11, 2026
38b3f64
test: fix stale empty-config mini-SDK tests broken by downgrade guard
adriantuk Jun 11, 2026
e471d9e
CI changes to add version to user property and validate the changelog…
adriantuk Jun 12, 2026
66cdea8
fix feature branch build test
adriantuk Jun 12, 2026
c95f51f
add env for USER_MANAGED/AUTOMATIC
adriantuk Jun 12, 2026
590d0a6
temp set the default publishing script to USER_MANAGED
adriantuk Jun 12, 2026
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
27 changes: 16 additions & 11 deletions .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ jobs:
WORKSPACE: "${{ github.workspace }}"
GIT_BRANCH: "${{ github.ref }}"
CURRENT_TAG: "${{ github.ref_name }}"
# Worker endpoints: consumed from GitHub organisation variables first.
# If the org variable is not set the value is an empty string; the probe
# step below falls back to the same hardcoded defaults that are baked into
# the mini-sdk (MiniAttesterConfig / Approov.m), keeping them in sync.
# Worker endpoints: consumed from GitHub organisation or repository variables.
# Variables MUST be set — no hardcoded fallback is provided to avoid
# exposing worker endpoints in the public repository.
# Worker management (create / redeploy) logic is centralized in the
# core-service-layers-testing script, but invoked here when needed.
TESTING_REPLY_URL: ${{ vars.TESTING_REPLY_URL }}
Expand Down Expand Up @@ -51,19 +50,25 @@ jobs:
# -----------------------------------------------------------------------
# 3. Verify worker endpoints (with auto-redeploy)
#
# URLs are resolved with the following precedence (mirrors the mini-sdk
# fallback logic in MiniAttesterConfig / Approov.m):
# 1. GitHub org variable (TESTING_REPLY_URL / _UNPROTECTED)
# 2. Mini-SDK hardcoded defaults (replay.ivol.workers.dev / ...)
#
# URLs MUST be set as GitHub organisation or repository variables.
# The step hard-fails if they are missing — no hardcoded fallback.
# If the workers are unavailable, this step invokes the management
# script in core-service-layers-testing to redeploy them.
# -----------------------------------------------------------------------
- name: Verify worker endpoints
run: |
# ── URL resolution ──────────────────────────────────────────────────
PROTECTED_URL="${TESTING_REPLY_URL:-https://replay.ivol.workers.dev}"
UNPROTECTED_URL="${TESTING_REPLY_URL_UNPROTECTED:-https://replay-unprotected.ivol.workers.dev}"
# URLs MUST be set as GitHub organisation or repository variables.
# No hardcoded fallback is provided to avoid exposing endpoints
# in the public repository.
if [[ -z "$TESTING_REPLY_URL" || -z "$TESTING_REPLY_URL_UNPROTECTED" ]]; then
echo "ERROR: TESTING_REPLY_URL and TESTING_REPLY_URL_UNPROTECTED must be"
echo " set as GitHub organisation or repository variables."
exit 1
Comment thread
ivolz marked this conversation as resolved.
fi
Comment thread
ivolz marked this conversation as resolved.

PROTECTED_URL="$TESTING_REPLY_URL"
UNPROTECTED_URL="$TESTING_REPLY_URL_UNPROTECTED"

echo "TESTING_REPLY_URL=$PROTECTED_URL" >> "$GITHUB_ENV"
echo "TESTING_REPLY_URL_UNPROTECTED=$UNPROTECTED_URL" >> "$GITHUB_ENV"
Expand Down
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "interactive"
}
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ All notable changes to this package will be documented in this file.
The format is based on Keep a Changelog and this project adheres to Semantic Versioning.


## [3.5.7] - 2026-05-19

### Added
- Consumer ProGuard rules (`consumer-rules.pro`) to automatically preserve native SDK interfaces and internal cryptography bindings.

### Changed
- Shaded and relocated the BouncyCastle dependency (`io.approov.internal.retrofit.bouncycastle`) to prevent version collisions for consuming applications.
- Removed the transitive `org.bouncycastle:bcprov-jdk15to18` dependency from `pom.xml`.
- Simplified `initialize` — removed the service-layer re-initialization guards (same-config short-circuit, `reinit` comment check). The service layer forwards non-empty config directly to the platform SDK and resets its own state only after the SDK confirms success. The SDK returns `false` if already initialized with the same config (service layer logs and continues), or throws `IllegalStateException` for a different config (service layer re-throws, preserving existing state).

### Fixed
- `initialize` now explicitly throws `IllegalArgumentException` when `config` is `null`, with a clear message directing callers to pass `""` for bypass mode. Passing `null` previously caused a silent coercion to `""` which masked caller errors.
- The 2-arg `initialize(context, config)` overload now correctly passes `null` (not `""`) as the comment to the native SDK, preventing unexpected re-initialization mismatches on subsequent calls.

## [3.5.6] - 2026-04-21

### Added
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2021 Approov Integration Examples
Copyright (c) 2026 Approov Limited

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
108 changes: 105 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,110 @@ A wrapper for the [Approov SDK](https://github.com/approov/approov-android-sdk)

See [Java](https://github.com/approov/quickstart-android-java-retrofit) and [Kotlin](https://github.com/approov/quickstart-android-kotlin-retrofit) quickstarts for instructions on how to use this.

## Adding Approov Service Dependency
The Approov integration is available via [`maven`](https://mvnrepository.com/repos/central). This allows inclusion into the project by simply specifying a dependency in the `gradle` files for the app.
The `Maven` repository is already present in the `build.gradle` file so the only import you need to make is the actual service layer itself:

Comment thread
ivolz marked this conversation as resolved.
```kotlin title="build.gradle.kts"
implementation("io.approov:service.retrofit:3.5.7")
```

If using a Groovy `build.gradle`, use:

```groovy
implementation 'io.approov:service.retrofit:3.5.7'
```

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.

This package is actually an open source wrapper layer that allows you to easily use Approov with `Retrofit`. This has a further dependency to the closed source [Approov SDK](https://mvnrepository.com/artifact/io.approov/approov-android-sdk). In some cases you may need to also add this implementation to your dependencies list to avoid build errors:

```
implementation("io.approov:approov-android-sdk:3.5.3")
```

## Manifest Changes
The following app permissions need to be available in the manifest to use Approov:

```xml
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
```

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.

## Initializing Approov Service
In order to use the `ApproovService` you must initialize it when your app is created, usually in the `onCreate` method:

```kotlin
import io.approov.service.retrofit.ApproovService

class YourApp: Application() {
override fun onCreate() {
super.onCreate()
ApproovService.initialize(applicationContext, "<enter-your-config-string-here>")
}
}
```

The `<enter-your-config-string-here>` is a custom string that configures your Approov account access. This will have been provided in your Approov onboarding email.

## Using Approov Service
You can then modify your code that obtains a `RetrofitInstance` to make API calls as follows:

```kotlin
object ClientInstance {
private const val BASE_URL = "https://your.domain"
private var retrofitBuilder: Retrofit.Builder? = null
val retrofitInstance: Retrofit
get() {
if (retrofitBuilder == null) {
retrofitBuilder = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
}
return ApproovService.getRetrofit(retrofitBuilder!!)
}
}
```

This obtains a Retrofit instance that 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:

```kotlin
val client = OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS).build()
val retrofit = retrofit2.Retrofit.Builder().baseUrl("https://your.domain/").client(client).build()
```
Pass the modified `OkHttp.Builder` to the `ApproovService` as follows:

```kotlin
ApproovService.setOkHttpClientBuilder(OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS))
val retrofitBuilder = retrofit2.Retrofit.Builder().baseUrl("https://your.domain/")
val retrofit = ApproovService.getRetrofit(retrofitBuilder)
```

This call to `setOkHttpClientBuilder` only needs to be made once. Subsequent calls to `ApproovService.getRetrofit()` will then always build and use an `OkHttpClient` with the builder values included.

## Checking it Works
Initially you won't have set which API domains to protect, so the interceptor will not add anything. It will have called Approov though and made contact with the Approov cloud service. You will see logging from Approov saying `UNKNOWN_URL`.

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.

## Next Steps
To actually protect your APIs and/or secrets there are some further steps. Approov provides two different options for protection:

**API Protection** 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 cryptographically signed JWT proving the authenticity of the call.

**Secrets Protection** This allows app secrets, including API keys for 3rd party services, to be protected so that they no longer need to be included in the released app code. These secrets are only made available to valid apps at runtime.

Note that it is possible to use both approaches side-by-side in the same app.

# Changelog

Please see the [CHANGELOG.md](CHANGELOG.md) for more information on the changes in each version.
Expand All @@ -18,9 +122,7 @@ Please see the [USAGE.md](USAGE.md) for more information on how to use this wrap

## Included 3rd party Source

To support message signing, this repo has adapted code released by two 3rd
party developers. The LICENSE files have been copied from the repos into the
associated directories listed below:
To support message signing, this repo has adapted code released by two 3rd party developers. The LICENSE files have been copied from the repos into the associated directories listed below:

* `approov-service/src/main/java/io/approov/util/http/sfv`
* Repo: https://github.com/reschke/structured-fields
Expand Down
33 changes: 29 additions & 4 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ If a method throws an `ApproovRejectionException` (a subclass of `ApproovFetchSt
## initialize
Initializes the Approov SDK and thus enables the Approov features. The `config` will have been provided in the initial onboarding or email or can be [obtained](https://approov.io/docs/latest/approov-usage-documentation/#getting-the-initial-sdk-configuration) using the approov CLI. This will generate an error if a second attempt is made at initialization with a different `config`.

This is the standard form and should be used in most cases. The `comment` parameter defaults to `null` when not supplied.

**Java:**
```Java
void initialize(Context context, String config)
Expand All @@ -34,7 +36,7 @@ The [application context](https://developer.android.com/reference/android/conten

It is possible to pass an empty `config` string to indicate that no initialization of the underlying native Approov SDK is required. This initializes the service layer in a bypass mode, allowing you to obtain a standard, non-Approov protected Retrofit client. If you attempt to use any direct native Approov SDK functions (such as `fetchToken` or `precheck`) while bypassed, an `ApproovException` will be thrown. You may later call `initialize` again with a valid `config` string to enable Approov protection for Retrofit instances obtained after that point. Retrofit instances obtained while bypassed remain standard, non-Approov protected clients and should be discarded if protection is later enabled.

An alternative initialization function allows to provide further options in the `comment` parameter. Please refer to the [Approov SDK documentation](https://approov.io/docs/latest/approov-direct-sdk-integration/#sdk-initialization-options) for details.
If you need to supply a `comment` to the native SDK (for example to pass `options:...` startup flags or trigger a `reinit...` flow), use the extended form instead:

**Java:**
```java
Expand All @@ -43,9 +45,18 @@ void initialize(Context context, String config, String comment)

**Kotlin:**
```kotlin
fun initialize(context: Context, config: String, comment: String)
fun initialize(context: Context, config: String, comment: String?)
```

The `comment` parameter is passed directly to the native Approov SDK. Key uses:
* Pass a string starting with `options:` during the initial setup to forward custom startup options to the native SDK.
* Pass a string starting with `reinit` to trigger native re-initialization on a subsequent same-config call.
* Pass `null` (or use the 2-arg form) when no comment is needed — this is the default.

Please refer to the [Approov SDK documentation](https://approov.io/docs/latest/approov-direct-sdk-integration/#sdk-initialization-options) for full details on supported comment values.



## isInitialized
Indicates whether the Approov service layer has been initialized.

Expand All @@ -59,8 +70,10 @@ boolean isInitialized()
fun isInitialized(): Boolean
```

Returns `true` if `initialize` has been called successfully, including when bypass mode is active (empty config string). Returns `false` if `initialize` has never been called. If a subsequent `initialize` call fails (e.g. due to a different config being rejected), the prior initialized state is preserved — the method does not flip back to `false`. Use `isApproovEnabled()` to distinguish between bypass and protected modes.

## isApproovEnabled
Indicates whether the underlying native Approov SDK is enabled and active. If the service layer was initialized with an empty configuration string, this will return `false`.
Indicates whether the underlying native Approov SDK is enabled and active.

**Java:**
```Java
Expand All @@ -72,6 +85,8 @@ boolean isApproovEnabled()
fun isApproovEnabled(): Boolean
```

Returns `true` only when the service layer was initialized with a valid, non-empty configuration string and the native Approov SDK is active. Returns `false` in all other cases: not initialized, or initialized in bypass mode (empty config). All direct Approov SDK methods (such as `fetchToken`, `precheck`, `fetchSecureString`) will throw `ApproovException` if called when this returns `false`.

## setApproovInterceptorExtensions

**OBSOLETED COMPATIBILITY API**: Use `setServiceMutator` instead for all new integrations, including message signing.
Expand Down Expand Up @@ -175,7 +190,7 @@ fun setProceedOnNetworkFail(proceed: Boolean)
Note that this should be used with *CAUTION* because it may allow a connection to be established before any dynamic pins have been received via Approov, thus potentially opening the channel to a MitM.

## setUseApproovStatusIfNoToken
If the provided `shouldUse` value is `true` then this indicates that the Approov fetch status (e.g. "NO_NETWORK", "MITM_DETECTED") should be used as the token header value if the actual token fetch fails or returns an empty token. This allows passing error condition information to the backend via the Approov-Token header, which might otherwise be empty or missing.
If the provided `shouldUse` value is `true` then this indicates that the Approov fetch status (e.g. `"NO_NETWORK"`, `"MITM_DETECTED"`) should be used as the token header value when a real Approov token is unavailable. This covers two cases: (1) the token fetch succeeds but returns an empty token, and (2) a failure status is returned but the active service mutator allows the request to proceed rather than aborting it. This allows passing error condition information to the backend via the Approov token header.

Comment thread
ivolz marked this conversation as resolved.
Comment thread
ivolz marked this conversation as resolved.
**Java:**
```Java
Expand All @@ -187,6 +202,16 @@ void setUseApproovStatusIfNoToken(boolean shouldUse)
fun setUseApproovStatusIfNoToken(shouldUse: Boolean)
```

**Token header behaviour when no real token is available:**

| `useApproovStatusIfNoToken` | Token returned by SDK | Header emitted |
|---|---|---|
| `false` (default) | non-empty | `prefix + token` |
| `false` (default) | empty | `prefix` only — header is still sent with just the configured prefix (e.g. `"Bearer "`) to signal that Approov processing occurred. The backend must be prepared to handle a prefix-only value gracefully. |
| `true` | empty or any failure status | `prefix + statusString` (e.g. `"NO_NETWORK"`) |

> **Note**: The header is always emitted when Approov processing succeeds (i.e. the mutator does not block the request). A prefix-only header value is intentional and signals to the backend that Approov ran but no signed token was available, distinguishing this from requests that bypassed Approov entirely.

## setFailureCacheTtlMs
Sets the time-to-live (in milliseconds) for caching Approov failure statuses (such as `NO_NETWORK`, `MITM_DETECTED`, etc.) during interceptor token fetches. The default TTL is 500 milliseconds. When the service is in a sustained failure state, caching the failure avoids redundant and potentially blocking calls to the native SDK for rapidly fired concurrent requests.

Expand Down
15 changes: 15 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

We maintain updates and patches in the latest release. Earlier versions will still work, but will have less functionality than later versions.
We encourage all users of these service layers to update to the latest version for the best experience.
Comment thread
ivolz marked this conversation as resolved.

| Version | Supported |
| ------- | ------------------ |
| 3.5.x | :white_check_mark: |
| < 3.5.0 (3.4.0) | :x: |

## Reporting a Vulnerability

Thank you for letting us know about possible security vulnerabilities in this project.
Please don't publish details in a public issue or PR. Send us a private email at support@approov.io and disclose which version your report refers to.

Your message will receive a prompt reply.
Loading
Loading