authguard-sdk 1.0.0__py3-none-any.whl
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.
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""AuthGuard client SDK for Python.
|
|
2
|
+
|
|
3
|
+
Example:
|
|
4
|
+
from authguard_sdk import AuthGuard, AuthGuardError
|
|
5
|
+
|
|
6
|
+
app = AuthGuard(app_id="...", version="1.0", public_key="...")
|
|
7
|
+
app.init()
|
|
8
|
+
app.license("XXXX-XXXX-XXXX-XXXX")
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .client import AuthGuard, AuthGuardError, get_hwid
|
|
12
|
+
|
|
13
|
+
__all__ = ["AuthGuard", "AuthGuardError", "get_hwid"]
|
|
14
|
+
__version__ = "1.0.0"
|
authguard_sdk/client.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""AuthGuard client SDK for Python."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import platform
|
|
8
|
+
import secrets
|
|
9
|
+
import subprocess
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import requests
|
|
15
|
+
from cryptography.exceptions import InvalidSignature
|
|
16
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AuthGuardError(Exception):
|
|
20
|
+
def __init__(self, message: str, response: dict[str, Any] | None = None) -> None:
|
|
21
|
+
super().__init__(message)
|
|
22
|
+
self.response: dict[str, Any] = response or {}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_hwid() -> str:
|
|
26
|
+
raw = ""
|
|
27
|
+
try:
|
|
28
|
+
if platform.system() == "Windows":
|
|
29
|
+
import winreg
|
|
30
|
+
|
|
31
|
+
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Cryptography", 0, winreg.KEY_READ | winreg.KEY_WOW64_64KEY) as k:
|
|
32
|
+
raw = winreg.QueryValueEx(k, "MachineGuid")[0]
|
|
33
|
+
elif platform.system() == "Darwin":
|
|
34
|
+
out = subprocess.check_output(["ioreg", "-rd1", "-c", "IOPlatformExpertDevice"], text=True)
|
|
35
|
+
raw = next(line.split('"')[-2] for line in out.splitlines() if "IOPlatformUUID" in line)
|
|
36
|
+
else:
|
|
37
|
+
with open("/etc/machine-id") as f:
|
|
38
|
+
raw = f.read().strip()
|
|
39
|
+
except Exception:
|
|
40
|
+
raw = str(uuid.getnode())
|
|
41
|
+
return hashlib.sha256(raw.encode()).hexdigest()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AuthGuard:
|
|
45
|
+
# Some hosts' firewalls block the default python-requests user agent, so send our own.
|
|
46
|
+
HEADERS = {"User-Agent": "AuthGuard-SDK/1.0"}
|
|
47
|
+
|
|
48
|
+
def __init__(self, app_id: str, version: str, public_key: str, url: str = "https://authguard.dk/api/client") -> None:
|
|
49
|
+
self.app_id = app_id
|
|
50
|
+
self.version = version
|
|
51
|
+
self.url = url
|
|
52
|
+
self.hwid = get_hwid()
|
|
53
|
+
self.session_id: str | None = None
|
|
54
|
+
self.info: dict[str, Any] = {}
|
|
55
|
+
self._key = Ed25519PublicKey.from_public_bytes(bytes.fromhex(public_key))
|
|
56
|
+
|
|
57
|
+
def _request(self, type_: str, **fields: Any) -> dict[str, Any]:
|
|
58
|
+
nonce = secrets.token_hex(16)
|
|
59
|
+
body = {"type": type_, "app_id": self.app_id, "nonce": nonce, **fields}
|
|
60
|
+
if self.session_id and type_ != "init":
|
|
61
|
+
body["sessionid"] = self.session_id
|
|
62
|
+
try:
|
|
63
|
+
r = requests.post(self.url, json=body, headers=self.HEADERS, timeout=15)
|
|
64
|
+
outer = r.json()
|
|
65
|
+
payload, signature = outer["payload"], outer["signature"]
|
|
66
|
+
except (requests.RequestException, ValueError, KeyError, TypeError) as e:
|
|
67
|
+
raise AuthGuardError(f"Could not reach the license server ({e.__class__.__name__})")
|
|
68
|
+
try:
|
|
69
|
+
self._key.verify(bytes.fromhex(signature), payload.encode())
|
|
70
|
+
except (InvalidSignature, ValueError):
|
|
71
|
+
raise AuthGuardError("Invalid server signature")
|
|
72
|
+
data = json.loads(payload)
|
|
73
|
+
if data.get("nonce") != nonce or abs(time.time() - data.get("timestamp", 0)) > 300:
|
|
74
|
+
raise AuthGuardError("Invalid server response")
|
|
75
|
+
if not data.get("success"):
|
|
76
|
+
raise AuthGuardError(data.get("message", "Request failed"), data)
|
|
77
|
+
return data
|
|
78
|
+
|
|
79
|
+
def init(self) -> None:
|
|
80
|
+
data = self._request("init", version=self.version, hwid=self.hwid)
|
|
81
|
+
self.session_id = data["sessionid"]
|
|
82
|
+
|
|
83
|
+
def license(self, key: str) -> dict[str, Any]:
|
|
84
|
+
self.info = self._request("license", key=key.strip(), hwid=self.hwid)["info"]
|
|
85
|
+
return self.info
|
|
86
|
+
|
|
87
|
+
def register(self, username: str, password: str, key: str) -> dict[str, Any]:
|
|
88
|
+
self.info = self._request("register", username=username, password=password, key=key.strip(), hwid=self.hwid)["info"]
|
|
89
|
+
return self.info
|
|
90
|
+
|
|
91
|
+
def login(self, username: str, password: str) -> dict[str, Any]:
|
|
92
|
+
self.info = self._request("login", username=username, password=password, hwid=self.hwid)["info"]
|
|
93
|
+
return self.info
|
|
94
|
+
|
|
95
|
+
def var(self, name: str) -> str:
|
|
96
|
+
return self._request("var", name=name)["value"]
|
|
97
|
+
|
|
98
|
+
def check(self) -> bool:
|
|
99
|
+
try:
|
|
100
|
+
self._request("check")
|
|
101
|
+
return True
|
|
102
|
+
except AuthGuardError:
|
|
103
|
+
return False
|
authguard_sdk/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: authguard-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Client SDK for AuthGuard, a software licensing platform with Ed25519-signed responses
|
|
5
|
+
Project-URL: Homepage, https://authguard.dk
|
|
6
|
+
Project-URL: Documentation, https://authguard.dk/docs.php
|
|
7
|
+
Author: AuthGuard
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: authguard,ed25519,keyauth,license-key,licensing
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Topic :: Security :: Cryptography
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Requires-Dist: cryptography<47,>=43
|
|
20
|
+
Requires-Dist: requests<3,>=2.32
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# AuthGuard Python SDK
|
|
24
|
+
|
|
25
|
+
Client SDK for [AuthGuard](https://authguard.dk), a software licensing platform.
|
|
26
|
+
It talks to a single signed endpoint and verifies every reply with Ed25519, so a
|
|
27
|
+
tampered or replayed response is rejected before your code sees it.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install authguard-sdk
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
Copy the app ID and public key from **Dashboard -> your app -> Settings**.
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import sys
|
|
41
|
+
|
|
42
|
+
from authguard_sdk import AuthGuard, AuthGuardError
|
|
43
|
+
|
|
44
|
+
app = AuthGuard(
|
|
45
|
+
app_id="YOUR_APP_ID",
|
|
46
|
+
version="1.0",
|
|
47
|
+
public_key="YOUR_PUBLIC_KEY",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
app.init()
|
|
52
|
+
app.license("XXXX-XXXX-XXXX-XXXX")
|
|
53
|
+
except AuthGuardError as e:
|
|
54
|
+
if e.response.get("update_required"):
|
|
55
|
+
print("Update required. Download:", e.response.get("download_url"))
|
|
56
|
+
else:
|
|
57
|
+
print("Error:", e)
|
|
58
|
+
sys.exit(1)
|
|
59
|
+
|
|
60
|
+
print("Access granted. Expires:", app.info.get("expires_at") or "never")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## API
|
|
64
|
+
|
|
65
|
+
`AuthGuard(app_id, version, public_key, url="https://authguard.dk/api/client")`
|
|
66
|
+
|
|
67
|
+
| Method | Purpose |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| `init()` | Open a session. Must be called first. Raises if the app is disabled or the version is out of date. |
|
|
70
|
+
| `license(key)` | Authenticate with a licence key. Returns the `info` dict. |
|
|
71
|
+
| `login(username, password)` | Authenticate an app user. Returns the `info` dict. |
|
|
72
|
+
| `register(username, password, key)` | Create an app user from a licence key. Returns the `info` dict. |
|
|
73
|
+
| `var(name)` | Read a server-side variable defined in the dashboard. |
|
|
74
|
+
| `check()` | Re-validate the session later on. Returns `True` or `False`. |
|
|
75
|
+
|
|
76
|
+
After a successful `license`, `login` or `register`, `app.info` holds the
|
|
77
|
+
subscription details (`expires_at`, `username`, `hwid`, ...).
|
|
78
|
+
|
|
79
|
+
Every failure raises `AuthGuardError`. Its `.response` attribute carries the
|
|
80
|
+
server's decoded payload when there was one, which is how you detect
|
|
81
|
+
`update_required` and read `download_url`.
|
|
82
|
+
|
|
83
|
+
## Security notes
|
|
84
|
+
|
|
85
|
+
- Responses are Ed25519-signed over the exact payload bytes; the signature is
|
|
86
|
+
verified before the payload is parsed.
|
|
87
|
+
- Each request carries a random nonce that must be echoed back, and replies more
|
|
88
|
+
than five minutes from the local clock are rejected.
|
|
89
|
+
- The app ID and public key are identifiers, not secrets. It is fine for them to
|
|
90
|
+
sit in your source.
|
|
91
|
+
- The SDK sends a `User-Agent` of `AuthGuard-SDK/1.0`. Do not change it: some
|
|
92
|
+
hosting firewalls block the default `python-requests` user agent.
|
|
93
|
+
|
|
94
|
+
Full documentation: <https://authguard.dk/docs.php>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
authguard_sdk/__init__.py,sha256=zqWO1N__QQRib5VPANTPWyHAP3emMQJ00xei8qSw1sI,361
|
|
2
|
+
authguard_sdk/client.py,sha256=sr0trDhuGZMLTAdsSSs4AltjdkFshfDkygU3HBSoeao,4072
|
|
3
|
+
authguard_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
authguard_sdk-1.0.0.dist-info/METADATA,sha256=Nsn9pzNd6isyEywPskJEBRbVx8njGk0aojLVImBRLPc,3323
|
|
5
|
+
authguard_sdk-1.0.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
6
|
+
authguard_sdk-1.0.0.dist-info/licenses/LICENSE,sha256=PYm9h5OLshu43fGAJA9lQ-Vjx8sIcoTADdWTpHbsMIg,1066
|
|
7
|
+
authguard_sdk-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AuthGuard
|
|
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.
|