Skip to content

Commit

Permalink
Merge pull request #66 from IABTechLab/ccm-UID2-3243-token-lifetime-c…
Browse files Browse the repository at this point in the history
…heck-fix-for-token-v2

UID2-3243 token lifetime check fix for token v2
  • Loading branch information
caroline-ttd authored Apr 24, 2024
2 parents 75e7b12 + a0ff2fb commit 8fb7f36
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 78 deletions.
20 changes: 11 additions & 9 deletions src/main/java/com/uid2/client/Uid2Encryption.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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:
Expand All @@ -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;
}

Expand Down
10 changes: 6 additions & 4 deletions src/main/java/com/uid2/client/Uid2TokenGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}

Expand All @@ -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());
Expand Down Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion src/test/java/com/uid2/client/AdvertisingTokenBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand Down
68 changes: 45 additions & 23 deletions src/test/java/com/uid2/client/BidstreamClientTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.minus(1, ChronoUnit.DAYS)).withExpiry(now.plus(2, ChronoUnit.DAYS)).build();
refresh(keyBidstreamResponse(identityScope, MASTER_KEY, SITE_KEY));

decryptAndAssertSuccess(advertisingToken, tokenVersion);
}
Expand All @@ -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());
Expand All @@ -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
Expand All @@ -86,10 +93,28 @@ 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
//Note V2 does not have a "token generated" field, therefore v2 tokens can't have a future "token generated" date and are excluded from this test.
@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);
Expand All @@ -107,16 +132,15 @@ 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);
}

@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);
Expand Down Expand Up @@ -165,7 +189,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);
Expand Down Expand Up @@ -194,7 +218,7 @@ private static Stream<Arguments> 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();
Expand All @@ -221,8 +245,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());
Expand All @@ -234,8 +257,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());
Expand All @@ -246,7 +268,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());
}
Expand All @@ -256,7 +278,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));
Expand All @@ -266,8 +288,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());
}

Expand Down
Loading

0 comments on commit 8fb7f36

Please sign in to comment.