All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.crypto.tink.apps.rewardedads.RewardedAdsVerifier Maven / Gradle / Ivy

// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////

package com.google.crypto.tink.apps.rewardedads;

import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.crypto.tink.subtle.Base64;
import com.google.crypto.tink.subtle.EcdsaVerifyJce;
import com.google.crypto.tink.subtle.EllipticCurves;
import com.google.crypto.tink.subtle.EllipticCurves.EcdsaEncoding;
import com.google.crypto.tink.subtle.Enums.HashType;
import com.google.crypto.tink.util.KeysDownloader;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.security.GeneralSecurityException;
import java.security.interfaces.ECPublicKey;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/**
 * An implementation of the verifier side of Server-Side Verification of Google AdMob Rewarded Ads.
 *
 * 

Typical usage: * *

{@code
 * RewardedAdsVerifier verifier = new RewardedAdsVerifier.Builder()
 *     .fetchVerifyingPublicKeysWith(
 *         RewardedAdsVerifier.KEYS_DOWNLOADER_INSTANCE_PROD)
 *     .build();
 * String rewardUrl = ...;
 * verifier.verify(rewardUrl);
 * }
* *

This usage ensures that you always have the latest public keys, even when the keys were * recently rotated. It will fetch and cache the latest public keys from {#PUBLIC_KEYS_URL_PROD}. * When the cache expires, it will re-fetch the public keys. When initializing your server, we also * recommend that you call {@link KeysDownloader#refreshInBackground()} of {@link * RewardedAdsVerifier.KEYS_DOWNLOADER_INSTANCE_PROD} to proactively fetch the public keys. * *

If you've already downloaded the public keys and have other means to manage key rotation, you * can use {@link RewardedAdsVerifier.Builder#setVerifyingPublicKeys} to set the public keys. The * Builder also allows you to customize other properties. */ public final class RewardedAdsVerifier { /** Default HTTP transport used by this class. */ private static final NetHttpTransport DEFAULT_HTTP_TRANSPORT = new NetHttpTransport.Builder().build(); private static final Executor DEFAULT_BACKGROUND_EXECUTOR = Executors.newCachedThreadPool(); private final List verifyingPublicKeysProviders; public static final String SIGNATURE_PARAM_NAME = "signature="; public static final String KEY_ID_PARAM_NAME = "key_id="; /** URL to fetch keys for environment production. */ public static final String PUBLIC_KEYS_URL_PROD = "https://www.gstatic.com/admob/reward/verifier-keys.json"; /** URL to fetch keys for environment test. */ public static final String PUBLIC_KEYS_URL_TEST = "https://www.gstatic.com/admob/reward/verifier-keys-test.json"; /** * Instance configured to talk to fetch keys from production environment (from {@link * KeysDownloader#PUBLIC_KEYS_URL_PROD}). */ public static final KeysDownloader KEYS_DOWNLOADER_INSTANCE_PROD = new KeysDownloader(DEFAULT_BACKGROUND_EXECUTOR, DEFAULT_HTTP_TRANSPORT, PUBLIC_KEYS_URL_PROD); /** * Instance configured to talk to fetch keys from test environment (from {@link * KeysDownloader#KEYS_URL_TEST}). */ public static final KeysDownloader KEYS_DOWNLOADER_INSTANCE_TEST = new KeysDownloader(DEFAULT_BACKGROUND_EXECUTOR, DEFAULT_HTTP_TRANSPORT, PUBLIC_KEYS_URL_TEST); RewardedAdsVerifier(List verifyingPublicKeysProviders) throws GeneralSecurityException { if (verifyingPublicKeysProviders == null || verifyingPublicKeysProviders.isEmpty()) { throw new IllegalArgumentException( "must set at least one way to get verifying key using" + " Builder.fetchVerifyingPublicKeysWith or Builder.setVerifyingPublicKeys"); } this.verifyingPublicKeysProviders = verifyingPublicKeysProviders; } private RewardedAdsVerifier(Builder builder) throws GeneralSecurityException { this(builder.verifyingPublicKeysProviders); } /** * Verifies that {@code rewardUrl} has a valid signature. * *

This method assumes that the name of the last two query parameters of {@code rewardUrl} are * {@link #SIGNATURE_PARAM_NAME} and {@link #KEY_ID_PARAM_NAME} in that order. */ public void verify(String rewardUrl) throws GeneralSecurityException { URI uri; try { uri = new URI(rewardUrl); } catch (URISyntaxException ex) { throw new GeneralSecurityException(ex); } String queryString = uri.getQuery(); int i = queryString.indexOf(SIGNATURE_PARAM_NAME); if (i == -1) { throw new GeneralSecurityException("needs a signature query parameter"); } byte[] tbsData = queryString .substring(0, i - 1 /* i - 1 instead of i because of & */) .getBytes(Charset.forName("UTF-8")); String sigAndKeyId = queryString.substring(i); i = sigAndKeyId.indexOf(KEY_ID_PARAM_NAME); if (i == -1) { throw new GeneralSecurityException("needs a key_id query parameter"); } String sig = sigAndKeyId.substring( SIGNATURE_PARAM_NAME.length(), i - 1 /* i - 1 instead of i because of & */); long keyId = Long.parseLong(sigAndKeyId.substring(i + KEY_ID_PARAM_NAME.length())); verify(tbsData, keyId, Base64.urlSafeDecode(sig)); } private void verify(final byte[] tbs, long keyId, final byte[] signature) throws GeneralSecurityException { boolean foundKeyId = false; for (VerifyingPublicKeysProvider provider : verifyingPublicKeysProviders) { Map publicKeys = provider.get(); if (publicKeys.containsKey(keyId)) { foundKeyId = true; ECPublicKey publicKey = publicKeys.get(keyId); EcdsaVerifyJce verifier = new EcdsaVerifyJce(publicKey, HashType.SHA256, EcdsaEncoding.DER); verifier.verify(signature, tbs); } } if (!foundKeyId) { throw new GeneralSecurityException("cannot find verifying key with key id: " + keyId); } } /** Builder for RewardedAdsVerifier. */ public static class Builder { private final List verifyingPublicKeysProviders = new ArrayList(); public Builder() {} /** * Fetches verifying public keys of the sender using {@link KeysDownloader}. * *

This is the preferred method of specifying the verifying public keys. */ public Builder fetchVerifyingPublicKeysWith(final KeysDownloader downloader) throws GeneralSecurityException { this.verifyingPublicKeysProviders.add( new VerifyingPublicKeysProvider() { @Override public Map get() throws GeneralSecurityException { try { return parsePublicKeysJson(downloader.download()); } catch (IOException e) { throw new GeneralSecurityException("Failed to fetch keys!", e); } } }); return this; } /** * Sets the trusted verifying public keys of the sender. * *

IMPORTANT: Instead of using this method to set the verifying public keys of the * sender, prefer calling {@link #fetchVerifyingPublicKeysWith} passing it an instance of {@link * KeysDownloader}. It will take care of fetching fresh keys and caching in memory. Only use * this method if you can't use {@link #fetchVerifyingPublicKeysWith} and be aware you will need * to handle key rotations yourself. * *

The given string is a JSON object formatted like the following: * *

     * {
     *   "keys": [
     *     {
     *       key_id: 1916455855,
     *       pem: "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUaWMKcBHWdhUE+DncSIHhFCLLEln\nUs0LB9oanZ4K/FNICIM8ltS4nzc9yjmhgVQOlmSS6unqvN9t8sqajRTPcw==\n-----END PUBLIC KEY-----"
     *       base64: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUaWMKcBHWdhUE+DncSIHhFCLLElnUs0LB9oanZ4K/FNICIM8ltS4nzc9yjmhgVQOlmSS6unqvN9t8sqajRTPcw=="
     *     },
     *     {
     *       key_id: 3901585526,
     *       pem: "-----BEGIN PUBLIC KEY-----\nMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEtxg2BsK/fllIeADtLspezS6YfHFWXZ8tiJncm8LDBa/NxEC84akdWbWDCUrMMGIV27/3/e7UuKSEonjGvaDUsw==\n-----END PUBLIC KEY-----"
     *       base64: "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEtxg2BsK/fllIeADtLspezS6YfHFWXZ8tiJncm8LDBa/NxEC84akdWbWDCUrMMGIV27/3/e7UuKSEonjGvaDUsw=="
     *     },
     *   ],
     * }
     * 
* *

Each public key will be a base64 (no wrapping, padded) version of the key encoded in ASN.1 * type SubjectPublicKeyInfo defined in the X.509 standard. */ public Builder setVerifyingPublicKeys(final String publicKeysJson) throws GeneralSecurityException { this.verifyingPublicKeysProviders.add( new VerifyingPublicKeysProvider() { @Override public Map get() throws GeneralSecurityException { return parsePublicKeysJson(publicKeysJson); } }); return this; } /** * Adds a verifying public key of the sender. * *

IMPORTANT: Instead of using this method to set the verifying public keys of the * sender, prefer calling {@link #fetchVerifyingPublicKeysWith} passing it an instance of {@link * KeysDownloader}. It will take care of fetching fresh keys and caching in memory. Only use * this method if you can't use {@link #fetchVerifyingPublicKeysWith} and be aware you will need * to handle Google key rotations yourself. * *

The public key is a base64 (no wrapping, padded) version of the key encoded in ASN.1 type * SubjectPublicKeyInfo defined in the X.509 standard. * *

Multiple keys may be added. This utility will then verify any message signed with any of * the private keys corresponding to the public keys added. Adding multiple keys is useful for * handling key rotation. */ public Builder addVerifyingPublicKey(final long keyId, final String val) throws GeneralSecurityException { this.verifyingPublicKeysProviders.add( new VerifyingPublicKeysProvider() { @Override public Map get() throws GeneralSecurityException { return Collections.singletonMap( keyId, EllipticCurves.getEcPublicKey(Base64.decode(val))); } }); return this; } /** * Adds a verifying public key of the sender. * *

IMPORTANT: Instead of using this method to set the verifying public keys of the * sender, prefer calling {@link #fetchVerifyingPublicKeysWith} passing it an instance of {@link * KeysDownloader}. It will take care of fetching fresh keys and caching in memory. Only use * this method if you can't use {@link #fetchVerifyingPublicKeysWith} and be aware you will need * to handle Google key rotations yourself. */ public Builder addVerifyingPublicKey(final long keyId, final ECPublicKey val) throws GeneralSecurityException { this.verifyingPublicKeysProviders.add( new VerifyingPublicKeysProvider() { @Override public Map get() throws GeneralSecurityException { return Collections.singletonMap(keyId, val); } }); return this; } public RewardedAdsVerifier build() throws GeneralSecurityException { return new RewardedAdsVerifier(this); } } private static Map parsePublicKeysJson(String publicKeysJson) throws GeneralSecurityException { Map publicKeys = new HashMap<>(); try { JSONArray keys = new JSONObject(publicKeysJson).getJSONArray("keys"); for (int i = 0; i < keys.length(); i++) { JSONObject key = keys.getJSONObject(i); publicKeys.put( key.getLong("keyId"), EllipticCurves.getEcPublicKey(Base64.decode(key.getString("base64")))); } } catch (JSONException e) { throw new GeneralSecurityException("failed to extract trusted signing public keys", e); } if (publicKeys.isEmpty()) { throw new GeneralSecurityException("no trusted keys are available for this protocol version"); } return publicKeys; } private interface VerifyingPublicKeysProvider { Map get() throws GeneralSecurityException; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy