altcha 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- altcha-0.1.0/LICENSE +21 -0
- altcha-0.1.0/MANIFEST.in +1 -0
- altcha-0.1.0/PKG-INFO +167 -0
- altcha-0.1.0/README.md +153 -0
- altcha-0.1.0/altcha/__init__.py +0 -0
- altcha-0.1.0/altcha/altcha.py +393 -0
- altcha-0.1.0/altcha.egg-info/PKG-INFO +167 -0
- altcha-0.1.0/altcha.egg-info/SOURCES.txt +11 -0
- altcha-0.1.0/altcha.egg-info/dependency_links.txt +1 -0
- altcha-0.1.0/altcha.egg-info/top_level.txt +1 -0
- altcha-0.1.0/setup.cfg +4 -0
- altcha-0.1.0/setup.py +22 -0
- altcha-0.1.0/tests/test_altcha.py +174 -0
altcha-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Daniel Regeci
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
altcha-0.1.0/MANIFEST.in
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include README.md
|
altcha-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: altcha
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A library for creating and verifying challenges for ALTCHA.
|
|
5
|
+
Home-page: https://github.com/altcha-org/altcha-lib-py
|
|
6
|
+
Author: Daniel Regeci
|
|
7
|
+
Author-email: 536331+ovx@users.noreply.github.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.6
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
|
|
15
|
+
# ALTCHA Python Library
|
|
16
|
+
|
|
17
|
+
The ALTCHA Python Library is a lightweight, zero-dependency library designed for creating and verifying [ALTCHA](https://altcha.org) challenges, specifically tailored for Python applications.
|
|
18
|
+
|
|
19
|
+
## Compatibility
|
|
20
|
+
|
|
21
|
+
This library is compatible with:
|
|
22
|
+
|
|
23
|
+
- Python 3.6+
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
To install the ALTCHA Python Library, use the following command:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
pip install altcha
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Build
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
python -m build
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Tests
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
python -m unittest discover tests
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
Here’s a basic example of how to use the ALTCHA Python Library:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from altcha import create_challenge, verify_solution
|
|
51
|
+
|
|
52
|
+
def main():
|
|
53
|
+
hmac_key = "secret hmac key"
|
|
54
|
+
|
|
55
|
+
# Create a new challenge
|
|
56
|
+
options = {
|
|
57
|
+
"hmac_key": hmac_key,
|
|
58
|
+
"max_number": 100000, # The maximum random number
|
|
59
|
+
}
|
|
60
|
+
challenge = create_challenge(options)
|
|
61
|
+
print("Challenge created:", challenge)
|
|
62
|
+
|
|
63
|
+
# Example payload to verify
|
|
64
|
+
payload = {
|
|
65
|
+
"algorithm": challenge.algorithm,
|
|
66
|
+
"challenge": challenge.challenge,
|
|
67
|
+
"number": 12345, # Example number
|
|
68
|
+
"salt": challenge.salt,
|
|
69
|
+
"signature": challenge.signature,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Verify the solution
|
|
73
|
+
ok, err = verify_solution(payload, hmac_key, check_expires=True)
|
|
74
|
+
if err:
|
|
75
|
+
print("Error:", err)
|
|
76
|
+
elif ok:
|
|
77
|
+
print("Solution verified!")
|
|
78
|
+
else:
|
|
79
|
+
print("Invalid solution.")
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
main()
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## API
|
|
86
|
+
|
|
87
|
+
### `create_challenge(options)`
|
|
88
|
+
|
|
89
|
+
Creates a new challenge for ALTCHA.
|
|
90
|
+
|
|
91
|
+
**Parameters:**
|
|
92
|
+
|
|
93
|
+
- `options (dict)`:
|
|
94
|
+
- `algorithm (str)`: Hashing algorithm to use (`'SHA-1'`, `'SHA-256'`, `'SHA-512'`, default: `'SHA-256'`).
|
|
95
|
+
- `max_number (int)`: Maximum number for the random number generator (default: 1,000,000).
|
|
96
|
+
- `salt_length (int)`: Length of the random salt in bytes (default: 12).
|
|
97
|
+
- `hmac_key (str)`: Required HMAC key.
|
|
98
|
+
- `salt (str)`: Optional salt string. If not provided, a random salt will be generated.
|
|
99
|
+
- `number (int)`: Optional specific number to use. If not provided, a random number will be generated.
|
|
100
|
+
- `expires (datetime)`: Optional expiration time for the challenge.
|
|
101
|
+
- `params (dict)`: Optional URL-encoded query parameters.
|
|
102
|
+
|
|
103
|
+
**Returns:** `Challenge`
|
|
104
|
+
|
|
105
|
+
### `verify_solution(payload, hmac_key, check_expires)`
|
|
106
|
+
|
|
107
|
+
Verifies an ALTCHA solution.
|
|
108
|
+
|
|
109
|
+
**Parameters:**
|
|
110
|
+
|
|
111
|
+
- `payload (dict)`: The solution payload to verify.
|
|
112
|
+
- `hmac_key (str)`: The HMAC key used for verification.
|
|
113
|
+
- `check_expires (bool)`: Whether to check if the challenge has expired.
|
|
114
|
+
|
|
115
|
+
**Returns:** `(bool, str or None)`
|
|
116
|
+
|
|
117
|
+
### `extract_params(payload)`
|
|
118
|
+
|
|
119
|
+
Extracts URL parameters from the payload's salt.
|
|
120
|
+
|
|
121
|
+
**Parameters:**
|
|
122
|
+
|
|
123
|
+
- `payload (dict)`: The payload containing the salt.
|
|
124
|
+
|
|
125
|
+
**Returns:** `dict`
|
|
126
|
+
|
|
127
|
+
### `verify_fields_hash(form_data, fields, fields_hash, algorithm)`
|
|
128
|
+
|
|
129
|
+
Verifies the hash of form fields.
|
|
130
|
+
|
|
131
|
+
**Parameters:**
|
|
132
|
+
|
|
133
|
+
- `form_data (dict)`: The form data to hash.
|
|
134
|
+
- `fields (list)`: The fields to include in the hash.
|
|
135
|
+
- `fields_hash (str)`: The expected hash value.
|
|
136
|
+
- `algorithm (str)`: Hashing algorithm (`'SHA-1'`, `'SHA-256'`, `'SHA-512'`).
|
|
137
|
+
|
|
138
|
+
**Returns:** `(bool, str or None)`
|
|
139
|
+
|
|
140
|
+
### `verify_server_signature(payload, hmac_key)`
|
|
141
|
+
|
|
142
|
+
Verifies the server signature.
|
|
143
|
+
|
|
144
|
+
**Parameters:**
|
|
145
|
+
|
|
146
|
+
- `payload (dict or str)`: The payload to verify (base64 encoded JSON string or dictionary).
|
|
147
|
+
- `hmac_key (str)`: The HMAC key used for verification.
|
|
148
|
+
|
|
149
|
+
**Returns:** `(bool, ServerSignatureVerificationData, str or None)`
|
|
150
|
+
|
|
151
|
+
### `solve_challenge(challenge, salt, algorithm, max_number, start, stop_chan)`
|
|
152
|
+
|
|
153
|
+
Finds a solution to the given challenge.
|
|
154
|
+
|
|
155
|
+
**Parameters:**
|
|
156
|
+
|
|
157
|
+
- `challenge (str)`: The challenge hash.
|
|
158
|
+
- `salt (str)`: The challenge salt.
|
|
159
|
+
- `algorithm (str)`: Hashing algorithm (`'SHA-1'`, `'SHA-256'`, `'SHA-512'`).
|
|
160
|
+
- `max_number (int)`: Maximum number to iterate to.
|
|
161
|
+
- `start (int)`: Starting number.
|
|
162
|
+
|
|
163
|
+
**Returns:** `(Solution or None, str or None)`
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT
|
altcha-0.1.0/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# ALTCHA Python Library
|
|
2
|
+
|
|
3
|
+
The ALTCHA Python Library is a lightweight, zero-dependency library designed for creating and verifying [ALTCHA](https://altcha.org) challenges, specifically tailored for Python applications.
|
|
4
|
+
|
|
5
|
+
## Compatibility
|
|
6
|
+
|
|
7
|
+
This library is compatible with:
|
|
8
|
+
|
|
9
|
+
- Python 3.6+
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
To install the ALTCHA Python Library, use the following command:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pip install altcha
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Build
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
python -m build
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Tests
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
python -m unittest discover tests
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
Here’s a basic example of how to use the ALTCHA Python Library:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from altcha import create_challenge, verify_solution
|
|
37
|
+
|
|
38
|
+
def main():
|
|
39
|
+
hmac_key = "secret hmac key"
|
|
40
|
+
|
|
41
|
+
# Create a new challenge
|
|
42
|
+
options = {
|
|
43
|
+
"hmac_key": hmac_key,
|
|
44
|
+
"max_number": 100000, # The maximum random number
|
|
45
|
+
}
|
|
46
|
+
challenge = create_challenge(options)
|
|
47
|
+
print("Challenge created:", challenge)
|
|
48
|
+
|
|
49
|
+
# Example payload to verify
|
|
50
|
+
payload = {
|
|
51
|
+
"algorithm": challenge.algorithm,
|
|
52
|
+
"challenge": challenge.challenge,
|
|
53
|
+
"number": 12345, # Example number
|
|
54
|
+
"salt": challenge.salt,
|
|
55
|
+
"signature": challenge.signature,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Verify the solution
|
|
59
|
+
ok, err = verify_solution(payload, hmac_key, check_expires=True)
|
|
60
|
+
if err:
|
|
61
|
+
print("Error:", err)
|
|
62
|
+
elif ok:
|
|
63
|
+
print("Solution verified!")
|
|
64
|
+
else:
|
|
65
|
+
print("Invalid solution.")
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
main()
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## API
|
|
72
|
+
|
|
73
|
+
### `create_challenge(options)`
|
|
74
|
+
|
|
75
|
+
Creates a new challenge for ALTCHA.
|
|
76
|
+
|
|
77
|
+
**Parameters:**
|
|
78
|
+
|
|
79
|
+
- `options (dict)`:
|
|
80
|
+
- `algorithm (str)`: Hashing algorithm to use (`'SHA-1'`, `'SHA-256'`, `'SHA-512'`, default: `'SHA-256'`).
|
|
81
|
+
- `max_number (int)`: Maximum number for the random number generator (default: 1,000,000).
|
|
82
|
+
- `salt_length (int)`: Length of the random salt in bytes (default: 12).
|
|
83
|
+
- `hmac_key (str)`: Required HMAC key.
|
|
84
|
+
- `salt (str)`: Optional salt string. If not provided, a random salt will be generated.
|
|
85
|
+
- `number (int)`: Optional specific number to use. If not provided, a random number will be generated.
|
|
86
|
+
- `expires (datetime)`: Optional expiration time for the challenge.
|
|
87
|
+
- `params (dict)`: Optional URL-encoded query parameters.
|
|
88
|
+
|
|
89
|
+
**Returns:** `Challenge`
|
|
90
|
+
|
|
91
|
+
### `verify_solution(payload, hmac_key, check_expires)`
|
|
92
|
+
|
|
93
|
+
Verifies an ALTCHA solution.
|
|
94
|
+
|
|
95
|
+
**Parameters:**
|
|
96
|
+
|
|
97
|
+
- `payload (dict)`: The solution payload to verify.
|
|
98
|
+
- `hmac_key (str)`: The HMAC key used for verification.
|
|
99
|
+
- `check_expires (bool)`: Whether to check if the challenge has expired.
|
|
100
|
+
|
|
101
|
+
**Returns:** `(bool, str or None)`
|
|
102
|
+
|
|
103
|
+
### `extract_params(payload)`
|
|
104
|
+
|
|
105
|
+
Extracts URL parameters from the payload's salt.
|
|
106
|
+
|
|
107
|
+
**Parameters:**
|
|
108
|
+
|
|
109
|
+
- `payload (dict)`: The payload containing the salt.
|
|
110
|
+
|
|
111
|
+
**Returns:** `dict`
|
|
112
|
+
|
|
113
|
+
### `verify_fields_hash(form_data, fields, fields_hash, algorithm)`
|
|
114
|
+
|
|
115
|
+
Verifies the hash of form fields.
|
|
116
|
+
|
|
117
|
+
**Parameters:**
|
|
118
|
+
|
|
119
|
+
- `form_data (dict)`: The form data to hash.
|
|
120
|
+
- `fields (list)`: The fields to include in the hash.
|
|
121
|
+
- `fields_hash (str)`: The expected hash value.
|
|
122
|
+
- `algorithm (str)`: Hashing algorithm (`'SHA-1'`, `'SHA-256'`, `'SHA-512'`).
|
|
123
|
+
|
|
124
|
+
**Returns:** `(bool, str or None)`
|
|
125
|
+
|
|
126
|
+
### `verify_server_signature(payload, hmac_key)`
|
|
127
|
+
|
|
128
|
+
Verifies the server signature.
|
|
129
|
+
|
|
130
|
+
**Parameters:**
|
|
131
|
+
|
|
132
|
+
- `payload (dict or str)`: The payload to verify (base64 encoded JSON string or dictionary).
|
|
133
|
+
- `hmac_key (str)`: The HMAC key used for verification.
|
|
134
|
+
|
|
135
|
+
**Returns:** `(bool, ServerSignatureVerificationData, str or None)`
|
|
136
|
+
|
|
137
|
+
### `solve_challenge(challenge, salt, algorithm, max_number, start, stop_chan)`
|
|
138
|
+
|
|
139
|
+
Finds a solution to the given challenge.
|
|
140
|
+
|
|
141
|
+
**Parameters:**
|
|
142
|
+
|
|
143
|
+
- `challenge (str)`: The challenge hash.
|
|
144
|
+
- `salt (str)`: The challenge salt.
|
|
145
|
+
- `algorithm (str)`: Hashing algorithm (`'SHA-1'`, `'SHA-256'`, `'SHA-512'`).
|
|
146
|
+
- `max_number (int)`: Maximum number to iterate to.
|
|
147
|
+
- `start (int)`: Starting number.
|
|
148
|
+
|
|
149
|
+
**Returns:** `(Solution or None, str or None)`
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
MIT
|
|
File without changes
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import hmac
|
|
3
|
+
import os
|
|
4
|
+
import base64
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
import urllib.parse
|
|
8
|
+
|
|
9
|
+
# Define algorithms
|
|
10
|
+
SHA1 = 'SHA-1'
|
|
11
|
+
SHA256 = 'SHA-256'
|
|
12
|
+
SHA512 = 'SHA-512'
|
|
13
|
+
|
|
14
|
+
DEFAULT_MAX_NUMBER = int(1e6) # Default maximum number for challenge
|
|
15
|
+
DEFAULT_SALT_LENGTH = 12 # Default length of salt in bytes
|
|
16
|
+
DEFAULT_ALGORITHM = SHA256 # Default hashing algorithm
|
|
17
|
+
|
|
18
|
+
class ChallengeOptions:
|
|
19
|
+
"""
|
|
20
|
+
Represents options for creating a challenge.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
algorithm (str): Hashing algorithm to use (e.g., 'SHA-1', 'SHA-256', 'SHA-512').
|
|
24
|
+
max_number (int): Maximum number to use for the challenge.
|
|
25
|
+
salt_length (int): Length of the salt in bytes.
|
|
26
|
+
hmac_key (str): HMAC key for generating the signature.
|
|
27
|
+
salt (str): Optional salt value. If not provided, a random salt is generated.
|
|
28
|
+
number (int): Optional number for the challenge. If not provided, a random number is used.
|
|
29
|
+
expires (datetime): Optional expiration time for the challenge.
|
|
30
|
+
params (dict): Optional additional parameters to include in the challenge.
|
|
31
|
+
"""
|
|
32
|
+
def __init__(self, algorithm=DEFAULT_ALGORITHM, max_number=DEFAULT_MAX_NUMBER, salt_length=DEFAULT_SALT_LENGTH,
|
|
33
|
+
hmac_key='', salt='', number=0, expires=None, params=None):
|
|
34
|
+
self.algorithm = algorithm
|
|
35
|
+
self.max_number = max_number
|
|
36
|
+
self.salt_length = salt_length
|
|
37
|
+
self.hmac_key = hmac_key
|
|
38
|
+
self.salt = salt
|
|
39
|
+
self.number = number
|
|
40
|
+
self.expires = expires
|
|
41
|
+
self.params = params if params else {}
|
|
42
|
+
|
|
43
|
+
class Challenge:
|
|
44
|
+
"""
|
|
45
|
+
Represents a generated challenge.
|
|
46
|
+
|
|
47
|
+
Attributes:
|
|
48
|
+
algorithm (str): Hashing algorithm used.
|
|
49
|
+
challenge (str): Challenge string.
|
|
50
|
+
max_number (int): Maximum number used for the challenge.
|
|
51
|
+
salt (str): Salt used for generating the challenge.
|
|
52
|
+
signature (str): HMAC signature for the challenge.
|
|
53
|
+
"""
|
|
54
|
+
def __init__(self, algorithm, challenge, max_number, salt, signature):
|
|
55
|
+
self.algorithm = algorithm
|
|
56
|
+
self.challenge = challenge
|
|
57
|
+
self.max_number = max_number
|
|
58
|
+
self.salt = salt
|
|
59
|
+
self.signature = signature
|
|
60
|
+
|
|
61
|
+
class Payload:
|
|
62
|
+
"""
|
|
63
|
+
Represents the payload of a challenge solution.
|
|
64
|
+
|
|
65
|
+
Attributes:
|
|
66
|
+
algorithm (str): Hashing algorithm used.
|
|
67
|
+
challenge (str): Challenge string.
|
|
68
|
+
number (int): Number used in the solution.
|
|
69
|
+
salt (str): Salt used in the solution.
|
|
70
|
+
signature (str): HMAC signature of the solution.
|
|
71
|
+
"""
|
|
72
|
+
def __init__(self, algorithm, challenge, number, salt, signature):
|
|
73
|
+
self.algorithm = algorithm
|
|
74
|
+
self.challenge = challenge
|
|
75
|
+
self.number = number
|
|
76
|
+
self.salt = salt
|
|
77
|
+
self.signature = signature
|
|
78
|
+
|
|
79
|
+
class ServerSignaturePayload:
|
|
80
|
+
"""
|
|
81
|
+
Represents the payload for server signature verification.
|
|
82
|
+
|
|
83
|
+
Attributes:
|
|
84
|
+
algorithm (str): Hashing algorithm used.
|
|
85
|
+
verificationData (str): Data used for verification.
|
|
86
|
+
signature (str): HMAC signature of the verification data.
|
|
87
|
+
verified (bool): Whether the signature was verified.
|
|
88
|
+
"""
|
|
89
|
+
def __init__(self, algorithm, verificationData, signature, verified):
|
|
90
|
+
self.algorithm = algorithm
|
|
91
|
+
self.verificationData = verificationData
|
|
92
|
+
self.signature = signature
|
|
93
|
+
self.verified = verified
|
|
94
|
+
|
|
95
|
+
class ServerSignatureVerificationData:
|
|
96
|
+
"""
|
|
97
|
+
Represents verification data for server signatures.
|
|
98
|
+
|
|
99
|
+
Attributes:
|
|
100
|
+
classification (str): Classification of the data.
|
|
101
|
+
country (str): Country associated with the data.
|
|
102
|
+
detectedLanguage (str): Language detected from the data.
|
|
103
|
+
email (str): Email address associated with the data.
|
|
104
|
+
expire (int): Expiration time in seconds since epoch.
|
|
105
|
+
fields (list): List of fields included in the data.
|
|
106
|
+
fieldsHash (str): Hash of the fields.
|
|
107
|
+
ipAddress (str): IP address associated with the data.
|
|
108
|
+
reasons (list): Reasons associated with the data.
|
|
109
|
+
score (float): Score associated with the data.
|
|
110
|
+
time (int): Time associated with the data.
|
|
111
|
+
verified (bool): Whether the data was verified.
|
|
112
|
+
"""
|
|
113
|
+
def __init__(self, classification='', country='', detected_language='', email='', expire=0, fields=None,
|
|
114
|
+
fields_hash='', ip_address='', reasons=None, score=0.0, time=0, verified=False):
|
|
115
|
+
self.classification = classification
|
|
116
|
+
self.country = country
|
|
117
|
+
self.detectedLanguage = detected_language
|
|
118
|
+
self.email = email
|
|
119
|
+
self.expire = expire
|
|
120
|
+
self.fields = fields if fields else []
|
|
121
|
+
self.fieldsHash = fields_hash
|
|
122
|
+
self.ipAddress = ip_address
|
|
123
|
+
self.reasons = reasons if reasons else []
|
|
124
|
+
self.score = score
|
|
125
|
+
self.time = time
|
|
126
|
+
self.verified = verified
|
|
127
|
+
|
|
128
|
+
class Solution:
|
|
129
|
+
"""
|
|
130
|
+
Represents a solution to a challenge.
|
|
131
|
+
|
|
132
|
+
Attributes:
|
|
133
|
+
number (int): Number that solved the challenge.
|
|
134
|
+
took (float): Time taken to solve the challenge, in seconds.
|
|
135
|
+
"""
|
|
136
|
+
def __init__(self, number, took):
|
|
137
|
+
self.number = number
|
|
138
|
+
self.took = took
|
|
139
|
+
|
|
140
|
+
def random_bytes(length):
|
|
141
|
+
"""
|
|
142
|
+
Generates a random byte string of the specified length.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
length (int): Length of the byte string.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
bytes: Random byte string.
|
|
149
|
+
"""
|
|
150
|
+
return os.urandom(length)
|
|
151
|
+
|
|
152
|
+
def random_int(max_number):
|
|
153
|
+
"""
|
|
154
|
+
Generates a random integer between 0 and max_number (inclusive).
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
max_number (int): Maximum value for the random integer.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
int: Random integer.
|
|
161
|
+
"""
|
|
162
|
+
return int.from_bytes(os.urandom(8), 'big') % (max_number + 1)
|
|
163
|
+
|
|
164
|
+
def hash_hex(algorithm, data):
|
|
165
|
+
"""
|
|
166
|
+
Computes the hexadecimal digest of the given data using the specified hashing algorithm.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
algorithm (str): Hashing algorithm to use (e.g., 'SHA-1', 'SHA-256', 'SHA-512').
|
|
170
|
+
data (bytes): Data to hash.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
str: Hexadecimal digest of the data.
|
|
174
|
+
"""
|
|
175
|
+
hash_obj = hash_algorithm(algorithm)
|
|
176
|
+
hash_obj.update(data)
|
|
177
|
+
return hash_obj.hexdigest()
|
|
178
|
+
|
|
179
|
+
def hash_algorithm(algorithm):
|
|
180
|
+
"""
|
|
181
|
+
Returns a hash object for the specified hashing algorithm.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
algorithm (str): Hashing algorithm to use (e.g., 'SHA-1', 'SHA-256', 'SHA-512').
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
hashlib.Hash: Hash object for the specified algorithm.
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
ValueError: If the algorithm is unsupported.
|
|
191
|
+
"""
|
|
192
|
+
if algorithm == SHA1:
|
|
193
|
+
return hashlib.sha1()
|
|
194
|
+
elif algorithm == SHA256:
|
|
195
|
+
return hashlib.sha256()
|
|
196
|
+
elif algorithm == SHA512:
|
|
197
|
+
return hashlib.sha512()
|
|
198
|
+
else:
|
|
199
|
+
raise ValueError(f"Unsupported algorithm: {algorithm}")
|
|
200
|
+
|
|
201
|
+
def hmac_hex(algorithm, data, key):
|
|
202
|
+
"""
|
|
203
|
+
Computes the HMAC hexadecimal digest of the given data using the specified algorithm and key.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
algorithm (str): Hashing algorithm to use (e.g., 'SHA-1', 'SHA-256', 'SHA-512').
|
|
207
|
+
data (bytes): Data to HMAC.
|
|
208
|
+
key (str): Key for the HMAC.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
str: Hexadecimal HMAC digest of the data.
|
|
212
|
+
"""
|
|
213
|
+
hmac_obj = hmac.new(key.encode(), data, getattr(hashlib, algorithm.replace('-', '').lower()))
|
|
214
|
+
return hmac_obj.hexdigest()
|
|
215
|
+
|
|
216
|
+
def create_challenge(options):
|
|
217
|
+
"""
|
|
218
|
+
Creates a challenge based on the provided options.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
options (ChallengeOptions): Options for creating the challenge.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Challenge: The generated challenge.
|
|
225
|
+
"""
|
|
226
|
+
algorithm = options.algorithm or DEFAULT_ALGORITHM
|
|
227
|
+
max_number = options.max_number or DEFAULT_MAX_NUMBER
|
|
228
|
+
salt_length = options.salt_length or DEFAULT_SALT_LENGTH
|
|
229
|
+
|
|
230
|
+
salt = options.salt or base64.b16encode(random_bytes(salt_length)).decode('utf-8').lower()
|
|
231
|
+
number = options.number or random_int(max_number)
|
|
232
|
+
|
|
233
|
+
if options.expires:
|
|
234
|
+
options.params['expires'] = str(int(time.mktime(options.expires.timetuple())))
|
|
235
|
+
|
|
236
|
+
salt += '?' + urllib.parse.urlencode(options.params)
|
|
237
|
+
|
|
238
|
+
challenge = hash_hex(algorithm, (salt + str(number)).encode())
|
|
239
|
+
signature = hmac_hex(algorithm, challenge.encode(), options.hmac_key)
|
|
240
|
+
|
|
241
|
+
return Challenge(algorithm, challenge, max_number, salt, signature)
|
|
242
|
+
|
|
243
|
+
def verify_solution(payload, hmac_key, check_expires):
|
|
244
|
+
"""
|
|
245
|
+
Verifies a challenge solution against the expected challenge.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
payload (str or dict): Payload containing the solution (base64 encoded JSON string or dictionary).
|
|
249
|
+
hmac_key (str): HMAC key for verifying the solution.
|
|
250
|
+
check_expires (bool): Whether to check the expiration time.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
tuple: A tuple (bool, str or None) where the first element is True if the solution is valid,
|
|
254
|
+
and the second element is an error message or None.
|
|
255
|
+
"""
|
|
256
|
+
if isinstance(payload, str):
|
|
257
|
+
try:
|
|
258
|
+
payload = json.loads(base64.b64decode(payload).decode())
|
|
259
|
+
except (ValueError, TypeError):
|
|
260
|
+
return False, "Invalid altcha payload"
|
|
261
|
+
|
|
262
|
+
required_fields = ['algorithm', 'challenge', 'number', 'salt', 'signature']
|
|
263
|
+
for field in required_fields:
|
|
264
|
+
if field not in payload:
|
|
265
|
+
return False, f"Missing required field: {field}"
|
|
266
|
+
|
|
267
|
+
expires = extract_params(payload).get('expires')
|
|
268
|
+
if check_expires and expires and int(expires) < time.time():
|
|
269
|
+
return False, None
|
|
270
|
+
|
|
271
|
+
options = ChallengeOptions(
|
|
272
|
+
algorithm=payload['algorithm'],
|
|
273
|
+
hmac_key=hmac_key,
|
|
274
|
+
number=payload['number'],
|
|
275
|
+
salt=payload['salt']
|
|
276
|
+
)
|
|
277
|
+
expected_challenge = create_challenge(options)
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
expected_challenge.challenge == payload['challenge'] and
|
|
281
|
+
expected_challenge.signature == payload['signature']
|
|
282
|
+
), None
|
|
283
|
+
|
|
284
|
+
def extract_params(payload):
|
|
285
|
+
"""
|
|
286
|
+
Extracts query parameters from the salt string in the payload.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
payload (dict): Payload containing the salt.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
dict: Dictionary of query parameters extracted from the salt.
|
|
293
|
+
"""
|
|
294
|
+
split_salt = payload['salt'].split('?')
|
|
295
|
+
if len(split_salt) > 1:
|
|
296
|
+
return urllib.parse.parse_qs(split_salt[1])
|
|
297
|
+
return {}
|
|
298
|
+
|
|
299
|
+
def verify_fields_hash(form_data, fields, fields_hash, algorithm):
|
|
300
|
+
"""
|
|
301
|
+
Verifies that the hash of specific form fields matches the expected hash.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
form_data (dict): Form data containing the fields to hash.
|
|
305
|
+
fields (list): List of field names to include in the hash.
|
|
306
|
+
fields_hash (str): Expected hash of the fields.
|
|
307
|
+
algorithm (str): Hashing algorithm to use (e.g., 'SHA-1', 'SHA-256', 'SHA-512').
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
tuple: A tuple (bool, str or None) where the first element is True if the hash matches,
|
|
311
|
+
and the second element is an error message or None.
|
|
312
|
+
"""
|
|
313
|
+
lines = [form_data.get(field, [''])[0] for field in fields]
|
|
314
|
+
joined_data = "\n".join(lines)
|
|
315
|
+
computed_hash = hash_hex(algorithm, joined_data)
|
|
316
|
+
return computed_hash == fields_hash, None
|
|
317
|
+
|
|
318
|
+
def verify_server_signature(payload, hmac_key):
|
|
319
|
+
"""
|
|
320
|
+
Verifies the server signature in the payload.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
payload (str or dict): Payload containing the server signature (base64 encoded JSON string or dictionary).
|
|
324
|
+
hmac_key (str): HMAC key for verifying the signature.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
tuple: A tuple (bool, ServerSignatureVerificationData, str or None) where the first element is True if the
|
|
328
|
+
signature is valid, the second element is an instance of ServerSignatureVerificationData containing the
|
|
329
|
+
verification data, and the third element is an error message or None.
|
|
330
|
+
"""
|
|
331
|
+
if isinstance(payload, str):
|
|
332
|
+
try:
|
|
333
|
+
payload = json.loads(base64.b64decode(payload).decode())
|
|
334
|
+
except (ValueError, TypeError):
|
|
335
|
+
return False, None, "Invalid altcha payload"
|
|
336
|
+
elif not isinstance(payload, dict):
|
|
337
|
+
return False, None, "Invalid altcha payload"
|
|
338
|
+
|
|
339
|
+
required_fields = ['algorithm', 'verificationData', 'signature', 'verified']
|
|
340
|
+
for field in required_fields:
|
|
341
|
+
if field not in payload:
|
|
342
|
+
return False, None, f"Missing required field: {field}"
|
|
343
|
+
|
|
344
|
+
hash_obj = hash_algorithm(payload['algorithm'])
|
|
345
|
+
hash_obj.update(payload['verificationData'].encode())
|
|
346
|
+
expected_signature = hmac_hex(payload['algorithm'], hash_obj.digest(), hmac_key)
|
|
347
|
+
|
|
348
|
+
data = ServerSignatureVerificationData()
|
|
349
|
+
params = urllib.parse.parse_qs(payload['verificationData'])
|
|
350
|
+
for key, value in params.items():
|
|
351
|
+
setattr(data, key, value[0] if len(value) == 1 else value)
|
|
352
|
+
|
|
353
|
+
now = int(time.time())
|
|
354
|
+
is_verified = (
|
|
355
|
+
payload['verified'] and
|
|
356
|
+
data.verified and
|
|
357
|
+
int(data.expire) > now and
|
|
358
|
+
payload['signature'] == expected_signature
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
return is_verified, data, None
|
|
362
|
+
|
|
363
|
+
def solve_challenge(challenge, salt, algorithm, max_number, start):
|
|
364
|
+
"""
|
|
365
|
+
Attempts to solve a challenge by finding a number that matches the challenge hash.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
challenge (str): Challenge hash to match.
|
|
369
|
+
salt (str): Salt used in the challenge.
|
|
370
|
+
algorithm (str): Hashing algorithm to use (e.g., 'SHA-1', 'SHA-256', 'SHA-512').
|
|
371
|
+
max_number (int): Maximum number to try.
|
|
372
|
+
start (int): Starting number to try.
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
tuple: A tuple (Solution or None, str or None) where the first element is a Solution object if the challenge
|
|
376
|
+
is solved, and the second element is an error message or None.
|
|
377
|
+
"""
|
|
378
|
+
if not algorithm:
|
|
379
|
+
algorithm = 'SHA-256'
|
|
380
|
+
if max_number <= 0:
|
|
381
|
+
max_number = 1000000
|
|
382
|
+
if start < 0:
|
|
383
|
+
start = 0
|
|
384
|
+
|
|
385
|
+
start_time = time.time()
|
|
386
|
+
|
|
387
|
+
for n in range(start, max_number + 1):
|
|
388
|
+
hash_hex_value = hash_hex(algorithm, (salt + str(n)).encode())
|
|
389
|
+
if hash_hex_value == challenge:
|
|
390
|
+
took = time.time() - start_time
|
|
391
|
+
return Solution(n, took), None
|
|
392
|
+
|
|
393
|
+
return None, None
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: altcha
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A library for creating and verifying challenges for ALTCHA.
|
|
5
|
+
Home-page: https://github.com/altcha-org/altcha-lib-py
|
|
6
|
+
Author: Daniel Regeci
|
|
7
|
+
Author-email: 536331+ovx@users.noreply.github.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.6
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
|
|
15
|
+
# ALTCHA Python Library
|
|
16
|
+
|
|
17
|
+
The ALTCHA Python Library is a lightweight, zero-dependency library designed for creating and verifying [ALTCHA](https://altcha.org) challenges, specifically tailored for Python applications.
|
|
18
|
+
|
|
19
|
+
## Compatibility
|
|
20
|
+
|
|
21
|
+
This library is compatible with:
|
|
22
|
+
|
|
23
|
+
- Python 3.6+
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
To install the ALTCHA Python Library, use the following command:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
pip install altcha
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Build
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
python -m build
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Tests
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
python -m unittest discover tests
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
Here’s a basic example of how to use the ALTCHA Python Library:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from altcha import create_challenge, verify_solution
|
|
51
|
+
|
|
52
|
+
def main():
|
|
53
|
+
hmac_key = "secret hmac key"
|
|
54
|
+
|
|
55
|
+
# Create a new challenge
|
|
56
|
+
options = {
|
|
57
|
+
"hmac_key": hmac_key,
|
|
58
|
+
"max_number": 100000, # The maximum random number
|
|
59
|
+
}
|
|
60
|
+
challenge = create_challenge(options)
|
|
61
|
+
print("Challenge created:", challenge)
|
|
62
|
+
|
|
63
|
+
# Example payload to verify
|
|
64
|
+
payload = {
|
|
65
|
+
"algorithm": challenge.algorithm,
|
|
66
|
+
"challenge": challenge.challenge,
|
|
67
|
+
"number": 12345, # Example number
|
|
68
|
+
"salt": challenge.salt,
|
|
69
|
+
"signature": challenge.signature,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Verify the solution
|
|
73
|
+
ok, err = verify_solution(payload, hmac_key, check_expires=True)
|
|
74
|
+
if err:
|
|
75
|
+
print("Error:", err)
|
|
76
|
+
elif ok:
|
|
77
|
+
print("Solution verified!")
|
|
78
|
+
else:
|
|
79
|
+
print("Invalid solution.")
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
main()
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## API
|
|
86
|
+
|
|
87
|
+
### `create_challenge(options)`
|
|
88
|
+
|
|
89
|
+
Creates a new challenge for ALTCHA.
|
|
90
|
+
|
|
91
|
+
**Parameters:**
|
|
92
|
+
|
|
93
|
+
- `options (dict)`:
|
|
94
|
+
- `algorithm (str)`: Hashing algorithm to use (`'SHA-1'`, `'SHA-256'`, `'SHA-512'`, default: `'SHA-256'`).
|
|
95
|
+
- `max_number (int)`: Maximum number for the random number generator (default: 1,000,000).
|
|
96
|
+
- `salt_length (int)`: Length of the random salt in bytes (default: 12).
|
|
97
|
+
- `hmac_key (str)`: Required HMAC key.
|
|
98
|
+
- `salt (str)`: Optional salt string. If not provided, a random salt will be generated.
|
|
99
|
+
- `number (int)`: Optional specific number to use. If not provided, a random number will be generated.
|
|
100
|
+
- `expires (datetime)`: Optional expiration time for the challenge.
|
|
101
|
+
- `params (dict)`: Optional URL-encoded query parameters.
|
|
102
|
+
|
|
103
|
+
**Returns:** `Challenge`
|
|
104
|
+
|
|
105
|
+
### `verify_solution(payload, hmac_key, check_expires)`
|
|
106
|
+
|
|
107
|
+
Verifies an ALTCHA solution.
|
|
108
|
+
|
|
109
|
+
**Parameters:**
|
|
110
|
+
|
|
111
|
+
- `payload (dict)`: The solution payload to verify.
|
|
112
|
+
- `hmac_key (str)`: The HMAC key used for verification.
|
|
113
|
+
- `check_expires (bool)`: Whether to check if the challenge has expired.
|
|
114
|
+
|
|
115
|
+
**Returns:** `(bool, str or None)`
|
|
116
|
+
|
|
117
|
+
### `extract_params(payload)`
|
|
118
|
+
|
|
119
|
+
Extracts URL parameters from the payload's salt.
|
|
120
|
+
|
|
121
|
+
**Parameters:**
|
|
122
|
+
|
|
123
|
+
- `payload (dict)`: The payload containing the salt.
|
|
124
|
+
|
|
125
|
+
**Returns:** `dict`
|
|
126
|
+
|
|
127
|
+
### `verify_fields_hash(form_data, fields, fields_hash, algorithm)`
|
|
128
|
+
|
|
129
|
+
Verifies the hash of form fields.
|
|
130
|
+
|
|
131
|
+
**Parameters:**
|
|
132
|
+
|
|
133
|
+
- `form_data (dict)`: The form data to hash.
|
|
134
|
+
- `fields (list)`: The fields to include in the hash.
|
|
135
|
+
- `fields_hash (str)`: The expected hash value.
|
|
136
|
+
- `algorithm (str)`: Hashing algorithm (`'SHA-1'`, `'SHA-256'`, `'SHA-512'`).
|
|
137
|
+
|
|
138
|
+
**Returns:** `(bool, str or None)`
|
|
139
|
+
|
|
140
|
+
### `verify_server_signature(payload, hmac_key)`
|
|
141
|
+
|
|
142
|
+
Verifies the server signature.
|
|
143
|
+
|
|
144
|
+
**Parameters:**
|
|
145
|
+
|
|
146
|
+
- `payload (dict or str)`: The payload to verify (base64 encoded JSON string or dictionary).
|
|
147
|
+
- `hmac_key (str)`: The HMAC key used for verification.
|
|
148
|
+
|
|
149
|
+
**Returns:** `(bool, ServerSignatureVerificationData, str or None)`
|
|
150
|
+
|
|
151
|
+
### `solve_challenge(challenge, salt, algorithm, max_number, start, stop_chan)`
|
|
152
|
+
|
|
153
|
+
Finds a solution to the given challenge.
|
|
154
|
+
|
|
155
|
+
**Parameters:**
|
|
156
|
+
|
|
157
|
+
- `challenge (str)`: The challenge hash.
|
|
158
|
+
- `salt (str)`: The challenge salt.
|
|
159
|
+
- `algorithm (str)`: Hashing algorithm (`'SHA-1'`, `'SHA-256'`, `'SHA-512'`).
|
|
160
|
+
- `max_number (int)`: Maximum number to iterate to.
|
|
161
|
+
- `start (int)`: Starting number.
|
|
162
|
+
|
|
163
|
+
**Returns:** `(Solution or None, str or None)`
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
altcha
|
altcha-0.1.0/setup.cfg
ADDED
altcha-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name='altcha',
|
|
5
|
+
version='0.1.0',
|
|
6
|
+
description='A library for creating and verifying challenges for ALTCHA.',
|
|
7
|
+
long_description=open('README.md').read(),
|
|
8
|
+
long_description_content_type='text/markdown',
|
|
9
|
+
author='Daniel Regeci',
|
|
10
|
+
author_email='536331+ovx@users.noreply.github.com',
|
|
11
|
+
url='https://github.com/altcha-org/altcha-lib-py',
|
|
12
|
+
packages=find_packages(),
|
|
13
|
+
classifiers=[
|
|
14
|
+
'Programming Language :: Python :: 3',
|
|
15
|
+
'License :: OSI Approved :: MIT License',
|
|
16
|
+
'Operating System :: OS Independent',
|
|
17
|
+
],
|
|
18
|
+
python_requires='>=3.6',
|
|
19
|
+
install_requires=[
|
|
20
|
+
# Add any dependencies here
|
|
21
|
+
],
|
|
22
|
+
)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import hmac
|
|
3
|
+
import time
|
|
4
|
+
import unittest
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
from altcha.altcha import ChallengeOptions, Payload, create_challenge, hash_algorithm, hash_hex, hmac_hex, solve_challenge, verify_server_signature, verify_solution
|
|
8
|
+
|
|
9
|
+
class TestALTCHA(unittest.TestCase):
|
|
10
|
+
|
|
11
|
+
def setUp(self):
|
|
12
|
+
self.hmac_key = 'test-key'
|
|
13
|
+
|
|
14
|
+
def test_create_challenge(self):
|
|
15
|
+
options = ChallengeOptions(
|
|
16
|
+
algorithm='SHA-256',
|
|
17
|
+
max_number=1000,
|
|
18
|
+
salt_length=16,
|
|
19
|
+
hmac_key=self.hmac_key,
|
|
20
|
+
salt='somesalt',
|
|
21
|
+
number=123
|
|
22
|
+
)
|
|
23
|
+
challenge = create_challenge(options)
|
|
24
|
+
self.assertIsNotNone(challenge)
|
|
25
|
+
self.assertEqual(challenge.algorithm, 'SHA-256')
|
|
26
|
+
|
|
27
|
+
def test_verify_solution_success(self):
|
|
28
|
+
options = ChallengeOptions(
|
|
29
|
+
algorithm='SHA-256',
|
|
30
|
+
max_number=1000,
|
|
31
|
+
salt_length=16,
|
|
32
|
+
hmac_key=self.hmac_key,
|
|
33
|
+
salt='somesalt',
|
|
34
|
+
number=123
|
|
35
|
+
)
|
|
36
|
+
challenge = create_challenge(options)
|
|
37
|
+
payload = Payload(
|
|
38
|
+
algorithm='SHA-256',
|
|
39
|
+
challenge=challenge.challenge,
|
|
40
|
+
number=123,
|
|
41
|
+
salt='somesalt',
|
|
42
|
+
signature=challenge.signature
|
|
43
|
+
)
|
|
44
|
+
payload_encoded = base64.b64encode(json.dumps(payload.__dict__).encode()).decode()
|
|
45
|
+
result, _ = verify_solution(payload_encoded, self.hmac_key, check_expires=False)
|
|
46
|
+
self.assertTrue(result)
|
|
47
|
+
|
|
48
|
+
def test_verify_solution_failure(self):
|
|
49
|
+
options = ChallengeOptions(
|
|
50
|
+
algorithm='SHA-256',
|
|
51
|
+
max_number=1000,
|
|
52
|
+
salt_length=16,
|
|
53
|
+
hmac_key=self.hmac_key,
|
|
54
|
+
salt='somesalt',
|
|
55
|
+
number=123
|
|
56
|
+
)
|
|
57
|
+
challenge = create_challenge(options)
|
|
58
|
+
payload = Payload(
|
|
59
|
+
algorithm='SHA-256',
|
|
60
|
+
challenge=challenge.challenge,
|
|
61
|
+
number=123,
|
|
62
|
+
salt='somesalt',
|
|
63
|
+
signature='wrong-signature'
|
|
64
|
+
)
|
|
65
|
+
payload_encoded = base64.b64encode(json.dumps(payload.__dict__).encode()).decode()
|
|
66
|
+
result, _ = verify_solution(payload_encoded, self.hmac_key, check_expires=False)
|
|
67
|
+
self.assertFalse(result)
|
|
68
|
+
|
|
69
|
+
def test_valid_signature(self):
|
|
70
|
+
expire_time = int(time.time()) + 600 # 10 minutes from now
|
|
71
|
+
verification_data = (
|
|
72
|
+
f"expire={expire_time}&fields=field1,field2&reasons=reason1,reason2"
|
|
73
|
+
f"&score=3&time={int(time.time())}&verified=true"
|
|
74
|
+
)
|
|
75
|
+
hash_obj = hash_algorithm('SHA-256')
|
|
76
|
+
hash_obj.update(verification_data.encode())
|
|
77
|
+
expected_signature = hmac_hex('SHA-256', hash_obj.digest(), self.hmac_key)
|
|
78
|
+
|
|
79
|
+
payload = {
|
|
80
|
+
'algorithm': 'SHA-256',
|
|
81
|
+
'verificationData': verification_data,
|
|
82
|
+
'signature': expected_signature,
|
|
83
|
+
'verified': True
|
|
84
|
+
}
|
|
85
|
+
payload_encoded = base64.b64encode(json.dumps(payload).encode()).decode()
|
|
86
|
+
|
|
87
|
+
is_valid, data, error = verify_server_signature(payload_encoded, self.hmac_key)
|
|
88
|
+
|
|
89
|
+
self.assertIsNone(error)
|
|
90
|
+
self.assertTrue(is_valid)
|
|
91
|
+
self.assertGreater(int(data.expire), 0)
|
|
92
|
+
self.assertGreater(len(data.fields), 0)
|
|
93
|
+
self.assertGreater(len(data.reasons), 0)
|
|
94
|
+
self.assertGreater(int(data.score), 0)
|
|
95
|
+
self.assertGreater(int(data.time), 0)
|
|
96
|
+
self.assertTrue(data.verified)
|
|
97
|
+
|
|
98
|
+
def test_invalid_signature(self):
|
|
99
|
+
expire_time = int(time.time()) + 600
|
|
100
|
+
verification_data = (
|
|
101
|
+
f"expire={expire_time}&fields=field1,field2&reasons=reason1,reason2"
|
|
102
|
+
f"&score=3&time={int(time.time())}&verified=true"
|
|
103
|
+
)
|
|
104
|
+
payload = {
|
|
105
|
+
'algorithm': 'SHA-256',
|
|
106
|
+
'verificationData': verification_data,
|
|
107
|
+
'signature': 'invalidSignature',
|
|
108
|
+
'verified': True
|
|
109
|
+
}
|
|
110
|
+
payload_encoded = base64.b64encode(json.dumps(payload).encode()).decode()
|
|
111
|
+
|
|
112
|
+
is_valid, _, error = verify_server_signature(payload_encoded, self.hmac_key)
|
|
113
|
+
|
|
114
|
+
self.assertIsNone(error)
|
|
115
|
+
self.assertFalse(is_valid)
|
|
116
|
+
|
|
117
|
+
def test_expired_payload(self):
|
|
118
|
+
expire_time = int(time.time()) - 600 # 10 minutes ago
|
|
119
|
+
verification_data = (
|
|
120
|
+
f"expire={expire_time}&fields=field1,field2&reasons=reason1,reason2"
|
|
121
|
+
f"&score=3&time={int(time.time())}&verified=true"
|
|
122
|
+
)
|
|
123
|
+
hash_obj = hash_algorithm('SHA-256')
|
|
124
|
+
hash_obj.update(verification_data.encode())
|
|
125
|
+
expected_signature = hmac_hex('SHA-256', hash_obj.digest(), self.hmac_key)
|
|
126
|
+
|
|
127
|
+
payload = {
|
|
128
|
+
'algorithm': 'SHA-256',
|
|
129
|
+
'verificationData': verification_data,
|
|
130
|
+
'signature': expected_signature,
|
|
131
|
+
'verified': True
|
|
132
|
+
}
|
|
133
|
+
payload_encoded = base64.b64encode(json.dumps(payload).encode()).decode()
|
|
134
|
+
|
|
135
|
+
is_valid, _, error = verify_server_signature(payload_encoded, self.hmac_key)
|
|
136
|
+
|
|
137
|
+
self.assertIsNone(error)
|
|
138
|
+
self.assertFalse(is_valid)
|
|
139
|
+
|
|
140
|
+
def test_solve_challenge(self):
|
|
141
|
+
start = 0
|
|
142
|
+
options = ChallengeOptions(
|
|
143
|
+
algorithm='SHA-256',
|
|
144
|
+
max_number=1000,
|
|
145
|
+
salt_length=16,
|
|
146
|
+
hmac_key=self.hmac_key,
|
|
147
|
+
salt='somesalt',
|
|
148
|
+
number=123
|
|
149
|
+
)
|
|
150
|
+
challenge = create_challenge(options)
|
|
151
|
+
|
|
152
|
+
solution, err = solve_challenge(
|
|
153
|
+
challenge.challenge,
|
|
154
|
+
challenge.salt,
|
|
155
|
+
challenge.algorithm,
|
|
156
|
+
1000,
|
|
157
|
+
start,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
self.assertIsNone(err, "Error occurred while solving the challenge")
|
|
161
|
+
self.assertIsNotNone(solution, "Solution should not be None")
|
|
162
|
+
self.assertEqual(challenge.challenge, hash_hex(challenge.algorithm, (challenge.salt + str(solution.number)).encode()))
|
|
163
|
+
|
|
164
|
+
def test_hash_hex(self):
|
|
165
|
+
result = hash_hex('SHA-256', 'testdata'.encode())
|
|
166
|
+
self.assertEqual(result, hashlib.sha256('testdata'.encode()).hexdigest())
|
|
167
|
+
|
|
168
|
+
def test_hmac_hex(self):
|
|
169
|
+
result = hmac_hex('SHA-256', 'testdata'.encode(), self.hmac_key)
|
|
170
|
+
expected = hmac.new(self.hmac_key.encode(), 'testdata'.encode(), hashlib.sha256).hexdigest()
|
|
171
|
+
self.assertEqual(result, expected)
|
|
172
|
+
|
|
173
|
+
if __name__ == '__main__':
|
|
174
|
+
unittest.main()
|