| 1 | """Pure-Python BLAKE3 (default 32-byte hash mode only), stdlib-only (ADR-0001).
|
| 2 |
|
| 3 | Independently written from the BLAKE3 specification (Aumasson, Neves,
|
| 4 | O'Connor, Wilcox-O'Hearn, 2020), section 2 (compression function, chunk
|
| 5 | chaining and binary tree). It is deliberately a separate implementation from
|
| 6 | the Rust ``blake3`` crate Oak links, so a hash verified here is not a hash the
|
| 7 | subject checked with its own code. Throughput is roughly 1-3 MB/s; callers
|
| 8 | bound the input size.
|
| 9 | """
|
| 10 | from __future__ import annotations
|
| 11 |
|
| 12 | import struct
|
| 13 |
|
| 14 | IV = (0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A,
|
| 15 | 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19)
|
| 16 | CHUNK_START, CHUNK_END, PARENT, ROOT = 1, 2, 4, 8
|
| 17 | CHUNK_LEN = 1024
|
| 18 | BLOCK_LEN = 64
|
| 19 | _M = 0xFFFFFFFF
|
| 20 | # Message word schedule per round (the spec's permutation applied r times).
|
| 21 | _PERM = (2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8)
|
| 22 |
|
| 23 |
|
| 24 | def _schedules():
|
| 25 | order = list(range(16))
|
| 26 | result = []
|
| 27 | for _ in range(7):
|
| 28 | result.append(tuple(order))
|
| 29 | order = [order[p] for p in _PERM]
|
| 30 | return tuple(result)
|
| 31 |
|
| 32 |
|
| 33 | _SCHEDULE = _schedules()
|
| 34 | _WORDS = struct.Struct('<16I')
|
| 35 |
|
| 36 |
|
| 37 | def _compress(cv, words, counter, block_len, flags):
|
| 38 | """Return the full 16-word output state of the compression function."""
|
| 39 | s0, s1, s2, s3, s4, s5, s6, s7 = cv
|
| 40 | s8, s9, s10, s11 = IV[0], IV[1], IV[2], IV[3]
|
| 41 | s12, s13, s14, s15 = counter & _M, (counter >> 32) & _M, block_len, flags
|
| 42 | for schedule in _SCHEDULE:
|
| 43 | m = [words[i] for i in schedule]
|
| 44 | # Column step.
|
| 45 | s0 = (s0 + s4 + m[0]) & _M; s12 ^= s0; s12 = ((s12 >> 16) | (s12 << 16)) & _M
|
| 46 | s8 = (s8 + s12) & _M; s4 ^= s8; s4 = ((s4 >> 12) | (s4 << 20)) & _M
|
| 47 | s0 = (s0 + s4 + m[1]) & _M; s12 ^= s0; s12 = ((s12 >> 8) | (s12 << 24)) & _M
|
| 48 | s8 = (s8 + s12) & _M; s4 ^= s8; s4 = ((s4 >> 7) | (s4 << 25)) & _M
|
| 49 | s1 = (s1 + s5 + m[2]) & _M; s13 ^= s1; s13 = ((s13 >> 16) | (s13 << 16)) & _M
|
| 50 | s9 = (s9 + s13) & _M; s5 ^= s9; s5 = ((s5 >> 12) | (s5 << 20)) & _M
|
| 51 | s1 = (s1 + s5 + m[3]) & _M; s13 ^= s1; s13 = ((s13 >> 8) | (s13 << 24)) & _M
|
| 52 | s9 = (s9 + s13) & _M; s5 ^= s9; s5 = ((s5 >> 7) | (s5 << 25)) & _M
|
| 53 | s2 = (s2 + s6 + m[4]) & _M; s14 ^= s2; s14 = ((s14 >> 16) | (s14 << 16)) & _M
|
| 54 | s10 = (s10 + s14) & _M; s6 ^= s10; s6 = ((s6 >> 12) | (s6 << 20)) & _M
|
| 55 | s2 = (s2 + s6 + m[5]) & _M; s14 ^= s2; s14 = ((s14 >> 8) | (s14 << 24)) & _M
|
| 56 | s10 = (s10 + s14) & _M; s6 ^= s10; s6 = ((s6 >> 7) | (s6 << 25)) & _M
|
| 57 | s3 = (s3 + s7 + m[6]) & _M; s15 ^= s3; s15 = ((s15 >> 16) | (s15 << 16)) & _M
|
| 58 | s11 = (s11 + s15) & _M; s7 ^= s11; s7 = ((s7 >> 12) | (s7 << 20)) & _M
|
| 59 | s3 = (s3 + s7 + m[7]) & _M; s15 ^= s3; s15 = ((s15 >> 8) | (s15 << 24)) & _M
|
| 60 | s11 = (s11 + s15) & _M; s7 ^= s11; s7 = ((s7 >> 7) | (s7 << 25)) & _M
|
| 61 | # Diagonal step.
|
| 62 | s0 = (s0 + s5 + m[8]) & _M; s15 ^= s0; s15 = ((s15 >> 16) | (s15 << 16)) & _M
|
| 63 | s10 = (s10 + s15) & _M; s5 ^= s10; s5 = ((s5 >> 12) | (s5 << 20)) & _M
|
| 64 | s0 = (s0 + s5 + m[9]) & _M; s15 ^= s0; s15 = ((s15 >> 8) | (s15 << 24)) & _M
|
| 65 | s10 = (s10 + s15) & _M; s5 ^= s10; s5 = ((s5 >> 7) | (s5 << 25)) & _M
|
| 66 | s1 = (s1 + s6 + m[10]) & _M; s12 ^= s1; s12 = ((s12 >> 16) | (s12 << 16)) & _M
|
| 67 | s11 = (s11 + s12) & _M; s6 ^= s11; s6 = ((s6 >> 12) | (s6 << 20)) & _M
|
| 68 | s1 = (s1 + s6 + m[11]) & _M; s12 ^= s1; s12 = ((s12 >> 8) | (s12 << 24)) & _M
|
| 69 | s11 = (s11 + s12) & _M; s6 ^= s11; s6 = ((s6 >> 7) | (s6 << 25)) & _M
|
| 70 | s2 = (s2 + s7 + m[12]) & _M; s13 ^= s2; s13 = ((s13 >> 16) | (s13 << 16)) & _M
|
| 71 | s8 = (s8 + s13) & _M; s7 ^= s8; s7 = ((s7 >> 12) | (s7 << 20)) & _M
|
| 72 | s2 = (s2 + s7 + m[13]) & _M; s13 ^= s2; s13 = ((s13 >> 8) | (s13 << 24)) & _M
|
| 73 | s8 = (s8 + s13) & _M; s7 ^= s8; s7 = ((s7 >> 7) | (s7 << 25)) & _M
|
| 74 | s3 = (s3 + s4 + m[14]) & _M; s14 ^= s3; s14 = ((s14 >> 16) | (s14 << 16)) & _M
|
| 75 | s9 = (s9 + s14) & _M; s4 ^= s9; s4 = ((s4 >> 12) | (s4 << 20)) & _M
|
| 76 | s3 = (s3 + s4 + m[15]) & _M; s14 ^= s3; s14 = ((s14 >> 8) | (s14 << 24)) & _M
|
| 77 | s9 = (s9 + s14) & _M; s4 ^= s9; s4 = ((s4 >> 7) | (s4 << 25)) & _M
|
| 78 | return (s0 ^ s8, s1 ^ s9, s2 ^ s10, s3 ^ s11, s4 ^ s12, s5 ^ s13, s6 ^ s14, s7 ^ s15,
|
| 79 | s8 ^ cv[0], s9 ^ cv[1], s10 ^ cv[2], s11 ^ cv[3],
|
| 80 | s12 ^ cv[4], s13 ^ cv[5], s14 ^ cv[6], s15 ^ cv[7])
|
| 81 |
|
| 82 |
|
| 83 | def _block_words(block: bytes):
|
| 84 | if len(block) < BLOCK_LEN:
|
| 85 | block = block + bytes(BLOCK_LEN - len(block))
|
| 86 | return _WORDS.unpack(block)
|
| 87 |
|
| 88 |
|
| 89 | def _chunk_output(chunk: bytes, counter: int):
|
| 90 | """(cv, words, counter, block_len, flags) of a chunk's final block, not yet finalized."""
|
| 91 | cv = IV
|
| 92 | blocks = max(1, (len(chunk) + BLOCK_LEN - 1) // BLOCK_LEN)
|
| 93 | for index in range(blocks - 1):
|
| 94 | flags = CHUNK_START if index == 0 else 0
|
| 95 | cv = _compress(cv, _block_words(chunk[index * BLOCK_LEN:(index + 1) * BLOCK_LEN]),
|
| 96 | counter, BLOCK_LEN, flags)[:8]
|
| 97 | last = chunk[(blocks - 1) * BLOCK_LEN:]
|
| 98 | flags = CHUNK_END | (CHUNK_START if blocks == 1 else 0)
|
| 99 | return cv, _block_words(last), counter, len(last), flags
|
| 100 |
|
| 101 |
|
| 102 | def _parent_output(left, right):
|
| 103 | return IV, tuple(left) + tuple(right), 0, BLOCK_LEN, PARENT
|
| 104 |
|
| 105 |
|
| 106 | def digest(data: bytes) -> bytes:
|
| 107 | """BLAKE3 hash (32 bytes) of ``data``."""
|
| 108 | data = bytes(data)
|
| 109 | chunks = max(1, (len(data) + CHUNK_LEN - 1) // CHUNK_LEN)
|
| 110 | stack = []
|
| 111 | for index in range(chunks - 1):
|
| 112 | output = _chunk_output(data[index * CHUNK_LEN:(index + 1) * CHUNK_LEN], index)
|
| 113 | cv = _compress(*output)[:8]
|
| 114 | total = index + 1
|
| 115 | while total & 1 == 0:
|
| 116 | cv = _compress(*_parent_output(stack.pop(), cv))[:8]
|
| 117 | total >>= 1
|
| 118 | stack.append(cv)
|
| 119 | output = _chunk_output(data[(chunks - 1) * CHUNK_LEN:], chunks - 1)
|
| 120 | while stack:
|
| 121 | output = _parent_output(stack.pop(), _compress(*output)[:8])
|
| 122 | cv, words, counter, block_len, flags = output
|
| 123 | return struct.pack('<8I', *_compress(cv, words, counter, block_len, flags | ROOT)[:8])
|
| 124 |
|
| 125 |
|
| 126 | def hexdigest(data: bytes) -> str:
|
| 127 | return digest(data).hex()
|