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

217 lines
7.9 KiB
Python

from __future__ import annotations
import argparse
import struct
from pathlib import Path
from .xfalcon_blake_core import compress, digest_hex, words_hex
from .xfalcon_te import (
XFALCON_PREFIX,
xfalcon_te_hex_from_digest_hex,
xfalcon_value_from_digest_hex,
)
MASK32 = 0xFFFFFFFF
INPUT_HEX_LEN = 80
PACKED_LEN = 64
M0_XOR_CONST = 0x3D
RAW_CHUNK_LEN = 256
FOLDED_BLOCK_LEN = 64
PARAM_BLOCK_BYTES = 0x01010020
CUSTOM_IV = [
0xA92157F6, 0xA9A24FF4, 0x9138D3FD, 0x2A2193F3,
0x2ADEF3F4, 0x9876EF16, 0x9ABED34F, 0x9103DE12,
]
def _validate_input_hex(input_hex: str) -> str:
input_hex = input_hex.strip()
if len(input_hex) != INPUT_HEX_LEN:
raise ValueError(f"input_hex must be {INPUT_HEX_LEN} chars")
if any(c not in "0123456789abcdefABCDEF" for c in input_hex):
raise ValueError("input_hex must contain only hex chars")
return input_hex
def _input_to_bytes(value: bytes | bytearray | str) -> bytes:
if isinstance(value, str):
return value.encode("utf-8")
return bytes(value)
def _initial_h() -> list[int]:
h = CUSTOM_IV.copy()
h[0] ^= PARAM_BLOCK_BYTES
return h
def _compress_v0(h: list[int], counter: int, final: bool) -> list[int]:
v0 = h.copy() + CUSTOM_IV.copy()
v0[12] ^= counter & MASK32
if final:
v0[14] ^= MASK32
return v0
def _state_after_compress(h: list[int], block: bytes, counter: int, final: bool) -> list[int]:
if len(block) != FOLDED_BLOCK_LEN:
raise ValueError(f"folded block must be {FOLDED_BLOCK_LEN} bytes")
m_words = [struct.unpack_from("<I", block, i * 4)[0] for i in range(16)]
v_final = compress(_compress_v0(h, counter, final), m_words)
return [(h[i] ^ v_final[i] ^ v_final[i + 8]) & MASK32 for i in range(8)]
def _chunk_logical_len(raw_len: int, offset: int) -> int:
if offset >= raw_len:
return 0
return min(FOLDED_BLOCK_LEN, (raw_len - offset + 3) // 4)
def xfalcon_folded_block_from_raw_bytes(raw: bytes | bytearray, offset: int) -> bytes:
"""Fold one 256-byte VM raw chunk into the 64-byte BLAKE message block.
The VM appends the fixed HUDR prefix to the caller input, then compresses
each 256-byte raw chunk into 64 bytes by XORing four 64-byte lanes.
"""
raw_bytes = bytes(raw)
if offset < 0:
raise ValueError("offset must be non-negative")
block = bytearray(FOLDED_BLOCK_LEN)
for j in range(FOLDED_BLOCK_LEN):
value = 0
for lane in range(4):
pos = offset + lane * FOLDED_BLOCK_LEN + j
if pos < len(raw_bytes):
value ^= raw_bytes[pos]
block[j] = value
return bytes(block)
def xfalcon_folded_block_from_input_bytes(input_bytes: bytes | bytearray | str, offset: int) -> bytes:
raw = _input_to_bytes(input_bytes) + XFALCON_PREFIX.encode("ascii")
return xfalcon_folded_block_from_raw_bytes(raw, offset)
def xfalcon_message_words_from_folded_block(block: bytes | bytearray) -> list[int]:
block_bytes = bytes(block)
if len(block_bytes) != FOLDED_BLOCK_LEN:
raise ValueError(f"folded block must be {FOLDED_BLOCK_LEN} bytes")
return [struct.unpack_from("<I", block_bytes, i * 4)[0] for i in range(16)]
def xfalcon_digest_words_from_raw_bytes(raw: bytes | bytearray) -> list[int]:
raw_bytes = bytes(raw)
if not raw_bytes:
raise ValueError("raw bytes must not be empty")
h = _initial_h()
counter = 0
for offset in range(0, len(raw_bytes), RAW_CHUNK_LEN):
block = xfalcon_folded_block_from_raw_bytes(raw_bytes, offset)
counter += _chunk_logical_len(len(raw_bytes), offset)
final = offset + RAW_CHUNK_LEN >= len(raw_bytes)
h = _state_after_compress(h, block, counter, final)
return h
def xfalcon_digest_words_from_input_bytes(input_bytes: bytes | bytearray | str) -> list[int]:
raw = _input_to_bytes(input_bytes) + XFALCON_PREFIX.encode("ascii")
return xfalcon_digest_words_from_raw_bytes(raw)
def xfalcon_digest_hex_from_input_bytes(input_bytes: bytes | bytearray | str) -> str:
return digest_hex(xfalcon_digest_words_from_input_bytes(input_bytes))
def xfalcon_te_hex_from_input_bytes(input_bytes: bytes | bytearray | str) -> str:
return xfalcon_te_hex_from_digest_hex(xfalcon_digest_hex_from_input_bytes(input_bytes))
def xfalcon_value_from_input_bytes(input_bytes: bytes | bytearray | str) -> str:
return xfalcon_value_from_digest_hex(xfalcon_digest_hex_from_input_bytes(input_bytes))
def xfalcon_packed_block_from_input_hex(input_hex: str) -> bytes:
"""Rebuild the legacy pre-XOR packed block from the 80-char ASCII input."""
input_hex = _validate_input_hex(input_hex)
block = bytearray(xfalcon_folded_block_from_input_bytes(input_hex, 0))
block[0] ^= M0_XOR_CONST
return bytes(block)
def xfalcon_message_words_from_input_hex(input_hex: str) -> list[int]:
"""Rebuild BLAKE message words at x23+0x2fa8."""
input_hex = _validate_input_hex(input_hex)
return xfalcon_message_words_from_folded_block(
xfalcon_folded_block_from_input_bytes(input_hex, 0)
)
def xfalcon_digest_words_from_input_hex(input_hex: str) -> list[int]:
input_hex = _validate_input_hex(input_hex)
return xfalcon_digest_words_from_input_bytes(input_hex)
def xfalcon_digest_hex_from_input_hex(input_hex: str) -> str:
return digest_hex(xfalcon_digest_words_from_input_hex(input_hex))
def xfalcon_te_hex_from_input_hex(input_hex: str) -> str:
return xfalcon_te_hex_from_digest_hex(xfalcon_digest_hex_from_input_hex(input_hex))
def xfalcon_value_from_input_hex(input_hex: str) -> str:
return xfalcon_value_from_digest_hex(xfalcon_digest_hex_from_input_hex(input_hex))
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("input_hex", nargs="?", help="80-char ASCII hex input passed to __NS_xfalcon")
source_group = parser.add_mutually_exclusive_group()
source_group.add_argument("--file", help="read arbitrary xfalcon input bytes from file")
source_group.add_argument("--text", help="use arbitrary UTF-8 xfalcon input text")
group = parser.add_mutually_exclusive_group()
group.add_argument("--digest", action="store_true", help="print digest hex only")
group.add_argument("--te", action="store_true", help="print $TE_ raw hex only")
group.add_argument("--value", action="store_true", help="print full __NS_xfalcon value only")
group.add_argument("--packed", action="store_true", help="print direct legacy packed block hex only")
group.add_argument("--m", action="store_true", help="print first folded BLAKE message words only")
args = parser.parse_args()
if args.file:
input_bytes = Path(args.file).read_bytes()
digest = xfalcon_digest_hex_from_input_bytes(input_bytes)
first_block = xfalcon_folded_block_from_input_bytes(input_bytes, 0)
elif args.text is not None:
digest = xfalcon_digest_hex_from_input_bytes(args.text)
first_block = xfalcon_folded_block_from_input_bytes(args.text, 0)
else:
if not args.input_hex:
parser.error("input_hex is required unless --file or --text is used")
digest = xfalcon_digest_hex_from_input_hex(args.input_hex)
first_block = xfalcon_folded_block_from_input_bytes(args.input_hex, 0)
if args.digest:
print(digest)
elif args.te:
print(xfalcon_te_hex_from_digest_hex(digest))
elif args.value:
print(xfalcon_value_from_digest_hex(digest))
elif args.packed:
if args.file or args.text is not None:
parser.error("--packed is only defined for direct 80-char ASCII hex input")
print(xfalcon_packed_block_from_input_hex(args.input_hex).hex())
elif args.m:
print(words_hex(xfalcon_message_words_from_folded_block(first_block)))
else:
print(f"digest={digest}")
print(f"te={xfalcon_te_hex_from_digest_hex(digest)}")
print(f"value={xfalcon_value_from_digest_hex(digest)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())