ms-python-utils 1.0.1__cp311-cp311-macosx_11_0_arm64.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,260 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
ms_python_utils - Universal Encrypted Payload Loader
|
|
4
|
+
|
|
5
|
+
Provides transparent loading of AES-256-GCM encrypted Python modules via import hook.
|
|
6
|
+
Odoo-compatible version: does NOT auto-install on import.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
import ms_python_utils
|
|
10
|
+
ms_python_utils.bootstrap()
|
|
11
|
+
|
|
12
|
+
# 密钥通过 key 文件交付(原生层直接读取,不经过 Python、不出现在代码里):
|
|
13
|
+
# 把 ms_python_utils.key 放在加密模块目录内 —— 与 .ylp 同目录,
|
|
14
|
+
# 即引导 __init__.py 所在的当前目录,import 加密模块时自动发现。
|
|
15
|
+
# 也可放上级目录(addons 根,一个 key 管全部模块),或用环境变量
|
|
16
|
+
# MSPU_KEY_FILE 指定 key 文件路径。
|
|
17
|
+
|
|
18
|
+
# 兼容旧契约:显式传入密钥(原生可用时密钥仍只存 C++ 层)
|
|
19
|
+
ms_python_utils.YlpLoader.set_master_secret(b"your-secret")
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import importlib
|
|
23
|
+
import sys
|
|
24
|
+
import zlib
|
|
25
|
+
from importlib.machinery import SOURCE_SUFFIXES, FileFinder, SourceFileLoader
|
|
26
|
+
from typing import Any, Dict
|
|
27
|
+
|
|
28
|
+
__version__ = "1.0.1"
|
|
29
|
+
import ms_python_utils_core # 原生加密内核:YLP 解密 + 密钥托管(必须在其它导入之前执行)
|
|
30
|
+
|
|
31
|
+
# ------------------------------------------------------------------
|
|
32
|
+
# C++ API wrappers (with pure-Python fallback)
|
|
33
|
+
# ------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
from ms_python_utils._core import (
|
|
37
|
+
encrypt_payload as _encrypt_payload,
|
|
38
|
+
decrypt_payload as _decrypt_payload,
|
|
39
|
+
derive_key as _derive_key,
|
|
40
|
+
exec_payload as _exec_payload,
|
|
41
|
+
decrypt_ylp_native as _decrypt_ylp_native,
|
|
42
|
+
set_master_secret as _set_master_secret_native,
|
|
43
|
+
)
|
|
44
|
+
except ImportError:
|
|
45
|
+
_encrypt_payload = None
|
|
46
|
+
_decrypt_payload = None
|
|
47
|
+
_derive_key = None
|
|
48
|
+
_exec_payload = None
|
|
49
|
+
_decrypt_ylp_native = None
|
|
50
|
+
_set_master_secret_native = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _get_fallback():
|
|
54
|
+
"""Lazy import pure-Python fallback."""
|
|
55
|
+
from .crypto_fallback import encrypt_payload, decrypt_payload, derive_key, exec_payload
|
|
56
|
+
return encrypt_payload, decrypt_payload, derive_key, exec_payload
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def encrypt_payload(plaintext: bytes, key: bytes, nonce: bytes) -> bytes:
|
|
60
|
+
"""Encrypt plaintext with AES-256-GCM."""
|
|
61
|
+
if _encrypt_payload is not None:
|
|
62
|
+
return _encrypt_payload(plaintext, key, nonce)
|
|
63
|
+
ep, _, _, _ = _get_fallback()
|
|
64
|
+
return ep(plaintext, key, nonce)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def decrypt_payload(ciphertext: bytes, key: bytes, nonce: bytes) -> bytes:
|
|
68
|
+
"""Decrypt an AES-256-GCM payload."""
|
|
69
|
+
if _decrypt_payload is not None:
|
|
70
|
+
return _decrypt_payload(ciphertext, key, nonce)
|
|
71
|
+
_, dp, _, _ = _get_fallback()
|
|
72
|
+
return dp(ciphertext, key, nonce)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def derive_key(master_secret: bytes, module_name: str, salt: bytes) -> bytes:
|
|
76
|
+
"""Derive a per-module key via PBKDF2-HMAC-SHA256."""
|
|
77
|
+
if _derive_key is not None:
|
|
78
|
+
return _derive_key(master_secret, module_name, salt)
|
|
79
|
+
_, _, dk, _ = _get_fallback()
|
|
80
|
+
return dk(master_secret, module_name, salt)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def exec_payload(source: str, globals_dict: Dict[str, Any], locals_dict: Dict[str, Any]) -> Any:
|
|
84
|
+
"""Execute Python source code in the given globals/locals."""
|
|
85
|
+
if _exec_payload is not None:
|
|
86
|
+
return _exec_payload(source, globals_dict, locals_dict)
|
|
87
|
+
_, _, _, ep = _get_fallback()
|
|
88
|
+
return ep(source, globals_dict, locals_dict)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ------------------------------------------------------------------
|
|
92
|
+
# YLP file parsing
|
|
93
|
+
# ------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
YLP_MAGIC = b"YLP\x01"
|
|
96
|
+
YLP_VERSION = 1
|
|
97
|
+
YLP_HEADER_SIZE = 4 + 1 + 32 + 12 # magic + version + salt + nonce
|
|
98
|
+
YLP_PAYLOAD_MAGIC = b"YSRC\x01" # 载荷魔数:'YSRC' + 1 字节格式版本(与 payload_generator 一致)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def parse_ylp(data: bytes) -> tuple:
|
|
102
|
+
"""Parse a .ylp file header and return (salt, nonce, ciphertext).
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
ValueError: If the file is invalid.
|
|
106
|
+
"""
|
|
107
|
+
if len(data) < YLP_HEADER_SIZE + 16:
|
|
108
|
+
raise ValueError("YLP file too small")
|
|
109
|
+
|
|
110
|
+
magic = data[:4]
|
|
111
|
+
if magic != YLP_MAGIC:
|
|
112
|
+
raise ValueError(f"Invalid YLP magic: {magic!r}")
|
|
113
|
+
|
|
114
|
+
version = data[4]
|
|
115
|
+
if version != YLP_VERSION:
|
|
116
|
+
raise ValueError(f"Unsupported YLP version: {version}")
|
|
117
|
+
|
|
118
|
+
salt = data[5:37]
|
|
119
|
+
nonce = data[37:49]
|
|
120
|
+
ciphertext = data[49:]
|
|
121
|
+
return salt, nonce, ciphertext
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def decrypt_ylp(data: bytes, master_secret: bytes, module_name: str) -> bytes:
|
|
125
|
+
"""Fully decrypt a .ylp payload and return the plaintext payload.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
data: Raw .ylp file contents.
|
|
129
|
+
master_secret: Master secret for key derivation.
|
|
130
|
+
module_name: Module name for key derivation context.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Decrypted plaintext payload bytes (YLP_PAYLOAD_MAGIC + zlib-compressed source).
|
|
134
|
+
"""
|
|
135
|
+
salt, nonce, ciphertext = parse_ylp(data)
|
|
136
|
+
key = derive_key(master_secret, module_name, salt)
|
|
137
|
+
return decrypt_payload(ciphertext, key, nonce)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ------------------------------------------------------------------
|
|
141
|
+
# Import Hook (inspired by pyconcrete's PyeLoader)
|
|
142
|
+
# ------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
class YlpLoader(SourceFileLoader):
|
|
145
|
+
"""Import loader for .ylp encrypted files.
|
|
146
|
+
|
|
147
|
+
When Python tries to import a module and finds a .ylp file, this loader
|
|
148
|
+
decrypts it, compiles the embedded (zlib-compressed) source with the
|
|
149
|
+
running interpreter, and returns the code object for execution.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
_master_secret: bytes = None
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def set_master_secret(cls, secret: bytes):
|
|
156
|
+
"""Set the master secret used for decrypting all .ylp files.
|
|
157
|
+
|
|
158
|
+
With the native core available the secret is handed to the C++ layer
|
|
159
|
+
and is not retained on the Python side.
|
|
160
|
+
"""
|
|
161
|
+
if _set_master_secret_native is not None:
|
|
162
|
+
_set_master_secret_native(secret)
|
|
163
|
+
cls._master_secret = None
|
|
164
|
+
else:
|
|
165
|
+
cls._master_secret = secret
|
|
166
|
+
|
|
167
|
+
def get_code(self, fullname):
|
|
168
|
+
path = self.get_filename(fullname)
|
|
169
|
+
if not path.endswith(".ylp"):
|
|
170
|
+
return super().get_code(fullname)
|
|
171
|
+
|
|
172
|
+
data = self.get_data(path)
|
|
173
|
+
if _decrypt_ylp_native is not None:
|
|
174
|
+
# 原生路径:master secret 存在 C++ 核心内,不回流 Python;
|
|
175
|
+
# 传 path 供原生侧 key 文件自动发现(MSPU_KEY_FILE → 同目录 → 上级目录)。
|
|
176
|
+
try:
|
|
177
|
+
plaintext = _decrypt_ylp_native(data, fullname, path)
|
|
178
|
+
except TypeError as sig_error:
|
|
179
|
+
# 旧版原生核心无 source_path 参数(无 key 文件自动发现)
|
|
180
|
+
try:
|
|
181
|
+
plaintext = _decrypt_ylp_native(data, fullname)
|
|
182
|
+
except Exception:
|
|
183
|
+
raise sig_error
|
|
184
|
+
except Exception as first_error:
|
|
185
|
+
# Backward compatibility: payloads generated before the __init__
|
|
186
|
+
# key-derivation fix used '<pkg>.__init__' as the module name for a
|
|
187
|
+
# package's __init__.py. Retry with that name; if it also fails,
|
|
188
|
+
# re-raise the original error.
|
|
189
|
+
try:
|
|
190
|
+
plaintext = _decrypt_ylp_native(data, fullname + '.__init__', path)
|
|
191
|
+
except Exception:
|
|
192
|
+
raise first_error
|
|
193
|
+
else:
|
|
194
|
+
if self._master_secret is None:
|
|
195
|
+
raise RuntimeError(
|
|
196
|
+
"ms_python_utils master secret not set. "
|
|
197
|
+
"Call ms_python_utils.YlpLoader.set_master_secret(secret) before importing encrypted modules."
|
|
198
|
+
)
|
|
199
|
+
try:
|
|
200
|
+
plaintext = decrypt_ylp(data, self._master_secret, fullname)
|
|
201
|
+
except Exception as first_error:
|
|
202
|
+
# Backward compatibility: payloads generated before the __init__
|
|
203
|
+
# key-derivation fix used '<pkg>.__init__' as the module name for a
|
|
204
|
+
# package's __init__.py. Retry with that name; if it also fails,
|
|
205
|
+
# re-raise the original error.
|
|
206
|
+
try:
|
|
207
|
+
plaintext = decrypt_ylp(data, self._master_secret, fullname + '.__init__')
|
|
208
|
+
except Exception:
|
|
209
|
+
raise first_error
|
|
210
|
+
|
|
211
|
+
# 载荷为解释器无关格式:YSRC 魔数 + zlib 压缩的 UTF-8 源码,
|
|
212
|
+
# 用当前解释器现场 compile —— 因此一个 .ylp 任何 Python 3.x 都能导入
|
|
213
|
+
if not plaintext.startswith(YLP_PAYLOAD_MAGIC):
|
|
214
|
+
raise ValueError(f"Unsupported YLP payload format in encrypted module {fullname}")
|
|
215
|
+
source = zlib.decompress(plaintext[len(YLP_PAYLOAD_MAGIC):]).decode("utf-8")
|
|
216
|
+
return compile(source, path, "exec")
|
|
217
|
+
|
|
218
|
+
def get_source(self, fullname):
|
|
219
|
+
path = self.get_filename(fullname)
|
|
220
|
+
if path.endswith(".ylp"):
|
|
221
|
+
return None
|
|
222
|
+
return super().get_source(fullname)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# ------------------------------------------------------------------
|
|
226
|
+
# Install hook into sys.path_hooks
|
|
227
|
+
# ------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
_bootstrapped = False
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def bootstrap():
|
|
233
|
+
"""Bootstrap ms_python_utils: install the YlpLoader into Python's import machinery.
|
|
234
|
+
|
|
235
|
+
This makes .ylp files importable alongside .py files.
|
|
236
|
+
Call this before importing encrypted modules. The master secret comes
|
|
237
|
+
from native key-file auto-discovery (ms_python_utils.key next to the
|
|
238
|
+
module or in its parent directory, or the MSPU_KEY_FILE env var), or
|
|
239
|
+
from a legacy explicit set_master_secret call.
|
|
240
|
+
|
|
241
|
+
Safe to call multiple times — only bootstraps once.
|
|
242
|
+
"""
|
|
243
|
+
global _bootstrapped
|
|
244
|
+
if _bootstrapped:
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
from importlib._bootstrap_external import _get_supported_file_loaders
|
|
248
|
+
|
|
249
|
+
# Register .ylp as a source suffix so inspect.getmodulename recognises it
|
|
250
|
+
if ".ylp" not in SOURCE_SUFFIXES:
|
|
251
|
+
SOURCE_SUFFIXES.append(".ylp")
|
|
252
|
+
|
|
253
|
+
loader_details = [(YlpLoader, [".ylp"])] + _get_supported_file_loaders()
|
|
254
|
+
|
|
255
|
+
sys.path_importer_cache.clear()
|
|
256
|
+
sys.path_hooks.insert(0, FileFinder.path_hook(*loader_details))
|
|
257
|
+
# Drop directory-listing caches too: a .ylp written after a previous
|
|
258
|
+
# finder pass would otherwise stay invisible on the same sys.path entry.
|
|
259
|
+
importlib.invalidate_caches()
|
|
260
|
+
_bootstrapped = True
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ms_python_utils
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Universal YLP encrypted payload loader (C++ protected)
|
|
5
|
+
Author: YLHC Technology, co., ltd.
|
|
6
|
+
Author-email: ylhc@gmail.com
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Dynamic: author
|
|
9
|
+
Dynamic: author-email
|
|
10
|
+
Dynamic: requires-python
|
|
11
|
+
Dynamic: summary
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
ms_python_utils_core.cpython-311-darwin.so,sha256=9xSvSYrXbv8cuuki1SwY3rLLrkDyjHw1YkWEPCVNh20,254008
|
|
2
|
+
ms_python_utils/__init__.py,sha256=xmsbUYwZHd3eTmU68YJZvJD_jZPADD59w6KcPyAr3XU,10160
|
|
3
|
+
ms_python_utils-1.0.1.dist-info/RECORD,,
|
|
4
|
+
ms_python_utils-1.0.1.dist-info/WHEEL,sha256=wdaoK8uQV7re3GGdks5HutT8_E2ub-nE_Mj1xJPmf6w,136
|
|
5
|
+
ms_python_utils-1.0.1.dist-info/top_level.txt,sha256=YDf0lufiRjwwG9fllpxG4ezlD5YMUsAJbxHGElwpcSQ,37
|
|
6
|
+
ms_python_utils-1.0.1.dist-info/METADATA,sha256=5lcnppqXBLPbBfeXv9UUt_0lavWO4VIXwAOLGsf2Qb4,291
|
|
Binary file
|