Hashing and Cryptography

Updated

September 13, 2026

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

Output:

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

Output:

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

Output:

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

Output:

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

Output:

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/sha256 is the go-to standard digest algorithm for checksums and content deduplication.
  • crypto/rand is mandatory for tokens, passwords, keys, and UUID generation.
  • subtle.ConstantTimeCompare prevents timing attacks on secrets and signatures.
  • For streaming files, pass hash.Hash into io.Copy.
  • Never invent custom encryption, padding, or hash algorithms.

Try this

  1. In order_digest.go, modify one character in payload and observe how completely the output hash changes.
  2. In secure_token.go, generate a 32-byte token and print its length in hex characters (should be 64).
  3. Compare two byte slices of different lengths using subtle.ConstantTimeCompare. Confirm it returns 0.