Skip to content

Commit 8fb7f36

Browse files
authored
Merge pull request #66 from IABTechLab/ccm-UID2-3243-token-lifetime-check-fix-for-token-v2
UID2-3243 token lifetime check fix for token v2
2 parents 75e7b12 + a0ff2fb commit 8fb7f36

File tree

6 files changed

+127
-78
lines changed

6 files changed

+127
-78
lines changed

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

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ static DecryptionResponse decryptV2(byte[] encryptedId, KeyContainer keys, Insta
103103
return DecryptionResponse.makeError(DecryptionStatus.EXPIRED_TOKEN, established, siteId, siteKey.getSiteId(), null, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry);
104104
}
105105

106-
if (!doesTokenHaveValidLifetime(clientType, keys, established, expiry, now)) {
106+
if (!doesTokenHaveValidLifetime(clientType, keys, now, expiry, now)) {
107107
return DecryptionResponse.makeError(DecryptionStatus.INVALID_TOKEN_LIFETIME, established, siteId, siteKey.getSiteId(), null, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry);
108108
}
109109

@@ -136,7 +136,8 @@ static DecryptionResponse decryptV3(byte[] encryptedId, KeyContainer keys, Insta
136136
final ByteBuffer masterReader = ByteBuffer.wrap(masterPayload);
137137

138138
final long expiresMilliseconds = masterReader.getLong();
139-
final long createdMilliseconds = masterReader.getLong();
139+
final long generatedMilliseconds = masterReader.getLong();
140+
Instant generated = Instant.ofEpochMilli(generatedMilliseconds);
140141

141142
final int operatorSideId = masterReader.getInt();
142143
final byte operatorType = masterReader.get();
@@ -168,8 +169,8 @@ static DecryptionResponse decryptV3(byte[] encryptedId, KeyContainer keys, Insta
168169
return DecryptionResponse.makeError(DecryptionStatus.EXPIRED_TOKEN, established, siteId, siteKey.getSiteId(), identityType, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry);
169170
}
170171

171-
if (!doesTokenHaveValidLifetime(clientType, keys, established, expiry, now)) {
172-
return DecryptionResponse.makeError(DecryptionStatus.INVALID_TOKEN_LIFETIME, established, siteId, siteKey.getSiteId(), identityType, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry);
172+
if (!doesTokenHaveValidLifetime(clientType, keys, generated, expiry, now)) {
173+
return DecryptionResponse.makeError(DecryptionStatus.INVALID_TOKEN_LIFETIME, generated, siteId, siteKey.getSiteId(), identityType, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry);
173174
}
174175

175176
return new DecryptionResponse(DecryptionStatus.SUCCESS, idString, established, siteId, siteKey.getSiteId(), identityType, advertisingTokenVersion, privacyBits.isClientSideGenerated(), expiry);
@@ -401,7 +402,7 @@ public CryptoException(Throwable inner) {
401402
}
402403
}
403404

404-
private static boolean doesTokenHaveValidLifetime(ClientType clientType, KeyContainer keys, Instant established, Instant expiry, Instant now) {
405+
private static boolean doesTokenHaveValidLifetime(ClientType clientType, KeyContainer keys, Instant generatedOrNow, Instant expiry, Instant now) {
405406
long maxLifetimeSeconds;
406407
switch (clientType) {
407408
case BIDSTREAM:
@@ -413,17 +414,18 @@ private static boolean doesTokenHaveValidLifetime(ClientType clientType, KeyCont
413414
default: //Legacy
414415
return true;
415416
}
416-
return doesTokenHaveValidLifetimeImpl(established, expiry, now, maxLifetimeSeconds, keys.getAllowClockSkewSeconds());
417+
//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.
418+
return doesTokenHaveValidLifetimeImpl(generatedOrNow, expiry, now, maxLifetimeSeconds, keys.getAllowClockSkewSeconds());
417419
}
418420

419-
private static boolean doesTokenHaveValidLifetimeImpl(Instant established, Instant expiry, Instant now, long maxLifetimeSeconds, long allowClockSkewSeconds)
421+
private static boolean doesTokenHaveValidLifetimeImpl(Instant generatedOrNow, Instant expiry, Instant now, long maxLifetimeSeconds, long allowClockSkewSeconds)
420422
{
421-
Duration lifetime = Duration.between(established, expiry);
423+
Duration lifetime = Duration.between(generatedOrNow, expiry);
422424
if (lifetime.getSeconds() > maxLifetimeSeconds) {
423425
return false;
424426
}
425427

426-
Duration skewDuration = Duration.between(now, established);
428+
Duration skewDuration = Duration.between(now, generatedOrNow);
427429
return skewDuration.getSeconds() <= allowClockSkewSeconds;
428430
}
429431

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ public static class Params
2020
Instant tokenExpiry = Instant.now().plus(1, ChronoUnit.HOURS);
2121
public int identityScope = IdentityScope.UID2.value;
2222
public Instant tokenGenerated = Instant.now();
23+
public Instant identityEstablished = Instant.now();
2324
public int tokenPrivacyBits = 0;
2425

2526
public Params() {}
2627
public Params withTokenExpiry(Instant expiry) { tokenExpiry = expiry; return this; }
27-
public Params WithTokenGenerated(Instant generated) { tokenGenerated = generated; return this; }
28+
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)
29+
public Params WithIdentityEstablished(Instant established) { identityEstablished = established; return this; } //when was the first call to /token/generate or CSTG
2830
public Params WithPrivacyBits(int privacyBits) { tokenPrivacyBits = privacyBits; return this; }
2931
}
3032

@@ -43,7 +45,7 @@ public static byte[] generateUid2TokenV2(String uid, Key masterKey, long siteId,
4345
identityWriter.putInt(uidBytes.length);
4446
identityWriter.put(uidBytes);
4547
identityWriter.putInt(params.tokenPrivacyBits);
46-
identityWriter.putLong(params.tokenGenerated.toEpochMilli());
48+
identityWriter.putLong(params.identityEstablished.toEpochMilli());
4749
byte[] identityIv = new byte[16];
4850
rd.nextBytes(identityIv);
4951
byte[] encryptedIdentity = encrypt(identityWriter.array(), identityIv, siteKey.getSecret());
@@ -90,13 +92,13 @@ private static String generateUID2TokenV3OrV4(String uid, Key masterKey, long si
9092

9193
// user identity data
9294
sitePayloadWriter.putInt(params.tokenPrivacyBits); // privacy bits
93-
sitePayloadWriter.putLong(params.tokenGenerated.toEpochMilli()); // established
95+
sitePayloadWriter.putLong(params.identityEstablished.toEpochMilli()); // established
9496
sitePayloadWriter.putLong(params.tokenGenerated.toEpochMilli()); // last refreshed
9597
sitePayloadWriter.put(Base64.getDecoder().decode(uid));
9698

9799
final ByteBuffer masterPayloadWriter = ByteBuffer.allocate(256);
98100
masterPayloadWriter.putLong(params.tokenExpiry.toEpochMilli());
99-
masterPayloadWriter.putLong(params.tokenGenerated.toEpochMilli()); // token created
101+
masterPayloadWriter.putLong(params.tokenGenerated.toEpochMilli()); //identity refreshed, seems to be identical to TokenGenerated in Operator
100102

101103
// operator identity data
102104
masterPayloadWriter.putInt(0); // site id

src/test/java/com/uid2/client/AdvertisingTokenBuilder.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class AdvertisingTokenBuilder {
1616
Instant expiry = Instant.now().plus(1, ChronoUnit.HOURS);
1717
IdentityScope identityScope = IdentityScope.UID2;
1818
Instant generated = Instant.now();
19+
Instant established = Instant.now();
1920

2021
static AdvertisingTokenBuilder builder() {
2122
return new AdvertisingTokenBuilder();
@@ -69,9 +70,14 @@ AdvertisingTokenBuilder withGenerated(Instant generated)
6970
return this;
7071
}
7172

73+
AdvertisingTokenBuilder withEstablished(Instant established)
74+
{
75+
this.established = established;
76+
return this;
77+
}
7278

7379
String build() throws Exception {
74-
Uid2TokenGenerator.Params params = Uid2TokenGenerator.defaultParams().WithPrivacyBits(privacyBits).withTokenExpiry(expiry).WithTokenGenerated(generated);
80+
Uid2TokenGenerator.Params params = Uid2TokenGenerator.defaultParams().WithPrivacyBits(privacyBits).withTokenExpiry(expiry).WithTokenGenerated(generated).WithIdentityEstablished(established);
7581

7682
params.identityScope = identityScope.value;
7783
String token;

src/test/java/com/uid2/client/BidstreamClientTests.java

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ public class BidstreamClientTests {
3333
"UID2, V4",
3434
"EUID, V4"
3535
})
36-
public void smokeTest(IdentityScope identityScope, TokenVersionForTesting tokenVersion) throws Exception {
37-
String advertisingToken = AdvertisingTokenBuilder.builder().withScope(identityScope).withVersion(tokenVersion).build();
38-
callAndVerifyRefreshJson(identityScope);
36+
public void smokeTestForBidstream(IdentityScope identityScope, TokenVersionForTesting tokenVersion) throws Exception {
37+
Instant now = Instant.now();
38+
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();
39+
refresh(keyBidstreamResponse(identityScope, MASTER_KEY, SITE_KEY));
3940

4041
decryptAndAssertSuccess(advertisingToken, tokenVersion);
4142
}
@@ -50,7 +51,7 @@ public void smokeTest(IdentityScope identityScope, TokenVersionForTesting tokenV
5051
public void phoneTest(IdentityScope identityScope, TokenVersionForTesting tokenVersion) throws Exception {
5152
String rawUidPhone = "BEOGxroPLdcY7LrSiwjY52+X05V0ryELpJmoWAyXiwbZ";
5253
String advertisingToken = AdvertisingTokenBuilder.builder().withRawUid(rawUidPhone).withScope(identityScope).withVersion(tokenVersion).build();
53-
callAndVerifyRefreshJson(identityScope);
54+
refresh(keyBidstreamResponse(identityScope, MASTER_KEY, SITE_KEY));
5455

5556
DecryptionResponse decryptionResponse = bidstreamClient.decryptTokenIntoRawUid(advertisingToken, null);
5657
assertTrue(decryptionResponse.isSuccess());
@@ -68,13 +69,19 @@ public void phoneTest(IdentityScope identityScope, TokenVersionForTesting tokenV
6869
"UID2, V4",
6970
"EUID, V4"
7071
})
71-
public void tokenLifetimeTooLongForBidstream(IdentityScope identityScope, TokenVersionForTesting tokenVersion) throws Exception {
72-
Instant tokenExpiry = Instant.now().plus(3, ChronoUnit.DAYS).plus(1, ChronoUnit.MINUTES);
73-
String advertisingToken = AdvertisingTokenBuilder.builder().withExpiry(tokenExpiry).withScope(identityScope).withVersion(tokenVersion).build();
74-
callAndVerifyRefreshJson(identityScope);
72+
public void tokenLifetimeTooLongForBidstreamButRemainingLifetimeAllowed(IdentityScope identityScope, TokenVersionForTesting tokenVersion) throws Exception {
73+
Instant generated = Instant.now().minus(1, ChronoUnit.DAYS);
74+
Instant tokenExpiry = generated.plus(3, ChronoUnit.DAYS).plus(1, ChronoUnit.MINUTES);
75+
String advertisingToken = AdvertisingTokenBuilder.builder().withExpiry(tokenExpiry).withScope(identityScope).withVersion(tokenVersion).withGenerated(generated).build();
76+
refresh(keyBidstreamResponse(identityScope, MASTER_KEY, SITE_KEY));
7577

7678
DecryptionResponse decryptionResponse = bidstreamClient.decryptTokenIntoRawUid(advertisingToken, null);
77-
assertFails(decryptionResponse, tokenVersion);
79+
80+
if (tokenVersion == TokenVersionForTesting.V2) {
81+
assertSuccess(decryptionResponse, tokenVersion);
82+
} else {
83+
assertFails(decryptionResponse, tokenVersion);
84+
}
7885
}
7986

8087
@ParameterizedTest
@@ -86,10 +93,28 @@ public void tokenLifetimeTooLongForBidstream(IdentityScope identityScope, TokenV
8693
"UID2, V4",
8794
"EUID, V4"
8895
})
96+
public void tokenRemainingLifetimeTooLongForBidstream(IdentityScope identityScope, TokenVersionForTesting tokenVersion) throws Exception {
97+
Instant tokenExpiry = Instant.now().plus(3, ChronoUnit.DAYS).plus(1, ChronoUnit.MINUTES);
98+
Instant generated = Instant.now();
99+
String advertisingToken = AdvertisingTokenBuilder.builder().withExpiry(tokenExpiry).withScope(identityScope).withVersion(tokenVersion).withGenerated(generated).build();
100+
refresh(keyBidstreamResponse(identityScope, MASTER_KEY, SITE_KEY));
101+
102+
DecryptionResponse decryptionResponse = bidstreamClient.decryptTokenIntoRawUid(advertisingToken, null);
103+
assertFails(decryptionResponse, tokenVersion);
104+
}
105+
106+
@ParameterizedTest
107+
//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.
108+
@CsvSource({
109+
"UID2, V3",
110+
"EUID, V3",
111+
"UID2, V4",
112+
"EUID, V4"
113+
})
89114
public void tokenGeneratedInTheFutureToSimulateClockSkew(IdentityScope identityScope, TokenVersionForTesting tokenVersion) throws Exception {
90115
Instant tokenGenerated = Instant.now().plus(31, ChronoUnit.MINUTES);
91116
String advertisingToken = AdvertisingTokenBuilder.builder().withGenerated(tokenGenerated).withScope(identityScope).withVersion(tokenVersion).build();
92-
callAndVerifyRefreshJson(identityScope);
117+
refresh(keyBidstreamResponse(identityScope, MASTER_KEY, SITE_KEY));
93118

94119
DecryptionResponse decryptionResponse = bidstreamClient.decryptTokenIntoRawUid(advertisingToken, null);
95120
assertFails(decryptionResponse, tokenVersion);
@@ -107,16 +132,15 @@ public void tokenGeneratedInTheFutureToSimulateClockSkew(IdentityScope identityS
107132
public void tokenGeneratedInTheFutureWithinAllowedClockSkew(IdentityScope identityScope, TokenVersionForTesting tokenVersion) throws Exception {
108133
Instant tokenGenerated = Instant.now().plus(30, ChronoUnit.MINUTES);
109134
String advertisingToken = AdvertisingTokenBuilder.builder().withGenerated(tokenGenerated).withScope(identityScope).withVersion(tokenVersion).build();
110-
callAndVerifyRefreshJson(identityScope);
135+
refresh(keyBidstreamResponse(identityScope, MASTER_KEY, SITE_KEY));
111136

112137
decryptAndAssertSuccess(advertisingToken, tokenVersion);
113138
}
114139

115140
@ParameterizedTest
116141
@ValueSource(strings = {"V2", "V3", "V4"})
117142
public void legacyResponseFromOldOperator(TokenVersionForTesting tokenVersion) throws Exception {
118-
RefreshResponse refreshResponse = bidstreamClient.refreshJson(keySetToJsonForSharing(MASTER_KEY, SITE_KEY));
119-
assertTrue(refreshResponse.isSuccess());
143+
refresh(keySetToJsonForSharing(MASTER_KEY, SITE_KEY));
120144
String advertisingToken = AdvertisingTokenBuilder.builder().withVersion(tokenVersion).build();
121145

122146
decryptAndAssertSuccess(advertisingToken, tokenVersion);
@@ -165,7 +189,7 @@ public void tokenLifetimeTooLongLegacyClient(IdentityScope identityScope, TokenV
165189
@ParameterizedTest
166190
@MethodSource("data_IdentityScopeAndType_TestCases")
167191
public void identityScopeAndType_TestCases(String uid, IdentityScope identityScope, IdentityType identityType) throws Exception {
168-
callAndVerifyRefreshJson(identityScope);
192+
refresh(keyBidstreamResponse(identityScope, MASTER_KEY, SITE_KEY));
169193

170194
String advertisingToken = AdvertisingTokenBuilder.builder().withRawUid(uid).withScope(identityScope).build();
171195
DecryptionResponse res = bidstreamClient.decryptTokenIntoRawUid(advertisingToken, null);
@@ -194,7 +218,7 @@ private static Stream<Arguments> data_IdentityScopeAndType_TestCases() {
194218
"example.org, V4"
195219
})
196220
public void TokenIsCstgDerivedTest(String domainName, TokenVersionForTesting tokenVersion) throws Exception {
197-
callAndVerifyRefreshJson(IdentityScope.UID2);
221+
refresh(keyBidstreamResponse(IdentityScope.UID2, MASTER_KEY, SITE_KEY));
198222
int privacyBits = PrivacyBitsBuilder.Builder().WithClientSideGenerated(true).Build();
199223

200224
String advertisingToken = AdvertisingTokenBuilder.builder().withVersion(tokenVersion).withPrivacyBits(privacyBits).build();
@@ -221,8 +245,7 @@ public void expiredKeyContainer() throws Exception {
221245

222246
Key masterKeyExpired = new Key(MASTER_KEY_ID, -1, NOW, NOW.minus(2, ChronoUnit.HOURS), NOW.minus(1, ChronoUnit.HOURS), getMasterSecret());
223247
Key siteKeyExpired = new Key(SITE_KEY_ID, SITE_ID, NOW, NOW.minus(2, ChronoUnit.HOURS), NOW.minus(1, ChronoUnit.HOURS), getSiteSecret());
224-
RefreshResponse refreshResponse = bidstreamClient.refreshJson(keyBidstreamResponse(IdentityScope.UID2, masterKeyExpired, siteKeyExpired));
225-
assertTrue(refreshResponse.isSuccess());
248+
refresh(keyBidstreamResponse(IdentityScope.UID2, masterKeyExpired, siteKeyExpired));
226249

227250
DecryptionResponse res = bidstreamClient.decryptTokenIntoRawUid(advertisingToken, null);
228251
assertEquals(DecryptionStatus.KEYS_NOT_SYNCED, res.getStatus());
@@ -234,8 +257,7 @@ public void notAuthorizedForMasterKey() throws Exception {
234257

235258
Key anotherMasterKey = new Key(MASTER_KEY_ID + SITE_KEY_ID + 1, -1, NOW, NOW, NOW.plus(1, ChronoUnit.HOURS), getMasterSecret());
236259
Key anotherSiteKey = new Key(MASTER_KEY_ID + SITE_KEY_ID + 2, SITE_ID, NOW, NOW, NOW.plus(1, ChronoUnit.HOURS), getSiteSecret());
237-
RefreshResponse refreshResponse = bidstreamClient.refreshJson(keyBidstreamResponse(IdentityScope.UID2, anotherMasterKey, anotherSiteKey));
238-
assertTrue(refreshResponse.isSuccess());
260+
refresh(keyBidstreamResponse(IdentityScope.UID2, anotherMasterKey, anotherSiteKey));
239261

240262
DecryptionResponse res = bidstreamClient.decryptTokenIntoRawUid(advertisingToken, null);
241263
assertEquals(DecryptionStatus.NOT_AUTHORIZED_FOR_MASTER_KEY, res.getStatus());
@@ -246,7 +268,7 @@ public void invalidPayload() throws Exception {
246268
String payload = AdvertisingTokenBuilder.builder().build();
247269
byte[] payloadInBytes = Uid2Base64UrlCoder.decode(payload);
248270
String advertisingToken = Uid2Base64UrlCoder.encode(Arrays.copyOfRange(payloadInBytes, 0, payloadInBytes.length - 1));
249-
bidstreamClient.refreshJson(keyBidstreamResponse(IdentityScope.UID2, MASTER_KEY, SITE_KEY));
271+
refresh(keyBidstreamResponse(IdentityScope.UID2, MASTER_KEY, SITE_KEY));
250272
DecryptionResponse res = bidstreamClient.decryptTokenIntoRawUid(advertisingToken, null);
251273
assertEquals(DecryptionStatus.INVALID_PAYLOAD, res.getStatus());
252274
}
@@ -256,7 +278,7 @@ public void tokenExpiryAndCustomNow() throws Exception {
256278
final Instant expiry = Instant.parse("2021-03-22T09:01:02Z");
257279
final Instant generated = expiry.minus(60, ChronoUnit.SECONDS);
258280

259-
bidstreamClient.refreshJson(keyBidstreamResponse(IdentityScope.UID2, MASTER_KEY, SITE_KEY));
281+
refresh(keyBidstreamResponse(IdentityScope.UID2, MASTER_KEY, SITE_KEY));
260282
String advertisingToken = AdvertisingTokenBuilder.builder().withExpiry(expiry).withGenerated(generated).build();
261283

262284
DecryptionResponse res = bidstreamClient.decryptTokenIntoRawUid(advertisingToken, null, expiry.plus(1, ChronoUnit.SECONDS));
@@ -266,8 +288,8 @@ public void tokenExpiryAndCustomNow() throws Exception {
266288
assertEquals(EXAMPLE_UID, res.getUid());
267289
}
268290

269-
private void callAndVerifyRefreshJson(IdentityScope identityScope) {
270-
RefreshResponse refreshResponse = bidstreamClient.refreshJson(keyBidstreamResponse(identityScope, MASTER_KEY, SITE_KEY));
291+
private void refresh(String json) {
292+
RefreshResponse refreshResponse = bidstreamClient.refreshJson(json);
271293
assertTrue(refreshResponse.isSuccess());
272294
}
273295

0 commit comments

Comments
 (0)