Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor #253

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}