std.hash
import std.hash
hash.sha256("abc") # hex digest
hash.sha256(stream) # a stream, read a chunk at a time
hash.hmac_sha256(key, message) # hex; the message may be a stream too
hash.sha256_bytes(data) # raw 32 bytes
hash.hmac_sha256_bytes(key, msg)
hash.password("pw") # Argon2id PHC string
hash.verify("pw", stored) # constant-time -> Bool
hash.constant_eq(a, b) # compare a MAC or token, no early exit
Inputs are a string, bytes or a stream.
Hashing a file without reading it
import std.fs
import std.hash
digest = hash.sha256(fs.open("release.tar.gz"))
The memory this costs is one chunk, whatever the file's size. sha256, sha1, their _bytes forms and the message argument of hmac_sha256 accept any stream: a file, a socket, a child's output, an HTTP body. The stream is read to its end and consumed, so a read after the digest returns null.
The answer is the same as hashing the same content as a string, so a digest taken this way compares with one taken any other way. There is no hash.file, because hash.sha256(fs.open(path)) already is one, and it works on everything else too.
A line stream from io.lines is refused. Its terminators have been removed, so its digest would be of something that was never the input - which is the kind of mismatch that costs an afternoon.
Passwords: use password and verify
stored = hash.password(reveal(plain)) # store this
ok = hash.verify(reveal(attempt), stored)
Argon2id with a per-call random salt, producing a self-describing PHC string that carries its own parameters - so verification keeps working when you change the cost settings later.
verify is constant-time and returns false rather than raising on a malformed stored hash, so a login path cannot be crashed by bad data in the database.
Never hash a password with sha256. A digest is designed to be fast, which is exactly wrong for a password: fast means a stolen table can be brute-forced at enormous rates. Argon2id is deliberately slow and memory-hard. Salting a sha256 yourself does not close that gap.
HMAC for authentication
hmac_sha256(key, message) proves a message was produced by someone holding the key. This is what signs a cookie or a webhook:
mac = hash.hmac_sha256(reveal(secret_key), payload)
Compare MACs with constant_eq, never ==. A comparison that returns on the first differing byte leaks how much of a forged MAC was correct, which is enough to forge one byte at a time.
if hash.constant_eq(mac, expected) { accept() }
constant_eq takes strings or bytes, so a hex digest and its _bytes form both work, and a string compares as its UTF-8. Reach for it for anything secret you check for equality: a MAC, a CSRF token, a session id, an API key.
It compares content in constant time but does not hide length - values of different lengths answer false immediately. Everything it is meant for is fixed-length, so the length reveals nothing.
It is native for the same reason password and verify are. A loop written in Ecko allocates, bounds-checks every index and can be descheduled between iterations, so it cannot promise anything about timing.
sha1 is legacy only
hash.sha1 and hash.sha1_bytes exist for legacy protocols that require it - the MySQL authentication scramble, for instance. SHA-1 is broken for collision resistance. Do not choose it for anything new.
Not encryption
Hashing is one-way. There is no decrypt. For confidentiality use TLS in transit (std.net, std.http) and your platform's facilities at rest.