Restricting usage for an Android key for a Google API

15,048

Solution 1

Hitting the API directly from your code rather than going through a Google-provided intermediate SDK means that there's no mechanism available to securely get your app's certificate fingerprint and pass that fingerprint along to the API. On the other hand, when you're using one of the provided Android SDKs instead of hitting the API directly—for example, when you send requests using the Android Google Maps SDK—the SDK can handle getting your app's certificate fingerprint so that the app restriction will work as intended.

The Google Developers Console is misleading in this respect because, for some of its APIs, it allows developers to set up key restrictions based on the Android app certificate fingerprint, but then doesn't make available an SDK for Android that's able to check that fingerprint at runtime. What developers are left with, then, is the worse, more insecure option of sending the X-Android-Cert and X-Android-Package headers alongside their requests as described in the other answer here.

So for the APIs for which no accompanying Android SDK to handle the checking of app certificate fingerprint has been published, it turns out that there's no hidden easy way to get something like, say, Google Play Services to handle getting your app's certificate fingerprint in order to properly use the app key restriction—there's just not a way to do it.

Solution 2

Everything you've done on Google Developer Console to restrict usage of your api key for Android app is OK. After restricted, this API key will only accept request from your app with package name and SHA-1 certificate fingerprint specified.

So how google know that request's sent FROM YOUR ANDROID APP? You MUST add your app's package name and SHA-1 in the header of each request (obviously). And you don't need GoogleAuthUtil and GET_ACCOUNTS permission.

FIRST, get your app SHA signature (you will need Guava library):

/**
 * Gets the SHA1 signature, hex encoded for inclusion with Google Cloud Platform API requests
 *
 * @param packageName Identifies the APK whose signature should be extracted.
 * @return a lowercase, hex-encoded
 */
public static String getSignature(@NonNull PackageManager pm, @NonNull String packageName) {
    try {
        PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
        if (packageInfo == null
                || packageInfo.signatures == null
                || packageInfo.signatures.length == 0
                || packageInfo.signatures[0] == null) {
            return null;
        }
        return signatureDigest(packageInfo.signatures[0]);
    } catch (PackageManager.NameNotFoundException e) {
        return null;
    }
}

private static String signatureDigest(Signature sig) {
    byte[] signature = sig.toByteArray();
    try {
        MessageDigest md = MessageDigest.getInstance("SHA1");
        byte[] digest = md.digest(signature);
        return BaseEncoding.base16().lowerCase().encode(digest);
    } catch (NoSuchAlgorithmException e) {
        return null;
    }
}

Then, add package name and SHA certificate signature to request header:

java.net.URL url = new URL(REQUEST_URL);
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
try {
    connection.setDoInput(true);
    connection.setDoOutput(true);

    connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
    connection.setRequestProperty("Accept", "application/json");

    // add package name to request header
    String packageName = mActivity.getPackageName();
    connection.setRequestProperty("X-Android-Package", packageName);
    // add SHA certificate to request header
    String sig = getSignature(mActivity.getPackageManager(), packageName);
    connection.setRequestProperty("X-Android-Cert", sig);
    connection.setRequestMethod("POST");

    // ADD YOUR REQUEST BODY HERE
    // ....................
} catch (Exception e) {
    e.printStackTrace();
} finally {
    connection.disconnect();
}

Other way, if you are using Google Vision API, you can build your request with VisionRequestInitializer:

