qyro-engine 0.1.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.
Files changed (40) hide show
  1. qyro/__init__.py +69 -0
  2. qyro/adapters/__init__.py +20 -0
  3. qyro/adapters/environment/__init__.py +3 -0
  4. qyro/adapters/environment/system_environment.py +57 -0
  5. qyro/adapters/platform/__init__.py +3 -0
  6. qyro/adapters/platform/detector.py +73 -0
  7. qyro/adapters/resources/__init__.py +3 -0
  8. qyro/adapters/resources/filesystem_resources.py +217 -0
  9. qyro/adapters/resources/protected_bundle.py +227 -0
  10. qyro/adapters/resources/runtime_secret.py +48 -0
  11. qyro/adapters/resources/secrets_crypto.py +133 -0
  12. qyro/adapters/settings/__init__.py +3 -0
  13. qyro/adapters/settings/json_settings.py +198 -0
  14. qyro/adapters/telemetry/__init__.py +4 -0
  15. qyro/adapters/telemetry/console_hook.py +38 -0
  16. qyro/adapters/telemetry/sentry_hook.py +51 -0
  17. qyro/application/__init__.py +26 -0
  18. qyro/application/ports/__init__.py +16 -0
  19. qyro/application/ports/environment.py +37 -0
  20. qyro/application/ports/resources.py +23 -0
  21. qyro/application/ports/settings.py +22 -0
  22. qyro/application/ports/telemetry.py +27 -0
  23. qyro/application/use_cases/__init__.py +13 -0
  24. qyro/application/use_cases/initialize_engine.py +41 -0
  25. qyro/application/use_cases/load_settings.py +25 -0
  26. qyro/application/use_cases/resolve_resource.py +30 -0
  27. qyro/client/__init__.py +3 -0
  28. qyro/client/development_changes.py +135 -0
  29. qyro/client/development_pydux.py +64 -0
  30. qyro/client/development_state.py +96 -0
  31. qyro/client/velto_context.py +520 -0
  32. qyro/container.py +66 -0
  33. qyro/domain/__init__.py +33 -0
  34. qyro/domain/entities.py +83 -0
  35. qyro/domain/errors.py +49 -0
  36. qyro_engine-0.1.0.dist-info/METADATA +351 -0
  37. qyro_engine-0.1.0.dist-info/RECORD +40 -0
  38. qyro_engine-0.1.0.dist-info/WHEEL +4 -0
  39. qyro_engine-0.1.0.dist-info/licenses/LICENSE +21 -0
  40. qyro_engine-0.1.0.dist-info/licenses/NOTICE +159 -0
