From fe0f5a5434953fb5a805a6f760005fb7c5d893df Mon Sep 17 00:00:00 2001 From: hsudhof Date: Fri, 10 Jul 2020 08:08:48 -0700 Subject: [PATCH] Refactor PiperOrigin-RevId: 320604527 --- .../gms/example/apidemo/DFPPPIDFragment.java | 16 ++- java/advanced/RewardedSSVExample/Dockerfile | 7 + java/advanced/RewardedSSVExample/README.md | 48 +++++++ java/advanced/RewardedSSVExample/build.gradle | 31 +++++ .../RewardedSSVExample/docker-compose.yml | 18 +++ .../gradle/wrapper/gradle-wrapper.properties | 6 + java/advanced/RewardedSSVExample/pom.xml | 59 +++++++++ .../RewardedSSVExample/settings.gradle | 1 + .../com/example/rewardedssv/Application.java | 21 +++ .../example/rewardedssv/SSVController.java | 122 ++++++++++++++++++ 10 files changed, 322 insertions(+), 7 deletions(-) create mode 100644 java/advanced/RewardedSSVExample/Dockerfile create mode 100644 java/advanced/RewardedSSVExample/README.md create mode 100644 java/advanced/RewardedSSVExample/build.gradle create mode 100644 java/advanced/RewardedSSVExample/docker-compose.yml create mode 100644 java/advanced/RewardedSSVExample/gradle/wrapper/gradle-wrapper.properties create mode 100644 java/advanced/RewardedSSVExample/pom.xml create mode 100644 java/advanced/RewardedSSVExample/settings.gradle create mode 100644 java/advanced/RewardedSSVExample/src/main/java/com/example/rewardedssv/Application.java create mode 100644 java/advanced/RewardedSSVExample/src/main/java/com/example/rewardedssv/SSVController.java diff --git a/java/advanced/APIDemo/app/src/main/java/com/google/android/gms/example/apidemo/DFPPPIDFragment.java b/java/advanced/APIDemo/app/src/main/java/com/google/android/gms/example/apidemo/DFPPPIDFragment.java index 7a790952f..f44685d66 100644 --- a/java/advanced/APIDemo/app/src/main/java/com/google/android/gms/example/apidemo/DFPPPIDFragment.java +++ b/java/advanced/APIDemo/app/src/main/java/com/google/android/gms/example/apidemo/DFPPPIDFragment.java @@ -26,6 +26,7 @@ import android.widget.Toast; import com.google.android.gms.ads.doubleclick.PublisherAdRequest; import com.google.android.gms.ads.doubleclick.PublisherAdView; +import com.google.security.annotations.SuppressInsecureCipherModeCheckerNoReview; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -75,14 +76,15 @@ public void onClick(View view) { }); } - // This is a simple method to generate a hash of a sample username to use as a PPID. It's being - // used here as a convenient stand-in for a true Publisher-Provided Identifier. In your own - // apps, you can decide for yourself how to generate the PPID value, though there are some - // restrictions on what the values can be. For details, see: - // - // https://support.google.com/dfp_premium/answer/2880055 + // This is a simple method to generate a hash of a sample username to use as a PPID. It's being + // used here as a convenient stand-in for a true Publisher-Provided Identifier. In your own + // apps, you can decide for yourself how to generate the PPID value, though there are some + // restrictions on what the values can be. For details, see: + // + // https://support.google.com/dfp_premium/answer/2880055 - private String generatePPID(String username) { + @SuppressInsecureCipherModeCheckerNoReview + private String generatePPID(String username) { StringBuilder ppid = new StringBuilder(); try { diff --git a/java/advanced/RewardedSSVExample/Dockerfile b/java/advanced/RewardedSSVExample/Dockerfile new file mode 100644 index 000000000..ceae366b0 --- /dev/null +++ b/java/advanced/RewardedSSVExample/Dockerfile @@ -0,0 +1,7 @@ +FROM openjdk:8-jdk-stretch + +RUN apt-get update --fix-missing +RUN apt-get install -y vim +COPY ./ /var/www +WORKDIR /var/www +EXPOSE 8080 diff --git a/java/advanced/RewardedSSVExample/README.md b/java/advanced/RewardedSSVExample/README.md new file mode 100644 index 000000000..9639e307a --- /dev/null +++ b/java/advanced/RewardedSSVExample/README.md @@ -0,0 +1,48 @@ +# Google AdMob Rewarded Ads Server Side Verification + +Server-side verification callbacks are URL requests, with query parameters +expanded by Google, that are sent by Google to an external system to notify it +that a user should be rewarded for interacting with a rewarded video ad. +Rewarded video SSV (server-side verification) callbacks provide an extra layer +of protection against spoofing of client-side callbacks to reward users. + +## Description + +This project is developed in Java spring-boot framework as an example to verify +rewarded video SSV callbacks by using the Tink third-party cryptographic library +to ensure that the query parameters in the callback are legitimate values. + +## How to use + +1. Deploy this project on your preferred web service provider. +2. Follow the + [Set up and test server-sideverification](https://support.google.com/admob/answer/9603226) + instructions to create an ad unit and configure/test your server-side + verification endpoint. + +## Local Development + +To start with Java: + +1 `cd RewardedSSVExample` 2 `./gradlew bootRun` + +To start with Docker: + +`docker-compose up --build` + +## Local testing + +To test a signature and message, send a `GET` request to +`localhost:8080/verify?&signature=&key_id=`. + +A successful response looks like this: + +``` +{ + "sig": "ME...Z1c", + "payload": "ad_network=54...55&ad_unit=12345678&reward_amount=10&reward_item=coins ×tamp=150777823&transaction_id=12...DEF&user_id=1234567", + "key_id": "1268887", + "verified": "true" +} +``` + diff --git a/java/advanced/RewardedSSVExample/build.gradle b/java/advanced/RewardedSSVExample/build.gradle new file mode 100644 index 000000000..5a05072d8 --- /dev/null +++ b/java/advanced/RewardedSSVExample/build.gradle @@ -0,0 +1,31 @@ +plugins { + id 'org.springframework.boot' version '2.2.2.RELEASE' + id 'io.spring.dependency-management' version '1.0.8.RELEASE' + id 'java' +} + +group = 'com.example.rewardedssv' +version = '1.0.0' +sourceCompatibility = '1.8' + +repositories { + mavenCentral() +} + +dependencies { + implementation 'com.vaadin.external.google:android-json:0.0.20131108.vaadin1' + implementation 'com.google.crypto.tink:tink-android:1.4.0-rc1' + // tag::actuator[] + implementation 'org.springframework.boot:spring-boot-starter-actuator' + // end::actuator[] + implementation 'org.springframework.boot:spring-boot-starter-web' + // tag::tests[] + testImplementation('org.springframework.boot:spring-boot-starter-test') { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + } + // end::tests[] +} + +test { + useJUnitPlatform() +} diff --git a/java/advanced/RewardedSSVExample/docker-compose.yml b/java/advanced/RewardedSSVExample/docker-compose.yml new file mode 100644 index 000000000..5150b05c1 --- /dev/null +++ b/java/advanced/RewardedSSVExample/docker-compose.yml @@ -0,0 +1,18 @@ +version: '3' + +services: + ssv: + image: ssv + build: + context: . + dockerfile: Dockerfile + container_name: ssv + ports: + - 8080:8080 + volumes: + - .:/var/www + command: ./gradlew bootRun + networks: ['stack'] +networks: + stack: + driver: bridge diff --git a/java/advanced/RewardedSSVExample/gradle/wrapper/gradle-wrapper.properties b/java/advanced/RewardedSSVExample/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..a36562c71 --- /dev/null +++ b/java/advanced/RewardedSSVExample/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu May 14 12:27:25 IST 2020 +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.3-all.zip +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/java/advanced/RewardedSSVExample/pom.xml b/java/advanced/RewardedSSVExample/pom.xml new file mode 100644 index 000000000..ba950c7fb --- /dev/null +++ b/java/advanced/RewardedSSVExample/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.2.RELEASE + + + com.example + rewardedssv + 1.0.0 + spring-boot + Demo project for Spring Boot + + + 1.8 + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/java/advanced/RewardedSSVExample/settings.gradle b/java/advanced/RewardedSSVExample/settings.gradle new file mode 100644 index 000000000..9c0074173 --- /dev/null +++ b/java/advanced/RewardedSSVExample/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'rewarded-ssv' diff --git a/java/advanced/RewardedSSVExample/src/main/java/com/example/rewardedssv/Application.java b/java/advanced/RewardedSSVExample/src/main/java/com/example/rewardedssv/Application.java new file mode 100644 index 000000000..e624556a9 --- /dev/null +++ b/java/advanced/RewardedSSVExample/src/main/java/com/example/rewardedssv/Application.java @@ -0,0 +1,21 @@ +package com.example.rewardedssv; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; + +/** Application entry point */ +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + public CommandLineRunner commandLineRunner(ApplicationContext ctx) { + return args -> {}; + } +} diff --git a/java/advanced/RewardedSSVExample/src/main/java/com/example/rewardedssv/SSVController.java b/java/advanced/RewardedSSVExample/src/main/java/com/example/rewardedssv/SSVController.java new file mode 100644 index 000000000..7fd4a2a1a --- /dev/null +++ b/java/advanced/RewardedSSVExample/src/main/java/com/example/rewardedssv/SSVController.java @@ -0,0 +1,122 @@ +package com.example.rewardedssv; + +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 java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.Charset; +import java.security.GeneralSecurityException; +import java.security.interfaces.ECPublicKey; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** SSV REST Controller */ +@RestController +public class SSVController { + private static final String SIGNATURE_PARAM_KEY = "signature"; + private static final String KEY_ID_PARAM_KEY = "key_id"; + private static final String REWARD_VERIFIER_KEYS_URL = + "https://www.gstatic.com/admob/reward/verifier-keys.json"; + + private static Map parsePublicKeysJson() + throws GeneralSecurityException, IOException, JSONException { + URL url = new URL(REWARD_VERIFIER_KEYS_URL); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + String inputLine; + StringBuffer content = new StringBuffer(); + while ((inputLine = reader.readLine()) != null) { + content.append(inputLine); + } + reader.close(); + connection.disconnect(); + String publicKeysJson = content.toString(); + JSONArray keys = new JSONObject(publicKeysJson).getJSONArray("keys"); + Map publicKeys = new HashMap<>(); + 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")))); + } + if (publicKeys.isEmpty()) { + throw new GeneralSecurityException("No trusted keys are available for this protocol version"); + } + return publicKeys; + } + + private void verify(final byte[] dataToVerify, Long keyId, final byte[] signature) + throws GeneralSecurityException { + try { + Map publicKeys = parsePublicKeysJson(); + if (publicKeys.containsKey(keyId)) { + ECPublicKey publicKey = publicKeys.get(keyId); + EcdsaVerifyJce verifier = new EcdsaVerifyJce(publicKey, HashType.SHA256, EcdsaEncoding.DER); + verifier.verify(signature, dataToVerify); + } else { + throw new GeneralSecurityException( + String.format("Cannot find verifying key with key id: %s.", keyId)); + } + } catch (JSONException exception) { + throw new GeneralSecurityException(exception); + } catch (IOException exception) { + throw new GeneralSecurityException(exception); + } + } + + @GetMapping(value = "/verify") + public ResponseEntity index(HttpServletRequest request) { + + Enumeration enumeration = request.getParameterNames(); + Map parameters = request.getParameterMap(); + + Map response = new HashMap<>(); + if (!parameters.containsKey(KEY_ID_PARAM_KEY) || !parameters.containsKey(SIGNATURE_PARAM_KEY)) { + response.put("verified", Boolean.FALSE.toString()); + response.put("error", "Missing key_id and/or signature parameters."); + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } + + Long keyId = Long.valueOf(parameters.get(KEY_ID_PARAM_KEY)[0]); + String signature = parameters.get(SIGNATURE_PARAM_KEY)[0]; + String queryString = request.getQueryString(); + /* The last two query parameters of rewarded video + SSV callbacks are always signature and key_id + https://developers.google.com/admob/android/rewarded-video-ssv#get_content_to_be_verified + */ + byte[] payload = + queryString + .substring(0, queryString.indexOf(SIGNATURE_PARAM_KEY) - 1) + .getBytes(Charset.forName("UTF-8")); + + response.put("payload", new String(payload)); + response.put("key_id", keyId.toString()); + response.put("sig", signature); + HttpStatus status = HttpStatus.OK; + try { + verify(payload, keyId, Base64.urlSafeDecode(signature)); + response.put("verified", Boolean.TRUE.toString()); + } catch (GeneralSecurityException exception) { + status = HttpStatus.BAD_REQUEST; + response.put("verified", Boolean.FALSE.toString()); + response.put("error", exception.getMessage()); + } + return new ResponseEntity<>(response, status); + } +}