try {
    HttpTransport httpTransport = AndroidHttp.newCompatibleTransport();
    JsonFactory jsonFactory = GsonFactory.getDefaultInstance();

    VisionRequestInitializer requestInitializer =
    new VisionRequestInitializer(CLOUD_VISION_API_KEY) {
    /**
         * We override this so we can inject important identifying fields into the HTTP
         * headers. This enables use of a restricted cloud platform API key.
         */
        @Override
        protected void initializeVisionRequest(VisionRequest<?> visionRequest)
            throws IOException {
            super.initializeVisionRequest(visionRequest);

            String packageName = mActivity.getPackageName();
            visionRequest.getRequestHeaders().set("X-Android-Package", packageName);

            String sig = getSignature(mActivity.getPackageManager(), packageName);
            visionRequest.getRequestHeaders().set("X-Android-Cert", sig);
        }
    };

    Vision.Builder builder = new Vision.Builder(httpTransport, jsonFactory, null);
    builder.setVisionRequestInitializer(requestInitializer);

    Vision vision = builder.build();

    BatchAnnotateImagesRequest batchAnnotateImagesRequest =
    new BatchAnnotateImagesRequest();
    batchAnnotateImagesRequest.setRequests(new ArrayList<AnnotateImageRequest>() {{
    AnnotateImageRequest annotateImageRequest = new AnnotateImageRequest();

    // Add the image
    Image base64EncodedImage = new Image();
    // Convert the bitmap to a JPEG
    // Just in case it's a format that Android understands but Cloud Vision
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    requestImage.compress(Bitmap.CompressFormat.JPEG, IMAGE_JPEG_QUALITY, byteArrayOutputStream);
    byte[] imageBytes = byteArrayOutputStream.toByteArray();

    // Base64 encode the JPEG
    base64EncodedImage.encodeContent(imageBytes);
    annotateImageRequest.setImage(base64EncodedImage);

    // add the features we want
    annotateImageRequest.setFeatures(new ArrayList<Feature>() {{
    Feature labelDetection = new Feature();
    labelDetection.setType(TYPE_TEXT_DETECTION);
    add(labelDetection);
    }});

    // Add the list of one thing to the request
    add(annotateImageRequest);
    }});

    Vision.Images.Annotate annotateRequest =
    vision.images().annotate(batchAnnotateImagesRequest);
    // Due to a bug: requests to Vision API containing large images fail when GZipped.
    annotateRequest.setDisableGZipContent(true);
    Log.d("TAG_SERVER", "created Cloud Vision request object, sending request");

    BatchAnnotateImagesResponse response = annotateRequest.execute();
        return convertResponseToString(response);
    } catch (GoogleJsonResponseException e) {
        Log.d("TAG_SERVER", "failed to make API request because " + e.getContent());
    } catch (IOException e) {
        Log.d("TAG_SERVER", "failed to make API request because of other IOException " +
        e.getMessage());
}

Add following dependencies to your gradle:

compile 'com.google.apis:google-api-services-vision:v1-rev2-1.21.0'
compile 'com.google.api-client:google-api-client-android:1.20.0' exclude module: 'httpclient'
compile 'com.google.http-client:google-http-client-gson:1.20.0' exclude module: 'httpclient'

Hope this help :)

Solution 3

When using a Google REST-only API, such as Translate, you'll need to use GoogleAuthUtil, which will generate a token for a specific user and package/fingerprint. However, that requires GET_ACCOUNTS permission, which smart users are wary of.

You could also use the AccountManager's getAuthToken() method, but that would require not only the GET_ACCOUNTS permission, but also USE_CREDENTIALS.

You might be best off using an API key and obscuring it a bit.

Solution 4

Package Restriction and Url signing

As I came across this post when I struggled with restricting access for inverse geo encoding and static map api I also want to share my findings.

Note that not all google services allow the same restrictions.

We use url signing and android / ios package restriction. Link to the Google documentation

Get apk fingerprint

There are multiple ways to get the fingerprint from the android apk.

With keystore

keytool -list -v keystore mystore.keystore

With apk

extract *.apk
navigate to folder META-INF
keytool.exe" -printcert -file *.RSA

C# Example code (Xamarin) to get started

In my productive code I have a base class for Headerinfo and provide an instace to the Geoprovider class. With this approach the code for the google services is 100% shared between windows, android and ios => nuget package.

Android Headers

httpWebRequest.Headers["x-android-package"] = "packageName";
httpWebRequest.Headers["x-android-package"] = "signature";

