AlgoMaster Logo

crypto Package

Medium Priority20 min readUpdated June 6, 2026

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 Package Landscape

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-packageWhat it providesTypical use
crypto/sha256, crypto/sha512SHA-2 family hash functionsReceipt fingerprints, content-addressed storage, building blocks for HMAC
crypto/sha1, crypto/md5Older hashes, broken for securityNon-security checksums only (cache keys, ETags)
crypto/hmacKeyed message authentication on top of any hashSigning tokens, verifying notifications from external services
crypto/subtleConstant-time comparison helpersComparing MACs, signatures, password hashes
crypto/randOS-backed cryptographic randomnessSalts, initialization vectors, secret tokens
crypto/aes, crypto/cipherSymmetric block ciphers and modes (GCM, CTR, CBC)Encrypting data at rest
crypto/rsa, crypto/ecdsa, crypto/ed25519Public-key signing and encryptionTLS, signed software updates
crypto/tls, crypto/x509TLS protocol, certificate handlingHTTPS 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.

The hash.Hash Interface

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.

SHA-256: One-Shot vs Streaming

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.

One-shot: sha256.Sum256

If 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[:].

Streaming: sha256.New + Write

If 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.

SHA-256 vs SHA-512

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.

HashOutput sizeSpeed on 64-bitWhen to pick
SHA-25632 bytesFastDefault choice for fingerprints, HMAC keys, content addressing
SHA-51264 bytesOften faster than SHA-256Larger output wanted, or you measured and SHA-512 is faster on your workload
SHA-120 bytesFastAvoid for any security use; collisions have been demonstrated
MD516 bytesFastAvoid for any security use; collisions are trivial to generate

HMAC: Authenticated Messages

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.

Verifying With hmac.Equal

When 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.

Password Hashing With Bcrypt

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:

AlgorithmWhere it livesNotes
bcryptgolang.org/x/crypto/bcryptWell-tested, fixed memory cost. Default choice if you have nothing else.
argon2idgolang.org/x/crypto/argon2Modern, tunable memory cost (resists GPU attacks better than bcrypt).
scryptgolang.org/x/crypto/scryptMemory-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.

Hashing a Password

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:

CostTime 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.

Verifying a Password

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.

Constant-Time Comparison With crypto/subtle

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.

MD5 and When Not to Use It

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.

UseMD5 OK?Why
Cache key from a config blobYesNo attacker, just dedupe
ETag for an HTTP responseYesFast, collisions don't matter for caching
Checksum of a file from a trusted sourceYesCatches accidental corruption
Detecting accidental duplicates in a datasetYesRandom collisions are vanishingly unlikely
Signing a download URLNoUse HMAC-SHA256
Hashing a passwordNo (catastrophic)Use bcrypt or argon2
Content-addressed storage where attackers can submit contentNoMD5 collisions are trivial to generate
Verifying a software updateNoUse 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.

Generating Salts and Initialization Vectors

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.

Putting It Together

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.

Quiz

crypto Package Quiz

10 quizzes