ocf-containers 1.0.0__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.
Files changed (34) hide show
  1. ocf_containers-1.0.0/LICENSE +30 -0
  2. ocf_containers-1.0.0/PKG-INFO +196 -0
  3. ocf_containers-1.0.0/README.md +133 -0
  4. ocf_containers-1.0.0/ocf/__init__.py +3 -0
  5. ocf_containers-1.0.0/ocf/binding/__init__.py +13 -0
  6. ocf_containers-1.0.0/ocf/binding/machine.py +125 -0
  7. ocf_containers-1.0.0/ocf/container/__init__.py +20 -0
  8. ocf_containers-1.0.0/ocf/container/layout.py +60 -0
  9. ocf_containers-1.0.0/ocf/container/policy.py +204 -0
  10. ocf_containers-1.0.0/ocf/container/reader.py +233 -0
  11. ocf_containers-1.0.0/ocf/container/state.py +42 -0
  12. ocf_containers-1.0.0/ocf/container/writer.py +163 -0
  13. ocf_containers-1.0.0/ocf/primitives/__init__.py +11 -0
  14. ocf_containers-1.0.0/ocf/primitives/aead.py +29 -0
  15. ocf_containers-1.0.0/ocf/primitives/kdf.py +41 -0
  16. ocf_containers-1.0.0/ocf/primitives/rng.py +5 -0
  17. ocf_containers-1.0.0/ocf/primitives/signing.py +25 -0
  18. ocf_containers-1.0.0/ocf/tools/__init__.py +0 -0
  19. ocf_containers-1.0.0/ocf/tools/ocfctl.py +168 -0
  20. ocf_containers-1.0.0/ocf_containers.egg-info/PKG-INFO +196 -0
  21. ocf_containers-1.0.0/ocf_containers.egg-info/SOURCES.txt +32 -0
  22. ocf_containers-1.0.0/ocf_containers.egg-info/dependency_links.txt +1 -0
  23. ocf_containers-1.0.0/ocf_containers.egg-info/entry_points.txt +2 -0
  24. ocf_containers-1.0.0/ocf_containers.egg-info/requires.txt +7 -0
  25. ocf_containers-1.0.0/ocf_containers.egg-info/top_level.txt +1 -0
  26. ocf_containers-1.0.0/pyproject.toml +63 -0
  27. ocf_containers-1.0.0/setup.cfg +4 -0
  28. ocf_containers-1.0.0/tests/test_container.py +107 -0
  29. ocf_containers-1.0.0/tests/test_fuzz.py +202 -0
  30. ocf_containers-1.0.0/tests/test_license.py +37 -0
  31. ocf_containers-1.0.0/tests/test_machine.py +110 -0
  32. ocf_containers-1.0.0/tests/test_policy.py +257 -0
  33. ocf_containers-1.0.0/tests/test_primitives.py +77 -0
  34. ocf_containers-1.0.0/tests/test_streaming.py +133 -0