qyro/__init__.py ADDED
@@ -0,0 +1,69 @@
1
+ """
2
+ Qyro.
3
+ Clean Architecture runtime engine and lifecycle framework for Python desktop GUI apps.
4
+ Decoupled from CLI tools, independent and multi-toolkit ready.
5
+ """
6
+
7
+ from qyro.client.velto_context import VeltoContext
8
+ from qyro.container import EngineContainer
9
+ from qyro.adapters.platform.detector import PlatformDetector
10
+ from qyro.domain.entities import PlatformType, ExecutionMode, AppMetadata
11
+ from qyro.domain.errors import (
12
+ QyroEngineError,
13
+ ResourceNotFoundError,
14
+ SettingsNotFoundError,
15
+ FrameworkNotAvailableError,
16
+ )
17
+
18
+ __version__ = "0.1.0"
19
+
20
+ # Module-level singleton container for quick procedural usage
21
+ _default_container: EngineContainer | None = None
22
+
23
+
24
+ def _get_default_container() -> EngineContainer:
25
+ global _default_container
26
+ app_container = VeltoContext._global_container
27
+ if app_container is not None:
28
+ _default_container = app_container
29
+ return app_container
30
+
31
+ if _default_container is None:
32
+ _default_container = EngineContainer()
33
+ return _default_container
34
+
35
+
36
+ def get_resource(*segments: str, required: bool = True) -> str:
37
+ """Convenience helper to resolve a resource path without a context instance."""
38
+ container = _get_default_container()
39
+ return str(container.resolve_resource_use_case.execute(*segments, required=required))
40
+
41
+
42
+ def load_build_settings() -> dict:
43
+ """Convenience helper to load build settings dictionary."""
44
+ container = _get_default_container()
45
+ return container.load_settings_use_case.execute().raw_settings
46
+
47
+
48
+ def is_frozen() -> bool:
49
+ """Convenience helper to check if running in a frozen executable."""
50
+ container = _get_default_container()
51
+ return container.env_adapter.is_frozen()
52
+
53
+
54
+
55
+ __all__ = [
56
+ "VeltoContext",
57
+ "EngineContainer",
58
+ "PlatformDetector",
59
+ "PlatformType",
60
+ "ExecutionMode",
61
+ "AppMetadata",
62
+ "QyroEngineError",
63
+ "ResourceNotFoundError",
64
+ "SettingsNotFoundError",
65
+ "FrameworkNotAvailableError",
66
+ "get_resource",
67
+ "load_build_settings",
68
+ "is_frozen",
69
+ ]
@@ -0,0 +1,20 @@
1
+ """
2
+ Qyro - Adapters Layer.
3
+ Concrete implementations of application ports.
4
+ """
5
+
6
+ from .environment.system_environment import SystemEnvironmentAdapter
7
+ from .settings.json_settings import JsonSettingsAdapter
8
+ from .resources.filesystem_resources import FileSystemResourceAdapter
9
+ from .telemetry.console_hook import ConsoleExceptionHookAdapter
10
+ from .telemetry.sentry_hook import SentryExceptionHookAdapter
11
+ from .platform.detector import PlatformDetector
12
+
13
+ __all__ = [
14
+ "SystemEnvironmentAdapter",
15
+ "JsonSettingsAdapter",
16
+ "FileSystemResourceAdapter",
17
+ "ConsoleExceptionHookAdapter",
18
+ "SentryExceptionHookAdapter",
19
+ "PlatformDetector",
20
+ ]
@@ -0,0 +1,3 @@
1
+ from .system_environment import SystemEnvironmentAdapter
2
+
3
+ __all__ = ["SystemEnvironmentAdapter"]
@@ -0,0 +1,57 @@
1
+ """
2
+ System Environment Adapter.
3
+ Implements IEnvironmentPort for PyInstaller, cx_Freeze, and Source environments.
4
+ """
5
+
6
+ import sys
7
+ from pathlib import Path
8
+ from qyro.application.ports.environment import IEnvironmentPort
9
+ from qyro.domain.entities import ExecutionMode, PlatformType
10
+ from qyro.adapters.platform.detector import PlatformDetector
11
+
12
+
13
+ class SystemEnvironmentAdapter(IEnvironmentPort):
14
+ """Detects runtime execution specifics across operating systems."""
15
+
16
+ def __init__(self, custom_root: Path | None = None) -> None:
17
+ self._custom_root = custom_root
18
+
19
+ def is_frozen(self) -> bool:
20
+ return getattr(sys, "frozen", False)
21
+
22
+ def get_execution_mode(self) -> ExecutionMode:
23
+ if self.is_frozen():
24
+ if hasattr(sys, "_MEIPASS"):
25
+ return ExecutionMode.FROZEN_PYINSTALLER
26
+ return ExecutionMode.FROZEN_GENERIC
27
+ return ExecutionMode.SOURCE
28
+
29
+ def get_platform(self) -> PlatformType:
30
+ return PlatformDetector.get_platform()
31
+
32
+ def get_root_dir(self) -> Path:
33
+ if self._custom_root:
34
+ return self._custom_root
35
+
36
+ if self.is_frozen():
37
+ # Executable directory
38
+ return Path(sys.executable).resolve().parent
39
+
40
+ # Source mode: find project root containing pyproject.toml or src/
41
+ cwd = Path.cwd()
42
+ for parent in [cwd] + list(cwd.parents):
43
+ if (parent / "pyproject.toml").exists() or (parent / "src").exists():
44
+ return parent
45
+ return cwd
46
+
47
+ def get_bundle_dir(self) -> Path:
48
+ if self.is_frozen() and hasattr(sys, "_MEIPASS"):
49
+ return Path(getattr(sys, "_MEIPASS")).resolve()
50
+
51
+ # macOS bundle check
52
+ if self.is_frozen() and sys.platform == "darwin":
53
+ mac_resources = Path(sys.executable).resolve().parent.parent / "Resources"
54
+ if mac_resources.exists():
55
+ return mac_resources
56
+
57
+ return self.get_root_dir()
@@ -0,0 +1,3 @@
1
+ from .detector import PlatformDetector
2
+
3
+ __all__ = ["PlatformDetector"]
@@ -0,0 +1,73 @@
1
+ """
2
+ Platform Detection Adapter.
3
+ Detects OS and desktop environment with fine-grained distro support.
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ from qyro.domain.entities import PlatformType
9
+
10
+
11
+ class PlatformDetector:
12
+ """Detects host operating system, architecture, and desktop flavors."""
13
+
14
+
15
+ @staticmethod
16
+ def get_platform() -> PlatformType:
17
+ """Return the platform type detected from the current operating system."""
18
+ if sys.platform.startswith("win"):
19
+ return PlatformType.WINDOWS
20
+ if sys.platform == "darwin":
21
+ return PlatformType.MACOS
22
+ if sys.platform.startswith("linux"):
23
+ return PlatformType.LINUX
24
+ if sys.platform.startswith("android"):
25
+ return PlatformType.ANDROID
26
+ return PlatformType.UNKNOWN
27
+
28
+ @classmethod
29
+ def is_windows(cls) -> bool:
30
+ return cls.get_platform() == PlatformType.WINDOWS
31
+
32
+ @classmethod
33
+ def is_mac(cls) -> bool:
34
+ return cls.get_platform() == PlatformType.MACOS
35
+
36
+ @classmethod
37
+ def is_linux(cls) -> bool:
38
+ return cls.get_platform() == PlatformType.LINUX
39
+
40
+ @classmethod
41
+ def is_ubuntu(cls) -> bool:
42
+ return cls._linux_distro_id() == "ubuntu"
43
+
44
+ @classmethod
45
+ def is_fedora(cls) -> bool:
46
+ return cls._linux_distro_id() == "fedora"
47
+
48
+ @classmethod
49
+ def is_arch(cls) -> bool:
50
+ return cls._linux_distro_id() in ("arch", "manjaro")
51
+
52
+ @classmethod
53
+ def is_gnome(cls) -> bool:
54
+ de = os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
55
+ return "gnome" in de
56
+
57
+ @classmethod
58
+ def is_kde(cls) -> bool:
59
+ de = os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
60
+ return "kde" in de
61
+
62
+ @staticmethod
63
+ def _linux_distro_id() -> str:
64
+ if not sys.platform.startswith("linux"):
65
+ return ""
66
+ try:
67
+ with open("/etc/os-release", "r", encoding="utf-8") as f:
68
+ for line in f:
69
+ if line.startswith("ID="):
70
+ return line.strip().split("=")[1].strip('"').lower()
71
+ except Exception:
72
+ pass
73
+ return ""
@@ -0,0 +1,3 @@
1
+ from .filesystem_resources import FileSystemResourceAdapter
2
+
3
+ __all__ = ["FileSystemResourceAdapter"]
@@ -0,0 +1,217 @@
1
+
2
+ """
3
+ Filesystem resource adapter implementing IResourcePort.
4
+
5
+ Resolves application resources across source and frozen environments,
6
+ including optional protected resource bundles.
7
+ """
8
+
9
+ from pathlib import Path
10
+
11
+ from qyro.application.ports.environment import IEnvironmentPort
12
+ from qyro.application.ports.resources import IResourcePort
13
+ from qyro.adapters.resources.protected_bundle import ProtectedResourceBundle
14
+ from qyro.domain.entities import PlatformType, ResourceQuery, ResourceResult
15
+
16
+
17
+ class FileSystemResourceAdapter(IResourcePort):
18
+ """Resolve resources across source and frozen directory hierarchies."""
19
+
20
+ _PLATFORM_DIRECTORIES = {
21
+ PlatformType.WINDOWS: ("windows", "win32"),
22
+ PlatformType.MACOS: ("mac", "darwin"),
23
+ PlatformType.LINUX: ("linux",),
24
+ }
25
+
26
+ _RESOURCE_ALIASES = frozenset({
27
+ "base",
28
+ "windows",
29
+ "win32",
30
+ "mac",
31
+ "darwin",
32
+ "linux",
33
+ })
34
+
35
+ def __init__(
36
+ self,
37
+ env_port: IEnvironmentPort,
38
+ custom_resources_dir: Path | None = None,
39
+ ) -> None:
40
+ """Initialize the adapter with its environment and optional resource root."""
41
+ self._env = env_port
42
+ self._custom_resources_dir = custom_resources_dir
43
+
44
+ def _get_candidate_roots(self) -> list[Path]:
45
+ """Return resource directories in resolution priority order."""
46
+ if self._custom_resources_dir is not None:
47
+ custom_root = Path(self._custom_resources_dir).resolve()
48
+ if custom_root.is_dir():
49
+ return [custom_root]
50
+
51
+ platform_dirs = self._PLATFORM_DIRECTORIES.get(
52
+ self._env.get_platform(),
53
+ (),
54
+ )
55
+
56
+ candidates: list[Path] = []
57
+
58
+ if self._env.is_frozen():
59
+ protected_root = ProtectedResourceBundle.get_extracted_root(self._env)
60
+ if protected_root is not None:
61
+ protected_resources = protected_root / "resources"
62
+ candidates.extend(
63
+ self._resource_directories(
64
+ protected_resources,
65
+ platform_dirs,
66
+ )
67
+ )
68
+
69
+ bundle = self._env.get_bundle_dir()
70
+
71
+ candidates.extend(
72
+ self._resource_directories(
73
+ bundle / "resources",
74
+ platform_dirs,
75
+ )
76
+ )
77
+ candidates.extend(
78
+ self._resource_directories(
79
+ bundle / "src" / "main" / "resources",
80
+ platform_dirs,
81
+ )
82
+ )
83
+ candidates.append(bundle)
84
+
85
+ else:
86
+ root = self._env.get_root_dir()
87
+
88
+ candidates.extend(
89
+ self._resource_directories(
90
+ root / "resources",
91
+ platform_dirs,
92
+ )
93
+ )
94
+ candidates.extend(
95
+ self._resource_directories(
96
+ root / "src" / "main" / "resources",
97
+ platform_dirs,
98
+ )
99
+ )
100
+
101
+ protected_root = ProtectedResourceBundle.get_extracted_root(self._env)
102
+ if protected_root is not None:
103
+ protected_resources = protected_root / "resources"
104
+ candidates.extend(
105
+ self._resource_directories(
106
+ protected_resources,
107
+ platform_dirs,
108
+ )
109
+ )
110
+
111
+ return self._existing_unique_directories(candidates)
112
+
113
+ @staticmethod
114
+ def _resource_directories(
115
+ root: Path,
116
+ platform_dirs: tuple[str, ...],
117
+ ) -> list[Path]:
118
+ """Build platform-specific and shared resource directories."""
119
+ return [
120
+ *(root / directory for directory in platform_dirs),
121
+ root / "base",
122
+ root,
123
+ ]
124
+
125
+ @staticmethod
126
+ def _existing_unique_directories(
127
+ candidates: list[Path],
128
+ ) -> list[Path]:
129
+ """Filter missing directories and duplicates while preserving order."""
130
+ unique: list[Path] = []
131
+ seen: set[Path] = set()
132
+
133
+ for candidate in candidates:
134
+ path = candidate.resolve()
135
+
136
+ if not path.is_dir() or path in seen:
137
+ continue
138
+
139
+ seen.add(path)
140
+ unique.append(path)
141
+
142
+ return unique
143
+
144
+ def resolve(self, query: ResourceQuery) -> ResourceResult:
145
+ """Resolve a resource query to an absolute filesystem path."""
146
+ rel_path = Path(*query.relative_path_segments)
147
+ rel_candidates = self._candidate_relative_paths(rel_path)
148
+ candidates = self._get_candidate_roots()
149
+
150
+ frozen = self._env.is_frozen()
151
+ platform = self._env.get_platform()
152
+
153
+ for root in candidates:
154
+ for relative_path in rel_candidates:
155
+ target = root / relative_path
156
+
157
+ if target.is_file():
158
+ return ResourceResult(
159
+ absolute_path=target.resolve(),
160
+ exists=True,
161
+ is_bundled=frozen,
162
+ target_os=platform,
163
+ )
164
+
165
+ primary_root = (
166
+ candidates[0]
167
+ if candidates
168
+ else self._env.get_root_dir()
169
+ )
170
+
171
+ return ResourceResult(
172
+ absolute_path=(primary_root / rel_path).resolve(),
173
+ exists=False,
174
+ is_bundled=frozen,
175
+ )
176
+
177
+ def _candidate_relative_paths(
178
+ self,
179
+ rel_path: Path,
180
+ ) -> list[Path]:
181
+ """Generate relative path variants for resource directory aliases."""
182
+ alternatives = [rel_path]
183
+ parts = rel_path.parts
184
+
185
+ if (
186
+ len(parts) >= 2
187
+ and parts[0].lower() in self._RESOURCE_ALIASES
188
+ ):
189
+ alternatives.append(Path(*parts[1:]))
190
+
191
+ return list(dict.fromkeys(alternatives))
192
+
193
+ def list_resources(
194
+ self,
195
+ subdirectory: str = "",
196
+ ) -> list[Path]:
197
+ """List resources while preserving directory resolution priority."""
198
+ relative_path = Path(subdirectory)
199
+ results: list[Path] = []
200
+ seen: set[Path] = set()
201
+
202
+ for root in self._get_candidate_roots():
203
+ target_dir = root / relative_path
204
+
205
+ if not target_dir.is_dir():
206
+ continue
207
+
208
+ for path in target_dir.iterdir():
209
+ resolved = path.resolve()
210
+
211
+ if resolved in seen:
212
+ continue
213
+
214
+ seen.add(resolved)
215
+ results.append(resolved)
216
+
217
+ return results
@@ -0,0 +1,227 @@
1
+ """
2
+ Protected bundle runtime loader.
3
+
4
+ Loads and extracts Velto's protected resources package generated by velto-cli,
5
+ making embedded settings/ and resources/ folders available through filesystem
6
+ paths without changing existing public APIs.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import atexit
12
+ import hashlib
13
+ import hmac
14
+ import io
15
+ import shutil
16
+ import tempfile
17
+ import zipfile
18
+ from pathlib import Path
19
+ from threading import Lock
20
+
21
+ from qyro.application.ports.environment import IEnvironmentPort
22
+ from qyro.adapters.resources.runtime_secret import (
23
+ find_runtime_secret_module,
24
+ read_runtime_secret,
25
+ )
26
+
27
+
28
+ _MAGIC = b"QYRPKG"
29
+ _NONCE_LEN = 16
30
+ _SALT_LEN = 16
31
+ _TAG_LEN = 32
32
+ _KEY_LEN = 32
33
+ _RUNTIME_SECRET_MODULE_STEM = "runtime"
34
+ _SECRETS_ARCHIVE_PATH = ".velto/secrets.enc"
35
+
36
+
37
+ class ProtectedResourceBundle:
38
+ """Utility that extracts protected bundle contents when available."""
39
+
40
+ _lock = Lock()
41
+ _extracted_cache: dict[str, Path] = {}
42
+ _encrypted_secrets_cache: dict[str, bytes | None] = {}
43
+
44
+ @classmethod
45
+ def get_extracted_root(cls, env_port: IEnvironmentPort) -> Path | None:
46
+ """Return extracted root containing settings/ and resources/ when present."""
47
+ base_dir = env_port.get_bundle_dir() if env_port.is_frozen() else env_port.get_root_dir()
48
+ package_path = base_dir / ".velto" / "resources.pak"
49
+ secret_module_path = cls._find_runtime_secret_module(base_dir / ".velto")
50
+
51
+ if not package_path.exists() or secret_module_path is None:
52
+ return None
53
+
54
+ cache_key = cls._cache_key(package_path, secret_module_path)
55
+ with cls._lock:
56
+ cached = cls._extracted_cache.get(cache_key)
57
+ if cached and cached.exists():
58
+ return cached
59
+ try:
60
+ extracted_root, encrypted_payload = cls._extract_bundle(package_path, secret_module_path)
61
+ except Exception:
62
+ return None
63
+ cls._extracted_cache[cache_key] = extracted_root
64
+ cls._encrypted_secrets_cache[cache_key] = encrypted_payload
65
+ return extracted_root
66
+
67
+ @classmethod
68
+ def get_encrypted_secrets_payload(cls, env_port: IEnvironmentPort) -> bytes | None:
69
+ """Return encrypted secrets payload embedded in resources.pak when available."""
70
+ base_dir = env_port.get_bundle_dir() if env_port.is_frozen() else env_port.get_root_dir()
71
+ package_path = base_dir / ".velto" / "resources.pak"
72
+ secret_module_path = cls._find_runtime_secret_module(base_dir / ".velto")
73
+
74
+ if not package_path.exists() or secret_module_path is None:
75
+ return None
76
+
77
+ cache_key = cls._cache_key(package_path, secret_module_path)
78
+ with cls._lock:
79
+ if cache_key in cls._encrypted_secrets_cache:
80
+ return cls._encrypted_secrets_cache[cache_key]
81
+
82
+ try:
83
+ extracted_root, encrypted_payload = cls._extract_bundle(package_path, secret_module_path)
84
+ except Exception:
85
+ return None
86
+
87
+ cls._extracted_cache[cache_key] = extracted_root
88
+ cls._encrypted_secrets_cache[cache_key] = encrypted_payload
89
+ return encrypted_payload
90
+
91
+ @staticmethod
92
+ def _find_runtime_secret_module(protected_dir: Path) -> Path | None:
93
+ return find_runtime_secret_module(protected_dir)
94
+
95
+ @staticmethod
96
+ def _cache_key(package_path: Path, secret_module_path: Path) -> str:
97
+ stat_a = package_path.stat()
98
+ stat_b = secret_module_path.stat()
99
+ return (
100
+ f"{package_path}:{stat_a.st_mtime_ns}:{stat_a.st_size}:"
101
+ f"{secret_module_path}:{stat_b.st_mtime_ns}:{stat_b.st_size}"
102
+ )
103
+
104
+ @classmethod
105
+ def _extract_bundle(cls, package_path: Path, secret_module_path: Path) -> tuple[Path, bytes | None]:
106
+ zip_bytes = cls._decrypt_package_to_zip_bytes(package_path, secret_module_path)
107
+ encrypted_secrets_payload = cls._read_embedded_secrets_payload(zip_bytes)
108
+ extract_root = Path(tempfile.mkdtemp(prefix="velto_protected_"))
109
+
110
+ try:
111
+ with zipfile.ZipFile(io.BytesIO(zip_bytes), "r") as archive:
112
+ for member in archive.infolist():
113
+ normalized_name = member.filename.strip("/")
114
+ if not normalized_name or member.is_dir():
115
+ continue
116
+ if normalized_name == _SECRETS_ARCHIVE_PATH:
117
+ continue
118
+ if normalized_name.startswith(".velto/"):
119
+ continue
120
+
121
+ relative_path = Path(normalized_name)
122
+ if (
123
+ "\\" in normalized_name
124
+ or relative_path.is_absolute()
125
+ or ".." in relative_path.parts
126
+ or not relative_path.parts
127
+ or relative_path.parts[0] not in {"settings", "resources"}
128
+ ):
129
+ raise ValueError("Protected resources package contains an invalid path")
130
+
131
+ destination = extract_root / relative_path
132
+ destination.parent.mkdir(parents=True, exist_ok=True)
133
+ with archive.open(member) as source, destination.open("wb") as target:
134
+ shutil.copyfileobj(source, target)
135
+ except Exception:
136
+ shutil.rmtree(extract_root, ignore_errors=True)
137
+ raise
138
+
139
+ atexit.register(shutil.rmtree, extract_root, ignore_errors=True)
140
+ return extract_root, encrypted_secrets_payload
141
+
142
+ @classmethod
143
+ def _decrypt_package_to_zip_bytes(cls, package_path: Path, secret_module_path: Path) -> bytes:
144
+ payload = package_path.read_bytes()
145
+ runtime_secret = cls._read_runtime_secret(secret_module_path)
146
+ key, ciphertext, nonce, tag = cls._parse_payload(payload, runtime_secret)
147
+
148
+ expected_tag = hmac.new(key, nonce + ciphertext, hashlib.sha256).digest()
149
+ if not hmac.compare_digest(tag, expected_tag):
150
+ raise ValueError("Protected resources signature mismatch")
151
+
152
+ return cls._xor_keystream(ciphertext, key=key, nonce=nonce)
153
+
154
+ @staticmethod
155
+ def _read_embedded_secrets_payload(zip_bytes: bytes) -> bytes | None:
156
+ with zipfile.ZipFile(io.BytesIO(zip_bytes), "r") as archive:
157
+ try:
158
+ return archive.read(_SECRETS_ARCHIVE_PATH)
159
+ except KeyError:
160
+ return None
161
+
162
+ @classmethod
163
+ def _parse_payload(cls, payload: bytes, runtime_secret: bytes) -> tuple[bytes, bytes, bytes, bytes]:
164
+ min_len = len(_MAGIC) + _SALT_LEN + _KEY_LEN + _NONCE_LEN + _TAG_LEN
165
+ if len(payload) <= min_len or not payload.startswith(_MAGIC):
166
+ raise ValueError("Invalid protected resources package format")
167
+
168
+ cursor = len(_MAGIC)
169
+ salt = payload[cursor:cursor + _SALT_LEN]
170
+ cursor += _SALT_LEN
171
+
172
+ wrapped_key = payload[cursor:cursor + _KEY_LEN]
173
+ cursor += _KEY_LEN
174
+
175
+ nonce = payload[cursor:cursor + _NONCE_LEN]
176
+ cursor += _NONCE_LEN
177
+
178
+ tag = payload[cursor:cursor + _TAG_LEN]
179
+ cursor += _TAG_LEN
180
+
181
+ ciphertext = payload[cursor:]
182
+ key = cls._unwrap_key(wrapped_key=wrapped_key, salt=salt, runtime_secret=runtime_secret)
183
+ return key, ciphertext, nonce, tag
184
+
185
+ @classmethod
186
+ def _unwrap_key(cls, *, wrapped_key: bytes, salt: bytes, runtime_secret: bytes) -> bytes:
187
+ mask = cls._keystream(seed=cls._master_seed(salt, runtime_secret), size=len(wrapped_key))
188
+ return bytes(value ^ mask[i] for i, value in enumerate(wrapped_key))
189
+
190
+ @staticmethod
191
+ def _master_seed(salt: bytes, runtime_secret: bytes) -> bytes:
192
+ return hashlib.sha256(runtime_secret + salt).digest()
193
+
194
+ @classmethod
195
+ def _keystream(cls, *, seed: bytes, size: int) -> bytes:
196
+ blocks: list[bytes] = []
197
+ index = 0
198
+ total = 0
199
+ while total < size:
200
+ block = hashlib.sha256(seed + index.to_bytes(8, "big")).digest()
201
+ blocks.append(block)
202
+ total += len(block)
203
+ index += 1
204
+ return b"".join(blocks)[:size]
205
+
206
+ @staticmethod
207
+ def _read_runtime_secret(secret_module_path: Path) -> bytes:
208
+ return read_runtime_secret(secret_module_path)
209
+
210
+ @staticmethod
211
+ def _xor_keystream(data: bytes, *, key: bytes, nonce: bytes) -> bytes:
212
+ out = bytearray(len(data))
213
+ block_index = 0
214
+ offset = 0
215
+
216
+ while offset < len(data):
217
+ block_seed = nonce + block_index.to_bytes(8, "big")
218
+ stream = hashlib.sha256(key + block_seed).digest()
219
+ chunk = data[offset:offset + len(stream)]
220
+
221
+ for i, value in enumerate(chunk):
222
+ out[offset + i] = value ^ stream[i]
223
+
224
+ offset += len(chunk)
225
+ block_index += 1
226
+
227
+ return bytes(out)