Skip to content

Commit 20b9aca

Browse files
authored
Merge pull request #16 from approov/feature/dontWrapOnMissingIpk
Feature/dont wrap on missing ipk
2 parents fac7e8b + 6106c1a commit 20b9aca

18 files changed

Lines changed: 2219 additions & 239 deletions

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Changelog
2+
3+
All notable changes to this package will be documented in this file.
4+
5+
The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
6+
7+
8+
## [3.5.5] - 2026-03-25
9+
10+
### Added
11+
- ApproovServiceMutator protocol with default behavior to centralize decision points in the service flow.
12+
- Mutator hooks for precheck, token fetch, secure string fetch, custom JWT fetch, interceptor decisions, and pinning.
13+
- REFERENCE.md & CHANGELOG.md & USAGE.md
14+
- Added `setUseApproovStatusIfNoToken` to allow using status as token value when token is missing.
15+
### Changed
16+
- ApproovService now routes decision logic through the service mutator and exposes set/get APIs.
17+
- Pinning logic is now applied via `ApproovPinningInterceptor` which checks `ApproovServiceMutator.handlePinningShouldProcessRequest`.
18+
- Update version to 3.5.5
19+
### Fixed
20+
- Prevented exceptions when key-pair generation fails. The service now logs an error and continues without the install message signature, allowing the backend to decide whether to reject the request. (inherited from shared sdk update)
21+
- Memory leak fix in pinned handshake cache using LinkedHashSet.
22+
- Initialized the Retrofit instance cache statically to avoid `NullPointerException` when `setOkHttpClientBuilder` or `getRetrofit` are called before `initialize`.
23+
### Deprecated
24+
- ApproovInterceptorExtensions in favor of ApproovServiceMutator.
25+
- setProceedOnNetworkFail() and getProceedOnNetworkFail() in favor of setServiceMutator.
26+
- prefetch() is now automatically called when the service is initialized.

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ A wrapper for the [Approov SDK](https://github.com/approov/approov-android-sdk)
44

55
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.
66

7+
# Changelog
8+
9+
Please see the [CHANGELOG.md](CHANGELOG.md) for more information on the changes in each version.
10+
11+
# Reference
12+
13+
Please see the [REFERENCE.md](REFERENCE.md) for more information on the Approov Service for Retrofit.
14+
15+
# Usage
16+
17+
Please see the [USAGE.md](USAGE.md) for more information on how to use this wrapper.
18+
719
## Included 3rd party Source
820

921
To support message signing, this repo has adapted code released by two 3rd

REFERENCE.md

Lines changed: 453 additions & 0 deletions
Large diffs are not rendered by default.

USAGE.md

Lines changed: 264 additions & 0 deletions
Large diffs are not rendered by default.

approov-service/build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ android {
2626
sourceCompatibility JavaVersion.VERSION_1_8
2727
targetCompatibility JavaVersion.VERSION_1_8
2828
}
29+
testOptions {
30+
unitTests.returnDefaultValues = true
31+
unitTests.all {
32+
jvmArgs '-Dnet.bytebuddy.experimental=true'
33+
}
34+
}
2935
}
3036

