Add hash.py

This commit is contained in:
NJL
2026-07-11 14:53:24 +00:00
parent 4b178ab2e3
commit f86e1e8f61
+241
View File
@@ -0,0 +1,241 @@
MASK64 = (1 << 64) - 1
LANES = 32
STRIDE = 128
ROWBITS = LANES * STRIDE
ROWFULL = (1 << ROWBITS) - 1
LANEMASK = sum(MASK64 << (i * STRIDE) for i in range(LANES))
A_ROUNDS = 12
A_BLOCK = 768
B_N = 12288
B_FULL = (1 << B_N) - 1
B_ROUNDS = 12
B_BLOCK = 1024
def _constants(count):
out = []
x = 0x243F6A8885A308D3
for _ in range(count):
x = (x + 0x9E3779B97F4A7C15) & MASK64
z = x
z = ((z ^ (z >> 30)) * 0xBF58476D1CE4E5B9) & MASK64
z = ((z ^ (z >> 27)) * 0x94D049BB133111EB) & MASK64
out.append(z ^ (z >> 31))
return out
def _pack_lanes(words):
v = 0
for i, w in enumerate(words):
v |= w << (i * STRIDE)
return v
def _pack_dense(words):
v = 0
for i, w in enumerate(words):
v |= w << (i * 64)
return v
_TABLE = _constants(A_ROUNDS * 4 * LANES + 4 * LANES + B_ROUNDS * 192 + 192 + 2 * B_ROUNDS)
_pos = 0
_A_RC = []
for _ in range(A_ROUNDS * 4):
_A_RC.append(_pack_lanes(_TABLE[_pos : _pos + LANES]))
_pos += LANES
_A_IV = []
for _ in range(4):
_A_IV.append(_pack_lanes(_TABLE[_pos : _pos + LANES]))
_pos += LANES
_B_RC = []
for _ in range(B_ROUNDS):
_B_RC.append(_pack_dense(_TABLE[_pos : _pos + 192]))
_pos += 192
_B_IV = _pack_dense(_TABLE[_pos : _pos + 192])
_pos += 192
_B_SHIFT = [(w % (B_N // 2 - 64)) + 32 for w in _TABLE[_pos : _pos + B_ROUNDS]]
_pos += B_ROUNDS
_B_ROT = [(w % (B_N - 128)) + 64 for w in _TABLE[_pos : _pos + B_ROUNDS]]
_ZERO8 = b"\x00" * 8
def _permute_a(rows):
r0, r1, r2, r3 = rows
LM = LANEMASK
RF = ROWFULL
for rnd in range(A_ROUNDS):
base = rnd * 4
r0 ^= _A_RC[base]
r1 ^= _A_RC[base + 1]
r2 ^= _A_RC[base + 2]
r3 ^= _A_RC[base + 3]
r0 = (r0 + r1) & LM
r3 ^= r0
r3 = ((r3 << 32) | (r3 >> 32)) & LM
r2 = (r2 + r3) & LM
r1 ^= r2
r1 = ((r1 << 24) | (r1 >> 40)) & LM
r0 = (r0 + r1) & LM
r3 ^= r0
r3 = ((r3 << 16) | (r3 >> 48)) & LM
r2 = (r2 + r3) & LM
r1 ^= r2
r1 = ((r1 << 63) | (r1 >> 1)) & LM
r1 = ((r1 << 128) | (r1 >> (ROWBITS - 128))) & RF
r2 = ((r2 << 640) | (r2 >> (ROWBITS - 640))) & RF
r3 = ((r3 << 1664) | (r3 >> (ROWBITS - 1664))) & RF
r0 = (r0 + r1) & LM
r3 ^= r0
r3 = ((r3 << 32) | (r3 >> 32)) & LM
r2 = (r2 + r3) & LM
r1 ^= r2
r1 = ((r1 << 24) | (r1 >> 40)) & LM
r0 = (r0 + r1) & LM
r3 ^= r0
r3 = ((r3 << 16) | (r3 >> 48)) & LM
r2 = (r2 + r3) & LM
r1 ^= r2
r1 = ((r1 << 63) | (r1 >> 1)) & LM
k = ((r2 >> (((rnd * 3 + 1) & 31) * STRIDE + 58)) & 63) | 1
r0 = ((r0 << k) | (r0 >> (64 - k))) & LM
k = ((r3 >> (((rnd * 3 + 2) & 31) * STRIDE + 58)) & 63) | 1
r1 = ((r1 << k) | (r1 >> (64 - k))) & LM
k = ((r0 >> (((rnd * 5 + 1) & 31) * STRIDE + 58)) & 63) | 1
r2 = ((r2 << k) | (r2 >> (64 - k))) & LM
k = ((r1 >> (((rnd * 5 + 2) & 31) * STRIDE + 58)) & 63) | 1
r3 = ((r3 << k) | (r3 >> (64 - k))) & LM
r0 = (r0 * 0xD1B54A32D192ED03) & LM
r0 ^= (r0 >> 29) & LM
r1 = (r1 * 0xD1B54A32D192ED03) & LM
r1 ^= (r1 >> 29) & LM
r2 = (r2 * 0xD1B54A32D192ED03) & LM
r2 ^= (r2 >> 29) & LM
r3 = (r3 * 0xD1B54A32D192ED03) & LM
r3 ^= (r3 >> 29) & LM
rows[0] = r0
rows[1] = r1
rows[2] = r2
rows[3] = r3
def _permute_b(x):
for rnd in range(B_ROUNDS):
x = (x + _B_RC[rnd]) & B_FULL
x = (x + 2 * x * x) & B_FULL
x ^= x >> _B_SHIFT[rnd]
r = _B_ROT[rnd]
x = ((x << r) | (x >> (B_N - r))) & B_FULL
return x
def _absorb_a(rows, block):
spread = b"".join(block[i : i + 8] + _ZERO8 for i in range(0, A_BLOCK, 8))
m = int.from_bytes(spread, "little")
rows[0] ^= m & ROWFULL
rows[1] ^= (m >> ROWBITS) & ROWFULL
rows[2] ^= (m >> (2 * ROWBITS)) & ROWFULL
_permute_a(rows)
def _squeeze_a(rows):
out = bytearray()
for lane in range(8):
w = (rows[0] >> (lane * STRIDE)) & MASK64
out += w.to_bytes(8, "little")
return bytes(out)
def _pad_tail(tail, total_bits, block):
padded = bytearray(tail)
padded.append(0x80)
while (len(padded) + 16) % block != 0:
padded.append(0)
padded += total_bits.to_bytes(16, "little")
return bytes(padded)
class NJLHash:
def __init__(self, data=b""):
self._rows = list(_A_IV)
self._bx = _B_IV
self._tail_a = bytearray()
self._tail_b = bytearray()
self._bits = 0
if data:
self.update(data)
def update(self, data):
if isinstance(data, str):
data = data.encode("utf-8")
self._bits += len(data) * 8
ta = self._tail_a
ta += data
while len(ta) >= A_BLOCK:
_absorb_a(self._rows, bytes(ta[:A_BLOCK]))
del ta[:A_BLOCK]
tb = self._tail_b
tb += data
while len(tb) >= B_BLOCK:
self._bx = _permute_b(self._bx ^ int.from_bytes(tb[:B_BLOCK], "little"))
del tb[:B_BLOCK]
return self
def digest(self):
rows = list(self._rows)
padded = _pad_tail(self._tail_a, self._bits, A_BLOCK)
for offset in range(0, len(padded), A_BLOCK):
_absorb_a(rows, padded[offset : offset + A_BLOCK])
part_a = _squeeze_a(rows)
x = self._bx
padded = _pad_tail(self._tail_b, self._bits, B_BLOCK)
for offset in range(0, len(padded), B_BLOCK):
x = _permute_b(x ^ int.from_bytes(padded[offset : offset + B_BLOCK], "little"))
part_b = (x & ((1 << 512) - 1)).to_bytes(64, "little")
return part_a + part_b
def hexdigest(self):
return self.digest().hex()
def _track_a(data):
rows = list(_A_IV)
padded = _pad_tail(data, len(data) * 8, A_BLOCK)
for offset in range(0, len(padded), A_BLOCK):
_absorb_a(rows, padded[offset : offset + A_BLOCK])
return _squeeze_a(rows)
def _track_b(data):
x = _B_IV
padded = _pad_tail(data, len(data) * 8, B_BLOCK)
for offset in range(0, len(padded), B_BLOCK):
x = _permute_b(x ^ int.from_bytes(padded[offset : offset + B_BLOCK], "little"))
return (x & ((1 << 512) - 1)).to_bytes(64, "little")
def njlhash(data):
if isinstance(data, str):
data = data.encode("utf-8")
return _track_a(data) + _track_b(data)
def njlhash_hex(data):
return njlhash(data).hex()
if __name__ == "__main__":
live = NJLHash()
for ch in "EXAMPLE":
live.update(ch)
print("+" + ch + " live: " + live.hexdigest()[:48] + "...")
print("EXAMPLE -> " + live.hexdigest())