Skip to main content Skip to sidebar

Derive keys using HKDF with SHA256 in Go

HKDF (HMAC-based Key Derivation Function) is a cryptographic key derivation function that expands a source key material into multiple cryptographically strong keys. Here’s how to implement HKDF with SHA256 in Go using the standard library.

Basic HKDF Implementation

package main

import (
    "crypto/rand"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "io"

    "golang.org/x/crypto/hkdf"
)

func main() {
    // Generate a random source key material (IKM)
    ikm := make([]byte, 32)
    _, err := rand.Read(ikm)
    if err != nil {
        fmt.Println("error generating source key:", err)
        return
    }

    // Optional salt (recommended for security)
    salt := make([]byte, 32)
    _, err = rand.Read(salt)
    if err != nil {
        fmt.Println("error generating salt:", err)
        return
    }

    // Optional info parameter for context
    info := []byte("key derivation context")

    // Create HKDF reader
    hkdf := hkdf.New(sha256.New, ikm, salt, info)

    // Derive multiple keys of different lengths
    key1 := make([]byte, 32) // 256-bit key
    key2 := make([]byte, 16) // 128-bit key
    key3 := make([]byte, 64) // 512-bit key

    // Read the derived keys
    _, err = io.ReadFull(hkdf, key1)
    if err != nil {
        fmt.Println("error deriving key1:", err)
        return
    }

    _, err = io.ReadFull(hkdf, key2)
    if err != nil {
        fmt.Println("error deriving key2:", err)
        return
    }

    _, err = io.ReadFull(hkdf, key3)
    if err != nil {
        fmt.Println("error deriving key3:", err)
        return
    }

    fmt.Println("Source Key Material:", hex.EncodeToString(ikm))
    fmt.Println("Salt:", hex.EncodeToString(salt))
    fmt.Println("Derived Key 1 (32 bytes):", hex.EncodeToString(key1))
    fmt.Println("Derived Key 2 (16 bytes):", hex.EncodeToString(key2))
    fmt.Println("Derived Key 3 (64 bytes):", hex.EncodeToString(key3))
}

Practical Example: Deriving Encryption and MAC Keys

package main

import (
    "crypto/rand"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "io"

    "golang.org/x/crypto/hkdf"
)

type DerivedKeys struct {
    EncryptionKey []byte
    MACKey        []byte
    IV            []byte
}

func deriveKeys(masterKey []byte, salt []byte, info string) (*DerivedKeys, error) {
    hkdf := hkdf.New(sha256.New, masterKey, salt, []byte(info))

    keys := &DerivedKeys{
        EncryptionKey: make([]byte, 32), // AES-256 key
        MACKey:        make([]byte, 32), // HMAC key
        IV:            make([]byte, 16), // AES IV
    }

    // Derive encryption key
    _, err := io.ReadFull(hkdf, keys.EncryptionKey)
    if err != nil {
        return nil, fmt.Errorf("failed to derive encryption key: %w", err)
    }

    // Derive MAC key
    _, err = io.ReadFull(hkdf, keys.MACKey)
    if err != nil {
        return nil, fmt.Errorf("failed to derive MAC key: %w", err)
    }

    // Derive IV
    _, err = io.ReadFull(hkdf, keys.IV)
    if err != nil {
        return nil, fmt.Errorf("failed to derive IV: %w", err)
    }

    return keys, nil
}

func main() {
    // Master key (could be from password-based key derivation)
    masterKey := make([]byte, 32)
    _, err := rand.Read(masterKey)
    if err != nil {
        fmt.Println("error generating master key:", err)
        return
    }

    // Salt should be unique per derivation
    salt := make([]byte, 32)
    _, err = rand.Read(salt)
    if err != nil {
        fmt.Println("error generating salt:", err)
        return
    }

    // Derive keys for encryption
    keys, err := deriveKeys(masterKey, salt, "encryption-context-v1")
    if err != nil {
        fmt.Println("error deriving keys:", err)
        return
    }

    fmt.Println("Master Key:", hex.EncodeToString(masterKey))
    fmt.Println("Salt:", hex.EncodeToString(salt))
    fmt.Println("Encryption Key:", hex.EncodeToString(keys.EncryptionKey))
    fmt.Println("MAC Key:", hex.EncodeToString(keys.MACKey))
    fmt.Println("IV:", hex.EncodeToString(keys.IV))
}

Usage Example

Here’s the expected output when running the basic HKDF implementation:

Source Key Material: a1b2c3d4e5f67890abcdef1234567890a1b2c3d4e5f67890abcdef1234567890
Salt: 1234567890abcdefa1b2c3d4e5f678901234567890abcdefa1b2c3d4e5f67890
Derived Key 1 (32 bytes): 4f2a8b3c7d9e1f5a6b8c2d4e7f0a1b3c4d5e6f8a9b0c1d2e3f4a5b6c7d8e9f0a
Derived Key 2 (16 bytes): 9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d
Derived Key 3 (64 bytes): 2f4e6d8c0b9a1f3e5d7c9b0a8f6e4d2c0b9a7f5e3d1c9b7a5f3e1d9c7b5a3f1e9d7c5b3a1f9e7d5c3b1a9f7e5d3c1b9a7f5e3d1c9b7a5f3e1d9c7b5a3f1e

For the practical encryption example:

Master Key: 5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b
Salt: f0e1d2c3b4a59687f0e1d2c3b4a59687f0e1d2c3b4a59687f0e1d2c3b4a59687
Encryption Key: 8f7e6d5c4b3a2910fedcba0987654321fedcba0987654321fedcba0987654321
MAC Key: 1a2b3c4d5e6f708192a3b4c5d6e7f809a1b2c3d4e5f6708192a3b4c5d6e7f809
IV: 9f8e7d6c5b4a39281f0e9d8c7b6a5948

Note: These are example outputs with sample values. In real usage, all values will be randomly generated and different each time.

Key Points

  • Source Key Material (IKM): The input key material, should have good entropy
  • Salt: Optional but recommended, adds randomness and prevents rainbow table attacks
  • Info: Optional context information to bind the derived keys to a specific purpose
  • Output Length: HKDF can generate up to 255 * HashLen bytes (8160 bytes for SHA256)

HKDF provides a standardized and secure way to derive multiple keys from a single source, making it ideal for protocols that need separate keys for encryption, authentication, and other cryptographic operations.