///|
/// SHA-256 round constants: the first 32 bits of the fractional parts of the
/// cube roots of the first sixty-four primes (FIPS 180-4, section 4.2.2).
///
/// The gate fingerprints each failing cell's input text with SHA-256 so that a
/// failure record can be traced to its input without ever storing the text
/// itself (AD section 2.5, PRD-A 10.7). The digest is pure MoonBit: no
/// native binding is involved, so the zero-FFI rule of the library surface
/// stays intact.
let sha256_round_constants : Array[UInt32] = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
0xc67178f2,
]
///|
/// Initial hash value: the first 32 bits of the fractional parts of the square
/// roots of the first eight primes (FIPS 180-4, section 5.3.3).
let sha256_initial_state : Array[UInt32] = [
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
0x5be0cd19,
]
///|
/// Rotate a 32-bit word right by `n` bits, where `1 <= n <= 31`
/// (FIPS 180-4, section 3.2). The shift-left operand is the complementary
/// amount, so the two shifted halves never overlap.
fn rotate_right_32(value : UInt32, n : Int32) -> UInt32 {
(value >> n) | (value << (32 - n))
}
///|
/// Read four message bytes as one big-endian 32-bit word
/// (FIPS 180-4, section 5.1.2). Each byte is widened through `land(0xFF)`
/// because `Byte` is signed and would otherwise sign-extend.
fn big_endian_word(bytes : Array[Byte], offset : Int32) -> UInt32 {
let b0 = bytes[offset].to_int().land(0xFF).reinterpret_as_uint()
let b1 = bytes[offset + 1].to_int().land(0xFF).reinterpret_as_uint()
let b2 = bytes[offset + 2].to_int().land(0xFF).reinterpret_as_uint()
let b3 = bytes[offset + 3].to_int().land(0xFF).reinterpret_as_uint()
(b0 << 24) | (b1 << 16) | (b2 << 8) | b3
}
///|
/// Pad the message as FIPS 180-4 section 5.1.1 prescribes: a single `0x80`
/// byte, then zero bytes up to a 56-mod-64 offset, then the message length in
/// bits as a big-endian 64-bit integer.
fn pad_message(message : Bytes) -> Array[Byte] {
let bytes = message.to_array()
let original_length = bytes.length()
let bit_length = original_length.to_uint64() * 8
let padded_length = (original_length + 9 + 63) / 64 * 64
bytes.push(b'\x80')
while bytes.length() < padded_length - 8 {
bytes.push(b'\x00')
}
for byte in bit_length.to_be_bytes() {
bytes.push(byte)
}
bytes
}
///|
/// Apply the SHA-256 compression function to one 64-byte block, updating the
/// eight-word chaining state in place (FIPS 180-4, section 6.2.2).
fn compress_block(
state : Array[UInt32],
message : Array[Byte],
offset : Int32,
) -> Unit {
let schedule : Array[UInt32] = Array::make(64, 0)
for index in 0..<16 {
schedule[index] = big_endian_word(message, offset + index * 4)
}
for index in 16..<64 {
let x = schedule[index - 15]
let y = schedule[index - 2]
let s0 = rotate_right_32(x, 7) ^ rotate_right_32(x, 18) ^ (x >> 3)
let s1 = rotate_right_32(y, 17) ^ rotate_right_32(y, 19) ^ (y >> 10)
schedule[index] = schedule[index - 16] + s0 + schedule[index - 7] + s1
}
let mut a = state[0]
let mut b = state[1]
let mut c = state[2]
let mut d = state[3]
let mut e = state[4]
let mut f = state[5]
let mut g = state[6]
let mut h = state[7]
for index in 0..<64 {
let big_s1 = rotate_right_32(e, 6) ^
rotate_right_32(e, 11) ^
rotate_right_32(e, 25)
let choose = (e & f) ^ (e.lnot() & g)
let temp1 = h +
big_s1 +
choose +
sha256_round_constants[index] +
schedule[index]
let big_s0 = rotate_right_32(a, 2) ^
rotate_right_32(a, 13) ^
rotate_right_32(a, 22)
let majority = (a & b) ^ (a & c) ^ (b & c)
let temp2 = big_s0 + majority
h = g
g = f
f = e
e = d + temp1
d = c
c = b
b = a
a = temp1 + temp2
}
state[0] = state[0] + a
state[1] = state[1] + b
state[2] = state[2] + c
state[3] = state[3] + d
state[4] = state[4] + e
state[5] = state[5] + f
state[6] = state[6] + g
state[7] = state[7] + h
}
///|
/// The SHA-256 digest of `message` as eight big-endian 32-bit words.
fn sha256_digest(message : Bytes) -> Array[UInt32] {
let state : Array[UInt32] = []
for word in sha256_initial_state {
state.push(word)
}
let padded = pad_message(message)
let mut offset = 0
while offset < padded.length() {
compress_block(state, padded, offset)
offset = offset + 64
}
state
}
///|
/// Lowercase hexadecimal rendering of one 32-bit word, zero-padded to eight
/// digits, so concatenated words always form the 64-character digest.
fn hex_word32(word : UInt32) -> String {
let raw = word.to_string(radix=16)
let builder = StringBuilder()
for _ in 0..<(8 - raw.length()) {
builder.write_char('0')
}
builder.write_string(raw)
builder.to_string()
}
///|
/// SHA-256 of the UTF-8 bytes of `input`, as 64 lowercase hexadecimal digits.
///
/// The input text itself never enters a failure record (AD section 2.5); this
/// digest is the only trace of it. UTF-8 is the encoding of the frozen sample
/// texts, so the digest is stable across hosts and targets.
fn sha256_hex(input : String) -> String {
let digest = sha256_digest(@utf8.encode(input))
let builder = StringBuilder()
for word in digest {
builder.write_string(hex_word32(word))
}
builder.to_string()
}