Skip to content

Commit eb1888e

Browse files
committed
Working OpenPAYGO Token implementation
1 parent ad8b00b commit eb1888e

8 files changed

Lines changed: 202 additions & 46 deletions

File tree

README.md

Lines changed: 86 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,30 +33,29 @@ You can install the library by running `pip install openpaygo` or adding `openpa
3333

3434
You can use the `generate_token()` function to generate an OpenPAYGOToken Token. The function takes the following parameters, and they should match the configuration in the hardware of the device:
3535

36-
- `secret_key` (required): The secret key of the device
37-
- `value` (required): The value to be passed in the token (typically the number of days of activation)
38-
- `count` (required): The token count used to make the last token.
39-
- `set_mode` (optional): If set to `true`, the generated token will be a Set Time token, otherwise it is Add Time token.
36+
- `secret_key` (required): The secret key of the device. Must be passed as an hexadecimal string (with 32 characters).
37+
- `count` (required): The token count used to make the last token.
38+
- `value` (optional): The value to be passed in the token (typically the number of days of activation). Optional if the `token_type` is Disable PAYG or Counter Sync. The value must be between 0 and 995.
39+
- `token_type` (optional): Used to set the type of token (default is Add Time). Token types can be found in the `TokenType` class: ADD_TIME, SET_TIME, DISABLE_PAYG, COUNTER_SYNC.
4040
- `starting_code` (optional): If not provided, it is generated according to the method defined in the standard (SipHash-2-4 of the key, transformed to digit by the same method as the token generation).
4141
- `value_divider` (optional): The dividing factor used for the value.
4242
- `restricted_digit_set` (optional): If set to `true`, the the restricted digit set will be used (only digits from 1 to 4).
4343
- `extended_token` (optional): If set to `true` then a larger token will be generated, able to contain values up to 999999. This is for special use cases of each device, such as settings change, and is not set in the standard.
4444

45-
46-
The function returns the `token` as a string as well as the `updated_count` as a number.
45+
The function returns the `updated_count` as a number as well as the `token` as a string, in that order. The function will raise a `ValueError` if the key is in the wrong format or the value invalid.
4746

4847

49-
**Example:**
48+
**Example 1 - Add 7 days:**
5049

5150
```
52-
from openpaygo.token import generate_token
51+
from openpaygo.optoken import generate_token
5352
from myexampleproject import device_getter
5453
5554
# We get a device with the parameters we need from our database, this will be specific to your project
5655
device = device_getter(serial=1234)
5756
5857
# We get the new token and update the count
59-
new_token, device.count = generate_token(
58+
device.count, new_token = generate_token(
6059
secret_key=device.secret_key,
6160
value=7,
6261
count=device.count
@@ -66,4 +65,81 @@ print('Token: '+new_token)
6665
device.save() # We save the new count that we set for the device
6766
```
6867

69-
## Getting Started - Decoding a Token
68+
**Example 2 - Disable PAYG (unlock forever):**
69+
70+
```
71+
from openpaygo.optoken import generate_token, TokenType
72+
73+
...
74+
75+
# We get the new token and update the count
76+
device.count, new_token = generate_token(
77+
secret_key=device.secret_key,
78+
token_type=TokenType.DISABLE_PAYG,
79+
count=device.count
80+
)
81+
82+
print('Token: '+new_token)
83+
device.save() # We save the new count that we set for the device
84+
```
85+
86+
87+
## Getting Started - Decoding a Token
88+
89+
You can use the `decode_token()` function to generate an OpenPAYGOToken Token. The function takes the following parameters, and they should match the configuration in the hardware of the device:
90+
91+
- `token` (required): The token that was given by the user
92+
- `secret_key` (required): The secret key of the device
93+
- `count` (required): The token count of the last valid token. When a device is new, this is 1.
94+
- `used_counts` (optional): An array of recently used token counts, as returned by the function itself after the last valid token was decoded. This allows for handling unordered token entry.
95+
- `starting_code` (optional): If not provided, it is generated according to the method defined in the standard (SipHash-2-4 of the key, transformed to digit by the same method as the token generation).
96+
- `value_divider` (optional): The dividing factor used for the value.
97+
- `restricted_digit_set` (optional): If set to `true`, the the restricted digit set will be used (only digits from 1 to 4).
98+
99+
100+
The function returns the following variable in this order:
101+
- `value`: The value associated with the token (if the token is ADD_TIME or SET_TIME).
102+
- `token_type`: The type of the token that was provided. Token types can be found in the `TokenType` class: ADD_TIME, SET_TIME, DISABLE_PAYG, COUNTER_SYNC or ALREADY_USED (if the token is valid but already used), INVALID (if the token was invalid).
103+
- `updated_count`: The token count of the token, if it was valid.
104+
- `updated_used_counts`: The updated array of recently used token, if the token was valid.
105+
106+
The function will raise a `ValueError` if the key is in the wrong format, but will not raise an error if the token is invalid (as it is a common expected behaviour), to check the validity of the token you must check the return `token_type` and proceed accordingly depending on the type of token.
107+
108+
109+
**Example:**
110+
111+
```
112+
from openpaygo.optoken import decode_token
113+
114+
# We assume the users enters a token and that the device state is saved in my_device_state
115+
...
116+
117+
# We decode the token
118+
value, token_type, updated_count, updated_used_counts = decode_token(
119+
token=token_input,
120+
secret_key=my_device_state.secret_key,
121+
count=my_device_state.count,
122+
used_counts=my_device_state.used_counts
123+
)
124+
125+
# If the token is valid, we update our count in the device state
126+
if token_type not in [TokenType.ALREADY_USED, TokenType.INVALID]:
127+
my_device_state.count = updated_count
128+
my_device_state.used_counts = updated_used_counts
129+
130+
# We perform the appropriate behaviour based on the token data
131+
if token_type == TokenType.ADD_TIME:
132+
my_device_state.days_remaining += value
133+
print(f'Added {value} days remaining')
134+
elif token_type == TokenType.SET_TIME:
135+
my_device_state.days_remaining = value
136+
print(f'Set to {value} days remaining')
137+
elif token_type == TokenType.DISABLE_PAYG:
138+
my_device_state.unlocked_forever = True
139+
elif token_type == TokenType.COUNTER_SYNC:
140+
print('Counter Synced')
141+
if token_type == TokenType.ALREADY_USED:
142+
print('Token was already used')
143+
elif token_type == TokenType.INVALID:
144+
print('Token is invalid')
145+
```

optoken/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from .encode_token import OpenPAYGOTokenEncoder
2+
from .shared import TokenType
3+
4+
5+
def generate_token(**kwargs):
6+
return OpenPAYGOTokenEncoder.generate_token(**kwargs)
Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from shared import OpenPAYGOTokenShared
1+
from shared import OpenPAYGOTokenShared, TokenType
22
from shared_extended import OpenPAYGOTokenSharedExtended
33

44

@@ -7,6 +7,34 @@ class OpenPAYGOTokenDecoder(object):
77
MAX_TOKEN_JUMP_COUNTER_SYNC = 100
88
MAX_UNUSED_OLDER_TOKENS = 8*2
99

10+
@classmethod
11+
def decode_token(cls, token, secret_key, last_count, used_counts=None, starting_code=None, value_divider=1, restricted_digit_set=False):
12+
secret_key = OpenPAYGOTokenShared.load_secret_key_from_hex(secret_key)
13+
if not starting_code:
14+
# We generate the starting code from the key if not provided
15+
starting_code = OpenPAYGOTokenShared.generate_starting_code(secret_key)
16+
if not restricted_digit_set:
17+
if len(token) <= 9:
18+
extended_token = False
19+
elif len(token) <= 12:
20+
extended_token = True
21+
else:
22+
raise ValueError("Token is too long")
23+
elif restricted_digit_set:
24+
if len(token) <= 15:
25+
extended_token = False
26+
elif len(token) <= 20:
27+
extended_token = True
28+
else:
29+
raise ValueError("Token is too long")
30+
if not extended_token:
31+
value, token_type, count, updated_counts = cls.get_activation_value_count_and_type_from_token(token, starting_code, secret_key, last_count, restricted_digit_set, used_counts)
32+
else:
33+
value, token_type, count, updated_counts = cls.get_activation_value_count_from_extended_token(token, starting_code, secret_key, last_count, restricted_digit_set, used_counts)
34+
if value and value_divider:
35+
value = value / value_divider
36+
return value, token_type, count, updated_counts
37+
1038
@classmethod
1139
def get_activation_value_count_and_type_from_token(cls, token, starting_code, key, last_count,
1240
restricted_digit_set=False, used_counts=None):
@@ -26,40 +54,48 @@ def get_activation_value_count_and_type_from_token(cls, token, starting_code, ke
2654
for count in range(0, max_count_try):
2755
masked_token = OpenPAYGOTokenShared.put_base_in_token(current_code, token_base)
2856
if count % 2:
29-
this_type = OpenPAYGOTokenShared.TOKEN_TYPE_SET_TIME
57+
if value == OpenPAYGOTokenShared.COUNTER_SYNC_VALUE:
58+
this_type = TokenType.COUNTER_SYNC
59+
elif value == OpenPAYGOTokenShared.PAYG_DISABLE_VALUE:
60+
this_type = TokenType.DISABLE_PAYG
61+
else:
62+
this_type = TokenType.SET_TIME
3063
else:
31-
this_type = OpenPAYGOTokenShared.TOKEN_TYPE_ADD_TIME
64+
this_type = TokenType.ADD_TIME
3265
if masked_token == token:
3366
if cls._count_is_valid(count, last_count, value, this_type, used_counts):
34-
return value, count, this_type
67+
updated_counts = cls.update_used_counts(used_counts, value, count, this_type)
68+
return value, this_type, count, updated_counts
3569
else:
3670
valid_older_token = True
3771
current_code = OpenPAYGOTokenShared.generate_next_token(current_code, key) # If not we go to the next token
3872
if valid_older_token:
39-
return -2, None, None
40-
return None, None, None
73+
return None, TokenType.ALREADY_USED, None, None
74+
return None, TokenType.INVALID, None, None
4175

4276
@classmethod
4377
def _count_is_valid(cls, count, last_count, value, type, used_counts):
4478
if value == OpenPAYGOTokenShared.COUNTER_SYNC_VALUE:
45-
if count > last_count - 30:
79+
if count > (last_count - cls.MAX_TOKEN_JUMP):
4680
return True
4781
elif count > last_count:
4882
return True
4983
elif cls.MAX_UNUSED_OLDER_TOKENS > 0:
5084
if count > last_count - cls.MAX_UNUSED_OLDER_TOKENS:
51-
if count not in used_counts and type == OpenPAYGOTokenShared.TOKEN_TYPE_ADD_TIME:
85+
if count not in used_counts and type == TokenType.ADD_TIME:
5286
return True
5387
return False
5488

5589
@classmethod
5690
def update_used_counts(cls, past_used_counts, value, new_count, type):
91+
if not past_used_counts:
92+
return None
5793
highest_count = max(past_used_counts) if past_used_counts else 0
5894
if new_count > highest_count:
5995
highest_count = new_count
6096
bottom_range = highest_count-cls.MAX_UNUSED_OLDER_TOKENS
6197
used_counts = []
62-
if type != OpenPAYGOTokenShared.TOKEN_TYPE_ADD_TIME or value == OpenPAYGOTokenShared.COUNTER_SYNC_VALUE or value == OpenPAYGOTokenShared.PAYG_DISABLE_VALUE:
98+
if type != TokenType.ADD_TIME or value == OpenPAYGOTokenShared.COUNTER_SYNC_VALUE or value == OpenPAYGOTokenShared.PAYG_DISABLE_VALUE:
6399
# If it is not an Add-Time token, we mark all the past tokens as used in the range
64100
for count in range(bottom_range, highest_count+1):
65101
used_counts.append(count)
@@ -80,20 +116,30 @@ def _decode_base(cls, starting_code_base, token_base):
80116

81117
@classmethod
82118
def get_activation_value_count_from_extended_token(cls, token, starting_code, key, last_count,
83-
restricted_digit_set=False):
119+
restricted_digit_set=False, used_counts=None):
84120
if restricted_digit_set:
85121
token = OpenPAYGOTokenSharedExtended.convert_from_4_digit_token(token)
86122
token_base = OpenPAYGOTokenSharedExtended.get_token_base(token) # We get the base of the token
87123
current_code = OpenPAYGOTokenSharedExtended.put_base_in_token(starting_code, token_base) # We put it into the starting code
88124
starting_code_base = OpenPAYGOTokenSharedExtended.get_token_base(starting_code) # We get the base of the starting code
89125
value = cls._decode_base_extended(starting_code_base, token_base) # If there is a match we get the value from the token
90-
for count in range(0, 30):
126+
max_count_try = last_count + cls.MAX_TOKEN_JUMP + 1
127+
for count in range(0, max_count_try):
91128
masked_token = OpenPAYGOTokenSharedExtended.put_base_in_token(current_code, token_base)
92-
if masked_token == token and count > last_count:
93-
clean_count = count-1
94-
return value, clean_count
129+
if count % 2:
130+
this_type = TokenType.SET_TIME
131+
else:
132+
this_type = TokenType.ADD_TIME
133+
if masked_token == token:
134+
if cls._count_is_valid(count, last_count, value, this_type, used_counts):
135+
updated_counts = cls.update_used_counts(used_counts, value, count, this_type)
136+
return value, this_type, count, updated_counts
137+
else:
138+
valid_older_token = True
95139
current_code = OpenPAYGOTokenSharedExtended.generate_next_token(current_code, key) # If not we go to the next token
96-
return None, None, None
140+
if valid_older_token:
141+
return None, TokenType.ALREADY_USED, None, None
142+
return None, TokenType.INVALID, None, None
97143

98144
@classmethod
99145
def _decode_base_extended(cls, starting_code_base, token_base):
Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,40 @@
1-
from shared import OpenPAYGOTokenShared
1+
from shared import OpenPAYGOTokenShared, TokenType
22
from shared_extended import OpenPAYGOTokenSharedExtended
33

44

55
class OpenPAYGOTokenEncoder(object):
66

77
@classmethod
8-
def generate_token(cls, secret_key, value, count, set_mode=False, starting_code=None, value_divider=1, restricted_digit_set=False, extended_token=False):
8+
def generate_token(cls, secret_key, count, value=None, token_type=TokenType.ADD_TIME, starting_code=None, value_divider=1, restricted_digit_set=False, extended_token=False):
9+
secret_key = OpenPAYGOTokenShared.load_secret_key_from_hex(secret_key)
910
if not starting_code:
1011
# We generate the starting code from the key if not provided
11-
starting_code = cls.generate_starting_code(secret_key)
12-
if set_mode:
13-
mode = OpenPAYGOTokenShared.TOKEN_TYPE_SET_TIME
12+
starting_code = OpenPAYGOTokenShared.generate_starting_code(secret_key)
13+
if token_type in [TokenType.ADD_TIME, TokenType.SET_TIME]:
14+
value = int(round(value * value_divider, 0))
15+
if not extended_token:
16+
max_value = OpenPAYGOTokenShared.MAX_ACTIVATION_VALUE
17+
else:
18+
max_value = OpenPAYGOTokenSharedExtended.MAX_ACTIVATION_VALUE
19+
if value > max_value:
20+
raise ValueError('The value provided is too high.')
21+
elif value:
22+
raise ValueError('A value is not allowed for this token type.')
1423
else:
15-
mode = OpenPAYGOTokenShared.TOKEN_TYPE_ADD_TIME
16-
value = int(round(value / value_divider))
24+
if token_type == TokenType.DISABLE_PAYG:
25+
value = OpenPAYGOTokenShared.PAYG_DISABLE_VALUE
26+
elif token_type == TokenType.COUNTER_SYNC:
27+
value = OpenPAYGOTokenShared.COUNTER_SYNC_VALUE
28+
else:
29+
raise ValueError('The token type provided is not supported.')
1730
if extended_token:
18-
return cls.generate_extended_token(starting_code, secret_key, value, count, mode, restricted_digit_set)
31+
return cls.generate_extended_token(starting_code, secret_key, value, count, token_type, restricted_digit_set)
1932
else:
20-
return cls.generate_standard_token(starting_code, secret_key, value, count, mode, restricted_digit_set)
33+
return cls.generate_standard_token(starting_code, secret_key, value, count, token_type, restricted_digit_set)
2134

2235
@classmethod
2336
def generate_standard_token(cls, starting_code=None, key=None, value=None, count=None,
24-
mode=OpenPAYGOTokenShared.TOKEN_TYPE_SET_TIME, restricted_digit_set=False):
37+
mode=TokenType.ADD_TIME, restricted_digit_set=False):
2538
# We get the first 3 digits with encoded value
2639
starting_code_base = OpenPAYGOTokenShared.get_token_base(starting_code)
2740
token_base = cls._encode_base(starting_code_base, value)
@@ -45,7 +58,7 @@ def _encode_base(cls, base, number):
4558
return number + base
4659

4760
@classmethod
48-
def generate_extended_token(cls, starting_code, key, value, count, mode=OpenPAYGOTokenShared.TOKEN_TYPE_SET_TIME, restricted_digit_set=False):
61+
def generate_extended_token(cls, starting_code, key, value, count, mode=TokenType.ADD_TIME, restricted_digit_set=False):
4962
starting_code_base = OpenPAYGOTokenSharedExtended.get_token_base(starting_code)
5063
token_base = cls._encode_base_extended(starting_code_base, value)
5164
current_token = OpenPAYGOTokenSharedExtended.put_base_in_token(starting_code, token_base)
@@ -59,12 +72,6 @@ def generate_extended_token(cls, starting_code, key, value, count, mode=OpenPAYG
5972
else:
6073
final_token = '{:012d}'.format(final_token)
6174
return new_count, final_token
62-
63-
@classmethod
64-
def generate_starting_code(cls, key):
65-
# We make a hash of the key
66-
starting_hash = OpenPAYGOTokenShared.generate_hash(key, key)
67-
return OpenPAYGOTokenShared.convert_hash_to_token(starting_hash)
6875

6976
@classmethod
7077
def _encode_base_extended(cls, base, number):
@@ -76,8 +83,8 @@ def _encode_base_extended(cls, base, number):
7683
@classmethod
7784
def _get_new_count(cls, count, mode):
7885
current_count_odd = count % 2
79-
if mode == OpenPAYGOTokenShared.TOKEN_TYPE_SET_TIME:
80-
if current_count_odd: # Odd numbers are for Set Time
86+
if mode in [TokenType.SET_TIME, TokenType.DISABLE_PAYG, TokenType.COUNTER_SYNC]:
87+
if current_count_odd: # Odd numbers are for Set Time, Disable PAYG or Counter Sync
8188
new_count = count+2
8289
else:
8390
new_count = count+1

token/shared.py renamed to optoken/shared.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import siphash
22
import struct
3+
import codecs
4+
5+
6+
class TokenType(object):
7+
ADD_TIME = 1
8+
SET_TIME = 2
9+
DISABLE_PAYG = 3
10+
COUNTER_SYNC = 4
11+
INVALID = 10
12+
ALREADY_USED = 11
313

414

515
class OpenPAYGOTokenShared(object):
@@ -8,8 +18,6 @@ class OpenPAYGOTokenShared(object):
818
PAYG_DISABLE_VALUE = 998
919
COUNTER_SYNC_VALUE = 999
1020
TOKEN_VALUE_OFFSET = 1000
11-
TOKEN_TYPE_SET_TIME = 1
12-
TOKEN_TYPE_ADD_TIME = 2
1321

1422
@classmethod
1523
def get_token_base(cls, code):
@@ -37,6 +45,19 @@ def convert_hash_to_token(cls, this_hash):
3745
result_hash = hi_hash ^ lo_hash # We XOR the two together to get a single 32bits INT
3846
token = cls._convert_to_29_5_bits(result_hash) # We convert the 32bits value to an INT no greater than 9 digits
3947
return token
48+
49+
@classmethod
50+
def generate_starting_code(cls, key):
51+
# We make a hash of the key
52+
starting_hash = OpenPAYGOTokenShared.generate_hash(key, key)
53+
return OpenPAYGOTokenShared.convert_hash_to_token(starting_hash)
54+
55+
@classmethod
56+
def load_secret_key_from_hex(cls, secret_key):
57+
try:
58+
return codecs.decode(secret_key, 'hex')
59+
except Exception as e:
60+
raise ValueError('The secret key provided is not correctly formatted, it should be 32 hexadecimal characters. ')
4061

4162
@classmethod
4263
def _convert_to_29_5_bits(cls, source):

token/__init__.py

Whitespace-only changes.

token/legacy.py

Whitespace-only changes.

0 commit comments

Comments
 (0)