Skip to content

Commit fd109f4

Browse files
authored
Merge pull request #1 from openpaygo/add-token
First version with OpenPAYGO Token support
2 parents 34991e5 + a217919 commit fd109f4

12 files changed

Lines changed: 633 additions & 3 deletions

File tree

.github/workflows/ci-cd.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# .github/workflows/ci-cd.yml
2+
name: PyPI Publish
3+
on: push
4+
jobs:
5+
pypi-publish:
6+
name: Upload release to PyPI
7+
if: github.ref == 'refs/heads/master' # On main only
8+
runs-on: ubuntu-latest
9+
environment:
10+
name: pypi
11+
url: https://pypi.org/p/openpaygo
12+
permissions:
13+
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
14+
steps:
15+
# retrieve your distributions here
16+
- name: Publish package distributions to PyPI
17+
uses: pypa/gh-action-pypi-publish@release/v1

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.pyc
2+
dist
3+
openpaygo.egg-info

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2023 OpenPAYGO
3+
Copyright (c) 2023 Eternum Ltd T/A Solaris Offgrid
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 152 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,152 @@
1-
# openpaygo-python
2-
A Python library for adding support for OpenPAYGO technologies on servers.
1+
# OpenPAYGOToken - Python Lib
2+
3+
This repository contains the Python library for using implementing the different OpenPAYGOToken Suite technologies on your server (for generating tokens and decoding openpaygo metrics payloads) or device (for decoding tokens and making openpaygo metrics payloads).
4+
5+
<p align="center">
6+
<img
7+
alt="Project Status"
8+
src="https://img.shields.io/badge/Project%20Status-beta-orange"
9+
>
10+
<a href="https://github.com/openpaygo/metrics/blob/main/LICENSE" target="_blank">
11+
<img
12+
alt="License"
13+
src="https://img.shields.io/github/license/openpaygo/openpaygo-python"
14+
>
15+
</a>
16+
</p>
17+
18+
This open-source project was sponsored by:
19+
- Solaris Offgrid
20+
- EnAccess
21+
22+
## Table of Contents
23+
24+
- [Installing the library](#installing-the-library)
25+
- [Getting Started - Generating Tokens](#getting-started---generating-tokens)
26+
- [Getting Started - Decoding a Token](#getting-started---decoding-a-token)
27+
28+
29+
## Installing the library
30+
31+
You can install the library by running `pip install openpaygo` or adding `openpaygo` in your requirements.txt file and running `pip install -r requirements.txt`.
32+
33+
## Getting Started - Generating Tokens
34+
35+
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:
36+
37+
- `secret_key` (required): The secret key of the device. Must be passed as an hexadecimal string (with 32 characters).
38+
- `count` (required): The token count used to make the last token.
39+
- `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.
40+
- `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.
41+
- `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).
42+
- `value_divider` (optional): The dividing factor used for the value.
43+
- `restricted_digit_set` (optional): If set to `true`, the the restricted digit set will be used (only digits from 1 to 4).
44+
- `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.
45+
46+
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.
47+
48+
49+
**Example 1 - Add 7 days:**
50+
51+
```
52+
from openpaygo import generate_token
53+
from myexampleproject import device_getter
54+
55+
# We get a device with the parameters we need from our database, this will be specific to your project
56+
device = device_getter(serial=1234)
57+
58+
# We get the new token and update the count
59+
device.count, new_token = generate_token(
60+
secret_key=device.secret_key,
61+
value=7,
62+
count=device.count
63+
)
64+
65+
print('Token: '+new_token)
66+
device.save() # We save the new count that we set for the device
67+
```
68+
69+
**Example 2 - Disable PAYG (unlock forever):**
70+
71+
```
72+
from openpaygo import generate_token, TokenType
73+
74+
...
75+
76+
# We get the new token and update the count
77+
device.count, new_token = generate_token(
78+
secret_key=device.secret_key,
79+
token_type=TokenType.DISABLE_PAYG,
80+
count=device.count
81+
)
82+
83+
print('Token: '+new_token)
84+
device.save() # We save the new count that we set for the device
85+
```
86+
87+
88+
## Getting Started - Decoding a Token
89+
90+
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:
91+
92+
- `token` (required): The token that was given by the user
93+
- `secret_key` (required): The secret key of the device
94+
- `count` (required): The token count of the last valid token. When a device is new, this is 1.
95+
- `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.
96+
- `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).
97+
- `value_divider` (optional): The dividing factor used for the value.
98+
- `restricted_digit_set` (optional): If set to `true`, the the restricted digit set will be used (only digits from 1 to 4).
99+
100+
101+
The function returns the following variable in this order:
102+
- `value`: The value associated with the token (if the token is ADD_TIME or SET_TIME).
103+
- `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).
104+
- `updated_count`: The token count of the token, if it was valid.
105+
- `updated_used_counts`: The updated array of recently used token, if the token was valid.
106+
107+
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.
108+
109+
110+
**Example:**
111+
112+
```
113+
from openpaygo import decode_token
114+
115+
# We assume the users enters a token and that the device state is saved in my_device_state
116+
...
117+
118+
# We decode the token
119+
value, token_type, updated_count, updated_used_counts = decode_token(
120+
token=token_input,
121+
secret_key=my_device_state.secret_key,
122+
count=my_device_state.count,
123+
used_counts=my_device_state.used_counts
124+
)
125+
126+
# If the token is valid, we update our count in the device state
127+
if token_type not in [TokenType.ALREADY_USED, TokenType.INVALID]:
128+
my_device_state.count = updated_count
129+
my_device_state.used_counts = updated_used_counts
130+
131+
# We perform the appropriate behaviour based on the token data
132+
if token_type == TokenType.ADD_TIME:
133+
my_device_state.days_remaining += value
134+
print(f'Added {value} days remaining')
135+
elif token_type == TokenType.SET_TIME:
136+
my_device_state.days_remaining = value
137+
print(f'Set to {value} days remaining')
138+
elif token_type == TokenType.DISABLE_PAYG:
139+
my_device_state.unlocked_forever = True
140+
elif token_type == TokenType.COUNTER_SYNC:
141+
print('Counter Synced')
142+
elif token_type == TokenType.ALREADY_USED:
143+
print('Token was already used')
144+
elif token_type == TokenType.INVALID:
145+
print('Token is invalid')
146+
```
147+
148+
149+
## Changelog
150+
151+
### 2023-10-03 - v0.1.2
152+
- First working version published on PyPi

openpaygo/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from .encode_token import OpenPAYGOTokenEncoder
2+
from .decode_token import OpenPAYGOTokenDecoder
3+
from .shared import TokenType
4+
5+
6+
def generate_token(**kwargs):
7+
return OpenPAYGOTokenEncoder.generate_token(**kwargs)
8+
9+
10+
def decode_token(**kwargs):
11+
return OpenPAYGOTokenDecoder.decode_token(**kwargs)

openpaygo/decode_token.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
from .shared import OpenPAYGOTokenShared, TokenType
2+
from .shared_extended import OpenPAYGOTokenSharedExtended
3+
4+
5+
class OpenPAYGOTokenDecoder(object):
6+
MAX_TOKEN_JUMP = 64
7+
MAX_TOKEN_JUMP_COUNTER_SYNC = 100
8+
MAX_UNUSED_OLDER_TOKENS = 8*2
9+
10+
@classmethod
11+
def decode_token(cls, token, secret_key, 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, 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, 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+
38+
@classmethod
39+
def get_activation_value_count_and_type_from_token(cls, token, starting_code, key, last_count,
40+
restricted_digit_set=False, used_counts=None):
41+
if restricted_digit_set:
42+
token = OpenPAYGOTokenShared.convert_from_4_digit_token(token)
43+
valid_older_token = False
44+
token_base = OpenPAYGOTokenShared.get_token_base(token) # We get the base of the token
45+
current_code = OpenPAYGOTokenShared.put_base_in_token(starting_code, token_base) # We put it into the starting code
46+
starting_code_base = OpenPAYGOTokenShared.get_token_base(starting_code) # We get the base of the starting code
47+
value = cls._decode_base(starting_code_base, token_base) # If there is a match we get the value from the token
48+
# We try all combination up until last_count + TOKEN_JUMP, or to the larger jump if syncing counter
49+
# We could start directly the loop at the last count if we kept the token value for the last count
50+
if value == OpenPAYGOTokenShared.COUNTER_SYNC_VALUE:
51+
max_count_try = last_count + cls.MAX_TOKEN_JUMP_COUNTER_SYNC + 1
52+
else:
53+
max_count_try = last_count + cls.MAX_TOKEN_JUMP + 1
54+
for count in range(0, max_count_try):
55+
masked_token = OpenPAYGOTokenShared.put_base_in_token(current_code, token_base)
56+
if count % 2:
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
63+
else:
64+
this_type = TokenType.ADD_TIME
65+
if masked_token == token:
66+
if cls._count_is_valid(count, last_count, value, this_type, used_counts):
67+
updated_counts = cls.update_used_counts(used_counts, value, count, this_type)
68+
return value, this_type, count, updated_counts
69+
else:
70+
valid_older_token = True
71+
current_code = OpenPAYGOTokenShared.generate_next_token(current_code, key) # If not we go to the next token
72+
if valid_older_token:
73+
return None, TokenType.ALREADY_USED, None, None
74+
return None, TokenType.INVALID, None, None
75+
76+
@classmethod
77+
def _count_is_valid(cls, count, last_count, value, type, used_counts):
78+
if value == OpenPAYGOTokenShared.COUNTER_SYNC_VALUE:
79+
if count > (last_count - cls.MAX_TOKEN_JUMP):
80+
return True
81+
elif count > last_count:
82+
return True
83+
elif cls.MAX_UNUSED_OLDER_TOKENS > 0:
84+
if count > last_count - cls.MAX_UNUSED_OLDER_TOKENS:
85+
if count not in used_counts and type == TokenType.ADD_TIME:
86+
return True
87+
return False
88+
89+
@classmethod
90+
def update_used_counts(cls, past_used_counts, value, new_count, type):
91+
if not past_used_counts:
92+
return None
93+
highest_count = max(past_used_counts) if past_used_counts else 0
94+
if new_count > highest_count:
95+
highest_count = new_count
96+
bottom_range = highest_count-cls.MAX_UNUSED_OLDER_TOKENS
97+
used_counts = []
98+
if type != TokenType.ADD_TIME or value == OpenPAYGOTokenShared.COUNTER_SYNC_VALUE or value == OpenPAYGOTokenShared.PAYG_DISABLE_VALUE:
99+
# If it is not an Add-Time token, we mark all the past tokens as used in the range
100+
for count in range(bottom_range, highest_count+1):
101+
used_counts.append(count)
102+
else:
103+
# If it is an Add-Time token, we just mark the tokens actually used in the range
104+
for count in range(bottom_range, highest_count+1):
105+
if count == new_count or count in past_used_counts:
106+
used_counts.append(count)
107+
return used_counts
108+
109+
@classmethod
110+
def _decode_base(cls, starting_code_base, token_base):
111+
decoded_value = token_base - starting_code_base
112+
if decoded_value < 0:
113+
return decoded_value + 1000
114+
else:
115+
return decoded_value
116+
117+
@classmethod
118+
def get_activation_value_count_from_extended_token(cls, token, starting_code, key, last_count,
119+
restricted_digit_set=False, used_counts=None):
120+
if restricted_digit_set:
121+
token = OpenPAYGOTokenSharedExtended.convert_from_4_digit_token(token)
122+
token_base = OpenPAYGOTokenSharedExtended.get_token_base(token) # We get the base of the token
123+
current_code = OpenPAYGOTokenSharedExtended.put_base_in_token(starting_code, token_base) # We put it into the starting code
124+
starting_code_base = OpenPAYGOTokenSharedExtended.get_token_base(starting_code) # We get the base of the starting code
125+
value = cls._decode_base_extended(starting_code_base, token_base) # If there is a match we get the value from the token
126+
max_count_try = last_count + cls.MAX_TOKEN_JUMP + 1
127+
for count in range(0, max_count_try):
128+
masked_token = OpenPAYGOTokenSharedExtended.put_base_in_token(current_code, token_base)
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
139+
current_code = OpenPAYGOTokenSharedExtended.generate_next_token(current_code, key) # If not we go to the next token
140+
if valid_older_token:
141+
return None, TokenType.ALREADY_USED, None, None
142+
return None, TokenType.INVALID, None, None
143+
144+
@classmethod
145+
def _decode_base_extended(cls, starting_code_base, token_base):
146+
decoded_value = token_base - starting_code_base
147+
if decoded_value < 0:
148+
return decoded_value + 1000000
149+
else:
150+
return decoded_value

0 commit comments

Comments
 (0)