Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7a86ada
Start V2 legitimation work (refs #710)
gijzelaerr Apr 27, 2026
0c9af7c
Send TIA-style CreateObject + V2 session-setup echo
gijzelaerr Apr 27, 2026
0ac2091
Fix CreateObject response parsing (10-byte header, ObjectIds in body)
gijzelaerr Apr 27, 2026
5ebf29e
Strip device PAOM string when echoing ServerSessionVersion
gijzelaerr Apr 28, 2026
a2111e7
Send V2 session-setup legitimation alongside ServerSessionVersion
gijzelaerr Apr 28, 2026
eae0160
Suppress misleading "not found" log when version was captured
gijzelaerr Apr 29, 2026
0a90945
Revert speculative SessionKey send; keep public-key checksum capture
gijzelaerr Apr 29, 2026
b44792d
session_auth: add public-key store ported from HarpoS7
gijzelaerr Apr 29, 2026
8fe9f2c
session_auth: add derive_key_id (8-byte SHA-256 fingerprint)
gijzelaerr Apr 29, 2026
6f91b65
session_auth: add SecurityKeyEncryptedKey metadata writer
gijzelaerr Apr 30, 2026
89d02b4
session_auth: add SHA-256 key-derivation helpers
gijzelaerr Apr 30, 2026
daf706b
session_auth: add HarpoHash transform (lut1, generate_lookup_table, h…
gijzelaerr May 1, 2026
45b18f0
session_auth: add HarpoAes (AES-128-ECB context)
gijzelaerr May 1, 2026
9437fec
session_auth: add HarpoAesCtr (custom CTR + integrity accumulator)
gijzelaerr May 1, 2026
be054db
session_auth: add HarpoAesCtr.calculate_checksum
gijzelaerr May 1, 2026
054789a
session_auth/family0: transpile + verify 9 of 11 Family-0 monoliths
gijzelaerr May 1, 2026
4bb517c
session_auth/family0: transpile Nine/Ten Parts + Monolith9/10 orchest…
gijzelaerr May 1, 2026
b9c11d1
session_auth/family0: add BigIntOperations (Prepare/Finalize/rotate)
gijzelaerr May 1, 2026
fa78b7e
session_auth/family0: add BigInt arithmetic transforms
gijzelaerr May 1, 2026
a9123e0
session_auth/family0: add LutGenerator and ChecksumTransform
gijzelaerr May 1, 2026
fe8e81b
transpiler: mask left-shift and multiply to uint32 in subexpressions
gijzelaerr May 9, 2026
3128a1f
fix: three transpiler bugs causing Monolith9/10 byte-divergence
gijzelaerr May 9, 2026
578d9e0
session_auth: complete Family-0 auth chain for S7-1200/1500
gijzelaerr May 10, 2026
f5e7769
add missing data tables and transform vector tests
gijzelaerr May 10, 2026
efa2500
replace 7 opaque .bin files with readable Python constants
gijzelaerr May 10, 2026
77df6f4
wire SessionKey auth blob into _setup_session
gijzelaerr May 10, 2026
6305923
fix pre-commit: format code, exclude .bin from end-of-file-fixer
gijzelaerr May 10, 2026
43d5df5
fix: mark transpile_harpo_monolith.py as executable
gijzelaerr May 10, 2026
f80dab3
fix: parse session challenge as USINT array at attribute 303
gijzelaerr May 11, 2026
bbcaf6f
fix: use absolute VLQ attr IDs in SecurityKey struct encoding
gijzelaerr May 12, 2026
1253598
fix: match PoC BLOB encoding and symmetric key flags
gijzelaerr May 12, 2026
ac84da7
add V3 HMAC integrity framing for post-SessionKey data ops
gijzelaerr May 13, 2026
ef9aee0
use TIA Portal EXPLORE format for V1-initial PLCs
gijzelaerr May 13, 2026
2b78b3c
use V3 explore format for list_datablocks and browse
gijzelaerr May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ repos:
rev: v6.0.0
hooks:
- id: trailing-whitespace
exclude: '\.bin$'
- id: end-of-file-fixer
exclude: '\.bin$'
- id: check-ast
- id: check-shebang-scripts-are-executable
- id: check-json
Expand Down
30 changes: 27 additions & 3 deletions s7/_s7commplus_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,10 @@ def explore(self, explore_id: int = 0) -> bytes:
if self._connection is None:
raise RuntimeError("Not connected")

payload = _build_explore_payload(explore_id)
if self._connection._session_key is not None:
payload = _build_explore_payload_v3(explore_id if explore_id else 0x38)
else:
payload = _build_explore_payload(explore_id)
response = self._connection.send_request(FunctionCode.EXPLORE, payload)
return response

Expand Down Expand Up @@ -365,7 +368,12 @@ def list_datablocks(self) -> list[dict[str, Any]]:
if self._connection is None:
raise RuntimeError("Not connected")

payload = _build_explore_request(Ids.NATIVE_THE_PLC_PROGRAM_RID, [Ids.OBJECT_VARIABLE_TYPE_NAME, Ids.BLOCK_BLOCK_NUMBER])
if self._connection._session_key is not None:
payload = _build_explore_payload_v3(Ids.NATIVE_THE_PLC_PROGRAM_RID)
else:
payload = _build_explore_request(
Ids.NATIVE_THE_PLC_PROGRAM_RID, [Ids.OBJECT_VARIABLE_TYPE_NAME, Ids.BLOCK_BLOCK_NUMBER]
)
response = self._connection.send_request(FunctionCode.EXPLORE, payload)
return _parse_explore_datablocks(response)

Expand Down Expand Up @@ -394,7 +402,10 @@ def browse(self) -> list[dict[str, Any]]:
db_rid = db_info.get("rid", 0)
if db_rid == 0:
continue
payload = _build_explore_request(db_rid, [Ids.OBJECT_VARIABLE_TYPE_NAME])
if self._connection._session_key is not None:
payload = _build_explore_payload_v3(db_rid)
else:
payload = _build_explore_request(db_rid, [Ids.OBJECT_VARIABLE_TYPE_NAME])
try:
response = self._connection.send_request(FunctionCode.EXPLORE, payload)
fields = _parse_explore_fields(response, db_info["number"], db_info["name"])
Expand Down Expand Up @@ -720,6 +731,19 @@ def _build_explore_payload(explore_id: int = 0) -> bytes:
return bytes(payload)


def _build_explore_payload_v3(explore_id: int, sequence: int = 10) -> bytes:
"""Build a V3-style EXPLORE request payload matching TIA Portal format.

V1-initial PLCs use a 4-byte big-endian InObjectId followed by
fixed parameters, rather than the VLQ-based format.
"""
payload = struct.pack(">I", explore_id)
payload += bytes([0x00, 0x01, 0x00, 0x01, 0x00, 0x00])
payload += bytes([sequence & 0xFF])
payload += bytes([0x00, 0x00, 0x00, 0x00, 0x00])
return payload


def _build_invoke_payload(state: int) -> bytes:
"""Build an INVOKE request payload for SetPlcOperatingState.

Expand Down
14 changes: 8 additions & 6 deletions s7/_s7commplus_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -635,23 +635,25 @@ def _handle_create_object(self, seq_num: int, request_data: bytes) -> bytes:
session_id = self._next_session_id
self._next_session_id += 1

# Build CreateObject response
# CreateObject response header is 10 bytes — there is no session_id
# field; session IDs come from the ResponseSet body's ObjectIds list.
# Reference: thomas-v2/S7CommPlusDriver CreateObjectResponse.Deserialize
response = bytearray()

# Response header
response += struct.pack(
">BHHHHIB",
">BHHHHB",
Opcode.RESPONSE,
0x0000, # Reserved
FunctionCode.CREATE_OBJECT,
0x0000, # Reserved
seq_num,
session_id,
0x00, # Transport flags
)

# Return code: success
response += encode_uint32_vlq(0)
# ResponseSet
response += encode_uint32_vlq(0) # ReturnValue
response += bytes([1]) # ObjectIdCount
response += encode_uint32_vlq(session_id) # ObjectIds[0] = session id

# Object with session info
response += bytes([ElementID.START_OF_OBJECT])
Expand Down
Loading
Loading