Skip to content

Commit 825dcea

Browse files
committed
Handle lifetime check for v2 tokens differently
1 parent d019440 commit 825dcea

File tree

6 files changed

+43
-36
lines changed

6 files changed

+43
-36
lines changed

tests/test_bidstream_client.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,11 @@ def test_token_lifetime_too_long_for_bidstream(self): # TokenLifetimeTooLongFor
124124
self.assert_fails(result, expected_version, expected_scope)
125125

126126
def test_token_generated_in_the_future_to_simulate_clock_skew(self): # TokenGeneratedInTheFutureToSimulateClockSkew
127+
# 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.
127128
created_at_future = dt.datetime.now(tz=timezone.utc) + dt.timedelta(minutes=31) #max allowed clock skew is 30m
128-
for expected_scope, expected_version in test_cases_all_scopes_all_versions:
129+
for expected_scope, expected_version in test_cases_all_scopes_v3_v4_versions:
129130
with self.subTest(expected_scope=expected_scope, expected_version=expected_version):
130-
token = generate_uid_token(expected_scope, expected_version, created_at=created_at_future)
131+
token = generate_uid_token(expected_scope, expected_version, generated_at=created_at_future)
131132
refresh_response = self._client._refresh_json(key_bidstream_response_json_default_keys(
132133
expected_scope))
133134
self.assertTrue(refresh_response.success)
@@ -164,7 +165,7 @@ def test_token_generated_in_the_future_legacy_client(self): # TokenGeneratedInT
164165
with self.subTest(expected_scope=expected_scope, expected_version=expected_version):
165166
legacy_client.refresh_json(key_bidstream_response_json_default_keys(
166167
expected_scope))
167-
token = generate_uid_token(expected_scope, expected_version, created_at=created_at_future)
168+
token = generate_uid_token(expected_scope, expected_version, generated_at=created_at_future)
168169
result = legacy_client.decrypt(token)
169170
self.assert_success(result, expected_version, expected_scope)
170171

@@ -246,7 +247,7 @@ def test_token_expiry_custom_decryption_time(self): #TokenExpiryAndCustomNow
246247
expires_at = now - dt.timedelta(days=60)
247248
created_at = expires_at - dt.timedelta(minutes=1)
248249
token = generate_uid_token(IdentityScope.UID2, AdvertisingTokenVersion.ADVERTISING_TOKEN_V4,
249-
created_at=created_at, expires_at=expires_at)
250+
identity_established_at=created_at, expires_at=expires_at)
250251
result = self._client._decrypt_token_into_raw_uid(token, None, expires_at + dt.timedelta(seconds=1))
251252
self.assertFalse(result.success)
252253
self.assertEqual(result.status, DecryptionStatus.EXPIRED_TOKEN)

tests/test_encryption.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ def test_smoke_token_v3(self):
418418
token_expiry = now + dt.timedelta(days=30) if keys.get_token_expiry_seconds() is None \
419419
else now + dt.timedelta(seconds=int(keys.get_token_expiry_seconds()))
420420
result = UID2TokenGenerator.generate_uid2_token_v3(uid2, _master_key, _site_id, _site_key,
421-
Params(expiry=token_expiry, token_generated_at=now))
421+
Params(expiry=token_expiry, token_generated=now))
422422
final = decrypt(result, keys, now=now)
423423

424424
self.assertEqual(uid2, final.uid)

tests/test_sharing_client.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,11 @@ def test_token_lifetime_too_long_for_sharing(self): # TokenLifetimeTooLongForSh
9898
self._test_bidstream_client.assert_fails(result, expected_version, expected_scope)
9999

