diff --git a/hash.py b/hash.py new file mode 100644 index 0000000..b2e84bf --- /dev/null +++ b/hash.py @@ -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())