IOS Headers

httpWebRequest.Headers["x-ios-bundle-identifier"] = "bundleIdentifier";

Example code to fetch a static map

public byte[] GenerateMap(double latitude, double longitude, int zoom, string size, string mapType)
{
    string lat = latitude.ToString(CultureInfo.InvariantCulture);
    string lng = longitude.ToString(CultureInfo.InvariantCulture);
    string url = $"https://maps.googleapis.com/maps/api/staticmap?center={lat},{lng}&zoom={zoom}&size={size}&maptype={mapType}&markers={lat},{lng}&key={_apiKey}";

    // get the secret from your firebase console don't create always an new instance in productive code
    string signedUrl = new GoogleUrlSigner("mysecret").Sign(url);

    HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(signedUrl);

    //Add your headers httpWebRequest.Headers...

    // get the response for the request
    HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse();

    // do whatever you want to do with the response
}

sample code for url signing provided by google

https://developers.google.com/maps/documentation/geocoding/get-api-key

internal class GoogleUrlSigner
{
    private readonly string _secret;

    public GoogleUrlSigner(string secret)
    {
        _secret = secret;
    }

    internal string Sign(string url)
    {
        ASCIIEncoding encoding = new ASCIIEncoding();

        // converting key to bytes will throw an exception, need to replace '-' and '_' characters first.
        string usablePrivateKey = _secret.Replace("-", "+").Replace("_", "/");
        byte[] privateKeyBytes = Convert.FromBase64String(usablePrivateKey);

        Uri uri = new Uri(url);
        byte[] encodedPathAndQueryBytes = encoding.GetBytes(uri.LocalPath + uri.Query);

        // compute the hash
        HMACSHA1 algorithm = new HMACSHA1(privateKeyBytes);
        byte[] hash = algorithm.ComputeHash(encodedPathAndQueryBytes);

        // convert the bytes to string and make url-safe by replacing '+' and '/' characters
        string signature = Convert.ToBase64String(hash).Replace("+", "-").Replace("/", "_");

        // Add the signature to the existing URI.
        return uri.Scheme + "://" + uri.Host + uri.LocalPath + uri.Query + "&signature=" + signature;
    }
}
Share:
15,048
rmtheis
Author by

rmtheis

Updated on June 03, 2022

