Skip to content

Commit

Permalink
token lifetime check fixes for token v2 (#55)
Browse files Browse the repository at this point in the history
  • Loading branch information
jon8787 authored Apr 17, 2024
1 parent 9086019 commit 2ea5202
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 72 deletions.
18 changes: 10 additions & 8 deletions src/UID2.Client/UID2Encryption.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ private static DecryptionResponse DecryptV2(byte[] encryptedId, KeyContainer key
return new DecryptionResponse(DecryptionStatus.DomainNameCheckFailed, null, established, siteId, siteKey.SiteId, null, advertisingTokenVersion, privacyBits.IsClientSideGenerated, expiry);
}

if (!DoesTokenHaveValidLifetime(clientType, keys, established, expiry, now))
if (!DoesTokenHaveValidLifetime(clientType, keys, now, expiry, now))
return new DecryptionResponse(DecryptionStatus.InvalidTokenLifetime, null, established, siteId, siteKey.SiteId, null, advertisingTokenVersion, privacyBits.IsClientSideGenerated, expiry);

return new DecryptionResponse(DecryptionStatus.Success, idString, established, siteId, siteKey.SiteId, null, advertisingTokenVersion, privacyBits.IsClientSideGenerated, expiry);
Expand Down Expand Up @@ -160,7 +160,8 @@ private static DecryptionResponse DecryptV3(byte[] encryptedId, KeyContainer key
var masterPayloadReader = new BigEndianByteReader(new MemoryStream(masterDecrypted));

long expiresMilliseconds = masterPayloadReader.ReadInt64();
long createdMilliseconds = masterPayloadReader.ReadInt64();
long generatedMilliseconds = masterPayloadReader.ReadInt64();
var generated = DateTimeUtils.FromEpochMilliseconds(generatedMilliseconds);

int operatorSiteId = masterPayloadReader.ReadInt32();
byte operatorType = masterPayloadReader.ReadByte();
Expand Down Expand Up @@ -207,13 +208,13 @@ private static DecryptionResponse DecryptV3(byte[] encryptedId, KeyContainer key
return new DecryptionResponse(DecryptionStatus.DomainNameCheckFailed, null, established, siteId, siteKey.SiteId, identityType, advertisingTokenVersion, privacyBits.IsClientSideGenerated, expiry);
}

if (!DoesTokenHaveValidLifetime(clientType, keys, established, expiry, now))
if (!DoesTokenHaveValidLifetime(clientType, keys, generated, expiry, now))
return new DecryptionResponse(DecryptionStatus.InvalidTokenLifetime, null, established, siteId, siteKey.SiteId, identityType, advertisingTokenVersion, privacyBits.IsClientSideGenerated, expiry);

return new DecryptionResponse(DecryptionStatus.Success, idString, established, siteId, siteKey.SiteId, identityType, advertisingTokenVersion, privacyBits.IsClientSideGenerated, expiry);
}

private static bool DoesTokenHaveValidLifetime(ClientType clientType, KeyContainer keys, DateTime established, DateTime expiry, DateTime now)
private static bool DoesTokenHaveValidLifetime(ClientType clientType, KeyContainer keys, DateTime generatedOrNow, DateTime expiry, DateTime now)
{
long maxLifetimeSeconds;
switch (clientType)
Expand All @@ -228,16 +229,17 @@ private static bool DoesTokenHaveValidLifetime(ClientType clientType, KeyContain
return true;
}

return DoesTokenHaveValidLifetimeImpl(established, expiry, now, maxLifetimeSeconds, keys.AllowClockSkewSeconds);
//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.AllowClockSkewSeconds);
}


private static bool DoesTokenHaveValidLifetimeImpl(DateTime established, DateTime expiry, DateTime now, long maxLifetimeSeconds, long allowClockSkewSeconds)
private static bool DoesTokenHaveValidLifetimeImpl(DateTime generatedOrNow, DateTime expiry, DateTime now, long maxLifetimeSeconds, long allowClockSkewSeconds)
{
if ((expiry - established).TotalSeconds > maxLifetimeSeconds)
if ((expiry - generatedOrNow).TotalSeconds > maxLifetimeSeconds)
return false;

return (established - now).TotalSeconds <= allowClockSkewSeconds; //returns false if token generated too far in the future
return (generatedOrNow - now).TotalSeconds <= allowClockSkewSeconds; //returns false if token generated too far in the future
}

private static bool IsDomainNameAllowedForSite(ClientType clientType, PrivacyBits privacyBits, int siteId, string domainName, KeyContainer keys)
Expand Down
12 changes: 7 additions & 5 deletions src/UID2.Client/Utils/UID2TokenGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ public class Params
{
public DateTime TokenExpiry = DateTime.UtcNow.AddHours(1);
public int PrivacyBits = 0;
public DateTime TokenGenerated = DateTime.UtcNow;
public DateTime TokenGenerated = DateTime.UtcNow;
public DateTime IdentityEstablished = DateTime.UtcNow;

public Params() { }
public Params WithTokenExpiry(DateTime expiry) { TokenExpiry = expiry; return this; }
public Params WithPrivacyBits(int privacyBits) { PrivacyBits = privacyBits; return this; }
public Params WithTokenGenerated(DateTime generated) { TokenGenerated = generated; return this; }
public Params WithTokenGenerated(DateTime 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(DateTime established) { IdentityEstablished = established; return this; } //when was the first call to /token/generate or CSTG

public int IdentityScope = (int)UID2.Client.IdentityScope.UID2;
}
Expand Down Expand Up @@ -50,7 +52,7 @@ public static string GenerateUid2TokenV2(string uid, Key masterKey, int siteId,
identityWriter.Write(uidBytes.Length);
identityWriter.Write(uidBytes);
identityWriter.Write(encryptParams.PrivacyBits);
identityWriter.Write(DateTimeUtils.DateTimeToEpochMilliseconds(encryptParams.TokenGenerated));
identityWriter.Write(DateTimeUtils.DateTimeToEpochMilliseconds(encryptParams.IdentityEstablished));
byte[] identityIv = new byte[16];
ThreadSafeRandom.PerThread.NextBytes(identityIv);
byte[] encryptedIdentity = Encrypt(identityStream.ToArray(), identityIv, siteKey.Secret);
Expand Down Expand Up @@ -122,14 +124,14 @@ private static string GenerateTokenV3orV4(string uid, Key masterKey, int siteId,

// user identity data
sitePayloadWriter.Write(encryptParams.PrivacyBits);
sitePayloadWriter.Write(DateTimeUtils.DateTimeToEpochMilliseconds(encryptParams.TokenGenerated)); // established
sitePayloadWriter.Write(DateTimeUtils.DateTimeToEpochMilliseconds(encryptParams.IdentityEstablished)); // established
sitePayloadWriter.Write(DateTimeUtils.DateTimeToEpochMilliseconds(encryptParams.TokenGenerated)); // last refreshed
sitePayloadWriter.Write(Convert.FromBase64String(uid));

var masterPayload = new MemoryStream();
var masterPayloadWriter = new BigEndianByteWriter(masterPayload);
masterPayloadWriter.Write(DateTimeUtils.DateTimeToEpochMilliseconds(encryptParams.TokenExpiry));
masterPayloadWriter.Write(DateTimeUtils.DateTimeToEpochMilliseconds(encryptParams.TokenGenerated)); // token created
masterPayloadWriter.Write(DateTimeUtils.DateTimeToEpochMilliseconds(encryptParams.TokenGenerated)); //identity refreshed, seems to be identical to TokenGenerated in Operator

// operator identity data
masterPayloadWriter.Write(0); // site id
Expand Down
78 changes: 53 additions & 25 deletions test/UID2.Client.Test/BidstreamClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,26 @@ private static string KeyBidstreamResponse(IEnumerable<Key> keys, IdentityScope
return json.ToString();
}

private void Refresh(string json)
{
var refreshResult = _client.RefreshJson(json);
Assert.True(refreshResult.Success);
}

[Theory]
[InlineData(IdentityScope.UID2, TokenVersion.V2)]
[InlineData(IdentityScope.EUID, TokenVersion.V2)]
[InlineData(IdentityScope.UID2, TokenVersion.V3)]
[InlineData(IdentityScope.EUID, TokenVersion.V3)]
[InlineData(IdentityScope.UID2, TokenVersion.V4)]
[InlineData(IdentityScope.EUID, TokenVersion.V4)]
private void SmokeTest(IdentityScope identityScope, TokenVersion tokenVersion)
private void SmokeTestForBidstream(IdentityScope identityScope, TokenVersion tokenVersion)
{
var refreshResult = _client.RefreshJson(KeyBidstreamResponse(new[] { MASTER_KEY, SITE_KEY}, identityScope));
Assert.True(refreshResult.Success);
Refresh(KeyBidstreamResponse(new[] { MASTER_KEY, SITE_KEY}, identityScope));

var advertisingToken = AdvertisingTokenBuilder.Builder().WithVersion(tokenVersion).WithScope(identityScope).Build();
var now = DateTime.UtcNow;
var advertisingToken = AdvertisingTokenBuilder.Builder().WithVersion(tokenVersion).WithScope(identityScope).WithEstablished(now.AddMonths(-4)).WithGenerated(now.AddDays(-1)).WithExpiry(now.AddDays(2)).
Build();
DecryptAndAssertSuccess(advertisingToken, tokenVersion);
}

Expand Down Expand Up @@ -107,8 +114,7 @@ internal static void AssertFails(DecryptionResponse res, TokenVersion tokenVersi
[InlineData(IdentityScope.EUID, TokenVersion.V4)]
private void PhoneTest(IdentityScope identityScope, TokenVersion tokenVersion)
{
var refreshResult = _client.RefreshJson(KeyBidstreamResponse(new[] { MASTER_KEY, SITE_KEY }, identityScope));
Assert.True(refreshResult.Success);
Refresh(KeyBidstreamResponse(new[] { MASTER_KEY, SITE_KEY }, identityScope));

const string rawUidPhone = "BEOGxroPLdcY7LrSiwjY52+X05V0ryELpJmoWAyXiwbZ";
var advertisingToken = AdvertisingTokenBuilder.Builder().WithVersion(tokenVersion).WithScope(identityScope).WithRawUid(rawUidPhone).Build();
Expand All @@ -127,15 +133,19 @@ private void PhoneTest(IdentityScope identityScope, TokenVersion tokenVersion)
[InlineData(IdentityScope.EUID, TokenVersion.V3)]
[InlineData(IdentityScope.UID2, TokenVersion.V4)]
[InlineData(IdentityScope.EUID, TokenVersion.V4)]
private void TokenLifetimeTooLongForBidstream(IdentityScope identityScope, TokenVersion tokenVersion)
private void TokenLifetimeTooLongForBidstreamButRemainingLifetimeAllowed(IdentityScope identityScope, TokenVersion tokenVersion)
{
var refreshResult = _client.RefreshJson(KeyBidstreamResponse(new[] { MASTER_KEY, SITE_KEY }, identityScope));
Assert.True(refreshResult.Success);
Refresh(KeyBidstreamResponse(new[] { MASTER_KEY, SITE_KEY }, identityScope));

var advertisingToken = AdvertisingTokenBuilder.Builder().WithVersion(tokenVersion).WithScope(identityScope).WithExpiry(DateTime.UtcNow.AddDays(3).AddMinutes(1)).Build();
var generated = DateTime.UtcNow.AddDays(-1);
var advertisingToken = AdvertisingTokenBuilder.Builder().WithVersion(tokenVersion).WithScope(identityScope).WithGenerated(generated).
WithExpiry(generated.AddDays(3).AddMinutes(1)).Build();

var res = _client.DecryptTokenIntoRawUid(advertisingToken, null);
AssertFails(res, tokenVersion);
if (tokenVersion == TokenVersion.V2)
AssertSuccess(res, tokenVersion);
else
AssertFails(res, tokenVersion);
}

[Theory]
Expand All @@ -145,10 +155,28 @@ private void TokenLifetimeTooLongForBidstream(IdentityScope identityScope, Token
[InlineData(IdentityScope.EUID, TokenVersion.V3)]
[InlineData(IdentityScope.UID2, TokenVersion.V4)]
[InlineData(IdentityScope.EUID, TokenVersion.V4)]
private void TokenRemainingLifetimeTooLongForBidstream(IdentityScope identityScope, TokenVersion tokenVersion)
{
Refresh(KeyBidstreamResponse(new[] { MASTER_KEY, SITE_KEY }, identityScope));

var now = DateTime.UtcNow;
var advertisingToken = AdvertisingTokenBuilder.Builder().WithVersion(tokenVersion).WithScope(identityScope).WithGenerated(now).
WithExpiry(now.AddDays(3).AddMinutes(1)).Build();

var res = _client.DecryptTokenIntoRawUid(advertisingToken, null);
AssertFails(res, tokenVersion);
}


[Theory]
//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.
[InlineData(IdentityScope.UID2, TokenVersion.V3)]
[InlineData(IdentityScope.EUID, TokenVersion.V3)]
[InlineData(IdentityScope.UID2, TokenVersion.V4)]
[InlineData(IdentityScope.EUID, TokenVersion.V4)]
private void TokenGeneratedInTheFutureToSimulateClockSkew(IdentityScope identityScope, TokenVersion tokenVersion)
{
var refreshResult = _client.RefreshJson(KeyBidstreamResponse(new[] { MASTER_KEY, SITE_KEY }, identityScope));
Assert.True(refreshResult.Success);
Refresh(KeyBidstreamResponse(new[] { MASTER_KEY, SITE_KEY }, identityScope));

var advertisingToken = AdvertisingTokenBuilder.Builder().WithVersion(tokenVersion).WithScope(identityScope).WithGenerated(DateTime.UtcNow.AddMinutes(31)).Build();

Expand All @@ -166,8 +194,7 @@ private void TokenGeneratedInTheFutureToSimulateClockSkew(IdentityScope identity
[InlineData(IdentityScope.EUID, TokenVersion.V4)]
private void TokenGeneratedInTheFutureWithinAllowedClockSkew(IdentityScope identityScope, TokenVersion tokenVersion)
{
var refreshResult = _client.RefreshJson(KeyBidstreamResponse(new[] { MASTER_KEY, SITE_KEY }, identityScope));
Assert.True(refreshResult.Success);
Refresh(KeyBidstreamResponse(new[] { MASTER_KEY, SITE_KEY }, identityScope));

var advertisingToken = AdvertisingTokenBuilder.Builder().WithVersion(tokenVersion).WithScope(identityScope).WithGenerated(DateTime.UtcNow.AddMinutes(30)).Build();

Expand All @@ -180,8 +207,7 @@ private void TokenGeneratedInTheFutureWithinAllowedClockSkew(IdentityScope ident
[InlineData(TokenVersion.V4)]
private void LegacyResponseFromOldOperator(TokenVersion tokenVersion)
{
var refreshResult = _client.RefreshJson(TestData.KeySharingResponse(new[] { MASTER_KEY, SITE_KEY }));
Assert.True(refreshResult.Success);
Refresh(TestData.KeySharingResponse(new[] { MASTER_KEY, SITE_KEY }));

var advertisingToken = AdvertisingTokenBuilder.Builder().WithVersion(tokenVersion).Build();

Expand Down Expand Up @@ -264,7 +290,8 @@ private static void ValidateAdvertisingToken(string advertisingTokenString, Iden
[InlineData(TokenVersion.V4)]
private void UserOptedOutTest(TokenVersion tokenVersion)
{
_client.RefreshJson(KeyBidstreamResponse(new [] {MASTER_KEY, SITE_KEY}));
Refresh(KeyBidstreamResponse(new [] {MASTER_KEY, SITE_KEY}));

var privacyBits = PrivacyBitsBuilder.Builder().WithOptedOut(true).Build();
string advertisingToken = _tokenBuilder.WithPrivacyBits(privacyBits).WithVersion(tokenVersion).Build();
ValidateAdvertisingToken(advertisingToken, IdentityScope.UID2, IdentityType.Email, tokenVersion);
Expand All @@ -284,7 +311,8 @@ private void UserOptedOutTest(TokenVersion tokenVersion)
[InlineData("example.org", TokenVersion.V4)]
private void TokenIsCstgDerivedTest(string domainName, TokenVersion tokenVersion)
{
_client.RefreshJson(KeySharingResponse(new[] { MASTER_KEY, SITE_KEY }));
Refresh(KeySharingResponse(new[] { MASTER_KEY, SITE_KEY }));

var privacyBits = PrivacyBitsBuilder.Builder().WithClientSideGenerated(true).Build();
string advertisingToken = _tokenBuilder.WithPrivacyBits(privacyBits).WithVersion(tokenVersion).Build();
ValidateAdvertisingToken(advertisingToken, IdentityScope.UID2, IdentityType.Email, tokenVersion);
Expand Down Expand Up @@ -313,7 +341,7 @@ private void TokenIsCstgDerivedTest(string domainName, TokenVersion tokenVersion
[InlineData("foo.com", TokenVersion.V4)] // Domain not associated with any site.
private void TokenIsCstgDerivedDomainNameFailTest(string domainName, TokenVersion tokenVersion)
{
_client.RefreshJson(KeySharingResponse(new[] { MASTER_KEY, SITE_KEY }));
Refresh(KeySharingResponse(new[] { MASTER_KEY, SITE_KEY }));
var privacyBits = PrivacyBitsBuilder.Builder().WithClientSideGenerated(true).Build();
var advertisingToken = _tokenBuilder.WithPrivacyBits(privacyBits).WithVersion(tokenVersion).Build();
var res = _client.DecryptTokenIntoRawUid(advertisingToken, domainName);
Expand All @@ -339,7 +367,7 @@ private void TokenIsCstgDerivedDomainNameFailTest(string domainName, TokenVersio
[InlineData("foo.com", TokenVersion.V4)]
private void TokenIsNotCstgDerivedDomainNameSuccessTest(string domainName, TokenVersion tokenVersion)
{
_client.RefreshJson(KeySharingResponse(new[] { MASTER_KEY, SITE_KEY }));
Refresh(KeySharingResponse(new[] { MASTER_KEY, SITE_KEY }));
var privacyBits = PrivacyBitsBuilder.Builder().WithClientSideGenerated(false).Build();
string advertisingToken = _tokenBuilder.WithPrivacyBits(privacyBits).WithVersion(tokenVersion).Build();
ValidateAdvertisingToken(advertisingToken, IdentityScope.UID2, IdentityType.Email, tokenVersion);
Expand Down Expand Up @@ -368,7 +396,7 @@ public void ExpiredKeyContainer()

Key masterKeyExpired = new Key(MASTER_KEY_ID, -1, NOW, NOW.AddHours(-2), NOW.AddHours(-1), MASTER_SECRET);
Key siteKeyExpired = new Key(SITE_KEY_ID, SITE_ID, NOW, NOW.AddHours(-2), NOW.AddHours(-1), SITE_SECRET);
_client.RefreshJson(KeyBidstreamResponse(new[] { masterKeyExpired, siteKeyExpired}));
Refresh(KeyBidstreamResponse(new[] { masterKeyExpired, siteKeyExpired}));

var res = _client.DecryptTokenIntoRawUid(advertisingToken, null);
Assert.False(res.Success);
Expand All @@ -380,7 +408,7 @@ public void NotAuthorizedForMasterKey()
{
Key anotherMasterKey = new Key(MASTER_KEY_ID + SITE_KEY_ID + 1, -1, NOW, NOW, NOW.AddHours(1), MASTER_SECRET);
Key anotherSiteKey = new Key(MASTER_KEY_ID + SITE_KEY_ID + 2, SITE_ID, NOW, NOW, NOW.AddHours(1), SITE_SECRET);
_client.RefreshJson(KeyBidstreamResponse(new[] { anotherMasterKey, anotherSiteKey}));
Refresh(KeyBidstreamResponse(new[] { anotherMasterKey, anotherSiteKey}));

var res = _client.DecryptTokenIntoRawUid(_tokenBuilder.Build(), null);
Assert.Equal(DecryptionStatus.NotAuthorizedForMasterKey, res.Status);
Expand All @@ -393,7 +421,7 @@ public void InvalidPayload()
var advertisingToken = UID2Base64UrlCoder.Encode(payload.SkipLast(1).ToArray());
ValidateAdvertisingToken(advertisingToken, IdentityScope.UID2, IdentityType.Email);

_client.RefreshJson(KeyBidstreamResponse(new[] { MASTER_KEY, SITE_KEY}));
Refresh(KeyBidstreamResponse(new[] { MASTER_KEY, SITE_KEY}));

var res = _client.DecryptTokenIntoRawUid(advertisingToken, null);
Assert.Equal(DecryptionStatus.InvalidPayload, res.Status);
Expand All @@ -403,7 +431,7 @@ public void InvalidPayload()
public void TokenExpiryAndCustomNow()
{
var expiry = NOW.AddDays(-60);
_client.RefreshJson(KeyBidstreamResponse(new[] { MASTER_KEY, SITE_KEY}));
Refresh(KeyBidstreamResponse(new[] { MASTER_KEY, SITE_KEY}));
string advertisingToken = _tokenBuilder.WithGenerated(expiry.AddSeconds(-60)).WithExpiry(expiry).Build();
ValidateAdvertisingToken(advertisingToken, IdentityScope.UID2, IdentityType.Email);

Expand Down
Loading

0 comments on commit 2ea5202

Please sign in to comment.