100100
def test_token_generated_in_the_future_to_simulate_clock_skew(self): # TokenGeneratedInTheFutureToSimulateClockSkew
101+
# 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.
101102
created_at_future = dt.datetime.now(tz=timezone.utc) + dt.timedelta(minutes=31) #max allowed clock skew is 30m
102-
for expected_scope, expected_version in test_cases_all_scopes_all_versions:
103+
for expected_scope, expected_version in test_cases_all_scopes_v3_v4_versions:
103104
with self.subTest(expected_scope=expected_scope, expected_version=expected_version):
104-
token = generate_uid_token(expected_scope, expected_version, created_at=created_at_future)
105+
token = generate_uid_token(expected_scope, expected_version, generated_at=created_at_future)
105106
refresh_response = self._client._refresh_json(key_sharing_response_json_default_keys(
106107
expected_scope))
107108
self.assertTrue(refresh_response.success)
@@ -151,7 +152,7 @@ def test_token_generated_in_the_future_legacy_client(self): # TokenGeneratedInT
151152
with self.subTest(expected_scope=expected_scope, expected_version=expected_version):
152153
legacy_client.refresh_json(key_sharing_response_json_default_keys(
153154
expected_scope))
154-
token = generate_uid_token(expected_scope, expected_version, created_at=created_at_future)
155+
token = generate_uid_token(expected_scope, expected_version, generated_at=created_at_future)
155156
result = legacy_client.decrypt(token)
156157
self._test_bidstream_client.assert_success(result, expected_version, expected_scope)
157158

tests/test_utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,6 @@ def create_default_key_collection(key_set):
114114
99999, 2)
115115

116116

117-
def generate_uid_token(identity_scope, version, raw_uid=example_uid, created_at=None, expires_at=None):
118-
return UID2TokenGenerator.generate_uid_token(raw_uid, master_key, site_id, site_key,
119-
identity_scope, version, created_at, expires_at)
117+
def generate_uid_token(identity_scope, version, raw_uid=example_uid, identity_established_at=None, generated_at=None, expires_at=None):
118+
return UID2TokenGenerator.generate_uid_token(raw_uid, master_key, site_id, site_key, identity_scope, version,
119+
identity_established_at, generated_at, expires_at)

uid2_client/encryption.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -111,18 +111,20 @@ def _decrypt_token(token, keys, domain_name, client_type, now):
111111
return DecryptedToken.make_error(DecryptionStatus.VERSION_NOT_SUPPORTED)
112112

113113

114-
def _token_has_valid_lifetime(keys, client_type, established, expires, now):
114+
def _token_has_valid_lifetime(keys, client_type, generated_or_now, expires, now):
115+
# generated_or_now allows "now" for token v2, since v2 does not contain a "token generated" field.
116+
# v2 therefore checks against remaining lifetime rather than total lifetime
115117
if client_type is ClientType.BIDSTREAM:
116118
max_life_time_seconds = keys.get_max_bidstream_lifetime_seconds()
117119
elif client_type is ClientType.SHARING:
118120
max_life_time_seconds = keys.get_max_sharing_lifetime_seconds()
119121
else:
120122
return True # Skip check for legacy clients
121123

122-
if (expires - established).total_seconds() > max_life_time_seconds:
124+
if (expires - generated_or_now).total_seconds() > max_life_time_seconds:
123125
return False
124-
elif established > now:
125-
return (established - now).total_seconds() <= keys.get_allow_clock_skew_seconds()
126+
elif generated_or_now > now:
127+
return (generated_or_now - now).total_seconds() <= keys.get_allow_clock_skew_seconds()
126128
else:
127129
return True
128130

@@ -165,7 +167,7 @@ def _decrypt_token_v2(token_bytes, keys, domain_name, client_type, now):
165167
established_ms = int.from_bytes(identity[idx:idx + 8], 'big')
166168
established = dt.datetime.fromtimestamp(established_ms / 1000.0, tz=timezone.utc)
167169

168-
if not _token_has_valid_lifetime(keys, client_type, established, expires, now):
170+
if not _token_has_valid_lifetime(keys, client_type, now, expires, now):
169171
return DecryptedToken(DecryptionStatus.INVALID_TOKEN_LIFETIME, id_str, established, site_id, site_key.site_id,
170172
keys.get_identity_scope(), None, AdvertisingTokenVersion.ADVERTISING_TOKEN_V2, False, expires)
171173

