Skip to content

Commit a94bb40

Browse files
authored
Merge pull request #76 from IABTechLab/aaq-UID2-3335-app-domain-name-check-bidstream
Support Domain or App name check in bidstream client
2 parents 33fab07 + 7e13212 commit a94bb40

File tree

10 files changed

+353
-23
lines changed

10 files changed

+353
-23
lines changed

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+
public 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 site/app where this token was generated
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: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
import java.time.Instant;
77
import java.util.ArrayList;
88
import java.util.Base64;
9+
import java.util.HashSet;
910
import java.util.List;
11+
import java.util.Set;
1012

1113

1214
class KeyParser {
@@ -61,10 +63,35 @@ static KeyContainer parse(InputStream stream) {
6163
keys.add(key);
6264
}
6365

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

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

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)