How do I hash data in EK9, and what is the difference between Digest and HMAC?
← Advanced Type System · Ref: Q1386
Digest produces UNKEYED cryptographic hashes. It answers ONE question - has this content changed? - and proves INTEGRITY only. Anyone at all can compute a digest, so anyone who can replace your content can replace the digest beside it.
THREE OVERLOADS
Digest().SHA256(text) // String -> String, UPPER-case hex Digest().SHA256(aGuid) // GUID -> String, UPPER-case hex Digest().SHA256(someBytes) // Bytes -> Bytes, the OCTETS
SHA512 HAS THE SAME THREE OVERLOADS
Digest().SHA512(...) takes a String, a GUID or Bytes exactly as SHA256 does - one shape, two algorithms. Its Bytes form returns 64 octets where SHA-256 returns 32, and its hex is 128 characters.
USE THE BYTES OVERLOAD FOR ANYTHING THAT IS NOT TEXT
A request body, a file, a frame off the wire - none of those have a guaranteed encoding, so routing them through a String needs an assumption you may not be entitled to make. SHA256(Bytes) hashes the octets with no decode at all, and the result chains straight into the next step (an HMAC key, a signature) with no hex round trip.
HEX IS UPPER-CASE ACROSS THE FAMILY
SHA256(String), Bytes' $ and toText("hex") all emit UPPER-case, so a String digest and a Bytes digest of the same input compare EQUAL with no normalising. That was not true before 2026-09-25: the String form emitted lower-case, inherited from the type formerly misnamed HMAC, and the two spellings of one SHA-256 were silently unequal. ⚠️ Published vectors are conventionally WRITTEN lower-case (shasum, git and openssl all print that way), so a literal copied from a spec needs its case folded - the value is identical, only the rendering differs.
TRI-STATE
Unset in, unset out - never a digest over a substitute value. An EMPTY Bytes is SET, not unset, and hashing zero octets is well defined: it gives the published SHA-256 of empty input.
IF YOU NEED TO PROVE WHO, THIS IS THE WRONG TYPE
A hash over attacker-supplied content is exactly as valid as a hash over yours. To prove a message came from someone holding a secret, use HMAC - see Q1387. This split exists because the type named HMAC used to hold only these unkeyed hashes, so code reaching for it got a bare digest and the authenticity it assumed was imaginary.
SHA-1 and MD5 are deliberately NOT on Digest - both are broken for security use. See Q1383 for Bytes and Q1387 for the keyed MAC. Use 'ek9 -h Digest' for the full API.
Example
defines module qa.digest.usage defines constant sampleText <- "the quick brown fox" defines program DigestUsage() stdout <- Stdout() //A digest answers ONE question: has this content changed? Anyone can compute it, //so it proves integrity and nothing whatsoever about who produced it. fingerprint <- Digest().SHA256(sampleText) stdout.println(`fingerprint: ${fingerprint}`) stdout.println(`length: ${length fingerprint}`) //The SAME content always gives the SAME digest - that is the whole mechanism. stdout.println(`stable: ${Digest().SHA256(sampleText) == fingerprint}`) //One changed character changes the whole value. There is no "nearly equal" digest. stdout.println(`avalanche: ${Digest().SHA256("the quick brown fix") <> fingerprint}`) //🔑 THE BYTES OVERLOAD takes and returns OCTETS. Use it for a request body, a file, //or anything else that is not text: no decode, so no encoding to assume wrongly. payload <- Bytes().withUtf8(sampleText) asOctets <- Digest().SHA256(payload) stdout.println(`octets length: ${length asOctets}`) //🔑 Both spellings of a SHA-256 agree EXACTLY - hex is UPPER-case across the whole family, //so a String digest and a Bytes digest of the same input compare equal with no normalise. stdout.println(`same value: ${asOctets.toText("hex") == fingerprint}`) //Published vector: SHA-256("abc"). Bytes("616263") is that ASCII, written as hex. stdout.println(`abc: ${Digest().SHA256(Bytes("616263"))}`) //SHA512 is the same shape: the same three overloads, 64 octets instead of 32. longer <- Digest().SHA512(payload) stdout.println(`sha512 octets length: ${length longer}`) stdout.println(`sha512 same value: ${longer.toText("hex") == Digest().SHA512(sampleText)}`) //A GUID hashes through its text form - handy for opaque, stable identifiers. stdout.println(`guid hashed: ${Digest().SHA256(GUID())?}`) //Unset in, unset out - never a digest over a substitute value. stdout.println(`unset in: ${Digest().SHA256(String())?}`)
Common mistakes
E50060 — HMAC is KEYED and takes SHA256(key, message) over Bytes. The unkeyed hashes live on Digest. The compiler names the only available signature in the error, so it tells you which of the two you actually reached.
Incorrect:
fingerprint <- HMAC().SHA256(sampleText)
Correct:
fingerprint <- Digest().SHA256(sampleText)
E07620 — SHA256(String) yields a String and SHA256(Bytes) yields Bytes - different types, so there is no == between them. Convert one side; both render UPPER-case hex, so once the types match the values compare equal.
Incorrect:
asOctets == fingerprint
Correct:
asOctets.toText("hex") == fingerprint
Other ways to ask this
- How do I compute a SHA-256 in EK9?
- Which type do I use for hashing, Digest or HMAC?
- How do I hash a request body or a file rather than a String?
- Why does my digest not equal my Bytes hex?
Coming from another language?
Java: MessageDigest.getInstance("SHA-256") throws a checked NoSuchAlgorithmException for an algorithm the platform mandates, returns a mutable byte[] with reference equality, and leaves hex formatting to you. Python: hashlib.sha256(...).hexdigest() is convenient but silently encodes a str with the default codec, which is the encoding assumption EK9 refuses to make. Go: crypto/sha256 returns [32]byte and hex.EncodeToString is a separate step. Node: crypto.createHash('sha256').update(x).digest('hex') guesses an encoding for a string input. EK9: the algorithm cannot fail to exist, the input is either text you named or octets you supplied, the result is tri-state, and Bytes carries its own length with value equality.
Keywords: hash, sha256, integrity, etag, sha512, digest, hashing, checksum, octets, unkeyed, fingerprint