3137
dependencies {
@@ -36,4 +42,5 @@ dependencies {
3642
implementation 'com.squareup.retrofit2:converter-gson:2.11.0'
3743
implementation 'androidx.annotation:annotation-jvm:1.9.1'
3844
testImplementation 'junit:junit:4.13.2'
45+
testImplementation 'org.mockito:mockito-inline:5.2.0'
3946
}

approov-service/pom.xml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
</dependency>
4040
<dependency>
4141
<groupId>org.bouncycastle</groupId>
42-
<artifactId>bcprov-jdk18on</artifactId>
42+
<artifactId>bcprov-jdk15to18</artifactId>
4343
<version>1.80</version>
4444
<scope>runtime</scope>
4545
</dependency>
@@ -52,15 +52,21 @@
5252
<dependency>
5353
<groupId>com.squareup.retrofit2</groupId>
5454
<artifactId>retrofit</artifactId>
55-
<version>2.9.0</version>
55+
<version>2.11.0</version>
5656
<scope>runtime</scope>
5757
</dependency>
5858
<dependency>
5959
<groupId>com.squareup.retrofit2</groupId>
6060
<artifactId>converter-gson</artifactId>
61-
<version>2.9.0</version>
61+
<version>2.11.0</version>
62+
<scope>runtime</scope>
63+
</dependency>
64+
<dependency>
65+
<groupId>androidx.annotation</groupId>
66+
<artifactId>annotation-jvm</artifactId>
67+
<version>1.9.1</version>
6268
<scope>runtime</scope>
6369
</dependency>
6470
</dependencies>
6571

66-
</project>
72+
</project>

approov-service/src/main/java/io/approov/service/retrofit/ApproovDefaultMessageSigning.java

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,38 @@ protected SignatureParameters buildSignatureParameters(OkHttpComponentProvider p
135135
return factory.buildSignatureParameters(provider, changes);
136136
}
137137

138+
/**
139+
* Retrieves an install message signature for the supplied message.
140+
*
141+
* @param message The message to be signed.
142+
* @return The base64-encoded ASN.1 DER signature.
143+
* @throws ApproovException If signing is unavailable.
144+
*/
145+
protected String getInstallMessageSignature(String message) throws ApproovException {
146+
return ApproovService.getInstallMessageSignature(message);
147+
}
148+
149+
/**
150+
* Retrieves an account message signature for the supplied message.
151+
*
152+
* @param message The message to be signed.
153+
* @return The base64-encoded signature.
154+
* @throws ApproovException If signing is unavailable.
155+
*/
156+
protected String getAccountMessageSignature(String message) throws ApproovException {
157+
return ApproovService.getAccountMessageSignature(message);
158+
}
159+
160+
/**
161+
* Decodes a base64-encoded signature value.
162+
*
163+
* @param base64 The signature bytes encoded as base64.
164+
* @return The decoded bytes.
165+
*/
166+
protected byte[] decodeBase64(String base64) {
167+
return Base64.decode(base64, Base64.NO_WRAP);
168+
}
169+
138170
/**
139171
* Converts one part, encoded as an ASN1Integer, of an ASN.1 DER encoded ES256 signature to a byte array of
140172
* exactly 32 bytes. Throws IllegalArgumentException if this is not possible.
@@ -197,8 +229,18 @@ public Request processedRequest(Request request, ApproovRequestMutations changes
197229
switch (params.getAlg()) {
198230
case ALG_ES256: {
199231
sigId = "install";
200-
String base64 = ApproovService.getInstallMessageSignature(message);
201-
signature = Base64.decode(base64, Base64.NO_WRAP);
232+
String base64;
233+
try {
234+
base64 = getInstallMessageSignature(message);
235+
} catch (ApproovException e) {
236+
Log.d(TAG, "Failed to get InstallMessageSignature - skipping message signing " + e);
237+
return request;
238+
}
239+
if (base64.isEmpty()) {
240+
Log.d(TAG, "InstallMessageSignature is empty - skipping message signing");
241+
return request;
242+
}
243+
signature = decodeBase64(base64);
202244
// decode the signature from ASN.1 DER format
203245
try (ASN1InputStream asn1InputStream = new ASN1InputStream(signature)) {
204246
ASN1Sequence sequence = (ASN1Sequence) asn1InputStream.readObject();
@@ -219,8 +261,8 @@ public Request processedRequest(Request request, ApproovRequestMutations changes
219261
}
220262
case ALG_HS256: {
221263
sigId = "account";
222-
String base64 = ApproovService.getAccountMessageSignature(message);
223-
signature = Base64.decode(base64, Base64.NO_WRAP);
264+
String base64 = getAccountMessageSignature(message);
265+
signature = decodeBase64(base64);
224266
break;
225267
}
226268
default:
@@ -242,16 +284,19 @@ public Request processedRequest(Request request, ApproovRequestMutations changes
242284
// Update the request from the one held by the component provider as the signature builder
243285
// may have modified it.
244286
Request.Builder signedBuilder = provider.getRequest().newBuilder()
245-
.addHeader("Signature", sigHeader)
246-
.addHeader("Signature-Input", sigInputHeader);
287+
.removeHeader("Signature")
288+
.removeHeader("Signature-Input")
289+
.removeHeader("Signature-Base-Digest")
290+
.header("Signature", sigHeader)
291+
.header("Signature-Input", sigInputHeader);
247292
if (params.isDebugMode()) {
248293
try {
249294
MessageDigest digestBuilder = MessageDigest.getInstance("SHA-256");
250295
digestBuilder.reset();
251296
byte[] digest = digestBuilder.digest(message.getBytes(StandardCharsets.UTF_8));
252297
String digestHeader = Dictionary.valueOf(Map.of(
253298
DIGEST_SHA256, ByteSequenceItem.valueOf(digest))).serialize();
254-
signedBuilder.addHeader("Signature-Base-Digest", digestHeader);
299+
signedBuilder.header("Signature-Base-Digest", digestHeader);
255300
} catch (NoSuchAlgorithmException e) {
256301
Log.d(TAG, "Failed to get digest algorithm - no debug entry " + e);
257302
}
@@ -490,7 +535,7 @@ protected boolean generateBodyDigest(OkHttpComponentProvider provider, Signature
490535
// add the digest to the request
491536
Request request = provider.getRequest();
492537
request = request.newBuilder()
493-
.addHeader("Content-Digest", digestHeader.serialize())
538+
.header("Content-Digest", digestHeader.serialize())
494539
.build();
495540
provider.setRequest(request);
496541
// add the header to the SignatureParameters
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//
2+
// MIT License
3+
//
4+
// Copyright (c) 2016-present, Approov Ltd.
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
7+
// (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge,
8+
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
9+
// subject to the following conditions:
10+
//
11+
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
14+
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
15+
// ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
16+
// THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
17+
18+
package io.approov.service.retrofit;
19+
20+
import com.criticalblue.approovsdk.Approov;
21+
22+
/**
23+
* Exception raised when an Approov token fetch returns a status other than success.
24+
*/
25+
public class ApproovFetchStatusException extends ApproovException {
26+
27+
private final Approov.TokenFetchStatus tokenFetchStatus;
28+
29+
/**
30+
* Constructs a token fetch status exception with the provided status.
31+
*
32+
* @param status status returned by the Approov SDK, may be {@code null} if unavailable
33+
* @param message information describing the exception cause
34+
*/
35+
public ApproovFetchStatusException(Approov.TokenFetchStatus status, String message) {
36+
super(message);
37+
this.tokenFetchStatus = status;
38+
}
39+
40+
/**
41+
* Retrieves the token fetch status associated with this exception.
42+
*
43+
* @return the status returned by the Approov SDK, or {@code null} if not provided
44+
*/
45+
public Approov.TokenFetchStatus getTokenFetchStatus() {
46+
return tokenFetchStatus;
47+
}
48+
}

approov-service/src/main/java/io/approov/service/retrofit/ApproovInterceptorExtensions.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,34 @@
2323
* ApproovInterceptorExtensions provides an interface for handling callbacks during
2424
* the processing of network requests by Approov. It allows further modifications
2525
* to requests after Approov has applied its changes.
26+
*
27+
* @deprecated Replace implementations of this interface with ApproovServiceMutator
28+
* while changing the name of the ApproovInterceptorExtensions.processedRequest
29+
* method to ApproovServiceMutator.handleInterceptorProcessedRequest.
2630
*/
27-
public interface ApproovInterceptorExtensions {
31+
@Deprecated
32+
public interface ApproovInterceptorExtensions extends ApproovServiceMutator{
2833

2934
/**
30-
* Called after Approov has processed a network request, allowing further modifications.
35+
* Replace the default implementation of ApproovServiceMutator.handleInterceptorProcessedRequest
36+
* to call the now deprecated ApproovInterceptorExtensions.processedRequest method.
3137
*
3238
* @param request the processed request
3339
* @param changes the mutations applied to the request by Approov
34-
* @return the modified request
40+
* @return the final request to use to complete the Approov interceptor step.
3541
* @throws ApproovException if there is an error during processing
3642
*/
37-
Request processedRequest(Request request, ApproovRequestMutations changes) throws ApproovException;
43+
default Request handleInterceptorProcessedRequest(Request request, ApproovRequestMutations changes) throws ApproovException {
44+
// call the deprecated method to maintain backwards compatibility
45+
return processedRequest(request, changes);
46+
}
47+
48+
/**
49+
* @deprecated Use ApproovServiceMutator.handleInterceptorProcessedRequest instead.
50+
*/
51+
@Deprecated
52+
default Request processedRequest(Request request, ApproovRequestMutations changes) throws ApproovException {
53+
// No further changes to the request are required
54+
return request;
55+
}
3856
}
Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//
22
// MIT License
3-
//
3+
//
44
// Copyright (c) 2016-present, Approov Ltd.
55
//
66
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
@@ -9,24 +9,39 @@
99
// subject to the following conditions:
1010
//
1111
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
12-
//
12+
//
1313
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
1414
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
1515
// ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
1616
// THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1717

1818
package io.approov.service.retrofit;
1919

20-
// ApproovNetworkException indicates an exception caused by networking conditions which is likely to be
21-
// temporary so a user initiated retry should be performed
22-
public class ApproovNetworkException extends ApproovException {
20+
import com.criticalblue.approovsdk.Approov;
21+
22+
/**
23+
* @deprecated Use {@link ApproovFetchStatusException} instead. This subtype is retained only to avoid
24+
* breaking existing handlers and call sites; migrate any explicit catches to use the parent class.
25+
*/
26+
@Deprecated
27+
public class ApproovNetworkException extends ApproovFetchStatusException {
2328

2429
/**
2530
* Constructs an Approov networking exception.
2631
*
27-
* @param message is the basic information about the exception cause
32+
* @param message basic information about the exception cause
2833
*/
2934
public ApproovNetworkException(String message) {
30-
super(message);
35+
super(null, message);
36+
}
37+
38+
/**
39+
* Constructs an Approov networking exception with a specific token fetch status.
40+
*
41+
* @param status token fetch status that triggered the error
42+
* @param message basic information about the exception cause
43+
*/
44+
public ApproovNetworkException(Approov.TokenFetchStatus status, String message) {
45+
super(status, message);
3146
}
3247
}

0 commit comments

Comments
 (0)