Comments

  • rmtheis
    rmtheis almost 2 years

    My question is about how to properly set the package name and SHA-1 certificate fingerprint in the Google Developers Console in order to restrict usage of my Android API key to my app.

    When I don't have anything set in the "Restrict usage to your Android apps" section, my requests to the Google Translate API work properly. The API responds normally with status code 200 and my expected result.

    But when I specify a package name and SHA-1 certificate fingerprint for my app using the Developers Console, I consistently get 403 Forbidden responses like the following:

    HTTP/1.1 403 Forbidden
    Vary: Origin
    Vary: X-Origin
    Content-Type: application/json; charset=UTF-8
    Date: Sun, 29 Nov 2015 21:01:39 GMT
    Expires: Sun, 29 Nov 2015 21:01:39 GMT
    Cache-Control: private, max-age=0
    X-Content-Type-Options: nosniff
    X-Frame-Options: SAMEORIGIN
    X-XSS-Protection: 1; mode=block
    Server: GSE
    Alternate-Protocol: 443:quic,p=1
    Alt-Svc: quic=":443"; ma=604800; v="30,29,28,27,26,25"
    Content-Length: 729
    
    {
     "error": {
      "errors": [
       {
        "domain": "usageLimits",
        "reason": "ipRefererBlocked",
        "message": "There is a per-IP or per-Referer restriction configured on your API key and the request does not match these restrictions. Please use the Google Developers Console to update your API key configuration if request from this IP or referer should be allowed.",
        "extendedHelp": "https://console.developers.google.com"
       }
      ],
      "code": 403,
      "message": "There is a per-IP or per-Referer restriction configured on your API key and the request does not match these restrictions. Please use the Google Developers Console to update your API key configuration if request from this IP or referer should be allowed."
     }
    }
    

    The request looks like the following. Notice that there's no referer header in the request:

    GET https://www.googleapis.com/language/translate/v2?key=XXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXXXXXXXXX&source=en&target=es&q=test HTTP/1.1
    User-Agent: Dalvik/2.1.0 (Linux; U; Android 5.1.1; Nexus 6 Build/LVY48H)
    Host: www.googleapis.com
    Connection: Keep-Alive
    Accept-Encoding: gzip
    

    I'm assuming that the error message indicates a package name or SHA-1 fingerprint problem, despite its message about a "per-IP or per-Referer restriction". While browser keys allow the setting of a per-referer restriction, I'm using an Android key with nowhere to set a per-IP or per-Referer restriction.

    I'm sure I have entered the package name correctly in the Google Developers Console. I'm reading the package name from the package attribute on the manifest tag in my Android manifest file.

    I'm also sure I have the SHA-1 fingerprint set correctly in the Google Developers Console. I'm reading this value from my keystore using the command keytool -list -v -keystore /path/to/my/keystore. I get the same value when I read it from the APK file using keytool -list -printcert -jarfile myAppName.apk. I'm installing that same APK file using adb.

    Here's what I see in the Developers Console:

    console screenshot

    I've tested this on multiple devices running stock Android. I get the error response on wifi and on the cell network, whether I'm proxying the traffic or not.

    When I remove the restriction from the Developers Console, the app works properly again.

    What am I doing wrong here?

    Note: Several similar questions have been asked before, but with no adequate answers. I don't want to use a browser key or remove the restriction altogether. I want to get the usage restriction to work properly.

  • abdfahim
    abdfahim about 8 years
    Is Google Books API also REST-only API? I have seen similar issue with Google Books API. It'll be a shame if I have to ask user for GET_ACCOUNTS permission (let alone USE_CREDENTIALS) just to show some basic info about a book.
  • rmtheis
    rmtheis about 8 years
    From the Android documentation: "Note: Beginning with Android 6.0 (API level 23), if an app shares the signature of the authenticator that manages an account, it does not need GET_ACCOUNTS permission to read information about that account. On Android 5.1 and lower, all apps need GET_ACCOUNTS permission to read information about any account."
  • rmtheis
    rmtheis about 8 years
    This looks like it should work, because I receive the token OK, but when I make the translate request with the API key along with the OAuth token in the Authorization header, I still see the ipRefererBlocked error. Still investigating...
  • rmtheis
    rmtheis almost 8 years
    As of recently the message returned by the API has changed to be The Android package name and signing-certificate fingerprint, null and null, do not match the app restrictions configured on your API key. I'm using Google Play Services 9.2.0. This seems like a step in the right direction, but it's still not working.
  • sandeepd
    sandeepd about 7 years
    I tried to use this with Cloud Endpoints(v2), where API key is restricted. Did not work. "Requests from this Android client application <empty> are blocked."
  • Patrick Boos
    Patrick Boos over 6 years
    Well, that would not really add any security, right? it is just adding headers. That is as much security as the API key. it would have to somehow use a signature with something that only the correctly signed app can get.
  • Duy Pham
    Duy Pham over 6 years
    I think there is no completely security way, you should hide your api key from codes, and use SSL/TLS for security request header, so i suggest you read 2 articles: androidauthority.com/how-to-hide-your-api-key-in-android-600‌​583, and developer.android.com/training/articles/security-ssl.html
  • arunit21
    arunit21 about 4 years
    Thanks for mentioning the cases to handle in the iOS application too.
  • l33t
    l33t about 3 years
    With emphasis on the worse, more insecure option.
  • João Pimentel Ferreira
    João Pimentel Ferreira about 3 years
    Can I do the same with the typical GET request https://maps.googleapis.com/maps/api/js?key=?