diff --git a/src/main/java/com/uid2/client/Uid2Encryption.java b/src/main/java/com/uid2/client/Uid2Encryption.java index d1282c3..1d5b626 100644 --- a/src/main/java/com/uid2/client/Uid2Encryption.java +++ b/src/main/java/com/uid2/client/Uid2Encryption.java @@ -103,7 +103,7 @@ static DecryptionResponse decryptV2(byte[] encryptedId, KeyContainer keys, Insta return DecryptionResponse.makeError(DecryptionStatus.EXPIRED_TOKEN, established, siteId, siteKey.getSiteId(), null, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry); } - if (!doesTokenHaveValidLifetime(clientType, keys, established, expiry, now)) { + if (!doesTokenHaveValidLifetime(clientType, keys, now, expiry, now)) { return DecryptionResponse.makeError(DecryptionStatus.INVALID_TOKEN_LIFETIME, established, siteId, siteKey.getSiteId(), null, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry); } @@ -136,7 +136,8 @@ static DecryptionResponse decryptV3(byte[] encryptedId, KeyContainer keys, Insta final ByteBuffer masterReader = ByteBuffer.wrap(masterPayload); final long expiresMilliseconds = masterReader.getLong(); - final long createdMilliseconds = masterReader.getLong(); + final long generatedMilliseconds = masterReader.getLong(); + Instant generated = Instant.ofEpochMilli(generatedMilliseconds); final int operatorSideId = masterReader.getInt(); final byte operatorType = masterReader.get(); @@ -168,8 +169,8 @@ static DecryptionResponse decryptV3(byte[] encryptedId, KeyContainer keys, Insta return DecryptionResponse.makeError(DecryptionStatus.EXPIRED_TOKEN, established, siteId, siteKey.getSiteId(), identityType, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry); } - if (!doesTokenHaveValidLifetime(clientType, keys, established, expiry, now)) { - return DecryptionResponse.makeError(DecryptionStatus.INVALID_TOKEN_LIFETIME, 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); } return new DecryptionResponse(DecryptionStatus.SUCCESS, idString, established, siteId, siteKey.getSiteId(), identityType, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry); @@ -401,7 +402,7 @@ public CryptoException(Throwable inner) { } } - private static boolean doesTokenHaveValidLifetime(ClientType clientType, KeyContainer keys, Instant established, Instant expiry, Instant now) { + private static boolean doesTokenHaveValidLifetime(ClientType clientType, KeyContainer keys, Instant generatedOrNow, Instant expiry, Instant now) { long maxLifetimeSeconds; switch (clientType) { case BIDSTREAM: @@ -413,17 +414,18 @@ private static boolean doesTokenHaveValidLifetime(ClientType clientType, KeyCont default: //Legacy return true; } - return doesTokenHaveValidLifetimeImpl(established, expiry, now, maxLifetimeSeconds, keys.getAllowClockSkewSeconds()); + //generatedOrNow allows "now" for token v2, since v2 does not contain a "token generated" field. v2 therefore checks against remaining lifetime rather than total lifetime. + return doesTokenHaveValidLifetimeImpl(generatedOrNow, expiry, now, maxLifetimeSeconds, keys.getAllowClockSkewSeconds()); } - private static boolean doesTokenHaveValidLifetimeImpl(Instant established, Instant expiry, Instant now, long maxLifetimeSeconds, long allowClockSkewSeconds) + private static boolean doesTokenHaveValidLifetimeImpl(Instant generatedOrNow, Instant expiry, Instant now, long maxLifetimeSeconds, long allowClockSkewSeconds) { - Duration lifetime = Duration.between(established, expiry); + Duration lifetime = Duration.between(generatedOrNow, expiry); if (lifetime.getSeconds() > maxLifetimeSeconds) { return false; } - Duration skewDuration = Duration.between(now, established); + Duration skewDuration = Duration.between(now, generatedOrNow); return skewDuration.getSeconds() <= allowClockSkewSeconds; } diff --git a/src/main/java/com/uid2/client/Uid2TokenGenerator.java b/src/main/java/com/uid2/client/Uid2TokenGenerator.java index 7dda6bc..d35e5b0 100644 --- a/src/main/java/com/uid2/client/Uid2TokenGenerator.java +++ b/src/main/java/com/uid2/client/Uid2TokenGenerator.java @@ -20,11 +20,13 @@ public static class Params Instant tokenExpiry = Instant.now().plus(1, ChronoUnit.HOURS); public int identityScope = IdentityScope.UID2.value; public Instant tokenGenerated = Instant.now(); + public Instant identityEstablished = Instant.now(); public int tokenPrivacyBits = 0; public Params() {} public Params withTokenExpiry(Instant expiry) { tokenExpiry = expiry; return this; } - public Params WithTokenGenerated(Instant generated) { tokenGenerated = generated; return this; } + public Params WithTokenGenerated(Instant generated) { tokenGenerated = generated; return this; } //when was the most recent refresh done (or if not refreshed, when was the /token/generate or CSTG call) + public Params WithIdentityEstablished(Instant established) { identityEstablished = established; return this; } //when was the first call to /token/generate or CSTG public Params WithPrivacyBits(int privacyBits) { tokenPrivacyBits = privacyBits; return this; } } @@ -43,7 +45,7 @@ public static byte[] generateUid2TokenV2(String uid, Key masterKey, long siteId, identityWriter.putInt(uidBytes.length); identityWriter.put(uidBytes); identityWriter.putInt(params.tokenPrivacyBits); - identityWriter.putLong(params.tokenGenerated.toEpochMilli()); + identityWriter.putLong(params.identityEstablished.toEpochMilli()); byte[] identityIv = new byte[16]; rd.nextBytes(identityIv); byte[] encryptedIdentity = encrypt(identityWriter.array(), identityIv, siteKey.getSecret()); @@ -90,13 +92,13 @@ private static String generateUID2TokenV3OrV4(String uid, Key masterKey, long si // user identity data sitePayloadWriter.putInt(params.tokenPrivacyBits); // privacy bits - sitePayloadWriter.putLong(params.tokenGenerated.toEpochMilli()); // established + sitePayloadWriter.putLong(params.identityEstablished.toEpochMilli()); // established sitePayloadWriter.putLong(params.tokenGenerated.toEpochMilli()); // last refreshed sitePayloadWriter.put(Base64.getDecoder().decode(uid)); final ByteBuffer masterPayloadWriter = ByteBuffer.allocate(256); masterPayloadWriter.putLong(params.tokenExpiry.toEpochMilli()); - masterPayloadWriter.putLong(params.tokenGenerated.toEpochMilli()); // token created + masterPayloadWriter.putLong(params.tokenGenerated.toEpochMilli()); //identity refreshed, seems to be identical to TokenGenerated in Operator // operator identity data masterPayloadWriter.putInt(0); // site id diff --git a/src/test/java/com/uid2/client/AdvertisingTokenBuilder.java b/src/test/java/com/uid2/client/AdvertisingTokenBuilder.java index 6261f0a..61189d6 100644 --- a/src/test/java/com/uid2/client/AdvertisingTokenBuilder.java +++ b/src/test/java/com/uid2/client/AdvertisingTokenBuilder.java @@ -16,6 +16,7 @@ class AdvertisingTokenBuilder { Instant expiry = Instant.now().plus(1, ChronoUnit.HOURS); IdentityScope identityScope = IdentityScope.UID2; Instant generated = Instant.now(); + Instant established = Instant.now(); static AdvertisingTokenBuilder builder() { return new AdvertisingTokenBuilder(); @@ -69,9 +70,14 @@ AdvertisingTokenBuilder withGenerated(Instant generated) return this; } + AdvertisingTokenBuilder withEstablished(Instant established) + { + this.established = established; + return this; + } String build() throws Exception { - Uid2TokenGenerator.Params params = Uid2TokenGenerator.defaultParams().WithPrivacyBits(privacyBits).withTokenExpiry(expiry).WithTokenGenerated(generated); + Uid2TokenGenerator.Params params = Uid2TokenGenerator.defaultParams().WithPrivacyBits(privacyBits).withTokenExpiry(expiry).WithTokenGenerated(generated).WithIdentityEstablished(established); params.identityScope = identityScope.value; String token; diff --git a/src/test/java/com/uid2/client/BidstreamClientTests.java b/src/test/java/com/uid2/client/BidstreamClientTests.java index 7e329e2..8ce062f 100644 --- a/src/test/java/com/uid2/client/BidstreamClientTests.java +++ b/src/test/java/com/uid2/client/BidstreamClientTests.java @@ -33,9 +33,10 @@ public class BidstreamClientTests { "UID2, V4", "EUID, V4" }) - public void smokeTest(IdentityScope identityScope, TokenVersionForTesting tokenVersion) throws Exception { - String advertisingToken = AdvertisingTokenBuilder.builder().withScope(identityScope).withVersion(tokenVersion).build(); - callAndVerifyRefreshJson(identityScope); + public void smokeTestForBidstream(IdentityScope identityScope, TokenVersionForTesting tokenVersion) throws Exception { + Instant now = Instant.now(); + String advertisingToken = AdvertisingTokenBuilder.builder().withScope(identityScope).withVersion(tokenVersion).withEstablished(now.minus(120, ChronoUnit.DAYS)).withGenerated(now.plus(-1, ChronoUnit.DAYS)).withExpiry(now.plus(2, ChronoUnit.DAYS)).build(); + refresh(keyBidstreamResponse(identityScope, MASTER_KEY, SITE_KEY)); decryptAndAssertSuccess(advertisingToken, tokenVersion); } @@ -50,7 +51,7 @@ public void smokeTest(IdentityScope identityScope, TokenVersionForTesting tokenV public void phoneTest(IdentityScope identityScope, TokenVersionForTesting tokenVersion) throws Exception { String rawUidPhone = "BEOGxroPLdcY7LrSiwjY52+X05V0ryELpJmoWAyXiwbZ"; String advertisingToken = AdvertisingTokenBuilder.builder().withRawUid(rawUidPhone).withScope(identityScope).withVersion(tokenVersion).build(); - callAndVerifyRefreshJson(identityScope); + refresh(keyBidstreamResponse(identityScope, MASTER_KEY, SITE_KEY)); DecryptionResponse decryptionResponse = bidstreamClient.decryptTokenIntoRawUid(advertisingToken, null); assertTrue(decryptionResponse.isSuccess()); @@ -68,13 +69,19 @@ public void phoneTest(IdentityScope identityScope, TokenVersionForTesting tokenV "UID2, V4", "EUID, V4" }) - public void tokenLifetimeTooLongForBidstream(IdentityScope identityScope, TokenVersionForTesting tokenVersion) throws Exception { - Instant tokenExpiry = Instant.now().plus(3, ChronoUnit.DAYS).plus(1, ChronoUnit.MINUTES); - String advertisingToken = AdvertisingTokenBuilder.builder().withExpiry(tokenExpiry).withScope(identityScope).withVersion(tokenVersion).build(); - callAndVerifyRefreshJson(identityScope); + public void tokenLifetimeTooLongForBidstreamButRemainingLifetimeAllowed(IdentityScope identityScope, TokenVersionForTesting tokenVersion) throws Exception { + Instant generated = Instant.now().minus(1, ChronoUnit.DAYS); + Instant tokenExpiry = generated.plus(3, ChronoUnit.DAYS).plus(1, ChronoUnit.MINUTES); + String advertisingToken = AdvertisingTokenBuilder.builder().withExpiry(tokenExpiry).withScope(identityScope).withVersion(tokenVersion).withGenerated(generated).build(); + refresh(keyBidstreamResponse(identityScope, MASTER_KEY, SITE_KEY)); DecryptionResponse decryptionResponse = bidstreamClient.decryptTokenIntoRawUid(advertisingToken, null); - assertFails(decryptionResponse, tokenVersion); + + if (tokenVersion == TokenVersionForTesting.V2) { + assertSuccess(decryptionResponse, tokenVersion); + } else { + assertFails(decryptionResponse, tokenVersion); + } } @ParameterizedTest @@ -86,10 +93,27 @@ public void tokenLifetimeTooLongForBidstream(IdentityScope identityScope, TokenV "UID2, V4", "EUID, V4" }) + public void tokenRemainingLifetimeTooLongForBidstream(IdentityScope identityScope, TokenVersionForTesting tokenVersion) throws Exception { + Instant tokenExpiry = Instant.now().plus(3, ChronoUnit.DAYS).plus(1, ChronoUnit.MINUTES); + Instant generated = Instant.now(); + String advertisingToken = AdvertisingTokenBuilder.builder().withExpiry(tokenExpiry).withScope(identityScope).withVersion(tokenVersion).withGenerated(generated).build(); + refresh(keyBidstreamResponse(identityScope, MASTER_KEY, SITE_KEY)); + + DecryptionResponse decryptionResponse = bidstreamClient.decryptTokenIntoRawUid(advertisingToken, null); + assertFails(decryptionResponse, tokenVersion); + } + + @ParameterizedTest + @CsvSource({ + "UID2, V3", + "EUID, V3", + "UID2, V4", + "EUID, V4" + }) public void tokenGeneratedInTheFutureToSimulateClockSkew(IdentityScope identityScope, TokenVersionForTesting tokenVersion) throws Exception { Instant tokenGenerated = Instant.now().plus(31, ChronoUnit.MINUTES); String advertisingToken = AdvertisingTokenBuilder.builder().withGenerated(tokenGenerated).withScope(identityScope).withVersion(tokenVersion).build(); - callAndVerifyRefreshJson(identityScope); + refresh(keyBidstreamResponse(identityScope, MASTER_KEY, SITE_KEY)); DecryptionResponse decryptionResponse = bidstreamClient.decryptTokenIntoRawUid(advertisingToken, null); assertFails(decryptionResponse, tokenVersion); @@ -107,7 +131,7 @@ public void tokenGeneratedInTheFutureToSimulateClockSkew(IdentityScope identityS public void tokenGeneratedInTheFutureWithinAllowedClockSkew(IdentityScope identityScope, TokenVersionForTesting tokenVersion) throws Exception { Instant tokenGenerated = Instant.now().plus(30, ChronoUnit.MINUTES); String advertisingToken = AdvertisingTokenBuilder.builder().withGenerated(tokenGenerated).withScope(identityScope).withVersion(tokenVersion).build(); - callAndVerifyRefreshJson(identityScope); + refresh(keyBidstreamResponse(identityScope, MASTER_KEY, SITE_KEY)); decryptAndAssertSuccess(advertisingToken, tokenVersion); } @@ -115,8 +139,7 @@ public void tokenGeneratedInTheFutureWithinAllowedClockSkew(IdentityScope identi @ParameterizedTest @ValueSource(strings = {"V2", "V3", "V4"}) public void legacyResponseFromOldOperator(TokenVersionForTesting tokenVersion) throws Exception { - RefreshResponse refreshResponse = bidstreamClient.refreshJson(keySetToJsonForSharing(MASTER_KEY, SITE_KEY)); - assertTrue(refreshResponse.isSuccess()); + refresh(keySetToJsonForSharing(MASTER_KEY, SITE_KEY)); String advertisingToken = AdvertisingTokenBuilder.builder().withVersion(tokenVersion).build(); decryptAndAssertSuccess(advertisingToken, tokenVersion); @@ -165,7 +188,7 @@ public void tokenLifetimeTooLongLegacyClient(IdentityScope identityScope, TokenV @ParameterizedTest @MethodSource("data_IdentityScopeAndType_TestCases") public void identityScopeAndType_TestCases(String uid, IdentityScope identityScope, IdentityType identityType) throws Exception { - callAndVerifyRefreshJson(identityScope); + refresh(keyBidstreamResponse(identityScope, MASTER_KEY, SITE_KEY)); String advertisingToken = AdvertisingTokenBuilder.builder().withRawUid(uid).withScope(identityScope).build(); DecryptionResponse res = bidstreamClient.decryptTokenIntoRawUid(advertisingToken, null); @@ -194,7 +217,7 @@ private static Stream data_IdentityScopeAndType_TestCases() { "example.org, V4" }) public void TokenIsCstgDerivedTest(String domainName, TokenVersionForTesting tokenVersion) throws Exception { - callAndVerifyRefreshJson(IdentityScope.UID2); + refresh(keyBidstreamResponse(IdentityScope.UID2, MASTER_KEY, SITE_KEY)); int privacyBits = PrivacyBitsBuilder.Builder().WithClientSideGenerated(true).Build(); String advertisingToken = AdvertisingTokenBuilder.builder().withVersion(tokenVersion).withPrivacyBits(privacyBits).build(); @@ -221,8 +244,7 @@ public void expiredKeyContainer() throws Exception { Key masterKeyExpired = new Key(MASTER_KEY_ID, -1, NOW, NOW.minus(2, ChronoUnit.HOURS), NOW.minus(1, ChronoUnit.HOURS), getMasterSecret()); Key siteKeyExpired = new Key(SITE_KEY_ID, SITE_ID, NOW, NOW.minus(2, ChronoUnit.HOURS), NOW.minus(1, ChronoUnit.HOURS), getSiteSecret()); - RefreshResponse refreshResponse = bidstreamClient.refreshJson(keyBidstreamResponse(IdentityScope.UID2, masterKeyExpired, siteKeyExpired)); - assertTrue(refreshResponse.isSuccess()); + refresh(keyBidstreamResponse(IdentityScope.UID2, masterKeyExpired, siteKeyExpired)); DecryptionResponse res = bidstreamClient.decryptTokenIntoRawUid(advertisingToken, null); assertEquals(DecryptionStatus.KEYS_NOT_SYNCED, res.getStatus()); @@ -234,8 +256,7 @@ public void notAuthorizedForMasterKey() throws Exception { Key anotherMasterKey = new Key(MASTER_KEY_ID + SITE_KEY_ID + 1, -1, NOW, NOW, NOW.plus(1, ChronoUnit.HOURS), getMasterSecret()); Key anotherSiteKey = new Key(MASTER_KEY_ID + SITE_KEY_ID + 2, SITE_ID, NOW, NOW, NOW.plus(1, ChronoUnit.HOURS), getSiteSecret()); - RefreshResponse refreshResponse = bidstreamClient.refreshJson(keyBidstreamResponse(IdentityScope.UID2, anotherMasterKey, anotherSiteKey)); - assertTrue(refreshResponse.isSuccess()); + refresh(keyBidstreamResponse(IdentityScope.UID2, anotherMasterKey, anotherSiteKey)); DecryptionResponse res = bidstreamClient.decryptTokenIntoRawUid(advertisingToken, null); assertEquals(DecryptionStatus.NOT_AUTHORIZED_FOR_MASTER_KEY, res.getStatus()); @@ -246,7 +267,7 @@ public void invalidPayload() throws Exception { String payload = AdvertisingTokenBuilder.builder().build(); byte[] payloadInBytes = Uid2Base64UrlCoder.decode(payload); String advertisingToken = Uid2Base64UrlCoder.encode(Arrays.copyOfRange(payloadInBytes, 0, payloadInBytes.length - 1)); - bidstreamClient.refreshJson(keyBidstreamResponse(IdentityScope.UID2, MASTER_KEY, SITE_KEY)); + refresh(keyBidstreamResponse(IdentityScope.UID2, MASTER_KEY, SITE_KEY)); DecryptionResponse res = bidstreamClient.decryptTokenIntoRawUid(advertisingToken, null); assertEquals(DecryptionStatus.INVALID_PAYLOAD, res.getStatus()); } @@ -256,7 +277,7 @@ public void tokenExpiryAndCustomNow() throws Exception { final Instant expiry = Instant.parse("2021-03-22T09:01:02Z"); final Instant generated = expiry.minus(60, ChronoUnit.SECONDS); - bidstreamClient.refreshJson(keyBidstreamResponse(IdentityScope.UID2, MASTER_KEY, SITE_KEY)); + refresh(keyBidstreamResponse(IdentityScope.UID2, MASTER_KEY, SITE_KEY)); String advertisingToken = AdvertisingTokenBuilder.builder().withExpiry(expiry).withGenerated(generated).build(); DecryptionResponse res = bidstreamClient.decryptTokenIntoRawUid(advertisingToken, null, expiry.plus(1, ChronoUnit.SECONDS)); @@ -266,8 +287,8 @@ public void tokenExpiryAndCustomNow() throws Exception { assertEquals(EXAMPLE_UID, res.getUid()); } - private void callAndVerifyRefreshJson(IdentityScope identityScope) { - RefreshResponse refreshResponse = bidstreamClient.refreshJson(keyBidstreamResponse(identityScope, MASTER_KEY, SITE_KEY)); + private void refresh(String json) { + RefreshResponse refreshResponse = bidstreamClient.refreshJson(json); assertTrue(refreshResponse.isSuccess()); } diff --git a/src/test/java/com/uid2/client/SharingClientTests.java b/src/test/java/com/uid2/client/SharingClientTests.java index 2870e2f..f2ce2aa 100644 --- a/src/test/java/com/uid2/client/SharingClientTests.java +++ b/src/test/java/com/uid2/client/SharingClientTests.java @@ -8,6 +8,7 @@ import java.io.IOException; import java.io.StringWriter; +import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Base64; @@ -40,7 +41,7 @@ private static String keySharingResponse(IdentityScope identityScope, Integer ca jsonWriter.name("token_expiry_seconds").value(tokenExpirySeconds); jsonWriter.name("identity_scope").value(identityScope.toString()); jsonWriter.name("allow_clock_skew_seconds").value(1800); // 30 min - jsonWriter.name("max_sharing_lifetime_seconds").value(2592000); // 30 days + jsonWriter.name("max_sharing_lifetime_seconds").value(Duration.ofDays(30).getSeconds()); // 30 days jsonWriter.name("unexpected_header_field").value("123"); //ensure new fields can be handled by old SDK versions if (defaultKeysetId != null) @@ -96,6 +97,11 @@ private void decryptAndAssertSuccess(String advertisingToken, TokenVersionForTes assertSuccess(decryptionResponse, tokenVersion); } + private void refresh(String json) throws IOException { + RefreshResponse refreshResponse = sharingClient.refreshJson(json); + assertTrue(refreshResponse.isSuccess()); + } + @ParameterizedTest @CsvSource({ "UID2, V2", @@ -105,11 +111,11 @@ private void decryptAndAssertSuccess(String advertisingToken, TokenVersionForTes "UID2, V4", "EUID, V4" }) - public void smokeTest(IdentityScope identityScope, TokenVersionForTesting tokenVersion) throws Exception { - String advertisingToken = AdvertisingTokenBuilder.builder().withScope(identityScope).withVersion(tokenVersion).build(); + public void smokeTestForSharing(IdentityScope identityScope, TokenVersionForTesting tokenVersion) throws Exception { + Instant now = Instant.now(); + String advertisingToken = AdvertisingTokenBuilder.builder().withScope(identityScope).withEstablished(now.minus(120, ChronoUnit.DAYS)).withExpiry(now.plus(29, ChronoUnit.DAYS)).withGenerated(now.minus(1, ChronoUnit.DAYS)).withVersion(tokenVersion).build(); - RefreshResponse refreshResponse = sharingClient.refreshJson(keySharingResponse(identityScope, MASTER_KEY, SITE_KEY)); - assertTrue(refreshResponse.isSuccess()); + refresh(keySharingResponse(identityScope, MASTER_KEY, SITE_KEY)); decryptAndAssertSuccess(advertisingToken, tokenVersion); } @@ -123,14 +129,20 @@ public void smokeTest(IdentityScope identityScope, TokenVersionForTesting tokenV "UID2, V4", "EUID, V4" }) - public void tokenLifetimeTooLongForSharing(IdentityScope identityScope, TokenVersionForTesting tokenVersion) throws Exception { - Instant tokenExpiry = Instant.now().plus(30, ChronoUnit.DAYS).plus(1, ChronoUnit.MINUTES); - String advertisingToken = AdvertisingTokenBuilder.builder().withExpiry(tokenExpiry).withScope(identityScope).withVersion(tokenVersion).build(); - RefreshResponse refreshResponse = sharingClient.refreshJson(keySharingResponse(identityScope, MASTER_KEY, SITE_KEY)); - assertTrue(refreshResponse.isSuccess()); + public void tokenLifetimeTooLongForSharingButRemainingLifetimeAllowed(IdentityScope identityScope, TokenVersionForTesting tokenVersion) throws Exception { + Instant now = Instant.now(); + Instant generated = now.minus(1, ChronoUnit.DAYS); + Instant tokenExpiry = generated.plus(30, ChronoUnit.DAYS).plus(1, ChronoUnit.MINUTES); + String advertisingToken = AdvertisingTokenBuilder.builder().withExpiry(tokenExpiry).withScope(identityScope).withVersion(tokenVersion).withGenerated(generated).build(); + refresh(keySharingResponse(identityScope, MASTER_KEY, SITE_KEY)); DecryptionResponse decryptionResponse = sharingClient.decryptTokenIntoRawUid(advertisingToken); - assertFails(decryptionResponse, tokenVersion); + + if (tokenVersion == TokenVersionForTesting.V2) { + assertSuccess(decryptionResponse, tokenVersion); + } else { + assertFails(decryptionResponse, tokenVersion); + } } @ParameterizedTest @@ -142,11 +154,27 @@ public void tokenLifetimeTooLongForSharing(IdentityScope identityScope, TokenVer "UID2, V4", "EUID, V4" }) + public void tokenRemainingLifetimeTooLongForSharing(IdentityScope identityScope, TokenVersionForTesting tokenVersion) throws Exception { + Instant now = Instant.now(); + Instant tokenExpiry = now.plus(30, ChronoUnit.DAYS).plus(1, ChronoUnit.MINUTES); + String advertisingToken = AdvertisingTokenBuilder.builder().withExpiry(tokenExpiry).withScope(identityScope).withVersion(tokenVersion).withGenerated(now).build(); + refresh(keySharingResponse(identityScope, MASTER_KEY, SITE_KEY)); + + DecryptionResponse decryptionResponse = sharingClient.decryptTokenIntoRawUid(advertisingToken); + assertFails(decryptionResponse, tokenVersion); + } + + @ParameterizedTest + @CsvSource({ + "UID2, V3", + "EUID, V3", + "UID2, V4", + "EUID, V4" + }) public void tokenGeneratedInTheFutureToSimulateClockSkew(IdentityScope identityScope, TokenVersionForTesting tokenVersion) throws Exception { Instant tokenGenerated = Instant.now().plus(31, ChronoUnit.MINUTES); String advertisingToken = AdvertisingTokenBuilder.builder().withGenerated(tokenGenerated).withScope(identityScope).withVersion(tokenVersion).build(); - RefreshResponse refreshResponse = sharingClient.refreshJson(keySharingResponse(identityScope, MASTER_KEY, SITE_KEY)); - assertTrue(refreshResponse.isSuccess()); + refresh(keySharingResponse(identityScope, MASTER_KEY, SITE_KEY)); DecryptionResponse decryptionResponse = sharingClient.decryptTokenIntoRawUid(advertisingToken); assertFails(decryptionResponse, tokenVersion); @@ -164,8 +192,7 @@ public void tokenGeneratedInTheFutureToSimulateClockSkew(IdentityScope identityS public void tokenGeneratedInTheFutureWithinAllowedClockSkew(IdentityScope identityScope, TokenVersionForTesting tokenVersion) throws Exception { Instant tokenGenerated = Instant.now().plus(30, ChronoUnit.MINUTES); String advertisingToken = AdvertisingTokenBuilder.builder().withGenerated(tokenGenerated).withScope(identityScope).withVersion(tokenVersion).build(); - RefreshResponse refreshResponse = sharingClient.refreshJson(keySharingResponse(identityScope, MASTER_KEY, SITE_KEY)); - assertTrue(refreshResponse.isSuccess()); + refresh(keySharingResponse(identityScope, MASTER_KEY, SITE_KEY)); decryptAndAssertSuccess(advertisingToken, tokenVersion); } @@ -180,8 +207,7 @@ public void tokenGeneratedInTheFutureWithinAllowedClockSkew(IdentityScope identi public void phoneTest(IdentityScope identityScope, TokenVersionForTesting tokenVersion) throws Exception { String rawUidPhone = "BEOGxroPLdcY7LrSiwjY52+X05V0ryELpJmoWAyXiwbZ"; String advertisingToken = AdvertisingTokenBuilder.builder().withRawUid(rawUidPhone).withScope(identityScope).withVersion(tokenVersion).build(); - RefreshResponse refreshResponse = sharingClient.refreshJson(keySharingResponse(identityScope, MASTER_KEY, SITE_KEY)); - assertTrue(refreshResponse.isSuccess()); + refresh(keySharingResponse(identityScope, MASTER_KEY, SITE_KEY)); DecryptionResponse decryptionResponse = sharingClient.decryptTokenIntoRawUid(advertisingToken); assertTrue(decryptionResponse.isSuccess()); @@ -193,8 +219,7 @@ public void phoneTest(IdentityScope identityScope, TokenVersionForTesting tokenV @ParameterizedTest @ValueSource(strings = {"V2", "V3", "V4"}) public void legacyResponseFromOldOperator(TokenVersionForTesting tokenVersion) throws Exception { - RefreshResponse refreshResponse = sharingClient.refreshJson(keySetToJsonForSharing(MASTER_KEY, SITE_KEY)); - assertTrue(refreshResponse.isSuccess()); + refresh(keySetToJsonForSharing(MASTER_KEY, SITE_KEY)); String advertisingToken = AdvertisingTokenBuilder.builder().withVersion(tokenVersion).build(); decryptAndAssertSuccess(advertisingToken, tokenVersion); @@ -243,8 +268,7 @@ public void tokenLifetimeTooLongLegacyClient(IdentityScope identityScope, TokenV // tests below taken from EncryptionTestsV4.cs under "// Sharing tests" comment and modified to use SharingClient and the new JSON /key/sharing response private SharingClient SharingSetupAndEncrypt() throws IOException { - RefreshResponse refreshResult = sharingClient.refreshJson(keySetToJsonForSharing(MASTER_KEY, SITE_KEY)); - assertTrue(refreshResult.isSuccess()); + refresh(keySetToJsonForSharing(MASTER_KEY, SITE_KEY)); return sharingClient; } @@ -264,8 +288,7 @@ private String sharingEncrypt(SharingClient client, IdentityScope identityScope) @ValueSource(strings = {"UID2", "EUID"}) public void ClientProducesTokenWithCorrectPrefix(IdentityScope identityScope) throws Exception { SharingClient sharingClient = new SharingClient("ep", "ak", CLIENT_SECRET); - RefreshResponse refreshResponse = sharingClient.refreshJson(keySetToJsonForSharing(identityScope, MASTER_KEY, SITE_KEY)); - assertTrue(refreshResponse.isSuccess()); + sharingClient.refreshJson(keySetToJsonForSharing(identityScope, MASTER_KEY, SITE_KEY)); sharingEncrypt(sharingClient, identityScope); //this validates token and asserts } @@ -290,8 +313,7 @@ public void CanDecryptAnotherClientsEncryptedToken() throws Exception SharingClient receivingClient = new SharingClient("ep", "ak", CLIENT_SECRET); String json = keySharingResponse(IdentityScope.UID2, 4874, 12345, null, MASTER_KEY, SITE_KEY); - RefreshResponse refreshResponse = receivingClient.refreshJson(json); - assertTrue(refreshResponse.isSuccess()); + receivingClient.refreshJson(json); DecryptionResponse res = receivingClient.decryptTokenIntoRawUid(advertisingToken); assertEquals(DecryptionStatus.SUCCESS, res.getStatus()); @@ -319,7 +341,6 @@ public void Uid2ClientProducesUid2Token() throws IOException { public void EuidClientProducesEuidToken() throws IOException { SharingClient sharingClient = new SharingClient("ep", "ak", CLIENT_SECRET); RefreshResponse refreshResponse = sharingClient.refreshJson(keySetToJsonForSharing(IdentityScope.EUID, MASTER_KEY, SITE_KEY)); - assertTrue(refreshResponse.isSuccess()); String advertisingToken = sharingEncrypt(sharingClient, IdentityScope.EUID); //this validates token and asserts @@ -329,8 +350,7 @@ public void EuidClientProducesEuidToken() throws IOException { @Test public void RawUidProducesCorrectIdentityTypeInToken() throws Exception { - RefreshResponse refreshResponse = sharingClient.refreshJson(keySetToJsonForSharing(IdentityScope.EUID, MASTER_KEY, SITE_KEY)); - assertTrue(refreshResponse.isSuccess()); + refresh(keySetToJsonForSharing(IdentityScope.EUID, MASTER_KEY, SITE_KEY)); //see UID2-79+Token+and+ID+format+v3 . Also note EUID does not support v2 or phone assertEquals(IdentityType.Email, GetTokenIdentityType("Q4bGug8t1xjsutKLCNjnb5fTlXSvIQukmahYDJeLBtk=")); //v2 +12345678901. Although this was generated from a phone number, it's a v2 raw UID which doesn't encode this information, so token assumes email by default. @@ -361,8 +381,7 @@ public void MultipleKeysPerKeyset() throws Exception Key MASTER_KEY2 = new Key(264, -1, NOW.minus(2, ChronoUnit.DAYS), NOW.minus(1, ChronoUnit.DAYS), NOW.minus(1, ChronoUnit.HOURS), getMasterSecret()); Key SITE_KEY2 = new Key(265, SITE_ID, NOW.minus(10, ChronoUnit.DAYS), NOW.minus(1, ChronoUnit.DAYS), NOW.minus(1, ChronoUnit.HOURS), getSiteSecret()); - RefreshResponse refreshResponse = sharingClient.refreshJson(keySetToJsonForSharing(MASTER_KEY, MASTER_KEY2, SITE_KEY, SITE_KEY2)); - assertTrue(refreshResponse.isSuccess()); + refresh(keySetToJsonForSharing(MASTER_KEY, MASTER_KEY2, SITE_KEY, SITE_KEY2)); String advertisingToken = sharingEncrypt(sharingClient); @@ -374,8 +393,7 @@ public void MultipleKeysPerKeyset() throws Exception @Test public void CannotEncryptIfNoKeyFromTheDefaultKeyset() throws IOException { String json = keySetToJsonForSharing(MASTER_KEY); - RefreshResponse refreshResponse = sharingClient.refreshJson(json); - assertTrue(refreshResponse.isSuccess()); + refresh(json); EncryptionDataResponse encrypted = sharingClient.encryptRawUidIntoToken(EXAMPLE_UID); assertEquals(EncryptionStatus.NOT_AUTHORIZED_FOR_KEY, encrypted.getStatus()); @@ -384,8 +402,7 @@ public void CannotEncryptIfNoKeyFromTheDefaultKeyset() throws IOException { @Test public void CannotEncryptIfTheresNoDefaultKeysetHeader() throws IOException { String json = keySetToJsonForSharing(MASTER_KEY); - RefreshResponse refreshResponse = sharingClient.refreshJson(json); - assertTrue(refreshResponse.isSuccess()); + refresh(json); EncryptionDataResponse encrypted = sharingClient.encryptRawUidIntoToken(EXAMPLE_UID); assertEquals(EncryptionStatus.NOT_AUTHORIZED_FOR_KEY, encrypted.getStatus()); @@ -394,8 +411,7 @@ public void CannotEncryptIfTheresNoDefaultKeysetHeader() throws IOException { @Test public void ExpiryInTokenMatchesExpiryInResponse() throws IOException { String json = keySharingResponse(IdentityScope.UID2, SITE_ID,99999, 2, MASTER_KEY, SITE_KEY); - RefreshResponse refreshResponse = sharingClient.refreshJson(json); - assertTrue(refreshResponse.isSuccess()); + refresh(json); Instant encryptedAt = Instant.now(); EncryptionDataResponse encrypted = sharingClient.encryptRawUidIntoToken(EXAMPLE_UID, encryptedAt); @@ -412,7 +428,7 @@ public void ExpiryInTokenMatchesExpiryInResponse() throws IOException { @Test public void EncryptKeyExpired() throws IOException { Key key = new Key(SITE_KEY_ID, SITE_ID, NOW, NOW, NOW.minus(1, ChronoUnit.DAYS), getTestSecret(9)); - sharingClient.refreshJson(keySetToJsonForSharing(MASTER_KEY, key)); + refresh(keySetToJsonForSharing(MASTER_KEY, key)); EncryptionDataResponse encrypted = sharingClient.encryptRawUidIntoToken(EXAMPLE_UID); assertEquals(EncryptionStatus.NOT_AUTHORIZED_FOR_KEY, encrypted.getStatus()); //note: KeyInactive was the result for EncryptData, because EncryptData allowed you to pass an expired key. In the Sharing scenario, expired and inactive keys are ignored when encrypting. } @@ -420,7 +436,7 @@ public void EncryptKeyExpired() throws IOException { @Test public void EncryptKeyInactive() throws IOException { Key key = new Key(SITE_KEY_ID, SITE_ID, NOW, NOW.plus(1, ChronoUnit.DAYS), NOW.plus(2, ChronoUnit.DAYS), getTestSecret(9)); - sharingClient.refreshJson(keySetToJsonForSharing(MASTER_KEY, key)); + refresh(keySetToJsonForSharing(MASTER_KEY, key)); EncryptionDataResponse encrypted = sharingClient.encryptRawUidIntoToken(EXAMPLE_UID); assertEquals(EncryptionStatus.NOT_AUTHORIZED_FOR_KEY, encrypted.getStatus()); } @@ -429,7 +445,7 @@ public void EncryptKeyInactive() throws IOException { @Test public void EncryptSiteKeyExpired() throws IOException { Key key = new Key(SITE_KEY_ID, SITE_ID, NOW, NOW, NOW.minus(1, ChronoUnit.DAYS), getTestSecret(9)); - sharingClient.refreshJson(keySetToJsonForSharing(MASTER_KEY, key)); + refresh(keySetToJsonForSharing(MASTER_KEY, key)); EncryptionDataResponse encrypted = sharingClient.encryptRawUidIntoToken(EXAMPLE_UID); assertEquals(EncryptionStatus.NOT_AUTHORIZED_FOR_KEY, encrypted.getStatus()); } @@ -437,7 +453,7 @@ public void EncryptSiteKeyExpired() throws IOException { @Test public void EncryptSiteKeyInactive() throws IOException { Key key = new Key(SITE_KEY_ID, SITE_ID, NOW, NOW.plus(1, ChronoUnit.DAYS), NOW.plus(2, ChronoUnit.DAYS), getTestSecret(9)); - sharingClient.refreshJson(keySetToJsonForSharing(MASTER_KEY, key)); + refresh(keySetToJsonForSharing(MASTER_KEY, key)); EncryptionDataResponse encrypted = sharingClient.encryptRawUidIntoToken(EXAMPLE_UID); assertEquals(EncryptionStatus.NOT_AUTHORIZED_FOR_KEY, encrypted.getStatus()); }