How to use crypto package

The Go `crypto` package is a meta-package that organizes cryptographic functionality into sub-packages; you never import `crypto` directly but instead import specific sub-packages like `crypto/sha256` for hashing or `crypto/rand` for secure random number generation.

The login system that almost broke

You are building a login system. You store passwords in a database. A hacker dumps the database. If you stored plain text, the game is over. If you hashed the passwords, the hacker sees gibberish. But if you used the wrong hash function, or generated session tokens with a predictable random number, the hacker cracks the data in minutes. Go's crypto package gives you the tools to do this right. It provides secure hashing, encryption, and random number generation. The package is designed as a collection of primitives. You get the building blocks and assemble them. This approach keeps the standard library focused and forces you to understand what you are using.

Why crypto is a namespace, not a toolbox

The crypto package is a meta-package. It organizes cryptographic functionality into sub-packages. You never import crypto directly to use a function. The root package has no exported names. If you try to call crypto.SHA256, the compiler rejects the code with undefined: crypto.SHA256. You must import the specific sub-package you need.

Think of crypto like a hardware store aisle sign. The sign tells you where the tools are, but you do not buy the sign. You walk to the shelf and pick the exact wrench you need. Import crypto/sha256 for hashing. Import crypto/rand for secure randomness. Import crypto/aes for encryption. This design makes dependencies explicit. If your code imports crypto/sha256, anyone reading the imports knows exactly which algorithm you use. It also keeps binaries small. You do not pull in RSA code if you only need a hash.

Go favors explicit imports over a monolithic library. This matches the language philosophy: clear, simple, and easy to reason about. When you see import "crypto/rand", you know the code relies on the operating system's entropy source. When you see import "math/rand", you know the code uses a deterministic pseudo-random generator. The distinction is visible at the call site.

Explicit imports prevent accidental algorithm upgrades. You choose the version. You choose the trade-offs.

Generating secrets that actually stay secret

Randomness is the foundation of cryptography. Tokens, keys, and nonces must be unpredictable. Go provides two random number generators. Use crypto/rand for security-sensitive operations. Use math/rand for simulations, games, or shuffling.

crypto/rand reads from the operating system's entropy pool. On Linux, this is /dev/urandom. On Windows, it uses BCryptGenRandom. The OS gathers entropy from hardware events, timing jitter, and other sources. The result is cryptographically secure. math/rand uses a Mersenne Twister algorithm. It is fast and has a long period, but the output is predictable if you know the seed. Never use math/rand for secrets.

Here is the simplest way to generate a secure token: allocate a byte slice, fill it with OS entropy, and encode it for safe transport.

package main

import (
	"crypto/rand"
	"encoding/hex"
	"fmt"
)

func main() {
	// 32 bytes gives 256 bits of entropy.
	// Enough for session tokens and API keys.
	token := make([]byte, 32)

	// Read blocks until the OS provides enough entropy.
	// It writes directly into the pre-allocated slice.
	if _, err := rand.Read(token); err != nil {
		// Entropy exhaustion is rare but fatal for security.
		// Abort immediately rather than returning weak data.
		panic(err)
	}

	// Hex encoding avoids control characters in logs or URLs.
	// Each byte becomes two readable ASCII characters.
	fmt.Println(hex.EncodeToString(token))
}

rand.Read writes random bytes into the slice. It returns the number of bytes written and an error. If the error is not nil, the slice may contain zeros or partial data. You must check the error. If rand.Read fails, you do not have a secure token. The convention in Go is to handle errors immediately. The verbose if err != nil pattern makes the failure path visible. In crypto code, hiding an error is dangerous. If randomness fails, the program should abort.

crypto/rand is the only random number generator you use for secrets. math/rand is for dice rolls.

Walking through the random byte generation

When you call rand.Read, the Go runtime makes a system call to the OS. The OS mixes hardware interrupts, disk timing, and network packet arrival times into a pool. It then extracts bytes from that pool and copies them into your slice. The call blocks if the pool is temporarily dry, which guarantees you never get predictable output.

The function returns two values. The first is the byte count. The second is an error. Go functions return multiple values naturally. You capture both with n, err := rand.Read(token). If you only care about the error, you can discard the count with an underscore: _, err := rand.Read(token). The underscore tells the compiler you intentionally ignored that return value. Use it sparingly with errors, but it is perfectly fine for byte counts when you already know the slice size.

Public names start with a capital letter. Private names start lowercase. Go uses capitalization for visibility, not keywords like public or private. This keeps the syntax minimal and forces you to think about package boundaries. If a function is only used inside the file, keep it lowercase. If other packages need it, capitalize it.

Trust the OS entropy pool. Do not roll your own mixing logic.

