Skip to content

Commit fe0f5a5

Browse files
hsudhofmaddevrelgithubbot
authored andcommitted
Refactor
PiperOrigin-RevId: 320604527
1 parent c9c358e commit fe0f5a5

File tree

10 files changed

+322
-7
lines changed

10 files changed

+322
-7
lines changed

java/advanced/APIDemo/app/src/main/java/com/google/android/gms/example/apidemo/DFPPPIDFragment.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import android.widget.Toast;
2727
import com.google.android.gms.ads.doubleclick.PublisherAdRequest;
2828
import com.google.android.gms.ads.doubleclick.PublisherAdView;
29+
import com.google.security.annotations.SuppressInsecureCipherModeCheckerNoReview;
2930
import java.security.MessageDigest;
3031
import java.security.NoSuchAlgorithmException;
3132

@@ -75,14 +76,15 @@ public void onClick(View view) {
7576
});
7677
}
7778

78-
// This is a simple method to generate a hash of a sample username to use as a PPID. It's being
79-
// used here as a convenient stand-in for a true Publisher-Provided Identifier. In your own
80-
// apps, you can decide for yourself how to generate the PPID value, though there are some
81-
// restrictions on what the values can be. For details, see:
82-
//
83-
// https://support.google.com/dfp_premium/answer/2880055
79+
// This is a simple method to generate a hash of a sample username to use as a PPID. It's being
80+
// used here as a convenient stand-in for a true Publisher-Provided Identifier. In your own
81+
// apps, you can decide for yourself how to generate the PPID value, though there are some
82+
// restrictions on what the values can be. For details, see:
83+
//
84+
// https://support.google.com/dfp_premium/answer/2880055
8485

