ksjsb/core/h5_sig3.py
2026-07-30 20:25:56 +08:00

380 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""H5 `__NS_sig3` 34-byte envelope helpers.
The Nebula H5 bridge returns a 68-hex digest shape that is distinct from the
regular API 10418 48-hex `__NS_sig3`. The final byte is a checksum mask; the
first 33 bytes are XOR-mixed with `(mask ^ index)`.
"""
from __future__ import annotations
import zlib
from dataclasses import dataclass
from .sig3 import KWSG_10418_HMAC_KEY, kwsg_10418_binary48, load_kwsg_10418_tables
H5_SIG3_LENGTH = 34
H5_SIG3_HEX_LENGTH = H5_SIG3_LENGTH * 2
H5_SIG3_MAGIC = bytes.fromhex("54450130")
H5_SIG3_BLOCK_TAG = bytes.fromhex("9f01")
H5_SIG3_DEFAULT_SESSION_SEED = 0x4B4B4BD1
H5_SIG3_DEFAULT_TAIL = bytes.fromhex("0100000000")
H5_ENCODE_SHA_IV1 = (
0xE066DF2C,
0xA9888E01,
0xB18EE43D,
0x501AFA25,
0x53A550E5,
0xA4BCE311,
0x70E554F5,
0x6AA046EE,
)
H5_ENCODE_SHA_IV2 = (
0x3C66483A,
0x0096038E,
0xD62FEC37,
0x346D7B3D,
0xDE7FB319,
0xF56D5DD5,
0xE614E937,
0x6DD5E338,
)
H5_ENCODE_SHA_K = (
0x428A2F98,
0x71374491,
0xB5C0FBCF,
0xE9B5DBA5,
0x3956C25B,
0x59F111F1,
0x923F82A4,
0xAB1C5ED5,
0xD807AA98,
0x12835B01,
0x243185BE,
0x550C7DC3,
0x72BE5D74,
0x80DEB1FE,
0x9BDC06A7,
0xC19BF174,
0xE49B69C1,
0xEFBE4786,
0x0FC19DC6,
0x240CA1CC,
0x2DE92C6F,
0x4A7484AA,
0x5CB0A9DC,
0x76F988DA,
0x983E5152,
0xA831C66D,
0xB00327C8,
0xBF597FC7,
0xC6E00BF3,
0xD5A79147,
0x06CA6351,
0x14292967,
0x27B70A85,
0x2E1B2138,
0x4D2C6DFC,
0x53380D13,
0x650A7354,
0x766A0ABB,
0x81C2C92E,
0x92722C85,
0xA2BFE8A1,
0xA81A664B,
0xC24B8B70,
0xC76C51A3,
0xD192E819,
0xD6990624,
0xF40E3585,
0x106AA070,
0x19A4C116,
0x1E376C08,
0x2748774C,
0x34B0BCB5,
0x391C0CB3,
0x4ED8AA4A,
0x5B9CCA4F,
0x682E6FF3,
0x748F82EE,
0x78A5636F,
0x84C87814,
0x8CC70208,
0x90BEFFFA,
0xA4506CEB,
0xBEF9A3F7,
0xC67178F2,
)
@dataclass(frozen=True)
class H5Sig3Fields:
"""扰动前的 H5 sig3 字段视图。"""
raw_hex: str
preimage: bytes
mask: int
mask_ok: bool
magic_ok: bool
first_tag_ok: bool
second_tag_ok: bool
tail_ok: bool
session_seed: int
counter: int
crc32: int
elapsed_ms: int
state_value: int
tail: bytes
@property
def ok(self) -> bool:
return (
self.mask_ok
and self.magic_ok
and self.first_tag_ok
and self.second_tag_ok
and self.tail_ok
)
def _coerce_digest(value: str | bytes | bytearray) -> bytes:
if isinstance(value, str):
if len(value) != H5_SIG3_HEX_LENGTH:
raise ValueError("H5 sig3 must be exactly 68 hex chars")
return bytes.fromhex(value)
digest = bytes(value)
if len(digest) != H5_SIG3_LENGTH:
raise ValueError("H5 sig3 must be exactly 34 bytes")
return digest
def h5_sig3_expected_mask(preimage33: bytes | bytearray) -> int:
"""按已观测到的 H5 envelope 规则计算最后 1 字节 mask。"""
if len(preimage33) != H5_SIG3_LENGTH - 1:
raise ValueError("H5 sig3 preimage prefix must be exactly 33 bytes")
return (-sum(preimage33)) & 0xFF
def h5_sig3_unmix(value: str | bytes | bytearray) -> tuple[bytes, int, bool]:
"""恢复扰动前 34 字节,其中最后 1 字节固定置 0。"""
digest = _coerce_digest(value)
mask = digest[-1]
preimage = bytearray(H5_SIG3_LENGTH)
for i in range(H5_SIG3_LENGTH - 1):
preimage[i] = digest[i] ^ ((mask ^ i) & 0xFF)
expected = h5_sig3_expected_mask(preimage[: H5_SIG3_LENGTH - 1])
return bytes(preimage), mask, mask == expected
def h5_sig3_mix(preimage: bytes | bytearray, mask: int | None = None) -> bytes:
"""由扰动前字段重建 34-byte H5 sig3 digest。"""
pre = bytearray(preimage)
if len(pre) == H5_SIG3_LENGTH - 1:
pre.append(0)
if len(pre) != H5_SIG3_LENGTH:
raise ValueError("H5 sig3 preimage must be exactly 33 or 34 bytes")
if mask is None:
mask = h5_sig3_expected_mask(pre[: H5_SIG3_LENGTH - 1])
mask &= 0xFF
out = bytearray(H5_SIG3_LENGTH)
for i in range(H5_SIG3_LENGTH - 1):
out[i] = pre[i] ^ ((mask ^ i) & 0xFF)
out[-1] = mask
return bytes(out)
def parse_h5_sig3(value: str | bytes | bytearray) -> H5Sig3Fields:
"""解析 H5 68hex `__NS_sig3` 的 envelope 字段。"""
digest = _coerce_digest(value)
pre, mask, mask_ok = h5_sig3_unmix(digest)
return H5Sig3Fields(
raw_hex=digest.hex(),
preimage=pre,
mask=mask,
mask_ok=mask_ok,
magic_ok=pre[:4] == H5_SIG3_MAGIC,
first_tag_ok=pre[8:10] == H5_SIG3_BLOCK_TAG,
second_tag_ok=pre[22:24] == H5_SIG3_BLOCK_TAG,
tail_ok=pre[28:33] == H5_SIG3_DEFAULT_TAIL,
session_seed=int.from_bytes(pre[4:8], "little"),
counter=int.from_bytes(pre[10:14], "little"),
crc32=int.from_bytes(pre[14:18], "little"),
elapsed_ms=int.from_bytes(pre[18:22], "little"),
state_value=int.from_bytes(pre[24:28], "little"),
tail=pre[28:33],
)
def build_h5_sig3_preimage(
*,
crc32_value: int,
counter: int,
elapsed_ms: int,
state_value: int,
session_seed: int = H5_SIG3_DEFAULT_SESSION_SEED,
tail: bytes = H5_SIG3_DEFAULT_TAIL,
) -> bytes:
"""构造 H5 sig3 扰动前字段,便于回放样本和后续接入真实 CRC。"""
if len(tail) != 5:
raise ValueError("H5 sig3 tail must be exactly 5 bytes")
pre = bytearray(H5_SIG3_LENGTH)
pre[:4] = H5_SIG3_MAGIC
pre[4:8] = (int(session_seed) & 0xFFFFFFFF).to_bytes(4, "little")
pre[8:10] = H5_SIG3_BLOCK_TAG
pre[10:14] = (int(counter) & 0xFFFFFFFF).to_bytes(4, "little")
pre[14:18] = (int(crc32_value) & 0xFFFFFFFF).to_bytes(4, "little")
pre[18:22] = (int(elapsed_ms) & 0xFFFFFFFF).to_bytes(4, "little")
pre[22:24] = H5_SIG3_BLOCK_TAG
pre[24:28] = (int(state_value) & 0xFFFFFFFF).to_bytes(4, "little")
pre[28:33] = tail
return bytes(pre)
def h5_sig3_from_fields(
*,
crc32_value: int,
counter: int,
elapsed_ms: int,
state_value: int,
session_seed: int = H5_SIG3_DEFAULT_SESSION_SEED,
tail: bytes = H5_SIG3_DEFAULT_TAIL,
) -> str:
"""按字段直接生成 H5 68hex sig3。"""
return h5_sig3_mix(
build_h5_sig3_preimage(
crc32_value=crc32_value,
counter=counter,
elapsed_ms=elapsed_ms,
state_value=state_value,
session_seed=session_seed,
tail=tail,
)
).hex()
def _rotr32(value: int, bits: int) -> int:
value &= 0xFFFFFFFF
return ((value >> bits) | (value << (32 - bits))) & 0xFFFFFFFF
def _sha256_compress_block(state: tuple[int, ...], block64: bytes) -> tuple[int, ...]:
if len(block64) != 64:
raise ValueError("SHA-256 compression block must be exactly 64 bytes")
w = [int.from_bytes(block64[i : i + 4], "big") for i in range(0, 64, 4)]
for i in range(16, 64):
s0 = _rotr32(w[i - 15], 7) ^ _rotr32(w[i - 15], 18) ^ (w[i - 15] >> 3)
s1 = _rotr32(w[i - 2], 17) ^ _rotr32(w[i - 2], 19) ^ (w[i - 2] >> 10)
w.append((w[i - 16] + s0 + w[i - 7] + s1) & 0xFFFFFFFF)
a, b, c, d, e, f, g, h = (item & 0xFFFFFFFF for item in state)
for i in range(64):
s1 = _rotr32(e, 6) ^ _rotr32(e, 11) ^ _rotr32(e, 25)
ch = (e & f) ^ ((~e) & g)
temp1 = (h + s1 + ch + H5_ENCODE_SHA_K[i] + w[i]) & 0xFFFFFFFF
s0 = _rotr32(a, 2) ^ _rotr32(a, 13) ^ _rotr32(a, 22)
maj = (a & b) ^ (a & c) ^ (b & c)
temp2 = (s0 + maj) & 0xFFFFFFFF
h = g
g = f
f = e
e = (d + temp1) & 0xFFFFFFFF
d = c
c = b
b = a
a = (temp1 + temp2) & 0xFFFFFFFF
return (
(state[0] + a) & 0xFFFFFFFF,
(state[1] + b) & 0xFFFFFFFF,
(state[2] + c) & 0xFFFFFFFF,
(state[3] + d) & 0xFFFFFFFF,
(state[4] + e) & 0xFFFFFFFF,
(state[5] + f) & 0xFFFFFFFF,
(state[6] + g) & 0xFFFFFFFF,
(state[7] + h) & 0xFFFFFFFF,
)
def _sha256_continue_from_virtual_prefix(
data: bytes,
state: tuple[int, ...],
prefix_len: int = 64,
) -> bytes:
"""从已压缩过 `prefix_len` 字节的自定义 SHA-256 state 继续计算。"""
if len(state) != 8:
raise ValueError("SHA-256 state must contain exactly 8 words")
total_len = prefix_len + len(data)
padded = bytearray(data)
padded.append(0x80)
while (prefix_len + len(padded)) % 64 != 56:
padded.append(0)
padded += (total_len * 8).to_bytes(8, "big")
current = tuple(word & 0xFFFFFFFF for word in state)
for offset in range(0, len(padded), 64):
current = _sha256_compress_block(current, bytes(padded[offset : offset + 64]))
return b"".join(word.to_bytes(4, "big") for word in current)
def h5_encode_sha_digest(sign_input: str | bytes | bytearray) -> bytes:
"""复现 H5 `$encode` 中用于 envelope CRC 字段的双层 SHA digest。"""
if isinstance(sign_input, str):
data = sign_input.encode("utf-8")
else:
data = bytes(sign_input)
first = _sha256_continue_from_virtual_prefix(data, H5_ENCODE_SHA_IV1, 64)
return _sha256_continue_from_virtual_prefix(first, H5_ENCODE_SHA_IV2, 64)
def h5_sig3_crc32_from_sign_input(sign_input: str | bytes | bytearray) -> int:
"""返回 H5 envelope 14..17 字节对应的 little-endian 字段值。
名字保留 `crc32` 是为了兼容既有字段命名;实际已确认不是标准 CRC32
而是 `$encode` 自定义双层 SHA digest 的首 32-bit word按 envelope
小端字段视图解释)。
"""
digest = h5_encode_sha_digest(sign_input)
return int.from_bytes(digest[:4], "little")
def h5_sig3_crc32_from_10418_input(
input_value: str | bytes | bytearray,
t1: bytes | None = None,
t2: bytes | None = None,
hmac_key: bytes = KWSG_10418_HMAC_KEY,
) -> int:
"""候选 CRC 生成器CRC32(10418 binary48(input))。
这个 helper 用于离线枚举 H5 `secPlain` 形态;当前只表示一个已知
强候选路径,不代表 H5 bridge 的输入已经完全闭合。
"""
if isinstance(input_value, str):
input_bytes = input_value.encode("utf-8")
else:
input_bytes = bytes(input_value)
if t1 is None and t2 is None:
t1, t2 = load_kwsg_10418_tables()
elif t1 is None or t2 is None:
raise ValueError("t1 and t2 must be provided together")
return zlib.crc32(kwsg_10418_binary48(input_bytes, t1, t2, hmac_key)) & 0xFFFFFFFF
__all__ = [
"H5_SIG3_BLOCK_TAG",
"H5_SIG3_DEFAULT_SESSION_SEED",
"H5_SIG3_DEFAULT_TAIL",
"H5_SIG3_HEX_LENGTH",
"H5_SIG3_LENGTH",
"H5_SIG3_MAGIC",
"H5Sig3Fields",
"build_h5_sig3_preimage",
"h5_sig3_crc32_from_10418_input",
"h5_sig3_crc32_from_sign_input",
"h5_sig3_expected_mask",
"h5_encode_sha_digest",
"h5_sig3_from_fields",
"h5_sig3_mix",
"h5_sig3_unmix",
"parse_h5_sig3",
]