Skip to content

Commit a9123e0

Browse files
gijzelaerrclaude
andcommitted
session_auth/family0: add LutGenerator and ChecksumTransform
Thirteenth slice of the HarpoS7 port (refs #717). Manual port of the two GHASH-style transforms — both vector-verified against HarpoS7's fixtures. LutGenerator builds a 4 KB table of 256 UInt128 values from a 16-byte seed by iterated GF(2¹²⁸) doubling under the AES-GCM-style reduction polynomial x¹²⁸ + x⁷ + x² + x + 1. ChecksumTransform consumes that table plus a 16-byte key to produce a 16-byte integrity tag, walking key bytes MSB→LSB and rotating the work buffer one byte after every 4-byte block. Neither depends on the still-broken Monolith9/10 path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent fa78b7e commit a9123e0

8 files changed

Lines changed: 187 additions & 0 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""GF(2¹²⁸)-based checksum / GHASH-style integrity transform.
2+
3+
Manual port of ``HarpoS7.Family0.Transforms.ChecksumTransform``. Takes
4+
a 16-byte key and a 4 KB lookup table (from ``lut_generator``) and
5+
produces a 16-byte checksum. The work buffer is a uint32[8] and the
6+
algorithm walks through key bytes from MSB to LSB, XORing in
7+
``lut[byte * 16 .. byte * 16 + 16]`` and rotating after every 4-byte
8+
block.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import struct
14+
15+
KEY_SIZE = 0x10
16+
DESTINATION_SIZE = 0x10
17+
LOOKUP_TABLE_SIZE = 0x1000
18+
19+
_U32 = 0xFFFFFFFF
20+
21+
22+
def _xor_128(work: list[int], offset: int, lut: list[int], lut_index: int) -> None:
23+
"""XOR four uint32s from ``lut[lut_index..lut_index+4]`` into
24+
``work[offset..offset+4]`` in place."""
25+
for i in range(4):
26+
work[offset + i] ^= lut[lut_index + i]
27+
28+
29+
def execute(destination: bytearray, key: bytes, lookup_table: bytes) -> None:
30+
"""Compute the 16-byte checksum.
31+
32+
Args:
33+
destination: 16-byte buffer for the result.
34+
key: 16-byte input key.
35+
lookup_table: 4 KB table from ``lut_generator.execute``.
36+
"""
37+
if len(destination) < DESTINATION_SIZE:
38+
raise ValueError(f"destination must be at least {DESTINATION_SIZE} bytes")
39+
if len(key) < KEY_SIZE:
40+
raise ValueError(f"key must be at least {KEY_SIZE} bytes")
41+
if len(lookup_table) < LOOKUP_TABLE_SIZE:
42+
raise ValueError(f"lookup_table must be at least {LOOKUP_TABLE_SIZE} bytes")
43+
44+
work = [0] * 8
45+
46+
key_dwords = list(struct.unpack("<4I", bytes(key[:16])))
47+
lut_dwords = list(struct.unpack(f"<{LOOKUP_TABLE_SIZE // 4}I", bytes(lookup_table[:LOOKUP_TABLE_SIZE])))
48+
49+
# Walk key bytes from byte 3 down to byte 1 (in each uint32 of the key).
50+
for i in (0x18, 0x10, 0x08):
51+
for j in range(4):
52+
lut_index = ((key_dwords[j] >> i) & 0xFF) << 2
53+
_xor_128(work, j, lut_dwords, lut_index)
54+
55+
# Rotate work buffer left by one byte.
56+
for j in range(7, 0, -1):
57+
work[j] = ((work[j - 1] >> 0x18) | ((work[j] << 0x08) & _U32)) & _U32
58+
work[0] = (work[0] << 0x08) & _U32
59+
60+
# Final round: lowest byte of each key uint32.
61+
for i in range(4):
62+
lut_index = (key_dwords[i] & 0xFF) << 2
63+
_xor_128(work, i, lut_dwords, lut_index)
64+
65+
# Final mixing.
66+
temp = (((work[7] >> 0x0D) ^ work[7]) >> 0x11 ^ work[4] ^ work[7]) & _U32
67+
68+
dst = [0] * 4
69+
dst[0] = (((((temp << 0x0D) & _U32) ^ temp) << 0x02) & _U32 ^ work[0] ^ temp) & _U32
70+
dst[1] = (
71+
((temp >> 0x0D) ^ temp) >> 0x11 ^ ((((work[5] << 0x0D) & _U32) ^ work[5]) << 2) & _U32 ^ work[1] ^ temp ^ work[5]
72+
) & _U32
73+
dst[2] = (
74+
((work[5] >> 0x0D) ^ work[5]) >> 0x11 ^ ((((work[6] << 0x0D) & _U32) ^ work[6]) << 2) & _U32 ^ work[2] ^ work[5] ^ work[6]
75+
) & _U32
76+
dst[3] = (
77+
((work[6] >> 0x0D) ^ work[6]) >> 0x11 ^ ((((work[7] << 0x0D) & _U32) ^ work[7]) << 2) & _U32 ^ work[3] ^ work[6] ^ work[7]
78+
) & _U32
79+
80+
struct.pack_into("<4I", destination, 0, *dst)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""Lookup-table generator used by ``ChecksumTransform`` (HarpoHash variant).
2+
3+
Builds a 4 KB table of 256 ``UInt128`` entries from a 16-byte seed
4+
key. Each entry is the seed multiplied by ``i`` over GF(2¹²⁸) under
5+
the polynomial ``x^128 + x^7 + x^2 + x + 1`` (canonical AES-GCM
6+
field). The construction doubles iteratively and cross-XORs to fill
7+
the rest of the rows.
8+
9+
Manual port of ``HarpoS7.Family0.Transforms.LutGenerator``.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import struct
15+
16+
SOURCE_SIZE = 0x10
17+
DESTINATION_SIZE = 0x1000
18+
19+
_U128 = (1 << 128) - 1
20+
_REDUCTION = 0x010000_8005 # x^128 + x^7 + x^2 + x + 1, low 33 bits
21+
22+
23+
def _to_u128_list(buf: bytes, count: int) -> list[int]:
24+
"""Read ``count`` little-endian 16-byte values as Python ints."""
25+
out = []
26+
for i in range(count):
27+
out.append(int.from_bytes(bytes(buf[i * 16 : (i + 1) * 16]), "little"))
28+
return out
29+
30+
31+
def _from_u128_list(values: list[int]) -> bytes:
32+
parts = [(v & _U128).to_bytes(16, "little") for v in values]
33+
return b"".join(parts)
34+
35+
36+
def execute(destination: bytearray, source: bytes) -> None:
37+
"""Generate the 4 KB lookup table from a 16-byte source.
38+
39+
Args:
40+
destination: 4096-byte buffer to populate.
41+
source: 16-byte seed key.
42+
"""
43+
if len(destination) < DESTINATION_SIZE:
44+
raise ValueError(f"destination must be at least {DESTINATION_SIZE} bytes, got {len(destination)}")
45+
if len(source) < SOURCE_SIZE:
46+
raise ValueError(f"source must be at least {SOURCE_SIZE} bytes, got {len(source)}")
47+
48+
# Initial layout:
49+
# dwords [0..4) = 0 → quadDwords[0] = 0
50+
# dwords [4..8) = source → quadDwords[1] = source as UInt128
51+
# dwords [8..) = 0 (zero-initialised destination)
52+
quads = [0] * 256
53+
quads[1] = int.from_bytes(bytes(source[:16]), "little")
54+
55+
i = 1
56+
while i < 128:
57+
multiplicand = quads[i]
58+
product = (multiplicand * 2) & _U128
59+
if (multiplicand >> 0x7F) != 0:
60+
product ^= _REDUCTION
61+
62+
product_index = i * 2
63+
quads[product_index] = product
64+
65+
for j in range(1, product_index):
66+
quads[product_index + j] = quads[j] ^ product
67+
68+
i *= 2
69+
70+
destination[:DESTINATION_SIZE] = _from_u128_list(quads)
71+
72+
73+
# Backwards-compatible shim: tests can import via class-style or
74+
# function-style. The function entry point is canonical.
75+
del struct # not used outside the helper functions
4 KB
Binary file not shown.
16 Bytes
Binary file not shown.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Gg"j.�����<��
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
%%%%%%%%%%%%%%%%
4 KB
Binary file not shown.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""Vector tests for the LutGenerator and ChecksumTransform Family-0 transforms.
2+
3+
Vendored from ``HarpoS7.Family0.Tests/Transforms/TransformTests.cs``
4+
plus the corresponding ``Blobs/Transforms`` fixtures.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from pathlib import Path
10+
11+
from s7.session_auth.family0 import checksum_transform, lut_generator
12+
13+
_FIXTURES = Path(__file__).parent / "fixtures" / "family0" / "transforms"
14+
15+
16+
def test_lut_generator_vector() -> None:
17+
src = (_FIXTURES / "transform3-src.bin").read_bytes()
18+
expected = (_FIXTURES / "transform3-dst.bin").read_bytes()
19+
dst = bytearray(lut_generator.DESTINATION_SIZE)
20+
lut_generator.execute(dst, src)
21+
assert bytes(dst) == expected
22+
23+
24+
def test_checksum_transform_vector() -> None:
25+
key = (_FIXTURES / "transform4-key.bin").read_bytes()
26+
lut = (_FIXTURES / "transform4-lut.bin").read_bytes()
27+
expected = (_FIXTURES / "transform4-dst.bin").read_bytes()
28+
dst = bytearray(checksum_transform.DESTINATION_SIZE)
29+
checksum_transform.execute(dst, key, lut)
30+
assert bytes(dst) == expected

0 commit comments

Comments
 (0)