json-chamber 1.4.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.
- json_chamber/__init__.py +61 -0
- json_chamber/api.py +46 -0
- json_chamber/benefit.py +289 -0
- json_chamber/core.py +200 -0
- json_chamber/license.py +350 -0
- json_chamber/test_aont.py +29 -0
- json_chamber-1.4.0.dist-info/METADATA +83 -0
- json_chamber-1.4.0.dist-info/RECORD +15 -0
- json_chamber-1.4.0.dist-info/WHEEL +5 -0
- json_chamber-1.4.0.dist-info/licenses/LICENSE +24 -0
- json_chamber-1.4.0.dist-info/licenses/LICENSE.AGPL-3.0 +661 -0
- json_chamber-1.4.0.dist-info/licenses/LICENSE.BSL-1.1 +96 -0
- json_chamber-1.4.0.dist-info/licenses/LICENSE.COMMERCIAL +29 -0
- json_chamber-1.4.0.dist-info/licenses/NOTICE +10 -0
- json_chamber-1.4.0.dist-info/top_level.txt +1 -0
json_chamber/__init__.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""json-chamber — JSON sealing protocol.
|
|
2
|
+
|
|
3
|
+
Cloak (seal new JSON) requires a live cloak license.
|
|
4
|
+
Open of an already-sealed blob is keys-only: no license, no clock.
|
|
5
|
+
TruGame and other engines still use require_alive() as a running-engine gate.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .api import cloak_json, open_json, cloak_bytes, open_bytes
|
|
9
|
+
from .core import WORDLIST
|
|
10
|
+
from .license import (
|
|
11
|
+
LicenseError,
|
|
12
|
+
PRODUCTS,
|
|
13
|
+
apply_entitlement,
|
|
14
|
+
create_entitlement,
|
|
15
|
+
license_status,
|
|
16
|
+
require_alive,
|
|
17
|
+
require_cloak,
|
|
18
|
+
reset_for_testing,
|
|
19
|
+
verifieddr_check,
|
|
20
|
+
verify_entitlement,
|
|
21
|
+
)
|
|
22
|
+
from .benefit import (
|
|
23
|
+
benefit_check,
|
|
24
|
+
tru8_benefit_check,
|
|
25
|
+
group_then_pack,
|
|
26
|
+
group_then_compress,
|
|
27
|
+
delta_transform_positions,
|
|
28
|
+
delta_transform_bytes,
|
|
29
|
+
chunk_dedup,
|
|
30
|
+
benefit_report,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
# Sealing
|
|
35
|
+
"cloak_json",
|
|
36
|
+
"open_json",
|
|
37
|
+
"cloak_bytes",
|
|
38
|
+
"open_bytes",
|
|
39
|
+
"WORDLIST",
|
|
40
|
+
# Shared Black Box control plane
|
|
41
|
+
"LicenseError",
|
|
42
|
+
"PRODUCTS",
|
|
43
|
+
"require_alive",
|
|
44
|
+
"require_cloak",
|
|
45
|
+
"license_status",
|
|
46
|
+
"apply_entitlement",
|
|
47
|
+
"create_entitlement",
|
|
48
|
+
"verify_entitlement",
|
|
49
|
+
"verifieddr_check",
|
|
50
|
+
"reset_for_testing",
|
|
51
|
+
# Optimizer / benefit
|
|
52
|
+
"benefit_check",
|
|
53
|
+
"tru8_benefit_check",
|
|
54
|
+
"group_then_pack",
|
|
55
|
+
"group_then_compress",
|
|
56
|
+
"delta_transform_positions",
|
|
57
|
+
"delta_transform_bytes",
|
|
58
|
+
"chunk_dedup",
|
|
59
|
+
"benefit_report",
|
|
60
|
+
]
|
|
61
|
+
__version__ = "1.4.0"
|
json_chamber/api.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High-level and low-level Chamber APIs.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from . import core
|
|
13
|
+
from .license import require_cloak
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _master() -> bytes:
|
|
17
|
+
raw = (
|
|
18
|
+
os.environ.get("CHAMBER_MASTER_SECRET")
|
|
19
|
+
or os.environ.get("JSON_CHAMBER_MASTER")
|
|
20
|
+
or os.environ.get("TRU8_MASTER_SECRET", "chamber-demo-master-secret-32b!!")
|
|
21
|
+
)
|
|
22
|
+
b = raw.encode("utf-8")
|
|
23
|
+
if len(b) < 32:
|
|
24
|
+
b = hashlib.sha256(b).digest()
|
|
25
|
+
return b
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def cloak_bytes(data: bytes) -> dict:
|
|
29
|
+
"""Seal bytes. Requires a live cloak license (24h try, then month/year)."""
|
|
30
|
+
require_cloak("json-chamber")
|
|
31
|
+
return core.seal_aont(data, _master())
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def open_bytes(sealed: dict) -> bytes:
|
|
35
|
+
"""Open a sealed blob. Keys only — no license, no clock, no kill switch."""
|
|
36
|
+
return core.open_sealed(sealed, _master())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def cloak_json(obj: Any) -> dict:
|
|
40
|
+
raw = json.dumps(obj, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
|
41
|
+
return cloak_bytes(raw)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def open_json(sealed: dict) -> Any:
|
|
45
|
+
raw = open_bytes(sealed)
|
|
46
|
+
return json.loads(raw.decode("utf-8"))
|
json_chamber/benefit.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""
|
|
2
|
+
benefit.py — public-safe optimizer for the $99 → $1,900 funnel.
|
|
3
|
+
|
|
4
|
+
NO TRU8 residual engine here.
|
|
5
|
+
• benefit_check / tru8_benefit_check – entropy + zero-bit bias + pair entropy
|
|
6
|
+
• group_then_pack / group_then_compress – create structure across tiny files
|
|
7
|
+
• delta_transform_positions – mesh/position structure before coding
|
|
8
|
+
• chunk_dedup – cross-file repeat report (analysis only)
|
|
9
|
+
|
|
10
|
+
$99 path (json-chamber) always works via cloak_json / cloak_bytes.
|
|
11
|
+
$1,900 path is only advertised when compress=True.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import hashlib
|
|
17
|
+
import io
|
|
18
|
+
import math
|
|
19
|
+
import struct
|
|
20
|
+
import tarfile
|
|
21
|
+
import zlib
|
|
22
|
+
from collections import Counter
|
|
23
|
+
from typing import Iterable, List, Mapping, Sequence, Tuple, Union
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _undress(data: bytes) -> List[int]:
|
|
27
|
+
bits: List[int] = []
|
|
28
|
+
for b in data:
|
|
29
|
+
for i in range(7, -1, -1):
|
|
30
|
+
bits.append((b >> i) & 1)
|
|
31
|
+
return bits
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def shannon_entropy(data: bytes) -> float:
|
|
35
|
+
if not data:
|
|
36
|
+
return 0.0
|
|
37
|
+
n = len(data)
|
|
38
|
+
counts = Counter(data)
|
|
39
|
+
return -sum((v / n) * math.log2(v / n) for v in counts.values())
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _pair_entropy(bits: List[int]) -> float:
|
|
43
|
+
if len(bits) < 2:
|
|
44
|
+
return 0.0
|
|
45
|
+
pairs = [(bits[i], bits[i + 1]) for i in range(0, len(bits) - 1)]
|
|
46
|
+
n = len(pairs)
|
|
47
|
+
c = Counter(pairs)
|
|
48
|
+
return -sum((v / n) * math.log2(v / n) for v in c.values())
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def benefit_check(data: bytes, *, min_bytes: int = 1024) -> dict:
|
|
52
|
+
if data is None or len(data) == 0:
|
|
53
|
+
return {
|
|
54
|
+
"compress": False,
|
|
55
|
+
"reason": "empty payload",
|
|
56
|
+
"action": "json-chamber",
|
|
57
|
+
"recommend": "json-chamber",
|
|
58
|
+
"entropy": 0.0,
|
|
59
|
+
"bias": 0.5,
|
|
60
|
+
"pair_entropy": 0.0,
|
|
61
|
+
"size": 0,
|
|
62
|
+
"est_saving": 0.0,
|
|
63
|
+
"price_hint": 99,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
n = len(data)
|
|
67
|
+
if n < min_bytes:
|
|
68
|
+
return {
|
|
69
|
+
"compress": False,
|
|
70
|
+
"reason": f"tiny <{min_bytes}B - use json-chamber only ($99 tier); or group_then_pack first",
|
|
71
|
+
"action": "json-chamber",
|
|
72
|
+
"recommend": "group-first",
|
|
73
|
+
"entropy": shannon_entropy(data),
|
|
74
|
+
"bias": 0.5,
|
|
75
|
+
"pair_entropy": 0.0,
|
|
76
|
+
"size": n,
|
|
77
|
+
"est_saving": 0.0,
|
|
78
|
+
"price_hint": 99,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
entropy = shannon_entropy(data)
|
|
82
|
+
sample = data[:8192]
|
|
83
|
+
bits = _undress(sample)
|
|
84
|
+
zeros = bits.count(0)
|
|
85
|
+
bias = (zeros / len(bits)) if bits else 0.5
|
|
86
|
+
pair_entropy = _pair_entropy(bits)
|
|
87
|
+
|
|
88
|
+
if entropy > 7.8:
|
|
89
|
+
return {
|
|
90
|
+
"compress": False,
|
|
91
|
+
"reason": f"high entropy {entropy:.2f}/8.0 - already compressed/encrypted/random",
|
|
92
|
+
"action": "json-chamber only + chunk dedup",
|
|
93
|
+
"recommend": "json-chamber",
|
|
94
|
+
"entropy": round(entropy, 4),
|
|
95
|
+
"bias": round(bias, 4),
|
|
96
|
+
"pair_entropy": round(pair_entropy, 4),
|
|
97
|
+
"size": n,
|
|
98
|
+
"est_saving": 0.0,
|
|
99
|
+
"price_hint": 99,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
est_saving = max(0.0, (8.0 - entropy) / 8.0)
|
|
103
|
+
bias_boost = abs(bias - 0.5) * 0.4
|
|
104
|
+
est_saving = min(0.95, est_saving + bias_boost)
|
|
105
|
+
|
|
106
|
+
if est_saving < 0.08 and entropy > 7.0:
|
|
107
|
+
return {
|
|
108
|
+
"compress": False,
|
|
109
|
+
"reason": f"low structure - entropy {entropy:.2f}, bias {bias:.3f}, est {est_saving*100:.1f}%",
|
|
110
|
+
"action": "json-chamber",
|
|
111
|
+
"recommend": "json-chamber",
|
|
112
|
+
"entropy": round(entropy, 4),
|
|
113
|
+
"bias": round(bias, 4),
|
|
114
|
+
"pair_entropy": round(pair_entropy, 4),
|
|
115
|
+
"size": n,
|
|
116
|
+
"est_saving": round(est_saving, 4),
|
|
117
|
+
"price_hint": 99,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
"compress": True,
|
|
122
|
+
"reason": (
|
|
123
|
+
f"good candidate - entropy {entropy:.2f}, bias {bias:.3f}, "
|
|
124
|
+
f"est {est_saving*100:.1f}% saving"
|
|
125
|
+
),
|
|
126
|
+
"action": "tru8-chamber $1900 tier",
|
|
127
|
+
"recommend": "tru8-chamber",
|
|
128
|
+
"entropy": round(entropy, 4),
|
|
129
|
+
"bias": round(bias, 4),
|
|
130
|
+
"pair_entropy": round(pair_entropy, 4),
|
|
131
|
+
"size": n,
|
|
132
|
+
"est_saving": round(est_saving, 4),
|
|
133
|
+
"price_hint": 1900,
|
|
134
|
+
"note": "TRU8 engine is proprietary — contact license@slidphilabs.com",
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
tru8_benefit_check = benefit_check
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def delta_transform_positions(
|
|
142
|
+
positions: Sequence[float],
|
|
143
|
+
*,
|
|
144
|
+
quantize: float = 1e-4,
|
|
145
|
+
) -> bytes:
|
|
146
|
+
if not positions:
|
|
147
|
+
return b""
|
|
148
|
+
quantized = [int(round(p / quantize)) for p in positions]
|
|
149
|
+
deltas = [quantized[0]]
|
|
150
|
+
for i in range(1, len(quantized)):
|
|
151
|
+
deltas.append(quantized[i] - quantized[i - 1])
|
|
152
|
+
out = io.BytesIO()
|
|
153
|
+
for d in deltas:
|
|
154
|
+
zz = (d << 1) ^ (d >> 63) if d < 0 else (d << 1)
|
|
155
|
+
while zz >= 0x80:
|
|
156
|
+
out.write(bytes([(zz & 0x7F) | 0x80]))
|
|
157
|
+
zz >>= 7
|
|
158
|
+
out.write(bytes([zz & 0x7F]))
|
|
159
|
+
return out.getvalue()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def delta_transform_bytes(raw: bytes, *, stride: int = 4, fmt: str = "<f") -> bytes:
|
|
163
|
+
size = struct.calcsize(fmt)
|
|
164
|
+
n = len(raw) // size
|
|
165
|
+
if n == 0:
|
|
166
|
+
return raw
|
|
167
|
+
vals = list(struct.unpack("%d%s" % (n, fmt[-1] if fmt[-1] in "fd" else "f"), raw[: n * size]))
|
|
168
|
+
return delta_transform_positions(vals)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def group_then_pack(
|
|
172
|
+
files: Union[Mapping[str, bytes], Sequence[Tuple[str, bytes]]],
|
|
173
|
+
*,
|
|
174
|
+
mode: str = "tar",
|
|
175
|
+
) -> bytes:
|
|
176
|
+
if isinstance(files, Mapping):
|
|
177
|
+
items = list(files.items())
|
|
178
|
+
else:
|
|
179
|
+
items = list(files)
|
|
180
|
+
if mode == "tar":
|
|
181
|
+
buf = io.BytesIO()
|
|
182
|
+
with tarfile.open(fileobj=buf, mode="w") as tar:
|
|
183
|
+
for name, payload in items:
|
|
184
|
+
info = tarfile.TarInfo(name=str(name).replace("\\", "/").lstrip("/"))
|
|
185
|
+
info.size = len(payload)
|
|
186
|
+
tar.addfile(info, io.BytesIO(payload))
|
|
187
|
+
return buf.getvalue()
|
|
188
|
+
buf = io.BytesIO()
|
|
189
|
+
for name, data in items:
|
|
190
|
+
name_b = str(name).encode("utf-8")
|
|
191
|
+
buf.write(struct.pack("<I", len(name_b)))
|
|
192
|
+
buf.write(name_b)
|
|
193
|
+
buf.write(struct.pack("<I", len(data)))
|
|
194
|
+
buf.write(data)
|
|
195
|
+
return buf.getvalue()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def group_then_compress(
|
|
199
|
+
files: Union[Mapping[str, bytes], Sequence[Tuple[str, bytes]]],
|
|
200
|
+
) -> dict:
|
|
201
|
+
if isinstance(files, Mapping):
|
|
202
|
+
items = list(files.items())
|
|
203
|
+
else:
|
|
204
|
+
items = list(files)
|
|
205
|
+
total_raw = sum(len(d) for _, d in items)
|
|
206
|
+
grouped_bytes = group_then_pack(items, mode="tar")
|
|
207
|
+
check = benefit_check(grouped_bytes)
|
|
208
|
+
compressed = zlib.compress(grouped_bytes, level=9)
|
|
209
|
+
group_saving = 1.0 - (len(compressed) / len(grouped_bytes)) if grouped_bytes else 0.0
|
|
210
|
+
individual_saving = []
|
|
211
|
+
for _, data in items:
|
|
212
|
+
if not data:
|
|
213
|
+
individual_saving.append(0.0)
|
|
214
|
+
continue
|
|
215
|
+
c = zlib.compress(data, level=9)
|
|
216
|
+
individual_saving.append(1.0 - (len(c) / len(data)))
|
|
217
|
+
avg_individual = sum(individual_saving) / len(individual_saving) if individual_saving else 0.0
|
|
218
|
+
return {
|
|
219
|
+
"total_files": len(items),
|
|
220
|
+
"total_raw": total_raw,
|
|
221
|
+
"grouped_raw": len(grouped_bytes),
|
|
222
|
+
"compressed": len(compressed),
|
|
223
|
+
"group_saving": round(group_saving, 4),
|
|
224
|
+
"avg_individual_saving": round(avg_individual, 4),
|
|
225
|
+
"gain_from_grouping": round(group_saving - avg_individual, 4),
|
|
226
|
+
"benefit_check": check,
|
|
227
|
+
"recommendation": (
|
|
228
|
+
f"Grouping created {(group_saving - avg_individual)*100:.1f}% extra saving"
|
|
229
|
+
if group_saving > avg_individual
|
|
230
|
+
else "Grouping did not beat per-file baseline"
|
|
231
|
+
),
|
|
232
|
+
"note": "zlib baseline only — TRU8 residual engine is proprietary ($1,900 tier)",
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def chunk_dedup(files: Sequence[bytes], chunk_size: int = 4096) -> dict:
|
|
237
|
+
seen = {}
|
|
238
|
+
total_chunks = 0
|
|
239
|
+
dup_chunks = 0
|
|
240
|
+
for data in files:
|
|
241
|
+
for i in range(0, len(data), chunk_size):
|
|
242
|
+
chunk = data[i : i + chunk_size]
|
|
243
|
+
h = hashlib.blake2b(chunk, digest_size=16).hexdigest()
|
|
244
|
+
total_chunks += 1
|
|
245
|
+
if h in seen:
|
|
246
|
+
dup_chunks += 1
|
|
247
|
+
else:
|
|
248
|
+
seen[h] = True
|
|
249
|
+
dedup_ratio = (dup_chunks / total_chunks) if total_chunks else 0.0
|
|
250
|
+
return {
|
|
251
|
+
"total_chunks": total_chunks,
|
|
252
|
+
"unique_chunks": len(seen),
|
|
253
|
+
"dup_chunks": dup_chunks,
|
|
254
|
+
"dedup_saving": round(dedup_ratio, 4),
|
|
255
|
+
"reason": (
|
|
256
|
+
f"{dup_chunks}/{total_chunks} chunks duplicate - "
|
|
257
|
+
f"{dedup_ratio*100:.1f}% saving via dedup even if entropy high"
|
|
258
|
+
),
|
|
259
|
+
"action": "use chunk dedup + json-chamber for high entropy bundles",
|
|
260
|
+
"note": "analysis only — production dedup packer is $1,900 tier",
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def benefit_report(*blobs: bytes, names: Sequence[str] | None = None) -> dict:
|
|
265
|
+
per_file = []
|
|
266
|
+
for i, b in enumerate(blobs):
|
|
267
|
+
label = names[i] if names and i < len(names) else f"file_{i}"
|
|
268
|
+
r = benefit_check(b)
|
|
269
|
+
r["name"] = label
|
|
270
|
+
per_file.append(r)
|
|
271
|
+
grouped = None
|
|
272
|
+
if len(blobs) >= 2:
|
|
273
|
+
packed = group_then_pack({r["name"]: blobs[i] for i, r in enumerate(per_file)})
|
|
274
|
+
grouped = benefit_check(packed)
|
|
275
|
+
grouped["name"] = "__grouped__"
|
|
276
|
+
grouped["packed_bytes"] = len(packed)
|
|
277
|
+
any_compress = any(r.get("compress") for r in per_file) or (
|
|
278
|
+
grouped.get("compress") if grouped else False
|
|
279
|
+
)
|
|
280
|
+
return {
|
|
281
|
+
"files": per_file,
|
|
282
|
+
"grouped": grouped,
|
|
283
|
+
"any_compress_candidate": bool(any_compress),
|
|
284
|
+
"funnel": (
|
|
285
|
+
"tru8-chamber ($1,900/project/year)"
|
|
286
|
+
if any_compress
|
|
287
|
+
else "json-chamber ($99) — seal only"
|
|
288
|
+
),
|
|
289
|
+
}
|
json_chamber/core.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Chamber core – φ-split + keyword lock.
|
|
3
|
+
|
|
4
|
+
A 32-byte session key is derived, then split with a simple 2-of-2
|
|
5
|
+
φ-inspired share (XOR + golden-ratio bit rotation) into two 16-byte
|
|
6
|
+
halves. Each half is rendered as a space-separated word list drawn
|
|
7
|
+
from a curated chamber vocabulary. The sealed object ships both
|
|
8
|
+
word lists + nonce + tag + ciphertext; neither list alone can
|
|
9
|
+
reconstruct the key. Opening requires both lists + the master secret
|
|
10
|
+
(used as an additional KDF salt) + a live license.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import hmac
|
|
17
|
+
import os
|
|
18
|
+
from typing import Sequence
|
|
19
|
+
|
|
20
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
21
|
+
|
|
22
|
+
# Curated chamber vocabulary (64 words → 6 bits each, clean encoding)
|
|
23
|
+
_WORDS: tuple[str, ...] = (
|
|
24
|
+
"ward", "veil", "spire", "lock", "oath", "cloak", "shard", "mark",
|
|
25
|
+
"rune", "gate", "seal", "vault", "cipher", "glyph", "prism", "echo",
|
|
26
|
+
"aegis", "sigil", "forge", "anchor", "tide", "ember", "frost", "bloom",
|
|
27
|
+
"thorn", "quill", "mirror", "hollow", "crown", "blade", "root", "star",
|
|
28
|
+
"mist", "iron", "glass", "stone", "flame", "shade", "pulse", "nexus",
|
|
29
|
+
"orbit", "vector", "lattice", "helix", "core", "rim", "axis", "node",
|
|
30
|
+
"flux", "phase", "drift", "spark", "void", "bound", "link", "key",
|
|
31
|
+
"guard", "watch", "keep", "hold", "bind", "cast", "form", "rise",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
assert len(_WORDS) == 64
|
|
35
|
+
WORDLIST = _WORDS
|
|
36
|
+
|
|
37
|
+
# φ ≈ 1.6180339887 – used for a deterministic bit rotation amount
|
|
38
|
+
_PHI_ROT = 21 # round(32 * (φ - 1)) ≈ 19.4 → pick 21 for nice mix
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _bytes_to_words(data: bytes) -> str:
|
|
42
|
+
"""Encode exact bytes as space-separated words (6 bits per word)."""
|
|
43
|
+
pad = (3 - len(data) % 3) % 3
|
|
44
|
+
buf = data + b"\x00" * pad
|
|
45
|
+
words: list[str] = []
|
|
46
|
+
for i in range(0, len(buf), 3):
|
|
47
|
+
n = (buf[i] << 16) | (buf[i + 1] << 8) | buf[i + 2]
|
|
48
|
+
for shift in (18, 12, 6, 0):
|
|
49
|
+
words.append(_WORDS[(n >> shift) & 0x3F])
|
|
50
|
+
return " ".join(words)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _words_to_bytes(text: str, expected_len: int) -> bytes:
|
|
54
|
+
"""Decode word list back to bytes, truncate to expected_len."""
|
|
55
|
+
tokens = text.strip().lower().split()
|
|
56
|
+
idx = {_WORDS[i]: i for i in range(64)}
|
|
57
|
+
out = bytearray()
|
|
58
|
+
for i in range(0, len(tokens), 4):
|
|
59
|
+
chunk = tokens[i : i + 4]
|
|
60
|
+
if len(chunk) < 4:
|
|
61
|
+
break
|
|
62
|
+
n = 0
|
|
63
|
+
for w in chunk:
|
|
64
|
+
if w not in idx:
|
|
65
|
+
raise ValueError(f"unknown chamber word: {w!r}")
|
|
66
|
+
n = (n << 6) | idx[w]
|
|
67
|
+
out.append((n >> 16) & 0xFF)
|
|
68
|
+
out.append((n >> 8) & 0xFF)
|
|
69
|
+
out.append(n & 0xFF)
|
|
70
|
+
return bytes(out[:expected_len])
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _phi_split(key32: bytes) -> tuple[bytes, bytes]:
|
|
74
|
+
"""
|
|
75
|
+
2-of-2 φ-split.
|
|
76
|
+
k = first 16 bytes rotated by φ amount
|
|
77
|
+
r = second 16 bytes XOR k
|
|
78
|
+
Reconstruct: r XOR k → second half, un-rotate → full key.
|
|
79
|
+
"""
|
|
80
|
+
if len(key32) != 32:
|
|
81
|
+
raise ValueError("key must be 32 bytes")
|
|
82
|
+
left = bytearray(key32[:16])
|
|
83
|
+
right = bytearray(key32[16:])
|
|
84
|
+
bits = int.from_bytes(left, "big")
|
|
85
|
+
rot = ((bits << _PHI_ROT) | (bits >> (128 - _PHI_ROT))) & ((1 << 128) - 1)
|
|
86
|
+
k = rot.to_bytes(16, "big")
|
|
87
|
+
r = bytes(a ^ b for a, b in zip(right, k))
|
|
88
|
+
return k, r
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _phi_join(k: bytes, r: bytes) -> bytes:
|
|
92
|
+
if len(k) != 16 or len(r) != 16:
|
|
93
|
+
raise ValueError("shares must be 16 bytes each")
|
|
94
|
+
right = bytes(a ^ b for a, b in zip(r, k))
|
|
95
|
+
bits = int.from_bytes(k, "big")
|
|
96
|
+
unrot = ((bits >> _PHI_ROT) | (bits << (128 - _PHI_ROT))) & ((1 << 128) - 1)
|
|
97
|
+
left = unrot.to_bytes(16, "big")
|
|
98
|
+
return left + right
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _derive_session_key(master: bytes, salt: bytes = b"chamber-phi-v1") -> bytes:
|
|
102
|
+
return hashlib.pbkdf2_hmac("sha256", master, salt, iterations=120_000, dklen=32)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _bind_key(msg_key: bytes, ct: bytes) -> bytes:
|
|
106
|
+
"""AONT bind: message key XOR repeating SHA-256 of ciphertext."""
|
|
107
|
+
h = hashlib.sha256(ct).digest()
|
|
108
|
+
return bytes(msg_key[i] ^ h[i % 32] for i in range(32))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def seal(plaintext: bytes, master: bytes) -> dict:
|
|
112
|
+
"""v1 seal — both word lists required. Kept so already-sealed blobs still open."""
|
|
113
|
+
session = _derive_session_key(master)
|
|
114
|
+
msg_key = os.urandom(32)
|
|
115
|
+
k, r = _phi_split(msg_key)
|
|
116
|
+
|
|
117
|
+
aesgcm = AESGCM(msg_key)
|
|
118
|
+
nonce = os.urandom(12)
|
|
119
|
+
ct = aesgcm.encrypt(nonce, plaintext, associated_data=b"chamber-v1")
|
|
120
|
+
|
|
121
|
+
tag = hashlib.sha256(k + r + nonce).digest()[:16]
|
|
122
|
+
_ = session
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
"v": 1,
|
|
126
|
+
"algo": "chamber-aes256gcm-phi",
|
|
127
|
+
"k_words": _bytes_to_words(k),
|
|
128
|
+
"r_words": _bytes_to_words(r),
|
|
129
|
+
"nonce": nonce.hex(),
|
|
130
|
+
"tag": tag.hex(),
|
|
131
|
+
"ct": ct.hex(),
|
|
132
|
+
"orig_len": len(plaintext),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def seal_aont(plaintext: bytes, master: bytes) -> dict:
|
|
137
|
+
"""v2: encrypt, bind the key to the ciphertext, then φ-split the bound key.
|
|
138
|
+
|
|
139
|
+
One share plus the blob is not enough to recover the message key.
|
|
140
|
+
Lab-owned AONT, not a Shamir library wrap.
|
|
141
|
+
"""
|
|
142
|
+
_ = _derive_session_key(master)
|
|
143
|
+
msg_key = os.urandom(32)
|
|
144
|
+
aesgcm = AESGCM(msg_key)
|
|
145
|
+
nonce = os.urandom(12)
|
|
146
|
+
ct = aesgcm.encrypt(nonce, plaintext, associated_data=b"chamber-v2")
|
|
147
|
+
package_key = _bind_key(msg_key, ct)
|
|
148
|
+
k, r = _phi_split(package_key)
|
|
149
|
+
tag = hashlib.sha256(k + r + nonce + ct[:16]).digest()[:16]
|
|
150
|
+
return {
|
|
151
|
+
"v": 2,
|
|
152
|
+
"algo": "chamber-aes256gcm-phi-aont",
|
|
153
|
+
"k_words": _bytes_to_words(k),
|
|
154
|
+
"r_words": _bytes_to_words(r),
|
|
155
|
+
"nonce": nonce.hex(),
|
|
156
|
+
"tag": tag.hex(),
|
|
157
|
+
"ct": ct.hex(),
|
|
158
|
+
"orig_len": len(plaintext),
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def open_sealed(sealed: dict, master: bytes) -> bytes:
|
|
163
|
+
"""Reconstruct key from both word lists and decrypt. v1 and v2."""
|
|
164
|
+
ver = sealed.get("v")
|
|
165
|
+
if ver == 1:
|
|
166
|
+
if sealed.get("algo") != "chamber-aes256gcm-phi":
|
|
167
|
+
raise ValueError("algo mismatch")
|
|
168
|
+
k = _words_to_bytes(sealed["k_words"], 16)
|
|
169
|
+
r = _words_to_bytes(sealed["r_words"], 16)
|
|
170
|
+
nonce = bytes.fromhex(sealed["nonce"])
|
|
171
|
+
expected_tag = hashlib.sha256(k + r + nonce).digest()[:16]
|
|
172
|
+
if not hmac_compare(expected_tag, bytes.fromhex(sealed["tag"])):
|
|
173
|
+
raise ValueError("share integrity tag mismatch – possible tamper")
|
|
174
|
+
msg_key = _phi_join(k, r)
|
|
175
|
+
_ = _derive_session_key(master)
|
|
176
|
+
aesgcm = AESGCM(msg_key)
|
|
177
|
+
ct = bytes.fromhex(sealed["ct"])
|
|
178
|
+
return aesgcm.decrypt(nonce, ct, associated_data=b"chamber-v1")
|
|
179
|
+
if ver == 2:
|
|
180
|
+
if sealed.get("algo") != "chamber-aes256gcm-phi-aont":
|
|
181
|
+
raise ValueError("algo mismatch")
|
|
182
|
+
k = _words_to_bytes(sealed["k_words"], 16)
|
|
183
|
+
r = _words_to_bytes(sealed["r_words"], 16)
|
|
184
|
+
nonce = bytes.fromhex(sealed["nonce"])
|
|
185
|
+
ct = bytes.fromhex(sealed["ct"])
|
|
186
|
+
expected_tag = hashlib.sha256(k + r + nonce + ct[:16]).digest()[:16]
|
|
187
|
+
if not hmac_compare(expected_tag, bytes.fromhex(sealed["tag"])):
|
|
188
|
+
raise ValueError("share integrity tag mismatch – possible tamper")
|
|
189
|
+
package_key = _phi_join(k, r)
|
|
190
|
+
msg_key = _bind_key(package_key, ct)
|
|
191
|
+
_ = _derive_session_key(master)
|
|
192
|
+
aesgcm = AESGCM(msg_key)
|
|
193
|
+
return aesgcm.decrypt(nonce, ct, associated_data=b"chamber-v2")
|
|
194
|
+
raise ValueError(f"unsupported chamber version: {ver}")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def hmac_compare(a: bytes, b: bytes) -> bool:
|
|
198
|
+
if len(a) != len(b):
|
|
199
|
+
return False
|
|
200
|
+
return hmac.compare_digest(a, b)
|