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.sha512(data)                   # hex; sha512_bytes for the raw 64 bytes
hash.sha3_256(data)                 # hex; sha3_256_bytes for the raw 32 bytes
hash.blake2b(data)                  # hex; blake2b_bytes for the raw 64 bytes

hash.md5(data)                      # hex; checksums and protocols, not security
hash.md5_bytes(data)                # raw 16 bytes

h = hash.hasher("sha256")           # also "sha512", "sha3_256", "sha1", "md5"
h = hash.update(h, piece)           # a new hasher; bytes, a string or a stream
hash.finish(h)                      # hex; hash.finish_bytes(h) for raw bytes

hash.key()                          # 32 random bytes
hash.encrypt(key, data, aad: ctx)   # sealed bytes (ChaCha20-Poly1305)
hash.decrypt(key, sealed, aad: ctx) # bytes; throws kind "crypto" if refused

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. Every digest function, its _bytes form 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.

Hashing in pieces

When the data passes through your code anyway - read in chunks to upload, to split into parts, or to hash two ways at once - a hasher takes it a piece at a time, so the file is read once:

import std.fs
import std.hash
import std.io

s = fs.open("backup.tar")
mut whole = hash.hasher("sha256")
mut chunk = io.read(s, 1048576)
while chunk != null {
    upload(chunk)
    whole = hash.update(whole, chunk)
    chunk = io.read(s, 1048576)
}
io.close(s)
print(hash.finish(whole))

A hasher is a value: update returns a new one and leaves the one it was given as it was, the way push returns a new list. So a hasher kept from earlier still finishes at its own point, which is how one prefix can be finished twice. hasher takes "sha256", "sha512", "sha3_256", "sha1" or "md5"; finish gives the same hex as the one-shot function over the same bytes, and finish_bytes the raw digest.

There is no BLAKE2b hasher. A hasher is the digest's saved state, and the BLAKE2 implementation does not expose its state. hash.blake2b(stream) still reads a file of any size a chunk at a time.

Choosing a digest

Use sha256 unless something tells you otherwise. The others are there for when a format, a protocol or another system names a different one:

functiondigesttypical reason
sha256SHA-256, 32 bytesthe default: checksums, content addressing, signatures
sha512SHA-512, 64 bytesmanifests and package formats that specify it
sha3_256SHA3-256, 32 bytessystems that require SHA-3
blake2bBLAKE2b-512, 64 bytestools that use BLAKE2, such as b2sum
sha1SHA-1, 20 byteslegacy protocols only (see below)
md5MD5, 16 byteschecksums and protocols only (see below)

blake2b is the full 512-bit BLAKE2b, so its hex matches b2sum's default output.

MD5 is for checksums, not security

md5 and md5_bytes take the same inputs as sha256 - a string, bytes or a stream - so hash.md5(fs.open(path)) checksums a file of any size. They exist for formats and protocols that name MD5. S3's Content-MD5 header, for example, is the base64 of the raw digest:

import std.encoding
import std.fs
import std.hash

content_md5 = encoding.base64_encode(hash.md5_bytes(fs.open("backup.tar.gz")))

MD5 is broken: two different inputs can be made to share a digest. Do not use it to verify that a download is genuine, to sign, or to store passwords. Use sha256 for integrity you must trust.

Encrypting data

import std.fs
import std.hash

key = hash.key()                       # keep this somewhere safe
sealed = hash.encrypt(key, fs.read_bytes("backup.tar"), aad: "backup-v1")
fs.write("backup.tar.enc", sealed)

plain = hash.decrypt(key, fs.read_bytes("backup.tar.enc"), aad: "backup-v1")

encrypt seals bytes (or a string's UTF-8) with ChaCha20-Poly1305, an authenticated cipher: the result cannot be read without the key, and cannot be changed without decrypt noticing. There is nothing else to set. The nonce is generated for you from OS entropy on every call, so encrypting the same data twice gives different output.

The key is 32 bytes of bytes. hash.key() makes one; a string is refused, because a passphrase is not a key. Any other length is an error that names the length you passed.

aad: is optional context that is checked but not stored - a file name, a record id, a format tag. It stops a sealed value from being moved to a place it was not made for: decrypt must be given the same aad: or it refuses.

decrypt throws a map with kind: "crypto" when the key is wrong, the aad: differs, the data was changed by even one bit, or the bytes are truncated or not sealed data. It does not say which, and it never returns partly decrypted data.

try {
    plain = hash.decrypt(key, sealed)
} catch (e) {
    if get(e, "kind") == "crypto" { print("wrong key or damaged file") }
}

The sealed bytes are version (1) || nonce (12) || ciphertext || tag (16). The version byte is 0x01 and means ChaCha20-Poly1305, so files written today stay readable if a later release adds another cipher. After the version byte it is the standard RFC 8439 layout, which other tools can open. Encryption holds the whole message in memory, so a stream is refused; random nonces are safe for about four billion messages under one key, so rotate keys well before that.

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 bad data in the database fails the login rather than raising an error.

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

A digest is one-way: there is no way back from it to the input. To keep data secret, use encrypt and decrypt above, and TLS in transit (std.net, std.http).