@@ -0,0 +1,30 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OCF Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ Exception note (not legal advice): cryptography primitives are drawn from
26
+ the `cryptography` and `argon2-cffi` distributions and remain under their
27
+ respective licenses (Apache-2.0 with linking exceptions, MIT,
28
+ Apache-2.0). This license governs this repository's code only. The
29
+ no-commercial-code clean-room rule documented in CONTRIBUTING.md is an
30
+ engineering policy of this project, not a license condition.
@@ -0,0 +1,196 @@
1
+ Metadata-Version: 2.2
2
+ Name: ocf-containers
3
+ Version: 1.0.0
4
+ Summary: Open Container Format - encrypted, seekable media container with passwords, machine binding, expiry, play limits and Ed25519 signed policy
5
+ Author: OCF Contributors
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 OCF Contributors
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ ---
29
+
30
+ Exception note (not legal advice): cryptography primitives are drawn from
31
+ the `cryptography` and `argon2-cffi` distributions and remain under their
32
+ respective licenses (Apache-2.0 with linking exceptions, MIT,
33
+ Apache-2.0). This license governs this repository's code only. The
34
+ no-commercial-code clean-room rule documented in CONTRIBUTING.md is an
35
+ engineering policy of this project, not a license condition.
36
+
37
+ Project-URL: Homepage, https://github.com/JustHackedOn/ocf-containers
38
+ Project-URL: Documentation, https://github.com/JustHackedOn/ocf-containers/tree/main/docs
39
+ Project-URL: Changelog, https://github.com/JustHackedOn/ocf-containers/blob/main/CHANGELOG.md
40
+ Project-URL: Security, https://github.com/JustHackedOn/ocf-containers/blob/main/docs/SECURITY.md
41
+ Project-URL: Issues, https://github.com/JustHackedOn/ocf-containers/issues
42
+ Keywords: encryption,container,media,drm,aead,aes-gcm,argon2,ed25519,watermark,clean-room
43
+ Classifier: Development Status :: 5 - Production/Stable
44
+ Classifier: Intended Audience :: Developers
45
+ Classifier: License :: OSI Approved :: MIT License
46
+ Classifier: Operating System :: Microsoft :: Windows
47
+ Classifier: Operating System :: POSIX :: Linux
48
+ Classifier: Operating System :: MacOS
49
+ Classifier: Programming Language :: Python :: 3
50
+ Classifier: Programming Language :: Python :: 3.11
51
+ Classifier: Programming Language :: Python :: 3.12
52
+ Classifier: Programming Language :: Python :: 3.13
53
+ Classifier: Topic :: Security :: Cryptography
54
+ Requires-Python: >=3.11
55
+ Description-Content-Type: text/markdown
56
+ License-File: LICENSE
57
+ Requires-Dist: cryptography<47,>=42
58
+ Requires-Dist: argon2-cffi<26,>=23
59
+ Provides-Extra: dev
60
+ Requires-Dist: pytest<9,>=8; extra == "dev"
61
+ Requires-Dist: hypothesis<7,>=6; extra == "dev"
62
+ Requires-Dist: build<2,>=1.2; extra == "dev"
63
+
64
+ # OCF — Open Container Format
65
+
66
+ [![CI](https://github.com/JustHackedOn/ocf-containers/actions/workflows/test.yml/badge.svg)](https://github.com/JustHackedOn/ocf-containers/actions)
67
+ [![PyPI](https://img.shields.io/pypi/v/ocf.svg)](https://pypi.org/project/ocf/)
68
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
69
+
70
+ OCF is a **clean-room, open-source encrypted media container** with password
71
+ gating, machine binding, expiry, play-count limits, watermarks, and Ed25519-signed
72
+ policy. It is built from standard primitives only — AES-256-GCM, Argon2id,
73
+ Ed25519, HMAC-SHA256 — with no proprietary dependencies and no embedded keys.
74
+
75
+ **Status:** v1.0.0 · 99 tests green · ~540 fuzz examples with 0 crashes ·
76
+ 1 GB file streamed at 245 MB/s (peak memory ≈ file size).
77
+
78
+ ## What it does
79
+
80
+ ```
81
+ plaintext ──chunked AES-256-GCM──► SEC_DATA
82
+ password ──Argon2id──► KEK ──wraps──► content_key (never stored in the clear)
83
+ policy ──Ed25519──► signed SEC_POLICY (open/play pw, machine bind,
84
+ expiry, play count, watermark)
85
+ everything┐
86
+ header ├─ HMAC-SHA256 footer MAC (tamper-evident, verified before decrypt)
87
+ sections ┘
88
+ ```
89
+
90
+ ## Features
91
+
92
+ - **AEAD everywhere** — per-chunk AES-256-GCM tags; bit-flips are rejected,
93
+ seek is O(1) via chunk table (256 KiB default chunks)
94
+ - **Key wrapping** — content key is wrapped under an Argon2id-derived KEK;
95
+ the derived key never touches disk
96
+ - **Signed policy** — open/password flags, machine binding, expiry,
97
+ play-count limit, watermark, all in one Ed25519-signed `SEC_POLICY`
98
+ (pin the vendor pubkey for authenticity; embedded key gives integrity)
99
+ - **Streaming** — `build_streaming()` writes O(chunk) memory for any size
100
+ (tested at 1 GB); reader is mmap-backed with zero-copy sections
101
+ - **Machine binding** — weighted fingerprint from machine GUID, BIOS UUID,
102
+ CPU ID, disk serial, MAC (each source hashed before combining; no raw
103
+ serials in the digest)
104
+ - **Play counting** — external state file keyed by signed `container_id`;
105
+ rewriting the container breaks the footer MAC, so counters can't be
106
+ reset by editing the file
107
+ - **CLI + library** — `ocfctl` for scripts, clean Python API for apps
108
+
109
+ ## Honest scope (read before trusting this with anything)
110
+
111
+ OCF protects **data at rest** and gates **player behavior**. Like every
112
+ client-side DRM scheme (including commercial ones), it cannot defend an
113
+ attacker who controls the running player: the unwrapped key must exist in
114
+ process memory to play. OCF raises the cost of casual redistribution and
115
+ tampering; it does not promise impossibility. See [docs/SECURITY.md](docs/SECURITY.md)
116
+ for the full threat model.
117
+
118
+ ## Install
119
+
120
+ ```bash
121
+ pip install ocf-containers # from PyPI (when published)
122
+ pip install . # from a clone
123
+ pip install -e ".[dev]" # dev install with pytest/hypothesis/build
124
+ ```
125
+
126
+ Python 3.11+ · Windows, Linux, macOS.
127
+
128
+ ## Quick start
129
+
130
+ ```bash
131
+ # Encrypt a folder for this machine only, expiring 2026-12-31, 5 plays max
132
+ ocfctl create movie.ocf video.mp4 --password openpw \
133
+ --machine-bind --expiry 2026-12-31 --play-count 5 \
134
+ --watermark "REVIEW COPY" --sign
135
+ # → prints the policy pubkey; PIN IT to verify signatures later
136
+
137
+ ocfctl info movie.ocf --password openpw
138
+ ocfctl authorize movie.ocf --password openpw --play-password REPLACEME
139
+ ```
140
+
141
+ See [QUICKSTART.md](QUICKSTART.md) for the 5-minute walkthrough and
142
+ [docs/CLI_REFERENCE.md](docs/CLI_REFERENCE.md) for every flag.
143
+
144
+ ## Library example
145
+
146
+ ```python
147
+ from ocf.container import ContainerWriter, ContainerReader, FileEntry, make_policy
148
+ from ocf.primitives.signing import generate_signing_key
149
+
150
+ policy = make_policy(signing_key=generate_signing_key(), watermark="TRIAL")
151
+ blob = ContainerWriter(password=b"openpw", policy=policy).build(
152
+ [FileEntry("video.mp4", open("video.mp4", "rb").read())]
153
+ )
154
+ r = ContainerReader(blob, password=b"openpw")
155
+ print(r.policy.watermark, r.files[0].name)
156
+ data = r.read_file(0)
157
+ ```
158
+
159
+ More in [`examples/`](examples/): basic, password, machine-bound, expiry,
160
+ play-count, batch, and 1 GB streaming.
161
+
162
+ ## Benchmarks
163
+
164
+ | Case | Encrypt | Decrypt |
165
+ |---|---|---|
166
+ | 100 MB (stream) | ~250 MB/s | ~290 MB/s |
167
+ | 1 GB (stream) | 245 MB/s | 250 MB/s |
168
+ | Password overhead | Argon2id ≈ 0.25 s fixed per open | |
169
+
170
+ Details and methodology: [docs/BENCHMARK.md](docs/BENCHMARK.md).
171
+ Parallel workers are off by default — measured to hurt on AES-NI.
172
+
173
+ ## Documentation
174
+
175
+ | Doc | Contents |
176
+ |---|---|
177
+ | [QUICKSTART.md](QUICKSTART.md) | first container in 5 minutes |
178
+ | [docs/CLI_REFERENCE.md](docs/CLI_REFERENCE.md) | all commands and flags |
179
+ | [docs/FORMAT_SPEC.md](docs/FORMAT_SPEC.md) | public container format spec |
180
+ | [docs/SECURITY.md](docs/SECURITY.md) | threat model, disclosure |
181
+ | [docs/BENCHMARK.md](docs/BENCHMARK.md) | numbers + recommendations |
182
+ | [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) | dev setup, PRs, clean-room policy |
183
+ | [docs/RELEASING.md](docs/RELEASING.md) | versioning, PyPI, tags |
184
+
185
+ ## Development
186
+
187
+ ```bash
188
+ git clone https://github.com/JustHackedOn/ocf-containers && cd ocf-containers
189
+ pip install -e ".[dev]"
190
+ pytest -q # 99 tests, including ~540 fuzz examples
191
+ python tools/benchmark.py --quick
192
+ ```
193
+
194
+ ## License
195
+
196
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,133 @@
1
+ # OCF — Open Container Format
2
+
3
+ [![CI](https://github.com/JustHackedOn/ocf-containers/actions/workflows/test.yml/badge.svg)](https://github.com/JustHackedOn/ocf-containers/actions)
4
+ [![PyPI](https://img.shields.io/pypi/v/ocf.svg)](https://pypi.org/project/ocf/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+
7
+ OCF is a **clean-room, open-source encrypted media container** with password
8
+ gating, machine binding, expiry, play-count limits, watermarks, and Ed25519-signed
9
+ policy. It is built from standard primitives only — AES-256-GCM, Argon2id,
10
+ Ed25519, HMAC-SHA256 — with no proprietary dependencies and no embedded keys.
11
+
12
+ **Status:** v1.0.0 · 99 tests green · ~540 fuzz examples with 0 crashes ·
13
+ 1 GB file streamed at 245 MB/s (peak memory ≈ file size).
14
+
15
+ ## What it does
16
+
17
+ ```
18
+ plaintext ──chunked AES-256-GCM──► SEC_DATA
19
+ password ──Argon2id──► KEK ──wraps──► content_key (never stored in the clear)
20
+ policy ──Ed25519──► signed SEC_POLICY (open/play pw, machine bind,
21
+ expiry, play count, watermark)
22
+ everything┐
23
+ header ├─ HMAC-SHA256 footer MAC (tamper-evident, verified before decrypt)
24
+ sections ┘
25
+ ```
26
+
27
+ ## Features
28
+
29
+ - **AEAD everywhere** — per-chunk AES-256-GCM tags; bit-flips are rejected,
30
+ seek is O(1) via chunk table (256 KiB default chunks)
31
+ - **Key wrapping** — content key is wrapped under an Argon2id-derived KEK;
32
+ the derived key never touches disk
33
+ - **Signed policy** — open/password flags, machine binding, expiry,
34
+ play-count limit, watermark, all in one Ed25519-signed `SEC_POLICY`
35
+ (pin the vendor pubkey for authenticity; embedded key gives integrity)
36
+ - **Streaming** — `build_streaming()` writes O(chunk) memory for any size
37
+ (tested at 1 GB); reader is mmap-backed with zero-copy sections
38
+ - **Machine binding** — weighted fingerprint from machine GUID, BIOS UUID,
39
+ CPU ID, disk serial, MAC (each source hashed before combining; no raw
40
+ serials in the digest)
41
+ - **Play counting** — external state file keyed by signed `container_id`;
42
+ rewriting the container breaks the footer MAC, so counters can't be
43
+ reset by editing the file
44
+ - **CLI + library** — `ocfctl` for scripts, clean Python API for apps
45
+
46
+ ## Honest scope (read before trusting this with anything)
47
+
48
+ OCF protects **data at rest** and gates **player behavior**. Like every
49
+ client-side DRM scheme (including commercial ones), it cannot defend an
50
+ attacker who controls the running player: the unwrapped key must exist in
51
+ process memory to play. OCF raises the cost of casual redistribution and
52
+ tampering; it does not promise impossibility. See [docs/SECURITY.md](docs/SECURITY.md)
53
+ for the full threat model.
54
+
55
+ ## Install
56
+
57
+ ```bash
58
+ pip install ocf-containers # from PyPI (when published)
59
+ pip install . # from a clone
60
+ pip install -e ".[dev]" # dev install with pytest/hypothesis/build
61
+ ```
62
+
63
+ Python 3.11+ · Windows, Linux, macOS.
64
+
65
+ ## Quick start
66
+
67
+ ```bash
68
+ # Encrypt a folder for this machine only, expiring 2026-12-31, 5 plays max
69
+ ocfctl create movie.ocf video.mp4 --password openpw \
70
+ --machine-bind --expiry 2026-12-31 --play-count 5 \
71
+ --watermark "REVIEW COPY" --sign
72
+ # → prints the policy pubkey; PIN IT to verify signatures later
73
+
74
+ ocfctl info movie.ocf --password openpw
75
+ ocfctl authorize movie.ocf --password openpw --play-password REPLACEME
76
+ ```
77
+
78
+ See [QUICKSTART.md](QUICKSTART.md) for the 5-minute walkthrough and
79
+ [docs/CLI_REFERENCE.md](docs/CLI_REFERENCE.md) for every flag.
80
+
81
+ ## Library example
82
+
83
+ ```python
84
+ from ocf.container import ContainerWriter, ContainerReader, FileEntry, make_policy
85
+ from ocf.primitives.signing import generate_signing_key
86
+
87
+ policy = make_policy(signing_key=generate_signing_key(), watermark="TRIAL")
88
+ blob = ContainerWriter(password=b"openpw", policy=policy).build(
89
+ [FileEntry("video.mp4", open("video.mp4", "rb").read())]
90
+ )
91
+ r = ContainerReader(blob, password=b"openpw")
92
+ print(r.policy.watermark, r.files[0].name)
93
+ data = r.read_file(0)
94
+ ```
95
+
96
+ More in [`examples/`](examples/): basic, password, machine-bound, expiry,
97
+ play-count, batch, and 1 GB streaming.
98
+
99
+ ## Benchmarks
100
+
101
+ | Case | Encrypt | Decrypt |
102
+ |---|---|---|
103
+ | 100 MB (stream) | ~250 MB/s | ~290 MB/s |
104
+ | 1 GB (stream) | 245 MB/s | 250 MB/s |
105
+ | Password overhead | Argon2id ≈ 0.25 s fixed per open | |
106
+
107
+ Details and methodology: [docs/BENCHMARK.md](docs/BENCHMARK.md).
108
+ Parallel workers are off by default — measured to hurt on AES-NI.
109
+
110
+ ## Documentation
111
+
112
+ | Doc | Contents |
113
+ |---|---|
114
+ | [QUICKSTART.md](QUICKSTART.md) | first container in 5 minutes |
115
+ | [docs/CLI_REFERENCE.md](docs/CLI_REFERENCE.md) | all commands and flags |
116
+ | [docs/FORMAT_SPEC.md](docs/FORMAT_SPEC.md) | public container format spec |
117
+ | [docs/SECURITY.md](docs/SECURITY.md) | threat model, disclosure |
118
+ | [docs/BENCHMARK.md](docs/BENCHMARK.md) | numbers + recommendations |
119
+ | [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) | dev setup, PRs, clean-room policy |
120
+ | [docs/RELEASING.md](docs/RELEASING.md) | versioning, PyPI, tags |
121
+
122
+ ## Development
123
+
124
+ ```bash
125
+ git clone https://github.com/JustHackedOn/ocf-containers && cd ocf-containers
126
+ pip install -e ".[dev]"
127
+ pytest -q # 99 tests, including ~540 fuzz examples
128
+ python tools/benchmark.py --quick
129
+ ```
130
+
131
+ ## License
132
+
133
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,3 @@
1
+ """OCF — Open Container Format."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,13 @@
1
+ from .machine import (
2
+ FingerprintResult,
3
+ WEIGHTED_SOURCES,
4
+ collect_components,
5
+ fingerprint_from,
6
+ get_fingerprint,
7
+ get_partial_fingerprint,
8
+ )
9
+
10
+ __all__ = [
11
+ "FingerprintResult", "WEIGHTED_SOURCES", "collect_components",
12
+ "fingerprint_from", "get_fingerprint", "get_partial_fingerprint",
13
+ ]
@@ -0,0 +1,125 @@
1
+ """Local machine fingerprinting (Windows-first, injectable for tests)."""
2
+
3
+ import hashlib
4
+ import platform
5
+ import subprocess
6
+ import uuid
7
+ from dataclasses import dataclass
8
+
9
+ WEIGHTED_SOURCES: dict[str, int] = {
10
+ "machine_guid": 4,
11
+ "bios_uuid": 3,
12
+ "cpu_id": 2,
13
+ "disk_serial": 2,
14
+ "mac_address": 1,
15
+ }
16
+
17
+ _FP_DOMAIN = b"ocf-mb:v1"
18
+
19
+
20
+ def _ps_first_line(cmd: str) -> str:
21
+ try:
22
+ out = subprocess.run(
23
+ ["powershell", "-NoProfile", "-NonInteractive", "-Command", cmd],
24
+ capture_output=True, text=True, timeout=15,
25
+ )
26
+ for line in out.stdout.splitlines():
27
+ line = line.strip()
28
+ if line and line != "SerialNumber":
29
+ return line
30
+ except Exception:
31
+ pass
32
+ return ""
33
+
34
+
35
+ def _collect_machine_guid() -> str:
36
+ if platform.system() != "Windows":
37
+ return ""
38
+ try:
39
+ import winreg
40
+ with winreg.OpenKey(
41
+ winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Cryptography"
42
+ ) as key:
43
+ return str(winreg.QueryValueEx(key, "MachineGuid")[0])
44
+ except Exception:
45
+ return ""
46
+
47
+
48
+ def _collect_bios_uuid() -> str:
49
+ if platform.system() != "Windows":
50
+ return ""
51
+ return _ps_first_line("(Get-CimInstance Win32_ComputerSystemProduct).UUID")
52
+
53
+
54
+ def _collect_cpu_id() -> str:
55
+ if platform.system() != "Windows":
56
+ return platform.processor()
57
+ return _ps_first_line(
58
+ "(Get-CimInstance Win32_Processor | Select-Object -First 1).ProcessorId"
59
+ )
60
+
61
+
62
+ def _collect_disk_serial() -> str:
63
+ if platform.system() != "Windows":
64
+ return ""
65
+ return _ps_first_line(
66
+ "(Get-PhysicalDisk | Select-Object -First 1).SerialNumber"
67
+ )
68
+
69
+
70
+ def _collect_mac_address() -> str:
71
+ node = uuid.getnode()
72
+ return ":".join(f"{(node >> (8 * i)) & 0xFF:02x}" for i in reversed(range(6)))
73
+
74
+
75
+ _COLLECTORS = {
76
+ "machine_guid": _collect_machine_guid,
77
+ "bios_uuid": _collect_bios_uuid,
78
+ "cpu_id": _collect_cpu_id,
79
+ "disk_serial": _collect_disk_serial,
80
+ "mac_address": _collect_mac_address,
81
+ }
82
+
83
+
84
+ def collect_components() -> dict[str, str]:
85
+ return {name: fn().strip() for name, fn in _COLLECTORS.items()}
86
+
87
+
88
+ @dataclass(frozen=True)
89
+ class FingerprintResult:
90
+ digest: bytes
91
+ sources: dict[str, str]
92
+ weights: dict[str, int]
93
+
94
+
95
+ def fingerprint_from(
96
+ components: dict[str, str], include: set[str] | None = None
97
+ ) -> FingerprintResult:
98
+ names = [
99
+ n for n in WEIGHTED_SOURCES
100
+ if n in components and components[n].strip()
101
+ and (include is None or n in include)
102
+ ]
103
+ h = hashlib.sha256(_FP_DOMAIN)
104
+ h.update(bytes([len(names)]))
105
+ used, weights = {}, {}
106
+ for n in names:
107
+ value = components[n].strip()
108
+ weight = WEIGHTED_SOURCES[n]
109
+ digest = hashlib.sha256(f"{n}|{value}".encode()).digest()
110
+ h.update(n.encode() + b"\x00" + bytes([weight]) + digest)
111
+ used[n] = value
112
+ weights[n] = weight
113
+ return FingerprintResult(h.digest(), used, weights)
114
+
115
+
116
+ def get_fingerprint(components: dict[str, str] | None = None) -> bytes:
117
+ comps = components if components is not None else collect_components()
118
+ return fingerprint_from(comps).digest
119
+
120
+
121
+ def get_partial_fingerprint(
122
+ include: set[str], components: dict[str, str] | None = None
123
+ ) -> FingerprintResult:
124
+ comps = components if components is not None else collect_components()
125
+ return fingerprint_from(comps, include=include)
@@ -0,0 +1,20 @@
1
+ from .layout import (
2
+ CHUNK_SIZE_DEFAULT, MAGIC_FOOTER, MAGIC_HEADER, SEC_DATA, SEC_END,
3
+ SEC_INDEX, SEC_KEYS, SEC_META, SEC_POLICY,
4
+ )
5
+ from .policy import (
6
+ Policy, PolicyError, check_play_password, enforce_policy, make_policy,
7
+ pack_policy, sign_policy, unpack_policy, verify_policy_signature,
8
+ )
9
+ from .reader import ContainerError, ContainerReader
10
+ from .state import PlayStateStore
11
+ from .writer import ContainerWriter, FileEntry
12
+
13
+ __all__ = [
14
+ "ContainerWriter", "ContainerReader", "ContainerError", "FileEntry",
15
+ "Policy", "PolicyError", "make_policy", "pack_policy", "unpack_policy",
16
+ "sign_policy", "verify_policy_signature", "enforce_policy",
17
+ "check_play_password", "PlayStateStore",
18
+ "MAGIC_HEADER", "MAGIC_FOOTER", "SEC_META", "SEC_KEYS", "SEC_POLICY",
19
+ "SEC_INDEX", "SEC_DATA", "SEC_END", "CHUNK_SIZE_DEFAULT",
20
+ ]
@@ -0,0 +1,60 @@
1
+ import struct
2
+
3
+ MAGIC_HEADER = b"OCF1"
4
+ MAGIC_FOOTER = b"OCFf"
5
+
6
+ HEADER_SIZE = 32
7
+ FOOTER_SIZE = 32
8
+
9
+ SEC_META = 0x0001
10
+ SEC_KEYS = 0x0002
11
+ SEC_INDEX = 0x0003
12
+ SEC_DATA = 0x0004
13
+ SEC_LICENSE = 0x0005
14
+ SEC_POLICY = 0x0006
15
+ SEC_PADDING = 0x00FE
16
+ SEC_END = 0x00FF
17
+
18
+ CRITICAL_BIT = 0x8000
19
+
20
+ CHUNK_SIZE_DEFAULT = 262144
21
+
22
+ KEY_PURPOSE_CONTENT = 1
23
+ KEY_PURPOSE_OPEN = 2
24
+ KEY_PURPOSE_PLAY = 3
25
+
26
+ HEADER_STRUCT = struct.Struct("<4sHHIIQQ")
27
+ FOOTER_STRUCT = struct.Struct("<4sIQ16s")
28
+ TLV_STRUCT = struct.Struct("<HI")
29
+
30
+
31
+ def build_header(section_count: int, part_index: int = 0, flags: int = 0) -> bytes:
32
+ return HEADER_STRUCT.pack(
33
+ MAGIC_HEADER, 1, 0, flags, section_count, part_index, 0
34
+ )
35
+
36
+
37
+ def parse_header(data: bytes) -> dict:
38
+ if len(data) < HEADER_SIZE:
39
+ raise ValueError("header truncated")
40
+ magic, maj, minr, flags, count, part, _ = HEADER_STRUCT.unpack(data[:HEADER_SIZE])
41
+ if magic != MAGIC_HEADER:
42
+ raise ValueError("bad magic")
43
+ if maj != 1:
44
+ raise ValueError(f"unsupported major version {maj}")
45
+ return dict(flags=flags, section_count=count, part_index=part)
46
+
47
+
48
+ def tlv(sec_type: int, payload: bytes) -> bytes:
49
+ return TLV_STRUCT.pack(sec_type, len(payload)) + payload
50
+
51
+
52
+ def iter_tlvs(buf: bytes):
53
+ off = 0
54
+ while off + TLV_STRUCT.size <= len(buf):
55
+ sec_type, length = TLV_STRUCT.unpack_from(buf, off)
56
+ off += TLV_STRUCT.size
57
+ if off + length > len(buf):
58
+ raise ValueError("tlv overruns buffer")
59
+ yield sec_type, buf[off:off + length]
60
+ off += length