@@ -199,7 +201,7 @@ def _decrypt_token_v3(token_bytes, keys, domain_name, client_type, now, token_ve
199201
return DecryptedToken(DecryptionStatus.EXPIRED_TOKEN, None, None, None, None,
200202
keys.get_identity_scope(), identity_type, token_version, None, expires)
201203

202-
# created 8:16
204+
generated_ms = int.from_bytes(master_payload[8:16], 'big') # Token Generated
203205
# operator site id 16:20
204206
# operator type 20
205207
# operator version 21:25
@@ -219,6 +221,10 @@ def _decrypt_token_v3(token_bytes, keys, domain_name, client_type, now, token_ve
219221
# privacy bits 16:20
220222
privacy_bits = bitarray()
221223
privacy_bits.frombytes(site_payload[16:20])
224+
established_ms = int.from_bytes(site_payload[20:28], 'big')
225+
id_bytes = site_payload[36:]
226+
id_str = base64.b64encode(id_bytes).decode('ascii')
227+
222228
is_client_side_generated = False
223229
if privacy_bits[1]:
224230
is_client_side_generated = True
@@ -227,17 +233,13 @@ def _decrypt_token_v3(token_bytes, keys, domain_name, client_type, now, token_ve
227233
return DecryptedToken(DecryptionStatus.DOMAIN_NAME_CHECK_FAILED, None, None, site_id, site_key.site_id,
228234
keys.get_identity_scope(), identity_type, token_version, is_client_side_generated, expires)
229235

230-
established_ms = int.from_bytes(site_payload[20:28], 'big')
231236
established = dt.datetime.fromtimestamp(established_ms / 1000.0, tz=timezone.utc)
232-
# refreshed_ms 28:36
237+
generated = dt.datetime.fromtimestamp(generated_ms / 1000.0, tz=timezone.utc)
233238

234-
if not _token_has_valid_lifetime(keys, client_type, established, expires, now):
239+
if not _token_has_valid_lifetime(keys, client_type, generated, expires, now):
235240
return DecryptedToken(DecryptionStatus.INVALID_TOKEN_LIFETIME, None, established, site_id, site_key.site_id,
236241
keys.get_identity_scope(), identity_type, token_version, is_client_side_generated, expires)
237242

238-
id_bytes = site_payload[36:]
239-
id_str = base64.b64encode(id_bytes).decode('ascii')
240-
241243
return DecryptedToken(DecryptionStatus.SUCCESS, id_str, established, site_id, site_key.site_id,
242244
keys.get_identity_scope(), identity_type, token_version, is_client_side_generated, expires)
243245

@@ -286,7 +288,7 @@ def encrypt(uid2, identity_scope, keys, keyset_id=None, **kwargs):
286288
if identity_scope is None:
287289
identity_scope = keys.get_identity_scope()
288290
try:
289-
params = Params(expiry=token_expiry, identity_scope=identity_scope, token_generated_at=now)
291+
params = Params(expiry=token_expiry, identity_scope=identity_scope, token_generated=now)
290292
return EncryptionDataResponse.make_success(UID2TokenGenerator.generate_uid2_token_v4(uid2, master_key, site_id, key, params))
291293
except Exception:
292294
return EncryptionDataResponse.make_error(EncryptionStatus.ENCRYPTION_FAILURE)

uid2_client/uid2_token_generator.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,12 @@ def _encrypt_data_v1(data, key, iv):
5151

5252
class Params:
5353
def __init__(self, expiry=dt.datetime.now(tz=timezone.utc) + dt.timedelta(hours=1),
54-
identity_scope=IdentityScope.UID2.value, token_generated_at=dt.datetime.now(tz=timezone.utc)):
55-
self.identity_scope = identity_scope
54+
identity_scope=IdentityScope.UID2.value, token_generated=dt.datetime.now(tz=timezone.utc),
55+
identity_established=dt.datetime.now(tz=timezone.utc)):
5656
self.token_expiry = expiry
57-
self.token_generated_at = token_generated_at
57+
self.identity_scope = identity_scope
58+
self.token_generated = token_generated
59+
self.identity_established = identity_established
5860
if not isinstance(expiry, dt.datetime):
5961
self.token_expiry = dt.datetime.now(tz=timezone.utc) + expiry
6062

@@ -66,15 +68,14 @@ def default_params():
6668
class UID2TokenGenerator:
6769

6870
@staticmethod
69-
def generate_uid2_token_v2(id_str, master_key, site_id, site_key, params = default_params(), version=2):
71+
def generate_uid2_token_v2(id_str, master_key, site_id, site_key, params=default_params(), version=2):
7072
id = bytes(id_str, 'utf-8')
7173
identity = int.to_bytes(site_id, 4, 'big')
7274
identity += int.to_bytes(len(id), 4, 'big')
7375
identity += id
7476
# old privacy_bits
7577
identity += int.to_bytes(0, 4, 'big')
76-
created = params.token_generated_at
77-
identity += int.to_bytes(int(created.timestamp()) * 1000, 8, 'big')
78+
identity += int.to_bytes(int(params.identity_established.timestamp()) * 1000, 8, 'big')
7879
identity_iv = bytes([10, 11, 12, 13, 14, 15, 16, 1, 2, 3, 4, 5, 6, 7, 8, 9])
7980
expiry = params.token_expiry
8081
master_payload = int.to_bytes(int(expiry.timestamp()) * 1000, 8, 'big')
@@ -98,11 +99,13 @@ def generate_uid2_token_v4(id_str, master_key, site_id, site_key, params=default
9899

99100
@staticmethod
100101
def generate_uid_token(id_str, master_key, site_id, site_key, identity_scope, token_version,
101-
created_at=None, expires_at=None):
102+
identity_established_at=None, generated_at=None, expires_at=None):
102103
params = default_params()
103104
params.identity_scope = identity_scope
104-
if created_at is not None:
105-
params.token_generated_at = created_at
105+
if identity_established_at is not None:
106+
params.identity_established = identity_established_at
107+
if generated_at is not None:
108+
params.token_generated = generated_at
106109
if expires_at is not None:
107110
params.token_expiry = expires_at
108111
if token_version == AdvertisingTokenVersion.ADVERTISING_TOKEN_V2:
@@ -124,13 +127,13 @@ def generate_uid2_token_with_debug_info(id_str, master_key, site_id, site_key, p
124127

125128
# User Identity Data
126129
site_payload += int.to_bytes(0, length=4, byteorder='big') # privacy bits
127-
generated_at_timestamp = int(params.token_generated_at.timestamp()) * 1000
128-
site_payload += int.to_bytes(generated_at_timestamp, length=8, byteorder='big') # established
130+
site_payload += int.to_bytes(int(params.identity_established.timestamp()) * 1000, length=8, byteorder='big') # established
131+
generated_at_timestamp = int(params.token_generated.timestamp()) * 1000
129132
site_payload += int.to_bytes(generated_at_timestamp, length=8, byteorder='big') # last refreshed/generated
130133
site_payload += base64.b64decode(id_str)
131134

132135
master_payload = int.to_bytes(int(params.token_expiry.timestamp()) * 1000, length=8, byteorder='big') # expiry
133-
master_payload += int.to_bytes(generated_at_timestamp, length=8, byteorder='big') # created
136+
master_payload += int.to_bytes(generated_at_timestamp, length=8, byteorder='big') # generated
134137

135138
# Operator Identity Data
136139
master_payload += int.to_bytes(0, length=4, byteorder='big') # site id

0 commit comments

Comments
 (0)