Skip to content

Commit 7c97dde

Browse files
Finished token_v3 encryption
1 parent 9a2985b commit 7c97dde

File tree

6 files changed

+147
-18
lines changed

6 files changed

+147
-18
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
*.swp
2+
.idea/*
3+
*__pycache__*
24
dist/*
35
*egg-info/*

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ classifiers = [
1919
"Operating System :: OS Independent",
2020
]
2121
dependencies = [
22-
"pycrypto"
22+
"pycryptodome"
2323
]
2424

2525
[project.urls]

tests/test_encryption.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import unittest
55

66
from tests.uid2_token_generator import UID2TokenGenerator, Params
7-
from uid2_client import decrypt_token, encrypt_data, decrypt_data, encryption_block_size, EncryptionError, Uid2Base64UrlCoder, AdvertisingTokenVersion
7+
from uid2_client import *
88
from uid2_client.identity_scope import IdentityScope
99
from uid2_client.identity_type import IdentityType
1010
from uid2_client.keys import *
@@ -19,8 +19,9 @@
1919

2020
_example_id = 'ywsvDNINiZOVSsfkHpLpSJzXzhr6Jx9Z/4Q0+lsEUvM='
2121
_now = dt.datetime.now(tz=timezone.utc)
22-
_master_key = EncryptionKey(_master_key_id, -1, _now - dt.timedelta(days=-1), _now, _now + dt.timedelta(days=1), _master_secret)
22+
_master_key = EncryptionKey(_master_key_id, -1, _now - dt.timedelta(days=-1), _now, _now + dt.timedelta(days=1), _master_secret, keyset_id=9999)
2323
_site_key = EncryptionKey(_site_key_id, _site_id, _now - dt.timedelta(days=-1), _now, _now + dt.timedelta(days=1), _site_secret)
24+
_keyset_key = EncryptionKey(_site_key_id, _site_id, _now - dt.timedelta(days=-1), _now, _now + dt.timedelta(days=1), _site_secret, keyset_id=20)
2425
_test_site_key = EncryptionKey(_test_site_key_id, _site_id, dt.datetime(2020, 1, 1, tzinfo=timezone.utc), dt.datetime(2020, 1, 1, tzinfo=timezone.utc), _now + dt.timedelta(days=1), encryption_block_size * b'9')
2526

2627
class TestEncryptionFunctions(unittest.TestCase):
@@ -317,6 +318,20 @@ def test_decrypt_token_v2_custom_now(self):
317318
self.assertEqual(_example_id, result.uid2)
318319

319320

321+
def test_encrypt_token_v3(self):
322+
uid2 = "Y29keWlzZ3JlYXQ="
323+
identity_scope = IdentityScope.UID2
324+
now = dt.datetime.now(tz=timezone.utc)
325+
326+
keys = EncryptionKeysCollection([_master_key, _site_key, _keyset_key], default_keyset_id=20,
327+
master_keyset_id=9999, caller_site_id=20)
328+
329+
result = encrypt_key(uid2, identity_scope, keys, now=now)
330+
print(result)
331+
final = decrypt_token(result, keys, now=now)
332+
333+
self.assertEqual(uid2, final.uid2)
334+
320335
def test_decrypt_token_v2_invalid_payload(self):
321336
params = Params(dt.datetime.now(tz=timezone.utc) + dt.timedelta(seconds=-1))
322337
token = UID2TokenGenerator.generate_uid2_token_v2(_example_id, _master_key, _site_id, _site_key, params)

uid2_client/client.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
>>> from uid2_client import Uid2Client
66
"""
77

8-
98
import base64
109
import datetime as dt
1110
from datetime import timezone
@@ -53,7 +52,6 @@ def __init__(self, base_url, auth_key, secret_key):
5352
self._auth_key = auth_key
5453
self._secret_key = base64.b64decode(secret_key)
5554

56-
5755
def refresh_keys(self):
5856
"""Get the latest encryption keys for advertising tokens.
5957
@@ -66,20 +64,21 @@ def refresh_keys(self):
6664
"""
6765
req, nonce = self._make_v2_request(dt.datetime.now(tz=timezone.utc))
6866
print(req)
69-
resp = self._post('/v2/key/latest', headers=self._auth_headers(), data=req)
70-
keys = [EncryptionKey(k['id'], k.get('site_id', -1), _make_dt(k['created']), _make_dt(k['activates']), _make_dt(k['expires']), base64.b64decode(k['secret']))
71-
for k in json.loads(self._parse_v2_response(resp.read(), nonce)).get('body')]
72-
return EncryptionKeysCollection(keys)
67+
resp = self._post('/v2/key/sharing', headers=self._auth_headers(), data=req)
68+
resp_body = json.loads(self._parse_v2_response(resp.read(), nonce)).get('body')
69+
keys = [EncryptionKey(k['id'], k.get('site_id', -1), _make_dt(k['created']), _make_dt(k['activates']),
70+
_make_dt(k['expires']), base64.b64decode(k['secret']), k['keyset_id'])
71+
for k in resp_body["keys"]]
7372

73+
return EncryptionKeysCollection(keys, resp_body["caller_site_id"], resp_body["master_keyset_id"],
74+
resp_body["default_keyset_id"], resp_body["token_expiry_seconds"])
7475

7576
def _make_url(self, path):
7677
return self._base_url + path
7778

78-
7979
def _auth_headers(self):
8080
return {'Authorization': 'Bearer ' + self._auth_key}
8181

82-
8382
def _make_v2_request(self, now):
8483
payload = int.to_bytes(int(now.timestamp() * 1000), 8, 'big')
8584
nonce = os.urandom(8)
@@ -90,14 +89,12 @@ def _make_v2_request(self, now):
9089

9190
return base64.b64encode(envelope), nonce
9291

93-
9492
def _parse_v2_response(self, encrypted, nonce):
9593
payload = _decrypt_gcm(base64.b64decode(encrypted), self._secret_key)
9694
if nonce != payload[8:16]:
9795
raise ValueError("nonce mismatch")
9896
return payload[16:]
9997

100-
10198
def _post(self, path, headers, data):
10299
req = request.Request(self._make_url(path), headers=headers, method='POST', data=data)
103100
return request.urlopen(req)

uid2_client/encryption.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,71 @@ def _decrypt_token_v3(token_bytes, keys, now):
146146

147147
return DecryptedToken(id_str, established, site_id, site_key.site_id)
148148

149+
def _encrypt_token_v3(uid2, identity_scope, master_key, site_key, site_id, now, token_expiry):
150+
site_payload = bytearray(128)
151+
#Site id
152+
site_payload[0:4] = int.to_bytes(site_id, byteorder='big', length=4)
153+
site_payload[4:12] = int.to_bytes(0, byteorder='big', length=8)
154+
site_payload[12:16] = int.to_bytes(0, byteorder='big', length=4)
155+
site_payload[16:20] = int.to_bytes(0, byteorder='big', length=4)
156+
site_payload[20:28] = int.to_bytes(int(now.timestamp())*1000, byteorder='big', length=8)
157+
site_payload[36:] = bytes(base64.b64decode(uid2))
158+
159+
id_payload = _encrypt_gcm(bytes(site_payload), None, site_key.secret)
160+
161+
master_payload = bytearray(256)
162+
master_payload[:8] = int.to_bytes(int(token_expiry.timestamp())*1000, byteorder='big', length=8)
163+
master_payload[8:16] = int.to_bytes(int(now.timestamp()), byteorder='big', length=8)
164+
master_payload[16:20] = int.to_bytes(0, byteorder='big', length=4)
165+
master_payload[20:21] = int.to_bytes(1, byteorder='big', length=1)
166+
master_payload[21:25] = int.to_bytes(0, byteorder='big', length=4)
167+
master_payload[25:29] = int.to_bytes(0, byteorder='big', length=4)
168+
master_payload[29:33] = int.to_bytes(site_key.key_id, byteorder='big', length=4)
169+
master_payload[33:] = bytes(id_payload)
170+
171+
encrypted_master_payload = _encrypt_gcm(bytes(master_payload), None, master_key.secret)
172+
173+
root_writer = bytearray(len(encrypted_master_payload)+6)
174+
root_writer[0:1] = int.to_bytes(0, byteorder='big', length=1)
175+
root_writer[1:2] = int.to_bytes(112, byteorder='big', length=1)
176+
root_writer[2:6] = int.to_bytes(master_key.key_id, byteorder='big', length=4)
177+
root_writer[6:] = bytes(encrypted_master_payload)
178+
179+
return base64.b64encode(root_writer)
180+
181+
182+
183+
def encrypt_key(uid2, indentity_scope, keys, keyset_id=None, **kwargs):
184+
""" Encrypt an uid2 into a sharing token
185+
186+
Args:
187+
uid2: the uid2 to be encrypted
188+
keys (EncryptionKeysCollection): collection of keys to choose from for encryption
189+
keyset_id (int) : An optional keyset id to use for the encryption. Will use default keyset if left blank
190+
191+
Returns (str): Sharing Token
192+
193+
"""
194+
now = kwargs.get("now")
195+
if now is None:
196+
now = dt.datetime.now(tz=timezone.utc)
197+
key = keys.get_default_keyset_key(now) if keyset_id is None else keys.get_by_keyset_key(keyset_id, now)
198+
master_key = keys.get_by_keyset_key(9999, now)
199+
200+
token_expiry = now + dt.timedelta(days=30) if keys.get_token_expiry_seconds() is None \
201+
else now + dt.timedelta(seconds=keys.get_token_expiry_seconds())
202+
203+
site_id = keys.get_caller_site_id()
204+
if site_id is None:
205+
print("No Site ID in keys")
206+
return
207+
208+
if key is None:
209+
print("No Keyset Key found")
210+
return
211+
212+
return _encrypt_token_v3(uid2, indentity_scope, master_key, key, site_id, now, token_expiry)
213+
149214

150215
def encrypt_data(data, identity_scope, **kwargs):
151216
"""Encrypt arbitrary binary data.
@@ -307,7 +372,7 @@ def _add_pkcs7_padding(data, block_size):
307372

308373

309374
def _encrypt(data, iv, key):
310-
cipher = AES.new(key.secret, AES.MODE_CBC, iv=iv)
375+
cipher = AES.new(key.secret, AES.MODE_CBC, IV=iv)
311376
return cipher.encrypt(_add_pkcs7_padding(data, AES.block_size))
312377

313378

uid2_client/keys.py

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77

88
import datetime as dt
9-
from bisect import bisect_right
9+
from bisect import bisect_right, bisect_left
1010

1111

1212
class EncryptionKey:
@@ -21,14 +21,15 @@ class EncryptionKey:
2121
secret (bytes): the actual encryption key
2222
"""
2323

24-
def __init__(self, key_id, site_id, created, activates, expires, secret):
24+
def __init__(self, key_id, site_id, created, activates, expires, secret, keyset_id=None):
2525
"""Create a new encryption key."""
2626
self._id = key_id
2727
self._site_id = site_id
2828
self._created = created
2929
self._activates = activates
3030
self._expires = expires
3131
self._secret = secret
32+
self._keyset_id = keyset_id
3233

3334

3435
@property
@@ -42,6 +43,11 @@ def site_id(self):
4243
"""int: id of the site the key belongs to."""
4344
return self._site_id
4445

46+
@property
47+
def keyset_id(self):
48+
"""int: keyset id, can be None"""
49+
return self._keyset_id
50+
4551

4652
@property
4753
def created(self):
@@ -94,20 +100,29 @@ class EncryptionKeysCollection:
94100
used for decoding UID2 advertising tokens.
95101
"""
96102

97-
def __init__(self, keys):
103+
def __init__(self, keys, caller_site_id=None, master_keyset_id=None, default_keyset_id=None, token_expiry_seconds=None):
98104
self._latest_expires = None
99105
self._keys = dict()
100106
self._keys_by_site = dict()
107+
self._keys_by_keyset = dict()
108+
self.set_keys(keys)
109+
self._caller_site_id = caller_site_id
110+
self._master_keyset_id = master_keyset_id
111+
self._default_keyset_id = default_keyset_id
112+
self._token_expiry_seconds = token_expiry_seconds
113+
114+
def set_keys(self, keys):
101115
for key in keys:
102116
self._keys[key.key_id] = key
103117
if key.site_id > 0:
104118
self._keys_by_site.setdefault(key.site_id, []).append(key)
119+
if key.keyset_id is not None:
120+
self._keys_by_keyset.setdefault(key.keyset_id, []).append(key)
105121
if self._latest_expires is None or key.expires > self._latest_expires:
106122
self._latest_expires = key.expires
107123
for _, site_keys in self._keys_by_site.items():
108124
site_keys.sort(key=lambda x: x.activates)
109125

110-
111126
def __len__(self):
112127
return len(self._keys)
113128

@@ -119,6 +134,18 @@ def __contains__(self, key_id):
119134
def __getitem__(self, key_id):
120135
return self._keys[key_id]
121136

137+
def get_caller_site_id(self):
138+
return self._caller_site_id
139+
140+
def get_master_keyset_id(self):
141+
return self._master_keyset_id
142+
143+
def get_default_keyset_id(self):
144+
return self._default_keyset_id
145+
146+
def get_token_expiry_seconds(self):
147+
return self._token_expiry_seconds
148+
122149

123150
def get(self, key_id, default=None):
124151
"""Get encryption key with the specified id, else default."""
@@ -135,6 +162,29 @@ def values(self):
135162
return self._keys.values()
136163

137164

165+
def get_default_keyset_key(self, now):
166+
return self.get_by_keyset_key(self._default_keyset_id, now)
167+
168+
def get_by_keyset_key(self, keyset_id, now):
169+
""" Gets Active Key by keyset_id
170+
171+
Args:
172+
keyset_id: the keyset id to get
173+
174+
Returns: EncryptionKey: active keyset key or None
175+
176+
"""
177+
keyset_keys = self._keys_by_keyset.get(keyset_id)
178+
if keyset_keys is None or len(keyset_keys) == 0:
179+
return None
180+
i = bisect_right(_SiteKeyActivatesList(keyset_keys), now)
181+
while i > 0:
182+
i -= 1
183+
key = keyset_keys[i]
184+
if key.is_active(now):
185+
return key
186+
return None
187+
138188
def get_active_site_key(self, site_id, now):
139189
"""Get active encryption key for the specified site, else None.
140190

0 commit comments

Comments
 (0)