Hashing and Cryptography
Hashing and Cryptography
Checksums, integrity hashes, and secure tokens are standard requirements for desks and services. Go provides cryptographic primitives in crypto/*. The boring default is: crypto/sha256 for integrity hashes and content digests, and crypto/rand for secure random tokens or IDs. Never use math/rand for secrets, and never hand-roll security logic.
Mental model
crypto/sha256 implements SHA-256 (a one-way hash function). sha256.Sum256([]byte) calculates a 32-byte fixed checksum. For larger payloads or streaming files, instantiate sha256.New() and write data into it like any other io.Writer.
crypto/rand provides cryptographically secure random bytes from the operating system (/dev/urandom or system entropy). Read from crypto/rand.Reader using io.ReadFull.
Constant-time comparison with crypto/subtle.ConstantTimeCompare prevents timing attacks when comparing sensitive tokens, signatures, or HMAC digests.
Worked examples
Case 1: Compute a SHA-256 digest
Save as order_digest.go. Calculate the checksum of an order receipt to detect tampering.
// order_digest.go
package main
import (
"crypto/sha256"
"fmt"
)
func main() {
payload := []byte("ticket: 7, table: 12, total: 34.50")
hash := sha256.Sum256(payload)
fmt.Printf("sha256: %x\n", hash)
}Run:
go run order_digest.goOutput:
sha256: 49206d2fe2b78b0f948f98ec839a9c2b4e85746b1981e4b52df39c1b3f60fec7
%x formats the 32-byte array as a lowercase hex string. Changing even one byte in the payload completely changes the hash.
Case 2: Streaming hash with io.Writer
Save as stream_hash.go. When processing a large ledger or file, do not load everything into memory.
// stream_hash.go
package main
import (
"crypto/sha256"
"fmt"
"io"
"strings"
)
func main() {
r := strings.NewReader("line 1: shift open\nline 2: ticket 7 printed\n")
hasher := sha256.New()
n, err := io.Copy(hasher, r)
if err != nil {
fmt.Println("error:", err)
return
}
digest := hasher.Sum(nil)
fmt.Printf("bytes hashed: %d\n", n)
fmt.Printf("digest: %x\n", digest)
}Run:
go run stream_hash.goOutput:
bytes hashed: 44
digest: df8503eebe66574f19b8ea007fa7b9f3d511979b06222b07e742813137887e07
hasher.Sum(nil) appends the calculated checksum to the provided slice. Passing nil returns a fresh slice with the digest.
Case 3: Cryptographically secure random tokens
Save as secure_token.go. Generate an unguessable ticket confirmation code.
// secure_token.go
package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
"io"
)
func generateToken(n int) (string, error) {
bytes := make([]byte, n)
if _, err := io.ReadFull(rand.Reader, bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}
func main() {
token, err := generateToken(16) // 16 bytes = 32 hex chars
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("token length:", len(token))
fmt.Println("is hex:", len(token) == 32)
}Run:
go run secure_token.goOutput:
token length: 32
is hex: true
Always check errors from io.ReadFull(rand.Reader, ...). If OS entropy is exhausted or unavailable, it will fail safely instead of generating predictable data.
Case 4: Constant-time comparison for tokens
Save as verify_token.go. Avoid timing side-channel attacks when authenticating desk tokens.
// verify_token.go
package main
import (
"crypto/subtle"
"fmt"
)
func main() {
expectedToken := []byte("secr3t-d3sk-tok3n")
userToken := []byte("secr3t-d3sk-tok3n")
badToken := []byte("wrong-d3sk-tok3n!")
valid1 := subtle.ConstantTimeCompare(expectedToken, userToken) == 1
valid2 := subtle.ConstantTimeCompare(expectedToken, badToken) == 1
fmt.Printf("user token match: %v\n", valid1)
fmt.Printf("bad token match: %v\n", valid2)
}Run:
go run verify_token.goOutput:
user token match: true
bad token match: false
Plain bytes.Equal or string == returns early on the first mismatched byte, leaking information about prefix correctness through response timing. subtle.ConstantTimeCompare takes the same time regardless of mismatch position.
The trap
Save as insecure_rand.go. Using math/rand (or math/rand/v2) for API keys or auth tokens is predictable because pseudo-random number generators can be reverse-engineered from their output.
// insecure_rand.go
package main
import (
"fmt"
"math/rand/v2"
)
func main() {
// Good for games, bad for security
orderCode := rand.IntN(1000)
fmt.Printf("desk order code: %03d\n", orderCode)
fmt.Println("use crypto/rand for security tokens!")
}Run:
go run insecure_rand.goOutput:
desk order code: 512
use crypto/rand for security tokens!
Reserve math/rand for simulations, shuffle algorithms, and non-security random numbers. For any credential, token, or session ID, use crypto/rand.
The boring rule
crypto/sha256is the go-to standard digest algorithm for checksums and content deduplication.crypto/randis mandatory for tokens, passwords, keys, and UUID generation.subtle.ConstantTimeCompareprevents timing attacks on secrets and signatures.- For streaming files, pass
hash.Hashintoio.Copy. - Never invent custom encryption, padding, or hash algorithms.
Try this
- In
order_digest.go, modify one character inpayloadand observe how completely the output hash changes. - In
secure_token.go, generate a 32-byte token and print its length in hex characters (should be 64). - Compare two byte slices of different lengths using
subtle.ConstantTimeCompare. Confirm it returns 0.