sha2
stdlib/extra/sha2.hk: SHA-256, HMAC-SHA-256, and constant-time comparison.
The cryptographic hash the supply-chain surfaces need. core/hash is a different thing entirely: it is structural hashing for Map keys, unkeyed and cheap by design. A value that must resist an adversary choosing it, a package's content pin or a checksum log entry or anything signed, hashes here instead.
Named for the family and not the algorithm: SHA-512 is the same construction over 64-bit words and belongs in this module when it arrives. digest and checksum were the alternatives; digest reads like a trait this module does not define, and checksum names the non-cryptographic class (fnv1a, CRC) that this exists to replace.
Pure Hanki on both tiers, no seam: SHA-256 needs only u32 arithmetic, whose bare operators wrap, which is the mod-2^32 the spec asks for, and the total bitwise methods. Every entry point is @encapsulated, and the builder it writes into is allocated and finished inside the call with nothing mutable escaping; that leaves all of them callable from meta and from ordinary pure code. Render a digest with hex.encode or base64.encode.
The message is never materialised with its padding attached. _padded_byte answers for any index of the virtual padded message, and hashing a large input allocates nothing beyond the block being read.
Know the throughput before reaching for this on a large input. Measured on the dev box over 64 KiB, hash-only: about 56 KiB/s on the bytecode tier and about 4.8 MiB/s on the AOT tier. The hundredfold gap comes from the round being a few hundred u32 bit operations, where the bytecode tier pays interpreter dispatch on each one and the AOT tier lowers them to one or two machine instructions. A script run with hanki run therefore hashes a megabyte in about twenty seconds, which is long enough to be the wrong tool: hash a few KB there, and compile the program when the input is a file tree. This is a cost and no defect, and no fix is promised. The number has been re-measured across three sessions and lands in the same place, and what would move it is a contiguous unboxed array and nothing in this module.
_h0
_h0: List<u32> = [
1779033703u32,
3144134277u32,
1013904242u32,
2773480762u32,
1359893119u32,
2600822924u32,
528734635u32,
1541459225u32
]
Initial hash state: the first 32 bits of the fractional parts of the square roots of the first eight primes (FIPS 180-4 §5.3.3).
_k
_k: List<u32> = [
1116352408u32,
1899447441u32,
3049323471u32,
3921009573u32,
961987163u32,
1508970993u32,
2453635748u32,
2870763221u32,
3624381080u32,
310598401u32,
607225278u32,
1426881987u32,
1925078388u32,
2162078206u32,
2614888103u32,
3248222580u32,
3835390401u32,
4022224774u32,
264347078u32,
604807628u32,
770255983u32,
1249150122u32,
1555081692u32,
1996064986u32,
2554220882u32,
2821834349u32,
2952996808u32,
3210313671u32,
3336571891u32,
3584528711u32,
113926993u32,
338241895u32,
666307205u32,
773529912u32,
1294757372u32,
1396182291u32,
1695183700u32,
1986661051u32,
2177026350u32,
2456956037u32,
2730485921u32,
2820302411u32,
3259730800u32,
3345764771u32,
3516065817u32,
3600352804u32,
4094571909u32,
275423344u32,
430227734u32,
506948616u32,
659060556u32,
883997877u32,
958139571u32,
1322822218u32,
1537002063u32,
1747873779u32,
1955562222u32,
2024104815u32,
2227730452u32,
2361852424u32,
2428436474u32,
2756734187u32,
3204031479u32,
3329325298u32
]
Round constants: the first 32 bits of the fractional parts of the cube roots of the first sixty-four primes (FIPS 180-4 §4.2.2).
_block
_block: int = 64
The block size the compression function consumes, and what HMAC pads its key to. The digest size is not a constant here: it falls out of the eight-word state that _state_bytes renders.
_rotr
def _rotr(x: u32, n: u32) -> u32
Rotate right. bit_shr/bit_shl take their count mod the word width, so this is total for every n: at n = 0 both halves return x and the or is idempotent, which is the identity rotate and no accident.
_ch
def _ch(x: u32, y: u32, z: u32) -> u32
_maj
def _maj(x: u32, y: u32, z: u32) -> u32
bigsigma0
def _big_sigma0(x: u32) -> u32
bigsigma1
def _big_sigma1(x: u32) -> u32
smallsigma0
def _small_sigma0(x: u32) -> u32
smallsigma1
def _small_sigma1(x: u32) -> u32
_at
def _at(xs: List<u32>, i: int) -> u32
A List<u32> read that cannot fail into a wrong answer: every index this module passes is in range already, and zero is the identity for the xor/add the callers do, which lets an impossible miss degrade in place of needing a Crash row on a pure function.
paddedbyte
def _padded_byte(input: bytes, i: int, total: int) -> u8
The byte at i of the virtual padded message: the input, then 0x80, then zeros, then the 64-bit big-endian bit length in the final eight bytes (FIPS 180-4 §5.1.1). total is the padded length, and the length field starts at total - 8.
paddedword
def _padded_word(input: bytes, pos: int, total: int) -> u32
The big-endian u32 at byte offset pos of the virtual padded message.
paddedlength
def _padded_length(n: int) -> int
The padded length for an n-byte message: n, one 0x80, then zeros up to a multiple of the block with eight bytes left for the length field.
compressblock
def _compress_block(acc: List<u32>, input: bytes, off: int, total: int) -> List<u32>
One block: the message schedule (FIPS 180-4 §6.2.2 step 1) and the sixty-four compression rounds (steps 2-4) together, then the result added into the incoming state. Every + here is u32 addition, which wraps; that is the specified mod-2^32.
The schedule is a rolling window of sixteen locals and no 64-word List, and that is what makes these two steps share a function. w0 is W[t] at the top of round t and w15 is W[t+15], which leaves the recurrence for W[t+16] reading w14, w9, w1 and w0 alone. The window is shifted down by one at the end of each round and the new word enters at w15. A List schedule instead costs a trie walk per read and an allocation per append, and the loop does four reads and one append per word: on the bytecode tier that is a dispatched intrinsic each time, and on AOT it defeats the register allocation the bit ops already get.
The last sixteen rounds compute no new word: W[63] is the final one needed, and past t = 47 the recurrence would be building words nobody reads.
statebytes
def _state_bytes(acc: List<u32>) -> bytes
The eight-word state rendered big-endian into the 32-byte digest.
digest
def digest(input: bytes) -> bytes
The SHA-256 digest of input: 32 bytes, big-endian.
Render it for display with hex.encode or base64.encode: the digest itself is bytes and no text.
digest("abc".to_bytes()).length => 32
hex.encode(digest("".to_bytes())).slice(0, 16) => "e3b0c44298fc1c14"
blockkey
def _block_key(key: bytes) -> bytes
key reduced to one block, as HMAC specifies (RFC 2104 §2): a key longer than the block is replaced by its own digest, and any key shorter than the block is zero-padded up to it.
xorpad
def _xor_pad(block_key: bytes, pad: u8) -> bytes
key xored with a repeated pad byte: the ipad/opad halves of HMAC.
hmac
def hmac(key: bytes, message: bytes) -> bytes
HMAC-SHA-256 of message under key (RFC 2104): 32 bytes.
Verify a received tag with constant_time_eq? and never ==; see its note.
hmac("key".to_bytes(), "msg".to_bytes()).length => 32
hex.encode(hmac("".to_bytes(), "".to_bytes())).slice(0, 16) => "b613679a0814d9ec"
constanttimeeq?
def constant_time_eq?(a: bytes, b: bytes) -> bool
Whether a and b are equal, in time that does not depend on where they first differ.
The comparison every verification path must use. An ordinary == on bytes stops at the first differing byte, and how long it takes tells an attacker how much of a guessed tag or digest was right, and a tag can be recovered byte by byte from that alone. This reads every byte of both, always.
Length is not secret and is not hidden: a length mismatch answers false immediately: a digest's or tag's length is a public property of the algorithm and no property of the secret.
constant_time_eq?("abc".to_bytes(), "abc".to_bytes()) => true
constant_time_eq?("abc".to_bytes(), "abd".to_bytes()) => false
constant_time_eq?("abc".to_bytes(), "ab".to_bytes()) => false