Skip to content

Commit

Permalink
Refactor
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 320604527
  • Loading branch information
hsudhof authored and maddevrelgithubbot committed Jul 15, 2020
1 parent c9c358e commit 6d9412d
Show file tree
Hide file tree
Showing 10 changed files with 322 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 7 additions & 0 deletions java/advanced/RewardedSSVExample/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
48 changes: 48 additions & 0 deletions java/advanced/RewardedSSVExample/README.md
Original file line number Diff line number Diff line change
@@ -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?<dataToVerify>&signature=<signature>&key_id=<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 &timestamp=150777823&transaction_id=12...DEF&user_id=1234567",
"key_id": "1268887",
"verified": "true"
}
```

31 changes: 31 additions & 0 deletions java/advanced/RewardedSSVExample/build.gradle
Original file line number Diff line number Diff line change
@@ -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()
}
18 changes: 18 additions & 0 deletions java/advanced/RewardedSSVExample/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
59 changes: 59 additions & 0 deletions java/advanced/RewardedSSVExample/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>rewardedssv</artifactId>
<version>1.0.0</version>
<name>spring-boot</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- tag::actuator[] -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- end::actuator[] -->

<!-- tag::tests[] -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- end::tests[] -->
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
1 change: 1 addition & 0 deletions java/advanced/RewardedSSVExample/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rootProject.name = 'rewarded-ssv'
Original file line number Diff line number Diff line change
@@ -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 -> {};
}
}
Original file line number Diff line number Diff line change
@@ -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<Long, ECPublicKey> 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<Long, ECPublicKey> 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<Long, ECPublicKey> 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<String, String[]> parameters = request.getParameterMap();

Map<String, String> 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);
}
}

0 comments on commit 6d9412d

Please sign in to comment.