Hashing data for integrity checks

Hashing maps data of arbitrary size to a fixed-size value. A good hash function is deterministic, fast to compute, and resistant to collisions. Two different inputs should not produce the same output. You cannot reverse a hash to get the original input. Hashes verify integrity. They do not encrypt data.

Use crypto/sha256 for general-purpose hashing. SHA-256 produces a 256-bit digest. It is widely supported and considered secure for integrity checks. The package provides two ways to hash. Use sha256.Sum256 for small data that fits in memory. Use sha256.New for streaming large data.

Here is how to hash a buffer in a single call.

package main

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
)

// HashData computes the SHA-256 digest of the input bytes.
func HashData(data []byte) string {
	// Sum256 returns a fixed-size [32]byte array.
	// Arrays are values in Go, so the caller gets a copy.
	hash := sha256.Sum256(data)

	// Slicing the array creates a []byte backed by the array.
	// hex.EncodeToString expects a slice, not an array.
	return hex.EncodeToString(hash[:])
}

func main() {
	input := []byte("verify this message")
	fmt.Println(HashData(input))
}

Sum256 returns [32]byte. This is an array type. Arrays in Go are values. When you return the hash, the caller gets a copy. This avoids pointer aliasing issues. The slice operation hash[:] converts the array to a []byte so hex.EncodeToString can process it. This pattern is common in Go crypto code. Functions return arrays for fixed-size results, and callers slice them when they need a slice interface.

Hashes verify integrity. They do not encrypt. You can't reverse a hash.

Streaming large files without blowing up memory

Sum256 requires the entire input in memory. If you hash a large file, loading it all into a slice wastes memory. Use sha256.New to create a hash state. Write data to the state incrementally. The hash updates as you write. Finally, call Sum to get the digest.

Here is how to hash a stream without holding the whole payload in RAM.

package main

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io"
	"strings"
)

// HashStream computes SHA-256 for large data without loading it all into memory.
func HashStream(r io.Reader) ([]byte, error) {
	// New creates a hash state that implements io.Writer.
	// You can pipe data into it chunk by chunk.
	h := sha256.New()

	// Copy reads from r and writes into h automatically.
	// It uses a fixed-size buffer to keep memory flat.
	if _, err := io.Copy(h, r); err != nil {
		return nil, err
	}

	// Sum returns the final digest.
	// Passing nil appends to a zero-length slice, returning a new slice.
	return h.Sum(nil), nil
}

func main() {
	// strings.NewReader satisfies io.Reader for demonstration.
	// Replace with os.Open or http.Response.Body in production.
	reader := strings.NewReader("streaming large data efficiently")
	hash, err := HashStream(reader)
	if err != nil {
		panic(err)
	}
	fmt.Println(hex.EncodeToString(hash))
}

The io.Hash interface is satisfied by sha256 states. This means the hash object implements io.Writer. io.Copy detects this and writes directly into the hash state instead of copying to a temporary buffer. The memory footprint stays constant regardless of input size. This is how Go handles large files, network streams, and tar archives efficiently.

Stream when the input is unknown or larger than available RAM.

When randomness or hashing goes wrong

The compiler catches structural mistakes early. If you forget to import a sub-package and call sha256.Sum256, you get undefined: sha256. If you pass a string to a function expecting []byte, the compiler rejects it with cannot use "text" (untyped string constant) as []byte value in argument. These errors force you to convert types explicitly, which prevents silent data corruption.

Runtime failures are more subtle. Ignoring the error from rand.Read is the most common crypto bug. If the OS entropy pool is exhausted, rand.Read returns an error and leaves the slice partially zeroed. Using that slice as a key gives you a predictable token. Always check the error. Always abort on failure.

Another trap is using math/rand for session tokens. math/rand seeds itself with 1 if you never call Seed. Even if you seed it with time.Now().UnixNano(), an attacker who knows the approximate generation time can brute-force the seed and predict every subsequent token. crypto/rand does not take a seed. It draws from the OS. The distinction is visible at the import line.

Goroutine leaks happen when a background task waits on a channel that never gets closed. Always have a cancellation path. Context is plumbing. Run it through every long-lived call site.

Picking the right tool

Use crypto/rand when you need unpredictable bytes for tokens, keys, or nonces. Use math/rand when you need fast, deterministic sequences for games, simulations, or shuffling. Use sha256.Sum256 when your input fits comfortably in memory and you want a single-line hash call. Use sha256.New with io.Copy when you are processing files, network streams, or archives larger than available RAM. Use plain sequential code when you do not need concurrency: the simplest thing that works is usually the right thing.

Goroutines are cheap. Channels are not magic. Trust gofmt. Argue logic, not formatting.

Where to go next