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.
- ocf_containers-1.0.0/LICENSE +30 -0
- ocf_containers-1.0.0/PKG-INFO +196 -0
- ocf_containers-1.0.0/README.md +133 -0
- ocf_containers-1.0.0/ocf/__init__.py +3 -0
- ocf_containers-1.0.0/ocf/binding/__init__.py +13 -0
- ocf_containers-1.0.0/ocf/binding/machine.py +125 -0
- ocf_containers-1.0.0/ocf/container/__init__.py +20 -0
- ocf_containers-1.0.0/ocf/container/layout.py +60 -0
- ocf_containers-1.0.0/ocf/container/policy.py +204 -0
- ocf_containers-1.0.0/ocf/container/reader.py +233 -0
- ocf_containers-1.0.0/ocf/container/state.py +42 -0
- ocf_containers-1.0.0/ocf/container/writer.py +163 -0
- ocf_containers-1.0.0/ocf/primitives/__init__.py +11 -0
- ocf_containers-1.0.0/ocf/primitives/aead.py +29 -0
- ocf_containers-1.0.0/ocf/primitives/kdf.py +41 -0
- ocf_containers-1.0.0/ocf/primitives/rng.py +5 -0
- ocf_containers-1.0.0/ocf/primitives/signing.py +25 -0
- ocf_containers-1.0.0/ocf/tools/__init__.py +0 -0
- ocf_containers-1.0.0/ocf/tools/ocfctl.py +168 -0
- ocf_containers-1.0.0/ocf_containers.egg-info/PKG-INFO +196 -0
- ocf_containers-1.0.0/ocf_containers.egg-info/SOURCES.txt +32 -0
- ocf_containers-1.0.0/ocf_containers.egg-info/dependency_links.txt +1 -0
- ocf_containers-1.0.0/ocf_containers.egg-info/entry_points.txt +2 -0
- ocf_containers-1.0.0/ocf_containers.egg-info/requires.txt +7 -0
- ocf_containers-1.0.0/ocf_containers.egg-info/top_level.txt +1 -0
- ocf_containers-1.0.0/pyproject.toml +63 -0
- ocf_containers-1.0.0/setup.cfg +4 -0
- ocf_containers-1.0.0/tests/test_container.py +107 -0
- ocf_containers-1.0.0/tests/test_fuzz.py +202 -0
- ocf_containers-1.0.0/tests/test_license.py +37 -0
- ocf_containers-1.0.0/tests/test_machine.py +110 -0
- ocf_containers-1.0.0/tests/test_policy.py +257 -0
- ocf_containers-1.0.0/tests/test_primitives.py +77 -0
- 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
|
+
[](https://github.com/JustHackedOn/ocf-containers/actions)
|
|
67
|
+
[](https://pypi.org/project/ocf/)
|
|
68
|
+
[](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
|
+
[](https://github.com/JustHackedOn/ocf-containers/actions)
|
|
4
|
+
[](https://pypi.org/project/ocf/)
|
|
5
|
+
[](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,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
|