shieldy-sdk 1.0.2__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.
- shieldy_sdk-1.0.2/.github/workflows/release.yml +66 -0
- shieldy_sdk-1.0.2/.gitignore +17 -0
- shieldy_sdk-1.0.2/CLAUDE.md +123 -0
- shieldy_sdk-1.0.2/PKG-INFO +208 -0
- shieldy_sdk-1.0.2/README.md +195 -0
- shieldy_sdk-1.0.2/docs/architecture.md +163 -0
- shieldy_sdk-1.0.2/examples/basic.py +76 -0
- shieldy_sdk-1.0.2/examples/protected_strings_marker.py +55 -0
- shieldy_sdk-1.0.2/native/native.lock.json +25 -0
- shieldy_sdk-1.0.2/pyproject.toml +73 -0
- shieldy_sdk-1.0.2/scripts/build_wheels.py +101 -0
- shieldy_sdk-1.0.2/scripts/fetch_native.py +123 -0
- shieldy_sdk-1.0.2/src/shieldy/__init__.py +88 -0
- shieldy_sdk-1.0.2/src/shieldy/_abi.py +120 -0
- shieldy_sdk-1.0.2/src/shieldy/_crypto.py +142 -0
- shieldy_sdk-1.0.2/src/shieldy/_native.py +128 -0
- shieldy_sdk-1.0.2/src/shieldy/_session.py +491 -0
- shieldy_sdk-1.0.2/src/shieldy/errors.py +83 -0
- shieldy_sdk-1.0.2/src/shieldy/obf.py +195 -0
- shieldy_sdk-1.0.2/src/shieldy/py.typed +0 -0
- shieldy_sdk-1.0.2/src/shieldy/values.py +176 -0
- shieldy_sdk-1.0.2/tests/conftest.py +54 -0
- shieldy_sdk-1.0.2/tests/e2e/README.md +66 -0
- shieldy_sdk-1.0.2/tests/e2e/conftest.py +182 -0
- shieldy_sdk-1.0.2/tests/e2e/dashboard.py +176 -0
- shieldy_sdk-1.0.2/tests/e2e/probe_app.py +110 -0
- shieldy_sdk-1.0.2/tests/e2e/test_e2e.py +201 -0
- shieldy_sdk-1.0.2/tests/e2e/test_offline.py +133 -0
- shieldy_sdk-1.0.2/tests/mock_runtime.py +256 -0
- shieldy_sdk-1.0.2/tests/test_abi.py +87 -0
- shieldy_sdk-1.0.2/tests/test_native_loading.py +141 -0
- shieldy_sdk-1.0.2/tests/test_no_typographic_dashes.py +29 -0
- shieldy_sdk-1.0.2/tests/test_obf.py +83 -0
- shieldy_sdk-1.0.2/tests/test_session.py +304 -0
- shieldy_sdk-1.0.2/tests/test_values.py +196 -0
- shieldy_sdk-1.0.2/third_party/shieldy_api.h +170 -0
- shieldy_sdk-1.0.2/uv.lock +1005 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
# Publishes shieldy-sdk with PyPI trusted publishing (OIDC), no stored token.
|
|
4
|
+
# - a version tag (v1.2.3) publishes to PyPI (prod);
|
|
5
|
+
# - a manual run (Actions -> release -> Run workflow) publishes to PyPI or TestPyPI (test).
|
|
6
|
+
# Register this repo + release.yml as a trusted publisher on BOTH indexes: pypi.org (GitHub
|
|
7
|
+
# environment "pypi") and test.pypi.org (environment "testpypi"). The native runtime is pulled from
|
|
8
|
+
# the Bunny pull zone, which has token auth on, so both BUNNY_PULL_ZONE_HOST and
|
|
9
|
+
# BUNNY_PULL_ZONE_TOKEN_KEY are needed.
|
|
10
|
+
on:
|
|
11
|
+
push:
|
|
12
|
+
tags: ["v*"]
|
|
13
|
+
workflow_dispatch:
|
|
14
|
+
inputs:
|
|
15
|
+
target:
|
|
16
|
+
description: "Index to publish to"
|
|
17
|
+
type: choice
|
|
18
|
+
options: [prod, test]
|
|
19
|
+
default: prod
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
publish:
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
# A tag push is always prod; a manual run uses the chosen target. The environment name must
|
|
25
|
+
# match the trusted publisher configured on each index.
|
|
26
|
+
environment: ${{ (github.event_name == 'workflow_dispatch' && inputs.target == 'test') && 'testpypi' || 'pypi' }}
|
|
27
|
+
permissions:
|
|
28
|
+
id-token: write # trusted publishing (OIDC)
|
|
29
|
+
contents: read # checkout of this private repo
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v4
|
|
32
|
+
- uses: astral-sh/setup-uv@v5
|
|
33
|
+
- name: Resolve the publish target
|
|
34
|
+
id: target
|
|
35
|
+
run: |
|
|
36
|
+
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.target }}" = "test" ]; then
|
|
37
|
+
echo "value=test" >> "$GITHUB_OUTPUT"
|
|
38
|
+
else
|
|
39
|
+
echo "value=prod" >> "$GITHUB_OUTPUT"
|
|
40
|
+
fi
|
|
41
|
+
# A tag release must match the package version; a manual (possibly test) run need not.
|
|
42
|
+
- name: The tag must match pyproject.toml
|
|
43
|
+
if: github.event_name == 'push'
|
|
44
|
+
run: |
|
|
45
|
+
version="$(python3 -c 'import tomllib;print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')"
|
|
46
|
+
test "v$version" = "${GITHUB_REF_NAME}" || { echo "tag ${GITHUB_REF_NAME} does not match version $version"; exit 1; }
|
|
47
|
+
# Pull the pinned hardened runtime (native/native.lock.json) from the Bunny pull zone (signed:
|
|
48
|
+
# the zone has token auth on) and build one platform wheel per blob (py3-none-<platform>),
|
|
49
|
+
# plus a pure sdist as the fallback for platforms with no bundled runtime.
|
|
50
|
+
- name: Fetch the pinned native runtime
|
|
51
|
+
env:
|
|
52
|
+
BUNNY_PULL_ZONE_HOST: ${{ secrets.BUNNY_PULL_ZONE_HOST }}
|
|
53
|
+
BUNNY_PULL_ZONE_TOKEN_KEY: ${{ secrets.BUNNY_PULL_ZONE_TOKEN_KEY }}
|
|
54
|
+
run: uv run python scripts/fetch_native.py
|
|
55
|
+
- name: Build platform wheels
|
|
56
|
+
run: uv run --with wheel python scripts/build_wheels.py
|
|
57
|
+
- name: Build the source distribution
|
|
58
|
+
run: uv build --sdist
|
|
59
|
+
- name: Publish to PyPI
|
|
60
|
+
if: steps.target.outputs.value == 'prod'
|
|
61
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
62
|
+
- name: Publish to TestPyPI
|
|
63
|
+
if: steps.target.outputs.value == 'test'
|
|
64
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
65
|
+
with:
|
|
66
|
+
repository-url: https://test.pypi.org/legacy/
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
/.venv/
|
|
4
|
+
/build/
|
|
5
|
+
/dist/
|
|
6
|
+
*.egg-info/
|
|
7
|
+
/.pytest_cache/
|
|
8
|
+
/.mypy_cache/
|
|
9
|
+
/.ruff_cache/
|
|
10
|
+
/.idea/
|
|
11
|
+
# Runtimes are build artifacts of ShieldyCore, never committed here.
|
|
12
|
+
*.dll
|
|
13
|
+
*.so
|
|
14
|
+
# Local settings for the end-to-end tests (dashboard credentials).
|
|
15
|
+
/tests/e2e/.env
|
|
16
|
+
# A DEV build of the runtime writes debug logs to ./logs of the working directory.
|
|
17
|
+
logs/
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Orientation for Claude Code (and anyone new) in this repository. The full design rationale is in
|
|
4
|
+
`docs/architecture.md`; the customer-facing guide is `README.md`. Read the relevant section there
|
|
5
|
+
before a structural change.
|
|
6
|
+
|
|
7
|
+
## What this is
|
|
8
|
+
|
|
9
|
+
`shieldy-sdk` (import `shieldy`): the Python client of Shieldy's native runtime (ShieldyCore's
|
|
10
|
+
`native.dll` / `native.so`), through `ctypes`. Python 3.9+, one dependency (`cryptography`).
|
|
11
|
+
Module-level functions operate on the one session a process has: load the runtime, sign in with a
|
|
12
|
+
license key, read license, variables, files and Protected Strings, all decrypted with a per-login
|
|
13
|
+
session key.
|
|
14
|
+
|
|
15
|
+
Sibling repositories implementing the same contract (ABI, wire crypto, error numbers, variable
|
|
16
|
+
grammar): `../../native/ShieldyCore` (runtime, `cpp-module/shieldy_api.h`),
|
|
17
|
+
`../examples/CppCmakeShieldy` (C++ SDK), `../ShieldyDotnetSdk` (.NET SDK),
|
|
18
|
+
`../../services/ShieldyBackend` (server; the `python` snippet in
|
|
19
|
+
`core-dashboard-api/services/string_obfuscation_service.py` must stay `shieldy.protected_string`).
|
|
20
|
+
|
|
21
|
+
## Map
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
src/shieldy/
|
|
25
|
+
__init__.py the public API, re-exported (names without underscore are public)
|
|
26
|
+
_session.py process-wide state, one RLock, every public call (start here)
|
|
27
|
+
_abi.py ctypes mirror of shieldy_api.h; no logic
|
|
28
|
+
_native.py path resolution (application dir, never cwd), loading, SC_GetApi
|
|
29
|
+
_crypto.py X25519 + SHA-256, HMAC attestation, ChaCha20-Poly1305 unwrap
|
|
30
|
+
errors.py Error IntEnum (numbers shared with C++/.NET), ShieldyError
|
|
31
|
+
values.py Config, License, SecureBytes, Feature
|
|
32
|
+
obf.py SHIELDY_OBF marker + `python -m shieldy.obf check`
|
|
33
|
+
tests/
|
|
34
|
+
mock_runtime.py SC_ApiV1 of real CFUNCTYPE callbacks, scenarios of the C++ mock_native.cpp
|
|
35
|
+
test_*.py unit tests (no server, no DLL)
|
|
36
|
+
e2e/ dashboard.py (stdlib REST client), probe_app.py (one process per scenario),
|
|
37
|
+
conftest.py (.env), test_e2e.py; marker `e2e`, excluded by default
|
|
38
|
+
examples/ basic.py, protected_strings_marker.py
|
|
39
|
+
third_party/shieldy_api.h copy of ShieldyCore's header; test_abi.py compares it
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Commands
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv sync # creates .venv with pytest, ruff, mypy
|
|
46
|
+
uv run pytest # unit tests, ~2 s (addopts exclude e2e)
|
|
47
|
+
uv run pytest -m e2e # real runtime + local server, see below
|
|
48
|
+
uv run ruff check src tests examples # line length 120
|
|
49
|
+
uv run mypy # strict, src only
|
|
50
|
+
uv run --python 3.9 pytest # the floor
|
|
51
|
+
uv run python scripts/test_matrix.py # 3.9 to 3.14 plus the oldest cryptography, in throwaway envs
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## E2E stack (local)
|
|
55
|
+
|
|
56
|
+
- Postgres + Redis containers (`shieldy-dev-postgres-1`, `shieldy-dev-redis-1`) from ShieldyDeploy.
|
|
57
|
+
- `ShieldyBackend/core-auth-api`: `uv run --package core-auth-api python main.py` (127.0.0.1:8010).
|
|
58
|
+
- `ShieldyBackend/core-dashboard-api`: same with `core-dashboard-api` (127.0.0.1:8020) and
|
|
59
|
+
`STRING_OBF_NATIVE_LIBRARY_PATH=../../native/ShieldyCore/cmake-builds/win-dev-x64/obfuscation-module/security.dll`.
|
|
60
|
+
Restart it after changing the backend: it does not reload.
|
|
61
|
+
- Runtime: `tests/e2e/runtime/native.dll` (gitignored), a DEV ShieldyCore build with the endpoint
|
|
62
|
+
edited to `http://127.0.0.1:8010` (how to build one: `tests/e2e/README.md`). Settings in
|
|
63
|
+
`tests/e2e/.env` (gitignored).
|
|
64
|
+
- Each test app gets `require_registered_native=false`, or `/initialize` refuses a local build.
|
|
65
|
+
- **Offline start tests (`test_offline.py`)** skip unless both deployment switches are on. A hand-started
|
|
66
|
+
service reads `ShieldyBackend/config/<env>/<service>.toml`, NOT `ShieldyDeploy/config/dev/` (that one is
|
|
67
|
+
for dev compose) and the backend copy has no `[offline_lease]`/`[offline_start]` tables, so start with
|
|
68
|
+
env vars (they beat the TOML): `OFFLINE_LEASE_ENABLED=true` for auth-api (the signing key
|
|
69
|
+
`OFFLINE_LEASE_SIGNING_KEY` is already in `ShieldyDeploy/env/dev/auth-api.env`) and
|
|
70
|
+
`OFFLINE_START_ENABLED=true` for dashboard-api. The skip message names the missing one.
|
|
71
|
+
The dev account's plan must include offline start (dev@shieldy.local does), or the suite skips on 402.
|
|
72
|
+
- Offline is simulated with `SHIELDY_DEV_FORCE_OFFLINE=1` (honored by DEV/TEST runtimes only):
|
|
73
|
+
`run_probe(..., offline=True)`. Leases go to a per-test `lease_directory=str(tmp_path)`, never the
|
|
74
|
+
machine's `%LOCALAPPDATA%\Shieldy\leases`. The runtime must be built from ShieldyCore `dev` with
|
|
75
|
+
offline start (Oct 2026+) or every offline test fails at `connection_state`.
|
|
76
|
+
- **Restarting the services on Windows:** auth-api (Sanic) spawns `multiprocessing` workers that survive
|
|
77
|
+
`Stop-Process` on `main.py` and keep port 8010 bound. Kill the children too: find them with
|
|
78
|
+
`Get-CimInstance Win32_Process -Filter "ParentProcessId=<pid>"` (command line
|
|
79
|
+
`spawn_main(parent_pid=...)`), or check `Get-NetTCPConnection -State Listen -LocalPort 8010,8020`
|
|
80
|
+
until both ports are free. Start from the service dir with the venv python:
|
|
81
|
+
`UV_NO_SYNC=1 ../.venv/Scripts/python.exe main.py` (`UV_NO_SYNC` keeps uv from re-syncing the
|
|
82
|
+
locally installed protocol packages back to the GitHub pin).
|
|
83
|
+
|
|
84
|
+
## Invariants (do not break)
|
|
85
|
+
|
|
86
|
+
- `Error` numbers equal the C++ SDK's `shieldy::Error` and the .NET SDK's `ShieldyError`.
|
|
87
|
+
- `SC_ApiV1._fields_` order = `shieldy_api.h`; append only. `test_abi.py` parses the header.
|
|
88
|
+
- Wire crypto: session key = SHA-256(X25519); attestation = HMAC-SHA256(key, 16-byte nonce); values
|
|
89
|
+
= `nonce(12) || tag(16) || ciphertext`, ChaCha20-Poly1305, no AAD (the tag goes first, so
|
|
90
|
+
`_crypto.Session.decrypt` reorders it for `cryptography`).
|
|
91
|
+
- `appMode = 3` (Python) in `SC_Config`.
|
|
92
|
+
- A relative runtime path resolves against the application directory (frozen exe dir, then
|
|
93
|
+
`sys._MEIPASS`, else the `__main__` script dir), never the working directory.
|
|
94
|
+
- Every runtime-allocated `SC_Buffer` goes back through `free_buffer` (tests assert none left).
|
|
95
|
+
- Variable grammar = C++ `from_chars` (no `+`, no `_` separators, no hex).
|
|
96
|
+
- Python 3.9 floor: `typing.Optional`/`List`, no `match`, no `X | Y` at runtime (ruff ignores UP006/7/35/45).
|
|
97
|
+
- Never write a typographic dash (U+2013/2014/2015); `test_no_typographic_dashes.py` scans the repo.
|
|
98
|
+
- English, US spelling, in code, docs and messages.
|
|
99
|
+
|
|
100
|
+
## Runtime versions
|
|
101
|
+
|
|
102
|
+
ShieldyCore since October 2026 (multi-session, ShieldyJavaSdk Phase 5) gives every context its own
|
|
103
|
+
server session: `login()` after a refused key and `init()` after `shutdown()` work, and the e2e
|
|
104
|
+
suite tests both. Older runtimes allowed one server session per process; `login()`, the README and
|
|
105
|
+
the architecture doc say what that means for integrators who still ship one.
|
|
106
|
+
|
|
107
|
+
## Release process
|
|
108
|
+
|
|
109
|
+
Releasing this SDK (pinning the runtime in `native/native.lock.json`, `fetch_native.py`,
|
|
110
|
+
`build_wheels.py`, `.github/workflows/release.yml`) is phases B and C of the process described in
|
|
111
|
+
`../../release/ShieldyRelease/steps.toml` (panel: `python -m shieldy_release.cli
|
|
112
|
+
serve`). A change to any of these, to where the package version lives, or to the publish targets,
|
|
113
|
+
secrets or tag rules must be checked against `steps.toml`, its checks and its runbooks in the same
|
|
114
|
+
piece of work. See ShieldyRelease's CLAUDE.md, "Keeping the process in sync".
|
|
115
|
+
|
|
116
|
+
## Gotchas
|
|
117
|
+
|
|
118
|
+
- ctypes callbacks in the mock must stay referenced (`MockRuntime._keep`), or the trampoline is
|
|
119
|
+
freed under the SDK.
|
|
120
|
+
- A DEV runtime writes `./logs` into the working directory (`logs/` is gitignored).
|
|
121
|
+
- The e2e `dashboard.py` carries the `__Host-` session cookie by hand: `http.cookiejar` will not
|
|
122
|
+
send a `Secure` cookie back over plain-http localhost.
|
|
123
|
+
- `cryptography` before 45 has no `decrypt_into`; `_crypto` falls back to one immutable copy.
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: shieldy-sdk
|
|
3
|
+
Version: 1.0.2
|
|
4
|
+
Summary: Python client for Shieldy's native runtime: licensing, remote variables, files and Protected Strings.
|
|
5
|
+
Project-URL: Documentation, https://docs.shieldy.app
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
8
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Requires-Dist: cryptography>=42
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Shieldy Python SDK
|
|
15
|
+
|
|
16
|
+
The Python client for Shieldy's native runtime (`native.dll` on Windows, `native.so` on Linux).
|
|
17
|
+
It signs your application in with the end user's license key and reads what the dashboard
|
|
18
|
+
delivers to that license: the license itself, remote variables, files and Protected Strings.
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import shieldy
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
shieldy.start(shieldy.Config("<App GUID>", "1.0"), read_license_key_from_user())
|
|
25
|
+
except shieldy.ShieldyError as exc:
|
|
26
|
+
# exc.error is a shieldy.Error, e.g. Error.AUTHENTICATION_FAILED
|
|
27
|
+
raise SystemExit(f"Could not sign in: {exc.error}")
|
|
28
|
+
|
|
29
|
+
lic = shieldy.license()
|
|
30
|
+
if lic.level >= 20:
|
|
31
|
+
enable_pro_features()
|
|
32
|
+
|
|
33
|
+
api_url = shieldy.variable_by_id("<Lookup ID>") # str
|
|
34
|
+
max_users = shieldy.variable_by_id_as("<Lookup ID>", int) # int
|
|
35
|
+
with shieldy.file_by_id("<Lookup ID>") as pak: # SecureBytes, wiped after the block
|
|
36
|
+
load_assets(pak.view())
|
|
37
|
+
token = shieldy.protected_string("<ciphertext>") # str
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Python 3.9 or newer, 64-bit or 32-bit to match the runtime you ship. The only dependency is
|
|
41
|
+
[`cryptography`](https://cryptography.io).
|
|
42
|
+
|
|
43
|
+
## Installing
|
|
44
|
+
|
|
45
|
+
```powershell
|
|
46
|
+
pip install shieldy-sdk # once published
|
|
47
|
+
pip install path/to/ShieldyPythonSdk # from a checkout
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Ship the runtime with your application. Download it from the dashboard (your app -> Activity ->
|
|
51
|
+
Runtime -> "Download the SDK"), and put it next to your entry script or your frozen executable.
|
|
52
|
+
|
|
53
|
+
## Where the runtime is loaded from
|
|
54
|
+
|
|
55
|
+
The SDK loads exactly one file, chosen in this order:
|
|
56
|
+
|
|
57
|
+
1. `Config.native_library`, if you set it;
|
|
58
|
+
2. the `SHIELDY_NATIVE_LIBRARY` environment variable;
|
|
59
|
+
3. `native.dll` (`native.so` on Linux).
|
|
60
|
+
|
|
61
|
+
A relative path is resolved against your application's directory, **never** the current working
|
|
62
|
+
directory or `PATH`, so a library planted where the user starts your program is not picked up.
|
|
63
|
+
Your application's directory is:
|
|
64
|
+
|
|
65
|
+
| How the app runs | Directory |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `python app.py` | the directory of `app.py` |
|
|
68
|
+
| `python -m package` | the directory of `package/__main__.py` |
|
|
69
|
+
| PyInstaller, cx_Freeze, py2exe | the directory of the executable, then PyInstaller's bundle directory (`sys._MEIPASS`) |
|
|
70
|
+
| interactive interpreter, `python -c` | none: give an absolute path |
|
|
71
|
+
|
|
72
|
+
`shieldy.application_directory()` shows what the SDK settled on.
|
|
73
|
+
|
|
74
|
+
With PyInstaller, either copy the runtime next to the built executable, or bundle it:
|
|
75
|
+
`pyinstaller --add-binary "native.dll;." app.py` (`native.so:.` on Linux).
|
|
76
|
+
|
|
77
|
+
## API
|
|
78
|
+
|
|
79
|
+
All calls are module-level functions on the one session the process has. They are thread-safe.
|
|
80
|
+
|
|
81
|
+
| Call | Returns |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `init(config)` | Loads the runtime and runs its protected start-up (`/initialize` on the server). |
|
|
84
|
+
| `login(key)` | Signs in with the license key. |
|
|
85
|
+
| `start(config, key)` | `init` then `login`. |
|
|
86
|
+
| `shutdown()` | Ends the session. Also runs automatically when the interpreter exits. |
|
|
87
|
+
| `is_logged_in()` | `bool` |
|
|
88
|
+
| `supports(Feature.LOOKUP_IDS)` | Whether the runtime has the `*_by_id` calls (protocol 1.6.0+). |
|
|
89
|
+
| `supports(Feature.OFFLINE_START)` | Whether the runtime has `connection_state()` (protocol 1.12.0+). |
|
|
90
|
+
| `connection_state()` | `ConnectionState`: `offline` (`True` when this run started without a connection), `offline_until` (until when this device can start offline; `None`: it cannot). |
|
|
91
|
+
| `license()` | `License`: `level`, `hwid_seats_used`, `hwid_seats_total`, `created`, `expires` (`None`: never), `is_expired()`, `days_left()`. Read once at login. |
|
|
92
|
+
| `variable_by_id(id)` / `variable(name)` | `str` |
|
|
93
|
+
| `variable_by_id_as(id, type)` / `variable_as(name, type)` | `int`, `float`, `bool` or `str` |
|
|
94
|
+
| `file_by_id(id)` / `file(name)` | `SecureBytes`, wiped from memory when done |
|
|
95
|
+
| `protected_string(ciphertext)` | `str`, cached for the rest of the session |
|
|
96
|
+
|
|
97
|
+
Prefer the `*_by_id` calls: a lookup id is random, a name ends up readable in your code. A
|
|
98
|
+
variable whose name is hidden from the client (dashboard: "Hide name from client") can only be
|
|
99
|
+
read by id.
|
|
100
|
+
|
|
101
|
+
`bool` variables accept `true`, `false`, `1`, `0`. Numbers follow the C++ SDK's rules: surrounding
|
|
102
|
+
whitespace is ignored, a leading `+`, digit separators (`1_000`) and hex are not accepted.
|
|
103
|
+
|
|
104
|
+
### Errors
|
|
105
|
+
|
|
106
|
+
Every call raises `shieldy.ShieldyError` when it cannot deliver; `exc.error` says why. The codes
|
|
107
|
+
have the same numbers as the C++ SDK's `shieldy::Error`.
|
|
108
|
+
|
|
109
|
+
| `Error` | Meaning |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `NATIVE_LIBRARY_NOT_FOUND` | No runtime at the resolved path, or one for the wrong architecture. |
|
|
112
|
+
| `NATIVE_LIBRARY_INVALID` | The file loaded but is not a Shieldy runtime. |
|
|
113
|
+
| `INITIALIZATION_FAILED` | The server refused to start the app: unknown App GUID, suspended app, outdated version, unregistered runtime build, maintenance. |
|
|
114
|
+
| `AUTHENTICATION_FAILED` | The key was refused: wrong, expired, banned, or no free device seat. The server does not say which. |
|
|
115
|
+
| `NOT_INITIALIZED`, `NOT_LOGGED_IN` | A call came too early. |
|
|
116
|
+
| `NOT_FOUND` | No variable with that id or name for this license. |
|
|
117
|
+
| `NATIVE_TOO_OLD` | The runtime predates the call; see `supports()`. |
|
|
118
|
+
| `ATTESTATION_FAILED`, `KEY_EXCHANGE_FAILED`, `DECRYPT_FAILED` | The runtime could not prove it holds the session key, or a value did not decrypt. Treat as tampering. |
|
|
119
|
+
| `INVALID_VALUE` | A variable did not parse as the requested type. |
|
|
120
|
+
| `SERVER_UNREACHABLE` | No connection, and this device cannot start offline (offline start is off, it never signed in online here, or that was with another key). |
|
|
121
|
+
| `OFFLINE_EXPIRED` | No connection, and this device's offline time (or the license) has run out. |
|
|
122
|
+
| `CLOCK_INVALID` | No connection, and the system clock reads earlier than the last run. |
|
|
123
|
+
| `OFFLINE_UNAVAILABLE` | The call needs a connection and the app is running offline (files). |
|
|
124
|
+
|
|
125
|
+
The full list is in `src/shieldy/errors.py`.
|
|
126
|
+
|
|
127
|
+
### Offline start
|
|
128
|
+
|
|
129
|
+
When the app has offline start on (dashboard: Security -> Availability), a device that signed in
|
|
130
|
+
online can start without internet for the number of days the app allows. Nothing changes in your
|
|
131
|
+
code: `init()` and `login()` succeed offline as they do online. Show it if you like:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
state = shieldy.connection_state()
|
|
135
|
+
if state.offline:
|
|
136
|
+
show_banner(f"Offline - works until {state.offline_until:%b %d}")
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Offline, `license()`, variables and Protected Strings work; files need a connection. The device
|
|
140
|
+
has to sign in with the same key it last used online. For a portable install, keep the lease next
|
|
141
|
+
to the app with `Config(..., lease_directory="leases")`. To test it without pulling the cable, set
|
|
142
|
+
`SHIELDY_DEV_FORCE_OFFLINE=1` with a development build of the runtime.
|
|
143
|
+
|
|
144
|
+
### Trying another key
|
|
145
|
+
|
|
146
|
+
When a key is refused, `login()` can be called again with another one, and `init()` works again
|
|
147
|
+
after `shutdown()`. Runtimes built before October 2026 allowed one sign-in per process (the server
|
|
148
|
+
ends the session at a refused key and they could not open another); with one of those, restart the
|
|
149
|
+
process to let the user try another key.
|
|
150
|
+
|
|
151
|
+
### Files and memory
|
|
152
|
+
|
|
153
|
+
`file_by_id()` returns a `SecureBytes`: a buffer the SDK zeroes when you call `wipe()`, when a
|
|
154
|
+
`with` block around it ends, or when it is garbage collected. Read it through the object
|
|
155
|
+
(`len()`, indexing, slicing, `view()`, `text()`); `bytes(data)` makes a copy that can never be
|
|
156
|
+
wiped. This is best effort, as it has to be in Python: copies the interpreter makes on its own
|
|
157
|
+
(a `str` from `text()`, for instance) are out of the SDK's reach.
|
|
158
|
+
|
|
159
|
+
## Protected Strings in the build
|
|
160
|
+
|
|
161
|
+
Write secrets as `SHIELDY_OBF("...")` (double quotes) and keep coding against the plain text:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
import shieldy
|
|
165
|
+
from shieldy.obf import SHIELDY_OBF
|
|
166
|
+
|
|
167
|
+
def api_endpoint() -> str:
|
|
168
|
+
return SHIELDY_OBF("https://api.example.com/v2")
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Before a release, run the dashboard's CLI over each file. It rewrites every marker to
|
|
172
|
+
`shieldy.protected_string("<ciphertext>")`:
|
|
173
|
+
|
|
174
|
+
```powershell
|
|
175
|
+
python tools/shieldy_strings_cli.py --api-url https://<dashboard>/api --app-guid <App GUID> `
|
|
176
|
+
--api-key <key with apps:write> --file src/endpoints.py --language python --in-place
|
|
177
|
+
python -m shieldy.obf check src/ # exit code 1 while any marker is left
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Python has no compile step that could refuse a forgotten marker, so `SHIELDY_OBF` raises
|
|
181
|
+
`UnencryptedMarkerError` when it runs, unless `SHIELDY_OBF_PLAINTEXT=1` is set (do that while you
|
|
182
|
+
develop, never in a release), and `python -m shieldy.obf check` belongs in your release pipeline.
|
|
183
|
+
Read protected strings where they are used, after `login()`, never at module level.
|
|
184
|
+
See `examples/protected_strings_marker.py`.
|
|
185
|
+
|
|
186
|
+
## Examples
|
|
187
|
+
|
|
188
|
+
- `examples/basic.py`: the whole flow, configured from environment variables
|
|
189
|
+
(`SHIELDY_APP_GUID`, `SHIELDY_LICENSE_KEY`, optional `SHIELDY_VARIABLE_ID`, `SHIELDY_FILE_ID`,
|
|
190
|
+
`SHIELDY_PROTECTED_STRING`).
|
|
191
|
+
- `examples/protected_strings_marker.py`: the `SHIELDY_OBF` workflow.
|
|
192
|
+
|
|
193
|
+
## Working on this repository
|
|
194
|
+
|
|
195
|
+
```powershell
|
|
196
|
+
uv sync # Python 3.9+, creates .venv with the dev tools
|
|
197
|
+
uv run pytest # unit tests against a mock runtime, no server needed
|
|
198
|
+
uv run pytest -m e2e # a real runtime against a real server, see tests/e2e/README.md
|
|
199
|
+
uv run ruff check src tests examples
|
|
200
|
+
uv run mypy # strict
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
How the SDK works inside, and why it is built the way it is: [docs/architecture.md](docs/architecture.md).
|
|
204
|
+
A quick map of the repository, its commands and invariants: [CLAUDE.md](CLAUDE.md).
|
|
205
|
+
|
|
206
|
+
`third_party/shieldy_api.h` is a copy of ShieldyCore's `cpp-module/shieldy_api.h`, the ABI the
|
|
207
|
+
SDK mirrors in `src/shieldy/_abi.py`. `tests/test_abi.py` fails when the two disagree, and when the
|
|
208
|
+
copy differs from a ShieldyCore checkout next to this repository.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Shieldy Python SDK
|
|
2
|
+
|
|
3
|
+
The Python client for Shieldy's native runtime (`native.dll` on Windows, `native.so` on Linux).
|
|
4
|
+
It signs your application in with the end user's license key and reads what the dashboard
|
|
5
|
+
delivers to that license: the license itself, remote variables, files and Protected Strings.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
import shieldy
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
shieldy.start(shieldy.Config("<App GUID>", "1.0"), read_license_key_from_user())
|
|
12
|
+
except shieldy.ShieldyError as exc:
|
|
13
|
+
# exc.error is a shieldy.Error, e.g. Error.AUTHENTICATION_FAILED
|
|
14
|
+
raise SystemExit(f"Could not sign in: {exc.error}")
|
|
15
|
+
|
|
16
|
+
lic = shieldy.license()
|
|
17
|
+
if lic.level >= 20:
|
|
18
|
+
enable_pro_features()
|
|
19
|
+
|
|
20
|
+
api_url = shieldy.variable_by_id("<Lookup ID>") # str
|
|
21
|
+
max_users = shieldy.variable_by_id_as("<Lookup ID>", int) # int
|
|
22
|
+
with shieldy.file_by_id("<Lookup ID>") as pak: # SecureBytes, wiped after the block
|
|
23
|
+
load_assets(pak.view())
|
|
24
|
+
token = shieldy.protected_string("<ciphertext>") # str
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Python 3.9 or newer, 64-bit or 32-bit to match the runtime you ship. The only dependency is
|
|
28
|
+
[`cryptography`](https://cryptography.io).
|
|
29
|
+
|
|
30
|
+
## Installing
|
|
31
|
+
|
|
32
|
+
```powershell
|
|
33
|
+
pip install shieldy-sdk # once published
|
|
34
|
+
pip install path/to/ShieldyPythonSdk # from a checkout
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Ship the runtime with your application. Download it from the dashboard (your app -> Activity ->
|
|
38
|
+
Runtime -> "Download the SDK"), and put it next to your entry script or your frozen executable.
|
|
39
|
+
|
|
40
|
+
## Where the runtime is loaded from
|
|
41
|
+
|
|
42
|
+
The SDK loads exactly one file, chosen in this order:
|
|
43
|
+
|
|
44
|
+
1. `Config.native_library`, if you set it;
|
|
45
|
+
2. the `SHIELDY_NATIVE_LIBRARY` environment variable;
|
|
46
|
+
3. `native.dll` (`native.so` on Linux).
|
|
47
|
+
|
|
48
|
+
A relative path is resolved against your application's directory, **never** the current working
|
|
49
|
+
directory or `PATH`, so a library planted where the user starts your program is not picked up.
|
|
50
|
+
Your application's directory is:
|
|
51
|
+
|
|
52
|
+
| How the app runs | Directory |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `python app.py` | the directory of `app.py` |
|
|
55
|
+
| `python -m package` | the directory of `package/__main__.py` |
|
|
56
|
+
| PyInstaller, cx_Freeze, py2exe | the directory of the executable, then PyInstaller's bundle directory (`sys._MEIPASS`) |
|
|
57
|
+
| interactive interpreter, `python -c` | none: give an absolute path |
|
|
58
|
+
|
|
59
|
+
`shieldy.application_directory()` shows what the SDK settled on.
|
|
60
|
+
|
|
61
|
+
With PyInstaller, either copy the runtime next to the built executable, or bundle it:
|
|
62
|
+
`pyinstaller --add-binary "native.dll;." app.py` (`native.so:.` on Linux).
|
|
63
|
+
|
|
64
|
+
## API
|
|
65
|
+
|
|
66
|
+
All calls are module-level functions on the one session the process has. They are thread-safe.
|
|
67
|
+
|
|
68
|
+
| Call | Returns |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `init(config)` | Loads the runtime and runs its protected start-up (`/initialize` on the server). |
|
|
71
|
+
| `login(key)` | Signs in with the license key. |
|
|
72
|
+
| `start(config, key)` | `init` then `login`. |
|
|
73
|
+
| `shutdown()` | Ends the session. Also runs automatically when the interpreter exits. |
|
|
74
|
+
| `is_logged_in()` | `bool` |
|
|
75
|
+
| `supports(Feature.LOOKUP_IDS)` | Whether the runtime has the `*_by_id` calls (protocol 1.6.0+). |
|
|
76
|
+
| `supports(Feature.OFFLINE_START)` | Whether the runtime has `connection_state()` (protocol 1.12.0+). |
|
|
77
|
+
| `connection_state()` | `ConnectionState`: `offline` (`True` when this run started without a connection), `offline_until` (until when this device can start offline; `None`: it cannot). |
|
|
78
|
+
| `license()` | `License`: `level`, `hwid_seats_used`, `hwid_seats_total`, `created`, `expires` (`None`: never), `is_expired()`, `days_left()`. Read once at login. |
|
|
79
|
+
| `variable_by_id(id)` / `variable(name)` | `str` |
|
|
80
|
+
| `variable_by_id_as(id, type)` / `variable_as(name, type)` | `int`, `float`, `bool` or `str` |
|
|
81
|
+
| `file_by_id(id)` / `file(name)` | `SecureBytes`, wiped from memory when done |
|
|
82
|
+
| `protected_string(ciphertext)` | `str`, cached for the rest of the session |
|
|
83
|
+
|
|
84
|
+
Prefer the `*_by_id` calls: a lookup id is random, a name ends up readable in your code. A
|
|
85
|
+
variable whose name is hidden from the client (dashboard: "Hide name from client") can only be
|
|
86
|
+
read by id.
|
|
87
|
+
|
|
88
|
+
`bool` variables accept `true`, `false`, `1`, `0`. Numbers follow the C++ SDK's rules: surrounding
|
|
89
|
+
whitespace is ignored, a leading `+`, digit separators (`1_000`) and hex are not accepted.
|
|
90
|
+
|
|
91
|
+
### Errors
|
|
92
|
+
|
|
93
|
+
Every call raises `shieldy.ShieldyError` when it cannot deliver; `exc.error` says why. The codes
|
|
94
|
+
have the same numbers as the C++ SDK's `shieldy::Error`.
|
|
95
|
+
|
|
96
|
+
| `Error` | Meaning |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `NATIVE_LIBRARY_NOT_FOUND` | No runtime at the resolved path, or one for the wrong architecture. |
|
|
99
|
+
| `NATIVE_LIBRARY_INVALID` | The file loaded but is not a Shieldy runtime. |
|
|
100
|
+
| `INITIALIZATION_FAILED` | The server refused to start the app: unknown App GUID, suspended app, outdated version, unregistered runtime build, maintenance. |
|
|
101
|
+
| `AUTHENTICATION_FAILED` | The key was refused: wrong, expired, banned, or no free device seat. The server does not say which. |
|
|
102
|
+
| `NOT_INITIALIZED`, `NOT_LOGGED_IN` | A call came too early. |
|
|
103
|
+
| `NOT_FOUND` | No variable with that id or name for this license. |
|
|
104
|
+
| `NATIVE_TOO_OLD` | The runtime predates the call; see `supports()`. |
|
|
105
|
+
| `ATTESTATION_FAILED`, `KEY_EXCHANGE_FAILED`, `DECRYPT_FAILED` | The runtime could not prove it holds the session key, or a value did not decrypt. Treat as tampering. |
|
|
106
|
+
| `INVALID_VALUE` | A variable did not parse as the requested type. |
|
|
107
|
+
| `SERVER_UNREACHABLE` | No connection, and this device cannot start offline (offline start is off, it never signed in online here, or that was with another key). |
|
|
108
|
+
| `OFFLINE_EXPIRED` | No connection, and this device's offline time (or the license) has run out. |
|
|
109
|
+
| `CLOCK_INVALID` | No connection, and the system clock reads earlier than the last run. |
|
|
110
|
+
| `OFFLINE_UNAVAILABLE` | The call needs a connection and the app is running offline (files). |
|
|
111
|
+
|
|
112
|
+
The full list is in `src/shieldy/errors.py`.
|
|
113
|
+
|
|
114
|
+
### Offline start
|
|
115
|
+
|
|
116
|
+
When the app has offline start on (dashboard: Security -> Availability), a device that signed in
|
|
117
|
+
online can start without internet for the number of days the app allows. Nothing changes in your
|
|
118
|
+
code: `init()` and `login()` succeed offline as they do online. Show it if you like:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
state = shieldy.connection_state()
|
|
122
|
+
if state.offline:
|
|
123
|
+
show_banner(f"Offline - works until {state.offline_until:%b %d}")
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Offline, `license()`, variables and Protected Strings work; files need a connection. The device
|
|
127
|
+
has to sign in with the same key it last used online. For a portable install, keep the lease next
|
|
128
|
+
to the app with `Config(..., lease_directory="leases")`. To test it without pulling the cable, set
|
|
129
|
+
`SHIELDY_DEV_FORCE_OFFLINE=1` with a development build of the runtime.
|
|
130
|
+
|
|
131
|
+
### Trying another key
|
|
132
|
+
|
|
133
|
+
When a key is refused, `login()` can be called again with another one, and `init()` works again
|
|
134
|
+
after `shutdown()`. Runtimes built before October 2026 allowed one sign-in per process (the server
|
|
135
|
+
ends the session at a refused key and they could not open another); with one of those, restart the
|
|
136
|
+
process to let the user try another key.
|
|
137
|
+
|
|
138
|
+
### Files and memory
|
|
139
|
+
|
|
140
|
+
`file_by_id()` returns a `SecureBytes`: a buffer the SDK zeroes when you call `wipe()`, when a
|
|
141
|
+
`with` block around it ends, or when it is garbage collected. Read it through the object
|
|
142
|
+
(`len()`, indexing, slicing, `view()`, `text()`); `bytes(data)` makes a copy that can never be
|
|
143
|
+
wiped. This is best effort, as it has to be in Python: copies the interpreter makes on its own
|
|
144
|
+
(a `str` from `text()`, for instance) are out of the SDK's reach.
|
|
145
|
+
|
|
146
|
+
## Protected Strings in the build
|
|
147
|
+
|
|
148
|
+
Write secrets as `SHIELDY_OBF("...")` (double quotes) and keep coding against the plain text:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
import shieldy
|
|
152
|
+
from shieldy.obf import SHIELDY_OBF
|
|
153
|
+
|
|
154
|
+
def api_endpoint() -> str:
|
|
155
|
+
return SHIELDY_OBF("https://api.example.com/v2")
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Before a release, run the dashboard's CLI over each file. It rewrites every marker to
|
|
159
|
+
`shieldy.protected_string("<ciphertext>")`:
|
|
160
|
+
|
|
161
|
+
```powershell
|
|
162
|
+
python tools/shieldy_strings_cli.py --api-url https://<dashboard>/api --app-guid <App GUID> `
|
|
163
|
+
--api-key <key with apps:write> --file src/endpoints.py --language python --in-place
|
|
164
|
+
python -m shieldy.obf check src/ # exit code 1 while any marker is left
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Python has no compile step that could refuse a forgotten marker, so `SHIELDY_OBF` raises
|
|
168
|
+
`UnencryptedMarkerError` when it runs, unless `SHIELDY_OBF_PLAINTEXT=1` is set (do that while you
|
|
169
|
+
develop, never in a release), and `python -m shieldy.obf check` belongs in your release pipeline.
|
|
170
|
+
Read protected strings where they are used, after `login()`, never at module level.
|
|
171
|
+
See `examples/protected_strings_marker.py`.
|
|
172
|
+
|
|
173
|
+
## Examples
|
|
174
|
+
|
|
175
|
+
- `examples/basic.py`: the whole flow, configured from environment variables
|
|
176
|
+
(`SHIELDY_APP_GUID`, `SHIELDY_LICENSE_KEY`, optional `SHIELDY_VARIABLE_ID`, `SHIELDY_FILE_ID`,
|
|
177
|
+
`SHIELDY_PROTECTED_STRING`).
|
|
178
|
+
- `examples/protected_strings_marker.py`: the `SHIELDY_OBF` workflow.
|
|
179
|
+
|
|
180
|
+
## Working on this repository
|
|
181
|
+
|
|
182
|
+
```powershell
|
|
183
|
+
uv sync # Python 3.9+, creates .venv with the dev tools
|
|
184
|
+
uv run pytest # unit tests against a mock runtime, no server needed
|
|
185
|
+
uv run pytest -m e2e # a real runtime against a real server, see tests/e2e/README.md
|
|
186
|
+
uv run ruff check src tests examples
|
|
187
|
+
uv run mypy # strict
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
How the SDK works inside, and why it is built the way it is: [docs/architecture.md](docs/architecture.md).
|
|
191
|
+
A quick map of the repository, its commands and invariants: [CLAUDE.md](CLAUDE.md).
|
|
192
|
+
|
|
193
|
+
`third_party/shieldy_api.h` is a copy of ShieldyCore's `cpp-module/shieldy_api.h`, the ABI the
|
|
194
|
+
SDK mirrors in `src/shieldy/_abi.py`. `tests/test_abi.py` fails when the two disagree, and when the
|
|
195
|
+
copy differs from a ShieldyCore checkout next to this repository.
|