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())