85-
private String generatePPID(String username) {
86+
@SuppressInsecureCipherModeCheckerNoReview
87+
private String generatePPID(String username) {
8688
StringBuilder ppid = new StringBuilder();
8789

8890
try {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
FROM openjdk:8-jdk-stretch
2+
3+
RUN apt-get update --fix-missing
4+
RUN apt-get install -y vim
5+
COPY ./ /var/www
6+
WORKDIR /var/www
7+
EXPOSE 8080
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Google AdMob Rewarded Ads Server Side Verification
2+
3+
Server-side verification callbacks are URL requests, with query parameters
4+
expanded by Google, that are sent by Google to an external system to notify it
5+
that a user should be rewarded for interacting with a rewarded video ad.
6+
Rewarded video SSV (server-side verification) callbacks provide an extra layer
7+
of protection against spoofing of client-side callbacks to reward users.
8+
9+
## Description
10+
11+
This project is developed in Java spring-boot framework as an example to verify
12+
rewarded video SSV callbacks by using the Tink third-party cryptographic library
13+
to ensure that the query parameters in the callback are legitimate values.
14+
15+
## How to use
16+
17+
1. Deploy this project on your preferred web service provider.
18+
2. Follow the
19+
[Set up and test server-sideverification](https://support.google.com/admob/answer/9603226)
20+
instructions to create an ad unit and configure/test your server-side
21+
verification endpoint.
22+
23+
## Local Development
24+
25+
To start with Java:
26+
27+
1 `cd RewardedSSVExample` 2 `./gradlew bootRun`
28+
29+
To start with Docker:
30+
31+
`docker-compose up --build`
32+
33+
## Local testing
34+
35+
To test a signature and message, send a `GET` request to
36+
`localhost:8080/verify?<dataToVerify>&signature=<signature>&key_id=<key_id>`.
37+
38+
A successful response looks like this:
39+
40+
```
41+
{
42+
"sig": "ME...Z1c",
43+
"payload": "ad_network=54...55&ad_unit=12345678&reward_amount=10&reward_item=coins &timestamp=150777823&transaction_id=12...DEF&user_id=1234567",
44+
"key_id": "1268887",
45+
"verified": "true"
46+
}
47+
```
48+
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
plugins {
2+
id 'org.springframework.boot' version '2.2.2.RELEASE'
3+
id 'io.spring.dependency-management' version '1.0.8.RELEASE'
4+
id 'java'
5+
}
6+
7+
group = 'com.example.rewardedssv'
8+
version = '1.0.0'
9+
sourceCompatibility = '1.8'
10+
11+
repositories {
12+
mavenCentral()
13+
}
14+
15+
dependencies {
16+
implementation 'com.vaadin.external.google:android-json:0.0.20131108.vaadin1'
17+
implementation 'com.google.crypto.tink:tink-android:1.4.0-rc1'
18+
// tag::actuator[]
19+
implementation 'org.springframework.boot:spring-boot-starter-actuator'
20+
// end::actuator[]
21+
implementation 'org.springframework.boot:spring-boot-starter-web'
22+
// tag::tests[]
23+
testImplementation('org.springframework.boot:spring-boot-starter-test') {
24+
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
25+
}
26+
// end::tests[]
27+
}
28+
29+
test {
30+
useJUnitPlatform()
31+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
version: '3'
2+
3+
services:
4+
ssv:
5+
image: ssv
6+
build:
7+
context: .
8+
dockerfile: Dockerfile
9+
container_name: ssv
10+
ports:
11+
- 8080:8080
12+
volumes:
13+
- .:/var/www
14+
command: ./gradlew bootRun
15+
networks: ['stack']
16+
networks:
17+
stack:
18+
driver: bridge
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#Thu May 14 12:27:25 IST 2020
2+
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.3-all.zip
3+
distributionBase=GRADLE_USER_HOME
4+
distributionPath=wrapper/dists
5+
zipStorePath=wrapper/dists
6+
zipStoreBase=GRADLE_USER_HOME
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>org.springframework.boot</groupId>
8+
<artifactId>spring-boot-starter-parent</artifactId>
9+
<version>2.2.2.RELEASE</version>
10+
<relativePath/> <!-- lookup parent from repository -->
11+
</parent>
12+
<groupId>com.example</groupId>
13+
<artifactId>rewardedssv</artifactId>
14+
<version>1.0.0</version>
15+
<name>spring-boot</name>
16+
<description>Demo project for Spring Boot</description>
17+
18+
<properties>
19+
<java.version>1.8</java.version>
20+
</properties>
21+
22+
<dependencies>
23+
<dependency>
24+
<groupId>org.springframework.boot</groupId>
25+
<artifactId>spring-boot-starter-web</artifactId>
26+
</dependency>
27+
28+
<!-- tag::actuator[] -->
29+
<dependency>
30+
<groupId>org.springframework.boot</groupId>
31+
<artifactId>spring-boot-starter-actuator</artifactId>
32+
</dependency>
33+
<!-- end::actuator[] -->
34+
35+
<!-- tag::tests[] -->
36+
<dependency>
37+
<groupId>org.springframework.boot</groupId>
38+
<artifactId>spring-boot-starter-test</artifactId>
39+
<scope>test</scope>
40+
<exclusions>
41+
<exclusion>
42+
<groupId>org.junit.vintage</groupId>
43+
<artifactId>junit-vintage-engine</artifactId>
44+
</exclusion>
45+
</exclusions>
46+
</dependency>
47+
<!-- end::tests[] -->
48+
</dependencies>
49+
50+
<build>
51+
<plugins>
52+
<plugin>
53+
<groupId>org.springframework.boot</groupId>
54+
<artifactId>spring-boot-maven-plugin</artifactId>
55+
</plugin>
56+
</plugins>
57+
</build>
58+
59+
</project>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rootProject.name = 'rewarded-ssv'
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.example.rewardedssv;
2+
3+
import org.springframework.boot.CommandLineRunner;
4+
import org.springframework.boot.SpringApplication;
5+
import org.springframework.boot.autoconfigure.SpringBootApplication;
6+
import org.springframework.context.ApplicationContext;
7+
import org.springframework.context.annotation.Bean;
8+
9+
/** Application entry point */
10+
@SpringBootApplication
11+
public class Application {
12+
13+
public static void main(String[] args) {
14+
SpringApplication.run(Application.class, args);
15+
}
16+
17+
@Bean
18+
public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
19+
return args -> {};
20+
}
21+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package com.example.rewardedssv;
2+
3+
import com.google.crypto.tink.subtle.Base64;
4+
import com.google.crypto.tink.subtle.EcdsaVerifyJce;
5+
import com.google.crypto.tink.subtle.EllipticCurves;
6+
import com.google.crypto.tink.subtle.EllipticCurves.EcdsaEncoding;
7+
import com.google.crypto.tink.subtle.Enums.HashType;
8+
import java.io.BufferedReader;
9+
import java.io.IOException;
10+
import java.io.InputStreamReader;
11+
import java.net.HttpURLConnection;
12+
import java.net.URL;
13+
import java.nio.charset.Charset;
14+
import java.security.GeneralSecurityException;
15+
import java.security.interfaces.ECPublicKey;
16+
import java.util.Enumeration;
17+
import java.util.HashMap;
18+
import java.util.Map;
19+
import javax.servlet.http.HttpServletRequest;
20+
import org.json.JSONArray;
21+
import org.json.JSONException;
22+
import org.json.JSONObject;
23+
import org.springframework.http.HttpStatus;
24+
import org.springframework.http.ResponseEntity;
25+
import org.springframework.web.bind.annotation.GetMapping;
26+
import org.springframework.web.bind.annotation.RestController;
27+
28+
/** SSV REST Controller */
29+
@RestController
30+
public class SSVController {
31+
private static final String SIGNATURE_PARAM_KEY = "signature";
32+
private static final String KEY_ID_PARAM_KEY = "key_id";
33+
private static final String REWARD_VERIFIER_KEYS_URL =
34+
"https://www.gstatic.com/admob/reward/verifier-keys.json";
35+
36+
private static Map<Long, ECPublicKey> parsePublicKeysJson()
37+
throws GeneralSecurityException, IOException, JSONException {
38+
URL url = new URL(REWARD_VERIFIER_KEYS_URL);
39+
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
40+
connection.setRequestMethod("GET");
41+
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
42+
String inputLine;
43+
StringBuffer content = new StringBuffer();
44+
while ((inputLine = reader.readLine()) != null) {
45+
content.append(inputLine);
46+
}
47+
reader.close();
48+
connection.disconnect();
49+
String publicKeysJson = content.toString();
50+
JSONArray keys = new JSONObject(publicKeysJson).getJSONArray("keys");
51+
Map<Long, ECPublicKey> publicKeys = new HashMap<>();
52+
for (int i = 0; i < keys.length(); i++) {
53+
JSONObject key = keys.getJSONObject(i);
54+
publicKeys.put(
55+
key.getLong("keyId"),
56+
EllipticCurves.getEcPublicKey(Base64.decode(key.getString("base64"))));
57+
}
58+
if (publicKeys.isEmpty()) {
59+
throw new GeneralSecurityException("No trusted keys are available for this protocol version");
60+
}
61+
return publicKeys;
62+
}
63+
64+
private void verify(final byte[] dataToVerify, Long keyId, final byte[] signature)
65+
throws GeneralSecurityException {
66+
try {
67+
Map<Long, ECPublicKey> publicKeys = parsePublicKeysJson();
68+
if (publicKeys.containsKey(keyId)) {
69+
ECPublicKey publicKey = publicKeys.get(keyId);
70+
EcdsaVerifyJce verifier = new EcdsaVerifyJce(publicKey, HashType.SHA256, EcdsaEncoding.DER);
71+
verifier.verify(signature, dataToVerify);
72+
} else {
73+
throw new GeneralSecurityException(
74+
String.format("Cannot find verifying key with key id: %s.", keyId));
75+
}
76+
} catch (JSONException exception) {
77+
throw new GeneralSecurityException(exception);
78+
} catch (IOException exception) {
79+
throw new GeneralSecurityException(exception);
80+
}
81+
}
82+
83+
@GetMapping(value = "/verify")
84+
public ResponseEntity<?> index(HttpServletRequest request) {
85+
86+
Enumeration enumeration = request.getParameterNames();
87+
Map<String, String[]> parameters = request.getParameterMap();
88+
89+
Map<String, String> response = new HashMap<>();
90+
if (!parameters.containsKey(KEY_ID_PARAM_KEY) || !parameters.containsKey(SIGNATURE_PARAM_KEY)) {
91+
response.put("verified", Boolean.FALSE.toString());
92+
response.put("error", "Missing key_id and/or signature parameters.");
93+
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
94+
}
95+
96+
Long keyId = Long.valueOf(parameters.get(KEY_ID_PARAM_KEY)[0]);
97+
String signature = parameters.get(SIGNATURE_PARAM_KEY)[0];
98+
String queryString = request.getQueryString();
99+
/* The last two query parameters of rewarded video
100+
SSV callbacks are always signature and key_id
101+
https://developers.google.com/admob/android/rewarded-video-ssv#get_content_to_be_verified
102+
*/
103+
byte[] payload =
104+
queryString
105+
.substring(0, queryString.indexOf(SIGNATURE_PARAM_KEY) - 1)
106+
.getBytes(Charset.forName("UTF-8"));
107+
108+
response.put("payload", new String(payload));
109+
response.put("key_id", keyId.toString());
110+
response.put("sig", signature);
111+
HttpStatus status = HttpStatus.OK;
112+
try {
113+
verify(payload, keyId, Base64.urlSafeDecode(signature));
114+
response.put("verified", Boolean.TRUE.toString());
115+
} catch (GeneralSecurityException exception) {
116+
status = HttpStatus.BAD_REQUEST;
117+
response.put("verified", Boolean.FALSE.toString());
118+
response.put("error", exception.getMessage());
119+
}
120+
return new ResponseEntity<>(response, status);
121+
}
122+
}

0 commit comments

Comments
 (0)