Skip to content

Commit 92278a7

Browse files
committed
Add check for domain and app names in BidStreamClient
1 parent 33fab07 commit 92278a7

File tree

8 files changed

+106
-17
lines changed

8 files changed

+106
-17
lines changed

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@
6666
<artifactId>junit-jupiter-params</artifactId>
6767
<scope>test</scope>
6868
</dependency>
69+
<dependency>
70+
<groupId>org.projectlombok</groupId>
71+
<artifactId>lombok</artifactId>
72+
<version>1.18.34</version>
73+
<scope>provided</scope>
74+
</dependency>
6975
</dependencies>
7076

7177
<dependencyManagement>

src/main/java/com/uid2/client/BidstreamClient.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ public BidstreamClient(String baseUrl, String clientApiKey, String base64SecretK
99
tokenHelper = new TokenHelper(baseUrl, clientApiKey, base64SecretKey);
1010
}
1111

12-
public DecryptionResponse decryptTokenIntoRawUid(String token, String domainNameFromBidRequest) {
13-
return tokenHelper.decrypt(token, Instant.now(), domainNameFromBidRequest, ClientType.BIDSTREAM);
12+
public DecryptionResponse decryptTokenIntoRawUid(String token, String domainOrAppNameFromBidRequest) {
13+
return tokenHelper.decrypt(token, Instant.now(), domainOrAppNameFromBidRequest, ClientType.BIDSTREAM);
1414
}
1515

16-
DecryptionResponse decryptTokenIntoRawUid(String token, String domainNameFromBidRequest, Instant now) {
17-
return tokenHelper.decrypt(token, now, domainNameFromBidRequest, ClientType.BIDSTREAM);
16+
DecryptionResponse decryptTokenIntoRawUid(String token, String domainOrAppNameFromBidRequest, Instant now) {
17+
return tokenHelper.decrypt(token, now, domainOrAppNameFromBidRequest, ClientType.BIDSTREAM);
1818
}
1919

2020
public RefreshResponse refresh() {

src/main/java/com/uid2/client/DecryptionStatus.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,9 @@ public enum DecryptionStatus {
4747
/**
4848
* INVALID_TOKEN_LIFETIME: The token has invalid timestamps.
4949
*/
50-
INVALID_TOKEN_LIFETIME
50+
INVALID_TOKEN_LIFETIME,
51+
/**
52+
* DOMAIN_OR_APP_NAME_CHECK_FAILED: The supplied domain name or app name doesn't match with the allowed names of the participant who generated this token
53+
*/
54+
DOMAIN_OR_APP_NAME_CHECK_FAILED
5155
}

src/main/java/com/uid2/client/KeyContainer.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class KeyContainer {
88
private final HashMap<Long, Key> keys = new HashMap<>();
99
private final HashMap<Integer, List<Key>> keysBySite = new HashMap<>(); //for legacy /key/latest
1010
private final HashMap<Integer, List<Key>> keysByKeyset = new HashMap<>();
11+
private final Map<Integer, Site> siteIdToSite = new HashMap<>();
1112
private Instant latestKeyExpiry;
1213
private int callerSiteId;
1314
private int masterKeysetId;
@@ -38,7 +39,7 @@ class KeyContainer {
3839
}
3940
}
4041

41-
KeyContainer(int callerSiteId, int masterKeysetId, int defaultKeysetId, long tokenExpirySeconds, List<Key> keyList, IdentityScope identityScope, long maxBidstreamLifetimeSeconds, long maxSharingLifetimeSeconds, long allowClockSkewSeconds) {
42+
KeyContainer(int callerSiteId, int masterKeysetId, int defaultKeysetId, long tokenExpirySeconds, List<Key> keyList, List<Site> sites, IdentityScope identityScope, long maxBidstreamLifetimeSeconds, long maxSharingLifetimeSeconds, long allowClockSkewSeconds) {
4243
this.callerSiteId = callerSiteId;
4344
this.masterKeysetId = masterKeysetId;
4445
this.defaultKeysetId = defaultKeysetId;
@@ -61,6 +62,10 @@ class KeyContainer {
6162
for(Map.Entry<Integer, List<Key>> entry : keysByKeyset.entrySet()) {
6263
entry.getValue().sort(Comparator.comparing(Key::getActivates));
6364
}
65+
66+
for (Site site : sites) {
67+
this.siteIdToSite.put(site.getId(), site);
68+
}
6469
}
6570

6671

@@ -82,6 +87,16 @@ public Key getMasterKey(Instant now)
8287
return getKeysetActiveKey(masterKeysetId, now);
8388
}
8489

90+
public boolean isDomainOrAppNameAllowedForSite(int siteId, String domainOrAppName) {
91+
if (domainOrAppName == null) {
92+
return false;
93+
}
94+
if (siteIdToSite.containsKey(siteId)) {
95+
return siteIdToSite.get(siteId).allowDomainOrAppName(domainOrAppName);
96+
}
97+
return false;
98+
}
99+
85100
private Key getKeysetActiveKey(int keysetId, Instant now)
86101
{
87102
List<Key> keyset = keysByKeyset.get(keysetId);

src/main/java/com/uid2/client/KeyParser.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,35 @@ static KeyContainer parse(InputStream stream) {
6161
keys.add(key);
6262
}
6363

64-
return new KeyContainer(callerSiteId, masterKeysetId, defaultKeysetId, tokenExpirySeconds, keys, identityScope, maxBidstreamLifetimeSeconds, maxSharingLifetimeSeconds, allowClockSkewSeconds);
64+
JsonArray sitesJson = body.getAsJsonArray("site_data");
65+
List<Site> sites = new ArrayList<>();
66+
if (!isNull(sitesJson)) {
67+
for (JsonElement siteJson : sitesJson.asList()) {
68+
Site site = getSiteFromJson(siteJson.getAsJsonObject());
69+
if (site != null) {
70+
sites.add(site);
71+
}
72+
}
73+
}
74+
75+
return new KeyContainer(callerSiteId, masterKeysetId, defaultKeysetId, tokenExpirySeconds, keys, sites, identityScope, maxBidstreamLifetimeSeconds, maxSharingLifetimeSeconds, allowClockSkewSeconds);
6576
}
6677
}
6778

79+
private static Site getSiteFromJson(JsonObject siteJson) {
80+
int siteId = getAsInt(siteJson, "id");
81+
if (siteId == 0) {
82+
return null;
83+
}
84+
JsonArray domainOrAppNamesJArray = siteJson.getAsJsonArray("domain_names");
85+
List<String> domainOrAppNames = new ArrayList<>();
86+
for (int i = 0; i < domainOrAppNamesJArray.size(); ++i) {
87+
domainOrAppNames.add(domainOrAppNamesJArray.get(i).getAsString());
88+
}
89+
90+
return new Site(siteId, domainOrAppNames);
91+
}
92+
6893
static private int getAsInt(JsonObject body, String memberName) {
6994
JsonElement element = body.get(memberName);
7095
return isNull(element) ? 0 : element.getAsInt();
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.uid2.client;
2+
3+
import java.util.HashSet;
4+
import java.util.List;
5+
import java.util.Set;
6+
import lombok.Getter;
7+
8+
@Getter
9+
public class Site {
10+
private final int id;
11+
12+
private final Set<String> domainOrAppNames;
13+
14+
public Site(int id, List<String> domainOrAppNames) {
15+
this.id = id;
16+
this.domainOrAppNames = new HashSet<>(domainOrAppNames);
17+
}
18+
19+
public boolean allowDomainOrAppName(String domainOrAppName) {
20+
// Using streams because HashSet's contains() is case sensitive
21+
return domainOrAppNames.stream().anyMatch(domainOrAppName::equalsIgnoreCase);
22+
}
23+
}

src/main/java/com/uid2/client/TokenHelper.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class TokenHelper {
1515
this.uid2Helper = new Uid2Helper(base64SecretKey);
1616
}
1717

18-
DecryptionResponse decrypt(String token, Instant now, String domainNameFromBidRequest, ClientType clientType) {
18+
DecryptionResponse decrypt(String token, Instant now, String domainOrAppNameFromBidRequest, ClientType clientType) {
1919
KeyContainer keyContainer = this.container.get();
2020
if (keyContainer == null) {
2121
return DecryptionResponse.makeError(DecryptionStatus.NOT_INITIALIZED);
@@ -26,7 +26,7 @@ DecryptionResponse decrypt(String token, Instant now, String domainNameFromBidRe
2626
}
2727

2828
try {
29-
return Uid2Encryption.decrypt(token, keyContainer, now, keyContainer.getIdentityScope(), domainNameFromBidRequest, clientType);
29+
return Uid2Encryption.decrypt(token, keyContainer, now, keyContainer.getIdentityScope(), domainOrAppNameFromBidRequest, clientType);
3030
} catch (Exception e) {
3131
return DecryptionResponse.makeError(DecryptionStatus.INVALID_PAYLOAD);
3232
}

src/main/java/com/uid2/client/Uid2Encryption.java

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class Uid2Encryption {
2020
public static final int GCM_AUTHTAG_LENGTH = 16;
2121
public static final int GCM_IV_LENGTH = 12;
2222

23-
static DecryptionResponse decrypt(String token, KeyContainer keys, Instant now, IdentityScope identityScope, String domainName, ClientType clientType) throws Exception {
23+
static DecryptionResponse decrypt(String token, KeyContainer keys, Instant now, IdentityScope identityScope, String domainOrAppName, ClientType clientType) throws Exception {
2424

2525
if (token.length() < 4)
2626
{
@@ -33,18 +33,18 @@ static DecryptionResponse decrypt(String token, KeyContainer keys, Instant now,
3333

3434
if (data[0] == 2)
3535
{
36-
return decryptV2(Base64.getDecoder().decode(token), keys, now, domainName, clientType);
36+
return decryptV2(Base64.getDecoder().decode(token), keys, now, domainOrAppName, clientType);
3737
}
3838
//java byte is signed so we wanna convert to unsigned before checking the enum
3939
int unsignedByte = ((int) data[1]) & 0xff;
4040
if (unsignedByte == AdvertisingTokenVersion.V3.value())
4141
{
42-
return decryptV3(Base64.getDecoder().decode(token), keys, now, identityScope, domainName, clientType, 3);
42+
return decryptV3(Base64.getDecoder().decode(token), keys, now, identityScope, domainOrAppName, clientType, 3);
4343
}
4444
else if (unsignedByte == AdvertisingTokenVersion.V4.value())
4545
{
4646
// Accept either base64 or base64url encoding.
47-
return decryptV3(Base64.getDecoder().decode(base64UrlToBase64(token)), keys, now, identityScope, domainName, clientType, 4);
47+
return decryptV3(Base64.getDecoder().decode(base64UrlToBase64(token)), keys, now, identityScope, domainOrAppName, clientType, 4);
4848
}
4949

5050
return DecryptionResponse.makeError(DecryptionStatus.VERSION_NOT_SUPPORTED);
@@ -56,7 +56,7 @@ static String base64UrlToBase64(String value) {
5656
.replace('_', '/');
5757
}
5858

59-
static DecryptionResponse decryptV2(byte[] encryptedId, KeyContainer keys, Instant now, String domainName, ClientType clientType) throws Exception {
59+
static DecryptionResponse decryptV2(byte[] encryptedId, KeyContainer keys, Instant now, String domainOrAppName, ClientType clientType) throws Exception {
6060
try {
6161
ByteBuffer rootReader = ByteBuffer.wrap(encryptedId);
6262
int version = (int) rootReader.get();
@@ -108,6 +108,9 @@ static DecryptionResponse decryptV2(byte[] encryptedId, KeyContainer keys, Insta
108108
if (now.isAfter(expiry)) {
109109
return DecryptionResponse.makeError(DecryptionStatus.EXPIRED_TOKEN, established, siteId, siteKey.getSiteId(), null, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry);
110110
}
111+
if (!isDomainOrAppNameAllowedForSite(clientType, privacyBits.isClientSideGenerated(), siteId, domainOrAppName, keys)) {
112+
return DecryptionResponse.makeError(DecryptionStatus.DOMAIN_OR_APP_NAME_CHECK_FAILED, established, siteId, siteKey.getSiteId(), null, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry);
113+
}
111114

112115
if (!doesTokenHaveValidLifetime(clientType, keys, now, expiry, now)) {
113116
return DecryptionResponse.makeError(DecryptionStatus.INVALID_TOKEN_LIFETIME, established, siteId, siteKey.getSiteId(), null, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry);
@@ -119,7 +122,7 @@ static DecryptionResponse decryptV2(byte[] encryptedId, KeyContainer keys, Insta
119122
}
120123
}
121124

122-
static DecryptionResponse decryptV3(byte[] encryptedId, KeyContainer keys, Instant now, IdentityScope identityScope, String domainName, ClientType clientType, int advertisingTokenVersion) {
125+
static DecryptionResponse decryptV3(byte[] encryptedId, KeyContainer keys, Instant now, IdentityScope identityScope, String domainOrAppName, ClientType clientType, int advertisingTokenVersion) {
123126
try {
124127
final IdentityType identityType = getIdentityType(encryptedId);
125128
final ByteBuffer rootReader = ByteBuffer.wrap(encryptedId);
@@ -174,6 +177,9 @@ static DecryptionResponse decryptV3(byte[] encryptedId, KeyContainer keys, Insta
174177
if (now.isAfter(expiry)) {
175178
return DecryptionResponse.makeError(DecryptionStatus.EXPIRED_TOKEN, established, siteId, siteKey.getSiteId(), identityType, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry);
176179
}
180+
if (!isDomainOrAppNameAllowedForSite(clientType, privacyBits.isClientSideGenerated(), siteId, domainOrAppName, keys)) {
181+
return DecryptionResponse.makeError(DecryptionStatus.DOMAIN_OR_APP_NAME_CHECK_FAILED, established, siteId, siteKey.getSiteId(), identityType, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry);
182+
}
177183

178184
if (!doesTokenHaveValidLifetime(clientType, keys, generated, expiry, now)) {
179185
return DecryptionResponse.makeError(DecryptionStatus.INVALID_TOKEN_LIFETIME, generated, siteId, siteKey.getSiteId(), identityType, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry);
@@ -220,7 +226,7 @@ else if (!keys.isValid(now))
220226
}
221227

222228

223-
static EncryptionDataResponse encryptData(EncryptionDataRequest request, KeyContainer keys, IdentityScope identityScope, String domainName, ClientType clientType) {
229+
static EncryptionDataResponse encryptData(EncryptionDataRequest request, KeyContainer keys, IdentityScope identityScope, String domainOrAppName, ClientType clientType) {
224230
if (request.getData() == null) {
225231
throw new IllegalArgumentException("data to encrypt must not be null");
226232
}
@@ -241,7 +247,7 @@ static EncryptionDataResponse encryptData(EncryptionDataRequest request, KeyCont
241247
siteKeySiteId = siteId;
242248
} else {
243249
try {
244-
DecryptionResponse decryptedToken = decrypt(request.getAdvertisingToken(), keys, now, identityScope, domainName, clientType);
250+
DecryptionResponse decryptedToken = decrypt(request.getAdvertisingToken(), keys, now, identityScope, domainOrAppName, clientType);
245251
if (!decryptedToken.isSuccess()) {
246252
return EncryptionDataResponse.makeError(EncryptionStatus.TOKEN_DECRYPT_FAILURE);
247253
}
@@ -408,6 +414,16 @@ public CryptoException(Throwable inner) {
408414
}
409415
}
410416

417+
private static boolean isDomainOrAppNameAllowedForSite(ClientType clientType, boolean isClientSideGenerated, Integer siteId, String domainOrAppName, KeyContainer keys) {
418+
if (!isClientSideGenerated) {
419+
return true;
420+
} else if (!clientType.equals(ClientType.BIDSTREAM) && !clientType.equals(ClientType.LEGACY)) {
421+
return true;
422+
} else {
423+
return keys.isDomainOrAppNameAllowedForSite(siteId, domainOrAppName);
424+
}
425+
}
426+
411427
private static boolean doesTokenHaveValidLifetime(ClientType clientType, KeyContainer keys, Instant generatedOrNow, Instant expiry, Instant now) {
412428
long maxLifetimeSeconds;
413429
switch (clientType) {

0 commit comments

Comments
 (0)