diff --git a/src/main/java/com/uid2/client/BidstreamClient.java b/src/main/java/com/uid2/client/BidstreamClient.java index 5aade73..6642368 100644 --- a/src/main/java/com/uid2/client/BidstreamClient.java +++ b/src/main/java/com/uid2/client/BidstreamClient.java @@ -9,12 +9,12 @@ public BidstreamClient(String baseUrl, String clientApiKey, String base64SecretK tokenHelper = new TokenHelper(baseUrl, clientApiKey, base64SecretKey); } - public DecryptionResponse decryptTokenIntoRawUid(String token, String domainNameFromBidRequest) { - return tokenHelper.decrypt(token, Instant.now(), domainNameFromBidRequest, ClientType.BIDSTREAM); + public DecryptionResponse decryptTokenIntoRawUid(String token, String domainOrAppNameFromBidRequest) { + return tokenHelper.decrypt(token, Instant.now(), domainOrAppNameFromBidRequest, ClientType.BIDSTREAM); } - DecryptionResponse decryptTokenIntoRawUid(String token, String domainNameFromBidRequest, Instant now) { - return tokenHelper.decrypt(token, now, domainNameFromBidRequest, ClientType.BIDSTREAM); + public DecryptionResponse decryptTokenIntoRawUid(String token, String domainOrAppNameFromBidRequest, Instant now) { + return tokenHelper.decrypt(token, now, domainOrAppNameFromBidRequest, ClientType.BIDSTREAM); } public RefreshResponse refresh() { diff --git a/src/main/java/com/uid2/client/DecryptionStatus.java b/src/main/java/com/uid2/client/DecryptionStatus.java index 385c9db..3e7747a 100644 --- a/src/main/java/com/uid2/client/DecryptionStatus.java +++ b/src/main/java/com/uid2/client/DecryptionStatus.java @@ -47,5 +47,9 @@ public enum DecryptionStatus { /** * INVALID_TOKEN_LIFETIME: The token has invalid timestamps. */ - INVALID_TOKEN_LIFETIME + INVALID_TOKEN_LIFETIME, + /** + * 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 + */ + DOMAIN_OR_APP_NAME_CHECK_FAILED } diff --git a/src/main/java/com/uid2/client/KeyContainer.java b/src/main/java/com/uid2/client/KeyContainer.java index 0d204fa..23c858f 100644 --- a/src/main/java/com/uid2/client/KeyContainer.java +++ b/src/main/java/com/uid2/client/KeyContainer.java @@ -8,6 +8,7 @@ class KeyContainer { private final HashMap keys = new HashMap<>(); private final HashMap> keysBySite = new HashMap<>(); //for legacy /key/latest private final HashMap> keysByKeyset = new HashMap<>(); + private final Map siteIdToSite = new HashMap<>(); private Instant latestKeyExpiry; private int callerSiteId; private int masterKeysetId; @@ -38,7 +39,7 @@ class KeyContainer { } } - KeyContainer(int callerSiteId, int masterKeysetId, int defaultKeysetId, long tokenExpirySeconds, List keyList, IdentityScope identityScope, long maxBidstreamLifetimeSeconds, long maxSharingLifetimeSeconds, long allowClockSkewSeconds) { + KeyContainer(int callerSiteId, int masterKeysetId, int defaultKeysetId, long tokenExpirySeconds, List keyList, List sites, IdentityScope identityScope, long maxBidstreamLifetimeSeconds, long maxSharingLifetimeSeconds, long allowClockSkewSeconds) { this.callerSiteId = callerSiteId; this.masterKeysetId = masterKeysetId; this.defaultKeysetId = defaultKeysetId; @@ -61,6 +62,10 @@ class KeyContainer { for(Map.Entry> entry : keysByKeyset.entrySet()) { entry.getValue().sort(Comparator.comparing(Key::getActivates)); } + + for (Site site : sites) { + this.siteIdToSite.put(site.getId(), site); + } } @@ -82,6 +87,16 @@ public Key getMasterKey(Instant now) return getKeysetActiveKey(masterKeysetId, now); } + public boolean isDomainOrAppNameAllowedForSite(int siteId, String domainOrAppName) { + if (domainOrAppName == null) { + return false; + } + if (siteIdToSite.containsKey(siteId)) { + return siteIdToSite.get(siteId).allowDomainOrAppName(domainOrAppName); + } + return false; + } + private Key getKeysetActiveKey(int keysetId, Instant now) { List keyset = keysByKeyset.get(keysetId); diff --git a/src/main/java/com/uid2/client/KeyParser.java b/src/main/java/com/uid2/client/KeyParser.java index 0eb4337..5c8b673 100644 --- a/src/main/java/com/uid2/client/KeyParser.java +++ b/src/main/java/com/uid2/client/KeyParser.java @@ -6,7 +6,9 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Base64; +import java.util.HashSet; import java.util.List; +import java.util.Set; class KeyParser { @@ -61,10 +63,35 @@ static KeyContainer parse(InputStream stream) { keys.add(key); } - return new KeyContainer(callerSiteId, masterKeysetId, defaultKeysetId, tokenExpirySeconds, keys, identityScope, maxBidstreamLifetimeSeconds, maxSharingLifetimeSeconds, allowClockSkewSeconds); + JsonArray sitesJson = body.getAsJsonArray("site_data"); + List sites = new ArrayList<>(); + if (!isNull(sitesJson)) { + for (JsonElement siteJson : sitesJson.asList()) { + Site site = getSiteFromJson(siteJson.getAsJsonObject()); + if (site != null) { + sites.add(site); + } + } + } + + return new KeyContainer(callerSiteId, masterKeysetId, defaultKeysetId, tokenExpirySeconds, keys, sites, identityScope, maxBidstreamLifetimeSeconds, maxSharingLifetimeSeconds, allowClockSkewSeconds); } } + private static Site getSiteFromJson(JsonObject siteJson) { + int siteId = getAsInt(siteJson, "id"); + if (siteId == 0) { + return null; + } + JsonArray domainOrAppNamesJArray = siteJson.getAsJsonArray("domain_names"); + Set domainOrAppNamesSet = new HashSet<>(); + for (int i = 0; i < domainOrAppNamesJArray.size(); ++i) { + domainOrAppNamesSet.add(domainOrAppNamesJArray.get(i).getAsString()); + } + + return new Site(siteId, domainOrAppNamesSet); + } + static private int getAsInt(JsonObject body, String memberName) { JsonElement element = body.get(memberName); return isNull(element) ? 0 : element.getAsInt(); diff --git a/src/main/java/com/uid2/client/Site.java b/src/main/java/com/uid2/client/Site.java new file mode 100644 index 0000000..dd0de28 --- /dev/null +++ b/src/main/java/com/uid2/client/Site.java @@ -0,0 +1,21 @@ +package com.uid2.client; + +import java.util.Set; + +public class Site { + private final int id; + + private final Set domainOrAppNames; + + public int getId() { return id;} + + public Site(int id, Set domainOrAppNames) { + this.id = id; + this.domainOrAppNames = domainOrAppNames; + } + + public boolean allowDomainOrAppName(String domainOrAppName) { + // Using streams because HashSet's contains() is case sensitive + return domainOrAppNames.stream().anyMatch(domainOrAppName::equalsIgnoreCase); + } +} diff --git a/src/main/java/com/uid2/client/TokenHelper.java b/src/main/java/com/uid2/client/TokenHelper.java index 423686e..1e55734 100644 --- a/src/main/java/com/uid2/client/TokenHelper.java +++ b/src/main/java/com/uid2/client/TokenHelper.java @@ -15,7 +15,7 @@ class TokenHelper { this.uid2Helper = new Uid2Helper(base64SecretKey); } - DecryptionResponse decrypt(String token, Instant now, String domainNameFromBidRequest, ClientType clientType) { + DecryptionResponse decrypt(String token, Instant now, String domainOrAppNameFromBidRequest, ClientType clientType) { KeyContainer keyContainer = this.container.get(); if (keyContainer == null) { return DecryptionResponse.makeError(DecryptionStatus.NOT_INITIALIZED); @@ -26,7 +26,7 @@ DecryptionResponse decrypt(String token, Instant now, String domainNameFromBidRe } try { - return Uid2Encryption.decrypt(token, keyContainer, now, keyContainer.getIdentityScope(), domainNameFromBidRequest, clientType); + return Uid2Encryption.decrypt(token, keyContainer, now, keyContainer.getIdentityScope(), domainOrAppNameFromBidRequest, clientType); } catch (Exception e) { return DecryptionResponse.makeError(DecryptionStatus.INVALID_PAYLOAD); } diff --git a/src/main/java/com/uid2/client/Uid2Encryption.java b/src/main/java/com/uid2/client/Uid2Encryption.java index 2fa16b1..d6a3c7f 100644 --- a/src/main/java/com/uid2/client/Uid2Encryption.java +++ b/src/main/java/com/uid2/client/Uid2Encryption.java @@ -20,7 +20,7 @@ class Uid2Encryption { public static final int GCM_AUTHTAG_LENGTH = 16; public static final int GCM_IV_LENGTH = 12; - static DecryptionResponse decrypt(String token, KeyContainer keys, Instant now, IdentityScope identityScope, String domainName, ClientType clientType) throws Exception { + static DecryptionResponse decrypt(String token, KeyContainer keys, Instant now, IdentityScope identityScope, String domainOrAppName, ClientType clientType) throws Exception { if (token.length() < 4) { @@ -33,18 +33,18 @@ static DecryptionResponse decrypt(String token, KeyContainer keys, Instant now, if (data[0] == 2) { - return decryptV2(Base64.getDecoder().decode(token), keys, now, domainName, clientType); + return decryptV2(Base64.getDecoder().decode(token), keys, now, domainOrAppName, clientType); } //java byte is signed so we wanna convert to unsigned before checking the enum int unsignedByte = ((int) data[1]) & 0xff; if (unsignedByte == AdvertisingTokenVersion.V3.value()) { - return decryptV3(Base64.getDecoder().decode(token), keys, now, identityScope, domainName, clientType, 3); + return decryptV3(Base64.getDecoder().decode(token), keys, now, identityScope, domainOrAppName, clientType, 3); } else if (unsignedByte == AdvertisingTokenVersion.V4.value()) { // Accept either base64 or base64url encoding. - return decryptV3(Base64.getDecoder().decode(base64UrlToBase64(token)), keys, now, identityScope, domainName, clientType, 4); + return decryptV3(Base64.getDecoder().decode(base64UrlToBase64(token)), keys, now, identityScope, domainOrAppName, clientType, 4); } return DecryptionResponse.makeError(DecryptionStatus.VERSION_NOT_SUPPORTED); @@ -56,7 +56,7 @@ static String base64UrlToBase64(String value) { .replace('_', '/'); } - static DecryptionResponse decryptV2(byte[] encryptedId, KeyContainer keys, Instant now, String domainName, ClientType clientType) throws Exception { + static DecryptionResponse decryptV2(byte[] encryptedId, KeyContainer keys, Instant now, String domainOrAppName, ClientType clientType) throws Exception { try { ByteBuffer rootReader = ByteBuffer.wrap(encryptedId); int version = (int) rootReader.get(); @@ -108,6 +108,9 @@ static DecryptionResponse decryptV2(byte[] encryptedId, KeyContainer keys, Insta if (now.isAfter(expiry)) { return DecryptionResponse.makeError(DecryptionStatus.EXPIRED_TOKEN, established, siteId, siteKey.getSiteId(), null, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry); } + if (!isDomainOrAppNameAllowedForSite(clientType, privacyBits.isClientSideGenerated(), siteId, domainOrAppName, keys)) { + return DecryptionResponse.makeError(DecryptionStatus.DOMAIN_OR_APP_NAME_CHECK_FAILED, established, siteId, siteKey.getSiteId(), null, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry); + } if (!doesTokenHaveValidLifetime(clientType, keys, now, expiry, now)) { 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 } } - static DecryptionResponse decryptV3(byte[] encryptedId, KeyContainer keys, Instant now, IdentityScope identityScope, String domainName, ClientType clientType, int advertisingTokenVersion) { + static DecryptionResponse decryptV3(byte[] encryptedId, KeyContainer keys, Instant now, IdentityScope identityScope, String domainOrAppName, ClientType clientType, int advertisingTokenVersion) { try { final IdentityType identityType = getIdentityType(encryptedId); final ByteBuffer rootReader = ByteBuffer.wrap(encryptedId); @@ -174,6 +177,9 @@ static DecryptionResponse decryptV3(byte[] encryptedId, KeyContainer keys, Insta if (now.isAfter(expiry)) { return DecryptionResponse.makeError(DecryptionStatus.EXPIRED_TOKEN, established, siteId, siteKey.getSiteId(), identityType, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry); } + if (!isDomainOrAppNameAllowedForSite(clientType, privacyBits.isClientSideGenerated(), siteId, domainOrAppName, keys)) { + return DecryptionResponse.makeError(DecryptionStatus.DOMAIN_OR_APP_NAME_CHECK_FAILED, established, siteId, siteKey.getSiteId(), identityType, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry); + } if (!doesTokenHaveValidLifetime(clientType, keys, generated, expiry, now)) { 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)) } - static EncryptionDataResponse encryptData(EncryptionDataRequest request, KeyContainer keys, IdentityScope identityScope, String domainName, ClientType clientType) { + static EncryptionDataResponse encryptData(EncryptionDataRequest request, KeyContainer keys, IdentityScope identityScope, String domainOrAppName, ClientType clientType) { if (request.getData() == null) { throw new IllegalArgumentException("data to encrypt must not be null"); } @@ -241,7 +247,7 @@ static EncryptionDataResponse encryptData(EncryptionDataRequest request, KeyCont siteKeySiteId = siteId; } else { try { - DecryptionResponse decryptedToken = decrypt(request.getAdvertisingToken(), keys, now, identityScope, domainName, clientType); + DecryptionResponse decryptedToken = decrypt(request.getAdvertisingToken(), keys, now, identityScope, domainOrAppName, clientType); if (!decryptedToken.isSuccess()) { return EncryptionDataResponse.makeError(EncryptionStatus.TOKEN_DECRYPT_FAILURE); } @@ -408,6 +414,16 @@ public CryptoException(Throwable inner) { } } + private static boolean isDomainOrAppNameAllowedForSite(ClientType clientType, boolean isClientSideGenerated, Integer siteId, String domainOrAppName, KeyContainer keys) { + if (!isClientSideGenerated) { + return true; + } else if (!clientType.equals(ClientType.BIDSTREAM) && !clientType.equals(ClientType.LEGACY)) { + return true; + } else { + return keys.isDomainOrAppNameAllowedForSite(siteId, domainOrAppName); + } + } + private static boolean doesTokenHaveValidLifetime(ClientType clientType, KeyContainer keys, Instant generatedOrNow, Instant expiry, Instant now) { long maxLifetimeSeconds; switch (clientType) { diff --git a/src/test/java/com/uid2/client/BidstreamClientTests.java b/src/test/java/com/uid2/client/BidstreamClientTests.java index c672567..a378b9a 100644 --- a/src/test/java/com/uid2/client/BidstreamClientTests.java +++ b/src/test/java/com/uid2/client/BidstreamClientTests.java @@ -206,16 +206,23 @@ private static Stream data_IdentityScopeAndType_TestCases() { ); } + // These are the domain or app names associated with site SITE_ID, as defined by keyBidstreamResponse() @ParameterizedTest @CsvSource({ "example.com, V2", "example.org, V2", + "com.123.Game.App.android, V2", + "123456789, V2", + "example.com, V3", + "example.org, V3", + "com.123.Game.App.android, V3", + "123456789, V3", "example.com, V4", "example.org, V4", - "example.com, V4", - "example.org, V4" + "com.123.Game.App.android, V4", + "123456789, V4" }) - public void TokenIsCstgDerivedTest(String domainName, TokenVersionForTesting tokenVersion) throws Exception { + public void tokenIsCstgDerivedTest(String domainName, TokenVersionForTesting tokenVersion) throws Exception { refresh(keyBidstreamResponse(IdentityScope.UID2, MASTER_KEY, SITE_KEY)); int privacyBits = PrivacyBitsBuilder.Builder().WithClientSideGenerated(true).Build(); @@ -229,6 +236,104 @@ public void TokenIsCstgDerivedTest(String domainName, TokenVersionForTesting tok assertEquals(EXAMPLE_UID, res.getUid()); } + // These are the domain or app names associated with site SITE_ID but vary in capitalization, as defined by keyBidstreamResponse() + @ParameterizedTest + @CsvSource({ + "Example.com, V2", + "Example.Org, V2", + "com.123.Game.App.android, V2", + "Example.com, V3", + "Example.Org, V3", + "com.123.Game.App.android, V3", + "Example.com, V4", + "Example.Org, V4", + "com.123.Game.App.android, V4", + }) + public void domainOrAppNameCaseInSensitiveTest(String domainName, TokenVersionForTesting tokenVersion) throws Exception { + refresh(keyBidstreamResponse(IdentityScope.UID2, MASTER_KEY, SITE_KEY)); + int privacyBits = PrivacyBitsBuilder.Builder().WithClientSideGenerated(true).Build(); + + String advertisingToken = AdvertisingTokenBuilder.builder().withVersion(tokenVersion).withPrivacyBits(privacyBits).build(); + + validateAdvertisingToken(advertisingToken, IdentityScope.UID2, IdentityType.Email, tokenVersion); + DecryptionResponse res = bidstreamClient.decryptTokenIntoRawUid(advertisingToken, domainName); + assertTrue(res.getIsClientSideGenerated()); + assertTrue(res.isSuccess()); + assertEquals(DecryptionStatus.SUCCESS, res.getStatus()); + assertEquals(EXAMPLE_UID, res.getUid()); + } + + @ParameterizedTest + @CsvSource({ + ", V2", + "example.net, V2", // Domain associated with site SITE_ID2, as defined by keyBidstreamResponse(). + "example.edu, V2", // Domain associated with site SITE_ID2, as defined by keyBidstreamResponse(). + "com.123.Game.App.ios, V2", // App associated with site SITE_ID2, as defined by keyBidstreamResponse(). + "123456780, V2", // App associated with site SITE_ID2, as defined by keyBidstreamResponse(). + "foo.com, V2", // Domain not associated with any site. + ", V3", + "example.net, V3", // Domain associated with site SITE_ID2, as defined by keyBidstreamResponse(). + "example.edu, V3", // Domain associated with site SITE_ID2, as defined by keyBidstreamResponse(). + "com.123.Game.App.ios, V3", // App associated with site SITE_ID2, as defined by keyBidstreamResponse(). + "123456780, V3", // App associated with site SITE_ID2, as defined by keyBidstreamResponse(). + "foo.com, V3", // Domain not associated with any site. + ", V4", + "example.net, V4", // Domain associated with site SITE_ID2, as defined by keyBidstreamResponse(). + "example.edu, V4", // Domain associated with site SITE_ID2, as defined by keyBidstreamResponse(). + "com.123.Game.App.ios, V4", // App associated with site SITE_ID2, as defined by keyBidstreamResponse(). + "123456780, V4", // App associated with site SITE_ID2, as defined by keyBidstreamResponse(). + "foo.com, V4", // Domain not associated with any site. + }) + public void tokenIsCstgDerivedDomainOrAppNameFailTest(String domainName, TokenVersionForTesting tokenVersion) throws Exception { + refresh(keyBidstreamResponse(IdentityScope.UID2, MASTER_KEY, SITE_KEY)); + int privacyBits = PrivacyBitsBuilder.Builder().WithClientSideGenerated(true).Build(); + + String advertisingToken = AdvertisingTokenBuilder.builder().withVersion(tokenVersion).withPrivacyBits(privacyBits).build(); + + validateAdvertisingToken(advertisingToken, IdentityScope.UID2, IdentityType.Email, tokenVersion); + DecryptionResponse res = bidstreamClient.decryptTokenIntoRawUid(advertisingToken, domainName); + assertTrue(res.getIsClientSideGenerated()); + assertFalse(res.isSuccess()); + assertEquals(DecryptionStatus.DOMAIN_OR_APP_NAME_CHECK_FAILED, res.getStatus()); + assertNull(res.getUid()); + } + + // Any domain or app name is OK, because the token is not client-side generated. + @ParameterizedTest + @CsvSource({ + ", V2", + "example.net, V2", // Domain associated with site SITE_ID2, as defined by keyBidstreamResponse(). + "example.edu, V2", // Domain associated with site SITE_ID2, as defined by keyBidstreamResponse(). + "com.123.Game.App.ios, V2", // App associated with site SITE_ID2, as defined by keyBidstreamResponse(). + "123456780, V2", // App associated with site SITE_ID2, as defined by keyBidstreamResponse(). + "foo.com, V2", // Domain not associated with any site. + ", V3", + "example.net, V3", // Domain associated with site SITE_ID2, as defined by keyBidstreamResponse(). + "example.edu, V3", // Domain associated with site SITE_ID2, as defined by keyBidstreamResponse(). + "com.123.Game.App.ios, V3", // App associated with site SITE_ID2, as defined by keyBidstreamResponse(). + "123456780, V3", // App associated with site SITE_ID2, as defined by keyBidstreamResponse(). + "foo.com, V3", // Domain not associated with any site. + ", V4", + "example.net, V4", // Domain associated with site SITE_ID2, as defined by keyBidstreamResponse(). + "example.edu, V4", // Domain associated with site SITE_ID2, as defined by keyBidstreamResponse(). + "com.123.Game.App.ios, V4", // App associated with site SITE_ID2, as defined by keyBidstreamResponse(). + "123456780, V4", // App associated with site SITE_ID2, as defined by keyBidstreamResponse(). + "foo.com, V4", // Domain not associated with any site. + }) + public void tokenIsNotCstgDerivedDomainNameSuccessTest(String domainName, TokenVersionForTesting tokenVersion) throws Exception { + refresh(keyBidstreamResponse(IdentityScope.UID2, MASTER_KEY, SITE_KEY)); + int privacyBits = PrivacyBitsBuilder.Builder().WithClientSideGenerated(false).Build(); + + String advertisingToken = AdvertisingTokenBuilder.builder().withVersion(tokenVersion).withPrivacyBits(privacyBits).build(); + + validateAdvertisingToken(advertisingToken, IdentityScope.UID2, IdentityType.Email, tokenVersion); + DecryptionResponse res = bidstreamClient.decryptTokenIntoRawUid(advertisingToken, domainName); + assertFalse(res.getIsClientSideGenerated()); + assertTrue(res.isSuccess()); + assertEquals(DecryptionStatus.SUCCESS, res.getStatus()); + assertEquals(EXAMPLE_UID, res.getUid()); + } + // tests below taken from EncryptionTestsV4.cs above "// Sharing tests" comment (but excluding deprecated EncryptData/DecryptData methods) and modified to use BidstreamClient and the new JSON /key/bidstream response @Test public void emptyKeyContainer() throws Exception { @@ -284,6 +389,13 @@ public void tokenExpiryAndCustomNow() throws Exception { res = bidstreamClient.decryptTokenIntoRawUid(advertisingToken, null, expiry.minus(1, ChronoUnit.SECONDS)); assertEquals(EXAMPLE_UID, res.getUid()); + + // case when domain / app name is present + int privacyBits = PrivacyBitsBuilder.Builder().WithClientSideGenerated(true).Build(); + String cstgAdvertisingToken = AdvertisingTokenBuilder.builder().withExpiry(expiry).withGenerated(generated) + .withPrivacyBits(privacyBits).build(); + res = bidstreamClient.decryptTokenIntoRawUid(cstgAdvertisingToken, "example.com", expiry.minus(1, ChronoUnit.SECONDS)); + assertTrue(res.isSuccess()); } @ParameterizedTest @@ -352,16 +464,19 @@ private static String keyBidstreamResponse(IdentityScope identityScope, Key... k JsonArray domainNames1 = new JsonArray(); domainNames1.add("example.com"); domainNames1.add("example.org"); + domainNames1.add("com.123.Game.App.android"); + domainNames1.add("123456789"); site1.add("domain_names", domainNames1); site1.addProperty("unexpected_domain_field", "123"); + JsonObject site2 = new JsonObject(); - site1.addProperty("id", SITE_ID2); + site2.addProperty("id", SITE_ID2); JsonArray domainNames2 = new JsonArray(); domainNames2.add("example.net"); domainNames2.add("example.edu"); - site1.add("domain_names", domainNames2); - site1.addProperty("unexpected_domain_field", "123"); + site2.add("domain_names", domainNames2); + site2.addProperty("unexpected_domain_field", "123"); JsonArray siteData = new JsonArray(); siteData.add(site1); diff --git a/src/test/java/com/uid2/client/KeyParserTests.java b/src/test/java/com/uid2/client/KeyParserTests.java index c98f0f1..e420f3d 100644 --- a/src/test/java/com/uid2/client/KeyParserTests.java +++ b/src/test/java/com/uid2/client/KeyParserTests.java @@ -96,6 +96,88 @@ public void parseErrorKeyList() { assertThrows(Exception.class, () -> parse("{\"body\": [{\"id\": 5}]}")); } + @Test + public void parseMissingSiteData() { + String json = "{ \"body\": {\n" + + " \"keys\": [\n" + + " {\n" + + " \"id\": 3,\n" + + " \"keyset_id\": 99999,\n" + + " \"created\": 1609459200,\n" + + " \"activates\": 1609459210,\n" + + " \"expires\": 1893456000,\n" + + " \"secret\": \"o8HsvkwJ5Ulnrd0uui3GpukpwDapj+JLqb7qfN/GJKo=\"\n" + + " }\n" + + " ]\n" + + " }\n" + + "}"; + KeyContainer keyContainer = parse(json); + boolean isAllowed = keyContainer.isDomainOrAppNameAllowedForSite(1, "example.com"); + assertFalse(isAllowed); + assertNotNull(keyContainer.getKey(3)); + } + + @Test + public void parseEmptySiteData() { + String json = "{ \"body\": {\n" + + " \"keys\": [\n" + + " {\n" + + " \"id\": 3,\n" + + " \"keyset_id\": 99999,\n" + + " \"created\": 1609459200,\n" + + " \"activates\": 1609459210,\n" + + " \"expires\": 1893456000,\n" + + " \"secret\": \"o8HsvkwJ5Ulnrd0uui3GpukpwDapj+JLqb7qfN/GJKo=\"\n" + + " }\n" + + " ],\n" + + " \"site_data\": []\n" + + " }\n" + + "}"; + KeyContainer keyContainer = parse(json); + boolean isAllowed = keyContainer.isDomainOrAppNameAllowedForSite(1, "example.com"); + assertFalse(isAllowed); + assertFalse(keyContainer.isDomainOrAppNameAllowedForSite(1, null)); + assertNotNull(keyContainer.getKey(3)); + } + + @Test + public void parseSiteDataSharingEndpoint() { + String json = "{\n" + + " \"body\": {\n" + + " \"keys\": [\n" + + " {\n" + + " \"id\": 3,\n" + + " \"keyset_id\": 99999,\n" + + " \"created\": 1609459200,\n" + + " \"activates\": 1609459210,\n" + + " \"expires\": 1893456000,\n" + + " \"secret\": \"o8HsvkwJ5Ulnrd0uui3GpukpwDapj+JLqb7qfN/GJKo=\"\n" + + " }\n" + + " ],\n" + + " \"site_data\": [\n" + + " {\n" + + " \"id\": 9,\n" + + " \"domain_names\": [\"example.com\"]\n" + + " },\n" + + " {\n" + + " \"id\": 100,\n" + + " \"domain_names\": [\"example.org\", \"example.net\"]\n" + + " }\n" + + " ]\n" + + " }\n" + + "}"; + KeyContainer keyContainer = parse(json); + assertTrue(keyContainer.isDomainOrAppNameAllowedForSite(9, "example.com")); + assertFalse(keyContainer.isDomainOrAppNameAllowedForSite(9, "example.org")); + assertFalse(keyContainer.isDomainOrAppNameAllowedForSite(9, "example.net")); + + assertFalse(keyContainer.isDomainOrAppNameAllowedForSite(100, "example.com")); + assertTrue(keyContainer.isDomainOrAppNameAllowedForSite(100, "example.org")); + assertTrue(keyContainer.isDomainOrAppNameAllowedForSite(100, "example.net")); + + assertNotNull(keyContainer.getKey(3)); + } + @Test public void parseWithNullTokenExpirySecondField() { String s = "{ \"body\": { " + diff --git a/src/test/java/com/uid2/client/SiteTests.java b/src/test/java/com/uid2/client/SiteTests.java new file mode 100644 index 0000000..6d9e753 --- /dev/null +++ b/src/test/java/com/uid2/client/SiteTests.java @@ -0,0 +1,50 @@ +package com.uid2.client; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Arrays; +import java.util.HashSet; + +public class SiteTests { + + private static Site site; + + @BeforeAll + public static void setup() { + site = new Site(101, new HashSet<>(Arrays.asList( + "example.com", + "example.org", + "com.123.Game.App.android", + "123456789" + ))); + } + + + @ParameterizedTest + @ValueSource(strings = { + "example.com", + "example.org", + "com.123.Game.App.android", + "123456789", + "EXAMPLE.COM", + "com.123.game.app.android", + }) + public void testAllowDomainOrAppNameSuccess(String domainOrAppName) { + Assertions.assertTrue(site.allowDomainOrAppName(domainOrAppName)); + } + + @ParameterizedTest + @CsvSource({ + "*", + "example", + "example*", + "example.net" + }) + public void testAllowDomainOrAppNameFailure(String domainOrAppName) { + Assertions.assertFalse(site.allowDomainOrAppName(domainOrAppName)); + } +}