Skip to content

Commit 9852dba

Browse files
authored
feat: Enforce API key restrictions when used in Android. (#740)
* feat: Enforce API key restrictions when used in Android. * Revert version to 0.18.2 * Update README. * Reformat. * Use api instead of implementation for okhttp. * Delete unused files.
1 parent 8bc0669 commit 9852dba

13 files changed

+485
-47
lines changed

README.md

Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -30,44 +30,28 @@ APIs:
3030
Keep in mind that the same [terms and conditions](https://developers.google.com/maps/terms) apply
3131
to usage of the APIs when they're accessed through this library.
3232

33-
## Intended usage of this library
34-
35-
The Java Client for Google Maps Services is designed for use in server applications. This library
36-
is not intended for use inside of an Android app, due to the potential for loss of API keys.
37-
38-
If you are building a mobile application, you will need to introduce a proxy server to act as
39-
intermediary between your mobile application and the [Google Maps API Web Services]. The Java
40-
Client for Google Maps Services would make an excellent choice as the basis for such a proxy server.
41-
42-
Please see [Making the most of the Google Maps Web Service APIs] for more detail.
43-
44-
Looking for our Android [Maps](https://developers.google.com/maps/documentation/android-sdk/intro) or
45-
[Places](https://developers.google.com/places/android-sdk/intro) SDKs?
46-
47-
## Support
48-
49-
This library is community supported. We're comfortable enough with the stability and features of
50-
the library that we want you to build real production applications on it. We will try to support,
51-
through Stack Overflow, the public and protected surface of the library and maintain backwards
52-
compatibility in the future; however, while the library is in version 0.x, we reserve the right
53-
to make backwards-incompatible changes. If we do remove some functionality (typically because
54-
better functionality exists or if the feature proved infeasible), our intention is to deprecate
55-
and give developers a year to update their code.
56-
57-
If you find a bug, or have a feature suggestion, please [log an issue][issues]. If you'd like to
58-
contribute, please read [How to Contribute][contrib].
59-
6033
## Requirements
6134

6235
- Java 1.8 or later.
6336
- A Google Maps API key.
6437

65-
### API keys
38+
## API keys
6639
Each Google Maps Web Service request requires an API key. API keys are generated in the 'Credentials' page of the 'APIs & Services' tab of Google Cloud console.
6740

68-
For even more information on getting started with Google Maps Platform and generating/restricting an API key, see [Get Started with Google Maps Platform](https://developers.google.com/maps/gmp-get-started) in our docs.
41+
For even more information on getting started with Google Maps Platform and generating an API key, see [Get Started with Google Maps Platform](https://developers.google.com/maps/gmp-get-started) in our docs.
42+
43+
### API Key Security
44+
45+
The Java Client for Google Maps Services is designed for use in both server and Android applications.
46+
In either case, it is important to add [API key restrictions](https://developers.google.com/maps/api-security-best-practices?hl=it)
47+
to improve the security of your API key. Additional security measures, such as hiding your key
48+
from version control, should also be put in place to further improve the security of your API key.
6949

70-
Important: This key should be kept secret on your server.
50+
You can refer to [API Security Best Practices](https://developers.google.com/maps/api-security-best-practices) to learn
51+
more about this topic.
52+
53+
**NOTE**: If you are using this library on Android, ensure that your application
54+
is using at least version 0.19.0 of this library so that API key restrictions can be enforced.
7155

7256
## Installation
7357

@@ -236,6 +220,19 @@ req.setCallback(new PendingResult.Callback<GeocodingResult[]>() {
236220
# Run the tests
237221
$ ./gradlew test
238222

223+
## Support
224+
225+
This library is community supported. We're comfortable enough with the stability and features of
226+
the library that we want you to build real production applications on it. We will try to support,
227+
through Stack Overflow, the public and protected surface of the library and maintain backwards
228+
compatibility in the future; however, while the library is in version 0.x, we reserve the right
229+
to make backwards-incompatible changes. If we do remove some functionality (typically because
230+
better functionality exists or if the feature proved infeasible), our intention is to deprecate
231+
and give developers a year to update their code.
232+
233+
If you find a bug, or have a feature suggestion, please [log an issue][issues]. If you'd like to
234+
contribute, please read [How to Contribute][contrib].
235+
239236

240237
[apikey]: https://developers.google.com/maps/faq#keysystem
241238
[clientid]: https://developers.google.com/maps/documentation/business/webservices/auth

build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,14 @@ dependencies {
5656
api 'com.google.code.gson:gson:2.8.6'
5757
api 'io.opencensus:opencensus-api:0.25.0'
5858
implementation 'org.slf4j:slf4j-api:1.7.26'
59-
testImplementation 'junit:junit:4.12'
60-
testImplementation 'org.mockito:mockito-core:3.0.0'
59+
testImplementation 'junit:junit:4.13.2'
6160
testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.1'
6261
testImplementation 'org.apache.httpcomponents:httpclient:4.5.9'
6362
testImplementation 'org.slf4j:slf4j-simple:1.7.26'
6463
testImplementation 'org.apache.commons:commons-lang3:3.9'
6564
testImplementation 'org.json:json:20180813'
6665
testImplementation 'io.opencensus:opencensus-impl:0.25.0'
66+
testImplementation "org.mockito:mockito-inline:3.11.2"
6767
}
6868

6969
task updateVersion(type: Copy) {

src/main/java/com/google/maps/OkHttpRequestHandler.java

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,24 @@
1717

1818
import com.google.gson.FieldNamingPolicy;
1919
import com.google.maps.GeoApiContext.RequestHandler;
20+
import com.google.maps.android.AndroidAuthenticationConfig;
21+
import com.google.maps.android.AndroidAuthenticationConfigProvider;
22+
import com.google.maps.android.AndroidAuthenticationInterceptor;
2023
import com.google.maps.internal.ApiResponse;
2124
import com.google.maps.internal.ExceptionsAllowedToRetry;
2225
import com.google.maps.internal.HttpHeaders;
2326
import com.google.maps.internal.OkHttpPendingResult;
2427
import com.google.maps.internal.RateLimitExecutorService;
2528
import com.google.maps.metrics.RequestMetrics;
26-
import java.io.IOException;
2729
import java.net.Proxy;
2830
import java.util.concurrent.ExecutorService;
2931
import java.util.concurrent.TimeUnit;
30-
import okhttp3.Authenticator;
3132
import okhttp3.Credentials;
3233
import okhttp3.Dispatcher;
3334
import okhttp3.MediaType;
3435
import okhttp3.OkHttpClient;
3536
import okhttp3.Request;
3637
import okhttp3.RequestBody;
37-
import okhttp3.Response;
38-
import okhttp3.Route;
3938

4039
/**
4140
* A strategy for handling URL requests using OkHttp.
@@ -130,6 +129,11 @@ public Builder() {
130129
rateLimitExecutorService = new RateLimitExecutorService();
131130
dispatcher = new Dispatcher(rateLimitExecutorService);
132131
builder.dispatcher(dispatcher);
132+
133+
final AndroidAuthenticationConfigProvider provider =
134+
new AndroidAuthenticationConfigProvider();
135+
final AndroidAuthenticationConfig config = provider.provide();
136+
builder.addInterceptor(new AndroidAuthenticationInterceptor(config));
133137
}
134138

135139
@Override
@@ -168,18 +172,14 @@ public Builder proxy(Proxy proxy) {
168172
public Builder proxyAuthentication(String proxyUserName, String proxyUserPassword) {
169173
final String userName = proxyUserName;
170174
final String password = proxyUserPassword;
171-
172175
builder.proxyAuthenticator(
173-
new Authenticator() {
174-
@Override
175-
public Request authenticate(Route route, Response response) throws IOException {
176-
String credential = Credentials.basic(userName, password);
177-
return response
178-
.request()
179-
.newBuilder()
180-
.header("Proxy-Authorization", credential)
181-
.build();
182-
}
176+
(route, response) -> {
177+
String credential = Credentials.basic(userName, password);
178+
return response
179+
.request()
180+
.newBuilder()
181+
.header("Proxy-Authorization", credential)
182+
.build();
183183
});
184184
return this;
185185
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.google.maps.android;
2+
3+
import org.jetbrains.annotations.Nullable;
4+
5+
/**
6+
* Configuration object containing Android authentication parameters for a particular installation.
7+
* The parameters in this config are used by all requests so that API key restrictions can be
8+
* enforced.
9+
*/
10+
public class AndroidAuthenticationConfig {
11+
public static AndroidAuthenticationConfig EMPTY = new AndroidAuthenticationConfig(null, null);
12+
13+
/** The package name of the Android app. */
14+
@Nullable public final String packageName;
15+
16+
/** The SHA-1 fingerprint of the certificate used to sign the Android app. */
17+
@Nullable public final String certFingerprint;
18+
19+
public AndroidAuthenticationConfig(
20+
@Nullable String packageName, @Nullable String certFingerprint) {
21+
this.packageName = packageName;
22+
this.certFingerprint = certFingerprint;
23+
}
24+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.google.maps.android;
2+
3+
/**
4+
* Provides a {@link AndroidAuthenticationConfig} that is specific to the environment the library is
5+
* in.
6+
*/
7+
public class AndroidAuthenticationConfigProvider {
8+
9+
/** @return the environment specific {@link AndroidAuthenticationConfig} */
10+
public AndroidAuthenticationConfig provide() {
11+
Context context = Context.getApplicationContext();
12+
if (context == null) {
13+
return AndroidAuthenticationConfig.EMPTY;
14+
}
15+
16+
return new AndroidAuthenticationConfig(
17+
context.getPackageName(), CertificateHelper.getSigningCertificateSha1Fingerprint(context));
18+
}
19+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.google.maps.android;
2+
3+
import com.google.maps.internal.HttpHeaders;
4+
import java.io.IOException;
5+
import okhttp3.Interceptor;
6+
import okhttp3.Request;
7+
import okhttp3.Response;
8+
import org.jetbrains.annotations.NotNull;
9+
10+
/**
11+
* Intercepts requests and provides Android-specific headers so that API key restrictions can be
12+
* enforced.
13+
*/
14+
public class AndroidAuthenticationInterceptor implements Interceptor {
15+
16+
private final AndroidAuthenticationConfig config;
17+
18+
public AndroidAuthenticationInterceptor(AndroidAuthenticationConfig config) {
19+
this.config = config;
20+
}
21+
22+
@NotNull
23+
@Override
24+
public Response intercept(@NotNull Chain chain) throws IOException {
25+
final Request request = chain.request();
26+
27+
if (config == AndroidAuthenticationConfig.EMPTY) {
28+
// Not in Android environment
29+
return chain.proceed(request);
30+
}
31+
32+
final Request.Builder builder = chain.request().newBuilder();
33+
if (config.packageName != null) {
34+
builder.addHeader(HttpHeaders.X_ANDROID_PACKAGE, config.packageName);
35+
}
36+
37+
if (config.certFingerprint != null) {
38+
builder.addHeader(HttpHeaders.X_ANDROID_CERT, config.certFingerprint);
39+
}
40+
41+
return chain.proceed(builder.build());
42+
}
43+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.google.maps.android;
2+
3+
import java.lang.reflect.InvocationTargetException;
4+
import java.lang.reflect.Method;
5+
import java.security.MessageDigest;
6+
import java.security.NoSuchAlgorithmException;
7+
import org.jetbrains.annotations.NotNull;
8+
import org.jetbrains.annotations.Nullable;
9+
10+
/** Helper class for obtaining information about an Android app's signing certificate. */
11+
public class CertificateHelper {
12+
13+
/**
14+
* Obtains the SHA-1 fingerprint of the certificate used to sign the app.
15+
*
16+
* @param context a Context
17+
* @return the SHA-1 fingerprint if obtainable, otherwise, <code>null</code>
18+
*/
19+
@Nullable
20+
public static String getSigningCertificateSha1Fingerprint(@NotNull Context context) {
21+
final PackageManager pm = context.getPackageManager();
22+
if (pm == null) {
23+
return null;
24+
}
25+
26+
// PackageManage.GET_SIGNATURES == 64
27+
PackageInfo packageInfo = pm.getPackageInfo(context.getPackageName(), 64);
28+
if (packageInfo == null) {
29+
return null;
30+
}
31+
32+
Object signingSignature = packageInfo.signingSignature();
33+
if (signingSignature == null) {
34+
return null;
35+
}
36+
37+
try {
38+
MessageDigest md = MessageDigest.getInstance("SHA-1");
39+
Class<?> signatureClass = Class.forName("android.content.pm.Signature");
40+
Method toByteArrayMethod = signatureClass.getMethod("toByteArray");
41+
byte[] byteArray = (byte[]) toByteArrayMethod.invoke(signingSignature);
42+
byte[] digest = md.digest(byteArray);
43+
return bytesToHex(digest);
44+
} catch (NoSuchAlgorithmException
45+
| ClassNotFoundException
46+
| IllegalAccessException
47+
| NoSuchMethodException
48+
| InvocationTargetException e) {
49+
return null;
50+
}
51+
}
52+
53+
private static String bytesToHex(byte[] byteArray) {
54+
final StringBuilder sb = new StringBuilder();
55+
for (byte b : byteArray) {
56+
sb.append(String.format("%02X", b));
57+
}
58+
return sb.toString();
59+
}
60+
}

0 commit comments

Comments
 (0)