Go ships a family of cryptographic packages under the crypto/ import path: hash functions, message authentication, primitives for symmetric and public-key crypto, a secure random source, and a few small helpers like constant-time comparison. This chapter is an overview of the parts an everyday e-commerce backend reaches for most often: hashing order data, signing download links, verifying notifications from a payment provider, storing customer passwords, and comparing secrets without leaking timing information. The cryptographic protocols built on top (TLS, AES-GCM, RSA signing) get their own dedicated treatment elsewhere and aren't covered here.
The crypto import path is a tree, not a single package. Each leaf is small, focused, and composable. Knowing which leaf does what is half the battle, because once you know where a primitive lives, the API for using it is usually three or four lines.
| Sub-package | What it provides | Typical use |
|---|---|---|
crypto/sha256, crypto/sha512 | SHA-2 family hash functions | Receipt fingerprints, content-addressed storage, building blocks for HMAC |
crypto/sha1, crypto/md5 | Older hashes, broken for security | Non-security checksums only (cache keys, ETags) |
crypto/hmac | Keyed message authentication on top of any hash | Signing tokens, verifying notifications from external services |
crypto/subtle | Constant-time comparison helpers | Comparing MACs, signatures, password hashes |
crypto/rand | OS-backed cryptographic randomness | Salts, initialization vectors, secret tokens |
crypto/aes, crypto/cipher | Symmetric block ciphers and modes (GCM, CTR, CBC) | Encrypting data at rest |
crypto/rsa, crypto/ecdsa, crypto/ed25519 | Public-key signing and encryption | TLS, signed software updates |
crypto/tls, crypto/x509 | TLS protocol, certificate handling | HTTPS clients and servers |
The top-level crypto package itself is tiny. It defines a crypto.Hash enum (so library code can say "I want SHA-256" without importing the SHA-256 package directly) and a crypto.Signer interface used by RSA and ECDSA. You rarely import crypto on its own. You import the leaf you need.
This chapter sticks to the parts in the top half of the table. SHA-256 for hashing. HMAC for signing. crypto/subtle for safe comparison. Bcrypt (from the extended golang.org/x/crypto repository) for passwords. And a brief mention of crypto/rand for salts.
The diagram shows the dependencies an everyday e-commerce service touches. Hashing and HMAC sit on top of a hash function. HMAC verification routes through crypto/subtle so it doesn't leak timing. Bcrypt and HMAC both pull random bytes from crypto/rand for salts and keys. Application code calls into each of these as needed and almost never composes them by hand.
Every hash function in the standard library returns a hash.Hash from its New() constructor. The interface is small, and once you know it, the API for SHA-256, SHA-512, MD5, SHA-1, and the keyed HMAC wrapper is identical.
The two methods you use constantly are Write and Sum. You feed bytes in with Write (or io.Copy, since Hash is an io.Writer), then you call Sum(nil) to get the digest back as a []byte. The argument to Sum is a slice to append to. Passing nil gives you a fresh slice with just the hash. Passing an existing slice lets you build a buffer like prefix-digest in one allocation. Most code passes nil.
Reset exists so you can reuse the same Hash across multiple inputs without allocating a new one. In a tight loop that hashes many small values, calling Reset and reusing the hasher avoids one allocation per iteration.
Size and BlockSize are constants for a given hash. SHA-256 has Size() == 32 (its output is 32 bytes, or 256 bits) and BlockSize() == 64 (it processes input in 64-byte chunks internally). You rarely need these directly, but HMAC and a few other constructions read them off the underlying hash.
The output's last line will match for any program that hashes the exact same input. SHA-256 is deterministic, which is the whole point.
Cost: Sum(nil) allocates a 32-byte slice for SHA-256. Inside a hot loop that hashes millions of values, passing in a reusable buffer (h.Sum(buf[:0])) avoids that per-call allocation.
crypto/sha256 is what you use when you need a cryptographic fingerprint of some bytes. Common e-commerce uses are receipt fingerprints (so you can detect whether a stored receipt was tampered with), content-addressed storage of product images, and dedupe keys for incoming notifications.
There are two ways to call it. Which one you pick depends on whether you have all the data in memory at once.
sha256.Sum256If the input is a []byte that already fits in memory, use the convenience function sha256.Sum256. It takes a []byte, returns a [32]byte (an array, not a slice), and is the shortest possible code path.
The return type is a fixed-size array. If you want a slice (for hex encoding, comparison, or storage), take a slice of it with digest[:].
sha256.New + WriteIf the input is a stream (a file, an HTTP response body, a database scan that returns rows), don't slurp it all into memory just to hash it. Use sha256.New() to get a hash.Hash, feed it bytes with Write or io.Copy, then call Sum(nil) at the end.
The streaming form uses constant memory regardless of input size. A one-gigabyte file goes through a 32-kilobyte buffer, not into RAM all at once. This matters for any code that hashes uploads or downloads.
Cost: SHA-256 throughput on modern CPUs is roughly 500 MB/s per core for software implementations and 2 GB/s+ when the CPU has SHA extensions (most Intel CPUs since Goldmont, all Apple Silicon, recent AMD). The Go standard library uses the hardware path automatically when it's available.
Both are part of the SHA-2 family. SHA-256 produces 32 bytes of output; SHA-512 produces 64. The interesting bit is that on 64-bit hardware, SHA-512 is often faster per byte than SHA-256, because its internal arithmetic uses 64-bit words and SHA-256 uses 32-bit words. The throughput difference is real but small enough that "use SHA-256 unless you have a reason not to" is fine advice. SHA-256 hashes are also half the size, which matters for storage and for embedding in URLs.
| Hash | Output size | Speed on 64-bit | When to pick |
|---|---|---|---|
| SHA-256 | 32 bytes | Fast | Default choice for fingerprints, HMAC keys, content addressing |
| SHA-512 | 64 bytes | Often faster than SHA-256 | Larger output wanted, or you measured and SHA-512 is faster on your workload |
| SHA-1 | 20 bytes | Fast | Avoid for any security use; collisions have been demonstrated |
| MD5 | 16 bytes | Fast | Avoid for any security use; collisions are trivial to generate |
A SHA-256 digest tells you "this is the fingerprint of these bytes". It does not tell you "these bytes came from someone who knows a shared secret". For that, you need a Message Authentication Code, and the standard one is HMAC.
The setting is concrete. Your e-commerce service generates a download link for a digital product invoice. The link is https://example.com/invoices/42. If you sign nothing, anyone who can guess that URL pattern can download anyone's invoice. You need to attach a short token to the URL that proves the server generated it. HMAC fits this use case: the server holds a secret key, computes HMAC(secret, "invoice=42&expires=1715900000"), and includes the result as a query parameter. When the link is clicked, the server recomputes the HMAC over the same parameters and compares the two tokens. If they match, the link was issued by the server. If they don't, it's a forgery.
The token is deterministic given the same key and message. Anyone with the key produces the same token. Without the key, the token is unguessable in practical terms (2^256 possibilities for SHA-256-based HMAC).
hmac.New takes a hash constructor (not a hash instance) and a key, and returns a hash.Hash you use the same way as sha256.New(). You pass sha256.New (no parentheses) because HMAC needs to be able to create fresh hash states internally.
hmac.EqualWhen the request comes back in, you have to compare the token in the URL with the token your server recomputes. This is where most homegrown crypto goes wrong.
The flow looks innocent, but the comparison step in the middle has a hidden trap. Use hmac.Equal. Never use == or bytes.Equal on a freshly received signature.
What's wrong with this code?
The comparison runs == on two strings, which short-circuits at the first differing byte. An attacker who can measure the response time of many requests can guess the signature byte by byte: try 00..., 01..., ..., until one of them takes a few nanoseconds longer to reject, indicating the first byte was correct. Then move to the second byte, and so on. With enough samples, the full signature falls out. This is a timing attack, and it's not theoretical.
Fix:
hmac.Equal always touches every byte of both inputs before returning a result. The time it takes does not depend on where the inputs differ. The signature stays opaque.
The same rule applies any time you compare a secret-shaped value (HMAC, signature, password hash, API key) to a value sent by a client. Use a constant-time comparison.
Customer passwords get their own treatment, separate from generic hashing. SHA-256 is a terrible password hash for a concrete reason.
SHA-256 is fast. On a recent laptop, a single core can compute roughly 500 million SHA-256 hashes per second. That speed is what makes it good for everything we've covered so far: signing tokens, fingerprinting receipts, content addressing. It's also exactly what makes it a disaster for passwords. An attacker who steals a database of sha256(password) values can run a 30-billion-guess dictionary attack in a minute on a GPU. Salting doesn't fix that: salts stop two users with the same password from showing the same hash, but they don't slow down per-password cracking.
The right primitive is a password hash function: slow on purpose, parameterized so the slowness can grow as hardware improves, and tied to a per-password salt. The choices people use in 2024 are:
| Algorithm | Where it lives | Notes |
|---|---|---|
bcrypt | golang.org/x/crypto/bcrypt | Well-tested, fixed memory cost. Default choice if you have nothing else. |
argon2id | golang.org/x/crypto/argon2 | Modern, tunable memory cost (resists GPU attacks better than bcrypt). |
scrypt | golang.org/x/crypto/scrypt | Memory-hard alternative to bcrypt. Less commonly used now. |
All three live in the extended golang.org/x/crypto repository, not the standard library. The Go team intentionally kept them out of crypto/ proper because password hashing parameters evolve faster than the standard library's stability promises allow. The trade-off is that you pull in an extra module:
This adds golang.org/x/crypto to your go.mod. It's maintained by the Go team, has a stable API, and is what every production Go service uses for password storage.
The output is a self-describing string: $2a$ says "bcrypt", 10 is the cost factor (more on that in a moment), and the rest is the salt and hash combined. You store the whole string in the database. There's no separate column for the salt, because the salt is part of the hash.
The cost factor 10 means bcrypt runs 2^10 = 1024 rounds of its internal mixing. Each increment of the cost factor doubles the work. bcrypt.DefaultCost is currently 10, but 12 is a common production setting, and 14 is reasonable for high-value accounts on modern hardware. Higher is safer but slower:
| Cost | Time per hash (rough, modern CPU) |
|---|---|
| 10 | ~60 ms |
| 12 | ~250 ms |
| 14 | ~1 s |
| 16 | ~4 s |
The cost is deliberately tunable so you can raise it every few years as hardware gets faster, without changing the underlying algorithm. Pick the highest cost your login flow can tolerate. A few hundred milliseconds per login is fine for users and is a wall for attackers running offline guesses.
Cost: Bcrypt is slow on purpose. A login endpoint that hashes a password on every request is single-threaded per CPU core. Plan for it: cap the cost factor so your server can still handle a sudden traffic burst, and never call bcrypt inside a tight loop on the request path.
To check a login, don't re-hash the input and compare. Bcrypt has a dedicated comparison function that knows how to read the stored hash's embedded salt and cost.
bcrypt.CompareHashAndPassword does three things in one call: extracts the salt and cost from the stored hash, recomputes bcrypt of the attempt with the same parameters, and compares the result in constant time. You don't need to call hmac.Equal separately, the bcrypt package handles it.
A wrong password returns bcrypt.ErrMismatchedHashAndPassword. Don't expose the specific error to the user. The login endpoint should respond identically for "wrong password", "no such user", and "account locked", so an attacker can't enumerate which usernames exist.
The diagram shows what GenerateFromPassword does internally: it pulls a random salt from crypto/rand, runs the bcrypt mixing function for 2^cost rounds, and packages the cost, salt, and final hash into one self-describing string. Verification reverses the packaging, recomputes with the same salt and cost, and compares.
hmac.Equal is a wrapper around the more general crypto/subtle package. The package is small: it provides constant-time alternatives for comparison and choice operations that, written naively, would leak information through timing.
The most useful function is subtle.ConstantTimeCompare. It takes two []byte of equal length and returns 1 if they match, 0 otherwise. Crucially, the time it takes is determined by the length of the inputs, not by where they differ.
The function returns an int, not a bool, on purpose. The constant-time discipline extends to how you use the result: if you write if compare == 1 { ... }, the branch itself runs in different time depending on the answer, which is fine for the comparison step but is something to keep in mind for downstream code. Most of the time you just want if subtle.ConstantTimeCompare(a, b) == 1 { ... }.
The other functions in the package (ConstantTimeSelect, ConstantTimeEq, ConstantTimeCopy) are lower-level building blocks for implementing crypto primitives. Application code rarely needs them. ConstantTimeCompare and hmac.Equal cover almost every realistic use case.
A subtle gotcha: if the two slices have different lengths, subtle.ConstantTimeCompare returns 0 immediately. That's the right behavior, but it means the function leaks length. For comparing HMACs that always have the same length, this is fine. For comparing arbitrary user input, the leak is usually irrelevant, but hmac.Equal is a safer default because it explicitly documents the length check.
Cost: Constant-time compare is roughly 2-5x slower than bytes.Equal for small inputs, because it can't short-circuit. The absolute numbers are nanoseconds, so the cost only matters in tight loops. For one comparison per request, the difference is invisible.
crypto/md5 is still in the standard library, and there are still places where it's the right answer. The line between those places and the password-hashing trap is sharp: never use MD5 for anything that needs to resist a motivated attacker. Use it only for non-adversarial integrity or addressing.
| Use | MD5 OK? | Why |
|---|---|---|
| Cache key from a config blob | Yes | No attacker, just dedupe |
| ETag for an HTTP response | Yes | Fast, collisions don't matter for caching |
| Checksum of a file from a trusted source | Yes | Catches accidental corruption |
| Detecting accidental duplicates in a dataset | Yes | Random collisions are vanishingly unlikely |
| Signing a download URL | No | Use HMAC-SHA256 |
| Hashing a password | No (catastrophic) | Use bcrypt or argon2 |
| Content-addressed storage where attackers can submit content | No | MD5 collisions are trivial to generate |
| Verifying a software update | No | Use a signed hash with SHA-256 or better |
The MD5 API is identical to SHA-256, because both implement hash.Hash.
The output is a stable 16-byte fingerprint of the filter combination, fine as a Redis or in-memory cache key. If two filters happen to collide, the cache returns the wrong page for the wrong filter, which is a minor bug. No attacker is involved.
The same code with SHA-256 would also work, just with a 32-byte key instead of 16. Use SHA-256 by default. Use MD5 only when you specifically want a shorter, faster hash and you've thought about whether collisions matter.
The last piece of the puzzle is randomness. Salts for password hashes, initialization vectors for symmetric encryption, nonces for protocols: all of them need bytes that an attacker cannot predict. The package for this is crypto/rand, and the function is rand.Read.
crypto/rand pulls from the operating system's secure random source (/dev/urandom on Linux, BCryptGenRandom on Windows, getentropy on macOS), which is the right source for any security-relevant randomness.
The function returns an error. In practice, on a healthy system, it doesn't fail. But the error path exists because on very early boot (before the kernel has gathered enough entropy) or in tightly sandboxed environments, it can. Always check the error.
The bcrypt and argon2 packages call rand.Read for you internally when you pass in a password, so you don't have to handle salts directly for password hashing. For symmetric encryption with crypto/cipher, you call rand.Read yourself to generate the nonce or IV.
The distinction between math/rand (predictable, fast, for simulations) and crypto/rand (unpredictable, slower, for secrets) matters: always use crypto/rand for anything security-related.
Cost: crypto/rand.Read involves a system call on most platforms. For one-off salt generation, the cost is irrelevant. For generating thousands of small tokens per second, batch your reads (ask for 256 bytes at once, slice them) rather than calling rand.Read per token.
A realistic e-commerce request flow exercises most of what we've covered: a customer logs in (bcrypt verify), a payment provider sends a notification (HMAC verify with constant-time compare), and the system issues a signed download link for the invoice (HMAC sign with SHA-256).
The flow is straightforward once each primitive is in its place. The signing secret never leaves the server. The token attached to the URL is unforgeable without the secret. Changing any parameter, including the expiration, invalidates the token. The comparison uses hmac.Equal, so a timing attack against the signature is closed off.
10 quizzes