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

194 lines
6.1 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Replay the VM's BLAKE2s-style compression core for xfalcon."""
from __future__ import annotations
import glob
import re
import struct
import sys
from pathlib import Path
MASK32 = 0xFFFFFFFF
SIGMA = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
[14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3],
[11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4],
[7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8],
[9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13],
[2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9],
[12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11],
[13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10],
[6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5],
[10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0],
]
SAMPLE_V0 = [
0xA82057D6, 0xA9A24FF4, 0x9138D3FD, 0x2A2193F3,
0x2ADEF3F4, 0x9876EF16, 0x9ABED34F, 0x9103DE12,
0xA92157F6, 0xA9A24FF4, 0x9138D3FD, 0x2A2193F3,
0x2ADEF3D5, 0x9876EF16, 0x65412CB0, 0x9103DE12,
]
SAMPLE_M = [
0x0C5B0466, 0x04580152, 0x51500555, 0x00565451,
0x36753729, 0x5B72176D, 0x070A1F60, 0x7C662341,
0x682F4A37, 0x26562F2D, 0x47573177, 0x540A115A,
0x715D5A79, 0x061C581D, 0x6D0E0D1C, 0x0F230801,
]
SAMPLE_V_FINAL = [
0x296C7D05, 0xA2E09447, 0x62AB2823, 0xB3FD4E92,
0xF96DE833, 0xA5517BB8, 0xBEBB2C06, 0xFAD21B26,
0x2929349D, 0xF418B33C, 0x7325D323, 0xB446F83B,
0x04D647B0, 0x11FB17F9, 0x0CD935BC, 0x858F3D9F,
]
SAMPLE_DIGEST = [
0xA8651E4E, 0xFF5A688F, 0x80B628FD, 0x2D9A255A,
0xD7655C77, 0x2CDC8357, 0x28DCCAF5, 0xEE5EF8AB,
]
def ror32(value: int, count: int) -> int:
value &= MASK32
count &= 31
return ((value >> count) | ((value << (32 - count)) & MASK32)) & MASK32
def compress(v0: list[int], m: list[int]) -> list[int]:
v = [x & MASK32 for x in v0]
def g(a: int, b: int, c: int, d: int, x: int, y: int) -> None:
v[a] = (v[a] + v[b] + x) & MASK32
v[d] = ror32(v[d] ^ v[a], 16)
v[c] = (v[c] + v[d]) & MASK32
v[b] = ror32(v[b] ^ v[c], 12)
v[a] = (v[a] + v[b] + y) & MASK32
v[d] = ror32(v[d] ^ v[a], 8)
v[c] = (v[c] + v[d]) & MASK32
v[b] = ror32(v[b] ^ v[c], 7)
for s in SIGMA:
g(0, 4, 8, 12, m[s[0]], m[s[1]])
g(1, 5, 9, 13, m[s[2]], m[s[3]])
g(2, 6, 10, 14, m[s[4]], m[s[5]])
g(3, 7, 11, 15, m[s[6]], m[s[7]])
g(0, 5, 10, 15, m[s[8]], m[s[9]])
g(1, 6, 11, 12, m[s[10]], m[s[11]])
g(2, 7, 8, 13, m[s[12]], m[s[13]])
g(3, 4, 9, 14, m[s[14]], m[s[15]])
return v
def finalize_digest(v0: list[int], v_final: list[int]) -> list[int]:
return [(v0[i] ^ v_final[i] ^ v_final[i + 8]) & MASK32 for i in range(8)]
def words_hex(words: list[int]) -> str:
return " ".join(f"{x & MASK32:08x}" for x in words)
def digest_hex(words: list[int]) -> str:
return "".join((x & MASK32).to_bytes(4, "big").hex() for x in words)
def newest_log() -> Path:
logs = sorted(glob.glob("out/xfalcon_blake_params_*.log"))
if not logs:
raise SystemExit("no xfalcon_blake_params log found")
return Path(logs[-1])
def collect_dump(text: str, name: str, base_rel: int) -> bytearray:
buf = bytearray()
pat = rf"\[BLK\]\[dump\.{re.escape(name)}\] rel=0x([0-9a-f]+).*? hex=([0-9a-f]+)"
for m in re.finditer(pat, text):
rel = int(m.group(1), 16)
off = rel - base_rel
data = bytes.fromhex(m.group(2))
if off < 0:
continue
if len(buf) < off + len(data):
buf.extend(b"\x00" * (off + len(data) - len(buf)))
buf[off : off + len(data)] = data
return buf
def qwords_low32(buf: bytes, off: int, count: int) -> list[int]:
return [struct.unpack_from("<Q", buf, off + i * 8)[0] & MASK32 for i in range(count)]
def parse_log(path: Path) -> tuple[list[int], list[int], list[int], list[int]]:
text = path.read_text(encoding="utf-8", errors="replace")
work = collect_dump(text, "work_2e00_at_compress", 0x2E00)
if len(work) < 0x228:
raise SystemExit("log missing work_2e00_at_compress dump")
v0 = qwords_low32(work, 0x2E28 - 0x2E00, 16)
m_words = qwords_low32(work, 0x2FA8 - 0x2E00, 16)
table: dict[int, int] = {}
for sm in re.finditer(r"\[BLK\]\[store\].*?rel=0x([0-9a-f]+).*?val=0x([0-9a-f]+)", text):
rel = int(sm.group(1), 16)
if 0x2E28 <= rel < 0x2EA8 and (rel - 0x2E28) % 8 == 0:
table[(rel - 0x2E28) // 8] = int(sm.group(2), 16) & MASK32
if len(table) < 16:
raise SystemExit(f"log missing final table stores: got {len(table)}")
v_final = [table[i] for i in range(16)]
digest: dict[int, int] = {}
for sm in re.finditer(r"\[BLK\]\[store\].*?rel=0x([0-9a-f]+).*?val=0x([0-9a-f]+)", text):
rel = int(sm.group(1), 16)
if 0x103A68 <= rel < 0x103AA8 and (rel - 0x103A68) % 8 == 0:
digest[(rel - 0x103A68) // 8] = int(sm.group(2), 16) & MASK32
digest_words = [digest[i] for i in range(8)] if len(digest) >= 8 else []
return v0, m_words, v_final, digest_words
def main() -> int:
if len(sys.argv) > 1:
path = Path(sys.argv[1])
else:
path = newest_log()
if path.exists():
v0, m_words, expected_v, expected_digest = parse_log(path)
print(f"log={path}")
else:
v0, m_words, expected_v, expected_digest = (
SAMPLE_V0,
SAMPLE_M,
SAMPLE_V_FINAL,
SAMPLE_DIGEST,
)
print("using built-in sample")
got_v = compress(v0, m_words)
got_digest = finalize_digest(v0, got_v)
print("v0: " + words_hex(v0))
print("m: " + words_hex(m_words))
print("v_calc: " + words_hex(got_v))
print("v_expect:" + words_hex(expected_v))
print("digest: " + words_hex(got_digest))
print("hex: " + digest_hex(got_digest))
ok_v = got_v == expected_v
ok_d = not expected_digest or got_digest == expected_digest
if ok_v and ok_d:
print("[OK] xfalcon blake core replay verified")
return 0
print(f"[FAIL] v_ok={ok_v} digest_ok={ok_d}")
return 1
if __name__ == "__main__":
raise SystemExit(main())