Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.plus(-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