shieldy-sdk 1.0.2__py3-none-win_amd64.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.
- shieldy/__init__.py +88 -0
- shieldy/_abi.py +120 -0
- shieldy/_crypto.py +142 -0
- shieldy/_native.py +128 -0
- shieldy/_session.py +491 -0
- shieldy/errors.py +83 -0
- shieldy/native.dll +0 -0
- shieldy/obf.py +195 -0
- shieldy/py.typed +0 -0
- shieldy/values.py +176 -0
- shieldy_sdk-1.0.2.dist-info/METADATA +208 -0
- shieldy_sdk-1.0.2.dist-info/RECORD +13 -0
- shieldy_sdk-1.0.2.dist-info/WHEEL +5 -0
shieldy/__init__.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Shieldy Python SDK: license checks, remote variables, files and Protected Strings.
|
|
2
|
+
|
|
3
|
+
One process holds one Shieldy session, because the native runtime does: the calls below are
|
|
4
|
+
module-level functions on that session and are safe to make from any thread.
|
|
5
|
+
|
|
6
|
+
import shieldy
|
|
7
|
+
|
|
8
|
+
shieldy.start(shieldy.Config("<App GUID>", "1.0"), license_key) # raises shieldy.ShieldyError
|
|
9
|
+
|
|
10
|
+
lic = shieldy.license()
|
|
11
|
+
if lic.level >= 20:
|
|
12
|
+
enable_pro_features()
|
|
13
|
+
|
|
14
|
+
url = shieldy.variable_by_id("<Lookup ID>") # str
|
|
15
|
+
max_users = shieldy.variable_by_id_as("<Lookup ID>", int) # int
|
|
16
|
+
with shieldy.file_by_id("<Lookup ID>") as pak: # SecureBytes, wiped after
|
|
17
|
+
load(pak.view())
|
|
18
|
+
token = shieldy.protected_string("<ciphertext>")
|
|
19
|
+
|
|
20
|
+
Every call raises :class:`ShieldyError` when it cannot deliver; ``exc.error`` is an
|
|
21
|
+
:class:`Error` code with the same numbers as the C++ SDK's ``shieldy::Error``.
|
|
22
|
+
|
|
23
|
+
The SDK is a thin client of the native runtime (``native.dll`` / ``native.so``), which does the
|
|
24
|
+
integrity checks and all network traffic. What happens in which module:
|
|
25
|
+
|
|
26
|
+
* ``_native`` - finding and loading the runtime
|
|
27
|
+
* ``_abi`` - the ctypes mirror of the runtime's C ABI (``shieldy_api.h``)
|
|
28
|
+
* ``_crypto`` - key agreement, attestation and decryption of what the runtime returns
|
|
29
|
+
* ``_session`` - the process-wide session and every public call
|
|
30
|
+
* ``obf`` - the ``SHIELDY_OBF`` marker and its release check
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from ._native import application_directory
|
|
34
|
+
from ._session import (
|
|
35
|
+
checkpoint,
|
|
36
|
+
checkpoint_failed,
|
|
37
|
+
connection_state,
|
|
38
|
+
file,
|
|
39
|
+
file_by_id,
|
|
40
|
+
init,
|
|
41
|
+
is_logged_in,
|
|
42
|
+
license,
|
|
43
|
+
login,
|
|
44
|
+
protected_bytes,
|
|
45
|
+
protected_int,
|
|
46
|
+
protected_string,
|
|
47
|
+
shutdown,
|
|
48
|
+
start,
|
|
49
|
+
supports,
|
|
50
|
+
variable,
|
|
51
|
+
variable_as,
|
|
52
|
+
variable_by_id,
|
|
53
|
+
variable_by_id_as,
|
|
54
|
+
)
|
|
55
|
+
from .errors import Error, ShieldyError
|
|
56
|
+
from .values import Config, ConnectionState, Feature, License, SecureBytes
|
|
57
|
+
|
|
58
|
+
__version__ = "1.0.0"
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
"Config",
|
|
62
|
+
"ConnectionState",
|
|
63
|
+
"Error",
|
|
64
|
+
"Feature",
|
|
65
|
+
"License",
|
|
66
|
+
"SecureBytes",
|
|
67
|
+
"ShieldyError",
|
|
68
|
+
"application_directory",
|
|
69
|
+
"checkpoint",
|
|
70
|
+
"checkpoint_failed",
|
|
71
|
+
"connection_state",
|
|
72
|
+
"file",
|
|
73
|
+
"file_by_id",
|
|
74
|
+
"init",
|
|
75
|
+
"is_logged_in",
|
|
76
|
+
"license",
|
|
77
|
+
"login",
|
|
78
|
+
"protected_bytes",
|
|
79
|
+
"protected_int",
|
|
80
|
+
"protected_string",
|
|
81
|
+
"shutdown",
|
|
82
|
+
"start",
|
|
83
|
+
"supports",
|
|
84
|
+
"variable",
|
|
85
|
+
"variable_as",
|
|
86
|
+
"variable_by_id",
|
|
87
|
+
"variable_by_id_as",
|
|
88
|
+
]
|
shieldy/_abi.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""ctypes mirror of ShieldyCore's public ABI, ``cpp-module/shieldy_api.h``.
|
|
2
|
+
|
|
3
|
+
The runtime exports exactly one symbol, ``SC_GetApi``, which fills an ``SC_ApiV1`` table of
|
|
4
|
+
function pointers. Everything else the SDK does goes through that table. The layout below must
|
|
5
|
+
match the header field for field: the table is read by offset, so a reordered or missing field
|
|
6
|
+
calls the wrong function.
|
|
7
|
+
|
|
8
|
+
Kept free of any SDK logic so it can be diffed against the C header on its own. When the header
|
|
9
|
+
gains fields, append them here in the same order (the header only ever appends after ``destroy``).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import ctypes
|
|
15
|
+
from ctypes import POINTER, Structure, c_char_p, c_int32, c_int64, c_size_t, c_uint8, c_uint32, c_void_p
|
|
16
|
+
|
|
17
|
+
SC_PUBLIC_KEY_SIZE = 32
|
|
18
|
+
SC_API_VERSION_1 = 1
|
|
19
|
+
|
|
20
|
+
# SC_Result. Only SC_OK is compared against here; the other codes are mapped onto
|
|
21
|
+
# shieldy.Error, which uses the same numbers for them (see errors.py).
|
|
22
|
+
SC_OK = 0
|
|
23
|
+
SC_RESULT_MIN_ERROR = 1 # SC_ERROR_UNKNOWN
|
|
24
|
+
SC_RESULT_MAX_ERROR = 17 # SC_ERROR_OFFLINE_UNAVAILABLE
|
|
25
|
+
|
|
26
|
+
# SC_Result is a C enum: int-sized on every platform the runtime ships for.
|
|
27
|
+
SC_Result = ctypes.c_int
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SC_PublicKey(Structure):
|
|
31
|
+
_fields_ = [("bytes", c_uint8 * SC_PUBLIC_KEY_SIZE)]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SC_Config(Structure):
|
|
35
|
+
# The caller sets struct_size = sizeof(SC_Config) before passing it to create(). A runtime
|
|
36
|
+
# before protocol 1.12.0 compares struct_size with its own, smaller sizeof and accepts it.
|
|
37
|
+
_fields_ = [
|
|
38
|
+
("struct_size", c_uint32),
|
|
39
|
+
("appGuid", c_char_p),
|
|
40
|
+
("appVersion", c_char_p),
|
|
41
|
+
("appMode", c_int32),
|
|
42
|
+
# Appended in protocol 1.12.0.
|
|
43
|
+
("leaseDirectory", c_char_p),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
SC_CONNECTION_MODE_ONLINE = 0
|
|
48
|
+
SC_CONNECTION_MODE_OFFLINE = 1
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class SC_ConnectionState(Structure):
|
|
52
|
+
# Protocol 1.12.0. The caller sets struct_size = sizeof(SC_ConnectionState).
|
|
53
|
+
_fields_ = [
|
|
54
|
+
("struct_size", c_uint32),
|
|
55
|
+
("mode", c_int32),
|
|
56
|
+
("offline_until", c_int64),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SC_Buffer(Structure):
|
|
61
|
+
# As input the caller owns `data`. As output the runtime allocates it and the caller hands it
|
|
62
|
+
# back through free_buffer(). Not NUL-terminated.
|
|
63
|
+
_fields_ = [
|
|
64
|
+
("data", POINTER(c_uint8)),
|
|
65
|
+
("size", c_size_t),
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SC_LoginRequest(Structure):
|
|
70
|
+
_fields_ = [
|
|
71
|
+
("licenseKey", c_char_p),
|
|
72
|
+
("sdkPublicKey", SC_PublicKey),
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# SC_Context is opaque: the SDK only ever passes the pointer back.
|
|
77
|
+
SC_ContextPtr = c_void_p
|
|
78
|
+
|
|
79
|
+
# Function pointer types of the table. All of them use the platform's default C calling
|
|
80
|
+
# convention (cdecl on x86), hence CFUNCTYPE rather than WINFUNCTYPE.
|
|
81
|
+
CreateFn = ctypes.CFUNCTYPE(SC_Result, POINTER(SC_Config), POINTER(SC_ContextPtr))
|
|
82
|
+
InitializeFn = ctypes.CFUNCTYPE(SC_Result, SC_ContextPtr)
|
|
83
|
+
LoginFn = ctypes.CFUNCTYPE(SC_Result, SC_ContextPtr, POINTER(SC_LoginRequest), POINTER(SC_PublicKey))
|
|
84
|
+
OpenBlackboxFn = ctypes.CFUNCTYPE(SC_Result, SC_ContextPtr, POINTER(SC_Buffer), POINTER(SC_Buffer))
|
|
85
|
+
# get_license_property, get_variable, deobf_string, download_file and the *_by_id pair all share
|
|
86
|
+
# this shape: (context, NUL-terminated key, out buffer).
|
|
87
|
+
LookupFn = ctypes.CFUNCTYPE(SC_Result, SC_ContextPtr, c_char_p, POINTER(SC_Buffer))
|
|
88
|
+
FreeBufferFn = ctypes.CFUNCTYPE(None, SC_ContextPtr, POINTER(SC_Buffer))
|
|
89
|
+
DestroyFn = ctypes.CFUNCTYPE(None, SC_ContextPtr)
|
|
90
|
+
ConnectionStateFn = ctypes.CFUNCTYPE(SC_Result, SC_ContextPtr, POINTER(SC_ConnectionState))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class SC_ApiV1(Structure):
|
|
94
|
+
# Field order is the ABI. The caller sets struct_size = sizeof(SC_ApiV1) before SC_GetApi.
|
|
95
|
+
_fields_ = [
|
|
96
|
+
("struct_size", c_uint32),
|
|
97
|
+
("abi_version", c_uint32),
|
|
98
|
+
("create", CreateFn),
|
|
99
|
+
("initialize", InitializeFn),
|
|
100
|
+
("login", LoginFn),
|
|
101
|
+
("open_blackbox", OpenBlackboxFn),
|
|
102
|
+
("get_license_property", LookupFn),
|
|
103
|
+
("get_variable", LookupFn),
|
|
104
|
+
("deobf_string", LookupFn),
|
|
105
|
+
("download_file", LookupFn),
|
|
106
|
+
("free_buffer", FreeBufferFn),
|
|
107
|
+
("destroy", DestroyFn),
|
|
108
|
+
# Appended in protocol 1.6.0. A runtime older than that leaves them NULL.
|
|
109
|
+
("get_variable_by_id", LookupFn),
|
|
110
|
+
("download_file_by_id", LookupFn),
|
|
111
|
+
# Appended in protocol 1.12.0 (offline start). NULL on older runtimes.
|
|
112
|
+
("get_connection_state", ConnectionStateFn),
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# The size of SC_ApiV1 as first shipped (up to and including `destroy`). SC_GetApi accepts any
|
|
117
|
+
# struct_size from this up, which is what lets a newer SDK run on an older runtime.
|
|
118
|
+
SC_API_V1_BASE_SIZE = SC_ApiV1.destroy.offset + ctypes.sizeof(DestroyFn)
|
|
119
|
+
|
|
120
|
+
GetApiFn = ctypes.CFUNCTYPE(SC_Result, c_uint32, POINTER(SC_ApiV1))
|
shieldy/_crypto.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""The SDK's half of a login: key agreement, attestation and unwrapping the runtime's output.
|
|
2
|
+
|
|
3
|
+
The wire format is fixed by ShieldyCore and mirrored from the C++ SDK (``src/session_crypto.cpp``):
|
|
4
|
+
|
|
5
|
+
* **Key agreement.** Each login generates a fresh X25519 key pair. The SDK sends its public key in
|
|
6
|
+
``SC_LoginRequest.sdkPublicKey``; the runtime answers with its own. Both sides compute the raw
|
|
7
|
+
X25519 shared secret and hash it with **plain SHA-256** (ShieldyCore ``crypto_ecdh.cpp``,
|
|
8
|
+
``compute_shared_secret``). That digest is the 32-byte session key.
|
|
9
|
+
* **Attestation.** Before trusting anything else, the SDK sends 16 random bytes to
|
|
10
|
+
``open_blackbox``; the runtime must answer ``HMAC-SHA256(session_key, nonce)``. A runtime that
|
|
11
|
+
cannot is not the one that did the key agreement.
|
|
12
|
+
* **Output wrapping.** Every value the runtime hands back (license fields, variables, files,
|
|
13
|
+
Protected Strings) is ``nonce(12) || tag(16) || ciphertext`` under ChaCha20-Poly1305 (IETF)
|
|
14
|
+
with the session key and no associated data. Note the tag sits *before* the ciphertext; most
|
|
15
|
+
libraries, ``cryptography`` included, expect it after, so :meth:`Session.decrypt` reorders.
|
|
16
|
+
|
|
17
|
+
Memory hygiene is best effort, as it has to be in Python: the session key lives in a
|
|
18
|
+
``bytearray`` that is zeroed when the session ends, and decrypted files are written straight into
|
|
19
|
+
a buffer :class:`~shieldy.SecureBytes` can wipe. Copies the interpreter or OpenSSL make
|
|
20
|
+
internally are out of the SDK's reach.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import hashlib
|
|
26
|
+
import hmac
|
|
27
|
+
import os
|
|
28
|
+
from typing import Optional
|
|
29
|
+
|
|
30
|
+
from cryptography.exceptions import InvalidTag
|
|
31
|
+
from cryptography.hazmat.primitives import serialization
|
|
32
|
+
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
|
|
33
|
+
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
|
|
34
|
+
|
|
35
|
+
from .errors import Error, ShieldyError
|
|
36
|
+
from .values import SecureBytes
|
|
37
|
+
|
|
38
|
+
KEY_SIZE = 32
|
|
39
|
+
CHALLENGE_NONCE_SIZE = 16 # the size open_blackbox accepts
|
|
40
|
+
NONCE_SIZE = 12
|
|
41
|
+
TAG_SIZE = 16
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def random_bytes(size: int) -> bytes:
|
|
45
|
+
return os.urandom(size)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def wipe(buffer: bytearray) -> None:
|
|
49
|
+
"""Overwrite a mutable buffer with zeros in place."""
|
|
50
|
+
buffer[:] = bytes(len(buffer))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Session:
|
|
54
|
+
"""One login's key material. A new :meth:`begin` drops the previous session entirely."""
|
|
55
|
+
|
|
56
|
+
def __init__(self) -> None:
|
|
57
|
+
self._private_key: Optional[X25519PrivateKey] = None
|
|
58
|
+
self._public_key = b""
|
|
59
|
+
self._session_key = bytearray()
|
|
60
|
+
self._aead: Optional[ChaCha20Poly1305] = None
|
|
61
|
+
|
|
62
|
+
def begin(self) -> bytes:
|
|
63
|
+
"""Generate a fresh key pair and return the public half to send to the runtime."""
|
|
64
|
+
self.clear()
|
|
65
|
+
self._private_key = X25519PrivateKey.generate()
|
|
66
|
+
self._public_key = self._private_key.public_key().public_bytes(
|
|
67
|
+
serialization.Encoding.Raw, serialization.PublicFormat.Raw
|
|
68
|
+
)
|
|
69
|
+
return self._public_key
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def public_key(self) -> bytes:
|
|
73
|
+
return self._public_key
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def has_key(self) -> bool:
|
|
77
|
+
return self._aead is not None
|
|
78
|
+
|
|
79
|
+
def derive(self, peer_public_key: bytes) -> bool:
|
|
80
|
+
"""Turn the runtime's public key into the session key. False when the exchange is invalid.
|
|
81
|
+
|
|
82
|
+
The private key is dropped either way: it is single use.
|
|
83
|
+
"""
|
|
84
|
+
private_key, self._private_key = self._private_key, None
|
|
85
|
+
if private_key is None or len(peer_public_key) != KEY_SIZE:
|
|
86
|
+
return False
|
|
87
|
+
try:
|
|
88
|
+
# Raises ValueError for a low-order peer key (all-zero shared secret), which is the
|
|
89
|
+
# same input libsodium's crypto_scalarmult refuses in the C++ SDK.
|
|
90
|
+
shared = bytearray(private_key.exchange(X25519PublicKey.from_public_bytes(peer_public_key)))
|
|
91
|
+
except ValueError:
|
|
92
|
+
return False
|
|
93
|
+
self._session_key = bytearray(hashlib.sha256(shared).digest())
|
|
94
|
+
wipe(shared)
|
|
95
|
+
self._aead = ChaCha20Poly1305(bytes(self._session_key))
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
def verify_challenge(self, nonce: bytes, response: bytes) -> bool:
|
|
99
|
+
"""Check ``response == HMAC-SHA256(session_key, nonce)`` in constant time."""
|
|
100
|
+
if not self.has_key or len(response) != hashlib.sha256().digest_size:
|
|
101
|
+
return False
|
|
102
|
+
expected = hmac.new(bytes(self._session_key), nonce, hashlib.sha256).digest()
|
|
103
|
+
return hmac.compare_digest(expected, response)
|
|
104
|
+
|
|
105
|
+
def decrypt(self, wrapped: bytes) -> SecureBytes:
|
|
106
|
+
"""Unwrap one value from the runtime into a wipeable buffer.
|
|
107
|
+
|
|
108
|
+
Raises ``ShieldyError(NOT_LOGGED_IN)`` without a session key and
|
|
109
|
+
``ShieldyError(DECRYPT_FAILED)`` when the value is malformed or fails authentication.
|
|
110
|
+
"""
|
|
111
|
+
if self._aead is None:
|
|
112
|
+
raise ShieldyError(Error.NOT_LOGGED_IN)
|
|
113
|
+
if len(wrapped) < NONCE_SIZE + TAG_SIZE:
|
|
114
|
+
raise ShieldyError(Error.DECRYPT_FAILED)
|
|
115
|
+
|
|
116
|
+
nonce = wrapped[:NONCE_SIZE]
|
|
117
|
+
tag = wrapped[NONCE_SIZE:NONCE_SIZE + TAG_SIZE]
|
|
118
|
+
ciphertext = wrapped[NONCE_SIZE + TAG_SIZE:]
|
|
119
|
+
plain = bytearray(len(ciphertext))
|
|
120
|
+
try:
|
|
121
|
+
self._decrypt_into(nonce, ciphertext + tag, plain)
|
|
122
|
+
except InvalidTag:
|
|
123
|
+
wipe(plain)
|
|
124
|
+
raise ShieldyError(Error.DECRYPT_FAILED) from None
|
|
125
|
+
return SecureBytes._adopt(plain)
|
|
126
|
+
|
|
127
|
+
def _decrypt_into(self, nonce: bytes, ciphertext_and_tag: bytes, out: bytearray) -> None:
|
|
128
|
+
assert self._aead is not None
|
|
129
|
+
decrypt_into = getattr(self._aead, "decrypt_into", None)
|
|
130
|
+
if decrypt_into is not None:
|
|
131
|
+
# cryptography >= 45: plaintext goes straight into our buffer, no immutable copy.
|
|
132
|
+
decrypt_into(nonce, ciphertext_and_tag, None, out)
|
|
133
|
+
else:
|
|
134
|
+
out[:] = self._aead.decrypt(nonce, ciphertext_and_tag, None)
|
|
135
|
+
|
|
136
|
+
def clear(self) -> None:
|
|
137
|
+
"""Forget everything; the next value needs a new login."""
|
|
138
|
+
self._private_key = None
|
|
139
|
+
self._public_key = b""
|
|
140
|
+
wipe(self._session_key)
|
|
141
|
+
self._session_key = bytearray()
|
|
142
|
+
self._aead = None
|
shieldy/_native.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Finding and loading the native runtime.
|
|
2
|
+
|
|
3
|
+
The runtime is a shared library (``native.dll`` on Windows, ``native.so`` on Linux) that your
|
|
4
|
+
application ships. Which file gets loaded is a security decision, so the rules are strict:
|
|
5
|
+
|
|
6
|
+
1. ``Config.native_library`` if set, else the ``SHIELDY_NATIVE_LIBRARY`` environment variable,
|
|
7
|
+
else the default file name.
|
|
8
|
+
2. A relative path is resolved against :func:`application_directory`, and then against the
|
|
9
|
+
package's own directory (:func:`bundled_directories`, where a wheel that bundles the runtime
|
|
10
|
+
puts it), **never** the current working directory or ``PATH``, so a library planted where the
|
|
11
|
+
user happens to start your program from is not picked up.
|
|
12
|
+
3. On Windows the library's own dependencies are searched only in its directory and System32
|
|
13
|
+
(``LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32``), as in the C++ SDK.
|
|
14
|
+
|
|
15
|
+
A loaded runtime is never unloaded: it keeps protection threads running for the life of the
|
|
16
|
+
process.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import ctypes
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
from typing import Any, List, Optional
|
|
25
|
+
|
|
26
|
+
from . import _abi
|
|
27
|
+
from .errors import Error, ShieldyError
|
|
28
|
+
|
|
29
|
+
ENVIRONMENT_VARIABLE = "SHIELDY_NATIVE_LIBRARY"
|
|
30
|
+
|
|
31
|
+
def _default_file_name(platform: str) -> str:
|
|
32
|
+
"""The runtime's file name on ``platform`` (a ``sys.platform`` value)."""
|
|
33
|
+
if platform == "win32":
|
|
34
|
+
return "native.dll"
|
|
35
|
+
if platform == "darwin":
|
|
36
|
+
return "native.dylib"
|
|
37
|
+
return "native.so"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
DEFAULT_FILE_NAME = _default_file_name(sys.platform)
|
|
41
|
+
|
|
42
|
+
_LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR = 0x00000100
|
|
43
|
+
_LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800
|
|
44
|
+
|
|
45
|
+
# Loaded libraries, kept referenced so ctypes never lets go of them.
|
|
46
|
+
_loaded: List[ctypes.CDLL] = []
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def application_directories() -> List[str]:
|
|
50
|
+
"""The directories a relative runtime path is looked up in, in order.
|
|
51
|
+
|
|
52
|
+
* A frozen application (PyInstaller, cx_Freeze, py2exe): the directory of the executable,
|
|
53
|
+
then PyInstaller's bundle directory (``sys._MEIPASS``) when it differs, which is where a
|
|
54
|
+
one-file build unpacks ``--add-binary`` files.
|
|
55
|
+
* A script: the directory of the ``__main__`` script, i.e. the file you ran with ``python``.
|
|
56
|
+
For ``python -m package`` that is the package's directory.
|
|
57
|
+
* No main script (interactive interpreter, ``python -c``, an embedding host): none. Pass an
|
|
58
|
+
absolute path then.
|
|
59
|
+
"""
|
|
60
|
+
if getattr(sys, "frozen", False):
|
|
61
|
+
directories = [os.path.dirname(os.path.abspath(sys.executable))]
|
|
62
|
+
bundle = getattr(sys, "_MEIPASS", None)
|
|
63
|
+
if bundle and os.path.abspath(bundle) not in directories:
|
|
64
|
+
directories.append(os.path.abspath(bundle))
|
|
65
|
+
return directories
|
|
66
|
+
|
|
67
|
+
main = sys.modules.get("__main__")
|
|
68
|
+
main_file = getattr(main, "__file__", None)
|
|
69
|
+
if main_file:
|
|
70
|
+
return [os.path.dirname(os.path.abspath(main_file))]
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def application_directory() -> Optional[str]:
|
|
75
|
+
"""The first of :func:`application_directories`, or ``None`` when there is none."""
|
|
76
|
+
directories = application_directories()
|
|
77
|
+
return directories[0] if directories else None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def bundled_directories() -> List[str]:
|
|
81
|
+
"""Install locations a relative runtime path falls back to, after the application directories.
|
|
82
|
+
|
|
83
|
+
The package's own directory: where a wheel that bundles the runtime (the per-platform releases
|
|
84
|
+
on PyPI) places ``native.dll`` / ``native.so`` / ``native.dylib``. This is the install location, not the current
|
|
85
|
+
working directory or ``PATH``, so it is as trustworthy as the application directory. A source
|
|
86
|
+
build with no bundled runtime simply has nothing here, and the application's own copy wins.
|
|
87
|
+
"""
|
|
88
|
+
return [os.path.dirname(os.path.abspath(__file__))]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def resolve(configured_path: str) -> Optional[str]:
|
|
92
|
+
"""The absolute path that :func:`load_get_api` would load, or ``None`` when it cannot tell."""
|
|
93
|
+
path = configured_path or os.environ.get(ENVIRONMENT_VARIABLE, "") or DEFAULT_FILE_NAME
|
|
94
|
+
if os.path.isabs(path):
|
|
95
|
+
return os.path.normpath(path)
|
|
96
|
+
candidates = [
|
|
97
|
+
os.path.normpath(os.path.join(directory, path))
|
|
98
|
+
for directory in application_directories() + bundled_directories()
|
|
99
|
+
]
|
|
100
|
+
for candidate in candidates:
|
|
101
|
+
if os.path.isfile(candidate):
|
|
102
|
+
return candidate
|
|
103
|
+
# Nothing exists yet: report the first candidate so the error names a useful place.
|
|
104
|
+
return candidates[0] if candidates else None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _open(path: str) -> ctypes.CDLL:
|
|
108
|
+
if sys.platform == "win32":
|
|
109
|
+
return ctypes.CDLL(path, winmode=_LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | _LOAD_LIBRARY_SEARCH_SYSTEM32)
|
|
110
|
+
return ctypes.CDLL(path, mode=os.RTLD_NOW | os.RTLD_LOCAL)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def load_get_api(configured_path: str) -> Any:
|
|
114
|
+
"""Load the runtime and return its ``SC_GetApi`` as a callable :data:`_abi.GetApiFn`."""
|
|
115
|
+
path = resolve(configured_path)
|
|
116
|
+
if path is None or not os.path.isfile(path):
|
|
117
|
+
raise ShieldyError(Error.NATIVE_LIBRARY_NOT_FOUND)
|
|
118
|
+
try:
|
|
119
|
+
library = _open(path)
|
|
120
|
+
except OSError:
|
|
121
|
+
# Also what a library for the wrong architecture (x86 DLL in 64-bit Python) ends up as.
|
|
122
|
+
raise ShieldyError(Error.NATIVE_LIBRARY_NOT_FOUND) from None
|
|
123
|
+
try:
|
|
124
|
+
get_api = _abi.GetApiFn(("SC_GetApi", library))
|
|
125
|
+
except AttributeError:
|
|
126
|
+
raise ShieldyError(Error.NATIVE_LIBRARY_INVALID) from None
|
|
127
|
+
_loaded.append(library)
|
|
128
|
+
return get_api
|